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
|
|
@ -26,6 +26,9 @@ import { InMemoryInvoiceRepo, InvoiceRepo } from './invoicing';
|
|||
import { PROJECT_ROOT } from './paths';
|
||||
import { PaymentClient, portalRouter } from './portal';
|
||||
import { proxyVerae } from './upstream';
|
||||
import { CreditLedger } from './credits';
|
||||
import { composeStatement } from './statement';
|
||||
import { BILLING_SUBJECTS, natsAdjust, natsPublish, natsStatement } from './billing-nats';
|
||||
|
||||
export interface StoredItem {
|
||||
id: string;
|
||||
|
|
@ -98,6 +101,9 @@ export function buildApp(deps: AppDeps = {}): {
|
|||
},
|
||||
};
|
||||
const items: StoredItem[] = [];
|
||||
const credits = new CreditLedger();
|
||||
const onUsage = (e: { customerId: string; endpointId: string; cents: number }) =>
|
||||
natsPublish(BILLING_SUBJECTS.USAGE_RECORDED, { ...e, at: new Date().toISOString() });
|
||||
const hashIndex = new Map<
|
||||
string,
|
||||
{ jobId: string; sha256: string; data?: string; timestamp: string }
|
||||
|
|
@ -114,7 +120,7 @@ export function buildApp(deps: AppDeps = {}): {
|
|||
'/admin/api',
|
||||
adminLoginRouter(adminUsers),
|
||||
adminAuth(),
|
||||
adminRouter(pricingStore, customers, { usage, invoices, users: adminUsers }),
|
||||
adminRouter(pricingStore, customers, { usage, invoices, users: adminUsers }, credits),
|
||||
);
|
||||
app.use('/admin', express.static(path.join(PROJECT_ROOT, 'admin')));
|
||||
|
||||
|
|
@ -140,6 +146,7 @@ export function buildApp(deps: AppDeps = {}): {
|
|||
rateCard: () => pricingStore.getRateCard(),
|
||||
payments,
|
||||
qr,
|
||||
credits,
|
||||
}),
|
||||
);
|
||||
app.use('/portal', express.static(path.join(PROJECT_ROOT, 'portal')));
|
||||
|
|
@ -158,16 +165,16 @@ export function buildApp(deps: AppDeps = {}): {
|
|||
}),
|
||||
);
|
||||
|
||||
app.get('/v1/status', meter('status', usage, pricing), (req, res) => {
|
||||
app.get('/v1/status', meter('status', usage, pricing, onUsage), (req, res) => {
|
||||
res.json({ status: 'ok', quote: res.locals.quote });
|
||||
});
|
||||
|
||||
app.post('/v1/transform', meter('transform', usage, pricing), (req, res) => {
|
||||
app.post('/v1/transform', meter('transform', usage, pricing, onUsage), (req, res) => {
|
||||
const text = String(req.body?.text ?? '');
|
||||
res.json({ output: text.toUpperCase(), quote: res.locals.quote });
|
||||
});
|
||||
|
||||
app.post('/v1/timestamp', meter('timestamp', usage, pricing), async (req, res) => {
|
||||
app.post('/v1/timestamp', meter('timestamp', usage, pricing, onUsage), async (req, res) => {
|
||||
if (await proxyVerae(req, res, '/zapier/v1/timestamp')) return;
|
||||
const data = req.body?.data != null ? String(req.body.data) : '';
|
||||
const sha256 =
|
||||
|
|
@ -190,7 +197,7 @@ export function buildApp(deps: AppDeps = {}): {
|
|||
res.status(202).json({ jobId, sha256, existing: false, timestamp });
|
||||
});
|
||||
|
||||
app.get('/v1/receipts/:jobId', meter('receipt', usage, pricing), async (req, res) => {
|
||||
app.get('/v1/receipts/:jobId', meter('receipt', usage, pricing, onUsage), async (req, res) => {
|
||||
if (await proxyVerae(req, res, `/zapier/v1/receipts/${req.params.jobId}`)) return;
|
||||
const rec = jobIndex.get(String(req.params.jobId));
|
||||
if (!rec) {
|
||||
|
|
@ -208,7 +215,7 @@ export function buildApp(deps: AppDeps = {}): {
|
|||
});
|
||||
});
|
||||
|
||||
app.get('/v1/hashes/:sha256', meter('hash-lookup', usage, pricing), async (req, res) => {
|
||||
app.get('/v1/hashes/:sha256', meter('hash-lookup', usage, pricing, onUsage), async (req, res) => {
|
||||
if (await proxyVerae(req, res, `/zapier/v1/hashes/${req.params.sha256}`)) return;
|
||||
const sha256 = String(req.params.sha256 || '').toLowerCase();
|
||||
const rec = hashIndex.get(sha256);
|
||||
|
|
@ -219,7 +226,7 @@ export function buildApp(deps: AppDeps = {}): {
|
|||
res.json({ exists: true, ...rec });
|
||||
});
|
||||
|
||||
app.post('/v1/add', meter('add', usage, pricing), (req, res) => {
|
||||
app.post('/v1/add', meter('add', usage, pricing, onUsage), (req, res) => {
|
||||
const number1 = Number(req.body?.number1);
|
||||
const number2 = Number(req.body?.number2);
|
||||
if (!Number.isFinite(number1) || !Number.isFinite(number2)) {
|
||||
|
|
@ -229,7 +236,7 @@ export function buildApp(deps: AppDeps = {}): {
|
|||
res.json({ number1, number2, sum: number1 + number2, quote: res.locals.quote });
|
||||
});
|
||||
|
||||
app.post('/v1/storage', parseMetadata, meter('storage', usage, pricing), (req, res) => {
|
||||
app.post('/v1/storage', parseMetadata, meter('storage', usage, pricing, onUsage), (req, res) => {
|
||||
const metadata = res.locals.parsedMetadata as Record<string, unknown>;
|
||||
const files = (req.files as Express.Multer.File[]) ?? [];
|
||||
const item: StoredItem = {
|
||||
|
|
@ -243,7 +250,7 @@ export function buildApp(deps: AppDeps = {}): {
|
|||
res.json({ id: item.id, quote: res.locals.quote });
|
||||
});
|
||||
|
||||
app.get('/v1/storage', meter('storage-list', usage, pricing), (req, res) => {
|
||||
app.get('/v1/storage', meter('storage-list', usage, pricing, onUsage), (req, res) => {
|
||||
res.json({ items: items.filter((i) => i.customerId === req.customer!.id) });
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue