Wire zappier-edge into the live stack and add billing department APIs
Some checks are pending
offline / test (push) Waiting to run

Fleet now spawns the real zappier and middleware processes. Metered
timestamp/receipt/hash calls proxy to middleware when ZAPPIER_UPSTREAM
is set. CS credits, sales per-customer pricing, and QuickBooks export
are separate repos plugged into zappier-edge admin.
This commit is contained in:
George Lambert 2026-09-11 15:15:55 -04:00
parent 65bfa544b2
commit a1a5b957fd
44 changed files with 822 additions and 18 deletions

View file

@ -10,6 +10,8 @@ import { PriceRule, PricingStore, TierConfig } from './pricing';
import { PROJECT_ROOT } from './paths';
import { billingRows, toCsv, usageTrend } from './reports';
import { UsageRepo } from './usage';
import { CreditLedger } from './credits';
import { invoicesToAccountingCsv, invoicesToQuickBooksIif } from './accounting-export';
// Issued login tokens (in-memory; a restart simply requires logging in again).
const sessions = new Map<string, number>();
@ -139,6 +141,7 @@ export function adminRouter(
store: PricingStore,
customers: CustomerRepo,
accounting: AccountingDeps,
credits: CreditLedger = new CreditLedger(),
): Router {
const router = Router();
@ -222,6 +225,70 @@ export function adminRouter(
res.json({ ok: true });
});
/* ---------------- customer service: credits ---------------- */
router.post('/credits', (req, res) => {
const { customerId, cents, reason, agent } = req.body ?? {};
if (typeof customerId !== 'string' || !Number.isFinite(Number(cents))) {
res.status(400).json({ error: 'customerId and cents required' });
return;
}
const customer = customers.list().find((c) => c.id === customerId);
if (!customer) {
res.status(404).json({ error: 'customer not found' });
return;
}
const delta = Math.trunc(Number(cents));
const rec = credits.add({
customerId,
cents: delta,
reason: typeof reason === 'string' ? reason : 'credit adjustment',
agent: typeof agent === 'string' ? agent : 'admin',
});
customers.save({ ...customer, balanceCents: (customer.balanceCents ?? 0) + delta });
res.status(201).json(rec);
});
router.get('/credits', (req, res) => {
const customerId = typeof req.query.customerId === 'string' ? req.query.customerId : undefined;
res.json({ credits: credits.list(customerId) });
});
router.get('/sales/quote/:id', (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 tier = store.getTiers().find((t) => t.id === customer.tierId);
res.json({
customerId: customer.id,
name: customer.name,
tierId: customer.tierId,
multiplierOverride: customer.multiplierOverride ?? null,
monthlyCreditCents: tier?.monthlyCreditCents ?? 0,
listMultiplier: tier?.multiplier ?? 1,
});
});
router.get('/exports/quickbooks.iif', (req, res) => {
const period = typeof req.query.period === 'string' ? req.query.period : undefined;
const invoices = accounting.invoices.list({ ...(period ? { period } : {}) });
res
.type('text/plain')
.set('content-disposition', 'attachment; filename="zappier-quickbooks.iif"')
.send(invoicesToQuickBooksIif(invoices));
});
router.get('/exports/accounting.csv', (req, res) => {
const period = typeof req.query.period === 'string' ? req.query.period : undefined;
const invoices = accounting.invoices.list({ ...(period ? { period } : {}) });
res
.type('text/csv')
.set('content-disposition', 'attachment; filename="zappier-accounting.csv"')
.send(invoicesToAccountingCsv(invoices));
});
/* ---------------- accounting: invoices ---------------- */
router.post('/invoices/generate', (req, res) => {