Add customer names on the ledger, staff session login, and exclusive jobs.events.
Some checks are pending
offline / test (push) Waiting to run
Some checks are pending
offline / test (push) Waiting to run
Account-balance stores display names and looks up by name. Edge writes names on customer create/edit; staff UIs join from edge when needed. New verae-staff-session issues a host cookie; department HTML redirects when STAFF_AUTH=1. JOBS_EVENTS_EXCLUSIVE lets jobs-events own the durable consumer. Catalog index is cards; disabled fleet machines are grey.
This commit is contained in:
parent
cb07f5b321
commit
9cc0018708
38 changed files with 533 additions and 68 deletions
|
|
@ -226,10 +226,10 @@ async function viewInvoice(id) {
|
|||
|
||||
function renderStatement() {
|
||||
const st = state.statement || { prepaidCents: 0, credits: [], usage: [], payments: [] };
|
||||
const emptyCard = (title) =>
|
||||
`<div class="empty">${EMPTY_SVG}<h3>${title}</h3><p>Nothing recorded yet.</p></div>`;
|
||||
const row = (list, cols) => {
|
||||
if (!list || !list.length) {
|
||||
return `<tr><td colspan="${cols.length}"><div class="empty" style="padding:1rem">${EMPTY_SVG}<h3>Nothing here yet</h3></div></td></tr>`;
|
||||
}
|
||||
if (!list || !list.length) return '';
|
||||
return list
|
||||
.map(
|
||||
(r) =>
|
||||
|
|
@ -251,15 +251,21 @@ function renderStatement() {
|
|||
<div class="stat"><div class="k">Usage events</div><div class="v">${st.usage?.length || 0}</div></div>
|
||||
<div class="stat"><div class="k">Payments</div><div class="v">${st.payments?.length || 0}</div></div>
|
||||
</div>
|
||||
<div class="card"><h3>Credits</h3><table>
|
||||
<thead><tr><th>Amount</th><th>Reason</th><th>Agent</th><th>When</th></tr></thead>
|
||||
<tbody>${row(st.credits, ['cents', 'reason', 'agent', 'at'])}</tbody></table></div>
|
||||
<div class="card"><h3>Usage</h3><table>
|
||||
<thead><tr><th>Endpoint</th><th>Amount</th><th>When</th></tr></thead>
|
||||
<tbody>${row(st.usage, ['endpointId', 'cents', 'at'])}</tbody></table></div>
|
||||
<div class="card"><h3>Payments</h3><table>
|
||||
<thead><tr><th>Amount</th><th>Kind</th><th>Reason</th><th>When</th></tr></thead>
|
||||
<tbody>${row(st.payments, ['cents', 'kind', 'reason', 'at'])}</tbody></table></div>`;
|
||||
<div class="card"><h3>Credits</h3>${
|
||||
st.credits?.length
|
||||
? `<table><thead><tr><th>Amount</th><th>Reason</th><th>Agent</th><th>When</th></tr></thead><tbody>${row(st.credits, ['cents', 'reason', 'agent', 'at'])}</tbody></table>`
|
||||
: emptyCard('No credits yet')
|
||||
}</div>
|
||||
<div class="card"><h3>Usage</h3>${
|
||||
st.usage?.length
|
||||
? `<table><thead><tr><th>Endpoint</th><th>Amount</th><th>When</th></tr></thead><tbody>${row(st.usage, ['endpointId', 'cents', 'at'])}</tbody></table>`
|
||||
: emptyCard('No usage yet')
|
||||
}</div>
|
||||
<div class="card"><h3>Payments</h3>${
|
||||
st.payments?.length
|
||||
? `<table><thead><tr><th>Amount</th><th>Kind</th><th>Reason</th><th>When</th></tr></thead><tbody>${row(st.payments, ['cents', 'kind', 'reason', 'at'])}</tbody></table>`
|
||||
: emptyCard('No payments yet')
|
||||
}</div>`;
|
||||
}
|
||||
|
||||
/* ---------------- billing ---------------- */
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { CreditLedger } from './credits';
|
|||
import { invoicesToAccountingCsv, invoicesToQuickBooksIif } from './accounting-export';
|
||||
import { composeStatement } from './statement';
|
||||
import { BILLING_SUBJECTS, natsPublish } from './billing-nats';
|
||||
import { booksConfigured, ledgerAdjust, ledgerStatement } from './ledger';
|
||||
import { booksConfigured, ledgerAdjust, ledgerPutCustomer, ledgerStatement } from './ledger';
|
||||
|
||||
// Issued login tokens (in-memory; a restart simply requires logging in again).
|
||||
const sessions = new Map<string, number>();
|
||||
|
|
@ -197,6 +197,7 @@ export function adminRouter(
|
|||
apiKey: `key-${randomBytes(12).toString('hex')}`,
|
||||
};
|
||||
customers.save(customer);
|
||||
void ledgerPutCustomer({ customerId: customer.id, name: customer.name });
|
||||
res.status(201).json(customer);
|
||||
});
|
||||
|
||||
|
|
@ -216,7 +217,7 @@ export function adminRouter(
|
|||
res.status(400).json({ error: 'billingType must be stripe or purchase_order' });
|
||||
return;
|
||||
}
|
||||
customers.save({
|
||||
const next = {
|
||||
...existing,
|
||||
...(name !== undefined ? { name } : {}),
|
||||
...(tierId !== undefined ? { tierId } : {}),
|
||||
|
|
@ -224,7 +225,9 @@ export function adminRouter(
|
|||
...(stripeCustomerId !== undefined ? { stripeCustomerId } : {}),
|
||||
...(billingType !== undefined ? { billingType } : {}),
|
||||
...(email !== undefined ? { email } : {}),
|
||||
});
|
||||
};
|
||||
customers.save(next);
|
||||
void ledgerPutCustomer({ customerId: next.id, name: next.name, veraeUserId: next.veraeUserId });
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
|
|
@ -286,6 +289,7 @@ export function adminRouter(
|
|||
res.json({
|
||||
...composeStatement({
|
||||
customerId: customer.id,
|
||||
name: customer.name,
|
||||
veraeUserId: customer.veraeUserId,
|
||||
prepaidCents: customer.balanceCents ?? 0,
|
||||
credits: credits.list(customer.id),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const BILLING_SUBJECTS = {
|
|||
USAGE_RECORDED: 'verae.billing.usage.recorded',
|
||||
PAYMENT_RECORDED: 'verae.billing.payment.recorded',
|
||||
CREDIT_APPLIED: 'verae.billing.credit.applied',
|
||||
CUSTOMER_PUT: 'verae.billing.customer.put',
|
||||
};
|
||||
|
||||
export const AUTHZ_CHECK = 'verae.access.authz.check';
|
||||
|
|
@ -15,6 +16,7 @@ export type AccessPlane = 'zapier' | 'web' | 'api' | 'leaf' | 'staff';
|
|||
|
||||
export type BillingStatement = {
|
||||
customerId: string;
|
||||
name?: string;
|
||||
veraeUserId?: string;
|
||||
prepaidCents: number;
|
||||
credits: unknown[];
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Prepaid mutations. Account-balance is the writer when NATS or HTTP books exist.
|
||||
* Tests (no NATS_URL, no ACCOUNT_BALANCE_URL) keep a local cache only.
|
||||
*/
|
||||
import { natsAdjust, natsStatement } from './billing-nats';
|
||||
import { natsAdjust, natsPublish, natsStatement } from './billing-nats';
|
||||
import type { AccessPlane } from './billing-nats';
|
||||
|
||||
export type PrepaidRow = {
|
||||
|
|
@ -39,6 +39,21 @@ export async function ledgerAdjust(row: PrepaidRow, plane: AccessPlane): Promise
|
|||
return (await r.json()) as PrepaidRow;
|
||||
}
|
||||
|
||||
export async function ledgerPutCustomer(row: {
|
||||
customerId: string;
|
||||
name?: string;
|
||||
veraeUserId?: string;
|
||||
}): Promise<void> {
|
||||
natsPublish('verae.billing.customer.put', row, 'staff');
|
||||
const base = booksUrl();
|
||||
if (!base) return;
|
||||
await fetch(`${base}/customer`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(row),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
export async function ledgerStatement(customerId: string, plane: AccessPlane, veraeUserId?: string) {
|
||||
const nats = await natsStatement(customerId, plane, veraeUserId);
|
||||
if (nats) return nats;
|
||||
|
|
|
|||
|
|
@ -212,12 +212,17 @@ export function portalRouter(deps: PortalDeps): Router {
|
|||
router.get('/statement', async (req, res) => {
|
||||
const fromBooks = await ledgerStatement(req.customer!.id, 'web', req.customer!.veraeUserId);
|
||||
if (fromBooks) {
|
||||
res.json({ ...fromBooks, source: fromBooks.source || 'account-balance' });
|
||||
res.json({
|
||||
...fromBooks,
|
||||
name: req.customer!.name,
|
||||
source: fromBooks.source || 'account-balance',
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
...composeStatement({
|
||||
customerId: req.customer!.id,
|
||||
name: req.customer!.name,
|
||||
veraeUserId: req.customer!.veraeUserId,
|
||||
prepaidCents: req.customer!.balanceCents ?? 0,
|
||||
credits: (deps.credits || new CreditLedger()).list(req.customer!.id),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { UsageEntry } from './usage';
|
|||
|
||||
export function composeStatement(args: {
|
||||
customerId: string;
|
||||
name?: string;
|
||||
veraeUserId?: string;
|
||||
prepaidCents: number;
|
||||
credits: CreditAdjustment[];
|
||||
|
|
@ -22,6 +23,7 @@ export function composeStatement(args: {
|
|||
}));
|
||||
return {
|
||||
customerId: args.customerId,
|
||||
name: args.name,
|
||||
veraeUserId: args.veraeUserId,
|
||||
prepaidCents: args.prepaidCents,
|
||||
credits: args.credits.filter((c) => c.customerId === args.customerId),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue