@dainamite/billing 0.19.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 (91) hide show
  1. package/CHANGELOG.md +82 -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 +5 -5
  59. package/src/modules/billing/__tests__/entityRegistry.test.ts +319 -0
  60. package/src/modules/billing/__tests__/events.test.ts +142 -0
  61. package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +3 -3
  62. package/src/modules/billing/api/export/account/[id]/route.ts +7 -7
  63. package/src/modules/billing/api/invoices/add-line/route.ts +1 -1
  64. package/src/modules/billing/api/invoices/edit-line/route.ts +2 -2
  65. package/src/modules/billing/api/invoices/remove-line/route.ts +1 -1
  66. package/src/modules/billing/api/run-outcomes/route.ts +2 -2
  67. package/src/modules/billing/api/runs/retry-failed/route.ts +2 -2
  68. package/src/modules/billing/api/runs/route.ts +2 -2
  69. package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +1 -1
  70. package/src/modules/billing/backend/billing/accounts/page.tsx +1 -1
  71. package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +1 -1
  72. package/src/modules/billing/backend/billing/invoices/page.tsx +1 -1
  73. package/src/modules/billing/backend/billing/items/page.tsx +1 -1
  74. package/src/modules/billing/backend/billing/runs/[id]/page.tsx +1 -1
  75. package/src/modules/billing/backend/billing/runs/page.tsx +1 -1
  76. package/src/modules/billing/backend/billing/statements/[id]/page.tsx +1 -1
  77. package/src/modules/billing/backend/billing/statements/page.tsx +1 -1
  78. package/src/modules/billing/backend/billing/topups/page.tsx +1 -1
  79. package/src/modules/billing/backend/billing/transactions/page.tsx +1 -1
  80. package/src/modules/billing/ce.ts +8 -7
  81. package/src/modules/billing/commands/invoices.ts +9 -9
  82. package/src/modules/billing/commands/runs.ts +3 -3
  83. package/src/modules/billing/components/PrepaidPanel.tsx +1 -1
  84. package/src/modules/billing/data/entities.ts +31 -10
  85. package/src/modules/billing/data/entityIds.ts +16 -11
  86. package/src/modules/billing/events.ts +26 -0
  87. package/src/modules/billing/lib/billRunEngine.ts +12 -12
  88. package/src/modules/billing/lib/billRunLabel.ts +1 -1
  89. package/src/modules/billing/lib/prepaidPeriodClose.ts +3 -3
  90. package/src/modules/billing/lib/tenantLock.ts +1 -1
  91. package/src/modules/billing/workers/reap-stale-bill-runs.ts +4 -4
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
3
  import { useRouter } from 'next/navigation'
4
- import type { ColumnDef } from '@tanstack/react-table'
4
+ import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
5
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
6
  import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
7
7
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
@@ -3,7 +3,7 @@ import * as React from 'react'
3
3
  import Link from 'next/link'
4
4
  import { useRouter, useSearchParams } from 'next/navigation'
5
5
  import { Plus } from 'lucide-react'
6
- import type { ColumnDef } from '@tanstack/react-table'
6
+ import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
7
7
  import { useT } from '@open-mercato/shared/lib/i18n/context'
8
8
  import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
9
9
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
@@ -2,7 +2,7 @@
2
2
  import * as React from 'react'
3
3
  import Link from 'next/link'
4
4
  import { useParams } from 'next/navigation'
5
- import type { ColumnDef } from '@tanstack/react-table'
5
+ import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
6
6
  import { useT } from '@open-mercato/shared/lib/i18n/context'
7
7
  import { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'
8
8
  import { FormHeader } from '@open-mercato/ui/backend/forms'
@@ -2,7 +2,7 @@
2
2
  import * as React from 'react'
3
3
  import { useRouter } from 'next/navigation'
4
4
  import { Play } from 'lucide-react'
5
- import type { ColumnDef } from '@tanstack/react-table'
5
+ import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
6
6
  import { useT } from '@open-mercato/shared/lib/i18n/context'
7
7
  import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
8
8
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
@@ -2,7 +2,7 @@
2
2
  import * as React from 'react'
3
3
  import Link from 'next/link'
4
4
  import { useParams } from 'next/navigation'
5
- import type { ColumnDef } from '@tanstack/react-table'
5
+ import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
6
6
  import { useT } from '@open-mercato/shared/lib/i18n/context'
7
7
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
8
8
  import { FormHeader } from '@open-mercato/ui/backend/forms'
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
3
  import { useRouter } from 'next/navigation'
4
- import type { ColumnDef } from '@tanstack/react-table'
4
+ import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
5
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
6
  import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
7
7
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
3
  import { useRouter } from 'next/navigation'
4
- import type { ColumnDef } from '@tanstack/react-table'
4
+ import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
5
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
6
  import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
7
7
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
  import * as React from 'react'
3
3
  import { useRouter } from 'next/navigation'
4
- import type { ColumnDef } from '@tanstack/react-table'
4
+ import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
5
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
6
  import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
7
7
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
@@ -1,14 +1,15 @@
1
1
  import type { CustomEntitySpec } from '@open-mercato/shared/modules/entities'
2
2
 
3
3
  /**
4
- * Billing module entity registry.
4
+ * Billing module custom-entity specs (labels / custom-field surfaces).
5
5
  *
6
- * Listing our MikroORM entities here gets them into the framework's
7
- * `E.billing.<entity>` generated registry (`entities.ids.generated.ts`),
8
- * which the CRUD factory, query engine, and indexers reference. Phase 0
9
- * shipped with no entries because the standalone CLI didn't traverse
10
- * `@dainamite/*` packages — adding them in Phase 1 once the resolver
11
- * patch lands.
6
+ * These ids are real MikroORM tables, not virtual custom entities. The
7
+ * framework only knows that while each id is also in the app's generated
8
+ * `E.billing` registry (fed by the package's shipped
9
+ * `generated/entities.ids.generated.ts`). For an id missing there,
10
+ * `mercato entities install` writes an active `custom_entities` row, and the
11
+ * query engine then reads the id from custom-entity storage instead of its
12
+ * table — every billing list comes back empty.
12
13
  *
13
14
  * `fields: []` means billing does NOT expose custom-field surfaces on
14
15
  * these entities today (no EAV). Adding fields here is reserved for
@@ -8,7 +8,7 @@ import {
8
8
  } from '@open-mercato/shared/lib/commands/scope'
9
9
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
10
10
  import { SalesInvoice, SalesInvoiceLine } from '@open-mercato/core/modules/sales/data/entities'
11
- import { BillingAccount, DraftInvoiceEdit } from '../data/entities'
11
+ import { BillingAccount, BillingDraftInvoiceEdit } from '../data/entities'
12
12
  import { emitBillingEvent, type BillingEventId } from '../events'
13
13
  import { buildPostpaidCreditCrossingEvents } from '../lib/accountStatus'
14
14
  import {
@@ -182,7 +182,7 @@ const postInvoiceCommand: CommandHandler<BillingInvoicePostInput, PostResult> =
182
182
  // Three commands sharing the same shape:
183
183
  // 1. Assert the invoice is still a draft (refuse otherwise).
184
184
  // 2. Mutate the line / collection.
185
- // 3. Write a `DraftInvoiceEdit` audit row carrying before+after
185
+ // 3. Write a `BillingDraftInvoiceEdit` audit row carrying before+after
186
186
  // snapshots (`null` on the "no prior state" / "no resulting state"
187
187
  // side per the spec — see entity column docs).
188
188
  // 4. Recompute the invoice totals from the surviving lines.
@@ -267,7 +267,7 @@ async function recomputeInvoiceTotals(
267
267
 
268
268
  function getUserIdFromCtx(ctx: CommandRuntimeContext): string {
269
269
  const userId = (ctx.auth?.sub as string | undefined) ?? null
270
- // The DraftInvoiceEdit.user_id column is NOT NULL. For system-driven
270
+ // The BillingDraftInvoiceEdit.user_id column is NOT NULL. For system-driven
271
271
  // edits (e.g. catch-up flow that revises a value), we fall back to
272
272
  // a sentinel UUID. Operator-driven edits always have a real
273
273
  // ctx.auth.sub.
@@ -303,7 +303,7 @@ const editDraftLineCommand: CommandHandler<
303
303
  }
304
304
 
305
305
  const before = snapshotLine(line)
306
- let audit: DraftInvoiceEdit | null = null
306
+ let audit: BillingDraftInvoiceEdit | null = null
307
307
 
308
308
  await withAtomicFlush(em, [
309
309
  () => {
@@ -339,7 +339,7 @@ const editDraftLineCommand: CommandHandler<
339
339
  },
340
340
  () => {
341
341
  const after = snapshotLine(line)
342
- audit = em.create(DraftInvoiceEdit, {
342
+ audit = em.create(BillingDraftInvoiceEdit, {
343
343
  tenantId: parsed.tenantId,
344
344
  organizationId: parsed.organizationId,
345
345
  invoiceId: invoice.id,
@@ -408,7 +408,7 @@ const addDraftLineCommand: CommandHandler<
408
408
  operator_added: true,
409
409
  },
410
410
  })
411
- let audit: DraftInvoiceEdit | null = null
411
+ let audit: BillingDraftInvoiceEdit | null = null
412
412
 
413
413
  await withAtomicFlush(em, [
414
414
  () => {
@@ -419,7 +419,7 @@ const addDraftLineCommand: CommandHandler<
419
419
  },
420
420
  () => {
421
421
  const after = snapshotLine(line)
422
- audit = em.create(DraftInvoiceEdit, {
422
+ audit = em.create(BillingDraftInvoiceEdit, {
423
423
  tenantId: parsed.tenantId,
424
424
  organizationId: parsed.organizationId,
425
425
  invoiceId: invoice.id,
@@ -466,7 +466,7 @@ const removeDraftLineCommand: CommandHandler<
466
466
  }
467
467
 
468
468
  const before = snapshotLine(line)
469
- let audit: DraftInvoiceEdit | null = null
469
+ let audit: BillingDraftInvoiceEdit | null = null
470
470
 
471
471
  await withAtomicFlush(em, [
472
472
  () => {
@@ -476,7 +476,7 @@ const removeDraftLineCommand: CommandHandler<
476
476
  await recomputeInvoiceTotals(em, invoice)
477
477
  },
478
478
  () => {
479
- audit = em.create(DraftInvoiceEdit, {
479
+ audit = em.create(BillingDraftInvoiceEdit, {
480
480
  tenantId: parsed.tenantId,
481
481
  organizationId: parsed.organizationId,
482
482
  invoiceId: invoice.id,
@@ -7,7 +7,7 @@ import {
7
7
  ensureTenantScope,
8
8
  } from '@open-mercato/shared/lib/commands/scope'
9
9
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
10
- import { BillRun, BillRunOutcome } from '../data/entities'
10
+ import { BillingRun, BillingRunOutcome } from '../data/entities'
11
11
  import {
12
12
  billingRunRetryFailedSchema,
13
13
  billingRunTriggerSchema,
@@ -107,7 +107,7 @@ const retryFailedCommand: CommandHandler<BillingRunRetryFailedInput, TriggerResu
107
107
  ensureOrganizationScope(ctx, parsed.organizationId)
108
108
 
109
109
  const em = getEm(ctx)
110
- const parentRun = await em.findOne(BillRun, {
110
+ const parentRun = await em.findOne(BillingRun, {
111
111
  id: parsed.billRunId,
112
112
  tenantId: parsed.tenantId,
113
113
  organizationId: parsed.organizationId,
@@ -116,7 +116,7 @@ const retryFailedCommand: CommandHandler<BillingRunRetryFailedInput, TriggerResu
116
116
  if (!parentRun) {
117
117
  throw new CrudHttpError(404, { error: 'Parent Bill Run not found' })
118
118
  }
119
- const failedOutcomes = await em.find(BillRunOutcome, {
119
+ const failedOutcomes = await em.find(BillingRunOutcome, {
120
120
  billRunId: parentRun.id,
121
121
  tenantId: parsed.tenantId,
122
122
  organizationId: parsed.organizationId,
@@ -2,7 +2,7 @@
2
2
  import * as React from 'react'
3
3
  import { useRouter } from 'next/navigation'
4
4
  import { Plus, SlidersHorizontal } from 'lucide-react'
5
- import type { ColumnDef } from '@tanstack/react-table'
5
+ import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
6
6
  import { useT } from '@open-mercato/shared/lib/i18n/context'
7
7
  import { DataTable } from '@open-mercato/ui/backend/DataTable'
8
8
  import { Tag } from '@open-mercato/ui/primitives/tag'
@@ -318,13 +318,20 @@ export class BillingAccountUsage {
318
318
  deletedAt?: Date | null
319
319
  }
320
320
 
321
- // ─── BillRun ─────────────────────────────────────────────────────
321
+ // ─── BillingRun ──────────────────────────────────────────────────
322
322
  //
323
323
  // A single execution of the engine — one row per trigger (schedule or
324
324
  // manual). The concurrency lock is a Postgres transaction-scoped advisory
325
325
  // lock keyed by hash(tenant_id || 'billing-run'); it is NOT stored on this
326
326
  // row. Crash recovery for zombie `status='running'` rows is handled by
327
327
  // workers/reap-stale-bill-runs.ts.
328
+ //
329
+ // Class names must be the PascalCase of the entity-id suffix (`billing:
330
+ // billing_run` → `BillingRun`) — that is how Open Mercato maps an id to its
331
+ // table. Until 0.20.0 this class was `BillRun` (likewise `BillRunOutcome`,
332
+ // `DraftInvoiceEdit`), and those three ids resolved to no table; see
333
+ // data/entityIds.ts. Type-only aliases for the old names are at the end of
334
+ // this file.
328
335
 
329
336
  @Entity({ tableName: 'billing_runs' })
330
337
  @Index({
@@ -335,7 +342,7 @@ export class BillingAccountUsage {
335
342
  name: 'billing_runs_parent_idx',
336
343
  properties: ['parentRunId'],
337
344
  })
338
- export class BillRun {
345
+ export class BillingRun {
339
346
  [OptionalProps]?:
340
347
  | 'parentRunId'
341
348
  | 'dryRun'
@@ -417,10 +424,10 @@ export class BillRun {
417
424
  deletedAt?: Date | null
418
425
  }
419
426
 
420
- // ─── BillRunOutcome ──────────────────────────────────────────────
427
+ // ─── BillingRunOutcome ───────────────────────────────────────────
421
428
  //
422
429
  // Per-account outcome of a Bill Run. Stored in its own table (not as a
423
- // JSONB array on BillRun.summary) so operators can query failures across
430
+ // JSONB array on BillingRun.summary) so operators can query failures across
424
431
  // runs without jsonb_array_elements gymnastics. See spec rationale.
425
432
 
426
433
  @Entity({ tableName: 'billing_run_outcomes' })
@@ -432,7 +439,7 @@ export class BillRun {
432
439
  name: 'billing_run_outcomes_account_history_idx',
433
440
  properties: ['organizationId', 'tenantId', 'billAccountId', 'createdAt'],
434
441
  })
435
- export class BillRunOutcome {
442
+ export class BillingRunOutcome {
436
443
  [OptionalProps]?:
437
444
  | 'errorMessage'
438
445
  | 'warnings'
@@ -493,7 +500,7 @@ export class BillRunOutcome {
493
500
  deletedAt?: Date | null
494
501
  }
495
502
 
496
- // ─── DraftInvoiceEdit (audit) ────────────────────────────────────
503
+ // ─── BillingDraftInvoiceEdit (audit) ─────────────────────────────
497
504
  //
498
505
  // Append-only audit of every operator edit on a draft invoice line.
499
506
  // Pattern mirrors `cpq_subscription_change_logs` — minimal columns
@@ -509,7 +516,7 @@ export class BillRunOutcome {
509
516
  name: 'billing_draft_invoice_edits_user_idx',
510
517
  properties: ['organizationId', 'tenantId', 'userId', 'createdAt'],
511
518
  })
512
- export class DraftInvoiceEdit {
519
+ export class BillingDraftInvoiceEdit {
513
520
  [OptionalProps]?: 'invoiceLineId' | 'beforeJson' | 'afterJson' | 'createdAt' | 'deletedAt'
514
521
 
515
522
  @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
@@ -630,8 +637,8 @@ export class BillingAccountBalance {
630
637
  // ─── BillingAccountTransaction (SPEC-002, append-only) ───────────
631
638
  //
632
639
  // The source of truth for every balance movement. Append-only — pattern
633
- // mirrors DraftInvoiceEdit (no `updated_at` / `is_active`; `deleted_at`
634
- // reserved for compliance erasure only). `balance = SUM(amount)`.
640
+ // mirrors BillingDraftInvoiceEdit (no `updated_at` / `is_active`;
641
+ // `deleted_at` reserved for compliance erasure only). `balance = SUM(amount)`.
635
642
  //
636
643
  // The unique `(tenant_id, bill_account_id, source_ref)` is the idempotency
637
644
  // gate: a deduplicated usage upload or a retried recurring charge can never
@@ -860,7 +867,7 @@ export class BillingStatement {
860
867
  @Property({ name: 'bill_account_id', type: 'uuid' })
861
868
  billAccountId!: string
862
869
 
863
- // FK (string) to the BillRun that generated it (period close reuses the
870
+ // FK (string) to the BillingRun that generated it (period close reuses the
864
871
  // run engine). Null for a manually-generated statement.
865
872
  @Property({ name: 'bill_run_id', type: 'uuid', nullable: true })
866
873
  billRunId?: string | null
@@ -914,3 +921,17 @@ export class BillingStatement {
914
921
  @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
915
922
  updatedAt: Date = new Date()
916
923
  }
924
+
925
+ // ─── Deprecated names (0.20.0) ───────────────────────────────────
926
+ //
927
+ // Type-only on purpose: a runtime alias (`export const BillRun = BillingRun`)
928
+ // would make the app's generated `entities` list carry the same class twice.
929
+ // Code that used the old names as values (`em.find(BillRun, …)`) must switch
930
+ // to the new ones — TypeScript flags it.
931
+
932
+ /** @deprecated Renamed to {@link BillingRun} in 0.20.0. */
933
+ export type BillRun = BillingRun
934
+ /** @deprecated Renamed to {@link BillingRunOutcome} in 0.20.0. */
935
+ export type BillRunOutcome = BillingRunOutcome
936
+ /** @deprecated Renamed to {@link BillingDraftInvoiceEdit} in 0.20.0. */
937
+ export type DraftInvoiceEdit = BillingDraftInvoiceEdit
@@ -1,17 +1,22 @@
1
1
  /**
2
- * Canonical entity IDs for the billing module.
2
+ * Canonical entity IDs for the billing module — the single source of truth.
3
+ * Billing code (CRUD factory `entityId`, indexer `entityType`, response
4
+ * enricher `targetEntity`, …) imports from here; a package must not import
5
+ * the app's `#generated/entities.ids.generated`.
3
6
  *
4
- * Mirrors what `E.billing.<entity>` would be in
5
- * `.mercato/generated/entities.ids.generated.ts` if the standalone-app
6
- * CLI properly discovered `@dainamite/*` workspace entities (see the
7
- * Phase 0 deviation note in `xd-249-billing-spec.md`). Until the CLI
8
- * gap is closed upstream, this is the single source of truth — every
9
- * place that previously would have referenced `E.billing.*` (CRUD
10
- * factory `entityId`, indexer `entityType`, response enricher
11
- * `targetEntity`, etc.) imports from here instead.
7
+ * The build turns this object into the shipped registry
8
+ * `generated/entities.ids.generated.ts` (scripts/generate-entity-registry.mjs),
9
+ * which a consuming app's `mercato generate` reads to register these ids in
10
+ * its own `E.billing`. Without that, the query index rejects billing writes
11
+ * and `mercato entities install` treats the ids as virtual custom entities.
12
12
  *
13
- * When the CLI is fixed, this file can be replaced by `import { E }
14
- * from '#generated/entities.ids.generated'` in one PR.
13
+ * Adding an entity: add it here, then `yarn workspace @dainamite/billing
14
+ * build` and commit `generated/`. The id suffix must be the snake_case of
15
+ * the entity class name (`billing_run` → `BillingRun`): that is how the
16
+ * framework maps an id to its table, and the build fails otherwise. Do not
17
+ * rely on its table-name fallback — on Open Mercato 0.9 with MikroORM 7 it
18
+ * never matches (it iterates `MetadataStorage.getAll()` as an array, but
19
+ * that returns a Map).
15
20
  */
16
21
  export const billingEntityIds = {
17
22
  account: 'billing:billing_account',
@@ -12,8 +12,34 @@ import { createModuleEvents } from '@open-mercato/shared/modules/events'
12
12
  * - `billing.run.completed` fires once per Bill Run after all per-account
13
13
  * transactions finished (status is one of `completed` / `partial_failure`
14
14
  * / `failed`). Useful for "did the nightly run go OK?" alerts.
15
+ *
16
+ * CRUD events (`billing.<entity>.<action>`) are emitted by the data engine
17
+ * from the commands' `emitCrudSideEffects({ events: { module: 'billing',
18
+ * entity } })`. Only the (entity, action) pairs a command actually emits are
19
+ * declared — `__tests__/events.test.ts` fails when a command emits one that
20
+ * is missing here (the data engine would log "Emitting undeclared event").
15
21
  */
16
22
  const events = [
23
+ // ─── CRUD (commands/*.ts) ────────────────────────────────────
24
+ { id: 'billing.account.created', label: 'Billing Account Created', entity: 'account', category: 'crud' },
25
+ { id: 'billing.account.updated', label: 'Billing Account Updated', entity: 'account', category: 'crud' },
26
+ { id: 'billing.account.deleted', label: 'Billing Account Deleted', entity: 'account', category: 'crud' },
27
+ { id: 'billing.item.created', label: 'Billing Item Created', entity: 'item', category: 'crud' },
28
+ { id: 'billing.item.updated', label: 'Billing Item Updated', entity: 'item', category: 'crud' },
29
+ { id: 'billing.item.deleted', label: 'Billing Item Deleted', entity: 'item', category: 'crud' },
30
+ {
31
+ // One per usage upload, on the same high-throughput path as
32
+ // `billing.usage.rated` — excluded from workflow triggers for the same
33
+ // reason (see Open Question #2).
34
+ id: 'billing.usage.created',
35
+ label: 'Billing Usage Record Created',
36
+ entity: 'usage',
37
+ category: 'crud',
38
+ excludeFromTriggers: true,
39
+ },
40
+ { id: 'billing.transaction.created', label: 'Billing Balance Transaction Created', entity: 'transaction', category: 'crud' },
41
+ { id: 'billing.topup.created', label: 'Billing Top-up Created', entity: 'topup', category: 'crud' },
42
+ // ─── Lifecycle ───────────────────────────────────────────────
17
43
  {
18
44
  id: 'billing.invoice.posted',
19
45
  label: 'Billing Invoice Posted',
@@ -4,8 +4,8 @@ import {
4
4
  BillingAccount,
5
5
  BillingAccountUsage,
6
6
  BillingItem,
7
- BillRun,
8
- BillRunOutcome,
7
+ BillingRun,
8
+ BillingRunOutcome,
9
9
  } from '../data/entities'
10
10
  import { advanceNextBillDate, deriveBillPeriod, isCycleDue } from './billPeriod'
11
11
  import {
@@ -72,8 +72,8 @@ export type RunBillRunParams = {
72
72
  }
73
73
 
74
74
  export type RunBillRunResult = {
75
- billRun: BillRun
76
- outcomes: BillRunOutcome[]
75
+ billRun: BillingRun
76
+ outcomes: BillingRunOutcome[]
77
77
  }
78
78
 
79
79
  type RunOutput = RunBillRunResult & {
@@ -136,7 +136,7 @@ async function executeRunWithinLock(
136
136
  params: RunBillRunParams,
137
137
  ): Promise<RunOutput> {
138
138
  const now = new Date()
139
- const billRun = em.create(BillRun, {
139
+ const billRun = em.create(BillingRun, {
140
140
  organizationId: params.organizationId,
141
141
  tenantId: params.tenantId,
142
142
  triggeredBy: params.triggeredBy,
@@ -157,7 +157,7 @@ async function executeRunWithinLock(
157
157
  await em.flush()
158
158
 
159
159
  const accounts = await selectTargetAccounts(em, params)
160
- const outcomes: BillRunOutcome[] = []
160
+ const outcomes: BillingRunOutcome[] = []
161
161
  const pendingEvents: DomainEvent[] = []
162
162
  let totalUsageRecordsRated = 0
163
163
 
@@ -166,7 +166,7 @@ async function executeRunWithinLock(
166
166
  if (processResult.status !== 'failed') {
167
167
  totalUsageRecordsRated += processResult.usageRecordsRated
168
168
  }
169
- const outcome = em.create(BillRunOutcome, {
169
+ const outcome = em.create(BillingRunOutcome, {
170
170
  organizationId: params.organizationId,
171
171
  tenantId: params.tenantId,
172
172
  billRunId: billRun.id,
@@ -222,7 +222,7 @@ async function selectTargetAccounts(
222
222
  async function processAccountSafely(
223
223
  em: EntityManager,
224
224
  container: AwilixContainer,
225
- billRun: BillRun,
225
+ billRun: BillingRun,
226
226
  account: BillingAccount,
227
227
  params: RunBillRunParams,
228
228
  ): Promise<AccountProcessResult> {
@@ -241,7 +241,7 @@ async function processAccountSafely(
241
241
  async function processAccount(
242
242
  em: EntityManager,
243
243
  container: AwilixContainer,
244
- billRun: BillRun,
244
+ billRun: BillingRun,
245
245
  account: BillingAccount,
246
246
  params: RunBillRunParams,
247
247
  ): Promise<AccountProcessResult> {
@@ -438,8 +438,8 @@ async function findExistingDraftForPeriod(
438
438
  }
439
439
 
440
440
  function computeFinalRunStatus(
441
- outcomes: readonly BillRunOutcome[],
442
- ): BillRun['status'] {
441
+ outcomes: readonly BillingRunOutcome[],
442
+ ): BillingRun['status'] {
443
443
  if (outcomes.length === 0) return 'completed'
444
444
  const anyFailed = outcomes.some((o) => o.status === 'failed')
445
445
  const anySuccess = outcomes.some(
@@ -451,7 +451,7 @@ function computeFinalRunStatus(
451
451
  }
452
452
 
453
453
  function buildRunSummary(
454
- outcomes: readonly BillRunOutcome[],
454
+ outcomes: readonly BillingRunOutcome[],
455
455
  totalAccounts: number,
456
456
  usageRecordsRated: number,
457
457
  ): Record<string, unknown> {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Human-readable label for a Bill Run.
3
3
  *
4
- * The BillRun entity has no `name` column — the engine keys runs by id.
4
+ * The BillingRun entity has no `name` column — the engine keys runs by id.
5
5
  * The admin UI derives a stable, scannable label from the run's as-of
6
6
  * date so operators see `BILL_RUN_2026-05-20` instead of a raw UUID.
7
7
  */
@@ -5,7 +5,7 @@ import {
5
5
  BillingAccountTransaction,
6
6
  BillingItem,
7
7
  BillingStatement,
8
- BillRun,
8
+ BillingRun,
9
9
  } from '../data/entities'
10
10
  import type { BillCycle } from '../data/validators'
11
11
  import { advanceNextBillDate, addDays, deriveBillPeriod, isCycleDue, type BillPeriod } from './billPeriod'
@@ -75,7 +75,7 @@ export type ProcessPrepaidParams = {
75
75
  export async function processPrepaidAccount(
76
76
  em: EntityManager,
77
77
  account: BillingAccount,
78
- billRun: BillRun,
78
+ billRun: BillingRun,
79
79
  params: ProcessPrepaidParams,
80
80
  ): Promise<PrepaidAccountResult> {
81
81
  const maxCycles =
@@ -212,7 +212,7 @@ async function closePrepaidPeriod(
212
212
  em: EntityManager,
213
213
  account: BillingAccount,
214
214
  period: BillPeriod,
215
- billRun: BillRun,
215
+ billRun: BillingRun,
216
216
  opts: {
217
217
  mode: PrepaidCloseMode
218
218
  statementEnabled: boolean
@@ -13,7 +13,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
13
13
  * signal instead of waiting on a row.
14
14
  * - It's released automatically when the transaction ends (commit,
15
15
  * rollback, or process death) — so there is no lock-leak path even
16
- * on `kill -9`. The `BillRun` row's `status='running'` may be left
16
+ * on `kill -9`. The `BillingRun` row's `status='running'` may be left
17
17
  * hanging, but the lock itself vanishes; the reaper worker
18
18
  * (workers/reap-stale-bill-runs.ts) handles the orphaned row.
19
19
  * - It works without a sentinel table — we hash a string key into the
@@ -1,7 +1,7 @@
1
1
  import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
2
2
  import type { FilterQuery } from '@mikro-orm/core'
3
3
  import type { EntityManager } from '@mikro-orm/postgresql'
4
- import { BillRun } from '../data/entities'
4
+ import { BillingRun } from '../data/entities'
5
5
 
6
6
  export const metadata: WorkerMeta = {
7
7
  queue: 'billing-reap-stale-runs',
@@ -19,7 +19,7 @@ type HandlerContext = JobContext & {
19
19
  * Background — the Bill Run engine acquires a per-tenant Postgres
20
20
  * transaction-scoped advisory lock (`pg_try_advisory_xact_lock`). When the
21
21
  * holding transaction ends (commit / rollback / process death) the lock is
22
- * released automatically — so there is no lock leak — but the `BillRun`
22
+ * released automatically — so there is no lock leak — but the `BillingRun`
23
23
  * row's `status='running'` stays set forever, blocking restarts and
24
24
  * confusing the dashboard.
25
25
  *
@@ -38,10 +38,10 @@ const STALE_THRESHOLD_MS = 60 * 60 * 1000 // 1 hour
38
38
  export default async function handle(_job: QueuedJob, ctx: HandlerContext): Promise<void> {
39
39
  const em = ctx.resolve<EntityManager>('em')
40
40
  const threshold = new Date(Date.now() - STALE_THRESHOLD_MS)
41
- const stale = await em.find(BillRun, {
41
+ const stale = await em.find(BillingRun, {
42
42
  status: 'running',
43
43
  startedAt: { $lt: threshold },
44
- } satisfies FilterQuery<BillRun>)
44
+ } satisfies FilterQuery<BillingRun>)
45
45
  if (stale.length === 0) return
46
46
  const now = new Date()
47
47
  for (const run of stale) {