@openlup/platform 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CHANGELOG.md +161 -231
  2. package/README.md +182 -8
  3. package/dist/composition/composeBundle.d.ts +6 -0
  4. package/dist/composition/composeBundle.js +2 -0
  5. package/dist/composition/index.js +1 -0
  6. package/dist/payments/adapterConformance.d.ts +8 -0
  7. package/dist/payments/adapterConformance.js +109 -0
  8. package/dist/payments/bindings.d.ts +3 -0
  9. package/dist/payments/bindings.js +17 -0
  10. package/dist/payments/checkoutActivePaymentActionResolver.d.ts +7 -0
  11. package/dist/payments/checkoutActivePaymentActionResolver.js +77 -0
  12. package/dist/payments/checkoutRecoveryPaymentResolver.d.ts +8 -0
  13. package/dist/payments/checkoutRecoveryPaymentResolver.js +87 -0
  14. package/dist/payments/contracts/adapter.d.ts +113 -0
  15. package/dist/payments/contracts/adapter.js +1 -0
  16. package/dist/payments/contracts/conformance.d.ts +155 -0
  17. package/dist/payments/contracts/conformance.js +1 -0
  18. package/dist/payments/contracts/evidence.d.ts +157 -0
  19. package/dist/payments/contracts/evidence.js +1 -0
  20. package/dist/payments/contracts/index.d.ts +10 -0
  21. package/dist/payments/contracts/index.js +1 -0
  22. package/dist/payments/contracts/preparedAttempt.d.ts +69 -0
  23. package/dist/payments/contracts/preparedAttempt.js +1 -0
  24. package/dist/payments/contracts/reconciliation.d.ts +303 -0
  25. package/dist/payments/contracts/reconciliation.js +1 -0
  26. package/dist/payments/contracts/repair.d.ts +92 -0
  27. package/dist/payments/contracts/repair.js +1 -0
  28. package/dist/payments/contracts/truth.d.ts +92 -0
  29. package/dist/payments/contracts/truth.js +1 -0
  30. package/dist/payments/eventConformanceRunner.d.ts +12 -0
  31. package/dist/payments/eventConformanceRunner.js +125 -0
  32. package/dist/payments/index.d.ts +16 -0
  33. package/dist/payments/index.js +16 -0
  34. package/dist/payments/paymentAttemptSilenceWindow.d.ts +10 -0
  35. package/dist/payments/paymentAttemptSilenceWindow.js +19 -0
  36. package/dist/payments/paymentMethodLifecycle.d.ts +11 -0
  37. package/dist/payments/paymentMethodLifecycle.js +37 -0
  38. package/dist/payments/paymentProviderReconciliationAbsenceReopen.d.ts +7 -0
  39. package/dist/payments/paymentProviderReconciliationAbsenceReopen.js +100 -0
  40. package/dist/payments/paymentProviderReconciliationEvidence.d.ts +36 -0
  41. package/dist/payments/paymentProviderReconciliationEvidence.js +91 -0
  42. package/dist/payments/paymentProviderReconciliationWorker.d.ts +4 -0
  43. package/dist/payments/paymentProviderReconciliationWorker.js +245 -0
  44. package/dist/payments/paymentTruth.d.ts +8 -0
  45. package/dist/payments/paymentTruth.js +125 -0
  46. package/dist/payments/paymentVerifyNowHandler.d.ts +27 -0
  47. package/dist/payments/paymentVerifyNowHandler.js +70 -0
  48. package/dist/payments/paymentVerifyNowService.d.ts +23 -0
  49. package/dist/payments/paymentVerifyNowService.js +93 -0
  50. package/dist/payments/paymentWebhookPostProcessing.d.ts +22 -0
  51. package/dist/payments/paymentWebhookPostProcessing.js +15 -0
  52. package/dist/payments/postgresPaymentTruth.d.ts +10 -0
  53. package/dist/payments/postgresPaymentTruth.js +126 -0
  54. package/dist/payments/preparedAttempt.d.ts +6 -0
  55. package/dist/payments/preparedAttempt.js +70 -0
  56. package/dist/payments/preparedAttemptFinalization.d.ts +5 -0
  57. package/dist/payments/preparedAttemptFinalization.js +12 -0
  58. package/dist/payments/schemaCapabilities.d.ts +10 -0
  59. package/dist/payments/schemaCapabilities.js +10 -0
  60. package/dist/payments/sql.d.ts +20 -0
  61. package/dist/payments/sql.js +6 -0
  62. package/dist/payments/stripe/classifyDecline.d.ts +46 -0
  63. package/dist/payments/stripe/classifyDecline.js +58 -0
  64. package/dist/payments/stripe/declineFailureHints.d.ts +46 -0
  65. package/dist/payments/stripe/declineFailureHints.js +87 -0
  66. package/dist/payments/stripe/donorContracts.d.ts +120 -0
  67. package/dist/payments/stripe/donorContracts.js +1 -0
  68. package/dist/payments/stripe/executionDecline.d.ts +53 -0
  69. package/dist/payments/stripe/executionDecline.js +107 -0
  70. package/dist/payments/stripe/index.d.ts +16 -0
  71. package/dist/payments/stripe/index.js +239 -0
  72. package/dist/payments/stripe/intentMapper.d.ts +26 -0
  73. package/dist/payments/stripe/intentMapper.js +51 -0
  74. package/dist/payments/stripe/methodFacts.d.ts +10 -0
  75. package/dist/payments/stripe/methodFacts.js +12 -0
  76. package/dist/payments/stripe/paymentFailureEvidence.d.ts +26 -0
  77. package/dist/payments/stripe/paymentFailureEvidence.js +33 -0
  78. package/dist/payments/stripe/stripeFailureEvidence.d.ts +41 -0
  79. package/dist/payments/stripe/stripeFailureEvidence.js +82 -0
  80. package/dist/payments/stripe/stripeIntentConfirmationEvidence.d.ts +28 -0
  81. package/dist/payments/stripe/stripeIntentConfirmationEvidence.js +32 -0
  82. package/dist/payments/stripe/stripePaymentReconciliationProvider.d.ts +35 -0
  83. package/dist/payments/stripe/stripePaymentReconciliationProvider.js +222 -0
  84. package/dist/payments/stripe/stripeRefundEvidence.d.ts +8 -0
  85. package/dist/payments/stripe/stripeRefundEvidence.js +17 -0
  86. package/dist/payments/stripe/stripeSandboxPaymentExecutionAdapter.d.ts +45 -0
  87. package/dist/payments/stripe/stripeSandboxPaymentExecutionAdapter.js +220 -0
  88. package/dist/payments/stripe/stripeWebhookNormalizer.d.ts +14 -0
  89. package/dist/payments/stripe/stripeWebhookNormalizer.js +243 -0
  90. package/dist/payments/stripe/webhookSignature.d.ts +14 -0
  91. package/dist/payments/stripe/webhookSignature.js +43 -0
  92. package/dist/payments/tpay/blikRecurringMapper.d.ts +37 -0
  93. package/dist/payments/tpay/blikRecurringMapper.js +24 -0
  94. package/dist/payments/tpay/capabilities.d.ts +3 -0
  95. package/dist/payments/tpay/capabilities.js +18 -0
  96. package/dist/payments/tpay/classifyDecline.d.ts +46 -0
  97. package/dist/payments/tpay/classifyDecline.js +58 -0
  98. package/dist/payments/tpay/declineFailureHints.d.ts +72 -0
  99. package/dist/payments/tpay/declineFailureHints.js +37 -0
  100. package/dist/payments/tpay/donorContracts.d.ts +140 -0
  101. package/dist/payments/tpay/donorContracts.js +1 -0
  102. package/dist/payments/tpay/index.d.ts +43 -0
  103. package/dist/payments/tpay/index.js +264 -0
  104. package/dist/payments/tpay/paymentFailureEvidence.d.ts +26 -0
  105. package/dist/payments/tpay/paymentFailureEvidence.js +33 -0
  106. package/dist/payments/tpay/preparedProviderAttempt.d.ts +22 -0
  107. package/dist/payments/tpay/preparedProviderAttempt.js +24 -0
  108. package/dist/payments/tpay/providerAttemptFailureDiagnostic.d.ts +3 -0
  109. package/dist/payments/tpay/providerAttemptFailureDiagnostic.js +95 -0
  110. package/dist/payments/tpay/tpayCreateTransactionInput.d.ts +25 -0
  111. package/dist/payments/tpay/tpayCreateTransactionInput.js +128 -0
  112. package/dist/payments/tpay/tpayDeclineEvidence.d.ts +126 -0
  113. package/dist/payments/tpay/tpayDeclineEvidence.js +177 -0
  114. package/dist/payments/tpay/tpayDispatchFailure.d.ts +60 -0
  115. package/dist/payments/tpay/tpayDispatchFailure.js +121 -0
  116. package/dist/payments/tpay/tpayFailureEvidence.d.ts +20 -0
  117. package/dist/payments/tpay/tpayFailureEvidence.js +61 -0
  118. package/dist/payments/tpay/tpayHttpClient.d.ts +52 -0
  119. package/dist/payments/tpay/tpayHttpClient.js +185 -0
  120. package/dist/payments/tpay/tpayHttpFailure.d.ts +57 -0
  121. package/dist/payments/tpay/tpayHttpFailure.js +143 -0
  122. package/dist/payments/tpay/tpayPaymentExecutionAdapter.d.ts +20 -0
  123. package/dist/payments/tpay/tpayPaymentExecutionAdapter.js +249 -0
  124. package/dist/payments/tpay/tpayPaymentReconciliationProvider.d.ts +5 -0
  125. package/dist/payments/tpay/tpayPaymentReconciliationProvider.js +244 -0
  126. package/dist/payments/tpay/tpayTransactionPayload.d.ts +120 -0
  127. package/dist/payments/tpay/tpayTransactionPayload.js +96 -0
  128. package/dist/payments/tpay/wallClockTime.d.ts +31 -0
  129. package/dist/payments/tpay/wallClockTime.js +57 -0
  130. package/dist/payments/tpay/webhookJwsVerifier.d.ts +17 -0
  131. package/dist/payments/tpay/webhookJwsVerifier.js +51 -0
  132. package/dist/payments/tpay/webhookNormalizer.d.ts +54 -0
  133. package/dist/payments/tpay/webhookNormalizer.js +200 -0
  134. package/dist/payments/verifiedEvidenceProjection.d.ts +12 -0
  135. package/dist/payments/verifiedEvidenceProjection.js +166 -0
  136. package/dist/payments/verifiedIngress.d.ts +119 -0
  137. package/dist/payments/verifiedIngress.js +220 -0
  138. package/dist/shipping/bindings.d.ts +5 -0
  139. package/dist/shipping/bindings.js +12 -2
  140. package/dist/shipping/index.d.ts +14 -0
  141. package/dist/shipping/index.js +5 -0
  142. package/package.json +22 -2
  143. package/release-gates.json +64 -3
  144. package/sql/20260810180000_payment_settlement_rail.sql +574 -0
  145. package/sql/20260815150000_payment_truth_reconciliation.sql +479 -0
  146. package/sql/20260826100000_late_payment_shifts_the_subscription_cycle.sql +215 -0
  147. package/sql/20261021200000_shipping_flat_rate_seed.sql +1 -0
  148. package/src/composition/composeBundle.ts +9 -0
  149. package/src/composition/index.ts +1 -0
  150. package/src/payments/adapterConformance.ts +118 -0
  151. package/src/payments/bindings.ts +17 -0
  152. package/src/payments/checkoutActivePaymentActionResolver.ts +97 -0
  153. package/src/payments/checkoutRecoveryPaymentResolver.ts +108 -0
  154. package/src/payments/contracts/adapter.ts +96 -0
  155. package/src/payments/contracts/conformance.ts +171 -0
  156. package/src/payments/contracts/evidence.ts +142 -0
  157. package/src/payments/contracts/index.ts +54 -0
  158. package/src/payments/contracts/preparedAttempt.ts +86 -0
  159. package/src/payments/contracts/reconciliation.ts +296 -0
  160. package/src/payments/contracts/repair.ts +95 -0
  161. package/src/payments/contracts/truth.ts +108 -0
  162. package/src/payments/eventConformanceRunner.ts +149 -0
  163. package/src/payments/index.ts +21 -0
  164. package/src/payments/paymentAttemptSilenceWindow.ts +26 -0
  165. package/src/payments/paymentMethodLifecycle.ts +50 -0
  166. package/src/payments/paymentProviderReconciliationAbsenceReopen.ts +130 -0
  167. package/src/payments/paymentProviderReconciliationEvidence.ts +117 -0
  168. package/src/payments/paymentProviderReconciliationWorker.ts +291 -0
  169. package/src/payments/paymentTruth.ts +131 -0
  170. package/src/payments/paymentVerifyNowHandler.ts +94 -0
  171. package/src/payments/paymentVerifyNowService.ts +125 -0
  172. package/src/payments/paymentWebhookPostProcessing.ts +36 -0
  173. package/src/payments/postgresPaymentTruth.ts +149 -0
  174. package/src/payments/preparedAttempt.ts +90 -0
  175. package/src/payments/preparedAttemptFinalization.ts +13 -0
  176. package/src/payments/schemaCapabilities.ts +10 -0
  177. package/src/payments/sql.ts +6 -0
  178. package/src/payments/stripe/classifyDecline.ts +62 -0
  179. package/src/payments/stripe/declineFailureHints.ts +89 -0
  180. package/src/payments/stripe/donorContracts.ts +87 -0
  181. package/src/payments/stripe/executionDecline.ts +126 -0
  182. package/src/payments/stripe/index.ts +235 -0
  183. package/src/payments/stripe/intentMapper.ts +58 -0
  184. package/src/payments/stripe/methodFacts.ts +21 -0
  185. package/src/payments/stripe/paymentFailureEvidence.ts +58 -0
  186. package/src/payments/stripe/stripeFailureEvidence.ts +115 -0
  187. package/src/payments/stripe/stripeIntentConfirmationEvidence.ts +34 -0
  188. package/src/payments/stripe/stripePaymentReconciliationProvider.ts +263 -0
  189. package/src/payments/stripe/stripeRefundEvidence.ts +19 -0
  190. package/src/payments/stripe/stripeSandboxPaymentExecutionAdapter.ts +283 -0
  191. package/src/payments/stripe/stripeWebhookNormalizer.ts +277 -0
  192. package/src/payments/stripe/webhookSignature.ts +55 -0
  193. package/src/payments/tpay/blikRecurringMapper.ts +42 -0
  194. package/src/payments/tpay/capabilities.ts +21 -0
  195. package/src/payments/tpay/classifyDecline.ts +62 -0
  196. package/src/payments/tpay/declineFailureHints.ts +90 -0
  197. package/src/payments/tpay/donorContracts.ts +104 -0
  198. package/src/payments/tpay/index.ts +257 -0
  199. package/src/payments/tpay/paymentFailureEvidence.ts +58 -0
  200. package/src/payments/tpay/preparedProviderAttempt.ts +38 -0
  201. package/src/payments/tpay/providerAttemptFailureDiagnostic.ts +99 -0
  202. package/src/payments/tpay/tpayCreateTransactionInput.ts +158 -0
  203. package/src/payments/tpay/tpayDeclineEvidence.ts +233 -0
  204. package/src/payments/tpay/tpayDispatchFailure.ts +186 -0
  205. package/src/payments/tpay/tpayFailureEvidence.ts +75 -0
  206. package/src/payments/tpay/tpayHttpClient.ts +290 -0
  207. package/src/payments/tpay/tpayHttpFailure.ts +186 -0
  208. package/src/payments/tpay/tpayPaymentExecutionAdapter.ts +297 -0
  209. package/src/payments/tpay/tpayPaymentReconciliationProvider.ts +285 -0
  210. package/src/payments/tpay/tpayTransactionPayload.ts +207 -0
  211. package/src/payments/tpay/wallClockTime.ts +57 -0
  212. package/src/payments/tpay/webhookJwsVerifier.ts +82 -0
  213. package/src/payments/tpay/webhookNormalizer.ts +279 -0
  214. package/src/payments/verifiedEvidenceProjection.ts +179 -0
  215. package/src/payments/verifiedIngress.ts +317 -0
  216. package/src/shipping/bindings.ts +18 -2
  217. package/src/shipping/index.ts +6 -0
@@ -0,0 +1,6 @@
1
+ /** Sparse projections of canonical units, never an install chain. */
2
+ export const PAYMENT_MIGRATIONS = [
3
+ { id: "20260810180000_payment_settlement_rail", catalogPath: "db/platform/migrations/20260810180000_payment_settlement_rail.sql", position: 10, sha256: "af153aadedc09c07b25f69300d335d8928526cad126646c1b9bf25341f1c2d0e", url: new URL("../../sql/20260810180000_payment_settlement_rail.sql", import.meta.url) },
4
+ { id: "20260815150000_payment_truth_reconciliation", catalogPath: "db/platform/migrations/20260815150000_payment_truth_reconciliation.sql", position: 35, sha256: "dd38a601edc2f43f259eefc2cdb9af14fd37fe792a7f82e58c351bd373a88905", url: new URL("../../sql/20260815150000_payment_truth_reconciliation.sql", import.meta.url) },
5
+ { id: "20260826100000_late_payment_shifts_the_subscription_cycle", catalogPath: "db/platform/migrations/20260826100000_late_payment_shifts_the_subscription_cycle.sql", position: 56, sha256: "992540b0f49713d604859926624b3bef09d5075b68b604f069dcbd688328e452", url: new URL("../../sql/20260826100000_late_payment_shifts_the_subscription_cycle.sql", import.meta.url) },
6
+ ];
@@ -0,0 +1,46 @@
1
+ import { type PaymentExecutionProviderDecline, type PaymentFailureClassification } from "@openlup/core/payment";
2
+ export declare const DECLINE_FAILURE_REASONS: {
3
+ /** The payer's bank cannot register a reusable mandate — offer a card instead. */
4
+ readonly mandateUnsupported: "blik_recurring_unsupported_bank";
5
+ /** Declined for a reason the provider did not distinguish. */
6
+ readonly generic: "provider_declined";
7
+ };
8
+ /** Maps a provider refusal to the stable code persisted as `failure_reason`. */
9
+ export declare function declineFailureReason(decline: PaymentExecutionProviderDecline): string;
10
+ /**
11
+ * The one place a refused payment gets a class, for every rail that consumes a
12
+ * synchronous decline: interactive checkout, checkout recovery, and the
13
+ * off-session renewal cron.
14
+ *
15
+ * Provider-agnostic on purpose. Every code the adapter understood has already
16
+ * been translated into `adviceCode` and `neutralReasonHints` by the adapter that
17
+ * owns the vocabulary, so nothing here needs to know which provider refused.
18
+ *
19
+ * The one reading this seam adds is the mandate flag. `mandateUnsupported` is a
20
+ * neutral capability statement any adapter may set, and it is what produces the
21
+ * scheme-named failure reason below; asserting the matching hint is what turns
22
+ * that reason into `mandate_dead` instead of leaving it unclassified. Doing it
23
+ * here rather than per adapter means a second adapter setting the same flag gets
24
+ * the same class without repeating the mapping.
25
+ *
26
+ * DISPLAY-LOAD-BEARING, and NOT a cadence input. Three live readers turn the
27
+ * class this seam decides into what the payer is told: `dunningFailureClassPort`
28
+ * reads it off the case, `customerAccountActionRequiredReadModel` selects it into
29
+ * the account surface, and `paymentRecoverySetupHandler` renders it as the cause
30
+ * sentence on the recovery page. What still does NOT read it is the retry ladder:
31
+ * no cadence, retry decision or scheduling branch consults the class, and that
32
+ * wave stays gated on validating this taxonomy against the historical corpus.
33
+ *
34
+ * `failureReasonKey` overrides the reason this seam would derive itself, and is
35
+ * supplied by a rail that persists a DIFFERENT reason string with the attempt:
36
+ * the reconciliation rail writes its own adapter-prefixed key, not
37
+ * `provider_declined`. That key is the classifier's weakest evidence tier, so
38
+ * the override changes a class only where the rail's own key is mapped; what it
39
+ * always changes is `decidedBy`, and that is the point. The historical-corpus
40
+ * replay reproduces a verdict from the stored attempt row, so the key named in
41
+ * the telemetry has to be the key actually on that row rather than one this seam
42
+ * invented for itself.
43
+ */
44
+ export declare function classifyDecline(decline: PaymentExecutionProviderDecline, options?: {
45
+ failureReasonKey?: string;
46
+ }): PaymentFailureClassification;
@@ -0,0 +1,58 @@
1
+ import { classifyPaymentFailure } from "@openlup/core/payment";
2
+ export const DECLINE_FAILURE_REASONS = {
3
+ /** The payer's bank cannot register a reusable mandate — offer a card instead. */
4
+ mandateUnsupported: "blik_recurring_unsupported_bank",
5
+ /** Declined for a reason the provider did not distinguish. */
6
+ generic: "provider_declined",
7
+ };
8
+ /** Maps a provider refusal to the stable code persisted as `failure_reason`. */
9
+ export function declineFailureReason(decline) {
10
+ return decline.mandateUnsupported
11
+ ? DECLINE_FAILURE_REASONS.mandateUnsupported
12
+ : DECLINE_FAILURE_REASONS.generic;
13
+ }
14
+ /**
15
+ * The one place a refused payment gets a class, for every rail that consumes a
16
+ * synchronous decline: interactive checkout, checkout recovery, and the
17
+ * off-session renewal cron.
18
+ *
19
+ * Provider-agnostic on purpose. Every code the adapter understood has already
20
+ * been translated into `adviceCode` and `neutralReasonHints` by the adapter that
21
+ * owns the vocabulary, so nothing here needs to know which provider refused.
22
+ *
23
+ * The one reading this seam adds is the mandate flag. `mandateUnsupported` is a
24
+ * neutral capability statement any adapter may set, and it is what produces the
25
+ * scheme-named failure reason below; asserting the matching hint is what turns
26
+ * that reason into `mandate_dead` instead of leaving it unclassified. Doing it
27
+ * here rather than per adapter means a second adapter setting the same flag gets
28
+ * the same class without repeating the mapping.
29
+ *
30
+ * DISPLAY-LOAD-BEARING, and NOT a cadence input. Three live readers turn the
31
+ * class this seam decides into what the payer is told: `dunningFailureClassPort`
32
+ * reads it off the case, `customerAccountActionRequiredReadModel` selects it into
33
+ * the account surface, and `paymentRecoverySetupHandler` renders it as the cause
34
+ * sentence on the recovery page. What still does NOT read it is the retry ladder:
35
+ * no cadence, retry decision or scheduling branch consults the class, and that
36
+ * wave stays gated on validating this taxonomy against the historical corpus.
37
+ *
38
+ * `failureReasonKey` overrides the reason this seam would derive itself, and is
39
+ * supplied by a rail that persists a DIFFERENT reason string with the attempt:
40
+ * the reconciliation rail writes its own adapter-prefixed key, not
41
+ * `provider_declined`. That key is the classifier's weakest evidence tier, so
42
+ * the override changes a class only where the rail's own key is mapped; what it
43
+ * always changes is `decidedBy`, and that is the point. The historical-corpus
44
+ * replay reproduces a verdict from the stored attempt row, so the key named in
45
+ * the telemetry has to be the key actually on that row rather than one this seam
46
+ * invented for itself.
47
+ */
48
+ export function classifyDecline(decline, options = {}) {
49
+ const asserted = [...(decline.neutralReasonHints ?? [])];
50
+ if (decline.mandateUnsupported)
51
+ asserted.push("mandateUnsupported");
52
+ return classifyPaymentFailure({
53
+ adviceCode: decline.adviceCode,
54
+ declineCode: decline.declineCode,
55
+ neutralReasonHints: asserted,
56
+ failureReasonKey: options.failureReasonKey ?? declineFailureReason(decline),
57
+ });
58
+ }
@@ -0,0 +1,46 @@
1
+ import type { PaymentFailureHint } from "@openlup/core/payment";
2
+ /**
3
+ * This adapter's own translation from its opaque refusal codes to the neutral
4
+ * hints the failure taxonomy accepts.
5
+ *
6
+ * The table lives here, in the adapter that owns the codes, and never in the
7
+ * neutral kernel: the kernel classifies evidence, and this file is what turns
8
+ * one acquirer's vocabulary into that evidence.
9
+ *
10
+ * It follows the published scheme research collected for the dunning programme.
11
+ * The class these hints produce is DISPLAY-load-bearing: it reaches the payer as
12
+ * the sentence explaining why the charge could not happen. It is still not a
13
+ * cadence input (nothing in the retry ladder branches on it), so a wrong row
14
+ * here costs a wrong sentence, not a wrong charge.
15
+ *
16
+ * Precedence note: this table is consulted only when the refusal carries NO
17
+ * issuer advice code. Advice is the verdict of the party that would refuse the
18
+ * next attempt, so it outranks anything read locally.
19
+ *
20
+ * Two deliberate silences, both from the research:
21
+ * - `do_not_honor` and `generic_decline` get NO entry. They are the issuer's
22
+ * way of declining without saying why, and they arrive on refusals that are
23
+ * dead and on refusals that clear on the next attempt alike. Guessing either
24
+ * way would invent a verdict; falling through to `indeterminate` is honest.
25
+ * - `restricted_card` IS mapped, to `credentialDead`, and the caveat is
26
+ * recorded rather than hidden: the research reads it as "this instrument may
27
+ * not be used for this transaction", which the payer's bank can lift, so the
28
+ * instrument is not necessarily finished. It is mapped anyway because every
29
+ * reading of it agrees that repeating the identical charge unattended cannot
30
+ * clear it, and that is the whole content of the class.
31
+ *
32
+ * The same caveat applies to `card_not_supported`, `currency_not_supported` and
33
+ * `transaction_not_allowed`: the credential may be perfectly alive and the
34
+ * refusal still final for the charge as submitted. `credentialDead` is chosen
35
+ * for the CLASS it yields, not as a claim about the plastic.
36
+ */
37
+ export declare const DECLINE_CODE_HINT_TABLE: Readonly<Record<string, readonly PaymentFailureHint[]>>;
38
+ /**
39
+ * The hints this adapter asserts for one refusal code. Own-property lookup, so
40
+ * an inherited key (`constructor`, `toString`) is not a mapping.
41
+ *
42
+ * The table itself is exported too, for a reader that holds a stored code and
43
+ * has no adapter in the loop: the historical-corpus replay hands it straight to
44
+ * the kernel's `declineCodeHints` option.
45
+ */
46
+ export declare function declineCodeHints(declineCode: string | undefined): readonly PaymentFailureHint[];
@@ -0,0 +1,87 @@
1
+ /**
2
+ * This adapter's own translation from its opaque refusal codes to the neutral
3
+ * hints the failure taxonomy accepts.
4
+ *
5
+ * The table lives here, in the adapter that owns the codes, and never in the
6
+ * neutral kernel: the kernel classifies evidence, and this file is what turns
7
+ * one acquirer's vocabulary into that evidence.
8
+ *
9
+ * It follows the published scheme research collected for the dunning programme.
10
+ * The class these hints produce is DISPLAY-load-bearing: it reaches the payer as
11
+ * the sentence explaining why the charge could not happen. It is still not a
12
+ * cadence input (nothing in the retry ladder branches on it), so a wrong row
13
+ * here costs a wrong sentence, not a wrong charge.
14
+ *
15
+ * Precedence note: this table is consulted only when the refusal carries NO
16
+ * issuer advice code. Advice is the verdict of the party that would refuse the
17
+ * next attempt, so it outranks anything read locally.
18
+ *
19
+ * Two deliberate silences, both from the research:
20
+ * - `do_not_honor` and `generic_decline` get NO entry. They are the issuer's
21
+ * way of declining without saying why, and they arrive on refusals that are
22
+ * dead and on refusals that clear on the next attempt alike. Guessing either
23
+ * way would invent a verdict; falling through to `indeterminate` is honest.
24
+ * - `restricted_card` IS mapped, to `credentialDead`, and the caveat is
25
+ * recorded rather than hidden: the research reads it as "this instrument may
26
+ * not be used for this transaction", which the payer's bank can lift, so the
27
+ * instrument is not necessarily finished. It is mapped anyway because every
28
+ * reading of it agrees that repeating the identical charge unattended cannot
29
+ * clear it, and that is the whole content of the class.
30
+ *
31
+ * The same caveat applies to `card_not_supported`, `currency_not_supported` and
32
+ * `transaction_not_allowed`: the credential may be perfectly alive and the
33
+ * refusal still final for the charge as submitted. `credentialDead` is chosen
34
+ * for the CLASS it yields, not as a claim about the plastic.
35
+ */
36
+ export const DECLINE_CODE_HINT_TABLE = Object.freeze({
37
+ approve_with_id: Object.freeze(["transient"]),
38
+ authentication_required: Object.freeze(["authenticationRequired"]),
39
+ card_not_supported: Object.freeze(["credentialDead"]),
40
+ card_velocity_exceeded: Object.freeze(["limitExceeded"]),
41
+ currency_not_supported: Object.freeze(["credentialDead"]),
42
+ expired_card: Object.freeze(["credentialDead"]),
43
+ fraudulent: Object.freeze(["credentialDead"]),
44
+ highest_risk_level: Object.freeze(["credentialDead"]),
45
+ incorrect_address: Object.freeze(["dataInvalid"]),
46
+ incorrect_cvc: Object.freeze(["dataInvalid"]),
47
+ incorrect_number: Object.freeze(["dataInvalid"]),
48
+ incorrect_zip: Object.freeze(["dataInvalid"]),
49
+ insufficient_funds: Object.freeze(["transient"]),
50
+ invalid_account: Object.freeze(["credentialDead"]),
51
+ invalid_amount: Object.freeze(["dataInvalid"]),
52
+ invalid_cvc: Object.freeze(["dataInvalid"]),
53
+ invalid_expiry_month: Object.freeze(["dataInvalid"]),
54
+ invalid_expiry_year: Object.freeze(["dataInvalid"]),
55
+ invalid_number: Object.freeze(["dataInvalid"]),
56
+ issuer_not_available: Object.freeze(["transient"]),
57
+ lost_card: Object.freeze(["credentialDead"]),
58
+ merchant_blacklist: Object.freeze(["credentialDead"]),
59
+ new_account_information_available: Object.freeze(["credentialDead"]),
60
+ pickup_card: Object.freeze(["credentialDead"]),
61
+ pin_try_exceeded: Object.freeze(["credentialDead"]),
62
+ processing_error: Object.freeze(["transient"]),
63
+ reenter_transaction: Object.freeze(["transient"]),
64
+ restricted_card: Object.freeze(["credentialDead"]),
65
+ revocation_of_all_authorizations: Object.freeze(["credentialDead"]),
66
+ revocation_of_authorization: Object.freeze(["credentialDead"]),
67
+ stolen_card: Object.freeze(["credentialDead"]),
68
+ stop_payment_order: Object.freeze(["credentialDead"]),
69
+ transaction_not_allowed: Object.freeze(["credentialDead"]),
70
+ try_again_later: Object.freeze(["transient"]),
71
+ withdrawal_count_limit_exceeded: Object.freeze(["limitExceeded"]),
72
+ });
73
+ /**
74
+ * The hints this adapter asserts for one refusal code. Own-property lookup, so
75
+ * an inherited key (`constructor`, `toString`) is not a mapping.
76
+ *
77
+ * The table itself is exported too, for a reader that holds a stored code and
78
+ * has no adapter in the loop: the historical-corpus replay hands it straight to
79
+ * the kernel's `declineCodeHints` option.
80
+ */
81
+ export function declineCodeHints(declineCode) {
82
+ if (declineCode === undefined)
83
+ return [];
84
+ return Object.prototype.hasOwnProperty.call(DECLINE_CODE_HINT_TABLE, declineCode)
85
+ ? DECLINE_CODE_HINT_TABLE[declineCode]
86
+ : [];
87
+ }
@@ -0,0 +1,120 @@
1
+ /** Private structural contracts for the unchanged execution and observation slices. */
2
+ import type { PaymentExecutionBaseInput, PaymentExecutionBaseResult } from "@openlup/core/payment";
3
+ import type { ProviderReadPurpose } from "../contracts/index.js";
4
+ export type PaymentProviderFlow = "one_time_payment" | "setup_reusable_method" | "off_session_payment";
5
+ export declare const PAYMENT_NEXT_ACTION_KINDS: readonly ["redirect", "3ds_challenge", "sca_required"];
6
+ export type PaymentNextActionKind = (typeof PAYMENT_NEXT_ACTION_KINDS)[number];
7
+ export interface PaymentExecutionInput extends PaymentExecutionBaseInput {
8
+ orderRef: string;
9
+ providerFlow?: PaymentProviderFlow;
10
+ customerRef?: string;
11
+ paymentMethodRef?: string;
12
+ saveForFutureUse?: boolean;
13
+ clientId?: string;
14
+ }
15
+ export interface PaymentExecutionResult extends PaymentExecutionBaseResult {
16
+ nextActionKind: PaymentNextActionKind | null;
17
+ clientSecret?: string | null;
18
+ customerProviderRef?: string | null;
19
+ paymentMethodRef?: string | null;
20
+ webhookExpected?: boolean;
21
+ recoveryRequired?: boolean;
22
+ }
23
+ export interface PaymentExecutionPort {
24
+ execute(input: PaymentExecutionInput): Promise<PaymentExecutionResult>;
25
+ }
26
+ export type PaymentWebhookEventType = "payment.succeeded" | "payment.failed" | "payment.refunded" | "payment.requires_action" | "payment.disputed" | "setup.succeeded" | "setup.failed" | "setup.requires_action";
27
+ export interface CanonicalPaymentEvent {
28
+ provider_event_id: string;
29
+ event_type: PaymentWebhookEventType;
30
+ payment_provider_id: string;
31
+ amount_minor?: number;
32
+ next_action_kind?: PaymentNextActionKind;
33
+ raw_payload: Record<string, unknown>;
34
+ }
35
+ export interface NormalizedProviderPaymentWebhook {
36
+ provider: string;
37
+ providerEventId: string;
38
+ eventType: PaymentWebhookEventType;
39
+ providerPaymentId: string;
40
+ paymentIntentId?: string | null;
41
+ paymentAttemptId?: string | null;
42
+ amountMinor?: number | null;
43
+ currency?: string | null;
44
+ occurredAt: string;
45
+ refundKind?: "full" | "partial" | null;
46
+ rawPayload: Record<string, unknown>;
47
+ reusableMethod?: {
48
+ clientId: string;
49
+ subscriptionId?: string | null;
50
+ providerCustomerRef?: string | null;
51
+ providerMethodRef: string;
52
+ providerMandateRef?: string | null;
53
+ methodKind: "card";
54
+ status: "active";
55
+ consentSnapshot?: Record<string, unknown>;
56
+ } | null;
57
+ }
58
+ export interface ProviderReconciliationStatus {
59
+ status: "succeeded" | "failed" | "pending" | "unknown";
60
+ providerStatus: string;
61
+ occurredAt: string | null;
62
+ failureReason: string | null;
63
+ amountMinor: number | null;
64
+ currency: string | null;
65
+ rawPayload: Record<string, unknown>;
66
+ }
67
+ export interface ClaimedPaymentAttempt {
68
+ paymentAttemptId: string;
69
+ paymentIntentId: string;
70
+ paymentId: string;
71
+ orderId: string;
72
+ subscriptionId: string | null;
73
+ subscriptionCycleId: string | null;
74
+ provider: string;
75
+ providerPaymentId: string | null;
76
+ providerAttemptId: string | null;
77
+ providerSessionId: string | null;
78
+ attemptStatus: string;
79
+ intentStatus: string;
80
+ amountMinor: number;
81
+ currency: string;
82
+ orderMode: "one_time" | "subscription_cycle";
83
+ cycleRetryAttempt: number;
84
+ cycleNextRetryAt: string | null;
85
+ localUpdatedAt: string;
86
+ }
87
+ export type ProviderRecoveryAction = {
88
+ kind: "provider_embedded";
89
+ provider: string;
90
+ clientSecret: string;
91
+ };
92
+ export interface PaymentProviderRecoveryProvider {
93
+ resolvePaymentReference(input: {
94
+ providerAttemptId: string | null;
95
+ intentProviderPaymentId: string | null;
96
+ providerSessionId: string | null;
97
+ }): string | null;
98
+ readPayment(input: {
99
+ providerPaymentId: string;
100
+ attempt?: ClaimedPaymentAttempt;
101
+ purpose?: ProviderReadPurpose;
102
+ }): Promise<ProviderReconciliationStatus>;
103
+ readRecoveryPayment(input: {
104
+ providerPaymentId: string;
105
+ attempt: ClaimedPaymentAttempt;
106
+ purpose?: ProviderReadPurpose;
107
+ }): Promise<{
108
+ status: ProviderReconciliationStatus;
109
+ identityMatches: boolean;
110
+ configuredMoneyMatches: boolean;
111
+ manualReviewRequired: boolean;
112
+ clientAction: ProviderRecoveryAction | null;
113
+ }>;
114
+ closePayment?(input: {
115
+ providerPaymentId: string;
116
+ }): Promise<ProviderReconciliationStatus>;
117
+ findPaymentByLocalIntent?(input: {
118
+ attempt: ClaimedPaymentAttempt;
119
+ }): Promise<"found" | "absent">;
120
+ }
@@ -0,0 +1 @@
1
+ export const PAYMENT_NEXT_ACTION_KINDS = ["redirect", "3ds_challenge", "sca_required"];
@@ -0,0 +1,53 @@
1
+ import { type PaymentFailureEvidence } from "./paymentFailureEvidence.js";
2
+ import type { PaymentExecutionResult } from "./donorContracts.js";
3
+ /**
4
+ * Normalization of a refusal the provider throws instead of returning.
5
+ *
6
+ * Split from the execution adapter because it is a different job: the adapter
7
+ * creates intents and reports what came back, this reads an exception and
8
+ * decides whether it is evidence of a refusal at all — the decision that keeps a
9
+ * possibly-paid charge out of the terminal `failed` path.
10
+ */
11
+ /**
12
+ * The provider's error type for an issuer refusal, spelled exactly as the SDK
13
+ * sets it. Exported so the decline branch and its proofs cannot drift apart.
14
+ */
15
+ export declare const PROVIDER_DECLINE_ERROR_TYPE = "StripeCardError";
16
+ /** What the adapter learned from a refusal: the fact itself plus its correlation handle. */
17
+ export interface ExecutionDecline {
18
+ diagnosticFailureEvidence?: PaymentFailureEvidence;
19
+ decline: NonNullable<PaymentExecutionResult["providerDecline"]>;
20
+ /** The intent the provider had already created, when the refusal names one. */
21
+ providerAttemptId: string | null;
22
+ providerStatus: string | null;
23
+ }
24
+ /** The half of an execution result a refusal determines; the adapter owns the rest. */
25
+ type DeclinedExecutionFacts = Omit<PaymentExecutionResult, "provider" | "requestPayload">;
26
+ /**
27
+ * Reads an issuer refusal out of a thrown provider error, or returns null.
28
+ *
29
+ * Deliberately narrow: only the provider's own refusal type qualifies. Codes
30
+ * alone would not — an infrastructure error can also carry a `code`, and
31
+ * treating one as a decline would apply a terminal `failed` result to a charge
32
+ * that may have gone through.
33
+ */
34
+ export declare function declineFromError(error: unknown): ExecutionDecline | null;
35
+ /**
36
+ * Turns a refusal into attempt facts, DELIBERATELY without a correlation handle.
37
+ *
38
+ * `attemptStatus` stays non-terminal by contract — only the control plane makes
39
+ * terminal transitions, and it does so from `providerDecline`.
40
+ *
41
+ * The refused intent's id is reported ONLY inside `responsePayload`, never as
42
+ * `providerAttemptId`. That is not an oversight: event ingest matches a callback
43
+ * to an attempt by `provider_attempt_id`, and the control plane's failed branch
44
+ * has no guard against applying `failed` to an already-failed intent. So a
45
+ * correlation handle would let the provider's later failure callback re-apply the
46
+ * same refusal under its own idempotency key — advancing the retry ladder a
47
+ * second rung, rewriting the failure reason, and sending the customer a second
48
+ * dunning email minutes after the first. Until that branch is idempotent
49
+ * (planned PR-0c), a refusal this rail already reported must stay uncorrelated,
50
+ * and the callback must stay inert.
51
+ */
52
+ export declare function declinedExecutionFacts(declined: ExecutionDecline): DeclinedExecutionFacts;
53
+ export {};
@@ -0,0 +1,107 @@
1
+ import { stripeFailureEvidence } from "./stripeFailureEvidence.js";
2
+ import { failureEvidencePayload } from "./paymentFailureEvidence.js";
3
+ import { declineCodeHints } from "./declineFailureHints.js";
4
+ /**
5
+ * Normalization of a refusal the provider throws instead of returning.
6
+ *
7
+ * Split from the execution adapter because it is a different job: the adapter
8
+ * creates intents and reports what came back, this reads an exception and
9
+ * decides whether it is evidence of a refusal at all — the decision that keeps a
10
+ * possibly-paid charge out of the terminal `failed` path.
11
+ */
12
+ /**
13
+ * The provider's error type for an issuer refusal, spelled exactly as the SDK
14
+ * sets it. Exported so the decline branch and its proofs cannot drift apart.
15
+ */
16
+ export const PROVIDER_DECLINE_ERROR_TYPE = "StripeCardError";
17
+ /** Used when the provider refuses without naming a code of its own. */
18
+ const UNSPECIFIED_DECLINE_CODE = "provider_declined";
19
+ /**
20
+ * Reads an issuer refusal out of a thrown provider error, or returns null.
21
+ *
22
+ * Deliberately narrow: only the provider's own refusal type qualifies. Codes
23
+ * alone would not — an infrastructure error can also carry a `code`, and
24
+ * treating one as a decline would apply a terminal `failed` result to a charge
25
+ * that may have gone through.
26
+ */
27
+ export function declineFromError(error) {
28
+ if (!error || typeof error !== "object")
29
+ return null;
30
+ const raw = error;
31
+ if (raw.type !== PROVIDER_DECLINE_ERROR_TYPE)
32
+ return null;
33
+ const intent = raw.payment_intent && typeof raw.payment_intent === "object"
34
+ ? raw.payment_intent
35
+ : null;
36
+ const declineCode = readOptionalString(raw, "decline_code");
37
+ const adviceCode = readOptionalString(raw, "advice_code");
38
+ // Translated here, where the code vocabulary is owned, so the control plane
39
+ // classifies the refusal without ever seeing an acquirer code.
40
+ const neutralReasonHints = declineCodeHints(declineCode);
41
+ const evidence = stripeFailureEvidence(raw, { source: "execution" });
42
+ return {
43
+ ...(evidence ? { diagnosticFailureEvidence: evidence } : {}),
44
+ decline: {
45
+ code: readOptionalString(raw, "code") ?? UNSPECIFIED_DECLINE_CODE,
46
+ // Reserved for a payer bank that cannot hold a reusable mandate at all.
47
+ // A refused charge on an already-registered method proves the opposite,
48
+ // so this rail never claims it.
49
+ mandateUnsupported: false,
50
+ ...(declineCode ? { declineCode } : {}),
51
+ ...(adviceCode ? { adviceCode } : {}),
52
+ ...(neutralReasonHints.length > 0 ? { neutralReasonHints } : {}),
53
+ },
54
+ providerAttemptId: intent ? readOptionalString(intent, "id") ?? null : null,
55
+ providerStatus: intent ? readOptionalString(intent, "status") ?? null : null,
56
+ };
57
+ }
58
+ /**
59
+ * Turns a refusal into attempt facts, DELIBERATELY without a correlation handle.
60
+ *
61
+ * `attemptStatus` stays non-terminal by contract — only the control plane makes
62
+ * terminal transitions, and it does so from `providerDecline`.
63
+ *
64
+ * The refused intent's id is reported ONLY inside `responsePayload`, never as
65
+ * `providerAttemptId`. That is not an oversight: event ingest matches a callback
66
+ * to an attempt by `provider_attempt_id`, and the control plane's failed branch
67
+ * has no guard against applying `failed` to an already-failed intent. So a
68
+ * correlation handle would let the provider's later failure callback re-apply the
69
+ * same refusal under its own idempotency key — advancing the retry ladder a
70
+ * second rung, rewriting the failure reason, and sending the customer a second
71
+ * dunning email minutes after the first. Until that branch is idempotent
72
+ * (planned PR-0c), a refusal this rail already reported must stay uncorrelated,
73
+ * and the callback must stay inert.
74
+ */
75
+ export function declinedExecutionFacts(declined) {
76
+ const { decline, providerAttemptId, providerStatus } = declined;
77
+ return {
78
+ providerAttemptId: null,
79
+ providerSessionId: null,
80
+ attemptStatus: "processing",
81
+ nextActionKind: null,
82
+ providerDecline: decline,
83
+ clientSecret: null,
84
+ customerProviderRef: null,
85
+ paymentMethodRef: null,
86
+ // This rail already reported the refusal synchronously; the callback that
87
+ // repeats it must not be waited for, and must not be able to land twice.
88
+ webhookExpected: false,
89
+ recoveryRequired: false,
90
+ responsePayload: {
91
+ // Forensics only. This payload is stored on the attempt but nothing joins
92
+ // on it, so an operator can still trace the refused intent by hand without
93
+ // the ingest matcher ever finding it.
94
+ providerStatus,
95
+ providerAttemptId,
96
+ providerDeclined: true,
97
+ ...failureEvidencePayload(declined.diagnosticFailureEvidence),
98
+ // Codes only, never the provider's message: that text is free-form and can
99
+ // carry payer-identifying detail, and this payload is persisted and logged.
100
+ providerErrorCodes: [decline.code, decline.declineCode, decline.adviceCode].filter((code) => Boolean(code)),
101
+ },
102
+ };
103
+ }
104
+ function readOptionalString(value, key) {
105
+ const raw = value[key];
106
+ return typeof raw === "string" && raw.length > 0 ? raw : undefined;
107
+ }
@@ -0,0 +1,16 @@
1
+ import type { PaymentAdapter, PaymentAdapterCapabilities } from "../contracts/index.js";
2
+ import { type StripeSandboxClient } from "./stripeSandboxPaymentExecutionAdapter.js";
3
+ import { type StripePaymentIntentReader } from "./stripePaymentReconciliationProvider.js";
4
+ /** Host-owned client, credentials, SDK construction and mode policy. clock returns UTC seconds. */
5
+ export interface StripePaymentAdapterConfig {
6
+ client: Omit<StripeSandboxClient, "createSetupIntent"> & Partial<Pick<StripeSandboxClient, "createSetupIntent">> & StripePaymentIntentReader;
7
+ webhookSigningSecrets: readonly string[];
8
+ mode: "live" | "test";
9
+ supportedCurrencies: readonly string[];
10
+ toleranceSeconds?: number;
11
+ clock?: () => number;
12
+ }
13
+ /** Observed protocol capabilities; account activation remains host-owned. */
14
+ export declare const stripePaymentCapabilities: Readonly<PaymentAdapterCapabilities>;
15
+ /** Explicit construction is the only activation point; importing performs no I/O. */
16
+ export declare function createStripePaymentAdapter(config: StripePaymentAdapterConfig): PaymentAdapter;