@dainamite/billing 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -19
- package/dist/modules/billing/acl.js +9 -1
- package/dist/modules/billing/acl.js.map +2 -2
- package/dist/modules/billing/api/accounts/[id]/balance/route.js +62 -0
- package/dist/modules/billing/api/accounts/[id]/balance/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/[id]/credit-status/route.js +56 -0
- package/dist/modules/billing/api/accounts/[id]/credit-status/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/[id]/topups/route.js +36 -0
- package/dist/modules/billing/api/accounts/[id]/topups/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/route.js +7 -0
- package/dist/modules/billing/api/accounts/route.js.map +2 -2
- package/dist/modules/billing/api/export/account/[id]/route.js +48 -3
- package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
- package/dist/modules/billing/api/statements/generate/route.js +68 -0
- package/dist/modules/billing/api/statements/generate/route.js.map +7 -0
- package/dist/modules/billing/api/statements/route.js +110 -0
- package/dist/modules/billing/api/statements/route.js.map +7 -0
- package/dist/modules/billing/api/topups/route.js +139 -0
- package/dist/modules/billing/api/topups/route.js.map +7 -0
- package/dist/modules/billing/api/transactions/adjust/route.js +71 -0
- package/dist/modules/billing/api/transactions/adjust/route.js.map +7 -0
- package/dist/modules/billing/api/transactions/route.js +109 -0
- package/dist/modules/billing/api/transactions/route.js.map +7 -0
- package/dist/modules/billing/api/usage/route.js +30 -6
- package/dist/modules/billing/api/usage/route.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js +16 -1
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/create/page.js +8 -1
- package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/page.js +26 -1
- package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/statements/[id]/page.js +129 -0
- package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/statements/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/statements/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/statements/page.js +152 -0
- package/dist/modules/billing/backend/billing/statements/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/statements/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/statements/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.js +90 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/page.js +147 -0
- package/dist/modules/billing/backend/billing/topups/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/topups/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.js +114 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/page.js +149 -0
- package/dist/modules/billing/backend/billing/transactions/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/transactions/page.meta.js.map +7 -0
- package/dist/modules/billing/commands/accounts.js +26 -1
- package/dist/modules/billing/commands/accounts.js.map +2 -2
- package/dist/modules/billing/commands/index.js +3 -0
- package/dist/modules/billing/commands/index.js.map +2 -2
- package/dist/modules/billing/commands/invoices.js +21 -1
- package/dist/modules/billing/commands/invoices.js.map +2 -2
- package/dist/modules/billing/commands/statements.js +74 -0
- package/dist/modules/billing/commands/statements.js.map +7 -0
- package/dist/modules/billing/commands/topups.js +139 -0
- package/dist/modules/billing/commands/topups.js.map +7 -0
- package/dist/modules/billing/commands/transactions.js +119 -0
- package/dist/modules/billing/commands/transactions.js.map +7 -0
- package/dist/modules/billing/commands/usage.js +72 -0
- package/dist/modules/billing/commands/usage.js.map +2 -2
- package/dist/modules/billing/components/AccountForm.js +53 -1
- package/dist/modules/billing/components/AccountForm.js.map +2 -2
- package/dist/modules/billing/components/PrepaidPanel.js +314 -0
- package/dist/modules/billing/components/PrepaidPanel.js.map +7 -0
- package/dist/modules/billing/components/accountNames.js +20 -0
- package/dist/modules/billing/components/accountNames.js.map +7 -0
- package/dist/modules/billing/data/entities.js +290 -0
- package/dist/modules/billing/data/entities.js.map +2 -2
- package/dist/modules/billing/data/entityIds.js +6 -1
- package/dist/modules/billing/data/entityIds.js.map +2 -2
- package/dist/modules/billing/data/validators.js +107 -2
- package/dist/modules/billing/data/validators.js.map +2 -2
- package/dist/modules/billing/events.js +52 -0
- package/dist/modules/billing/events.js.map +2 -2
- package/dist/modules/billing/i18n/en.json +112 -4
- package/dist/modules/billing/i18n/pl.json +112 -4
- package/dist/modules/billing/index.js +1 -1
- package/dist/modules/billing/index.js.map +2 -2
- package/dist/modules/billing/lib/accountStatus.js +106 -0
- package/dist/modules/billing/lib/accountStatus.js.map +7 -0
- package/dist/modules/billing/lib/balanceEvents.js +48 -0
- package/dist/modules/billing/lib/balanceEvents.js.map +7 -0
- package/dist/modules/billing/lib/balanceLedger.js +136 -0
- package/dist/modules/billing/lib/balanceLedger.js.map +7 -0
- package/dist/modules/billing/lib/balanceStatus.js +29 -0
- package/dist/modules/billing/lib/balanceStatus.js.map +7 -0
- package/dist/modules/billing/lib/billRunEngine.js +40 -2
- package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
- package/dist/modules/billing/lib/creditStatus.js +65 -0
- package/dist/modules/billing/lib/creditStatus.js.map +7 -0
- package/dist/modules/billing/lib/money.js +47 -0
- package/dist/modules/billing/lib/money.js.map +7 -0
- package/dist/modules/billing/lib/prepaidConfig.js +45 -0
- package/dist/modules/billing/lib/prepaidConfig.js.map +7 -0
- package/dist/modules/billing/lib/prepaidConsume.js +214 -0
- package/dist/modules/billing/lib/prepaidConsume.js.map +7 -0
- package/dist/modules/billing/lib/prepaidPeriodClose.js +285 -0
- package/dist/modules/billing/lib/prepaidPeriodClose.js.map +7 -0
- package/dist/modules/billing/lib/topupCapture.js +62 -0
- package/dist/modules/billing/lib/topupCapture.js.map +7 -0
- package/dist/modules/billing/lib/topupReceipt.js +97 -0
- package/dist/modules/billing/lib/topupReceipt.js.map +7 -0
- package/dist/modules/billing/lib/topupTransition.js +19 -0
- package/dist/modules/billing/lib/topupTransition.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20260619000000_billing_prepaid.js +110 -0
- package/dist/modules/billing/migrations/Migration20260619000000_billing_prepaid.js.map +7 -0
- package/dist/modules/billing/setup.js +32 -1
- package/dist/modules/billing/setup.js.map +2 -2
- package/dist/modules/billing/subscribers/prepaid-topup-cancelled.js +26 -0
- package/dist/modules/billing/subscribers/prepaid-topup-cancelled.js.map +7 -0
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js +87 -0
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +7 -0
- package/dist/modules/billing/subscribers/prepaid-topup-expired.js +26 -0
- package/dist/modules/billing/subscribers/prepaid-topup-expired.js.map +7 -0
- package/dist/modules/billing/subscribers/prepaid-topup-failed.js +26 -0
- package/dist/modules/billing/subscribers/prepaid-topup-failed.js.map +7 -0
- package/dist/modules/billing/workers/reconcile-prepaid-balances.js +50 -0
- package/dist/modules/billing/workers/reconcile-prepaid-balances.js.map +7 -0
- package/package.json +1 -1
- package/src/modules/billing/__tests__/commands/topups.test.ts +104 -0
- package/src/modules/billing/__tests__/commands/transactions.test.ts +84 -0
- package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +235 -0
- package/src/modules/billing/__tests__/lib/accountStatus.test.ts +93 -0
- package/src/modules/billing/__tests__/lib/balanceLedger.test.ts +125 -0
- package/src/modules/billing/__tests__/lib/balanceStatus.test.ts +65 -0
- package/src/modules/billing/__tests__/lib/creditStatus.test.ts +111 -0
- package/src/modules/billing/__tests__/lib/money.test.ts +56 -0
- package/src/modules/billing/__tests__/lib/prepaidConsume.test.ts +179 -0
- package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +232 -0
- package/src/modules/billing/__tests__/lib/topupCapture.test.ts +123 -0
- package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +84 -0
- package/src/modules/billing/__tests__/setup.test.ts +28 -2
- package/src/modules/billing/acl.ts +8 -0
- package/src/modules/billing/api/accounts/[id]/balance/route.ts +74 -0
- package/src/modules/billing/api/accounts/[id]/credit-status/route.ts +71 -0
- package/src/modules/billing/api/accounts/[id]/topups/route.ts +49 -0
- package/src/modules/billing/api/accounts/route.ts +7 -0
- package/src/modules/billing/api/export/account/[id]/route.ts +47 -2
- package/src/modules/billing/api/statements/generate/route.ts +78 -0
- package/src/modules/billing/api/statements/route.ts +117 -0
- package/src/modules/billing/api/topups/route.ts +154 -0
- package/src/modules/billing/api/transactions/adjust/route.ts +80 -0
- package/src/modules/billing/api/transactions/route.ts +116 -0
- package/src/modules/billing/api/usage/route.ts +36 -5
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +28 -0
- package/src/modules/billing/backend/billing/accounts/create/page.tsx +7 -0
- package/src/modules/billing/backend/billing/accounts/page.tsx +32 -1
- package/src/modules/billing/backend/billing/statements/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/statements/[id]/page.tsx +198 -0
- package/src/modules/billing/backend/billing/statements/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/statements/page.tsx +190 -0
- package/src/modules/billing/backend/billing/topups/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/topups/[id]/page.tsx +151 -0
- package/src/modules/billing/backend/billing/topups/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/topups/page.tsx +182 -0
- package/src/modules/billing/backend/billing/transactions/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/transactions/[id]/page.tsx +179 -0
- package/src/modules/billing/backend/billing/transactions/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/transactions/page.tsx +187 -0
- package/src/modules/billing/commands/accounts.ts +42 -1
- package/src/modules/billing/commands/index.ts +3 -0
- package/src/modules/billing/commands/invoices.ts +26 -2
- package/src/modules/billing/commands/statements.ts +101 -0
- package/src/modules/billing/commands/topups.ts +181 -0
- package/src/modules/billing/commands/transactions.ts +152 -0
- package/src/modules/billing/commands/usage.ts +114 -4
- package/src/modules/billing/components/AccountForm.tsx +66 -0
- package/src/modules/billing/components/PrepaidPanel.tsx +382 -0
- package/src/modules/billing/components/accountNames.ts +23 -0
- package/src/modules/billing/data/entities.ts +383 -0
- package/src/modules/billing/data/entityIds.ts +5 -0
- package/src/modules/billing/data/validators.ts +144 -0
- package/src/modules/billing/events.ts +52 -0
- package/src/modules/billing/i18n/en.json +112 -4
- package/src/modules/billing/i18n/pl.json +112 -4
- package/src/modules/billing/index.ts +1 -1
- package/src/modules/billing/lib/accountStatus.ts +155 -0
- package/src/modules/billing/lib/balanceEvents.ts +74 -0
- package/src/modules/billing/lib/balanceLedger.ts +258 -0
- package/src/modules/billing/lib/balanceStatus.ts +78 -0
- package/src/modules/billing/lib/billRunEngine.ts +66 -5
- package/src/modules/billing/lib/creditStatus.ts +153 -0
- package/src/modules/billing/lib/money.ts +73 -0
- package/src/modules/billing/lib/prepaidConfig.ts +58 -0
- package/src/modules/billing/lib/prepaidConsume.ts +299 -0
- package/src/modules/billing/lib/prepaidPeriodClose.ts +453 -0
- package/src/modules/billing/lib/topupCapture.ts +117 -0
- package/src/modules/billing/lib/topupReceipt.ts +153 -0
- package/src/modules/billing/lib/topupTransition.ts +31 -0
- package/src/modules/billing/migrations/Migration20260619000000_billing_prepaid.ts +131 -0
- package/src/modules/billing/setup.ts +45 -0
- package/src/modules/billing/subscribers/prepaid-topup-cancelled.ts +31 -0
- package/src/modules/billing/subscribers/prepaid-topup-captured.ts +120 -0
- package/src/modules/billing/subscribers/prepaid-topup-expired.ts +31 -0
- package/src/modules/billing/subscribers/prepaid-topup-failed.ts +32 -0
- package/src/modules/billing/workers/reconcile-prepaid-balances.ts +68 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# @dainamite/billing
|
|
2
2
|
|
|
3
|
+
## 0.18.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f8210ac: Prepaid billing mode (SPEC-002 / XD-304). Adds an additive `prepaid` billing
|
|
8
|
+
mode alongside the existing postpaid recurring engine: top-up balance,
|
|
9
|
+
real-time atomic usage debit (never rejects usage), append-only transaction
|
|
10
|
+
ledger, prepaid period-close statements, manual balance adjustments, and the
|
|
11
|
+
prepaid admin UI (balance panel, Transactions / Top-ups / Statements lists +
|
|
12
|
+
detail pages). Postpaid behaviour is unchanged.
|
|
13
|
+
|
|
14
|
+
## 0.18.0 — Prepaid balance billing (SPEC-002, XD-304, unreleased)
|
|
15
|
+
|
|
16
|
+
Adds a second billing **mode** to the package: `prepaid`. A customer tops up
|
|
17
|
+
a monetary **balance**, then consumes a metered product; each usage upload is
|
|
18
|
+
rated in real time, debited from the balance, and the API returns the
|
|
19
|
+
remaining balance. Usage is **never rejected** — low/exhausted is reported and
|
|
20
|
+
the client throttles itself. Postpaid accounts are completely unaffected.
|
|
21
|
+
|
|
22
|
+
- **Schema (additive).** `billing_accounts.billing_mode` (`postpaid`|`prepaid`,
|
|
23
|
+
immutable) + `credit_limit`. New tables: `billing_account_balances` (1:1
|
|
24
|
+
running-balance cache), `billing_account_transactions` (append-only ledger,
|
|
25
|
+
`balance = SUM(amount)`), `billing_topups` (registered top-up lifecycle),
|
|
26
|
+
`billing_statements` (period close). `billing_run_outcomes.statement_id`.
|
|
27
|
+
- **Real-time consume.** `POST /api/billing/usage` on a prepaid account rates
|
|
28
|
+
synchronously (reusing `lib/usageRater`), debits atomically
|
|
29
|
+
(`UPDATE … RETURNING` + paired transaction), and returns `ratedAmount`,
|
|
30
|
+
`balance`, `balanceStatus`, `creditStatus`, `tierBreakdown`. Idempotent on
|
|
31
|
+
`sourceRef` (no double-debit). `GET …/balance`, `GET /transactions`.
|
|
32
|
+
- **Top-ups.** `POST /api/billing/accounts/{id}/topups` registers a
|
|
33
|
+
`BillingTopup` (pending) and opens a `core/payment_gateways` session; the
|
|
34
|
+
`payment.captured` subscriber idempotently credits the balance, issues a
|
|
35
|
+
**posted+paid VAT receipt** in `core/sales` (VAT at top-up), and flips the
|
|
36
|
+
top-up to captured. Sibling subscribers handle failed/cancelled/expired.
|
|
37
|
+
- **Period close.** The Bill Run still runs for prepaid accounts, charging
|
|
38
|
+
`recurring`/`one_time` items to the **balance** (deterministic `source_ref`,
|
|
39
|
+
`billed_to_date` guard) and rolling them into a non-fiscal
|
|
40
|
+
**`BillingStatement`** instead of a payable invoice.
|
|
41
|
+
- **Credit limit (both modes).** Report-only `creditStatus` + `GET …/credit-status`
|
|
42
|
+
(prepaid `max(0,−balance)`; postpaid Σ posted-unpaid outstanding) +
|
|
43
|
+
`billing.credit.over_limit`. Billing **never blocks**.
|
|
44
|
+
- **Admin UI.** Account Prepaid panel (balance, status, credit, top-up,
|
|
45
|
+
transactions), top-ups + statements list/detail pages, account create/edit
|
|
46
|
+
mode + credit fields; en/pl i18n.
|
|
47
|
+
- **Hardening.** GDPR export extended (balance/transactions/top-ups/statements);
|
|
48
|
+
optional reconciliation worker; manual balance adjustments
|
|
49
|
+
(`POST /transactions/adjust`, audited).
|
|
50
|
+
- **Deviation.** The top-up receipt is created **posted+paid** (not draft) —
|
|
51
|
+
money already received has nothing to review (mirrors the xd-249 deviation
|
|
52
|
+
convention). VAT rate resolves from `prepaid.topup_tax_rate` config (the
|
|
53
|
+
postpaid path resolves no rate today).
|
|
54
|
+
|
|
3
55
|
## 0.17.0 — Bill Run + invoices live fixes, CPQ-style list views (unreleased)
|
|
4
56
|
|
|
5
57
|
Continued live-database shakedown — running an actual Bill Run and
|
|
@@ -59,7 +111,7 @@ and fixed; covered by new Playwright integration tests.
|
|
|
59
111
|
`mapInput` read `raw.id` directly, never found it, and threw
|
|
60
112
|
"id is required". `mapInput` now unwraps the envelope.
|
|
61
113
|
- **`?id=` list filter was a no-op.** `GET /api/billing/{accounts,
|
|
62
|
-
|
|
114
|
+
items,runs}?id=<uuid>` is how the detail pages fetch one record,
|
|
63
115
|
but `buildFilters` never handled `id`, so the endpoint returned an
|
|
64
116
|
unrelated row. `id` is now an indexed filter on all three list
|
|
65
117
|
routes (and added to the list query schemas).
|
|
@@ -136,14 +188,14 @@ write path.
|
|
|
136
188
|
- runs **one** idempotency query over all `source_ref`s in the batch
|
|
137
189
|
(`sourceRef: { $in }` + `billAccountId: { $in }`) instead of one
|
|
138
190
|
per item;
|
|
139
|
-
- dedups `source_ref`s repeated
|
|
191
|
+
- dedups `source_ref`s repeated _within_ the same payload (first
|
|
140
192
|
wins — the unique index would reject the rest on flush anyway);
|
|
141
193
|
- `em.create` + `persist`s every new row, then issues a **single**
|
|
142
194
|
`em.flush()` for the whole batch;
|
|
143
195
|
- emits per-entity CRUD side effects (`markOrmEntityChange` +
|
|
144
196
|
events) only after the flush commits.
|
|
145
197
|
- Returns `{ created, deduplicated, items: [{ sourceRef, id,
|
|
146
|
-
|
|
198
|
+
deduplicated }] }` so callers can map results back to inputs.
|
|
147
199
|
- **`POST /api/billing/items/bulk`** — REST surface for the command,
|
|
148
200
|
gated by `billing.item.manage`, `openApi` documented.
|
|
149
201
|
- **Validators.** `billingItemBulkEntrySchema` (per-item, no scope,
|
|
@@ -176,16 +228,16 @@ on the way through.
|
|
|
176
228
|
no limit, materialising every row in JS memory before summing. For
|
|
177
229
|
a telco account with 1M usage records per cycle this OOMs. New
|
|
178
230
|
implementation does a single `SELECT uom_code, SUM(quantity),
|
|
179
|
-
|
|
231
|
+
COUNT(*) FROM billing_account_usage WHERE … GROUP BY uom_code` —
|
|
180
232
|
result set is `O(distinct uom_codes)` (typically 1-20) regardless
|
|
181
233
|
of record count. Index `(bill_account_id, uom_code,
|
|
182
|
-
|
|
234
|
+
rated_in_bill_run_id, period_end)` already exists from Phase 0.
|
|
183
235
|
- **Engine rated-marking: id list → predicate update.** The
|
|
184
236
|
follow-up "mark consumed records" step used to take a `consumedUsageIds`
|
|
185
237
|
array and issue `UPDATE … WHERE id IN (millions of UUIDs)`. New
|
|
186
238
|
shape: the runner returns `matchedUoms` (small) and the engine does
|
|
187
239
|
`UPDATE … WHERE uom_code IN (matchedUoms) AND <account scope>
|
|
188
|
-
|
|
240
|
+
AND rated_in_bill_run_id IS NULL AND period_end <= ?` — no ID list
|
|
189
241
|
ever leaves Postgres. Memory-bounded regardless of how many records
|
|
190
242
|
the predicate touches.
|
|
191
243
|
- **GDPR export: time window + hard cap.** The
|
|
@@ -230,9 +282,9 @@ connector created — without dropping to `curl`.
|
|
|
230
282
|
- `usage` (tiered): raw-JSON textarea for the advanced shape.
|
|
231
283
|
Server validators (Phase 0 Zod) reject malformed structures;
|
|
232
284
|
the textarea includes the canonical example as placeholder.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
285
|
+
Plus optional `subscriptionId` / `subscriptionItemId` external
|
|
286
|
+
references. UX matches `AccountForm` (Cmd/Ctrl+Enter submit, Kbd
|
|
287
|
+
footer hint, no raw HTML buttons).
|
|
236
288
|
- `/backend/billing/items` — list with filters (type, account,
|
|
237
289
|
subscription, active) + per-row "Open" link. **New item** CTA in
|
|
238
290
|
the page header.
|
|
@@ -251,6 +303,7 @@ Validation: yarn build + generate + typecheck + test all green;
|
|
|
251
303
|
auto-discovered (3 accounts + 3 items + 2 runs + 2 invoices).
|
|
252
304
|
|
|
253
305
|
Deferred to follow-up:
|
|
306
|
+
|
|
254
307
|
- Locale files (en, pl) — UI ships with inline English fallbacks.
|
|
255
308
|
- "Add item" button on the account detail page (uses the deep-link
|
|
256
309
|
parameter the create page already supports).
|
|
@@ -282,6 +335,7 @@ Validation: yarn build + generate + typecheck + test all green; 797
|
|
|
282
335
|
repo tests, 0 regressions; all 7 backend pages auto-discovered.
|
|
283
336
|
|
|
284
337
|
Deferred to follow-up:
|
|
338
|
+
|
|
285
339
|
- Item create / edit pages (items are typically integration-driven —
|
|
286
340
|
the REST API at `/api/billing/items` covers operator-side needs).
|
|
287
341
|
- Locale files (en, pl).
|
|
@@ -308,7 +362,7 @@ edits no longer require `curl`.
|
|
|
308
362
|
test-invoices flow and the line-remove flow, satisfying the
|
|
309
363
|
UI AGENTS.md MUST rule.
|
|
310
364
|
- All writes go through the existing `billing.invoices.{edit,add,
|
|
311
|
-
|
|
365
|
+
remove}_draft_line` commands — every line mutation is still
|
|
312
366
|
audited via `DraftInvoiceEdit` with before/after snapshots, and
|
|
313
367
|
invoice totals are recomputed by the engine on each write.
|
|
314
368
|
|
|
@@ -317,6 +371,7 @@ Validation: yarn build + generate + typecheck + test all green;
|
|
|
317
371
|
pages + 1 shared component.
|
|
318
372
|
|
|
319
373
|
Deferred to follow-up:
|
|
374
|
+
|
|
320
375
|
- Account / item editor pages
|
|
321
376
|
- Locale files (en, pl)
|
|
322
377
|
|
|
@@ -348,7 +403,7 @@ to review or post.
|
|
|
348
403
|
- **Wipe test invoices for this run** — visible only for
|
|
349
404
|
`metadata.test_run=true`. Hard-deletes every test invoice from
|
|
350
405
|
the same `bill_run_id` via `billing.invoices.wipe_test`. `window
|
|
351
|
-
|
|
406
|
+
.confirm` gate (will move to `ConfirmDialog` in a follow-up to
|
|
352
407
|
match the OM UX convention).
|
|
353
408
|
|
|
354
409
|
Sidebar entry under the `Billing` group at order 30.
|
|
@@ -359,6 +414,7 @@ add-line, edit-line, remove-line, invoices list, invoices detail);
|
|
|
359
414
|
5 backend pages.
|
|
360
415
|
|
|
361
416
|
Deferred to follow-up:
|
|
417
|
+
|
|
362
418
|
- Inline line edit / add / remove dialogs on the detail page (REST
|
|
363
419
|
endpoints already exist from Phase 4b).
|
|
364
420
|
- ConfirmDialog instead of `window.confirm` for the wipe action.
|
|
@@ -399,6 +455,7 @@ Validation: yarn build + generate + typecheck + test all green;
|
|
|
399
455
|
`.mercato/generated/backend-routes.generated.ts`.
|
|
400
456
|
|
|
401
457
|
Deferred to a follow-up:
|
|
458
|
+
|
|
402
459
|
- Account / item / usage / draft-invoice editor pages
|
|
403
460
|
- Triggers (manual Bill Run / dry-run preview) as buttons in the
|
|
404
461
|
list page
|
|
@@ -423,7 +480,7 @@ fully usable from the REST surface today.
|
|
|
423
480
|
pre-removal snapshot preserved in `before_json`. `after_json`
|
|
424
481
|
and `invoice_line_id` are `null`.
|
|
425
482
|
- All three refuse non-draft invoices with `HTTP 409 +
|
|
426
|
-
|
|
483
|
+
billing.invoice.not_draft` and require the
|
|
427
484
|
`billing.invoice.edit_draft` feature.
|
|
428
485
|
- **Test-invoice wipe command** `billing.invoices.wipe_test`:
|
|
429
486
|
hard-deletes invoices flagged `metadata.test_run=true`, optionally
|
|
@@ -449,6 +506,7 @@ fully usable from the REST surface today.
|
|
|
449
506
|
`billRunId`).
|
|
450
507
|
|
|
451
508
|
Deferred to **Phase 4c** (UI dedicated effort):
|
|
509
|
+
|
|
452
510
|
- Backend pages (DataTable + CrudForm) for accounts / items / usage /
|
|
453
511
|
bill runs / draft invoice review.
|
|
454
512
|
- Sidebar navigation + page metadata.
|
|
@@ -491,6 +549,7 @@ Deferred to **Phase 4c** (UI dedicated effort):
|
|
|
491
549
|
- 6 new unit tests for the resolver (199 total for billing).
|
|
492
550
|
|
|
493
551
|
Deferred to **Phase 4b**:
|
|
552
|
+
|
|
494
553
|
- Draft-edit endpoint + `DraftInvoiceEdit` audit row writing.
|
|
495
554
|
- GDPR-portability `GET /api/billing/export/account/{id}`.
|
|
496
555
|
- Admin UI (backend pages for accounts / items / runs / draft review).
|
|
@@ -499,6 +558,7 @@ Deferred to **Phase 4b**:
|
|
|
499
558
|
|
|
500
559
|
- **Usage rater** in `lib/usageRater.ts` — pure functions for all four
|
|
501
560
|
spec-mandated rate shapes:
|
|
561
|
+
|
|
502
562
|
- Simple flat (`{ unit_price }`): `quantity × unit_price`.
|
|
503
563
|
- Volume tier (`{ model: 'volume', tiers }`): entire quantity at the
|
|
504
564
|
`unit_price` of the tier its total falls into.
|
|
@@ -509,12 +569,13 @@ Deferred to **Phase 4b**:
|
|
|
509
569
|
|
|
510
570
|
Returns `{ amount, breakdown }` where `breakdown` is the per-tier
|
|
511
571
|
audit trail persisted into `SalesInvoiceLine.metadata
|
|
512
|
-
|
|
572
|
+
.usage_tier_breakdown`. Half-up rounding to 2dp on the amount;
|
|
513
573
|
unit prices preserve sub-cent precision via the dedicated
|
|
514
574
|
`formatUnitPrice` helper (e.g. `0.001 → "0.0010"` instead of being
|
|
515
575
|
rounded to zero).
|
|
516
576
|
|
|
517
577
|
- **Usage runner** in `lib/usageRunner.ts` — per-account processor:
|
|
578
|
+
|
|
518
579
|
- Selects unrated usage records (`rated_in_bill_run_id IS NULL`,
|
|
519
580
|
`period_end <= bill_period_end`).
|
|
520
581
|
- Groups by `uom_code`, matches against the account's `type=usage`
|
|
@@ -526,9 +587,10 @@ Deferred to **Phase 4b**:
|
|
|
526
587
|
signal to add the missing item then trigger retry-failed.
|
|
527
588
|
|
|
528
589
|
- **Engine integration** in `lib/billRunEngine.ts`:
|
|
590
|
+
|
|
529
591
|
- Per-cycle loop now combines `buildInvoiceLinesFromItems` (one_time
|
|
530
|
-
|
|
531
|
-
|
|
592
|
+
- recurring) with `processUsageForAccount` (usage) into a single
|
|
593
|
+
`SalesInvoice` write.
|
|
532
594
|
- Real-mode commits mark consumed usage records'
|
|
533
595
|
`rated_in_bill_run_id` via a single `nativeUpdate` (scales to
|
|
534
596
|
accounts with thousands of records per cycle).
|
|
@@ -548,9 +610,9 @@ Deferred to **Phase 4b**:
|
|
|
548
610
|
|
|
549
611
|
- 27 new unit tests (193 total for the package):
|
|
550
612
|
- `usageRater` covers every spec example end-to-end (`25k vs
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
613
|
+
[10k=0, ∞=0.001]` graduated → 15 EUR, `25k vs [10k=0,
|
|
614
|
+
50k=0.001, ∞=0.0005]` volume → 25 EUR, `25k vs
|
|
615
|
+
[10k=50, 50k=200, ∞=1000]` flat → 200 EUR) plus boundary
|
|
554
616
|
conditions and the `1.005 → 1.01` half-up edge case.
|
|
555
617
|
- Engine integration: graduated 25k → 15 EUR end-to-end, unmatched
|
|
556
618
|
UoM warning + un-rated guard, simple-flat unit_price emission,
|
|
@@ -624,7 +686,7 @@ Phase 2 deviations):
|
|
|
624
686
|
- `GET / POST / PUT / DELETE /api/billing/accounts` (Billing Accounts CRUD)
|
|
625
687
|
- `GET / POST / PUT / DELETE /api/billing/items` (Billing Items CRUD with
|
|
626
688
|
`source_ref` idempotency on POST — duplicate `(tenant, account,
|
|
627
|
-
|
|
689
|
+
sourceRef)` returns the existing row with `deduplicated: true`)
|
|
628
690
|
- `GET / POST /api/billing/usage` (append-only usage ingest; idempotent
|
|
629
691
|
by `source_ref`; no update / delete by design)
|
|
630
692
|
- Lean command pattern (no undo / snapshot yet — Phase 4 adds those):
|
|
@@ -10,7 +10,15 @@ const features = [
|
|
|
10
10
|
{ id: "billing.run.view", title: "View Bill Run History", module: "billing" },
|
|
11
11
|
{ id: "billing.invoice.post", title: "Post Draft Invoice", module: "billing" },
|
|
12
12
|
{ id: "billing.invoice.edit_draft", title: "Edit Draft Invoice", module: "billing" },
|
|
13
|
-
{ id: "billing.invoice.view", title: "View Billing Invoices", module: "billing" }
|
|
13
|
+
{ id: "billing.invoice.view", title: "View Billing Invoices", module: "billing" },
|
|
14
|
+
// ─── Prepaid balance billing (SPEC-002) ──────────────────────
|
|
15
|
+
{ id: "billing.balance.view", title: "View Balance & Transactions", module: "billing" },
|
|
16
|
+
{ id: "billing.balance.adjust", title: "Adjust Balance (manual credit/debit)", module: "billing" },
|
|
17
|
+
{ id: "billing.topup.create", title: "Initiate Account Top-up", module: "billing" },
|
|
18
|
+
{ id: "billing.topup.view", title: "View Account Top-ups", module: "billing" },
|
|
19
|
+
{ id: "billing.statement.view", title: "View Consumption Statements", module: "billing" },
|
|
20
|
+
{ id: "billing.statement.generate", title: "Generate Consumption Statement", module: "billing" },
|
|
21
|
+
{ id: "billing.credit.view", title: "View Account Credit Status", module: "billing" }
|
|
14
22
|
];
|
|
15
23
|
var acl_default = features;
|
|
16
24
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/billing/acl.ts"],
|
|
4
|
-
"sourcesContent": ["export const features = [\n { id: 'billing.account.manage', title: 'Manage Billing Accounts', module: 'billing' },\n { id: 'billing.account.view', title: 'View Billing Accounts', module: 'billing' },\n { id: 'billing.item.manage', title: 'Manage Billing Items', module: 'billing' },\n { id: 'billing.item.view', title: 'View Billing Items', module: 'billing' },\n { id: 'billing.usage.ingest', title: 'Ingest Usage Records', module: 'billing' },\n { id: 'billing.usage.view', title: 'View Usage Records', module: 'billing' },\n { id: 'billing.run.trigger', title: 'Trigger Bill Run', module: 'billing' },\n { id: 'billing.run.dry_run', title: 'Trigger Bill Run (dry-run)', module: 'billing' },\n { id: 'billing.run.view', title: 'View Bill Run History', module: 'billing' },\n { id: 'billing.invoice.post', title: 'Post Draft Invoice', module: 'billing' },\n { id: 'billing.invoice.edit_draft', title: 'Edit Draft Invoice', module: 'billing' },\n { id: 'billing.invoice.view', title: 'View Billing Invoices', module: 'billing' },\n] as const\n\nexport default features\n"],
|
|
5
|
-
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,0BAA0B,OAAO,2BAA2B,QAAQ,UAAU;AAAA,EACpF,EAAE,IAAI,wBAAwB,OAAO,yBAAyB,QAAQ,UAAU;AAAA,EAChF,EAAE,IAAI,uBAAuB,OAAO,wBAAwB,QAAQ,UAAU;AAAA,EAC9E,EAAE,IAAI,qBAAqB,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EAC1E,EAAE,IAAI,wBAAwB,OAAO,wBAAwB,QAAQ,UAAU;AAAA,EAC/E,EAAE,IAAI,sBAAsB,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EAC3E,EAAE,IAAI,uBAAuB,OAAO,oBAAoB,QAAQ,UAAU;AAAA,EAC1E,EAAE,IAAI,uBAAuB,OAAO,8BAA8B,QAAQ,UAAU;AAAA,EACpF,EAAE,IAAI,oBAAoB,OAAO,yBAAyB,QAAQ,UAAU;AAAA,EAC5E,EAAE,IAAI,wBAAwB,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EAC7E,EAAE,IAAI,8BAA8B,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EACnF,EAAE,IAAI,wBAAwB,OAAO,yBAAyB,QAAQ,UAAU;
|
|
4
|
+
"sourcesContent": ["export const features = [\n { id: 'billing.account.manage', title: 'Manage Billing Accounts', module: 'billing' },\n { id: 'billing.account.view', title: 'View Billing Accounts', module: 'billing' },\n { id: 'billing.item.manage', title: 'Manage Billing Items', module: 'billing' },\n { id: 'billing.item.view', title: 'View Billing Items', module: 'billing' },\n { id: 'billing.usage.ingest', title: 'Ingest Usage Records', module: 'billing' },\n { id: 'billing.usage.view', title: 'View Usage Records', module: 'billing' },\n { id: 'billing.run.trigger', title: 'Trigger Bill Run', module: 'billing' },\n { id: 'billing.run.dry_run', title: 'Trigger Bill Run (dry-run)', module: 'billing' },\n { id: 'billing.run.view', title: 'View Bill Run History', module: 'billing' },\n { id: 'billing.invoice.post', title: 'Post Draft Invoice', module: 'billing' },\n { id: 'billing.invoice.edit_draft', title: 'Edit Draft Invoice', module: 'billing' },\n { id: 'billing.invoice.view', title: 'View Billing Invoices', module: 'billing' },\n // \u2500\u2500\u2500 Prepaid balance billing (SPEC-002) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n { id: 'billing.balance.view', title: 'View Balance & Transactions', module: 'billing' },\n { id: 'billing.balance.adjust', title: 'Adjust Balance (manual credit/debit)', module: 'billing' },\n { id: 'billing.topup.create', title: 'Initiate Account Top-up', module: 'billing' },\n { id: 'billing.topup.view', title: 'View Account Top-ups', module: 'billing' },\n { id: 'billing.statement.view', title: 'View Consumption Statements', module: 'billing' },\n { id: 'billing.statement.generate', title: 'Generate Consumption Statement', module: 'billing' },\n { id: 'billing.credit.view', title: 'View Account Credit Status', module: 'billing' },\n] as const\n\nexport default features\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,0BAA0B,OAAO,2BAA2B,QAAQ,UAAU;AAAA,EACpF,EAAE,IAAI,wBAAwB,OAAO,yBAAyB,QAAQ,UAAU;AAAA,EAChF,EAAE,IAAI,uBAAuB,OAAO,wBAAwB,QAAQ,UAAU;AAAA,EAC9E,EAAE,IAAI,qBAAqB,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EAC1E,EAAE,IAAI,wBAAwB,OAAO,wBAAwB,QAAQ,UAAU;AAAA,EAC/E,EAAE,IAAI,sBAAsB,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EAC3E,EAAE,IAAI,uBAAuB,OAAO,oBAAoB,QAAQ,UAAU;AAAA,EAC1E,EAAE,IAAI,uBAAuB,OAAO,8BAA8B,QAAQ,UAAU;AAAA,EACpF,EAAE,IAAI,oBAAoB,OAAO,yBAAyB,QAAQ,UAAU;AAAA,EAC5E,EAAE,IAAI,wBAAwB,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EAC7E,EAAE,IAAI,8BAA8B,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EACnF,EAAE,IAAI,wBAAwB,OAAO,yBAAyB,QAAQ,UAAU;AAAA;AAAA,EAEhF,EAAE,IAAI,wBAAwB,OAAO,+BAA+B,QAAQ,UAAU;AAAA,EACtF,EAAE,IAAI,0BAA0B,OAAO,wCAAwC,QAAQ,UAAU;AAAA,EACjG,EAAE,IAAI,wBAAwB,OAAO,2BAA2B,QAAQ,UAAU;AAAA,EAClF,EAAE,IAAI,sBAAsB,OAAO,wBAAwB,QAAQ,UAAU;AAAA,EAC7E,EAAE,IAAI,0BAA0B,OAAO,+BAA+B,QAAQ,UAAU;AAAA,EACxF,EAAE,IAAI,8BAA8B,OAAO,kCAAkC,QAAQ,UAAU;AAAA,EAC/F,EAAE,IAAI,uBAAuB,OAAO,8BAA8B,QAAQ,UAAU;AACtF;AAEA,IAAO,cAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
3
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
|
+
import { BillingAccount } from "../../../../data/entities.js";
|
|
5
|
+
import { getBalanceSnapshot } from "../../../../lib/accountStatus.js";
|
|
6
|
+
const metadata = {
|
|
7
|
+
GET: { requireAuth: true, requireFeatures: ["billing.balance.view"] }
|
|
8
|
+
};
|
|
9
|
+
async function GET(req, { params }) {
|
|
10
|
+
let container = null;
|
|
11
|
+
try {
|
|
12
|
+
container = await createRequestContainer();
|
|
13
|
+
const auth = await getAuthFromRequest(req);
|
|
14
|
+
if (!auth || !auth.tenantId) {
|
|
15
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
const organizationId = auth.orgId ?? null;
|
|
18
|
+
if (!organizationId) {
|
|
19
|
+
return NextResponse.json({ error: "Organization scope required" }, { status: 403 });
|
|
20
|
+
}
|
|
21
|
+
const { id: accountId } = await params;
|
|
22
|
+
const em = container.resolve("em").fork();
|
|
23
|
+
const account = await em.findOne(BillingAccount, {
|
|
24
|
+
id: accountId,
|
|
25
|
+
tenantId: auth.tenantId,
|
|
26
|
+
organizationId,
|
|
27
|
+
deletedAt: null
|
|
28
|
+
});
|
|
29
|
+
if (!account) {
|
|
30
|
+
return NextResponse.json({ error: "Account not found" }, { status: 404 });
|
|
31
|
+
}
|
|
32
|
+
if (account.billingMode !== "prepaid") {
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
{ error: "Account is not prepaid", code: "billing.account.not_prepaid" },
|
|
35
|
+
{ status: 409 }
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const snapshot = await getBalanceSnapshot(em, account);
|
|
39
|
+
return NextResponse.json(snapshot);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error("[billing/accounts/balance.GET]", err);
|
|
42
|
+
return NextResponse.json({ error: "Internal error" }, { status: 500 });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const openApi = {
|
|
46
|
+
methods: {
|
|
47
|
+
GET: {
|
|
48
|
+
tags: ["Billing"],
|
|
49
|
+
summary: "Get a prepaid account balance + status",
|
|
50
|
+
parameters: [
|
|
51
|
+
{ name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } }
|
|
52
|
+
],
|
|
53
|
+
description: "Returns the current running balance, `balanceStatus` (ok/low/exhausted), the resolved low-balance threshold, the currency, and the last-movement timestamp for a prepaid account. 409 (`billing.account.not_prepaid`) for postpaid accounts."
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
export {
|
|
58
|
+
GET,
|
|
59
|
+
metadata,
|
|
60
|
+
openApi
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../src/modules/billing/api/accounts/%5Bid%5D/balance/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { BillingAccount } from '../../../../data/entities'\nimport { getBalanceSnapshot } from '../../../../lib/accountStatus'\n\n/**\n * `GET /api/billing/accounts/[id]/balance` \u2014 current prepaid balance, status,\n * and resolved low-balance threshold (SPEC-002). Read-only.\n */\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.balance.view'] },\n}\n\nexport async function GET(\n req: Request,\n { params }: { params: Promise<{ id: string }> },\n) {\n let container: Awaited<ReturnType<typeof createRequestContainer>> | null = null\n try {\n container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = (auth as { orgId?: string }).orgId ?? null\n if (!organizationId) {\n return NextResponse.json({ error: 'Organization scope required' }, { status: 403 })\n }\n\n const { id: accountId } = await params\n const em = (container.resolve('em') as EntityManager).fork()\n const account = await em.findOne(BillingAccount, {\n id: accountId,\n tenantId: auth.tenantId,\n organizationId,\n deletedAt: null,\n })\n if (!account) {\n return NextResponse.json({ error: 'Account not found' }, { status: 404 })\n }\n if (account.billingMode !== 'prepaid') {\n return NextResponse.json(\n { error: 'Account is not prepaid', code: 'billing.account.not_prepaid' },\n { status: 409 },\n )\n }\n\n const snapshot = await getBalanceSnapshot(em, account)\n return NextResponse.json(snapshot)\n } catch (err) {\n console.error('[billing/accounts/balance.GET]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n\nexport const openApi = {\n methods: {\n GET: {\n tags: ['Billing'],\n summary: 'Get a prepaid account balance + status',\n parameters: [\n { name: 'id', in: 'path', required: true, schema: { type: 'string', format: 'uuid' } },\n ],\n description:\n 'Returns the current running balance, `balanceStatus` (ok/low/exhausted), ' +\n 'the resolved low-balance threshold, the currency, and the last-movement ' +\n 'timestamp for a prepaid account. 409 (`billing.account.not_prepaid`) for ' +\n 'postpaid accounts.',\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAC/B,SAAS,0BAA0B;AAO5B,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,sBAAsB,EAAE;AACtE;AAEA,eAAsB,IACpB,KACA,EAAE,OAAO,GACT;AACA,MAAI,YAAuE;AAC3E,MAAI;AACF,gBAAY,MAAM,uBAAuB;AACzC,UAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,aAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AACA,UAAM,iBAAkB,KAA4B,SAAS;AAC7D,QAAI,CAAC,gBAAgB;AACnB,aAAO,aAAa,KAAK,EAAE,OAAO,8BAA8B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpF;AAEA,UAAM,EAAE,IAAI,UAAU,IAAI,MAAM;AAChC,UAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,UAAM,UAAU,MAAM,GAAG,QAAQ,gBAAgB;AAAA,MAC/C,IAAI;AAAA,MACJ,UAAU,KAAK;AAAA,MACf;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,SAAS;AACZ,aAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC1E;AACA,QAAI,QAAQ,gBAAgB,WAAW;AACrC,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,0BAA0B,MAAM,8BAA8B;AAAA,QACvE,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAEA,UAAM,WAAW,MAAM,mBAAmB,IAAI,OAAO;AACrD,WAAO,aAAa,KAAK,QAAQ;AAAA,EACnC,SAAS,KAAK;AACZ,YAAQ,MAAM,kCAAkC,GAAG;AACnD,WAAO,aAAa,KAAK,EAAE,OAAO,iBAAiB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACvE;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,SAAS;AAAA,IACP,KAAK;AAAA,MACH,MAAM,CAAC,SAAS;AAAA,MAChB,SAAS;AAAA,MACT,YAAY;AAAA,QACV,EAAE,MAAM,MAAM,IAAI,QAAQ,UAAU,MAAM,QAAQ,EAAE,MAAM,UAAU,QAAQ,OAAO,EAAE;AAAA,MACvF;AAAA,MACA,aACE;AAAA,IAIJ;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
3
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
|
+
import { BillingAccount } from "../../../../data/entities.js";
|
|
5
|
+
import { getCreditSnapshot } from "../../../../lib/accountStatus.js";
|
|
6
|
+
const metadata = {
|
|
7
|
+
GET: { requireAuth: true, requireFeatures: ["billing.credit.view"] }
|
|
8
|
+
};
|
|
9
|
+
async function GET(req, { params }) {
|
|
10
|
+
let container = null;
|
|
11
|
+
try {
|
|
12
|
+
container = await createRequestContainer();
|
|
13
|
+
const auth = await getAuthFromRequest(req);
|
|
14
|
+
if (!auth || !auth.tenantId) {
|
|
15
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
const organizationId = auth.orgId ?? null;
|
|
18
|
+
if (!organizationId) {
|
|
19
|
+
return NextResponse.json({ error: "Organization scope required" }, { status: 403 });
|
|
20
|
+
}
|
|
21
|
+
const { id: accountId } = await params;
|
|
22
|
+
const em = container.resolve("em").fork();
|
|
23
|
+
const account = await em.findOne(BillingAccount, {
|
|
24
|
+
id: accountId,
|
|
25
|
+
tenantId: auth.tenantId,
|
|
26
|
+
organizationId,
|
|
27
|
+
deletedAt: null
|
|
28
|
+
});
|
|
29
|
+
if (!account) {
|
|
30
|
+
return NextResponse.json({ error: "Account not found" }, { status: 404 });
|
|
31
|
+
}
|
|
32
|
+
const snapshot = await getCreditSnapshot(em, account);
|
|
33
|
+
return NextResponse.json(snapshot);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error("[billing/accounts/credit-status.GET]", err);
|
|
36
|
+
return NextResponse.json({ error: "Internal error" }, { status: 500 });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const openApi = {
|
|
40
|
+
methods: {
|
|
41
|
+
GET: {
|
|
42
|
+
tags: ["Billing"],
|
|
43
|
+
summary: "Get an account credit status (both modes)",
|
|
44
|
+
parameters: [
|
|
45
|
+
{ name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } }
|
|
46
|
+
],
|
|
47
|
+
description: "Returns `{ mode, creditLimit, creditUsed, availableCredit, creditStatus, currencyCode }`. Prepaid creditUsed = max(0,\u2212balance); postpaid = \u03A3 posted-unpaid invoice outstanding. Report-only \u2014 billing never blocks."
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
export {
|
|
52
|
+
GET,
|
|
53
|
+
metadata,
|
|
54
|
+
openApi
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../src/modules/billing/api/accounts/%5Bid%5D/credit-status/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { BillingAccount } from '../../../../data/entities'\nimport { getCreditSnapshot } from '../../../../lib/accountStatus'\n\n/**\n * `GET /api/billing/accounts/[id]/credit-status` \u2014 credit limit / used /\n * available / status for BOTH modes (SPEC-002 P6). Report-only \u2014 billing never\n * blocks; the quote\u2192order flow (e.g. a CPQ credit guard) decides what to do.\n *\n * - prepaid: credit_used = max(0, \u2212balance)\n * - postpaid: credit_used = \u03A3 posted-unpaid invoice outstanding (core/sales)\n */\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.credit.view'] },\n}\n\nexport async function GET(\n req: Request,\n { params }: { params: Promise<{ id: string }> },\n) {\n let container: Awaited<ReturnType<typeof createRequestContainer>> | null = null\n try {\n container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = (auth as { orgId?: string }).orgId ?? null\n if (!organizationId) {\n return NextResponse.json({ error: 'Organization scope required' }, { status: 403 })\n }\n\n const { id: accountId } = await params\n const em = (container.resolve('em') as EntityManager).fork()\n const account = await em.findOne(BillingAccount, {\n id: accountId,\n tenantId: auth.tenantId,\n organizationId,\n deletedAt: null,\n })\n if (!account) {\n return NextResponse.json({ error: 'Account not found' }, { status: 404 })\n }\n\n const snapshot = await getCreditSnapshot(em, account)\n return NextResponse.json(snapshot)\n } catch (err) {\n console.error('[billing/accounts/credit-status.GET]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n\nexport const openApi = {\n methods: {\n GET: {\n tags: ['Billing'],\n summary: 'Get an account credit status (both modes)',\n parameters: [\n { name: 'id', in: 'path', required: true, schema: { type: 'string', format: 'uuid' } },\n ],\n description:\n 'Returns `{ mode, creditLimit, creditUsed, availableCredit, creditStatus, ' +\n 'currencyCode }`. Prepaid creditUsed = max(0,\u2212balance); postpaid = \u03A3 ' +\n 'posted-unpaid invoice outstanding. Report-only \u2014 billing never blocks.',\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAC/B,SAAS,yBAAyB;AAW3B,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AACrE;AAEA,eAAsB,IACpB,KACA,EAAE,OAAO,GACT;AACA,MAAI,YAAuE;AAC3E,MAAI;AACF,gBAAY,MAAM,uBAAuB;AACzC,UAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,aAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AACA,UAAM,iBAAkB,KAA4B,SAAS;AAC7D,QAAI,CAAC,gBAAgB;AACnB,aAAO,aAAa,KAAK,EAAE,OAAO,8BAA8B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpF;AAEA,UAAM,EAAE,IAAI,UAAU,IAAI,MAAM;AAChC,UAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,UAAM,UAAU,MAAM,GAAG,QAAQ,gBAAgB;AAAA,MAC/C,IAAI;AAAA,MACJ,UAAU,KAAK;AAAA,MACf;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,SAAS;AACZ,aAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC1E;AAEA,UAAM,WAAW,MAAM,kBAAkB,IAAI,OAAO;AACpD,WAAO,aAAa,KAAK,QAAQ;AAAA,EACnC,SAAS,KAAK;AACZ,YAAQ,MAAM,wCAAwC,GAAG;AACzD,WAAO,aAAa,KAAK,EAAE,OAAO,iBAAiB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACvE;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,SAAS;AAAA,IACP,KAAK;AAAA,MACH,MAAM,CAAC,SAAS;AAAA,MAChB,SAAS;AAAA,MACT,YAAY;AAAA,QACV,EAAE,MAAM,MAAM,IAAI,QAAQ,UAAU,MAAM,QAAQ,EAAE,MAAM,UAAU,QAAQ,OAAO,EAAE;AAAA,MACvF;AAAA,MACA,aACE;AAAA,IAGJ;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { POST as topupsPost } from "../../../topups/route.js";
|
|
2
|
+
const metadata = {
|
|
3
|
+
POST: { requireAuth: true, requireFeatures: ["billing.topup.create"] }
|
|
4
|
+
};
|
|
5
|
+
async function POST(req, { params }) {
|
|
6
|
+
const { id } = await params;
|
|
7
|
+
const body = await req.json().catch(() => ({}));
|
|
8
|
+
const merged = { ...body, billAccountId: id };
|
|
9
|
+
const headers = new Headers(req.headers);
|
|
10
|
+
headers.delete("content-length");
|
|
11
|
+
headers.set("content-type", "application/json");
|
|
12
|
+
const forwarded = new Request(req.url, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers,
|
|
15
|
+
body: JSON.stringify(merged)
|
|
16
|
+
});
|
|
17
|
+
return topupsPost(forwarded);
|
|
18
|
+
}
|
|
19
|
+
const openApi = {
|
|
20
|
+
methods: {
|
|
21
|
+
POST: {
|
|
22
|
+
tags: ["Billing"],
|
|
23
|
+
summary: "Initiate a prepaid top-up for an account",
|
|
24
|
+
parameters: [
|
|
25
|
+
{ name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } }
|
|
26
|
+
],
|
|
27
|
+
description: "Registers a BillingTopup (pending) and opens a payment session. The path `id` is the prepaid account; the balance is credited on capture. Body: `{ amount, providerKey?, successUrl?, cancelUrl?, sourceRef? }`."
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
export {
|
|
32
|
+
POST,
|
|
33
|
+
metadata,
|
|
34
|
+
openApi
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../src/modules/billing/api/accounts/%5Bid%5D/topups/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { POST as topupsPost } from '../../../topups/route'\n\n/**\n * `POST /api/billing/accounts/{id}/topups` (SPEC-002 P2).\n *\n * Spec-faithful nested path. Injects the path `id` as `billAccountId` into the\n * body and delegates to the flat `POST /api/billing/topups` handler so all the\n * RBAC / command-bus / side-effect plumbing lives in exactly one place.\n */\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['billing.topup.create'] },\n}\n\nexport async function POST(\n req: Request,\n { params }: { params: Promise<{ id: string }> },\n): Promise<Response> {\n const { id } = await params\n const body = (await req.json().catch(() => ({}))) as Record<string, unknown>\n const merged = { ...body, billAccountId: id }\n // Rebuild headers WITHOUT the stale content-length (the body changed); keep\n // auth (cookies / Authorization) intact and force JSON content-type.\n const headers = new Headers(req.headers)\n headers.delete('content-length')\n headers.set('content-type', 'application/json')\n const forwarded = new Request(req.url, {\n method: 'POST',\n headers,\n body: JSON.stringify(merged),\n })\n return topupsPost(forwarded)\n}\n\nexport const openApi = {\n methods: {\n POST: {\n tags: ['Billing'],\n summary: 'Initiate a prepaid top-up for an account',\n parameters: [\n { name: 'id', in: 'path', required: true, schema: { type: 'string', format: 'uuid' } },\n ],\n description:\n 'Registers a BillingTopup (pending) and opens a payment session. The ' +\n 'path `id` is the prepaid account; the balance is credited on capture. ' +\n 'Body: `{ amount, providerKey?, successUrl?, cancelUrl?, sourceRef? }`.',\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,QAAQ,kBAAkB;AAU5B,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,sBAAsB,EAAE;AACvE;AAEA,eAAsB,KACpB,KACA,EAAE,OAAO,GACU;AACnB,QAAM,EAAE,GAAG,IAAI,MAAM;AACrB,QAAM,OAAQ,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC/C,QAAM,SAAS,EAAE,GAAG,MAAM,eAAe,GAAG;AAG5C,QAAM,UAAU,IAAI,QAAQ,IAAI,OAAO;AACvC,UAAQ,OAAO,gBAAgB;AAC/B,UAAQ,IAAI,gBAAgB,kBAAkB;AAC9C,QAAM,YAAY,IAAI,QAAQ,IAAI,KAAK;AAAA,IACrC,QAAQ;AAAA,IACR;AAAA,IACA,MAAM,KAAK,UAAU,MAAM;AAAA,EAC7B,CAAC;AACD,SAAO,WAAW,SAAS;AAC7B;AAEO,MAAM,UAAU;AAAA,EACrB,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,MAAM,CAAC,SAAS;AAAA,MAChB,SAAS;AAAA,MACT,YAAY;AAAA,QACV,EAAE,MAAM,MAAM,IAAI,QAAQ,UAAU,MAAM,QAAQ,EAAE,MAAM,UAAU,QAAQ,OAAO,EAAE;AAAA,MACvF;AAAA,MACA,aACE;AAAA,IAGJ;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -35,6 +35,8 @@ const billingAccountListItemSchema = z.object({
|
|
|
35
35
|
invoice_address: z.record(z.string(), z.unknown()),
|
|
36
36
|
next_bill_date: z.string(),
|
|
37
37
|
last_bill_date: z.string().nullable(),
|
|
38
|
+
billing_mode: z.string(),
|
|
39
|
+
credit_limit: z.string(),
|
|
38
40
|
is_active: z.boolean(),
|
|
39
41
|
organization_id: z.string().uuid(),
|
|
40
42
|
tenant_id: z.string().uuid(),
|
|
@@ -67,6 +69,8 @@ const crud = makeCrudRoute({
|
|
|
67
69
|
"invoice_address",
|
|
68
70
|
"next_bill_date",
|
|
69
71
|
"last_bill_date",
|
|
72
|
+
"billing_mode",
|
|
73
|
+
"credit_limit",
|
|
70
74
|
"is_active",
|
|
71
75
|
"organization_id",
|
|
72
76
|
"tenant_id",
|
|
@@ -93,6 +97,9 @@ const crud = makeCrudRoute({
|
|
|
93
97
|
if (typeof query.billCycle === "string" && query.billCycle) {
|
|
94
98
|
filters.bill_cycle = { $eq: query.billCycle };
|
|
95
99
|
}
|
|
100
|
+
if (typeof query.billingMode === "string" && query.billingMode) {
|
|
101
|
+
filters.billing_mode = { $eq: query.billingMode };
|
|
102
|
+
}
|
|
96
103
|
if (typeof query.isActive === "boolean") {
|
|
97
104
|
filters.is_active = { $eq: query.isActive };
|
|
98
105
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/billing/api/accounts/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { BillingAccount } from '../../data/entities'\nimport { billingEntityIds } from '../../data/entityIds'\nimport {\n billingAccountCreateSchema,\n billingAccountListQuerySchema,\n billingAccountUpdateSchema,\n} from '../../data/validators'\nimport {\n createBillingCrudOpenApi,\n createPagedListResponseSchema,\n defaultCreateResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\n// \u2500\u2500\u2500 Auth / RBAC \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.account.view'] },\n POST: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n}\n\nexport const metadata = routeMetadata\n\n// \u2500\u2500\u2500 List response item shape (informational \u2014 used by OpenAPI) \u2500\u2500\n\n// The list endpoint projects raw column names \u2014 snake_case.\nconst billingAccountListItemSchema = z.object({\n id: z.string().uuid(),\n customer_id: z.string(),\n name: z.string(),\n currency_code: z.string(),\n bill_cycle: z.string(),\n bill_cycle_anchor: z.string(),\n invoice_email: z.string(),\n invoice_language: z.string(),\n tax_id: z.string().nullable(),\n invoice_address: z.record(z.string(), z.unknown()),\n next_bill_date: z.string(),\n last_bill_date: z.string().nullable(),\n is_active: z.boolean(),\n organization_id: z.string().uuid(),\n tenant_id: z.string().uuid(),\n created_at: z.string(),\n updated_at: z.string(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\n// \u2500\u2500\u2500 CRUD route \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillingAccount,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: billingAccountListQuerySchema,\n entityId: billingEntityIds.account,\n fields: [\n 'id',\n 'customer_id',\n 'name',\n 'currency_code',\n 'bill_cycle',\n 'bill_cycle_anchor',\n 'invoice_email',\n 'invoice_language',\n 'tax_id',\n 'invoice_address',\n 'next_bill_date',\n 'last_bill_date',\n 'is_active',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n name: 'name',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n nextBillDate: 'next_bill_date',\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n // `?id=<uuid>` narrows the list to one row \u2014 the detail page\n // reads a single account through this filter.\n if (typeof query.id === 'string' && query.id) {\n filters.id = { $eq: query.id }\n }\n if (typeof query.customerId === 'string' && query.customerId) {\n filters.customer_id = { $eq: query.customerId }\n }\n if (typeof query.currencyCode === 'string' && query.currencyCode) {\n filters.currency_code = { $eq: query.currencyCode.toUpperCase() }\n }\n if (typeof query.billCycle === 'string' && query.billCycle) {\n filters.bill_cycle = { $eq: query.billCycle }\n }\n if (typeof query.isActive === 'boolean') {\n filters.is_active = { $eq: query.isActive }\n }\n if (typeof query.search === 'string' && query.search.trim()) {\n const pattern = `%${query.search.trim().replace(/[%_]/g, (m) => `\\\\${m}`)}%`\n filters.name = { $ilike: pattern }\n }\n return filters\n },\n },\n actions: {\n create: {\n commandId: 'billing.accounts.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingAccountCreateSchema.parse(scoped)\n },\n response: ({ result }) => ({ id: result?.id ?? null }),\n status: 201,\n },\n update: {\n commandId: 'billing.accounts.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingAccountUpdateSchema.parse(scoped)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'billing.accounts.delete',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n // The factory hands delete a `{ body, query }` envelope \u2014 the\n // id may arrive in either, depending on the caller.\n const envelope = (raw ?? {}) as {\n body?: Record<string, unknown>\n query?: Record<string, unknown>\n }\n const body = envelope.body ?? {}\n const query = envelope.query ?? {}\n const scoped = withScopedPayload(body, ctx, translate)\n const id =\n (typeof body.id === 'string' && body.id) ||\n (typeof query.id === 'string' && query.id) ||\n null\n if (!id) {\n throw new Error('id is required')\n }\n return {\n id,\n tenantId: (scoped as { tenantId?: string }).tenantId,\n organizationId: (scoped as { organizationId?: string }).organizationId,\n }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\nexport { POST, PUT, DELETE }\nexport const GET = crud.GET\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingAccount',\n pluralName: 'BillingAccounts',\n querySchema: billingAccountListQuerySchema,\n listResponseSchema: createPagedListResponseSchema(billingAccountListItemSchema),\n create: {\n schema: billingAccountCreateSchema,\n responseSchema: defaultCreateResponseSchema,\n description: 'Creates a new billing account for a customer.',\n },\n update: {\n schema: billingAccountUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates a billing account. `currencyCode` and `customerId` are immutable.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes a billing account (sets `deleted_at`).',\n },\n})\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { BillingAccount } from '../../data/entities'\nimport { billingEntityIds } from '../../data/entityIds'\nimport {\n billingAccountCreateSchema,\n billingAccountListQuerySchema,\n billingAccountUpdateSchema,\n} from '../../data/validators'\nimport {\n createBillingCrudOpenApi,\n createPagedListResponseSchema,\n defaultCreateResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\n// \u2500\u2500\u2500 Auth / RBAC \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.account.view'] },\n POST: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n}\n\nexport const metadata = routeMetadata\n\n// \u2500\u2500\u2500 List response item shape (informational \u2014 used by OpenAPI) \u2500\u2500\n\n// The list endpoint projects raw column names \u2014 snake_case.\nconst billingAccountListItemSchema = z.object({\n id: z.string().uuid(),\n customer_id: z.string(),\n name: z.string(),\n currency_code: z.string(),\n bill_cycle: z.string(),\n bill_cycle_anchor: z.string(),\n invoice_email: z.string(),\n invoice_language: z.string(),\n tax_id: z.string().nullable(),\n invoice_address: z.record(z.string(), z.unknown()),\n next_bill_date: z.string(),\n last_bill_date: z.string().nullable(),\n billing_mode: z.string(),\n credit_limit: z.string(),\n is_active: z.boolean(),\n organization_id: z.string().uuid(),\n tenant_id: z.string().uuid(),\n created_at: z.string(),\n updated_at: z.string(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\n// \u2500\u2500\u2500 CRUD route \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillingAccount,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: billingAccountListQuerySchema,\n entityId: billingEntityIds.account,\n fields: [\n 'id',\n 'customer_id',\n 'name',\n 'currency_code',\n 'bill_cycle',\n 'bill_cycle_anchor',\n 'invoice_email',\n 'invoice_language',\n 'tax_id',\n 'invoice_address',\n 'next_bill_date',\n 'last_bill_date',\n 'billing_mode',\n 'credit_limit',\n 'is_active',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n name: 'name',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n nextBillDate: 'next_bill_date',\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n // `?id=<uuid>` narrows the list to one row \u2014 the detail page\n // reads a single account through this filter.\n if (typeof query.id === 'string' && query.id) {\n filters.id = { $eq: query.id }\n }\n if (typeof query.customerId === 'string' && query.customerId) {\n filters.customer_id = { $eq: query.customerId }\n }\n if (typeof query.currencyCode === 'string' && query.currencyCode) {\n filters.currency_code = { $eq: query.currencyCode.toUpperCase() }\n }\n if (typeof query.billCycle === 'string' && query.billCycle) {\n filters.bill_cycle = { $eq: query.billCycle }\n }\n if (typeof query.billingMode === 'string' && query.billingMode) {\n filters.billing_mode = { $eq: query.billingMode }\n }\n if (typeof query.isActive === 'boolean') {\n filters.is_active = { $eq: query.isActive }\n }\n if (typeof query.search === 'string' && query.search.trim()) {\n const pattern = `%${query.search.trim().replace(/[%_]/g, (m) => `\\\\${m}`)}%`\n filters.name = { $ilike: pattern }\n }\n return filters\n },\n },\n actions: {\n create: {\n commandId: 'billing.accounts.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingAccountCreateSchema.parse(scoped)\n },\n response: ({ result }) => ({ id: result?.id ?? null }),\n status: 201,\n },\n update: {\n commandId: 'billing.accounts.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingAccountUpdateSchema.parse(scoped)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'billing.accounts.delete',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n // The factory hands delete a `{ body, query }` envelope \u2014 the\n // id may arrive in either, depending on the caller.\n const envelope = (raw ?? {}) as {\n body?: Record<string, unknown>\n query?: Record<string, unknown>\n }\n const body = envelope.body ?? {}\n const query = envelope.query ?? {}\n const scoped = withScopedPayload(body, ctx, translate)\n const id =\n (typeof body.id === 'string' && body.id) ||\n (typeof query.id === 'string' && query.id) ||\n null\n if (!id) {\n throw new Error('id is required')\n }\n return {\n id,\n tenantId: (scoped as { tenantId?: string }).tenantId,\n organizationId: (scoped as { organizationId?: string }).organizationId,\n }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\nexport { POST, PUT, DELETE }\nexport const GET = crud.GET\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingAccount',\n pluralName: 'BillingAccounts',\n querySchema: billingAccountListQuerySchema,\n listResponseSchema: createPagedListResponseSchema(billingAccountListItemSchema),\n create: {\n schema: billingAccountCreateSchema,\n responseSchema: defaultCreateResponseSchema,\n description: 'Creates a new billing account for a customer.',\n },\n update: {\n schema: billingAccountUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates a billing account. `currencyCode` and `customerId` are immutable.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes a billing account (sets `deleted_at`).',\n },\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|