Add NATS account-balance SoT and statement review for customers, CS, and sales
Some checks are pending
offline / test (push) Waiting to run
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:
parent
a1a5b957fd
commit
ac38676645
135 changed files with 3078 additions and 130 deletions
|
|
@ -12,6 +12,8 @@ import { billingRows, toCsv, usageTrend } from './reports';
|
|||
import { UsageRepo } from './usage';
|
||||
import { CreditLedger } from './credits';
|
||||
import { invoicesToAccountingCsv, invoicesToQuickBooksIif } from './accounting-export';
|
||||
import { composeStatement } from './statement';
|
||||
import { BILLING_SUBJECTS, natsAdjust, natsPublish, natsStatement } from './billing-nats';
|
||||
|
||||
// Issued login tokens (in-memory; a restart simply requires logging in again).
|
||||
const sessions = new Map<string, number>();
|
||||
|
|
@ -246,6 +248,14 @@ export function adminRouter(
|
|||
agent: typeof agent === 'string' ? agent : 'admin',
|
||||
});
|
||||
customers.save({ ...customer, balanceCents: (customer.balanceCents ?? 0) + delta });
|
||||
natsPublish(BILLING_SUBJECTS.CREDIT_APPLIED, rec);
|
||||
void natsAdjust({
|
||||
customerId,
|
||||
cents: delta,
|
||||
reason: rec.reason,
|
||||
agent: rec.agent,
|
||||
kind: 'credit',
|
||||
});
|
||||
res.status(201).json(rec);
|
||||
});
|
||||
|
||||
|
|
@ -254,6 +264,31 @@ export function adminRouter(
|
|||
res.json({ credits: credits.list(customerId) });
|
||||
});
|
||||
|
||||
router.get('/statement/:id', async (req, res) => {
|
||||
const customer = customers.list().find((c) => c.id === req.params.id);
|
||||
if (!customer) {
|
||||
res.status(404).json({ error: 'customer not found' });
|
||||
return;
|
||||
}
|
||||
const fromNats = await natsStatement(customer.id);
|
||||
if (fromNats) {
|
||||
res.json({ ...fromNats, name: customer.name, tierId: customer.tierId, source: 'nats' });
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
...composeStatement({
|
||||
customerId: customer.id,
|
||||
prepaidCents: customer.balanceCents ?? 0,
|
||||
credits: credits.list(customer.id),
|
||||
usage: accounting.usage.listFor(customer.id),
|
||||
invoices: accounting.invoices.list({ customerId: customer.id }),
|
||||
}),
|
||||
name: customer.name,
|
||||
tierId: customer.tierId,
|
||||
source: 'local',
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/sales/quote/:id', (req, res) => {
|
||||
const customer = customers.list().find((c) => c.id === req.params.id);
|
||||
if (!customer) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue