@dainamite/billing 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +658 -0
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/modules/billing/acl.js +20 -0
- package/dist/modules/billing/acl.js.map +7 -0
- package/dist/modules/billing/api/accounts/route.js +182 -0
- package/dist/modules/billing/api/accounts/route.js.map +7 -0
- package/dist/modules/billing/api/export/account/[id]/route.js +170 -0
- package/dist/modules/billing/api/export/account/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/route.js +85 -0
- package/dist/modules/billing/api/invoices/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/add-line/route.js +69 -0
- package/dist/modules/billing/api/invoices/add-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js +78 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/post/route.js +74 -0
- package/dist/modules/billing/api/invoices/post/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/remove-line/route.js +69 -0
- package/dist/modules/billing/api/invoices/remove-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/route.js +109 -0
- package/dist/modules/billing/api/invoices/route.js.map +7 -0
- package/dist/modules/billing/api/items/bulk/route.js +74 -0
- package/dist/modules/billing/api/items/bulk/route.js.map +7 -0
- package/dist/modules/billing/api/items/route.js +224 -0
- package/dist/modules/billing/api/items/route.js.map +7 -0
- package/dist/modules/billing/api/openapi.js +25 -0
- package/dist/modules/billing/api/openapi.js.map +7 -0
- package/dist/modules/billing/api/run-outcomes/route.js +122 -0
- package/dist/modules/billing/api/run-outcomes/route.js.map +7 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js +70 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js.map +7 -0
- package/dist/modules/billing/api/runs/route.js +143 -0
- package/dist/modules/billing/api/runs/route.js.map +7 -0
- package/dist/modules/billing/api/test-invoices/wipe/route.js +65 -0
- package/dist/modules/billing/api/test-invoices/wipe/route.js.map +7 -0
- package/dist/modules/billing/api/usage/route.js +138 -0
- package/dist/modules/billing/api/usage/route.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js +402 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js +82 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/page.js +173 -0
- package/dist/modules/billing/backend/billing/accounts/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/accounts/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js +500 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/page.js +187 -0
- package/dist/modules/billing/backend/billing/invoices/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/invoices/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.js +265 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/create/page.js +90 -0
- package/dist/modules/billing/backend/billing/items/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/items/create/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/page.js +208 -0
- package/dist/modules/billing/backend/billing/items/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/items/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js +293 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/page.js +341 -0
- package/dist/modules/billing/backend/billing/runs/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/runs/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/usage/create/page.js +100 -0
- package/dist/modules/billing/backend/billing/usage/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/usage/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/usage/create/page.meta.js.map +7 -0
- package/dist/modules/billing/ce.js +56 -0
- package/dist/modules/billing/ce.js.map +7 -0
- package/dist/modules/billing/cli.js +67 -0
- package/dist/modules/billing/cli.js.map +7 -0
- package/dist/modules/billing/commands/accounts.js +157 -0
- package/dist/modules/billing/commands/accounts.js.map +7 -0
- package/dist/modules/billing/commands/index.js +6 -0
- package/dist/modules/billing/commands/index.js.map +7 -0
- package/dist/modules/billing/commands/invoices.js +401 -0
- package/dist/modules/billing/commands/invoices.js.map +7 -0
- package/dist/modules/billing/commands/items.js +317 -0
- package/dist/modules/billing/commands/items.js.map +7 -0
- package/dist/modules/billing/commands/runs.js +123 -0
- package/dist/modules/billing/commands/runs.js.map +7 -0
- package/dist/modules/billing/commands/usage.js +88 -0
- package/dist/modules/billing/commands/usage.js.map +7 -0
- package/dist/modules/billing/components/AccountForm.js +420 -0
- package/dist/modules/billing/components/AccountForm.js.map +7 -0
- package/dist/modules/billing/components/DetailFields.js +37 -0
- package/dist/modules/billing/components/DetailFields.js.map +7 -0
- package/dist/modules/billing/components/ItemForm.js +440 -0
- package/dist/modules/billing/components/ItemForm.js.map +7 -0
- package/dist/modules/billing/components/LineFormDialog.js +195 -0
- package/dist/modules/billing/components/LineFormDialog.js.map +7 -0
- package/dist/modules/billing/components/UsageForm.js +262 -0
- package/dist/modules/billing/components/UsageForm.js.map +7 -0
- package/dist/modules/billing/data/entities.js +433 -0
- package/dist/modules/billing/data/entities.js.map +7 -0
- package/dist/modules/billing/data/entityIds.js +12 -0
- package/dist/modules/billing/data/entityIds.js.map +7 -0
- package/dist/modules/billing/data/validators.js +396 -0
- package/dist/modules/billing/data/validators.js.map +7 -0
- package/dist/modules/billing/di.js +6 -0
- package/dist/modules/billing/di.js.map +7 -0
- package/dist/modules/billing/events.js +35 -0
- package/dist/modules/billing/events.js.map +7 -0
- package/dist/modules/billing/i18n/en.json +336 -0
- package/dist/modules/billing/i18n/pl.json +336 -0
- package/dist/modules/billing/index.js +17 -0
- package/dist/modules/billing/index.js.map +7 -0
- package/dist/modules/billing/lib/billPeriod.js +101 -0
- package/dist/modules/billing/lib/billPeriod.js.map +7 -0
- package/dist/modules/billing/lib/billRunEngine.js +256 -0
- package/dist/modules/billing/lib/billRunEngine.js.map +7 -0
- package/dist/modules/billing/lib/billRunLabel.js +8 -0
- package/dist/modules/billing/lib/billRunLabel.js.map +7 -0
- package/dist/modules/billing/lib/idempotency.js +13 -0
- package/dist/modules/billing/lib/idempotency.js.map +7 -0
- package/dist/modules/billing/lib/invoiceStatus.js +38 -0
- package/dist/modules/billing/lib/invoiceStatus.js.map +7 -0
- package/dist/modules/billing/lib/invoiceWriter.js +153 -0
- package/dist/modules/billing/lib/invoiceWriter.js.map +7 -0
- package/dist/modules/billing/lib/itemSelector.js +77 -0
- package/dist/modules/billing/lib/itemSelector.js.map +7 -0
- package/dist/modules/billing/lib/seeds/demo.js +130 -0
- package/dist/modules/billing/lib/seeds/demo.js.map +7 -0
- package/dist/modules/billing/lib/tenantLock.js +32 -0
- package/dist/modules/billing/lib/tenantLock.js.map +7 -0
- package/dist/modules/billing/lib/usageRater.js +150 -0
- package/dist/modules/billing/lib/usageRater.js.map +7 -0
- package/dist/modules/billing/lib/usageRunner.js +82 -0
- package/dist/modules/billing/lib/usageRunner.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20260519000000_billing.js +144 -0
- package/dist/modules/billing/migrations/Migration20260519000000_billing.js.map +7 -0
- package/dist/modules/billing/setup.js +190 -0
- package/dist/modules/billing/setup.js.map +7 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js +47 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js.map +7 -0
- package/dist/modules/billing/translations.js +5 -0
- package/dist/modules/billing/translations.js.map +7 -0
- package/dist/modules/billing/workers/reap-stale-bill-runs.js +31 -0
- package/dist/modules/billing/workers/reap-stale-bill-runs.js.map +7 -0
- package/package.json +148 -0
- package/src/modules/billing/__integration__/TC-BILL-001.spec.ts +116 -0
- package/src/modules/billing/__integration__/TC-BILL-002.spec.ts +169 -0
- package/src/modules/billing/__integration__/TC-BILL-003.spec.ts +28 -0
- package/src/modules/billing/__integration__/meta.ts +4 -0
- package/src/modules/billing/__tests__/commands/invoices.test.ts +464 -0
- package/src/modules/billing/__tests__/commands/itemsBulk.test.ts +284 -0
- package/src/modules/billing/__tests__/lib/billPeriod.test.ts +131 -0
- package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +561 -0
- package/src/modules/billing/__tests__/lib/idempotency.test.ts +81 -0
- package/src/modules/billing/__tests__/lib/invoiceStatus.test.ts +97 -0
- package/src/modules/billing/__tests__/lib/invoiceWriter.test.ts +119 -0
- package/src/modules/billing/__tests__/lib/itemSelector.test.ts +192 -0
- package/src/modules/billing/__tests__/lib/seedsDemo.test.ts +86 -0
- package/src/modules/billing/__tests__/lib/tenantLock.test.ts +126 -0
- package/src/modules/billing/__tests__/lib/usageRater.test.ts +190 -0
- package/src/modules/billing/__tests__/package-isolation.test.ts +223 -0
- package/src/modules/billing/__tests__/setup.test.ts +107 -0
- package/src/modules/billing/__tests__/validators.test.ts +594 -0
- package/src/modules/billing/acl.ts +16 -0
- package/src/modules/billing/api/accounts/route.ts +198 -0
- package/src/modules/billing/api/export/account/[id]/route.ts +233 -0
- package/src/modules/billing/api/invoices/[id]/route.ts +143 -0
- package/src/modules/billing/api/invoices/add-line/route.ts +74 -0
- package/src/modules/billing/api/invoices/edit-line/route.ts +97 -0
- package/src/modules/billing/api/invoices/post/route.ts +97 -0
- package/src/modules/billing/api/invoices/remove-line/route.ts +75 -0
- package/src/modules/billing/api/invoices/route.ts +167 -0
- package/src/modules/billing/api/items/bulk/route.ts +92 -0
- package/src/modules/billing/api/items/route.ts +255 -0
- package/src/modules/billing/api/openapi.ts +33 -0
- package/src/modules/billing/api/run-outcomes/route.ts +149 -0
- package/src/modules/billing/api/runs/retry-failed/route.ts +88 -0
- package/src/modules/billing/api/runs/route.ts +170 -0
- package/src/modules/billing/api/test-invoices/wipe/route.ts +87 -0
- package/src/modules/billing/api/usage/route.ts +148 -0
- package/src/modules/billing/backend/billing/accounts/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +574 -0
- package/src/modules/billing/backend/billing/accounts/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/accounts/create/page.tsx +101 -0
- package/src/modules/billing/backend/billing/accounts/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/accounts/page.tsx +222 -0
- package/src/modules/billing/backend/billing/invoices/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +613 -0
- package/src/modules/billing/backend/billing/invoices/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/invoices/page.tsx +245 -0
- package/src/modules/billing/backend/billing/items/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/items/[id]/page.tsx +366 -0
- package/src/modules/billing/backend/billing/items/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/items/create/page.tsx +110 -0
- package/src/modules/billing/backend/billing/items/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/items/page.tsx +272 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.tsx +404 -0
- package/src/modules/billing/backend/billing/runs/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/runs/page.tsx +426 -0
- package/src/modules/billing/backend/billing/usage/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/usage/create/page.tsx +120 -0
- package/src/modules/billing/ce.ts +74 -0
- package/src/modules/billing/cli.ts +89 -0
- package/src/modules/billing/commands/accounts.ts +205 -0
- package/src/modules/billing/commands/index.ts +13 -0
- package/src/modules/billing/commands/invoices.ts +542 -0
- package/src/modules/billing/commands/items.ts +438 -0
- package/src/modules/billing/commands/runs.ts +166 -0
- package/src/modules/billing/commands/usage.ts +118 -0
- package/src/modules/billing/components/AccountForm.tsx +517 -0
- package/src/modules/billing/components/DetailFields.tsx +60 -0
- package/src/modules/billing/components/ItemForm.tsx +558 -0
- package/src/modules/billing/components/LineFormDialog.tsx +253 -0
- package/src/modules/billing/components/UsageForm.tsx +326 -0
- package/src/modules/billing/data/entities.ts +533 -0
- package/src/modules/billing/data/entityIds.ts +25 -0
- package/src/modules/billing/data/validators.ts +562 -0
- package/src/modules/billing/di.ts +14 -0
- package/src/modules/billing/events.ts +47 -0
- package/src/modules/billing/i18n/en.json +336 -0
- package/src/modules/billing/i18n/pl.json +336 -0
- package/src/modules/billing/index.ts +22 -0
- package/src/modules/billing/lib/billPeriod.ts +181 -0
- package/src/modules/billing/lib/billRunEngine.ts +416 -0
- package/src/modules/billing/lib/billRunLabel.ts +14 -0
- package/src/modules/billing/lib/idempotency.ts +46 -0
- package/src/modules/billing/lib/invoiceStatus.ts +61 -0
- package/src/modules/billing/lib/invoiceWriter.ts +260 -0
- package/src/modules/billing/lib/itemSelector.ts +154 -0
- package/src/modules/billing/lib/seeds/demo.ts +199 -0
- package/src/modules/billing/lib/tenantLock.ts +87 -0
- package/src/modules/billing/lib/usageRater.ts +250 -0
- package/src/modules/billing/lib/usageRunner.ts +189 -0
- package/src/modules/billing/migrations/Migration20260519000000_billing.ts +175 -0
- package/src/modules/billing/setup.ts +252 -0
- package/src/modules/billing/subscribers/payment-captured-to-paid.ts +89 -0
- package/src/modules/billing/translations.ts +9 -0
- package/src/modules/billing/workers/reap-stale-bill-runs.ts +57 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
// ─── Enums ───────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export const BILLING_ITEM_TYPES = ['one_time', 'recurring', 'usage'] as const
|
|
6
|
+
export type BillingItemType = (typeof BILLING_ITEM_TYPES)[number]
|
|
7
|
+
|
|
8
|
+
export const BILL_CYCLES = ['weekly', 'monthly', 'quarterly', 'annually'] as const
|
|
9
|
+
export type BillCycle = (typeof BILL_CYCLES)[number]
|
|
10
|
+
|
|
11
|
+
export const BILL_RUN_TRIGGERS = ['schedule', 'manual'] as const
|
|
12
|
+
export type BillRunTrigger = (typeof BILL_RUN_TRIGGERS)[number]
|
|
13
|
+
|
|
14
|
+
export const BILL_RUN_STATUSES = ['running', 'completed', 'partial_failure', 'failed'] as const
|
|
15
|
+
export type BillRunStatus = (typeof BILL_RUN_STATUSES)[number]
|
|
16
|
+
|
|
17
|
+
export const BILL_RUN_OUTCOME_STATUSES = [
|
|
18
|
+
'success',
|
|
19
|
+
'success_with_warnings',
|
|
20
|
+
'skipped_existing_draft',
|
|
21
|
+
'failed',
|
|
22
|
+
] as const
|
|
23
|
+
export type BillRunOutcomeStatus = (typeof BILL_RUN_OUTCOME_STATUSES)[number]
|
|
24
|
+
|
|
25
|
+
export const DRAFT_INVOICE_EDIT_ACTIONS = ['line_added', 'line_removed', 'line_edited'] as const
|
|
26
|
+
export type DraftInvoiceEditAction = (typeof DRAFT_INVOICE_EDIT_ACTIONS)[number]
|
|
27
|
+
|
|
28
|
+
export const INVOICE_STATUSES = ['draft', 'posted', 'paid', 'void'] as const
|
|
29
|
+
export type InvoiceStatus = (typeof INVOICE_STATUSES)[number]
|
|
30
|
+
|
|
31
|
+
// `mon`–`sun` for weekly cycles; 1–28 (as text) for the others.
|
|
32
|
+
// Why text for numeric anchors: the column type stays uniform across cycles
|
|
33
|
+
// and we never do arithmetic on the raw value (the engine parses it on use).
|
|
34
|
+
const WEEKLY_ANCHORS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const
|
|
35
|
+
|
|
36
|
+
export const billCycleAnchorSchema = z
|
|
37
|
+
.string()
|
|
38
|
+
.min(1)
|
|
39
|
+
.refine(
|
|
40
|
+
(val) => {
|
|
41
|
+
if ((WEEKLY_ANCHORS as readonly string[]).includes(val.toLowerCase())) return true
|
|
42
|
+
const n = Number.parseInt(val, 10)
|
|
43
|
+
return Number.isInteger(n) && n >= 1 && n <= 28
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
message:
|
|
47
|
+
'bill_cycle_anchor must be 1–28 (monthly/quarterly/annually) or mon–sun (weekly)',
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// ─── rate_json — pricing definitions ─────────────────────────────
|
|
52
|
+
//
|
|
53
|
+
// Validated at API ingress so the engine can trust the shape downstream.
|
|
54
|
+
// Negative amounts are intentionally allowed only on `one_time` (refunds /
|
|
55
|
+
// credits / proration credits). Recurring and usage unit prices MUST be
|
|
56
|
+
// non-negative — discounts on those are modeled as separate `one_time`
|
|
57
|
+
// credit items.
|
|
58
|
+
|
|
59
|
+
const numericNonNegative = z.number().finite().nonnegative()
|
|
60
|
+
const numericAny = z.number().finite()
|
|
61
|
+
|
|
62
|
+
export const rateJsonOneTime = z
|
|
63
|
+
.object({ amount: numericAny })
|
|
64
|
+
.strict()
|
|
65
|
+
|
|
66
|
+
export const rateJsonRecurring = z
|
|
67
|
+
.object({ unit_price: numericNonNegative })
|
|
68
|
+
.strict()
|
|
69
|
+
|
|
70
|
+
export const rateJsonUsageSimple = z
|
|
71
|
+
.object({ unit_price: numericNonNegative })
|
|
72
|
+
.strict()
|
|
73
|
+
|
|
74
|
+
const tierWithUnitPrice = z
|
|
75
|
+
.object({
|
|
76
|
+
up_to: z.union([z.number().int().positive(), z.null()]),
|
|
77
|
+
unit_price: numericNonNegative,
|
|
78
|
+
})
|
|
79
|
+
.strict()
|
|
80
|
+
|
|
81
|
+
const tierWithFlatAmount = z
|
|
82
|
+
.object({
|
|
83
|
+
up_to: z.union([z.number().int().positive(), z.null()]),
|
|
84
|
+
flat_amount: numericNonNegative,
|
|
85
|
+
})
|
|
86
|
+
.strict()
|
|
87
|
+
|
|
88
|
+
// All tiers must use the same key shape (consistent with the model). Mixing
|
|
89
|
+
// `unit_price` and `flat_amount` across tiers is rejected — see spec
|
|
90
|
+
// validation rules.
|
|
91
|
+
const tiersUnitPrice = z
|
|
92
|
+
.array(tierWithUnitPrice)
|
|
93
|
+
.min(1, 'tiers must be a non-empty array')
|
|
94
|
+
const tiersFlatAmount = z
|
|
95
|
+
.array(tierWithFlatAmount)
|
|
96
|
+
.min(1, 'tiers must be a non-empty array')
|
|
97
|
+
|
|
98
|
+
function refineTierProgression<T extends { up_to: number | null }>(
|
|
99
|
+
tiers: T[],
|
|
100
|
+
ctx: z.RefinementCtx,
|
|
101
|
+
): void {
|
|
102
|
+
const openTopCount = tiers.filter((t) => t.up_to === null).length
|
|
103
|
+
if (openTopCount !== 1) {
|
|
104
|
+
ctx.addIssue({
|
|
105
|
+
code: z.ZodIssueCode.custom,
|
|
106
|
+
message: 'tiers must contain exactly one open-ended top tier (up_to: null)',
|
|
107
|
+
})
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
if (tiers[tiers.length - 1].up_to !== null) {
|
|
111
|
+
ctx.addIssue({
|
|
112
|
+
code: z.ZodIssueCode.custom,
|
|
113
|
+
message: 'the open-ended top tier (up_to: null) must be the last entry',
|
|
114
|
+
})
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
for (let i = 1; i < tiers.length - 1; i++) {
|
|
118
|
+
const prev = tiers[i - 1].up_to
|
|
119
|
+
const curr = tiers[i].up_to
|
|
120
|
+
if (prev !== null && curr !== null && curr <= prev) {
|
|
121
|
+
ctx.addIssue({
|
|
122
|
+
code: z.ZodIssueCode.custom,
|
|
123
|
+
message: `up_to values must be strictly increasing across tiers (failed at index ${i})`,
|
|
124
|
+
})
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const rateJsonUsageVolume = z
|
|
131
|
+
.object({
|
|
132
|
+
model: z.literal('volume'),
|
|
133
|
+
tiers: tiersUnitPrice,
|
|
134
|
+
})
|
|
135
|
+
.strict()
|
|
136
|
+
.superRefine((val, ctx) => refineTierProgression(val.tiers, ctx))
|
|
137
|
+
|
|
138
|
+
export const rateJsonUsageGraduated = z
|
|
139
|
+
.object({
|
|
140
|
+
model: z.literal('graduated'),
|
|
141
|
+
tiers: tiersUnitPrice,
|
|
142
|
+
})
|
|
143
|
+
.strict()
|
|
144
|
+
.superRefine((val, ctx) => refineTierProgression(val.tiers, ctx))
|
|
145
|
+
|
|
146
|
+
export const rateJsonUsageFlat = z
|
|
147
|
+
.object({
|
|
148
|
+
model: z.literal('flat'),
|
|
149
|
+
tiers: tiersFlatAmount,
|
|
150
|
+
})
|
|
151
|
+
.strict()
|
|
152
|
+
.superRefine((val, ctx) => refineTierProgression(val.tiers, ctx))
|
|
153
|
+
|
|
154
|
+
export const rateJsonUsageTiered = z.discriminatedUnion('model', [
|
|
155
|
+
rateJsonUsageVolume,
|
|
156
|
+
rateJsonUsageGraduated,
|
|
157
|
+
rateJsonUsageFlat,
|
|
158
|
+
])
|
|
159
|
+
|
|
160
|
+
export const rateJsonUsage = z.union([rateJsonUsageSimple, rateJsonUsageTiered])
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validate a `rate_json` payload against the rules for a given Billing Item
|
|
164
|
+
* `type`. Returns the parsed value or throws a `ZodError` whose message
|
|
165
|
+
* surfaces the specific shape violation.
|
|
166
|
+
*/
|
|
167
|
+
export function validateRateJson(
|
|
168
|
+
type: BillingItemType,
|
|
169
|
+
rateJson: unknown,
|
|
170
|
+
):
|
|
171
|
+
| z.infer<typeof rateJsonOneTime>
|
|
172
|
+
| z.infer<typeof rateJsonRecurring>
|
|
173
|
+
| z.infer<typeof rateJsonUsage> {
|
|
174
|
+
switch (type) {
|
|
175
|
+
case 'one_time':
|
|
176
|
+
return rateJsonOneTime.parse(rateJson)
|
|
177
|
+
case 'recurring':
|
|
178
|
+
return rateJsonRecurring.parse(rateJson)
|
|
179
|
+
case 'usage':
|
|
180
|
+
return rateJsonUsage.parse(rateJson)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Invoice address (BillingAccount.invoice_address) ────────────
|
|
185
|
+
|
|
186
|
+
export const invoiceAddressSchema = z
|
|
187
|
+
.object({
|
|
188
|
+
line1: z.string().min(1),
|
|
189
|
+
line2: z.string().optional(),
|
|
190
|
+
city: z.string().min(1),
|
|
191
|
+
postal_code: z.string().min(1),
|
|
192
|
+
country: z.string().min(2).max(3), // ISO 3166-1 alpha-2 or alpha-3
|
|
193
|
+
})
|
|
194
|
+
.strict()
|
|
195
|
+
|
|
196
|
+
export type InvoiceAddress = z.infer<typeof invoiceAddressSchema>
|
|
197
|
+
|
|
198
|
+
// ─── Scope (injected by withScopedPayload at API ingress) ────────
|
|
199
|
+
|
|
200
|
+
const scopedSchema = z.object({
|
|
201
|
+
tenantId: z.string().uuid(),
|
|
202
|
+
organizationId: z.string().uuid(),
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// ─── List pagination (shared shape) ──────────────────────────────
|
|
206
|
+
|
|
207
|
+
const listPaginationBase = {
|
|
208
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
209
|
+
pageSize: z.coerce.number().int().min(1).max(100).default(50),
|
|
210
|
+
sortField: z.string().optional(),
|
|
211
|
+
sortDir: z.enum(['asc', 'desc']).optional(),
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── BillingAccount payloads ─────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
export const billingAccountCreateSchema = scopedSchema.extend({
|
|
217
|
+
customerId: z.string().min(1).max(255),
|
|
218
|
+
name: z.string().trim().min(1).max(255),
|
|
219
|
+
currencyCode: z.string().trim().toUpperCase().length(3),
|
|
220
|
+
billCycle: z.enum(BILL_CYCLES),
|
|
221
|
+
billCycleAnchor: billCycleAnchorSchema,
|
|
222
|
+
invoiceEmail: z.string().trim().email().max(320),
|
|
223
|
+
invoiceLanguage: z.string().trim().toLowerCase().min(2).max(5),
|
|
224
|
+
// Nullable + optional — consistent with the update schema, and the
|
|
225
|
+
// cpq-billing-connector's shell-account seed sends `taxId: null`
|
|
226
|
+
// for customers with no tax id.
|
|
227
|
+
taxId: z.string().trim().max(64).nullable().optional(),
|
|
228
|
+
invoiceAddress: invoiceAddressSchema,
|
|
229
|
+
nextBillDate: z.coerce.date(),
|
|
230
|
+
lastBillDate: z.coerce.date().optional(),
|
|
231
|
+
isActive: z.boolean().optional(),
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
export type BillingAccountCreateInput = z.infer<typeof billingAccountCreateSchema>
|
|
235
|
+
|
|
236
|
+
export const billingAccountUpdateSchema = scopedSchema.extend({
|
|
237
|
+
id: z.string().uuid(),
|
|
238
|
+
name: z.string().trim().min(1).max(255).optional(),
|
|
239
|
+
billCycle: z.enum(BILL_CYCLES).optional(),
|
|
240
|
+
billCycleAnchor: billCycleAnchorSchema.optional(),
|
|
241
|
+
invoiceEmail: z.string().trim().email().max(320).optional(),
|
|
242
|
+
invoiceLanguage: z.string().trim().toLowerCase().min(2).max(5).optional(),
|
|
243
|
+
taxId: z.string().trim().max(64).nullable().optional(),
|
|
244
|
+
invoiceAddress: invoiceAddressSchema.optional(),
|
|
245
|
+
nextBillDate: z.coerce.date().optional(),
|
|
246
|
+
lastBillDate: z.coerce.date().nullable().optional(),
|
|
247
|
+
isActive: z.boolean().optional(),
|
|
248
|
+
// `currencyCode` is intentionally NOT updatable — currency is frozen at
|
|
249
|
+
// account create time so historical invoices keep their currency context.
|
|
250
|
+
// `customerId` is also immutable; create a new account if the customer
|
|
251
|
+
// FK changes.
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
export type BillingAccountUpdateInput = z.infer<typeof billingAccountUpdateSchema>
|
|
255
|
+
|
|
256
|
+
export const billingAccountDeleteSchema = scopedSchema.extend({
|
|
257
|
+
id: z.string().uuid(),
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
export type BillingAccountDeleteInput = z.infer<typeof billingAccountDeleteSchema>
|
|
261
|
+
|
|
262
|
+
export const billingAccountListQuerySchema = z
|
|
263
|
+
.object({
|
|
264
|
+
...listPaginationBase,
|
|
265
|
+
id: z.string().uuid().optional(),
|
|
266
|
+
customerId: z.string().optional(),
|
|
267
|
+
currencyCode: z.string().optional(),
|
|
268
|
+
billCycle: z.enum(BILL_CYCLES).optional(),
|
|
269
|
+
isActive: z.coerce.boolean().optional(),
|
|
270
|
+
search: z.string().optional(),
|
|
271
|
+
})
|
|
272
|
+
.passthrough()
|
|
273
|
+
|
|
274
|
+
export type BillingAccountListQuery = z.infer<typeof billingAccountListQuerySchema>
|
|
275
|
+
|
|
276
|
+
// ─── BillingItem payloads ────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
const ratePayloadByType = z
|
|
279
|
+
.object({
|
|
280
|
+
type: z.enum(BILLING_ITEM_TYPES),
|
|
281
|
+
rateJson: z.unknown(),
|
|
282
|
+
})
|
|
283
|
+
.superRefine((val, ctx) => {
|
|
284
|
+
try {
|
|
285
|
+
validateRateJson(val.type, val.rateJson)
|
|
286
|
+
} catch (err) {
|
|
287
|
+
ctx.addIssue({
|
|
288
|
+
code: z.ZodIssueCode.custom,
|
|
289
|
+
path: ['rateJson'],
|
|
290
|
+
message: err instanceof Error ? err.message : 'rate_json is invalid for this type',
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
export const billingItemCreateSchema = scopedSchema
|
|
296
|
+
.extend({
|
|
297
|
+
billAccountId: z.string().uuid(),
|
|
298
|
+
type: z.enum(BILLING_ITEM_TYPES),
|
|
299
|
+
billStartDate: z.coerce.date(),
|
|
300
|
+
billEndDate: z.coerce.date().nullable().optional(),
|
|
301
|
+
description: z.string().trim().min(1).max(500),
|
|
302
|
+
rateJson: z.unknown(), // shape validated via the refine below
|
|
303
|
+
uomCode: z.string().trim().max(64).optional(),
|
|
304
|
+
subscriptionId: z.string().trim().max(255).optional(),
|
|
305
|
+
subscriptionItemId: z.string().trim().max(255).optional(),
|
|
306
|
+
// `source_ref` is the idempotency key. NULL → no dedup, always create.
|
|
307
|
+
sourceRef: z.string().trim().min(1).max(255).optional(),
|
|
308
|
+
// currency_mismatch and billed_to_date are system-managed, never set by
|
|
309
|
+
// clients on create.
|
|
310
|
+
})
|
|
311
|
+
.superRefine((val, ctx) => {
|
|
312
|
+
try {
|
|
313
|
+
validateRateJson(val.type, val.rateJson)
|
|
314
|
+
} catch (err) {
|
|
315
|
+
const message = err instanceof Error ? err.message : 'rate_json is invalid for this type'
|
|
316
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['rateJson'], message })
|
|
317
|
+
}
|
|
318
|
+
if (val.type === 'usage' && !val.uomCode) {
|
|
319
|
+
ctx.addIssue({
|
|
320
|
+
code: z.ZodIssueCode.custom,
|
|
321
|
+
path: ['uomCode'],
|
|
322
|
+
message: 'uomCode is required for type=usage',
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
export type BillingItemCreateInput = z.infer<typeof billingItemCreateSchema>
|
|
328
|
+
|
|
329
|
+
// ─── Bulk item create (Phase 4g) ─────────────────────────────────
|
|
330
|
+
//
|
|
331
|
+
// One scope envelope, N item payloads. The CPQ connector activating
|
|
332
|
+
// a large subscription used to fire one `billing.items.create` per
|
|
333
|
+
// charge — bulk-create collapses that into a single command with a
|
|
334
|
+
// batched `source_ref` idempotency check + single flush.
|
|
335
|
+
|
|
336
|
+
const billingItemBulkEntrySchema = z
|
|
337
|
+
.object({
|
|
338
|
+
billAccountId: z.string().uuid(),
|
|
339
|
+
type: z.enum(BILLING_ITEM_TYPES),
|
|
340
|
+
billStartDate: z.coerce.date(),
|
|
341
|
+
billEndDate: z.coerce.date().nullable().optional(),
|
|
342
|
+
description: z.string().trim().min(1).max(500),
|
|
343
|
+
rateJson: z.unknown(),
|
|
344
|
+
uomCode: z.string().trim().max(64).optional(),
|
|
345
|
+
subscriptionId: z.string().trim().max(255).optional(),
|
|
346
|
+
subscriptionItemId: z.string().trim().max(255).optional(),
|
|
347
|
+
sourceRef: z.string().trim().min(1).max(255).optional(),
|
|
348
|
+
})
|
|
349
|
+
.superRefine((val, ctx) => {
|
|
350
|
+
try {
|
|
351
|
+
validateRateJson(val.type, val.rateJson)
|
|
352
|
+
} catch (err) {
|
|
353
|
+
const message = err instanceof Error ? err.message : 'rate_json is invalid for this type'
|
|
354
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['rateJson'], message })
|
|
355
|
+
}
|
|
356
|
+
if (val.type === 'usage' && !val.uomCode) {
|
|
357
|
+
ctx.addIssue({
|
|
358
|
+
code: z.ZodIssueCode.custom,
|
|
359
|
+
path: ['uomCode'],
|
|
360
|
+
message: 'uomCode is required for type=usage',
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
export type BillingItemBulkEntry = z.infer<typeof billingItemBulkEntrySchema>
|
|
366
|
+
|
|
367
|
+
export const billingItemBulkCreateSchema = scopedSchema.extend({
|
|
368
|
+
// Cap at 500 per call — keeps a single bulk transaction bounded.
|
|
369
|
+
// A CPQ subscription with >500 charges should be split by the
|
|
370
|
+
// caller (the connector); 500 is already far past any realistic
|
|
371
|
+
// single-subscription charge count.
|
|
372
|
+
items: z.array(billingItemBulkEntrySchema).min(1).max(500),
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
export type BillingItemBulkCreateInput = z.infer<typeof billingItemBulkCreateSchema>
|
|
376
|
+
|
|
377
|
+
export const billingItemUpdateSchema = scopedSchema.extend({
|
|
378
|
+
id: z.string().uuid(),
|
|
379
|
+
// `type` is immutable — changing one_time → recurring would silently
|
|
380
|
+
// change billing semantics. Cancel the old item, create a new one.
|
|
381
|
+
billStartDate: z.coerce.date().optional(),
|
|
382
|
+
billEndDate: z.coerce.date().nullable().optional(),
|
|
383
|
+
description: z.string().trim().min(1).max(500).optional(),
|
|
384
|
+
rateJson: z.unknown().optional(),
|
|
385
|
+
uomCode: z.string().trim().max(64).nullable().optional(),
|
|
386
|
+
subscriptionId: z.string().trim().max(255).nullable().optional(),
|
|
387
|
+
subscriptionItemId: z.string().trim().max(255).nullable().optional(),
|
|
388
|
+
// `source_ref` immutable — it's the idempotency identity of the item.
|
|
389
|
+
isActive: z.boolean().optional(),
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
export type BillingItemUpdateInput = z.infer<typeof billingItemUpdateSchema>
|
|
393
|
+
|
|
394
|
+
export const billingItemDeleteSchema = scopedSchema.extend({
|
|
395
|
+
id: z.string().uuid(),
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
export type BillingItemDeleteInput = z.infer<typeof billingItemDeleteSchema>
|
|
399
|
+
|
|
400
|
+
export const billingItemListQuerySchema = z
|
|
401
|
+
.object({
|
|
402
|
+
...listPaginationBase,
|
|
403
|
+
id: z.string().uuid().optional(),
|
|
404
|
+
search: z.string().optional(),
|
|
405
|
+
billAccountId: z.string().uuid().optional(),
|
|
406
|
+
type: z.enum(BILLING_ITEM_TYPES).optional(),
|
|
407
|
+
subscriptionId: z.string().optional(),
|
|
408
|
+
isActive: z.coerce.boolean().optional(),
|
|
409
|
+
activeAt: z.coerce.date().optional(), // bill_start_date <= X AND (bill_end_date IS NULL OR bill_end_date >= X)
|
|
410
|
+
})
|
|
411
|
+
.passthrough()
|
|
412
|
+
|
|
413
|
+
export type BillingItemListQuery = z.infer<typeof billingItemListQuerySchema>
|
|
414
|
+
|
|
415
|
+
// ─── BillingAccountUsage payloads ────────────────────────────────
|
|
416
|
+
|
|
417
|
+
export const billingUsageCreateSchema = scopedSchema.extend({
|
|
418
|
+
billAccountId: z.string().uuid(),
|
|
419
|
+
uomCode: z.string().trim().min(1).max(64),
|
|
420
|
+
// `quantity` arrives as a number from JSON; MikroORM stores it as
|
|
421
|
+
// numeric(18,4). The service coerces to a string at persist time.
|
|
422
|
+
quantity: z.number().finite().nonnegative(),
|
|
423
|
+
periodStart: z.coerce.date(),
|
|
424
|
+
periodEnd: z.coerce.date(),
|
|
425
|
+
lineDescription: z.string().trim().min(1).max(500).optional(),
|
|
426
|
+
sourceRef: z.string().trim().min(1).max(255).optional(),
|
|
427
|
+
}).superRefine((val, ctx) => {
|
|
428
|
+
if (val.periodEnd < val.periodStart) {
|
|
429
|
+
ctx.addIssue({
|
|
430
|
+
code: z.ZodIssueCode.custom,
|
|
431
|
+
path: ['periodEnd'],
|
|
432
|
+
message: 'periodEnd must be on or after periodStart',
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
export type BillingUsageCreateInput = z.infer<typeof billingUsageCreateSchema>
|
|
438
|
+
|
|
439
|
+
export const billingUsageListQuerySchema = z
|
|
440
|
+
.object({
|
|
441
|
+
...listPaginationBase,
|
|
442
|
+
billAccountId: z.string().uuid().optional(),
|
|
443
|
+
uomCode: z.string().optional(),
|
|
444
|
+
rated: z.coerce.boolean().optional(), // true → already rated; false → unrated
|
|
445
|
+
periodEndFrom: z.coerce.date().optional(),
|
|
446
|
+
periodEndTo: z.coerce.date().optional(),
|
|
447
|
+
})
|
|
448
|
+
.passthrough()
|
|
449
|
+
|
|
450
|
+
export type BillingUsageListQuery = z.infer<typeof billingUsageListQuerySchema>
|
|
451
|
+
|
|
452
|
+
// Suppress unused-export TS warning — `ratePayloadByType` is reserved for
|
|
453
|
+
// reuse by a future per-tenant rate-validation CLI tool.
|
|
454
|
+
void ratePayloadByType
|
|
455
|
+
|
|
456
|
+
// ─── Bill Run trigger payloads (Phase 2) ─────────────────────────
|
|
457
|
+
|
|
458
|
+
export const BILL_RUN_MODES = ['real', 'test', 'dry'] as const
|
|
459
|
+
export type BillRunMode = (typeof BILL_RUN_MODES)[number]
|
|
460
|
+
|
|
461
|
+
export const billingRunTriggerSchema = scopedSchema.extend({
|
|
462
|
+
// `triggeredBy` is filled in by the route layer (always 'manual' for
|
|
463
|
+
// API triggers; the cron worker sets 'schedule'). The client cannot
|
|
464
|
+
// forge a 'schedule' trigger — the field is intentionally omitted
|
|
465
|
+
// from the input shape.
|
|
466
|
+
mode: z.enum(BILL_RUN_MODES).default('real'),
|
|
467
|
+
asOfDate: z.coerce.date().optional(),
|
|
468
|
+
scopedAccountIds: z.array(z.string().uuid()).optional(),
|
|
469
|
+
catchUp: z.boolean().optional(),
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
export type BillingRunTriggerInput = z.infer<typeof billingRunTriggerSchema>
|
|
473
|
+
|
|
474
|
+
export const billingRunRetryFailedSchema = scopedSchema.extend({
|
|
475
|
+
billRunId: z.string().uuid(),
|
|
476
|
+
mode: z.enum(BILL_RUN_MODES).default('real'),
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
export type BillingRunRetryFailedInput = z.infer<typeof billingRunRetryFailedSchema>
|
|
480
|
+
|
|
481
|
+
export const billingRunListQuerySchema = z
|
|
482
|
+
.object({
|
|
483
|
+
...listPaginationBase,
|
|
484
|
+
id: z.string().uuid().optional(),
|
|
485
|
+
search: z.string().optional(),
|
|
486
|
+
status: z.enum(BILL_RUN_STATUSES).optional(),
|
|
487
|
+
triggeredBy: z.enum(BILL_RUN_TRIGGERS).optional(),
|
|
488
|
+
parentRunId: z.string().uuid().optional(),
|
|
489
|
+
asOfDateFrom: z.coerce.date().optional(),
|
|
490
|
+
asOfDateTo: z.coerce.date().optional(),
|
|
491
|
+
})
|
|
492
|
+
.passthrough()
|
|
493
|
+
|
|
494
|
+
export type BillingRunListQuery = z.infer<typeof billingRunListQuerySchema>
|
|
495
|
+
|
|
496
|
+
// ─── Invoice post (Phase 4) ──────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
export const billingInvoicePostSchema = scopedSchema.extend({
|
|
499
|
+
invoiceId: z.string().uuid(),
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
export type BillingInvoicePostInput = z.infer<typeof billingInvoicePostSchema>
|
|
503
|
+
|
|
504
|
+
// ─── Draft invoice edits (Phase 4b) ──────────────────────────────
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Fields the operator may change on a draft invoice line. Per spec:
|
|
508
|
+
* "operator CAN: add lines, remove lines, edit line description /
|
|
509
|
+
* quantity / unit_price / amount". `taxRate` is owned by core/sales
|
|
510
|
+
* tax service and `currencyCode` is frozen at account create — both
|
|
511
|
+
* intentionally absent from this schema.
|
|
512
|
+
*/
|
|
513
|
+
const lineEditableFields = z.object({
|
|
514
|
+
description: z.string().trim().min(1).max(500).optional(),
|
|
515
|
+
quantity: z.coerce.number().finite().nonnegative().optional(),
|
|
516
|
+
unitPriceNet: z.coerce.number().finite().optional(),
|
|
517
|
+
// `totalNetAmount` is operator-overridable for the "I know better
|
|
518
|
+
// than `unit_price × quantity`" cases (rare; mostly for negative
|
|
519
|
+
// credits or rounding fix-ups). When provided, it short-circuits
|
|
520
|
+
// the auto-compute path.
|
|
521
|
+
totalNetAmount: z.coerce.number().finite().optional(),
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
export const billingInvoiceEditLineSchema = scopedSchema.extend({
|
|
525
|
+
invoiceId: z.string().uuid(),
|
|
526
|
+
invoiceLineId: z.string().uuid(),
|
|
527
|
+
changes: lineEditableFields,
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
export type BillingInvoiceEditLineInput = z.infer<typeof billingInvoiceEditLineSchema>
|
|
531
|
+
|
|
532
|
+
export const billingInvoiceAddLineSchema = scopedSchema.extend({
|
|
533
|
+
invoiceId: z.string().uuid(),
|
|
534
|
+
description: z.string().trim().min(1).max(500),
|
|
535
|
+
quantity: z.coerce.number().finite().nonnegative(),
|
|
536
|
+
unitPriceNet: z.coerce.number().finite(),
|
|
537
|
+
// Optional metadata for traceability — typically operators leave
|
|
538
|
+
// `billing_item_id` blank because ad-hoc lines aren't tied to a
|
|
539
|
+
// Billing Item.
|
|
540
|
+
billingItemId: z.string().uuid().optional(),
|
|
541
|
+
billingType: z.enum(BILLING_ITEM_TYPES).optional(),
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
export type BillingInvoiceAddLineInput = z.infer<typeof billingInvoiceAddLineSchema>
|
|
545
|
+
|
|
546
|
+
export const billingInvoiceRemoveLineSchema = scopedSchema.extend({
|
|
547
|
+
invoiceId: z.string().uuid(),
|
|
548
|
+
invoiceLineId: z.string().uuid(),
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
export type BillingInvoiceRemoveLineInput = z.infer<typeof billingInvoiceRemoveLineSchema>
|
|
552
|
+
|
|
553
|
+
// ─── Test-invoices wipe (Phase 4b) ───────────────────────────────
|
|
554
|
+
|
|
555
|
+
export const billingWipeTestInvoicesSchema = scopedSchema.extend({
|
|
556
|
+
/** Optional — when set, wipes only test invoices from this run. */
|
|
557
|
+
billRunId: z.string().uuid().optional(),
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
export type BillingWipeTestInvoicesInput = z.infer<typeof billingWipeTestInvoicesSchema>
|
|
561
|
+
|
|
562
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AppContainer } from '@open-mercato/shared/lib/di/container'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Billing module DI registrar.
|
|
5
|
+
*
|
|
6
|
+
* Phase 0 ships no services — entities, ACL, and tenant setup only. The
|
|
7
|
+
* REST API surface (Phase 1) and Bill Run engine (Phase 2) bring services
|
|
8
|
+
* here. Keeping the file in place so the auto-discovery generator picks
|
|
9
|
+
* up `billing` as a DI-aware module without surprise on the first new
|
|
10
|
+
* registration.
|
|
11
|
+
*/
|
|
12
|
+
export function register(_container: AppContainer): void {
|
|
13
|
+
// intentionally empty — see file header
|
|
14
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createModuleEvents } from '@open-mercato/shared/modules/events'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Billing module events.
|
|
5
|
+
*
|
|
6
|
+
* Emission contract:
|
|
7
|
+
* - `billing.invoice.posted` fires once per posted draft AFTER the post
|
|
8
|
+
* transaction commits (sequence number assigned, status set to `posted`
|
|
9
|
+
* in core/sales). Consumed by mailers / accounting bridges / analytics.
|
|
10
|
+
* - `billing.invoice.line_posted` fires once per line on a posted invoice
|
|
11
|
+
* so analytics can aggregate without scanning core/sales lines directly.
|
|
12
|
+
* - `billing.run.completed` fires once per Bill Run after all per-account
|
|
13
|
+
* transactions finished (status is one of `completed` / `partial_failure`
|
|
14
|
+
* / `failed`). Useful for "did the nightly run go OK?" alerts.
|
|
15
|
+
*/
|
|
16
|
+
const events = [
|
|
17
|
+
{
|
|
18
|
+
id: 'billing.invoice.posted',
|
|
19
|
+
label: 'Billing Invoice Posted',
|
|
20
|
+
entity: 'invoice',
|
|
21
|
+
category: 'lifecycle',
|
|
22
|
+
clientBroadcast: true,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'billing.invoice.line_posted',
|
|
26
|
+
label: 'Billing Invoice Line Posted',
|
|
27
|
+
entity: 'invoice_line',
|
|
28
|
+
category: 'lifecycle',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'billing.run.completed',
|
|
32
|
+
label: 'Bill Run Completed',
|
|
33
|
+
entity: 'bill_run',
|
|
34
|
+
category: 'lifecycle',
|
|
35
|
+
clientBroadcast: true,
|
|
36
|
+
},
|
|
37
|
+
] as const
|
|
38
|
+
|
|
39
|
+
export const eventsConfig = createModuleEvents({
|
|
40
|
+
moduleId: 'billing',
|
|
41
|
+
events,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export const emitBillingEvent = eventsConfig.emit
|
|
45
|
+
export type BillingEventId = (typeof events)[number]['id']
|
|
46
|
+
|
|
47
|
+
export default eventsConfig
|