@dainamite/cpq 0.2.2 → 0.3.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/MIGRATION.md +31 -1
  3. package/README.md +17 -12
  4. package/dist/modules/cpq/api/product-attributes/route.js +28 -53
  5. package/dist/modules/cpq/api/product-attributes/route.js.map +2 -2
  6. package/dist/modules/cpq/api/product-charges/route.js +12 -2
  7. package/dist/modules/cpq/api/product-charges/route.js.map +2 -2
  8. package/dist/modules/cpq/api/product-offerings/route.js +2 -0
  9. package/dist/modules/cpq/api/product-offerings/route.js.map +2 -2
  10. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js +128 -59
  11. package/dist/modules/cpq/backend/cpq/offerings/[id]/page.js.map +2 -2
  12. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js +11 -11
  13. package/dist/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.js.map +2 -2
  14. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js +55 -1
  15. package/dist/modules/cpq/backend/cpq/specifications/[id]/page.js.map +2 -2
  16. package/dist/modules/cpq/cli.js +58 -4
  17. package/dist/modules/cpq/cli.js.map +3 -3
  18. package/dist/modules/cpq/data/charge-pricing.js +12 -0
  19. package/dist/modules/cpq/data/charge-pricing.js.map +7 -0
  20. package/dist/modules/cpq/data/entities.js +3 -0
  21. package/dist/modules/cpq/data/entities.js.map +2 -2
  22. package/dist/modules/cpq/data/validators.js +36 -15
  23. package/dist/modules/cpq/data/validators.js.map +2 -2
  24. package/dist/modules/cpq/events.js +7 -0
  25. package/dist/modules/cpq/events.js.map +2 -2
  26. package/dist/modules/cpq/lib/seeds/demo.js +103 -0
  27. package/dist/modules/cpq/lib/seeds/demo.js.map +7 -0
  28. package/dist/modules/cpq/migrations/Migration20260602000000_cpq_charge_model_split.js +28 -0
  29. package/dist/modules/cpq/migrations/Migration20260602000000_cpq_charge_model_split.js.map +7 -0
  30. package/dist/modules/cpq/services/cpqOrderService.js +130 -76
  31. package/dist/modules/cpq/services/cpqOrderService.js.map +2 -2
  32. package/dist/modules/cpq/services/cpqPricingService.js +53 -29
  33. package/dist/modules/cpq/services/cpqPricingService.js.map +3 -3
  34. package/dist/modules/cpq/services/types.js.map +2 -2
  35. package/package.json +4 -2
  36. package/src/modules/cpq/__integration__/TC-CPQ-012.spec.ts +34 -4
  37. package/src/modules/cpq/__tests__/package-isolation.test.ts +8 -3
  38. package/src/modules/cpq/api/product-attributes/__tests__/serializeAttribute.test.ts +62 -0
  39. package/src/modules/cpq/api/product-attributes/route.ts +34 -52
  40. package/src/modules/cpq/api/product-charges/route.ts +23 -2
  41. package/src/modules/cpq/api/product-offerings/route.ts +2 -0
  42. package/src/modules/cpq/backend/cpq/offerings/[id]/page.tsx +151 -62
  43. package/src/modules/cpq/backend/cpq/pricing/[id]/EntriesEditor.tsx +24 -22
  44. package/src/modules/cpq/backend/cpq/specifications/[id]/page.tsx +73 -1
  45. package/src/modules/cpq/cli.ts +83 -4
  46. package/src/modules/cpq/data/__tests__/charge-pricing.test.ts +49 -0
  47. package/src/modules/cpq/data/__tests__/validators.test.ts +60 -5
  48. package/src/modules/cpq/data/charge-pricing.ts +48 -0
  49. package/src/modules/cpq/data/entities.ts +10 -0
  50. package/src/modules/cpq/data/validators.ts +56 -26
  51. package/src/modules/cpq/events.ts +11 -3
  52. package/src/modules/cpq/lib/seeds/__tests__/demo.test.ts +78 -0
  53. package/src/modules/cpq/lib/seeds/demo.ts +145 -0
  54. package/src/modules/cpq/migrations/.snapshot-open-mercato.json +16 -0
  55. package/src/modules/cpq/migrations/Migration20260602000000_cpq_charge_model_split.ts +57 -0
  56. package/src/modules/cpq/services/__tests__/cpqPricingService.test.ts +172 -1
  57. package/src/modules/cpq/services/cpqOrderService.ts +214 -103
  58. package/src/modules/cpq/services/cpqPricingService.ts +82 -34
  59. 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 arcEvents = [];
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
- arcEvents.push(
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 arcEvents) {
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(sourceLog, merge.mergedSubscription.id, eventBaseEnvelope)
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 emitArcEvent(eventId, result, envelope) {
832
- const sub = result.subscription;
833
- await emitCpqEvent(eventId, {
834
- subscriptionId: sub.id,
835
- customerId: sub.customerId,
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
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
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
- performedByUserId: null,
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, mergedFromSubscriptionIds) {
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
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
932
+ organizationId: envelope.organizationId,
933
+ mergedSubscriptionId: merge.mergedSubscription.id,
934
+ sourceSubscriptionIds,
859
935
  sourceQuoteId: envelope.sourceQuoteId,
860
936
  sourceOrderId: envelope.sourceOrderId,
861
- performedByUserId: null,
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, newMergeSubId, envelope) {
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
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
943
+ organizationId: envelope.organizationId,
944
+ subscriptionId: sourceLog.subscriptionId,
945
+ effectiveDate,
878
946
  sourceQuoteId: envelope.sourceQuoteId,
879
947
  sourceOrderId: envelope.sourceOrderId,
880
- performedByUserId: null,
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