@dainamite/billing 0.18.0 → 0.20.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +14 -0
  3. package/dist/modules/billing/api/export/account/[id]/route.js +5 -5
  4. package/dist/modules/billing/api/export/account/[id]/route.js.map +1 -1
  5. package/dist/modules/billing/api/invoices/add-line/route.js +1 -1
  6. package/dist/modules/billing/api/invoices/add-line/route.js.map +1 -1
  7. package/dist/modules/billing/api/invoices/edit-line/route.js +1 -1
  8. package/dist/modules/billing/api/invoices/edit-line/route.js.map +1 -1
  9. package/dist/modules/billing/api/invoices/remove-line/route.js +1 -1
  10. package/dist/modules/billing/api/invoices/remove-line/route.js.map +1 -1
  11. package/dist/modules/billing/api/run-outcomes/route.js +2 -2
  12. package/dist/modules/billing/api/run-outcomes/route.js.map +2 -2
  13. package/dist/modules/billing/api/runs/retry-failed/route.js +2 -2
  14. package/dist/modules/billing/api/runs/retry-failed/route.js.map +2 -2
  15. package/dist/modules/billing/api/runs/route.js +2 -2
  16. package/dist/modules/billing/api/runs/route.js.map +2 -2
  17. package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +1 -1
  18. package/dist/modules/billing/backend/billing/accounts/page.js.map +1 -1
  19. package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +1 -1
  20. package/dist/modules/billing/backend/billing/invoices/page.js.map +1 -1
  21. package/dist/modules/billing/backend/billing/items/page.js.map +1 -1
  22. package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +1 -1
  23. package/dist/modules/billing/backend/billing/runs/page.js.map +1 -1
  24. package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +1 -1
  25. package/dist/modules/billing/backend/billing/statements/page.js.map +1 -1
  26. package/dist/modules/billing/backend/billing/topups/page.js.map +1 -1
  27. package/dist/modules/billing/backend/billing/transactions/page.js.map +1 -1
  28. package/dist/modules/billing/ce.js.map +2 -2
  29. package/dist/modules/billing/commands/invoices.js +4 -4
  30. package/dist/modules/billing/commands/invoices.js.map +2 -2
  31. package/dist/modules/billing/commands/runs.js +3 -3
  32. package/dist/modules/billing/commands/runs.js.map +2 -2
  33. package/dist/modules/billing/components/PrepaidPanel.js.map +1 -1
  34. package/dist/modules/billing/data/entities.js +56 -56
  35. package/dist/modules/billing/data/entities.js.map +2 -2
  36. package/dist/modules/billing/data/entityIds.js.map +2 -2
  37. package/dist/modules/billing/events.js +20 -0
  38. package/dist/modules/billing/events.js.map +2 -2
  39. package/dist/modules/billing/lib/billRunEngine.js +4 -4
  40. package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
  41. package/dist/modules/billing/lib/billRunLabel.js.map +1 -1
  42. package/dist/modules/billing/lib/prepaidPeriodClose.js.map +1 -1
  43. package/dist/modules/billing/lib/tenantLock.js.map +1 -1
  44. package/dist/modules/billing/workers/reap-stale-bill-runs.js +2 -2
  45. package/dist/modules/billing/workers/reap-stale-bill-runs.js.map +2 -2
  46. package/generated/entities/billing_account/index.ts +23 -0
  47. package/generated/entities/billing_account_balance/index.ts +13 -0
  48. package/generated/entities/billing_account_transaction/index.ts +21 -0
  49. package/generated/entities/billing_account_usage/index.ts +18 -0
  50. package/generated/entities/billing_draft_invoice_edit/index.ts +14 -0
  51. package/generated/entities/billing_item/index.ts +22 -0
  52. package/generated/entities/billing_run/index.ts +21 -0
  53. package/generated/entities/billing_run_outcome/index.ts +17 -0
  54. package/generated/entities/billing_statement/index.ts +21 -0
  55. package/generated/entities/billing_topup/index.ts +20 -0
  56. package/generated/entities.ids.generated.ts +28 -0
  57. package/package.json +11 -10
  58. package/src/modules/billing/__tests__/commands/invoices.test.ts +8 -8
  59. package/src/modules/billing/__tests__/commands/topups.test.ts +1 -1
  60. package/src/modules/billing/__tests__/commands/transactions.test.ts +25 -2
  61. package/src/modules/billing/__tests__/entityRegistry.test.ts +319 -0
  62. package/src/modules/billing/__tests__/events.test.ts +142 -0
  63. package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +3 -3
  64. package/src/modules/billing/api/export/account/[id]/route.ts +7 -7
  65. package/src/modules/billing/api/invoices/add-line/route.ts +1 -1
  66. package/src/modules/billing/api/invoices/edit-line/route.ts +2 -2
  67. package/src/modules/billing/api/invoices/remove-line/route.ts +1 -1
  68. package/src/modules/billing/api/run-outcomes/route.ts +2 -2
  69. package/src/modules/billing/api/runs/retry-failed/route.ts +2 -2
  70. package/src/modules/billing/api/runs/route.ts +2 -2
  71. package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +1 -1
  72. package/src/modules/billing/backend/billing/accounts/page.tsx +1 -1
  73. package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +1 -1
  74. package/src/modules/billing/backend/billing/invoices/page.tsx +1 -1
  75. package/src/modules/billing/backend/billing/items/page.tsx +1 -1
  76. package/src/modules/billing/backend/billing/runs/[id]/page.tsx +1 -1
  77. package/src/modules/billing/backend/billing/runs/page.tsx +1 -1
  78. package/src/modules/billing/backend/billing/statements/[id]/page.tsx +1 -1
  79. package/src/modules/billing/backend/billing/statements/page.tsx +1 -1
  80. package/src/modules/billing/backend/billing/topups/page.tsx +1 -1
  81. package/src/modules/billing/backend/billing/transactions/page.tsx +1 -1
  82. package/src/modules/billing/ce.ts +8 -7
  83. package/src/modules/billing/commands/invoices.ts +9 -9
  84. package/src/modules/billing/commands/runs.ts +3 -3
  85. package/src/modules/billing/components/PrepaidPanel.tsx +1 -1
  86. package/src/modules/billing/data/entities.ts +31 -10
  87. package/src/modules/billing/data/entityIds.ts +16 -11
  88. package/src/modules/billing/events.ts +26 -0
  89. package/src/modules/billing/lib/billRunEngine.ts +12 -12
  90. package/src/modules/billing/lib/billRunLabel.ts +1 -1
  91. package/src/modules/billing/lib/prepaidPeriodClose.ts +3 -3
  92. package/src/modules/billing/lib/tenantLock.ts +1 -1
  93. package/src/modules/billing/workers/reap-stale-bill-runs.ts +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,105 @@
1
1
  # @dainamite/billing
2
2
 
3
+ ## 0.20.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 924c8b6: Ship the entity-id registry and declare the CRUD events, so billing records are
8
+ indexed on Open Mercato 0.9.
9
+
10
+ **What was broken.** A standalone app's `mercato generate` found no billing
11
+ entity ids: the package shipped no `generated/entities.ids.generated.*`, and the
12
+ compiled `dist/.../data/entities.js` exports its classes with `export { … }`,
13
+ which the CLI's class scanner does not see. The app's registry therefore held
14
+ `"billing": {}`, and:
15
+
16
+ - every billing account, item, top-up, usage record and balance transaction
17
+ written through a command was saved but not indexed. Each write logged
18
+ `QueryIndexScopeError: Query index entity type is not registered`, added a row
19
+ to `indexer_error_logs`, and wrote no `entity_indexes` row;
20
+ - `mercato entities install` (or `mercato init` with examples) registered the
21
+ billing ids as virtual custom entities, after which `GET /api/billing/*`
22
+ returned no rows;
23
+ - the CRUD events (`billing.account.created`, …) logged "Emitting undeclared
24
+ event".
25
+
26
+ **What changed.**
27
+
28
+ - The package now ships `generated/entities.ids.generated.ts` (all 10
29
+ `billing:*` ids) plus one column file per entity under
30
+ `generated/entities/<key>/`. The build generates them from
31
+ `data/entityIds.ts` and `data/entities.ts`
32
+ (`scripts/generate-entity-registry.mjs`), and a unit test fails if the
33
+ committed files drift.
34
+ - `events.ts` declares the CRUD events the commands emit:
35
+ `billing.account.{created,updated,deleted}`,
36
+ `billing.item.{created,updated,deleted}`, `billing.topup.created`,
37
+ `billing.transaction.created` and `billing.usage.created`. Like
38
+ `billing.usage.rated`, `billing.usage.created` fires once per usage upload and
39
+ is excluded from workflow triggers.
40
+ - **Renamed entity classes:** `BillRun` → `BillingRun`, `BillRunOutcome` →
41
+ `BillingRunOutcome`, `DraftInvoiceEdit` → `BillingDraftInvoiceEdit`. Open
42
+ Mercato maps an entity id to its table by class name, and its table-name
43
+ fallback never matches on 0.9 with MikroORM 7. So `billing:billing_run`,
44
+ `billing:billing_run_outcome` and `billing:billing_draft_invoice_edit`
45
+ resolved to no table. Tables, columns, ids, API routes and OpenAPI schema
46
+ names are unchanged; no migration. The old names remain as deprecated
47
+ **type-only** aliases. Code that used them as values (`em.find(BillRun, …)`)
48
+ must switch to the new names.
49
+
50
+ **After upgrading:**
51
+
52
+ 1. Run `yarn generate`, then restart the app. `.mercato/generated/entities.ids.generated.ts`
53
+ should list the 10 `billing:*` ids.
54
+ 2. Index the rows written while the ids were unregistered:
55
+ `yarn mercato query_index reindex --entity billing:billing_account` (add
56
+ `--tenant <id>` to limit it to one tenant), then the same for
57
+ `billing:billing_item`, `billing:billing_account_usage`,
58
+ `billing:billing_account_transaction` and `billing:billing_topup`.
59
+ 3. If you ran `mercato entities install` (or `mercato init` with examples) on an
60
+ affected version, the billing lists recover on their own once the ids are
61
+ registered. To remove the leftover virtual-entity rows, run:
62
+
63
+ ```sql
64
+ update custom_entities set is_active = false where entity_id like 'billing:%';
65
+ ```
66
+
67
+ - d7ce539: Require Open Mercato `0.9.0`. Peer ranges move from `^0.6.7` to `^0.9.0` for
68
+ `@open-mercato/core`, `shared`, `ui`, `events` and `queue`, and the MikroORM
69
+ peers from `^7.1.5` to `^7.1.14`. Consumers on Open Mercato `0.6.x`–`0.8.x`
70
+ must upgrade before taking these versions.
71
+
72
+ `@dainamite/cpq` and `@dainamite/billing` now peer on `@tanstack/react-table`
73
+ `^9.0.0` (was `^8.0.0 || ^9.0.0`): Open Mercato 0.7 moved its `DataTable` to
74
+ TanStack Table v9, and list pages now import `LegacyColumnDef` from
75
+ `@tanstack/react-table/legacy`, which v8 does not have.
76
+
77
+ `@dainamite/cpq` also fixes CPQ quote and order API routes answering `500`
78
+ instead of the service's typed status (e.g. `404 Quote not found`). Next
79
+ bundles the package once per server layer and the DI container is shared, so
80
+ the route could catch a `QuotingError` / `OrderError` from the other copy of
81
+ the class and `instanceof` failed. Both classes now carry a `Symbol.for`
82
+ brand; new `isQuotingError` / `isOrderError` guards are exported alongside
83
+ them and should be used instead of `instanceof` in route handlers.
84
+
85
+ ## 0.19.0
86
+
87
+ ### Minor Changes
88
+
89
+ - 8834b8b: Require Open Mercato `0.6.7`. Peer ranges move from `^0.6.0` to `^0.6.7` for
90
+ `@open-mercato/core`, `shared`, `ui`, `events` and `queue`, and the MikroORM
91
+ peers from `^7.0.0` to `^7.1.5` (7.1 split the SQL layer into
92
+ `@mikro-orm/sql`, so a shared 7.1 instance is required).
93
+
94
+ No package source changes — this only narrows the supported framework range.
95
+ Consumers on Open Mercato `0.6.0`–`0.6.6` must upgrade before taking these
96
+ versions.
97
+
98
+ Note for consumers running commands with a system identity (`ctx.auth: null`):
99
+ `0.6.7` hardened `ensureTenantScope` / `ensureOrganizationScope`. Those calls
100
+ are still no-ops with `auth: null`, but an unscoped organization command now
101
+ logs a warning and will throw `403` if `OM_ENFORCE_ORG_SCOPE_STRICT=true`.
102
+
3
103
  ## 0.18.0
4
104
 
5
105
  ### Minor Changes
package/README.md CHANGED
@@ -71,6 +71,20 @@ Items automatically. Register it **after** both `cpq` and `billing`.
71
71
  a direct relation.
72
72
  - **Ships its own migrations** under `src/modules/billing/migrations/`.
73
73
  - Ships `src/` (types) + `dist/` (runtime JS); no `.d.ts` emitted.
74
+ - **Ships its entity-id registry** in `generated/` (see below).
75
+
76
+ ## Entity ids and `generated/`
77
+
78
+ A host app's `yarn generate` registers billing's entity ids (`billing:billing_account`,
79
+ …) from `generated/entities.ids.generated.ts`, which ships with the package. The
80
+ query index rejects unregistered ids, and `mercato entities install` treats them
81
+ as virtual custom entities, so **run `yarn generate` after every upgrade**.
82
+
83
+ `generated/` is derived. Never edit it by hand. The build regenerates it from
84
+ `src/modules/billing/data/entityIds.ts` (ids) and `data/entities.ts` (columns),
85
+ using `scripts/generate-entity-registry.mjs`. Commit the result; a unit test
86
+ fails when it drifts. Each id suffix must be the snake_case of its entity class
87
+ name (`billing:billing_run` → `BillingRun`), or the build fails.
74
88
 
75
89
  ## License
76
90
 
@@ -9,8 +9,8 @@ import {
9
9
  BillingItem,
10
10
  BillingStatement,
11
11
  BillingTopup,
12
- BillRunOutcome,
13
- DraftInvoiceEdit
12
+ BillingRunOutcome,
13
+ BillingDraftInvoiceEdit
14
14
  } from "../../../../data/entities.js";
15
15
  const metadata = {
16
16
  GET: { requireAuth: true, requireFeatures: ["billing.account.view"] }
@@ -66,7 +66,7 @@ async function GET(req, { params }) {
66
66
  { limit: EXPORT_HARD_LIMIT, orderBy: { periodEnd: "DESC" } }
67
67
  ),
68
68
  em.find(
69
- BillRunOutcome,
69
+ BillingRunOutcome,
70
70
  {
71
71
  tenantId,
72
72
  organizationId,
@@ -129,7 +129,7 @@ async function GET(req, { params }) {
129
129
  );
130
130
  const invoiceIds = invoiceRows.map((r) => r.id);
131
131
  const draftEdits = invoiceIds.length > 0 ? await em.find(
132
- DraftInvoiceEdit,
132
+ BillingDraftInvoiceEdit,
133
133
  {
134
134
  tenantId,
135
135
  organizationId,
@@ -203,7 +203,7 @@ const openApi = {
203
203
  schema: { type: "string", format: "date-time" }
204
204
  }
205
205
  ],
206
- 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."
206
+ 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 / BillingDraftInvoiceEdit 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."
207
207
  }
208
208
  }
209
209
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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 BillingAccountBalance,\n BillingAccountTransaction,\n BillingAccountUsage,\n BillingItem,\n BillingStatement,\n BillingTopup,\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, balance, transactions, topups, statements] = 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 // SPEC-002 prepaid data \u2014 the balance row + the windowed money trail.\n em.findOne(BillingAccountBalance, {\n tenantId,\n billAccountId: account.id,\n } as never),\n em.find(\n BillingAccountTransaction,\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 em.find(\n BillingTopup,\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 em.find(\n BillingStatement,\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 ])\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 // v2 adds prepaid balance / transactions / top-ups / statements (SPEC-002).\n schemaVersion: 2,\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 // SPEC-002 prepaid section.\n balance,\n transactions,\n topups,\n statements,\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"],
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 BillingAccountBalance,\n BillingAccountTransaction,\n BillingAccountUsage,\n BillingItem,\n BillingStatement,\n BillingTopup,\n BillingRunOutcome,\n BillingDraftInvoiceEdit,\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 `BillingRunOutcome` row referencing the account.\n * - Every `core/sales` invoice the engine emitted for the account\n * (matched via `metadata->>'bill_account_id'`).\n * - Every `BillingDraftInvoiceEdit` 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, balance, transactions, topups, statements] = 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 BillingRunOutcome,\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 // SPEC-002 prepaid data \u2014 the balance row + the windowed money trail.\n em.findOne(BillingAccountBalance, {\n tenantId,\n billAccountId: account.id,\n } as never),\n em.find(\n BillingAccountTransaction,\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 em.find(\n BillingTopup,\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 em.find(\n BillingStatement,\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 ])\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 BillingDraftInvoiceEdit,\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 // v2 adds prepaid balance / transactions / top-ups / statements (SPEC-002).\n schemaVersion: 2,\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 // SPEC-002 prepaid section.\n balance,\n transactions,\n topups,\n statements,\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 / BillingDraftInvoiceEdit 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
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -58,7 +58,7 @@ const openApi = createBillingCrudOpenApi({
58
58
  create: {
59
59
  schema: billingInvoiceAddLineSchema,
60
60
  responseSchema: addLineResponseSchema,
61
- description: "Appends a new line to a draft invoice (e.g. operator adds a one-off adjustment). Writes a `DraftInvoiceEdit` audit row (action=`line_added`, `before_json=null`). Recomputes invoice totals. Refuses non-draft invoices."
61
+ description: "Appends a new line to a draft invoice (e.g. operator adds a one-off adjustment). Writes a `BillingDraftInvoiceEdit` audit row (action=`line_added`, `before_json=null`). Recomputes invoice totals. Refuses non-draft invoices."
62
62
  }
63
63
  });
64
64
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/billing/api/invoices/add-line/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 { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'\nimport { billingEntityIds } from '../../../data/entityIds'\nimport { billingInvoiceAddLineSchema } from '../../../data/validators'\nimport { createBillingCrudOpenApi } from '../../openapi'\n\nconst routeMetadata = {\n POST: { requireAuth: true, requireFeatures: ['billing.invoice.edit_draft'] },\n}\n\nexport const metadata = routeMetadata\n\nconst addLineResponseSchema = z.object({\n invoiceId: z.string().uuid(),\n invoiceLineId: z.string().uuid(),\n auditId: z.string().uuid(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: SalesInvoice,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: z.object({}).passthrough(),\n entityId: billingEntityIds.draftInvoiceEdit,\n fields: ['id'],\n buildFilters: async () => ({}),\n },\n actions: {\n create: {\n commandId: 'billing.invoices.add_draft_line',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingInvoiceAddLineSchema.parse(scoped)\n },\n response: ({ result }) => ({\n invoiceId: result?.invoiceId ?? null,\n invoiceLineId: result?.invoiceLineId ?? null,\n auditId: result?.auditId ?? null,\n }),\n status: 201,\n },\n },\n})\n\nexport const POST = crud.POST\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingInvoiceLineAdd',\n pluralName: 'BillingInvoiceLineAdds',\n querySchema: z.object({}).passthrough(),\n listResponseSchema: z.object({ items: z.array(z.unknown()) }),\n create: {\n schema: billingInvoiceAddLineSchema,\n responseSchema: addLineResponseSchema,\n description:\n 'Appends a new line to a draft invoice (e.g. operator adds a ' +\n 'one-off adjustment). Writes a `DraftInvoiceEdit` audit row ' +\n '(action=`line_added`, `before_json=null`). Recomputes invoice ' +\n 'totals. Refuses non-draft invoices.',\n },\n})\n"],
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 { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'\nimport { billingEntityIds } from '../../../data/entityIds'\nimport { billingInvoiceAddLineSchema } from '../../../data/validators'\nimport { createBillingCrudOpenApi } from '../../openapi'\n\nconst routeMetadata = {\n POST: { requireAuth: true, requireFeatures: ['billing.invoice.edit_draft'] },\n}\n\nexport const metadata = routeMetadata\n\nconst addLineResponseSchema = z.object({\n invoiceId: z.string().uuid(),\n invoiceLineId: z.string().uuid(),\n auditId: z.string().uuid(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: SalesInvoice,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: z.object({}).passthrough(),\n entityId: billingEntityIds.draftInvoiceEdit,\n fields: ['id'],\n buildFilters: async () => ({}),\n },\n actions: {\n create: {\n commandId: 'billing.invoices.add_draft_line',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingInvoiceAddLineSchema.parse(scoped)\n },\n response: ({ result }) => ({\n invoiceId: result?.invoiceId ?? null,\n invoiceLineId: result?.invoiceLineId ?? null,\n auditId: result?.auditId ?? null,\n }),\n status: 201,\n },\n },\n})\n\nexport const POST = crud.POST\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingInvoiceLineAdd',\n pluralName: 'BillingInvoiceLineAdds',\n querySchema: z.object({}).passthrough(),\n listResponseSchema: z.object({ items: z.array(z.unknown()) }),\n create: {\n schema: billingInvoiceAddLineSchema,\n responseSchema: addLineResponseSchema,\n description:\n 'Appends a new line to a draft invoice (e.g. operator adds a ' +\n 'one-off adjustment). Writes a `BillingDraftInvoiceEdit` audit row ' +\n '(action=`line_added`, `before_json=null`). Recomputes invoice ' +\n 'totals. Refuses non-draft invoices.',\n },\n})\n"],
5
5
  "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AACjC,SAAS,mCAAmC;AAC5C,SAAS,gCAAgC;AAEzC,MAAM,gBAAgB;AAAA,EACpB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEO,MAAM,WAAW;AAExB,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,WAAW,EAAE,OAAO,EAAE,KAAK;AAAA,EAC3B,eAAe,EAAE,OAAO,EAAE,KAAK;AAAA,EAC/B,SAAS,EAAE,OAAO,EAAE,KAAK;AAC3B,CAAC;AAED,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAE/C,MAAM,OAAO,cAAc;AAAA,EACzB,UAAU;AAAA,EACV,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB;AAAA,EACA,MAAM;AAAA,IACJ,QAAQ,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAAA,IACjC,UAAU,iBAAiB;AAAA,IAC3B,QAAQ,CAAC,IAAI;AAAA,IACb,cAAc,aAAa,CAAC;AAAA,EAC9B;AAAA,EACA,SAAS;AAAA,IACP,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,OAAO,EAAE,KAAK,IAAI,MAAM;AAChC,cAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,cAAM,SAAS,kBAAkB,OAAO,CAAC,GAAG,KAAK,SAAS;AAC1D,eAAO,4BAA4B,MAAM,MAAM;AAAA,MACjD;AAAA,MACA,UAAU,CAAC,EAAE,OAAO,OAAO;AAAA,QACzB,WAAW,QAAQ,aAAa;AAAA,QAChC,eAAe,QAAQ,iBAAiB;AAAA,QACxC,SAAS,QAAQ,WAAW;AAAA,MAC9B;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,EACF;AACF,CAAC;AAEM,MAAM,OAAO,KAAK;AAElB,MAAM,UAAU,yBAAyB;AAAA,EAC9C,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,aAAa,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAAA,EACtC,oBAAoB,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;AAAA,EAC5D,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,aACE;AAAA,EAIJ;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -62,7 +62,7 @@ const openApi = createBillingCrudOpenApi({
62
62
  create: {
63
63
  schema: billingInvoiceEditLineSchema,
64
64
  responseSchema: editLineResponseSchema,
65
- description: "Edits one line of a draft invoice (description, quantity, unit_price, or explicit `totalNetAmount` override). Writes a `DraftInvoiceEdit` audit row with before+after snapshots and recomputes invoice totals. Refuses non-draft invoices with HTTP 409 + `billing.invoice.not_draft`."
65
+ description: "Edits one line of a draft invoice (description, quantity, unit_price, or explicit `totalNetAmount` override). Writes a `BillingDraftInvoiceEdit` audit row with before+after snapshots and recomputes invoice totals. Refuses non-draft invoices with HTTP 409 + `billing.invoice.not_draft`."
66
66
  }
67
67
  });
68
68
  const _phaseSchemaRefs = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/billing/api/invoices/edit-line/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 { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'\nimport { billingEntityIds } from '../../../data/entityIds'\nimport {\n billingInvoiceAddLineSchema,\n billingInvoiceEditLineSchema,\n billingInvoiceRemoveLineSchema,\n} from '../../../data/validators'\nimport { createBillingCrudOpenApi } from '../../openapi'\n\n/**\n * `POST /api/billing/invoices/edit-line` \u2014 edits one line of a draft\n * invoice. Refuses non-drafts (409 / `billing.invoice.not_draft`).\n * Writes a `DraftInvoiceEdit` audit row per spec. Recomputes invoice\n * totals from the surviving lines after the edit.\n *\n * Operator surface: route is gated by `billing.invoice.edit_draft`.\n * Future Phase 4c admin UI calls into the same command via this route.\n */\n\nconst routeMetadata = {\n POST: { requireAuth: true, requireFeatures: ['billing.invoice.edit_draft'] },\n}\n\nexport const metadata = routeMetadata\n\nconst editLineResponseSchema = z.object({\n invoiceId: z.string().uuid(),\n invoiceLineId: z.string().uuid(),\n auditId: z.string().uuid(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: SalesInvoice,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: z.object({}).passthrough(),\n entityId: billingEntityIds.draftInvoiceEdit,\n fields: ['id'],\n buildFilters: async () => ({}),\n },\n actions: {\n create: {\n commandId: 'billing.invoices.edit_draft_line',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingInvoiceEditLineSchema.parse(scoped)\n },\n response: ({ result }) => ({\n invoiceId: result?.invoiceId ?? null,\n invoiceLineId: result?.invoiceLineId ?? null,\n auditId: result?.auditId ?? null,\n }),\n status: 200,\n },\n },\n})\n\nexport const POST = crud.POST\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingInvoiceLineEdit',\n pluralName: 'BillingInvoiceLineEdits',\n querySchema: z.object({}).passthrough(),\n listResponseSchema: z.object({ items: z.array(z.unknown()) }),\n create: {\n schema: billingInvoiceEditLineSchema,\n responseSchema: editLineResponseSchema,\n description:\n 'Edits one line of a draft invoice (description, quantity, ' +\n \"unit_price, or explicit `totalNetAmount` override). Writes a \" +\n '`DraftInvoiceEdit` audit row with before+after snapshots and ' +\n 'recomputes invoice totals. Refuses non-draft invoices with ' +\n 'HTTP 409 + `billing.invoice.not_draft`.',\n },\n})\n\n// Re-export the add/remove route schemas so the OpenAPI bundle keeps\n// them discoverable from a single import surface \u2014 the dedicated\n// routes (sibling folders) own the actual handlers.\nexport const _phaseSchemaRefs = {\n add: billingInvoiceAddLineSchema,\n remove: billingInvoiceRemoveLineSchema,\n}\n"],
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 { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'\nimport { billingEntityIds } from '../../../data/entityIds'\nimport {\n billingInvoiceAddLineSchema,\n billingInvoiceEditLineSchema,\n billingInvoiceRemoveLineSchema,\n} from '../../../data/validators'\nimport { createBillingCrudOpenApi } from '../../openapi'\n\n/**\n * `POST /api/billing/invoices/edit-line` \u2014 edits one line of a draft\n * invoice. Refuses non-drafts (409 / `billing.invoice.not_draft`).\n * Writes a `BillingDraftInvoiceEdit` audit row per spec. Recomputes invoice\n * totals from the surviving lines after the edit.\n *\n * Operator surface: route is gated by `billing.invoice.edit_draft`.\n * Future Phase 4c admin UI calls into the same command via this route.\n */\n\nconst routeMetadata = {\n POST: { requireAuth: true, requireFeatures: ['billing.invoice.edit_draft'] },\n}\n\nexport const metadata = routeMetadata\n\nconst editLineResponseSchema = z.object({\n invoiceId: z.string().uuid(),\n invoiceLineId: z.string().uuid(),\n auditId: z.string().uuid(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: SalesInvoice,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: z.object({}).passthrough(),\n entityId: billingEntityIds.draftInvoiceEdit,\n fields: ['id'],\n buildFilters: async () => ({}),\n },\n actions: {\n create: {\n commandId: 'billing.invoices.edit_draft_line',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingInvoiceEditLineSchema.parse(scoped)\n },\n response: ({ result }) => ({\n invoiceId: result?.invoiceId ?? null,\n invoiceLineId: result?.invoiceLineId ?? null,\n auditId: result?.auditId ?? null,\n }),\n status: 200,\n },\n },\n})\n\nexport const POST = crud.POST\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingInvoiceLineEdit',\n pluralName: 'BillingInvoiceLineEdits',\n querySchema: z.object({}).passthrough(),\n listResponseSchema: z.object({ items: z.array(z.unknown()) }),\n create: {\n schema: billingInvoiceEditLineSchema,\n responseSchema: editLineResponseSchema,\n description:\n 'Edits one line of a draft invoice (description, quantity, ' +\n \"unit_price, or explicit `totalNetAmount` override). Writes a \" +\n '`BillingDraftInvoiceEdit` audit row with before+after snapshots and ' +\n 'recomputes invoice totals. Refuses non-draft invoices with ' +\n 'HTTP 409 + `billing.invoice.not_draft`.',\n },\n})\n\n// Re-export the add/remove route schemas so the OpenAPI bundle keeps\n// them discoverable from a single import surface \u2014 the dedicated\n// routes (sibling folders) own the actual handlers.\nexport const _phaseSchemaRefs = {\n add: billingInvoiceAddLineSchema,\n remove: billingInvoiceRemoveLineSchema,\n}\n"],
5
5
  "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gCAAgC;AAYzC,MAAM,gBAAgB;AAAA,EACpB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEO,MAAM,WAAW;AAExB,MAAM,yBAAyB,EAAE,OAAO;AAAA,EACtC,WAAW,EAAE,OAAO,EAAE,KAAK;AAAA,EAC3B,eAAe,EAAE,OAAO,EAAE,KAAK;AAAA,EAC/B,SAAS,EAAE,OAAO,EAAE,KAAK;AAC3B,CAAC;AAED,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAE/C,MAAM,OAAO,cAAc;AAAA,EACzB,UAAU;AAAA,EACV,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB;AAAA,EACA,MAAM;AAAA,IACJ,QAAQ,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAAA,IACjC,UAAU,iBAAiB;AAAA,IAC3B,QAAQ,CAAC,IAAI;AAAA,IACb,cAAc,aAAa,CAAC;AAAA,EAC9B;AAAA,EACA,SAAS;AAAA,IACP,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,OAAO,EAAE,KAAK,IAAI,MAAM;AAChC,cAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,cAAM,SAAS,kBAAkB,OAAO,CAAC,GAAG,KAAK,SAAS;AAC1D,eAAO,6BAA6B,MAAM,MAAM;AAAA,MAClD;AAAA,MACA,UAAU,CAAC,EAAE,OAAO,OAAO;AAAA,QACzB,WAAW,QAAQ,aAAa;AAAA,QAChC,eAAe,QAAQ,iBAAiB;AAAA,QACxC,SAAS,QAAQ,WAAW;AAAA,MAC9B;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,EACF;AACF,CAAC;AAEM,MAAM,OAAO,KAAK;AAElB,MAAM,UAAU,yBAAyB;AAAA,EAC9C,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,aAAa,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAAA,EACtC,oBAAoB,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;AAAA,EAC5D,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,aACE;AAAA,EAKJ;AACF,CAAC;AAKM,MAAM,mBAAmB;AAAA,EAC9B,KAAK;AAAA,EACL,QAAQ;AACV;",
6
6
  "names": []
7
7
  }
@@ -58,7 +58,7 @@ const openApi = createBillingCrudOpenApi({
58
58
  create: {
59
59
  schema: billingInvoiceRemoveLineSchema,
60
60
  responseSchema: removeLineResponseSchema,
61
- description: "Removes one line from a draft invoice (hard delete \u2014 the line never existed as far as future readers are concerned). Writes a `DraftInvoiceEdit` audit row (action=`line_removed`, `after_json=null`) carrying the before-snapshot for forensics. Recomputes invoice totals. Refuses non-draft invoices."
61
+ description: "Removes one line from a draft invoice (hard delete \u2014 the line never existed as far as future readers are concerned). Writes a `BillingDraftInvoiceEdit` audit row (action=`line_removed`, `after_json=null`) carrying the before-snapshot for forensics. Recomputes invoice totals. Refuses non-draft invoices."
62
62
  }
63
63
  });
64
64
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/billing/api/invoices/remove-line/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 { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'\nimport { billingEntityIds } from '../../../data/entityIds'\nimport { billingInvoiceRemoveLineSchema } from '../../../data/validators'\nimport { createBillingCrudOpenApi } from '../../openapi'\n\nconst routeMetadata = {\n POST: { requireAuth: true, requireFeatures: ['billing.invoice.edit_draft'] },\n}\n\nexport const metadata = routeMetadata\n\nconst removeLineResponseSchema = z.object({\n invoiceId: z.string().uuid(),\n invoiceLineId: z.string().uuid(),\n auditId: z.string().uuid(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: SalesInvoice,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: z.object({}).passthrough(),\n entityId: billingEntityIds.draftInvoiceEdit,\n fields: ['id'],\n buildFilters: async () => ({}),\n },\n actions: {\n create: {\n commandId: 'billing.invoices.remove_draft_line',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingInvoiceRemoveLineSchema.parse(scoped)\n },\n response: ({ result }) => ({\n invoiceId: result?.invoiceId ?? null,\n invoiceLineId: result?.invoiceLineId ?? null,\n auditId: result?.auditId ?? null,\n }),\n status: 200,\n },\n },\n})\n\nexport const POST = crud.POST\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingInvoiceLineRemove',\n pluralName: 'BillingInvoiceLineRemoves',\n querySchema: z.object({}).passthrough(),\n listResponseSchema: z.object({ items: z.array(z.unknown()) }),\n create: {\n schema: billingInvoiceRemoveLineSchema,\n responseSchema: removeLineResponseSchema,\n description:\n 'Removes one line from a draft invoice (hard delete \u2014 the line ' +\n 'never existed as far as future readers are concerned). Writes ' +\n 'a `DraftInvoiceEdit` audit row (action=`line_removed`, ' +\n '`after_json=null`) carrying the before-snapshot for forensics. ' +\n 'Recomputes invoice totals. Refuses non-draft invoices.',\n },\n})\n"],
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 { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'\nimport { billingEntityIds } from '../../../data/entityIds'\nimport { billingInvoiceRemoveLineSchema } from '../../../data/validators'\nimport { createBillingCrudOpenApi } from '../../openapi'\n\nconst routeMetadata = {\n POST: { requireAuth: true, requireFeatures: ['billing.invoice.edit_draft'] },\n}\n\nexport const metadata = routeMetadata\n\nconst removeLineResponseSchema = z.object({\n invoiceId: z.string().uuid(),\n invoiceLineId: z.string().uuid(),\n auditId: z.string().uuid(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: SalesInvoice,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: z.object({}).passthrough(),\n entityId: billingEntityIds.draftInvoiceEdit,\n fields: ['id'],\n buildFilters: async () => ({}),\n },\n actions: {\n create: {\n commandId: 'billing.invoices.remove_draft_line',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingInvoiceRemoveLineSchema.parse(scoped)\n },\n response: ({ result }) => ({\n invoiceId: result?.invoiceId ?? null,\n invoiceLineId: result?.invoiceLineId ?? null,\n auditId: result?.auditId ?? null,\n }),\n status: 200,\n },\n },\n})\n\nexport const POST = crud.POST\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingInvoiceLineRemove',\n pluralName: 'BillingInvoiceLineRemoves',\n querySchema: z.object({}).passthrough(),\n listResponseSchema: z.object({ items: z.array(z.unknown()) }),\n create: {\n schema: billingInvoiceRemoveLineSchema,\n responseSchema: removeLineResponseSchema,\n description:\n 'Removes one line from a draft invoice (hard delete \u2014 the line ' +\n 'never existed as far as future readers are concerned). Writes ' +\n 'a `BillingDraftInvoiceEdit` audit row (action=`line_removed`, ' +\n '`after_json=null`) carrying the before-snapshot for forensics. ' +\n 'Recomputes invoice totals. Refuses non-draft invoices.',\n },\n})\n"],
5
5
  "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AACjC,SAAS,sCAAsC;AAC/C,SAAS,gCAAgC;AAEzC,MAAM,gBAAgB;AAAA,EACpB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,4BAA4B,EAAE;AAC7E;AAEO,MAAM,WAAW;AAExB,MAAM,2BAA2B,EAAE,OAAO;AAAA,EACxC,WAAW,EAAE,OAAO,EAAE,KAAK;AAAA,EAC3B,eAAe,EAAE,OAAO,EAAE,KAAK;AAAA,EAC/B,SAAS,EAAE,OAAO,EAAE,KAAK;AAC3B,CAAC;AAED,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAE/C,MAAM,OAAO,cAAc;AAAA,EACzB,UAAU;AAAA,EACV,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB;AAAA,EACA,MAAM;AAAA,IACJ,QAAQ,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAAA,IACjC,UAAU,iBAAiB;AAAA,IAC3B,QAAQ,CAAC,IAAI;AAAA,IACb,cAAc,aAAa,CAAC;AAAA,EAC9B;AAAA,EACA,SAAS;AAAA,IACP,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,OAAO,EAAE,KAAK,IAAI,MAAM;AAChC,cAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,cAAM,SAAS,kBAAkB,OAAO,CAAC,GAAG,KAAK,SAAS;AAC1D,eAAO,+BAA+B,MAAM,MAAM;AAAA,MACpD;AAAA,MACA,UAAU,CAAC,EAAE,OAAO,OAAO;AAAA,QACzB,WAAW,QAAQ,aAAa;AAAA,QAChC,eAAe,QAAQ,iBAAiB;AAAA,QACxC,SAAS,QAAQ,WAAW;AAAA,MAC9B;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,EACF;AACF,CAAC;AAEM,MAAM,OAAO,KAAK;AAElB,MAAM,UAAU,yBAAyB;AAAA,EAC9C,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,aAAa,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAAA,EACtC,oBAAoB,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;AAAA,EAC5D,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,aACE;AAAA,EAKJ;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { SalesInvoice } from "@open-mercato/core/modules/sales/data/entities";
3
3
  import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
4
- import { BillingAccount, BillRunOutcome } from "../../data/entities.js";
4
+ import { BillingAccount, BillingRunOutcome } from "../../data/entities.js";
5
5
  import { billingEntityIds } from "../../data/entityIds.js";
6
6
  import {
7
7
  BILL_RUN_OUTCOME_STATUSES
@@ -38,7 +38,7 @@ const billRunOutcomeListItemSchema = z.object({
38
38
  const crud = makeCrudRoute({
39
39
  metadata: routeMetadata,
40
40
  orm: {
41
- entity: BillRunOutcome,
41
+ entity: BillingRunOutcome,
42
42
  idField: "id",
43
43
  orgField: "organizationId",
44
44
  tenantField: "tenantId",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/billing/api/run-outcomes/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { BillingAccount, BillRunOutcome } from '../../data/entities'\nimport { billingEntityIds } from '../../data/entityIds'\nimport {\n BILL_RUN_OUTCOME_STATUSES,\n} from '../../data/validators'\nimport { createBillingCrudOpenApi, createPagedListResponseSchema } from '../openapi'\n\n/**\n * `GET /api/billing/run-outcomes` \u2014 list per-account outcomes,\n * typically filtered by `billRunId` to populate the \"show me what\n * happened in this run\" detail panel.\n *\n * Read-only; outcomes are written by the engine itself, never by API\n * clients.\n */\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.run.view'] },\n}\n\nexport const metadata = routeMetadata\n\nconst listQuerySchema = z\n .object({\n page: z.coerce.number().int().min(1).default(1),\n pageSize: z.coerce.number().int().min(1).max(100).default(50),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n billRunId: z.string().uuid().optional(),\n billAccountId: z.string().uuid().optional(),\n status: z.enum(BILL_RUN_OUTCOME_STATUSES).optional(),\n })\n .passthrough()\n\nconst billRunOutcomeListItemSchema = z.object({\n id: z.string().uuid(),\n billRunId: z.string().uuid(),\n billAccountId: z.string().uuid(),\n status: z.string(),\n errorMessage: z.string().nullable(),\n warnings: z.record(z.string(), z.unknown()).nullable(),\n draftInvoiceId: z.string().uuid().nullable(),\n bill_account_name: z.string().nullable(),\n invoice_number: z.string().nullable(),\n organizationId: z.string().uuid(),\n tenantId: z.string().uuid(),\n createdAt: z.string(),\n updatedAt: z.string(),\n})\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillRunOutcome,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: listQuerySchema,\n entityId: billingEntityIds.runOutcome,\n fields: [\n 'id',\n 'bill_run_id',\n 'bill_account_id',\n 'status',\n 'error_message',\n 'warnings',\n 'draft_invoice_id',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n createdAt: 'created_at',\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n if (typeof query.billRunId === 'string') filters.bill_run_id = { $eq: query.billRunId }\n if (typeof query.billAccountId === 'string') {\n filters.bill_account_id = { $eq: query.billAccountId }\n }\n if (typeof query.status === 'string') filters.status = { $eq: query.status }\n return filters\n },\n },\n hooks: {\n // Outcomes reference account + draft invoice by FK id only. The\n // run detail UI wants names \u2014 resolve both in two bounded queries.\n afterList: async (payload: unknown, ctx: unknown) => {\n const items = ((payload as { items?: unknown[] } | null)?.items ?? []) as Array<\n Record<string, unknown>\n >\n if (items.length === 0) return\n const em = (ctx as { container: AwilixContainer }).container.resolve<EntityManager>('em')\n const accountIds = Array.from(\n new Set(\n items\n .map((row) => row.bill_account_id)\n .filter((v): v is string => typeof v === 'string' && v.length > 0),\n ),\n )\n const invoiceIds = Array.from(\n new Set(\n items\n .map((row) => row.draft_invoice_id)\n .filter((v): v is string => typeof v === 'string' && v.length > 0),\n ),\n )\n const [accounts, invoices] = await Promise.all([\n accountIds.length\n ? em.find(BillingAccount, { id: { $in: accountIds } })\n : [],\n invoiceIds.length\n ? em.find(SalesInvoice, { id: { $in: invoiceIds } })\n : [],\n ])\n const accountName = new Map(\n accounts.map((a) => [String(a.id), a.name] as [string, unknown]),\n )\n const invoiceNumber = new Map(\n invoices.map((i) => [String(i.id), i.invoiceNumber] as [string, unknown]),\n )\n for (const row of items) {\n row.bill_account_name = accountName.get(String(row.bill_account_id)) ?? null\n row.invoice_number =\n typeof row.draft_invoice_id === 'string'\n ? invoiceNumber.get(row.draft_invoice_id) ?? null\n : null\n }\n },\n },\n})\n\nexport const GET = crud.GET\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillRunOutcome',\n pluralName: 'BillRunOutcomes',\n querySchema: listQuerySchema,\n listResponseSchema: createPagedListResponseSchema(billRunOutcomeListItemSchema),\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { BillingAccount, BillingRunOutcome } from '../../data/entities'\nimport { billingEntityIds } from '../../data/entityIds'\nimport {\n BILL_RUN_OUTCOME_STATUSES,\n} from '../../data/validators'\nimport { createBillingCrudOpenApi, createPagedListResponseSchema } from '../openapi'\n\n/**\n * `GET /api/billing/run-outcomes` \u2014 list per-account outcomes,\n * typically filtered by `billRunId` to populate the \"show me what\n * happened in this run\" detail panel.\n *\n * Read-only; outcomes are written by the engine itself, never by API\n * clients.\n */\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.run.view'] },\n}\n\nexport const metadata = routeMetadata\n\nconst listQuerySchema = z\n .object({\n page: z.coerce.number().int().min(1).default(1),\n pageSize: z.coerce.number().int().min(1).max(100).default(50),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n billRunId: z.string().uuid().optional(),\n billAccountId: z.string().uuid().optional(),\n status: z.enum(BILL_RUN_OUTCOME_STATUSES).optional(),\n })\n .passthrough()\n\nconst billRunOutcomeListItemSchema = z.object({\n id: z.string().uuid(),\n billRunId: z.string().uuid(),\n billAccountId: z.string().uuid(),\n status: z.string(),\n errorMessage: z.string().nullable(),\n warnings: z.record(z.string(), z.unknown()).nullable(),\n draftInvoiceId: z.string().uuid().nullable(),\n bill_account_name: z.string().nullable(),\n invoice_number: z.string().nullable(),\n organizationId: z.string().uuid(),\n tenantId: z.string().uuid(),\n createdAt: z.string(),\n updatedAt: z.string(),\n})\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillingRunOutcome,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: listQuerySchema,\n entityId: billingEntityIds.runOutcome,\n fields: [\n 'id',\n 'bill_run_id',\n 'bill_account_id',\n 'status',\n 'error_message',\n 'warnings',\n 'draft_invoice_id',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n createdAt: 'created_at',\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n if (typeof query.billRunId === 'string') filters.bill_run_id = { $eq: query.billRunId }\n if (typeof query.billAccountId === 'string') {\n filters.bill_account_id = { $eq: query.billAccountId }\n }\n if (typeof query.status === 'string') filters.status = { $eq: query.status }\n return filters\n },\n },\n hooks: {\n // Outcomes reference account + draft invoice by FK id only. The\n // run detail UI wants names \u2014 resolve both in two bounded queries.\n afterList: async (payload: unknown, ctx: unknown) => {\n const items = ((payload as { items?: unknown[] } | null)?.items ?? []) as Array<\n Record<string, unknown>\n >\n if (items.length === 0) return\n const em = (ctx as { container: AwilixContainer }).container.resolve<EntityManager>('em')\n const accountIds = Array.from(\n new Set(\n items\n .map((row) => row.bill_account_id)\n .filter((v): v is string => typeof v === 'string' && v.length > 0),\n ),\n )\n const invoiceIds = Array.from(\n new Set(\n items\n .map((row) => row.draft_invoice_id)\n .filter((v): v is string => typeof v === 'string' && v.length > 0),\n ),\n )\n const [accounts, invoices] = await Promise.all([\n accountIds.length\n ? em.find(BillingAccount, { id: { $in: accountIds } })\n : [],\n invoiceIds.length\n ? em.find(SalesInvoice, { id: { $in: invoiceIds } })\n : [],\n ])\n const accountName = new Map(\n accounts.map((a) => [String(a.id), a.name] as [string, unknown]),\n )\n const invoiceNumber = new Map(\n invoices.map((i) => [String(i.id), i.invoiceNumber] as [string, unknown]),\n )\n for (const row of items) {\n row.bill_account_name = accountName.get(String(row.bill_account_id)) ?? null\n row.invoice_number =\n typeof row.draft_invoice_id === 'string'\n ? invoiceNumber.get(row.draft_invoice_id) ?? null\n : null\n }\n },\n },\n})\n\nexport const GET = crud.GET\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillRunOutcome',\n pluralName: 'BillRunOutcomes',\n querySchema: listQuerySchema,\n listResponseSchema: createPagedListResponseSchema(billRunOutcomeListItemSchema),\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
3
3
  import { withScopedPayload } from "@open-mercato/shared/lib/api/scoped";
4
4
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
- import { BillRun } from "../../../data/entities.js";
5
+ import { BillingRun } from "../../../data/entities.js";
6
6
  import { billingEntityIds } from "../../../data/entityIds.js";
7
7
  import { billingRunRetryFailedSchema } from "../../../data/validators.js";
8
8
  import { createBillingCrudOpenApi } from "../../openapi.js";
@@ -19,7 +19,7 @@ const rawBodySchema = z.object({}).passthrough();
19
19
  const crud = makeCrudRoute({
20
20
  metadata: routeMetadata,
21
21
  orm: {
22
- entity: BillRun,
22
+ entity: BillingRun,
23
23
  idField: "id",
24
24
  orgField: "organizationId",
25
25
  tenantField: "tenantId",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/billing/api/runs/retry-failed/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 { BillRun } from '../../../data/entities'\nimport { billingEntityIds } from '../../../data/entityIds'\nimport { billingRunRetryFailedSchema } from '../../../data/validators'\nimport { createBillingCrudOpenApi } from '../../openapi'\n\n/**\n * `POST /api/billing/runs/retry-failed`\n *\n * Creates a new Bill Run scoped to the failed-account ids from a parent\n * run, with `parent_run_id` set on the new run so the retry chain is\n * traceable. Returns the new run's id + status + outcomes count.\n *\n * Implemented as a \"create-only\" `makeCrudRoute` so the framework's\n * RBAC + OpenAPI + command-bus plumbing stays consistent with the rest\n * of the billing surface. The list/get half of the factory is never\n * exercised on this URL \u2014 only POST is exported.\n */\n\nconst routeMetadata = {\n POST: { requireAuth: true, requireFeatures: ['billing.run.trigger'] },\n}\n\nexport const metadata = routeMetadata\n\nconst retryResponseSchema = z.object({\n billRunId: z.string().uuid(),\n status: z.string(),\n outcomesCount: z.number().int().nonnegative(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillRun,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n // No `list` \u2014 POST-only route. Keep a stub so the factory type-checks.\n list: {\n schema: z.object({}).passthrough(),\n entityId: billingEntityIds.run,\n fields: ['id'],\n buildFilters: async () => ({}),\n },\n actions: {\n create: {\n commandId: 'billing.runs.retry_failed',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingRunRetryFailedSchema.parse(scoped)\n },\n response: ({ result }) => ({\n billRunId: result?.billRunId ?? null,\n status: result?.status ?? null,\n outcomesCount: result?.outcomesCount ?? 0,\n }),\n status: 201,\n },\n },\n})\n\nexport const POST = crud.POST\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillRunRetry',\n pluralName: 'BillRunRetries',\n querySchema: z.object({}).passthrough(),\n listResponseSchema: z.object({ items: z.array(z.unknown()) }),\n create: {\n schema: billingRunRetryFailedSchema,\n responseSchema: retryResponseSchema,\n description:\n 'Re-runs the failed accounts of an earlier Bill Run. The new run ' +\n 'inherits the parent run\\'s `asOfDate` and `catchUp` flag; its ' +\n '`parent_run_id` points back at the source. Returns 409 if there ' +\n 'are no failed outcomes or if another run is in flight.',\n },\n})\n"],
5
- "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,2BAA2B;AACpC,SAAS,eAAe;AACxB,SAAS,wBAAwB;AACjC,SAAS,mCAAmC;AAC5C,SAAS,gCAAgC;AAezC,MAAM,gBAAgB;AAAA,EACpB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AACtE;AAEO,MAAM,WAAW;AAExB,MAAM,sBAAsB,EAAE,OAAO;AAAA,EACnC,WAAW,EAAE,OAAO,EAAE,KAAK;AAAA,EAC3B,QAAQ,EAAE,OAAO;AAAA,EACjB,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAC9C,CAAC;AAED,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAE/C,MAAM,OAAO,cAAc;AAAA,EACzB,UAAU;AAAA,EACV,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB;AAAA;AAAA,EAEA,MAAM;AAAA,IACJ,QAAQ,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAAA,IACjC,UAAU,iBAAiB;AAAA,IAC3B,QAAQ,CAAC,IAAI;AAAA,IACb,cAAc,aAAa,CAAC;AAAA,EAC9B;AAAA,EACA,SAAS;AAAA,IACP,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,OAAO,EAAE,KAAK,IAAI,MAAM;AAChC,cAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,cAAM,SAAS,kBAAkB,OAAO,CAAC,GAAG,KAAK,SAAS;AAC1D,eAAO,4BAA4B,MAAM,MAAM;AAAA,MACjD;AAAA,MACA,UAAU,CAAC,EAAE,OAAO,OAAO;AAAA,QACzB,WAAW,QAAQ,aAAa;AAAA,QAChC,QAAQ,QAAQ,UAAU;AAAA,QAC1B,eAAe,QAAQ,iBAAiB;AAAA,MAC1C;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,EACF;AACF,CAAC;AAEM,MAAM,OAAO,KAAK;AAElB,MAAM,UAAU,yBAAyB;AAAA,EAC9C,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,aAAa,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAAA,EACtC,oBAAoB,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;AAAA,EAC5D,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,aACE;AAAA,EAIJ;AACF,CAAC;",
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 { BillingRun } from '../../../data/entities'\nimport { billingEntityIds } from '../../../data/entityIds'\nimport { billingRunRetryFailedSchema } from '../../../data/validators'\nimport { createBillingCrudOpenApi } from '../../openapi'\n\n/**\n * `POST /api/billing/runs/retry-failed`\n *\n * Creates a new Bill Run scoped to the failed-account ids from a parent\n * run, with `parent_run_id` set on the new run so the retry chain is\n * traceable. Returns the new run's id + status + outcomes count.\n *\n * Implemented as a \"create-only\" `makeCrudRoute` so the framework's\n * RBAC + OpenAPI + command-bus plumbing stays consistent with the rest\n * of the billing surface. The list/get half of the factory is never\n * exercised on this URL \u2014 only POST is exported.\n */\n\nconst routeMetadata = {\n POST: { requireAuth: true, requireFeatures: ['billing.run.trigger'] },\n}\n\nexport const metadata = routeMetadata\n\nconst retryResponseSchema = z.object({\n billRunId: z.string().uuid(),\n status: z.string(),\n outcomesCount: z.number().int().nonnegative(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillingRun,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n // No `list` \u2014 POST-only route. Keep a stub so the factory type-checks.\n list: {\n schema: z.object({}).passthrough(),\n entityId: billingEntityIds.run,\n fields: ['id'],\n buildFilters: async () => ({}),\n },\n actions: {\n create: {\n commandId: 'billing.runs.retry_failed',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingRunRetryFailedSchema.parse(scoped)\n },\n response: ({ result }) => ({\n billRunId: result?.billRunId ?? null,\n status: result?.status ?? null,\n outcomesCount: result?.outcomesCount ?? 0,\n }),\n status: 201,\n },\n },\n})\n\nexport const POST = crud.POST\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillRunRetry',\n pluralName: 'BillRunRetries',\n querySchema: z.object({}).passthrough(),\n listResponseSchema: z.object({ items: z.array(z.unknown()) }),\n create: {\n schema: billingRunRetryFailedSchema,\n responseSchema: retryResponseSchema,\n description:\n 'Re-runs the failed accounts of an earlier Bill Run. The new run ' +\n 'inherits the parent run\\'s `asOfDate` and `catchUp` flag; its ' +\n '`parent_run_id` points back at the source. Returns 409 if there ' +\n 'are no failed outcomes or if another run is in flight.',\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,2BAA2B;AACpC,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AACjC,SAAS,mCAAmC;AAC5C,SAAS,gCAAgC;AAezC,MAAM,gBAAgB;AAAA,EACpB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AACtE;AAEO,MAAM,WAAW;AAExB,MAAM,sBAAsB,EAAE,OAAO;AAAA,EACnC,WAAW,EAAE,OAAO,EAAE,KAAK;AAAA,EAC3B,QAAQ,EAAE,OAAO;AAAA,EACjB,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAC9C,CAAC;AAED,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAE/C,MAAM,OAAO,cAAc;AAAA,EACzB,UAAU;AAAA,EACV,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB;AAAA;AAAA,EAEA,MAAM;AAAA,IACJ,QAAQ,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAAA,IACjC,UAAU,iBAAiB;AAAA,IAC3B,QAAQ,CAAC,IAAI;AAAA,IACb,cAAc,aAAa,CAAC;AAAA,EAC9B;AAAA,EACA,SAAS;AAAA,IACP,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,OAAO,EAAE,KAAK,IAAI,MAAM;AAChC,cAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,cAAM,SAAS,kBAAkB,OAAO,CAAC,GAAG,KAAK,SAAS;AAC1D,eAAO,4BAA4B,MAAM,MAAM;AAAA,MACjD;AAAA,MACA,UAAU,CAAC,EAAE,OAAO,OAAO;AAAA,QACzB,WAAW,QAAQ,aAAa;AAAA,QAChC,QAAQ,QAAQ,UAAU;AAAA,QAC1B,eAAe,QAAQ,iBAAiB;AAAA,MAC1C;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,EACF;AACF,CAAC;AAEM,MAAM,OAAO,KAAK;AAElB,MAAM,UAAU,yBAAyB;AAAA,EAC9C,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,aAAa,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAAA,EACtC,oBAAoB,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;AAAA,EAC5D,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,aACE;AAAA,EAIJ;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
3
3
  import { withScopedPayload } from "@open-mercato/shared/lib/api/scoped";
4
4
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
- import { BillRun } from "../../data/entities.js";
5
+ import { BillingRun } from "../../data/entities.js";
6
6
  import { billingEntityIds } from "../../data/entityIds.js";
7
7
  import {
8
8
  billingRunListQuerySchema,
@@ -41,7 +41,7 @@ const rawBodySchema = z.object({}).passthrough();
41
41
  const crud = makeCrudRoute({
42
42
  metadata: routeMetadata,
43
43
  orm: {
44
- entity: BillRun,
44
+ entity: BillingRun,
45
45
  idField: "id",
46
46
  orgField: "organizationId",
47
47
  tenantField: "tenantId",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/billing/api/runs/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 { BillRun } from '../../data/entities'\nimport { billingEntityIds } from '../../data/entityIds'\nimport {\n billingRunListQuerySchema,\n billingRunTriggerSchema,\n} from '../../data/validators'\nimport { createBillingCrudOpenApi, createPagedListResponseSchema } from '../openapi'\n\n/**\n * `/api/billing/runs` \u2014 GET (history list) + POST (manual trigger).\n *\n * The triggered-by enum gate:\n * - `trigger` feature is required for `real` mode (which mutates\n * production state).\n * - `dry_run` feature alone is enough for `mode=dry` (finance auditor\n * persona). The route-level RBAC check on POST requires `trigger`,\n * so dry-run callers must hold both `dry_run` AND `trigger` here \u2014\n * a deliberate tightening over what the spec persona table suggests.\n *\n * If we ever want auditors to drive dry-runs without holding `trigger`,\n * we split into two routes (POST `/runs` for real, POST `/runs/dry` for\n * dry). Postponed until an operator actually asks.\n */\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.run.view'] },\n POST: { requireAuth: true, requireFeatures: ['billing.run.trigger'] },\n}\n\nexport const metadata = routeMetadata\n\n// The list endpoint projects raw column names \u2014 snake_case.\nconst billRunListItemSchema = z.object({\n id: z.string().uuid(),\n triggered_by: z.string(),\n parent_run_id: z.string().uuid().nullable(),\n dry_run: z.boolean(),\n test_mode: z.boolean(),\n catch_up: z.boolean(),\n scoped_account_ids: z.array(z.string().uuid()).nullable(),\n as_of_date: z.string(),\n started_at: z.string().nullable(),\n finished_at: z.string().nullable(),\n status: z.string(),\n summary: z.record(z.string(), z.unknown()).nullable(),\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 triggerResponseSchema = z.object({\n billRunId: z.string().uuid(),\n status: z.string(),\n outcomesCount: z.number().int().nonnegative(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillRun,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: billingRunListQuerySchema,\n entityId: billingEntityIds.run,\n fields: [\n 'id',\n 'triggered_by',\n 'parent_run_id',\n 'dry_run',\n 'test_mode',\n 'catch_up',\n 'scoped_account_ids',\n 'as_of_date',\n 'started_at',\n 'finished_at',\n 'status',\n 'summary',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n startedAt: 'started_at',\n finishedAt: 'finished_at',\n asOfDate: 'as_of_date',\n createdAt: 'created_at',\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 Bill Run through this filter.\n if (typeof query.id === 'string' && query.id) {\n filters.id = { $eq: query.id }\n }\n // Run ids are UUIDs \u2014 search matches a pasted id exactly.\n if (typeof query.search === 'string' && /^[0-9a-fA-F-]{36}$/.test(query.search.trim())) {\n filters.id = { $eq: query.search.trim() }\n }\n if (typeof query.status === 'string') filters.status = { $eq: query.status }\n if (typeof query.triggeredBy === 'string') filters.triggered_by = { $eq: query.triggeredBy }\n if (typeof query.parentRunId === 'string') filters.parent_run_id = { $eq: query.parentRunId }\n if (query.asOfDateFrom instanceof Date) {\n filters.as_of_date = Object.assign(\n (filters.as_of_date as Record<string, unknown>) ?? {},\n { $gte: query.asOfDateFrom },\n )\n }\n if (query.asOfDateTo instanceof Date) {\n filters.as_of_date = Object.assign(\n (filters.as_of_date as Record<string, unknown>) ?? {},\n { $lte: query.asOfDateTo },\n )\n }\n return filters\n },\n },\n actions: {\n create: {\n commandId: 'billing.runs.trigger',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingRunTriggerSchema.parse(scoped)\n },\n response: ({ result }) => ({\n billRunId: result?.billRunId ?? null,\n status: result?.status ?? null,\n outcomesCount: result?.outcomesCount ?? 0,\n }),\n status: 201,\n },\n // Runs are append-only \u2014 no update / delete on this surface. A\n // failed run is corrected by triggering a `retry-failed` (separate\n // route).\n },\n})\n\nconst { POST } = crud\nexport { POST }\nexport const GET = crud.GET\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillRun',\n pluralName: 'BillRuns',\n querySchema: billingRunListQuerySchema,\n listResponseSchema: createPagedListResponseSchema(billRunListItemSchema),\n create: {\n schema: billingRunTriggerSchema,\n responseSchema: triggerResponseSchema,\n description:\n 'Triggers a Bill Run. `mode=real` (default) persists drafts and ' +\n 'advances state; `mode=test` persists test-flagged drafts without ' +\n 'advancing state; `mode=dry` computes everything in memory and ' +\n 'persists nothing. Returns HTTP 409 (`billing.run.lock_busy`) when ' +\n 'another run is in flight for the same tenant.',\n },\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { BillingRun } from '../../data/entities'\nimport { billingEntityIds } from '../../data/entityIds'\nimport {\n billingRunListQuerySchema,\n billingRunTriggerSchema,\n} from '../../data/validators'\nimport { createBillingCrudOpenApi, createPagedListResponseSchema } from '../openapi'\n\n/**\n * `/api/billing/runs` \u2014 GET (history list) + POST (manual trigger).\n *\n * The triggered-by enum gate:\n * - `trigger` feature is required for `real` mode (which mutates\n * production state).\n * - `dry_run` feature alone is enough for `mode=dry` (finance auditor\n * persona). The route-level RBAC check on POST requires `trigger`,\n * so dry-run callers must hold both `dry_run` AND `trigger` here \u2014\n * a deliberate tightening over what the spec persona table suggests.\n *\n * If we ever want auditors to drive dry-runs without holding `trigger`,\n * we split into two routes (POST `/runs` for real, POST `/runs/dry` for\n * dry). Postponed until an operator actually asks.\n */\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.run.view'] },\n POST: { requireAuth: true, requireFeatures: ['billing.run.trigger'] },\n}\n\nexport const metadata = routeMetadata\n\n// The list endpoint projects raw column names \u2014 snake_case.\nconst billRunListItemSchema = z.object({\n id: z.string().uuid(),\n triggered_by: z.string(),\n parent_run_id: z.string().uuid().nullable(),\n dry_run: z.boolean(),\n test_mode: z.boolean(),\n catch_up: z.boolean(),\n scoped_account_ids: z.array(z.string().uuid()).nullable(),\n as_of_date: z.string(),\n started_at: z.string().nullable(),\n finished_at: z.string().nullable(),\n status: z.string(),\n summary: z.record(z.string(), z.unknown()).nullable(),\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 triggerResponseSchema = z.object({\n billRunId: z.string().uuid(),\n status: z.string(),\n outcomesCount: z.number().int().nonnegative(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillingRun,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: billingRunListQuerySchema,\n entityId: billingEntityIds.run,\n fields: [\n 'id',\n 'triggered_by',\n 'parent_run_id',\n 'dry_run',\n 'test_mode',\n 'catch_up',\n 'scoped_account_ids',\n 'as_of_date',\n 'started_at',\n 'finished_at',\n 'status',\n 'summary',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n startedAt: 'started_at',\n finishedAt: 'finished_at',\n asOfDate: 'as_of_date',\n createdAt: 'created_at',\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 Bill Run through this filter.\n if (typeof query.id === 'string' && query.id) {\n filters.id = { $eq: query.id }\n }\n // Run ids are UUIDs \u2014 search matches a pasted id exactly.\n if (typeof query.search === 'string' && /^[0-9a-fA-F-]{36}$/.test(query.search.trim())) {\n filters.id = { $eq: query.search.trim() }\n }\n if (typeof query.status === 'string') filters.status = { $eq: query.status }\n if (typeof query.triggeredBy === 'string') filters.triggered_by = { $eq: query.triggeredBy }\n if (typeof query.parentRunId === 'string') filters.parent_run_id = { $eq: query.parentRunId }\n if (query.asOfDateFrom instanceof Date) {\n filters.as_of_date = Object.assign(\n (filters.as_of_date as Record<string, unknown>) ?? {},\n { $gte: query.asOfDateFrom },\n )\n }\n if (query.asOfDateTo instanceof Date) {\n filters.as_of_date = Object.assign(\n (filters.as_of_date as Record<string, unknown>) ?? {},\n { $lte: query.asOfDateTo },\n )\n }\n return filters\n },\n },\n actions: {\n create: {\n commandId: 'billing.runs.trigger',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingRunTriggerSchema.parse(scoped)\n },\n response: ({ result }) => ({\n billRunId: result?.billRunId ?? null,\n status: result?.status ?? null,\n outcomesCount: result?.outcomesCount ?? 0,\n }),\n status: 201,\n },\n // Runs are append-only \u2014 no update / delete on this surface. A\n // failed run is corrected by triggering a `retry-failed` (separate\n // route).\n },\n})\n\nconst { POST } = crud\nexport { POST }\nexport const GET = crud.GET\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillRun',\n pluralName: 'BillRuns',\n querySchema: billingRunListQuerySchema,\n listResponseSchema: createPagedListResponseSchema(billRunListItemSchema),\n create: {\n schema: billingRunTriggerSchema,\n responseSchema: triggerResponseSchema,\n description:\n 'Triggers a Bill Run. `mode=real` (default) persists drafts and ' +\n 'advances state; `mode=test` persists test-flagged drafts without ' +\n 'advancing state; `mode=dry` computes everything in memory and ' +\n 'persists nothing. Returns HTTP 409 (`billing.run.lock_busy`) when ' +\n 'another run is in flight for the same tenant.',\n },\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }