@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,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts of talking to a payment gateway that can be got wrong without a network — and therefore
|
|
3
|
+
* the parts worth proving without one.
|
|
4
|
+
*
|
|
5
|
+
* Three things live here, and each is here because its failure mode is silent:
|
|
6
|
+
*
|
|
7
|
+
* 1. MINOR-UNIT CONVERSION. Stripe and most gateways transact in the currency's smallest unit, so
|
|
8
|
+
* $12.34 is `1234`. Getting that wrong is a factor-of-100 error that looks like a plausible
|
|
9
|
+
* amount at both ends — charge 12 cents for a $12 order, or $1,234 for a $12.34 one. Worse, JPY
|
|
10
|
+
* has NO minor unit, so the "obvious" `× 100` overcharges a Japanese customer a hundredfold.
|
|
11
|
+
*
|
|
12
|
+
* 2. WEBHOOK SIGNATURE VERIFICATION. The webhook route is unauthenticated by necessity (D19) — the
|
|
13
|
+
* signature IS the authentication. A verifier that accepts anything is a public endpoint that
|
|
14
|
+
* marks orders paid, and it passes every test that only ever feeds it valid input.
|
|
15
|
+
*
|
|
16
|
+
* 3. IDEMPOTENCY. Gateways retry, and they retry on success too — a webhook that times out on our
|
|
17
|
+
* side is redelivered. Processing one twice books the cash twice.
|
|
18
|
+
*
|
|
19
|
+
* CONNECTS TO:
|
|
20
|
+
* SERVER: ./BasePaymentProvider.ts and the drivers under it
|
|
21
|
+
* ROUTE: ./PaymentWebhookHandler.ts
|
|
22
|
+
* DOC: plans/archive/bizapps-orders-master.md D19, D37
|
|
23
|
+
*/
|
|
24
|
+
// WEB CRYPTO, NOT `node:crypto`. The shared server tsconfig sets `"types": []`, so this package has no
|
|
25
|
+
// Node globals by design and no other file in it reaches for one. `globalThis.crypto` is typed by the
|
|
26
|
+
// `dom` lib that IS included, exists in Node 18+, and needs no dependency — the only cost is that HMAC
|
|
27
|
+
// becomes async, which the webhook path already is.
|
|
28
|
+
/**
|
|
29
|
+
* Currencies with NO minor unit, so an amount is already in its smallest unit.
|
|
30
|
+
*
|
|
31
|
+
* From Stripe's zero-decimal list. Kept explicit rather than derived from `Intl` because the gateway's
|
|
32
|
+
* opinion is what matters here, not the locale database's: we must send what Stripe expects, and a
|
|
33
|
+
* disagreement between the two would be a hundredfold error in whichever direction it fell.
|
|
34
|
+
*/
|
|
35
|
+
const ZERO_DECIMAL = new Set([
|
|
36
|
+
'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF',
|
|
37
|
+
'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF',
|
|
38
|
+
]);
|
|
39
|
+
/** Currencies whose smallest unit is a THOUSANDTH. Rare, and wrong by 10x if treated as ordinary. */
|
|
40
|
+
const THREE_DECIMAL = new Set(['BHD', 'JOD', 'KWD', 'OMR', 'TND']);
|
|
41
|
+
/** How many minor units make one major unit. */
|
|
42
|
+
export function CurrencyExponent(currencyCode) {
|
|
43
|
+
const code = (currencyCode ?? 'USD').toUpperCase();
|
|
44
|
+
if (ZERO_DECIMAL.has(code))
|
|
45
|
+
return 0;
|
|
46
|
+
if (THREE_DECIMAL.has(code))
|
|
47
|
+
return 3;
|
|
48
|
+
return 2;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Turn an amount we hold as a decimal into the integer the gateway wants.
|
|
52
|
+
*
|
|
53
|
+
* Rounds rather than truncates, and rounds the SCALED value: `Math.round(12.345 * 100)` is 1235 while
|
|
54
|
+
* `Math.round(12.345) * 100` is 1200. Both look like money.
|
|
55
|
+
*
|
|
56
|
+
* Throws on a non-finite amount instead of sending `NaN` to a gateway, which some accept as zero.
|
|
57
|
+
*/
|
|
58
|
+
export function ToMinorUnits(amount, currencyCode) {
|
|
59
|
+
if (!Number.isFinite(amount)) {
|
|
60
|
+
throw new Error(`Cannot convert ${amount} to minor units — it is not a finite amount.`);
|
|
61
|
+
}
|
|
62
|
+
const factor = 10 ** CurrencyExponent(currencyCode);
|
|
63
|
+
// `Number.EPSILON` nudge for the same reason `Money()` uses one: 1.005 * 100 is 100.49999… in
|
|
64
|
+
// binary floating point, and rounding that gives 100 rather than 101.
|
|
65
|
+
return Math.round((amount + Number.EPSILON * Math.sign(amount)) * factor);
|
|
66
|
+
}
|
|
67
|
+
/** And back, for reading what the gateway reported. */
|
|
68
|
+
export function FromMinorUnits(minor, currencyCode) {
|
|
69
|
+
if (!Number.isFinite(minor)) {
|
|
70
|
+
throw new Error(`Cannot convert ${minor} from minor units — it is not a finite amount.`);
|
|
71
|
+
}
|
|
72
|
+
const factor = 10 ** CurrencyExponent(currencyCode);
|
|
73
|
+
return Math.round((minor / factor) * 100) / 100;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Verify a Stripe-style `Stripe-Signature` header.
|
|
77
|
+
*
|
|
78
|
+
* The scheme: `t=<unix seconds>,v1=<hex hmac>[,v1=<another>]`, where the HMAC is SHA-256 over
|
|
79
|
+
* `${t}.${rawBody}` keyed with the endpoint secret. Multiple `v1` values appear during a secret
|
|
80
|
+
* rotation, and ANY match is a pass — a verifier that only reads the first one breaks silently
|
|
81
|
+
* halfway through a rotation, which is the worst possible moment.
|
|
82
|
+
*
|
|
83
|
+
* THE RAW BODY IS NOT NEGOTIABLE. The signature covers the exact bytes Stripe sent. Any JSON
|
|
84
|
+
* round-trip — key reordering, whitespace, unicode normalisation — changes them, and the signature
|
|
85
|
+
* then fails for a legitimate event. That is why the route captures the raw body before any parser
|
|
86
|
+
* touches it.
|
|
87
|
+
*
|
|
88
|
+
* TIMESTAMP TOLERANCE defends against replay: a valid old payload stays valid forever without it.
|
|
89
|
+
* Default five minutes, matching Stripe's own guidance. `nowSeconds` is a parameter so this is
|
|
90
|
+
* testable without waiting.
|
|
91
|
+
*/
|
|
92
|
+
export async function VerifyWebhookSignature(rawBody, signatureHeader, secret, opts = {}) {
|
|
93
|
+
if (!secret) {
|
|
94
|
+
// A missing secret must FAIL rather than skip verification. "No secret configured, so accept
|
|
95
|
+
// everything" turns a misconfiguration into an open endpoint that marks orders paid.
|
|
96
|
+
return { Valid: false, Reason: 'no endpoint secret is configured for this provider' };
|
|
97
|
+
}
|
|
98
|
+
if (!signatureHeader)
|
|
99
|
+
return { Valid: false, Reason: 'the request carried no signature header' };
|
|
100
|
+
let timestamp = null;
|
|
101
|
+
const candidates = [];
|
|
102
|
+
for (const part of signatureHeader.split(',')) {
|
|
103
|
+
const [key, value] = part.split('=', 2).map((s) => s?.trim());
|
|
104
|
+
if (key === 't' && value)
|
|
105
|
+
timestamp = Number(value);
|
|
106
|
+
if (key === 'v1' && value)
|
|
107
|
+
candidates.push(value);
|
|
108
|
+
}
|
|
109
|
+
if (timestamp == null || !Number.isFinite(timestamp)) {
|
|
110
|
+
return { Valid: false, Reason: 'the signature header carried no usable timestamp' };
|
|
111
|
+
}
|
|
112
|
+
if (!candidates.length)
|
|
113
|
+
return { Valid: false, Reason: 'the signature header carried no v1 signature' };
|
|
114
|
+
const tolerance = opts.ToleranceSeconds ?? 300;
|
|
115
|
+
const now = opts.NowSeconds ?? Math.floor(Date.now() / 1000);
|
|
116
|
+
if (Math.abs(now - timestamp) > tolerance) {
|
|
117
|
+
return {
|
|
118
|
+
Valid: false,
|
|
119
|
+
Reason: `the signature timestamp is ${Math.abs(now - timestamp)}s away, outside the ${tolerance}s tolerance`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const expected = await HmacSha256Hex(secret, `${timestamp}.${rawBody}`);
|
|
123
|
+
for (const candidate of candidates) {
|
|
124
|
+
if (SignaturesMatch(expected, candidate))
|
|
125
|
+
return { Valid: true };
|
|
126
|
+
}
|
|
127
|
+
return { Valid: false, Reason: 'no provided signature matched' };
|
|
128
|
+
}
|
|
129
|
+
/** HMAC-SHA256 as lowercase hex, via Web Crypto. */
|
|
130
|
+
export async function HmacSha256Hex(secret, payload) {
|
|
131
|
+
const encoder = new TextEncoder();
|
|
132
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
133
|
+
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));
|
|
134
|
+
return [...new Uint8Array(signature)].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Constant-time hex comparison.
|
|
138
|
+
*
|
|
139
|
+
* `===` on a signature leaks how many leading characters were right through how long the comparison
|
|
140
|
+
* took. That is a real attack against a public endpoint, and the fix costs nothing. `timingSafeEqual`
|
|
141
|
+
* throws on a length mismatch, so the lengths are checked first — and a mismatched length is itself
|
|
142
|
+
* not secret, since the algorithm fixes it.
|
|
143
|
+
*/
|
|
144
|
+
export function SignaturesMatch(expected, provided) {
|
|
145
|
+
if (expected.length !== provided.length)
|
|
146
|
+
return false;
|
|
147
|
+
// XOR-accumulate every position rather than returning on the first difference. `Node`'s
|
|
148
|
+
// `timingSafeEqual` would do this for us but lives behind `node:crypto`; the loop is the same
|
|
149
|
+
// guarantee in six lines, and it never short-circuits.
|
|
150
|
+
let diff = 0;
|
|
151
|
+
for (let i = 0; i < expected.length; i++) {
|
|
152
|
+
diff |= expected.charCodeAt(i) ^ provided.charCodeAt(i);
|
|
153
|
+
}
|
|
154
|
+
return diff === 0;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Map a gateway's own status onto ours.
|
|
158
|
+
*
|
|
159
|
+
* UNKNOWN MAPS TO `Processing`, NOT to `Succeeded` or `Failed`. A status we do not recognise means the
|
|
160
|
+
* gateway has told us something this code was not written for, and both confident answers are
|
|
161
|
+
* dangerous: `Succeeded` books cash that may not exist, `Failed` abandons a payment that may be fine.
|
|
162
|
+
* `Processing` is the honest reading — something is happening and we do not yet know what — and it
|
|
163
|
+
* leaves the payment for a human or a later event to resolve.
|
|
164
|
+
*/
|
|
165
|
+
export function MapStripeIntentStatus(providerStatus) {
|
|
166
|
+
switch ((providerStatus ?? '').toLowerCase()) {
|
|
167
|
+
case 'requires_payment_method':
|
|
168
|
+
case 'requires_confirmation':
|
|
169
|
+
case 'requires_action':
|
|
170
|
+
case 'requires_source':
|
|
171
|
+
case 'requires_source_action':
|
|
172
|
+
return 'RequiresPayment';
|
|
173
|
+
case 'processing':
|
|
174
|
+
return 'Processing';
|
|
175
|
+
case 'succeeded':
|
|
176
|
+
return 'Succeeded';
|
|
177
|
+
case 'canceled':
|
|
178
|
+
case 'cancelled':
|
|
179
|
+
return 'Canceled';
|
|
180
|
+
// Stripe has no `failed` intent status — a failure arrives as an EVENT against an intent that
|
|
181
|
+
// is back to requires_payment_method. `payment_intent.payment_failed` is mapped by the event
|
|
182
|
+
// reader, not here, and this case exists for gateways that do report it directly.
|
|
183
|
+
case 'failed':
|
|
184
|
+
return 'Failed';
|
|
185
|
+
default:
|
|
186
|
+
return 'Processing';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Decide what to do with an event, given what we have already seen.
|
|
191
|
+
*
|
|
192
|
+
* `AlreadyApplied` IS A SUCCESS, and that distinction is the whole point. A gateway that does not get
|
|
193
|
+
* a 2xx retries, so returning an error for a duplicate guarantees it comes back — forever. The
|
|
194
|
+
* response must say "yes, we have this" without doing the work twice.
|
|
195
|
+
*
|
|
196
|
+
* `Ignore` is for events we are not interested in. Gateways send far more than any one integration
|
|
197
|
+
* consumes, and treating an unrecognised event as an error means a Stripe dashboard full of red for
|
|
198
|
+
* events we were never going to read.
|
|
199
|
+
*/
|
|
200
|
+
export function DecideWebhookAction(input) {
|
|
201
|
+
if (!input.EventID) {
|
|
202
|
+
return { Action: 'Reject', Reason: 'the event carried no id, so it cannot be de-duplicated' };
|
|
203
|
+
}
|
|
204
|
+
if (input.AlreadySeen) {
|
|
205
|
+
return { Action: 'AlreadyApplied', Reason: `event ${input.EventID} has already been applied` };
|
|
206
|
+
}
|
|
207
|
+
if (!input.EventKind || !input.HandledKinds.includes(input.EventKind)) {
|
|
208
|
+
return { Action: 'Ignore', Reason: `'${input.EventKind ?? 'unknown'}' is not an event this integration acts on` };
|
|
209
|
+
}
|
|
210
|
+
if (!input.IntentKnown) {
|
|
211
|
+
// NOT an error. A gateway account may serve more than this application, and an intent we did
|
|
212
|
+
// not create is simply not ours — rejecting it would mean retries forever for someone else's
|
|
213
|
+
// traffic.
|
|
214
|
+
return { Action: 'Ignore', Reason: 'the event names a payment intent this application did not create' };
|
|
215
|
+
}
|
|
216
|
+
return { Action: 'Apply', Reason: `event ${input.EventID} is new and actionable` };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Split a captured amount into the cash actually received and the gateway's cut.
|
|
220
|
+
*
|
|
221
|
+
* The capture entry is `Dr Cash (net) / Dr Processing Fee / Cr A/R (gross)` (D18), so these three
|
|
222
|
+
* numbers must reconcile exactly or the entry will not balance. Deriving `net` by subtraction rather
|
|
223
|
+
* than trusting a reported net is deliberate: a gateway that rounds its own net differently would
|
|
224
|
+
* leave a penny with no home in the ledger.
|
|
225
|
+
*
|
|
226
|
+
* ALL THREE ARE EXACT AT 2DP, BUT `Net + Fee` IN JAVASCRIPT IS NOT. Adding two correctly-rounded
|
|
227
|
+
* decimals drifts in binary floating point — 32.06 + 1.27 is 33.330000000000005 — so a caller
|
|
228
|
+
* reconciling them must round the SUM before comparing, exactly as `Money()` does everywhere else in
|
|
229
|
+
* this package. This is the same hazard that made a balanced journal entry fail an exact-equality check
|
|
230
|
+
* in accounting; the numbers were never wrong, the comparison was.
|
|
231
|
+
*/
|
|
232
|
+
export function SplitCapturedAmount(grossAmount, feeAmount) {
|
|
233
|
+
const gross = Math.round(grossAmount * 100) / 100;
|
|
234
|
+
const fee = Math.round(Math.abs(feeAmount ?? 0) * 100) / 100;
|
|
235
|
+
if (fee > gross) {
|
|
236
|
+
throw new Error(`A processing fee of ${fee} cannot exceed the ${gross} captured. Booking this would credit ` +
|
|
237
|
+
`receivables less than the cash and fee together, and the entry would not balance.`);
|
|
238
|
+
}
|
|
239
|
+
return { Gross: gross, Fee: fee, Net: Math.round((gross - fee) * 100) / 100 };
|
|
240
|
+
}
|
|
241
|
+
// ─── Bank debits (ACH) ─────────────────────────────────────────────────────────────────────────
|
|
242
|
+
/**
|
|
243
|
+
* Stripe's published US bank-debit price: 0.8% of the amount, capped at $5.
|
|
244
|
+
*
|
|
245
|
+
* FOR THE STUB ONLY, and the name says so deliberately. The live path reads the fee off the charge's
|
|
246
|
+
* balance transaction like every other capture — what Stripe actually charged is a fact, and computing
|
|
247
|
+
* it here would be this file's opinion competing with the gateway's record. A stub that reported a
|
|
248
|
+
* ZERO fee would be worse still: it would make the fee leg of the capture entry (D18) unreachable in
|
|
249
|
+
* every test, so the arithmetic most worth exercising would be the arithmetic never exercised. That is
|
|
250
|
+
* the same reasoning behind the card stub's 2.9% + 30c, and the same reason this constant will drift
|
|
251
|
+
* from Stripe's price list one day without breaking anything real.
|
|
252
|
+
*
|
|
253
|
+
* The cap is a MAJOR-UNIT USD figure. A bank debit is a US rail, so the currency is USD in practice;
|
|
254
|
+
* a caller passing anything else gets the percentage without the cap rather than a cap converted at a
|
|
255
|
+
* rate this module has no business knowing.
|
|
256
|
+
*/
|
|
257
|
+
export function AchFeeEstimate(grossAmount, currencyCode = 'USD') {
|
|
258
|
+
const gross = Math.round(Math.abs(grossAmount ?? 0) * 100) / 100;
|
|
259
|
+
if (!Number.isFinite(gross) || gross <= 0)
|
|
260
|
+
return 0;
|
|
261
|
+
const raw = gross * 0.008;
|
|
262
|
+
const capped = currencyCode.toUpperCase() === 'USD' ? Math.min(raw, 5) : raw;
|
|
263
|
+
return Math.round(capped * 100) / 100;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Decide what a bank-debit event does to its payment.
|
|
267
|
+
*
|
|
268
|
+
* THE ASYMMETRY IS THE WHOLE DESIGN. A card tells you at the till; a bank debit tells you up to four
|
|
269
|
+
* business days later, and it can tell you TWICE — "taken" and then, a week after that, "returned"
|
|
270
|
+
* (insufficient funds, account closed, unauthorised). So there is no single moment where the answer is
|
|
271
|
+
* known, and a payment has to be able to move forward AND backward:
|
|
272
|
+
*
|
|
273
|
+
* Pending + succeeded → `Promote` the payment becomes Captured, which is what books the cash
|
|
274
|
+
* Pending + failed → `Fail` nothing was ever booked, so nothing needs reversing
|
|
275
|
+
* Captured + failed → `Reverse` the money left again; a REVERSING payment says so in the ledger
|
|
276
|
+
*
|
|
277
|
+
* WHY `Reverse` AND NOT AN UPDATE. A returned debit is not "the payment never happened" — it happened,
|
|
278
|
+
* the ledger recorded it, a period may have closed over it. Editing the original would erase a true
|
|
279
|
+
* fact about a past date. A reversing payment is the same answer the rest of this application already
|
|
280
|
+
* gives (`ReversesPaymentHeaderID`, D53), and it leaves both facts standing.
|
|
281
|
+
*
|
|
282
|
+
* `None` covers the ordinary quiet cases — an event that changes nothing, a promotion that already
|
|
283
|
+
* happened — and is a SUCCESS. A gateway that does not get a 2xx retries forever.
|
|
284
|
+
*/
|
|
285
|
+
export function DecideSettlement(input) {
|
|
286
|
+
const header = (input.HeaderStatus ?? '').trim();
|
|
287
|
+
if (!header) {
|
|
288
|
+
// NOT an error. An intent can exist without a payment — a checkout the customer abandoned, or
|
|
289
|
+
// one whose payment has not been captured yet. There is simply nothing to settle.
|
|
290
|
+
return { Action: 'None', Reason: 'the intent has no payment to settle' };
|
|
291
|
+
}
|
|
292
|
+
switch (input.EventStatus) {
|
|
293
|
+
case 'Succeeded':
|
|
294
|
+
if (header === 'Pending') {
|
|
295
|
+
return { Action: 'Promote', Reason: 'the bank confirmed the debit, so the payment becomes Captured' };
|
|
296
|
+
}
|
|
297
|
+
if (header === 'Captured') {
|
|
298
|
+
return { Action: 'None', Reason: 'the payment is already Captured' };
|
|
299
|
+
}
|
|
300
|
+
// Succeeded against Failed / Refunded / Disputed. A bank does not un-return a debit, so
|
|
301
|
+
// this is either a gateway we have misread or an event arriving wildly out of order.
|
|
302
|
+
return {
|
|
303
|
+
Action: 'Hold',
|
|
304
|
+
Reason: `the bank reported success against a payment that is ${header} — this needs a person`,
|
|
305
|
+
};
|
|
306
|
+
case 'Failed':
|
|
307
|
+
case 'Canceled':
|
|
308
|
+
if (header === 'Pending') {
|
|
309
|
+
return { Action: 'Fail', Reason: 'the debit did not clear and nothing was booked' };
|
|
310
|
+
}
|
|
311
|
+
if (header === 'Captured') {
|
|
312
|
+
if (input.AlreadyReversed) {
|
|
313
|
+
return { Action: 'None', Reason: 'this payment has already been reversed' };
|
|
314
|
+
}
|
|
315
|
+
if (!input.HeaderBooked) {
|
|
316
|
+
// Captured without a journal entry should be impossible — booking is part of the
|
|
317
|
+
// same transaction as the transition. If it happened, the two records disagree
|
|
318
|
+
// about whether cash exists, and guessing which is right is how the disagreement
|
|
319
|
+
// becomes permanent.
|
|
320
|
+
return {
|
|
321
|
+
Action: 'Hold',
|
|
322
|
+
Reason: 'the payment is Captured but carries no journal entry, so what to reverse is unclear',
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return { Action: 'Reverse', Reason: 'the debit was returned after being booked, so the cash comes back out' };
|
|
326
|
+
}
|
|
327
|
+
if (header === 'Failed') {
|
|
328
|
+
return { Action: 'None', Reason: 'the payment is already Failed' };
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
Action: 'Hold',
|
|
332
|
+
Reason: `the bank reported a failure against a payment that is ${header} — this needs a person`,
|
|
333
|
+
};
|
|
334
|
+
// Processing / RequiresPayment / unknown. The gateway is telling us it is still working, which
|
|
335
|
+
// is exactly the state a Pending payment already records.
|
|
336
|
+
default:
|
|
337
|
+
return { Action: 'None', Reason: `nothing to settle while the intent reads ${input.EventStatus ?? 'unknown'}` };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Whether a payment asking to be `Captured` must be held at `Pending` because its rail settles on
|
|
342
|
+
* somebody else's schedule.
|
|
343
|
+
*
|
|
344
|
+
* WHY THIS IS A FUNCTION AND NOT AN `IF` IN THE SAVE PATH. The rule used to live only in
|
|
345
|
+
* `Orders.CapturePayment`, which made it a rule that ONE CALLER followed rather than one the system
|
|
346
|
+
* enforced — a workflow, a UI form or a test builder writing `Status: 'Captured'` went straight past
|
|
347
|
+
* it and booked `Dr Cash` for a bank debit that had not cleared. Balanced, posted, and undetectable
|
|
348
|
+
* downstream. Stating it here means the entity server and the operation ask the same question of the
|
|
349
|
+
* same code instead of both remembering.
|
|
350
|
+
*
|
|
351
|
+
* THE PROMOTION MUST PASS. When the webhook moves a `Pending` payment to `Captured`, that IS the
|
|
352
|
+
* bank answering; holding it again would mean a bank debit could never book at all. The persisted
|
|
353
|
+
* status is the signal — already `Pending` means promotion, anything else means a caller declaring
|
|
354
|
+
* cash that has not moved.
|
|
355
|
+
*/
|
|
356
|
+
export function ShouldHoldForLateSettlement(facts) {
|
|
357
|
+
if (facts.RequestedStatus !== 'Captured')
|
|
358
|
+
return false;
|
|
359
|
+
if (!facts.HasProvider)
|
|
360
|
+
return false;
|
|
361
|
+
if (!facts.SettlesAsynchronously)
|
|
362
|
+
return false;
|
|
363
|
+
// The webhook promoting a payment that was already waiting — let it book.
|
|
364
|
+
if (facts.IsSaved && facts.PersistedStatus === 'Pending')
|
|
365
|
+
return false;
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
//# sourceMappingURL=PaymentProviderBehavior.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PaymentProviderBehavior.js","sourceRoot":"","sources":["../src/PaymentProviderBehavior.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PaymentProviderResolver — turns a `PaymentProvider` row into a working driver.
|
|
3
|
+
*
|
|
4
|
+
* Two jobs, and the second is the one with teeth.
|
|
5
|
+
*
|
|
6
|
+
* 1. FIND THE DRIVER. `PaymentProviderType.Code` is the `@RegisterClass` key (D37), so resolution is a
|
|
7
|
+
* ClassFactory lookup and adding a gateway needs no schema change. A missing registration REFUSES
|
|
8
|
+
* rather than falling back to the base class: the base's methods all decline, so a fallback would
|
|
9
|
+
* look like a gateway that cannot do anything rather than like a driver nobody registered — and the
|
|
10
|
+
* difference is a five-minute fix versus an afternoon.
|
|
11
|
+
*
|
|
12
|
+
* 2. FIND THE CREDENTIALS, WITHOUT EVER STORING THEM. `PaymentProvider.CredentialsRef` is a POINTER —
|
|
13
|
+
* the name of a place to look, not a secret. Nothing in this schema holds an API key, because a
|
|
14
|
+
* database that holds live payment credentials is a database whose backups do too.
|
|
15
|
+
*
|
|
16
|
+
* THE SECRET SEAM. `CredentialsRef` resolves through `BaseSecretResolver`, registered by ClassFactory
|
|
17
|
+
* like everything else here. The default reads environment variables, which is right for a single
|
|
18
|
+
* deployment and inadequate for a real one — so a customer using Vault, AWS Secrets Manager or Azure
|
|
19
|
+
* Key Vault registers a subclass and changes no other code. Naming the seam is the point; pretending
|
|
20
|
+
* environment variables are a secret store would be the mistake.
|
|
21
|
+
*
|
|
22
|
+
* CREDENTIALS ARE NEVER CACHED. They are fetched per operation and dropped. A cache would keep live
|
|
23
|
+
* keys in process memory for the lifetime of a server for the sake of avoiding a lookup that happens
|
|
24
|
+
* once per payment, which is not a trade worth making.
|
|
25
|
+
*
|
|
26
|
+
* CONNECTS TO:
|
|
27
|
+
* BASE: ./BasePaymentProvider.ts
|
|
28
|
+
* DRIVERS: ./StripePaymentProvider.ts · ./ManualPaymentProvider.ts · ./StoredValuePaymentProvider.ts
|
|
29
|
+
* DOC: plans/archive/bizapps-orders-master.md D19, D37
|
|
30
|
+
*/
|
|
31
|
+
import { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
32
|
+
import { BasePaymentProvider, type PaymentCredentials, type PaymentProviderConfig } from './BasePaymentProvider.js';
|
|
33
|
+
/**
|
|
34
|
+
* How a `CredentialsRef` becomes actual secrets.
|
|
35
|
+
*
|
|
36
|
+
* The default treats the ref as an environment-variable PREFIX, so `STRIPE_BLUECYPRESS` reads
|
|
37
|
+
* `STRIPE_BLUECYPRESS_API_KEY` and `STRIPE_BLUECYPRESS_WEBHOOK_SECRET`. Prefixed rather than a single
|
|
38
|
+
* variable because one deployment routinely holds several configured accounts — a live one and a test
|
|
39
|
+
* one at minimum — and they need separate keys.
|
|
40
|
+
*/
|
|
41
|
+
export declare class BaseSecretResolver {
|
|
42
|
+
/**
|
|
43
|
+
* Returns whatever it can find. An EMPTY result is not an error here: a Manual provider has no
|
|
44
|
+
* credentials at all, and the driver that needs a key is the one that should complain about its
|
|
45
|
+
* absence, with the context to say what it was trying to do.
|
|
46
|
+
*/
|
|
47
|
+
Resolve(_credentialsRef: string | null | undefined): Promise<PaymentCredentials>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The default: `CredentialsRef` is an environment-variable PREFIX.
|
|
51
|
+
*
|
|
52
|
+
* `STRIPE_BLUECYPRESS` reads `STRIPE_BLUECYPRESS_API_KEY` and `STRIPE_BLUECYPRESS_WEBHOOK_SECRET`.
|
|
53
|
+
* Prefixed rather than a single variable because one deployment routinely holds several configured
|
|
54
|
+
* accounts — a live one and a test one at minimum — and they need separate keys.
|
|
55
|
+
*
|
|
56
|
+
* Registered with NO key, so it is the fallback a deployment replaces by registering its own against
|
|
57
|
+
* `BaseSecretResolver` — the same shape as `DefaultPriceResolver`.
|
|
58
|
+
*/
|
|
59
|
+
export declare class EnvironmentSecretResolver extends BaseSecretResolver {
|
|
60
|
+
Resolve(credentialsRef: string | null | undefined): Promise<PaymentCredentials>;
|
|
61
|
+
}
|
|
62
|
+
/** Tree-shaking anchor for the secret seam. */
|
|
63
|
+
export declare function LoadEnvironmentSecretResolver(): void;
|
|
64
|
+
export declare class PaymentProviderNotConfiguredError extends Error {
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Load a configured provider row and hand back a ready driver.
|
|
68
|
+
*
|
|
69
|
+
* Throws rather than returning null. Every caller of this is about to move money and cannot proceed
|
|
70
|
+
* without a driver, so there is no branch for a caller to take — and an exception carries the reason to
|
|
71
|
+
* a log, where a returned null would arrive as "cannot read property of undefined" somewhere else.
|
|
72
|
+
*/
|
|
73
|
+
export declare function ResolvePaymentProvider(paymentProviderID: string, provider: IMetadataProvider, user: UserInfo): Promise<BasePaymentProvider>;
|
|
74
|
+
/** The configured account, with its type's code and capability flags flattened onto it. */
|
|
75
|
+
export declare function LoadPaymentProviderConfig(paymentProviderID: string, provider: IMetadataProvider, user: UserInfo): Promise<PaymentProviderConfig>;
|
|
76
|
+
/**
|
|
77
|
+
* Instantiate the driver for a config and attach everything it needs.
|
|
78
|
+
*
|
|
79
|
+
* Exposed separately from `ResolvePaymentProvider` so the webhook route can build a driver from a config
|
|
80
|
+
* it already loaded — the route resolves a provider from the URL before it has a payment in hand, and
|
|
81
|
+
* making it re-query would mean two lookups for one request.
|
|
82
|
+
*/
|
|
83
|
+
export declare function BuildPaymentProvider(config: PaymentProviderConfig, provider: IMetadataProvider, user: UserInfo): Promise<BasePaymentProvider>;
|
|
84
|
+
//# sourceMappingURL=PaymentProviderResolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PaymentProviderResolver.d.ts","sourceRoot":"","sources":["../src/PaymentProviderResolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,iBAAiB,EAA6B,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAG9F,OAAO,EACH,mBAAmB,EACnB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC7B,MAAM,0BAA0B,CAAC;AAYlC;;;;;;;GAOG;AACH,qBAAa,kBAAkB;IAC3B;;;;OAIG;IACU,OAAO,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAGhG;AAED;;;;;;;;;GASG;AACH,qBACa,yBAA0B,SAAQ,kBAAkB;IACvC,OAAO,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAWxG;AAED,+CAA+C;AAC/C,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD;AAED,qBAAa,iCAAkC,SAAQ,KAAK;CAAG;AAE/D;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CACxC,iBAAiB,EAAE,MAAM,EACzB,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,QAAQ,GACf,OAAO,CAAC,mBAAmB,CAAC,CAG9B;AAED,2FAA2F;AAC3F,wBAAsB,yBAAyB,CAC3C,iBAAiB,EAAE,MAAM,EACzB,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,QAAQ,GACf,OAAO,CAAC,qBAAqB,CAAC,CAyDhC;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACtC,MAAM,EAAE,qBAAqB,EAC7B,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,QAAQ,GACf,OAAO,CAAC,mBAAmB,CAAC,CAiC9B"}
|