@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,317 @@
|
|
|
1
|
+
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
2
|
+
import {
|
|
3
|
+
ensureOrganizationScope,
|
|
4
|
+
ensureTenantScope
|
|
5
|
+
} from "@open-mercato/shared/lib/commands/scope";
|
|
6
|
+
import {
|
|
7
|
+
emitCrudSideEffects,
|
|
8
|
+
requireId
|
|
9
|
+
} from "@open-mercato/shared/lib/commands/helpers";
|
|
10
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
11
|
+
import { BillingAccount, BillingItem } from "../data/entities.js";
|
|
12
|
+
import { billingEntityIds } from "../data/entityIds.js";
|
|
13
|
+
import { findBySourceRef } from "../lib/idempotency.js";
|
|
14
|
+
import {
|
|
15
|
+
billingItemBulkCreateSchema,
|
|
16
|
+
billingItemCreateSchema,
|
|
17
|
+
billingItemDeleteSchema,
|
|
18
|
+
billingItemUpdateSchema,
|
|
19
|
+
validateRateJson
|
|
20
|
+
} from "../data/validators.js";
|
|
21
|
+
const itemIndexer = { entityType: billingEntityIds.item };
|
|
22
|
+
const itemEvents = { module: "billing", entity: "item", persistent: true };
|
|
23
|
+
function getEm(ctx) {
|
|
24
|
+
return ctx.container.resolve("em").fork();
|
|
25
|
+
}
|
|
26
|
+
function getDataEngine(ctx) {
|
|
27
|
+
return ctx.container.resolve("dataEngine");
|
|
28
|
+
}
|
|
29
|
+
async function assertAccountInScope(em, params) {
|
|
30
|
+
const account = await em.findOne(BillingAccount, {
|
|
31
|
+
id: params.billAccountId,
|
|
32
|
+
tenantId: params.tenantId,
|
|
33
|
+
organizationId: params.organizationId,
|
|
34
|
+
deletedAt: null
|
|
35
|
+
});
|
|
36
|
+
if (!account) {
|
|
37
|
+
throw new CrudHttpError(404, { error: "Billing account not found" });
|
|
38
|
+
}
|
|
39
|
+
return account;
|
|
40
|
+
}
|
|
41
|
+
const createItemCommand = {
|
|
42
|
+
id: "billing.items.create",
|
|
43
|
+
async execute(rawInput, ctx) {
|
|
44
|
+
const parsed = billingItemCreateSchema.parse(rawInput);
|
|
45
|
+
ensureTenantScope(ctx, parsed.tenantId);
|
|
46
|
+
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
47
|
+
const em = getEm(ctx);
|
|
48
|
+
const account = await assertAccountInScope(em, parsed);
|
|
49
|
+
if (parsed.sourceRef) {
|
|
50
|
+
const existing = await findBySourceRef(em, BillingItem, {
|
|
51
|
+
tenantId: parsed.tenantId,
|
|
52
|
+
billAccountId: parsed.billAccountId,
|
|
53
|
+
sourceRef: parsed.sourceRef
|
|
54
|
+
});
|
|
55
|
+
if (existing) {
|
|
56
|
+
return { id: existing.id, deduplicated: true };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
validateRateJson(parsed.type, parsed.rateJson);
|
|
60
|
+
const now = /* @__PURE__ */ new Date();
|
|
61
|
+
const entity = em.create(BillingItem, {
|
|
62
|
+
organizationId: parsed.organizationId,
|
|
63
|
+
tenantId: parsed.tenantId,
|
|
64
|
+
billAccountId: account.id,
|
|
65
|
+
type: parsed.type,
|
|
66
|
+
billStartDate: parsed.billStartDate,
|
|
67
|
+
billEndDate: parsed.billEndDate ?? null,
|
|
68
|
+
description: parsed.description,
|
|
69
|
+
rateJson: parsed.rateJson,
|
|
70
|
+
uomCode: parsed.uomCode ?? null,
|
|
71
|
+
subscriptionId: parsed.subscriptionId ?? null,
|
|
72
|
+
subscriptionItemId: parsed.subscriptionItemId ?? null,
|
|
73
|
+
sourceRef: parsed.sourceRef ?? null,
|
|
74
|
+
// `currency_mismatch` is set by Phase 2 / connectors when the
|
|
75
|
+
// posted value's currency differs from the account's. Stays
|
|
76
|
+
// false on create (operator opts in to flagging).
|
|
77
|
+
currencyMismatch: false,
|
|
78
|
+
// `billed_to_date` is set by the Bill Run engine on the first
|
|
79
|
+
// real-run that emits this item. Always null on create.
|
|
80
|
+
billedToDate: null,
|
|
81
|
+
isActive: true,
|
|
82
|
+
createdAt: now,
|
|
83
|
+
updatedAt: now
|
|
84
|
+
});
|
|
85
|
+
em.persist(entity);
|
|
86
|
+
await em.flush();
|
|
87
|
+
await emitCrudSideEffects({
|
|
88
|
+
dataEngine: getDataEngine(ctx),
|
|
89
|
+
action: "created",
|
|
90
|
+
entity,
|
|
91
|
+
identifiers: {
|
|
92
|
+
id: entity.id,
|
|
93
|
+
tenantId: entity.tenantId,
|
|
94
|
+
organizationId: entity.organizationId
|
|
95
|
+
},
|
|
96
|
+
indexer: itemIndexer,
|
|
97
|
+
events: itemEvents
|
|
98
|
+
});
|
|
99
|
+
return { id: entity.id, deduplicated: false };
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const updateItemCommand = {
|
|
103
|
+
id: "billing.items.update",
|
|
104
|
+
async execute(rawInput, ctx) {
|
|
105
|
+
const parsed = billingItemUpdateSchema.parse(rawInput);
|
|
106
|
+
ensureTenantScope(ctx, parsed.tenantId);
|
|
107
|
+
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
108
|
+
const em = getEm(ctx);
|
|
109
|
+
const record = await em.findOne(BillingItem, {
|
|
110
|
+
id: parsed.id,
|
|
111
|
+
tenantId: parsed.tenantId,
|
|
112
|
+
organizationId: parsed.organizationId,
|
|
113
|
+
deletedAt: null
|
|
114
|
+
});
|
|
115
|
+
if (!record) {
|
|
116
|
+
throw new CrudHttpError(404, { error: "Billing item not found" });
|
|
117
|
+
}
|
|
118
|
+
if (parsed.billStartDate !== void 0) record.billStartDate = parsed.billStartDate;
|
|
119
|
+
if (parsed.billEndDate !== void 0) record.billEndDate = parsed.billEndDate;
|
|
120
|
+
if (parsed.description !== void 0) record.description = parsed.description;
|
|
121
|
+
if (parsed.rateJson !== void 0) {
|
|
122
|
+
validateRateJson(record.type, parsed.rateJson);
|
|
123
|
+
record.rateJson = parsed.rateJson;
|
|
124
|
+
}
|
|
125
|
+
if (parsed.uomCode !== void 0) record.uomCode = parsed.uomCode;
|
|
126
|
+
if (parsed.subscriptionId !== void 0) record.subscriptionId = parsed.subscriptionId;
|
|
127
|
+
if (parsed.subscriptionItemId !== void 0) {
|
|
128
|
+
record.subscriptionItemId = parsed.subscriptionItemId;
|
|
129
|
+
}
|
|
130
|
+
if (parsed.isActive !== void 0) record.isActive = parsed.isActive;
|
|
131
|
+
record.updatedAt = /* @__PURE__ */ new Date();
|
|
132
|
+
await em.flush();
|
|
133
|
+
await emitCrudSideEffects({
|
|
134
|
+
dataEngine: getDataEngine(ctx),
|
|
135
|
+
action: "updated",
|
|
136
|
+
entity: record,
|
|
137
|
+
identifiers: {
|
|
138
|
+
id: record.id,
|
|
139
|
+
tenantId: record.tenantId,
|
|
140
|
+
organizationId: record.organizationId
|
|
141
|
+
},
|
|
142
|
+
indexer: itemIndexer,
|
|
143
|
+
events: itemEvents
|
|
144
|
+
});
|
|
145
|
+
return { id: record.id };
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const deleteItemCommand = {
|
|
149
|
+
id: "billing.items.delete",
|
|
150
|
+
async execute(rawInput, ctx) {
|
|
151
|
+
const parsed = billingItemDeleteSchema.parse(rawInput);
|
|
152
|
+
ensureTenantScope(ctx, parsed.tenantId);
|
|
153
|
+
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
154
|
+
const id = requireId(parsed);
|
|
155
|
+
const em = getEm(ctx);
|
|
156
|
+
const record = await em.findOne(BillingItem, {
|
|
157
|
+
id,
|
|
158
|
+
tenantId: parsed.tenantId,
|
|
159
|
+
organizationId: parsed.organizationId,
|
|
160
|
+
deletedAt: null
|
|
161
|
+
});
|
|
162
|
+
if (!record) {
|
|
163
|
+
throw new CrudHttpError(404, { error: "Billing item not found" });
|
|
164
|
+
}
|
|
165
|
+
record.deletedAt = /* @__PURE__ */ new Date();
|
|
166
|
+
record.updatedAt = record.deletedAt;
|
|
167
|
+
await em.flush();
|
|
168
|
+
await emitCrudSideEffects({
|
|
169
|
+
dataEngine: getDataEngine(ctx),
|
|
170
|
+
action: "deleted",
|
|
171
|
+
entity: record,
|
|
172
|
+
identifiers: {
|
|
173
|
+
id: record.id,
|
|
174
|
+
tenantId: record.tenantId,
|
|
175
|
+
organizationId: record.organizationId
|
|
176
|
+
},
|
|
177
|
+
indexer: itemIndexer,
|
|
178
|
+
events: itemEvents
|
|
179
|
+
});
|
|
180
|
+
return { id: record.id };
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const bulkCreateItemsCommand = {
|
|
184
|
+
id: "billing.items.bulk_create",
|
|
185
|
+
async execute(rawInput, ctx) {
|
|
186
|
+
const parsed = billingItemBulkCreateSchema.parse(rawInput);
|
|
187
|
+
ensureTenantScope(ctx, parsed.tenantId);
|
|
188
|
+
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
189
|
+
const em = getEm(ctx);
|
|
190
|
+
const accountIds = Array.from(new Set(parsed.items.map((i) => i.billAccountId)));
|
|
191
|
+
const accounts = await em.find(BillingAccount, {
|
|
192
|
+
id: { $in: accountIds },
|
|
193
|
+
tenantId: parsed.tenantId,
|
|
194
|
+
organizationId: parsed.organizationId,
|
|
195
|
+
deletedAt: null
|
|
196
|
+
});
|
|
197
|
+
const knownAccountIds = new Set(accounts.map((a) => a.id));
|
|
198
|
+
const missingAccount = accountIds.find((id) => !knownAccountIds.has(id));
|
|
199
|
+
if (missingAccount) {
|
|
200
|
+
throw new CrudHttpError(404, {
|
|
201
|
+
error: "Billing account not found",
|
|
202
|
+
billAccountId: missingAccount
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const sourceRefs = parsed.items.map((i) => i.sourceRef).filter((ref) => typeof ref === "string" && ref.length > 0);
|
|
206
|
+
const existingBySourceKey = /* @__PURE__ */ new Map();
|
|
207
|
+
if (sourceRefs.length > 0) {
|
|
208
|
+
const existing = await em.find(BillingItem, {
|
|
209
|
+
tenantId: parsed.tenantId,
|
|
210
|
+
organizationId: parsed.organizationId,
|
|
211
|
+
billAccountId: { $in: accountIds },
|
|
212
|
+
sourceRef: { $in: Array.from(new Set(sourceRefs)) },
|
|
213
|
+
deletedAt: null
|
|
214
|
+
});
|
|
215
|
+
for (const row of existing) {
|
|
216
|
+
if (row.sourceRef) {
|
|
217
|
+
existingBySourceKey.set(`${row.billAccountId}::${row.sourceRef}`, row.id);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const now = /* @__PURE__ */ new Date();
|
|
222
|
+
const results = [];
|
|
223
|
+
const createdEntities = [];
|
|
224
|
+
const deferredIdLinks = [];
|
|
225
|
+
const seenInBatch = /* @__PURE__ */ new Set();
|
|
226
|
+
for (const entry of parsed.items) {
|
|
227
|
+
const sourceKey = entry.sourceRef ? `${entry.billAccountId}::${entry.sourceRef}` : null;
|
|
228
|
+
if (sourceKey) {
|
|
229
|
+
const existingId = existingBySourceKey.get(sourceKey);
|
|
230
|
+
if (existingId) {
|
|
231
|
+
results.push({ sourceRef: entry.sourceRef ?? null, id: existingId, deduplicated: true });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (seenInBatch.has(sourceKey)) {
|
|
235
|
+
const firstCreated = createdEntities.find(
|
|
236
|
+
(e) => e.billAccountId === entry.billAccountId && e.sourceRef === entry.sourceRef
|
|
237
|
+
);
|
|
238
|
+
const dedupResult = {
|
|
239
|
+
sourceRef: entry.sourceRef ?? null,
|
|
240
|
+
id: "",
|
|
241
|
+
deduplicated: true
|
|
242
|
+
};
|
|
243
|
+
results.push(dedupResult);
|
|
244
|
+
if (firstCreated) deferredIdLinks.push({ result: dedupResult, entity: firstCreated });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
seenInBatch.add(sourceKey);
|
|
248
|
+
}
|
|
249
|
+
validateRateJson(entry.type, entry.rateJson);
|
|
250
|
+
const item = em.create(BillingItem, {
|
|
251
|
+
organizationId: parsed.organizationId,
|
|
252
|
+
tenantId: parsed.tenantId,
|
|
253
|
+
billAccountId: entry.billAccountId,
|
|
254
|
+
type: entry.type,
|
|
255
|
+
billStartDate: entry.billStartDate,
|
|
256
|
+
billEndDate: entry.billEndDate ?? null,
|
|
257
|
+
description: entry.description,
|
|
258
|
+
rateJson: entry.rateJson,
|
|
259
|
+
uomCode: entry.uomCode ?? null,
|
|
260
|
+
subscriptionId: entry.subscriptionId ?? null,
|
|
261
|
+
subscriptionItemId: entry.subscriptionItemId ?? null,
|
|
262
|
+
sourceRef: entry.sourceRef ?? null,
|
|
263
|
+
currencyMismatch: false,
|
|
264
|
+
billedToDate: null,
|
|
265
|
+
isActive: true,
|
|
266
|
+
createdAt: now,
|
|
267
|
+
updatedAt: now
|
|
268
|
+
});
|
|
269
|
+
em.persist(item);
|
|
270
|
+
createdEntities.push(item);
|
|
271
|
+
const createdResult = {
|
|
272
|
+
sourceRef: entry.sourceRef ?? null,
|
|
273
|
+
id: "",
|
|
274
|
+
deduplicated: false
|
|
275
|
+
};
|
|
276
|
+
results.push(createdResult);
|
|
277
|
+
deferredIdLinks.push({ result: createdResult, entity: item });
|
|
278
|
+
}
|
|
279
|
+
if (createdEntities.length > 0) {
|
|
280
|
+
await em.flush();
|
|
281
|
+
}
|
|
282
|
+
for (const link of deferredIdLinks) {
|
|
283
|
+
link.result.id = link.entity.id;
|
|
284
|
+
}
|
|
285
|
+
const dataEngine = getDataEngine(ctx);
|
|
286
|
+
for (const item of createdEntities) {
|
|
287
|
+
await emitCrudSideEffects({
|
|
288
|
+
dataEngine,
|
|
289
|
+
action: "created",
|
|
290
|
+
entity: item,
|
|
291
|
+
identifiers: {
|
|
292
|
+
id: item.id,
|
|
293
|
+
tenantId: item.tenantId,
|
|
294
|
+
organizationId: item.organizationId
|
|
295
|
+
},
|
|
296
|
+
indexer: itemIndexer,
|
|
297
|
+
events: itemEvents
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
created: createdEntities.length,
|
|
302
|
+
deduplicated: results.filter((r) => r.deduplicated).length,
|
|
303
|
+
items: results
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
registerCommand(createItemCommand);
|
|
308
|
+
registerCommand(updateItemCommand);
|
|
309
|
+
registerCommand(deleteItemCommand);
|
|
310
|
+
registerCommand(bulkCreateItemsCommand);
|
|
311
|
+
export {
|
|
312
|
+
bulkCreateItemsCommand,
|
|
313
|
+
createItemCommand,
|
|
314
|
+
deleteItemCommand,
|
|
315
|
+
updateItemCommand
|
|
316
|
+
};
|
|
317
|
+
//# sourceMappingURL=items.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/commands/items.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport {\n emitCrudSideEffects,\n requireId,\n} from '@open-mercato/shared/lib/commands/helpers'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { BillingAccount, BillingItem } from '../data/entities'\nimport { billingEntityIds } from '../data/entityIds'\nimport { findBySourceRef } from '../lib/idempotency'\nimport {\n billingItemBulkCreateSchema,\n billingItemCreateSchema,\n billingItemDeleteSchema,\n billingItemUpdateSchema,\n validateRateJson,\n type BillingItemBulkCreateInput,\n type BillingItemCreateInput,\n type BillingItemDeleteInput,\n type BillingItemUpdateInput,\n} from '../data/validators'\n\n/**\n * BillingItem writes \u2014 create / update / soft-delete.\n *\n * Create is idempotent via `source_ref`: if the integrator POSTs the\n * same `(tenant_id, bill_account_id, source_ref)` twice, the second\n * call returns the existing item with `deduplicated: true`. The\n * underlying unique index is the correctness gate; this pre-check keeps\n * the happy path single-round-trip and the response body informative.\n */\n\nconst itemIndexer = { entityType: billingEntityIds.item } as const\nconst itemEvents = { module: 'billing', entity: 'item', persistent: true } as const\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nfunction getDataEngine(ctx: CommandRuntimeContext): DataEngine {\n return ctx.container.resolve('dataEngine') as DataEngine\n}\n\nasync function assertAccountInScope(\n em: EntityManager,\n params: { tenantId: string; organizationId: string; billAccountId: string },\n): Promise<BillingAccount> {\n const account = await em.findOne(BillingAccount, {\n id: params.billAccountId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n deletedAt: null,\n })\n if (!account) {\n throw new CrudHttpError(404, { error: 'Billing account not found' })\n }\n return account\n}\n\n// \u2500\u2500\u2500 Create (idempotent) \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\u2500\u2500\u2500\n\nconst createItemCommand: CommandHandler<\n BillingItemCreateInput,\n { id: string; deduplicated: boolean }\n> = {\n id: 'billing.items.create',\n\n async execute(rawInput, ctx) {\n const parsed = billingItemCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const account = await assertAccountInScope(em, parsed)\n\n // Idempotency: if `source_ref` provided and matches an existing\n // (non-deleted) row, return that row. The unique index is the\n // final correctness gate \u2014 this is the happy-path early return.\n if (parsed.sourceRef) {\n const existing = await findBySourceRef(em, BillingItem, {\n tenantId: parsed.tenantId,\n billAccountId: parsed.billAccountId,\n sourceRef: parsed.sourceRef,\n })\n if (existing) {\n return { id: existing.id, deduplicated: true }\n }\n }\n\n // Re-validate the rate_json shape against the resolved type to\n // future-proof against schema drift between the API surface and\n // the persistence layer.\n validateRateJson(parsed.type, parsed.rateJson)\n\n const now = new Date()\n const entity = em.create(BillingItem, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n billAccountId: account.id,\n type: parsed.type,\n billStartDate: parsed.billStartDate,\n billEndDate: parsed.billEndDate ?? null,\n description: parsed.description,\n rateJson: parsed.rateJson as Record<string, unknown>,\n uomCode: parsed.uomCode ?? null,\n subscriptionId: parsed.subscriptionId ?? null,\n subscriptionItemId: parsed.subscriptionItemId ?? null,\n sourceRef: parsed.sourceRef ?? null,\n // `currency_mismatch` is set by Phase 2 / connectors when the\n // posted value's currency differs from the account's. Stays\n // false on create (operator opts in to flagging).\n currencyMismatch: false,\n // `billed_to_date` is set by the Bill Run engine on the first\n // real-run that emits this item. Always null on create.\n billedToDate: null,\n isActive: true,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(entity)\n await em.flush()\n\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'created',\n entity,\n identifiers: {\n id: entity.id,\n tenantId: entity.tenantId,\n organizationId: entity.organizationId,\n },\n indexer: itemIndexer,\n events: itemEvents,\n })\n\n return { id: entity.id, deduplicated: false }\n },\n}\n\n// \u2500\u2500\u2500 Update \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst updateItemCommand: CommandHandler<BillingItemUpdateInput, { id: string }> = {\n id: 'billing.items.update',\n\n async execute(rawInput, ctx) {\n const parsed = billingItemUpdateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const record = await em.findOne(BillingItem, {\n id: parsed.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!record) {\n throw new CrudHttpError(404, { error: 'Billing item not found' })\n }\n\n if (parsed.billStartDate !== undefined) record.billStartDate = parsed.billStartDate\n if (parsed.billEndDate !== undefined) record.billEndDate = parsed.billEndDate\n if (parsed.description !== undefined) record.description = parsed.description\n if (parsed.rateJson !== undefined) {\n // Re-validate against the current (immutable) type before persist.\n validateRateJson(record.type as 'one_time' | 'recurring' | 'usage', parsed.rateJson)\n record.rateJson = parsed.rateJson as Record<string, unknown>\n }\n if (parsed.uomCode !== undefined) record.uomCode = parsed.uomCode\n if (parsed.subscriptionId !== undefined) record.subscriptionId = parsed.subscriptionId\n if (parsed.subscriptionItemId !== undefined) {\n record.subscriptionItemId = parsed.subscriptionItemId\n }\n if (parsed.isActive !== undefined) record.isActive = parsed.isActive\n\n record.updatedAt = new Date()\n await em.flush()\n\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'updated',\n entity: record,\n identifiers: {\n id: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n },\n indexer: itemIndexer,\n events: itemEvents,\n })\n\n return { id: record.id }\n },\n}\n\n// \u2500\u2500\u2500 Delete (soft) \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst deleteItemCommand: CommandHandler<BillingItemDeleteInput, { id: string }> = {\n id: 'billing.items.delete',\n\n async execute(rawInput, ctx) {\n const parsed = billingItemDeleteSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n const id = requireId(parsed)\n\n const em = getEm(ctx)\n const record = await em.findOne(BillingItem, {\n id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!record) {\n throw new CrudHttpError(404, { error: 'Billing item not found' })\n }\n\n record.deletedAt = new Date()\n record.updatedAt = record.deletedAt\n await em.flush()\n\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'deleted',\n entity: record,\n identifiers: {\n id: record.id,\n tenantId: record.tenantId,\n organizationId: record.organizationId,\n },\n indexer: itemIndexer,\n events: itemEvents,\n })\n\n return { id: record.id }\n },\n}\n\n// \u2500\u2500\u2500 Bulk create (idempotent, batched) \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//\n// Collapses N `billing.items.create` round-trips into one command.\n// The CPQ connector activating a large subscription is the primary\n// caller \u2014 it used to fire one create per charge.\n//\n// Idempotency is still per-`source_ref` but the existence check is a\n// SINGLE query over all source_refs in the batch, instead of one\n// `findOne` per item. Inserts are one `em.flush()` for the whole\n// batch. So a 200-charge activation goes from ~200 round-trips +\n// 200 flushes to ~3 queries + 1 flush.\n\ntype BulkCreateResultEntry = {\n sourceRef: string | null\n id: string\n deduplicated: boolean\n}\n\nconst bulkCreateItemsCommand: CommandHandler<\n BillingItemBulkCreateInput,\n { created: number; deduplicated: number; items: BulkCreateResultEntry[] }\n> = {\n id: 'billing.items.bulk_create',\n\n async execute(rawInput, ctx) {\n const parsed = billingItemBulkCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n\n // Validate every referenced account exists + is in scope. One\n // `$in` query for the distinct account ids, not one per item.\n const accountIds = Array.from(new Set(parsed.items.map((i) => i.billAccountId)))\n const accounts = await em.find(BillingAccount, {\n id: { $in: accountIds },\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n } as never)\n const knownAccountIds = new Set(accounts.map((a) => a.id))\n const missingAccount = accountIds.find((id) => !knownAccountIds.has(id))\n if (missingAccount) {\n throw new CrudHttpError(404, {\n error: 'Billing account not found',\n billAccountId: missingAccount,\n })\n }\n\n // Batched idempotency check \u2014 one query over every source_ref in\n // the batch. `source_ref` is unique per `(tenant, bill_account,\n // source_ref)`, so we key the existing-set on that triple.\n const sourceRefs = parsed.items\n .map((i) => i.sourceRef)\n .filter((ref): ref is string => typeof ref === 'string' && ref.length > 0)\n const existingBySourceKey = new Map<string, string>()\n if (sourceRefs.length > 0) {\n const existing = await em.find(BillingItem, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n billAccountId: { $in: accountIds },\n sourceRef: { $in: Array.from(new Set(sourceRefs)) },\n deletedAt: null,\n } as never)\n for (const row of existing) {\n if (row.sourceRef) {\n existingBySourceKey.set(`${row.billAccountId}::${row.sourceRef}`, row.id)\n }\n }\n }\n\n const now = new Date()\n const results: BulkCreateResultEntry[] = []\n const createdEntities: BillingItem[] = []\n // Result entries whose `id` is only known after the flush \u2014 the\n // BillingItem PK is a DB-generated UUID, unassigned until then.\n // Covers freshly-created rows and in-batch dedup rows that point\n // at a sibling created earlier in the same payload.\n const deferredIdLinks: Array<{ result: BulkCreateResultEntry; entity: BillingItem }> = []\n\n // Track source_refs created within THIS batch so a payload that\n // repeats the same source_ref twice dedups against itself (the\n // unique index would otherwise blow up the whole flush).\n const seenInBatch = new Set<string>()\n\n for (const entry of parsed.items) {\n const sourceKey = entry.sourceRef\n ? `${entry.billAccountId}::${entry.sourceRef}`\n : null\n\n if (sourceKey) {\n const existingId = existingBySourceKey.get(sourceKey)\n if (existingId) {\n results.push({ sourceRef: entry.sourceRef ?? null, id: existingId, deduplicated: true })\n continue\n }\n if (seenInBatch.has(sourceKey)) {\n // Duplicate source_ref inside the same payload \u2014 first wins,\n // the repeat is reported deduplicated against the first.\n const firstCreated = createdEntities.find(\n (e) => e.billAccountId === entry.billAccountId && e.sourceRef === entry.sourceRef,\n )\n const dedupResult: BulkCreateResultEntry = {\n sourceRef: entry.sourceRef ?? null,\n id: '',\n deduplicated: true,\n }\n results.push(dedupResult)\n if (firstCreated) deferredIdLinks.push({ result: dedupResult, entity: firstCreated })\n continue\n }\n seenInBatch.add(sourceKey)\n }\n\n validateRateJson(entry.type, entry.rateJson)\n const item = em.create(BillingItem, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n billAccountId: entry.billAccountId,\n type: entry.type,\n billStartDate: entry.billStartDate,\n billEndDate: entry.billEndDate ?? null,\n description: entry.description,\n rateJson: entry.rateJson as Record<string, unknown>,\n uomCode: entry.uomCode ?? null,\n subscriptionId: entry.subscriptionId ?? null,\n subscriptionItemId: entry.subscriptionItemId ?? null,\n sourceRef: entry.sourceRef ?? null,\n currencyMismatch: false,\n billedToDate: null,\n isActive: true,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(item)\n createdEntities.push(item)\n const createdResult: BulkCreateResultEntry = {\n sourceRef: entry.sourceRef ?? null,\n id: '',\n deduplicated: false,\n }\n results.push(createdResult)\n deferredIdLinks.push({ result: createdResult, entity: item })\n }\n\n // Single flush for the whole batch.\n if (createdEntities.length > 0) {\n await em.flush()\n }\n\n // The BillingItem UUID PK is assigned by the flush \u2014 backfill it\n // into the result entries now that the value exists.\n for (const link of deferredIdLinks) {\n link.result.id = link.entity.id\n }\n\n // Side effects after the commit. Emitting per-entity keeps the\n // query-index + event contract identical to single-create \u2014 the\n // engine / search index can't tell a bulk insert from N singles.\n const dataEngine = getDataEngine(ctx)\n for (const item of createdEntities) {\n await emitCrudSideEffects({\n dataEngine,\n action: 'created',\n entity: item,\n identifiers: {\n id: item.id,\n tenantId: item.tenantId,\n organizationId: item.organizationId,\n },\n indexer: itemIndexer,\n events: itemEvents,\n })\n }\n\n return {\n created: createdEntities.length,\n deduplicated: results.filter((r) => r.deduplicated).length,\n items: results,\n }\n },\n}\n\nregisterCommand(createItemCommand)\nregisterCommand(updateItemCommand)\nregisterCommand(deleteItemCommand)\nregisterCommand(bulkCreateItemsCommand)\n\nexport {\n createItemCommand,\n updateItemCommand,\n deleteItemCommand,\n bulkCreateItemsCommand,\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
2
|
+
import {
|
|
3
|
+
ensureOrganizationScope,
|
|
4
|
+
ensureTenantScope
|
|
5
|
+
} from "@open-mercato/shared/lib/commands/scope";
|
|
6
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
7
|
+
import { BillRun, BillRunOutcome } from "../data/entities.js";
|
|
8
|
+
import {
|
|
9
|
+
billingRunRetryFailedSchema,
|
|
10
|
+
billingRunTriggerSchema
|
|
11
|
+
} from "../data/validators.js";
|
|
12
|
+
import { runBillRun } from "../lib/billRunEngine.js";
|
|
13
|
+
import { TenantLockBusyError } from "../lib/tenantLock.js";
|
|
14
|
+
function getEm(ctx) {
|
|
15
|
+
return ctx.container.resolve("em").fork();
|
|
16
|
+
}
|
|
17
|
+
function getContainer(ctx) {
|
|
18
|
+
return ctx.container;
|
|
19
|
+
}
|
|
20
|
+
function asLockBusyError(err) {
|
|
21
|
+
return err instanceof TenantLockBusyError;
|
|
22
|
+
}
|
|
23
|
+
const triggerRunCommand = {
|
|
24
|
+
id: "billing.runs.trigger",
|
|
25
|
+
async execute(rawInput, ctx) {
|
|
26
|
+
const parsed = billingRunTriggerSchema.parse(rawInput);
|
|
27
|
+
ensureTenantScope(ctx, parsed.tenantId);
|
|
28
|
+
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
29
|
+
const em = getEm(ctx);
|
|
30
|
+
const asOfDate = parsed.asOfDate ?? /* @__PURE__ */ new Date();
|
|
31
|
+
try {
|
|
32
|
+
const { billRun, outcomes } = await runBillRun(em, getContainer(ctx), {
|
|
33
|
+
tenantId: parsed.tenantId,
|
|
34
|
+
organizationId: parsed.organizationId,
|
|
35
|
+
asOfDate,
|
|
36
|
+
triggeredBy: "manual",
|
|
37
|
+
mode: parsed.mode,
|
|
38
|
+
scopedAccountIds: parsed.scopedAccountIds,
|
|
39
|
+
catchUp: parsed.catchUp
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
billRunId: billRun.id,
|
|
43
|
+
status: billRun.status,
|
|
44
|
+
outcomesCount: outcomes.length
|
|
45
|
+
};
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (asLockBusyError(err)) {
|
|
48
|
+
throw new CrudHttpError(409, {
|
|
49
|
+
error: "Another Bill Run is already in progress for this tenant",
|
|
50
|
+
code: "billing.run.lock_busy",
|
|
51
|
+
tenantId: err.tenantId,
|
|
52
|
+
lockName: err.lockName
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const retryFailedCommand = {
|
|
60
|
+
id: "billing.runs.retry_failed",
|
|
61
|
+
async execute(rawInput, ctx) {
|
|
62
|
+
const parsed = billingRunRetryFailedSchema.parse(rawInput);
|
|
63
|
+
ensureTenantScope(ctx, parsed.tenantId);
|
|
64
|
+
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
65
|
+
const em = getEm(ctx);
|
|
66
|
+
const parentRun = await em.findOne(BillRun, {
|
|
67
|
+
id: parsed.billRunId,
|
|
68
|
+
tenantId: parsed.tenantId,
|
|
69
|
+
organizationId: parsed.organizationId,
|
|
70
|
+
deletedAt: null
|
|
71
|
+
});
|
|
72
|
+
if (!parentRun) {
|
|
73
|
+
throw new CrudHttpError(404, { error: "Parent Bill Run not found" });
|
|
74
|
+
}
|
|
75
|
+
const failedOutcomes = await em.find(BillRunOutcome, {
|
|
76
|
+
billRunId: parentRun.id,
|
|
77
|
+
tenantId: parsed.tenantId,
|
|
78
|
+
organizationId: parsed.organizationId,
|
|
79
|
+
status: "failed"
|
|
80
|
+
});
|
|
81
|
+
if (failedOutcomes.length === 0) {
|
|
82
|
+
throw new CrudHttpError(409, {
|
|
83
|
+
error: "Parent run has no failed outcomes to retry",
|
|
84
|
+
code: "billing.run.nothing_to_retry"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const scopedAccountIds = Array.from(new Set(failedOutcomes.map((o) => o.billAccountId)));
|
|
88
|
+
try {
|
|
89
|
+
const { billRun, outcomes } = await runBillRun(em, getContainer(ctx), {
|
|
90
|
+
tenantId: parsed.tenantId,
|
|
91
|
+
organizationId: parsed.organizationId,
|
|
92
|
+
asOfDate: parentRun.asOfDate,
|
|
93
|
+
triggeredBy: "manual",
|
|
94
|
+
mode: parsed.mode,
|
|
95
|
+
scopedAccountIds,
|
|
96
|
+
parentRunId: parentRun.id,
|
|
97
|
+
catchUp: parentRun.catchUp
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
billRunId: billRun.id,
|
|
101
|
+
status: billRun.status,
|
|
102
|
+
outcomesCount: outcomes.length
|
|
103
|
+
};
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (asLockBusyError(err)) {
|
|
106
|
+
throw new CrudHttpError(409, {
|
|
107
|
+
error: "Another Bill Run is already in progress for this tenant",
|
|
108
|
+
code: "billing.run.lock_busy",
|
|
109
|
+
tenantId: err.tenantId,
|
|
110
|
+
lockName: err.lockName
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
registerCommand(triggerRunCommand);
|
|
118
|
+
registerCommand(retryFailedCommand);
|
|
119
|
+
export {
|
|
120
|
+
retryFailedCommand,
|
|
121
|
+
triggerRunCommand
|
|
122
|
+
};
|
|
123
|
+
//# sourceMappingURL=runs.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/commands/runs.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { BillRun, BillRunOutcome } from '../data/entities'\nimport {\n billingRunRetryFailedSchema,\n billingRunTriggerSchema,\n type BillingRunRetryFailedInput,\n type BillingRunTriggerInput,\n} from '../data/validators'\nimport { runBillRun } from '../lib/billRunEngine'\nimport { TenantLockBusyError } from '../lib/tenantLock'\n\n/**\n * Bill Run trigger commands.\n *\n * Both commands wrap `runBillRun(...)` from `lib/billRunEngine`. The\n * lock-busy condition (concurrent run on the same tenant) is mapped to\n * HTTP 409 here so the API surface returns the canonical Conflict\n * response without each route knowing the lock primitive's\n * implementation.\n *\n * `triggeredBy` is intentionally NOT exposed on the input schema \u2014\n * client-driven triggers are always `'manual'`. The scheduler worker\n * (Phase 4) injects `'schedule'` directly through a separate\n * orchestrator path, not through this command.\n */\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nfunction getContainer(ctx: CommandRuntimeContext): AwilixContainer {\n return ctx.container as AwilixContainer\n}\n\nfunction asLockBusyError(err: unknown): err is TenantLockBusyError {\n return err instanceof TenantLockBusyError\n}\n\n// \u2500\u2500\u2500 Trigger \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype TriggerResult = {\n billRunId: string\n status: string\n outcomesCount: number\n}\n\nconst triggerRunCommand: CommandHandler<BillingRunTriggerInput, TriggerResult> = {\n id: 'billing.runs.trigger',\n\n async execute(rawInput, ctx) {\n const parsed = billingRunTriggerSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const asOfDate = parsed.asOfDate ?? new Date()\n\n try {\n const { billRun, outcomes } = await runBillRun(em, getContainer(ctx), {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n asOfDate,\n triggeredBy: 'manual',\n mode: parsed.mode,\n scopedAccountIds: parsed.scopedAccountIds,\n catchUp: parsed.catchUp,\n })\n return {\n billRunId: billRun.id,\n status: billRun.status,\n outcomesCount: outcomes.length,\n }\n } catch (err) {\n if (asLockBusyError(err)) {\n // The route metadata maps `CrudHttpError(409)` to the canonical\n // 409 Conflict response. The error body carries the\n // `error_lock_busy` discriminator so clients can distinguish\n // \"lock busy\" from other 409 conditions.\n throw new CrudHttpError(409, {\n error: 'Another Bill Run is already in progress for this tenant',\n code: 'billing.run.lock_busy',\n tenantId: err.tenantId,\n lockName: err.lockName,\n })\n }\n throw err\n }\n },\n}\n\n// \u2500\u2500\u2500 Retry failed \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst retryFailedCommand: CommandHandler<BillingRunRetryFailedInput, TriggerResult> = {\n id: 'billing.runs.retry_failed',\n\n async execute(rawInput, ctx) {\n const parsed = billingRunRetryFailedSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const parentRun = await em.findOne(BillRun, {\n id: parsed.billRunId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!parentRun) {\n throw new CrudHttpError(404, { error: 'Parent Bill Run not found' })\n }\n const failedOutcomes = await em.find(BillRunOutcome, {\n billRunId: parentRun.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n status: 'failed',\n })\n if (failedOutcomes.length === 0) {\n throw new CrudHttpError(409, {\n error: 'Parent run has no failed outcomes to retry',\n code: 'billing.run.nothing_to_retry',\n })\n }\n const scopedAccountIds = Array.from(new Set(failedOutcomes.map((o) => o.billAccountId)))\n\n try {\n const { billRun, outcomes } = await runBillRun(em, getContainer(ctx), {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n asOfDate: parentRun.asOfDate,\n triggeredBy: 'manual',\n mode: parsed.mode,\n scopedAccountIds,\n parentRunId: parentRun.id,\n catchUp: parentRun.catchUp,\n })\n return {\n billRunId: billRun.id,\n status: billRun.status,\n outcomesCount: outcomes.length,\n }\n } catch (err) {\n if (asLockBusyError(err)) {\n throw new CrudHttpError(409, {\n error: 'Another Bill Run is already in progress for this tenant',\n code: 'billing.run.lock_busy',\n tenantId: err.tenantId,\n lockName: err.lockName,\n })\n }\n throw err\n }\n },\n}\n\nregisterCommand(triggerRunCommand)\nregisterCommand(retryFailedCommand)\n\nexport { triggerRunCommand, retryFailedCommand }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
2
|
+
import {
|
|
3
|
+
ensureOrganizationScope,
|
|
4
|
+
ensureTenantScope
|
|
5
|
+
} from "@open-mercato/shared/lib/commands/scope";
|
|
6
|
+
import { emitCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
|
|
7
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
8
|
+
import { BillingAccount, BillingAccountUsage } from "../data/entities.js";
|
|
9
|
+
import { billingEntityIds } from "../data/entityIds.js";
|
|
10
|
+
import { findBySourceRef } from "../lib/idempotency.js";
|
|
11
|
+
import {
|
|
12
|
+
billingUsageCreateSchema
|
|
13
|
+
} from "../data/validators.js";
|
|
14
|
+
const usageIndexer = { entityType: billingEntityIds.usage };
|
|
15
|
+
const usageEvents = { module: "billing", entity: "usage", persistent: true };
|
|
16
|
+
function getEm(ctx) {
|
|
17
|
+
return ctx.container.resolve("em").fork();
|
|
18
|
+
}
|
|
19
|
+
function getDataEngine(ctx) {
|
|
20
|
+
return ctx.container.resolve("dataEngine");
|
|
21
|
+
}
|
|
22
|
+
const createUsageCommand = {
|
|
23
|
+
id: "billing.usage.create",
|
|
24
|
+
async execute(rawInput, ctx) {
|
|
25
|
+
const parsed = billingUsageCreateSchema.parse(rawInput);
|
|
26
|
+
ensureTenantScope(ctx, parsed.tenantId);
|
|
27
|
+
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
28
|
+
const em = getEm(ctx);
|
|
29
|
+
const account = await em.findOne(BillingAccount, {
|
|
30
|
+
id: parsed.billAccountId,
|
|
31
|
+
tenantId: parsed.tenantId,
|
|
32
|
+
organizationId: parsed.organizationId,
|
|
33
|
+
deletedAt: null
|
|
34
|
+
});
|
|
35
|
+
if (!account) {
|
|
36
|
+
throw new CrudHttpError(404, { error: "Billing account not found" });
|
|
37
|
+
}
|
|
38
|
+
if (parsed.sourceRef) {
|
|
39
|
+
const existing = await findBySourceRef(em, BillingAccountUsage, {
|
|
40
|
+
tenantId: parsed.tenantId,
|
|
41
|
+
billAccountId: parsed.billAccountId,
|
|
42
|
+
sourceRef: parsed.sourceRef
|
|
43
|
+
});
|
|
44
|
+
if (existing) {
|
|
45
|
+
return { id: existing.id, deduplicated: true };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const now = /* @__PURE__ */ new Date();
|
|
49
|
+
const entity = em.create(BillingAccountUsage, {
|
|
50
|
+
organizationId: parsed.organizationId,
|
|
51
|
+
tenantId: parsed.tenantId,
|
|
52
|
+
billAccountId: parsed.billAccountId,
|
|
53
|
+
uomCode: parsed.uomCode,
|
|
54
|
+
// numeric(18,4) — MikroORM stores numeric columns as string in app
|
|
55
|
+
// code. JSON arrives as number; coerce here so the persisted shape
|
|
56
|
+
// matches what later reads expect.
|
|
57
|
+
quantity: parsed.quantity.toFixed(4),
|
|
58
|
+
periodStart: parsed.periodStart,
|
|
59
|
+
periodEnd: parsed.periodEnd,
|
|
60
|
+
lineDescription: parsed.lineDescription ?? null,
|
|
61
|
+
sourceRef: parsed.sourceRef ?? null,
|
|
62
|
+
ratedInBillRunId: null,
|
|
63
|
+
isActive: true,
|
|
64
|
+
createdAt: now,
|
|
65
|
+
updatedAt: now
|
|
66
|
+
});
|
|
67
|
+
em.persist(entity);
|
|
68
|
+
await em.flush();
|
|
69
|
+
await emitCrudSideEffects({
|
|
70
|
+
dataEngine: getDataEngine(ctx),
|
|
71
|
+
action: "created",
|
|
72
|
+
entity,
|
|
73
|
+
identifiers: {
|
|
74
|
+
id: entity.id,
|
|
75
|
+
tenantId: entity.tenantId,
|
|
76
|
+
organizationId: entity.organizationId
|
|
77
|
+
},
|
|
78
|
+
indexer: usageIndexer,
|
|
79
|
+
events: usageEvents
|
|
80
|
+
});
|
|
81
|
+
return { id: entity.id, deduplicated: false };
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
registerCommand(createUsageCommand);
|
|
85
|
+
export {
|
|
86
|
+
createUsageCommand
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=usage.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/commands/usage.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { BillingAccount, BillingAccountUsage } from '../data/entities'\nimport { billingEntityIds } from '../data/entityIds'\nimport { findBySourceRef } from '../lib/idempotency'\nimport {\n billingUsageCreateSchema,\n type BillingUsageCreateInput,\n} from '../data/validators'\n\n/**\n * Usage ingest \u2014 append-only.\n *\n * Usage records are immutable once persisted. The Bill Run engine\n * (Phase 2) writes `rated_in_bill_run_id` on them but the data itself\n * never changes. There is therefore no update or delete command \u2014 if\n * an external system mis-aggregates and needs to \"correct\" a record,\n * they POST a compensating record (negative quantity is not allowed\n * by the validator; the connector posts a `one_time` Billing Item\n * credit instead, which is the spec's discount/refund model).\n *\n * Create is idempotent via `source_ref` \u2014 same shape as items.\n */\n\nconst usageIndexer = { entityType: billingEntityIds.usage } as const\nconst usageEvents = { module: 'billing', entity: 'usage', persistent: true } as const\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nfunction getDataEngine(ctx: CommandRuntimeContext): DataEngine {\n return ctx.container.resolve('dataEngine') as DataEngine\n}\n\nconst createUsageCommand: CommandHandler<\n BillingUsageCreateInput,\n { id: string; deduplicated: boolean }\n> = {\n id: 'billing.usage.create',\n\n async execute(rawInput, ctx) {\n const parsed = billingUsageCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const account = await em.findOne(BillingAccount, {\n id: parsed.billAccountId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!account) {\n throw new CrudHttpError(404, { error: 'Billing account not found' })\n }\n\n if (parsed.sourceRef) {\n const existing = await findBySourceRef(em, BillingAccountUsage, {\n tenantId: parsed.tenantId,\n billAccountId: parsed.billAccountId,\n sourceRef: parsed.sourceRef,\n })\n if (existing) {\n return { id: existing.id, deduplicated: true }\n }\n }\n\n const now = new Date()\n const entity = em.create(BillingAccountUsage, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n billAccountId: parsed.billAccountId,\n uomCode: parsed.uomCode,\n // numeric(18,4) \u2014 MikroORM stores numeric columns as string in app\n // code. JSON arrives as number; coerce here so the persisted shape\n // matches what later reads expect.\n quantity: parsed.quantity.toFixed(4),\n periodStart: parsed.periodStart,\n periodEnd: parsed.periodEnd,\n lineDescription: parsed.lineDescription ?? null,\n sourceRef: parsed.sourceRef ?? null,\n ratedInBillRunId: null,\n isActive: true,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(entity)\n await em.flush()\n\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'created',\n entity,\n identifiers: {\n id: entity.id,\n tenantId: entity.tenantId,\n organizationId: entity.organizationId,\n },\n indexer: usageIndexer,\n events: usageEvents,\n })\n\n return { id: entity.id, deduplicated: false }\n },\n}\n\nregisterCommand(createUsageCommand)\n\nexport { createUsageCommand }\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,2BAA2B;AACpC,SAAS,qBAAqB;AAE9B,SAAS,gBAAgB,2BAA2B;AACpD,SAAS,wBAAwB;AACjC,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,OAEK;AAgBP,MAAM,eAAe,EAAE,YAAY,iBAAiB,MAAM;AAC1D,MAAM,cAAc,EAAE,QAAQ,WAAW,QAAQ,SAAS,YAAY,KAAK;AAE3E,SAAS,MAAM,KAA2C;AACxD,SAAQ,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC7D;AAEA,SAAS,cAAc,KAAwC;AAC7D,SAAO,IAAI,UAAU,QAAQ,YAAY;AAC3C;AAEA,MAAM,qBAGF;AAAA,EACF,IAAI;AAAA,EAEJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,yBAAyB,MAAM,QAAQ;AACtD,sBAAkB,KAAK,OAAO,QAAQ;AACtC,4BAAwB,KAAK,OAAO,cAAc;AAElD,UAAM,KAAK,MAAM,GAAG;AACpB,UAAM,UAAU,MAAM,GAAG,QAAQ,gBAAgB;AAAA,MAC/C,IAAI,OAAO;AAAA,MACX,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,4BAA4B,CAAC;AAAA,IACrE;AAEA,QAAI,OAAO,WAAW;AACpB,YAAM,WAAW,MAAM,gBAAgB,IAAI,qBAAqB;AAAA,QAC9D,UAAU,OAAO;AAAA,QACjB,eAAe,OAAO;AAAA,QACtB,WAAW,OAAO;AAAA,MACpB,CAAC;AACD,UAAI,UAAU;AACZ,eAAO,EAAE,IAAI,SAAS,IAAI,cAAc,KAAK;AAAA,MAC/C;AAAA,IACF;AAEA,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,SAAS,GAAG,OAAO,qBAAqB;AAAA,MAC5C,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,MACjB,eAAe,OAAO;AAAA,MACtB,SAAS,OAAO;AAAA;AAAA;AAAA;AAAA,MAIhB,UAAU,OAAO,SAAS,QAAQ,CAAC;AAAA,MACnC,aAAa,OAAO;AAAA,MACpB,WAAW,OAAO;AAAA,MAClB,iBAAiB,OAAO,mBAAmB;AAAA,MAC3C,WAAW,OAAO,aAAa;AAAA,MAC/B,kBAAkB;AAAA,MAClB,UAAU;AAAA,MACV,WAAW;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AACD,OAAG,QAAQ,MAAM;AACjB,UAAM,GAAG,MAAM;AAEf,UAAM,oBAAoB;AAAA,MACxB,YAAY,cAAc,GAAG;AAAA,MAC7B,QAAQ;AAAA,MACR;AAAA,MACA,aAAa;AAAA,QACX,IAAI,OAAO;AAAA,QACX,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,MACzB;AAAA,MACA,SAAS;AAAA,MACT,QAAQ;AAAA,IACV,CAAC;AAED,WAAO,EAAE,IAAI,OAAO,IAAI,cAAc,MAAM;AAAA,EAC9C;AACF;AAEA,gBAAgB,kBAAkB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|