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

@ -98,6 +98,7 @@ async function load() {
renderEndpoints();
renderTiers();
renderCustomers();
renderStatement();
renderInvoices();
renderReports();
renderSystem();
@ -277,7 +278,8 @@ function renderCustomers() {
<td><select data-customer="${c.id}" data-field="tierId">${tierOptions(c.tierId)}</select></td>
<td><input data-customer="${c.id}" data-field="multiplierOverride" type="number" step="any" size="5" value="${c.multiplierOverride ?? ''}" placeholder="—"></td>
<td><select data-customer="${c.id}" data-field="billingType">${btOptions(c.billingType)}</select></td>
<td class="row-actions"><button class="btn" onclick="saveCustomer('${c.id}')">Save</button></td>
<td class="row-actions"><button class="btn ghost" onclick="reviewCustomer('${c.id}')">Statement</button>
<button class="btn" onclick="saveCustomer('${c.id}')">Save</button></td>
</tr>`,
)
.join('');
@ -308,6 +310,47 @@ async function saveCustomer(id) {
await load();
}
function renderStatement() {
const options = customerOptions(state.customers[0]?.id || '');
document.getElementById('statement').innerHTML = `
<h2>Customer statement</h2>
<p class="lede">Credits, prepaid balance, usage, and payments. Prefers NATS account-balance.</p>
<div class="card">
<div class="filterbar">
<label><span>Customer</span><select id="stmt-customer">${options}</select></label>
<button class="btn" onclick="reviewCustomer(document.getElementById('stmt-customer').value)">Load</button>
</div>
<div id="stmt-out"></div>
</div>`;
}
async function reviewCustomer(id) {
if (!id) return;
const st = await api(`/statement/${id}`);
const row = (list, cols) =>
(list || [])
.map((r) => `<tr>${cols.map((c) => `<td>${r[c] ?? ''}</td>`).join('')}</tr>`)
.join('') || `<tr><td colspan="${cols.length}" style="color:var(--muted)">None.</td></tr>`;
const html = `
<p class="lede">${st.name || id} · prepaid ${fmt(st.prepaidCents || 0)} · source ${st.source || 'local'}</p>
<div class="card"><h3>Credits</h3><table><thead><tr><th>Cents</th><th>Reason</th><th>Agent</th><th>When</th></tr></thead>
<tbody>${row(st.credits, ['cents', 'reason', 'agent', 'at'])}</tbody></table></div>
<div class="card"><h3>Usage</h3><table><thead><tr><th>Endpoint</th><th>Cents</th><th>When</th></tr></thead>
<tbody>${row(st.usage, ['endpointId', 'cents', 'at'])}</tbody></table></div>
<div class="card"><h3>Payments</h3><table><thead><tr><th>Cents</th><th>Kind</th><th>Reason</th><th>When</th></tr></thead>
<tbody>${row(st.payments, ['cents', 'kind', 'reason', 'at'])}</tbody></table></div>`;
const out = document.getElementById('stmt-out');
if (out) out.innerHTML = html;
else {
document.getElementById('statement').innerHTML = `<h2>Customer statement</h2>${html}`;
document.querySelectorAll('nav button').forEach((b) => b.classList.toggle('active', b.dataset.tab === 'statement'));
document.querySelectorAll('main section').forEach((s) => (s.hidden = s.id !== 'statement'));
}
const sel = document.getElementById('stmt-customer');
if (sel) sel.value = id;
say(`Loaded statement for ${id}.`);
}
async function addCustomer() {
const name = document.getElementById('new-customer-name').value.trim();
const tierId = document.getElementById('new-customer-tier').value;

View file

@ -236,6 +236,7 @@
<button data-tab="endpoints" class="active">&nbsp; Rate card</button>
<button data-tab="tiers">&nbsp; Customer types</button>
<button data-tab="customers">&nbsp; Customers</button>
<button data-tab="statement">&nbsp; Statement</button>
<button data-tab="invoices">&nbsp; Invoices</button>
<button data-tab="reports">&nbsp; Reports</button>
<button data-tab="system">&nbsp; System</button>
@ -248,6 +249,7 @@
<section id="endpoints"></section>
<section id="tiers" hidden></section>
<section id="customers" hidden></section>
<section id="statement" hidden></section>
<section id="invoices" hidden></section>
<section id="reports" hidden></section>
<section id="system" hidden></section>