@mj-biz-apps/orders-core-entities-server 0.0.1 → 5.2.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/LICENSE +183 -0
- package/README.md +141 -2
- package/dist/AccountingBridge.d.ts +65 -0
- package/dist/AccountingBridge.d.ts.map +1 -0
- package/dist/AccountingBridge.js +102 -0
- package/dist/AccountingBridge.js.map +1 -0
- package/dist/AdvanceOrderStateOperation.d.ts +80 -0
- package/dist/AdvanceOrderStateOperation.d.ts.map +1 -0
- package/dist/AdvanceOrderStateOperation.js +267 -0
- package/dist/AdvanceOrderStateOperation.js.map +1 -0
- package/dist/ApplyAccountCreditOperation.d.ts +97 -0
- package/dist/ApplyAccountCreditOperation.d.ts.map +1 -0
- package/dist/ApplyAccountCreditOperation.js +275 -0
- package/dist/ApplyAccountCreditOperation.js.map +1 -0
- package/dist/BaseDeliveryChannel.d.ts +106 -0
- package/dist/BaseDeliveryChannel.d.ts.map +1 -0
- package/dist/BaseDeliveryChannel.js +36 -0
- package/dist/BaseDeliveryChannel.js.map +1 -0
- package/dist/BasePaymentProvider.d.ts +239 -0
- package/dist/BasePaymentProvider.d.ts.map +1 -0
- package/dist/BasePaymentProvider.js +91 -0
- package/dist/BasePaymentProvider.js.map +1 -0
- package/dist/BundleBehavior.d.ts +139 -0
- package/dist/BundleBehavior.d.ts.map +1 -0
- package/dist/BundleBehavior.js +173 -0
- package/dist/BundleBehavior.js.map +1 -0
- package/dist/BundleEngine.d.ts +60 -0
- package/dist/BundleEngine.d.ts.map +1 -0
- package/dist/BundleEngine.js +197 -0
- package/dist/BundleEngine.js.map +1 -0
- package/dist/CancelSubscriptionOperation.d.ts +98 -0
- package/dist/CancelSubscriptionOperation.d.ts.map +1 -0
- package/dist/CancelSubscriptionOperation.js +317 -0
- package/dist/CancelSubscriptionOperation.js.map +1 -0
- package/dist/CapturePaymentOperation.d.ts +62 -0
- package/dist/CapturePaymentOperation.d.ts.map +1 -0
- package/dist/CapturePaymentOperation.js +463 -0
- package/dist/CapturePaymentOperation.js.map +1 -0
- package/dist/CheckEntitlementOperation.d.ts +27 -0
- package/dist/CheckEntitlementOperation.d.ts.map +1 -0
- package/dist/CheckEntitlementOperation.js +45 -0
- package/dist/CheckEntitlementOperation.js.map +1 -0
- package/dist/CheckoutSessionService.d.ts +258 -0
- package/dist/CheckoutSessionService.d.ts.map +1 -0
- package/dist/CheckoutSessionService.js +1557 -0
- package/dist/CheckoutSessionService.js.map +1 -0
- package/dist/DeliveryBehavior.d.ts +121 -0
- package/dist/DeliveryBehavior.d.ts.map +1 -0
- package/dist/DeliveryBehavior.js +145 -0
- package/dist/DeliveryBehavior.js.map +1 -0
- package/dist/DeliveryRecipientResolver.d.ts +38 -0
- package/dist/DeliveryRecipientResolver.d.ts.map +1 -0
- package/dist/DeliveryRecipientResolver.js +93 -0
- package/dist/DeliveryRecipientResolver.js.map +1 -0
- package/dist/DeliveryResolver.d.ts +14 -0
- package/dist/DeliveryResolver.d.ts.map +1 -0
- package/dist/DeliveryResolver.js +45 -0
- package/dist/DeliveryResolver.js.map +1 -0
- package/dist/EmailDeliveryChannel.d.ts +31 -0
- package/dist/EmailDeliveryChannel.d.ts.map +1 -0
- package/dist/EmailDeliveryChannel.js +158 -0
- package/dist/EmailDeliveryChannel.js.map +1 -0
- package/dist/EntitlementBehavior.d.ts +235 -0
- package/dist/EntitlementBehavior.d.ts.map +1 -0
- package/dist/EntitlementBehavior.js +330 -0
- package/dist/EntitlementBehavior.js.map +1 -0
- package/dist/EntitlementEngine.d.ts +97 -0
- package/dist/EntitlementEngine.d.ts.map +1 -0
- package/dist/EntitlementEngine.js +338 -0
- package/dist/EntitlementEngine.js.map +1 -0
- package/dist/EntitlementGrantClaimDriver.d.ts +42 -0
- package/dist/EntitlementGrantClaimDriver.d.ts.map +1 -0
- package/dist/EntitlementGrantClaimDriver.js +158 -0
- package/dist/EntitlementGrantClaimDriver.js.map +1 -0
- package/dist/EntitlementRead.d.ts +82 -0
- package/dist/EntitlementRead.d.ts.map +1 -0
- package/dist/EntitlementRead.js +368 -0
- package/dist/EntitlementRead.js.map +1 -0
- package/dist/FulfillOrderLinesOperation.d.ts +34 -0
- package/dist/FulfillOrderLinesOperation.d.ts.map +1 -0
- package/dist/FulfillOrderLinesOperation.js +208 -0
- package/dist/FulfillOrderLinesOperation.js.map +1 -0
- package/dist/FulfillmentBehavior.d.ts +101 -0
- package/dist/FulfillmentBehavior.d.ts.map +1 -0
- package/dist/FulfillmentBehavior.js +145 -0
- package/dist/FulfillmentBehavior.js.map +1 -0
- package/dist/GLAccountResolver.d.ts +123 -0
- package/dist/GLAccountResolver.d.ts.map +1 -0
- package/dist/GLAccountResolver.js +175 -0
- package/dist/GLAccountResolver.js.map +1 -0
- package/dist/GetFulfillmentQueueOperation.d.ts +35 -0
- package/dist/GetFulfillmentQueueOperation.d.ts.map +1 -0
- package/dist/GetFulfillmentQueueOperation.js +221 -0
- package/dist/GetFulfillmentQueueOperation.js.map +1 -0
- package/dist/GetOverdueWorklistOperation.d.ts +41 -0
- package/dist/GetOverdueWorklistOperation.d.ts.map +1 -0
- package/dist/GetOverdueWorklistOperation.js +210 -0
- package/dist/GetOverdueWorklistOperation.js.map +1 -0
- package/dist/GiftCardBehavior.d.ts +97 -0
- package/dist/GiftCardBehavior.d.ts.map +1 -0
- package/dist/GiftCardBehavior.js +121 -0
- package/dist/GiftCardBehavior.js.map +1 -0
- package/dist/GiftCardEngine.d.ts +59 -0
- package/dist/GiftCardEngine.d.ts.map +1 -0
- package/dist/GiftCardEngine.js +197 -0
- package/dist/GiftCardEngine.js.map +1 -0
- package/dist/GuestOrderClaimDriver.d.ts +36 -0
- package/dist/GuestOrderClaimDriver.d.ts.map +1 -0
- package/dist/GuestOrderClaimDriver.js +161 -0
- package/dist/GuestOrderClaimDriver.js.map +1 -0
- package/dist/InvoiceBehavior.d.ts +394 -0
- package/dist/InvoiceBehavior.d.ts.map +1 -0
- package/dist/InvoiceBehavior.js +496 -0
- package/dist/InvoiceBehavior.js.map +1 -0
- package/dist/InvoiceBuilder.d.ts +48 -0
- package/dist/InvoiceBuilder.d.ts.map +1 -0
- package/dist/InvoiceBuilder.js +352 -0
- package/dist/InvoiceBuilder.js.map +1 -0
- package/dist/InvoiceDisplay.d.ts +96 -0
- package/dist/InvoiceDisplay.d.ts.map +1 -0
- package/dist/InvoiceDisplay.js +121 -0
- package/dist/InvoiceDisplay.js.map +1 -0
- package/dist/ListEntitlementsOperation.d.ts +21 -0
- package/dist/ListEntitlementsOperation.d.ts.map +1 -0
- package/dist/ListEntitlementsOperation.js +39 -0
- package/dist/ListEntitlementsOperation.js.map +1 -0
- package/dist/ManualPaymentProvider.d.ts +19 -0
- package/dist/ManualPaymentProvider.d.ts.map +1 -0
- package/dist/ManualPaymentProvider.js +92 -0
- package/dist/ManualPaymentProvider.js.map +1 -0
- package/dist/OrderEntityServer.d.ts +542 -0
- package/dist/OrderEntityServer.d.ts.map +1 -0
- package/dist/OrderEntityServer.js +2100 -0
- package/dist/OrderEntityServer.js.map +1 -0
- package/dist/OrderJournalEntryFactory.d.ts +140 -0
- package/dist/OrderJournalEntryFactory.d.ts.map +1 -0
- package/dist/OrderJournalEntryFactory.js +466 -0
- package/dist/OrderJournalEntryFactory.js.map +1 -0
- package/dist/OrderLineEntityServer.d.ts +90 -0
- package/dist/OrderLineEntityServer.d.ts.map +1 -0
- package/dist/OrderLineEntityServer.js +260 -0
- package/dist/OrderLineEntityServer.js.map +1 -0
- package/dist/OrdersSettings.d.ts +36 -0
- package/dist/OrdersSettings.d.ts.map +1 -0
- package/dist/OrdersSettings.js +147 -0
- package/dist/OrdersSettings.js.map +1 -0
- package/dist/PaymentAllocationFactory.d.ts +128 -0
- package/dist/PaymentAllocationFactory.d.ts.map +1 -0
- package/dist/PaymentAllocationFactory.js +235 -0
- package/dist/PaymentAllocationFactory.js.map +1 -0
- package/dist/PaymentHeaderEntityServer.d.ts +180 -0
- package/dist/PaymentHeaderEntityServer.d.ts.map +1 -0
- package/dist/PaymentHeaderEntityServer.js +656 -0
- package/dist/PaymentHeaderEntityServer.js.map +1 -0
- package/dist/PaymentIntentService.d.ts +101 -0
- package/dist/PaymentIntentService.d.ts.map +1 -0
- package/dist/PaymentIntentService.js +150 -0
- package/dist/PaymentIntentService.js.map +1 -0
- package/dist/PaymentJournalEntryFactory.d.ts +99 -0
- package/dist/PaymentJournalEntryFactory.d.ts.map +1 -0
- package/dist/PaymentJournalEntryFactory.js +127 -0
- package/dist/PaymentJournalEntryFactory.js.map +1 -0
- package/dist/PaymentLineEntityServer.d.ts +73 -0
- package/dist/PaymentLineEntityServer.d.ts.map +1 -0
- package/dist/PaymentLineEntityServer.js +303 -0
- package/dist/PaymentLineEntityServer.js.map +1 -0
- package/dist/PaymentProviderBehavior.d.ts +222 -0
- package/dist/PaymentProviderBehavior.d.ts.map +1 -0
- package/dist/PaymentProviderBehavior.js +368 -0
- package/dist/PaymentProviderBehavior.js.map +1 -0
- package/dist/PaymentProviderResolver.d.ts +84 -0
- package/dist/PaymentProviderResolver.d.ts.map +1 -0
- package/dist/PaymentProviderResolver.js +220 -0
- package/dist/PaymentProviderResolver.js.map +1 -0
- package/dist/PaymentReversalFactory.d.ts +107 -0
- package/dist/PaymentReversalFactory.d.ts.map +1 -0
- package/dist/PaymentReversalFactory.js +174 -0
- package/dist/PaymentReversalFactory.js.map +1 -0
- package/dist/PaymentSettlement.d.ts +56 -0
- package/dist/PaymentSettlement.d.ts.map +1 -0
- package/dist/PaymentSettlement.js +243 -0
- package/dist/PaymentSettlement.js.map +1 -0
- package/dist/PaymentTermsBehavior.d.ts +109 -0
- package/dist/PaymentTermsBehavior.d.ts.map +1 -0
- package/dist/PaymentTermsBehavior.js +172 -0
- package/dist/PaymentTermsBehavior.js.map +1 -0
- package/dist/PaymentWebhookHandler.d.ts +103 -0
- package/dist/PaymentWebhookHandler.d.ts.map +1 -0
- package/dist/PaymentWebhookHandler.js +246 -0
- package/dist/PaymentWebhookHandler.js.map +1 -0
- package/dist/PreviewPriceOperation.d.ts +62 -0
- package/dist/PreviewPriceOperation.d.ts.map +1 -0
- package/dist/PreviewPriceOperation.js +161 -0
- package/dist/PreviewPriceOperation.js.map +1 -0
- package/dist/PriceOrderOperation.d.ts +93 -0
- package/dist/PriceOrderOperation.d.ts.map +1 -0
- package/dist/PriceOrderOperation.js +146 -0
- package/dist/PriceOrderOperation.js.map +1 -0
- package/dist/ProductPriceEntityServer.d.ts +34 -0
- package/dist/ProductPriceEntityServer.d.ts.map +1 -0
- package/dist/ProductPriceEntityServer.js +97 -0
- package/dist/ProductPriceEntityServer.js.map +1 -0
- package/dist/RefundPaymentOperation.d.ts +61 -0
- package/dist/RefundPaymentOperation.d.ts.map +1 -0
- package/dist/RefundPaymentOperation.js +177 -0
- package/dist/RefundPaymentOperation.js.map +1 -0
- package/dist/RevenueRecognition.d.ts +76 -0
- package/dist/RevenueRecognition.d.ts.map +1 -0
- package/dist/RevenueRecognition.js +133 -0
- package/dist/RevenueRecognition.js.map +1 -0
- package/dist/ReversalBehavior.d.ts +82 -0
- package/dist/ReversalBehavior.d.ts.map +1 -0
- package/dist/ReversalBehavior.js +100 -0
- package/dist/ReversalBehavior.js.map +1 -0
- package/dist/ReversalResolver.d.ts +37 -0
- package/dist/ReversalResolver.d.ts.map +1 -0
- package/dist/ReversalResolver.js +96 -0
- package/dist/ReversalResolver.js.map +1 -0
- package/dist/SpawnRenewalsOperation.d.ts +109 -0
- package/dist/SpawnRenewalsOperation.d.ts.map +1 -0
- package/dist/SpawnRenewalsOperation.js +295 -0
- package/dist/SpawnRenewalsOperation.js.map +1 -0
- package/dist/StoredValuePaymentProvider.d.ts +52 -0
- package/dist/StoredValuePaymentProvider.d.ts.map +1 -0
- package/dist/StoredValuePaymentProvider.js +205 -0
- package/dist/StoredValuePaymentProvider.js.map +1 -0
- package/dist/StripeACHPaymentProvider.d.ts +50 -0
- package/dist/StripeACHPaymentProvider.d.ts.map +1 -0
- package/dist/StripeACHPaymentProvider.js +211 -0
- package/dist/StripeACHPaymentProvider.js.map +1 -0
- package/dist/StripePaymentProvider.d.ts +83 -0
- package/dist/StripePaymentProvider.d.ts.map +1 -0
- package/dist/StripePaymentProvider.js +442 -0
- package/dist/StripePaymentProvider.js.map +1 -0
- package/dist/SubscriptionBehavior.d.ts +197 -0
- package/dist/SubscriptionBehavior.d.ts.map +1 -0
- package/dist/SubscriptionBehavior.js +415 -0
- package/dist/SubscriptionBehavior.js.map +1 -0
- package/dist/checkoutCaptureAlert.d.ts +16 -0
- package/dist/checkoutCaptureAlert.d.ts.map +1 -0
- package/dist/checkoutCaptureAlert.js +55 -0
- package/dist/checkoutCaptureAlert.js.map +1 -0
- package/dist/checkoutCaptureRetry.d.ts +27 -0
- package/dist/checkoutCaptureRetry.d.ts.map +1 -0
- package/dist/checkoutCaptureRetry.js +50 -0
- package/dist/checkoutCaptureRetry.js.map +1 -0
- package/dist/claimDriverHelpers.d.ts +15 -0
- package/dist/claimDriverHelpers.d.ts.map +1 -0
- package/dist/claimDriverHelpers.js +34 -0
- package/dist/claimDriverHelpers.js.map +1 -0
- package/dist/entity-names.d.ts +15 -0
- package/dist/entity-names.d.ts.map +1 -0
- package/dist/entity-names.js +15 -0
- package/dist/entity-names.js.map +1 -0
- package/dist/identityClaimContracts.d.ts +118 -0
- package/dist/identityClaimContracts.d.ts.map +1 -0
- package/dist/identityClaimContracts.js +58 -0
- package/dist/identityClaimContracts.js.map +1 -0
- package/dist/index.d.ts +126 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +115 -0
- package/dist/index.js.map +1 -0
- package/dist/sql-guards.d.ts +78 -0
- package/dist/sql-guards.d.ts.map +1 -0
- package/dist/sql-guards.js +115 -0
- package/dist/sql-guards.js.map +1 -0
- package/package.json +50 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BundleBehavior.d.ts","sourceRoot":"","sources":["../src/BundleBehavior.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,0CAA0C;AAC1C,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,YAAY,CAAC;AAEzD,4DAA4D;AAC5D,MAAM,WAAW,eAAe;IAC5B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,iBAAiB,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC;AAED,oCAAoC;AACpC,MAAM,WAAW,kBAAkB;IAC/B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,aAAa,GACnB,YAAY,GACZ,cAAc,GACd,qBAAqB,GACrB,cAAc,GACd,cAAc,CAAC;AAErB,MAAM,WAAW,UAAU;IACvB,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC;IAC9B,2FAA2F;IAC3F,eAAe,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CA2BvE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAC/B,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,eAAe,EAAE,GAC9B;IAAE,YAAY,EAAE,MAAM,EAAE,CAAC;IAAC,eAAe,EAAE,OAAO,CAAA;CAAE,CAqBtD;AAED,kDAAkD;AAClD,MAAM,WAAW,eAAe;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;IAClB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,0FAA0F;IAC1F,SAAS,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAC/B,IAAI,EAAE,eAAe,EACrB,UAAU,EAAE,eAAe,EAAE,GAC9B,UAAU,CAgCZ;AAED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,cAAc,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAKvF;AAED,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAC9B,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,WAAW,EAAE,GACxB,YAAY,EAAE,CAchB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,MAAM,CAE7F"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle expansion arithmetic, with no database in sight.
|
|
3
|
+
*
|
|
4
|
+
* WHY EXPAND AT ALL. A bundle sold as one line forces ONE of everything: one tax treatment, one
|
|
5
|
+
* revenue schedule, one GL account, one entitlement, one returnable unit. A bundle of a publication
|
|
6
|
+
* (often exempt) and a conference registration (not) taxed at the header is simply wrong, and a
|
|
7
|
+
* bundle spanning a subscription and an event cannot be scheduled from a single line at all. So the
|
|
8
|
+
* components become real order lines and everything downstream keeps working per line.
|
|
9
|
+
*
|
|
10
|
+
* WHAT THE PARENT IS FOR. The customer bought "the Gold Package", not four things, and reporting
|
|
11
|
+
* needs to know a component came from a bundle — and from WHICH bundle line, since two Gold Packages
|
|
12
|
+
* on one order produce two indistinguishable sets otherwise. So the parent line survives as the
|
|
13
|
+
* customer-facing row, carrying `IsRollupParent`, and contributes ZERO to every total. The children
|
|
14
|
+
* carry the money.
|
|
15
|
+
*
|
|
16
|
+
* THE ARITHMETIC THAT BITES. Allocating one bundle price across components by relative standalone
|
|
17
|
+
* selling price does not divide evenly. Allocate $100 across three equal components and naive
|
|
18
|
+
* rounding gives 33.33 × 3 = 99.99 — a penny that vanishes, on an order that still balances because
|
|
19
|
+
* every line agrees with itself. `AllocateBundlePrice` uses largest-remainder so the parts sum to
|
|
20
|
+
* the whole EXACTLY, and its tests assert that on deliberately awkward numbers.
|
|
21
|
+
*
|
|
22
|
+
* CONNECTS TO:
|
|
23
|
+
* CODE: BundleEngine (the rows) · OrderEntityServer (the lifecycle point)
|
|
24
|
+
* DOC: plans/archive/bizapps-orders-master.md D32/D41 (bundles), D45 (ParentOrderLineID)
|
|
25
|
+
*/
|
|
26
|
+
/** Round to cents the way the rest of the engine does. */
|
|
27
|
+
const Money = (n) => Math.round((n + Number.EPSILON) * 100) / 100;
|
|
28
|
+
/**
|
|
29
|
+
* Split `total` across `weights` so the parts sum to `total` EXACTLY.
|
|
30
|
+
*
|
|
31
|
+
* Largest-remainder: floor every share to cents, then hand the leftover pennies one at a time to
|
|
32
|
+
* whichever shares were cut by the most. The naive alternative — round each share independently —
|
|
33
|
+
* loses or invents money on most inputs, and does it invisibly: each line is individually plausible
|
|
34
|
+
* and the order still balances against itself.
|
|
35
|
+
*
|
|
36
|
+
* Ties break toward the earlier index, so the result is deterministic for a given input rather than
|
|
37
|
+
* dependent on sort stability.
|
|
38
|
+
*/
|
|
39
|
+
export function SplitExactly(total, weights) {
|
|
40
|
+
const n = weights.length;
|
|
41
|
+
if (n === 0)
|
|
42
|
+
return [];
|
|
43
|
+
const cents = Math.round(Money(total) * 100);
|
|
44
|
+
const weightSum = weights.reduce((s, w) => s + w, 0);
|
|
45
|
+
// No usable weights: split as evenly as cents allow rather than putting it all on the first.
|
|
46
|
+
if (!(weightSum > 0)) {
|
|
47
|
+
const base = Math.floor(cents / n);
|
|
48
|
+
const out = new Array(n).fill(base);
|
|
49
|
+
for (let i = 0; i < cents - base * n; i++)
|
|
50
|
+
out[i] += 1;
|
|
51
|
+
return out.map((c) => c / 100);
|
|
52
|
+
}
|
|
53
|
+
const exact = weights.map((w) => (cents * w) / weightSum);
|
|
54
|
+
const floors = exact.map((e) => Math.floor(e));
|
|
55
|
+
let remaining = cents - floors.reduce((s, f) => s + f, 0);
|
|
56
|
+
const order = exact
|
|
57
|
+
.map((e, i) => ({ i, rem: e - Math.floor(e) }))
|
|
58
|
+
.sort((a, b) => (b.rem === a.rem ? a.i - b.i : b.rem - a.rem));
|
|
59
|
+
const out = [...floors];
|
|
60
|
+
for (let k = 0; k < order.length && remaining > 0; k++, remaining--)
|
|
61
|
+
out[order[k].i] += 1;
|
|
62
|
+
return out.map((c) => c / 100);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Allocate a bundle's price across its components by relative standalone selling price.
|
|
66
|
+
*
|
|
67
|
+
* `Bundled` components share the bundle price. `SumOfParts` components are priced on their own and
|
|
68
|
+
* take no share of it — a bundle may mix the two, which is how "everything in the package plus a
|
|
69
|
+
* discounted add-on priced separately" is expressed.
|
|
70
|
+
*
|
|
71
|
+
* WHEN NO COMPONENT HAS A PRICE the weights are all zero and there is nothing to allocate BY. Rather
|
|
72
|
+
* than refuse the sale or silently dump the whole amount on the first component, it splits evenly
|
|
73
|
+
* and says so through `AllocatedEvenly`, so a caller can record that the allocation was arbitrary.
|
|
74
|
+
* An even split is defensible; a silent lopsided one is not.
|
|
75
|
+
*/
|
|
76
|
+
export function AllocateBundlePrice(bundleTotal, components) {
|
|
77
|
+
const bundled = components.map((c) => c.PricingMode === 'Bundled');
|
|
78
|
+
// Weight by the component's standalone value FOR THE WHOLE LINE — its price times how many of
|
|
79
|
+
// it the bundle contains. Weighting by unit price alone would under-allocate to a component
|
|
80
|
+
// that appears three times.
|
|
81
|
+
const weights = components.map((c, i) => bundled[i] ? Math.max(0, Number(c.StandaloneSellingPrice ?? 0)) * Math.max(0, c.Quantity) : 0);
|
|
82
|
+
const bundledCount = bundled.filter(Boolean).length;
|
|
83
|
+
const weightSum = weights.reduce((s, w) => s + w, 0);
|
|
84
|
+
const allocatedEvenly = bundledCount > 0 && !(weightSum > 0);
|
|
85
|
+
if (allocatedEvenly) {
|
|
86
|
+
// Even across the BUNDLED ones only; SumOfParts components still take nothing.
|
|
87
|
+
const evenWeights = components.map((_, i) => (bundled[i] ? 1 : 0));
|
|
88
|
+
return { PerComponent: SplitExactly(bundleTotal, evenWeights), AllocatedEvenly: true };
|
|
89
|
+
}
|
|
90
|
+
return { PerComponent: SplitExactly(bundleTotal, weights), AllocatedEvenly: false };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Plan the child lines a bundle line expands into.
|
|
94
|
+
*
|
|
95
|
+
* ONE LEVEL ONLY. A component that is itself a bundle is refused rather than expanded recursively:
|
|
96
|
+
* the quantity ripple and the allocation both get considerably harder, and nothing in the catalog
|
|
97
|
+
* needs it yet. Easy to relax; very hard to un-ship once orders exist that depend on it.
|
|
98
|
+
*/
|
|
99
|
+
export function PlanBundleExpansion(line, components) {
|
|
100
|
+
const empty = (reason) => ({
|
|
101
|
+
Children: [],
|
|
102
|
+
Refusal: reason,
|
|
103
|
+
AllocatedEvenly: false,
|
|
104
|
+
});
|
|
105
|
+
if (!components.length)
|
|
106
|
+
return empty('NotABundle');
|
|
107
|
+
if (line.ReversesOrderLineID)
|
|
108
|
+
return empty('ReversalLine');
|
|
109
|
+
if (line.HasParent)
|
|
110
|
+
return empty('NestedBundle');
|
|
111
|
+
const qty = Number(line.Quantity ?? 0);
|
|
112
|
+
if (!(qty > 0))
|
|
113
|
+
return empty('NonPositiveQuantity');
|
|
114
|
+
// What the whole parent line is worth, which is what gets allocated.
|
|
115
|
+
const bundleTotal = Money(Money(line.UnitPrice ?? 0) * qty);
|
|
116
|
+
const { PerComponent, AllocatedEvenly } = AllocateBundlePrice(bundleTotal, components);
|
|
117
|
+
const children = components.map((c, i) => {
|
|
118
|
+
const childQty = ChildQuantity(qty, c.Quantity);
|
|
119
|
+
// The allocation is a TOTAL for the component across the whole line; the line stores a UNIT
|
|
120
|
+
// price, so divide back out. Dividing by zero would be a NaN that saves cleanly, so guard it.
|
|
121
|
+
const unitPrice = childQty > 0 ? Money(PerComponent[i] / childQty) : 0;
|
|
122
|
+
return {
|
|
123
|
+
ComponentProductID: c.ComponentProductID,
|
|
124
|
+
Quantity: childQty,
|
|
125
|
+
UnitPrice: c.PricingMode === 'Bundled' ? unitPrice : Money(c.StandaloneSellingPrice ?? 0),
|
|
126
|
+
SortOrder: c.SortOrder,
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
return { Children: children, Refusal: null, AllocatedEvenly };
|
|
130
|
+
}
|
|
131
|
+
/** A child's quantity: how many bundles times how many of the component each contains. */
|
|
132
|
+
export function ChildQuantity(parentQuantity, componentQuantity) {
|
|
133
|
+
const q = Number(parentQuantity ?? 0) * Number(componentQuantity ?? 0);
|
|
134
|
+
// Component quantities are DECIMAL(18,4); keep the same scale rather than rounding to an integer,
|
|
135
|
+
// since 0.5 hours of a service per bundle is a legitimate thing to sell.
|
|
136
|
+
return Math.round((q + Number.EPSILON) * 10000) / 10000;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* What a change to the parent's quantity should do to its children.
|
|
140
|
+
*
|
|
141
|
+
* A child whose quantity was hand-edited DETACHES from the ripple. Without that, bumping the bundle
|
|
142
|
+
* quantity silently overwrites a deliberate correction — data loss that looks exactly like
|
|
143
|
+
* arithmetic, because the number it lands on is the number the formula says it should be.
|
|
144
|
+
*
|
|
145
|
+
* Returns only the children that actually change, so a no-op ripple writes nothing.
|
|
146
|
+
*/
|
|
147
|
+
export function PlanQuantityRipple(parentQuantity, children) {
|
|
148
|
+
const changes = [];
|
|
149
|
+
for (const child of children) {
|
|
150
|
+
if (child.IsQuantityOverridden)
|
|
151
|
+
continue;
|
|
152
|
+
const target = ChildQuantity(parentQuantity, child.ComponentQuantity);
|
|
153
|
+
if (target !== child.CurrentQuantity) {
|
|
154
|
+
changes.push({
|
|
155
|
+
OrderLineID: child.ID,
|
|
156
|
+
FromQuantity: child.CurrentQuantity,
|
|
157
|
+
ToQuantity: target,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return changes;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Does this set of lines total correctly once parents are excluded?
|
|
165
|
+
*
|
|
166
|
+
* The invariant every rollup depends on: a rollup parent contributes nothing, so the sum over
|
|
167
|
+
* non-parent lines is the order's real value. Exported because it is worth asserting from the
|
|
168
|
+
* outside rather than trusting each rollup to remember.
|
|
169
|
+
*/
|
|
170
|
+
export function RollupTotal(lines) {
|
|
171
|
+
return Money(lines.filter((l) => !l.IsRollupParent).reduce((s, l) => s + Number(l.Amount ?? 0), 0));
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=BundleBehavior.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BundleBehavior.js","sourceRoot":"","sources":["../src/BundleBehavior.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expand bundle lines into their components, in memory, before anything is written.
|
|
3
|
+
*
|
|
4
|
+
* The ARITHMETIC lives in `BundleBehavior` and is unit-tested without a database. What lives here is
|
|
5
|
+
* the catalog lookup and the mutation of the pending line collection.
|
|
6
|
+
*
|
|
7
|
+
* WHY IT RUNS IN MEMORY, BEFORE THE INSERT. A Confirmed line is frozen by trigger 51003, and because
|
|
8
|
+
* the CRUD procs run under INSERT-EXEC a trigger rollback surfaces as "Cannot use the ROLLBACK
|
|
9
|
+
* statement within an INSERT-EXEC statement" — an error naming neither the line nor the rule. So
|
|
10
|
+
* anything that changes a line's money has to be settled before the row goes down, not corrected
|
|
11
|
+
* afterwards. Expansion changes a great deal of money: it zeroes the parent and creates children.
|
|
12
|
+
*
|
|
13
|
+
* WHY THE PARENT'S ID IS ASSIGNED HERE. A child needs `ParentOrderLineID` at INSERT time for the
|
|
14
|
+
* same reason — setting it afterwards would be an update to a frozen line. So the parent gets its
|
|
15
|
+
* ID before either row is written, and the children point at it from the start.
|
|
16
|
+
*
|
|
17
|
+
* SNAPSHOT, NOT A LIVE VIEW. Expansion is frozen onto the order when it is placed. Editing
|
|
18
|
+
* `ProductBundleItem` later must never mutate a historical order, so nothing re-derives children
|
|
19
|
+
* from the bundle definition after the fact.
|
|
20
|
+
*
|
|
21
|
+
* CONNECTS TO:
|
|
22
|
+
* PURE: BundleBehavior (+ its unit tests)
|
|
23
|
+
* CODE: OrderEntityServer.expandBundles (the lifecycle point)
|
|
24
|
+
* DOC: plans/archive/bizapps-orders-master.md D32/D41, D45
|
|
25
|
+
*/
|
|
26
|
+
import { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
27
|
+
import { mjBizAppsOrdersOrderLineEntity } from '@mj-biz-apps/orders-entities';
|
|
28
|
+
/** The subset of an order line this engine reads and writes. Kept structural so there is no import cycle. */
|
|
29
|
+
/**
|
|
30
|
+
* An order line this engine can expand.
|
|
31
|
+
*
|
|
32
|
+
* Was a structural duck-type built on `Get`/`Set` — which meant every field this module touched
|
|
33
|
+
* was a string literal the compiler could not check. It is the order-line entity; say so.
|
|
34
|
+
*/
|
|
35
|
+
export type ExpandableLine = mjBizAppsOrdersOrderLineEntity;
|
|
36
|
+
export interface BundleExpansionOutcome {
|
|
37
|
+
/** How many parent lines were expanded. */
|
|
38
|
+
Expanded: number;
|
|
39
|
+
/** How many child lines were created. */
|
|
40
|
+
ChildrenCreated: number;
|
|
41
|
+
/** Parent line IDs whose allocation had no prices to go on and was split evenly. */
|
|
42
|
+
ArbitrarilyAllocated: string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Expand every bundle line in `lines`, appending children to the same collection.
|
|
46
|
+
*
|
|
47
|
+
* `makeLine` creates a fresh, unsaved order-line entity — supplied by the caller so this module does
|
|
48
|
+
* not need to know how the host builds one. Returns what happened rather than throwing when there is
|
|
49
|
+
* nothing to expand, since most orders contain no bundles.
|
|
50
|
+
*/
|
|
51
|
+
export declare function ExpandBundleLines(lines: ExpandableLine[], makeLine: () => Promise<ExpandableLine>, provider: IMetadataProvider, user: UserInfo): Promise<BundleExpansionOutcome>;
|
|
52
|
+
/**
|
|
53
|
+
* Apply a parent's quantity change to its children.
|
|
54
|
+
*
|
|
55
|
+
* Exported for the edit path rather than the confirm path — on a DRAFT order a parent's quantity may
|
|
56
|
+
* still change, and its children have to follow. A child whose quantity was hand-edited detaches, so
|
|
57
|
+
* a deliberate correction is not silently overwritten by the formula.
|
|
58
|
+
*/
|
|
59
|
+
export declare function RippleBundleQuantity(parentOrderLineID: string, parentQuantity: number, provider: IMetadataProvider, user: UserInfo): Promise<number>;
|
|
60
|
+
//# sourceMappingURL=BundleEngine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BundleEngine.d.ts","sourceRoot":"","sources":["../src/BundleEngine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAEH,iBAAiB,EAGjB,QAAQ,EACX,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,8BAA8B,EAAE,MAAM,8BAA8B,CAAC;AAc9E,6GAA6G;AAC7G;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,8BAA8B,CAAC;AAE5D,MAAM,WAAW,sBAAsB;IACnC,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,eAAe,EAAE,MAAM,CAAC;IACxB,oFAAoF;IACpF,oBAAoB,EAAE,MAAM,EAAE,CAAC;CAClC;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACnC,KAAK,EAAE,cAAc,EAAE,EACvB,QAAQ,EAAE,MAAM,OAAO,CAAC,cAAc,CAAC,EACvC,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,QAAQ,GACf,OAAO,CAAC,sBAAsB,CAAC,CAkIjC;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACtC,iBAAiB,EAAE,MAAM,EACzB,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,QAAQ,GACf,OAAO,CAAC,MAAM,CAAC,CA0DjB"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expand bundle lines into their components, in memory, before anything is written.
|
|
3
|
+
*
|
|
4
|
+
* The ARITHMETIC lives in `BundleBehavior` and is unit-tested without a database. What lives here is
|
|
5
|
+
* the catalog lookup and the mutation of the pending line collection.
|
|
6
|
+
*
|
|
7
|
+
* WHY IT RUNS IN MEMORY, BEFORE THE INSERT. A Confirmed line is frozen by trigger 51003, and because
|
|
8
|
+
* the CRUD procs run under INSERT-EXEC a trigger rollback surfaces as "Cannot use the ROLLBACK
|
|
9
|
+
* statement within an INSERT-EXEC statement" — an error naming neither the line nor the rule. So
|
|
10
|
+
* anything that changes a line's money has to be settled before the row goes down, not corrected
|
|
11
|
+
* afterwards. Expansion changes a great deal of money: it zeroes the parent and creates children.
|
|
12
|
+
*
|
|
13
|
+
* WHY THE PARENT'S ID IS ASSIGNED HERE. A child needs `ParentOrderLineID` at INSERT time for the
|
|
14
|
+
* same reason — setting it afterwards would be an update to a frozen line. So the parent gets its
|
|
15
|
+
* ID before either row is written, and the children point at it from the start.
|
|
16
|
+
*
|
|
17
|
+
* SNAPSHOT, NOT A LIVE VIEW. Expansion is frozen onto the order when it is placed. Editing
|
|
18
|
+
* `ProductBundleItem` later must never mutate a historical order, so nothing re-derives children
|
|
19
|
+
* from the bundle definition after the fact.
|
|
20
|
+
*
|
|
21
|
+
* CONNECTS TO:
|
|
22
|
+
* PURE: BundleBehavior (+ its unit tests)
|
|
23
|
+
* CODE: OrderEntityServer.expandBundles (the lifecycle point)
|
|
24
|
+
* DOC: plans/archive/bizapps-orders-master.md D32/D41, D45
|
|
25
|
+
*/
|
|
26
|
+
import { RunView, } from '@memberjunction/core';
|
|
27
|
+
import { PlanBundleExpansion, } from './BundleBehavior.js';
|
|
28
|
+
const PRODUCT_BUNDLE_ITEM_ENTITY = 'MJ_BizApps_Orders: Product Bundle Items';
|
|
29
|
+
const PRODUCT_PRICE_ENTITY = 'MJ_BizApps_Orders: Product Prices';
|
|
30
|
+
const ORDER_LINE_ENTITY = 'MJ_BizApps_Orders: Order Lines';
|
|
31
|
+
const key = (id) => (id ?? '').toLowerCase();
|
|
32
|
+
const quote = (ids) => [...new Set(ids.map((i) => `'${i}'`))].join(',');
|
|
33
|
+
/**
|
|
34
|
+
* Expand every bundle line in `lines`, appending children to the same collection.
|
|
35
|
+
*
|
|
36
|
+
* `makeLine` creates a fresh, unsaved order-line entity — supplied by the caller so this module does
|
|
37
|
+
* not need to know how the host builds one. Returns what happened rather than throwing when there is
|
|
38
|
+
* nothing to expand, since most orders contain no bundles.
|
|
39
|
+
*/
|
|
40
|
+
export async function ExpandBundleLines(lines, makeLine, provider, user) {
|
|
41
|
+
const out = { Expanded: 0, ChildrenCreated: 0, ArbitrarilyAllocated: [] };
|
|
42
|
+
if (!lines.length)
|
|
43
|
+
return out;
|
|
44
|
+
const rv = new RunView(provider);
|
|
45
|
+
// WHICH of these products are bundles? One query, not one per line.
|
|
46
|
+
const productIDs = [...new Set(lines.map((l) => l.ProductID))].filter(Boolean);
|
|
47
|
+
if (!productIDs.length)
|
|
48
|
+
return out;
|
|
49
|
+
const items = await rv.RunView({
|
|
50
|
+
EntityName: PRODUCT_BUNDLE_ITEM_ENTITY,
|
|
51
|
+
ExtraFilter: `BundleProductID IN (${quote(productIDs)})`,
|
|
52
|
+
ResultType: 'simple',
|
|
53
|
+
}, user);
|
|
54
|
+
const rows = items.Results ?? [];
|
|
55
|
+
if (!rows.length)
|
|
56
|
+
return out;
|
|
57
|
+
const componentsByBundle = new Map();
|
|
58
|
+
for (const row of rows) {
|
|
59
|
+
const k = key(row.BundleProductID);
|
|
60
|
+
if (!componentsByBundle.has(k))
|
|
61
|
+
componentsByBundle.set(k, []);
|
|
62
|
+
componentsByBundle.get(k).push(row);
|
|
63
|
+
}
|
|
64
|
+
for (const list of componentsByBundle.values())
|
|
65
|
+
list.sort((a, b) => a.SortOrder - b.SortOrder);
|
|
66
|
+
// STANDALONE SELLING PRICES for the components, so allocation has something to weight by. Only
|
|
67
|
+
// Active, undated-or-current rules with no price list — a list-scoped price is a negotiated
|
|
68
|
+
// price for one customer, not the product's standalone value.
|
|
69
|
+
const componentIDs = [...new Set(rows.map((r) => r.ComponentProductID))];
|
|
70
|
+
const prices = await rv.RunView({
|
|
71
|
+
EntityName: PRODUCT_PRICE_ENTITY,
|
|
72
|
+
ExtraFilter: `ProductID IN (${quote(componentIDs)}) AND Status = 'Active' ` +
|
|
73
|
+
`AND PriceListID IS NULL AND MinQuantity IS NULL AND MaxQuantity IS NULL`,
|
|
74
|
+
ResultType: 'simple',
|
|
75
|
+
}, user);
|
|
76
|
+
const sspByProduct = new Map();
|
|
77
|
+
for (const p of (prices.Results ?? []).sort((a, b) => (b.Priority ?? 0) - (a.Priority ?? 0))) {
|
|
78
|
+
// Highest priority wins; the ambiguity guard already stops two rules tying.
|
|
79
|
+
if (!sspByProduct.has(key(p.ProductID)))
|
|
80
|
+
sspByProduct.set(key(p.ProductID), Number(p.Amount));
|
|
81
|
+
}
|
|
82
|
+
// Snapshot the collection: children are appended as we go and must not themselves be scanned.
|
|
83
|
+
const originals = [...lines];
|
|
84
|
+
for (const line of originals) {
|
|
85
|
+
const components = componentsByBundle.get(key(line.ProductID));
|
|
86
|
+
if (!components?.length)
|
|
87
|
+
continue;
|
|
88
|
+
const facts = {
|
|
89
|
+
ID: line.ID,
|
|
90
|
+
ProductID: line.ProductID,
|
|
91
|
+
Quantity: Number(line.Quantity ?? 0),
|
|
92
|
+
UnitPrice: Number(line.UnitPrice ?? 0),
|
|
93
|
+
ReversesOrderLineID: line.ReversesOrderLineID ?? null,
|
|
94
|
+
HasParent: !!line.ParentOrderLineID,
|
|
95
|
+
};
|
|
96
|
+
const plan = PlanBundleExpansion(facts, components.map((c) => ({
|
|
97
|
+
ComponentProductID: c.ComponentProductID,
|
|
98
|
+
Quantity: Number(c.Quantity ?? 1),
|
|
99
|
+
PricingMode: c.PricingMode,
|
|
100
|
+
SortOrder: c.SortOrder,
|
|
101
|
+
StandaloneSellingPrice: sspByProduct.get(key(c.ComponentProductID)) ?? null,
|
|
102
|
+
})));
|
|
103
|
+
if (!plan.Children.length) {
|
|
104
|
+
// A reversal line is a legitimate non-expansion — the return path unwinds the children
|
|
105
|
+
// that already exist. A NESTED bundle is not, and saying so beats expanding it wrongly.
|
|
106
|
+
if (plan.Refusal === 'NestedBundle') {
|
|
107
|
+
throw new Error(`Order line ${line.ID} is a bundle inside another bundle. Bundles expand ONE level ` +
|
|
108
|
+
`only (D45) — the quantity ripple and the price allocation are not defined for ` +
|
|
109
|
+
`deeper nesting. Sell the inner bundle's components directly, or sell it as its ` +
|
|
110
|
+
`own line.`);
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// THE PARENT KEEPS ITS PRICE FOR DISPLAY AND CONTRIBUTES NOTHING. Every rollup skips a
|
|
115
|
+
// rollup parent, so leaving money on it would double the order — which is exactly what the
|
|
116
|
+
// CK_OrderLine_RollupParentIsFree constraint refuses at the database.
|
|
117
|
+
line.IsRollupParent = true;
|
|
118
|
+
line.DiscountAmount = 0;
|
|
119
|
+
line.ChargeAmount = 0;
|
|
120
|
+
line.LineTax = 0;
|
|
121
|
+
for (const child of plan.Children) {
|
|
122
|
+
const row = await makeLine();
|
|
123
|
+
row.ProductID = child.ComponentProductID;
|
|
124
|
+
row.Quantity = child.Quantity;
|
|
125
|
+
row.UnitPrice = child.UnitPrice;
|
|
126
|
+
row.ParentOrderLineID = line.ID;
|
|
127
|
+
row.SourceBundleProductID = line.ProductID;
|
|
128
|
+
row.IsRollupParent = false;
|
|
129
|
+
row.IsQuantityOverridden = false;
|
|
130
|
+
lines.push(row);
|
|
131
|
+
out.ChildrenCreated++;
|
|
132
|
+
}
|
|
133
|
+
out.Expanded++;
|
|
134
|
+
if (plan.AllocatedEvenly)
|
|
135
|
+
out.ArbitrarilyAllocated.push(line.ID);
|
|
136
|
+
}
|
|
137
|
+
if (out.ArbitrarilyAllocated.length) {
|
|
138
|
+
console.warn(`Bundle line(s) ${out.ArbitrarilyAllocated.join(', ')}: no component carried a standalone ` +
|
|
139
|
+
`price, so the bundle price was split EVENLY rather than by relative value. The order ` +
|
|
140
|
+
`totals correctly, but the per-component revenue split is arbitrary. Price the ` +
|
|
141
|
+
`components to make the allocation meaningful.`);
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Apply a parent's quantity change to its children.
|
|
147
|
+
*
|
|
148
|
+
* Exported for the edit path rather than the confirm path — on a DRAFT order a parent's quantity may
|
|
149
|
+
* still change, and its children have to follow. A child whose quantity was hand-edited detaches, so
|
|
150
|
+
* a deliberate correction is not silently overwritten by the formula.
|
|
151
|
+
*/
|
|
152
|
+
export async function RippleBundleQuantity(parentOrderLineID, parentQuantity, provider, user) {
|
|
153
|
+
const rv = new RunView(provider);
|
|
154
|
+
const children = await rv.RunView({
|
|
155
|
+
EntityName: ORDER_LINE_ENTITY,
|
|
156
|
+
ExtraFilter: `ParentOrderLineID = '${parentOrderLineID}'`,
|
|
157
|
+
ResultType: 'simple',
|
|
158
|
+
}, user);
|
|
159
|
+
const rows = children.Results ?? [];
|
|
160
|
+
if (!rows.length)
|
|
161
|
+
return 0;
|
|
162
|
+
// The per-bundle component quantity is not stored on the child — it is the catalog's, and the
|
|
163
|
+
// catalog may have changed since. Read it from the SNAPSHOT instead: the child's current
|
|
164
|
+
// quantity divided by the parent's current quantity is what was agreed at expansion time.
|
|
165
|
+
// Falls back to the catalog only when the parent's old quantity is unknown.
|
|
166
|
+
const bundleProductID = rows[0].SourceBundleProductID;
|
|
167
|
+
const items = bundleProductID
|
|
168
|
+
? await rv.RunView({
|
|
169
|
+
EntityName: PRODUCT_BUNDLE_ITEM_ENTITY,
|
|
170
|
+
ExtraFilter: `BundleProductID = '${bundleProductID}'`,
|
|
171
|
+
ResultType: 'simple',
|
|
172
|
+
}, user)
|
|
173
|
+
: { Results: [] };
|
|
174
|
+
const perBundle = new Map((items.Results ?? []).map((i) => [key(i.ComponentProductID), Number(i.Quantity ?? 1)]));
|
|
175
|
+
let changed = 0;
|
|
176
|
+
for (const child of rows) {
|
|
177
|
+
if (child.IsQuantityOverridden)
|
|
178
|
+
continue;
|
|
179
|
+
const per = perBundle.get(key(child.ProductID));
|
|
180
|
+
if (per == null)
|
|
181
|
+
continue;
|
|
182
|
+
const target = Math.round((parentQuantity * per + Number.EPSILON) * 10000) / 10000;
|
|
183
|
+
if (target === Number(child.Quantity))
|
|
184
|
+
continue;
|
|
185
|
+
const entity = await provider.GetEntityObject(ORDER_LINE_ENTITY, user);
|
|
186
|
+
if (!(await entity.Load(child.ID)))
|
|
187
|
+
continue;
|
|
188
|
+
entity.Quantity = target;
|
|
189
|
+
if (!(await entity.Save())) {
|
|
190
|
+
throw new Error(`Could not ripple quantity ${target} to bundle child ${child.ID}: ` +
|
|
191
|
+
`${entity.LatestResult?.CompleteMessage ?? 'no reason given'}`);
|
|
192
|
+
}
|
|
193
|
+
changed++;
|
|
194
|
+
}
|
|
195
|
+
return changed;
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=BundleEngine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BundleEngine.js","sourceRoot":"","sources":["../src/BundleEngine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAIH,OAAO,GAEV,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACH,mBAAmB,GAGtB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,0BAA0B,GAAG,yCAAyC,CAAC;AAC7E,MAAM,oBAAoB,GAAG,mCAAmC,CAAC;AACjE,MAAM,iBAAiB,GAAG,gCAAgC,CAAC;AAE3D,MAAM,GAAG,GAAG,CAAC,EAA6B,EAAU,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;AAChF,MAAM,KAAK,GAAG,CAAC,GAAa,EAAU,EAAE,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAoB1F;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACnC,KAAuB,EACvB,QAAuC,EACvC,QAA2B,EAC3B,IAAc;IAEd,MAAM,GAAG,GAA2B,EAAE,QAAQ,EAAE,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,oBAAoB,EAAE,EAAE,EAAE,CAAC;IAClG,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,GAAG,CAAC;IAE9B,MAAM,EAAE,GAAG,IAAI,OAAO,CAAC,QAAuC,CAAC,CAAC;IAEhE,oEAAoE;IACpE,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/E,IAAI,CAAC,UAAU,CAAC,MAAM;QAAE,OAAO,GAAG,CAAC;IAEnC,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,OAAO,CAO1B;QACI,UAAU,EAAE,0BAA0B;QACtC,WAAW,EAAE,uBAAuB,KAAK,CAAC,UAAU,CAAC,GAAG;QACxD,UAAU,EAAE,QAAQ;KACvB,EACD,IAAI,CACP,CAAC;IACF,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;IACjC,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,OAAO,GAAG,CAAC;IAE7B,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC1D,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QACnC,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,kBAAkB,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9D,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACzC,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,kBAAkB,CAAC,MAAM,EAAE;QAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC;IAE/F,+FAA+F;IAC/F,4FAA4F;IAC5F,8DAA8D;IAC9D,MAAM,YAAY,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC;IACzE,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,OAAO,CAC3B;QACI,UAAU,EAAE,oBAAoB;QAChC,WAAW,EACP,iBAAiB,KAAK,CAAC,YAAY,CAAC,0BAA0B;YAC9D,yEAAyE;QAC7E,UAAU,EAAE,QAAQ;KACvB,EACD,IAAI,CACP,CAAC;IACF,MAAM,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC/C,KAAK,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3F,4EAA4E;QAC5E,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;YAAE,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IAClG,CAAC;IAED,8FAA8F;IAC9F,MAAM,SAAS,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IAE7B,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC3B,MAAM,UAAU,GAAG,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;QAC/D,IAAI,CAAC,UAAU,EAAE,MAAM;YAAE,SAAS;QAElC,MAAM,KAAK,GAAoB;YAC3B,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;YACpC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC;YACtC,mBAAmB,EAAE,IAAI,CAAC,mBAAmB,IAAI,IAAI;YACrD,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,iBAAiB;SACtC,CAAC;QAEF,MAAM,IAAI,GAAG,mBAAmB,CAC5B,KAAK,EACL,UAAU,CAAC,GAAG,CAAkB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACpC,kBAAkB,EAAE,CAAC,CAAC,kBAAkB;YACxC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC;YACjC,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,sBAAsB,EAAE,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,IAAI,IAAI;SAC9E,CAAC,CAAC,CACN,CAAC;QAEF,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YACxB,uFAAuF;YACvF,wFAAwF;YACxF,IAAI,IAAI,CAAC,OAAO,KAAK,cAAc,EAAE,CAAC;gBAClC,MAAM,IAAI,KAAK,CACX,cAAc,IAAI,CAAC,EAAE,+DAA+D;oBAChF,gFAAgF;oBAChF,iFAAiF;oBACjF,WAAW,CAClB,CAAC;YACN,CAAC;YACD,SAAS;QACb,CAAC;QAED,uFAAuF;QACvF,2FAA2F;QAC3F,sEAAsE;QACtE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC3B,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC;QACxB,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC;QAEjB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,MAAM,QAAQ,EAAE,CAAC;YAC7B,GAAG,CAAC,SAAS,GAAG,KAAK,CAAC,kBAAkB,CAAC;YACzC,GAAG,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;YAC9B,GAAG,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;YAChC,GAAG,CAAC,iBAAiB,GAAG,IAAI,CAAC,EAAE,CAAC;YAChC,GAAG,CAAC,qBAAqB,GAAG,IAAI,CAAC,SAAS,CAAC;YAC3C,GAAG,CAAC,cAAc,GAAG,KAAK,CAAC;YAC3B,GAAG,CAAC,oBAAoB,GAAG,KAAK,CAAC;YACjC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAChB,GAAG,CAAC,eAAe,EAAE,CAAC;QAC1B,CAAC;QAED,GAAG,CAAC,QAAQ,EAAE,CAAC;QACf,IAAI,IAAI,CAAC,eAAe;YAAE,GAAG,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACrE,CAAC;IAED,IAAI,GAAG,CAAC,oBAAoB,CAAC,MAAM,EAAE,CAAC;QAClC,OAAO,CAAC,IAAI,CACR,kBAAkB,GAAG,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,sCAAsC;YACvF,uFAAuF;YACvF,gFAAgF;YAChF,+CAA+C,CACtD,CAAC;IACN,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACtC,iBAAyB,EACzB,cAAsB,EACtB,QAA2B,EAC3B,IAAc;IAEd,MAAM,EAAE,GAAG,IAAI,OAAO,CAAC,QAAuC,CAAC,CAAC;IAChE,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,OAAO,CAO7B;QACI,UAAU,EAAE,iBAAiB;QAC7B,WAAW,EAAE,wBAAwB,iBAAiB,GAAG;QACzD,UAAU,EAAE,QAAQ;KACvB,EACD,IAAI,CACP,CAAC;IACF,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,IAAI,EAAE,CAAC;IACpC,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,OAAO,CAAC,CAAC;IAE3B,8FAA8F;IAC9F,yFAAyF;IACzF,0FAA0F;IAC1F,4EAA4E;IAC5E,MAAM,eAAe,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,qBAAqB,CAAC;IACtD,MAAM,KAAK,GAAG,eAAe;QACzB,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CACZ;YACI,UAAU,EAAE,0BAA0B;YACtC,WAAW,EAAE,sBAAsB,eAAe,GAAG;YACrD,UAAU,EAAE,QAAQ;SACvB,EACD,IAAI,CACP;QACH,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IACtB,MAAM,SAAS,GAAG,IAAI,GAAG,CACrB,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,CACzF,CAAC;IAEF,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,KAAK,CAAC,oBAAoB;YAAE,SAAS;QACzC,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;QAChD,IAAI,GAAG,IAAI,IAAI;YAAE,SAAS;QAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,cAAc,GAAG,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC;QACnF,IAAI,MAAM,KAAK,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC;YAAE,SAAS;QAEhD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,eAAe,CAAiC,iBAAiB,EAAE,IAAI,CAAC,CAAC;QACvG,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAAE,SAAS;QAC7C,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC;QACzB,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CACX,6BAA6B,MAAM,oBAAoB,KAAK,CAAC,EAAE,IAAI;gBAC/D,GAAG,MAAM,CAAC,YAAY,EAAE,eAAe,IAAI,iBAAiB,EAAE,CACrE,CAAC;QACN,CAAC;QACD,OAAO,EAAE,CAAC;IACd,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orders.CancelSubscription — cancel a subscription atomically, by POLICY (design §5).
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS AN OPERATION AND NOT A FORM
|
|
5
|
+
* The mechanics already worked: a reversal order line with a negative quantity produces mirrored
|
|
6
|
+
* journal entries through the ordinary booking path (D16). What did not work was asking a human to
|
|
7
|
+
* do it. Amith's case — a subscription running 1/1–12/31, cancelled on 7/1 — needs a line of
|
|
8
|
+
* quantity `-0.5`. That is correct double-entry and terrible data entry, and it silently ignores
|
|
9
|
+
* the `SubscriptionType`'s own rules about WHEN coverage may end and WHETHER money comes back.
|
|
10
|
+
*
|
|
11
|
+
* So the caller supplies a subscription, a date and a reason. `SubscriptionBehavior` decides what
|
|
12
|
+
* the rules permit; this operation performs it in ONE transaction:
|
|
13
|
+
*
|
|
14
|
+
* 1. resolve the affected term (the one whose window covers the request, else the latest)
|
|
15
|
+
* 2. ask the behaviour what the rules say — effective date, refund, reversal fraction
|
|
16
|
+
* 3. when there is something to reverse, emit a reversal ORDER whose single line carries the
|
|
17
|
+
* negative quantity and points at the original line, and confirm it — booking mirrors the JEs
|
|
18
|
+
* 4. stamp the term (Canceled/Completed, CanceledAt, CancellationEffectiveDate)
|
|
19
|
+
* 5. stamp the subscription (Canceled, EndDate = access-through, so grace is visible)
|
|
20
|
+
* 6. revoke standing grants when access-through has already passed; leave them when
|
|
21
|
+
* grace remains — the read evaluator honours `subscription.EndDate` either way
|
|
22
|
+
* 7. log a `SubscriptionEvent` — the lifecycle record that made the table worth having
|
|
23
|
+
*
|
|
24
|
+
* ATOMICITY: everything above shares one transaction opened here. The reversal order's own
|
|
25
|
+
* `OrderEntityServer.Save` nests inside it as savepoints (the same composition booking already
|
|
26
|
+
* relies on), so a failure anywhere leaves the subscription exactly as it was — never a reversed
|
|
27
|
+
* ledger with a still-active subscription, or vice versa.
|
|
28
|
+
*
|
|
29
|
+
* FAILURE MODEL: logical failures (unknown subscription, already cancelled, no term) come back
|
|
30
|
+
* INSIDE the output as `Success: false` with a message — the same contract accounting's operations
|
|
31
|
+
* use. Only genuine faults throw.
|
|
32
|
+
*
|
|
33
|
+
* CONNECTS TO:
|
|
34
|
+
* POLICY: SubscriptionBehavior.DecideCancellation (./SubscriptionBehavior.ts)
|
|
35
|
+
* BOOKING: OrderEntityServer.Save (./OrderEntityServer.ts) — the reversal order goes through it
|
|
36
|
+
* TABLES: __mj_BizAppsOrders.{Subscription,SubscriptionTerm,SubscriptionEvent,OrderHeader,OrderLine}
|
|
37
|
+
*/
|
|
38
|
+
import { BaseRemotableOperation, IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
39
|
+
import { type CancellationDecision } from './SubscriptionBehavior.js';
|
|
40
|
+
export interface CancelSubscriptionInput {
|
|
41
|
+
SubscriptionID: string;
|
|
42
|
+
/** When the customer asked. Defaults to today. The RULES decide when coverage actually ends. */
|
|
43
|
+
RequestDate?: Date | string;
|
|
44
|
+
/** Free text, stored on the lifecycle event. */
|
|
45
|
+
Reason?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Compute and return the decision WITHOUT writing anything — for a confirmation screen that
|
|
48
|
+
* shows "you will be refunded $X, coverage ends Y" before the user commits.
|
|
49
|
+
*/
|
|
50
|
+
Preview?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface CancelSubscriptionOutput {
|
|
53
|
+
Success: boolean;
|
|
54
|
+
Message?: string;
|
|
55
|
+
/** What the rules decided. Present even on a preview. */
|
|
56
|
+
Decision?: CancellationDecision;
|
|
57
|
+
/** The term that was (or would be) cancelled. */
|
|
58
|
+
SubscriptionTermID?: string;
|
|
59
|
+
/** The reversal order, when one was needed. Absent when nothing was refunded. */
|
|
60
|
+
ReversalOrderID?: string;
|
|
61
|
+
ReversalOrderNumber?: string;
|
|
62
|
+
}
|
|
63
|
+
export declare class CancelSubscriptionOperation extends BaseRemotableOperation<CancelSubscriptionInput, CancelSubscriptionOutput> {
|
|
64
|
+
OperationKey: string;
|
|
65
|
+
protected InternalExecute(input: CancelSubscriptionInput, provider: IMetadataProvider, user: UserInfo): Promise<CancelSubscriptionOutput>;
|
|
66
|
+
private loadSubscription;
|
|
67
|
+
private loadRules;
|
|
68
|
+
/**
|
|
69
|
+
* The term the request lands in, or — when the request falls outside every window (an early
|
|
70
|
+
* cancellation of a future term, or a late one after everything lapsed) — the latest term.
|
|
71
|
+
* Cancelling always has to act on SOMETHING for the reversal to point at.
|
|
72
|
+
*/
|
|
73
|
+
private resolveTerm;
|
|
74
|
+
/**
|
|
75
|
+
* A reversal order carrying ONE line: the negative slice of the original purchase.
|
|
76
|
+
*
|
|
77
|
+
* It goes through the ordinary confirm path rather than writing journal entries directly, so the
|
|
78
|
+
* mirrored ledger, the GL account resolution and the all-or-none guarantee are the SAME code
|
|
79
|
+
* that booked the original. `ReversesOrderLineID` is what makes a negative quantity legal
|
|
80
|
+
* (OrderLineEntityServer validates exactly that).
|
|
81
|
+
*/
|
|
82
|
+
private emitReversalOrder;
|
|
83
|
+
private loadOriginalLine;
|
|
84
|
+
private stampTerm;
|
|
85
|
+
private stampSubscription;
|
|
86
|
+
private logEvent;
|
|
87
|
+
/**
|
|
88
|
+
* Stored grant Status is a lie after cancel unless we revoke when access has already
|
|
89
|
+
* ended. Grace (AccessThroughDate still in the future) leaves the rows standing — the
|
|
90
|
+
* evaluator honours subscription.EndDate rather than the original ValidTo.
|
|
91
|
+
*/
|
|
92
|
+
private syncGrantsOnCancel;
|
|
93
|
+
/** The base behaviour, or the type's registered subclass when it names one (D45). */
|
|
94
|
+
private behaviorFor;
|
|
95
|
+
}
|
|
96
|
+
/** Tree-shaking anchor — called from the server bootstrap so the registration is retained. */
|
|
97
|
+
export declare function LoadCancelSubscriptionOperation(): void;
|
|
98
|
+
//# sourceMappingURL=CancelSubscriptionOperation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CancelSubscriptionOperation.d.ts","sourceRoot":"","sources":["../src/CancelSubscriptionOperation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,EAEH,sBAAsB,EAGtB,iBAAiB,EAIjB,QAAQ,EACX,MAAM,sBAAsB,CAAC;AAW9B,OAAO,EAEH,KAAK,oBAAoB,EAE5B,MAAM,2BAA2B,CAAC;AASnC,MAAM,WAAW,uBAAuB;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,gGAAgG;IAChG,WAAW,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAC5B,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,iDAAiD;IACjD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iFAAiF;IACjF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAChC;AAgCD,qBACa,2BAA4B,SAAQ,sBAAsB,CACnE,uBAAuB,EACvB,wBAAwB,CAC3B;IACU,YAAY,SAA+B;cAElC,eAAe,CAC3B,KAAK,EAAE,uBAAuB,EAC9B,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,QAAQ,GACf,OAAO,CAAC,wBAAwB,CAAC;YAuFtB,gBAAgB;YA6BhB,SAAS;IAavB;;;;OAIG;YACW,WAAW;IA6BzB;;;;;;;OAOG;YACW,iBAAiB;YAgEjB,gBAAgB;YAmBhB,SAAS;YAmBT,iBAAiB;YAwBjB,QAAQ;IA8BtB;;;;OAIG;YACW,kBAAkB;IAgBhC,qFAAqF;IACrF,OAAO,CAAC,WAAW;CAatB;AAED,8FAA8F;AAC9F,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD"}
|