@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dainamite Technologies
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @dainamite/billing
|
|
2
|
+
|
|
3
|
+
Lightweight recurring-billing engine for Open Mercato applications.
|
|
4
|
+
|
|
5
|
+
Collects items to charge (`one_time`, `recurring`, `usage`), runs a scheduled
|
|
6
|
+
**Bill Run** that calculates totals and produces **draft invoices** in
|
|
7
|
+
`@open-mercato/core/sales`, and waits for human approval before posting. It
|
|
8
|
+
does **not** replace your accounting system — it prepares billing data and
|
|
9
|
+
hands drafts to a human.
|
|
10
|
+
|
|
11
|
+
## Status
|
|
12
|
+
|
|
13
|
+
**Built — publishing soon.** Schema, ACL, tenant setup, the Bill Run engine,
|
|
14
|
+
usage rating, the full admin UI, and the `@dainamite/cpq-billing-connector`
|
|
15
|
+
companion are all in place. The package is not yet on npm; until it publishes
|
|
16
|
+
it is available inside the `dainamite-core` monorepo.
|
|
17
|
+
|
|
18
|
+
See [`specs/implementation/xd-249-billing-spec.md`](../../specs/implementation/xd-249-billing-spec.md)
|
|
19
|
+
for the full implementation plan.
|
|
20
|
+
|
|
21
|
+
## What's inside
|
|
22
|
+
|
|
23
|
+
- **Billing Items** — the charge backlog (`one_time`, `recurring`, `usage`),
|
|
24
|
+
each scoped to a Billing Account with a bill start/end window.
|
|
25
|
+
- **Bill Runs** — a background worker (`@open-mercato/queue`) that, on a
|
|
26
|
+
schedule or on demand, rates usage, sums the due items for the period, and
|
|
27
|
+
writes **draft invoices into `@open-mercato/core/sales`**. Runs are
|
|
28
|
+
idempotent and tenant-locked; stale runs are reaped by a worker.
|
|
29
|
+
- **Draft invoice approval** — invoices land in `draft`; a human reviews and
|
|
30
|
+
**posts** them (a guarded ACL action) before they leave draft.
|
|
31
|
+
- **Usage rating** — metered items are rated at run time from ingested usage.
|
|
32
|
+
- **Admin UI** under `/backend/billing/*` — Accounts, Items, Bill Runs,
|
|
33
|
+
Invoices (list + detail pages).
|
|
34
|
+
- **Commands** for every write (accounts / items / runs / invoices / usage),
|
|
35
|
+
validated with Zod.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
`@dainamite/billing` is an Open Mercato module. Install it and register it in
|
|
40
|
+
your host app's `src/modules.ts`:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
yarn mercato module add @dainamite/billing # installs + registers (once published to npm)
|
|
44
|
+
# …or: yarn add @dainamite/billing then add { id: 'billing', from: '@dainamite/billing' }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
yarn generate
|
|
49
|
+
yarn mercato db migrate
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Required Open Mercato modules
|
|
53
|
+
|
|
54
|
+
`sales` (draft invoices are written there), `@open-mercato/queue` (Bill Run
|
|
55
|
+
worker), and `@open-mercato/events` (lifecycle events consumed by the
|
|
56
|
+
connector). Engines: **Node ≥ 24**.
|
|
57
|
+
|
|
58
|
+
## CPQ integration
|
|
59
|
+
|
|
60
|
+
To bill subscriptions sold through CPQ, add
|
|
61
|
+
[`@dainamite/cpq-billing-connector`](../cpq-billing-connector) — it subscribes
|
|
62
|
+
to CPQ subscription lifecycle events and creates/updates Billing Accounts and
|
|
63
|
+
Items automatically. Register it **after** both `cpq` and `billing`.
|
|
64
|
+
|
|
65
|
+
## Distribution rules (per SPEC-001)
|
|
66
|
+
|
|
67
|
+
- **`peerDependencies`, never `dependencies`** for `@open-mercato/*` and
|
|
68
|
+
sibling `@dainamite/*` — one shared instance in `node_modules`.
|
|
69
|
+
- **No ORM relations across packages** — cross-module references are FK
|
|
70
|
+
strings; draft invoices are created through the sales service / events, not
|
|
71
|
+
a direct relation.
|
|
72
|
+
- **Ships its own migrations** under `src/modules/billing/migrations/`.
|
|
73
|
+
- Ships `src/` (types) + `dist/` (runtime JS); no `.d.ts` emitted.
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const features = [
|
|
2
|
+
{ id: "billing.account.manage", title: "Manage Billing Accounts", module: "billing" },
|
|
3
|
+
{ id: "billing.account.view", title: "View Billing Accounts", module: "billing" },
|
|
4
|
+
{ id: "billing.item.manage", title: "Manage Billing Items", module: "billing" },
|
|
5
|
+
{ id: "billing.item.view", title: "View Billing Items", module: "billing" },
|
|
6
|
+
{ id: "billing.usage.ingest", title: "Ingest Usage Records", module: "billing" },
|
|
7
|
+
{ id: "billing.usage.view", title: "View Usage Records", module: "billing" },
|
|
8
|
+
{ id: "billing.run.trigger", title: "Trigger Bill Run", module: "billing" },
|
|
9
|
+
{ id: "billing.run.dry_run", title: "Trigger Bill Run (dry-run)", module: "billing" },
|
|
10
|
+
{ id: "billing.run.view", title: "View Bill Run History", module: "billing" },
|
|
11
|
+
{ id: "billing.invoice.post", title: "Post Draft Invoice", module: "billing" },
|
|
12
|
+
{ id: "billing.invoice.edit_draft", title: "Edit Draft Invoice", module: "billing" },
|
|
13
|
+
{ id: "billing.invoice.view", title: "View Billing Invoices", module: "billing" }
|
|
14
|
+
];
|
|
15
|
+
var acl_default = features;
|
|
16
|
+
export {
|
|
17
|
+
acl_default as default,
|
|
18
|
+
features
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=acl.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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;AAClF;AAEA,IAAO,cAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
|
|
3
|
+
import { withScopedPayload } from "@open-mercato/shared/lib/api/scoped";
|
|
4
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
5
|
+
import { BillingAccount } from "../../data/entities.js";
|
|
6
|
+
import { billingEntityIds } from "../../data/entityIds.js";
|
|
7
|
+
import {
|
|
8
|
+
billingAccountCreateSchema,
|
|
9
|
+
billingAccountListQuerySchema,
|
|
10
|
+
billingAccountUpdateSchema
|
|
11
|
+
} from "../../data/validators.js";
|
|
12
|
+
import {
|
|
13
|
+
createBillingCrudOpenApi,
|
|
14
|
+
createPagedListResponseSchema,
|
|
15
|
+
defaultCreateResponseSchema,
|
|
16
|
+
defaultOkResponseSchema
|
|
17
|
+
} from "../openapi.js";
|
|
18
|
+
const routeMetadata = {
|
|
19
|
+
GET: { requireAuth: true, requireFeatures: ["billing.account.view"] },
|
|
20
|
+
POST: { requireAuth: true, requireFeatures: ["billing.account.manage"] },
|
|
21
|
+
PUT: { requireAuth: true, requireFeatures: ["billing.account.manage"] },
|
|
22
|
+
DELETE: { requireAuth: true, requireFeatures: ["billing.account.manage"] }
|
|
23
|
+
};
|
|
24
|
+
const metadata = routeMetadata;
|
|
25
|
+
const billingAccountListItemSchema = z.object({
|
|
26
|
+
id: z.string().uuid(),
|
|
27
|
+
customer_id: z.string(),
|
|
28
|
+
name: z.string(),
|
|
29
|
+
currency_code: z.string(),
|
|
30
|
+
bill_cycle: z.string(),
|
|
31
|
+
bill_cycle_anchor: z.string(),
|
|
32
|
+
invoice_email: z.string(),
|
|
33
|
+
invoice_language: z.string(),
|
|
34
|
+
tax_id: z.string().nullable(),
|
|
35
|
+
invoice_address: z.record(z.string(), z.unknown()),
|
|
36
|
+
next_bill_date: z.string(),
|
|
37
|
+
last_bill_date: z.string().nullable(),
|
|
38
|
+
is_active: z.boolean(),
|
|
39
|
+
organization_id: z.string().uuid(),
|
|
40
|
+
tenant_id: z.string().uuid(),
|
|
41
|
+
created_at: z.string(),
|
|
42
|
+
updated_at: z.string()
|
|
43
|
+
});
|
|
44
|
+
const rawBodySchema = z.object({}).passthrough();
|
|
45
|
+
const crud = makeCrudRoute({
|
|
46
|
+
metadata: routeMetadata,
|
|
47
|
+
orm: {
|
|
48
|
+
entity: BillingAccount,
|
|
49
|
+
idField: "id",
|
|
50
|
+
orgField: "organizationId",
|
|
51
|
+
tenantField: "tenantId",
|
|
52
|
+
softDeleteField: "deletedAt"
|
|
53
|
+
},
|
|
54
|
+
list: {
|
|
55
|
+
schema: billingAccountListQuerySchema,
|
|
56
|
+
entityId: billingEntityIds.account,
|
|
57
|
+
fields: [
|
|
58
|
+
"id",
|
|
59
|
+
"customer_id",
|
|
60
|
+
"name",
|
|
61
|
+
"currency_code",
|
|
62
|
+
"bill_cycle",
|
|
63
|
+
"bill_cycle_anchor",
|
|
64
|
+
"invoice_email",
|
|
65
|
+
"invoice_language",
|
|
66
|
+
"tax_id",
|
|
67
|
+
"invoice_address",
|
|
68
|
+
"next_bill_date",
|
|
69
|
+
"last_bill_date",
|
|
70
|
+
"is_active",
|
|
71
|
+
"organization_id",
|
|
72
|
+
"tenant_id",
|
|
73
|
+
"created_at",
|
|
74
|
+
"updated_at"
|
|
75
|
+
],
|
|
76
|
+
sortFieldMap: {
|
|
77
|
+
name: "name",
|
|
78
|
+
createdAt: "created_at",
|
|
79
|
+
updatedAt: "updated_at",
|
|
80
|
+
nextBillDate: "next_bill_date"
|
|
81
|
+
},
|
|
82
|
+
buildFilters: async (query) => {
|
|
83
|
+
const filters = {};
|
|
84
|
+
if (typeof query.id === "string" && query.id) {
|
|
85
|
+
filters.id = { $eq: query.id };
|
|
86
|
+
}
|
|
87
|
+
if (typeof query.customerId === "string" && query.customerId) {
|
|
88
|
+
filters.customer_id = { $eq: query.customerId };
|
|
89
|
+
}
|
|
90
|
+
if (typeof query.currencyCode === "string" && query.currencyCode) {
|
|
91
|
+
filters.currency_code = { $eq: query.currencyCode.toUpperCase() };
|
|
92
|
+
}
|
|
93
|
+
if (typeof query.billCycle === "string" && query.billCycle) {
|
|
94
|
+
filters.bill_cycle = { $eq: query.billCycle };
|
|
95
|
+
}
|
|
96
|
+
if (typeof query.isActive === "boolean") {
|
|
97
|
+
filters.is_active = { $eq: query.isActive };
|
|
98
|
+
}
|
|
99
|
+
if (typeof query.search === "string" && query.search.trim()) {
|
|
100
|
+
const pattern = `%${query.search.trim().replace(/[%_]/g, (m) => `\\${m}`)}%`;
|
|
101
|
+
filters.name = { $ilike: pattern };
|
|
102
|
+
}
|
|
103
|
+
return filters;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
actions: {
|
|
107
|
+
create: {
|
|
108
|
+
commandId: "billing.accounts.create",
|
|
109
|
+
schema: rawBodySchema,
|
|
110
|
+
mapInput: async ({ raw, ctx }) => {
|
|
111
|
+
const { translate } = await resolveTranslations();
|
|
112
|
+
const scoped = withScopedPayload(raw ?? {}, ctx, translate);
|
|
113
|
+
return billingAccountCreateSchema.parse(scoped);
|
|
114
|
+
},
|
|
115
|
+
response: ({ result }) => ({ id: result?.id ?? null }),
|
|
116
|
+
status: 201
|
|
117
|
+
},
|
|
118
|
+
update: {
|
|
119
|
+
commandId: "billing.accounts.update",
|
|
120
|
+
schema: rawBodySchema,
|
|
121
|
+
mapInput: async ({ raw, ctx }) => {
|
|
122
|
+
const { translate } = await resolveTranslations();
|
|
123
|
+
const scoped = withScopedPayload(raw ?? {}, ctx, translate);
|
|
124
|
+
return billingAccountUpdateSchema.parse(scoped);
|
|
125
|
+
},
|
|
126
|
+
response: () => ({ ok: true })
|
|
127
|
+
},
|
|
128
|
+
delete: {
|
|
129
|
+
commandId: "billing.accounts.delete",
|
|
130
|
+
schema: rawBodySchema,
|
|
131
|
+
mapInput: async ({ raw, ctx }) => {
|
|
132
|
+
const { translate } = await resolveTranslations();
|
|
133
|
+
const envelope = raw ?? {};
|
|
134
|
+
const body = envelope.body ?? {};
|
|
135
|
+
const query = envelope.query ?? {};
|
|
136
|
+
const scoped = withScopedPayload(body, ctx, translate);
|
|
137
|
+
const id = typeof body.id === "string" && body.id || typeof query.id === "string" && query.id || null;
|
|
138
|
+
if (!id) {
|
|
139
|
+
throw new Error("id is required");
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
id,
|
|
143
|
+
tenantId: scoped.tenantId,
|
|
144
|
+
organizationId: scoped.organizationId
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
response: () => ({ ok: true })
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
const { POST, PUT, DELETE } = crud;
|
|
152
|
+
const GET = crud.GET;
|
|
153
|
+
const openApi = createBillingCrudOpenApi({
|
|
154
|
+
resourceName: "BillingAccount",
|
|
155
|
+
pluralName: "BillingAccounts",
|
|
156
|
+
querySchema: billingAccountListQuerySchema,
|
|
157
|
+
listResponseSchema: createPagedListResponseSchema(billingAccountListItemSchema),
|
|
158
|
+
create: {
|
|
159
|
+
schema: billingAccountCreateSchema,
|
|
160
|
+
responseSchema: defaultCreateResponseSchema,
|
|
161
|
+
description: "Creates a new billing account for a customer."
|
|
162
|
+
},
|
|
163
|
+
update: {
|
|
164
|
+
schema: billingAccountUpdateSchema,
|
|
165
|
+
responseSchema: defaultOkResponseSchema,
|
|
166
|
+
description: "Updates a billing account. `currencyCode` and `customerId` are immutable."
|
|
167
|
+
},
|
|
168
|
+
del: {
|
|
169
|
+
schema: z.object({ id: z.string().uuid() }),
|
|
170
|
+
responseSchema: defaultOkResponseSchema,
|
|
171
|
+
description: "Soft-deletes a billing account (sets `deleted_at`)."
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
export {
|
|
175
|
+
DELETE,
|
|
176
|
+
GET,
|
|
177
|
+
POST,
|
|
178
|
+
PUT,
|
|
179
|
+
metadata,
|
|
180
|
+
openApi
|
|
181
|
+
};
|
|
182
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
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 {
|
|
5
|
+
BillingAccount,
|
|
6
|
+
BillingAccountUsage,
|
|
7
|
+
BillingItem,
|
|
8
|
+
BillRunOutcome,
|
|
9
|
+
DraftInvoiceEdit
|
|
10
|
+
} from "../../../../data/entities.js";
|
|
11
|
+
const metadata = {
|
|
12
|
+
GET: { requireAuth: true, requireFeatures: ["billing.account.view"] }
|
|
13
|
+
};
|
|
14
|
+
async function GET(req, { params }) {
|
|
15
|
+
let container = null;
|
|
16
|
+
try {
|
|
17
|
+
container = await createRequestContainer();
|
|
18
|
+
const auth = await getAuthFromRequest(req);
|
|
19
|
+
if (!auth || !auth.tenantId) {
|
|
20
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
21
|
+
}
|
|
22
|
+
const tenantId = auth.tenantId;
|
|
23
|
+
const organizationId = auth.orgId ?? null;
|
|
24
|
+
if (!organizationId) {
|
|
25
|
+
return NextResponse.json(
|
|
26
|
+
{ error: "Organization scope required" },
|
|
27
|
+
{ status: 403 }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const { id: accountId } = await params;
|
|
31
|
+
const em = container.resolve("em").fork();
|
|
32
|
+
const account = await em.findOne(BillingAccount, {
|
|
33
|
+
id: accountId,
|
|
34
|
+
tenantId,
|
|
35
|
+
organizationId,
|
|
36
|
+
deletedAt: null
|
|
37
|
+
});
|
|
38
|
+
if (!account) {
|
|
39
|
+
return NextResponse.json({ error: "Account not found" }, { status: 404 });
|
|
40
|
+
}
|
|
41
|
+
const url = new URL(req.url);
|
|
42
|
+
const since = parseDate(url.searchParams.get("since")) ?? defaultSince();
|
|
43
|
+
const until = parseDate(url.searchParams.get("until")) ?? /* @__PURE__ */ new Date();
|
|
44
|
+
const [items, usage, outcomes] = await Promise.all([
|
|
45
|
+
// Items are bounded by integration volume (typically <100 per
|
|
46
|
+
// account); no time-window needed — they often outlive a single
|
|
47
|
+
// export window and the operator needs to see active items
|
|
48
|
+
// regardless of when they were created.
|
|
49
|
+
em.find(
|
|
50
|
+
BillingItem,
|
|
51
|
+
{ tenantId, organizationId, billAccountId: account.id },
|
|
52
|
+
{ limit: EXPORT_HARD_LIMIT }
|
|
53
|
+
),
|
|
54
|
+
em.find(
|
|
55
|
+
BillingAccountUsage,
|
|
56
|
+
{
|
|
57
|
+
tenantId,
|
|
58
|
+
organizationId,
|
|
59
|
+
billAccountId: account.id,
|
|
60
|
+
periodEnd: { $gte: since, $lte: until }
|
|
61
|
+
},
|
|
62
|
+
{ limit: EXPORT_HARD_LIMIT, orderBy: { periodEnd: "DESC" } }
|
|
63
|
+
),
|
|
64
|
+
em.find(
|
|
65
|
+
BillRunOutcome,
|
|
66
|
+
{
|
|
67
|
+
tenantId,
|
|
68
|
+
organizationId,
|
|
69
|
+
billAccountId: account.id,
|
|
70
|
+
createdAt: { $gte: since, $lte: until }
|
|
71
|
+
},
|
|
72
|
+
{ limit: EXPORT_HARD_LIMIT, orderBy: { createdAt: "DESC" } }
|
|
73
|
+
)
|
|
74
|
+
]);
|
|
75
|
+
const invoiceRows = await em.execute(
|
|
76
|
+
`SELECT id, invoice_number, status, status_entry_id, issue_date, due_date,
|
|
77
|
+
currency_code, subtotal_net_amount, grand_total_net_amount,
|
|
78
|
+
grand_total_gross_amount, paid_total_amount, outstanding_amount,
|
|
79
|
+
metadata, created_at, updated_at, deleted_at
|
|
80
|
+
FROM sales_invoices
|
|
81
|
+
WHERE tenant_id = ?
|
|
82
|
+
AND organization_id = ?
|
|
83
|
+
AND deleted_at IS NULL
|
|
84
|
+
AND metadata->>'bill_account_id' = ?
|
|
85
|
+
AND created_at >= ?
|
|
86
|
+
AND created_at <= ?
|
|
87
|
+
ORDER BY created_at DESC
|
|
88
|
+
LIMIT ?`,
|
|
89
|
+
[tenantId, organizationId, account.id, since, until, EXPORT_HARD_LIMIT]
|
|
90
|
+
);
|
|
91
|
+
const invoiceIds = invoiceRows.map((r) => r.id);
|
|
92
|
+
const draftEdits = invoiceIds.length > 0 ? await em.find(
|
|
93
|
+
DraftInvoiceEdit,
|
|
94
|
+
{
|
|
95
|
+
tenantId,
|
|
96
|
+
organizationId,
|
|
97
|
+
invoiceId: { $in: invoiceIds }
|
|
98
|
+
},
|
|
99
|
+
{ limit: EXPORT_HARD_LIMIT, orderBy: { createdAt: "DESC" } }
|
|
100
|
+
) : [];
|
|
101
|
+
return NextResponse.json({
|
|
102
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
tenantId,
|
|
105
|
+
organizationId,
|
|
106
|
+
window: {
|
|
107
|
+
since: since.toISOString(),
|
|
108
|
+
until: until.toISOString(),
|
|
109
|
+
hardLimit: EXPORT_HARD_LIMIT
|
|
110
|
+
},
|
|
111
|
+
account,
|
|
112
|
+
items,
|
|
113
|
+
usage,
|
|
114
|
+
runOutcomes: outcomes,
|
|
115
|
+
invoices: invoiceRows,
|
|
116
|
+
draftInvoiceEdits: draftEdits
|
|
117
|
+
});
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error("[billing/export/account.GET]", err);
|
|
120
|
+
return NextResponse.json({ error: "Internal error" }, { status: 500 });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const EXPORT_HARD_LIMIT = 5e4;
|
|
124
|
+
function defaultSince() {
|
|
125
|
+
const d = /* @__PURE__ */ new Date();
|
|
126
|
+
d.setUTCFullYear(d.getUTCFullYear() - 1);
|
|
127
|
+
return d;
|
|
128
|
+
}
|
|
129
|
+
function parseDate(value) {
|
|
130
|
+
if (!value) return null;
|
|
131
|
+
const d = new Date(value);
|
|
132
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
133
|
+
}
|
|
134
|
+
const openApi = {
|
|
135
|
+
methods: {
|
|
136
|
+
GET: {
|
|
137
|
+
tags: ["Billing"],
|
|
138
|
+
summary: "Export a billing account (GDPR portability)",
|
|
139
|
+
parameters: [
|
|
140
|
+
{
|
|
141
|
+
name: "id",
|
|
142
|
+
in: "path",
|
|
143
|
+
required: true,
|
|
144
|
+
schema: { type: "string", format: "uuid" }
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "since",
|
|
148
|
+
in: "query",
|
|
149
|
+
required: false,
|
|
150
|
+
description: "ISO timestamp; lower bound for usage / outcomes / invoices windows. Defaults to today \u2212 12 months.",
|
|
151
|
+
schema: { type: "string", format: "date-time" }
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "until",
|
|
155
|
+
in: "query",
|
|
156
|
+
required: false,
|
|
157
|
+
description: "ISO timestamp; upper bound. Defaults to now.",
|
|
158
|
+
schema: { type: "string", format: "date-time" }
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
description: "Returns a JSON dump of a single Billing Account: the account itself, every Billing Item (no time cap \u2014 items often outlive a single export window), and the Usage records / BillRunOutcomes / emitted core/sales invoices / DraftInvoiceEdit rows whose timestamps fall in the [since, until] window (defaults to the last 12 months). Each collection is independently capped by EXPORT_HARD_LIMIT = 50,000 rows. Lifetime dumps for outlier accounts need a follow-up chunked background-job approach \u2014 out of scope for this v1 endpoint."
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
export {
|
|
166
|
+
GET,
|
|
167
|
+
metadata,
|
|
168
|
+
openApi
|
|
169
|
+
};
|
|
170
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../src/modules/billing/api/export/account/%5Bid%5D/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 {\n BillingAccount,\n BillingAccountUsage,\n BillingItem,\n BillRunOutcome,\n DraftInvoiceEdit,\n} from '../../../../data/entities'\n\n/**\n * `GET /api/billing/export/account/[id]` \u2014 GDPR portability dump.\n *\n * Returns a full JSON serialization of a single Billing Account\n * including:\n * - The account row itself.\n * - Every active + soft-deleted `BillingItem` for the account.\n * - Every `BillingAccountUsage` record.\n * - Every `BillRunOutcome` row referencing the account.\n * - Every `core/sales` invoice the engine emitted for the account\n * (matched via `metadata->>'bill_account_id'`).\n * - Every `DraftInvoiceEdit` audit row for those invoices.\n *\n * Scope: the caller's auth tenant/organization is the authority \u2014 a\n * 404 is returned even for valid UUIDs that belong to a different\n * tenant (no cross-tenant leak).\n *\n * Soft-deleted rows are included on purpose: GDPR portability is\n * about the data we still hold, not just the data the UI shows.\n */\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.account.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 tenantId = auth.tenantId\n const organizationId = (auth as { orgId?: string }).orgId ?? null\n if (!organizationId) {\n return NextResponse.json(\n { error: 'Organization scope required' },\n { status: 403 },\n )\n }\n\n const { id: accountId } = await params\n\n const em = (container.resolve('em') as EntityManager).fork()\n const account = await em.findOne(BillingAccount, {\n id: accountId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n if (!account) {\n return NextResponse.json({ error: 'Account not found' }, { status: 404 })\n }\n\n // Time-window cap for usage records, run outcomes and invoices.\n //\n // Scale note: a telco account can carry millions of usage rows.\n // Returning everything would OOM on the export path. We default\n // to the last 12 months and let the caller override via `since` /\n // `until` for full GDPR-compliance dumps (a regulator request that\n // legitimately needs the lifetime export should set `since=1970-01-01`).\n // Each query is independently capped by `EXPORT_HARD_LIMIT` so a\n // single year of pathological volume still returns rather than\n // hanging.\n const url = new URL(req.url)\n const since = parseDate(url.searchParams.get('since'))\n ?? defaultSince()\n const until = parseDate(url.searchParams.get('until')) ?? new Date()\n\n const [items, usage, outcomes] = await Promise.all([\n // Items are bounded by integration volume (typically <100 per\n // account); no time-window needed \u2014 they often outlive a single\n // export window and the operator needs to see active items\n // regardless of when they were created.\n em.find(\n BillingItem,\n { tenantId, organizationId, billAccountId: account.id } as never,\n { limit: EXPORT_HARD_LIMIT },\n ),\n em.find(\n BillingAccountUsage,\n {\n tenantId,\n organizationId,\n billAccountId: account.id,\n periodEnd: { $gte: since, $lte: until },\n } as never,\n { limit: EXPORT_HARD_LIMIT, orderBy: { periodEnd: 'DESC' } },\n ),\n em.find(\n BillRunOutcome,\n {\n tenantId,\n organizationId,\n billAccountId: account.id,\n createdAt: { $gte: since, $lte: until },\n } as never,\n { limit: EXPORT_HARD_LIMIT, orderBy: { createdAt: 'DESC' } },\n ),\n ])\n\n // Invoices created in the window. The `metadata->>'bill_account_id'`\n // predicate keeps non-billing core/sales invoices out.\n const invoiceRows = (await em.execute(\n `SELECT id, invoice_number, status, status_entry_id, issue_date, due_date,\n currency_code, subtotal_net_amount, grand_total_net_amount,\n grand_total_gross_amount, paid_total_amount, outstanding_amount,\n metadata, created_at, updated_at, deleted_at\n FROM sales_invoices\n WHERE tenant_id = ?\n AND organization_id = ?\n AND deleted_at IS NULL\n AND metadata->>'bill_account_id' = ?\n AND created_at >= ?\n AND created_at <= ?\n ORDER BY created_at DESC\n LIMIT ?`,\n [tenantId, organizationId, account.id, since, until, EXPORT_HARD_LIMIT],\n )) as unknown as Array<{ id: string }>\n\n const invoiceIds = invoiceRows.map((r) => r.id)\n const draftEdits =\n invoiceIds.length > 0\n ? await em.find(\n DraftInvoiceEdit,\n {\n tenantId,\n organizationId,\n invoiceId: { $in: invoiceIds },\n } as never,\n { limit: EXPORT_HARD_LIMIT, orderBy: { createdAt: 'DESC' } },\n )\n : []\n\n return NextResponse.json({\n exportedAt: new Date().toISOString(),\n schemaVersion: 1,\n tenantId,\n organizationId,\n window: {\n since: since.toISOString(),\n until: until.toISOString(),\n hardLimit: EXPORT_HARD_LIMIT,\n },\n account,\n items,\n usage,\n runOutcomes: outcomes,\n invoices: invoiceRows,\n draftInvoiceEdits: draftEdits,\n })\n } catch (err) {\n console.error('[billing/export/account.GET]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n\n/**\n * Per-collection cap to keep a single export bounded in size and time\n * even when the caller asks for `since=1970-01-01`. The full GDPR\n * lifetime dump for an outlier account requires a follow-up\n * background-job approach (chunked NDJSON to object storage) \u2014 out of\n * scope for the v1 admin export endpoint.\n */\nconst EXPORT_HARD_LIMIT = 50_000\n\n/**\n * Default time window for the export \u2014 last 12 months. Operators\n * pulling for routine GDPR requests almost always want a finite\n * window; the regulator-compliance \"lifetime\" case overrides via\n * `?since=1970-01-01`.\n */\nfunction defaultSince(): Date {\n const d = new Date()\n d.setUTCFullYear(d.getUTCFullYear() - 1)\n return d\n}\n\nfunction parseDate(value: string | null | undefined): Date | null {\n if (!value) return null\n const d = new Date(value)\n return Number.isNaN(d.getTime()) ? null : d\n}\n\nexport const openApi = {\n methods: {\n GET: {\n tags: ['Billing'],\n summary: 'Export a billing account (GDPR portability)',\n parameters: [\n {\n name: 'id',\n in: 'path',\n required: true,\n schema: { type: 'string', format: 'uuid' },\n },\n {\n name: 'since',\n in: 'query',\n required: false,\n description:\n 'ISO timestamp; lower bound for usage / outcomes / invoices windows. Defaults to today \u2212 12 months.',\n schema: { type: 'string', format: 'date-time' },\n },\n {\n name: 'until',\n in: 'query',\n required: false,\n description: 'ISO timestamp; upper bound. Defaults to now.',\n schema: { type: 'string', format: 'date-time' },\n },\n ],\n description:\n \"Returns a JSON dump of a single Billing Account: the account itself, every Billing Item (no time cap \u2014 items often outlive a single export window), and the Usage records / BillRunOutcomes / emitted core/sales invoices / DraftInvoiceEdit rows whose timestamps fall in the [since, until] window (defaults to the last 12 months). Each collection is independently capped by EXPORT_HARD_LIMIT = 50,000 rows. Lifetime dumps for outlier accounts need a follow-up chunked background-job approach \u2014 out of scope for this v1 endpoint.\",\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
const metadata = {
|
|
5
|
+
GET: { requireAuth: true, requireFeatures: ["billing.invoice.view"] }
|
|
6
|
+
};
|
|
7
|
+
async function GET(req, { params }) {
|
|
8
|
+
try {
|
|
9
|
+
const container = await createRequestContainer();
|
|
10
|
+
const auth = await getAuthFromRequest(req);
|
|
11
|
+
if (!auth || !auth.tenantId) {
|
|
12
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
13
|
+
}
|
|
14
|
+
const organizationId = auth.orgId ?? null;
|
|
15
|
+
if (!organizationId) {
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{ error: "Organization scope required" },
|
|
18
|
+
{ status: 403 }
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
const { id: invoiceId } = await params;
|
|
22
|
+
const em = container.resolve("em").fork();
|
|
23
|
+
const invoiceRows = await em.execute(
|
|
24
|
+
`SELECT id, invoice_number, status, status_entry_id, currency_code,
|
|
25
|
+
issue_date, due_date,
|
|
26
|
+
subtotal_net_amount, subtotal_gross_amount,
|
|
27
|
+
discount_total_amount, tax_total_amount,
|
|
28
|
+
grand_total_net_amount, grand_total_gross_amount,
|
|
29
|
+
paid_total_amount, outstanding_amount,
|
|
30
|
+
metadata, created_at, updated_at
|
|
31
|
+
FROM sales_invoices
|
|
32
|
+
WHERE id = ?
|
|
33
|
+
AND tenant_id = ?
|
|
34
|
+
AND organization_id = ?
|
|
35
|
+
AND deleted_at IS NULL
|
|
36
|
+
AND jsonb_exists(metadata, 'bill_run_id')`,
|
|
37
|
+
[invoiceId, auth.tenantId, organizationId]
|
|
38
|
+
);
|
|
39
|
+
if (!invoiceRows || invoiceRows.length === 0) {
|
|
40
|
+
return NextResponse.json({ error: "Invoice not found" }, { status: 404 });
|
|
41
|
+
}
|
|
42
|
+
const invoice = invoiceRows[0];
|
|
43
|
+
const lineRows = await em.execute(
|
|
44
|
+
`SELECT id, line_number, kind, description, quantity, currency_code,
|
|
45
|
+
unit_price_net, unit_price_gross, tax_rate, tax_amount,
|
|
46
|
+
total_net_amount, total_gross_amount, metadata
|
|
47
|
+
FROM sales_invoice_lines
|
|
48
|
+
WHERE invoice_id = ?
|
|
49
|
+
AND tenant_id = ?
|
|
50
|
+
AND organization_id = ?
|
|
51
|
+
ORDER BY line_number ASC`,
|
|
52
|
+
[invoiceId, auth.tenantId, organizationId]
|
|
53
|
+
);
|
|
54
|
+
return NextResponse.json({
|
|
55
|
+
invoice,
|
|
56
|
+
lines: lineRows
|
|
57
|
+
});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error("[billing/invoices/[id].GET]", err);
|
|
60
|
+
return NextResponse.json({ error: "Internal error" }, { status: 500 });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const openApi = {
|
|
64
|
+
methods: {
|
|
65
|
+
GET: {
|
|
66
|
+
tags: ["Billing"],
|
|
67
|
+
summary: "Get a billing-managed invoice (with lines)",
|
|
68
|
+
description: "Returns the `core/sales` invoice + every line, scoped to billing-managed invoices (`metadata.bill_run_id` set). Non-billing invoices return 404 even when the UUID resolves at `/api/sales/invoices/[id]`.",
|
|
69
|
+
parameters: [
|
|
70
|
+
{
|
|
71
|
+
name: "id",
|
|
72
|
+
in: "path",
|
|
73
|
+
required: true,
|
|
74
|
+
schema: { type: "string", format: "uuid" }
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
export {
|
|
81
|
+
GET,
|
|
82
|
+
metadata,
|
|
83
|
+
openApi
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/billing/api/invoices/%5Bid%5D/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'\n\n/**\n * `GET /api/billing/invoices/[id]` \u2014 billing-invoice detail.\n *\n * Returns the `core/sales` invoice row + its lines, but only if the\n * invoice is billing-managed (carries `metadata.bill_run_id`). Other\n * `core/sales` invoices return 404 here even though the same UUID\n * resolves at `/api/sales/invoices/[id]` \u2014 this endpoint is the\n * billing operator's surface, scoped to the billing universe.\n */\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.invoice.view'] },\n}\n\ntype InvoiceRow = {\n id: string\n invoice_number: string\n status: string | null\n status_entry_id: string | null\n currency_code: string\n issue_date: string | null\n due_date: string | null\n subtotal_net_amount: string\n subtotal_gross_amount: string\n discount_total_amount: string\n tax_total_amount: string\n grand_total_net_amount: string\n grand_total_gross_amount: string\n paid_total_amount: string\n outstanding_amount: string\n metadata: Record<string, unknown> | null\n created_at: string\n updated_at: string\n}\n\ntype InvoiceLineRow = {\n id: string\n line_number: number\n kind: string\n description: string | null\n quantity: string\n currency_code: string\n unit_price_net: string\n unit_price_gross: string\n tax_rate: string\n tax_amount: string\n total_net_amount: string\n total_gross_amount: string\n metadata: Record<string, unknown> | null\n}\n\nexport async function GET(\n req: Request,\n { params }: { params: Promise<{ id: string }> },\n) {\n try {\n const 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(\n { error: 'Organization scope required' },\n { status: 403 },\n )\n }\n\n const { id: invoiceId } = await params\n const em = (container.resolve('em') as EntityManager).fork()\n\n const invoiceRows = (await em.execute(\n `SELECT id, invoice_number, status, status_entry_id, currency_code,\n issue_date, due_date,\n subtotal_net_amount, subtotal_gross_amount,\n discount_total_amount, tax_total_amount,\n grand_total_net_amount, grand_total_gross_amount,\n paid_total_amount, outstanding_amount,\n metadata, created_at, updated_at\n FROM sales_invoices\n WHERE id = ?\n AND tenant_id = ?\n AND organization_id = ?\n AND deleted_at IS NULL\n AND jsonb_exists(metadata, 'bill_run_id')`,\n [invoiceId, auth.tenantId, organizationId],\n )) as unknown as InvoiceRow[]\n\n if (!invoiceRows || invoiceRows.length === 0) {\n return NextResponse.json({ error: 'Invoice not found' }, { status: 404 })\n }\n\n const invoice = invoiceRows[0]\n\n const lineRows = (await em.execute(\n `SELECT id, line_number, kind, description, quantity, currency_code,\n unit_price_net, unit_price_gross, tax_rate, tax_amount,\n total_net_amount, total_gross_amount, metadata\n FROM sales_invoice_lines\n WHERE invoice_id = ?\n AND tenant_id = ?\n AND organization_id = ?\n ORDER BY line_number ASC`,\n [invoiceId, auth.tenantId, organizationId],\n )) as unknown as InvoiceLineRow[]\n\n return NextResponse.json({\n invoice,\n lines: lineRows,\n })\n } catch (err) {\n console.error('[billing/invoices/[id].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 billing-managed invoice (with lines)',\n description:\n 'Returns the `core/sales` invoice + every line, scoped to ' +\n \"billing-managed invoices (`metadata.bill_run_id` set). Non-\" +\n 'billing invoices return 404 even when the UUID resolves at ' +\n '`/api/sales/invoices/[id]`.',\n parameters: [\n {\n name: 'id',\n in: 'path',\n required: true,\n schema: { type: 'string', format: 'uuid' },\n },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AAY5B,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,sBAAsB,EAAE;AACtE;AAuCA,eAAsB,IACpB,KACA,EAAE,OAAO,GACT;AACA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,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;AAAA,QAClB,EAAE,OAAO,8BAA8B;AAAA,QACvC,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAEA,UAAM,EAAE,IAAI,UAAU,IAAI,MAAM;AAChC,UAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAE3D,UAAM,cAAe,MAAM,GAAG;AAAA,MAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAaA,CAAC,WAAW,KAAK,UAAU,cAAc;AAAA,IAC3C;AAEA,QAAI,CAAC,eAAe,YAAY,WAAW,GAAG;AAC5C,aAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC1E;AAEA,UAAM,UAAU,YAAY,CAAC;AAE7B,UAAM,WAAY,MAAM,GAAG;AAAA,MACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQA,CAAC,WAAW,KAAK,UAAU,cAAc;AAAA,IAC3C;AAEA,WAAO,aAAa,KAAK;AAAA,MACvB;AAAA,MACA,OAAO;AAAA,IACT,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,YAAQ,MAAM,+BAA+B,GAAG;AAChD,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,aACE;AAAA,MAIF,YAAY;AAAA,QACV;AAAA,UACE,MAAM;AAAA,UACN,IAAI;AAAA,UACJ,UAAU;AAAA,UACV,QAAQ,EAAE,MAAM,UAAU,QAAQ,OAAO;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|