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
|
|
@ -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