@openlup/platform 0.20.0 → 0.22.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 +272 -278
- package/README.md +195 -6
- package/dist/composition/composeBundle.d.ts +13 -0
- package/dist/composition/composeBundle.js +4 -0
- package/dist/composition/fulfillmentContribution.d.ts +21 -0
- package/dist/composition/fulfillmentContribution.js +19 -0
- package/dist/composition/index.d.ts +1 -0
- package/dist/composition/index.js +2 -0
- package/dist/fulfillment/adapterConformance.d.ts +3 -0
- package/dist/fulfillment/adapterConformance.js +223 -0
- package/dist/fulfillment/bindings.d.ts +3 -0
- package/dist/fulfillment/bindings.js +29 -0
- package/dist/fulfillment/commerceFulfillmentHandlers.d.ts +29 -0
- package/dist/fulfillment/commerceFulfillmentHandlers.js +184 -0
- package/dist/fulfillment/commerceFulfillmentReadModel.d.ts +86 -0
- package/dist/fulfillment/commerceFulfillmentReadModel.js +131 -0
- package/dist/fulfillment/contracts/alignment.d.ts +25 -0
- package/dist/fulfillment/contracts/alignment.js +1 -0
- package/dist/fulfillment/contracts/bindings.d.ts +51 -0
- package/dist/fulfillment/contracts/bindings.js +1 -0
- package/dist/fulfillment/contracts/capabilities.d.ts +29 -0
- package/dist/fulfillment/contracts/capabilities.js +1 -0
- package/dist/fulfillment/contracts/conformance.d.ts +93 -0
- package/dist/fulfillment/contracts/conformance.js +1 -0
- package/dist/fulfillment/contracts/customerAlignment.d.ts +6 -0
- package/dist/fulfillment/contracts/customerAlignment.js +1 -0
- package/dist/fulfillment/contracts/dispatch.d.ts +105 -0
- package/dist/fulfillment/contracts/dispatch.js +1 -0
- package/dist/fulfillment/contracts/index.d.ts +18 -0
- package/dist/fulfillment/contracts/index.js +1 -0
- package/dist/fulfillment/contracts/label.d.ts +35 -0
- package/dist/fulfillment/contracts/label.js +1 -0
- package/dist/fulfillment/contracts/nativeUnsupported.d.ts +6 -0
- package/dist/fulfillment/contracts/nativeUnsupported.js +1 -0
- package/dist/fulfillment/contracts/observation.d.ts +52 -0
- package/dist/fulfillment/contracts/observation.js +1 -0
- package/dist/fulfillment/contracts/operator.d.ts +64 -0
- package/dist/fulfillment/contracts/operator.js +1 -0
- package/dist/fulfillment/contracts/read.d.ts +110 -0
- package/dist/fulfillment/contracts/read.js +1 -0
- package/dist/fulfillment/contracts/readback.d.ts +38 -0
- package/dist/fulfillment/contracts/readback.js +1 -0
- package/dist/fulfillment/contracts/replacement.d.ts +34 -0
- package/dist/fulfillment/contracts/replacement.js +1 -0
- package/dist/fulfillment/contracts/store.d.ts +86 -0
- package/dist/fulfillment/contracts/store.js +1 -0
- package/dist/fulfillment/contracts/sync.d.ts +170 -0
- package/dist/fulfillment/contracts/sync.js +1 -0
- package/dist/fulfillment/currentFulfillmentParcel.d.ts +2 -0
- package/dist/fulfillment/currentFulfillmentParcel.js +1 -0
- package/dist/fulfillment/delayReadModel.d.ts +11 -0
- package/dist/fulfillment/delayReadModel.js +45 -0
- package/dist/fulfillment/deliveryAlignmentReadModel.d.ts +4 -0
- package/dist/fulfillment/deliveryAlignmentReadModel.js +12 -0
- package/dist/fulfillment/dispatchProjection.d.ts +7 -0
- package/dist/fulfillment/dispatchProjection.js +18 -0
- package/dist/fulfillment/fulfillmentObservationEffects.d.ts +66 -0
- package/dist/fulfillment/fulfillmentObservationEffects.js +36 -0
- package/dist/fulfillment/handlerSchemas.d.ts +647 -0
- package/dist/fulfillment/handlerSchemas.js +213 -0
- package/dist/fulfillment/handlerSupport.d.ts +33 -0
- package/dist/fulfillment/handlerSupport.js +25 -0
- package/dist/fulfillment/index.d.ts +37 -0
- package/dist/fulfillment/index.js +37 -0
- package/dist/fulfillment/labelLifecycle.d.ts +24 -0
- package/dist/fulfillment/labelLifecycle.js +9 -0
- package/dist/fulfillment/lowStockEvidenceHandler.d.ts +36 -0
- package/dist/fulfillment/lowStockEvidenceHandler.js +46 -0
- package/dist/fulfillment/migrations.d.ts +27 -0
- package/dist/fulfillment/migrations.js +8 -0
- package/dist/fulfillment/nativeUnsupported.d.ts +10 -0
- package/dist/fulfillment/nativeUnsupported.js +18 -0
- package/dist/fulfillment/normalizedFulfillmentFacts.d.ts +2 -0
- package/dist/fulfillment/normalizedFulfillmentFacts.js +1 -0
- package/dist/fulfillment/observationProjection.d.ts +11 -0
- package/dist/fulfillment/observationProjection.js +37 -0
- package/dist/fulfillment/omnipack/capabilities.d.ts +3 -0
- package/dist/fulfillment/omnipack/capabilities.js +16 -0
- package/dist/fulfillment/omnipack/client.d.ts +37 -0
- package/dist/fulfillment/omnipack/client.js +162 -0
- package/dist/fulfillment/omnipack/index.d.ts +52 -0
- package/dist/fulfillment/omnipack/index.js +349 -0
- package/dist/fulfillment/omnipack/mappers.d.ts +73 -0
- package/dist/fulfillment/omnipack/mappers.js +177 -0
- package/dist/fulfillment/omnipack/observation.d.ts +21 -0
- package/dist/fulfillment/omnipack/observation.js +57 -0
- package/dist/fulfillment/omnipack/omnipackDispatchPayload.d.ts +36 -0
- package/dist/fulfillment/omnipack/omnipackDispatchPayload.js +63 -0
- package/dist/fulfillment/omnipack/omnipackOutboundOrderPayload.d.ts +83 -0
- package/dist/fulfillment/omnipack/omnipackOutboundOrderPayload.js +113 -0
- package/dist/fulfillment/omnipack/omnipackProductPackPayload.d.ts +11 -0
- package/dist/fulfillment/omnipack/omnipackProductPackPayload.js +38 -0
- package/dist/fulfillment/omnipack/omnipackProductPayload.d.ts +18 -0
- package/dist/fulfillment/omnipack/omnipackProductPayload.js +51 -0
- package/dist/fulfillment/omnipack/omnipackReconciliationPayload.d.ts +1 -0
- package/dist/fulfillment/omnipack/omnipackReconciliationPayload.js +3 -0
- package/dist/fulfillment/omnipack/omnipackStatusVocabulary.d.ts +4 -0
- package/dist/fulfillment/omnipack/omnipackStatusVocabulary.js +87 -0
- package/dist/fulfillment/omnipack/omnipackWebhookPayload.d.ts +9 -0
- package/dist/fulfillment/omnipack/omnipackWebhookPayload.js +76 -0
- package/dist/fulfillment/omnipack/productSyncMappers.d.ts +18 -0
- package/dist/fulfillment/omnipack/productSyncMappers.js +43 -0
- package/dist/fulfillment/omnipack/providerError.d.ts +25 -0
- package/dist/fulfillment/omnipack/providerError.js +47 -0
- package/dist/fulfillment/omnipack/trackingReferences.d.ts +16 -0
- package/dist/fulfillment/omnipack/trackingReferences.js +52 -0
- package/dist/fulfillment/paidFulfillmentRecovery.d.ts +67 -0
- package/dist/fulfillment/paidFulfillmentRecovery.js +154 -0
- package/dist/fulfillment/paidHandoff.d.ts +27 -0
- package/dist/fulfillment/paidHandoff.js +86 -0
- package/dist/fulfillment/paidHandoffPorts.d.ts +24 -0
- package/dist/fulfillment/paidHandoffPorts.js +3 -0
- package/dist/fulfillment/postgresDeliveryAlignment.d.ts +4 -0
- package/dist/fulfillment/postgresDeliveryAlignment.js +12 -0
- package/dist/fulfillment/postgresDispatchStore.d.ts +16 -0
- package/dist/fulfillment/postgresDispatchStore.js +44 -0
- package/dist/fulfillment/postgresEvidenceReadPort.d.ts +25 -0
- package/dist/fulfillment/postgresEvidenceReadPort.js +45 -0
- package/dist/fulfillment/postgresPaidFulfillmentRecovery.d.ts +25 -0
- package/dist/fulfillment/postgresPaidFulfillmentRecovery.js +26 -0
- package/dist/fulfillment/postgresPaidHandoffStore.d.ts +4 -0
- package/dist/fulfillment/postgresPaidHandoffStore.js +10 -0
- package/dist/fulfillment/postgresShipmentSpine.d.ts +63 -0
- package/dist/fulfillment/postgresShipmentSpine.js +232 -0
- package/dist/fulfillment/postgresStockSyncPort.d.ts +8 -0
- package/dist/fulfillment/postgresStockSyncPort.js +63 -0
- package/dist/fulfillment/productReconciliationEvidenceHandler.d.ts +36 -0
- package/dist/fulfillment/productReconciliationEvidenceHandler.js +50 -0
- package/dist/fulfillment/providerCapabilities.d.ts +9 -0
- package/dist/fulfillment/providerCapabilities.js +17 -0
- package/dist/fulfillment/providerCommandLedger.d.ts +143 -0
- package/dist/fulfillment/providerCommandLedger.js +14 -0
- package/dist/fulfillment/providerDispatchAcceptance.d.ts +26 -0
- package/dist/fulfillment/providerDispatchAcceptance.js +23 -0
- package/dist/fulfillment/providerDispatchError.d.ts +6 -0
- package/dist/fulfillment/providerDispatchError.js +5 -0
- package/dist/fulfillment/providerDispatchWorker.d.ts +31 -0
- package/dist/fulfillment/providerDispatchWorker.js +172 -0
- package/dist/fulfillment/providerProductSyncWorker.d.ts +28 -0
- package/dist/fulfillment/providerProductSyncWorker.js +122 -0
- package/dist/fulfillment/providerReconciliationError.d.ts +9 -0
- package/dist/fulfillment/providerReconciliationError.js +17 -0
- package/dist/fulfillment/providerReconciliationTerminalSkip.d.ts +2 -0
- package/dist/fulfillment/providerReconciliationTerminalSkip.js +6 -0
- package/dist/fulfillment/providerReconciliationTimestamp.d.ts +1 -0
- package/dist/fulfillment/providerReconciliationTimestamp.js +27 -0
- package/dist/fulfillment/providerReconciliationWorker.d.ts +82 -0
- package/dist/fulfillment/providerReconciliationWorker.js +151 -0
- package/dist/fulfillment/providerStockAvailability.d.ts +13 -0
- package/dist/fulfillment/providerStockAvailability.js +51 -0
- package/dist/fulfillment/providerStockSyncWorker.d.ts +37 -0
- package/dist/fulfillment/providerStockSyncWorker.js +186 -0
- package/dist/fulfillment/providerTrackingReferences.d.ts +15 -0
- package/dist/fulfillment/providerTrackingReferences.js +23 -0
- package/dist/fulfillment/replacementRefusals.d.ts +5 -0
- package/dist/fulfillment/replacementRefusals.js +38 -0
- package/dist/fulfillment/replacementShipmentHandler.d.ts +5 -0
- package/dist/fulfillment/replacementShipmentHandler.js +46 -0
- package/dist/fulfillment/schemaCapabilities.d.ts +13 -0
- package/dist/fulfillment/schemaCapabilities.js +7 -0
- package/dist/fulfillment/shipmentsOverviewHandler.d.ts +16 -0
- package/dist/fulfillment/shipmentsOverviewHandler.js +54 -0
- package/dist/fulfillment/statusMap.d.ts +2 -0
- package/dist/fulfillment/statusMap.js +1 -0
- package/dist/fulfillment/trackingProgress.d.ts +1 -0
- package/dist/fulfillment/trackingProgress.js +1 -0
- package/dist/fulfillment/verifiedIngress.d.ts +52 -0
- package/dist/fulfillment/verifiedIngress.js +66 -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 +41 -0
- package/dist/payments/stripe/index.js +264 -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 +73 -0
- package/dist/payments/tpay/index.js +294 -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/package.json +37 -2
- package/release-gates.json +105 -3
- package/sql/20260808210000_fulfillment_shipment_rail.sql +810 -0
- package/sql/20260810180000_payment_settlement_rail.sql +574 -0
- package/sql/20260815150000_payment_truth_reconciliation.sql +479 -0
- package/sql/20260819150000_fulfillment_shipment_sequence.sql +98 -0
- package/sql/20260826100000_late_payment_shifts_the_subscription_cycle.sql +215 -0
- package/sql/20261022080000_fulfillment_command_ledger.sql +786 -0
- package/sql/20261022090000_fulfillment_delivery_alignment.sql +1106 -0
- package/src/composition/composeBundle.ts +13 -0
- package/src/composition/fulfillmentContribution.ts +28 -0
- package/src/composition/index.ts +2 -0
- package/src/fulfillment/adapterConformance.ts +195 -0
- package/src/fulfillment/bindings.ts +26 -0
- package/src/fulfillment/commerceFulfillmentHandlers.ts +263 -0
- package/src/fulfillment/commerceFulfillmentReadModel.ts +257 -0
- package/src/fulfillment/contracts/alignment.ts +13 -0
- package/src/fulfillment/contracts/bindings.ts +30 -0
- package/src/fulfillment/contracts/capabilities.ts +15 -0
- package/src/fulfillment/contracts/conformance.ts +61 -0
- package/src/fulfillment/contracts/customerAlignment.ts +4 -0
- package/src/fulfillment/contracts/dispatch.ts +58 -0
- package/src/fulfillment/contracts/index.ts +18 -0
- package/src/fulfillment/contracts/label.ts +9 -0
- package/src/fulfillment/contracts/nativeUnsupported.ts +6 -0
- package/src/fulfillment/contracts/observation.ts +18 -0
- package/src/fulfillment/contracts/operator.ts +31 -0
- package/src/fulfillment/contracts/read.ts +42 -0
- package/src/fulfillment/contracts/readback.ts +11 -0
- package/src/fulfillment/contracts/replacement.ts +15 -0
- package/src/fulfillment/contracts/store.ts +26 -0
- package/src/fulfillment/contracts/sync.ts +61 -0
- package/src/fulfillment/currentFulfillmentParcel.ts +2 -0
- package/src/fulfillment/delayReadModel.ts +60 -0
- package/src/fulfillment/deliveryAlignmentReadModel.ts +15 -0
- package/src/fulfillment/dispatchProjection.ts +20 -0
- package/src/fulfillment/fulfillmentObservationEffects.ts +86 -0
- package/src/fulfillment/handlerSchemas.ts +255 -0
- package/src/fulfillment/handlerSupport.ts +25 -0
- package/src/fulfillment/index.ts +38 -0
- package/src/fulfillment/labelLifecycle.ts +11 -0
- package/src/fulfillment/lowStockEvidenceHandler.ts +60 -0
- package/src/fulfillment/migrations.ts +8 -0
- package/src/fulfillment/nativeUnsupported.ts +23 -0
- package/src/fulfillment/normalizedFulfillmentFacts.ts +2 -0
- package/src/fulfillment/observationProjection.ts +41 -0
- package/src/fulfillment/omnipack/capabilities.ts +18 -0
- package/src/fulfillment/omnipack/client.ts +258 -0
- package/src/fulfillment/omnipack/index.ts +312 -0
- package/src/fulfillment/omnipack/mappers.ts +261 -0
- package/src/fulfillment/omnipack/observation.ts +61 -0
- package/src/fulfillment/omnipack/omnipackDispatchPayload.ts +93 -0
- package/src/fulfillment/omnipack/omnipackOutboundOrderPayload.ts +194 -0
- package/src/fulfillment/omnipack/omnipackProductPackPayload.ts +47 -0
- package/src/fulfillment/omnipack/omnipackProductPayload.ts +67 -0
- package/src/fulfillment/omnipack/omnipackReconciliationPayload.ts +7 -0
- package/src/fulfillment/omnipack/omnipackStatusVocabulary.ts +103 -0
- package/src/fulfillment/omnipack/omnipackWebhookPayload.ts +73 -0
- package/src/fulfillment/omnipack/productSyncMappers.ts +74 -0
- package/src/fulfillment/omnipack/providerError.ts +64 -0
- package/src/fulfillment/omnipack/trackingReferences.ts +77 -0
- package/src/fulfillment/paidFulfillmentRecovery.ts +252 -0
- package/src/fulfillment/paidHandoff.ts +122 -0
- package/src/fulfillment/paidHandoffPorts.ts +21 -0
- package/src/fulfillment/postgresDeliveryAlignment.ts +14 -0
- package/src/fulfillment/postgresDispatchStore.ts +55 -0
- package/src/fulfillment/postgresEvidenceReadPort.ts +59 -0
- package/src/fulfillment/postgresPaidFulfillmentRecovery.ts +29 -0
- package/src/fulfillment/postgresPaidHandoffStore.ts +11 -0
- package/src/fulfillment/postgresShipmentSpine.ts +321 -0
- package/src/fulfillment/postgresStockSyncPort.ts +65 -0
- package/src/fulfillment/productReconciliationEvidenceHandler.ts +64 -0
- package/src/fulfillment/providerCapabilities.ts +18 -0
- package/src/fulfillment/providerCommandLedger.ts +53 -0
- package/src/fulfillment/providerDispatchAcceptance.ts +31 -0
- package/src/fulfillment/providerDispatchError.ts +7 -0
- package/src/fulfillment/providerDispatchWorker.ts +131 -0
- package/src/fulfillment/providerProductSyncWorker.ts +154 -0
- package/src/fulfillment/providerReconciliationError.ts +20 -0
- package/src/fulfillment/providerReconciliationTerminalSkip.ts +8 -0
- package/src/fulfillment/providerReconciliationTimestamp.ts +34 -0
- package/src/fulfillment/providerReconciliationWorker.ts +150 -0
- package/src/fulfillment/providerStockAvailability.ts +60 -0
- package/src/fulfillment/providerStockSyncWorker.ts +222 -0
- package/src/fulfillment/providerTrackingReferences.ts +31 -0
- package/src/fulfillment/replacementRefusals.ts +43 -0
- package/src/fulfillment/replacementShipmentHandler.ts +47 -0
- package/src/fulfillment/schemaCapabilities.ts +7 -0
- package/src/fulfillment/shipmentsOverviewHandler.ts +73 -0
- package/src/fulfillment/statusMap.ts +2 -0
- package/src/fulfillment/trackingProgress.ts +1 -0
- package/src/fulfillment/verifiedIngress.ts +80 -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 +265 -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 +293 -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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { aggregateAtpCompatibleStock, emptyLocal, stockMovementWindow } from "./providerStockAvailability.js";
|
|
2
|
+
import type { FulfillmentStockSyncPort, FulfillmentStockSyncProvider, LocalStock, StockSnapshot, StockMovement } from "./contracts/index.js";
|
|
3
|
+
type Threshold = Parameters<FulfillmentStockSyncPort["resolveLowStockEvidence"]>[0]["activeThresholdKinds"][number];
|
|
4
|
+
interface StockSyncResult {
|
|
5
|
+
ok: boolean; checked: number; updated: number; mismatches: number; reservationCoverage: number;
|
|
6
|
+
unclassified: number; lowStock: number; movements: number; replayed: number; failures: number;
|
|
7
|
+
providerCalls: number; readBacks: number; skipped: boolean; syncRunId: string;
|
|
8
|
+
lastMovementOccurredAt: string | null; reason?: string;
|
|
9
|
+
}
|
|
10
|
+
export async function runFulfillmentStockSyncWorker(input: {
|
|
11
|
+
port: FulfillmentStockSyncPort;
|
|
12
|
+
provider: FulfillmentStockSyncProvider;
|
|
13
|
+
movementBatchSize: number;
|
|
14
|
+
staleSyncThresholdHours?: number;
|
|
15
|
+
now?: () => Date;
|
|
16
|
+
compatibility: {
|
|
17
|
+
syncRunId(now: Date): string; staleEvidenceSku: string; timeZone: string;
|
|
18
|
+
currentKey(run: string, sku: string): string; snapshotKey(run: string, sku: string): string;
|
|
19
|
+
lowStockKey(sku: string, threshold: Threshold): string; staleKey(lastSyncedAt: string): string;
|
|
20
|
+
};
|
|
21
|
+
}): Promise<StockSyncResult> {
|
|
22
|
+
const now = input.now?.() ?? new Date();
|
|
23
|
+
const staleThresholdHours = input.staleSyncThresholdHours && input.staleSyncThresholdHours > 0
|
|
24
|
+
? input.staleSyncThresholdHours
|
|
25
|
+
: 6;
|
|
26
|
+
const syncRunId = input.compatibility.syncRunId(now);
|
|
27
|
+
const result: StockSyncResult = {
|
|
28
|
+
ok: true,
|
|
29
|
+
checked: 0,
|
|
30
|
+
updated: 0,
|
|
31
|
+
mismatches: 0,
|
|
32
|
+
reservationCoverage: 0,
|
|
33
|
+
unclassified: 0,
|
|
34
|
+
lowStock: 0,
|
|
35
|
+
movements: 0,
|
|
36
|
+
replayed: 0,
|
|
37
|
+
failures: 0,
|
|
38
|
+
providerCalls: 0,
|
|
39
|
+
readBacks: 0,
|
|
40
|
+
skipped: false,
|
|
41
|
+
syncRunId,
|
|
42
|
+
lastMovementOccurredAt: null,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const previousCursor = await input.port.readCursor();
|
|
46
|
+
|
|
47
|
+
// Stale-sync alert: if the last successful sync aged past the threshold, open a stale_sync
|
|
48
|
+
// low-stock evidence row (evidence/warn — never blocks sales); a healthy run resolves it
|
|
49
|
+
// below. A never-synced state (no prior cursor) is not "stale".
|
|
50
|
+
const lastSyncedMs = previousCursor?.lastStockSyncedAt ? Date.parse(previousCursor.lastStockSyncedAt) : null;
|
|
51
|
+
const staleGapHours =
|
|
52
|
+
lastSyncedMs !== null && !Number.isNaN(lastSyncedMs) ? (now.getTime() - lastSyncedMs) / 3_600_000 : null;
|
|
53
|
+
const syncIsStale = staleGapHours !== null && staleGapHours >= staleThresholdHours;
|
|
54
|
+
if (syncIsStale) {
|
|
55
|
+
await input.port.recordLowStockEvidence({
|
|
56
|
+
requestKey: input.compatibility.staleKey(previousCursor!.lastStockSyncedAt!),
|
|
57
|
+
skuRef: input.compatibility.staleEvidenceSku,
|
|
58
|
+
thresholdKind: "stale_sync",
|
|
59
|
+
severity: "warning",
|
|
60
|
+
providerForSaleQuantity: 0,
|
|
61
|
+
localAvailableQuantity: 0,
|
|
62
|
+
syncRunId,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await input.port.recordCursor({
|
|
67
|
+
status: "running",
|
|
68
|
+
lastStockSyncedAt: null,
|
|
69
|
+
lastMovementOccurredAt: null,
|
|
70
|
+
continuation: { nextRef: null, startLocalDate: null, endLocalDate: null },
|
|
71
|
+
coverageGapSkus: [], refusal: null,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const [localStock, providerStock] = await Promise.all([
|
|
75
|
+
input.port.readLocalInventoryStock(now.toISOString()),
|
|
76
|
+
input.provider.getStock(),
|
|
77
|
+
]);
|
|
78
|
+
result.providerCalls += 1;
|
|
79
|
+
|
|
80
|
+
const stockItems = providerStock.filter((item) => item.skuRef.trim());
|
|
81
|
+
const skus = [...new Set(stockItems.map((item) => item.skuRef))];
|
|
82
|
+
const [inventoryClasses, activeReservations] = await Promise.all([
|
|
83
|
+
input.port.readSkuInventoryClasses(skus),
|
|
84
|
+
input.port.readActiveReservations(skus, now.toISOString()),
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const movements = await input.provider.getStockMovements({ ...stockMovementWindow(previousCursor, now, input.compatibility.timeZone), size: input.movementBatchSize });
|
|
88
|
+
result.providerCalls += 1;
|
|
89
|
+
result.movements = movements.length;
|
|
90
|
+
result.lastMovementOccurredAt = latestOccurredAt(movements);
|
|
91
|
+
|
|
92
|
+
const localBySku = aggregateAtpCompatibleStock(localStock, now);
|
|
93
|
+
const coverageCandidates: Array<{
|
|
94
|
+
stock: StockSnapshot;
|
|
95
|
+
local: LocalStock;
|
|
96
|
+
activeReserved: number;
|
|
97
|
+
}> = [];
|
|
98
|
+
const currentCoverageGapSkus = new Set<string>();
|
|
99
|
+
const packagingSkus = new Set<string>();
|
|
100
|
+
const previousCoverageGapSkus = previousCursor?.status === "succeeded"
|
|
101
|
+
? readStringSet(previousCursor.coverageGapSkus)
|
|
102
|
+
: new Set<string>();
|
|
103
|
+
|
|
104
|
+
for (const stock of stockItems) {
|
|
105
|
+
try {
|
|
106
|
+
const local = localBySku.get(stock.skuRef) ?? emptyLocal(stock.skuRef);
|
|
107
|
+
const inventoryClass = inventoryClasses.get(stock.skuRef) ?? null;
|
|
108
|
+
const activeReserved = activeReservations.get(stock.skuRef) ?? 0;
|
|
109
|
+
const staleAfter = new Date(now.getTime() + staleThresholdHours * 3_600_000).toISOString();
|
|
110
|
+
const current = await input.port.recordProviderStockCurrent({
|
|
111
|
+
requestKey: input.compatibility.currentKey(syncRunId, stock.skuRef),
|
|
112
|
+
snapshot: stock, inventoryClass, lastSyncedAt: now.toISOString(), staleAfter,
|
|
113
|
+
});
|
|
114
|
+
const snapshot = await input.port.recordStockSnapshot({
|
|
115
|
+
requestKey: input.compatibility.snapshotKey(syncRunId, stock.skuRef),
|
|
116
|
+
snapshot: stock, local, mismatchKind: "none", inventoryClass,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
result.checked += 1;
|
|
120
|
+
result.updated += (snapshot.replayed ? 0 : 1) + (current.replayed ? 0 : 1);
|
|
121
|
+
if (snapshot.replayed) result.replayed += 1;
|
|
122
|
+
if (current.replayed) result.replayed += 1;
|
|
123
|
+
if (snapshot.readBack) result.readBacks += 1;
|
|
124
|
+
if (current.readBack) result.readBacks += 1;
|
|
125
|
+
if (inventoryClass === null) result.unclassified += 1;
|
|
126
|
+
if (inventoryClass === "packaging") packagingSkus.add(stock.skuRef);
|
|
127
|
+
if (inventoryClass === "sellable") {
|
|
128
|
+
coverageCandidates.push({ stock, local, activeReserved });
|
|
129
|
+
if (activeReserved > stock.forSaleQuantity) currentCoverageGapSkus.add(stock.skuRef);
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
result.ok = false;
|
|
133
|
+
result.failures += 1;
|
|
134
|
+
result.reason = result.reason ?? "stock_sync_operation_failed";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (result.ok) {
|
|
139
|
+
for (const { stock, local, activeReserved } of coverageCandidates) {
|
|
140
|
+
const localAvailable = availableQuantity(local);
|
|
141
|
+
const confirmedCoverageGap = currentCoverageGapSkus.has(stock.skuRef) && previousCoverageGapSkus.has(stock.skuRef);
|
|
142
|
+
const lowStockSignals = lowStockSignalsFor(stock, local, activeReserved, confirmedCoverageGap);
|
|
143
|
+
const activeThresholdKinds: Threshold[] = lowStockSignals.map((signal) => signal.thresholdKind);
|
|
144
|
+
if (currentCoverageGapSkus.has(stock.skuRef) && !activeThresholdKinds.includes("reservation_coverage")) {
|
|
145
|
+
activeThresholdKinds.push("reservation_coverage");
|
|
146
|
+
}
|
|
147
|
+
for (const signal of lowStockSignals) {
|
|
148
|
+
const evidence = await input.port.recordLowStockEvidence({
|
|
149
|
+
requestKey: input.compatibility.lowStockKey(stock.skuRef, signal.thresholdKind),
|
|
150
|
+
skuRef: stock.skuRef, thresholdKind: signal.thresholdKind, severity: signal.severity,
|
|
151
|
+
providerForSaleQuantity: stock.forSaleQuantity, localAvailableQuantity: localAvailable, syncRunId,
|
|
152
|
+
});
|
|
153
|
+
if (evidence.replayed) result.replayed += 1;
|
|
154
|
+
result.lowStock += evidence.replayed ? 0 : 1;
|
|
155
|
+
if (signal.thresholdKind === "reservation_coverage") result.reservationCoverage += 1;
|
|
156
|
+
}
|
|
157
|
+
await input.port.resolveLowStockEvidence({
|
|
158
|
+
skuRef: stock.skuRef,
|
|
159
|
+
activeThresholdKinds,
|
|
160
|
+
syncRunId,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
for (const sku of packagingSkus) {
|
|
164
|
+
await input.port.resolveLowStockEvidence({
|
|
165
|
+
skuRef: sku,
|
|
166
|
+
activeThresholdKinds: [],
|
|
167
|
+
syncRunId,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await input.port.recordCursor({
|
|
173
|
+
status: result.ok ? "succeeded" : "failed",
|
|
174
|
+
lastStockSyncedAt: result.ok ? now.toISOString() : null,
|
|
175
|
+
lastMovementOccurredAt: result.lastMovementOccurredAt,
|
|
176
|
+
continuation: { nextRef: null, startLocalDate: null, endLocalDate: null },
|
|
177
|
+
coverageGapSkus: result.ok ? [...currentCoverageGapSkus].sort() : [],
|
|
178
|
+
refusal: result.ok ? null : { reason: "io_failure", phase: "sync", retryable: true },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Recovery: a healthy run (succeeded, not itself stale) closes any open stale_sync row.
|
|
182
|
+
if (result.ok && !syncIsStale) {
|
|
183
|
+
await input.port.resolveLowStockEvidence({
|
|
184
|
+
skuRef: input.compatibility.staleEvidenceSku,
|
|
185
|
+
activeThresholdKinds: [],
|
|
186
|
+
syncRunId,
|
|
187
|
+
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function lowStockSignalsFor(
|
|
195
|
+
stock: StockSnapshot,
|
|
196
|
+
local: LocalStock,
|
|
197
|
+
activeReserved: number,
|
|
198
|
+
confirmedCoverageGap: boolean,
|
|
199
|
+
): Array<{ thresholdKind: "safety_stock" | "reservation_coverage"; severity: "warning" | "critical" }> {
|
|
200
|
+
const signals: Array<{ thresholdKind: "safety_stock" | "reservation_coverage"; severity: "warning" | "critical" }> = [];
|
|
201
|
+
const remainingAfterReservations = Math.max(0, stock.forSaleQuantity - activeReserved);
|
|
202
|
+
if (local.safetyStock > 0 && remainingAfterReservations <= local.safetyStock) {
|
|
203
|
+
signals.push({ thresholdKind: "safety_stock", severity: remainingAfterReservations <= 0 ? "critical" : "warning" });
|
|
204
|
+
}
|
|
205
|
+
if (confirmedCoverageGap) signals.push({ thresholdKind: "reservation_coverage", severity: "critical" });
|
|
206
|
+
return signals;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function readStringSet(value: unknown): Set<string> {
|
|
210
|
+
return new Set(Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.trim().length > 0) : []);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function availableQuantity(balance: LocalStock): number {
|
|
214
|
+
return Math.max(0, balance.onHand - balance.reserved - balance.unavailable - balance.safetyStock);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function latestOccurredAt(movements: StockMovement[]): string | null {
|
|
218
|
+
return movements
|
|
219
|
+
.map((movement) => movement.occurredAt)
|
|
220
|
+
.filter((value): value is string => Boolean(value))
|
|
221
|
+
.sort((a, b) => Date.parse(b) - Date.parse(a))[0] ?? null;
|
|
222
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FulfillmentTrackingReference } from "./contracts/index.js";
|
|
2
|
+
|
|
3
|
+
export function trackingReferenceComplete(ref: FulfillmentTrackingReference): boolean {
|
|
4
|
+
return Boolean(ref.carrierRef && ref.publicUrl && ref.serviceRef);
|
|
5
|
+
}
|
|
6
|
+
/** Provider evidence wins as a unit; private carrier/URL codecs stay injected. */
|
|
7
|
+
export function enrichTrackingReference(
|
|
8
|
+
ref: FulfillmentTrackingReference,
|
|
9
|
+
deliveryCarrier: { carrierRef: string | null; serviceRef: string | null } | null,
|
|
10
|
+
codecs: {
|
|
11
|
+
normalizeCarrier(input: { carrierRef: string | null; serviceRef: string | null }): string | null;
|
|
12
|
+
publicUrl(carrierRef: string | null, trackingNumber: string): string | null;
|
|
13
|
+
},
|
|
14
|
+
): FulfillmentTrackingReference {
|
|
15
|
+
const providerCarrier = ref.carrierRef ?? codecs.normalizeCarrier(ref);
|
|
16
|
+
const carrierRef = providerCarrier ?? codecs.normalizeCarrier({ carrierRef: deliveryCarrier?.carrierRef ?? null, serviceRef: deliveryCarrier?.serviceRef ?? null });
|
|
17
|
+
return { trackingNumber: ref.trackingNumber, publicUrl: ref.publicUrl ?? codecs.publicUrl(carrierRef, ref.trackingNumber),
|
|
18
|
+
carrierRef, serviceRef: ref.serviceRef ?? (providerCarrier ? null : deliveryCarrier?.serviceRef ?? null) };
|
|
19
|
+
}
|
|
20
|
+
/** First occurrence wins, including its selection, after trimming the number. */
|
|
21
|
+
export function uniqueTrackingReferences(refs: readonly FulfillmentTrackingReference[]): FulfillmentTrackingReference[] {
|
|
22
|
+
const seen = new Set<string>();
|
|
23
|
+
const result: FulfillmentTrackingReference[] = [];
|
|
24
|
+
for (const ref of refs) {
|
|
25
|
+
const trackingNumber = ref.trackingNumber.trim();
|
|
26
|
+
if (!trackingNumber || seen.has(trackingNumber)) continue;
|
|
27
|
+
seen.add(trackingNumber);
|
|
28
|
+
result.push({ trackingNumber, publicUrl: ref.publicUrl ?? null, carrierRef: ref.carrierRef ?? null, serviceRef: ref.serviceRef ?? null });
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const FULFILLMENT_REPLACEMENT_COMMAND_REFUSALS = [
|
|
2
|
+
"invalid_input",
|
|
3
|
+
"idempotency_conflict",
|
|
4
|
+
"order_not_found",
|
|
5
|
+
"order_not_replaceable",
|
|
6
|
+
"missing_client",
|
|
7
|
+
"missing_shipping_address",
|
|
8
|
+
"shipping_address_not_found",
|
|
9
|
+
"payment_not_succeeded",
|
|
10
|
+
"subscription_cycle_not_paid",
|
|
11
|
+
"no_parcel_to_replace",
|
|
12
|
+
"undispatched_replacement_exists",
|
|
13
|
+
"missing_order_items",
|
|
14
|
+
"missing_catalog_sku",
|
|
15
|
+
"missing_inventory_reservation",
|
|
16
|
+
"blocked_by_payment_not_succeeded_hold",
|
|
17
|
+
"blocked_by_inventory_review_hold",
|
|
18
|
+
"blocked_by_risk_review_hold",
|
|
19
|
+
"blocked_by_address_review_hold",
|
|
20
|
+
"blocked_by_manual_support_hold",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export const FULFILLMENT_REPLACEMENT_RESERVATION_REFUSALS = [
|
|
24
|
+
"inventory_external_provider_insufficient_available_stock",
|
|
25
|
+
"inventory_external_provider_stock_stale",
|
|
26
|
+
"inventory_external_provider_stock_missing",
|
|
27
|
+
"inventory_external_provider_sku_not_found",
|
|
28
|
+
"inventory_external_provider_location_missing",
|
|
29
|
+
"inventory_reservation_insufficient_available_stock",
|
|
30
|
+
"inventory_reservation_allocation_incomplete",
|
|
31
|
+
"inventory_reservation_payment_status_not_reservable",
|
|
32
|
+
"inventory_reservation_invalid_quantity",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
export const UNSPECIFIED_FULFILLMENT_REPLACEMENT_REFUSAL = "unspecified_refusal";
|
|
36
|
+
|
|
37
|
+
export const FULFILLMENT_REPLACEMENT_REFUSALS = [
|
|
38
|
+
...FULFILLMENT_REPLACEMENT_COMMAND_REFUSALS,
|
|
39
|
+
...FULFILLMENT_REPLACEMENT_RESERVATION_REFUSALS,
|
|
40
|
+
UNSPECIFIED_FULFILLMENT_REPLACEMENT_REFUSAL,
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
export type FulfillmentReplacementRefusal = (typeof FULFILLMENT_REPLACEMENT_REFUSALS)[number];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { adminCommerceOrderRequestReplacementShipmentRequestSchema, adminCommerceOrderRequestReplacementShipmentResponseSchema } from './handlerSchemas.js';
|
|
2
|
+
import { authorize, CommerceFulfillmentConflictError, isNativeUnsupported, type BaseDeps, type FulfillmentHandlerRequest, type FulfillmentHandlerResponder } from './handlerSupport.js';
|
|
3
|
+
import type { FulfillmentReplacementPort } from './contracts/replacement.js';
|
|
4
|
+
export function createAdminCommerceOrderRequestReplacementShipmentHandler<Response>({
|
|
5
|
+
authorizeAdmin,
|
|
6
|
+
respond,
|
|
7
|
+
omsPort,
|
|
8
|
+
}: BaseDeps<Response> & {
|
|
9
|
+
omsPort: FulfillmentReplacementPort;
|
|
10
|
+
}) {
|
|
11
|
+
const { sendMethodNotAllowed } = respond;
|
|
12
|
+
return async function handler(req: FulfillmentHandlerRequest, res: Response): Promise<void> {
|
|
13
|
+
if (req.method !== "POST") return sendMethodNotAllowed(res, ["POST"]);
|
|
14
|
+
const auth = await authorize(req, res, authorizeAdmin, respond);
|
|
15
|
+
if (!auth) return;
|
|
16
|
+
|
|
17
|
+
const request = parseOmsRequest(res, respond, adminCommerceOrderRequestReplacementShipmentRequestSchema, req.body ?? {}, "Invalid admin commerce replacement shipment request");
|
|
18
|
+
if (request === undefined) return;
|
|
19
|
+
|
|
20
|
+
await sendOmsMutationResponse(
|
|
21
|
+
res,
|
|
22
|
+
respond,
|
|
23
|
+
() => omsPort.requestReplacementShipment({ ...request, actorUserId: auth.userId }),
|
|
24
|
+
adminCommerceOrderRequestReplacementShipmentResponseSchema,
|
|
25
|
+
"Admin commerce replacement shipment returned invalid response",
|
|
26
|
+
"Admin commerce replacement shipment failed",
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseOmsRequest<Response, T>(res: Response, respond: FulfillmentHandlerResponder<Response>, schema: { safeParse(input: unknown): { success: true; data: T } | { success: false; error: { flatten(): unknown } } }, input: unknown, message: string): T | undefined {
|
|
32
|
+
const request = schema.safeParse(input);
|
|
33
|
+
if (!request.success) { respond.sendBffError(res, 'BAD_REQUEST', message, { details: request.error.flatten() }); return undefined; }
|
|
34
|
+
return request.data;
|
|
35
|
+
}
|
|
36
|
+
async function sendOmsMutationResponse<Response>(res: Response, respond: FulfillmentHandlerResponder<Response>, action: () => Promise<unknown>, schema: typeof adminCommerceOrderRequestReplacementShipmentResponseSchema, invalidMessage: string, failureMessage: string) {
|
|
37
|
+
try {
|
|
38
|
+
const result = await action();
|
|
39
|
+
if (isNativeUnsupported(result)) { respond.sendBffError(res, 'UPSTREAM_UNAVAILABLE', 'Native fulfillment operation unsupported', { details: result }); return; }
|
|
40
|
+
const response = schema.safeParse(result);
|
|
41
|
+
if (!response.success) { respond.sendBffError(res, 'INVALID_RESPONSE', invalidMessage); return; }
|
|
42
|
+
respond.sendBffSuccess(res, response.data);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof CommerceFulfillmentConflictError) { respond.sendBffError(res, 'CONFLICT', error.message, { details: error.details }); return; }
|
|
45
|
+
respond.sendBffError(res, 'UPSTREAM_UNAVAILABLE', failureMessage);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Sparse projections require the full canonical graph and a trusted native binding. */
|
|
2
|
+
export const FULFILLMENT_NATIVE_CAPABILITIES = {
|
|
3
|
+
shipment: 'supported', commandFence: 'supported', deliveryAlignment: 'supported',
|
|
4
|
+
stockEvidence: 'supported', observationPersistence: 'supported',
|
|
5
|
+
localInventoryBalances: 'host_required', dispatchContactPreparation: 'host_required',
|
|
6
|
+
productPacks: 'unsupported', providerAccess: 'unsupported', automaticReplacementConfirmation: 'unsupported',
|
|
7
|
+
} as const;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { isNativeUnsupported, type FulfillmentHandlerRequest, type FulfillmentHandlerResponder } from './handlerSupport.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { FulfillmentShipmentsOverviewPort, FulfillmentShipmentsOverviewResponse } from './contracts/read.js';
|
|
4
|
+
|
|
5
|
+
const adminShipmentsOverviewRequestSchema = z.object({
|
|
6
|
+
shippedFilter: z.enum(['today', '7d', '30d', 'all']).default('7d'),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export interface FulfillmentShipmentsOverviewHandlerDeps<Response> {
|
|
10
|
+
respond: FulfillmentHandlerResponder<Response>;
|
|
11
|
+
overviewPort: FulfillmentShipmentsOverviewPort;
|
|
12
|
+
responseSchema: { safeParse(input: unknown): { success: true; data: FulfillmentShipmentsOverviewResponse } | { success: false } };
|
|
13
|
+
authorizeAdmin: (req: FulfillmentHandlerRequest) => Promise<boolean>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createFulfillmentShipmentsOverviewHandler<Response>({
|
|
17
|
+
respond,
|
|
18
|
+
overviewPort,
|
|
19
|
+
responseSchema: adminShipmentsOverviewResponseSchema,
|
|
20
|
+
authorizeAdmin,
|
|
21
|
+
}: FulfillmentShipmentsOverviewHandlerDeps<Response>) {
|
|
22
|
+
const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = respond;
|
|
23
|
+
return async function handler(req: FulfillmentHandlerRequest, res: Response): Promise<void> {
|
|
24
|
+
if (req.method !== "GET") {
|
|
25
|
+
sendMethodNotAllowed(res, ["GET"]);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let authorized = false;
|
|
30
|
+
try {
|
|
31
|
+
authorized = await authorizeAdmin(req);
|
|
32
|
+
} catch {
|
|
33
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin authorization failed");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!authorized) {
|
|
38
|
+
sendBffError(res, "UNAUTHORIZED", "Admin session required");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const request = adminShipmentsOverviewRequestSchema.safeParse({
|
|
43
|
+
shippedFilter: scalarQuery(req.query.shippedFilter),
|
|
44
|
+
});
|
|
45
|
+
if (!request.success) {
|
|
46
|
+
sendBffError(res, "BAD_REQUEST", "Invalid shipments overview request", {
|
|
47
|
+
details: request.error.flatten(),
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const result = await overviewPort.getShipmentsOverview(request.data);
|
|
54
|
+
if (isNativeUnsupported(result)) {
|
|
55
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Native fulfillment operation unsupported", { details: result });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const response = adminShipmentsOverviewResponseSchema.safeParse(result);
|
|
59
|
+
if (!response.success) {
|
|
60
|
+
sendBffError(res, "INVALID_RESPONSE", "Shipments overview returned invalid response");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
sendBffSuccess(res, response.data);
|
|
65
|
+
} catch {
|
|
66
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Shipments overview read failed");
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function scalarQuery(value: string | string[] | undefined): string | undefined {
|
|
72
|
+
return Array.isArray(value) ? value[0] : value;
|
|
73
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { FULFILLMENT_STATUS_MAP, CANONICAL_LOCAL_FULFILLMENT_STATUSES, CUSTOMER_STEP_SEQUENCE, canonicalStatusToken, stepForToken, customerStepFromSignals, phaseIndexForStep, stepLabelKey, timelineLabelForStep, trackingPhaseKeyForStep, stepUsesTruckIcon } from "@openlup/core/fulfillment";
|
|
2
|
+
export type { FulfillmentStatusStage, FulfillmentStatusMap } from "@openlup/core/fulfillment";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { fulfillmentProgressPresentation } from "@openlup/core/fulfillment";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { FulfillmentObservation, FulfillmentObservationPort, FulfillmentRefusal } from "./contracts/index.js";
|
|
2
|
+
import { applyFulfillmentObservationEffects } from "./fulfillmentObservationEffects.js";
|
|
3
|
+
import { observationStatusPolicy, projectObservation } from "./observationProjection.js";
|
|
4
|
+
import { uniqueTrackingReferences } from "./providerTrackingReferences.js";
|
|
5
|
+
|
|
6
|
+
type EffectInput = Parameters<typeof applyFulfillmentObservationEffects<FulfillmentObservation["fact"]["canonicalStatus"], "in_transit" | "delivered">>[0];
|
|
7
|
+
type DispatchRef = { orderRef: string; fulfillmentRef: string; dispatchRef: string; externalRef: string | null };
|
|
8
|
+
/** Adapter verification owns auth-before-parse; the host owns durable replay. */
|
|
9
|
+
export async function verifyAndNormalizeFulfillmentObservation(input: {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
provider: FulfillmentObservationPort;
|
|
12
|
+
verification: Parameters<FulfillmentObservationPort["verifyAndNormalize"]>[0];
|
|
13
|
+
localPersistence: {
|
|
14
|
+
/** G17: atomic insert by eventKey; retries keep the first stored firstReceivedAt. */
|
|
15
|
+
recordObservation(observation: FulfillmentObservation): Promise<{ replayed: boolean }>;
|
|
16
|
+
/** Idempotent; only called after completion or a classified superseding conflict. */
|
|
17
|
+
markProcessed(eventKey: string, outcome: { status: "processed" | "ignored"; refusal?: FulfillmentRefusal }): Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
lifecycle: {
|
|
20
|
+
findDispatchRef(observation: FulfillmentObservation): Promise<DispatchRef | null>;
|
|
21
|
+
acknowledgeDispatchAcceptance(ref: DispatchRef, observation: FulfillmentObservation): Promise<void>;
|
|
22
|
+
recordStatusEvidence(ref: DispatchRef, observation: FulfillmentObservation): Promise<{ replayed: boolean }>;
|
|
23
|
+
effects: EffectInput["port"];
|
|
24
|
+
keys: { stock(ref: string): string; handoff(ref: string): string; tracking(eventKey: string, number: string): string };
|
|
25
|
+
issueAccountingInvoice?(ref: string): Promise<void>;
|
|
26
|
+
classifyFailure(error: unknown): "stock_contention" | "state_conflict" | "other";
|
|
27
|
+
annotateStockContention?(eventKey: string, fulfillmentRef: string): Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
}): Promise<{ kind: "refused"; refusal: FulfillmentRefusal } | { kind: "processed" | "ignored" | "superseded"; observation: FulfillmentObservation; replayed: boolean; trackingRefs: number; trackingReadBacks: number }> {
|
|
30
|
+
if (!input.enabled) return { kind: "refused", refusal: { reason: "disabled", phase: "verify", retryable: false } };
|
|
31
|
+
const verified = await input.provider.verifyAndNormalize(input.verification);
|
|
32
|
+
if (verified.kind === "refused") return verified;
|
|
33
|
+
const observation = projectObservation(verified.observation);
|
|
34
|
+
const inbound = await input.localPersistence.recordObservation(observation);
|
|
35
|
+
const ref = await input.lifecycle.findDispatchRef(observation);
|
|
36
|
+
if (!ref) {
|
|
37
|
+
await input.localPersistence.markProcessed(observation.eventKey, { status: "ignored", refusal: { reason: "local_missing", phase: "apply", retryable: false } });
|
|
38
|
+
return { kind: "ignored", observation, replayed: inbound.replayed, trackingRefs: 0, trackingReadBacks: 0 };
|
|
39
|
+
}
|
|
40
|
+
const externalRef = observation.correlation.externalRef?.trim() || ref.externalRef?.trim();
|
|
41
|
+
if (!externalRef) throw new TypeError("fulfillment_dispatch_acceptance_external_ref_missing");
|
|
42
|
+
await input.lifecycle.acknowledgeDispatchAcceptance(ref, observation);
|
|
43
|
+
const statusEvidence = await input.lifecycle.recordStatusEvidence(ref, observation);
|
|
44
|
+
let trackingRefs = 0;
|
|
45
|
+
let trackingReadBacks = 0;
|
|
46
|
+
const effectPort: EffectInput["port"] = {
|
|
47
|
+
...input.lifecycle.effects,
|
|
48
|
+
async markProviderStockConsumed(request) {
|
|
49
|
+
try { return await input.lifecycle.effects.markProviderStockConsumed(request); }
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (input.lifecycle.classifyFailure(error) === "stock_contention") {
|
|
52
|
+
try {
|
|
53
|
+
await input.lifecycle.annotateStockContention?.(observation.eventKey, ref.fulfillmentRef);
|
|
54
|
+
} catch { /* Annotation is best-effort; preserve the original stock failure. */ }
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const localStatus = observation.fact.canonicalStatus;
|
|
61
|
+
const effects: EffectInput = {
|
|
62
|
+
port: effectPort, localStatus, ids: { orderId: ref.orderRef, fulfillmentOrderId: ref.fulfillmentRef }, observation,
|
|
63
|
+
trackingRefs: uniqueTrackingReferences(observation.trackingRefs).map(tracking => ({ trackingNumber: tracking.trackingNumber, trackingUrl: tracking.publicUrl, carrierKind: tracking.carrierRef, service: tracking.serviceRef })),
|
|
64
|
+
stockIdempotencyKey: input.lifecycle.keys.stock(ref.fulfillmentRef), handoffIdempotencyKey: input.lifecycle.keys.handoff(ref.fulfillmentRef),
|
|
65
|
+
trackingIdempotencyKey: number => input.lifecycle.keys.tracking(observation.eventKey, number),
|
|
66
|
+
issueAccountingInvoice: input.lifecycle.issueAccountingInvoice ? () => input.lifecycle.issueAccountingInvoice!(ref.fulfillmentRef) : undefined,
|
|
67
|
+
onTrackingRecorded: result => { trackingRefs += result.replayed ? 0 : 1; trackingReadBacks += result.readBack ? 1 : 0; },
|
|
68
|
+
};
|
|
69
|
+
if (!observationStatusPolicy.handsOver(localStatus)) await applyFulfillmentObservationEffects({ ...effects, trackingRefs: [] }, observationStatusPolicy);
|
|
70
|
+
else {
|
|
71
|
+
try { await applyFulfillmentObservationEffects(effects, observationStatusPolicy); }
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (input.lifecycle.classifyFailure(error) !== "state_conflict") throw error;
|
|
74
|
+
await input.localPersistence.markProcessed(observation.eventKey, { status: "processed" });
|
|
75
|
+
return { kind: "superseded", observation, replayed: inbound.replayed || statusEvidence.replayed, trackingRefs, trackingReadBacks };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
await input.localPersistence.markProcessed(observation.eventKey, { status: "processed" });
|
|
79
|
+
return { kind: "processed", observation, replayed: inbound.replayed || statusEvidence.replayed, trackingRefs, trackingReadBacks };
|
|
80
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { projectVerifiedEvidence } from "./verifiedEvidenceProjection.js";
|
|
2
|
+
import { validatePaymentExecutionContractResult } from "@openlup/core/testing";
|
|
3
|
+
import type {
|
|
4
|
+
PaymentAdapterConformanceEvidence, PaymentAdapterConformanceSubject,
|
|
5
|
+
PaymentAttemptIdentity, PaymentMoney, PaymentVerifyNormalizePort,
|
|
6
|
+
} from "./contracts/index.js";
|
|
7
|
+
|
|
8
|
+
/** Runner-independent adapter proof. Verification fixtures are owned by each adapter. */
|
|
9
|
+
export async function runPaymentAdapterConformance(
|
|
10
|
+
subjects: readonly (PaymentAdapterConformanceSubject & {
|
|
11
|
+
verification: readonly {
|
|
12
|
+
input: Parameters<PaymentVerifyNormalizePort["verifyAndNormalize"]>[0];
|
|
13
|
+
expected: Awaited<ReturnType<PaymentVerifyNormalizePort["verifyAndNormalize"]>>;
|
|
14
|
+
}[];
|
|
15
|
+
})[],
|
|
16
|
+
): Promise<PaymentAdapterConformanceEvidence> {
|
|
17
|
+
const observations: PaymentAdapterConformanceEvidence["subjects"][number][] = [];
|
|
18
|
+
const cases: PaymentAdapterConformanceEvidence["cases"][number][] = [];
|
|
19
|
+
for (const subject of subjects) {
|
|
20
|
+
const { adapter, executionInput, readbackInput } = subject;
|
|
21
|
+
const provider = adapter.capabilities.providerKind;
|
|
22
|
+
requireFact(executionInput.money.currency.length > 0, "configured currency");
|
|
23
|
+
adapter.attemptPort.validateInput(executionInput);
|
|
24
|
+
const result = await adapter.attemptPort.execute(executionInput);
|
|
25
|
+
const { handoff, ...execution } = result;
|
|
26
|
+
const sensitiveValues = [executionInput.payer?.email, executionInput.payer?.name, executionInput.customerAuthorization,
|
|
27
|
+
handoff?.kind === "client-confirmed" ? handoff.payload : handoff?.url].filter((v): v is NonNullable<typeof v> => v !== undefined && v.length > 0);
|
|
28
|
+
const sensitiveForms = sensitiveValues.flatMap(value => [value, JSON.stringify(value).slice(1, -1)]);
|
|
29
|
+
const payerForms = [executionInput.payer?.email, executionInput.payer?.name]
|
|
30
|
+
.filter((value): value is string => Boolean(value?.length))
|
|
31
|
+
.flatMap(value => [value, JSON.stringify(value).slice(1, -1)]);
|
|
32
|
+
const input = {
|
|
33
|
+
idempotencyKey: executionInput.attemptRef, providerIdempotencyKey: executionInput.providerIdempotencyKey,
|
|
34
|
+
providerRequestFingerprint: executionInput.fingerprint, paymentIntentId: executionInput.intentRef,
|
|
35
|
+
amountMinor: executionInput.money.amountMinor, currency: executionInput.money.currency, mode: "one_time" as const,
|
|
36
|
+
};
|
|
37
|
+
const validation = validatePaymentExecutionContractResult({ input, expectedProvider: provider, sensitiveValues: sensitiveForms,
|
|
38
|
+
result: { provider, providerAttemptId: result.dispatch.kind === "dispatched" ? result.dispatch.providerPaymentRef : null,
|
|
39
|
+
providerSessionId: null, attemptStatus: result.outcome.kind === "attempt" ? result.outcome.status : "processing",
|
|
40
|
+
nextActionKind: null, requestPayload: { providerIdempotencyKey: input.providerIdempotencyKey,
|
|
41
|
+
providerRequestFingerprint: input.providerRequestFingerprint }, responsePayload: result.diagnostics },
|
|
42
|
+
});
|
|
43
|
+
requireFact(validation.ok, "execution diagnostics and taxonomy");
|
|
44
|
+
requireFact(!payerForms.some(value => JSON.stringify(result).includes(value)), "execution payer redaction");
|
|
45
|
+
if (adapter.capabilities.payerContext.requiresPayerBlock && !executionInput.payer) {
|
|
46
|
+
requireFact(result.dispatch.kind === "notDispatched" && result.dispatch.evidence.exact
|
|
47
|
+
&& result.dispatch.evidence.phase === "before-dispatch", "missing required input exact notDispatched");
|
|
48
|
+
}
|
|
49
|
+
const returned = await adapter.readbackPort.readPayment(readbackInput);
|
|
50
|
+
const { handoff: readbackHandoff, ...readback } = "handoff" in returned ? returned : { ...returned, handoff: undefined };
|
|
51
|
+
requireFact(!sensitiveForms.some(value => JSON.stringify(readback).includes(value)), "readback secret redaction");
|
|
52
|
+
requireFact(sameIdentity(readback.identity, readbackInput.identity) && sameMoney(readback.expectedMoney, readbackInput.expectedMoney), "readback exact identity and money");
|
|
53
|
+
requireFact(adapter.capabilities.readback === "supported" ? returned.kind !== "unsupported" : returned.kind === "unsupported", "readback capability");
|
|
54
|
+
if ("observedMoney" in returned) requireFact(sameMoney(returned.observedMoney, readbackInput.expectedMoney), "observed money");
|
|
55
|
+
if (adapter.capabilities.close === "supported") {
|
|
56
|
+
requireFact(typeof adapter.readbackPort.close === "function", "close method present");
|
|
57
|
+
const closed = await adapter.readbackPort.close!(readbackInput);
|
|
58
|
+
requireFact(closed.kind !== "unsupported" && sameIdentity(closed.identity, readbackInput.identity)
|
|
59
|
+
&& sameMoney(closed.expectedMoney, readbackInput.expectedMoney), "close exact identity and money");
|
|
60
|
+
} else if (adapter.readbackPort.close) {
|
|
61
|
+
requireFact((await adapter.readbackPort.close(readbackInput)).kind === "unsupported", "close refuses");
|
|
62
|
+
}
|
|
63
|
+
let absence;
|
|
64
|
+
if (adapter.capabilities.exactAbsence === "supported") requireFact(adapter.absencePort !== undefined, "absence port present");
|
|
65
|
+
if (adapter.absencePort) {
|
|
66
|
+
requireFact(subject.absenceInput !== undefined, "absence input present");
|
|
67
|
+
absence = await adapter.absencePort.proveAbsence(subject.absenceInput!);
|
|
68
|
+
requireFact(sameIdentity(absence.identity, subject.absenceInput!.identity)
|
|
69
|
+
&& sameMoney(absence.expectedMoney, subject.absenceInput!.expectedMoney), "absence exact identity and money");
|
|
70
|
+
requireFact(adapter.capabilities.exactAbsence === "supported" ? absence.kind !== "unsupported" : absence.kind === "unsupported", "absence capability");
|
|
71
|
+
if (absence.kind === "absent") requireFact(absence.evidence.exact && absence.evidence.complete
|
|
72
|
+
&& absence.evidence.lookupReference.trim().length > 0, "complete absence evidence");
|
|
73
|
+
}
|
|
74
|
+
const fingerprints = new Map<string, { fingerprint: string; outcome: string }>();
|
|
75
|
+
const moneyEvents: { reference: string; outcome: string; eventId: string; fingerprint: string }[] = [];
|
|
76
|
+
for (const fixture of subject.verification) {
|
|
77
|
+
const verified = await adapter.verifyPort.verifyAndNormalize(fixture.input);
|
|
78
|
+
requireFact(JSON.stringify(verified) === JSON.stringify(fixture.expected), "verified fixture");
|
|
79
|
+
if (verified.kind !== "refusal") {
|
|
80
|
+
requireFact(verified.kind !== "money" || (verified.money !== null
|
|
81
|
+
&& Number.isSafeInteger(verified.money.amountMinor) && verified.money.amountMinor > 0
|
|
82
|
+
&& verified.money.currency.trim().length > 0)
|
|
83
|
+
|| (verified.outcome !== "succeeded" && verified.outcome !== "refunded" && verified.outcome !== "disputed"),
|
|
84
|
+
"settling outcome requires positive money");
|
|
85
|
+
requireFact(!sensitiveForms.some(value => JSON.stringify({ payload: projectVerifiedEvidence(verified, fixture.input, "require-evidence").rawPayload, diagnostics: result.diagnostics }).includes(value)),
|
|
86
|
+
"audit payload secret redaction");
|
|
87
|
+
const outcome = verified.kind + ":" + verified.outcome;
|
|
88
|
+
const previous = fingerprints.get(verified.providerEventId);
|
|
89
|
+
if (previous) requireFact(previous.outcome === outcome && previous.fingerprint === verified.fingerprint, "replay identity fingerprint");
|
|
90
|
+
if (verified.kind === "money") {
|
|
91
|
+
for (const event of moneyEvents) {
|
|
92
|
+
if (event.reference === verified.providerPaymentRef && event.outcome !== outcome) {
|
|
93
|
+
requireFact(event.eventId !== verified.providerEventId && event.fingerprint !== verified.fingerprint, "changing outcome identity");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
moneyEvents.push({ reference: verified.providerPaymentRef, outcome, eventId: verified.providerEventId, fingerprint: verified.fingerprint });
|
|
97
|
+
}
|
|
98
|
+
fingerprints.set(verified.providerEventId, { fingerprint: verified.fingerprint, outcome });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Transient handoffs are deliberately excluded from the returned evidence.
|
|
102
|
+
void readbackHandoff;
|
|
103
|
+
observations.push({ subjectId: subject.subjectId, execution, readback, ...(absence ? { absence } : {}) });
|
|
104
|
+
cases.push({ subjectIds: [subject.subjectId], name: "execution, verification, capability and exact readback", passed: true });
|
|
105
|
+
}
|
|
106
|
+
return { subjects: observations, cases };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function requireFact(value: boolean, label: string): void {
|
|
110
|
+
if (!value) throw new Error(`payment_adapter_conformance:${label}`);
|
|
111
|
+
}
|
|
112
|
+
function sameMoney(left: PaymentMoney, right: PaymentMoney): boolean {
|
|
113
|
+
return left.amountMinor === right.amountMinor && left.currency === right.currency;
|
|
114
|
+
}
|
|
115
|
+
function sameIdentity(left: PaymentAttemptIdentity, right: typeof left): boolean {
|
|
116
|
+
return left.provider === right.provider && left.providerPaymentRef === right.providerPaymentRef
|
|
117
|
+
&& left.intentRef === right.intentRef && left.attemptRef === right.attemptRef;
|
|
118
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PaymentServices, PaymentServicesOptions } from "./contracts/repair.js";
|
|
2
|
+
|
|
3
|
+
/** One host-owned scope per runtime unit; no driver or environment discovery. */
|
|
4
|
+
export function createPaymentServices(options: PaymentServicesOptions): PaymentServices | null {
|
|
5
|
+
if (options.schema?.truth !== "supported" || !options.openTruth) return null;
|
|
6
|
+
const openTruth = options.openTruth;
|
|
7
|
+
return {
|
|
8
|
+
async run(work) {
|
|
9
|
+
const scope = await openTruth();
|
|
10
|
+
try {
|
|
11
|
+
return await work(scope.port);
|
|
12
|
+
} finally {
|
|
13
|
+
await scope.close();
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|