@dainamite/billing 0.17.0 → 0.19.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 +99 -19
- package/dist/modules/billing/acl.js +9 -1
- package/dist/modules/billing/acl.js.map +2 -2
- package/dist/modules/billing/api/accounts/[id]/balance/route.js +62 -0
- package/dist/modules/billing/api/accounts/[id]/balance/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/[id]/credit-status/route.js +56 -0
- package/dist/modules/billing/api/accounts/[id]/credit-status/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/[id]/topups/route.js +36 -0
- package/dist/modules/billing/api/accounts/[id]/topups/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/route.js +7 -0
- package/dist/modules/billing/api/accounts/route.js.map +2 -2
- package/dist/modules/billing/api/export/account/[id]/route.js +48 -3
- package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
- package/dist/modules/billing/api/statements/generate/route.js +68 -0
- package/dist/modules/billing/api/statements/generate/route.js.map +7 -0
- package/dist/modules/billing/api/statements/route.js +110 -0
- package/dist/modules/billing/api/statements/route.js.map +7 -0
- package/dist/modules/billing/api/topups/route.js +139 -0
- package/dist/modules/billing/api/topups/route.js.map +7 -0
- package/dist/modules/billing/api/transactions/adjust/route.js +71 -0
- package/dist/modules/billing/api/transactions/adjust/route.js.map +7 -0
- package/dist/modules/billing/api/transactions/route.js +109 -0
- package/dist/modules/billing/api/transactions/route.js.map +7 -0
- package/dist/modules/billing/api/usage/route.js +30 -6
- package/dist/modules/billing/api/usage/route.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js +16 -1
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/create/page.js +8 -1
- package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/page.js +26 -1
- package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/statements/[id]/page.js +129 -0
- package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/statements/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/statements/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/statements/page.js +152 -0
- package/dist/modules/billing/backend/billing/statements/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/statements/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/statements/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.js +90 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/page.js +147 -0
- package/dist/modules/billing/backend/billing/topups/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/topups/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.js +114 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/page.js +149 -0
- package/dist/modules/billing/backend/billing/transactions/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/transactions/page.meta.js.map +7 -0
- package/dist/modules/billing/commands/accounts.js +26 -1
- package/dist/modules/billing/commands/accounts.js.map +2 -2
- package/dist/modules/billing/commands/index.js +3 -0
- package/dist/modules/billing/commands/index.js.map +2 -2
- package/dist/modules/billing/commands/invoices.js +21 -1
- package/dist/modules/billing/commands/invoices.js.map +2 -2
- package/dist/modules/billing/commands/statements.js +74 -0
- package/dist/modules/billing/commands/statements.js.map +7 -0
- package/dist/modules/billing/commands/topups.js +139 -0
- package/dist/modules/billing/commands/topups.js.map +7 -0
- package/dist/modules/billing/commands/transactions.js +119 -0
- package/dist/modules/billing/commands/transactions.js.map +7 -0
- package/dist/modules/billing/commands/usage.js +72 -0
- package/dist/modules/billing/commands/usage.js.map +2 -2
- package/dist/modules/billing/components/AccountForm.js +53 -1
- package/dist/modules/billing/components/AccountForm.js.map +2 -2
- package/dist/modules/billing/components/PrepaidPanel.js +314 -0
- package/dist/modules/billing/components/PrepaidPanel.js.map +7 -0
- package/dist/modules/billing/components/accountNames.js +20 -0
- package/dist/modules/billing/components/accountNames.js.map +7 -0
- package/dist/modules/billing/data/entities.js +290 -0
- package/dist/modules/billing/data/entities.js.map +2 -2
- package/dist/modules/billing/data/entityIds.js +6 -1
- package/dist/modules/billing/data/entityIds.js.map +2 -2
- package/dist/modules/billing/data/validators.js +107 -2
- package/dist/modules/billing/data/validators.js.map +2 -2
- package/dist/modules/billing/events.js +52 -0
- package/dist/modules/billing/events.js.map +2 -2
- package/dist/modules/billing/i18n/en.json +112 -4
- package/dist/modules/billing/i18n/pl.json +112 -4
- package/dist/modules/billing/index.js +1 -1
- package/dist/modules/billing/index.js.map +2 -2
- package/dist/modules/billing/lib/accountStatus.js +106 -0
- package/dist/modules/billing/lib/accountStatus.js.map +7 -0
- package/dist/modules/billing/lib/balanceEvents.js +48 -0
- package/dist/modules/billing/lib/balanceEvents.js.map +7 -0
- package/dist/modules/billing/lib/balanceLedger.js +136 -0
- package/dist/modules/billing/lib/balanceLedger.js.map +7 -0
- package/dist/modules/billing/lib/balanceStatus.js +29 -0
- package/dist/modules/billing/lib/balanceStatus.js.map +7 -0
- package/dist/modules/billing/lib/billRunEngine.js +40 -2
- package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
- package/dist/modules/billing/lib/creditStatus.js +65 -0
- package/dist/modules/billing/lib/creditStatus.js.map +7 -0
- package/dist/modules/billing/lib/money.js +47 -0
- package/dist/modules/billing/lib/money.js.map +7 -0
- package/dist/modules/billing/lib/prepaidConfig.js +45 -0
- package/dist/modules/billing/lib/prepaidConfig.js.map +7 -0
- package/dist/modules/billing/lib/prepaidConsume.js +214 -0
- package/dist/modules/billing/lib/prepaidConsume.js.map +7 -0
- package/dist/modules/billing/lib/prepaidPeriodClose.js +285 -0
- package/dist/modules/billing/lib/prepaidPeriodClose.js.map +7 -0
- package/dist/modules/billing/lib/topupCapture.js +62 -0
- package/dist/modules/billing/lib/topupCapture.js.map +7 -0
- package/dist/modules/billing/lib/topupReceipt.js +97 -0
- package/dist/modules/billing/lib/topupReceipt.js.map +7 -0
- package/dist/modules/billing/lib/topupTransition.js +19 -0
- package/dist/modules/billing/lib/topupTransition.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20260619000000_billing_prepaid.js +110 -0
- package/dist/modules/billing/migrations/Migration20260619000000_billing_prepaid.js.map +7 -0
- package/dist/modules/billing/setup.js +32 -1
- package/dist/modules/billing/setup.js.map +2 -2
- package/dist/modules/billing/subscribers/prepaid-topup-cancelled.js +26 -0
- package/dist/modules/billing/subscribers/prepaid-topup-cancelled.js.map +7 -0
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js +87 -0
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +7 -0
- package/dist/modules/billing/subscribers/prepaid-topup-expired.js +26 -0
- package/dist/modules/billing/subscribers/prepaid-topup-expired.js.map +7 -0
- package/dist/modules/billing/subscribers/prepaid-topup-failed.js +26 -0
- package/dist/modules/billing/subscribers/prepaid-topup-failed.js.map +7 -0
- package/dist/modules/billing/workers/reconcile-prepaid-balances.js +50 -0
- package/dist/modules/billing/workers/reconcile-prepaid-balances.js.map +7 -0
- package/package.json +9 -9
- package/src/modules/billing/__tests__/commands/invoices.test.ts +3 -3
- package/src/modules/billing/__tests__/commands/topups.test.ts +104 -0
- package/src/modules/billing/__tests__/commands/transactions.test.ts +107 -0
- package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +235 -0
- package/src/modules/billing/__tests__/lib/accountStatus.test.ts +93 -0
- package/src/modules/billing/__tests__/lib/balanceLedger.test.ts +125 -0
- package/src/modules/billing/__tests__/lib/balanceStatus.test.ts +65 -0
- package/src/modules/billing/__tests__/lib/creditStatus.test.ts +111 -0
- package/src/modules/billing/__tests__/lib/money.test.ts +56 -0
- package/src/modules/billing/__tests__/lib/prepaidConsume.test.ts +179 -0
- package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +232 -0
- package/src/modules/billing/__tests__/lib/topupCapture.test.ts +123 -0
- package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +84 -0
- package/src/modules/billing/__tests__/setup.test.ts +28 -2
- package/src/modules/billing/acl.ts +8 -0
- package/src/modules/billing/api/accounts/[id]/balance/route.ts +74 -0
- package/src/modules/billing/api/accounts/[id]/credit-status/route.ts +71 -0
- package/src/modules/billing/api/accounts/[id]/topups/route.ts +49 -0
- package/src/modules/billing/api/accounts/route.ts +7 -0
- package/src/modules/billing/api/export/account/[id]/route.ts +47 -2
- package/src/modules/billing/api/statements/generate/route.ts +78 -0
- package/src/modules/billing/api/statements/route.ts +117 -0
- package/src/modules/billing/api/topups/route.ts +154 -0
- package/src/modules/billing/api/transactions/adjust/route.ts +80 -0
- package/src/modules/billing/api/transactions/route.ts +116 -0
- package/src/modules/billing/api/usage/route.ts +36 -5
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +28 -0
- package/src/modules/billing/backend/billing/accounts/create/page.tsx +7 -0
- package/src/modules/billing/backend/billing/accounts/page.tsx +32 -1
- package/src/modules/billing/backend/billing/statements/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/statements/[id]/page.tsx +198 -0
- package/src/modules/billing/backend/billing/statements/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/statements/page.tsx +190 -0
- package/src/modules/billing/backend/billing/topups/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/topups/[id]/page.tsx +151 -0
- package/src/modules/billing/backend/billing/topups/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/topups/page.tsx +182 -0
- package/src/modules/billing/backend/billing/transactions/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/transactions/[id]/page.tsx +179 -0
- package/src/modules/billing/backend/billing/transactions/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/transactions/page.tsx +187 -0
- package/src/modules/billing/commands/accounts.ts +42 -1
- package/src/modules/billing/commands/index.ts +3 -0
- package/src/modules/billing/commands/invoices.ts +26 -2
- package/src/modules/billing/commands/statements.ts +101 -0
- package/src/modules/billing/commands/topups.ts +181 -0
- package/src/modules/billing/commands/transactions.ts +152 -0
- package/src/modules/billing/commands/usage.ts +114 -4
- package/src/modules/billing/components/AccountForm.tsx +66 -0
- package/src/modules/billing/components/PrepaidPanel.tsx +382 -0
- package/src/modules/billing/components/accountNames.ts +23 -0
- package/src/modules/billing/data/entities.ts +383 -0
- package/src/modules/billing/data/entityIds.ts +5 -0
- package/src/modules/billing/data/validators.ts +144 -0
- package/src/modules/billing/events.ts +52 -0
- package/src/modules/billing/i18n/en.json +112 -4
- package/src/modules/billing/i18n/pl.json +112 -4
- package/src/modules/billing/index.ts +1 -1
- package/src/modules/billing/lib/accountStatus.ts +155 -0
- package/src/modules/billing/lib/balanceEvents.ts +74 -0
- package/src/modules/billing/lib/balanceLedger.ts +258 -0
- package/src/modules/billing/lib/balanceStatus.ts +78 -0
- package/src/modules/billing/lib/billRunEngine.ts +66 -5
- package/src/modules/billing/lib/creditStatus.ts +153 -0
- package/src/modules/billing/lib/money.ts +73 -0
- package/src/modules/billing/lib/prepaidConfig.ts +58 -0
- package/src/modules/billing/lib/prepaidConsume.ts +299 -0
- package/src/modules/billing/lib/prepaidPeriodClose.ts +453 -0
- package/src/modules/billing/lib/topupCapture.ts +117 -0
- package/src/modules/billing/lib/topupReceipt.ts +153 -0
- package/src/modules/billing/lib/topupTransition.ts +31 -0
- package/src/modules/billing/migrations/Migration20260619000000_billing_prepaid.ts +131 -0
- package/src/modules/billing/setup.ts +45 -0
- package/src/modules/billing/subscribers/prepaid-topup-cancelled.ts +31 -0
- package/src/modules/billing/subscribers/prepaid-topup-captured.ts +120 -0
- package/src/modules/billing/subscribers/prepaid-topup-expired.ts +31 -0
- package/src/modules/billing/subscribers/prepaid-topup-failed.ts +32 -0
- package/src/modules/billing/workers/reconcile-prepaid-balances.ts +68 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Migration } from "@mikro-orm/migrations";
|
|
2
|
+
class Migration20260619000000_billing_prepaid extends Migration {
|
|
3
|
+
async up() {
|
|
4
|
+
this.addSql(
|
|
5
|
+
`alter table "billing_accounts" add column "billing_mode" text not null default 'postpaid';`
|
|
6
|
+
);
|
|
7
|
+
this.addSql(
|
|
8
|
+
`alter table "billing_accounts" add column "credit_limit" numeric(18, 4) not null default 0;`
|
|
9
|
+
);
|
|
10
|
+
this.addSql(`alter table "billing_run_outcomes" add column "statement_id" uuid null;`);
|
|
11
|
+
this.addSql(`create table "billing_account_balances" (
|
|
12
|
+
"id" uuid not null default gen_random_uuid(),
|
|
13
|
+
"organization_id" uuid not null,
|
|
14
|
+
"tenant_id" uuid not null,
|
|
15
|
+
"bill_account_id" uuid not null,
|
|
16
|
+
"currency_code" text not null,
|
|
17
|
+
"balance" numeric(18, 4) not null default 0,
|
|
18
|
+
"low_balance_threshold" numeric(18, 4) null,
|
|
19
|
+
"last_movement_at" timestamptz null,
|
|
20
|
+
"created_at" timestamptz not null,
|
|
21
|
+
"updated_at" timestamptz not null,
|
|
22
|
+
constraint "billing_account_balances_pkey" primary key ("id")
|
|
23
|
+
);`);
|
|
24
|
+
this.addSql(`alter table "billing_account_balances" add constraint "billing_account_balances_account_unique" unique ("tenant_id", "bill_account_id");`);
|
|
25
|
+
this.addSql(`create index "billing_account_balances_balance_idx" on "billing_account_balances" ("organization_id", "tenant_id", "balance");`);
|
|
26
|
+
this.addSql(`create table "billing_account_transactions" (
|
|
27
|
+
"id" uuid not null default gen_random_uuid(),
|
|
28
|
+
"organization_id" uuid not null,
|
|
29
|
+
"tenant_id" uuid not null,
|
|
30
|
+
"bill_account_id" uuid not null,
|
|
31
|
+
"type" text not null,
|
|
32
|
+
"amount" numeric(18, 4) not null,
|
|
33
|
+
"currency_code" text not null,
|
|
34
|
+
"balance_after" numeric(18, 4) not null,
|
|
35
|
+
"usage_id" uuid null,
|
|
36
|
+
"billing_item_id" uuid null,
|
|
37
|
+
"topup_id" uuid null,
|
|
38
|
+
"statement_id" uuid null,
|
|
39
|
+
"description" text not null,
|
|
40
|
+
"source_ref" text null,
|
|
41
|
+
"metadata" jsonb null,
|
|
42
|
+
"user_id" uuid null,
|
|
43
|
+
"created_at" timestamptz not null,
|
|
44
|
+
"deleted_at" timestamptz null,
|
|
45
|
+
constraint "billing_account_transactions_pkey" primary key ("id")
|
|
46
|
+
);`);
|
|
47
|
+
this.addSql(`alter table "billing_account_transactions" add constraint "billing_account_transactions_source_ref_unique" unique ("tenant_id", "bill_account_id", "source_ref");`);
|
|
48
|
+
this.addSql(`create index "billing_account_transactions_history_idx" on "billing_account_transactions" ("organization_id", "tenant_id", "bill_account_id", "created_at");`);
|
|
49
|
+
this.addSql(`create index "billing_account_transactions_account_type_idx" on "billing_account_transactions" ("bill_account_id", "type", "created_at");`);
|
|
50
|
+
this.addSql(`create table "billing_topups" (
|
|
51
|
+
"id" uuid not null default gen_random_uuid(),
|
|
52
|
+
"organization_id" uuid not null,
|
|
53
|
+
"tenant_id" uuid not null,
|
|
54
|
+
"bill_account_id" uuid not null,
|
|
55
|
+
"status" text not null,
|
|
56
|
+
"amount" numeric(18, 4) not null,
|
|
57
|
+
"currency_code" text not null,
|
|
58
|
+
"provider_key" text not null,
|
|
59
|
+
"payment_id" uuid not null,
|
|
60
|
+
"gateway_transaction_id" uuid null,
|
|
61
|
+
"transaction_id" uuid null,
|
|
62
|
+
"receipt_invoice_id" uuid null,
|
|
63
|
+
"source_ref" text null,
|
|
64
|
+
"metadata" jsonb null,
|
|
65
|
+
"created_at" timestamptz not null,
|
|
66
|
+
"updated_at" timestamptz not null,
|
|
67
|
+
"deleted_at" timestamptz null,
|
|
68
|
+
constraint "billing_topups_pkey" primary key ("id")
|
|
69
|
+
);`);
|
|
70
|
+
this.addSql(`alter table "billing_topups" add constraint "billing_topups_payment_id_unique" unique ("tenant_id", "payment_id");`);
|
|
71
|
+
this.addSql(`create index "billing_topups_account_history_idx" on "billing_topups" ("organization_id", "tenant_id", "bill_account_id", "created_at");`);
|
|
72
|
+
this.addSql(`create index "billing_topups_status_idx" on "billing_topups" ("tenant_id", "status");`);
|
|
73
|
+
this.addSql(`create table "billing_statements" (
|
|
74
|
+
"id" uuid not null default gen_random_uuid(),
|
|
75
|
+
"organization_id" uuid not null,
|
|
76
|
+
"tenant_id" uuid not null,
|
|
77
|
+
"bill_account_id" uuid not null,
|
|
78
|
+
"bill_run_id" uuid null,
|
|
79
|
+
"period_start" date not null,
|
|
80
|
+
"period_end" date not null,
|
|
81
|
+
"currency_code" text not null,
|
|
82
|
+
"opening_balance" numeric(18, 4) not null,
|
|
83
|
+
"total_topups" numeric(18, 4) not null,
|
|
84
|
+
"total_usage" numeric(18, 4) not null,
|
|
85
|
+
"total_recurring" numeric(18, 4) not null,
|
|
86
|
+
"total_adjustments" numeric(18, 4) not null default 0,
|
|
87
|
+
"closing_balance" numeric(18, 4) not null,
|
|
88
|
+
"debit_breakdown" jsonb not null,
|
|
89
|
+
"status" text not null,
|
|
90
|
+
"created_at" timestamptz not null,
|
|
91
|
+
"updated_at" timestamptz not null,
|
|
92
|
+
constraint "billing_statements_pkey" primary key ("id")
|
|
93
|
+
);`);
|
|
94
|
+
this.addSql(`alter table "billing_statements" add constraint "billing_statements_period_unique" unique ("tenant_id", "bill_account_id", "period_start", "period_end");`);
|
|
95
|
+
this.addSql(`create index "billing_statements_account_period_idx" on "billing_statements" ("organization_id", "tenant_id", "bill_account_id", "period_end");`);
|
|
96
|
+
}
|
|
97
|
+
async down() {
|
|
98
|
+
this.addSql(`drop table if exists "billing_statements" cascade;`);
|
|
99
|
+
this.addSql(`drop table if exists "billing_topups" cascade;`);
|
|
100
|
+
this.addSql(`drop table if exists "billing_account_transactions" cascade;`);
|
|
101
|
+
this.addSql(`drop table if exists "billing_account_balances" cascade;`);
|
|
102
|
+
this.addSql(`alter table "billing_run_outcomes" drop column if exists "statement_id";`);
|
|
103
|
+
this.addSql(`alter table "billing_accounts" drop column if exists "credit_limit";`);
|
|
104
|
+
this.addSql(`alter table "billing_accounts" drop column if exists "billing_mode";`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
Migration20260619000000_billing_prepaid
|
|
109
|
+
};
|
|
110
|
+
//# sourceMappingURL=Migration20260619000000_billing_prepaid.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/migrations/Migration20260619000000_billing_prepaid.ts"],
|
|
4
|
+
"sourcesContent": ["import { Migration } from '@mikro-orm/migrations'\n\n/**\n * Prepaid balance billing schema (SPEC-002).\n *\n * Additive only \u2014 extends `billing_accounts` + `billing_run_outcomes` and\n * adds the four new prepaid tables. Postpaid accounts are unaffected\n * (`billing_mode` defaults to 'postpaid', `credit_limit` to 0).\n *\n * Hand-written to match `data/entities.ts` 1:1 (index/constraint names mirror\n * the decorator names so a later `yarn mercato db generate` diffs clean). The\n * statements here are pure `create table` / `alter table \u2026 add column` \u2014\n * never destructive to existing billing data.\n */\nexport class Migration20260619000000_billing_prepaid extends Migration {\n async up(): Promise<void> {\n // \u2500\u2500\u2500 billing_accounts: billing_mode + credit_limit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\n this.addSql(\n `alter table \"billing_accounts\" add column \"billing_mode\" text not null default 'postpaid';`,\n )\n this.addSql(\n `alter table \"billing_accounts\" add column \"credit_limit\" numeric(18, 4) not null default 0;`,\n )\n\n // \u2500\u2500\u2500 billing_run_outcomes: statement_id (prepaid analogue) \u2500\n this.addSql(`alter table \"billing_run_outcomes\" add column \"statement_id\" uuid null;`)\n\n // \u2500\u2500\u2500 billing_account_balances \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n this.addSql(`create table \"billing_account_balances\" (\n \"id\" uuid not null default gen_random_uuid(),\n \"organization_id\" uuid not null,\n \"tenant_id\" uuid not null,\n \"bill_account_id\" uuid not null,\n \"currency_code\" text not null,\n \"balance\" numeric(18, 4) not null default 0,\n \"low_balance_threshold\" numeric(18, 4) null,\n \"last_movement_at\" timestamptz null,\n \"created_at\" timestamptz not null,\n \"updated_at\" timestamptz not null,\n constraint \"billing_account_balances_pkey\" primary key (\"id\")\n );`)\n this.addSql(`alter table \"billing_account_balances\" add constraint \"billing_account_balances_account_unique\" unique (\"tenant_id\", \"bill_account_id\");`)\n this.addSql(`create index \"billing_account_balances_balance_idx\" on \"billing_account_balances\" (\"organization_id\", \"tenant_id\", \"balance\");`)\n\n // \u2500\u2500\u2500 billing_account_transactions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n this.addSql(`create table \"billing_account_transactions\" (\n \"id\" uuid not null default gen_random_uuid(),\n \"organization_id\" uuid not null,\n \"tenant_id\" uuid not null,\n \"bill_account_id\" uuid not null,\n \"type\" text not null,\n \"amount\" numeric(18, 4) not null,\n \"currency_code\" text not null,\n \"balance_after\" numeric(18, 4) not null,\n \"usage_id\" uuid null,\n \"billing_item_id\" uuid null,\n \"topup_id\" uuid null,\n \"statement_id\" uuid null,\n \"description\" text not null,\n \"source_ref\" text null,\n \"metadata\" jsonb null,\n \"user_id\" uuid null,\n \"created_at\" timestamptz not null,\n \"deleted_at\" timestamptz null,\n constraint \"billing_account_transactions_pkey\" primary key (\"id\")\n );`)\n this.addSql(`alter table \"billing_account_transactions\" add constraint \"billing_account_transactions_source_ref_unique\" unique (\"tenant_id\", \"bill_account_id\", \"source_ref\");`)\n this.addSql(`create index \"billing_account_transactions_history_idx\" on \"billing_account_transactions\" (\"organization_id\", \"tenant_id\", \"bill_account_id\", \"created_at\");`)\n this.addSql(`create index \"billing_account_transactions_account_type_idx\" on \"billing_account_transactions\" (\"bill_account_id\", \"type\", \"created_at\");`)\n\n // \u2500\u2500\u2500 billing_topups \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n this.addSql(`create table \"billing_topups\" (\n \"id\" uuid not null default gen_random_uuid(),\n \"organization_id\" uuid not null,\n \"tenant_id\" uuid not null,\n \"bill_account_id\" uuid not null,\n \"status\" text not null,\n \"amount\" numeric(18, 4) not null,\n \"currency_code\" text not null,\n \"provider_key\" text not null,\n \"payment_id\" uuid not null,\n \"gateway_transaction_id\" uuid null,\n \"transaction_id\" uuid null,\n \"receipt_invoice_id\" uuid null,\n \"source_ref\" text null,\n \"metadata\" jsonb null,\n \"created_at\" timestamptz not null,\n \"updated_at\" timestamptz not null,\n \"deleted_at\" timestamptz null,\n constraint \"billing_topups_pkey\" primary key (\"id\")\n );`)\n this.addSql(`alter table \"billing_topups\" add constraint \"billing_topups_payment_id_unique\" unique (\"tenant_id\", \"payment_id\");`)\n this.addSql(`create index \"billing_topups_account_history_idx\" on \"billing_topups\" (\"organization_id\", \"tenant_id\", \"bill_account_id\", \"created_at\");`)\n this.addSql(`create index \"billing_topups_status_idx\" on \"billing_topups\" (\"tenant_id\", \"status\");`)\n\n // \u2500\u2500\u2500 billing_statements \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n this.addSql(`create table \"billing_statements\" (\n \"id\" uuid not null default gen_random_uuid(),\n \"organization_id\" uuid not null,\n \"tenant_id\" uuid not null,\n \"bill_account_id\" uuid not null,\n \"bill_run_id\" uuid null,\n \"period_start\" date not null,\n \"period_end\" date not null,\n \"currency_code\" text not null,\n \"opening_balance\" numeric(18, 4) not null,\n \"total_topups\" numeric(18, 4) not null,\n \"total_usage\" numeric(18, 4) not null,\n \"total_recurring\" numeric(18, 4) not null,\n \"total_adjustments\" numeric(18, 4) not null default 0,\n \"closing_balance\" numeric(18, 4) not null,\n \"debit_breakdown\" jsonb not null,\n \"status\" text not null,\n \"created_at\" timestamptz not null,\n \"updated_at\" timestamptz not null,\n constraint \"billing_statements_pkey\" primary key (\"id\")\n );`)\n this.addSql(`alter table \"billing_statements\" add constraint \"billing_statements_period_unique\" unique (\"tenant_id\", \"bill_account_id\", \"period_start\", \"period_end\");`)\n this.addSql(`create index \"billing_statements_account_period_idx\" on \"billing_statements\" (\"organization_id\", \"tenant_id\", \"bill_account_id\", \"period_end\");`)\n }\n\n async down(): Promise<void> {\n this.addSql(`drop table if exists \"billing_statements\" cascade;`)\n this.addSql(`drop table if exists \"billing_topups\" cascade;`)\n this.addSql(`drop table if exists \"billing_account_transactions\" cascade;`)\n this.addSql(`drop table if exists \"billing_account_balances\" cascade;`)\n this.addSql(`alter table \"billing_run_outcomes\" drop column if exists \"statement_id\";`)\n this.addSql(`alter table \"billing_accounts\" drop column if exists \"credit_limit\";`)\n this.addSql(`alter table \"billing_accounts\" drop column if exists \"billing_mode\";`)\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB;AAcnB,MAAM,gDAAgD,UAAU;AAAA,EACrE,MAAM,KAAoB;AAExB,SAAK;AAAA,MACH;AAAA,IACF;AACA,SAAK;AAAA,MACH;AAAA,IACF;AAGA,SAAK,OAAO,yEAAyE;AAGrF,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAYT;AACH,SAAK,OAAO,0IAA0I;AACtJ,SAAK,OAAO,gIAAgI;AAG5I,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAoBT;AACH,SAAK,OAAO,mKAAmK;AAC/K,SAAK,OAAO,8JAA8J;AAC1K,SAAK,OAAO,2IAA2I;AAGvJ,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAmBT;AACH,SAAK,OAAO,oHAAoH;AAChI,SAAK,OAAO,0IAA0I;AACtJ,SAAK,OAAO,uFAAuF;AAGnG,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAoBT;AACH,SAAK,OAAO,2JAA2J;AACvK,SAAK,OAAO,iJAAiJ;AAAA,EAC/J;AAAA,EAEA,MAAM,OAAsB;AAC1B,SAAK,OAAO,oDAAoD;AAChE,SAAK,OAAO,gDAAgD;AAC5D,SAAK,OAAO,8DAA8D;AAC1E,SAAK,OAAO,0DAA0D;AACtE,SAAK,OAAO,0EAA0E;AACtF,SAAK,OAAO,sEAAsE;AAClF,SAAK,OAAO,sEAAsE;AAAA,EACpF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -144,6 +144,31 @@ async function seedBillingConfigs(em) {
|
|
|
144
144
|
name: "invoice_number.reset_cycle",
|
|
145
145
|
value: "yearly"
|
|
146
146
|
});
|
|
147
|
+
await ensureModuleConfig(em, {
|
|
148
|
+
moduleId: "billing",
|
|
149
|
+
name: "prepaid.low_balance_threshold_default",
|
|
150
|
+
value: 0
|
|
151
|
+
});
|
|
152
|
+
await ensureModuleConfig(em, {
|
|
153
|
+
moduleId: "billing",
|
|
154
|
+
name: "prepaid.topup_provider",
|
|
155
|
+
value: "stripe"
|
|
156
|
+
});
|
|
157
|
+
await ensureModuleConfig(em, {
|
|
158
|
+
moduleId: "billing",
|
|
159
|
+
name: "prepaid.statement_enabled",
|
|
160
|
+
value: true
|
|
161
|
+
});
|
|
162
|
+
await ensureModuleConfig(em, {
|
|
163
|
+
moduleId: "billing",
|
|
164
|
+
name: "prepaid.topup_tax_rate",
|
|
165
|
+
value: 0
|
|
166
|
+
});
|
|
167
|
+
await ensureModuleConfig(em, {
|
|
168
|
+
moduleId: "billing",
|
|
169
|
+
name: "credit.near_limit_buffer_default",
|
|
170
|
+
value: 0
|
|
171
|
+
});
|
|
147
172
|
}
|
|
148
173
|
const ALL_BILLING_FEATURES = billingAclFeatures.map((f) => f.id);
|
|
149
174
|
const setup = {
|
|
@@ -160,7 +185,13 @@ const setup = {
|
|
|
160
185
|
"billing.usage.view",
|
|
161
186
|
"billing.run.view",
|
|
162
187
|
"billing.run.dry_run",
|
|
163
|
-
"billing.invoice.view"
|
|
188
|
+
"billing.invoice.view",
|
|
189
|
+
// Prepaid read-only (SPEC-002): balances, transactions, top-ups,
|
|
190
|
+
// statements, credit status — no adjust / no top-up initiation.
|
|
191
|
+
"billing.balance.view",
|
|
192
|
+
"billing.credit.view",
|
|
193
|
+
"billing.topup.view",
|
|
194
|
+
"billing.statement.view"
|
|
164
195
|
],
|
|
165
196
|
// Narrow scope for external usage-ingestion API keys.
|
|
166
197
|
billing_usage_writer: ["billing.usage.ingest"]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/billing/setup.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { features as billingAclFeatures } from './acl'\nimport {\n Dictionary,\n DictionaryEntry,\n} from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { ModuleConfig } from '@open-mercato/core/modules/configs/data/entities'\n\n/**\n * Billing module setup \u2014 per-tenant defaults at module installation.\n *\n * Idempotency contract:\n * - All seed helpers below check for an existing row before inserting.\n * - Safe to re-run on every `mercato init` and on `auth sync-role-acls`.\n *\n * What this file seeds:\n * - `billing.uom` dictionary with 16 global codes (operators can add more).\n * - `sales.invoice_status` dictionary with `draft` / `posted` / `paid` /\n * `void` so the core/sales Invoice entity's dictionary-driven status\n * workflow has values to point at. (Core/sales does NOT seed these \u2014\n * billing owns them per Phase 0 validation.)\n * - `billing.cron_schedule` + `billing.cron_enabled` module configs.\n * - `billing.invoice_number.format` + `.reset_cycle` configs (per-tenant\n * defaults; the actual atomic numbering uses core/sales'\n * `SalesDocumentSequence` which is created by core/sales' own\n * `onTenantCreated`).\n *\n * Default role-feature grants:\n * - `admin` gets every `billing.*` feature.\n * - `billing_admin` is the day-to-day finance operator.\n * - `billing_finance_user` is read + dry-run (no posting / no real trigger).\n * - `billing_usage_writer` is the narrow scope for external system API keys.\n */\n\nconst BILLING_UOM_DICTIONARY_KEY = 'billing.uom'\nconst SALES_INVOICE_STATUS_DICTIONARY_KEY = 'sales.invoice_status'\n\nconst BILLING_UOM_CODES = [\n // time\n { value: 'hour', label: 'Hour' },\n { value: 'day', label: 'Day' },\n { value: 'week', label: 'Week' },\n { value: 'month', label: 'Month' },\n // volume\n { value: 'gb', label: 'Gigabyte' },\n { value: 'tb', label: 'Terabyte' },\n { value: 'mb', label: 'Megabyte' },\n // requests / tokens\n { value: 'request', label: 'Request' },\n { value: 'api_request', label: 'API Request' },\n { value: 'token', label: 'Token' },\n { value: 'input_token', label: 'Input Token' },\n { value: 'output_token', label: 'Output Token' },\n // seats / devices\n { value: 'seat', label: 'Seat' },\n { value: 'user', label: 'User' },\n { value: 'device', label: 'Device' },\n { value: 'active_user', label: 'Active User' },\n] as const\n\nconst INVOICE_STATUS_ENTRIES = [\n // `posted` is the canonical \"issued\" state and is also the default a\n // freshly created invoice transitions to \u2014 but billing creates drafts,\n // so `draft` is the default DictionaryEntry for new rows.\n { value: 'draft', label: 'Draft', isDefault: true, color: '#9ca3af' },\n { value: 'posted', label: 'Posted', isDefault: false, color: '#2563eb' },\n { value: 'paid', label: 'Paid', isDefault: false, color: '#16a34a' },\n { value: 'void', label: 'Void', isDefault: false, color: '#dc2626' },\n] as const\n\ntype SeedScope = { tenantId: string; organizationId: string }\n\nasync function ensureDictionary(\n em: EntityManager,\n scope: SeedScope,\n params: { key: string; name: string; description: string },\n): Promise<Dictionary> {\n const existing = await em.findOne(Dictionary, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n key: params.key,\n deletedAt: null,\n })\n if (existing) return existing\n const dictionary = em.create(Dictionary, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n key: params.key,\n name: params.name,\n description: params.description,\n isSystem: true,\n isActive: true,\n managerVisibility: 'hidden',\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(dictionary)\n await em.flush()\n return dictionary\n}\n\nasync function ensureDictionaryEntries(\n em: EntityManager,\n scope: SeedScope,\n dictionary: Dictionary,\n entries: ReadonlyArray<{\n value: string\n label: string\n isDefault?: boolean\n color?: string\n }>,\n): Promise<void> {\n const existing = await em.find(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const existingValues = new Set(existing.map((e) => e.value))\n let position = existing.length\n for (const entry of entries) {\n if (existingValues.has(entry.value)) continue\n em.persist(\n em.create(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n value: entry.value,\n normalizedValue: entry.value.trim().toLowerCase(),\n label: entry.label,\n color: entry.color ?? null,\n position,\n isDefault: entry.isDefault ?? false,\n createdAt: new Date(),\n updatedAt: new Date(),\n }),\n )\n position += 1\n }\n await em.flush()\n}\n\nasync function seedBillingUomDictionary(em: EntityManager, scope: SeedScope): Promise<void> {\n const dict = await ensureDictionary(em, scope, {\n key: BILLING_UOM_DICTIONARY_KEY,\n name: 'Billing units of measure',\n description: 'Units billing accepts on usage records (exact match \u2014 no conversion).',\n })\n await ensureDictionaryEntries(em, scope, dict, BILLING_UOM_CODES)\n}\n\nasync function seedInvoiceStatusDictionary(em: EntityManager, scope: SeedScope): Promise<void> {\n const dict = await ensureDictionary(em, scope, {\n key: SALES_INVOICE_STATUS_DICTIONARY_KEY,\n name: 'Sales invoice statuses',\n description: 'draft \u2192 posted \u2192 paid (or void). Drives the billing draft/post lifecycle.',\n })\n await ensureDictionaryEntries(em, scope, dict, INVOICE_STATUS_ENTRIES)\n}\n\nasync function ensureModuleConfig(\n em: EntityManager,\n params: { moduleId: string; name: string; value: unknown },\n): Promise<void> {\n const existing = await em.findOne(ModuleConfig, {\n moduleId: params.moduleId,\n name: params.name,\n })\n if (existing) return\n em.persist(\n em.create(ModuleConfig, {\n moduleId: params.moduleId,\n name: params.name,\n valueJson: params.value as Record<string, unknown> | null,\n createdAt: new Date(),\n updatedAt: new Date(),\n }),\n )\n await em.flush()\n}\n\nasync function seedBillingConfigs(em: EntityManager): Promise<void> {\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'cron_schedule',\n value: '0 2 * * *',\n })\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'cron_enabled',\n value: true,\n })\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'invoice_number.format',\n value: 'INV-{YYYY}-{0000001}',\n })\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'invoice_number.reset_cycle',\n value: 'yearly',\n })\n}\n\n// Derived from `acl.ts` so a new feature added there automatically lands\n// in every role that should get \"everything billing\" \u2014 no parallel list\n// to keep in sync.\nconst ALL_BILLING_FEATURES = billingAclFeatures.map((f) => f.id)\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n // Org admins get every billing feature \u2014 `billing.*` wildcard is matched\n // by the shared feature matcher (`hasFeature` is wildcard-aware).\n admin: ['billing.*'],\n\n // Day-to-day finance operator persona.\n billing_admin: [...ALL_BILLING_FEATURES],\n\n // Read-only + dry-run preview \u2014 auditor / finance-watcher persona.\n billing_finance_user: [\n 'billing.account.view',\n 'billing.item.view',\n 'billing.usage.view',\n 'billing.run.view',\n 'billing.run.dry_run',\n 'billing.invoice.view',\n ],\n\n // Narrow scope for external usage-ingestion API keys.\n billing_usage_writer: ['billing.usage.ingest'],\n },\n\n /**\n * Tenant init: per-tenant module configs (cron, invoice number defaults).\n * Runs inside `setupInitialTenant()`, always \u2014 no `--no-examples` gate.\n */\n async onTenantCreated({ em }) {\n await seedBillingConfigs(em)\n },\n\n /**\n * Reference data: dictionaries (UoM, invoice statuses). Always runs\n * during `mercato init`. Idempotent \u2014 re-seed is safe.\n */\n async seedDefaults({ em, tenantId, organizationId }) {\n const scope = { tenantId, organizationId }\n await seedBillingUomDictionary(em, scope)\n await seedInvoiceStatusDictionary(em, scope)\n },\n}\n\nexport default setup\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { features as billingAclFeatures } from './acl'\nimport {\n Dictionary,\n DictionaryEntry,\n} from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { ModuleConfig } from '@open-mercato/core/modules/configs/data/entities'\n\n/**\n * Billing module setup \u2014 per-tenant defaults at module installation.\n *\n * Idempotency contract:\n * - All seed helpers below check for an existing row before inserting.\n * - Safe to re-run on every `mercato init` and on `auth sync-role-acls`.\n *\n * What this file seeds:\n * - `billing.uom` dictionary with 16 global codes (operators can add more).\n * - `sales.invoice_status` dictionary with `draft` / `posted` / `paid` /\n * `void` so the core/sales Invoice entity's dictionary-driven status\n * workflow has values to point at. (Core/sales does NOT seed these \u2014\n * billing owns them per Phase 0 validation.)\n * - `billing.cron_schedule` + `billing.cron_enabled` module configs.\n * - `billing.invoice_number.format` + `.reset_cycle` configs (per-tenant\n * defaults; the actual atomic numbering uses core/sales'\n * `SalesDocumentSequence` which is created by core/sales' own\n * `onTenantCreated`).\n *\n * Default role-feature grants:\n * - `admin` gets every `billing.*` feature.\n * - `billing_admin` is the day-to-day finance operator.\n * - `billing_finance_user` is read + dry-run (no posting / no real trigger).\n * - `billing_usage_writer` is the narrow scope for external system API keys.\n */\n\nconst BILLING_UOM_DICTIONARY_KEY = 'billing.uom'\nconst SALES_INVOICE_STATUS_DICTIONARY_KEY = 'sales.invoice_status'\n\nconst BILLING_UOM_CODES = [\n // time\n { value: 'hour', label: 'Hour' },\n { value: 'day', label: 'Day' },\n { value: 'week', label: 'Week' },\n { value: 'month', label: 'Month' },\n // volume\n { value: 'gb', label: 'Gigabyte' },\n { value: 'tb', label: 'Terabyte' },\n { value: 'mb', label: 'Megabyte' },\n // requests / tokens\n { value: 'request', label: 'Request' },\n { value: 'api_request', label: 'API Request' },\n { value: 'token', label: 'Token' },\n { value: 'input_token', label: 'Input Token' },\n { value: 'output_token', label: 'Output Token' },\n // seats / devices\n { value: 'seat', label: 'Seat' },\n { value: 'user', label: 'User' },\n { value: 'device', label: 'Device' },\n { value: 'active_user', label: 'Active User' },\n] as const\n\nconst INVOICE_STATUS_ENTRIES = [\n // `posted` is the canonical \"issued\" state and is also the default a\n // freshly created invoice transitions to \u2014 but billing creates drafts,\n // so `draft` is the default DictionaryEntry for new rows.\n { value: 'draft', label: 'Draft', isDefault: true, color: '#9ca3af' },\n { value: 'posted', label: 'Posted', isDefault: false, color: '#2563eb' },\n { value: 'paid', label: 'Paid', isDefault: false, color: '#16a34a' },\n { value: 'void', label: 'Void', isDefault: false, color: '#dc2626' },\n] as const\n\ntype SeedScope = { tenantId: string; organizationId: string }\n\nasync function ensureDictionary(\n em: EntityManager,\n scope: SeedScope,\n params: { key: string; name: string; description: string },\n): Promise<Dictionary> {\n const existing = await em.findOne(Dictionary, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n key: params.key,\n deletedAt: null,\n })\n if (existing) return existing\n const dictionary = em.create(Dictionary, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n key: params.key,\n name: params.name,\n description: params.description,\n isSystem: true,\n isActive: true,\n managerVisibility: 'hidden',\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(dictionary)\n await em.flush()\n return dictionary\n}\n\nasync function ensureDictionaryEntries(\n em: EntityManager,\n scope: SeedScope,\n dictionary: Dictionary,\n entries: ReadonlyArray<{\n value: string\n label: string\n isDefault?: boolean\n color?: string\n }>,\n): Promise<void> {\n const existing = await em.find(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const existingValues = new Set(existing.map((e) => e.value))\n let position = existing.length\n for (const entry of entries) {\n if (existingValues.has(entry.value)) continue\n em.persist(\n em.create(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n value: entry.value,\n normalizedValue: entry.value.trim().toLowerCase(),\n label: entry.label,\n color: entry.color ?? null,\n position,\n isDefault: entry.isDefault ?? false,\n createdAt: new Date(),\n updatedAt: new Date(),\n }),\n )\n position += 1\n }\n await em.flush()\n}\n\nasync function seedBillingUomDictionary(em: EntityManager, scope: SeedScope): Promise<void> {\n const dict = await ensureDictionary(em, scope, {\n key: BILLING_UOM_DICTIONARY_KEY,\n name: 'Billing units of measure',\n description: 'Units billing accepts on usage records (exact match \u2014 no conversion).',\n })\n await ensureDictionaryEntries(em, scope, dict, BILLING_UOM_CODES)\n}\n\nasync function seedInvoiceStatusDictionary(em: EntityManager, scope: SeedScope): Promise<void> {\n const dict = await ensureDictionary(em, scope, {\n key: SALES_INVOICE_STATUS_DICTIONARY_KEY,\n name: 'Sales invoice statuses',\n description: 'draft \u2192 posted \u2192 paid (or void). Drives the billing draft/post lifecycle.',\n })\n await ensureDictionaryEntries(em, scope, dict, INVOICE_STATUS_ENTRIES)\n}\n\nasync function ensureModuleConfig(\n em: EntityManager,\n params: { moduleId: string; name: string; value: unknown },\n): Promise<void> {\n const existing = await em.findOne(ModuleConfig, {\n moduleId: params.moduleId,\n name: params.name,\n })\n if (existing) return\n em.persist(\n em.create(ModuleConfig, {\n moduleId: params.moduleId,\n name: params.name,\n valueJson: params.value as Record<string, unknown> | null,\n createdAt: new Date(),\n updatedAt: new Date(),\n }),\n )\n await em.flush()\n}\n\nasync function seedBillingConfigs(em: EntityManager): Promise<void> {\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'cron_schedule',\n value: '0 2 * * *',\n })\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'cron_enabled',\n value: true,\n })\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'invoice_number.format',\n value: 'INV-{YYYY}-{0000001}',\n })\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'invoice_number.reset_cycle',\n value: 'yearly',\n })\n\n // \u2500\u2500\u2500 Prepaid balance billing (SPEC-002) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Fallback low-balance threshold when a prepaid account leaves its own\n // `low_balance_threshold` null. 0 = off (status never flips to `low`\n // until an operator sets a per-account threshold).\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'prepaid.low_balance_threshold_default',\n value: 0,\n })\n // Default gateway provider key for top-up sessions.\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'prepaid.topup_provider',\n value: 'stripe',\n })\n // Master switch for period statements.\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'prepaid.statement_enabled',\n value: true,\n })\n // VAT rate (percent) applied to top-up receipts. The top-up `amount` is\n // treated as GROSS (what the customer pays = what the balance is credited);\n // the core tax service computes net + VAT from this rate. 0 = no VAT until\n // an operator sets the jurisdiction's rate. (Deviation from xd-249, which\n // never resolves a rate \u2014 see CHANGELOG.)\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'prepaid.topup_tax_rate',\n value: 0,\n })\n // Fallback buffer for the `near_limit` credit status. 0 = warn only at the\n // limit itself.\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'credit.near_limit_buffer_default',\n value: 0,\n })\n}\n\n// Derived from `acl.ts` so a new feature added there automatically lands\n// in every role that should get \"everything billing\" \u2014 no parallel list\n// to keep in sync.\nconst ALL_BILLING_FEATURES = billingAclFeatures.map((f) => f.id)\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n // Org admins get every billing feature \u2014 `billing.*` wildcard is matched\n // by the shared feature matcher (`hasFeature` is wildcard-aware).\n admin: ['billing.*'],\n\n // Day-to-day finance operator persona.\n billing_admin: [...ALL_BILLING_FEATURES],\n\n // Read-only + dry-run preview \u2014 auditor / finance-watcher persona.\n billing_finance_user: [\n 'billing.account.view',\n 'billing.item.view',\n 'billing.usage.view',\n 'billing.run.view',\n 'billing.run.dry_run',\n 'billing.invoice.view',\n // Prepaid read-only (SPEC-002): balances, transactions, top-ups,\n // statements, credit status \u2014 no adjust / no top-up initiation.\n 'billing.balance.view',\n 'billing.credit.view',\n 'billing.topup.view',\n 'billing.statement.view',\n ],\n\n // Narrow scope for external usage-ingestion API keys.\n billing_usage_writer: ['billing.usage.ingest'],\n },\n\n /**\n * Tenant init: per-tenant module configs (cron, invoice number defaults).\n * Runs inside `setupInitialTenant()`, always \u2014 no `--no-examples` gate.\n */\n async onTenantCreated({ em }) {\n await seedBillingConfigs(em)\n },\n\n /**\n * Reference data: dictionaries (UoM, invoice statuses). Always runs\n * during `mercato init`. Idempotent \u2014 re-seed is safe.\n */\n async seedDefaults({ em, tenantId, organizationId }) {\n const scope = { tenantId, organizationId }\n await seedBillingUomDictionary(em, scope)\n await seedInvoiceStatusDictionary(em, scope)\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
|
+
import { transitionTopupByPaymentId } from "../lib/topupTransition.js";
|
|
3
|
+
const metadata = {
|
|
4
|
+
event: "payment_gateways.payment.cancelled",
|
|
5
|
+
persistent: true,
|
|
6
|
+
id: "billing-prepaid-topup-cancelled"
|
|
7
|
+
};
|
|
8
|
+
async function handle(payload) {
|
|
9
|
+
if (!payload?.paymentId || !payload.tenantId || !payload.organizationId) return;
|
|
10
|
+
const container = await createRequestContainer();
|
|
11
|
+
const em = container.resolve("em").fork();
|
|
12
|
+
await transitionTopupByPaymentId(
|
|
13
|
+
em,
|
|
14
|
+
{
|
|
15
|
+
paymentId: payload.paymentId,
|
|
16
|
+
tenantId: payload.tenantId,
|
|
17
|
+
organizationId: payload.organizationId
|
|
18
|
+
},
|
|
19
|
+
"cancelled"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
handle as default,
|
|
24
|
+
metadata
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=prepaid-topup-cancelled.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/subscribers/prepaid-topup-cancelled.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { transitionTopupByPaymentId } from '../lib/topupTransition'\n\n/**\n * Subscriber: `payment_gateways.payment.cancelled` \u2192 BillingTopup \u2192 cancelled.\n * No-op for any non-top-up payment. No balance movement.\n */\n\nexport const metadata = {\n event: 'payment_gateways.payment.cancelled',\n persistent: true,\n id: 'billing-prepaid-topup-cancelled',\n}\n\ntype Payload = { paymentId?: string; tenantId?: string; organizationId?: string }\n\nexport default async function handle(payload: Payload): Promise<void> {\n if (!payload?.paymentId || !payload.tenantId || !payload.organizationId) return\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n await transitionTopupByPaymentId(\n em,\n {\n paymentId: payload.paymentId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n },\n 'cancelled',\n )\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,8BAA8B;AACvC,SAAS,kCAAkC;AAOpC,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAIA,eAAO,OAA8B,SAAiC;AACpE,MAAI,CAAC,SAAS,aAAa,CAAC,QAAQ,YAAY,CAAC,QAAQ,eAAgB;AACzE,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,MACE,WAAW,QAAQ;AAAA,MACnB,UAAU,QAAQ;AAAA,MAClB,gBAAgB,QAAQ;AAAA,IAC1B;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
|
+
import { BillingAccount, BillingTopup } from "../data/entities.js";
|
|
3
|
+
import { emitBillingEvent } from "../events.js";
|
|
4
|
+
import { captureTopup } from "../lib/topupCapture.js";
|
|
5
|
+
import { getTopupTaxRate } from "../lib/prepaidConfig.js";
|
|
6
|
+
const metadata = {
|
|
7
|
+
event: "payment_gateways.payment.captured",
|
|
8
|
+
persistent: true,
|
|
9
|
+
id: "billing-prepaid-topup-captured"
|
|
10
|
+
};
|
|
11
|
+
async function handle(payload) {
|
|
12
|
+
if (!payload?.paymentId || !payload.tenantId || !payload.organizationId) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const container = await createRequestContainer();
|
|
16
|
+
const em = container.resolve("em").fork();
|
|
17
|
+
const topup = await em.findOne(BillingTopup, {
|
|
18
|
+
paymentId: payload.paymentId,
|
|
19
|
+
tenantId: payload.tenantId,
|
|
20
|
+
organizationId: payload.organizationId,
|
|
21
|
+
deletedAt: null
|
|
22
|
+
});
|
|
23
|
+
if (!topup) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (topup.status === "captured") {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const account = await em.findOne(BillingAccount, {
|
|
30
|
+
id: topup.billAccountId,
|
|
31
|
+
tenantId: payload.tenantId,
|
|
32
|
+
organizationId: payload.organizationId,
|
|
33
|
+
deletedAt: null
|
|
34
|
+
});
|
|
35
|
+
if (!account) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const taxRate = await getTopupTaxRate(em);
|
|
39
|
+
const numberGenerator = container.resolve(
|
|
40
|
+
"salesDocumentNumberGenerator"
|
|
41
|
+
);
|
|
42
|
+
const taxService = container.resolve("taxCalculationService");
|
|
43
|
+
const result = await captureTopup(
|
|
44
|
+
em,
|
|
45
|
+
{ numberGenerator, taxService },
|
|
46
|
+
{
|
|
47
|
+
topup,
|
|
48
|
+
account: {
|
|
49
|
+
id: account.id,
|
|
50
|
+
organizationId: account.organizationId,
|
|
51
|
+
tenantId: account.tenantId,
|
|
52
|
+
currencyCode: account.currencyCode
|
|
53
|
+
},
|
|
54
|
+
taxRate,
|
|
55
|
+
gatewayTransactionId: payload.transactionId ?? null
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
if (result.status !== "captured") return;
|
|
59
|
+
await emitBillingEvent("billing.balance.topped_up", {
|
|
60
|
+
billAccountId: account.id,
|
|
61
|
+
tenantId: account.tenantId,
|
|
62
|
+
organizationId: account.organizationId,
|
|
63
|
+
topupId: topup.id,
|
|
64
|
+
amount: topup.amount,
|
|
65
|
+
currencyCode: account.currencyCode,
|
|
66
|
+
balance: result.balanceAfter ?? null,
|
|
67
|
+
transactionId: result.transactionId ?? null,
|
|
68
|
+
receiptInvoiceId: result.receiptInvoiceId ?? null
|
|
69
|
+
});
|
|
70
|
+
if (result.receiptInvoiceId) {
|
|
71
|
+
await emitBillingEvent("billing.invoice.posted", {
|
|
72
|
+
invoiceId: result.receiptInvoiceId,
|
|
73
|
+
invoiceNumber: result.invoiceNumber ?? null,
|
|
74
|
+
tenantId: account.tenantId,
|
|
75
|
+
organizationId: account.organizationId,
|
|
76
|
+
grandTotalGross: result.grossAmount ?? null,
|
|
77
|
+
currencyCode: account.currencyCode,
|
|
78
|
+
billAccountId: account.id,
|
|
79
|
+
kind: "prepaid_topup"
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
handle as default,
|
|
85
|
+
metadata
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=prepaid-topup-captured.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/subscribers/prepaid-topup-captured.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { SalesDocumentNumberGenerator } from '@open-mercato/core/modules/sales/services/salesDocumentNumberGenerator'\nimport type { TaxCalculationService } from '@open-mercato/core/modules/sales/services/taxCalculationService'\nimport { BillingAccount, BillingTopup } from '../data/entities'\nimport { emitBillingEvent } from '../events'\nimport { captureTopup } from '../lib/topupCapture'\nimport { getTopupTaxRate } from '../lib/prepaidConfig'\n\n/**\n * Subscriber: `payment_gateways.payment.captured` \u2192 prepaid top-up captured.\n *\n * Coexists with `payment-captured-to-paid.ts`: that subscriber matches\n * `paymentId == invoiceId` (postpaid), while a top-up's `paymentId` is a\n * minted UUID that only ever matches a `BillingTopup`. Finding the\n * BillingTopup IS the guard \u2014 for any non-top-up payment this is a silent\n * no-op.\n *\n * Idempotent: a re-fired capture on an already-captured top-up is a no-op\n * (the gateway delivers webhooks at-least-once).\n */\n\nexport const metadata = {\n event: 'payment_gateways.payment.captured',\n persistent: true,\n id: 'billing-prepaid-topup-captured',\n}\n\ntype CapturedPayload = {\n paymentId?: string\n transactionId?: string\n organizationId?: string\n tenantId?: string\n providerKey?: string\n}\n\nexport default async function handle(payload: CapturedPayload): Promise<void> {\n if (!payload?.paymentId || !payload.tenantId || !payload.organizationId) {\n return\n }\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n\n const topup = await em.findOne(BillingTopup, {\n paymentId: payload.paymentId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n deletedAt: null,\n })\n if (!topup) {\n // Not a prepaid top-up \u2014 other subscribers own this captured payment.\n return\n }\n if (topup.status === 'captured') {\n return // idempotent re-fire\n }\n\n const account = await em.findOne(BillingAccount, {\n id: topup.billAccountId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n deletedAt: null,\n })\n if (!account) {\n return\n }\n\n const taxRate = await getTopupTaxRate(em)\n const numberGenerator = container.resolve(\n 'salesDocumentNumberGenerator',\n ) as SalesDocumentNumberGenerator\n const taxService = container.resolve('taxCalculationService') as TaxCalculationService\n\n const result = await captureTopup(\n em,\n { numberGenerator, taxService },\n {\n topup,\n account: {\n id: account.id,\n organizationId: account.organizationId,\n tenantId: account.tenantId,\n currencyCode: account.currencyCode,\n },\n taxRate,\n gatewayTransactionId: payload.transactionId ?? null,\n },\n )\n\n if (result.status !== 'captured') return\n\n // Side effects fire AFTER the capture transaction commits.\n await emitBillingEvent('billing.balance.topped_up', {\n billAccountId: account.id,\n tenantId: account.tenantId,\n organizationId: account.organizationId,\n topupId: topup.id,\n amount: topup.amount,\n currencyCode: account.currencyCode,\n balance: result.balanceAfter ?? null,\n transactionId: result.transactionId ?? null,\n receiptInvoiceId: result.receiptInvoiceId ?? null,\n })\n\n // Reuse the postpaid receipt path so the existing mailer / accounting bridge\n // sends the top-up VAT receipt.\n if (result.receiptInvoiceId) {\n await emitBillingEvent('billing.invoice.posted', {\n invoiceId: result.receiptInvoiceId,\n invoiceNumber: result.invoiceNumber ?? null,\n tenantId: account.tenantId,\n organizationId: account.organizationId,\n grandTotalGross: result.grossAmount ?? null,\n currencyCode: account.currencyCode,\n billAccountId: account.id,\n kind: 'prepaid_topup',\n })\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,8BAA8B;AAGvC,SAAS,gBAAgB,oBAAoB;AAC7C,SAAS,wBAAwB;AACjC,SAAS,oBAAoB;AAC7B,SAAS,uBAAuB;AAezB,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAUA,eAAO,OAA8B,SAAyC;AAC5E,MAAI,CAAC,SAAS,aAAa,CAAC,QAAQ,YAAY,CAAC,QAAQ,gBAAgB;AACvE;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAE3D,QAAM,QAAQ,MAAM,GAAG,QAAQ,cAAc;AAAA,IAC3C,WAAW,QAAQ;AAAA,IACnB,UAAU,QAAQ;AAAA,IAClB,gBAAgB,QAAQ;AAAA,IACxB,WAAW;AAAA,EACb,CAAC;AACD,MAAI,CAAC,OAAO;AAEV;AAAA,EACF;AACA,MAAI,MAAM,WAAW,YAAY;AAC/B;AAAA,EACF;AAEA,QAAM,UAAU,MAAM,GAAG,QAAQ,gBAAgB;AAAA,IAC/C,IAAI,MAAM;AAAA,IACV,UAAU,QAAQ;AAAA,IAClB,gBAAgB,QAAQ;AAAA,IACxB,WAAW;AAAA,EACb,CAAC;AACD,MAAI,CAAC,SAAS;AACZ;AAAA,EACF;AAEA,QAAM,UAAU,MAAM,gBAAgB,EAAE;AACxC,QAAM,kBAAkB,UAAU;AAAA,IAChC;AAAA,EACF;AACA,QAAM,aAAa,UAAU,QAAQ,uBAAuB;AAE5D,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA,EAAE,iBAAiB,WAAW;AAAA,IAC9B;AAAA,MACE;AAAA,MACA,SAAS;AAAA,QACP,IAAI,QAAQ;AAAA,QACZ,gBAAgB,QAAQ;AAAA,QACxB,UAAU,QAAQ;AAAA,QAClB,cAAc,QAAQ;AAAA,MACxB;AAAA,MACA;AAAA,MACA,sBAAsB,QAAQ,iBAAiB;AAAA,IACjD;AAAA,EACF;AAEA,MAAI,OAAO,WAAW,WAAY;AAGlC,QAAM,iBAAiB,6BAA6B;AAAA,IAClD,eAAe,QAAQ;AAAA,IACvB,UAAU,QAAQ;AAAA,IAClB,gBAAgB,QAAQ;AAAA,IACxB,SAAS,MAAM;AAAA,IACf,QAAQ,MAAM;AAAA,IACd,cAAc,QAAQ;AAAA,IACtB,SAAS,OAAO,gBAAgB;AAAA,IAChC,eAAe,OAAO,iBAAiB;AAAA,IACvC,kBAAkB,OAAO,oBAAoB;AAAA,EAC/C,CAAC;AAID,MAAI,OAAO,kBAAkB;AAC3B,UAAM,iBAAiB,0BAA0B;AAAA,MAC/C,WAAW,OAAO;AAAA,MAClB,eAAe,OAAO,iBAAiB;AAAA,MACvC,UAAU,QAAQ;AAAA,MAClB,gBAAgB,QAAQ;AAAA,MACxB,iBAAiB,OAAO,eAAe;AAAA,MACvC,cAAc,QAAQ;AAAA,MACtB,eAAe,QAAQ;AAAA,MACvB,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
|
+
import { transitionTopupByPaymentId } from "../lib/topupTransition.js";
|
|
3
|
+
const metadata = {
|
|
4
|
+
event: "payment_gateways.session.expired",
|
|
5
|
+
persistent: true,
|
|
6
|
+
id: "billing-prepaid-topup-expired"
|
|
7
|
+
};
|
|
8
|
+
async function handle(payload) {
|
|
9
|
+
if (!payload?.paymentId || !payload.tenantId || !payload.organizationId) return;
|
|
10
|
+
const container = await createRequestContainer();
|
|
11
|
+
const em = container.resolve("em").fork();
|
|
12
|
+
await transitionTopupByPaymentId(
|
|
13
|
+
em,
|
|
14
|
+
{
|
|
15
|
+
paymentId: payload.paymentId,
|
|
16
|
+
tenantId: payload.tenantId,
|
|
17
|
+
organizationId: payload.organizationId
|
|
18
|
+
},
|
|
19
|
+
"expired"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
handle as default,
|
|
24
|
+
metadata
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=prepaid-topup-expired.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/subscribers/prepaid-topup-expired.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { transitionTopupByPaymentId } from '../lib/topupTransition'\n\n/**\n * Subscriber: `payment_gateways.session.expired` \u2192 BillingTopup \u2192 expired.\n * No-op for any non-top-up session. No balance movement.\n */\n\nexport const metadata = {\n event: 'payment_gateways.session.expired',\n persistent: true,\n id: 'billing-prepaid-topup-expired',\n}\n\ntype Payload = { paymentId?: string; tenantId?: string; organizationId?: string }\n\nexport default async function handle(payload: Payload): Promise<void> {\n if (!payload?.paymentId || !payload.tenantId || !payload.organizationId) return\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n await transitionTopupByPaymentId(\n em,\n {\n paymentId: payload.paymentId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n },\n 'expired',\n )\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,8BAA8B;AACvC,SAAS,kCAAkC;AAOpC,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAIA,eAAO,OAA8B,SAAiC;AACpE,MAAI,CAAC,SAAS,aAAa,CAAC,QAAQ,YAAY,CAAC,QAAQ,eAAgB;AACzE,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,MACE,WAAW,QAAQ;AAAA,MACnB,UAAU,QAAQ;AAAA,MAClB,gBAAgB,QAAQ;AAAA,IAC1B;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
|
+
import { transitionTopupByPaymentId } from "../lib/topupTransition.js";
|
|
3
|
+
const metadata = {
|
|
4
|
+
event: "payment_gateways.payment.failed",
|
|
5
|
+
persistent: true,
|
|
6
|
+
id: "billing-prepaid-topup-failed"
|
|
7
|
+
};
|
|
8
|
+
async function handle(payload) {
|
|
9
|
+
if (!payload?.paymentId || !payload.tenantId || !payload.organizationId) return;
|
|
10
|
+
const container = await createRequestContainer();
|
|
11
|
+
const em = container.resolve("em").fork();
|
|
12
|
+
await transitionTopupByPaymentId(
|
|
13
|
+
em,
|
|
14
|
+
{
|
|
15
|
+
paymentId: payload.paymentId,
|
|
16
|
+
tenantId: payload.tenantId,
|
|
17
|
+
organizationId: payload.organizationId
|
|
18
|
+
},
|
|
19
|
+
"failed"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
handle as default,
|
|
24
|
+
metadata
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=prepaid-topup-failed.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/subscribers/prepaid-topup-failed.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { transitionTopupByPaymentId } from '../lib/topupTransition'\n\n/**\n * Subscriber: `payment_gateways.payment.failed` \u2192 BillingTopup \u2192 failed.\n * No-op for any non-top-up payment (no matching BillingTopup). No balance\n * movement.\n */\n\nexport const metadata = {\n event: 'payment_gateways.payment.failed',\n persistent: true,\n id: 'billing-prepaid-topup-failed',\n}\n\ntype Payload = { paymentId?: string; tenantId?: string; organizationId?: string }\n\nexport default async function handle(payload: Payload): Promise<void> {\n if (!payload?.paymentId || !payload.tenantId || !payload.organizationId) return\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n await transitionTopupByPaymentId(\n em,\n {\n paymentId: payload.paymentId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n },\n 'failed',\n )\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,8BAA8B;AACvC,SAAS,kCAAkC;AAQpC,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAIA,eAAO,OAA8B,SAAiC;AACpE,MAAI,CAAC,SAAS,aAAa,CAAC,QAAQ,YAAY,CAAC,QAAQ,eAAgB;AACzE,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,MACE,WAAW,QAAQ;AAAA,MACnB,UAAU,QAAQ;AAAA,MAClB,gBAAgB,QAAQ;AAAA,IAC1B;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { BillingAccountBalance } from "../data/entities.js";
|
|
2
|
+
import { reconcileAccountBalance } from "../lib/balanceLedger.js";
|
|
3
|
+
const metadata = {
|
|
4
|
+
queue: "billing-reconcile-prepaid-balances",
|
|
5
|
+
id: "billing:reconcile-prepaid-balances",
|
|
6
|
+
concurrency: 1
|
|
7
|
+
};
|
|
8
|
+
const SCAN_LIMIT = 1e4;
|
|
9
|
+
async function handle(_job, ctx) {
|
|
10
|
+
const em = ctx.resolve("em");
|
|
11
|
+
const rows = await em.find(
|
|
12
|
+
BillingAccountBalance,
|
|
13
|
+
{},
|
|
14
|
+
{ limit: SCAN_LIMIT, orderBy: { updatedAt: "DESC" } }
|
|
15
|
+
);
|
|
16
|
+
let checked = 0;
|
|
17
|
+
let drifted = 0;
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
const result = await reconcileAccountBalance(em.fork(), {
|
|
20
|
+
tenantId: row.tenantId,
|
|
21
|
+
organizationId: row.organizationId,
|
|
22
|
+
billAccountId: row.billAccountId
|
|
23
|
+
});
|
|
24
|
+
checked += 1;
|
|
25
|
+
if (!result.reconciled) {
|
|
26
|
+
drifted += 1;
|
|
27
|
+
console.warn("[billing/reconcile] balance drift detected", {
|
|
28
|
+
billAccountId: row.billAccountId,
|
|
29
|
+
tenantId: row.tenantId,
|
|
30
|
+
organizationId: row.organizationId,
|
|
31
|
+
cachedBalance: result.cachedBalance,
|
|
32
|
+
sumOfAmounts: result.sumOfAmounts,
|
|
33
|
+
latestBalanceAfter: result.latestBalanceAfter
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (rows.length === SCAN_LIMIT) {
|
|
38
|
+
console.warn(
|
|
39
|
+
`[billing/reconcile] scan hit the ${SCAN_LIMIT}-row cap \u2014 some accounts were not checked this run`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (drifted > 0) {
|
|
43
|
+
console.warn(`[billing/reconcile] ${drifted}/${checked} prepaid accounts drifted`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
handle as default,
|
|
48
|
+
metadata
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=reconcile-prepaid-balances.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/workers/reconcile-prepaid-balances.ts"],
|
|
4
|
+
"sourcesContent": ["import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { BillingAccountBalance } from '../data/entities'\nimport { reconcileAccountBalance } from '../lib/balanceLedger'\n\nexport const metadata: WorkerMeta = {\n queue: 'billing-reconcile-prepaid-balances',\n id: 'billing:reconcile-prepaid-balances',\n concurrency: 1,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\n/**\n * Reconciliation guard (SPEC-002 P5, optional).\n *\n * Periodically asserts the ledger invariant per prepaid account:\n * `cachedBalance == SUM(transactions.amount) == latest.balance_after`.\n *\n * The atomic `UPDATE \u2026 RETURNING` + paired transaction insert keeps this true\n * by construction, so a healthy system always reconciles. This worker is a\n * cheap backstop against any future non-atomic write slipping in \u2014 it logs a\n * warning per drifted account (the operator-facing alert) and never mutates\n * the balance (a fix is a deliberate operator action, not an automatic one).\n */\nconst SCAN_LIMIT = 10_000\n\nexport default async function handle(_job: QueuedJob, ctx: HandlerContext): Promise<void> {\n const em = ctx.resolve<EntityManager>('em')\n const rows = await em.find(\n BillingAccountBalance,\n {} as never,\n { limit: SCAN_LIMIT, orderBy: { updatedAt: 'DESC' } },\n )\n\n let checked = 0\n let drifted = 0\n for (const row of rows) {\n const result = await reconcileAccountBalance(em.fork(), {\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n billAccountId: row.billAccountId,\n })\n checked += 1\n if (!result.reconciled) {\n drifted += 1\n console.warn('[billing/reconcile] balance drift detected', {\n billAccountId: row.billAccountId,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n cachedBalance: result.cachedBalance,\n sumOfAmounts: result.sumOfAmounts,\n latestBalanceAfter: result.latestBalanceAfter,\n })\n }\n }\n\n if (rows.length === SCAN_LIMIT) {\n console.warn(\n `[billing/reconcile] scan hit the ${SCAN_LIMIT}-row cap \u2014 some accounts were not checked this run`,\n )\n }\n if (drifted > 0) {\n console.warn(`[billing/reconcile] ${drifted}/${checked} prepaid accounts drifted`)\n }\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AAEjC,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AACf;AAkBA,MAAM,aAAa;AAEnB,eAAO,OAA8B,MAAiB,KAAoC;AACxF,QAAM,KAAK,IAAI,QAAuB,IAAI;AAC1C,QAAM,OAAO,MAAM,GAAG;AAAA,IACpB;AAAA,IACA,CAAC;AAAA,IACD,EAAE,OAAO,YAAY,SAAS,EAAE,WAAW,OAAO,EAAE;AAAA,EACtD;AAEA,MAAI,UAAU;AACd,MAAI,UAAU;AACd,aAAW,OAAO,MAAM;AACtB,UAAM,SAAS,MAAM,wBAAwB,GAAG,KAAK,GAAG;AAAA,MACtD,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI;AAAA,MACpB,eAAe,IAAI;AAAA,IACrB,CAAC;AACD,eAAW;AACX,QAAI,CAAC,OAAO,YAAY;AACtB,iBAAW;AACX,cAAQ,KAAK,8CAA8C;AAAA,QACzD,eAAe,IAAI;AAAA,QACnB,UAAU,IAAI;AAAA,QACd,gBAAgB,IAAI;AAAA,QACpB,eAAe,OAAO;AAAA,QACtB,cAAc,OAAO;AAAA,QACrB,oBAAoB,OAAO;AAAA,MAC7B,CAAC;AAAA,IACH;AAAA,EACF;AAEA,MAAI,KAAK,WAAW,YAAY;AAC9B,YAAQ;AAAA,MACN,oCAAoC,UAAU;AAAA,IAChD;AAAA,EACF;AACA,MAAI,UAAU,GAAG;AACf,YAAQ,KAAK,uBAAuB,OAAO,IAAI,OAAO,2BAA2B;AAAA,EACnF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dainamite/billing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Dainamite Billing — lightweight recurring-billing engine for Open Mercato (Bill Runs, one-time / recurring / usage items, draft invoices in core/sales with human approval).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/modules/billing/index.js",
|
|
@@ -117,14 +117,14 @@
|
|
|
117
117
|
}
|
|
118
118
|
},
|
|
119
119
|
"peerDependencies": {
|
|
120
|
-
"@mikro-orm/core": "^7.
|
|
121
|
-
"@mikro-orm/decorators": "^7.
|
|
122
|
-
"@mikro-orm/postgresql": "^7.
|
|
123
|
-
"@open-mercato/core": "^0.6.
|
|
124
|
-
"@open-mercato/events": "^0.6.
|
|
125
|
-
"@open-mercato/queue": "^0.6.
|
|
126
|
-
"@open-mercato/shared": "^0.6.
|
|
127
|
-
"@open-mercato/ui": "^0.6.
|
|
120
|
+
"@mikro-orm/core": "^7.1.5",
|
|
121
|
+
"@mikro-orm/decorators": "^7.1.5",
|
|
122
|
+
"@mikro-orm/postgresql": "^7.1.5",
|
|
123
|
+
"@open-mercato/core": "^0.6.7",
|
|
124
|
+
"@open-mercato/events": "^0.6.7",
|
|
125
|
+
"@open-mercato/queue": "^0.6.7",
|
|
126
|
+
"@open-mercato/shared": "^0.6.7",
|
|
127
|
+
"@open-mercato/ui": "^0.6.7",
|
|
128
128
|
"@tanstack/react-table": "^8.0.0 || ^9.0.0",
|
|
129
129
|
"awilix": "^12.0.0",
|
|
130
130
|
"lucide-react": "^0.400.0",
|
|
@@ -106,7 +106,7 @@ function makeLine(invoice: FakeInvoice, overrides: Partial<FakeLine> = {}): Fake
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
type Env = {
|
|
109
|
-
ctx: { container: { resolve: jest.MockedFunction<(name: string) => unknown> }; auth: { sub?: string } | null }
|
|
109
|
+
ctx: { container: { resolve: jest.MockedFunction<(name: string) => unknown> }; auth: { sub?: string; tenantId?: string; orgId?: string } | null }
|
|
110
110
|
em: {
|
|
111
111
|
findOne: jest.MockedFunction<(entity: unknown, where: unknown) => Promise<unknown>>
|
|
112
112
|
find: jest.MockedFunction<(entity: unknown, where: unknown) => Promise<unknown[]>>
|
|
@@ -131,7 +131,7 @@ function createEnv(
|
|
|
131
131
|
statusEntryPosted?: string | null
|
|
132
132
|
initialLines?: FakeLine[]
|
|
133
133
|
invoiceFinalLines?: FakeLine[]
|
|
134
|
-
auth?: { sub?: string } | null
|
|
134
|
+
auth?: { sub?: string; tenantId?: string; orgId?: string } | null
|
|
135
135
|
testInvoiceRows?: Array<{ id: string }>
|
|
136
136
|
} = {},
|
|
137
137
|
): Env {
|
|
@@ -199,7 +199,7 @@ function createEnv(
|
|
|
199
199
|
return {
|
|
200
200
|
ctx: {
|
|
201
201
|
container,
|
|
202
|
-
auth: options.auth ?? { sub: '99999999-9999-4999-8999-999999999999' },
|
|
202
|
+
auth: options.auth ?? { sub: '99999999-9999-4999-8999-999999999999', tenantId: TENANT, orgId: ORG },
|
|
203
203
|
},
|
|
204
204
|
em,
|
|
205
205
|
persistedEntities,
|