@dainamite/billing 0.17.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 +658 -0
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/modules/billing/acl.js +20 -0
- package/dist/modules/billing/acl.js.map +7 -0
- package/dist/modules/billing/api/accounts/route.js +182 -0
- package/dist/modules/billing/api/accounts/route.js.map +7 -0
- package/dist/modules/billing/api/export/account/[id]/route.js +170 -0
- package/dist/modules/billing/api/export/account/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/route.js +85 -0
- package/dist/modules/billing/api/invoices/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/add-line/route.js +69 -0
- package/dist/modules/billing/api/invoices/add-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js +78 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/post/route.js +74 -0
- package/dist/modules/billing/api/invoices/post/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/remove-line/route.js +69 -0
- package/dist/modules/billing/api/invoices/remove-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/route.js +109 -0
- package/dist/modules/billing/api/invoices/route.js.map +7 -0
- package/dist/modules/billing/api/items/bulk/route.js +74 -0
- package/dist/modules/billing/api/items/bulk/route.js.map +7 -0
- package/dist/modules/billing/api/items/route.js +224 -0
- package/dist/modules/billing/api/items/route.js.map +7 -0
- package/dist/modules/billing/api/openapi.js +25 -0
- package/dist/modules/billing/api/openapi.js.map +7 -0
- package/dist/modules/billing/api/run-outcomes/route.js +122 -0
- package/dist/modules/billing/api/run-outcomes/route.js.map +7 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js +70 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js.map +7 -0
- package/dist/modules/billing/api/runs/route.js +143 -0
- package/dist/modules/billing/api/runs/route.js.map +7 -0
- package/dist/modules/billing/api/test-invoices/wipe/route.js +65 -0
- package/dist/modules/billing/api/test-invoices/wipe/route.js.map +7 -0
- package/dist/modules/billing/api/usage/route.js +138 -0
- package/dist/modules/billing/api/usage/route.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js +402 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js +82 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/page.js +173 -0
- package/dist/modules/billing/backend/billing/accounts/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/accounts/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js +500 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/page.js +187 -0
- package/dist/modules/billing/backend/billing/invoices/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/invoices/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.js +265 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/create/page.js +90 -0
- package/dist/modules/billing/backend/billing/items/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/items/create/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/page.js +208 -0
- package/dist/modules/billing/backend/billing/items/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/items/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js +293 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/page.js +341 -0
- package/dist/modules/billing/backend/billing/runs/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/runs/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/usage/create/page.js +100 -0
- package/dist/modules/billing/backend/billing/usage/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/usage/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/usage/create/page.meta.js.map +7 -0
- package/dist/modules/billing/ce.js +56 -0
- package/dist/modules/billing/ce.js.map +7 -0
- package/dist/modules/billing/cli.js +67 -0
- package/dist/modules/billing/cli.js.map +7 -0
- package/dist/modules/billing/commands/accounts.js +157 -0
- package/dist/modules/billing/commands/accounts.js.map +7 -0
- package/dist/modules/billing/commands/index.js +6 -0
- package/dist/modules/billing/commands/index.js.map +7 -0
- package/dist/modules/billing/commands/invoices.js +401 -0
- package/dist/modules/billing/commands/invoices.js.map +7 -0
- package/dist/modules/billing/commands/items.js +317 -0
- package/dist/modules/billing/commands/items.js.map +7 -0
- package/dist/modules/billing/commands/runs.js +123 -0
- package/dist/modules/billing/commands/runs.js.map +7 -0
- package/dist/modules/billing/commands/usage.js +88 -0
- package/dist/modules/billing/commands/usage.js.map +7 -0
- package/dist/modules/billing/components/AccountForm.js +420 -0
- package/dist/modules/billing/components/AccountForm.js.map +7 -0
- package/dist/modules/billing/components/DetailFields.js +37 -0
- package/dist/modules/billing/components/DetailFields.js.map +7 -0
- package/dist/modules/billing/components/ItemForm.js +440 -0
- package/dist/modules/billing/components/ItemForm.js.map +7 -0
- package/dist/modules/billing/components/LineFormDialog.js +195 -0
- package/dist/modules/billing/components/LineFormDialog.js.map +7 -0
- package/dist/modules/billing/components/UsageForm.js +262 -0
- package/dist/modules/billing/components/UsageForm.js.map +7 -0
- package/dist/modules/billing/data/entities.js +433 -0
- package/dist/modules/billing/data/entities.js.map +7 -0
- package/dist/modules/billing/data/entityIds.js +12 -0
- package/dist/modules/billing/data/entityIds.js.map +7 -0
- package/dist/modules/billing/data/validators.js +396 -0
- package/dist/modules/billing/data/validators.js.map +7 -0
- package/dist/modules/billing/di.js +6 -0
- package/dist/modules/billing/di.js.map +7 -0
- package/dist/modules/billing/events.js +35 -0
- package/dist/modules/billing/events.js.map +7 -0
- package/dist/modules/billing/i18n/en.json +336 -0
- package/dist/modules/billing/i18n/pl.json +336 -0
- package/dist/modules/billing/index.js +17 -0
- package/dist/modules/billing/index.js.map +7 -0
- package/dist/modules/billing/lib/billPeriod.js +101 -0
- package/dist/modules/billing/lib/billPeriod.js.map +7 -0
- package/dist/modules/billing/lib/billRunEngine.js +256 -0
- package/dist/modules/billing/lib/billRunEngine.js.map +7 -0
- package/dist/modules/billing/lib/billRunLabel.js +8 -0
- package/dist/modules/billing/lib/billRunLabel.js.map +7 -0
- package/dist/modules/billing/lib/idempotency.js +13 -0
- package/dist/modules/billing/lib/idempotency.js.map +7 -0
- package/dist/modules/billing/lib/invoiceStatus.js +38 -0
- package/dist/modules/billing/lib/invoiceStatus.js.map +7 -0
- package/dist/modules/billing/lib/invoiceWriter.js +153 -0
- package/dist/modules/billing/lib/invoiceWriter.js.map +7 -0
- package/dist/modules/billing/lib/itemSelector.js +77 -0
- package/dist/modules/billing/lib/itemSelector.js.map +7 -0
- package/dist/modules/billing/lib/seeds/demo.js +130 -0
- package/dist/modules/billing/lib/seeds/demo.js.map +7 -0
- package/dist/modules/billing/lib/tenantLock.js +32 -0
- package/dist/modules/billing/lib/tenantLock.js.map +7 -0
- package/dist/modules/billing/lib/usageRater.js +150 -0
- package/dist/modules/billing/lib/usageRater.js.map +7 -0
- package/dist/modules/billing/lib/usageRunner.js +82 -0
- package/dist/modules/billing/lib/usageRunner.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20260519000000_billing.js +144 -0
- package/dist/modules/billing/migrations/Migration20260519000000_billing.js.map +7 -0
- package/dist/modules/billing/setup.js +190 -0
- package/dist/modules/billing/setup.js.map +7 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js +47 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js.map +7 -0
- package/dist/modules/billing/translations.js +5 -0
- package/dist/modules/billing/translations.js.map +7 -0
- package/dist/modules/billing/workers/reap-stale-bill-runs.js +31 -0
- package/dist/modules/billing/workers/reap-stale-bill-runs.js.map +7 -0
- package/package.json +148 -0
- package/src/modules/billing/__integration__/TC-BILL-001.spec.ts +116 -0
- package/src/modules/billing/__integration__/TC-BILL-002.spec.ts +169 -0
- package/src/modules/billing/__integration__/TC-BILL-003.spec.ts +28 -0
- package/src/modules/billing/__integration__/meta.ts +4 -0
- package/src/modules/billing/__tests__/commands/invoices.test.ts +464 -0
- package/src/modules/billing/__tests__/commands/itemsBulk.test.ts +284 -0
- package/src/modules/billing/__tests__/lib/billPeriod.test.ts +131 -0
- package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +561 -0
- package/src/modules/billing/__tests__/lib/idempotency.test.ts +81 -0
- package/src/modules/billing/__tests__/lib/invoiceStatus.test.ts +97 -0
- package/src/modules/billing/__tests__/lib/invoiceWriter.test.ts +119 -0
- package/src/modules/billing/__tests__/lib/itemSelector.test.ts +192 -0
- package/src/modules/billing/__tests__/lib/seedsDemo.test.ts +86 -0
- package/src/modules/billing/__tests__/lib/tenantLock.test.ts +126 -0
- package/src/modules/billing/__tests__/lib/usageRater.test.ts +190 -0
- package/src/modules/billing/__tests__/package-isolation.test.ts +223 -0
- package/src/modules/billing/__tests__/setup.test.ts +107 -0
- package/src/modules/billing/__tests__/validators.test.ts +594 -0
- package/src/modules/billing/acl.ts +16 -0
- package/src/modules/billing/api/accounts/route.ts +198 -0
- package/src/modules/billing/api/export/account/[id]/route.ts +233 -0
- package/src/modules/billing/api/invoices/[id]/route.ts +143 -0
- package/src/modules/billing/api/invoices/add-line/route.ts +74 -0
- package/src/modules/billing/api/invoices/edit-line/route.ts +97 -0
- package/src/modules/billing/api/invoices/post/route.ts +97 -0
- package/src/modules/billing/api/invoices/remove-line/route.ts +75 -0
- package/src/modules/billing/api/invoices/route.ts +167 -0
- package/src/modules/billing/api/items/bulk/route.ts +92 -0
- package/src/modules/billing/api/items/route.ts +255 -0
- package/src/modules/billing/api/openapi.ts +33 -0
- package/src/modules/billing/api/run-outcomes/route.ts +149 -0
- package/src/modules/billing/api/runs/retry-failed/route.ts +88 -0
- package/src/modules/billing/api/runs/route.ts +170 -0
- package/src/modules/billing/api/test-invoices/wipe/route.ts +87 -0
- package/src/modules/billing/api/usage/route.ts +148 -0
- package/src/modules/billing/backend/billing/accounts/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +574 -0
- package/src/modules/billing/backend/billing/accounts/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/accounts/create/page.tsx +101 -0
- package/src/modules/billing/backend/billing/accounts/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/accounts/page.tsx +222 -0
- package/src/modules/billing/backend/billing/invoices/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +613 -0
- package/src/modules/billing/backend/billing/invoices/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/invoices/page.tsx +245 -0
- package/src/modules/billing/backend/billing/items/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/items/[id]/page.tsx +366 -0
- package/src/modules/billing/backend/billing/items/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/items/create/page.tsx +110 -0
- package/src/modules/billing/backend/billing/items/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/items/page.tsx +272 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.tsx +404 -0
- package/src/modules/billing/backend/billing/runs/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/runs/page.tsx +426 -0
- package/src/modules/billing/backend/billing/usage/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/usage/create/page.tsx +120 -0
- package/src/modules/billing/ce.ts +74 -0
- package/src/modules/billing/cli.ts +89 -0
- package/src/modules/billing/commands/accounts.ts +205 -0
- package/src/modules/billing/commands/index.ts +13 -0
- package/src/modules/billing/commands/invoices.ts +542 -0
- package/src/modules/billing/commands/items.ts +438 -0
- package/src/modules/billing/commands/runs.ts +166 -0
- package/src/modules/billing/commands/usage.ts +118 -0
- package/src/modules/billing/components/AccountForm.tsx +517 -0
- package/src/modules/billing/components/DetailFields.tsx +60 -0
- package/src/modules/billing/components/ItemForm.tsx +558 -0
- package/src/modules/billing/components/LineFormDialog.tsx +253 -0
- package/src/modules/billing/components/UsageForm.tsx +326 -0
- package/src/modules/billing/data/entities.ts +533 -0
- package/src/modules/billing/data/entityIds.ts +25 -0
- package/src/modules/billing/data/validators.ts +562 -0
- package/src/modules/billing/di.ts +14 -0
- package/src/modules/billing/events.ts +47 -0
- package/src/modules/billing/i18n/en.json +336 -0
- package/src/modules/billing/i18n/pl.json +336 -0
- package/src/modules/billing/index.ts +22 -0
- package/src/modules/billing/lib/billPeriod.ts +181 -0
- package/src/modules/billing/lib/billRunEngine.ts +416 -0
- package/src/modules/billing/lib/billRunLabel.ts +14 -0
- package/src/modules/billing/lib/idempotency.ts +46 -0
- package/src/modules/billing/lib/invoiceStatus.ts +61 -0
- package/src/modules/billing/lib/invoiceWriter.ts +260 -0
- package/src/modules/billing/lib/itemSelector.ts +154 -0
- package/src/modules/billing/lib/seeds/demo.ts +199 -0
- package/src/modules/billing/lib/tenantLock.ts +87 -0
- package/src/modules/billing/lib/usageRater.ts +250 -0
- package/src/modules/billing/lib/usageRunner.ts +189 -0
- package/src/modules/billing/migrations/Migration20260519000000_billing.ts +175 -0
- package/src/modules/billing/setup.ts +252 -0
- package/src/modules/billing/subscribers/payment-captured-to-paid.ts +89 -0
- package/src/modules/billing/translations.ts +9 -0
- package/src/modules/billing/workers/reap-stale-bill-runs.ts +57 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
# @dainamite/billing
|
|
2
|
+
|
|
3
|
+
## 0.17.0 — Bill Run + invoices live fixes, CPQ-style list views (unreleased)
|
|
4
|
+
|
|
5
|
+
Continued live-database shakedown — running an actual Bill Run and
|
|
6
|
+
opening the invoice screens surfaced two more bugs, plus a UI
|
|
7
|
+
consistency pass on the list views.
|
|
8
|
+
|
|
9
|
+
- **Bill Run crashed on every account.** `billPeriod.ts`'s
|
|
10
|
+
`cloneAtMidnight` called `date.getTime()`, but MikroORM returns
|
|
11
|
+
`date`-typed columns (`next_bill_date` etc.) as `'YYYY-MM-DD'`
|
|
12
|
+
strings, not Date objects — so every account outcome failed with
|
|
13
|
+
"date.getTime is not a function". `cloneAtMidnight` now coerces
|
|
14
|
+
`Date | string | number`, making all period math input-tolerant
|
|
15
|
+
(same approach `itemSelector` already used).
|
|
16
|
+
- **Invoice list + detail returned 500.** Both invoice GET endpoints
|
|
17
|
+
filtered with the raw-SQL JSONB operator `metadata ? 'bill_run_id'`,
|
|
18
|
+
but `em.execute` treats a literal `?` as a positional parameter
|
|
19
|
+
placeholder — the binding count never matched. Switched to the
|
|
20
|
+
function form `jsonb_exists(metadata, 'bill_run_id')`.
|
|
21
|
+
- **List views aligned to the CPQ layout.** The account / item /
|
|
22
|
+
invoice / Bill Run lists rendered their title in a separate
|
|
23
|
+
`PageHeader` above the table; CPQ (and OM core) render it inside the
|
|
24
|
+
`DataTable` card. All four now pass `title` / `actions` /
|
|
25
|
+
`refreshButton` / `columnChooser` / `perspective` to `DataTable`
|
|
26
|
+
directly and drop the standalone header — matching the CPQ look
|
|
27
|
+
(billing can't import `CpqListView` across the package boundary, so
|
|
28
|
+
it uses the same shared `DataTable` props). The item and Bill Run
|
|
29
|
+
lists also gained a search box so all four toolbars render the same
|
|
30
|
+
single integrated row — items search by description, Bill Runs by
|
|
31
|
+
pasted run id (`?search=` is now wired into those two list routes'
|
|
32
|
+
`buildFilters`).
|
|
33
|
+
|
|
34
|
+
Validation: yarn typecheck + 804 unit tests + 15 Playwright tests all
|
|
35
|
+
green; verified end to end against a live database (5 demo accounts,
|
|
36
|
+
10 items, a completed test Bill Run, 3 draft invoices).
|
|
37
|
+
|
|
38
|
+
## 0.16.0 — admin UI + CRUD API end-to-end fixes (unreleased)
|
|
39
|
+
|
|
40
|
+
First run of the billing admin against a live database surfaced a set
|
|
41
|
+
of bugs in code paths that had only ever been typechecked. All found
|
|
42
|
+
and fixed; covered by new Playwright integration tests.
|
|
43
|
+
|
|
44
|
+
- **List read casing.** The list endpoints (`accounts` / `items` /
|
|
45
|
+
`runs`) project raw column names, so responses are snake_case — but
|
|
46
|
+
the account / item / Bill Run list and detail pages were written
|
|
47
|
+
against camelCase keys, so every non-name column rendered blank and
|
|
48
|
+
the detail edit forms loaded empty. Pages now read the snake_case
|
|
49
|
+
keys the API actually returns (matching the already-correct invoice
|
|
50
|
+
pages); the routes' OpenAPI list-item schemas were camelCase too and
|
|
51
|
+
are corrected to match.
|
|
52
|
+
- **Detail pages couldn't read their own id.** OM serves backend
|
|
53
|
+
pages through a catch-all route — the dynamic `[id]` segment arrives
|
|
54
|
+
as a page prop, not via `useParams()`. The four detail pages only
|
|
55
|
+
read `useParams().id`, so they hung on "Loading…" forever. They now
|
|
56
|
+
read `props.params?.id` with `useParams()` as the fallback.
|
|
57
|
+
- **`DELETE` returned 500.** The CRUD factory hands a delete action a
|
|
58
|
+
`{ body, query }` envelope as `raw`; the accounts/items delete
|
|
59
|
+
`mapInput` read `raw.id` directly, never found it, and threw
|
|
60
|
+
"id is required". `mapInput` now unwraps the envelope.
|
|
61
|
+
- **`?id=` list filter was a no-op.** `GET /api/billing/{accounts,
|
|
62
|
+
items,runs}?id=<uuid>` is how the detail pages fetch one record,
|
|
63
|
+
but `buildFilters` never handled `id`, so the endpoint returned an
|
|
64
|
+
unrelated row. `id` is now an indexed filter on all three list
|
|
65
|
+
routes (and added to the list query schemas).
|
|
66
|
+
- **`billing.items.bulk_create` reported empty ids.** The result
|
|
67
|
+
entries captured `item.id` before `em.flush()` assigned the
|
|
68
|
+
DB-generated UUID, so every created/in-batch-deduped entry came back
|
|
69
|
+
with `id: ''`. Ids are now backfilled after the flush. The unit-test
|
|
70
|
+
mock was assigning ids at `create()` time, hiding the bug — it now
|
|
71
|
+
mirrors MikroORM and assigns at `flush()`.
|
|
72
|
+
|
|
73
|
+
Validation: yarn typecheck + 804 unit tests + 15 Playwright
|
|
74
|
+
integration tests (`TC-BILL-001..003`) all green.
|
|
75
|
+
|
|
76
|
+
## 0.15.0 — detail pages on FormHeader (unreleased)
|
|
77
|
+
|
|
78
|
+
UI consistency pass against the CPQ module and the Open Mercato core
|
|
79
|
+
reference modules. Both render entity detail pages through
|
|
80
|
+
`FormHeader` in `detail` mode — a documented non-negotiable ("Detail
|
|
81
|
+
uses FormHeader in detail mode"). The billing detail pages had
|
|
82
|
+
shipped with an ad-hoc `PageHeader` + custom card header instead.
|
|
83
|
+
|
|
84
|
+
- **Account / Item / Invoice / Bill Run detail pages** now render
|
|
85
|
+
their header via `FormHeader mode="detail"` — back link, entity
|
|
86
|
+
type label, entity title, status badge and action buttons in the
|
|
87
|
+
framework's standard slots. The page body keeps the field grid /
|
|
88
|
+
totals / edit form / outcomes table unchanged.
|
|
89
|
+
- No behaviour change to actions — Post / Wipe / Retry / Soft delete /
|
|
90
|
+
View items / Add item keep their existing handlers, relocated into
|
|
91
|
+
the header's `actionsContent` slot.
|
|
92
|
+
- Loading and 404/error states keep their `PageHeader` fallback
|
|
93
|
+
(there is no entity to title yet).
|
|
94
|
+
|
|
95
|
+
Validation: yarn typecheck + test green; 804 tests, 0 regressions.
|
|
96
|
+
UI not exercised in a browser — local PostgreSQL was unavailable so
|
|
97
|
+
the dev server could not be started.
|
|
98
|
+
|
|
99
|
+
## 0.14.0 — i18n locale files (unreleased)
|
|
100
|
+
|
|
101
|
+
Phase 4c deferred the admin-UI locale files — the pages shipped with
|
|
102
|
+
inline English `useT()` fallbacks only. This adds the real
|
|
103
|
+
dictionaries so the Billing admin renders in Polish (the target
|
|
104
|
+
market) as well as English.
|
|
105
|
+
|
|
106
|
+
- **`i18n/en.json` + `i18n/pl.json`** — 263 keys covering every
|
|
107
|
+
`t('billing.…')` call across the account / item / invoice / Bill
|
|
108
|
+
Run list, detail, create and form surfaces. The framework's
|
|
109
|
+
generator auto-discovers `src/modules/billing/i18n/<lang>.json`
|
|
110
|
+
and wires them into the module's `translations` map; no manual
|
|
111
|
+
registration needed.
|
|
112
|
+
- **Split `billing.items.type.*` into two key sets.** The same key
|
|
113
|
+
was used with two different fallbacks — the list filter wanted the
|
|
114
|
+
short label ("One-time"), the create form wanted the descriptive
|
|
115
|
+
one ("One-time charge"). Since a locale value resolves a key
|
|
116
|
+
globally, the form selector now uses a dedicated
|
|
117
|
+
`billing.items.form.type.*` set; the filter keeps the short
|
|
118
|
+
`billing.items.type.*`.
|
|
119
|
+
|
|
120
|
+
Validation: yarn generate (i18n imports confirmed in the registry) +
|
|
121
|
+
typecheck + test all green; 804 tests, 0 regressions.
|
|
122
|
+
|
|
123
|
+
## 0.13.0 — Phase 4g — bulk item create (unreleased)
|
|
124
|
+
|
|
125
|
+
The CPQ connector created Billing Items one command-bus round-trip at
|
|
126
|
+
a time — a subscription with N charges meant N account lookups, N
|
|
127
|
+
idempotency queries and N flushes. For a multi-line enterprise order
|
|
128
|
+
that is N× the work it needs to be. Phase 4g adds a single batched
|
|
129
|
+
write path.
|
|
130
|
+
|
|
131
|
+
- **`billing.items.bulk_create` command.** New command accepting
|
|
132
|
+
`{ tenantId, organizationId, items: [...] }` (1–500 entries). It:
|
|
133
|
+
- validates every referenced account in **one** `id: { $in: [...] }`
|
|
134
|
+
query over the distinct account ids — a missing account throws the
|
|
135
|
+
same `404 { billAccountId }` shape as the single-item command;
|
|
136
|
+
- runs **one** idempotency query over all `source_ref`s in the batch
|
|
137
|
+
(`sourceRef: { $in }` + `billAccountId: { $in }`) instead of one
|
|
138
|
+
per item;
|
|
139
|
+
- dedups `source_ref`s repeated *within* the same payload (first
|
|
140
|
+
wins — the unique index would reject the rest on flush anyway);
|
|
141
|
+
- `em.create` + `persist`s every new row, then issues a **single**
|
|
142
|
+
`em.flush()` for the whole batch;
|
|
143
|
+
- emits per-entity CRUD side effects (`markOrmEntityChange` +
|
|
144
|
+
events) only after the flush commits.
|
|
145
|
+
- Returns `{ created, deduplicated, items: [{ sourceRef, id,
|
|
146
|
+
deduplicated }] }` so callers can map results back to inputs.
|
|
147
|
+
- **`POST /api/billing/items/bulk`** — REST surface for the command,
|
|
148
|
+
gated by `billing.item.manage`, `openApi` documented.
|
|
149
|
+
- **Validators.** `billingItemBulkEntrySchema` (per-item, no scope,
|
|
150
|
+
full `rate_json` discriminated-union validation) and
|
|
151
|
+
`billingItemBulkCreateSchema` (scoped wrapper, `items` 1–500).
|
|
152
|
+
- **CPQ connector switched to bulk.** `activated` / `amended` /
|
|
153
|
+
`renewed` subscribers now make one `bulkCreateItems` call instead
|
|
154
|
+
of an N-item `createItem` loop; `amended` folds its proration
|
|
155
|
+
`one_time` line into the same batch.
|
|
156
|
+
- **Fix: detail-page headings silently missing.** The invoice,
|
|
157
|
+
account and Bill Run detail pages passed `title` straight to
|
|
158
|
+
`<Page>` on their main render path. `Page` only spreads
|
|
159
|
+
`HTMLAttributes`, so `title` landed as a `<div title>` tooltip
|
|
160
|
+
instead of an `<h1>` — the heading rendered on the loading/error
|
|
161
|
+
branches but vanished once data arrived. All three now render the
|
|
162
|
+
heading through `<PageHeader>`.
|
|
163
|
+
|
|
164
|
+
Validation: yarn typecheck + test all green; 804 repo tests
|
|
165
|
+
(7 new in `__tests__/commands/itemsBulk.test.ts`), 0 regressions.
|
|
166
|
+
|
|
167
|
+
## 0.12.0 — Phase 4f — scale audit + bulk refactor (unreleased)
|
|
168
|
+
|
|
169
|
+
Audit of hot paths against the spec's Performance Considerations
|
|
170
|
+
(Shape A: many accounts × few items; Shape B: few accounts × millions
|
|
171
|
+
of usage records). Two real OOM risks closed; one UI deep-link added
|
|
172
|
+
on the way through.
|
|
173
|
+
|
|
174
|
+
- **Usage runner: load-all-records → SQL aggregate.** Pre-Phase-4f the
|
|
175
|
+
per-account usage path did `em.find(BillingAccountUsage, {…})` with
|
|
176
|
+
no limit, materialising every row in JS memory before summing. For
|
|
177
|
+
a telco account with 1M usage records per cycle this OOMs. New
|
|
178
|
+
implementation does a single `SELECT uom_code, SUM(quantity),
|
|
179
|
+
COUNT(*) FROM billing_account_usage WHERE … GROUP BY uom_code` —
|
|
180
|
+
result set is `O(distinct uom_codes)` (typically 1-20) regardless
|
|
181
|
+
of record count. Index `(bill_account_id, uom_code,
|
|
182
|
+
rated_in_bill_run_id, period_end)` already exists from Phase 0.
|
|
183
|
+
- **Engine rated-marking: id list → predicate update.** The
|
|
184
|
+
follow-up "mark consumed records" step used to take a `consumedUsageIds`
|
|
185
|
+
array and issue `UPDATE … WHERE id IN (millions of UUIDs)`. New
|
|
186
|
+
shape: the runner returns `matchedUoms` (small) and the engine does
|
|
187
|
+
`UPDATE … WHERE uom_code IN (matchedUoms) AND <account scope>
|
|
188
|
+
AND rated_in_bill_run_id IS NULL AND period_end <= ?` — no ID list
|
|
189
|
+
ever leaves Postgres. Memory-bounded regardless of how many records
|
|
190
|
+
the predicate touches.
|
|
191
|
+
- **GDPR export: time window + hard cap.** The
|
|
192
|
+
`GET /api/billing/export/account/[id]` endpoint used to do
|
|
193
|
+
unbounded `em.find` against usage / outcomes / invoices —
|
|
194
|
+
pathological for telco accounts with millions of usage rows. Now:
|
|
195
|
+
defaults to the last 12 months (regulator lifetime requests
|
|
196
|
+
override via `?since=1970-01-01&until=...`), every collection
|
|
197
|
+
capped by `EXPORT_HARD_LIMIT = 50_000`, response carries the
|
|
198
|
+
`window: { since, until, hardLimit }` block so the operator
|
|
199
|
+
knows what was sliced. Lifetime dumps for outlier accounts need
|
|
200
|
+
a follow-up chunked NDJSON background job — documented in the
|
|
201
|
+
endpoint's OpenAPI description.
|
|
202
|
+
- **Account detail UI: "View items" + "Add item" deep-link** buttons
|
|
203
|
+
that pass `?billAccountId=<id>` to the items list and items create
|
|
204
|
+
pages. Items list pre-fills the filter on mount when the param is
|
|
205
|
+
present.
|
|
206
|
+
|
|
207
|
+
Engine tests updated to mock the new aggregate path
|
|
208
|
+
(`em.execute(SELECT … GROUP BY uom_code)` + `em.nativeUpdate` with
|
|
209
|
+
the `uomCode: { $in: [...] }` predicate). All 12 billing test suites
|
|
210
|
+
still green (214 cases).
|
|
211
|
+
|
|
212
|
+
Validation: yarn build + generate + typecheck + test all green; 797
|
|
213
|
+
repo tests, 0 regressions.
|
|
214
|
+
|
|
215
|
+
## 0.11.0 — Phase 4e — item create + edit UI (unreleased)
|
|
216
|
+
|
|
217
|
+
Billing Item management from the admin closes the last UI gap.
|
|
218
|
+
Operators can now hand-create one-off charges (e.g. manual
|
|
219
|
+
adjustments, custom proration credits) or inspect what the CPQ
|
|
220
|
+
connector created — without dropping to `curl`.
|
|
221
|
+
|
|
222
|
+
- `components/ItemForm.tsx` — shared form (create + edit). Renders
|
|
223
|
+
account picker (create-only), type selector (create-only,
|
|
224
|
+
immutable on edit per `billingItemUpdateSchema`), bill dates,
|
|
225
|
+
description, **type-discriminated rate fields**:
|
|
226
|
+
- `one_time`: single `amount` input (negative allowed for
|
|
227
|
+
refunds / credits).
|
|
228
|
+
- `recurring`: `unit_price` input.
|
|
229
|
+
- `usage` (simple): `unit_price` + required `uom_code`.
|
|
230
|
+
- `usage` (tiered): raw-JSON textarea for the advanced shape.
|
|
231
|
+
Server validators (Phase 0 Zod) reject malformed structures;
|
|
232
|
+
the textarea includes the canonical example as placeholder.
|
|
233
|
+
Plus optional `subscriptionId` / `subscriptionItemId` external
|
|
234
|
+
references. UX matches `AccountForm` (Cmd/Ctrl+Enter submit, Kbd
|
|
235
|
+
footer hint, no raw HTML buttons).
|
|
236
|
+
- `/backend/billing/items` — list with filters (type, account,
|
|
237
|
+
subscription, active) + per-row "Open" link. **New item** CTA in
|
|
238
|
+
the page header.
|
|
239
|
+
- `/backend/billing/items/create` — pre-loads the account picker,
|
|
240
|
+
supports `?billAccountId=<id>` deep-link for future "Add item from
|
|
241
|
+
account detail" CTAs. POSTs `/api/billing/items`, navigates to
|
|
242
|
+
detail on success.
|
|
243
|
+
- `/backend/billing/items/[id]` — detail + edit. Read-only badges
|
|
244
|
+
at the top surface engine-managed state the operator can't change
|
|
245
|
+
(`currencyMismatch`, `billedToDate`, `sourceRef`). Account ID is
|
|
246
|
+
a deep-link back into `/backend/billing/accounts/[id]`. **Soft
|
|
247
|
+
Delete** button with `ConfirmDialog` gate.
|
|
248
|
+
|
|
249
|
+
Validation: yarn build + generate + typecheck + test all green;
|
|
250
|
+
797 repo tests, 0 regressions; all 10 billing backend pages now
|
|
251
|
+
auto-discovered (3 accounts + 3 items + 2 runs + 2 invoices).
|
|
252
|
+
|
|
253
|
+
Deferred to follow-up:
|
|
254
|
+
- Locale files (en, pl) — UI ships with inline English fallbacks.
|
|
255
|
+
- "Add item" button on the account detail page (uses the deep-link
|
|
256
|
+
parameter the create page already supports).
|
|
257
|
+
|
|
258
|
+
## 0.10.0 — Phase 4d — account create + edit UI (unreleased)
|
|
259
|
+
|
|
260
|
+
Account management from the admin — no more REST-only onboarding.
|
|
261
|
+
|
|
262
|
+
- `components/AccountForm.tsx` — shared form (create + edit modes).
|
|
263
|
+
Renders 9 scalar fields + 5 `invoiceAddress` sub-fields + an
|
|
264
|
+
`isActive` switch (edit-only). On `mode='edit'`, `customerId` and
|
|
265
|
+
`currencyCode` are disabled with explanatory copy — both fields are
|
|
266
|
+
immutable per `billingAccountUpdateSchema`. UX per OM convention:
|
|
267
|
+
`Cmd/Ctrl + Enter` submits, footer hint via `Kbd` / `KbdShortcut`.
|
|
268
|
+
- `/backend/billing/accounts/create` — POSTs `/api/billing/accounts`.
|
|
269
|
+
On success, navigates to the new account's detail page.
|
|
270
|
+
- `/backend/billing/accounts/[id]` — loads via `GET /api/billing/accounts?id=<id>`
|
|
271
|
+
(saves a dedicated `[id]` route). Form in edit mode for in-place
|
|
272
|
+
updates via PUT. **Soft Delete** button at the top-right with a
|
|
273
|
+
`ConfirmDialog` gate; on success, navigates back to the list.
|
|
274
|
+
- List page (`/backend/billing/accounts`) now has a **New account**
|
|
275
|
+
CTA in the page header + an **Open** row link per account row.
|
|
276
|
+
- **Bonus fix**: every existing backend page (`runs`, `runs/[id]`,
|
|
277
|
+
`invoices`, `invoices/[id]`) used `<Page title="…">` which silently
|
|
278
|
+
passed `title` as an HTML attribute (a tooltip), so no visible
|
|
279
|
+
title rendered. All migrated to the proper `PageHeader` wrapper.
|
|
280
|
+
|
|
281
|
+
Validation: yarn build + generate + typecheck + test all green; 797
|
|
282
|
+
repo tests, 0 regressions; all 7 backend pages auto-discovered.
|
|
283
|
+
|
|
284
|
+
Deferred to follow-up:
|
|
285
|
+
- Item create / edit pages (items are typically integration-driven —
|
|
286
|
+
the REST API at `/api/billing/items` covers operator-side needs).
|
|
287
|
+
- Locale files (en, pl).
|
|
288
|
+
|
|
289
|
+
## 0.9.0 — Phase 4c v3 — inline line editing (unreleased)
|
|
290
|
+
|
|
291
|
+
Closes the last operator-UX gap on the draft-review surface — line
|
|
292
|
+
edits no longer require `curl`.
|
|
293
|
+
|
|
294
|
+
- **`components/LineFormDialog.tsx`** — shared add / edit form. Fields:
|
|
295
|
+
description (Textarea), quantity, unit_price_net, and an optional
|
|
296
|
+
`totalNetAmount` override (operator "I know better than
|
|
297
|
+
qty × unit_price" path; pre-shows the computed total as a
|
|
298
|
+
placeholder). UX per OM convention: `Cmd/Ctrl + Enter` submits,
|
|
299
|
+
`Escape` cancels (Radix Dialog handles cancel for free).
|
|
300
|
+
Footer hint row uses `Kbd` / `KbdShortcut` (no raw `<span>`).
|
|
301
|
+
- **Per-line row actions** on the detail page (visible only when
|
|
302
|
+
`invoice.status === 'draft'`): Edit (opens the dialog pre-filled
|
|
303
|
+
from the row) and Remove (opens `ConfirmDialog` then calls
|
|
304
|
+
`billing.invoices.remove_draft_line`).
|
|
305
|
+
- **"+ Add line" button** next to the Lines header (draft-only).
|
|
306
|
+
Opens the same dialog in `mode='add'`.
|
|
307
|
+
- **`ConfirmDialog` replaces `window.confirm`** for both the wipe-
|
|
308
|
+
test-invoices flow and the line-remove flow, satisfying the
|
|
309
|
+
UI AGENTS.md MUST rule.
|
|
310
|
+
- All writes go through the existing `billing.invoices.{edit,add,
|
|
311
|
+
remove}_draft_line` commands — every line mutation is still
|
|
312
|
+
audited via `DraftInvoiceEdit` with before/after snapshots, and
|
|
313
|
+
invoice totals are recomputed by the engine on each write.
|
|
314
|
+
|
|
315
|
+
Validation: yarn build + generate + typecheck + test all green;
|
|
316
|
+
797 repo tests, 0 regressions; 6 billing API routes + 5 backend
|
|
317
|
+
pages + 1 shared component.
|
|
318
|
+
|
|
319
|
+
Deferred to follow-up:
|
|
320
|
+
- Account / item editor pages
|
|
321
|
+
- Locale files (en, pl)
|
|
322
|
+
|
|
323
|
+
## 0.8.0 — Phase 4c v2 — invoice review UI (unreleased)
|
|
324
|
+
|
|
325
|
+
The "I want to see and post drafts from the UI" workflow. Closes the
|
|
326
|
+
biggest remaining operator UX gap — drafts no longer require `curl`
|
|
327
|
+
to review or post.
|
|
328
|
+
|
|
329
|
+
- `GET /api/billing/invoices` — billing-managed invoices list. Filters
|
|
330
|
+
by `metadata->>'bill_run_id'` presence so non-billing invoices
|
|
331
|
+
(sales orders, etc.) never appear on this surface. Supports
|
|
332
|
+
status / billRunId / billAccountId / testRun / search filters.
|
|
333
|
+
- `GET /api/billing/invoices/[id]` — invoice detail with all lines.
|
|
334
|
+
Hand-rolled because the filter predicate (`metadata ? 'bill_run_id'`)
|
|
335
|
+
doesn't map onto `makeCrudRoute`'s where shape, and the entity
|
|
336
|
+
itself is owned by `core/sales`.
|
|
337
|
+
- `/backend/billing/invoices` — list page. Shows invoice number with
|
|
338
|
+
TEST tag for `metadata.test_run`, status (draft / posted / paid /
|
|
339
|
+
void), bill-period range, gross total, outstanding amount, issue
|
|
340
|
+
date. Filters on status + test-run inclusion.
|
|
341
|
+
- `/backend/billing/invoices/[id]` — detail page. Header card with
|
|
342
|
+
status + period + metadata refs, four-tile totals (subtotal /
|
|
343
|
+
grand total / paid / outstanding), lines DataTable with billing-type
|
|
344
|
+
tag per line (`one_time` / `recurring` / `usage`), operator-added
|
|
345
|
+
flag. Two operator actions:
|
|
346
|
+
- **Post invoice** — visible only for `draft` non-test invoices.
|
|
347
|
+
POSTs `billing.invoices.post`. Refresh on success.
|
|
348
|
+
- **Wipe test invoices for this run** — visible only for
|
|
349
|
+
`metadata.test_run=true`. Hard-deletes every test invoice from
|
|
350
|
+
the same `bill_run_id` via `billing.invoices.wipe_test`. `window
|
|
351
|
+
.confirm` gate (will move to `ConfirmDialog` in a follow-up to
|
|
352
|
+
match the OM UX convention).
|
|
353
|
+
|
|
354
|
+
Sidebar entry under the `Billing` group at order 30.
|
|
355
|
+
|
|
356
|
+
Validation: yarn build + generate + typecheck + test all green; 797
|
|
357
|
+
repo tests, 0 regressions. 6 billing API routes total now (post,
|
|
358
|
+
add-line, edit-line, remove-line, invoices list, invoices detail);
|
|
359
|
+
5 backend pages.
|
|
360
|
+
|
|
361
|
+
Deferred to follow-up:
|
|
362
|
+
- Inline line edit / add / remove dialogs on the detail page (REST
|
|
363
|
+
endpoints already exist from Phase 4b).
|
|
364
|
+
- ConfirmDialog instead of `window.confirm` for the wipe action.
|
|
365
|
+
- Account / item editor pages.
|
|
366
|
+
|
|
367
|
+
## 0.7.0 — Phase 4c admin UI v1 (unreleased)
|
|
368
|
+
|
|
369
|
+
Three operator-facing backend pages — the minimum viable shell so
|
|
370
|
+
operators can drive billing from the admin without `curl`:
|
|
371
|
+
|
|
372
|
+
- `/backend/billing/runs` — Bill Runs list. Status / mode tags
|
|
373
|
+
(running / completed / partial_failure / failed + dry-run / test /
|
|
374
|
+
catch-up flags), as-of date, accounts processed / drafts created /
|
|
375
|
+
failed counts, started + finished timestamps. Filter on status +
|
|
376
|
+
triggered-by. Open into detail.
|
|
377
|
+
- `/backend/billing/runs/[id]` — Bill Run detail. Header card with
|
|
378
|
+
status badges, full summary (accounts processed / drafts / skipped
|
|
379
|
+
/ warnings / failed / usage records rated), "Retry failed accounts
|
|
380
|
+
(N)" button (POSTs `billing.runs.retry_failed`, enabled only when
|
|
381
|
+
N > 0). Outcomes table below: per-account status / draft invoice
|
|
382
|
+
id / error message / warnings JSON.
|
|
383
|
+
- `/backend/billing/accounts` — Billing Accounts list. Read-only for
|
|
384
|
+
v1 (CRUD lives at `/api/billing/accounts` — admin edit UI ships
|
|
385
|
+
in a follow-up). Filters on bill cycle / currency / active; search
|
|
386
|
+
on name. Status badges from the shared OM `Tag` primitive
|
|
387
|
+
(`success` / `default`).
|
|
388
|
+
|
|
389
|
+
Sidebar nav: each page declares `pageGroup: 'Billing'` /
|
|
390
|
+
`pageGroupKey: 'billing.nav.group'` so all three appear under one
|
|
391
|
+
collapsible Billing group; detail page sets `navHidden: true`.
|
|
392
|
+
|
|
393
|
+
All i18n keys default to inline English fallbacks (`t('key',
|
|
394
|
+
'English')` shape). Locale files ship in a follow-up — operators
|
|
395
|
+
running the dev server see correct copy out of the box.
|
|
396
|
+
|
|
397
|
+
Validation: yarn build + generate + typecheck + test all green;
|
|
398
|
+
797 repo tests, 0 regressions; routes registered in
|
|
399
|
+
`.mercato/generated/backend-routes.generated.ts`.
|
|
400
|
+
|
|
401
|
+
Deferred to a follow-up:
|
|
402
|
+
- Account / item / usage / draft-invoice editor pages
|
|
403
|
+
- Triggers (manual Bill Run / dry-run preview) as buttons in the
|
|
404
|
+
list page
|
|
405
|
+
- Locale files (en/pl)
|
|
406
|
+
|
|
407
|
+
## 0.6.0 — Phase 4b backend slice (unreleased)
|
|
408
|
+
|
|
409
|
+
Draft-edit audit pipeline + GDPR portability + test-invoice wipe.
|
|
410
|
+
The Admin UI half of Phase 4 (backend pages) is intentionally
|
|
411
|
+
deferred to its own dedicated effort — every command shipped here is
|
|
412
|
+
fully usable from the REST surface today.
|
|
413
|
+
|
|
414
|
+
- **Three draft-edit commands**, each writing a `DraftInvoiceEdit`
|
|
415
|
+
audit row with the spec's before/after snapshot contract:
|
|
416
|
+
- `billing.invoices.edit_draft_line` — change `description`,
|
|
417
|
+
`quantity`, `unit_price`, or an explicit `totalNetAmount`
|
|
418
|
+
override. Recomputes invoice totals from the surviving lines.
|
|
419
|
+
Audit row carries both `before_json` and `after_json`.
|
|
420
|
+
- `billing.invoices.add_draft_line` — operator-added line (e.g.
|
|
421
|
+
one-off adjustment). `before_json` is `null` per spec.
|
|
422
|
+
- `billing.invoices.remove_draft_line` — hard delete with the
|
|
423
|
+
pre-removal snapshot preserved in `before_json`. `after_json`
|
|
424
|
+
and `invoice_line_id` are `null`.
|
|
425
|
+
- All three refuse non-draft invoices with `HTTP 409 +
|
|
426
|
+
billing.invoice.not_draft` and require the
|
|
427
|
+
`billing.invoice.edit_draft` feature.
|
|
428
|
+
- **Test-invoice wipe command** `billing.invoices.wipe_test`:
|
|
429
|
+
hard-deletes invoices flagged `metadata.test_run=true`, optionally
|
|
430
|
+
scoped to a single `billRunId`. Test data never pollutes production
|
|
431
|
+
history. Gated by `billing.run.trigger`.
|
|
432
|
+
- **GDPR portability endpoint** `GET /api/billing/export/account/[id]`:
|
|
433
|
+
returns a single-account JSON dump covering the account itself,
|
|
434
|
+
every Billing Item, every Usage record, every BillRunOutcome
|
|
435
|
+
referencing the account, every emitted `core/sales` invoice
|
|
436
|
+
(matched via `metadata`), and every `DraftInvoiceEdit` audit row
|
|
437
|
+
for those invoices. Tenant- + organization-scoped via the caller's
|
|
438
|
+
auth — 404 for cross-tenant UUIDs.
|
|
439
|
+
- New REST routes:
|
|
440
|
+
- `POST /api/billing/invoices/edit-line`
|
|
441
|
+
- `POST /api/billing/invoices/add-line`
|
|
442
|
+
- `POST /api/billing/invoices/remove-line`
|
|
443
|
+
- `POST /api/billing/test-invoices/wipe`
|
|
444
|
+
- `GET /api/billing/export/account/[id]`
|
|
445
|
+
- 15 new unit tests covering the command contract: non-draft
|
|
446
|
+
refusal, audit row before/after population, totals recompute
|
|
447
|
+
(unit_price × quantity + explicit override path), null
|
|
448
|
+
invoice/line resolution, wipe scope safety (with vs without
|
|
449
|
+
`billRunId`).
|
|
450
|
+
|
|
451
|
+
Deferred to **Phase 4c** (UI dedicated effort):
|
|
452
|
+
- Backend pages (DataTable + CrudForm) for accounts / items / usage /
|
|
453
|
+
bill runs / draft invoice review.
|
|
454
|
+
- Sidebar navigation + page metadata.
|
|
455
|
+
|
|
456
|
+
## 0.5.0 — Phase 4a posting + payment subscriber (unreleased)
|
|
457
|
+
|
|
458
|
+
- **Post invoice command** `billing.invoices.post`:
|
|
459
|
+
- Asserts the invoice is currently `draft` (refuses with HTTP 409 +
|
|
460
|
+
`billing.invoice.not_draft` otherwise).
|
|
461
|
+
- Refuses test-mode invoices (`metadata.test_run=true`) with HTTP
|
|
462
|
+
409 + `billing.invoice.test_run` — operators wipe test drafts
|
|
463
|
+
through a dedicated endpoint (Phase 4b), never by posting them.
|
|
464
|
+
- Resolves the `posted` `DictionaryEntry` once via the shared
|
|
465
|
+
cache, flips `status` + `status_entry_id`.
|
|
466
|
+
- Emits `billing.invoice.posted` once + one
|
|
467
|
+
`billing.invoice.line_posted` per line. Events carry the
|
|
468
|
+
spec-mandated traceability payload (`billRunId`, `billAccountId`,
|
|
469
|
+
`billPeriodStart` / `End`, line metadata).
|
|
470
|
+
- Invoice number is **not** re-assigned at post (Phase 2 deviation:
|
|
471
|
+
`SalesInvoice.invoice_number` is `NOT NULL` upstream → numbers
|
|
472
|
+
are issued at draft-create via `salesDocumentNumberGenerator`).
|
|
473
|
+
- **Post API route** `POST /api/billing/invoices/post`. RBAC-gated by
|
|
474
|
+
the new `billing.invoice.post` feature (already declared in
|
|
475
|
+
`acl.ts` since Phase 0). Returns
|
|
476
|
+
`{ invoiceId, invoiceNumber, status: 'posted', lineCount }`.
|
|
477
|
+
- **Payment-captured subscriber**
|
|
478
|
+
`subscribers/payment-captured-to-paid.ts`:
|
|
479
|
+
- Listens to `payment_gateways.payment.captured` (the actual
|
|
480
|
+
upstream event — the spec's `payments.payment.completed` doesn't
|
|
481
|
+
exist; this resolves the Phase 0-flagged deviation #2).
|
|
482
|
+
- Matches `payload.paymentId` against `SalesInvoice.id` scoped by
|
|
483
|
+
tenant + organization. Foreign payments (non-billing invoices) →
|
|
484
|
+
silent no-op. Idempotent re-fire on already-paid invoices.
|
|
485
|
+
- Sets `paidTotalAmount = grand_total_gross`, `outstanding = 0`,
|
|
486
|
+
`status = 'paid'`, `status_entry_id = <paid entry>`.
|
|
487
|
+
- **Shared invoice-status resolver** in `lib/invoiceStatus.ts` —
|
|
488
|
+
factored out of `invoiceWriter`'s previously-private cache so the
|
|
489
|
+
draft/posted/paid lookups share one chokepoint. Per-tenant +
|
|
490
|
+
per-status cache.
|
|
491
|
+
- 6 new unit tests for the resolver (199 total for billing).
|
|
492
|
+
|
|
493
|
+
Deferred to **Phase 4b**:
|
|
494
|
+
- Draft-edit endpoint + `DraftInvoiceEdit` audit row writing.
|
|
495
|
+
- GDPR-portability `GET /api/billing/export/account/{id}`.
|
|
496
|
+
- Admin UI (backend pages for accounts / items / runs / draft review).
|
|
497
|
+
|
|
498
|
+
## 0.4.0 — Phase 3 usage rating (unreleased)
|
|
499
|
+
|
|
500
|
+
- **Usage rater** in `lib/usageRater.ts` — pure functions for all four
|
|
501
|
+
spec-mandated rate shapes:
|
|
502
|
+
- Simple flat (`{ unit_price }`): `quantity × unit_price`.
|
|
503
|
+
- Volume tier (`{ model: 'volume', tiers }`): entire quantity at the
|
|
504
|
+
`unit_price` of the tier its total falls into.
|
|
505
|
+
- Graduated tier (`{ model: 'graduated', tiers }`): per-unit pricing
|
|
506
|
+
by tier, the canonical "metered API" model.
|
|
507
|
+
- Flat tier (`{ model: 'flat', tiers }`): one `flat_amount` for the
|
|
508
|
+
highest tier reached.
|
|
509
|
+
|
|
510
|
+
Returns `{ amount, breakdown }` where `breakdown` is the per-tier
|
|
511
|
+
audit trail persisted into `SalesInvoiceLine.metadata
|
|
512
|
+
.usage_tier_breakdown`. Half-up rounding to 2dp on the amount;
|
|
513
|
+
unit prices preserve sub-cent precision via the dedicated
|
|
514
|
+
`formatUnitPrice` helper (e.g. `0.001 → "0.0010"` instead of being
|
|
515
|
+
rounded to zero).
|
|
516
|
+
|
|
517
|
+
- **Usage runner** in `lib/usageRunner.ts` — per-account processor:
|
|
518
|
+
- Selects unrated usage records (`rated_in_bill_run_id IS NULL`,
|
|
519
|
+
`period_end <= bill_period_end`).
|
|
520
|
+
- Groups by `uom_code`, matches against the account's `type=usage`
|
|
521
|
+
Billing Items (exact match — NO conversion per spec).
|
|
522
|
+
- Aggregates quantity per uom, rates, emits ONE invoice line per
|
|
523
|
+
matched item with the breakdown in metadata.
|
|
524
|
+
- Surfaces `unmatched_usage_uoms` for usage records that have no
|
|
525
|
+
matching Billing Item — operator's `success_with_warnings`
|
|
526
|
+
signal to add the missing item then trigger retry-failed.
|
|
527
|
+
|
|
528
|
+
- **Engine integration** in `lib/billRunEngine.ts`:
|
|
529
|
+
- Per-cycle loop now combines `buildInvoiceLinesFromItems` (one_time
|
|
530
|
+
+ recurring) with `processUsageForAccount` (usage) into a single
|
|
531
|
+
`SalesInvoice` write.
|
|
532
|
+
- Real-mode commits mark consumed usage records'
|
|
533
|
+
`rated_in_bill_run_id` via a single `nativeUpdate` (scales to
|
|
534
|
+
accounts with thousands of records per cycle).
|
|
535
|
+
- `AccountWarnings` now carries `unmatched_usage_uoms` alongside
|
|
536
|
+
`currency_mismatch_items`; both raise the outcome status to
|
|
537
|
+
`success_with_warnings`.
|
|
538
|
+
- `BillRun.summary.usage_records_rated` is populated by summing the
|
|
539
|
+
per-account totals — the spec's required summary key, previously
|
|
540
|
+
a hard-coded zero.
|
|
541
|
+
|
|
542
|
+
- **Per-record `line_description` itemization** (emitting one invoice
|
|
543
|
+
line per Usage record when `line_description` is set) is
|
|
544
|
+
intentionally deferred to Phase 4 (admin UI iteration). Phase 3
|
|
545
|
+
ships one aggregated line per matched usage item — sufficient for
|
|
546
|
+
correct billing; richer invoice presentation lands when there's a
|
|
547
|
+
UI to show it.
|
|
548
|
+
|
|
549
|
+
- 27 new unit tests (193 total for the package):
|
|
550
|
+
- `usageRater` covers every spec example end-to-end (`25k vs
|
|
551
|
+
[10k=0, ∞=0.001]` graduated → 15 EUR, `25k vs [10k=0,
|
|
552
|
+
50k=0.001, ∞=0.0005]` volume → 25 EUR, `25k vs
|
|
553
|
+
[10k=50, 50k=200, ∞=1000]` flat → 200 EUR) plus boundary
|
|
554
|
+
conditions and the `1.005 → 1.01` half-up edge case.
|
|
555
|
+
- Engine integration: graduated 25k → 15 EUR end-to-end, unmatched
|
|
556
|
+
UoM warning + un-rated guard, simple-flat unit_price emission,
|
|
557
|
+
test-mode does NOT mark records, mixed recurring + usage line
|
|
558
|
+
aggregation (49.99 + 12.00 = 61.99).
|
|
559
|
+
|
|
560
|
+
## 0.3.0 — Phase 2 Bill Run engine (unreleased)
|
|
561
|
+
|
|
562
|
+
- **Bill Run engine** in `lib/billRunEngine.ts` — the heart of the
|
|
563
|
+
module. Handles cron-schedulable + manual triggers, dry-run / test /
|
|
564
|
+
real modes, catch-up looping (manual only), per-tenant Postgres
|
|
565
|
+
advisory lock (Phase 1 primitive), per-account independent
|
|
566
|
+
transactions via savepoint, open-draft anti-duplicate check (queries
|
|
567
|
+
`core/sales` invoices by `metadata->>'bill_period_*'`), item-level
|
|
568
|
+
`billed_to_date` advance on real-mode commit, final summary
|
|
569
|
+
aggregation, and four-way outcome reporting (success /
|
|
570
|
+
success_with_warnings / skipped_existing_draft / failed).
|
|
571
|
+
- **Calendar-aware bill-period math** in `lib/billPeriod.ts` — clamps
|
|
572
|
+
month-end edge cases (`Jan 31 + 1 month → Feb 29` in 2024,
|
|
573
|
+
`Mar 31 - 1 month → Feb 28` in 2026) so periods match the spec's
|
|
574
|
+
Postgres-interval semantics exactly. Replaces the naive
|
|
575
|
+
`setUTCMonth(±1)` which overflows.
|
|
576
|
+
- **Item selector** in `lib/itemSelector.ts` — predicates the spec's
|
|
577
|
+
"Mid-cycle recurring items" + `billed_to_date` guard rules; emits
|
|
578
|
+
diagnostic skip reasons. `usage` items are routed to the (future)
|
|
579
|
+
Phase 3 rater.
|
|
580
|
+
- **Invoice writer** in `lib/invoiceWriter.ts` — creates the
|
|
581
|
+
`core/sales` `SalesInvoice` + `SalesInvoiceLine` rows with the
|
|
582
|
+
spec-mandated `metadata` (`bill_run_id`, `bill_account_id`,
|
|
583
|
+
`bill_period_start`, `bill_period_end`, `test_run`); resolves the
|
|
584
|
+
draft `DictionaryEntry` once per scope and caches it. Per-line
|
|
585
|
+
pricing follows the spec's `BillingItem → InvoiceLine` table:
|
|
586
|
+
`one_time → amount × 1`, `recurring → unit_price × 1`. Half-up
|
|
587
|
+
rounding survives JS floating-point edge cases (`1.005 → 1.01`).
|
|
588
|
+
- **Trigger / retry commands** — `billing.runs.trigger` and
|
|
589
|
+
`billing.runs.retry_failed`; lock-busy condition is mapped to
|
|
590
|
+
HTTP 409 with discriminator `code: 'billing.run.lock_busy'`.
|
|
591
|
+
- **REST surface**:
|
|
592
|
+
- `GET / POST /api/billing/runs` (list + manual trigger)
|
|
593
|
+
- `POST /api/billing/runs/retry-failed` (re-runs failed accounts
|
|
594
|
+
of a parent run; sets `parent_run_id` on the new run)
|
|
595
|
+
- `GET /api/billing/run-outcomes` (paginated outcomes list,
|
|
596
|
+
filterable by `billRunId` / `billAccountId` / `status`)
|
|
597
|
+
- 47 new unit tests (166 total for billing):
|
|
598
|
+
- `billPeriod` calendar math: every cell of the spec's reference
|
|
599
|
+
table + leap-year + chained-advance drift cases.
|
|
600
|
+
- `itemSelector` predicates: every inclusion / skip path.
|
|
601
|
+
- `invoiceWriter.buildInvoiceLinesFromItems`: per-type pricing,
|
|
602
|
+
rounding, negative amounts, malformed rate-shape defense.
|
|
603
|
+
- `billRunEngine` orchestration with thin EM mock: happy path,
|
|
604
|
+
zero-accounts, open-draft skip, dry-run, test-mode, currency
|
|
605
|
+
mismatch warnings, empty-items advance, summary aggregation.
|
|
606
|
+
|
|
607
|
+
**Deviations from spec** (documented in `xd-249-billing-spec.md`
|
|
608
|
+
Phase 2 deviations):
|
|
609
|
+
|
|
610
|
+
1. `SalesInvoice.invoice_number` is NOT NULL upstream — drafts get a
|
|
611
|
+
real number assigned at draft-create time via
|
|
612
|
+
`salesDocumentNumberGenerator.generate({ kind: 'invoice' })`. The
|
|
613
|
+
spec's "sequence number assigned at Post" intent (gap-free, atomic
|
|
614
|
+
SQL upsert) is preserved; only the timing differs.
|
|
615
|
+
2. Test-mode invoices use a `TEST-<timestamp>-<uuid>` placeholder
|
|
616
|
+
number so test runs don't burn production numbers and stay
|
|
617
|
+
idempotent.
|
|
618
|
+
3. VAT not computed on drafts — `core/sales` tax service runs at
|
|
619
|
+
POST. Draft totals reflect net amounts only; gross == net.
|
|
620
|
+
|
|
621
|
+
## 0.2.0 — Phase 1 REST API (unreleased)
|
|
622
|
+
|
|
623
|
+
- REST API surface for the three integrator-facing resources:
|
|
624
|
+
- `GET / POST / PUT / DELETE /api/billing/accounts` (Billing Accounts CRUD)
|
|
625
|
+
- `GET / POST / PUT / DELETE /api/billing/items` (Billing Items CRUD with
|
|
626
|
+
`source_ref` idempotency on POST — duplicate `(tenant, account,
|
|
627
|
+
sourceRef)` returns the existing row with `deduplicated: true`)
|
|
628
|
+
- `GET / POST /api/billing/usage` (append-only usage ingest; idempotent
|
|
629
|
+
by `source_ref`; no update / delete by design)
|
|
630
|
+
- Lean command pattern (no undo / snapshot yet — Phase 4 adds those):
|
|
631
|
+
- `billing.accounts.create / update / delete`
|
|
632
|
+
- `billing.items.create / update / delete`
|
|
633
|
+
- `billing.usage.create`
|
|
634
|
+
- New helpers under `lib/`:
|
|
635
|
+
- `withTenantLock(em, { tenantId, lockName }, fn)` — per-tenant
|
|
636
|
+
Postgres transaction-scoped advisory lock; throws
|
|
637
|
+
`TenantLockBusyError` on busy. Used by the Phase 2 Bill Run engine
|
|
638
|
+
to enforce at-most-one concurrent run per tenant.
|
|
639
|
+
- `findBySourceRef(em, Entity, { tenantId, billAccountId, sourceRef })`
|
|
640
|
+
— the single source-ref pre-check helper shared by items and usage.
|
|
641
|
+
- Zod CRUD payload schemas added to `data/validators.ts`:
|
|
642
|
+
- `billingAccount(Create|Update|Delete|ListQuery)Schema`
|
|
643
|
+
- `billingItem(Create|Update|Delete|ListQuery)Schema`
|
|
644
|
+
- `billingUsage(Create|ListQuery)Schema`
|
|
645
|
+
- 43 new unit tests (119 total for the package).
|
|
646
|
+
- Migration not required — Phase 1 uses the existing Phase 0 schema.
|
|
647
|
+
|
|
648
|
+
## 0.1.0 — Phase 0 scaffold (unreleased)
|
|
649
|
+
|
|
650
|
+
- Initial workspace scaffold mirroring `@dainamite/cpq`.
|
|
651
|
+
- Six billing-owned entities: `BillingAccount`, `BillingItem`,
|
|
652
|
+
`BillingAccountUsage`, `BillRun`, `BillRunOutcome`, `DraftInvoiceEdit`.
|
|
653
|
+
- 12 ACL features (`billing.*`) and three default roles
|
|
654
|
+
(`billing.admin` / `billing.finance_user` / `billing.usage_writer`).
|
|
655
|
+
- Tenant setup seeds the global UoM dictionary (16 codes), invoice statuses
|
|
656
|
+
(`draft` / `posted` / `paid` / `void`), per-organization invoice sequence
|
|
657
|
+
configs, and the cron schedule defaults.
|
|
658
|
+
- Reaper worker stub for stale `BillRun` rows.
|