@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.
- package/CHANGELOG.md +161 -231
- package/README.md +182 -8
- package/dist/composition/composeBundle.d.ts +6 -0
- package/dist/composition/composeBundle.js +2 -0
- package/dist/composition/index.js +1 -0
- package/dist/payments/adapterConformance.d.ts +8 -0
- package/dist/payments/adapterConformance.js +109 -0
- package/dist/payments/bindings.d.ts +3 -0
- package/dist/payments/bindings.js +17 -0
- package/dist/payments/checkoutActivePaymentActionResolver.d.ts +7 -0
- package/dist/payments/checkoutActivePaymentActionResolver.js +77 -0
- package/dist/payments/checkoutRecoveryPaymentResolver.d.ts +8 -0
- package/dist/payments/checkoutRecoveryPaymentResolver.js +87 -0
- package/dist/payments/contracts/adapter.d.ts +113 -0
- package/dist/payments/contracts/adapter.js +1 -0
- package/dist/payments/contracts/conformance.d.ts +155 -0
- package/dist/payments/contracts/conformance.js +1 -0
- package/dist/payments/contracts/evidence.d.ts +157 -0
- package/dist/payments/contracts/evidence.js +1 -0
- package/dist/payments/contracts/index.d.ts +10 -0
- package/dist/payments/contracts/index.js +1 -0
- package/dist/payments/contracts/preparedAttempt.d.ts +69 -0
- package/dist/payments/contracts/preparedAttempt.js +1 -0
- package/dist/payments/contracts/reconciliation.d.ts +303 -0
- package/dist/payments/contracts/reconciliation.js +1 -0
- package/dist/payments/contracts/repair.d.ts +92 -0
- package/dist/payments/contracts/repair.js +1 -0
- package/dist/payments/contracts/truth.d.ts +92 -0
- package/dist/payments/contracts/truth.js +1 -0
- package/dist/payments/eventConformanceRunner.d.ts +12 -0
- package/dist/payments/eventConformanceRunner.js +125 -0
- package/dist/payments/index.d.ts +16 -0
- package/dist/payments/index.js +16 -0
- package/dist/payments/paymentAttemptSilenceWindow.d.ts +10 -0
- package/dist/payments/paymentAttemptSilenceWindow.js +19 -0
- package/dist/payments/paymentMethodLifecycle.d.ts +11 -0
- package/dist/payments/paymentMethodLifecycle.js +37 -0
- package/dist/payments/paymentProviderReconciliationAbsenceReopen.d.ts +7 -0
- package/dist/payments/paymentProviderReconciliationAbsenceReopen.js +100 -0
- package/dist/payments/paymentProviderReconciliationEvidence.d.ts +36 -0
- package/dist/payments/paymentProviderReconciliationEvidence.js +91 -0
- package/dist/payments/paymentProviderReconciliationWorker.d.ts +4 -0
- package/dist/payments/paymentProviderReconciliationWorker.js +245 -0
- package/dist/payments/paymentTruth.d.ts +8 -0
- package/dist/payments/paymentTruth.js +125 -0
- package/dist/payments/paymentVerifyNowHandler.d.ts +27 -0
- package/dist/payments/paymentVerifyNowHandler.js +70 -0
- package/dist/payments/paymentVerifyNowService.d.ts +23 -0
- package/dist/payments/paymentVerifyNowService.js +93 -0
- package/dist/payments/paymentWebhookPostProcessing.d.ts +22 -0
- package/dist/payments/paymentWebhookPostProcessing.js +15 -0
- package/dist/payments/postgresPaymentTruth.d.ts +10 -0
- package/dist/payments/postgresPaymentTruth.js +126 -0
- package/dist/payments/preparedAttempt.d.ts +6 -0
- package/dist/payments/preparedAttempt.js +70 -0
- package/dist/payments/preparedAttemptFinalization.d.ts +5 -0
- package/dist/payments/preparedAttemptFinalization.js +12 -0
- package/dist/payments/schemaCapabilities.d.ts +10 -0
- package/dist/payments/schemaCapabilities.js +10 -0
- package/dist/payments/sql.d.ts +20 -0
- package/dist/payments/sql.js +6 -0
- package/dist/payments/stripe/classifyDecline.d.ts +46 -0
- package/dist/payments/stripe/classifyDecline.js +58 -0
- package/dist/payments/stripe/declineFailureHints.d.ts +46 -0
- package/dist/payments/stripe/declineFailureHints.js +87 -0
- package/dist/payments/stripe/donorContracts.d.ts +120 -0
- package/dist/payments/stripe/donorContracts.js +1 -0
- package/dist/payments/stripe/executionDecline.d.ts +53 -0
- package/dist/payments/stripe/executionDecline.js +107 -0
- package/dist/payments/stripe/index.d.ts +16 -0
- package/dist/payments/stripe/index.js +239 -0
- package/dist/payments/stripe/intentMapper.d.ts +26 -0
- package/dist/payments/stripe/intentMapper.js +51 -0
- package/dist/payments/stripe/methodFacts.d.ts +10 -0
- package/dist/payments/stripe/methodFacts.js +12 -0
- package/dist/payments/stripe/paymentFailureEvidence.d.ts +26 -0
- package/dist/payments/stripe/paymentFailureEvidence.js +33 -0
- package/dist/payments/stripe/stripeFailureEvidence.d.ts +41 -0
- package/dist/payments/stripe/stripeFailureEvidence.js +82 -0
- package/dist/payments/stripe/stripeIntentConfirmationEvidence.d.ts +28 -0
- package/dist/payments/stripe/stripeIntentConfirmationEvidence.js +32 -0
- package/dist/payments/stripe/stripePaymentReconciliationProvider.d.ts +35 -0
- package/dist/payments/stripe/stripePaymentReconciliationProvider.js +222 -0
- package/dist/payments/stripe/stripeRefundEvidence.d.ts +8 -0
- package/dist/payments/stripe/stripeRefundEvidence.js +17 -0
- package/dist/payments/stripe/stripeSandboxPaymentExecutionAdapter.d.ts +45 -0
- package/dist/payments/stripe/stripeSandboxPaymentExecutionAdapter.js +220 -0
- package/dist/payments/stripe/stripeWebhookNormalizer.d.ts +14 -0
- package/dist/payments/stripe/stripeWebhookNormalizer.js +243 -0
- package/dist/payments/stripe/webhookSignature.d.ts +14 -0
- package/dist/payments/stripe/webhookSignature.js +43 -0
- package/dist/payments/tpay/blikRecurringMapper.d.ts +37 -0
- package/dist/payments/tpay/blikRecurringMapper.js +24 -0
- package/dist/payments/tpay/capabilities.d.ts +3 -0
- package/dist/payments/tpay/capabilities.js +18 -0
- package/dist/payments/tpay/classifyDecline.d.ts +46 -0
- package/dist/payments/tpay/classifyDecline.js +58 -0
- package/dist/payments/tpay/declineFailureHints.d.ts +72 -0
- package/dist/payments/tpay/declineFailureHints.js +37 -0
- package/dist/payments/tpay/donorContracts.d.ts +140 -0
- package/dist/payments/tpay/donorContracts.js +1 -0
- package/dist/payments/tpay/index.d.ts +43 -0
- package/dist/payments/tpay/index.js +264 -0
- package/dist/payments/tpay/paymentFailureEvidence.d.ts +26 -0
- package/dist/payments/tpay/paymentFailureEvidence.js +33 -0
- package/dist/payments/tpay/preparedProviderAttempt.d.ts +22 -0
- package/dist/payments/tpay/preparedProviderAttempt.js +24 -0
- package/dist/payments/tpay/providerAttemptFailureDiagnostic.d.ts +3 -0
- package/dist/payments/tpay/providerAttemptFailureDiagnostic.js +95 -0
- package/dist/payments/tpay/tpayCreateTransactionInput.d.ts +25 -0
- package/dist/payments/tpay/tpayCreateTransactionInput.js +128 -0
- package/dist/payments/tpay/tpayDeclineEvidence.d.ts +126 -0
- package/dist/payments/tpay/tpayDeclineEvidence.js +177 -0
- package/dist/payments/tpay/tpayDispatchFailure.d.ts +60 -0
- package/dist/payments/tpay/tpayDispatchFailure.js +121 -0
- package/dist/payments/tpay/tpayFailureEvidence.d.ts +20 -0
- package/dist/payments/tpay/tpayFailureEvidence.js +61 -0
- package/dist/payments/tpay/tpayHttpClient.d.ts +52 -0
- package/dist/payments/tpay/tpayHttpClient.js +185 -0
- package/dist/payments/tpay/tpayHttpFailure.d.ts +57 -0
- package/dist/payments/tpay/tpayHttpFailure.js +143 -0
- package/dist/payments/tpay/tpayPaymentExecutionAdapter.d.ts +20 -0
- package/dist/payments/tpay/tpayPaymentExecutionAdapter.js +249 -0
- package/dist/payments/tpay/tpayPaymentReconciliationProvider.d.ts +5 -0
- package/dist/payments/tpay/tpayPaymentReconciliationProvider.js +244 -0
- package/dist/payments/tpay/tpayTransactionPayload.d.ts +120 -0
- package/dist/payments/tpay/tpayTransactionPayload.js +96 -0
- package/dist/payments/tpay/wallClockTime.d.ts +31 -0
- package/dist/payments/tpay/wallClockTime.js +57 -0
- package/dist/payments/tpay/webhookJwsVerifier.d.ts +17 -0
- package/dist/payments/tpay/webhookJwsVerifier.js +51 -0
- package/dist/payments/tpay/webhookNormalizer.d.ts +54 -0
- package/dist/payments/tpay/webhookNormalizer.js +200 -0
- package/dist/payments/verifiedEvidenceProjection.d.ts +12 -0
- package/dist/payments/verifiedEvidenceProjection.js +166 -0
- package/dist/payments/verifiedIngress.d.ts +119 -0
- package/dist/payments/verifiedIngress.js +220 -0
- package/dist/shipping/bindings.d.ts +5 -0
- package/dist/shipping/bindings.js +12 -2
- package/dist/shipping/index.d.ts +14 -0
- package/dist/shipping/index.js +5 -0
- package/package.json +22 -2
- package/release-gates.json +64 -3
- package/sql/20260810180000_payment_settlement_rail.sql +574 -0
- package/sql/20260815150000_payment_truth_reconciliation.sql +479 -0
- package/sql/20260826100000_late_payment_shifts_the_subscription_cycle.sql +215 -0
- package/sql/20261021200000_shipping_flat_rate_seed.sql +1 -0
- package/src/composition/composeBundle.ts +9 -0
- package/src/composition/index.ts +1 -0
- package/src/payments/adapterConformance.ts +118 -0
- package/src/payments/bindings.ts +17 -0
- package/src/payments/checkoutActivePaymentActionResolver.ts +97 -0
- package/src/payments/checkoutRecoveryPaymentResolver.ts +108 -0
- package/src/payments/contracts/adapter.ts +96 -0
- package/src/payments/contracts/conformance.ts +171 -0
- package/src/payments/contracts/evidence.ts +142 -0
- package/src/payments/contracts/index.ts +54 -0
- package/src/payments/contracts/preparedAttempt.ts +86 -0
- package/src/payments/contracts/reconciliation.ts +296 -0
- package/src/payments/contracts/repair.ts +95 -0
- package/src/payments/contracts/truth.ts +108 -0
- package/src/payments/eventConformanceRunner.ts +149 -0
- package/src/payments/index.ts +21 -0
- package/src/payments/paymentAttemptSilenceWindow.ts +26 -0
- package/src/payments/paymentMethodLifecycle.ts +50 -0
- package/src/payments/paymentProviderReconciliationAbsenceReopen.ts +130 -0
- package/src/payments/paymentProviderReconciliationEvidence.ts +117 -0
- package/src/payments/paymentProviderReconciliationWorker.ts +291 -0
- package/src/payments/paymentTruth.ts +131 -0
- package/src/payments/paymentVerifyNowHandler.ts +94 -0
- package/src/payments/paymentVerifyNowService.ts +125 -0
- package/src/payments/paymentWebhookPostProcessing.ts +36 -0
- package/src/payments/postgresPaymentTruth.ts +149 -0
- package/src/payments/preparedAttempt.ts +90 -0
- package/src/payments/preparedAttemptFinalization.ts +13 -0
- package/src/payments/schemaCapabilities.ts +10 -0
- package/src/payments/sql.ts +6 -0
- package/src/payments/stripe/classifyDecline.ts +62 -0
- package/src/payments/stripe/declineFailureHints.ts +89 -0
- package/src/payments/stripe/donorContracts.ts +87 -0
- package/src/payments/stripe/executionDecline.ts +126 -0
- package/src/payments/stripe/index.ts +235 -0
- package/src/payments/stripe/intentMapper.ts +58 -0
- package/src/payments/stripe/methodFacts.ts +21 -0
- package/src/payments/stripe/paymentFailureEvidence.ts +58 -0
- package/src/payments/stripe/stripeFailureEvidence.ts +115 -0
- package/src/payments/stripe/stripeIntentConfirmationEvidence.ts +34 -0
- package/src/payments/stripe/stripePaymentReconciliationProvider.ts +263 -0
- package/src/payments/stripe/stripeRefundEvidence.ts +19 -0
- package/src/payments/stripe/stripeSandboxPaymentExecutionAdapter.ts +283 -0
- package/src/payments/stripe/stripeWebhookNormalizer.ts +277 -0
- package/src/payments/stripe/webhookSignature.ts +55 -0
- package/src/payments/tpay/blikRecurringMapper.ts +42 -0
- package/src/payments/tpay/capabilities.ts +21 -0
- package/src/payments/tpay/classifyDecline.ts +62 -0
- package/src/payments/tpay/declineFailureHints.ts +90 -0
- package/src/payments/tpay/donorContracts.ts +104 -0
- package/src/payments/tpay/index.ts +257 -0
- package/src/payments/tpay/paymentFailureEvidence.ts +58 -0
- package/src/payments/tpay/preparedProviderAttempt.ts +38 -0
- package/src/payments/tpay/providerAttemptFailureDiagnostic.ts +99 -0
- package/src/payments/tpay/tpayCreateTransactionInput.ts +158 -0
- package/src/payments/tpay/tpayDeclineEvidence.ts +233 -0
- package/src/payments/tpay/tpayDispatchFailure.ts +186 -0
- package/src/payments/tpay/tpayFailureEvidence.ts +75 -0
- package/src/payments/tpay/tpayHttpClient.ts +290 -0
- package/src/payments/tpay/tpayHttpFailure.ts +186 -0
- package/src/payments/tpay/tpayPaymentExecutionAdapter.ts +297 -0
- package/src/payments/tpay/tpayPaymentReconciliationProvider.ts +285 -0
- package/src/payments/tpay/tpayTransactionPayload.ts +207 -0
- package/src/payments/tpay/wallClockTime.ts +57 -0
- package/src/payments/tpay/webhookJwsVerifier.ts +82 -0
- package/src/payments/tpay/webhookNormalizer.ts +279 -0
- package/src/payments/verifiedEvidenceProjection.ts +179 -0
- package/src/payments/verifiedIngress.ts +317 -0
- package/src/shipping/bindings.ts +18 -2
- 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
|
|
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.
|
|
8
|
-
limitations](CHANGELOG.md#
|
|
9
|
-
M06 composition bindings.
|
|
10
|
-
|
|
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
|
|
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
|
|
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,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
|
+
}
|