@dainamite/cpq-billing-connector 0.1.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 +34 -0
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/modules/cpq_billing_connector/acl.js +7 -0
- package/dist/modules/cpq_billing_connector/acl.js.map +7 -0
- package/dist/modules/cpq_billing_connector/ce.js +7 -0
- package/dist/modules/cpq_billing_connector/ce.js.map +7 -0
- package/dist/modules/cpq_billing_connector/di.js +6 -0
- package/dist/modules/cpq_billing_connector/di.js.map +7 -0
- package/dist/modules/cpq_billing_connector/index.js +20 -0
- package/dist/modules/cpq_billing_connector/index.js.map +7 -0
- package/dist/modules/cpq_billing_connector/lib/billingApiClient.js +116 -0
- package/dist/modules/cpq_billing_connector/lib/billingApiClient.js.map +7 -0
- package/dist/modules/cpq_billing_connector/lib/chargeMapper.js +49 -0
- package/dist/modules/cpq_billing_connector/lib/chargeMapper.js.map +7 -0
- package/dist/modules/cpq_billing_connector/lib/prorationHelper.js +19 -0
- package/dist/modules/cpq_billing_connector/lib/prorationHelper.js.map +7 -0
- package/dist/modules/cpq_billing_connector/setup.js +7 -0
- package/dist/modules/cpq_billing_connector/setup.js.map +7 -0
- package/dist/modules/cpq_billing_connector/subscribers/cpq-subscription-activated.js +78 -0
- package/dist/modules/cpq_billing_connector/subscribers/cpq-subscription-activated.js.map +7 -0
- package/dist/modules/cpq_billing_connector/subscribers/cpq-subscription-amended.js +112 -0
- package/dist/modules/cpq_billing_connector/subscribers/cpq-subscription-amended.js.map +7 -0
- package/dist/modules/cpq_billing_connector/subscribers/cpq-subscription-cancelled.js +39 -0
- package/dist/modules/cpq_billing_connector/subscribers/cpq-subscription-cancelled.js.map +7 -0
- package/dist/modules/cpq_billing_connector/subscribers/cpq-subscription-merged.js +32 -0
- package/dist/modules/cpq_billing_connector/subscribers/cpq-subscription-merged.js.map +7 -0
- package/dist/modules/cpq_billing_connector/subscribers/cpq-subscription-renewed.js +53 -0
- package/dist/modules/cpq_billing_connector/subscribers/cpq-subscription-renewed.js.map +7 -0
- package/dist/modules/cpq_billing_connector/subscribers/cpq-subscription-superseded.js +39 -0
- package/dist/modules/cpq_billing_connector/subscribers/cpq-subscription-superseded.js.map +7 -0
- package/package.json +119 -0
- package/src/modules/cpq_billing_connector/__tests__/lib/chargeMapper.test.ts +158 -0
- package/src/modules/cpq_billing_connector/__tests__/lib/prorationHelper.test.ts +93 -0
- package/src/modules/cpq_billing_connector/__tests__/package-isolation.test.ts +184 -0
- package/src/modules/cpq_billing_connector/acl.ts +12 -0
- package/src/modules/cpq_billing_connector/ce.ts +6 -0
- package/src/modules/cpq_billing_connector/di.ts +12 -0
- package/src/modules/cpq_billing_connector/index.ts +20 -0
- package/src/modules/cpq_billing_connector/lib/billingApiClient.ts +215 -0
- package/src/modules/cpq_billing_connector/lib/chargeMapper.ts +120 -0
- package/src/modules/cpq_billing_connector/lib/prorationHelper.ts +74 -0
- package/src/modules/cpq_billing_connector/setup.ts +11 -0
- package/src/modules/cpq_billing_connector/subscribers/cpq-subscription-activated.ts +134 -0
- package/src/modules/cpq_billing_connector/subscribers/cpq-subscription-amended.ts +169 -0
- package/src/modules/cpq_billing_connector/subscribers/cpq-subscription-cancelled.ts +64 -0
- package/src/modules/cpq_billing_connector/subscribers/cpq-subscription-merged.ts +56 -0
- package/src/modules/cpq_billing_connector/subscribers/cpq-subscription-renewed.ts +92 -0
- package/src/modules/cpq_billing_connector/subscribers/cpq-subscription-superseded.ts +66 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
|
+
import { cycleContaining } from "@dainamite/billing/modules/billing/lib/billPeriod";
|
|
3
|
+
import { BillingApiClient } from "../lib/billingApiClient.js";
|
|
4
|
+
import {
|
|
5
|
+
mapSubscriptionItemsToBillingItems
|
|
6
|
+
} from "../lib/chargeMapper.js";
|
|
7
|
+
import {
|
|
8
|
+
computeProration,
|
|
9
|
+
formatProrationDescription
|
|
10
|
+
} from "../lib/prorationHelper.js";
|
|
11
|
+
const metadata = {
|
|
12
|
+
event: "cpq.subscription.amended",
|
|
13
|
+
persistent: true,
|
|
14
|
+
id: "cpq-billing-connector:subscription-amended"
|
|
15
|
+
};
|
|
16
|
+
const DAY_MS = 864e5;
|
|
17
|
+
function toMidnightMs(value) {
|
|
18
|
+
const d = value instanceof Date ? new Date(value.getTime()) : /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
19
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
20
|
+
return d.getTime();
|
|
21
|
+
}
|
|
22
|
+
function inclusiveDays(from, to) {
|
|
23
|
+
return Math.round((toMidnightMs(to) - toMidnightMs(from)) / DAY_MS) + 1;
|
|
24
|
+
}
|
|
25
|
+
function isoDateMinusOneDay(iso) {
|
|
26
|
+
const d = /* @__PURE__ */ new Date(`${iso}T00:00:00.000Z`);
|
|
27
|
+
d.setUTCDate(d.getUTCDate() - 1);
|
|
28
|
+
return d.toISOString().slice(0, 10);
|
|
29
|
+
}
|
|
30
|
+
async function handle(payload) {
|
|
31
|
+
if (!payload?.tenantId || !payload.organizationId) return;
|
|
32
|
+
if (!payload.subscriptionId || !payload.effectiveDate) return;
|
|
33
|
+
const added = payload.addedItems ?? [];
|
|
34
|
+
const removed = payload.removedSubscriptionItemIds ?? [];
|
|
35
|
+
if (added.length === 0 && removed.length === 0) return;
|
|
36
|
+
const container = await createRequestContainer();
|
|
37
|
+
const billingApi = new BillingApiClient(container);
|
|
38
|
+
const scope = {
|
|
39
|
+
tenantId: payload.tenantId,
|
|
40
|
+
organizationId: payload.organizationId
|
|
41
|
+
};
|
|
42
|
+
const account = await billingApi.findAccountByCustomer(scope, payload.customerId);
|
|
43
|
+
if (!account) return;
|
|
44
|
+
const itemsToCreate = mapSubscriptionItemsToBillingItems({
|
|
45
|
+
subscriptionId: payload.subscriptionId,
|
|
46
|
+
items: added,
|
|
47
|
+
billStartDate: payload.effectiveDate
|
|
48
|
+
}).map((item) => ({
|
|
49
|
+
billAccountId: account.id,
|
|
50
|
+
type: item.type,
|
|
51
|
+
billStartDate: item.billStartDate,
|
|
52
|
+
description: item.description,
|
|
53
|
+
rateJson: item.rateJson,
|
|
54
|
+
subscriptionId: item.subscriptionId,
|
|
55
|
+
subscriptionItemId: item.subscriptionItemId,
|
|
56
|
+
sourceRef: item.sourceRef
|
|
57
|
+
}));
|
|
58
|
+
if (added.length > 0) {
|
|
59
|
+
const period = cycleContaining(
|
|
60
|
+
account.nextBillDate,
|
|
61
|
+
account.billCycle,
|
|
62
|
+
payload.effectiveDate
|
|
63
|
+
);
|
|
64
|
+
const cycleEnd = period.periodEnd.toISOString().slice(0, 10);
|
|
65
|
+
const daysInPeriod = inclusiveDays(period.periodStart, period.periodEnd);
|
|
66
|
+
const daysRemaining = inclusiveDays(payload.effectiveDate, period.periodEnd);
|
|
67
|
+
for (const addedItem of added) {
|
|
68
|
+
for (const charge of addedItem.charges) {
|
|
69
|
+
if (charge.type !== "recurring" || typeof charge.unitPrice !== "number") {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const { amount } = computeProration({
|
|
73
|
+
unitPrice: charge.unitPrice,
|
|
74
|
+
quantity: addedItem.quantity,
|
|
75
|
+
daysInPeriod,
|
|
76
|
+
daysRemaining
|
|
77
|
+
});
|
|
78
|
+
if (amount === 0) continue;
|
|
79
|
+
itemsToCreate.push({
|
|
80
|
+
billAccountId: account.id,
|
|
81
|
+
type: "one_time",
|
|
82
|
+
billStartDate: payload.effectiveDate,
|
|
83
|
+
description: formatProrationDescription({
|
|
84
|
+
productName: addedItem.productName,
|
|
85
|
+
effectiveDate: payload.effectiveDate,
|
|
86
|
+
cycleEnd
|
|
87
|
+
}),
|
|
88
|
+
rateJson: { amount },
|
|
89
|
+
subscriptionId: payload.subscriptionId,
|
|
90
|
+
subscriptionItemId: addedItem.subscriptionItemId,
|
|
91
|
+
sourceRef: `cpq-${payload.subscriptionId}-${addedItem.subscriptionItemId}-proration-${payload.effectiveDate}`
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
await billingApi.bulkCreateItems(scope, itemsToCreate);
|
|
97
|
+
const endDate = isoDateMinusOneDay(payload.effectiveDate);
|
|
98
|
+
for (const removedSubItemId of removed) {
|
|
99
|
+
const items = await billingApi.findItemsBySubscriptionItem(scope, removedSubItemId);
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
await billingApi.updateItem(scope, {
|
|
102
|
+
id: item.id,
|
|
103
|
+
billEndDate: endDate
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
handle as default,
|
|
110
|
+
metadata
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=cpq-subscription-amended.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/cpq_billing_connector/subscribers/cpq-subscription-amended.ts"],
|
|
4
|
+
"sourcesContent": ["import { createRequestContainer } from '@open-mercato/shared/lib/di/container'\n// TODO(L2-boundary): `cycleContaining` is a runtime function imported from\n// `@dainamite/billing` internals (not a stable public surface). A breaking\n// rename or signature change in billing's `billPeriod` will manifest here\n// as a runtime crash on the first `cpq.subscription.amended` event. Same\n// trade-off as `billingApiClient.ts` \u2014 kept inline for now because the\n// proration math is non-trivial and we don't want to fork it. Promote\n// `cycleContaining` to a documented public export of @dainamite/billing\n// (or expose via a billing REST helper) before the connector hits v1.\nimport { cycleContaining } from '@dainamite/billing/modules/billing/lib/billPeriod'\nimport type { BillCycle } from '@dainamite/billing/modules/billing/data/validators'\nimport { BillingApiClient, type Scope } from '../lib/billingApiClient'\nimport {\n mapSubscriptionItemsToBillingItems,\n type CpqSubscriptionItem,\n} from '../lib/chargeMapper'\nimport {\n computeProration,\n formatProrationDescription,\n} from '../lib/prorationHelper'\n\n/**\n * Subscriber: `cpq.subscription.amended` \u2192 mid-cycle add / remove.\n *\n * CPQ's amended event carries `addedItems`, `removedSubscriptionItemIds`\n * and the `effectiveDate` of the amendment. The connector:\n * 1. Creates a Billing Item for every added charge \u2014 recurring items\n * start on `effectiveDate`; the engine's mid-cycle rule skips the\n * current cycle and bills from the next full cycle.\n * 2. For every added RECURRING charge, posts a `one_time` proration\n * line covering the partial current cycle. The billing cycle is\n * derived HERE (not in CPQ) \u2014 proration is a billing concept, so\n * the connector computes it from the account's `nextBillDate` +\n * `billCycle` via billing's `cycleContaining`.\n * 3. End-dates every Billing Item of each removed subscription item\n * at `effectiveDate - 1 day`.\n */\n\nexport const metadata = {\n event: 'cpq.subscription.amended',\n persistent: true,\n id: 'cpq-billing-connector:subscription-amended',\n}\n\ntype AmendedPayload = {\n tenantId: string\n organizationId: string\n subscriptionId: string\n customerId: string\n /** YYYY-MM-DD; the date the amend takes effect mid-cycle. */\n effectiveDate: string\n addedItems?: CpqSubscriptionItem[]\n removedSubscriptionItemIds?: string[]\n}\n\nconst DAY_MS = 86_400_000\n\nfunction toMidnightMs(value: Date | string): number {\n const d =\n value instanceof Date\n ? new Date(value.getTime())\n : new Date(`${value}T00:00:00.000Z`)\n d.setUTCHours(0, 0, 0, 0)\n return d.getTime()\n}\n\n/** Inclusive day count from `from` through `to`. */\nfunction inclusiveDays(from: Date | string, to: Date | string): number {\n return Math.round((toMidnightMs(to) - toMidnightMs(from)) / DAY_MS) + 1\n}\n\nfunction isoDateMinusOneDay(iso: string): string {\n const d = new Date(`${iso}T00:00:00.000Z`)\n d.setUTCDate(d.getUTCDate() - 1)\n return d.toISOString().slice(0, 10)\n}\n\nexport default async function handle(payload: AmendedPayload): Promise<void> {\n if (!payload?.tenantId || !payload.organizationId) return\n if (!payload.subscriptionId || !payload.effectiveDate) return\n\n const added = payload.addedItems ?? []\n const removed = payload.removedSubscriptionItemIds ?? []\n if (added.length === 0 && removed.length === 0) return\n\n const container = await createRequestContainer()\n const billingApi = new BillingApiClient(container)\n const scope: Scope = {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n }\n\n // Activation runs first and creates the account. If it's missing,\n // there is nothing to amend \u2014 the queue will have retried activation.\n const account = await billingApi.findAccountByCustomer(scope, payload.customerId)\n if (!account) return\n\n // 1. New Billing Items for every added charge.\n const itemsToCreate = mapSubscriptionItemsToBillingItems({\n subscriptionId: payload.subscriptionId,\n items: added,\n billStartDate: payload.effectiveDate,\n }).map((item) => ({\n billAccountId: account.id,\n type: item.type,\n billStartDate: item.billStartDate,\n description: item.description,\n rateJson: item.rateJson,\n subscriptionId: item.subscriptionId,\n subscriptionItemId: item.subscriptionItemId,\n sourceRef: item.sourceRef,\n }))\n\n // 2. Mid-cycle proration. The billing cycle the amend lands in is\n // derived from the account here \u2014 CPQ does not own the billing cycle.\n if (added.length > 0) {\n const period = cycleContaining(\n account.nextBillDate,\n account.billCycle as BillCycle,\n payload.effectiveDate,\n )\n const cycleEnd = period.periodEnd.toISOString().slice(0, 10)\n const daysInPeriod = inclusiveDays(period.periodStart, period.periodEnd)\n const daysRemaining = inclusiveDays(payload.effectiveDate, period.periodEnd)\n\n for (const addedItem of added) {\n for (const charge of addedItem.charges) {\n if (charge.type !== 'recurring' || typeof charge.unitPrice !== 'number') {\n continue\n }\n const { amount } = computeProration({\n unitPrice: charge.unitPrice,\n quantity: addedItem.quantity,\n daysInPeriod,\n daysRemaining,\n })\n if (amount === 0) continue\n itemsToCreate.push({\n billAccountId: account.id,\n type: 'one_time',\n billStartDate: payload.effectiveDate,\n description: formatProrationDescription({\n productName: addedItem.productName,\n effectiveDate: payload.effectiveDate,\n cycleEnd,\n }),\n rateJson: { amount },\n subscriptionId: payload.subscriptionId,\n subscriptionItemId: addedItem.subscriptionItemId,\n sourceRef: `cpq-${payload.subscriptionId}-${addedItem.subscriptionItemId}-proration-${payload.effectiveDate}`,\n })\n }\n }\n }\n\n await billingApi.bulkCreateItems(scope, itemsToCreate)\n\n // 3. End-date every Billing Item of each removed subscription item.\n const endDate = isoDateMinusOneDay(payload.effectiveDate)\n for (const removedSubItemId of removed) {\n const items = await billingApi.findItemsBySubscriptionItem(scope, removedSubItemId)\n for (const item of items) {\n await billingApi.updateItem(scope, {\n id: item.id,\n billEndDate: endDate,\n })\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
|
+
import { BillingApiClient } from "../lib/billingApiClient.js";
|
|
3
|
+
const metadata = {
|
|
4
|
+
event: "cpq.subscription.cancelled",
|
|
5
|
+
persistent: true,
|
|
6
|
+
id: "cpq-billing-connector:subscription-cancelled"
|
|
7
|
+
};
|
|
8
|
+
function isoDateMinusOneDay(iso) {
|
|
9
|
+
const d = /* @__PURE__ */ new Date(`${iso}T00:00:00.000Z`);
|
|
10
|
+
d.setUTCDate(d.getUTCDate() - 1);
|
|
11
|
+
return d.toISOString().slice(0, 10);
|
|
12
|
+
}
|
|
13
|
+
async function handle(payload) {
|
|
14
|
+
if (!payload?.tenantId || !payload.organizationId) return;
|
|
15
|
+
if (!payload.subscriptionId || !payload.effectiveDate) return;
|
|
16
|
+
const container = await createRequestContainer();
|
|
17
|
+
const billingApi = new BillingApiClient(container);
|
|
18
|
+
const scope = {
|
|
19
|
+
tenantId: payload.tenantId,
|
|
20
|
+
organizationId: payload.organizationId
|
|
21
|
+
};
|
|
22
|
+
const items = await billingApi.findItemsBySubscription(scope, payload.subscriptionId);
|
|
23
|
+
if (items.length === 0) return;
|
|
24
|
+
const endDate = isoDateMinusOneDay(payload.effectiveDate);
|
|
25
|
+
for (const item of items) {
|
|
26
|
+
if (item.billEndDate && item.billEndDate.toISOString().slice(0, 10) === endDate) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
await billingApi.updateItem(scope, {
|
|
30
|
+
id: item.id,
|
|
31
|
+
billEndDate: endDate
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
handle as default,
|
|
37
|
+
metadata
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=cpq-subscription-cancelled.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/cpq_billing_connector/subscribers/cpq-subscription-cancelled.ts"],
|
|
4
|
+
"sourcesContent": ["import { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { BillingApiClient, type Scope } from '../lib/billingApiClient'\n\n/**\n * Subscriber: `cpq.subscription.cancelled` \u2192 set bill_end_date on\n * every billing item attached to the subscription.\n *\n * The connector uses the spec's `effectiveDate` (cancellation date)\n * minus one day as `bill_end_date` so the engine's recurring\n * selector stops including the item from the cycle containing the\n * cancellation onward.\n *\n * Final-period proration / refund credits (per spec) are issued by\n * CPQ \u2014 the connector does not invent them here. If a credit is\n * needed, CPQ posts a negative `one_time` Billing Item via this\n * same connector path (e.g. an `amend` event preceding the cancel).\n */\n\nexport const metadata = {\n event: 'cpq.subscription.cancelled',\n persistent: true,\n id: 'cpq-billing-connector:subscription-cancelled',\n}\n\ntype CancelledPayload = {\n tenantId: string\n organizationId: string\n subscriptionId: string\n /** YYYY-MM-DD; the date the cancellation takes effect. */\n effectiveDate: string\n}\n\nfunction isoDateMinusOneDay(iso: string): string {\n const d = new Date(`${iso}T00:00:00.000Z`)\n d.setUTCDate(d.getUTCDate() - 1)\n return d.toISOString().slice(0, 10)\n}\n\nexport default async function handle(payload: CancelledPayload): Promise<void> {\n if (!payload?.tenantId || !payload.organizationId) return\n if (!payload.subscriptionId || !payload.effectiveDate) return\n\n const container = await createRequestContainer()\n const billingApi = new BillingApiClient(container)\n const scope: Scope = {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n }\n\n const items = await billingApi.findItemsBySubscription(scope, payload.subscriptionId)\n if (items.length === 0) return\n\n const endDate = isoDateMinusOneDay(payload.effectiveDate)\n for (const item of items) {\n // Idempotency: skip if already end-dated to this exact date.\n if (item.billEndDate && item.billEndDate.toISOString().slice(0, 10) === endDate) {\n continue\n }\n await billingApi.updateItem(scope, {\n id: item.id,\n billEndDate: endDate,\n })\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,8BAA8B;AACvC,SAAS,wBAAoC;AAiBtC,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAUA,SAAS,mBAAmB,KAAqB;AAC/C,QAAM,IAAI,oBAAI,KAAK,GAAG,GAAG,gBAAgB;AACzC,IAAE,WAAW,EAAE,WAAW,IAAI,CAAC;AAC/B,SAAO,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACpC;AAEA,eAAO,OAA8B,SAA0C;AAC7E,MAAI,CAAC,SAAS,YAAY,CAAC,QAAQ,eAAgB;AACnD,MAAI,CAAC,QAAQ,kBAAkB,CAAC,QAAQ,cAAe;AAEvD,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,aAAa,IAAI,iBAAiB,SAAS;AACjD,QAAM,QAAe;AAAA,IACnB,UAAU,QAAQ;AAAA,IAClB,gBAAgB,QAAQ;AAAA,EAC1B;AAEA,QAAM,QAAQ,MAAM,WAAW,wBAAwB,OAAO,QAAQ,cAAc;AACpF,MAAI,MAAM,WAAW,EAAG;AAExB,QAAM,UAAU,mBAAmB,QAAQ,aAAa;AACxD,aAAW,QAAQ,OAAO;AAExB,QAAI,KAAK,eAAe,KAAK,YAAY,YAAY,EAAE,MAAM,GAAG,EAAE,MAAM,SAAS;AAC/E;AAAA,IACF;AACA,UAAM,WAAW,WAAW,OAAO;AAAA,MACjC,IAAI,KAAK;AAAA,MACT,aAAa;AAAA,IACf,CAAC;AAAA,EACH;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
|
+
import { BillingApiClient } from "../lib/billingApiClient.js";
|
|
3
|
+
const metadata = {
|
|
4
|
+
event: "cpq.subscription.merged",
|
|
5
|
+
persistent: true,
|
|
6
|
+
id: "cpq-billing-connector:subscription-merged"
|
|
7
|
+
};
|
|
8
|
+
async function handle(payload) {
|
|
9
|
+
if (!payload?.tenantId || !payload.organizationId) return;
|
|
10
|
+
if (!payload.mergedSubscriptionId || !Array.isArray(payload.sourceSubscriptionIds)) return;
|
|
11
|
+
if (payload.sourceSubscriptionIds.length === 0) return;
|
|
12
|
+
const container = await createRequestContainer();
|
|
13
|
+
const billingApi = new BillingApiClient(container);
|
|
14
|
+
const scope = {
|
|
15
|
+
tenantId: payload.tenantId,
|
|
16
|
+
organizationId: payload.organizationId
|
|
17
|
+
};
|
|
18
|
+
for (const sourceId of payload.sourceSubscriptionIds) {
|
|
19
|
+
const items = await billingApi.findItemsBySubscription(scope, sourceId);
|
|
20
|
+
for (const item of items) {
|
|
21
|
+
await billingApi.updateItem(scope, {
|
|
22
|
+
id: item.id,
|
|
23
|
+
subscriptionId: payload.mergedSubscriptionId
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
handle as default,
|
|
30
|
+
metadata
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=cpq-subscription-merged.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/cpq_billing_connector/subscribers/cpq-subscription-merged.ts"],
|
|
4
|
+
"sourcesContent": ["import { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { BillingApiClient, type Scope } from '../lib/billingApiClient'\n\n/**\n * Subscriber: `cpq.subscription.merged` \u2192 re-link items to the\n * merged subscription.\n *\n * Per the spec: \"Move Billing Items from source subscriptions to\n * the merged subscription (update `subscription_id`).\" This keeps\n * the audit trail continuous \u2014 same Billing Items, just pointing\n * at the new subscription so any future lookup by subscription_id\n * lands at the merged target.\n *\n * The source subscriptions transition to the terminal `superseded`\n * status in CPQ; the `superseded` subscriber separately end-dates\n * any items still pointing at the source ids (defense in depth \u2014 if\n * a race left an item un-relinked, superseded catches it).\n */\n\nexport const metadata = {\n event: 'cpq.subscription.merged',\n persistent: true,\n id: 'cpq-billing-connector:subscription-merged',\n}\n\ntype MergedPayload = {\n tenantId: string\n organizationId: string\n /** The new merged subscription's id (the survivor). */\n mergedSubscriptionId: string\n /** The source subscriptions absorbed into the merge. */\n sourceSubscriptionIds: string[]\n}\n\nexport default async function handle(payload: MergedPayload): Promise<void> {\n if (!payload?.tenantId || !payload.organizationId) return\n if (!payload.mergedSubscriptionId || !Array.isArray(payload.sourceSubscriptionIds)) return\n if (payload.sourceSubscriptionIds.length === 0) return\n\n const container = await createRequestContainer()\n const billingApi = new BillingApiClient(container)\n const scope: Scope = {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n }\n\n for (const sourceId of payload.sourceSubscriptionIds) {\n const items = await billingApi.findItemsBySubscription(scope, sourceId)\n for (const item of items) {\n await billingApi.updateItem(scope, {\n id: item.id,\n subscriptionId: payload.mergedSubscriptionId,\n })\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,8BAA8B;AACvC,SAAS,wBAAoC;AAkBtC,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAWA,eAAO,OAA8B,SAAuC;AAC1E,MAAI,CAAC,SAAS,YAAY,CAAC,QAAQ,eAAgB;AACnD,MAAI,CAAC,QAAQ,wBAAwB,CAAC,MAAM,QAAQ,QAAQ,qBAAqB,EAAG;AACpF,MAAI,QAAQ,sBAAsB,WAAW,EAAG;AAEhD,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,aAAa,IAAI,iBAAiB,SAAS;AACjD,QAAM,QAAe;AAAA,IACnB,UAAU,QAAQ;AAAA,IAClB,gBAAgB,QAAQ;AAAA,EAC1B;AAEA,aAAW,YAAY,QAAQ,uBAAuB;AACpD,UAAM,QAAQ,MAAM,WAAW,wBAAwB,OAAO,QAAQ;AACtE,eAAW,QAAQ,OAAO;AACxB,YAAM,WAAW,WAAW,OAAO;AAAA,QACjC,IAAI,KAAK;AAAA,QACT,gBAAgB,QAAQ;AAAA,MAC1B,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
|
+
import { BillingApiClient } from "../lib/billingApiClient.js";
|
|
3
|
+
import {
|
|
4
|
+
mapSubscriptionItemsToBillingItems
|
|
5
|
+
} from "../lib/chargeMapper.js";
|
|
6
|
+
const metadata = {
|
|
7
|
+
event: "cpq.subscription.renewed",
|
|
8
|
+
persistent: true,
|
|
9
|
+
id: "cpq-billing-connector:subscription-renewed"
|
|
10
|
+
};
|
|
11
|
+
async function handle(payload) {
|
|
12
|
+
if (!payload?.tenantId || !payload.organizationId) return;
|
|
13
|
+
if (!payload.subscriptionId || !payload.newTermStart) return;
|
|
14
|
+
const container = await createRequestContainer();
|
|
15
|
+
const billingApi = new BillingApiClient(container);
|
|
16
|
+
const scope = {
|
|
17
|
+
tenantId: payload.tenantId,
|
|
18
|
+
organizationId: payload.organizationId
|
|
19
|
+
};
|
|
20
|
+
const existingItems = await billingApi.findItemsBySubscription(scope, payload.subscriptionId);
|
|
21
|
+
for (const item of existingItems) {
|
|
22
|
+
if (!item.billEndDate) continue;
|
|
23
|
+
await billingApi.updateItem(scope, {
|
|
24
|
+
id: item.id,
|
|
25
|
+
billEndDate: payload.newTermEnd ?? null
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const accountId = existingItems[0]?.billAccountId ?? (await billingApi.findAccountByCustomer(scope, payload.customerId))?.id ?? null;
|
|
29
|
+
if (!accountId) return;
|
|
30
|
+
const newPayloads = mapSubscriptionItemsToBillingItems({
|
|
31
|
+
subscriptionId: payload.subscriptionId,
|
|
32
|
+
items: payload.addedItems ?? [],
|
|
33
|
+
billStartDate: payload.newTermStart
|
|
34
|
+
});
|
|
35
|
+
await billingApi.bulkCreateItems(
|
|
36
|
+
scope,
|
|
37
|
+
newPayloads.map((item) => ({
|
|
38
|
+
billAccountId: accountId,
|
|
39
|
+
type: item.type,
|
|
40
|
+
billStartDate: item.billStartDate,
|
|
41
|
+
description: item.description,
|
|
42
|
+
rateJson: item.rateJson,
|
|
43
|
+
subscriptionId: item.subscriptionId,
|
|
44
|
+
subscriptionItemId: item.subscriptionItemId,
|
|
45
|
+
sourceRef: item.sourceRef
|
|
46
|
+
}))
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
handle as default,
|
|
51
|
+
metadata
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=cpq-subscription-renewed.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/cpq_billing_connector/subscribers/cpq-subscription-renewed.ts"],
|
|
4
|
+
"sourcesContent": ["import { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { BillingApiClient, type Scope } from '../lib/billingApiClient'\nimport {\n mapSubscriptionItemsToBillingItems,\n type CpqSubscriptionItem,\n} from '../lib/chargeMapper'\n\n/**\n * Subscriber: `cpq.subscription.renewed` \u2192 extend term + add items.\n *\n * On renewal the existing items either:\n * - Stay active (no `bill_end_date` set, so the engine keeps\n * billing them as recurring) \u2014 connector touches nothing.\n * - Had a previously-set `bill_end_date` (e.g. fixed-term\n * subscription) \u2014 connector clears or extends it to the new\n * term's end (`newTermEnd` from the payload).\n *\n * New items added at renewal (e.g. upsell at renewal time) are\n * created with `bill_start_date = newTermStart`, same shape as\n * activation.\n *\n * The connector does NOT post a proration line for renewals \u2014 by\n * definition renewal happens at the cycle boundary (or it's an early\n * renewal, which the integrator should issue as `amend` + `renew`\n * via the spec's ARC flow rather than this single event).\n */\n\nexport const metadata = {\n event: 'cpq.subscription.renewed',\n persistent: true,\n id: 'cpq-billing-connector:subscription-renewed',\n}\n\ntype RenewedPayload = {\n tenantId: string\n organizationId: string\n subscriptionId: string\n customerId: string\n newTermStart: string\n newTermEnd?: string | null\n addedItems?: CpqSubscriptionItem[]\n}\n\nexport default async function handle(payload: RenewedPayload): Promise<void> {\n if (!payload?.tenantId || !payload.organizationId) return\n if (!payload.subscriptionId || !payload.newTermStart) return\n\n const container = await createRequestContainer()\n const billingApi = new BillingApiClient(container)\n const scope: Scope = {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n }\n\n const existingItems = await billingApi.findItemsBySubscription(scope, payload.subscriptionId)\n\n // Extend the bill_end_date on items that had one. Items without\n // an end date are open-ended; nothing to do.\n for (const item of existingItems) {\n if (!item.billEndDate) continue\n await billingApi.updateItem(scope, {\n id: item.id,\n billEndDate: payload.newTermEnd ?? null,\n })\n }\n\n // Add new items at the renewal boundary.\n const accountId =\n existingItems[0]?.billAccountId ??\n (await billingApi.findAccountByCustomer(scope, payload.customerId))?.id ??\n null\n if (!accountId) return\n\n const newPayloads = mapSubscriptionItemsToBillingItems({\n subscriptionId: payload.subscriptionId,\n items: payload.addedItems ?? [],\n billStartDate: payload.newTermStart,\n })\n await billingApi.bulkCreateItems(\n scope,\n newPayloads.map((item) => ({\n billAccountId: accountId,\n type: item.type,\n billStartDate: item.billStartDate,\n description: item.description,\n rateJson: item.rateJson,\n subscriptionId: item.subscriptionId,\n subscriptionItemId: item.subscriptionItemId,\n sourceRef: item.sourceRef,\n })),\n )\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,8BAA8B;AACvC,SAAS,wBAAoC;AAC7C;AAAA,EACE;AAAA,OAEK;AAsBA,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAYA,eAAO,OAA8B,SAAwC;AAC3E,MAAI,CAAC,SAAS,YAAY,CAAC,QAAQ,eAAgB;AACnD,MAAI,CAAC,QAAQ,kBAAkB,CAAC,QAAQ,aAAc;AAEtD,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,aAAa,IAAI,iBAAiB,SAAS;AACjD,QAAM,QAAe;AAAA,IACnB,UAAU,QAAQ;AAAA,IAClB,gBAAgB,QAAQ;AAAA,EAC1B;AAEA,QAAM,gBAAgB,MAAM,WAAW,wBAAwB,OAAO,QAAQ,cAAc;AAI5F,aAAW,QAAQ,eAAe;AAChC,QAAI,CAAC,KAAK,YAAa;AACvB,UAAM,WAAW,WAAW,OAAO;AAAA,MACjC,IAAI,KAAK;AAAA,MACT,aAAa,QAAQ,cAAc;AAAA,IACrC,CAAC;AAAA,EACH;AAGA,QAAM,YACJ,cAAc,CAAC,GAAG,kBACjB,MAAM,WAAW,sBAAsB,OAAO,QAAQ,UAAU,IAAI,MACrE;AACF,MAAI,CAAC,UAAW;AAEhB,QAAM,cAAc,mCAAmC;AAAA,IACrD,gBAAgB,QAAQ;AAAA,IACxB,OAAO,QAAQ,cAAc,CAAC;AAAA,IAC9B,eAAe,QAAQ;AAAA,EACzB,CAAC;AACD,QAAM,WAAW;AAAA,IACf;AAAA,IACA,YAAY,IAAI,CAAC,UAAU;AAAA,MACzB,eAAe;AAAA,MACf,MAAM,KAAK;AAAA,MACX,eAAe,KAAK;AAAA,MACpB,aAAa,KAAK;AAAA,MAClB,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,MACrB,oBAAoB,KAAK;AAAA,MACzB,WAAW,KAAK;AAAA,IAClB,EAAE;AAAA,EACJ;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
|
+
import { BillingApiClient } from "../lib/billingApiClient.js";
|
|
3
|
+
const metadata = {
|
|
4
|
+
event: "cpq.subscription.superseded",
|
|
5
|
+
persistent: true,
|
|
6
|
+
id: "cpq-billing-connector:subscription-superseded"
|
|
7
|
+
};
|
|
8
|
+
function isoDateMinusOneDay(iso) {
|
|
9
|
+
const d = /* @__PURE__ */ new Date(`${iso}T00:00:00.000Z`);
|
|
10
|
+
d.setUTCDate(d.getUTCDate() - 1);
|
|
11
|
+
return d.toISOString().slice(0, 10);
|
|
12
|
+
}
|
|
13
|
+
async function handle(payload) {
|
|
14
|
+
if (!payload?.tenantId || !payload.organizationId) return;
|
|
15
|
+
if (!payload.subscriptionId || !payload.effectiveDate) return;
|
|
16
|
+
const container = await createRequestContainer();
|
|
17
|
+
const billingApi = new BillingApiClient(container);
|
|
18
|
+
const scope = {
|
|
19
|
+
tenantId: payload.tenantId,
|
|
20
|
+
organizationId: payload.organizationId
|
|
21
|
+
};
|
|
22
|
+
const stragglers = await billingApi.findItemsBySubscription(scope, payload.subscriptionId);
|
|
23
|
+
if (stragglers.length === 0) return;
|
|
24
|
+
const endDate = isoDateMinusOneDay(payload.effectiveDate);
|
|
25
|
+
for (const item of stragglers) {
|
|
26
|
+
if (item.billEndDate && item.billEndDate.toISOString().slice(0, 10) === endDate) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
await billingApi.updateItem(scope, {
|
|
30
|
+
id: item.id,
|
|
31
|
+
billEndDate: endDate
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
handle as default,
|
|
37
|
+
metadata
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=cpq-subscription-superseded.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/cpq_billing_connector/subscribers/cpq-subscription-superseded.ts"],
|
|
4
|
+
"sourcesContent": ["import { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { BillingApiClient, type Scope } from '../lib/billingApiClient'\n\n/**\n * Subscriber: `cpq.subscription.superseded` \u2192 end-date items still\n * pointing at a superseded source.\n *\n * Per the spec's XD-250 ARC notes, merge-renewal produces a brand\n * new subscription (the \"merge target\") and transitions the source\n * subscriptions to `superseded`. The `merged` subscriber already\n * re-links Billing Items to the target \u2014 this subscriber is the\n * defense-in-depth net: if any items remain attached to the\n * superseded source (re-link missed them, race, manual fix-up),\n * end-date them at the supersession effective date so the engine\n * stops billing them.\n *\n * In the steady-state \"no race\" path, this subscriber finds zero\n * items and is a no-op \u2014 which is fine.\n */\n\nexport const metadata = {\n event: 'cpq.subscription.superseded',\n persistent: true,\n id: 'cpq-billing-connector:subscription-superseded',\n}\n\ntype SupersededPayload = {\n tenantId: string\n organizationId: string\n /** The id that is being superseded (the OLD subscription). */\n subscriptionId: string\n /** YYYY-MM-DD; when the supersession took effect. */\n effectiveDate: string\n}\n\nfunction isoDateMinusOneDay(iso: string): string {\n const d = new Date(`${iso}T00:00:00.000Z`)\n d.setUTCDate(d.getUTCDate() - 1)\n return d.toISOString().slice(0, 10)\n}\n\nexport default async function handle(payload: SupersededPayload): Promise<void> {\n if (!payload?.tenantId || !payload.organizationId) return\n if (!payload.subscriptionId || !payload.effectiveDate) return\n\n const container = await createRequestContainer()\n const billingApi = new BillingApiClient(container)\n const scope: Scope = {\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n }\n\n const stragglers = await billingApi.findItemsBySubscription(scope, payload.subscriptionId)\n if (stragglers.length === 0) return\n\n const endDate = isoDateMinusOneDay(payload.effectiveDate)\n for (const item of stragglers) {\n if (item.billEndDate && item.billEndDate.toISOString().slice(0, 10) === endDate) {\n continue\n }\n await billingApi.updateItem(scope, {\n id: item.id,\n billEndDate: endDate,\n })\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,8BAA8B;AACvC,SAAS,wBAAoC;AAmBtC,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAWA,SAAS,mBAAmB,KAAqB;AAC/C,QAAM,IAAI,oBAAI,KAAK,GAAG,GAAG,gBAAgB;AACzC,IAAE,WAAW,EAAE,WAAW,IAAI,CAAC;AAC/B,SAAO,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACpC;AAEA,eAAO,OAA8B,SAA2C;AAC9E,MAAI,CAAC,SAAS,YAAY,CAAC,QAAQ,eAAgB;AACnD,MAAI,CAAC,QAAQ,kBAAkB,CAAC,QAAQ,cAAe;AAEvD,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,aAAa,IAAI,iBAAiB,SAAS;AACjD,QAAM,QAAe;AAAA,IACnB,UAAU,QAAQ;AAAA,IAClB,gBAAgB,QAAQ;AAAA,EAC1B;AAEA,QAAM,aAAa,MAAM,WAAW,wBAAwB,OAAO,QAAQ,cAAc;AACzF,MAAI,WAAW,WAAW,EAAG;AAE7B,QAAM,UAAU,mBAAmB,QAAQ,aAAa;AACxD,aAAW,QAAQ,YAAY;AAC7B,QAAI,KAAK,eAAe,KAAK,YAAY,YAAY,EAAE,MAAM,GAAG,EAAE,MAAM,SAAS;AAC/E;AAAA,IACF;AACA,UAAM,WAAW,WAAW,OAAO;AAAA,MACjC,IAAI,KAAK;AAAA,MACT,aAAa;AAAA,IACf,CAAC;AAAA,EACH;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dainamite/cpq-billing-connector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Wires @dainamite/cpq subscription lifecycle events to @dainamite/billing API calls (activated / amended / renewed / cancelled / merged / superseded).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/modules/cpq_billing_connector/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"src",
|
|
10
|
+
"README.md",
|
|
11
|
+
"CHANGELOG.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "node build.mjs",
|
|
15
|
+
"watch": "node watch.mjs",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"chokidar": "^4.0.3"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./src/modules/cpq_billing_connector/index.ts",
|
|
24
|
+
"default": "./dist/modules/cpq_billing_connector/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./modules/cpq_billing_connector": {
|
|
27
|
+
"types": "./src/modules/cpq_billing_connector/index.ts",
|
|
28
|
+
"default": "./dist/modules/cpq_billing_connector/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./*.ts": {
|
|
31
|
+
"types": "./src/*.ts",
|
|
32
|
+
"default": "./dist/*.js"
|
|
33
|
+
},
|
|
34
|
+
"./*.tsx": {
|
|
35
|
+
"types": "./src/*.tsx",
|
|
36
|
+
"default": "./dist/*.js"
|
|
37
|
+
},
|
|
38
|
+
"./*.json": "./src/*.json",
|
|
39
|
+
"./*": {
|
|
40
|
+
"types": [
|
|
41
|
+
"./src/*.ts",
|
|
42
|
+
"./src/*.tsx"
|
|
43
|
+
],
|
|
44
|
+
"default": "./dist/*.js"
|
|
45
|
+
},
|
|
46
|
+
"./*/*.json": "./src/*/*.json",
|
|
47
|
+
"./*/*": {
|
|
48
|
+
"types": [
|
|
49
|
+
"./src/*/*.ts",
|
|
50
|
+
"./src/*/*.tsx"
|
|
51
|
+
],
|
|
52
|
+
"default": "./dist/*/*.js"
|
|
53
|
+
},
|
|
54
|
+
"./*/*/*.json": "./src/*/*/*.json",
|
|
55
|
+
"./*/*/*": {
|
|
56
|
+
"types": [
|
|
57
|
+
"./src/*/*/*.ts",
|
|
58
|
+
"./src/*/*/*.tsx"
|
|
59
|
+
],
|
|
60
|
+
"default": "./dist/*/*/*.js"
|
|
61
|
+
},
|
|
62
|
+
"./*/*/*/*.json": "./src/*/*/*/*.json",
|
|
63
|
+
"./*/*/*/*": {
|
|
64
|
+
"types": [
|
|
65
|
+
"./src/*/*/*/*.ts",
|
|
66
|
+
"./src/*/*/*/*.tsx"
|
|
67
|
+
],
|
|
68
|
+
"default": "./dist/*/*/*/*.js"
|
|
69
|
+
},
|
|
70
|
+
"./*/*/*/*/*.json": "./src/*/*/*/*/*.json",
|
|
71
|
+
"./*/*/*/*/*": {
|
|
72
|
+
"types": [
|
|
73
|
+
"./src/*/*/*/*/*.ts",
|
|
74
|
+
"./src/*/*/*/*/*.tsx"
|
|
75
|
+
],
|
|
76
|
+
"default": "./dist/*/*/*/*/*.js"
|
|
77
|
+
},
|
|
78
|
+
"./*/*/*/*/*/*.json": "./src/*/*/*/*/*/*.json",
|
|
79
|
+
"./*/*/*/*/*/*": {
|
|
80
|
+
"types": [
|
|
81
|
+
"./src/*/*/*/*/*/*.ts",
|
|
82
|
+
"./src/*/*/*/*/*/*.tsx"
|
|
83
|
+
],
|
|
84
|
+
"default": "./dist/*/*/*/*/*/*.js"
|
|
85
|
+
},
|
|
86
|
+
"./*/*/*/*/*/*/*.json": "./src/*/*/*/*/*/*/*.json",
|
|
87
|
+
"./*/*/*/*/*/*/*": {
|
|
88
|
+
"types": [
|
|
89
|
+
"./src/*/*/*/*/*/*/*.ts",
|
|
90
|
+
"./src/*/*/*/*/*/*/*.tsx"
|
|
91
|
+
],
|
|
92
|
+
"default": "./dist/*/*/*/*/*/*/*.js"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"peerDependencies": {
|
|
96
|
+
"@dainamite/billing": "^0.17.0",
|
|
97
|
+
"@dainamite/cpq": "^0.3.0",
|
|
98
|
+
"@mikro-orm/core": "^7.0.0",
|
|
99
|
+
"@mikro-orm/decorators": "^7.0.0",
|
|
100
|
+
"@mikro-orm/postgresql": "^7.0.0",
|
|
101
|
+
"@open-mercato/core": "^0.6.0",
|
|
102
|
+
"@open-mercato/events": "^0.6.0",
|
|
103
|
+
"@open-mercato/shared": "^0.6.0",
|
|
104
|
+
"awilix": "^12.0.0",
|
|
105
|
+
"zod": "^4.0.0"
|
|
106
|
+
},
|
|
107
|
+
"publishConfig": {
|
|
108
|
+
"access": "public"
|
|
109
|
+
},
|
|
110
|
+
"repository": {
|
|
111
|
+
"type": "git",
|
|
112
|
+
"url": "https://github.com/dainamite-technologies/dainamite-core.git",
|
|
113
|
+
"directory": "packages/cpq-billing-connector"
|
|
114
|
+
},
|
|
115
|
+
"license": "MIT",
|
|
116
|
+
"engines": {
|
|
117
|
+
"node": ">=24.0.0"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSourceRef,
|
|
3
|
+
mapChargeToBillingItem,
|
|
4
|
+
mapSubscriptionItemsToBillingItems,
|
|
5
|
+
type CpqCharge,
|
|
6
|
+
type CpqSubscriptionItem,
|
|
7
|
+
} from '../../lib/chargeMapper'
|
|
8
|
+
|
|
9
|
+
const SUB_ID = 'sub-1'
|
|
10
|
+
const SUB_ITEM_ID = 'sub-item-1'
|
|
11
|
+
const BILL_START = '2026-05-01'
|
|
12
|
+
|
|
13
|
+
function subItem(charges: CpqCharge[], overrides: Partial<CpqSubscriptionItem> = {}): CpqSubscriptionItem {
|
|
14
|
+
return {
|
|
15
|
+
subscriptionItemId: overrides.subscriptionItemId ?? SUB_ITEM_ID,
|
|
16
|
+
productName: overrides.productName ?? 'Plan Pro',
|
|
17
|
+
quantity: overrides.quantity ?? 1,
|
|
18
|
+
charges,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('buildSourceRef', () => {
|
|
23
|
+
it('produces a deterministic key including all 3 components', () => {
|
|
24
|
+
expect(buildSourceRef('s1', 'i1', 'recurring')).toBe('cpq-s1-i1-recurring')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('different charge types yield different keys for the same item', () => {
|
|
28
|
+
expect(buildSourceRef('s1', 'i1', 'recurring')).not.toEqual(
|
|
29
|
+
buildSourceRef('s1', 'i1', 'one_time'),
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('mapChargeToBillingItem — one_time', () => {
|
|
35
|
+
it('maps a well-formed one_time charge', () => {
|
|
36
|
+
const charge: CpqCharge = { type: 'one_time', amount: 99, description: 'Activation fee' }
|
|
37
|
+
const result = mapChargeToBillingItem({
|
|
38
|
+
subscriptionId: SUB_ID,
|
|
39
|
+
subscriptionItem: subItem([charge]),
|
|
40
|
+
charge,
|
|
41
|
+
billStartDate: BILL_START,
|
|
42
|
+
})
|
|
43
|
+
expect(result).toMatchObject({
|
|
44
|
+
type: 'one_time',
|
|
45
|
+
description: 'Plan Pro — Activation fee',
|
|
46
|
+
rateJson: { amount: 99 },
|
|
47
|
+
subscriptionId: SUB_ID,
|
|
48
|
+
subscriptionItemId: SUB_ITEM_ID,
|
|
49
|
+
sourceRef: `cpq-${SUB_ID}-${SUB_ITEM_ID}-one_time`,
|
|
50
|
+
billStartDate: BILL_START,
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('returns null when one_time is missing amount', () => {
|
|
55
|
+
const charge = { type: 'one_time' as const, description: 'No amount' }
|
|
56
|
+
const result = mapChargeToBillingItem({
|
|
57
|
+
subscriptionId: SUB_ID,
|
|
58
|
+
subscriptionItem: subItem([charge]),
|
|
59
|
+
charge,
|
|
60
|
+
billStartDate: BILL_START,
|
|
61
|
+
})
|
|
62
|
+
expect(result).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('mapChargeToBillingItem — recurring', () => {
|
|
67
|
+
it('maps a well-formed recurring charge to unit_price rate_json', () => {
|
|
68
|
+
const charge: CpqCharge = { type: 'recurring', unitPrice: 49.99, description: 'MRC' }
|
|
69
|
+
const result = mapChargeToBillingItem({
|
|
70
|
+
subscriptionId: SUB_ID,
|
|
71
|
+
subscriptionItem: subItem([charge]),
|
|
72
|
+
charge,
|
|
73
|
+
billStartDate: BILL_START,
|
|
74
|
+
})
|
|
75
|
+
expect(result).toMatchObject({
|
|
76
|
+
type: 'recurring',
|
|
77
|
+
description: 'Plan Pro — MRC',
|
|
78
|
+
rateJson: { unit_price: 49.99 },
|
|
79
|
+
sourceRef: `cpq-${SUB_ID}-${SUB_ITEM_ID}-recurring`,
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('returns null when recurring is missing unitPrice', () => {
|
|
84
|
+
const charge = { type: 'recurring' as const, description: 'No price' }
|
|
85
|
+
const result = mapChargeToBillingItem({
|
|
86
|
+
subscriptionId: SUB_ID,
|
|
87
|
+
subscriptionItem: subItem([charge]),
|
|
88
|
+
charge,
|
|
89
|
+
billStartDate: BILL_START,
|
|
90
|
+
})
|
|
91
|
+
expect(result).toBeNull()
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('mapSubscriptionItemsToBillingItems', () => {
|
|
96
|
+
it('one item with both one_time + recurring → TWO billing items (spec rule)', () => {
|
|
97
|
+
const item = subItem([
|
|
98
|
+
{ type: 'one_time', amount: 99, description: 'Activation' },
|
|
99
|
+
{ type: 'recurring', unitPrice: 49.99, description: 'MRC' },
|
|
100
|
+
])
|
|
101
|
+
const result = mapSubscriptionItemsToBillingItems({
|
|
102
|
+
subscriptionId: SUB_ID,
|
|
103
|
+
items: [item],
|
|
104
|
+
billStartDate: BILL_START,
|
|
105
|
+
})
|
|
106
|
+
expect(result).toHaveLength(2)
|
|
107
|
+
expect(result.map((r) => r.type)).toEqual(['one_time', 'recurring'])
|
|
108
|
+
// Both items share the same subscriptionItemId but differ in sourceRef
|
|
109
|
+
// because the type is embedded in the key.
|
|
110
|
+
expect(result[0].sourceRef).not.toEqual(result[1].sourceRef)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('skips malformed charges (no amount on one_time, no unitPrice on recurring)', () => {
|
|
114
|
+
const item = subItem([
|
|
115
|
+
{ type: 'one_time', amount: 99, description: 'OK' },
|
|
116
|
+
{ type: 'recurring', description: 'No price' }, // skipped
|
|
117
|
+
{ type: 'one_time', description: 'No amount' }, // skipped
|
|
118
|
+
])
|
|
119
|
+
const result = mapSubscriptionItemsToBillingItems({
|
|
120
|
+
subscriptionId: SUB_ID,
|
|
121
|
+
items: [item],
|
|
122
|
+
billStartDate: BILL_START,
|
|
123
|
+
})
|
|
124
|
+
expect(result).toHaveLength(1)
|
|
125
|
+
expect(result[0].rateJson).toEqual({ amount: 99 })
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('walks multiple subscription items independently', () => {
|
|
129
|
+
const a = subItem(
|
|
130
|
+
[{ type: 'recurring', unitPrice: 10, description: 'Plan A MRC' }],
|
|
131
|
+
{ subscriptionItemId: 'i-a', productName: 'Plan A' },
|
|
132
|
+
)
|
|
133
|
+
const b = subItem(
|
|
134
|
+
[{ type: 'recurring', unitPrice: 20, description: 'Plan B MRC' }],
|
|
135
|
+
{ subscriptionItemId: 'i-b', productName: 'Plan B' },
|
|
136
|
+
)
|
|
137
|
+
const result = mapSubscriptionItemsToBillingItems({
|
|
138
|
+
subscriptionId: SUB_ID,
|
|
139
|
+
items: [a, b],
|
|
140
|
+
billStartDate: BILL_START,
|
|
141
|
+
})
|
|
142
|
+
expect(result.map((r) => r.subscriptionItemId)).toEqual(['i-a', 'i-b'])
|
|
143
|
+
expect(result.map((r) => r.description)).toEqual([
|
|
144
|
+
'Plan A — Plan A MRC',
|
|
145
|
+
'Plan B — Plan B MRC',
|
|
146
|
+
])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('empty items array → empty result', () => {
|
|
150
|
+
expect(
|
|
151
|
+
mapSubscriptionItemsToBillingItems({
|
|
152
|
+
subscriptionId: SUB_ID,
|
|
153
|
+
items: [],
|
|
154
|
+
billStartDate: BILL_START,
|
|
155
|
+
}),
|
|
156
|
+
).toEqual([])
|
|
157
|
+
})
|
|
158
|
+
})
|