@openlup/platform 0.20.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 (210) hide show
  1. package/CHANGELOG.md +62 -231
  2. package/README.md +172 -6
  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/package.json +22 -2
  139. package/release-gates.json +64 -3
  140. package/sql/20260810180000_payment_settlement_rail.sql +574 -0
  141. package/sql/20260815150000_payment_truth_reconciliation.sql +479 -0
  142. package/sql/20260826100000_late_payment_shifts_the_subscription_cycle.sql +215 -0
  143. package/src/composition/composeBundle.ts +9 -0
  144. package/src/composition/index.ts +1 -0
  145. package/src/payments/adapterConformance.ts +118 -0
  146. package/src/payments/bindings.ts +17 -0
  147. package/src/payments/checkoutActivePaymentActionResolver.ts +97 -0
  148. package/src/payments/checkoutRecoveryPaymentResolver.ts +108 -0
  149. package/src/payments/contracts/adapter.ts +96 -0
  150. package/src/payments/contracts/conformance.ts +171 -0
  151. package/src/payments/contracts/evidence.ts +142 -0
  152. package/src/payments/contracts/index.ts +54 -0
  153. package/src/payments/contracts/preparedAttempt.ts +86 -0
  154. package/src/payments/contracts/reconciliation.ts +296 -0
  155. package/src/payments/contracts/repair.ts +95 -0
  156. package/src/payments/contracts/truth.ts +108 -0
  157. package/src/payments/eventConformanceRunner.ts +149 -0
  158. package/src/payments/index.ts +21 -0
  159. package/src/payments/paymentAttemptSilenceWindow.ts +26 -0
  160. package/src/payments/paymentMethodLifecycle.ts +50 -0
  161. package/src/payments/paymentProviderReconciliationAbsenceReopen.ts +130 -0
  162. package/src/payments/paymentProviderReconciliationEvidence.ts +117 -0
  163. package/src/payments/paymentProviderReconciliationWorker.ts +291 -0
  164. package/src/payments/paymentTruth.ts +131 -0
  165. package/src/payments/paymentVerifyNowHandler.ts +94 -0
  166. package/src/payments/paymentVerifyNowService.ts +125 -0
  167. package/src/payments/paymentWebhookPostProcessing.ts +36 -0
  168. package/src/payments/postgresPaymentTruth.ts +149 -0
  169. package/src/payments/preparedAttempt.ts +90 -0
  170. package/src/payments/preparedAttemptFinalization.ts +13 -0
  171. package/src/payments/schemaCapabilities.ts +10 -0
  172. package/src/payments/sql.ts +6 -0
  173. package/src/payments/stripe/classifyDecline.ts +62 -0
  174. package/src/payments/stripe/declineFailureHints.ts +89 -0
  175. package/src/payments/stripe/donorContracts.ts +87 -0
  176. package/src/payments/stripe/executionDecline.ts +126 -0
  177. package/src/payments/stripe/index.ts +235 -0
  178. package/src/payments/stripe/intentMapper.ts +58 -0
  179. package/src/payments/stripe/methodFacts.ts +21 -0
  180. package/src/payments/stripe/paymentFailureEvidence.ts +58 -0
  181. package/src/payments/stripe/stripeFailureEvidence.ts +115 -0
  182. package/src/payments/stripe/stripeIntentConfirmationEvidence.ts +34 -0
  183. package/src/payments/stripe/stripePaymentReconciliationProvider.ts +263 -0
  184. package/src/payments/stripe/stripeRefundEvidence.ts +19 -0
  185. package/src/payments/stripe/stripeSandboxPaymentExecutionAdapter.ts +283 -0
  186. package/src/payments/stripe/stripeWebhookNormalizer.ts +277 -0
  187. package/src/payments/stripe/webhookSignature.ts +55 -0
  188. package/src/payments/tpay/blikRecurringMapper.ts +42 -0
  189. package/src/payments/tpay/capabilities.ts +21 -0
  190. package/src/payments/tpay/classifyDecline.ts +62 -0
  191. package/src/payments/tpay/declineFailureHints.ts +90 -0
  192. package/src/payments/tpay/donorContracts.ts +104 -0
  193. package/src/payments/tpay/index.ts +257 -0
  194. package/src/payments/tpay/paymentFailureEvidence.ts +58 -0
  195. package/src/payments/tpay/preparedProviderAttempt.ts +38 -0
  196. package/src/payments/tpay/providerAttemptFailureDiagnostic.ts +99 -0
  197. package/src/payments/tpay/tpayCreateTransactionInput.ts +158 -0
  198. package/src/payments/tpay/tpayDeclineEvidence.ts +233 -0
  199. package/src/payments/tpay/tpayDispatchFailure.ts +186 -0
  200. package/src/payments/tpay/tpayFailureEvidence.ts +75 -0
  201. package/src/payments/tpay/tpayHttpClient.ts +290 -0
  202. package/src/payments/tpay/tpayHttpFailure.ts +186 -0
  203. package/src/payments/tpay/tpayPaymentExecutionAdapter.ts +297 -0
  204. package/src/payments/tpay/tpayPaymentReconciliationProvider.ts +285 -0
  205. package/src/payments/tpay/tpayTransactionPayload.ts +207 -0
  206. package/src/payments/tpay/wallClockTime.ts +57 -0
  207. package/src/payments/tpay/webhookJwsVerifier.ts +82 -0
  208. package/src/payments/tpay/webhookNormalizer.ts +279 -0
  209. package/src/payments/verifiedEvidenceProjection.ts +179 -0
  210. package/src/payments/verifiedIngress.ts +317 -0
@@ -0,0 +1,82 @@
1
+ import { isPaymentRetryAdviceCode } from "@openlup/core/payment";
2
+ import { evidenceRecord, evidenceToken, failureEvidencePayload, serializeFailureEvidence } from "./paymentFailureEvidence.js";
3
+ /** Reads only one current PaymentIntent/error, never an arbitrary historical Charge. */
4
+ export function stripeFailureEvidence(input, options) {
5
+ const object = evidenceRecord(input);
6
+ const thrown = object.type === "StripeCardError";
7
+ const intent = thrown ? evidenceRecord(object.payment_intent) : object;
8
+ const error = thrown ? object : evidenceRecord(intent.last_payment_error);
9
+ if (!thrown && !options.confirmedFailureEvent && Object.keys(error).length === 0)
10
+ return null;
11
+ const refusalVerified = thrown || options.confirmedFailureEvent === true
12
+ || (intent.status === "requires_payment_method" && Object.keys(error).length > 0
13
+ && (error.type === "card_error" || error.code === "card_declined" || Boolean(intent.latest_charge)));
14
+ const errorMethod = evidenceRecord(error.payment_method);
15
+ const currentMethod = evidenceRecord(intent.payment_method);
16
+ const errorMethodId = typeof error.payment_method === "string" ? error.payment_method : errorMethod.id;
17
+ const currentMethodId = typeof intent.payment_method === "string" ? intent.payment_method : currentMethod.id;
18
+ const conflictingMethod = Boolean(errorMethodId && currentMethodId && errorMethodId !== currentMethodId)
19
+ || Boolean(errorMethod.type && currentMethod.type && errorMethod.type !== currentMethod.type);
20
+ const actualMethod = errorMethod.type ?? currentMethod.type;
21
+ const cardContract = options.knownCardOnlyRequest === true
22
+ || (Array.isArray(intent.payment_method_types) && intent.payment_method_types.length === 1
23
+ && intent.payment_method_types[0] === "card");
24
+ const flow = evidenceRecord(intent.metadata).providerFlow;
25
+ const operation = flow === "off_session_payment" ? "stored_method_payment"
26
+ : intent.setup_future_usage === "off_session" ? "recurring_setup"
27
+ : intent.setup_future_usage === null && flow === "one_time_payment" ? "one_time_payment" : null;
28
+ const methodKind = actualMethod === "card" || actualMethod === "blik" ? actualMethod
29
+ : actualMethod == null && cardContract ? "card" : null;
30
+ const method = methodKind
31
+ ? { kind: methodKind, recoveryMethodKey: methodKind, interaction: operation === "stored_method_payment" ? "stored_instrument" : "new_instrument" } : null;
32
+ const latestCharge = typeof intent.latest_charge === "string" ? intent.latest_charge : evidenceRecord(intent.latest_charge).id;
33
+ const errorCharge = typeof error.charge === "string" ? error.charge : evidenceRecord(error.charge).id;
34
+ // A last error tied to another charge cannot explain the currently observed charge.
35
+ const conflictingEvidence = conflictingMethod || Boolean(latestCharge && errorCharge && latestCharge !== errorCharge);
36
+ return serializeFailureEvidence({
37
+ version: 1, source: options.source,
38
+ disposition: conflictingEvidence ? "unreadable" : Object.keys(error).length ? "present" : "absent",
39
+ refusalVerified: refusalVerified && !conflictingEvidence, providerPaymentId: evidenceToken(intent.id),
40
+ providerChargeId: evidenceToken(errorCharge ?? latestCharge),
41
+ code: conflictingEvidence ? null : evidenceToken(error.code),
42
+ declineCode: conflictingEvidence ? null : evidenceToken(error.decline_code),
43
+ adviceCode: conflictingEvidence ? null : evidenceToken(error.advice_code),
44
+ adviceOrigin: "provider", operation: conflictingEvidence ? null : operation, method: conflictingEvidence ? null : method,
45
+ methodSource: actualMethod != null ? "provider" : cardContract ? "execution_contract" : "unknown",
46
+ });
47
+ }
48
+ const SAFE_CAUSES = {
49
+ insufficient_funds: "insufficient_funds", expired_card: "expired_card",
50
+ incorrect_cvc: "invalid_payment_data", invalid_cvc: "invalid_payment_data",
51
+ incorrect_number: "invalid_payment_data", invalid_number: "invalid_payment_data",
52
+ invalid_expiry_month: "invalid_payment_data", invalid_expiry_year: "invalid_payment_data",
53
+ authentication_required: "authentication_required",
54
+ card_not_supported: "operation_unsupported", currency_not_supported: "operation_unsupported",
55
+ transaction_not_allowed: "operation_unsupported",
56
+ };
57
+ const RESTRICTED = new Set(["fraudulent", "highest_risk_level", "lost_card", "stolen_card", "merchant_blacklist"]);
58
+ /** UI evidence only: does not call or alter the shared failure taxonomy. */
59
+ export function normalizeStripeFailureEvidence(input) {
60
+ const evidence = serializeFailureEvidence(input);
61
+ const code = evidence.declineCode ?? evidence.code;
62
+ const restricted = code !== null && RESTRICTED.has(code);
63
+ const readable = evidence.refusalVerified && evidence.disposition === "present";
64
+ // A refused BLIK registration names its operation, not a setup cause: no code
65
+ // read here distinguishes a refused agreement from a mistyped or expired code.
66
+ const cause = readable && !restricted && code && Object.hasOwn(SAFE_CAUSES, code)
67
+ ? SAFE_CAUSES[code] : "generic_decline";
68
+ return {
69
+ refusalVerified: evidence.refusalVerified, cause,
70
+ certainty: cause === "generic_decline" ? "unknown" : "verified",
71
+ disclosure: restricted ? "restricted" : "safe", method: evidence.method, operation: evidence.operation,
72
+ advice: readable && evidence.adviceOrigin === "provider" && evidence.adviceCode !== null && isPaymentRetryAdviceCode(evidence.adviceCode)
73
+ ? { code: evidence.adviceCode, scope: evidence.method?.kind === "card" ? "instrument" : "unknown" } : null,
74
+ };
75
+ }
76
+ /** Preserve the existing webhook projection and append only failure diagnostics. */
77
+ export function stripeWebhookPayload(input, object) {
78
+ return { provider: "stripe", ...input,
79
+ ...(input.eventType === "payment_intent.payment_failed"
80
+ ? failureEvidencePayload(stripeFailureEvidence(object, { source: "webhook", confirmedFailureEvent: true })) : {}),
81
+ };
82
+ }
@@ -0,0 +1,28 @@
1
+ import type { StripeIntentLike } from "./stripeSandboxPaymentExecutionAdapter.js";
2
+ /**
3
+ * `requires_payment_method` on an intent that was never confirmed: no payment
4
+ * method attached, no charge, no payment error. The provider object exists —
5
+ * the request reached the provider and was accepted — but the payer never
6
+ * submitted an instrument, so no issuer was ever asked to approve anything.
7
+ *
8
+ * Both absences beside the status are load-bearing. A payment method attached
9
+ * but not yet confirmed means a charge may be seconds from moving; a charge or a
10
+ * payment error means an issuer HAS been asked and answered. Either presence
11
+ * makes this claim false, and the claim is used to decide what a durable row
12
+ * says about the payer's money, so it must stay narrow rather than convenient.
13
+ *
14
+ * Shared deliberately. The execution rail asserts it to record what a fresh
15
+ * intent is, and the reconciliation rail asserts it to answer a payer who is
16
+ * still standing in the checkout. Those are two readings of ONE provider state,
17
+ * and two copies of the predicate would let them drift into disagreeing about
18
+ * whether the same intent had been attempted — the kind of drift that is only
19
+ * discovered by a payer being told the wrong thing about their own payment.
20
+ *
21
+ * ⛔ Not to be widened into "the payer has gone" (abandonment). That is a
22
+ * separate verdict, passed on a payer who is no longer present, and it
23
+ * deliberately asserts fewer absences so a real refusal is never mislabelled as
24
+ * abandonment. Merging the two would put one predicate under two opposite
25
+ * pressures: this one must stay narrow so no live charge is called idle, that
26
+ * one must stay broad so no refusal is silently dropped out of dunning.
27
+ */
28
+ export declare function noIssuerWasAsked(intent: StripeIntentLike): boolean;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `requires_payment_method` on an intent that was never confirmed: no payment
3
+ * method attached, no charge, no payment error. The provider object exists —
4
+ * the request reached the provider and was accepted — but the payer never
5
+ * submitted an instrument, so no issuer was ever asked to approve anything.
6
+ *
7
+ * Both absences beside the status are load-bearing. A payment method attached
8
+ * but not yet confirmed means a charge may be seconds from moving; a charge or a
9
+ * payment error means an issuer HAS been asked and answered. Either presence
10
+ * makes this claim false, and the claim is used to decide what a durable row
11
+ * says about the payer's money, so it must stay narrow rather than convenient.
12
+ *
13
+ * Shared deliberately. The execution rail asserts it to record what a fresh
14
+ * intent is, and the reconciliation rail asserts it to answer a payer who is
15
+ * still standing in the checkout. Those are two readings of ONE provider state,
16
+ * and two copies of the predicate would let them drift into disagreeing about
17
+ * whether the same intent had been attempted — the kind of drift that is only
18
+ * discovered by a payer being told the wrong thing about their own payment.
19
+ *
20
+ * ⛔ Not to be widened into "the payer has gone" (abandonment). That is a
21
+ * separate verdict, passed on a payer who is no longer present, and it
22
+ * deliberately asserts fewer absences so a real refusal is never mislabelled as
23
+ * abandonment. Merging the two would put one predicate under two opposite
24
+ * pressures: this one must stay narrow so no live charge is called idle, that
25
+ * one must stay broad so no refusal is silently dropped out of dunning.
26
+ */
27
+ export function noIssuerWasAsked(intent) {
28
+ return intent.status === "requires_payment_method"
29
+ && !intent.payment_method
30
+ && !intent.latest_charge
31
+ && !intent.last_payment_error;
32
+ }
@@ -0,0 +1,35 @@
1
+ import type { ProviderReconciliationStatus } from "./donorContracts.js";
2
+ import type { PaymentProviderRecoveryProvider } from "./donorContracts.js";
3
+ import type { StripeIntentLike } from "./stripeSandboxPaymentExecutionAdapter.js";
4
+ export interface StripePaymentIntentReader {
5
+ retrievePaymentIntent(paymentIntentId: string): Promise<StripeIntentLike>;
6
+ cancelPaymentIntent?(paymentIntentId: string): Promise<StripeIntentLike>;
7
+ }
8
+ export interface StripePaymentIntentSearcher {
9
+ /**
10
+ * Search Stripe PaymentIntents by a metadata key/value pair (GET
11
+ * /v1/payment_intents/search). Every intent the payment-control adapter
12
+ * creates carries metadata.paymentIntentId = <local intent uuid>
13
+ * (stripeSandboxPaymentExecutionAdapter metadataFor), so this is the
14
+ * intent-correlated absence probe for prepared attempts that never recorded
15
+ * a provider payment id.
16
+ */
17
+ searchPaymentIntentsByMetadata(input: {
18
+ key: string;
19
+ value: string;
20
+ limit?: number;
21
+ }): Promise<{
22
+ ids: string[];
23
+ }>;
24
+ }
25
+ export declare function createStripePaymentReconciliationProvider(client: StripePaymentIntentReader & Partial<StripePaymentIntentSearcher>): PaymentProviderRecoveryProvider;
26
+ export declare function normalizeStripeIntent(intent: StripeIntentLike): ProviderReconciliationStatus;
27
+ /**
28
+ * The disposition of a failed intent nobody actually refused.
29
+ *
30
+ * Deliberately NOT a taxonomy class. The kernel classifies REFUSALS — it answers
31
+ * "may this instrument be charged again, and by whom" — and an absence of
32
+ * attempt is not a refusal, so an "abandonment" class would be a category error
33
+ * inside a vocabulary whose every other member describes an issuer's verdict.
34
+ */
35
+ export declare const ABANDONED_BEFORE_CONFIRMATION = "abandoned_before_confirmation";
@@ -0,0 +1,222 @@
1
+ import { failureEvidencePayload } from "./paymentFailureEvidence.js";
2
+ import { classifyDecline } from "./classifyDecline.js";
3
+ import { declineCodeHints } from "./declineFailureHints.js";
4
+ import { noIssuerWasAsked } from "./stripeIntentConfirmationEvidence.js";
5
+ export function createStripePaymentReconciliationProvider(client) {
6
+ const search = client.searchPaymentIntentsByMetadata?.bind(client);
7
+ const cancel = client.cancelPaymentIntent?.bind(client);
8
+ return {
9
+ resolvePaymentReference(input) {
10
+ return input.providerAttemptId ?? input.intentProviderPaymentId ?? input.providerSessionId;
11
+ },
12
+ async readRecoveryPayment(input) {
13
+ const intent = await client.retrievePaymentIntent(input.providerPaymentId);
14
+ let status = normalizeStripeIntent(intent);
15
+ let clientAction = null;
16
+ if (intent.status === "processing" || intent.status === "requires_capture") {
17
+ clientAction = null;
18
+ }
19
+ else if (input.purpose === "active_checkout" && noIssuerWasAsked(intent)) {
20
+ clientAction = embeddedAction(intent.client_secret);
21
+ // The status is lowered whether or not a form can be handed back. Our
22
+ // inability to reopen the card panel is a fact about US; it is not an
23
+ // issuer's refusal, and it must not be recorded as one.
24
+ status = { ...status, status: "pending" };
25
+ }
26
+ else if (intent.status === "requires_action" || intent.status === "requires_confirmation") {
27
+ clientAction = embeddedAction(intent.client_secret);
28
+ // Global reconciliation treats requires_action as terminal for
29
+ // off-session dunning. Buyer recovery is different: without a current
30
+ // client secret it is uncertain, not permission to open a new intent.
31
+ if (!clientAction || input.purpose === "active_checkout") {
32
+ status = { ...status, status: "pending" };
33
+ }
34
+ }
35
+ return {
36
+ status,
37
+ identityMatches: intent.id === input.providerPaymentId,
38
+ configuredMoneyMatches: stripeConfiguredMoneyMatches(intent, input.attempt),
39
+ manualReviewRequired: false,
40
+ clientAction,
41
+ };
42
+ },
43
+ async readPayment(input) {
44
+ const intent = await client.retrievePaymentIntent(input.providerPaymentId);
45
+ const status = normalizeStripeIntent(intent);
46
+ // Same rule as the recovery read above, on the rail the buyer's own
47
+ // "check it now" travels: an intent no issuer has been asked about is
48
+ // unsettled, not refused. Only `active_checkout` gets this reading, so
49
+ // the reconciliation worker — which names no purpose — goes on
50
+ // terminalizing abandonment exactly as before.
51
+ return input.purpose === "active_checkout" && noIssuerWasAsked(intent)
52
+ ? { ...status, status: "pending" }
53
+ : status;
54
+ },
55
+ ...(cancel
56
+ ? {
57
+ async closePayment(input) {
58
+ return normalizeStripeIntent(await cancel(input.providerPaymentId));
59
+ },
60
+ }
61
+ : {}),
62
+ ...(search
63
+ ? {
64
+ async findPaymentByLocalIntent(input) {
65
+ const result = await search({
66
+ key: "paymentIntentId",
67
+ value: input.attempt.paymentIntentId,
68
+ limit: 1,
69
+ });
70
+ return result.ids.length > 0 ? "found" : "absent";
71
+ },
72
+ }
73
+ : {}),
74
+ };
75
+ }
76
+ export function normalizeStripeIntent(intent) {
77
+ const status = normalizeStripeStatus(intent.status);
78
+ return {
79
+ status,
80
+ providerStatus: intent.status,
81
+ occurredAt: null,
82
+ failureReason: failureReason(intent),
83
+ amountMinor: readAmountMinor(intent),
84
+ currency: typeof intent.currency === "string" ? intent.currency.toUpperCase() : null,
85
+ rawPayload: {
86
+ provider: "stripe",
87
+ paymentIntentId: intent.id,
88
+ status: intent.status,
89
+ latestChargePresent: Boolean(intent.latest_charge),
90
+ nextActionType: intent.next_action?.type ?? null,
91
+ lastPaymentErrorCode: intent.last_payment_error?.code ?? null,
92
+ lastPaymentErrorType: intent.last_payment_error?.type ?? null,
93
+ ...failureCapture(intent),
94
+ ...failureEvidencePayload(intent.diagnosticFailureEvidence),
95
+ },
96
+ };
97
+ }
98
+ /**
99
+ * The disposition of a failed intent nobody actually refused.
100
+ *
101
+ * Deliberately NOT a taxonomy class. The kernel classifies REFUSALS — it answers
102
+ * "may this instrument be charged again, and by whom" — and an absence of
103
+ * attempt is not a refusal, so an "abandonment" class would be a category error
104
+ * inside a vocabulary whose every other member describes an issuer's verdict.
105
+ */
106
+ export const ABANDONED_BEFORE_CONFIRMATION = "abandoned_before_confirmation";
107
+ /**
108
+ * `requires_payment_method` on an intent that was never confirmed: no
109
+ * `last_payment_error`, no charge. The buyer closed the tab; no issuer was ever
110
+ * asked. Six of the fifteen production failure rows in the historical corpus are
111
+ * this state.
112
+ *
113
+ * Both absences are required before the claim is made. Presence of either is
114
+ * evidence an attempt happened, and mislabelling a real decline as abandonment
115
+ * would silently drop it out of dunning — the more expensive of the two errors.
116
+ */
117
+ function abandonedBeforeConfirmation(intent) {
118
+ return intent.status === "requires_payment_method"
119
+ && !intent.last_payment_error
120
+ && !intent.latest_charge;
121
+ }
122
+ function embeddedAction(clientSecret) {
123
+ return typeof clientSecret === "string" && clientSecret.length > 0
124
+ ? { kind: "provider_embedded", provider: "stripe", clientSecret }
125
+ : null;
126
+ }
127
+ /**
128
+ * The refusal evidence this rail can state, and the class it produces.
129
+ *
130
+ * This rail births most production failure rows, and it used to keep only
131
+ * `last_payment_error.code` — the coarse bucket ("card_declined") — while
132
+ * dropping `decline_code`, the fine code the adapter's hint table is written
133
+ * against, and `advice_code`, the issuer's own retry verdict. With both gone the
134
+ * taxonomy had nothing to read here, so the programme's classification telemetry
135
+ * stayed empty no matter how many refusals arrived.
136
+ *
137
+ * Classification goes through the shared `classifyDecline` seam, the same one
138
+ * the synchronous decline path uses, so there is exactly one place a refusal
139
+ * becomes a class. Only neutral evidence crosses: this provider's own code
140
+ * vocabulary is translated here, by this adapter's table, and travels no further.
141
+ *
142
+ * The class does not stop here. Since wave 3i-b it reaches the attempt and the
143
+ * dunning case through the terminal write's two classification parameters, and
144
+ * from there it becomes the cause sentence the payer is shown. It remains no
145
+ * part of any retry cadence.
146
+ */
147
+ function failureCapture(intent) {
148
+ if (normalizeStripeStatus(intent.status) !== "failed")
149
+ return {};
150
+ if (abandonedBeforeConfirmation(intent)) {
151
+ return { nonDeclineDisposition: ABANDONED_BEFORE_CONFIRMATION };
152
+ }
153
+ const declineCode = readCode(intent.last_payment_error?.decline_code);
154
+ const adviceCode = readCode(intent.last_payment_error?.advice_code);
155
+ const neutralReasonHints = declineCodeHints(declineCode);
156
+ const classification = classifyDecline({
157
+ // Correlation handle only — the kernel never reads it. The intent's own
158
+ // status is the provider-native fact when the refusal named no code.
159
+ code: readCode(intent.last_payment_error?.code) ?? intent.status,
160
+ // A read of an existing intent says nothing about whether the payer's bank
161
+ // can hold a reusable mandate, so this rail never claims it.
162
+ mandateUnsupported: false,
163
+ ...(declineCode ? { declineCode } : {}),
164
+ ...(adviceCode ? { adviceCode } : {}),
165
+ ...(neutralReasonHints.length > 0 ? { neutralReasonHints } : {}),
166
+ }, { failureReasonKey: failureReason(intent) ?? undefined });
167
+ return {
168
+ declineCode: declineCode ?? null,
169
+ adviceCode: adviceCode ?? null,
170
+ neutralReasonHints,
171
+ failureClass: classification.failureClass,
172
+ failureClassDecidedBy: classification.decidedBy,
173
+ };
174
+ }
175
+ function readCode(value) {
176
+ return typeof value === "string" && value.length > 0 ? value : undefined;
177
+ }
178
+ function stripeConfiguredMoneyMatches(intent, attempt) {
179
+ return typeof intent.amount === "number"
180
+ && Number.isFinite(intent.amount)
181
+ && intent.amount === attempt.amountMinor
182
+ && typeof intent.currency === "string"
183
+ && intent.currency.toUpperCase() === attempt.currency.toUpperCase();
184
+ }
185
+ function normalizeStripeStatus(status) {
186
+ if (status === "succeeded")
187
+ return "succeeded";
188
+ if (status === "requires_action" ||
189
+ status === "requires_confirmation" ||
190
+ status === "requires_payment_method" ||
191
+ status === "canceled") {
192
+ return "failed";
193
+ }
194
+ if (status === "processing" || status === "requires_capture")
195
+ return "pending";
196
+ return "unknown";
197
+ }
198
+ function failureReason(intent) {
199
+ const status = normalizeStripeStatus(intent.status);
200
+ if (status !== "failed")
201
+ return null;
202
+ return intent.last_payment_error?.code
203
+ ? `stripe_${intent.last_payment_error.code}`
204
+ : `stripe_${intent.status}`;
205
+ }
206
+ function readAmountMinor(intent) {
207
+ // amount_received is settlement truth only after success. For an incomplete,
208
+ // requires-payment-method, cancelled, or requires-capture intent Stripe
209
+ // legitimately returns 0 while `amount` still holds the configured amount.
210
+ // Comparing that zero to the local order created a false mismatch and made
211
+ // reconciliation 502-loop on an ordinary failed payment.
212
+ if (intent.status === "succeeded" && typeof intent.amount_received === "number" && Number.isFinite(intent.amount_received)) {
213
+ return intent.amount_received;
214
+ }
215
+ if (typeof intent.amount === "number" && Number.isFinite(intent.amount)) {
216
+ return intent.amount;
217
+ }
218
+ if (typeof intent.amount_received === "number" && Number.isFinite(intent.amount_received)) {
219
+ return intent.amount_received;
220
+ }
221
+ return null;
222
+ }
@@ -0,0 +1,8 @@
1
+ /** A Charge observation is cumulative; its amount never replaces payment amount. */
2
+ export declare function stripeRefundEvidence(eventType: string, charge: Record<string, unknown>): {
3
+ refundKind: "full" | "partial" | null;
4
+ snapshot: {
5
+ chargeId: string;
6
+ amountRefundedMinor: number;
7
+ } | null;
8
+ };
@@ -0,0 +1,17 @@
1
+ /** A Charge observation is cumulative; its amount never replaces payment amount. */
2
+ export function stripeRefundEvidence(eventType, charge) {
3
+ if (eventType !== "charge.refunded")
4
+ return { refundKind: null, snapshot: null };
5
+ const amount = charge.amount;
6
+ const refunded = charge.amount_refunded;
7
+ const chargeId = charge.id;
8
+ if (typeof chargeId !== "string" || !chargeId.trim()
9
+ || typeof amount !== "number" || !Number.isSafeInteger(amount) || amount <= 0
10
+ || typeof refunded !== "number" || !Number.isSafeInteger(refunded) || refunded <= 0 || refunded > amount) {
11
+ return { refundKind: null, snapshot: null };
12
+ }
13
+ return {
14
+ refundKind: refunded === amount ? "full" : "partial",
15
+ snapshot: { chargeId, amountRefundedMinor: refunded },
16
+ };
17
+ }
@@ -0,0 +1,45 @@
1
+ import type { PaymentExecutionPort } from "./donorContracts.js";
2
+ import type { CanonicalPaymentEvent, PaymentProviderFlow } from "./donorContracts.js";
3
+ /** The one place this adapter spells its own provider identity. */
4
+ export declare const PROVIDER_KIND: "stripe";
5
+ import type { StripeIntentLike } from "./stripeFailureEvidence.js";
6
+ export type { StripeIntentLike } from "./stripeFailureEvidence.js";
7
+ export interface StripeSandboxClient {
8
+ createPaymentIntent(input: {
9
+ amount: number;
10
+ currency: string;
11
+ customer?: string;
12
+ paymentMethod?: string;
13
+ offSession?: boolean;
14
+ setupFutureUsage?: "off_session";
15
+ metadata: Record<string, string>;
16
+ }, options: {
17
+ idempotencyKey: string;
18
+ }): Promise<StripeIntentLike>;
19
+ createSetupIntent(input: {
20
+ customer?: string;
21
+ paymentMethod?: string;
22
+ usage: "off_session";
23
+ metadata: Record<string, string>;
24
+ }, options: {
25
+ idempotencyKey: string;
26
+ }): Promise<StripeIntentLike>;
27
+ /**
28
+ * Ensure-or-create a provider Customer for the client. Required before a
29
+ * `setup_future_usage='off_session'` PaymentIntent (the provider rejects it
30
+ * without a customer). Idempotent via the idempotency key.
31
+ */
32
+ ensureCustomer?(input: {
33
+ clientId: string;
34
+ metadata: Record<string, string>;
35
+ }, options: {
36
+ idempotencyKey: string;
37
+ }): Promise<string>;
38
+ retrievePaymentIntent?(paymentIntentId: string): Promise<StripeIntentLike>;
39
+ }
40
+ export interface StripeSandboxAdapterOptions {
41
+ client: StripeSandboxClient;
42
+ defaultFlow?: Extract<PaymentProviderFlow, "one_time_payment" | "setup_reusable_method" | "off_session_payment">;
43
+ }
44
+ export declare function createStripeSandboxPaymentExecutionAdapter({ client, defaultFlow, }: StripeSandboxAdapterOptions): PaymentExecutionPort;
45
+ export declare function parseStripePaymentWebhookEvent(rawPayload: Record<string, unknown>): CanonicalPaymentEvent;