@dainamite/billing 0.17.0 → 0.19.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 +99 -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 +9 -9
- package/src/modules/billing/__tests__/commands/invoices.test.ts +3 -3
- package/src/modules/billing/__tests__/commands/topups.test.ts +104 -0
- package/src/modules/billing/__tests__/commands/transactions.test.ts +107 -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,75 @@
|
|
|
1
1
|
# @dainamite/billing
|
|
2
2
|
|
|
3
|
+
## 0.19.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8834b8b: Require Open Mercato `0.6.7`. Peer ranges move from `^0.6.0` to `^0.6.7` for
|
|
8
|
+
`@open-mercato/core`, `shared`, `ui`, `events` and `queue`, and the MikroORM
|
|
9
|
+
peers from `^7.0.0` to `^7.1.5` (7.1 split the SQL layer into
|
|
10
|
+
`@mikro-orm/sql`, so a shared 7.1 instance is required).
|
|
11
|
+
|
|
12
|
+
No package source changes — this only narrows the supported framework range.
|
|
13
|
+
Consumers on Open Mercato `0.6.0`–`0.6.6` must upgrade before taking these
|
|
14
|
+
versions.
|
|
15
|
+
|
|
16
|
+
Note for consumers running commands with a system identity (`ctx.auth: null`):
|
|
17
|
+
`0.6.7` hardened `ensureTenantScope` / `ensureOrganizationScope`. Those calls
|
|
18
|
+
are still no-ops with `auth: null`, but an unscoped organization command now
|
|
19
|
+
logs a warning and will throw `403` if `OM_ENFORCE_ORG_SCOPE_STRICT=true`.
|
|
20
|
+
|
|
21
|
+
## 0.18.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- f8210ac: Prepaid billing mode (SPEC-002 / XD-304). Adds an additive `prepaid` billing
|
|
26
|
+
mode alongside the existing postpaid recurring engine: top-up balance,
|
|
27
|
+
real-time atomic usage debit (never rejects usage), append-only transaction
|
|
28
|
+
ledger, prepaid period-close statements, manual balance adjustments, and the
|
|
29
|
+
prepaid admin UI (balance panel, Transactions / Top-ups / Statements lists +
|
|
30
|
+
detail pages). Postpaid behaviour is unchanged.
|
|
31
|
+
|
|
32
|
+
## 0.18.0 — Prepaid balance billing (SPEC-002, XD-304, unreleased)
|
|
33
|
+
|
|
34
|
+
Adds a second billing **mode** to the package: `prepaid`. A customer tops up
|
|
35
|
+
a monetary **balance**, then consumes a metered product; each usage upload is
|
|
36
|
+
rated in real time, debited from the balance, and the API returns the
|
|
37
|
+
remaining balance. Usage is **never rejected** — low/exhausted is reported and
|
|
38
|
+
the client throttles itself. Postpaid accounts are completely unaffected.
|
|
39
|
+
|
|
40
|
+
- **Schema (additive).** `billing_accounts.billing_mode` (`postpaid`|`prepaid`,
|
|
41
|
+
immutable) + `credit_limit`. New tables: `billing_account_balances` (1:1
|
|
42
|
+
running-balance cache), `billing_account_transactions` (append-only ledger,
|
|
43
|
+
`balance = SUM(amount)`), `billing_topups` (registered top-up lifecycle),
|
|
44
|
+
`billing_statements` (period close). `billing_run_outcomes.statement_id`.
|
|
45
|
+
- **Real-time consume.** `POST /api/billing/usage` on a prepaid account rates
|
|
46
|
+
synchronously (reusing `lib/usageRater`), debits atomically
|
|
47
|
+
(`UPDATE … RETURNING` + paired transaction), and returns `ratedAmount`,
|
|
48
|
+
`balance`, `balanceStatus`, `creditStatus`, `tierBreakdown`. Idempotent on
|
|
49
|
+
`sourceRef` (no double-debit). `GET …/balance`, `GET /transactions`.
|
|
50
|
+
- **Top-ups.** `POST /api/billing/accounts/{id}/topups` registers a
|
|
51
|
+
`BillingTopup` (pending) and opens a `core/payment_gateways` session; the
|
|
52
|
+
`payment.captured` subscriber idempotently credits the balance, issues a
|
|
53
|
+
**posted+paid VAT receipt** in `core/sales` (VAT at top-up), and flips the
|
|
54
|
+
top-up to captured. Sibling subscribers handle failed/cancelled/expired.
|
|
55
|
+
- **Period close.** The Bill Run still runs for prepaid accounts, charging
|
|
56
|
+
`recurring`/`one_time` items to the **balance** (deterministic `source_ref`,
|
|
57
|
+
`billed_to_date` guard) and rolling them into a non-fiscal
|
|
58
|
+
**`BillingStatement`** instead of a payable invoice.
|
|
59
|
+
- **Credit limit (both modes).** Report-only `creditStatus` + `GET …/credit-status`
|
|
60
|
+
(prepaid `max(0,−balance)`; postpaid Σ posted-unpaid outstanding) +
|
|
61
|
+
`billing.credit.over_limit`. Billing **never blocks**.
|
|
62
|
+
- **Admin UI.** Account Prepaid panel (balance, status, credit, top-up,
|
|
63
|
+
transactions), top-ups + statements list/detail pages, account create/edit
|
|
64
|
+
mode + credit fields; en/pl i18n.
|
|
65
|
+
- **Hardening.** GDPR export extended (balance/transactions/top-ups/statements);
|
|
66
|
+
optional reconciliation worker; manual balance adjustments
|
|
67
|
+
(`POST /transactions/adjust`, audited).
|
|
68
|
+
- **Deviation.** The top-up receipt is created **posted+paid** (not draft) —
|
|
69
|
+
money already received has nothing to review (mirrors the xd-249 deviation
|
|
70
|
+
convention). VAT rate resolves from `prepaid.topup_tax_rate` config (the
|
|
71
|
+
postpaid path resolves no rate today).
|
|
72
|
+
|
|
3
73
|
## 0.17.0 — Bill Run + invoices live fixes, CPQ-style list views (unreleased)
|
|
4
74
|
|
|
5
75
|
Continued live-database shakedown — running an actual Bill Run and
|
|
@@ -59,7 +129,7 @@ and fixed; covered by new Playwright integration tests.
|
|
|
59
129
|
`mapInput` read `raw.id` directly, never found it, and threw
|
|
60
130
|
"id is required". `mapInput` now unwraps the envelope.
|
|
61
131
|
- **`?id=` list filter was a no-op.** `GET /api/billing/{accounts,
|
|
62
|
-
|
|
132
|
+
items,runs}?id=<uuid>` is how the detail pages fetch one record,
|
|
63
133
|
but `buildFilters` never handled `id`, so the endpoint returned an
|
|
64
134
|
unrelated row. `id` is now an indexed filter on all three list
|
|
65
135
|
routes (and added to the list query schemas).
|
|
@@ -136,14 +206,14 @@ write path.
|
|
|
136
206
|
- runs **one** idempotency query over all `source_ref`s in the batch
|
|
137
207
|
(`sourceRef: { $in }` + `billAccountId: { $in }`) instead of one
|
|
138
208
|
per item;
|
|
139
|
-
- dedups `source_ref`s repeated
|
|
209
|
+
- dedups `source_ref`s repeated _within_ the same payload (first
|
|
140
210
|
wins — the unique index would reject the rest on flush anyway);
|
|
141
211
|
- `em.create` + `persist`s every new row, then issues a **single**
|
|
142
212
|
`em.flush()` for the whole batch;
|
|
143
213
|
- emits per-entity CRUD side effects (`markOrmEntityChange` +
|
|
144
214
|
events) only after the flush commits.
|
|
145
215
|
- Returns `{ created, deduplicated, items: [{ sourceRef, id,
|
|
146
|
-
|
|
216
|
+
deduplicated }] }` so callers can map results back to inputs.
|
|
147
217
|
- **`POST /api/billing/items/bulk`** — REST surface for the command,
|
|
148
218
|
gated by `billing.item.manage`, `openApi` documented.
|
|
149
219
|
- **Validators.** `billingItemBulkEntrySchema` (per-item, no scope,
|
|
@@ -176,16 +246,16 @@ on the way through.
|
|
|
176
246
|
no limit, materialising every row in JS memory before summing. For
|
|
177
247
|
a telco account with 1M usage records per cycle this OOMs. New
|
|
178
248
|
implementation does a single `SELECT uom_code, SUM(quantity),
|
|
179
|
-
|
|
249
|
+
COUNT(*) FROM billing_account_usage WHERE … GROUP BY uom_code` —
|
|
180
250
|
result set is `O(distinct uom_codes)` (typically 1-20) regardless
|
|
181
251
|
of record count. Index `(bill_account_id, uom_code,
|
|
182
|
-
|
|
252
|
+
rated_in_bill_run_id, period_end)` already exists from Phase 0.
|
|
183
253
|
- **Engine rated-marking: id list → predicate update.** The
|
|
184
254
|
follow-up "mark consumed records" step used to take a `consumedUsageIds`
|
|
185
255
|
array and issue `UPDATE … WHERE id IN (millions of UUIDs)`. New
|
|
186
256
|
shape: the runner returns `matchedUoms` (small) and the engine does
|
|
187
257
|
`UPDATE … WHERE uom_code IN (matchedUoms) AND <account scope>
|
|
188
|
-
|
|
258
|
+
AND rated_in_bill_run_id IS NULL AND period_end <= ?` — no ID list
|
|
189
259
|
ever leaves Postgres. Memory-bounded regardless of how many records
|
|
190
260
|
the predicate touches.
|
|
191
261
|
- **GDPR export: time window + hard cap.** The
|
|
@@ -230,9 +300,9 @@ connector created — without dropping to `curl`.
|
|
|
230
300
|
- `usage` (tiered): raw-JSON textarea for the advanced shape.
|
|
231
301
|
Server validators (Phase 0 Zod) reject malformed structures;
|
|
232
302
|
the textarea includes the canonical example as placeholder.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
303
|
+
Plus optional `subscriptionId` / `subscriptionItemId` external
|
|
304
|
+
references. UX matches `AccountForm` (Cmd/Ctrl+Enter submit, Kbd
|
|
305
|
+
footer hint, no raw HTML buttons).
|
|
236
306
|
- `/backend/billing/items` — list with filters (type, account,
|
|
237
307
|
subscription, active) + per-row "Open" link. **New item** CTA in
|
|
238
308
|
the page header.
|
|
@@ -251,6 +321,7 @@ Validation: yarn build + generate + typecheck + test all green;
|
|
|
251
321
|
auto-discovered (3 accounts + 3 items + 2 runs + 2 invoices).
|
|
252
322
|
|
|
253
323
|
Deferred to follow-up:
|
|
324
|
+
|
|
254
325
|
- Locale files (en, pl) — UI ships with inline English fallbacks.
|
|
255
326
|
- "Add item" button on the account detail page (uses the deep-link
|
|
256
327
|
parameter the create page already supports).
|
|
@@ -282,6 +353,7 @@ Validation: yarn build + generate + typecheck + test all green; 797
|
|
|
282
353
|
repo tests, 0 regressions; all 7 backend pages auto-discovered.
|
|
283
354
|
|
|
284
355
|
Deferred to follow-up:
|
|
356
|
+
|
|
285
357
|
- Item create / edit pages (items are typically integration-driven —
|
|
286
358
|
the REST API at `/api/billing/items` covers operator-side needs).
|
|
287
359
|
- Locale files (en, pl).
|
|
@@ -308,7 +380,7 @@ edits no longer require `curl`.
|
|
|
308
380
|
test-invoices flow and the line-remove flow, satisfying the
|
|
309
381
|
UI AGENTS.md MUST rule.
|
|
310
382
|
- All writes go through the existing `billing.invoices.{edit,add,
|
|
311
|
-
|
|
383
|
+
remove}_draft_line` commands — every line mutation is still
|
|
312
384
|
audited via `DraftInvoiceEdit` with before/after snapshots, and
|
|
313
385
|
invoice totals are recomputed by the engine on each write.
|
|
314
386
|
|
|
@@ -317,6 +389,7 @@ Validation: yarn build + generate + typecheck + test all green;
|
|
|
317
389
|
pages + 1 shared component.
|
|
318
390
|
|
|
319
391
|
Deferred to follow-up:
|
|
392
|
+
|
|
320
393
|
- Account / item editor pages
|
|
321
394
|
- Locale files (en, pl)
|
|
322
395
|
|
|
@@ -348,7 +421,7 @@ to review or post.
|
|
|
348
421
|
- **Wipe test invoices for this run** — visible only for
|
|
349
422
|
`metadata.test_run=true`. Hard-deletes every test invoice from
|
|
350
423
|
the same `bill_run_id` via `billing.invoices.wipe_test`. `window
|
|
351
|
-
|
|
424
|
+
.confirm` gate (will move to `ConfirmDialog` in a follow-up to
|
|
352
425
|
match the OM UX convention).
|
|
353
426
|
|
|
354
427
|
Sidebar entry under the `Billing` group at order 30.
|
|
@@ -359,6 +432,7 @@ add-line, edit-line, remove-line, invoices list, invoices detail);
|
|
|
359
432
|
5 backend pages.
|
|
360
433
|
|
|
361
434
|
Deferred to follow-up:
|
|
435
|
+
|
|
362
436
|
- Inline line edit / add / remove dialogs on the detail page (REST
|
|
363
437
|
endpoints already exist from Phase 4b).
|
|
364
438
|
- ConfirmDialog instead of `window.confirm` for the wipe action.
|
|
@@ -399,6 +473,7 @@ Validation: yarn build + generate + typecheck + test all green;
|
|
|
399
473
|
`.mercato/generated/backend-routes.generated.ts`.
|
|
400
474
|
|
|
401
475
|
Deferred to a follow-up:
|
|
476
|
+
|
|
402
477
|
- Account / item / usage / draft-invoice editor pages
|
|
403
478
|
- Triggers (manual Bill Run / dry-run preview) as buttons in the
|
|
404
479
|
list page
|
|
@@ -423,7 +498,7 @@ fully usable from the REST surface today.
|
|
|
423
498
|
pre-removal snapshot preserved in `before_json`. `after_json`
|
|
424
499
|
and `invoice_line_id` are `null`.
|
|
425
500
|
- All three refuse non-draft invoices with `HTTP 409 +
|
|
426
|
-
|
|
501
|
+
billing.invoice.not_draft` and require the
|
|
427
502
|
`billing.invoice.edit_draft` feature.
|
|
428
503
|
- **Test-invoice wipe command** `billing.invoices.wipe_test`:
|
|
429
504
|
hard-deletes invoices flagged `metadata.test_run=true`, optionally
|
|
@@ -449,6 +524,7 @@ fully usable from the REST surface today.
|
|
|
449
524
|
`billRunId`).
|
|
450
525
|
|
|
451
526
|
Deferred to **Phase 4c** (UI dedicated effort):
|
|
527
|
+
|
|
452
528
|
- Backend pages (DataTable + CrudForm) for accounts / items / usage /
|
|
453
529
|
bill runs / draft invoice review.
|
|
454
530
|
- Sidebar navigation + page metadata.
|
|
@@ -491,6 +567,7 @@ Deferred to **Phase 4c** (UI dedicated effort):
|
|
|
491
567
|
- 6 new unit tests for the resolver (199 total for billing).
|
|
492
568
|
|
|
493
569
|
Deferred to **Phase 4b**:
|
|
570
|
+
|
|
494
571
|
- Draft-edit endpoint + `DraftInvoiceEdit` audit row writing.
|
|
495
572
|
- GDPR-portability `GET /api/billing/export/account/{id}`.
|
|
496
573
|
- Admin UI (backend pages for accounts / items / runs / draft review).
|
|
@@ -499,6 +576,7 @@ Deferred to **Phase 4b**:
|
|
|
499
576
|
|
|
500
577
|
- **Usage rater** in `lib/usageRater.ts` — pure functions for all four
|
|
501
578
|
spec-mandated rate shapes:
|
|
579
|
+
|
|
502
580
|
- Simple flat (`{ unit_price }`): `quantity × unit_price`.
|
|
503
581
|
- Volume tier (`{ model: 'volume', tiers }`): entire quantity at the
|
|
504
582
|
`unit_price` of the tier its total falls into.
|
|
@@ -509,12 +587,13 @@ Deferred to **Phase 4b**:
|
|
|
509
587
|
|
|
510
588
|
Returns `{ amount, breakdown }` where `breakdown` is the per-tier
|
|
511
589
|
audit trail persisted into `SalesInvoiceLine.metadata
|
|
512
|
-
|
|
590
|
+
.usage_tier_breakdown`. Half-up rounding to 2dp on the amount;
|
|
513
591
|
unit prices preserve sub-cent precision via the dedicated
|
|
514
592
|
`formatUnitPrice` helper (e.g. `0.001 → "0.0010"` instead of being
|
|
515
593
|
rounded to zero).
|
|
516
594
|
|
|
517
595
|
- **Usage runner** in `lib/usageRunner.ts` — per-account processor:
|
|
596
|
+
|
|
518
597
|
- Selects unrated usage records (`rated_in_bill_run_id IS NULL`,
|
|
519
598
|
`period_end <= bill_period_end`).
|
|
520
599
|
- Groups by `uom_code`, matches against the account's `type=usage`
|
|
@@ -526,9 +605,10 @@ Deferred to **Phase 4b**:
|
|
|
526
605
|
signal to add the missing item then trigger retry-failed.
|
|
527
606
|
|
|
528
607
|
- **Engine integration** in `lib/billRunEngine.ts`:
|
|
608
|
+
|
|
529
609
|
- Per-cycle loop now combines `buildInvoiceLinesFromItems` (one_time
|
|
530
|
-
|
|
531
|
-
|
|
610
|
+
- recurring) with `processUsageForAccount` (usage) into a single
|
|
611
|
+
`SalesInvoice` write.
|
|
532
612
|
- Real-mode commits mark consumed usage records'
|
|
533
613
|
`rated_in_bill_run_id` via a single `nativeUpdate` (scales to
|
|
534
614
|
accounts with thousands of records per cycle).
|
|
@@ -548,9 +628,9 @@ Deferred to **Phase 4b**:
|
|
|
548
628
|
|
|
549
629
|
- 27 new unit tests (193 total for the package):
|
|
550
630
|
- `usageRater` covers every spec example end-to-end (`25k vs
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
631
|
+
[10k=0, ∞=0.001]` graduated → 15 EUR, `25k vs [10k=0,
|
|
632
|
+
50k=0.001, ∞=0.0005]` volume → 25 EUR, `25k vs
|
|
633
|
+
[10k=50, 50k=200, ∞=1000]` flat → 200 EUR) plus boundary
|
|
554
634
|
conditions and the `1.005 → 1.01` half-up edge case.
|
|
555
635
|
- Engine integration: graduated 25k → 15 EUR end-to-end, unmatched
|
|
556
636
|
UoM warning + un-rated guard, simple-flat unit_price emission,
|
|
@@ -624,7 +704,7 @@ Phase 2 deviations):
|
|
|
624
704
|
- `GET / POST / PUT / DELETE /api/billing/accounts` (Billing Accounts CRUD)
|
|
625
705
|
- `GET / POST / PUT / DELETE /api/billing/items` (Billing Items CRUD with
|
|
626
706
|
`source_ref` idempotency on POST — duplicate `(tenant, account,
|
|
627
|
-
|
|
707
|
+
sourceRef)` returns the existing row with `deduplicated: true`)
|
|
628
708
|
- `GET / POST /api/billing/usage` (append-only usage ingest; idempotent
|
|
629
709
|
by `source_ref`; no update / delete by design)
|
|
630
710
|
- 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
|
}
|