@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,252 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
|
|
3
|
+
import { features as billingAclFeatures } from './acl'
|
|
4
|
+
import {
|
|
5
|
+
Dictionary,
|
|
6
|
+
DictionaryEntry,
|
|
7
|
+
} from '@open-mercato/core/modules/dictionaries/data/entities'
|
|
8
|
+
import { ModuleConfig } from '@open-mercato/core/modules/configs/data/entities'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Billing module setup — per-tenant defaults at module installation.
|
|
12
|
+
*
|
|
13
|
+
* Idempotency contract:
|
|
14
|
+
* - All seed helpers below check for an existing row before inserting.
|
|
15
|
+
* - Safe to re-run on every `mercato init` and on `auth sync-role-acls`.
|
|
16
|
+
*
|
|
17
|
+
* What this file seeds:
|
|
18
|
+
* - `billing.uom` dictionary with 16 global codes (operators can add more).
|
|
19
|
+
* - `sales.invoice_status` dictionary with `draft` / `posted` / `paid` /
|
|
20
|
+
* `void` so the core/sales Invoice entity's dictionary-driven status
|
|
21
|
+
* workflow has values to point at. (Core/sales does NOT seed these —
|
|
22
|
+
* billing owns them per Phase 0 validation.)
|
|
23
|
+
* - `billing.cron_schedule` + `billing.cron_enabled` module configs.
|
|
24
|
+
* - `billing.invoice_number.format` + `.reset_cycle` configs (per-tenant
|
|
25
|
+
* defaults; the actual atomic numbering uses core/sales'
|
|
26
|
+
* `SalesDocumentSequence` which is created by core/sales' own
|
|
27
|
+
* `onTenantCreated`).
|
|
28
|
+
*
|
|
29
|
+
* Default role-feature grants:
|
|
30
|
+
* - `admin` gets every `billing.*` feature.
|
|
31
|
+
* - `billing_admin` is the day-to-day finance operator.
|
|
32
|
+
* - `billing_finance_user` is read + dry-run (no posting / no real trigger).
|
|
33
|
+
* - `billing_usage_writer` is the narrow scope for external system API keys.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const BILLING_UOM_DICTIONARY_KEY = 'billing.uom'
|
|
37
|
+
const SALES_INVOICE_STATUS_DICTIONARY_KEY = 'sales.invoice_status'
|
|
38
|
+
|
|
39
|
+
const BILLING_UOM_CODES = [
|
|
40
|
+
// time
|
|
41
|
+
{ value: 'hour', label: 'Hour' },
|
|
42
|
+
{ value: 'day', label: 'Day' },
|
|
43
|
+
{ value: 'week', label: 'Week' },
|
|
44
|
+
{ value: 'month', label: 'Month' },
|
|
45
|
+
// volume
|
|
46
|
+
{ value: 'gb', label: 'Gigabyte' },
|
|
47
|
+
{ value: 'tb', label: 'Terabyte' },
|
|
48
|
+
{ value: 'mb', label: 'Megabyte' },
|
|
49
|
+
// requests / tokens
|
|
50
|
+
{ value: 'request', label: 'Request' },
|
|
51
|
+
{ value: 'api_request', label: 'API Request' },
|
|
52
|
+
{ value: 'token', label: 'Token' },
|
|
53
|
+
{ value: 'input_token', label: 'Input Token' },
|
|
54
|
+
{ value: 'output_token', label: 'Output Token' },
|
|
55
|
+
// seats / devices
|
|
56
|
+
{ value: 'seat', label: 'Seat' },
|
|
57
|
+
{ value: 'user', label: 'User' },
|
|
58
|
+
{ value: 'device', label: 'Device' },
|
|
59
|
+
{ value: 'active_user', label: 'Active User' },
|
|
60
|
+
] as const
|
|
61
|
+
|
|
62
|
+
const INVOICE_STATUS_ENTRIES = [
|
|
63
|
+
// `posted` is the canonical "issued" state and is also the default a
|
|
64
|
+
// freshly created invoice transitions to — but billing creates drafts,
|
|
65
|
+
// so `draft` is the default DictionaryEntry for new rows.
|
|
66
|
+
{ value: 'draft', label: 'Draft', isDefault: true, color: '#9ca3af' },
|
|
67
|
+
{ value: 'posted', label: 'Posted', isDefault: false, color: '#2563eb' },
|
|
68
|
+
{ value: 'paid', label: 'Paid', isDefault: false, color: '#16a34a' },
|
|
69
|
+
{ value: 'void', label: 'Void', isDefault: false, color: '#dc2626' },
|
|
70
|
+
] as const
|
|
71
|
+
|
|
72
|
+
type SeedScope = { tenantId: string; organizationId: string }
|
|
73
|
+
|
|
74
|
+
async function ensureDictionary(
|
|
75
|
+
em: EntityManager,
|
|
76
|
+
scope: SeedScope,
|
|
77
|
+
params: { key: string; name: string; description: string },
|
|
78
|
+
): Promise<Dictionary> {
|
|
79
|
+
const existing = await em.findOne(Dictionary, {
|
|
80
|
+
tenantId: scope.tenantId,
|
|
81
|
+
organizationId: scope.organizationId,
|
|
82
|
+
key: params.key,
|
|
83
|
+
deletedAt: null,
|
|
84
|
+
})
|
|
85
|
+
if (existing) return existing
|
|
86
|
+
const dictionary = em.create(Dictionary, {
|
|
87
|
+
tenantId: scope.tenantId,
|
|
88
|
+
organizationId: scope.organizationId,
|
|
89
|
+
key: params.key,
|
|
90
|
+
name: params.name,
|
|
91
|
+
description: params.description,
|
|
92
|
+
isSystem: true,
|
|
93
|
+
isActive: true,
|
|
94
|
+
managerVisibility: 'hidden',
|
|
95
|
+
createdAt: new Date(),
|
|
96
|
+
updatedAt: new Date(),
|
|
97
|
+
})
|
|
98
|
+
em.persist(dictionary)
|
|
99
|
+
await em.flush()
|
|
100
|
+
return dictionary
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function ensureDictionaryEntries(
|
|
104
|
+
em: EntityManager,
|
|
105
|
+
scope: SeedScope,
|
|
106
|
+
dictionary: Dictionary,
|
|
107
|
+
entries: ReadonlyArray<{
|
|
108
|
+
value: string
|
|
109
|
+
label: string
|
|
110
|
+
isDefault?: boolean
|
|
111
|
+
color?: string
|
|
112
|
+
}>,
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
const existing = await em.find(DictionaryEntry, {
|
|
115
|
+
dictionary,
|
|
116
|
+
tenantId: scope.tenantId,
|
|
117
|
+
organizationId: scope.organizationId,
|
|
118
|
+
})
|
|
119
|
+
const existingValues = new Set(existing.map((e) => e.value))
|
|
120
|
+
let position = existing.length
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (existingValues.has(entry.value)) continue
|
|
123
|
+
em.persist(
|
|
124
|
+
em.create(DictionaryEntry, {
|
|
125
|
+
dictionary,
|
|
126
|
+
tenantId: scope.tenantId,
|
|
127
|
+
organizationId: scope.organizationId,
|
|
128
|
+
value: entry.value,
|
|
129
|
+
normalizedValue: entry.value.trim().toLowerCase(),
|
|
130
|
+
label: entry.label,
|
|
131
|
+
color: entry.color ?? null,
|
|
132
|
+
position,
|
|
133
|
+
isDefault: entry.isDefault ?? false,
|
|
134
|
+
createdAt: new Date(),
|
|
135
|
+
updatedAt: new Date(),
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
position += 1
|
|
139
|
+
}
|
|
140
|
+
await em.flush()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function seedBillingUomDictionary(em: EntityManager, scope: SeedScope): Promise<void> {
|
|
144
|
+
const dict = await ensureDictionary(em, scope, {
|
|
145
|
+
key: BILLING_UOM_DICTIONARY_KEY,
|
|
146
|
+
name: 'Billing units of measure',
|
|
147
|
+
description: 'Units billing accepts on usage records (exact match — no conversion).',
|
|
148
|
+
})
|
|
149
|
+
await ensureDictionaryEntries(em, scope, dict, BILLING_UOM_CODES)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function seedInvoiceStatusDictionary(em: EntityManager, scope: SeedScope): Promise<void> {
|
|
153
|
+
const dict = await ensureDictionary(em, scope, {
|
|
154
|
+
key: SALES_INVOICE_STATUS_DICTIONARY_KEY,
|
|
155
|
+
name: 'Sales invoice statuses',
|
|
156
|
+
description: 'draft → posted → paid (or void). Drives the billing draft/post lifecycle.',
|
|
157
|
+
})
|
|
158
|
+
await ensureDictionaryEntries(em, scope, dict, INVOICE_STATUS_ENTRIES)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function ensureModuleConfig(
|
|
162
|
+
em: EntityManager,
|
|
163
|
+
params: { moduleId: string; name: string; value: unknown },
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
const existing = await em.findOne(ModuleConfig, {
|
|
166
|
+
moduleId: params.moduleId,
|
|
167
|
+
name: params.name,
|
|
168
|
+
})
|
|
169
|
+
if (existing) return
|
|
170
|
+
em.persist(
|
|
171
|
+
em.create(ModuleConfig, {
|
|
172
|
+
moduleId: params.moduleId,
|
|
173
|
+
name: params.name,
|
|
174
|
+
valueJson: params.value as Record<string, unknown> | null,
|
|
175
|
+
createdAt: new Date(),
|
|
176
|
+
updatedAt: new Date(),
|
|
177
|
+
}),
|
|
178
|
+
)
|
|
179
|
+
await em.flush()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function seedBillingConfigs(em: EntityManager): Promise<void> {
|
|
183
|
+
await ensureModuleConfig(em, {
|
|
184
|
+
moduleId: 'billing',
|
|
185
|
+
name: 'cron_schedule',
|
|
186
|
+
value: '0 2 * * *',
|
|
187
|
+
})
|
|
188
|
+
await ensureModuleConfig(em, {
|
|
189
|
+
moduleId: 'billing',
|
|
190
|
+
name: 'cron_enabled',
|
|
191
|
+
value: true,
|
|
192
|
+
})
|
|
193
|
+
await ensureModuleConfig(em, {
|
|
194
|
+
moduleId: 'billing',
|
|
195
|
+
name: 'invoice_number.format',
|
|
196
|
+
value: 'INV-{YYYY}-{0000001}',
|
|
197
|
+
})
|
|
198
|
+
await ensureModuleConfig(em, {
|
|
199
|
+
moduleId: 'billing',
|
|
200
|
+
name: 'invoice_number.reset_cycle',
|
|
201
|
+
value: 'yearly',
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Derived from `acl.ts` so a new feature added there automatically lands
|
|
206
|
+
// in every role that should get "everything billing" — no parallel list
|
|
207
|
+
// to keep in sync.
|
|
208
|
+
const ALL_BILLING_FEATURES = billingAclFeatures.map((f) => f.id)
|
|
209
|
+
|
|
210
|
+
export const setup: ModuleSetupConfig = {
|
|
211
|
+
defaultRoleFeatures: {
|
|
212
|
+
// Org admins get every billing feature — `billing.*` wildcard is matched
|
|
213
|
+
// by the shared feature matcher (`hasFeature` is wildcard-aware).
|
|
214
|
+
admin: ['billing.*'],
|
|
215
|
+
|
|
216
|
+
// Day-to-day finance operator persona.
|
|
217
|
+
billing_admin: [...ALL_BILLING_FEATURES],
|
|
218
|
+
|
|
219
|
+
// Read-only + dry-run preview — auditor / finance-watcher persona.
|
|
220
|
+
billing_finance_user: [
|
|
221
|
+
'billing.account.view',
|
|
222
|
+
'billing.item.view',
|
|
223
|
+
'billing.usage.view',
|
|
224
|
+
'billing.run.view',
|
|
225
|
+
'billing.run.dry_run',
|
|
226
|
+
'billing.invoice.view',
|
|
227
|
+
],
|
|
228
|
+
|
|
229
|
+
// Narrow scope for external usage-ingestion API keys.
|
|
230
|
+
billing_usage_writer: ['billing.usage.ingest'],
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Tenant init: per-tenant module configs (cron, invoice number defaults).
|
|
235
|
+
* Runs inside `setupInitialTenant()`, always — no `--no-examples` gate.
|
|
236
|
+
*/
|
|
237
|
+
async onTenantCreated({ em }) {
|
|
238
|
+
await seedBillingConfigs(em)
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Reference data: dictionaries (UoM, invoice statuses). Always runs
|
|
243
|
+
* during `mercato init`. Idempotent — re-seed is safe.
|
|
244
|
+
*/
|
|
245
|
+
async seedDefaults({ em, tenantId, organizationId }) {
|
|
246
|
+
const scope = { tenantId, organizationId }
|
|
247
|
+
await seedBillingUomDictionary(em, scope)
|
|
248
|
+
await seedInvoiceStatusDictionary(em, scope)
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export default setup
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
|
+
import { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'
|
|
4
|
+
import { resolveInvoiceStatusEntryId } from '../lib/invoiceStatus'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Subscriber: payment captured → billing invoice paid.
|
|
8
|
+
*
|
|
9
|
+
* Spec wishlist was `payments.payment.completed` carrying `invoice_id`.
|
|
10
|
+
* The actual upstream event is `payment_gateways.payment.captured` and
|
|
11
|
+
* its payload carries `paymentId` — set by whoever created the payment
|
|
12
|
+
* session. Billing flows that drive their own payment sessions can
|
|
13
|
+
* pass `paymentId = invoice.id`; the subscriber matches on that.
|
|
14
|
+
*
|
|
15
|
+
* Behaviour:
|
|
16
|
+
* 1. Try to find a `core/sales` invoice with `id = payload.paymentId`
|
|
17
|
+
* scoped to the same tenant/organization.
|
|
18
|
+
* 2. If found AND its `status == 'posted'`, flip it to `'paid'`
|
|
19
|
+
* (resolves the `paid` `DictionaryEntry`, updates `status` +
|
|
20
|
+
* `status_entry_id`). Drafts and already-paid / voided invoices
|
|
21
|
+
* are left untouched.
|
|
22
|
+
* 3. If `paymentId` does not point at a billing-known invoice (e.g.
|
|
23
|
+
* the payment was for a `sales.order`), the subscriber is a
|
|
24
|
+
* no-op — every other module's own subscriber owns its slice.
|
|
25
|
+
*
|
|
26
|
+
* Cross-tenant safety: we always re-scope by `tenantId` +
|
|
27
|
+
* `organizationId` from the event payload. A malformed payload (e.g.
|
|
28
|
+
* missing scope fields) short-circuits without touching the DB.
|
|
29
|
+
*
|
|
30
|
+
* Idempotency: re-firing the same captured event finds the invoice
|
|
31
|
+
* already paid and skips silently.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export const metadata = {
|
|
35
|
+
event: 'payment_gateways.payment.captured',
|
|
36
|
+
persistent: true,
|
|
37
|
+
id: 'billing-payment-captured-to-paid',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type CapturedPayload = {
|
|
41
|
+
paymentId?: string
|
|
42
|
+
transactionId?: string
|
|
43
|
+
organizationId?: string
|
|
44
|
+
tenantId?: string
|
|
45
|
+
providerKey?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default async function handle(payload: CapturedPayload): Promise<void> {
|
|
49
|
+
if (!payload?.paymentId || !payload.tenantId || !payload.organizationId) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const container = await createRequestContainer()
|
|
54
|
+
const em = (container.resolve('em') as EntityManager).fork()
|
|
55
|
+
|
|
56
|
+
const invoice = await em.findOne(SalesInvoice, {
|
|
57
|
+
id: payload.paymentId,
|
|
58
|
+
tenantId: payload.tenantId,
|
|
59
|
+
organizationId: payload.organizationId,
|
|
60
|
+
deletedAt: null,
|
|
61
|
+
})
|
|
62
|
+
if (!invoice) {
|
|
63
|
+
// The captured payment isn't for a billing invoice — silent
|
|
64
|
+
// no-op. Other subscribers (sales, checkout) handle their slice.
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
if (invoice.status === 'paid') {
|
|
68
|
+
// Already paid — idempotent re-firing.
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
if (invoice.status !== 'posted') {
|
|
72
|
+
// A captured payment for a draft / void invoice is suspicious but
|
|
73
|
+
// not actionable here. Future iteration: log to audit_logs.
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const paidStatusEntryId = await resolveInvoiceStatusEntryId(
|
|
78
|
+
em,
|
|
79
|
+
payload.tenantId,
|
|
80
|
+
payload.organizationId,
|
|
81
|
+
'paid',
|
|
82
|
+
)
|
|
83
|
+
invoice.status = 'paid'
|
|
84
|
+
invoice.statusEntryId = paidStatusEntryId
|
|
85
|
+
invoice.paidTotalAmount = invoice.grandTotalGrossAmount
|
|
86
|
+
invoice.outstandingAmount = '0'
|
|
87
|
+
invoice.updatedAt = new Date()
|
|
88
|
+
await em.flush()
|
|
89
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translatable fields for billing entities.
|
|
3
|
+
*
|
|
4
|
+
* Phase 0 keeps this empty — the Billing Account name and invoice line
|
|
5
|
+
* descriptions are user-entered free text, not localizable templates.
|
|
6
|
+
* If we ever ship localized invoice templates (Phase 4 admin UI), the
|
|
7
|
+
* template label fields land here.
|
|
8
|
+
*/
|
|
9
|
+
export const translatableFields: Record<string, string[]> = {}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
|
|
2
|
+
import type { FilterQuery } from '@mikro-orm/core'
|
|
3
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
|
+
import { BillRun } from '../data/entities'
|
|
5
|
+
|
|
6
|
+
export const metadata: WorkerMeta = {
|
|
7
|
+
queue: 'billing-reap-stale-runs',
|
|
8
|
+
id: 'billing:reap-stale-bill-runs',
|
|
9
|
+
concurrency: 1,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type HandlerContext = JobContext & {
|
|
13
|
+
resolve: <T = unknown>(name: string) => T
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reaper for "zombie" Bill Runs left in `status='running'` after a crash.
|
|
18
|
+
*
|
|
19
|
+
* Background — the Bill Run engine acquires a per-tenant Postgres
|
|
20
|
+
* transaction-scoped advisory lock (`pg_try_advisory_xact_lock`). When the
|
|
21
|
+
* holding transaction ends (commit / rollback / process death) the lock is
|
|
22
|
+
* released automatically — so there is no lock leak — but the `BillRun`
|
|
23
|
+
* row's `status='running'` stays set forever, blocking restarts and
|
|
24
|
+
* confusing the dashboard.
|
|
25
|
+
*
|
|
26
|
+
* Phase 0 ships this as the **registered worker shell** so the module's
|
|
27
|
+
* worker auto-discovery includes it from day one. The active-advisory-lock
|
|
28
|
+
* check (which is what distinguishes a real zombie from a healthy
|
|
29
|
+
* long-running run) lands with the Bill Run engine itself in Phase 2.
|
|
30
|
+
*
|
|
31
|
+
* For now the heuristic is time-only: any `running` row older than 1 hour
|
|
32
|
+
* is marked failed with an explanatory message. That is conservative —
|
|
33
|
+
* real runs almost never take an hour even for 500k-account tenants
|
|
34
|
+
* (see spec Performance Considerations).
|
|
35
|
+
*/
|
|
36
|
+
const STALE_THRESHOLD_MS = 60 * 60 * 1000 // 1 hour
|
|
37
|
+
|
|
38
|
+
export default async function handle(_job: QueuedJob, ctx: HandlerContext): Promise<void> {
|
|
39
|
+
const em = ctx.resolve<EntityManager>('em')
|
|
40
|
+
const threshold = new Date(Date.now() - STALE_THRESHOLD_MS)
|
|
41
|
+
const stale = await em.find(BillRun, {
|
|
42
|
+
status: 'running',
|
|
43
|
+
startedAt: { $lt: threshold },
|
|
44
|
+
} satisfies FilterQuery<BillRun>)
|
|
45
|
+
if (stale.length === 0) return
|
|
46
|
+
const now = new Date()
|
|
47
|
+
for (const run of stale) {
|
|
48
|
+
run.status = 'failed'
|
|
49
|
+
run.finishedAt = now
|
|
50
|
+
const summary = (run.summary ?? {}) as Record<string, unknown>
|
|
51
|
+
summary.reaper_message =
|
|
52
|
+
'Stale running run reaped after crash; advisory lock released'
|
|
53
|
+
summary.reaped_at = now.toISOString()
|
|
54
|
+
run.summary = summary
|
|
55
|
+
}
|
|
56
|
+
await em.flush()
|
|
57
|
+
}
|