@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":"PaymentWebhookHandler.d.ts","sourceRoot":"","sources":["../src/PaymentWebhookHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAGH,iBAAiB,EAKjB,QAAQ,EACX,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAYvF,MAAM,WAAW,cAAc;IAC3B,kGAAkG;IAClG,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC5C,2EAA2E;IAC3E,iBAAiB,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IACxB,2CAA2C;IAC3C,IAAI,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,aAAa,CAAA;KAAE,CAAC;CACxD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,oBAAoB,CACtC,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,QAAQ,GACf,OAAO,CAAC,eAAe,CAAC,CAoF1B;AAiHD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CAC/B,OAAO,EAAE,MAAM;IAAE,QAAQ,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC/D,CAAC,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,mBAAmB,KAAK,OAAO,CAAC,IAAI,CAAC,CAiBtE;AAED,oGAAoG;AACpG,MAAM,WAAW,kBAAkB;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC/C;AAED,iCAAiC;AACjC,MAAM,WAAW,mBAAmB;IAChC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAC;CACvD"}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PaymentWebhookHandler — the unauthenticated door, and everything that makes it safe to open.
|
|
3
|
+
*
|
|
4
|
+
* THE ROUTE HAS NO AUTHENTICATION, by necessity (D19). Stripe will not present a bearer token; the
|
|
5
|
+
* signature IS the credential. Every guard below exists because of that.
|
|
6
|
+
*
|
|
7
|
+
* TRANSPORT-AGNOSTIC ON PURPOSE. This is a function over `(rawBody, headers)` returning a status and a
|
|
8
|
+
* body, not an Express handler. Two reasons. It is unit-testable without standing up a server, which
|
|
9
|
+
* matters because the interesting cases are a forged signature and a replayed event — neither of which
|
|
10
|
+
* you want to be exercising through HTTP. And the host application owns its middleware order: the route
|
|
11
|
+
* must be mounted BEFORE the auth middleware and with a raw-body parser, and only the bootstrap knows
|
|
12
|
+
* how to do that. `MountPaymentWebhook` at the bottom is the thin adapter.
|
|
13
|
+
*
|
|
14
|
+
* THE RAW BODY IS LOAD-BEARING. The signature covers the exact bytes sent. A JSON round-trip — key
|
|
15
|
+
* order, whitespace, unicode normalisation — invalidates it, so any parser that runs before this
|
|
16
|
+
* silently breaks every webhook. `express.raw({ type: 'application/json' })` on this path only.
|
|
17
|
+
*
|
|
18
|
+
* WHAT THE RESPONSE SAYS, AND WHY IT SAYS SO LITTLE. A 2xx means "we will not need this again". A
|
|
19
|
+
* gateway retries anything else, so:
|
|
20
|
+
*
|
|
21
|
+
* 200 applied, or ALREADY applied, or deliberately ignored — all three are settled
|
|
22
|
+
* 400 malformed or unverifiable — retrying will not help, so do not ask again
|
|
23
|
+
* 500 we failed to record a valid event — PLEASE retry, this one is ours
|
|
24
|
+
*
|
|
25
|
+
* The body carries no detail. A public endpoint that explains why a signature failed is an oracle for
|
|
26
|
+
* anyone probing it, and the reason belongs in our logs where it is useful and not in a response where
|
|
27
|
+
* it is a hint.
|
|
28
|
+
*
|
|
29
|
+
* CONNECTS TO:
|
|
30
|
+
* PURE: ./PaymentProviderBehavior.ts — signature, idempotency decision
|
|
31
|
+
* LOOKUP: ./PaymentProviderResolver.ts
|
|
32
|
+
* DOC: plans/archive/bizapps-orders-master.md D19
|
|
33
|
+
*/
|
|
34
|
+
import { CompositeKey, LogError, LogStatus, RunView, } from '@memberjunction/core';
|
|
35
|
+
import { DecideWebhookAction } from './PaymentProviderBehavior.js';
|
|
36
|
+
import { SettlePaymentForEvent } from './PaymentSettlement.js';
|
|
37
|
+
import { BuildPaymentProvider, LoadPaymentProviderConfig } from './PaymentProviderResolver.js';
|
|
38
|
+
import { CheckoutSessionService } from './CheckoutSessionService.js';
|
|
39
|
+
import { CHECKOUT_CAPTURE_TERMINAL_LOG_MARKER, webhookEventExceedsRetryWindow, } from './checkoutCaptureRetry.js';
|
|
40
|
+
const PAYMENT_INTENT_ENTITY = 'MJ_BizApps_Orders: Payment Intents';
|
|
41
|
+
/**
|
|
42
|
+
* Handle one delivery.
|
|
43
|
+
*
|
|
44
|
+
* The order of operations is the security model, and it is deliberate:
|
|
45
|
+
*
|
|
46
|
+
* 1. resolve the provider — so we know which secret to verify against
|
|
47
|
+
* 2. VERIFY THE SIGNATURE — before the payload is parsed, let alone trusted
|
|
48
|
+
* 3. parse — now that we know it came from the gateway
|
|
49
|
+
* 4. decide idempotently — before anything is written
|
|
50
|
+
* 5. apply — inside a transaction
|
|
51
|
+
*
|
|
52
|
+
* Nothing before step 2 reads the payload. Parsing first would mean acting on attacker-controlled JSON
|
|
53
|
+
* to decide whether to trust attacker-controlled JSON.
|
|
54
|
+
*/
|
|
55
|
+
export async function HandlePaymentWebhook(request, provider, user) {
|
|
56
|
+
// ── 1. Which provider is this endpoint for? ────────────────────────────
|
|
57
|
+
let driver;
|
|
58
|
+
try {
|
|
59
|
+
const config = await LoadPaymentProviderConfig(request.PaymentProviderID, provider, user);
|
|
60
|
+
if (!config.Capabilities.SupportsWebhooks) {
|
|
61
|
+
LogError(`Webhook delivered for provider '${config.Name}', whose type does not support webhooks.`);
|
|
62
|
+
return { Status: 400, Body: { received: false } };
|
|
63
|
+
}
|
|
64
|
+
driver = await BuildPaymentProvider(config, provider, user);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
// An unknown or misconfigured provider is a 400: the gateway is calling a URL we cannot serve,
|
|
68
|
+
// and retrying will not change that.
|
|
69
|
+
LogError(`Webhook could not resolve provider ${request.PaymentProviderID}: ${err.message}`);
|
|
70
|
+
return { Status: 400, Body: { received: false } };
|
|
71
|
+
}
|
|
72
|
+
// ── 2. Is it really from them? ─────────────────────────────────────────
|
|
73
|
+
const verification = await driver.VerifyWebhook(request.RawBody, request.Headers);
|
|
74
|
+
if (!verification.Valid) {
|
|
75
|
+
// Logged in full, returned as nothing. This is the line between a useful log and an oracle.
|
|
76
|
+
LogError(`Rejected a webhook for provider ${request.PaymentProviderID}: ${verification.Reason ?? 'signature did not verify'}`);
|
|
77
|
+
return { Status: 400, Body: { received: false } };
|
|
78
|
+
}
|
|
79
|
+
// ── 3. Now it can be read. ─────────────────────────────────────────────
|
|
80
|
+
const event = driver.ParseWebhookEvent(request.RawBody);
|
|
81
|
+
if (!event) {
|
|
82
|
+
LogError(`A verified webhook for provider ${request.PaymentProviderID} could not be parsed.`);
|
|
83
|
+
return { Status: 400, Body: { received: false } };
|
|
84
|
+
}
|
|
85
|
+
// ── 4. Have we seen it, and do we care? ────────────────────────────────
|
|
86
|
+
const existing = await findIntent(event, provider, user);
|
|
87
|
+
const decision = DecideWebhookAction({
|
|
88
|
+
EventID: event.EventID,
|
|
89
|
+
EventKind: event.Kind,
|
|
90
|
+
AlreadySeen: existing?.ProviderEventID === event.EventID,
|
|
91
|
+
HandledKinds: driver.HandledEventKinds,
|
|
92
|
+
IntentKnown: existing != null,
|
|
93
|
+
});
|
|
94
|
+
if (decision.Action !== 'Apply') {
|
|
95
|
+
// Ignore / Reject stay 200 or 400 as before. AlreadyApplied is a settled *intent*
|
|
96
|
+
// stamp, not a settled checkout capture: if CapturePayment failed after confirm,
|
|
97
|
+
// Stripe's retry lands here and we still try to book (idempotent).
|
|
98
|
+
LogStatus(`Webhook ${event.EventID}: ${decision.Action} — ${decision.Reason}`);
|
|
99
|
+
if (decision.Action === 'AlreadyApplied' && existing) {
|
|
100
|
+
const booked = await bookCheckoutCaptureFromWebhook(event, existing.ID, user);
|
|
101
|
+
if (!booked) {
|
|
102
|
+
return { Status: 500, Body: { received: false } };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { Status: 200, Body: { received: true, outcome: decision.Action } };
|
|
106
|
+
}
|
|
107
|
+
// ── 5. Apply it. ───────────────────────────────────────────────────────
|
|
108
|
+
try {
|
|
109
|
+
// SETTLEMENT RUNS FIRST, AND THE ORDER IS LOAD-BEARING. `applyEvent` stamps
|
|
110
|
+
// `ProviderEventID`, which is the idempotency key — so an event recorded BEFORE settlement
|
|
111
|
+
// that then failed to settle would be judged `AlreadyApplied` on every retry, and a payment
|
|
112
|
+
// the bank confirmed would sit `Pending` for ever with nothing left to move it. Settling
|
|
113
|
+
// first means a failure returns 500 with nothing stamped and the gateway asks again.
|
|
114
|
+
//
|
|
115
|
+
// Only for drivers that have SAID they settle late. A card driver never reaches this line, so
|
|
116
|
+
// the guarantee in `applyEvent`'s note — that a webhook cannot reach the ledger — still holds
|
|
117
|
+
// everywhere it held before. Checkout CapturePayment is a separate, explicit call below.
|
|
118
|
+
if (driver.SettlesAsynchronously) {
|
|
119
|
+
await SettlePaymentForEvent(event, existing.ID, provider, user);
|
|
120
|
+
}
|
|
121
|
+
await applyEvent(event, existing.ID, provider, user);
|
|
122
|
+
const booked = await bookCheckoutCaptureFromWebhook(event, existing.ID, user);
|
|
123
|
+
if (!booked) {
|
|
124
|
+
return { Status: 500, Body: { received: false } };
|
|
125
|
+
}
|
|
126
|
+
return { Status: 200, Body: { received: true, outcome: 'Apply' } };
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
// OURS, not theirs. The event was valid and we failed to record it, so ask again — a 200 here
|
|
130
|
+
// would lose a real payment notification silently.
|
|
131
|
+
LogError(`Failed to apply webhook ${event.EventID}: ${err.message}`);
|
|
132
|
+
return { Status: 500, Body: { received: false } };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* After the intent row reflects Succeeded, book CapturePayment for a Confirmed
|
|
137
|
+
* checkout session if the complete path failed to. No-op for back-office intents.
|
|
138
|
+
* Returns false when a checkout capture was attempted, did not book, and is
|
|
139
|
+
* Retryable inside the event-age window — caller 500s so Stripe retries.
|
|
140
|
+
* Terminal refusals (and events older than the window) return true after a
|
|
141
|
+
* CHECKOUT-CAPTURE-TERMINAL log so Stripe stops and a human can see it.
|
|
142
|
+
*/
|
|
143
|
+
async function bookCheckoutCaptureFromWebhook(event, paymentIntentID, user) {
|
|
144
|
+
if (event.Status !== 'Succeeded') {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const book = await CheckoutSessionService.BookSettledCheckoutPaymentIfNeeded(paymentIntentID, user);
|
|
149
|
+
if (!(book.Attempted && !book.Booked)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
const retryable = book.Retryable !== false;
|
|
153
|
+
if (retryable && !event.OccurredAt) {
|
|
154
|
+
LogError(`[CHECKOUT-CAPTURE-RETRY] Webhook event ${event.EventID} has no OccurredAt; retry window cannot be applied — treating as Retryable`);
|
|
155
|
+
}
|
|
156
|
+
const pastWindow = webhookEventExceedsRetryWindow(event);
|
|
157
|
+
if (retryable && !pastWindow) {
|
|
158
|
+
LogError(`Checkout CapturePayment did not book for intent ${paymentIntentID} (retryable): ${book.ErrorMessage ?? 'unknown error'}`);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
LogError(`${CHECKOUT_CAPTURE_TERMINAL_LOG_MARKER} Settled checkout payment will not be retried via webhook` +
|
|
162
|
+
` intent=${paymentIntentID} event=${event.EventID}` +
|
|
163
|
+
` retryable=${retryable} pastWindow=${pastWindow}: ${book.ErrorMessage ?? 'unknown error'}`);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
LogError(`Checkout CapturePayment threw for intent ${paymentIntentID}: ${err instanceof Error ? err.message : String(err)}`);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** The `PaymentIntent` row this event is about, if we opened it. */
|
|
172
|
+
async function findIntent(event, provider, user) {
|
|
173
|
+
if (!event.ProviderIntentID)
|
|
174
|
+
return null;
|
|
175
|
+
// Escaped rather than interpolated raw: this value came off the wire. It is inside a verified
|
|
176
|
+
// payload, so it is not attacker-controlled in practice — but "verified" and "safe to concatenate
|
|
177
|
+
// into SQL" are different claims, and only one of them is being made here.
|
|
178
|
+
const safe = event.ProviderIntentID.replace(/'/g, "''");
|
|
179
|
+
const rv = new RunView(provider);
|
|
180
|
+
const result = await rv.RunView({
|
|
181
|
+
EntityName: PAYMENT_INTENT_ENTITY,
|
|
182
|
+
ExtraFilter: `ProviderIntentID = '${safe}'`,
|
|
183
|
+
ResultType: 'simple',
|
|
184
|
+
}, user);
|
|
185
|
+
return result?.Results?.[0] ?? null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Record what the gateway told us.
|
|
189
|
+
*
|
|
190
|
+
* DELIBERATELY NARROW: this moves the `PaymentIntent` and nothing else. It does not capture the
|
|
191
|
+
* `PaymentHeader`, book cash, or touch allocations — `PaymentHeaderEntityServer` owns all of that, and a
|
|
192
|
+
* webhook that reached into it would give the gateway a second path into the ledger.
|
|
193
|
+
*
|
|
194
|
+
* The intent is the RECORD OF WHAT THE GATEWAY SAYS; promoting that into cash is a separate, deliberate
|
|
195
|
+
* step. `ProviderEventID` is stamped in the same save, which is what makes the next delivery of this
|
|
196
|
+
* event a no-op rather than a second application.
|
|
197
|
+
*/
|
|
198
|
+
async function applyEvent(event, paymentIntentID, provider, user) {
|
|
199
|
+
const intent = await provider.GetEntityObject(PAYMENT_INTENT_ENTITY, CompositeKey.FromID(paymentIntentID), user);
|
|
200
|
+
if (event.Status)
|
|
201
|
+
intent.Status = event.Status;
|
|
202
|
+
intent.ProviderEventID = event.EventID;
|
|
203
|
+
intent.LastEventAt = new Date();
|
|
204
|
+
if (!(await intent.Save())) {
|
|
205
|
+
throw new Error(`Could not record event ${event.EventID} against payment intent ${paymentIntentID}: ` +
|
|
206
|
+
`${intent.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
207
|
+
}
|
|
208
|
+
LogStatus(`Payment intent ${paymentIntentID} is now ${event.Status ?? 'unchanged'} from event ${event.EventID}` +
|
|
209
|
+
`${event.FailureReason ? ` (${event.FailureReason})` : ''}`);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Mount the route on an Express-shaped app.
|
|
213
|
+
*
|
|
214
|
+
* Typed structurally rather than against `express`, so this package takes no dependency on it — the
|
|
215
|
+
* host already has one, and this file should not be the reason a shared server package pulls in a web
|
|
216
|
+
* framework.
|
|
217
|
+
*
|
|
218
|
+
* TWO THINGS THE CALLER MUST GET RIGHT, and neither can be enforced from here:
|
|
219
|
+
*
|
|
220
|
+
* · Mount BEFORE the auth middleware. Stripe presents no token; auth would reject every delivery.
|
|
221
|
+
* · Give this path a RAW body parser and nothing else. `express.json()` anywhere upstream re-encodes
|
|
222
|
+
* the payload and every signature fails.
|
|
223
|
+
*
|
|
224
|
+
* ```ts
|
|
225
|
+
* app.post(
|
|
226
|
+
* '/webhooks/payments/:providerId',
|
|
227
|
+
* express.raw({ type: 'application/json' }),
|
|
228
|
+
* MountPaymentWebhook(() => ({ provider: Metadata.Provider, user: systemUser })),
|
|
229
|
+
* );
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
export function MountPaymentWebhook(context) {
|
|
233
|
+
return async (req, res) => {
|
|
234
|
+
const { provider, user } = context();
|
|
235
|
+
// `req.body` is a Buffer under a raw parser and a string if something already decoded it.
|
|
236
|
+
// Both are accepted; anything else has been through a JSON parser and cannot verify.
|
|
237
|
+
const rawBody = typeof req.body === 'string' ? req.body : String(req.body ?? '');
|
|
238
|
+
const result = await HandlePaymentWebhook({
|
|
239
|
+
RawBody: rawBody,
|
|
240
|
+
Headers: req.headers ?? {},
|
|
241
|
+
PaymentProviderID: req.params?.providerId ?? '',
|
|
242
|
+
}, provider, user);
|
|
243
|
+
res.status(result.Status).json(result.Body);
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=PaymentWebhookHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PaymentWebhookHandler.js","sourceRoot":"","sources":["../src/PaymentWebhookHandler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orders.PreviewPrice — what would this customer pay for this product, and why? (plan D69)
|
|
3
|
+
*
|
|
4
|
+
* THE ONE RULE THAT MATTERS: this is a one-line `OrderPricingService` call. Save, `Orders.PriceOrder`,
|
|
5
|
+
* and this operation share that walk. Calling `ResolvePrice` here used to be a second entry point;
|
|
6
|
+
* it could return base list while booking used the member list (IT PC14).
|
|
7
|
+
*
|
|
8
|
+
* Nothing is written. Lines are materialised with `NewRecord()` and never `Save()`'d — same as
|
|
9
|
+
* `Orders.PriceOrder`.
|
|
10
|
+
*
|
|
11
|
+
* The output is still one SKU's unit price and decomposition. Promotions / charges / tax that the
|
|
12
|
+
* walk also runs on a one-line order are not the contract of this operation (they need a full
|
|
13
|
+
* header; use `Orders.PriceOrder`). UnitPrice / ExtendedAmount come from the walk's line stamp.
|
|
14
|
+
*
|
|
15
|
+
* Refusals come back INSIDE the output as `Success: false`. Ambiguous rules and unpriced products
|
|
16
|
+
* are refusals, not faults.
|
|
17
|
+
*/
|
|
18
|
+
import { BaseRemotableOperation, IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
19
|
+
export interface PreviewPriceInput {
|
|
20
|
+
ProductID: string;
|
|
21
|
+
/** Defaults to 1 — the common "what does one cost" question. */
|
|
22
|
+
Quantity?: number;
|
|
23
|
+
/** Who is buying. Either may be omitted; both omitted means base pricing. */
|
|
24
|
+
OrganizationID?: string | null;
|
|
25
|
+
PersonID?: string | null;
|
|
26
|
+
/** Defaults to now. Pass a future date to check a seasonal rate before it starts. */
|
|
27
|
+
AsOf?: string | Date;
|
|
28
|
+
/** Force a specific list, ignoring the customer's assignment — for "what if" comparisons. */
|
|
29
|
+
PriceListID?: string | null;
|
|
30
|
+
FeeType?: string;
|
|
31
|
+
}
|
|
32
|
+
/** One line of the explanation. */
|
|
33
|
+
export interface PreviewComponent {
|
|
34
|
+
ComponentType: string;
|
|
35
|
+
Label: string;
|
|
36
|
+
Amount: number;
|
|
37
|
+
RunningTotal: number;
|
|
38
|
+
}
|
|
39
|
+
export interface PreviewPriceOutput {
|
|
40
|
+
Success: boolean;
|
|
41
|
+
Message?: string;
|
|
42
|
+
UnitPrice?: number;
|
|
43
|
+
ExtendedAmount?: number;
|
|
44
|
+
Quantity?: number;
|
|
45
|
+
/** The list that applied, and how it was arrived at. */
|
|
46
|
+
PriceListID?: string | null;
|
|
47
|
+
PriceListName?: string | null;
|
|
48
|
+
/** Which rule won, for a rule author checking their work. */
|
|
49
|
+
ProductPriceID?: string | null;
|
|
50
|
+
/** Which resolver answered — 'default', or a plugin key like `Company:<id>`. */
|
|
51
|
+
ResolvedBy?: string;
|
|
52
|
+
Components?: PreviewComponent[];
|
|
53
|
+
}
|
|
54
|
+
export declare class PreviewPriceOperation extends BaseRemotableOperation<PreviewPriceInput, PreviewPriceOutput> {
|
|
55
|
+
OperationKey: string;
|
|
56
|
+
protected InternalExecute(input: PreviewPriceInput, provider: IMetadataProvider, user: UserInfo): Promise<PreviewPriceOutput>;
|
|
57
|
+
private loadProduct;
|
|
58
|
+
private loadPriceListName;
|
|
59
|
+
}
|
|
60
|
+
/** Tree-shaking anchor — called from the server bootstrap so the registration is retained. */
|
|
61
|
+
export declare function LoadPreviewPriceOperation(): void;
|
|
62
|
+
//# sourceMappingURL=PreviewPriceOperation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PreviewPriceOperation.d.ts","sourceRoot":"","sources":["../src/PreviewPriceOperation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EACH,sBAAsB,EACtB,iBAAiB,EAKjB,QAAQ,EACX,MAAM,sBAAsB,CAAC;AAQ9B,MAAM,WAAW,iBAAiB;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,qFAAqF;IACrF,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,6FAA6F;IAC7F,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,mCAAmC;AACnC,MAAM,WAAW,gBAAgB;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACnC;AAED,qBACa,qBAAsB,SAAQ,sBAAsB,CAAC,iBAAiB,EAAE,kBAAkB,CAAC;IAC7F,YAAY,SAAyB;cAE5B,eAAe,CAC3B,KAAK,EAAE,iBAAiB,EACxB,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,QAAQ,GACf,OAAO,CAAC,kBAAkB,CAAC;YA2GhB,WAAW;YAmBX,iBAAiB;CAclC;AAED,8FAA8F;AAC9F,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Orders.PreviewPrice — what would this customer pay for this product, and why? (plan D69)
|
|
9
|
+
*
|
|
10
|
+
* THE ONE RULE THAT MATTERS: this is a one-line `OrderPricingService` call. Save, `Orders.PriceOrder`,
|
|
11
|
+
* and this operation share that walk. Calling `ResolvePrice` here used to be a second entry point;
|
|
12
|
+
* it could return base list while booking used the member list (IT PC14).
|
|
13
|
+
*
|
|
14
|
+
* Nothing is written. Lines are materialised with `NewRecord()` and never `Save()`'d — same as
|
|
15
|
+
* `Orders.PriceOrder`.
|
|
16
|
+
*
|
|
17
|
+
* The output is still one SKU's unit price and decomposition. Promotions / charges / tax that the
|
|
18
|
+
* walk also runs on a one-line order are not the contract of this operation (they need a full
|
|
19
|
+
* header; use `Orders.PriceOrder`). UnitPrice / ExtendedAmount come from the walk's line stamp.
|
|
20
|
+
*
|
|
21
|
+
* Refusals come back INSIDE the output as `Success: false`. Ambiguous rules and unpriced products
|
|
22
|
+
* are refusals, not faults.
|
|
23
|
+
*/
|
|
24
|
+
import { BaseRemotableOperation, LogError, Metadata, RunView, } from '@memberjunction/core';
|
|
25
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
26
|
+
import { OrderPricingService, PriceResolutionError, ResolvePriceListForCustomer } from '@mj-biz-apps/orders-entities';
|
|
27
|
+
import { RequireOptionalUUID, RequireUUID } from './sql-guards.js';
|
|
28
|
+
const ORDER_LINE_ENTITY = 'MJ_BizApps_Orders: Order Lines';
|
|
29
|
+
let PreviewPriceOperation = class PreviewPriceOperation extends BaseRemotableOperation {
|
|
30
|
+
constructor() {
|
|
31
|
+
super(...arguments);
|
|
32
|
+
this.OperationKey = 'Orders.PreviewPrice';
|
|
33
|
+
}
|
|
34
|
+
async InternalExecute(input, provider, user) {
|
|
35
|
+
if (!input?.ProductID) {
|
|
36
|
+
return { Success: false, Message: 'ProductID is required.' };
|
|
37
|
+
}
|
|
38
|
+
// Caller-supplied ids reach SQL filter text downstream. Validated here,
|
|
39
|
+
// at the boundary, so every frame below this one can trust them.
|
|
40
|
+
RequireUUID(input.ProductID, 'ProductID');
|
|
41
|
+
RequireOptionalUUID(input.PriceListID, 'PriceListID');
|
|
42
|
+
RequireOptionalUUID(input.OrganizationID, 'OrganizationID');
|
|
43
|
+
RequireOptionalUUID(input.PersonID, 'PersonID');
|
|
44
|
+
const quantity = input.Quantity == null ? 1 : Number(input.Quantity);
|
|
45
|
+
if (!(quantity > 0)) {
|
|
46
|
+
return { Success: false, Message: `Quantity must be greater than zero (received ${input.Quantity}).` };
|
|
47
|
+
}
|
|
48
|
+
const product = await this.loadProduct(provider, user, input.ProductID);
|
|
49
|
+
if (!product) {
|
|
50
|
+
return { Success: false, Message: `Product ${input.ProductID} was not found.` };
|
|
51
|
+
}
|
|
52
|
+
const asOf = input.AsOf ? new Date(input.AsOf) : new Date();
|
|
53
|
+
if (Number.isNaN(asOf.getTime())) {
|
|
54
|
+
return { Success: false, Message: `AsOf is not a valid date (received ${String(input.AsOf)}).` };
|
|
55
|
+
}
|
|
56
|
+
const md = new Metadata();
|
|
57
|
+
const line = await md.GetEntityObject(ORDER_LINE_ENTITY, user);
|
|
58
|
+
line.NewRecord();
|
|
59
|
+
line.ProductID = input.ProductID;
|
|
60
|
+
line.Quantity = quantity;
|
|
61
|
+
const listCtx = {
|
|
62
|
+
ProductID: input.ProductID,
|
|
63
|
+
ProductCategoryID: product.ProductCategoryID,
|
|
64
|
+
CompanyID: product.CompanyID,
|
|
65
|
+
Quantity: quantity,
|
|
66
|
+
AsOf: asOf,
|
|
67
|
+
OrganizationID: input.OrganizationID ?? null,
|
|
68
|
+
PersonID: input.PersonID ?? null,
|
|
69
|
+
...(input.PriceListID !== undefined ? { PriceListID: input.PriceListID } : {}),
|
|
70
|
+
...(input.FeeType ? { FeeType: input.FeeType } : {}),
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
const result = await new OrderPricingService({ Provider: provider, User: user }).Price({
|
|
74
|
+
OrderHeaderID: null,
|
|
75
|
+
CompanyID: product.CompanyID,
|
|
76
|
+
BillToPersonID: input.PersonID ?? null,
|
|
77
|
+
BillToOrganizationID: input.OrganizationID ?? null,
|
|
78
|
+
OrderDate: asOf,
|
|
79
|
+
ShipToAddressID: null,
|
|
80
|
+
Lines: [line],
|
|
81
|
+
PromotionCodes: [],
|
|
82
|
+
ManualDiscounts: [],
|
|
83
|
+
Charges: [],
|
|
84
|
+
...(input.PriceListID !== undefined ? { PriceListID: input.PriceListID } : {}),
|
|
85
|
+
...(input.FeeType ? { FeeType: input.FeeType } : {}),
|
|
86
|
+
});
|
|
87
|
+
const resolved = result.PriceComponents.get(line);
|
|
88
|
+
if (!resolved) {
|
|
89
|
+
return {
|
|
90
|
+
Success: false,
|
|
91
|
+
Message: `No price is configured for ${product.Name}. Add a ProductPrice for it — either a base ` +
|
|
92
|
+
`price (no list) or one on the list this customer is assigned to.`,
|
|
93
|
+
Quantity: quantity,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// Report the list actually in force, even when the winning rule was a base rule: "you are
|
|
97
|
+
// on WHOLESALE but this SKU has no wholesale rate" is the answer people are looking for.
|
|
98
|
+
const listID = resolved.PriceListID ?? (await ResolvePriceListForCustomer(listCtx, provider, user));
|
|
99
|
+
const listName = listID ? await this.loadPriceListName(provider, user, listID) : null;
|
|
100
|
+
return {
|
|
101
|
+
Success: true,
|
|
102
|
+
Message: `${product.Name} × ${quantity} = ${resolved.ExtendedAmount}` +
|
|
103
|
+
(listName ? ` on price list ${listName}` : ' at base price'),
|
|
104
|
+
UnitPrice: resolved.UnitPrice,
|
|
105
|
+
ExtendedAmount: resolved.ExtendedAmount,
|
|
106
|
+
Quantity: quantity,
|
|
107
|
+
PriceListID: listID,
|
|
108
|
+
PriceListName: listName,
|
|
109
|
+
ProductPriceID: resolved.ProductPriceID,
|
|
110
|
+
ResolvedBy: resolved.ResolvedBy,
|
|
111
|
+
Components: resolved.Components.map((c) => ({
|
|
112
|
+
ComponentType: c.ComponentType,
|
|
113
|
+
Label: c.Label,
|
|
114
|
+
Amount: c.Amount,
|
|
115
|
+
RunningTotal: c.RunningTotal,
|
|
116
|
+
})),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
// Configuration problems (ambiguous rules, unpriced SKU) are REFUSALS with a reason, not
|
|
121
|
+
// faults. The walk throws those; mapping them here keeps the PreviewPrice contract.
|
|
122
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
123
|
+
if (err instanceof PriceResolutionError || /cannot be priced|ambiguous/i.test(message)) {
|
|
124
|
+
return { Success: false, Message: message, Quantity: quantity };
|
|
125
|
+
}
|
|
126
|
+
LogError(err);
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async loadProduct(provider, user, id) {
|
|
131
|
+
const rv = new RunView(provider);
|
|
132
|
+
const res = await rv.RunView({
|
|
133
|
+
EntityName: 'MJ_BizApps_Orders: Products',
|
|
134
|
+
ExtraFilter: `ID = '${id}'`,
|
|
135
|
+
Fields: ['Name', 'ProductCategoryID', 'CompanyID'],
|
|
136
|
+
ResultType: 'simple',
|
|
137
|
+
BypassCache: true,
|
|
138
|
+
}, user);
|
|
139
|
+
return res?.Results?.[0] ?? null;
|
|
140
|
+
}
|
|
141
|
+
async loadPriceListName(provider, user, id) {
|
|
142
|
+
const rv = new RunView(provider);
|
|
143
|
+
const res = await rv.RunView({
|
|
144
|
+
EntityName: 'MJ_BizApps_Orders: Price Lists',
|
|
145
|
+
ExtraFilter: `ID = '${id}'`,
|
|
146
|
+
Fields: ['Name'],
|
|
147
|
+
ResultType: 'simple',
|
|
148
|
+
BypassCache: true,
|
|
149
|
+
}, user);
|
|
150
|
+
return res?.Results?.[0]?.Name ?? null;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
PreviewPriceOperation = __decorate([
|
|
154
|
+
RegisterClass(BaseRemotableOperation, 'Orders.PreviewPrice')
|
|
155
|
+
], PreviewPriceOperation);
|
|
156
|
+
export { PreviewPriceOperation };
|
|
157
|
+
/** Tree-shaking anchor — called from the server bootstrap so the registration is retained. */
|
|
158
|
+
export function LoadPreviewPriceOperation() {
|
|
159
|
+
// intentionally empty
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=PreviewPriceOperation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PreviewPriceOperation.js","sourceRoot":"","sources":["../src/PreviewPriceOperation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `Orders.PriceOrder` — what a whole order comes to, persisting nothing.
|
|
3
|
+
*
|
|
4
|
+
* THE ONE RULE THAT MATTERS: this runs `OrderPricingService`, which is precisely what
|
|
5
|
+
* `OrderEntityServer.Save()` and `Orders.PreviewPrice` call. There is no second implementation to
|
|
6
|
+
* drift, which is the only way the number on the screen and the number in the ledger stay the same.
|
|
7
|
+
*
|
|
8
|
+
* WHY IT EXISTS ALONGSIDE `PreviewPrice`. That operation is a one-line call to the same service and
|
|
9
|
+
* reports one SKU's unit price. This one prices the whole header — promotions, charges, tax.
|
|
10
|
+
*
|
|
11
|
+
* WHAT IT REPLACED. An earlier `Orders.PreviewOrder` ran the REAL save inside a transaction that
|
|
12
|
+
* always rolled back, then read the computed values off the entities before they vanished. The
|
|
13
|
+
* reasoning was sound — a preview that reimplements pricing is a second copy of the rules — but the
|
|
14
|
+
* cost was not: it fired on every keystroke, so composing one order ran the full booking walk
|
|
15
|
+
* (journal entries, subscription decisions, entitlement grants, sequence numbers) dozens of times
|
|
16
|
+
* and discarded all of it, and the confirm was GATED on it. Extracting the pricing walk is what
|
|
17
|
+
* makes the honest version cheap: the decide step without the write.
|
|
18
|
+
*
|
|
19
|
+
* @module @mj-biz-apps/orders-core-entities-server
|
|
20
|
+
*/
|
|
21
|
+
import { BaseRemotableOperation, type IMetadataProvider, type UserInfo } from '@memberjunction/core';
|
|
22
|
+
interface PriceOrderInput {
|
|
23
|
+
OrderHeaderID?: string | null;
|
|
24
|
+
CompanyID: string;
|
|
25
|
+
BillToPersonID?: string | null;
|
|
26
|
+
BillToOrganizationID?: string | null;
|
|
27
|
+
OrderDate?: string | null;
|
|
28
|
+
ShipToAddressID?: string | null;
|
|
29
|
+
Lines: Array<{
|
|
30
|
+
ProductID: string;
|
|
31
|
+
Quantity: number;
|
|
32
|
+
UnitPrice?: number | null;
|
|
33
|
+
DiscountPct?: number | null;
|
|
34
|
+
ServicePeriodStart?: string | null;
|
|
35
|
+
ServicePeriodEnd?: string | null;
|
|
36
|
+
}>;
|
|
37
|
+
PromotionCodes?: string[];
|
|
38
|
+
ManualDiscounts?: Array<{
|
|
39
|
+
LineIndex: number;
|
|
40
|
+
Amount?: number | null;
|
|
41
|
+
Percent?: number | null;
|
|
42
|
+
Reason: string;
|
|
43
|
+
}>;
|
|
44
|
+
Charges?: Array<{
|
|
45
|
+
Code: string;
|
|
46
|
+
Amount?: number | null;
|
|
47
|
+
Rate?: number | null;
|
|
48
|
+
TargetLineIndex?: number | null;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
interface PricedLine {
|
|
52
|
+
ProductID: string;
|
|
53
|
+
Quantity: number;
|
|
54
|
+
UnitPrice: number;
|
|
55
|
+
DiscountAmount: number;
|
|
56
|
+
ChargeAmount: number;
|
|
57
|
+
LineTax: number;
|
|
58
|
+
LineTotalNet: number;
|
|
59
|
+
LineTotalGross: number;
|
|
60
|
+
Components?: Array<{
|
|
61
|
+
Kind: string;
|
|
62
|
+
Label: string;
|
|
63
|
+
Amount: number;
|
|
64
|
+
}>;
|
|
65
|
+
TaxExemptReason?: string | null;
|
|
66
|
+
}
|
|
67
|
+
interface PriceOrderOutput {
|
|
68
|
+
Success: boolean;
|
|
69
|
+
Message?: string | null;
|
|
70
|
+
Lines: PricedLine[];
|
|
71
|
+
Totals: {
|
|
72
|
+
Net: number;
|
|
73
|
+
Discount: number;
|
|
74
|
+
Charges: number;
|
|
75
|
+
Tax: number;
|
|
76
|
+
Gross: number;
|
|
77
|
+
};
|
|
78
|
+
UnusableCodes: Array<{
|
|
79
|
+
Code: string;
|
|
80
|
+
Reason: string;
|
|
81
|
+
}>;
|
|
82
|
+
}
|
|
83
|
+
export declare class PriceOrderOperation extends BaseRemotableOperation<PriceOrderInput, PriceOrderOutput> {
|
|
84
|
+
OperationKey: string;
|
|
85
|
+
protected InternalExecute(input: PriceOrderInput, provider: IMetadataProvider, user: UserInfo): Promise<PriceOrderOutput>;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Force the class registration. Tree-shaking removes a class nobody imports, and the decorator only
|
|
89
|
+
* runs if the module is loaded — so the server's bootstrap calls this.
|
|
90
|
+
*/
|
|
91
|
+
export declare function LoadPriceOrderOperation(): void;
|
|
92
|
+
export {};
|
|
93
|
+
//# sourceMappingURL=PriceOrderOperation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PriceOrderOperation.d.ts","sourceRoot":"","sources":["../src/PriceOrderOperation.ts"],"names":[],"mappings":"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"}
|