@dainamite/cpq 0.2.2 → 0.4.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 +62 -0
- package/MIGRATION.md +34 -1
- package/README.md +17 -12
- package/dist/modules/cpq/api/product-attributes/route.js +28 -53
- package/dist/modules/cpq/api/product-attributes/route.js.map +2 -2
- package/dist/modules/cpq/api/product-charges/route.js +12 -2
- package/dist/modules/cpq/api/product-charges/route.js.map +2 -2
- package/dist/modules/cpq/api/product-offerings/route.js +2 -0
- package/dist/modules/cpq/api/product-offerings/route.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +128 -59
- package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +11 -11
- package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +55 -1
- package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +2 -2
- package/dist/modules/cpq/cli.js +58 -4
- package/dist/modules/cpq/cli.js.map +3 -3
- package/dist/modules/cpq/data/charge-pricing.js +12 -0
- package/dist/modules/cpq/data/charge-pricing.js.map +7 -0
- package/dist/modules/cpq/data/entities.js +3 -0
- package/dist/modules/cpq/data/entities.js.map +2 -2
- package/dist/modules/cpq/data/validators.js +36 -15
- package/dist/modules/cpq/data/validators.js.map +2 -2
- package/dist/modules/cpq/events.js +7 -0
- package/dist/modules/cpq/events.js.map +2 -2
- package/dist/modules/cpq/lib/seeds/demo.js +103 -0
- package/dist/modules/cpq/lib/seeds/demo.js.map +7 -0
- package/dist/modules/cpq/migrations/Migration20260602000000_cpq_charge_model_split.js +28 -0
- package/dist/modules/cpq/migrations/Migration20260602000000_cpq_charge_model_split.js.map +7 -0
- package/dist/modules/cpq/services/cpqOrderService.js +130 -76
- package/dist/modules/cpq/services/cpqOrderService.js.map +2 -2
- package/dist/modules/cpq/services/cpqPricingService.js +53 -29
- package/dist/modules/cpq/services/cpqPricingService.js.map +3 -3
- package/dist/modules/cpq/services/types.js.map +2 -2
- package/package.json +11 -9
- package/src/modules/cpq/__integration__/TC-CPQ-012.spec.ts +34 -4
- package/src/modules/cpq/__tests__/package-isolation.test.ts +8 -3
- package/src/modules/cpq/api/product-attributes/__tests__/serializeAttribute.test.ts +62 -0
- package/src/modules/cpq/api/product-attributes/route.ts +34 -52
- package/src/modules/cpq/api/product-charges/route.ts +23 -2
- package/src/modules/cpq/api/product-offerings/route.ts +2 -0
- package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +151 -62
- package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +24 -22
- package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +73 -1
- package/src/modules/cpq/cli.ts +83 -4
- package/src/modules/cpq/data/__tests__/charge-pricing.test.ts +49 -0
- package/src/modules/cpq/data/__tests__/validators.test.ts +60 -5
- package/src/modules/cpq/data/charge-pricing.ts +48 -0
- package/src/modules/cpq/data/entities.ts +10 -0
- package/src/modules/cpq/data/validators.ts +56 -26
- package/src/modules/cpq/events.ts +11 -3
- package/src/modules/cpq/lib/seeds/__tests__/demo.test.ts +78 -0
- package/src/modules/cpq/lib/seeds/demo.ts +145 -0
- package/src/modules/cpq/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/cpq/migrations/Migration20260602000000_cpq_charge_model_split.ts +57 -0
- package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +172 -1
- package/src/modules/cpq/services/cpqOrderService.ts +214 -103
- package/src/modules/cpq/services/cpqPricingService.ts +82 -34
- package/src/modules/cpq/services/types.ts +4 -1
|
@@ -237,11 +237,14 @@ class DefaultCpqOrderService {
|
|
|
237
237
|
deletedAt: null
|
|
238
238
|
}) : null;
|
|
239
239
|
const quoteType = sourceQuote?.quoteType ?? "new";
|
|
240
|
-
const
|
|
240
|
+
const postCommitEvents = [];
|
|
241
241
|
if (quoteType === "new") {
|
|
242
|
-
await this.createInventoryFromOrder(orderConfig, orderLines, scope);
|
|
242
|
+
const inventory = await this.createInventoryFromOrder(orderConfig, orderLines, scope);
|
|
243
|
+
if (inventory) {
|
|
244
|
+
postCommitEvents.push(() => emitActivatedEvent(orderConfig, inventory, scope));
|
|
245
|
+
}
|
|
243
246
|
} else {
|
|
244
|
-
|
|
247
|
+
postCommitEvents.push(
|
|
245
248
|
...await this.applyArcOrder(orderConfig, sourceQuote, orderLines, scope)
|
|
246
249
|
);
|
|
247
250
|
}
|
|
@@ -250,7 +253,7 @@ class DefaultCpqOrderService {
|
|
|
250
253
|
salesOrder.status = "active";
|
|
251
254
|
salesOrder.placedAt = /* @__PURE__ */ new Date();
|
|
252
255
|
await em.flush();
|
|
253
|
-
for (const fire of
|
|
256
|
+
for (const fire of postCommitEvents) {
|
|
254
257
|
try {
|
|
255
258
|
await fire();
|
|
256
259
|
} catch (err) {
|
|
@@ -323,9 +326,7 @@ class DefaultCpqOrderService {
|
|
|
323
326
|
},
|
|
324
327
|
scope
|
|
325
328
|
);
|
|
326
|
-
events.push(
|
|
327
|
-
() => emitArcEvent("cpq.subscription.amended", result, eventBaseEnvelope)
|
|
328
|
-
);
|
|
329
|
+
events.push(() => emitAmendedEvent(result, eventBaseEnvelope));
|
|
329
330
|
}
|
|
330
331
|
} else if (quoteType === "cancel") {
|
|
331
332
|
for (const target of targets) {
|
|
@@ -342,15 +343,7 @@ class DefaultCpqOrderService {
|
|
|
342
343
|
},
|
|
343
344
|
scope
|
|
344
345
|
);
|
|
345
|
-
events.push(
|
|
346
|
-
() => emitArcEvent("cpq.subscription.cancelled", result, {
|
|
347
|
-
...eventBaseEnvelope,
|
|
348
|
-
etfAmount: sourceQuote.arcEtfAmount ?? null,
|
|
349
|
-
etfCurrency: sourceQuote.arcEtfCurrency ?? null,
|
|
350
|
-
reasonCode: sourceQuote.arcReasonCode ?? null,
|
|
351
|
-
reasonText: sourceQuote.arcReasonText ?? null
|
|
352
|
-
})
|
|
353
|
-
);
|
|
346
|
+
events.push(() => emitCancelledEvent(result, eventBaseEnvelope));
|
|
354
347
|
}
|
|
355
348
|
} else if (quoteType === "renew") {
|
|
356
349
|
const standalone = targets.filter((t) => t.mergeAction === "standalone");
|
|
@@ -379,15 +372,7 @@ class DefaultCpqOrderService {
|
|
|
379
372
|
},
|
|
380
373
|
scope
|
|
381
374
|
);
|
|
382
|
-
events.push(
|
|
383
|
-
() => emitArcEvent("cpq.subscription.renewed", result, {
|
|
384
|
-
...eventBaseEnvelope,
|
|
385
|
-
term: {
|
|
386
|
-
newTermStart: target.newTermStart.toISOString(),
|
|
387
|
-
newTermEnd: target.newTermEnd.toISOString()
|
|
388
|
-
}
|
|
389
|
-
})
|
|
390
|
-
);
|
|
375
|
+
events.push(() => emitRenewedEvent(result, eventBaseEnvelope));
|
|
391
376
|
}
|
|
392
377
|
} else if (absorb.length >= 2 && standalone.length === 0) {
|
|
393
378
|
if (!sourceQuote.arcMergeNewTermStart || !sourceQuote.arcMergeNewTermEnd) {
|
|
@@ -422,7 +407,11 @@ class DefaultCpqOrderService {
|
|
|
422
407
|
);
|
|
423
408
|
for (const sourceLog of merge.sourceChangeLogs) {
|
|
424
409
|
events.push(
|
|
425
|
-
() => emitSupersededEvent(
|
|
410
|
+
() => emitSupersededEvent(
|
|
411
|
+
sourceLog,
|
|
412
|
+
iso10(merge.mergedSubscription.currentTermStart),
|
|
413
|
+
eventBaseEnvelope
|
|
414
|
+
)
|
|
426
415
|
);
|
|
427
416
|
}
|
|
428
417
|
} else {
|
|
@@ -438,7 +427,7 @@ class DefaultCpqOrderService {
|
|
|
438
427
|
}
|
|
439
428
|
// ─── Inventory Creation ─────────────────────────────────────
|
|
440
429
|
async createInventoryFromOrder(orderConfig, orderLines, scope) {
|
|
441
|
-
if (orderLines.length === 0) return;
|
|
430
|
+
if (orderLines.length === 0) return null;
|
|
442
431
|
const specIds = [...new Set(orderLines.map((l) => l.specId).filter(Boolean))];
|
|
443
432
|
const specs = specIds.length > 0 ? await this.em.find(CpqProductSpecification, {
|
|
444
433
|
id: { $in: specIds },
|
|
@@ -458,7 +447,7 @@ class DefaultCpqOrderService {
|
|
|
458
447
|
const hasSubscriptionContent = subscriptionLines.some(
|
|
459
448
|
(l) => Number(l.mrcTotal) > 0 || Number(l.nrcTotal) > 0
|
|
460
449
|
);
|
|
461
|
-
if (!hasSubscriptionContent && assetLines.length === 0) return;
|
|
450
|
+
if (!hasSubscriptionContent && assetLines.length === 0) return null;
|
|
462
451
|
const subscriptionName = await this.buildSubscriptionName(orderConfig, orderLines, scope);
|
|
463
452
|
const lineWithDates = orderLines.find((l) => l.startDate != null) ?? orderLines[0];
|
|
464
453
|
const lineWithTerm = orderLines.find((l) => l.termMonths != null) ?? orderLines[0];
|
|
@@ -537,6 +526,13 @@ class DefaultCpqOrderService {
|
|
|
537
526
|
scope
|
|
538
527
|
);
|
|
539
528
|
}
|
|
529
|
+
const eventItems = subscriptionLines.slice(0, allSubItems.length).map((line, i) => ({
|
|
530
|
+
subscriptionItemId: allSubItems[i].id,
|
|
531
|
+
productName: this.getLineName(line),
|
|
532
|
+
mrcTotal: Number(line.mrcTotal),
|
|
533
|
+
nrcTotal: Number(line.nrcTotal)
|
|
534
|
+
})).filter((it) => it.mrcTotal > 0 || it.nrcTotal > 0);
|
|
535
|
+
return { subscriptionId: subscription.id, items: eventItems };
|
|
540
536
|
}
|
|
541
537
|
// ─── Get / List ─────────────────────────────────────────────
|
|
542
538
|
async getOrder(orderId, scope) {
|
|
@@ -828,72 +824,130 @@ function resolveLineName(line, specMap) {
|
|
|
828
824
|
}
|
|
829
825
|
return "Configured Item";
|
|
830
826
|
}
|
|
831
|
-
async function
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
827
|
+
async function emitActivatedEvent(orderConfig, inventory, scope) {
|
|
828
|
+
const items = inventory.items.map((it) => ({
|
|
829
|
+
subscriptionItemId: it.subscriptionItemId,
|
|
830
|
+
productName: it.productName,
|
|
831
|
+
// `mrcTotal` / `nrcTotal` are line totals — CPQ pricing already
|
|
832
|
+
// applied the line quantity. Quantity is therefore 1 here so
|
|
833
|
+
// `unitPrice × quantity` stays internally consistent no matter
|
|
834
|
+
// how the connector treats the multiplier.
|
|
835
|
+
quantity: 1,
|
|
836
|
+
charges: [
|
|
837
|
+
...it.mrcTotal > 0 ? [{ type: "recurring", unitPrice: it.mrcTotal, description: "Recurring charge" }] : [],
|
|
838
|
+
...it.nrcTotal > 0 ? [{ type: "one_time", amount: it.nrcTotal, description: "One-time charge" }] : []
|
|
839
|
+
]
|
|
840
|
+
}));
|
|
841
|
+
await emitCpqEvent("cpq.subscription.activated", {
|
|
842
|
+
subscriptionId: inventory.subscriptionId,
|
|
843
|
+
customerId: orderConfig.customerId,
|
|
844
|
+
organizationId: scope.organizationId,
|
|
845
|
+
tenantId: scope.tenantId,
|
|
846
|
+
currencyCode: orderConfig.currencyCode,
|
|
847
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
848
|
+
activationDate: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
849
|
+
sourceQuoteId: orderConfig.sourceQuoteId ?? null,
|
|
850
|
+
sourceOrderId: orderConfig.id,
|
|
851
|
+
items
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
function iso10(value) {
|
|
855
|
+
if (!value) return null;
|
|
856
|
+
if (value instanceof Date) return value.toISOString().slice(0, 10);
|
|
857
|
+
return String(value).slice(0, 10);
|
|
858
|
+
}
|
|
859
|
+
function buildArcAddedItems(changeLog) {
|
|
860
|
+
const after = changeLog.afterSnapshot;
|
|
861
|
+
const nameById = new Map(
|
|
862
|
+
(after?.items ?? []).map((i) => [String(i.id), i.name ?? ""])
|
|
863
|
+
);
|
|
864
|
+
const summary = changeLog.lineChanges ?? [];
|
|
865
|
+
const out = [];
|
|
866
|
+
for (const change of summary) {
|
|
867
|
+
if (change.action !== "add" || typeof change.itemId !== "string") continue;
|
|
868
|
+
const mrc = Number(change.mrcAmount);
|
|
869
|
+
const nrc = Number(change.nrcAmount);
|
|
870
|
+
out.push({
|
|
871
|
+
subscriptionItemId: change.itemId,
|
|
872
|
+
productName: nameById.get(change.itemId) || "Configured item",
|
|
873
|
+
quantity: 1,
|
|
874
|
+
charges: [
|
|
875
|
+
...mrc > 0 ? [{ type: "recurring", unitPrice: mrc, description: "Recurring charge" }] : [],
|
|
876
|
+
...nrc > 0 ? [{ type: "one_time", amount: nrc, description: "One-time charge" }] : []
|
|
877
|
+
]
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
return out;
|
|
881
|
+
}
|
|
882
|
+
function buildArcRemovedItemIds(changeLog) {
|
|
883
|
+
const summary = changeLog.lineChanges ?? [];
|
|
884
|
+
return summary.filter((c) => c.action === "cancel" && typeof c.itemId === "string").map((c) => c.itemId);
|
|
885
|
+
}
|
|
886
|
+
async function emitAmendedEvent(result, envelope) {
|
|
887
|
+
await emitCpqEvent("cpq.subscription.amended", {
|
|
888
|
+
tenantId: envelope.tenantId,
|
|
889
|
+
organizationId: envelope.organizationId,
|
|
890
|
+
subscriptionId: result.subscription.id,
|
|
891
|
+
customerId: result.subscription.customerId,
|
|
892
|
+
// No operator-chosen amend date today — the activation moment is
|
|
893
|
+
// the effective date. The billing connector computes the mid-cycle
|
|
894
|
+
// proration from this date + the account's billing cycle.
|
|
895
|
+
effectiveDate: iso10(result.changeLog.effectiveAt),
|
|
896
|
+
addedItems: buildArcAddedItems(result.changeLog),
|
|
897
|
+
removedSubscriptionItemIds: buildArcRemovedItemIds(result.changeLog),
|
|
898
|
+
sourceQuoteId: envelope.sourceQuoteId,
|
|
899
|
+
sourceOrderId: envelope.sourceOrderId,
|
|
900
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
async function emitRenewedEvent(result, envelope) {
|
|
904
|
+
await emitCpqEvent("cpq.subscription.renewed", {
|
|
905
|
+
tenantId: envelope.tenantId,
|
|
836
906
|
organizationId: envelope.organizationId,
|
|
907
|
+
subscriptionId: result.subscription.id,
|
|
908
|
+
customerId: result.subscription.customerId,
|
|
909
|
+
newTermStart: iso10(result.subscription.currentTermStart),
|
|
910
|
+
newTermEnd: iso10(result.subscription.currentTermEnd),
|
|
911
|
+
addedItems: buildArcAddedItems(result.changeLog),
|
|
912
|
+
sourceQuoteId: envelope.sourceQuoteId,
|
|
913
|
+
sourceOrderId: envelope.sourceOrderId,
|
|
914
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
async function emitCancelledEvent(result, envelope) {
|
|
918
|
+
await emitCpqEvent("cpq.subscription.cancelled", {
|
|
837
919
|
tenantId: envelope.tenantId,
|
|
838
|
-
|
|
920
|
+
organizationId: envelope.organizationId,
|
|
921
|
+
subscriptionId: result.subscription.id,
|
|
922
|
+
customerId: result.subscription.customerId,
|
|
923
|
+
effectiveDate: iso10(result.changeLog.effectiveAt),
|
|
839
924
|
sourceQuoteId: envelope.sourceQuoteId,
|
|
840
925
|
sourceOrderId: envelope.sourceOrderId,
|
|
841
|
-
|
|
842
|
-
changeLogId: result.changeLog.id,
|
|
843
|
-
proration: buildProrationPayload(sub, result.changeLog.beforeSnapshot ?? null),
|
|
844
|
-
...envelope.term ? { term: envelope.term } : {},
|
|
845
|
-
...envelope.etfAmount !== void 0 ? { etfAmount: envelope.etfAmount } : {},
|
|
846
|
-
...envelope.etfCurrency !== void 0 ? { etfCurrency: envelope.etfCurrency } : {},
|
|
847
|
-
...envelope.reasonCode !== void 0 ? { reasonCode: envelope.reasonCode } : {},
|
|
848
|
-
...envelope.reasonText !== void 0 ? { reasonText: envelope.reasonText } : {}
|
|
926
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
849
927
|
});
|
|
850
928
|
}
|
|
851
|
-
async function emitMergedEvent(merge, envelope,
|
|
852
|
-
const m = merge.mergedSubscription;
|
|
929
|
+
async function emitMergedEvent(merge, envelope, sourceSubscriptionIds) {
|
|
853
930
|
await emitCpqEvent("cpq.subscription.merged", {
|
|
854
|
-
subscriptionId: m.id,
|
|
855
|
-
customerId: m.customerId,
|
|
856
|
-
organizationId: envelope.organizationId,
|
|
857
931
|
tenantId: envelope.tenantId,
|
|
858
|
-
|
|
932
|
+
organizationId: envelope.organizationId,
|
|
933
|
+
mergedSubscriptionId: merge.mergedSubscription.id,
|
|
934
|
+
sourceSubscriptionIds,
|
|
859
935
|
sourceQuoteId: envelope.sourceQuoteId,
|
|
860
936
|
sourceOrderId: envelope.sourceOrderId,
|
|
861
|
-
|
|
862
|
-
changeLogId: merge.mergeResultChangeLog.id,
|
|
863
|
-
mergedFromSubscriptionIds,
|
|
864
|
-
term: {
|
|
865
|
-
newTermStart: m.currentTermStart?.toISOString() ?? null,
|
|
866
|
-
newTermEnd: m.currentTermEnd?.toISOString() ?? null,
|
|
867
|
-
newTermMonths: m.termMonths ?? null
|
|
868
|
-
},
|
|
869
|
-
proration: buildProrationPayload(m, null)
|
|
937
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
870
938
|
});
|
|
871
939
|
}
|
|
872
|
-
async function emitSupersededEvent(sourceLog,
|
|
940
|
+
async function emitSupersededEvent(sourceLog, effectiveDate, envelope) {
|
|
873
941
|
await emitCpqEvent("cpq.subscription.superseded", {
|
|
874
|
-
subscriptionId: sourceLog.subscriptionId,
|
|
875
|
-
organizationId: envelope.organizationId,
|
|
876
942
|
tenantId: envelope.tenantId,
|
|
877
|
-
|
|
943
|
+
organizationId: envelope.organizationId,
|
|
944
|
+
subscriptionId: sourceLog.subscriptionId,
|
|
945
|
+
effectiveDate,
|
|
878
946
|
sourceQuoteId: envelope.sourceQuoteId,
|
|
879
947
|
sourceOrderId: envelope.sourceOrderId,
|
|
880
|
-
|
|
881
|
-
changeLogId: sourceLog.id,
|
|
882
|
-
mergedIntoSubscriptionId: newMergeSubId
|
|
948
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
883
949
|
});
|
|
884
950
|
}
|
|
885
|
-
function buildProrationPayload(sub, beforeSnapshot) {
|
|
886
|
-
const oldMrc = beforeSnapshot ? Number(beforeSnapshot.mrcAmount ?? 0) : 0;
|
|
887
|
-
return {
|
|
888
|
-
oldMrcAmount: oldMrc,
|
|
889
|
-
newMrcAmount: Number(sub.mrcAmount),
|
|
890
|
-
currency: sub.currencyCode,
|
|
891
|
-
billingCycleStart: null,
|
|
892
|
-
billingCycleEnd: null,
|
|
893
|
-
daysElapsedInCycle: null,
|
|
894
|
-
daysRemainingInCycle: null
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
951
|
export {
|
|
898
952
|
DefaultCpqOrderService,
|
|
899
953
|
OrderError
|