Add NATS account-balance SoT and statement review for customers, CS, and sales
Some checks are pending
offline / test (push) Waiting to run

Internal billing now uses verae.billing.* request-reply and pubs. zappier-account-balance tracks prepaid, credits, usage, and payments. Portal, admin, CS, and sales all review the same statement. Independent Forgejo repos stay split via push-module-repos.
This commit is contained in:
George Lambert 2026-09-11 15:35:37 -04:00
parent a1a5b957fd
commit ac38676645
135 changed files with 3078 additions and 130 deletions

View file

@ -14,6 +14,9 @@ import { applyMonthlyCredit } from './billing/credit';
import { InvoiceRepo } from './invoicing';
import { RateCard, TierConfig } from './pricing';
import { UsageRepo } from './usage';
import { CreditLedger } from './credits';
import { composeStatement } from './statement';
import { BILLING_SUBJECTS, natsPublish, natsStatement, natsAdjust } from './billing-nats';
/**
* Customer portal API (/portal/api): signup, login with optional TOTP 2FA,
@ -44,6 +47,7 @@ export interface PortalDeps {
qr: (uri: string) => Promise<string>;
sessionTtlMs?: number;
now?: () => number;
credits?: CreditLedger;
}
const DEFAULT_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
@ -198,6 +202,24 @@ export function portalRouter(deps: PortalDeps): Router {
res.json({ tiers: deps.tiers(), rateCard: deps.rateCard() });
});
router.get('/statement', async (req, res) => {
const fromNats = await natsStatement(req.customer!.id);
if (fromNats) {
res.json({ ...fromNats, source: 'nats' });
return;
}
res.json({
...composeStatement({
customerId: req.customer!.id,
prepaidCents: req.customer!.balanceCents ?? 0,
credits: (deps.credits || new CreditLedger()).list(req.customer!.id),
usage: deps.usage.listFor(req.customer!.id),
invoices: deps.invoices.list({ customerId: req.customer!.id }),
}),
source: 'local',
});
});
router.get('/invoices/:id', (req, res) => {
const invoice = deps.invoices.get(req.params.id);
if (!invoice || invoice.customerId !== req.customer!.id) {
@ -263,6 +285,20 @@ export function portalRouter(deps: PortalDeps): Router {
balanceCents: (req.customer!.balanceCents ?? 0) + result.creditedCents,
};
if (result.creditedCents > 0) save(deps, customer);
if (result.creditedCents > 0) {
natsPublish(BILLING_SUBJECTS.PAYMENT_RECORDED, {
customerId: customer.id,
cents: result.creditedCents,
reason: 'reload',
});
void natsAdjust({
customerId: customer.id,
cents: result.creditedCents,
reason: 'reload',
agent: 'portal',
kind: 'payment',
});
}
res.json({
balanceCents: customer.balanceCents,
mode: result.mode,