@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,239 @@
1
+ import { createHash } from "node:crypto";
2
+ import { classifyPaymentFailure, isPaymentRetryAdviceCode } from "@openlup/core/payment";
3
+ import { createStripeSandboxPaymentExecutionAdapter } from "./stripeSandboxPaymentExecutionAdapter.js";
4
+ import { createStripePaymentReconciliationProvider } from "./stripePaymentReconciliationProvider.js";
5
+ import { normalizeStripeWebhookEvent } from "./stripeWebhookNormalizer.js";
6
+ import { verifyStripeSignature } from "./webhookSignature.js";
7
+ import { declineCodeHints } from "./declineFailureHints.js";
8
+ /** Observed protocol capabilities; account activation remains host-owned. */
9
+ export const stripePaymentCapabilities = Object.freeze({
10
+ providerKind: "stripe",
11
+ captureFlows: Object.freeze([Object.freeze({ kind: "card_payment", handoff: "client-confirmed", methodFamilies: Object.freeze(["card", "wallet"]) })]),
12
+ requiresStoredMandateEvidence: false,
13
+ assessMandate: () => ({ chargeable: true }),
14
+ unattendedChargeFlow: "off_session_payment",
15
+ payerContext: Object.freeze({ requiresPayerBlock: false, customerRefFallsBackToContactEmail: false }),
16
+ methodHealth: Object.freeze({ requiresCustomerRef: true, requiredMethodKind: null, requiresPayerContact: false }),
17
+ terminalOutcomeReporting: Object.freeze({ kind: "status_field", silenceBecomesSuspectAfterMinutes: 360 }),
18
+ mandateUpsertIsSubscriptionScoped: false,
19
+ /** declared by the host through the adapter config */
20
+ supportedCurrencies: Object.freeze([]),
21
+ unattended: "supported", readback: "supported", close: "unsupported", exactAbsence: "unsupported",
22
+ refundObservation: "supported", refundCreate: "unsupported", authorizationCaptureSplit: "unsupported",
23
+ });
24
+ const refusal = (reason) => ({ kind: "refusal", reason });
25
+ /** Explicit construction is the only activation point; importing performs no I/O. */
26
+ export function createStripePaymentAdapter(config) {
27
+ if (config.mode !== "live" && config.mode !== "test")
28
+ throw new Error("Invalid payment mode");
29
+ const declaredCurrencies = config.supportedCurrencies;
30
+ if (!Array.isArray(declaredCurrencies) || declaredCurrencies.length === 0
31
+ || ![...declaredCurrencies].every(currency => typeof currency === "string" && currency.length === 3 && /^[A-Z]{3}$/u.test(currency))) {
32
+ throw new Error("Invalid supported currencies");
33
+ }
34
+ const supportedCurrencies = Object.freeze([...declaredCurrencies]);
35
+ const tolerance = config.toleranceSeconds ?? 300;
36
+ if (!Number.isFinite(tolerance) || tolerance < 0)
37
+ throw new Error("Invalid signature tolerance");
38
+ const secrets = [...config.webhookSigningSecrets];
39
+ const now = config.clock ?? (() => Math.floor(Date.now() / 1000));
40
+ const execution = createStripeSandboxPaymentExecutionAdapter({ client: {
41
+ createPaymentIntent: config.client.createPaymentIntent.bind(config.client),
42
+ createSetupIntent: inputSetup,
43
+ ensureCustomer: config.client.ensureCustomer?.bind(config.client),
44
+ } });
45
+ function inputSetup(...args) {
46
+ if (!config.client.createSetupIntent)
47
+ throw new Error("Method setup unsupported by injected client");
48
+ return config.client.createSetupIntent(...args);
49
+ }
50
+ const reader = createStripePaymentReconciliationProvider(config.client);
51
+ const validateInput = input => {
52
+ if (input.mode !== config.mode || !["initial", "recovery", "renewal-unattended"].includes(input.purpose)
53
+ || !input.attemptRef || !input.intentRef || !input.fingerprint || !input.providerIdempotencyKey
54
+ || !Number.isSafeInteger(input.money.amountMinor) || input.money.amountMinor <= 0
55
+ || !supportedCurrencies.includes(input.money.currency)
56
+ || input.customerAuthorization !== undefined
57
+ || (input.purpose === "renewal-unattended" && !input.storedMethod?.reference)) {
58
+ throw new Error("Unsupported payment input");
59
+ }
60
+ };
61
+ const read = async (input) => {
62
+ const base = { identity: input.identity, expectedMoney: input.expectedMoney, purpose: input.purpose };
63
+ if (input.identity.provider !== "stripe" || !input.identity.providerPaymentRef)
64
+ return { ...base, kind: "unsupported" };
65
+ try {
66
+ // Bind the response to the requested remote object before translating status.
67
+ const intent = await config.client.retrievePaymentIntent(input.identity.providerPaymentRef);
68
+ if (intent.id !== input.identity.providerPaymentRef || intent.amount !== input.expectedMoney.amountMinor
69
+ || intent.currency?.toUpperCase() !== input.expectedMoney.currency.toUpperCase()) {
70
+ return { ...base, kind: "unknown", reason: "indeterminate" };
71
+ }
72
+ const status = await createStripePaymentReconciliationProvider({ retrievePaymentIntent: async () => intent })
73
+ .readPayment({ providerPaymentId: input.identity.providerPaymentRef, purpose: input.purpose });
74
+ return readback(base, status);
75
+ }
76
+ catch {
77
+ return { ...base, kind: "unknown", reason: "indeterminate" };
78
+ }
79
+ };
80
+ return {
81
+ capabilities: { ...stripePaymentCapabilities, supportedCurrencies, close: config.client.cancelPaymentIntent ? "supported" : "unsupported" },
82
+ attemptPort: {
83
+ validateInput,
84
+ async execute(input) {
85
+ validateInput(input);
86
+ const result = await execution.execute({
87
+ idempotencyKey: input.attemptRef, providerIdempotencyKey: input.providerIdempotencyKey,
88
+ providerRequestFingerprint: input.fingerprint, paymentIntentId: input.intentRef,
89
+ amountMinor: input.money.amountMinor, currency: input.money.currency,
90
+ mode: input.purpose === "renewal-unattended" ? "subscription_cycle" : "one_time",
91
+ orderRef: input.intentRef,
92
+ providerFlow: input.purpose === "renewal-unattended" ? "off_session_payment" : "one_time_payment",
93
+ paymentMethodRef: input.storedMethod?.reference,
94
+ });
95
+ return {
96
+ outcome: result.providerDecline
97
+ ? { kind: "decline", refusal: neutralDecline(result.providerDecline) }
98
+ : { kind: "attempt", status: result.attemptStatus },
99
+ // A synchronous issuer decline is not proof of a pre-dispatch refusal.
100
+ dispatch: result.providerAttemptId ? { kind: "dispatched", providerPaymentRef: result.providerAttemptId } : { kind: "unknown" },
101
+ ...(result.clientSecret ? { handoff: { kind: "client-confirmed", payload: result.clientSecret } } : {}),
102
+ diagnostics: { webhookExpected: result.webhookExpected === true, recoveryRequired: result.recoveryRequired === true },
103
+ };
104
+ },
105
+ },
106
+ verifyPort: {
107
+ async verifyAndNormalize(input) {
108
+ const header = Object.entries(input.headers).find(([key]) => key.toLowerCase() === "stripe-signature")?.[1];
109
+ if (typeof header !== "string")
110
+ return refusal("signature");
111
+ const parts = Object.fromEntries(header.split(",").map(part => part.split("=")).filter(([key, value]) => key && value));
112
+ const timestamp = Number(parts.t);
113
+ const current = now();
114
+ if (!parts.t || !parts.v1 || !Number.isFinite(timestamp) || !Number.isFinite(current))
115
+ return refusal("signature");
116
+ // The preserved verifier supplies HMAC and comparison; this layer owns configurable freshness.
117
+ if (Math.abs(current - timestamp) > tolerance)
118
+ return refusal("stale");
119
+ const rawBody = Buffer.from(input.rawBody).toString("utf8");
120
+ if (!Buffer.from(rawBody).equals(Buffer.from(input.rawBody)))
121
+ return refusal("malformed");
122
+ if (!verifyStripeSignature({ rawBody, signatureHeader: header, endpointSecret: secrets, now: () => timestamp }))
123
+ return refusal("signature");
124
+ try {
125
+ const raw = JSON.parse(rawBody);
126
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
127
+ return refusal("malformed");
128
+ const event = raw;
129
+ if (typeof event.livemode === "boolean" && event.livemode !== (config.mode === "live"))
130
+ return refusal("unsupported-event");
131
+ return mapVerifiedStripeEvent(event);
132
+ }
133
+ catch {
134
+ return refusal("malformed");
135
+ }
136
+ },
137
+ },
138
+ readbackPort: {
139
+ readPayment: read,
140
+ ...(reader.closePayment ? { close: async (input) => {
141
+ const base = { identity: input.identity, expectedMoney: input.expectedMoney, purpose: input.purpose };
142
+ if (input.identity.provider !== "stripe" || !input.identity.providerPaymentRef)
143
+ return { ...base, kind: "unsupported" };
144
+ try {
145
+ // Check configured money and object identity before a destructive provider operation.
146
+ const before = await read(input);
147
+ if (before.kind === "unknown" || before.kind === "unsupported")
148
+ return before;
149
+ await reader.closePayment({ providerPaymentId: input.identity.providerPaymentRef });
150
+ return read(input);
151
+ }
152
+ catch {
153
+ return { ...base, kind: "unknown", reason: "indeterminate" };
154
+ }
155
+ } } : {}),
156
+ },
157
+ };
158
+ }
159
+ function neutralDecline(decline) {
160
+ const hints = decline.neutralReasonHints ?? [];
161
+ const classified = classifyPaymentFailure({ adviceCode: decline.adviceCode, neutralReasonHints: hints });
162
+ return { code: classified.failureClass, hints,
163
+ ...(decline.adviceCode && isPaymentRetryAdviceCode(decline.adviceCode) ? { adviceCode: decline.adviceCode } : {}) };
164
+ }
165
+ function readback(base, status) {
166
+ if (status.status === "unknown" || status.amountMinor === null || !Number.isSafeInteger(status.amountMinor) || !status.currency) {
167
+ return { ...base, kind: "unknown", reason: "indeterminate" };
168
+ }
169
+ return { ...base, kind: status.status, observedMoney: { amountMinor: status.amountMinor, currency: status.currency }, providerOccurredAt: status.occurredAt };
170
+ }
171
+ /** Preserve donor money classifications in the neutral evidence union. */
172
+ function mapStripeOutcome(eventType) {
173
+ switch (eventType) {
174
+ case "payment.succeeded": return "succeeded";
175
+ case "payment.failed": return "failed";
176
+ case "payment.refunded": return "refunded";
177
+ case "payment.requires_action": return "pending";
178
+ case "payment.disputed": return "disputed";
179
+ default: return null;
180
+ }
181
+ }
182
+ function mapVerifiedStripeEvent(raw) {
183
+ let normalized;
184
+ try {
185
+ normalized = normalizeStripeWebhookEvent(raw);
186
+ }
187
+ catch (error) {
188
+ return refusal(error instanceof Error && error.message.startsWith("Unsupported Stripe webhook event:") ? "unsupported-event" : "malformed");
189
+ }
190
+ const object = raw.data.object;
191
+ const occurred = typeof raw.created === "number" && Number.isFinite(raw.created)
192
+ ? normalized.occurredAt : typeof raw.created === "string" && Number.isFinite(Date.parse(raw.created)) ? normalized.occurredAt : null;
193
+ const metadata = object.metadata && typeof object.metadata === "object" ? object.metadata : {};
194
+ const intentRef = typeof metadata.paymentIntentId === "string" ? metadata.paymentIntentId : null;
195
+ const identity = { provider: "stripe", providerEventId: normalized.providerEventId,
196
+ providerOccurredAt: occurred };
197
+ let result;
198
+ if (normalized.methodLifecycle || normalized.eventType.startsWith("setup.")) {
199
+ const lifecycle = normalized.methodLifecycle;
200
+ const method = lifecycle?.providerMethodRef ?? (typeof object.payment_method === "string" ? object.payment_method
201
+ : object.payment_method && typeof object.payment_method === "object" ? object.payment_method.id : null);
202
+ if (typeof method !== "string" || !method)
203
+ return refusal("unsupported-event");
204
+ const outcomes = {
205
+ method_registered: "registered", method_updated: "updated", method_revoked: "revoked", method_expired: "expired", method_suspended: "suspended",
206
+ "setup.succeeded": "registered", "setup.failed": "failed", "setup.requires_action": "requires-action",
207
+ };
208
+ const outcome = outcomes[lifecycle?.kind ?? normalized.eventType];
209
+ if (!outcome)
210
+ return refusal("unsupported-event");
211
+ result = { ...identity, kind: "method", outcome, methodRef: method,
212
+ setupRef: typeof raw.type === "string" && raw.type.startsWith("setup_intent.") ? normalized.providerPaymentId : null, intentRef,
213
+ // Missing provider time must not turn the donor's epoch fallback into a lifecycle fact.
214
+ ...(lifecycle && occurred !== null ? { methodLifecycle: lifecycle } : {}) };
215
+ }
216
+ else {
217
+ const outcome = mapStripeOutcome(normalized.eventType);
218
+ if (!outcome)
219
+ return refusal("unsupported-event");
220
+ const money = Number.isSafeInteger(normalized.amountMinor) && normalized.amountMinor > 0 && normalized.currency
221
+ ? { amountMinor: normalized.amountMinor, currency: normalized.currency } : null;
222
+ const snapshot = normalized.rawPayload.stripeRefundSnapshot;
223
+ if (outcome === "refunded" && (!snapshot || !money))
224
+ return refusal("malformed");
225
+ result = { ...identity, kind: "money", providerPaymentRef: normalized.providerPaymentId, intentRef, attemptRef: null, outcome, money,
226
+ ...(snapshot && money ? { refund: { kind: "cumulative-snapshot", operationId: snapshot.chargeId,
227
+ originalPaymentMoney: money, refundedMoney: { amountMinor: snapshot.amountRefundedMinor, currency: money.currency } } } : {}),
228
+ ...(outcome === "failed" && object.last_payment_error && typeof object.last_payment_error === "object"
229
+ ? { refusal: neutralDecline({ code: "provider_declined", mandateUnsupported: false,
230
+ neutralReasonHints: declineCodeHints(object.last_payment_error.decline_code),
231
+ adviceCode: typeof object.last_payment_error.advice_code === "string"
232
+ ? object.last_payment_error.advice_code : undefined }) } : {}),
233
+ };
234
+ }
235
+ // The donor normalizer constructs this filtered carrier, never the raw body.
236
+ // Keep its reader dialect and provenance outside normalized event identity.
237
+ return { ...result, fingerprint: createHash("sha256").update(JSON.stringify(result)).digest("hex"),
238
+ providerAuditPayload: normalized.rawPayload };
239
+ }
@@ -0,0 +1,26 @@
1
+ import { type StripeIntentLike } from "./stripeFailureEvidence.js";
2
+ type Reference = string | {
3
+ id: string;
4
+ } | null;
5
+ interface StripeIntentShape {
6
+ id: string;
7
+ status: string;
8
+ customer?: Reference;
9
+ payment_method?: Reference;
10
+ latest_charge?: Reference;
11
+ client_secret?: string | null;
12
+ next_action?: {
13
+ type?: string | null;
14
+ } | null;
15
+ amount?: number;
16
+ amount_received?: number;
17
+ currency?: string;
18
+ last_payment_error?: {
19
+ code?: string | null;
20
+ message?: string | null;
21
+ type?: string | null;
22
+ } | null;
23
+ }
24
+ export declare function mapStripeIntent(intent: StripeIntentShape, knownCardOnlyRequest?: boolean, source?: "execution" | "readback"): StripeIntentLike;
25
+ export declare function mapSetupIntent(intent: StripeIntentShape): StripeIntentLike;
26
+ export {};
@@ -0,0 +1,51 @@
1
+ import { stripeFailureEvidence } from "./stripeFailureEvidence.js";
2
+ export function mapStripeIntent(intent, knownCardOnlyRequest = false, source = "readback") {
3
+ const customer = typeof intent.customer === "string" ? intent.customer : intent.customer?.id ?? null;
4
+ const paymentMethod = typeof intent.payment_method === "string"
5
+ ? intent.payment_method
6
+ : intent.payment_method?.id ?? null;
7
+ const latestCharge = typeof intent.latest_charge === "string"
8
+ ? intent.latest_charge
9
+ : intent.latest_charge?.id ?? null;
10
+ const mapped = {
11
+ id: intent.id,
12
+ status: intent.status,
13
+ client_secret: intent.client_secret,
14
+ customer,
15
+ payment_method: paymentMethod,
16
+ latest_charge: latestCharge,
17
+ next_action: intent.next_action ? { type: intent.next_action.type ?? null } : null,
18
+ };
19
+ if (typeof intent.amount === "number")
20
+ mapped.amount = intent.amount;
21
+ if (typeof intent.amount_received === "number")
22
+ mapped.amount_received = intent.amount_received;
23
+ if (typeof intent.currency === "string")
24
+ mapped.currency = intent.currency;
25
+ if (intent.last_payment_error) {
26
+ mapped.last_payment_error = {
27
+ code: intent.last_payment_error.code ?? null,
28
+ message: intent.last_payment_error.message ?? null,
29
+ type: intent.last_payment_error.type ?? null,
30
+ };
31
+ }
32
+ const evidence = stripeFailureEvidence(intent, { source, knownCardOnlyRequest });
33
+ if (evidence)
34
+ mapped.diagnosticFailureEvidence = evidence;
35
+ return mapped;
36
+ }
37
+ export function mapSetupIntent(intent) {
38
+ const customer = typeof intent.customer === "string" ? intent.customer : intent.customer?.id ?? null;
39
+ const paymentMethod = typeof intent.payment_method === "string"
40
+ ? intent.payment_method
41
+ : intent.payment_method?.id ?? null;
42
+ return {
43
+ id: intent.id,
44
+ status: intent.status,
45
+ client_secret: intent.client_secret,
46
+ customer,
47
+ payment_method: paymentMethod,
48
+ latest_charge: null,
49
+ next_action: intent.next_action ? { type: intent.next_action.type ?? null } : null,
50
+ };
51
+ }
@@ -0,0 +1,10 @@
1
+ import type { PaymentMethodReplacementFacts } from "@openlup/core/payment";
2
+ import type { NormalizedProviderPaymentWebhook } from "./donorContracts.js";
3
+ import type { PaymentMethodLifecycleEvent } from "@openlup/core/payment";
4
+ export interface ProviderWebhookWithMethodLifecycle extends NormalizedProviderPaymentWebhook {
5
+ methodLifecycle?: PaymentMethodLifecycleEvent | null;
6
+ }
7
+ export declare const METHOD_SCHEME_SNAPSHOT_KEY = "methodScheme";
8
+ export declare const METHOD_LAST_DIGITS_SNAPSHOT_KEY = "methodLastDigits";
9
+ export declare const METHOD_EXPIRES_AT_SNAPSHOT_KEY = "methodExpiresAt";
10
+ export declare function methodFactSnapshotKeys(facts: PaymentMethodReplacementFacts | null | undefined): Record<string, unknown>;
@@ -0,0 +1,12 @@
1
+ export const METHOD_SCHEME_SNAPSHOT_KEY = "methodScheme";
2
+ export const METHOD_LAST_DIGITS_SNAPSHOT_KEY = "methodLastDigits";
3
+ export const METHOD_EXPIRES_AT_SNAPSHOT_KEY = "methodExpiresAt";
4
+ export function methodFactSnapshotKeys(facts) {
5
+ if (!facts)
6
+ return {};
7
+ return {
8
+ ...(facts.schemeLabel ? { [METHOD_SCHEME_SNAPSHOT_KEY]: facts.schemeLabel } : {}),
9
+ ...(facts.lastDigits ? { [METHOD_LAST_DIGITS_SNAPSHOT_KEY]: facts.lastDigits } : {}),
10
+ ...(facts.expiresAt ? { [METHOD_EXPIRES_AT_SNAPSHOT_KEY]: facts.expiresAt } : {}),
11
+ };
12
+ }
@@ -0,0 +1,26 @@
1
+ import type { PaymentRecoveryMethod, PaymentRecoveryOperation } from "@openlup/core/payment";
2
+ export type FailureObservationSource = "execution" | "webhook" | "readback";
3
+ export type FailureObservationDisposition = "present" | "absent" | "unreadable" | "read_failed" | "read_timeout";
4
+ /** Protected provider facts, not a customer message or a retry-policy input. */
5
+ export interface PaymentFailureEvidence {
6
+ version: 1;
7
+ source: FailureObservationSource;
8
+ disposition: FailureObservationDisposition;
9
+ refusalVerified: boolean;
10
+ providerPaymentId: string | null;
11
+ providerChargeId: string | null;
12
+ code: string | null;
13
+ declineCode: string | null;
14
+ adviceCode: string | null;
15
+ adviceOrigin: "provider" | "inferred" | "unknown";
16
+ operation: PaymentRecoveryOperation | null;
17
+ method: PaymentRecoveryMethod | null;
18
+ methodSource: "provider" | "execution_contract" | "unknown";
19
+ }
20
+ /** Bounded machine tokens only. Free-form response fields never cross this seam. */
21
+ export declare function evidenceToken(value: unknown): string | null;
22
+ export declare function evidenceRecord(value: unknown): Record<string, unknown>;
23
+ /** Explicit construction prevents accidental payload expansion by object spreading. */
24
+ export declare function serializeFailureEvidence(input: PaymentFailureEvidence): PaymentFailureEvidence;
25
+ /** Persist beside legacy fields; never merge the diagnostic codes into them. */
26
+ export declare function failureEvidencePayload(evidence: PaymentFailureEvidence | null | undefined): Record<string, unknown>;
@@ -0,0 +1,33 @@
1
+ /** Bounded machine tokens only. Free-form response fields never cross this seam. */
2
+ export function evidenceToken(value) {
3
+ return typeof value === "string" && /^[a-zA-Z0-9_:.-]{1,96}$/.test(value) ? value : null;
4
+ }
5
+ export function evidenceRecord(value) {
6
+ return value !== null && typeof value === "object" && !Array.isArray(value)
7
+ ? value : {};
8
+ }
9
+ /** Explicit construction prevents accidental payload expansion by object spreading. */
10
+ export function serializeFailureEvidence(input) {
11
+ const method = input.method;
12
+ const kind = evidenceToken(method?.kind);
13
+ const recoveryMethodKey = evidenceToken(method?.recoveryMethodKey);
14
+ return {
15
+ version: 1,
16
+ source: input.source,
17
+ disposition: input.disposition,
18
+ refusalVerified: input.refusalVerified,
19
+ providerPaymentId: evidenceToken(input.providerPaymentId),
20
+ providerChargeId: evidenceToken(input.providerChargeId),
21
+ code: evidenceToken(input.code),
22
+ declineCode: evidenceToken(input.declineCode),
23
+ adviceCode: evidenceToken(input.adviceCode),
24
+ adviceOrigin: input.adviceOrigin,
25
+ operation: input.operation,
26
+ method: kind && recoveryMethodKey && method ? { kind, recoveryMethodKey, interaction: method.interaction } : null,
27
+ methodSource: kind && recoveryMethodKey ? input.methodSource : "unknown",
28
+ };
29
+ }
30
+ /** Persist beside legacy fields; never merge the diagnostic codes into them. */
31
+ export function failureEvidencePayload(evidence) {
32
+ return evidence ? { failureEvidence: serializeFailureEvidence(evidence) } : {};
33
+ }
@@ -0,0 +1,41 @@
1
+ import { type PaymentRecoveryEvidence } from "@openlup/core/payment";
2
+ import { type FailureObservationSource, type PaymentFailureEvidence } from "./paymentFailureEvidence.js";
3
+ export interface StripeIntentLike {
4
+ id: string;
5
+ status: string;
6
+ amount?: number | null;
7
+ amount_received?: number | null;
8
+ currency?: string | null;
9
+ client_secret?: string | null;
10
+ customer?: string | null;
11
+ payment_method?: string | null;
12
+ latest_charge?: string | null;
13
+ next_action?: {
14
+ type?: string | null;
15
+ } | null;
16
+ last_payment_error?: {
17
+ code?: string | null;
18
+ message?: string | null;
19
+ type?: string | null;
20
+ decline_code?: string | null;
21
+ advice_code?: string | null;
22
+ } | null;
23
+ /** Kept separate: populating legacy fine fields would change renewal classification. */
24
+ diagnosticFailureEvidence?: PaymentFailureEvidence;
25
+ }
26
+ /** Reads only one current PaymentIntent/error, never an arbitrary historical Charge. */
27
+ export declare function stripeFailureEvidence(input: unknown, options: {
28
+ source: FailureObservationSource;
29
+ confirmedFailureEvent?: boolean;
30
+ knownCardOnlyRequest?: boolean;
31
+ }): PaymentFailureEvidence | null;
32
+ /** UI evidence only: does not call or alter the shared failure taxonomy. */
33
+ export declare function normalizeStripeFailureEvidence(input: PaymentFailureEvidence): PaymentRecoveryEvidence;
34
+ /** Preserve the existing webhook projection and append only failure diagnostics. */
35
+ export declare function stripeWebhookPayload(input: {
36
+ eventId: string;
37
+ eventType: string;
38
+ providerPaymentId: string;
39
+ amountMinor: number | null;
40
+ currency: string | null;
41
+ }, object: unknown): Record<string, unknown>;
@@ -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";