@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
package/README.md CHANGED
@@ -1,19 +1,21 @@
1
1
  # @openlup/platform
2
2
 
3
- Development preview. This package joins core and outbox in the publishable set from the next set version; this configuration does not establish a registry release or a stable API. Its repository-directory publish command refuses publication; the authorized package-release workflow owns publication.
3
+ Development preview. This package is part of the publishable set with core and outbox; preparation does not establish a registry release or a stable API. Its repository-directory publish command refuses publication; the authorized package-release workflow owns publication.
4
4
 
5
5
  It holds the runnable server substrate an OpenLup application composes: the Node HTTP runtime, BFF dispatch, scheduling and job leases, blob storage, data gateways, the node-postgres migration runner and bundle composition. Ports and their contracts stay in `@openlup/core/platform-runtime`.
6
6
 
7
- For the prepared `0.19.0` set, read the [adjacent-release migration and known
8
- limitations](CHANGELOG.md#0190) before selecting the opt-in M05 parity seams or
9
- M06 composition bindings. M06 in progress: inventory and shipping entries are early surfaces; module not complete
10
- The size gate compares this candidate with the published `0.18.0` tarball;
7
+ For the prepared `0.21.0` set, read the [adjacent-release migration and known
8
+ limitations](CHANGELOG.md#0210) before selecting the opt-in M05 parity seams or
9
+ M06 composition bindings, or the M07 payment selectors. M07 payment truth is complete for this wave; its selectors are default-off. M06 remains incomplete: inventory and shipping entries
10
+ are development-preview surfaces, with managed-host proof classes pending in the
11
+ [M06 parity ledger](../../docs/platform/plans/oss-m06-stock-reservations-delivery.md#parity-pending-rows).
12
+ The size gate compares the prepared `0.21.0` set with the published `0.20.0` tarball;
11
13
  preparation, offline adapter evidence and a passing size cap grant no publication
12
14
  or provider-account certification.
13
15
 
14
16
  ## Package surface
15
17
 
16
- `release-gates.json` declares kind `rail`: this foundation provides runtime infrastructure over core ports, matching the architecture's rail responsibility. Its twenty-five experimental entries each bind a declaration snapshot and package-owned smoke tests. The API checker reads this same inventory.
18
+ `release-gates.json` declares kind `rail`: this foundation provides runtime infrastructure over core ports, matching the architecture's rail responsibility. Its experimental entries each bind a declaration snapshot and package-owned smoke tests. The API checker reads this same inventory.
17
19
 
18
20
  | Export | Contents |
19
21
  | --- | --- |
@@ -30,6 +32,9 @@ or provider-account certification.
30
32
  | `./identity/contracts` | Experimental capability: browser-safe customer auth port, value types and provider parser |
31
33
  | `./communications` | Experimental existing public consent/status, newsletter sync, Auth email hook, transport contracts/capture, observation/polling orchestration, standard webhook verification and delivery receipts; host-selected rendering, egress, protocol mapping and control policy |
32
34
  | `./communications/contracts` | Pure renderer, block builders, copy-injected commerce/subscription factories and email/newsletter/Auth/intake/receipt types; no Zod, Node or provider runtime imports |
35
+ | `./payments/stripe` | Opt-in Node payment example with an injected structural client, verified observations and purpose-sensitive readback; no SDK dependency or default activation. |
36
+ | `./payments/tpay` | Opt-in Node payment example with injected HTTP/certificate I/O, Model O aliases and verified observations; no default activation. |
37
+ | `./payments` | Experimental prepared-attempt ordering with injected persistence, execution and failure policy; contracts at `./payments/contracts`. |
33
38
  | `./inventory` | Experimental schemas/ports, handlers, native stores, projection, injected forecast and SQL descriptor; `./inventory/contracts` DTOs. |
34
39
  | `./catalog` | Experimental neutral catalog schemas, drafts, reads, media source packs and publication ports |
35
40
  | `./catalog/contracts` | Dependency-free catalog DTOs, media resolver, authority and canonicalization helpers |
@@ -37,13 +42,19 @@ or provider-account certification.
37
42
  | `./pricing/contracts` | Dependency-free authority, money, quote and offer DTOs and version constants |
38
43
  | `./promotions` | Experimental promotion application, admin handlers, native read/sweep, SQL projections and acceptance |
39
44
  | `./promotions/contracts` | Dependency-free preview and admin DTOs |
40
- | `./shipping` | Experimental delivery ports, injected-schema handlers, summary and evidence |
45
+ | `./shipping` | Experimental delivery ports, injected-schema handlers, summary/evidence; native flat-rate port + Core schema readiness |
41
46
  | `./shipping/contracts` | Type-only delivery, pickup-point, summary and evidence DTOs |
42
47
 
43
48
  ## Shipping
44
49
 
45
50
  Shipping handlers require injected host schemas; contracts are type-only with opaque provider/carrier strings.
46
51
  No vendor code, inventory import, activation or parity claim.
52
+ Native flat-rate save requires the rate port, a ready Core report and
53
+ `nativeRateSchema`, a same-database Core `SchemaObservation` proving the applied
54
+ seed identity and settings objects. A candidate schema label is not proof;
55
+ later forwards do not invalidate an applied seed. The reference host exposes
56
+ `observeNativeRateSchema()` for this ledger/object observation and retains
57
+ caller-declared schema requirements. Missing proof keeps the named refusal.
47
58
 
48
59
  ## Inventory
49
60
 
@@ -763,7 +774,7 @@ for both-bundle commands, the committed managed grants and installation limits.
763
774
 
764
775
  ## Communications import
765
776
 
766
- For adoption from `0.16.0`, follow the [0.17.0 Migration guidance](CHANGELOG.md#0170)
777
+ For adoption from `0.16.0`, follow the [0.17.0 Migration guidance](CHANGELOG-HISTORY.md#0170) (source repository)
767
778
  for the complete package set, canonical SQL installation and host-owned bindings.
768
779
 
769
780
  M04 P1 relocates the existing public implementation without changing its reference
@@ -1044,3 +1055,166 @@ opt-in browser profile adds no npm entry or UI package export.
1044
1055
  Source contributors record notable changes in package changelog fragments;
1045
1056
  release preparation assembles the shipped CHANGELOG. Follow the
1046
1057
  [public contribution guide](https://github.com/openlup/openlup/blob/main/CONTRIBUTING.md#development-preview-checks).
1058
+
1059
+ ## Payments prepared attempts
1060
+
1061
+ `@openlup/platform/payments` exports `executePreparedAttemptMechanism`: validate before durable preparation, refuse prepared replay, dispatch once unless the host proves no charge, and retry one immutable local finalization. The host owns keys, fingerprints, nominal error trust and persistence. An unknown execution never authorizes redispatch. The reference facade retains its existing binding.
1062
+
1063
+ `./payments/contracts` is type-only: execution/dispatch, transient method handoff, capability overlay, verified money/method evidence, bounded readback/absence, neutral refusals, refund observations and multi-subject conformance fixtures. Verification uses receipt time for freshness; ingress starts processing at receipt time and reuses durable first receipt on redelivery. Event identity includes outcome class. Transient authorization and opaque handoffs never enter persisted diagnostics. Capability support is explicit; timeout/truncation is unknown, never exact absence. Refunds require explicit evidence by default; `legacy-assume-full` is an explicit host opt-in. See the [M07 ledger](../../docs/platform/plans/oss-m07-payments-money-truth.md).
1064
+
1065
+ The payments entry also exports verified ingress, canonical truth fingerprints, method lifecycle/refusal handoff, reconciliation/verify-now and three runner-independent conformance functions. A new internal boundary projects verified envelopes into the contributed processing carrier: original bytes reach verification unchanged, receipt time starts processing, and durable first receipt overrides redelivery time. Full-reversal chargebacks use the existing dispute path. Snapshot and delta observations preserve original payment money; missing observation, required money or identity refuses before terminal application. Missing money, amounts that are not safe positive integers, or blank currencies on succeeded, refunded or disputed adapter evidence use the existing amount-mismatch refusal before ingestion or terminal application; ingress returns the existing processing-failure response. Failed and pending evidence may still omit money. Hosts own durable stores and response transport. Conformance requires safe positive integer amounts and nonblank currencies on settling money and checks exact identity/money, capability/port agreement and transient-secret exclusion; its evidence contains no handoff. Real-store durability still requires a selected-store proof.
1066
+
1067
+ `createPostgresPaymentTruthPort` accepts the platform data executor structural
1068
+ shape and `PostgresPaymentTruthOptions` from the contracts entry. It preserves
1069
+ the native truth ingest, reconciliation, sweep and readback SQL. `PAYMENT_MIGRATIONS`
1070
+ projects canonical units 10/35/56 byte for byte, with path, position, SHA-256 and
1071
+ asset URL; install the complete catalogue, never those three files alone.
1072
+ `paymentSchemaDescriptor` admits direct truth, settlement and later-only cycle
1073
+ alignment, and explicitly refuses prepared durable storage, method setup,
1074
+ absence reopen and cumulative refund. Refunds are purely financial. Seven real
1075
+ PostgreSQL cases cover fresh/prefix installs, replay/conflict, unknown readback,
1076
+ lost acknowledgement, rollback, denied access, dunning/recovery, concurrency and
1077
+ later-only dates; this does not claim managed-store parity or host activation.
1078
+
1079
+ Attempt input adds optional method selection with opaque, persistable channelRef
1080
+ and transient payer email/name. Payer contact is dispatch-only: never persist,
1081
+ audit, log or diagnose it. Missing adapter-required input must refuse before
1082
+ dispatch with exact `notDispatched` evidence. Conformance scans the execution
1083
+ and complete projected stored payload for transient values. Nested normalized
1084
+ evidence copies only declared typed fields, including nested facts; unknown
1085
+ extra properties are excluded. Incomplete chargeback evidence remains stored as
1086
+ a refund and unapplied, a declared operator recovery limit: reclassification
1087
+ would apply the donor dispute path without checking completeness.
1088
+
1089
+ `createCheckoutActivePaymentActionResolver` authorizes the exact owner, order,
1090
+ intent, attempt and execution rail, reads the provider once, then rechecks the
1091
+ complete canonical attempt. Hosts inject synchronous `railHandoffKinds` from
1092
+ adapter capture capabilities; an unknown rail supplies an empty list. Actions
1093
+ carry the provider address and the existing transient `PaymentMethodHandoff`.
1094
+ Pending evidence alone may expose a handoff; terminal, mismatched, unknown or
1095
+ raced evidence refuses it. `createCheckoutRecoveryPaymentResolver` preserves
1096
+ paid/retry/resume/wait/manual-review ordering without creating a new obligation.
1097
+ `createCommercePaymentVerifyHandler` validates the existing identity triple,
1098
+ checks stored ownership before readback, and uses the shared HTTP/responder
1099
+ contracts. A host may inject its responder to preserve request references.
1100
+
1101
+ `PaymentRepairMethodPort` collects a method only; it grants no token redemption,
1102
+ subscription change or payment success. Unsupported setup is explicit. Method
1103
+ evidence may carry neutral registration refs/kind/status. Ingress projects them
1104
+ only with `resolveMethodOwner` supplied by the host; adapter-supplied client or
1105
+ subscription fields never confer ownership. Without host ownership resolution,
1106
+ registration is not projected. Lifecycle carriers copy typed facts only.
1107
+ The existing refusal handoff accepts a failure-reason string, not a typed
1108
+ adapter refusal envelope; envelope forwarding remains unsupported.
1109
+
1110
+ ### Payment service composition
1111
+
1112
+ `createPaymentServices` accepts `PaymentServicesOptions` and returns
1113
+ `PaymentServices` or null when truth schema/capability is unavailable. The host
1114
+ supplies `openTruth`, which opens one port and its asynchronous close operation.
1115
+ `run` closes that scope after success or failure. Construction performs no I/O.
1116
+ Use the existing `createBundleComposer` payments binder for named registration;
1117
+ omitting it leaves the host cold. Schema declarations do not prove installation:
1118
+ retain the existing readiness/schema checks before admitting work. Existing jobs
1119
+ and routes keep their owners; this binding adds no schedule or event handler.
1120
+
1121
+ The reference direct truth binding injects its existing executor, templates and
1122
+ recovery path. The separate verified-ingress selector is explicit and default-off;
1123
+ unknown selections refuse. Its projected carrier includes the adapter fingerprint,
1124
+ so the reference store records stable identity on the first receipt independently
1125
+ of audit payload or clock. On replay the store reads the first stored instant;
1126
+ application and follow-up work reuse it. Missing durable clock evidence refuses
1127
+ rather than advancing a cycle. This D11 behaviour change does not mount a provider
1128
+ route or enable an adapter; hosts must explicitly wire the selected ingress.
1129
+
1130
+ ### Opt-in payment example
1131
+
1132
+ Import `createStripePaymentAdapter`, `type StripePaymentAdapterConfig` and
1133
+ `stripePaymentCapabilities` from `@openlup/platform/payments/stripe`. Supply a
1134
+ structural client, `webhookSigningSecrets`, explicit `mode`, required
1135
+ `supportedCurrencies` (a non-empty list of upper-case three-letter codes) and optionally
1136
+ `toleranceSeconds` (default 300) and `clock` (UTC seconds). The host constructs
1137
+ and configures the SDK, associates saved methods with customers and confirms
1138
+ off-session requests. Interactive methods follow the declared card/wallet offer.
1139
+ Importing creates no I/O or default binding. The adapter advertises the host
1140
+ currency list as a frozen copy; the frozen exported descriptor keeps an empty list.
1141
+ Account-specific readiness remains pending TEST verification. Exact absence,
1142
+ refund creation and split authorization/capture are unsupported. Cancellation
1143
+ is enabled only when the injected client supplies it. See the
1144
+ [A2 ledger](../../docs/platform/plans/oss-a2-payment-adapters.md) for the current
1145
+ preview boundary and the host's responsibilities.
1146
+
1147
+ Verified dispute events retain their distinct money outcome and original payment
1148
+ identity. Audit payloads carry only donor-filtered facts: the source event type,
1149
+ refund charge snapshot and failure-evidence provenance when present. They exclude
1150
+ raw bodies, client handoffs and customer authorization and do not affect event
1151
+ IDs or fingerprints. The offline adapter conformance witness uses two distinct
1152
+ intents and attempts with separate money and provider references.
1153
+
1154
+ Method evidence carries the donor lifecycle and replacement facts through M07
1155
+ projection when provider time is present. Without provider time it retains the
1156
+ method outcome and omits the lifecycle instead of inventing a transition time.
1157
+
1158
+ ### Opt-in bank and code payment example
1159
+
1160
+ Import `createTpayPaymentAdapter`, `type TpayPaymentAdapterConfig` and
1161
+ `tpayPaymentCapabilities` from `@openlup/platform/payments/tpay`. Supply `http`
1162
+ with explicit `baseUrl`, `clientId`, `clientSecret`, `fetchImpl` and optional
1163
+ `timeoutMs`; `urls` with `notificationUrl`, `successUrl`, `errorUrl`;
1164
+ `modelOActivationEnabled`, `mode`, and optional UTC-seconds `clock`.
1165
+ `notification` requires `rootCertificatePem`, `allowedCertificateUrlPrefix`
1166
+ (HTTPS directory ending in `/`) and `resolveCertificate`. The resolver owns
1167
+ redirect/egress policy. Both certificates must be valid at verification time.
1168
+
1169
+ Verify certificate validity and notification freshness using original raw bytes.
1170
+ A signed protected-header `iat` supplies UTC-seconds provider time; absent time
1171
+ stays null. `maxNotificationAgeSeconds` defaults to seven days, a generous
1172
+ assumption pending confirmation of the provider retry schedule. Signed issue time
1173
+ more than `maxClockSkewSeconds` (default 300) after receipt refuses as stale;
1174
+ both freshness limits must be finite and nonnegative. Host dedupe
1175
+ uses exact event identity plus stable fingerprint when time is absent. The age
1176
+ and future-skew limits apply only when the signed header carries a provider issue time. Receipt
1177
+ time is used only for freshness. Transaction identity includes outcome class;
1178
+ method identity includes event/alias plus outcome.
1179
+
1180
+ Bank redirects, one-time codes and unattended Model O alias charges are supported
1181
+ when the host supplies the M07 P3 contract payer and method selection inputs.
1182
+ Supply transient `payer: { email, name? }` on every
1183
+ attempt, including renewals. An omitted or blank name uses the donor
1184
+ neutral display label `Customer`. Select `methodSelection: { family: "bank-redirect",
1185
+ channelRef }` for the bank channel, or `{ family: "one-time-code" }` with
1186
+ `customerAuthorization` as a `TransientSecret`. A `renewal-unattended` attempt
1187
+ uses `storedMethod.reference` and its Model O mandate. Missing payer/channel/code
1188
+ and Model M or unknown mandates refuse before any dispatch with neutral
1189
+ data-correction evidence. Payer and authorization never cross the return boundary
1190
+ in diagnostics or request/response payloads. Readback uses the returned API
1191
+ transaction ID; notification titles remain distinct. The donor's typed no-charge
1192
+ errors return an exact `notDispatched` envelope: proven request-document refusals
1193
+ require customer data correction; OAuth, encoding and exhausted pre-send deadlines
1194
+ remain transient retryable refusals. Host mode/purpose/currency mismatches throw before I/O.
1195
+ A renewal the provider refuses for data validation is classified as needing customer data correction: the host should prompt the customer to update payment details, and the bundled retry schedule keeps its normal retries unless the host treats that class as terminating. The subscription stays active; nothing is paused or suspended by this classification. The contributed adopter applies this classification only when reopening a checkout; the example applies it to every purpose.
1196
+ Every other execution error propagates unchanged as ambiguous.
1197
+
1198
+ The exported capability descriptor and its nested records and arrays are frozen.
1199
+ Currencyless money notifications preserve the paid amount and bind to the adapter's single declared currency; multiple declared currencies refuse as malformed.
1200
+ Alias method evidence preserves the donor lifecycle through M07 projection with
1201
+ signed provider time; absent provider time leaves the lifecycle field omitted.
1202
+ The adapter offers its declared market currency; alias registration and recurring
1203
+ cards are not offered by this factory. Account offers
1204
+ remain `verification_pending` until owner TEST readback. Chargeback observations carry full reversal,
1205
+ original money and stable operation identity; refunded/chargeback readback stays
1206
+ unknown for manual review, including historical attempt/error refusals in either
1207
+ readback purpose. Pending and refused readback report validated configured money,
1208
+ including when the paid amount is zero or absent; a refused result with mismatched
1209
+ nonzero paid money stays unknown for manual review. Succeeded readback requires the donor's
1210
+ observed paid amount to equal expected money; underpayment and overpayment remain
1211
+ unknown for manual review. There is no close or exact-absence port. Refund
1212
+ creation and authorization/capture separation are unsupported. See the
1213
+ [A2 ledger](../../docs/platform/plans/oss-a2-payment-adapters.md#o-p2-boundary-and-named-deltas).
1214
+
1215
+ Verified audit payloads retain only donor-filtered transaction and alias facts,
1216
+ including `eventKind`, alias `event` and `aliasType` when supplied. Missing failure
1217
+ provenance and failure prose stay absent. Payer, authorization, certificates and
1218
+ raw bodies never enter the evidence; audit facts do not affect event identity or
1219
+ fingerprints. The offline M07 P2 conformance witness uses two distinct intents,
1220
+ attempts, amounts and provider references, with replay and refusal boundaries.
@@ -1,7 +1,11 @@
1
1
  import type { AnalyticsPort, BlobStoragePort, DataGatewayPort, HttpRuntimePort, MigrationRunnerPort, SchedulerPort, TransactionalRuntimePort } from "@openlup/core/platform-runtime";
2
2
  import { type PlatformBundleDescriptor, type PlatformBundleId } from "../kernel/index.js";
3
+ interface PaymentServices {
4
+ run<T>(work: (port: unknown) => Promise<T>): Promise<T>;
5
+ }
3
6
  type Env = Record<string, string | undefined>;
4
7
  export interface PlatformBundlePorts {
8
+ payments?: PaymentServices | null;
5
9
  http: HttpRuntimePort | null;
6
10
  scheduler: SchedulerPort | null;
7
11
  blob: BlobStoragePort | null;
@@ -21,6 +25,8 @@ export interface PlatformBundle {
21
25
  * transactional binder also receives the bundle's data capability.
22
26
  */
23
27
  export interface PlatformBundleBinders {
28
+ /** Explicit host selection only; no default payment executor. */
29
+ payments?: (env: Env) => PaymentServices | null;
24
30
  /**
25
31
  * The supabase-js `createClient` used by the default Supabase Storage blob binder. Declared
26
32
  * structurally here so the composition entry's declarations reference no data-module type.
@@ -39,6 +39,8 @@ export function createBundleComposer(binders = {}) {
39
39
  analytics: binders.analytics?.(capabilities.analytics, env) ?? null,
40
40
  transactional: binders.transactional?.(capabilities.transactional, capabilities.data, env) ?? null,
41
41
  };
42
+ if (binders.payments)
43
+ ports.payments = binders.payments(env);
42
44
  ports.scheduler = binders.scheduler?.(capabilities.scheduler, env) ?? null;
43
45
  return { id, descriptor, ports };
44
46
  };
@@ -1,3 +1,4 @@
1
+ // The existing composer accepts one explicit payments binder; default hosts stay cold.
1
2
  export { createBundleComposer, } from "./composeBundle.js";
2
3
  export { bindNodeHttpPort, createScheduledEntrypointResolver } from "./httpBinding.js";
3
4
  export { createNodeHost, } from "./nodeHost.js";
@@ -0,0 +1,8 @@
1
+ import type { PaymentAdapterConformanceEvidence, PaymentAdapterConformanceSubject, PaymentVerifyNormalizePort } from "./contracts/index.js";
2
+ /** Runner-independent adapter proof. Verification fixtures are owned by each adapter. */
3
+ export declare function runPaymentAdapterConformance(subjects: readonly (PaymentAdapterConformanceSubject & {
4
+ verification: readonly {
5
+ input: Parameters<PaymentVerifyNormalizePort["verifyAndNormalize"]>[0];
6
+ expected: Awaited<ReturnType<PaymentVerifyNormalizePort["verifyAndNormalize"]>>;
7
+ }[];
8
+ })[]): Promise<PaymentAdapterConformanceEvidence>;
@@ -0,0 +1,109 @@
1
+ import { projectVerifiedEvidence } from "./verifiedEvidenceProjection.js";
2
+ import { validatePaymentExecutionContractResult } from "@openlup/core/testing";
3
+ /** Runner-independent adapter proof. Verification fixtures are owned by each adapter. */
4
+ export async function runPaymentAdapterConformance(subjects) {
5
+ const observations = [];
6
+ const cases = [];
7
+ for (const subject of subjects) {
8
+ const { adapter, executionInput, readbackInput } = subject;
9
+ const provider = adapter.capabilities.providerKind;
10
+ requireFact(executionInput.money.currency.length > 0, "configured currency");
11
+ adapter.attemptPort.validateInput(executionInput);
12
+ const result = await adapter.attemptPort.execute(executionInput);
13
+ const { handoff, ...execution } = result;
14
+ const sensitiveValues = [executionInput.payer?.email, executionInput.payer?.name, executionInput.customerAuthorization,
15
+ handoff?.kind === "client-confirmed" ? handoff.payload : handoff?.url].filter((v) => v !== undefined && v.length > 0);
16
+ const sensitiveForms = sensitiveValues.flatMap(value => [value, JSON.stringify(value).slice(1, -1)]);
17
+ const payerForms = [executionInput.payer?.email, executionInput.payer?.name]
18
+ .filter((value) => Boolean(value?.length))
19
+ .flatMap(value => [value, JSON.stringify(value).slice(1, -1)]);
20
+ const input = {
21
+ idempotencyKey: executionInput.attemptRef, providerIdempotencyKey: executionInput.providerIdempotencyKey,
22
+ providerRequestFingerprint: executionInput.fingerprint, paymentIntentId: executionInput.intentRef,
23
+ amountMinor: executionInput.money.amountMinor, currency: executionInput.money.currency, mode: "one_time",
24
+ };
25
+ const validation = validatePaymentExecutionContractResult({ input, expectedProvider: provider, sensitiveValues: sensitiveForms,
26
+ result: { provider, providerAttemptId: result.dispatch.kind === "dispatched" ? result.dispatch.providerPaymentRef : null,
27
+ providerSessionId: null, attemptStatus: result.outcome.kind === "attempt" ? result.outcome.status : "processing",
28
+ nextActionKind: null, requestPayload: { providerIdempotencyKey: input.providerIdempotencyKey,
29
+ providerRequestFingerprint: input.providerRequestFingerprint }, responsePayload: result.diagnostics },
30
+ });
31
+ requireFact(validation.ok, "execution diagnostics and taxonomy");
32
+ requireFact(!payerForms.some(value => JSON.stringify(result).includes(value)), "execution payer redaction");
33
+ if (adapter.capabilities.payerContext.requiresPayerBlock && !executionInput.payer) {
34
+ requireFact(result.dispatch.kind === "notDispatched" && result.dispatch.evidence.exact
35
+ && result.dispatch.evidence.phase === "before-dispatch", "missing required input exact notDispatched");
36
+ }
37
+ const returned = await adapter.readbackPort.readPayment(readbackInput);
38
+ const { handoff: readbackHandoff, ...readback } = "handoff" in returned ? returned : { ...returned, handoff: undefined };
39
+ requireFact(!sensitiveForms.some(value => JSON.stringify(readback).includes(value)), "readback secret redaction");
40
+ requireFact(sameIdentity(readback.identity, readbackInput.identity) && sameMoney(readback.expectedMoney, readbackInput.expectedMoney), "readback exact identity and money");
41
+ requireFact(adapter.capabilities.readback === "supported" ? returned.kind !== "unsupported" : returned.kind === "unsupported", "readback capability");
42
+ if ("observedMoney" in returned)
43
+ requireFact(sameMoney(returned.observedMoney, readbackInput.expectedMoney), "observed money");
44
+ if (adapter.capabilities.close === "supported") {
45
+ requireFact(typeof adapter.readbackPort.close === "function", "close method present");
46
+ const closed = await adapter.readbackPort.close(readbackInput);
47
+ requireFact(closed.kind !== "unsupported" && sameIdentity(closed.identity, readbackInput.identity)
48
+ && sameMoney(closed.expectedMoney, readbackInput.expectedMoney), "close exact identity and money");
49
+ }
50
+ else if (adapter.readbackPort.close) {
51
+ requireFact((await adapter.readbackPort.close(readbackInput)).kind === "unsupported", "close refuses");
52
+ }
53
+ let absence;
54
+ if (adapter.capabilities.exactAbsence === "supported")
55
+ requireFact(adapter.absencePort !== undefined, "absence port present");
56
+ if (adapter.absencePort) {
57
+ requireFact(subject.absenceInput !== undefined, "absence input present");
58
+ absence = await adapter.absencePort.proveAbsence(subject.absenceInput);
59
+ requireFact(sameIdentity(absence.identity, subject.absenceInput.identity)
60
+ && sameMoney(absence.expectedMoney, subject.absenceInput.expectedMoney), "absence exact identity and money");
61
+ requireFact(adapter.capabilities.exactAbsence === "supported" ? absence.kind !== "unsupported" : absence.kind === "unsupported", "absence capability");
62
+ if (absence.kind === "absent")
63
+ requireFact(absence.evidence.exact && absence.evidence.complete
64
+ && absence.evidence.lookupReference.trim().length > 0, "complete absence evidence");
65
+ }
66
+ const fingerprints = new Map();
67
+ const moneyEvents = [];
68
+ for (const fixture of subject.verification) {
69
+ const verified = await adapter.verifyPort.verifyAndNormalize(fixture.input);
70
+ requireFact(JSON.stringify(verified) === JSON.stringify(fixture.expected), "verified fixture");
71
+ if (verified.kind !== "refusal") {
72
+ requireFact(verified.kind !== "money" || (verified.money !== null
73
+ && Number.isSafeInteger(verified.money.amountMinor) && verified.money.amountMinor > 0
74
+ && verified.money.currency.trim().length > 0)
75
+ || (verified.outcome !== "succeeded" && verified.outcome !== "refunded" && verified.outcome !== "disputed"), "settling outcome requires positive money");
76
+ requireFact(!sensitiveForms.some(value => JSON.stringify({ payload: projectVerifiedEvidence(verified, fixture.input, "require-evidence").rawPayload, diagnostics: result.diagnostics }).includes(value)), "audit payload secret redaction");
77
+ const outcome = verified.kind + ":" + verified.outcome;
78
+ const previous = fingerprints.get(verified.providerEventId);
79
+ if (previous)
80
+ requireFact(previous.outcome === outcome && previous.fingerprint === verified.fingerprint, "replay identity fingerprint");
81
+ if (verified.kind === "money") {
82
+ for (const event of moneyEvents) {
83
+ if (event.reference === verified.providerPaymentRef && event.outcome !== outcome) {
84
+ requireFact(event.eventId !== verified.providerEventId && event.fingerprint !== verified.fingerprint, "changing outcome identity");
85
+ }
86
+ }
87
+ moneyEvents.push({ reference: verified.providerPaymentRef, outcome, eventId: verified.providerEventId, fingerprint: verified.fingerprint });
88
+ }
89
+ fingerprints.set(verified.providerEventId, { fingerprint: verified.fingerprint, outcome });
90
+ }
91
+ }
92
+ // Transient handoffs are deliberately excluded from the returned evidence.
93
+ void readbackHandoff;
94
+ observations.push({ subjectId: subject.subjectId, execution, readback, ...(absence ? { absence } : {}) });
95
+ cases.push({ subjectIds: [subject.subjectId], name: "execution, verification, capability and exact readback", passed: true });
96
+ }
97
+ return { subjects: observations, cases };
98
+ }
99
+ function requireFact(value, label) {
100
+ if (!value)
101
+ throw new Error(`payment_adapter_conformance:${label}`);
102
+ }
103
+ function sameMoney(left, right) {
104
+ return left.amountMinor === right.amountMinor && left.currency === right.currency;
105
+ }
106
+ function sameIdentity(left, right) {
107
+ return left.provider === right.provider && left.providerPaymentRef === right.providerPaymentRef
108
+ && left.intentRef === right.intentRef && left.attemptRef === right.attemptRef;
109
+ }
@@ -0,0 +1,3 @@
1
+ import type { PaymentServices, PaymentServicesOptions } from "./contracts/repair.js";
2
+ /** One host-owned scope per runtime unit; no driver or environment discovery. */
3
+ export declare function createPaymentServices(options: PaymentServicesOptions): PaymentServices | null;
@@ -0,0 +1,17 @@
1
+ /** One host-owned scope per runtime unit; no driver or environment discovery. */
2
+ export function createPaymentServices(options) {
3
+ if (options.schema?.truth !== "supported" || !options.openTruth)
4
+ return null;
5
+ const openTruth = options.openTruth;
6
+ return {
7
+ async run(work) {
8
+ const scope = await openTruth();
9
+ try {
10
+ return await work(scope.port);
11
+ }
12
+ finally {
13
+ await scope.close();
14
+ }
15
+ },
16
+ };
17
+ }
@@ -0,0 +1,7 @@
1
+ import type { CheckoutActivePaymentActionResolver, CheckoutActionReadProvider } from "./contracts/repair.js";
2
+ import type { PaymentVerifyReadPort } from "./contracts/reconciliation.js";
3
+ export declare function createCheckoutActivePaymentActionResolver(deps: {
4
+ readPort: PaymentVerifyReadPort;
5
+ providers: Partial<Record<string, CheckoutActionReadProvider>>;
6
+ railHandoffKinds(rail: string): readonly ("redirect" | "client-confirmed")[];
7
+ }): CheckoutActivePaymentActionResolver;
@@ -0,0 +1,77 @@
1
+ import { toClaimedAttemptShape } from "./paymentVerifyNowService.js";
2
+ const LIVE_INTENT_STATUSES = new Set(["requires_action", "processing"]);
3
+ const LIVE_ATTEMPT_STATUSES = new Set(["sent_to_provider", "requires_action", "processing"]);
4
+ export function createCheckoutActivePaymentActionResolver(deps) {
5
+ return {
6
+ async readActiveAction(authority) {
7
+ try {
8
+ const initial = await deps.readPort.readVerifiableAttempt({
9
+ orderId: authority.orderId,
10
+ paymentIntentId: authority.paymentIntentId,
11
+ });
12
+ if (!matchesAuthority(initial, authority) || !isLive(initial))
13
+ return null;
14
+ const provider = deps.providers[authority.executionRail];
15
+ const providerPaymentId = provider?.resolvePaymentReference(initial) ?? null;
16
+ if (!provider || !providerPaymentId)
17
+ return null;
18
+ const attempt = toClaimedAttemptShape(initial, providerPaymentId);
19
+ const reading = await provider.readRecoveryPayment({
20
+ providerPaymentId,
21
+ attempt,
22
+ purpose: "active_checkout",
23
+ });
24
+ if (!reading.clientAction
25
+ || !actionMatchesRail(reading.clientAction, authority.executionRail, deps.railHandoffKinds)
26
+ || reading.manualReviewRequired
27
+ || !reading.identityMatches
28
+ || !reading.configuredMoneyMatches
29
+ || reading.status.status !== "pending")
30
+ return null;
31
+ const current = await deps.readPort.readVerifiableAttempt({
32
+ orderId: authority.orderId,
33
+ paymentIntentId: authority.paymentIntentId,
34
+ });
35
+ if (!sameCanonicalAttempt(initial, current) || !isLive(current))
36
+ return null;
37
+ return reading.clientAction;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ },
43
+ };
44
+ }
45
+ function matchesAuthority(snapshot, authority) {
46
+ return Boolean(snapshot)
47
+ && snapshot?.orderId === authority.orderId
48
+ && snapshot.orderClientId === authority.clientId
49
+ && snapshot.paymentIntentId === authority.paymentIntentId
50
+ && snapshot.paymentAttemptId === authority.paymentAttemptId
51
+ && snapshot.provider === authority.executionRail;
52
+ }
53
+ function isLive(snapshot) {
54
+ return Boolean(snapshot)
55
+ && LIVE_INTENT_STATUSES.has(snapshot?.intentStatus ?? "")
56
+ && LIVE_ATTEMPT_STATUSES.has(snapshot?.attemptStatus ?? "");
57
+ }
58
+ function actionMatchesRail(action, rail, railHandoffKinds) {
59
+ return action.provider === rail && railHandoffKinds(rail).includes(action.kind);
60
+ }
61
+ function sameCanonicalAttempt(before, after) {
62
+ return Boolean(after)
63
+ && after?.orderId === before.orderId
64
+ && after.orderClientId === before.orderClientId
65
+ && after.paymentIntentId === before.paymentIntentId
66
+ && after.paymentAttemptId === before.paymentAttemptId
67
+ && after.paymentId === before.paymentId
68
+ && after.provider === before.provider
69
+ && after.providerAttemptId === before.providerAttemptId
70
+ && after.providerSessionId === before.providerSessionId
71
+ && after.intentProviderPaymentId === before.intentProviderPaymentId
72
+ && after.intentStatus === before.intentStatus
73
+ && after.attemptStatus === before.attemptStatus
74
+ && after.amountMinor === before.amountMinor
75
+ && after.currency.toUpperCase() === before.currency.toUpperCase()
76
+ && after.localUpdatedAt === before.localUpdatedAt;
77
+ }
@@ -0,0 +1,8 @@
1
+ import type { CheckoutRecoveryPaymentResolver, PaymentProviderRecoveryProvider } from "./contracts/repair.js";
2
+ import type { PaymentVerifyReadPort, PaymentProviderReconciliationPort } from "./contracts/reconciliation.js";
3
+ export declare function createCheckoutRecoveryPaymentResolver(deps: {
4
+ readPort: PaymentVerifyReadPort;
5
+ applyPort: Pick<PaymentProviderReconciliationPort, "applyTerminalResult">;
6
+ providers: Partial<Record<string, PaymentProviderRecoveryProvider>>;
7
+ now?: () => Date;
8
+ }): CheckoutRecoveryPaymentResolver;
@@ -0,0 +1,87 @@
1
+ import { terminalAmountMismatch } from "./paymentProviderReconciliationEvidence.js";
2
+ import { toClaimedAttemptShape, verifyPaymentAttemptNow } from "./paymentVerifyNowService.js";
3
+ const LIVE_ATTEMPT_STATUSES = new Set(["sent_to_provider", "requires_action", "processing", "created"]);
4
+ export function createCheckoutRecoveryPaymentResolver(deps) {
5
+ const now = deps.now ?? (() => new Date());
6
+ return {
7
+ async resolve(input) {
8
+ if (!input.paymentIntentId)
9
+ return { kind: "retry_new" };
10
+ const snapshot = await deps.readPort.readVerifiableAttempt({
11
+ orderId: input.orderId,
12
+ paymentIntentId: input.paymentIntentId,
13
+ });
14
+ if (!snapshot) {
15
+ return input.intentStatus === "created" || input.intentStatus === "failed"
16
+ ? { kind: "retry_new" }
17
+ : { kind: "awaiting_provider" };
18
+ }
19
+ return resolveSnapshot(snapshot, deps, now);
20
+ },
21
+ };
22
+ }
23
+ async function resolveSnapshot(snapshot, deps, now) {
24
+ if (snapshot.attemptStatus === "succeeded" || snapshot.intentStatus === "succeeded") {
25
+ return { kind: "paid" };
26
+ }
27
+ if (snapshot.attemptStatus === "failed" || snapshot.intentStatus === "failed") {
28
+ return { kind: "retry_new" };
29
+ }
30
+ if (!LIVE_ATTEMPT_STATUSES.has(snapshot.attemptStatus)) {
31
+ return { kind: "awaiting_provider" };
32
+ }
33
+ const provider = deps.providers[snapshot.provider];
34
+ const providerPaymentId = provider?.resolvePaymentReference(snapshot) ?? null;
35
+ if (!provider || !providerPaymentId)
36
+ return { kind: "awaiting_provider" };
37
+ const attempt = toClaimedAttemptShape(snapshot, providerPaymentId);
38
+ try {
39
+ // Every caller of this resolver is a buyer holding a recovery link open, so
40
+ // an intent still waiting for its first payment method is one they can be
41
+ // handed back — not one to close under them and replace.
42
+ const recovery = await provider.readRecoveryPayment({
43
+ providerPaymentId,
44
+ attempt,
45
+ purpose: "active_checkout",
46
+ });
47
+ if (recovery.manualReviewRequired
48
+ || !recovery.identityMatches
49
+ || !recovery.configuredMoneyMatches) {
50
+ return { kind: "manual_review" };
51
+ }
52
+ if (recovery.clientAction) {
53
+ return { kind: "resume_existing", clientAction: recovery.clientAction };
54
+ }
55
+ if (recovery.status.status === "pending" || recovery.status.status === "unknown") {
56
+ return { kind: "awaiting_provider" };
57
+ }
58
+ if (terminalAmountMismatch(attempt, recovery.status)) {
59
+ return { kind: "manual_review" };
60
+ }
61
+ const verified = await verifyPaymentAttemptNow({
62
+ snapshot,
63
+ applyPort: deps.applyPort,
64
+ providers: deps.providers,
65
+ providerPaymentId,
66
+ providerStatus: recovery.status,
67
+ now,
68
+ });
69
+ if (verified.status !== "succeeded" && verified.status !== "failed") {
70
+ return { kind: "awaiting_provider" };
71
+ }
72
+ const current = await deps.readPort.readVerifiableAttempt({
73
+ orderId: snapshot.orderId,
74
+ paymentIntentId: snapshot.paymentIntentId,
75
+ });
76
+ if (current?.attemptStatus === "succeeded" || current?.intentStatus === "succeeded") {
77
+ return { kind: "paid" };
78
+ }
79
+ if (current?.attemptStatus === "failed" || current?.intentStatus === "failed") {
80
+ return { kind: "retry_new" };
81
+ }
82
+ return verified.status === "succeeded" ? { kind: "paid" } : { kind: "retry_new" };
83
+ }
84
+ catch {
85
+ return { kind: "awaiting_provider" };
86
+ }
87
+ }