@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,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;
@@ -0,0 +1,220 @@
1
+ import { failureEvidencePayload } from "./paymentFailureEvidence.js";
2
+ import { declineFromError, declinedExecutionFacts } from "./executionDecline.js";
3
+ import { noIssuerWasAsked } from "./stripeIntentConfirmationEvidence.js";
4
+ /** The one place this adapter spells its own provider identity. */
5
+ export const PROVIDER_KIND = "stripe";
6
+ export function createStripeSandboxPaymentExecutionAdapter({ client, defaultFlow = "one_time_payment", }) {
7
+ return {
8
+ async execute(input) {
9
+ const flow = stripeFlow(input.providerFlow ?? defaultFlow);
10
+ // A reusable-mandate PaymentIntent (setup_future_usage) requires a Stripe
11
+ // Customer. Ensure-or-create one for the client when none was passed.
12
+ let customerRef = input.customerRef;
13
+ if (!customerRef && input.saveForFutureUse && input.clientId && client.ensureCustomer) {
14
+ customerRef = await client.ensureCustomer({ clientId: input.clientId, metadata: { clientId: input.clientId } },
15
+ // A provider attempt is per click; a Stripe Customer is per OpenLup
16
+ // client. A stable key avoids orphan Customers when checkout recovery
17
+ // is reloaded before the first method webhook stores the reference.
18
+ { idempotencyKey: `openlup:stripe:customer:${input.clientId}` });
19
+ }
20
+ const created = flow === "setup_reusable_method"
21
+ ? await client.createSetupIntent({
22
+ customer: customerRef,
23
+ paymentMethod: input.paymentMethodRef,
24
+ usage: "off_session",
25
+ metadata: metadataFor(input, flow),
26
+ }, { idempotencyKey: input.providerIdempotencyKey })
27
+ : await client.createPaymentIntent({
28
+ amount: input.amountMinor,
29
+ currency: input.currency.toLowerCase(),
30
+ customer: customerRef,
31
+ paymentMethod: input.paymentMethodRef,
32
+ offSession: flow === "off_session_payment" ? true : undefined,
33
+ setupFutureUsage: input.saveForFutureUse ? "off_session" : undefined,
34
+ metadata: metadataFor(input, flow),
35
+ }, { idempotencyKey: input.providerIdempotencyKey }).catch((error) => {
36
+ // An off-session charge is confirmed inside this call, so the payer's
37
+ // issuer can refuse it here and nowhere else. That refusal is a FACT,
38
+ // not an ambiguous call: rethrowing it makes the renewal orchestrator
39
+ // record an indeterminate outcome, which applies no result, opens no
40
+ // dunning case and tells the customer nothing. Every other error class
41
+ // — network, auth, config, rate limit — keeps throwing, because after
42
+ // those the charge may still have been accepted and only the
43
+ // fail-closed path is safe.
44
+ const decline = flow === "off_session_payment" ? declineFromError(error) : null;
45
+ if (!decline)
46
+ throw error;
47
+ return decline;
48
+ });
49
+ if ("decline" in created)
50
+ return { ...executionResultBase(input, flow), ...declinedExecutionFacts(created) };
51
+ const intent = created;
52
+ return {
53
+ ...executionResultBase(input, flow),
54
+ providerAttemptId: intent.id,
55
+ providerSessionId: intent.id,
56
+ attemptStatus: attemptStatusFromStripe(intent),
57
+ nextActionKind: nextActionKindFromStripe(intent),
58
+ // `clientSecret` is the single canonical exit for the FE to confirm with
59
+ // the provider's browser SDK. It MUST NOT be persisted or duplicated
60
+ // into `responsePayload` (the latter is logged); the consumer reads it
61
+ // once from the top-level result and discards.
62
+ clientSecret: intent.client_secret ?? null,
63
+ customerProviderRef: intent.customer ?? null,
64
+ paymentMethodRef: intent.payment_method ?? null,
65
+ webhookExpected: true,
66
+ recoveryRequired: intent.status === "requires_action" && flow === "off_session_payment",
67
+ responsePayload: {
68
+ providerStatus: intent.status,
69
+ providerAttemptId: intent.id,
70
+ providerSessionIdPresent: Boolean(intent.client_secret),
71
+ latestChargePresent: Boolean(intent.latest_charge),
72
+ customerRefPresent: Boolean(intent.customer),
73
+ paymentMethodRefPresent: Boolean(intent.payment_method),
74
+ ...failureEvidencePayload(intent.diagnosticFailureEvidence),
75
+ },
76
+ };
77
+ },
78
+ };
79
+ }
80
+ export function parseStripePaymentWebhookEvent(rawPayload) {
81
+ const eventId = readString(rawPayload, "id");
82
+ const eventType = readString(rawPayload, "type");
83
+ const data = readObject(rawPayload, "data");
84
+ const object = readObject(data, "object");
85
+ const providerPaymentId = readString(object, "id");
86
+ const amount = readOptionalNumber(object, "amount_received") ?? readOptionalNumber(object, "amount");
87
+ const currency = readOptionalString(object, "currency")?.toUpperCase();
88
+ return {
89
+ provider_event_id: eventId,
90
+ event_type: canonicalStripeEventType(eventType, object),
91
+ payment_provider_id: providerPaymentId,
92
+ amount_minor: amount,
93
+ next_action_kind: nextActionKindFromStripe({
94
+ status: readOptionalString(object, "status") ?? "",
95
+ next_action: readOptionalObject(object, "next_action"),
96
+ }) ?? undefined,
97
+ raw_payload: {
98
+ provider: PROVIDER_KIND,
99
+ eventId,
100
+ eventType,
101
+ providerPaymentId,
102
+ amountMinor: amount,
103
+ currency,
104
+ },
105
+ };
106
+ }
107
+ /** The facts every result of this adapter carries, whatever the provider answered. */
108
+ function executionResultBase(input, flow) {
109
+ return {
110
+ provider: PROVIDER_KIND,
111
+ requestPayload: {
112
+ providerIdempotencyKey: input.providerIdempotencyKey,
113
+ providerRequestFingerprint: input.providerRequestFingerprint,
114
+ providerFlow: flow,
115
+ amountMinor: input.amountMinor,
116
+ currency: input.currency,
117
+ orderRef: input.orderRef,
118
+ },
119
+ };
120
+ }
121
+ function stripeFlow(flow) {
122
+ if (flow === "one_time_payment" || flow === "setup_reusable_method" || flow === "off_session_payment") {
123
+ return flow;
124
+ }
125
+ throw new Error(`Unsupported Stripe provider flow: ${flow}`);
126
+ }
127
+ function metadataFor(input, flow) {
128
+ const metadata = {
129
+ source: "openlup.payment-control",
130
+ paymentIntentId: input.paymentIntentId,
131
+ orderRef: input.orderRef,
132
+ providerRequestFingerprint: input.providerRequestFingerprint,
133
+ providerFlow: flow,
134
+ };
135
+ if (input.clientId)
136
+ metadata.clientId = input.clientId;
137
+ if (input.mode === "subscription_cycle")
138
+ metadata.mode = input.mode;
139
+ return metadata;
140
+ }
141
+ /**
142
+ * What the durable attempt row is allowed to claim about this execution.
143
+ *
144
+ * Takes the intent, not just its status, because the two states sharing
145
+ * `requires_payment_method` differ only in what else is on the object: a fresh
146
+ * intent the payer never confirmed, and one an issuer has just refused (which
147
+ * carries a payment error and usually a charge).
148
+ *
149
+ * `processing` claims money is moving, and a catch-all used to reach it — so a
150
+ * never-confirmed intent was recorded as a charge in flight, and a payer was
151
+ * told their payment was already processing when no issuer had been asked.
152
+ * `sent_to_provider` is the truth: the request reached the provider, no outcome
153
+ * is known. It belongs to every live-attempt set the platform keeps, so the
154
+ * correction costs no watchdog coverage, and it is fail-closed at the
155
+ * provider-attempt admission gate exactly as `processing` is — admission does
156
+ * not widen by one state.
157
+ *
158
+ * The catch-all stays `processing` on purpose: an unrecognised provider status is
159
+ * not evidence that nothing happened.
160
+ */
161
+ function attemptStatusFromStripe(intent) {
162
+ if (intent.status === "requires_action" || intent.status === "requires_confirmation") {
163
+ return "requires_action";
164
+ }
165
+ if (noIssuerWasAsked(intent))
166
+ return "sent_to_provider";
167
+ return "processing";
168
+ }
169
+ function nextActionKindFromStripe(intent) {
170
+ if (intent.status !== "requires_action" && intent.status !== "requires_confirmation")
171
+ return null;
172
+ if (intent.next_action?.type === "redirect_to_url")
173
+ return "redirect";
174
+ if (intent.next_action?.type === "use_stripe_sdk")
175
+ return "3ds_challenge";
176
+ return "sca_required";
177
+ }
178
+ function canonicalStripeEventType(stripeEventType, object) {
179
+ if (stripeEventType === "payment_intent.succeeded")
180
+ return "payment.succeeded";
181
+ if (stripeEventType === "payment_intent.payment_failed" || stripeEventType === "payment_intent.canceled") {
182
+ return "payment.failed";
183
+ }
184
+ if (stripeEventType === "payment_intent.requires_action")
185
+ return "payment.requires_action";
186
+ if (stripeEventType === "charge.refunded")
187
+ return "payment.refunded";
188
+ if (stripeEventType.startsWith("charge.dispute."))
189
+ return "payment.disputed";
190
+ const status = readOptionalString(object, "status");
191
+ if (status === "requires_action")
192
+ return "payment.requires_action";
193
+ throw new Error(`Unsupported Stripe webhook event: ${stripeEventType}`);
194
+ }
195
+ function readObject(value, key) {
196
+ const raw = value[key];
197
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
198
+ throw new Error(`Stripe payload missing object ${key}`);
199
+ }
200
+ return raw;
201
+ }
202
+ function readOptionalObject(value, key) {
203
+ const raw = value[key];
204
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
205
+ }
206
+ function readString(value, key) {
207
+ const raw = value[key];
208
+ if (typeof raw !== "string" || raw.length === 0) {
209
+ throw new Error(`Stripe payload missing string ${key}`);
210
+ }
211
+ return raw;
212
+ }
213
+ function readOptionalString(value, key) {
214
+ const raw = value[key];
215
+ return typeof raw === "string" && raw.length > 0 ? raw : undefined;
216
+ }
217
+ function readOptionalNumber(value, key) {
218
+ const raw = value[key];
219
+ return typeof raw === "number" ? raw : undefined;
220
+ }
@@ -0,0 +1,14 @@
1
+ import { type ProviderWebhookWithMethodLifecycle } from "./methodFacts.js";
2
+ /**
3
+ * Convert a verified raw Stripe webhook payload into the plural handler's
4
+ * `NormalizedProviderPaymentWebhook` shape.
5
+ *
6
+ * Reusable method extraction (Wave C): populated for `setup_intent.succeeded`
7
+ * always, and for `payment_intent.succeeded` when `setup_future_usage` was
8
+ * requested at PaymentIntent creation. Requires `metadata.clientId` to be set
9
+ * (the adapter does this when the runtime caller passes `clientId` through
10
+ * `PaymentExecutionInput`). When metadata is missing, `reusableMethod` stays
11
+ * null and the plural handler skips the method-ref upsert — safe degradation
12
+ * rather than crashing the webhook.
13
+ */
14
+ export declare function normalizeStripeWebhookEvent(rawPayload: Record<string, unknown>): ProviderWebhookWithMethodLifecycle;