@openlup/platform 0.21.0 → 0.23.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 +432 -146
- package/README.md +82 -589
- package/dist/composition/composeBundle.d.ts +7 -0
- package/dist/composition/composeBundle.js +2 -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 +1 -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 +95 -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 +143 -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 +72 -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 +59 -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 +89 -0
- package/dist/fulfillment/contracts/store.js +1 -0
- package/dist/fulfillment/contracts/sync.d.ts +226 -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 +26 -0
- package/dist/fulfillment/dispatchProjection.js +107 -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 +38 -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 +32 -0
- package/dist/fulfillment/migrations.js +9 -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 +32 -0
- package/dist/fulfillment/observationProjection.js +90 -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 +39 -0
- package/dist/fulfillment/omnipack/client.js +172 -0
- package/dist/fulfillment/omnipack/index.d.ts +55 -0
- package/dist/fulfillment/omnipack/index.js +408 -0
- package/dist/fulfillment/omnipack/mappers.d.ts +79 -0
- package/dist/fulfillment/omnipack/mappers.js +199 -0
- package/dist/fulfillment/omnipack/observation.d.ts +28 -0
- package/dist/fulfillment/omnipack/observation.js +72 -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/reconciliation.d.ts +15 -0
- package/dist/fulfillment/omnipack/reconciliation.js +55 -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 +70 -0
- package/dist/fulfillment/postgresShipmentSpine.js +246 -0
- package/dist/fulfillment/postgresStockSyncPort.d.ts +8 -0
- package/dist/fulfillment/postgresStockSyncPort.js +66 -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 +27 -0
- package/dist/fulfillment/providerDispatchAcceptance.js +29 -0
- package/dist/fulfillment/providerDispatchError.d.ts +6 -0
- package/dist/fulfillment/providerDispatchError.js +5 -0
- package/dist/fulfillment/providerDispatchWorker.d.ts +44 -0
- package/dist/fulfillment/providerDispatchWorker.js +199 -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 +90 -0
- package/dist/fulfillment/providerReconciliationWorker.js +207 -0
- package/dist/fulfillment/providerStockAvailability.d.ts +13 -0
- package/dist/fulfillment/providerStockAvailability.js +51 -0
- package/dist/fulfillment/providerStockSyncWorker.d.ts +41 -0
- package/dist/fulfillment/providerStockSyncWorker.js +295 -0
- package/dist/fulfillment/providerTrackingReferences.d.ts +16 -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 +79 -0
- package/dist/fulfillment/verifiedIngress.js +94 -0
- package/dist/invoicing/capabilities.d.ts +11 -0
- package/dist/invoicing/capabilities.js +10 -0
- package/dist/invoicing/contracts/capabilities.d.ts +40 -0
- package/dist/invoicing/contracts/capabilities.js +1 -0
- package/dist/invoicing/contracts/companyLookup.d.ts +49 -0
- package/dist/invoicing/contracts/companyLookup.js +1 -0
- package/dist/invoicing/contracts/delivery.d.ts +74 -0
- package/dist/invoicing/contracts/delivery.js +1 -0
- package/dist/invoicing/contracts/facts.d.ts +100 -0
- package/dist/invoicing/contracts/facts.js +1 -0
- package/dist/invoicing/contracts/host.d.ts +246 -0
- package/dist/invoicing/contracts/host.js +1 -0
- package/dist/invoicing/contracts/index.d.ts +16 -0
- package/dist/invoicing/contracts/index.js +1 -0
- package/dist/invoicing/contracts/provider.d.ts +126 -0
- package/dist/invoicing/contracts/provider.js +1 -0
- package/dist/invoicing/contracts/settlement.d.ts +66 -0
- package/dist/invoicing/contracts/settlement.js +1 -0
- package/dist/invoicing/contracts/store.d.ts +267 -0
- package/dist/invoicing/contracts/store.js +1 -0
- package/dist/invoicing/index.d.ts +1 -0
- package/dist/invoicing/index.js +1 -0
- package/dist/payments/stripe/index.d.ts +25 -0
- package/dist/payments/stripe/index.js +25 -0
- package/dist/payments/tpay/index.d.ts +30 -0
- package/dist/payments/tpay/index.js +30 -0
- package/dist/subscriptions/automaticRenewal.d.ts +44 -0
- package/dist/subscriptions/automaticRenewal.js +81 -0
- package/dist/subscriptions/automaticRenewalExecution.d.ts +21 -0
- package/dist/subscriptions/automaticRenewalExecution.js +44 -0
- package/dist/subscriptions/contracts/commands.d.ts +115 -0
- package/dist/subscriptions/contracts/commands.js +1 -0
- package/dist/subscriptions/contracts/dunning.d.ts +133 -0
- package/dist/subscriptions/contracts/dunning.js +1 -0
- package/dist/subscriptions/contracts/index.d.ts +6 -0
- package/dist/subscriptions/contracts/index.js +1 -0
- package/dist/subscriptions/contracts/messages.d.ts +43 -0
- package/dist/subscriptions/contracts/messages.js +1 -0
- package/dist/subscriptions/contracts/renewal.d.ts +533 -0
- package/dist/subscriptions/contracts/renewal.js +1 -0
- package/dist/subscriptions/contracts/stockWait.d.ts +73 -0
- package/dist/subscriptions/contracts/stockWait.js +1 -0
- package/dist/subscriptions/index.d.ts +39 -0
- package/dist/subscriptions/index.js +17 -0
- package/dist/subscriptions/paymentOutcomes.d.ts +8 -0
- package/dist/subscriptions/paymentOutcomes.js +27 -0
- package/dist/subscriptions/postgresRenewalStore.d.ts +20 -0
- package/dist/subscriptions/postgresRenewalStore.js +61 -0
- package/dist/subscriptions/postgresRetryStore.d.ts +78 -0
- package/dist/subscriptions/postgresRetryStore.js +142 -0
- package/dist/subscriptions/renewalCycleFailureMechanism.d.ts +34 -0
- package/dist/subscriptions/renewalCycleFailureMechanism.js +79 -0
- package/dist/subscriptions/renewalCycleMechanism.d.ts +46 -0
- package/dist/subscriptions/renewalCycleMechanism.js +157 -0
- package/dist/subscriptions/renewalCycleRefusalMechanism.d.ts +41 -0
- package/dist/subscriptions/renewalCycleRefusalMechanism.js +160 -0
- package/dist/subscriptions/renewalIdentity.d.ts +3 -0
- package/dist/subscriptions/renewalIdentity.js +4 -0
- package/package.json +37 -2
- package/release-gates.json +97 -3
- package/sql/20260808210000_fulfillment_shipment_rail.sql +810 -0
- package/sql/20260819150000_fulfillment_shipment_sequence.sql +98 -0
- package/sql/20261022080000_fulfillment_command_ledger.sql +786 -0
- package/sql/20261022090000_fulfillment_delivery_alignment.sql +1106 -0
- package/sql/20261022160000_subscription_retry_lifecycle.sql +328 -0
- package/sql/20261022165000_fulfillment_stock_evidence_replay_reopen.sql +30 -0
- package/src/composition/composeBundle.ts +4 -0
- package/src/composition/fulfillmentContribution.ts +28 -0
- package/src/composition/index.ts +1 -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 +63 -0
- package/src/fulfillment/contracts/customerAlignment.ts +4 -0
- package/src/fulfillment/contracts/dispatch.ts +73 -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 +26 -0
- package/src/fulfillment/contracts/operator.ts +31 -0
- package/src/fulfillment/contracts/read.ts +42 -0
- package/src/fulfillment/contracts/readback.ts +24 -0
- package/src/fulfillment/contracts/replacement.ts +15 -0
- package/src/fulfillment/contracts/store.ts +27 -0
- package/src/fulfillment/contracts/sync.ts +72 -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 +105 -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 +39 -0
- package/src/fulfillment/labelLifecycle.ts +11 -0
- package/src/fulfillment/lowStockEvidenceHandler.ts +60 -0
- package/src/fulfillment/migrations.ts +9 -0
- package/src/fulfillment/nativeUnsupported.ts +23 -0
- package/src/fulfillment/normalizedFulfillmentFacts.ts +2 -0
- package/src/fulfillment/observationProjection.ts +92 -0
- package/src/fulfillment/omnipack/capabilities.ts +18 -0
- package/src/fulfillment/omnipack/client.ts +270 -0
- package/src/fulfillment/omnipack/index.ts +369 -0
- package/src/fulfillment/omnipack/mappers.ts +282 -0
- package/src/fulfillment/omnipack/observation.ts +79 -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/reconciliation.ts +56 -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 +338 -0
- package/src/fulfillment/postgresStockSyncPort.ts +68 -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 +37 -0
- package/src/fulfillment/providerDispatchError.ts +7 -0
- package/src/fulfillment/providerDispatchWorker.ts +159 -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 +206 -0
- package/src/fulfillment/providerStockAvailability.ts +60 -0
- package/src/fulfillment/providerStockSyncWorker.ts +318 -0
- package/src/fulfillment/providerTrackingReferences.ts +32 -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 +113 -0
- package/src/invoicing/capabilities.ts +9 -0
- package/src/invoicing/contracts/capabilities.ts +15 -0
- package/src/invoicing/contracts/companyLookup.ts +13 -0
- package/src/invoicing/contracts/delivery.ts +24 -0
- package/src/invoicing/contracts/facts.ts +60 -0
- package/src/invoicing/contracts/host.ts +85 -0
- package/src/invoicing/contracts/index.ts +16 -0
- package/src/invoicing/contracts/provider.ts +78 -0
- package/src/invoicing/contracts/settlement.ts +39 -0
- package/src/invoicing/contracts/store.ts +98 -0
- package/src/invoicing/index.ts +1 -0
- package/src/payments/stripe/index.ts +30 -0
- package/src/payments/tpay/index.ts +36 -0
- package/src/subscriptions/automaticRenewal.ts +87 -0
- package/src/subscriptions/automaticRenewalExecution.ts +39 -0
- package/src/subscriptions/contracts/commands.ts +46 -0
- package/src/subscriptions/contracts/dunning.ts +48 -0
- package/src/subscriptions/contracts/index.ts +6 -0
- package/src/subscriptions/contracts/messages.ts +18 -0
- package/src/subscriptions/contracts/renewal.ts +325 -0
- package/src/subscriptions/contracts/stockWait.ts +34 -0
- package/src/subscriptions/index.ts +24 -0
- package/src/subscriptions/paymentOutcomes.ts +23 -0
- package/src/subscriptions/postgresRenewalStore.ts +76 -0
- package/src/subscriptions/postgresRetryStore.ts +174 -0
- package/src/subscriptions/renewalCycleFailureMechanism.ts +119 -0
- package/src/subscriptions/renewalCycleMechanism.ts +225 -0
- package/src/subscriptions/renewalCycleRefusalMechanism.ts +215 -0
- package/src/subscriptions/renewalIdentity.ts +5 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mapOmnipackFulfilmentRequestsResponse,
|
|
3
|
+
mapOmnipackFulfilmentsResponse,
|
|
4
|
+
mapOmnipackOrderCreatedResponse,
|
|
5
|
+
mapOmnipackProductCreatedResponse,
|
|
6
|
+
mapOmnipackStockMovementsResponse,
|
|
7
|
+
mapOmnipackStockResponse,
|
|
8
|
+
sanitizeOmnipackPayload,
|
|
9
|
+
type OmnipackFulfilmentEvidence,
|
|
10
|
+
type OmnipackFulfilmentRequestEvidence,
|
|
11
|
+
type OmnipackOrderCreatedEvidence,
|
|
12
|
+
type OmnipackProductCreatedEvidence,
|
|
13
|
+
type OmnipackStockEvidence,
|
|
14
|
+
type OmnipackStockMovementEvidence,
|
|
15
|
+
} from "./mappers.js";
|
|
16
|
+
import {
|
|
17
|
+
mapOmnipackProductPackCreatedResponse,
|
|
18
|
+
mapOmnipackProductResponse,
|
|
19
|
+
type OmnipackProductEvidence,
|
|
20
|
+
type OmnipackProductPackCreatedEvidence,
|
|
21
|
+
} from "./productSyncMappers.js";
|
|
22
|
+
import { mayPostHaveSucceeded, OmnipackProviderError } from "./providerError.js";
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
OmnipackProviderError,
|
|
26
|
+
sanitizeOmnipackProviderError,
|
|
27
|
+
type SanitizedOmnipackProviderError,
|
|
28
|
+
} from "./providerError.js";
|
|
29
|
+
|
|
30
|
+
const PRODUCT_PACKS_SEGMENT = "packs";
|
|
31
|
+
|
|
32
|
+
export interface OmnipackClientConfig {
|
|
33
|
+
baseUrl: string;
|
|
34
|
+
credentials: () => { username: string; password: string } | Promise<{ username: string; password: string }>;
|
|
35
|
+
timeoutMs: number;
|
|
36
|
+
safeReadRetries: number;
|
|
37
|
+
productPath: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface OmnipackClient {
|
|
41
|
+
createOrder(body: string): Promise<OmnipackOrderCreatedEvidence>;
|
|
42
|
+
// Product sync. createProduct/createProductPack are POST, so
|
|
43
|
+
// requestJson does NOT retry them (mutations must not be silently re-sent). getProduct is a
|
|
44
|
+
// GET (retried) reading the SKU's packs[] — one SKU ↔ many EANs.
|
|
45
|
+
createProduct(payload: Record<string, unknown>, sku: string): Promise<OmnipackProductCreatedEvidence>;
|
|
46
|
+
getProduct(sku: string): Promise<OmnipackProductEvidence>;
|
|
47
|
+
createProductPack(sku: string, payload: Record<string, unknown>, ean: string): Promise<OmnipackProductPackCreatedEvidence>;
|
|
48
|
+
getStock(options?: { refuseRedirectRetry: boolean }): Promise<OmnipackStockEvidence[]>;
|
|
49
|
+
getStockMovements(params?: OmnipackQueryParams): Promise<OmnipackStockMovementEvidence[]>;
|
|
50
|
+
getFulfilmentsPage(params?: OmnipackQueryParams): Promise<{ rows: OmnipackFulfilmentEvidence[]; complete: boolean }>;
|
|
51
|
+
getFulfilments(params?: OmnipackQueryParams): Promise<OmnipackFulfilmentEvidence[]>;
|
|
52
|
+
getFulfilmentRequests(params?: OmnipackQueryParams): Promise<OmnipackFulfilmentRequestEvidence[]>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface OmnipackQueryParams {
|
|
56
|
+
startLocalDate?: string;
|
|
57
|
+
endLocalDate?: string;
|
|
58
|
+
page?: number;
|
|
59
|
+
size?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createOmnipackClient(
|
|
63
|
+
config: OmnipackClientConfig,
|
|
64
|
+
fetchImpl: typeof fetch,
|
|
65
|
+
): OmnipackClient {
|
|
66
|
+
return {
|
|
67
|
+
async createOrder(body) {
|
|
68
|
+
return mapOmnipackOrderCreatedResponse(await requestJson(fetchImpl, config, "createOrder", "/orders", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body,
|
|
71
|
+
}));
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async createProduct(payload, sku) {
|
|
75
|
+
const body = await requestJson(fetchImpl, config, "createProduct", config.productPath, post(payload));
|
|
76
|
+
return mapOmnipackProductCreatedResponse(body, sku);
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async getProduct(sku) {
|
|
80
|
+
const path = productResourcePath(config.productPath, sku);
|
|
81
|
+
return mapOmnipackProductResponse(await requestJson(fetchImpl, config, "getProduct", path, { method: "GET" }), sku);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async createProductPack(sku, payload, ean) {
|
|
85
|
+
const path = `${productResourcePath(config.productPath, sku)}/${PRODUCT_PACKS_SEGMENT}`;
|
|
86
|
+
const body = await requestJson(fetchImpl, config, "createProductPack", path, post(payload));
|
|
87
|
+
return mapOmnipackProductPackCreatedResponse(body, sku, ean);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async getStock(options) {
|
|
91
|
+
return mapOmnipackStockResponse(await requestJson(fetchImpl, config, "getStock", "/stock", { method: "GET" }, options?.refuseRedirectRetry));
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async getStockMovements(params) {
|
|
95
|
+
return mapOmnipackStockMovementsResponse(await requestJson(
|
|
96
|
+
fetchImpl,
|
|
97
|
+
config,
|
|
98
|
+
"getStockMovements",
|
|
99
|
+
withQuery("/stock-movements", params),
|
|
100
|
+
{ method: "GET" },
|
|
101
|
+
true,
|
|
102
|
+
));
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async getFulfilmentsPage(params) {
|
|
106
|
+
const body = await requestJson(fetchImpl, config, "getFulfilments", withQuery("/fulfilments", params), { method: "GET" });
|
|
107
|
+
const rows = mapOmnipackFulfilmentsResponse(body);
|
|
108
|
+
// Only explicit single-page totals establish whole-collection uniqueness.
|
|
109
|
+
const record = body as { page?: { number?: unknown; size?: unknown; totalPages?: unknown; totalElements?: unknown }; _links?: { next?: unknown }; incomplete?: unknown; partial?: unknown } | null;
|
|
110
|
+
const page = record?.page;
|
|
111
|
+
const complete = page?.number === 0 && page.totalPages === 1 && page.totalElements === rows.length
|
|
112
|
+
&& typeof page.size === "number" && Number.isSafeInteger(page.size) && page.size >= rows.length && page.size > 0
|
|
113
|
+
&& !record?._links?.next && !record?.incomplete && !record?.partial;
|
|
114
|
+
return { rows, complete };
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async getFulfilments(params) {
|
|
118
|
+
return mapOmnipackFulfilmentsResponse(await requestJson(
|
|
119
|
+
fetchImpl,
|
|
120
|
+
config,
|
|
121
|
+
"getFulfilments",
|
|
122
|
+
withQuery("/fulfilments", params),
|
|
123
|
+
{ method: "GET" },
|
|
124
|
+
));
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
async getFulfilmentRequests(params) {
|
|
128
|
+
return mapOmnipackFulfilmentRequestsResponse(await requestJson(
|
|
129
|
+
fetchImpl,
|
|
130
|
+
config,
|
|
131
|
+
"getFulfilmentRequests",
|
|
132
|
+
withQuery("/fulfilment-requests", params),
|
|
133
|
+
{ method: "GET" },
|
|
134
|
+
));
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function requestJson(
|
|
140
|
+
fetchImpl: typeof fetch,
|
|
141
|
+
config: OmnipackClientConfig,
|
|
142
|
+
operation: string,
|
|
143
|
+
path: string,
|
|
144
|
+
init: RequestInit,
|
|
145
|
+
refuseRedirectRetry = false,
|
|
146
|
+
): Promise<unknown> {
|
|
147
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
148
|
+
const attempts = method === "GET" ? config.safeReadRetries + 1 : 1;
|
|
149
|
+
// Dispatch/readback diagnostics must not change product-sync or stock retry policy.
|
|
150
|
+
const diagnoseRedirectFailure = refuseRedirectRetry || operation === "createOrder"
|
|
151
|
+
|| operation === "getFulfilments" || operation === "getFulfilmentRequests";
|
|
152
|
+
let lastError: OmnipackProviderError | null = null;
|
|
153
|
+
|
|
154
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
155
|
+
let submitted = false;
|
|
156
|
+
try {
|
|
157
|
+
const base = new URL(config.baseUrl);
|
|
158
|
+
const url = new URL(path, base);
|
|
159
|
+
if (url.origin !== base.origin || url.username || url.password || url.protocol !== "https:") throw new Error("Invalid fulfillment path");
|
|
160
|
+
const response = await fetchWithTimeout(fetchImpl, url, config, init, () => { submitted = true; });
|
|
161
|
+
if (response.ok) return await readJsonBody(response);
|
|
162
|
+
|
|
163
|
+
const retryable = method === "GET" && isRetryableStatus(response.status);
|
|
164
|
+
// A failed body read cannot erase the rejection status already received.
|
|
165
|
+
const body = await readJsonBody(response).catch(() => null);
|
|
166
|
+
lastError = new OmnipackProviderError({
|
|
167
|
+
operation,
|
|
168
|
+
status: response.status,
|
|
169
|
+
code: refuseRedirectRetry && response.status >= 300 && response.status < 400 ? "redirect_refused" : readProviderErrorCode(body) ?? `http_${response.status}`,
|
|
170
|
+
retryable,
|
|
171
|
+
mayHaveSucceeded: mayPostHaveSucceeded(method, response.status),
|
|
172
|
+
});
|
|
173
|
+
if (!retryable || attempt === attempts) throw lastError;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const retryable = method === "GET" && attempt < attempts;
|
|
176
|
+
lastError = submitted && error instanceof OmnipackProviderError
|
|
177
|
+
? error
|
|
178
|
+
: new OmnipackProviderError({
|
|
179
|
+
operation,
|
|
180
|
+
status: null,
|
|
181
|
+
code: diagnoseRedirectFailure && isRedirectRefusal(error) ? "redirect_refused" : "network_error",
|
|
182
|
+
retryable,
|
|
183
|
+
mayHaveSucceeded: submitted && mayPostHaveSucceeded(method, null),
|
|
184
|
+
});
|
|
185
|
+
if (!retryable || (refuseRedirectRetry && lastError.code === "redirect_refused")) throw lastError;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
throw lastError;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function fetchWithTimeout(
|
|
193
|
+
fetchImpl: typeof fetch,
|
|
194
|
+
url: URL,
|
|
195
|
+
config: OmnipackClientConfig,
|
|
196
|
+
init: RequestInit,
|
|
197
|
+
onSubmit: () => void,
|
|
198
|
+
): Promise<Response> {
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
201
|
+
try {
|
|
202
|
+
const credentials = await config.credentials();
|
|
203
|
+
const requestInit: RequestInit = {
|
|
204
|
+
...init,
|
|
205
|
+
signal: controller.signal,
|
|
206
|
+
redirect: "error",
|
|
207
|
+
headers: {
|
|
208
|
+
Accept: "application/json",
|
|
209
|
+
"Content-Type": "application/json",
|
|
210
|
+
Authorization: `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString("base64")}`,
|
|
211
|
+
...(init.headers ?? {}),
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
new Request(url, requestInit); // Validate construction before crossing the submission boundary.
|
|
215
|
+
controller.signal.throwIfAborted();
|
|
216
|
+
onSubmit();
|
|
217
|
+
return await fetchImpl(url, requestInit);
|
|
218
|
+
} finally {
|
|
219
|
+
clearTimeout(timeout);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function readJsonBody(response: Response): Promise<unknown> {
|
|
224
|
+
const text = await response.text();
|
|
225
|
+
if (!text) return {};
|
|
226
|
+
try {
|
|
227
|
+
return sanitizeOmnipackPayload(JSON.parse(text) as Record<string, unknown>);
|
|
228
|
+
} catch {
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// A non-retried JSON POST init (mutations must not be silently re-sent — requestJson keys retry
|
|
234
|
+
// off the GET method).
|
|
235
|
+
function post(payload: Record<string, unknown>): RequestInit {
|
|
236
|
+
return { method: "POST", body: JSON.stringify(payload) };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Per-product resource path (GET /products/{sku}, base for /packs). sku is percent-encoded; the
|
|
240
|
+
// product path is right-trimmed so a trailing-slash path override does not double the slash.
|
|
241
|
+
function productResourcePath(productPath: string, sku: string): string {
|
|
242
|
+
return `${productPath.replace(/\/+$/, "")}/${encodeURIComponent(sku)}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function withQuery(path: string, params: OmnipackQueryParams | undefined): string {
|
|
246
|
+
const url = new URL(path, "https://example.test");
|
|
247
|
+
if (params?.startLocalDate) url.searchParams.set("startLocalDate", params.startLocalDate);
|
|
248
|
+
if (params?.endLocalDate) url.searchParams.set("endLocalDate", params.endLocalDate);
|
|
249
|
+
if (typeof params?.page === "number") url.searchParams.set("page", String(params.page));
|
|
250
|
+
if (typeof params?.size === "number") url.searchParams.set("size", String(params.size));
|
|
251
|
+
return `${url.pathname}${url.search}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function readProviderErrorCode(body: unknown): string | null {
|
|
255
|
+
return typeof body === "object" && body !== null && "code" in body && typeof body.code === "string"
|
|
256
|
+
&& /^[A-Z][A-Z0-9_]{0,63}$/.test(body.code)
|
|
257
|
+
? body.code
|
|
258
|
+
: null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isRetryableStatus(status: number): boolean {
|
|
262
|
+
return status === 408 || status === 429 || status >= 500;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Fetch implementations expose redirect refusal as an exception, sometimes through cause.
|
|
266
|
+
// Inspect it only to select a closed diagnostic; no exception text leaves this adapter.
|
|
267
|
+
function isRedirectRefusal(error: unknown): boolean {
|
|
268
|
+
if (!(error instanceof Error)) return false;
|
|
269
|
+
return /redirect/i.test(error.message) || (error.cause instanceof Error && /redirect/i.test(error.cause.message));
|
|
270
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { publicTrackingUrl } from "../observationProjection.js";
|
|
3
|
+
import type { DispatchEvidence, FulfillmentCapabilityDescriptor, FulfillmentDispatchPort, FulfillmentDispatchRequest,
|
|
4
|
+
FulfillmentLabelPort, FulfillmentObservationPort, FulfillmentReadbackPort, FulfillmentObservationPagePort, FulfillmentStockSyncProvider, FulfillmentProductSyncProvider, FulfillmentRefusal } from "../contracts/index.js";
|
|
5
|
+
import { createOmnipackClient, OmnipackProviderError } from "./client.js";
|
|
6
|
+
import { omnipackFulfillmentCapabilities } from "./capabilities.js";
|
|
7
|
+
import { buildOmnipackDispatchCommand, serializeOmnipackDispatchPayload, stableDispatchHash } from "./omnipackDispatchPayload.js";
|
|
8
|
+
import { mapOmnipackWebhookPayload } from "./mappers.js";
|
|
9
|
+
import { verifyOmnipackWebhookToken, type OmnipackWebhookRouteEvent } from "./omnipackWebhookPayload.js";
|
|
10
|
+
import { createReconciliationPagePort, reconciliationObservation } from "./reconciliation.js";
|
|
11
|
+
import { dispatchEvidence, operationError } from "../dispatchProjection.js";
|
|
12
|
+
import { correlationReference, digest, eventKind, observation, reference, refusal, timestamp, type OmnipackObservationCodecs } from "./observation.js";
|
|
13
|
+
export { omnipackFulfillmentCapabilities } from "./capabilities.js";
|
|
14
|
+
|
|
15
|
+
// Internal error projection: retain diagnostics without exposing a provider payload or cause.
|
|
16
|
+
class OmnipackSyncError extends Error implements FulfillmentRefusal {
|
|
17
|
+
readonly phase = "sync";
|
|
18
|
+
readonly retryable: boolean;
|
|
19
|
+
readonly providerFailure: boolean;
|
|
20
|
+
readonly status: number | null;
|
|
21
|
+
readonly code: string | null;
|
|
22
|
+
|
|
23
|
+
constructor(readonly reason: FulfillmentRefusal["reason"], operation: string, error?: unknown) {
|
|
24
|
+
super(reason === "io_failure" ? `OmniPack request failed: ${operation}` : `OmniPack sync refused: ${reason}`);
|
|
25
|
+
this.name = "OmnipackSyncError";
|
|
26
|
+
this.providerFailure = reason === "io_failure";
|
|
27
|
+
this.retryable = error instanceof OmnipackProviderError ? error.retryable : false;
|
|
28
|
+
this.status = error instanceof OmnipackProviderError ? error.status : null;
|
|
29
|
+
this.code = error instanceof OmnipackProviderError ? error.code : null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function syncFailure(operation: string, error: unknown): OmnipackSyncError {
|
|
33
|
+
return error instanceof OmnipackSyncError ? error : new OmnipackSyncError("io_failure", operation, error);
|
|
34
|
+
}
|
|
35
|
+
function requireProductSku(sku: string): void {
|
|
36
|
+
// URL resolution collapses dot segments; encodeURIComponent rejects lone surrogates.
|
|
37
|
+
// Keep these SKUs in the listing so the worker counts a visible per-SKU failure.
|
|
38
|
+
try {
|
|
39
|
+
if (!sku.trim() || sku === "." || sku === "..") throw Error();
|
|
40
|
+
encodeURIComponent(sku);
|
|
41
|
+
} catch { throw new OmnipackSyncError("malformed", "getProduct"); }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Node-only, explicit construction. Secrets are supplied per call, never retained as values. */
|
|
45
|
+
export interface OmnipackFulfillmentAdapterConfig {
|
|
46
|
+
http: {
|
|
47
|
+
baseUrl: string; credentials: () => { username: string; password: string } | Promise<{ username: string; password: string }>;
|
|
48
|
+
fetchImpl: typeof fetch; timeoutMs: number; safeReadRetries: number; productPath: string;
|
|
49
|
+
};
|
|
50
|
+
selected?: boolean;
|
|
51
|
+
/** Per-instance retention target; default 10,000. Evicts oldest completed submissions, never in-flight. */
|
|
52
|
+
maxRetainedSubmissions?: number;
|
|
53
|
+
/** Host account evidence, independent of credentials. Omission remains not ready. */
|
|
54
|
+
readiness?: FulfillmentCapabilityDescriptor["readiness"];
|
|
55
|
+
/** Called in prepare and again for new dispatches; return identical metadata for the same request or dispatch refuses. */
|
|
56
|
+
resolveProduct: (line: FulfillmentDispatchRequest["lines"][number]) => Promise<{ lotNumber?: string; expirationDate?: string } | null>;
|
|
57
|
+
notification: { token: () => string | undefined | Promise<string | undefined>; expectedEvent: OmnipackWebhookRouteEvent };
|
|
58
|
+
clock: () => string;
|
|
59
|
+
mintEvidenceReference: () => `evidence:${string}`;
|
|
60
|
+
mintSyncRunId: () => string;
|
|
61
|
+
approveTrackingUrl: (url: string) => boolean;
|
|
62
|
+
observationCodecs?: OmnipackObservationCodecs;
|
|
63
|
+
/** Writes occur only through deliberate calls; scheduled composition must remain pull-only. */
|
|
64
|
+
product?: { resolvePushMetadata: (sku: string) => Promise<Record<string, unknown> | null> };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** No route, schedule, environment reader or default binding is installed. */
|
|
68
|
+
export function createOmnipackFulfillmentAdapter(config: OmnipackFulfillmentAdapterConfig): {
|
|
69
|
+
capabilities: FulfillmentCapabilityDescriptor;
|
|
70
|
+
dispatch: FulfillmentDispatchPort;
|
|
71
|
+
label: FulfillmentLabelPort;
|
|
72
|
+
observation: FulfillmentObservationPort;
|
|
73
|
+
readback: FulfillmentReadbackPort;
|
|
74
|
+
reconciliation: FulfillmentObservationPagePort;
|
|
75
|
+
stockSync: FulfillmentStockSyncProvider;
|
|
76
|
+
productSync: FulfillmentProductSyncProvider;
|
|
77
|
+
} {
|
|
78
|
+
const maxRetainedSubmissions = config.maxRetainedSubmissions === undefined ? 10_000 : config.maxRetainedSubmissions;
|
|
79
|
+
const base = new URL(config.http.baseUrl);
|
|
80
|
+
if (base.protocol !== "https:" || base.username || base.password || base.search || base.hash
|
|
81
|
+
|| !Number.isSafeInteger(maxRetainedSubmissions) || maxRetainedSubmissions <= 0
|
|
82
|
+
|| typeof config.http.fetchImpl !== "function" || typeof config.http.credentials !== "function"
|
|
83
|
+
|| !Number.isSafeInteger(config.http.timeoutMs) || config.http.timeoutMs <= 0
|
|
84
|
+
|| !Number.isSafeInteger(config.http.safeReadRetries) || config.http.safeReadRetries < 0
|
|
85
|
+
|| !/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*\/?$/u.test(config.http.productPath)
|
|
86
|
+
|| !eventKind(config.notification.expectedEvent)) throw new Error("Invalid fulfillment configuration");
|
|
87
|
+
const client = createOmnipackClient({ ...config.http }, config.http.fetchImpl);
|
|
88
|
+
// Copy the profile so host selection never mutates the exported default-off descriptor.
|
|
89
|
+
const capabilities: FulfillmentCapabilityDescriptor = structuredClone(omnipackFulfillmentCapabilities);
|
|
90
|
+
capabilities.selected = config.selected === true;
|
|
91
|
+
capabilities.readiness = config.readiness?.ready === true ? { ready: true }
|
|
92
|
+
: { ready: false, refusal: refusal("not_ready", "selection") };
|
|
93
|
+
const submissions = new Map<string, { evidenceKey: string; commandDigest: string; completed: boolean; result: ReturnType<FulfillmentDispatchPort["dispatch"]> }>();
|
|
94
|
+
const evictCompleted = () => {
|
|
95
|
+
for (const [fingerprint, submission] of submissions) {
|
|
96
|
+
if (submissions.size <= maxRetainedSubmissions) break;
|
|
97
|
+
if (submission.completed) submissions.delete(fingerprint);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const selectionFailure = (request: FulfillmentDispatchRequest) => {
|
|
101
|
+
if (!request.selection?.providerRef?.trim()) return refusal("selection_required", "selection");
|
|
102
|
+
if (request.selection.providerRef !== capabilities.providerRef) return refusal("provider_mismatch", "selection");
|
|
103
|
+
if (!reference(request.selection.carrierRef) || !reference(request.selection.serviceRef)) return refusal("carrier_service_required", "selection");
|
|
104
|
+
if (!capabilities.selected) return refusal("disabled", "selection");
|
|
105
|
+
if (!capabilities.readiness.ready) return refusal("not_ready", "selection");
|
|
106
|
+
return null;
|
|
107
|
+
};
|
|
108
|
+
const requireSync = () => {
|
|
109
|
+
if (!capabilities.selected || !capabilities.readiness.ready) throw new OmnipackSyncError(capabilities.selected ? "not_ready" : "disabled", "selection");
|
|
110
|
+
};
|
|
111
|
+
const syncIdentity = () => {
|
|
112
|
+
const syncRunId = reference(config.mintSyncRunId());
|
|
113
|
+
const collectedAt = timestamp(config.clock());
|
|
114
|
+
if (!syncRunId || !collectedAt) throw new OmnipackSyncError("malformed", "sync");
|
|
115
|
+
return { providerRef: capabilities.providerRef, syncRunId, collectedAt, sourceRevision: null, revisionSource: "collection" as const };
|
|
116
|
+
};
|
|
117
|
+
// Shared computation: host metadata and serialized bytes live only for this call.
|
|
118
|
+
const buildCommand = async (request: FulfillmentDispatchRequest) => {
|
|
119
|
+
const requestDigest = stableDispatchHash(request);
|
|
120
|
+
if (!Number.isSafeInteger(request.contactRevision.schemaVersion) || request.contactRevision.schemaVersion < 1
|
|
121
|
+
|| !reference(request.contactRevision.source) || !Number.isSafeInteger(request.contactRevision.revision)
|
|
122
|
+
|| request.contactRevision.revision < 0) throw Error();
|
|
123
|
+
const lines = [];
|
|
124
|
+
for (const line of request.lines) {
|
|
125
|
+
const metadata = await config.resolveProduct(line);
|
|
126
|
+
if (!metadata) throw Error();
|
|
127
|
+
lines.push({ sku: line.skuRef, quantity: line.quantity, productSnapshot: { lotNumber: metadata.lotNumber, expirationDate: metadata.expirationDate } });
|
|
128
|
+
}
|
|
129
|
+
if (stableDispatchHash(request) !== requestDigest) throw Error();
|
|
130
|
+
const command = buildOmnipackDispatchCommand({
|
|
131
|
+
orderId: request.orderRef, orderNumber: request.orderNumber,
|
|
132
|
+
deliveryContact: { revision: request.contactRevision.revision, recipientName: request.recipient.name,
|
|
133
|
+
contactEmail: request.recipient.email, contactPhone: request.recipient.phone, line1: request.recipient.line1,
|
|
134
|
+
city: request.recipient.city, postalCode: request.recipient.postalCode, country: request.recipient.country,
|
|
135
|
+
selectedDelivery: { providerKind: capabilities.providerRef, carrierKind: request.selection.carrierRef,
|
|
136
|
+
serviceCode: request.selection.serviceRef, deliveryKind: request.selection.deliveryKind, pickupPoint: request.selection.pickupPoint } }, lines,
|
|
137
|
+
});
|
|
138
|
+
const evidence: DispatchEvidence = dispatchEvidence({ commandFingerprint: command.requestFingerprint,
|
|
139
|
+
contactRevision: { schemaVersion: request.contactRevision.schemaVersion, source: request.contactRevision.source, revision: request.contactRevision.revision },
|
|
140
|
+
providerRef: capabilities.providerRef, carrierRef: request.selection.carrierRef, serviceRef: request.selection.serviceRef,
|
|
141
|
+
transcript: { itemCount: command.providerPayload.items.length,
|
|
142
|
+
items: command.providerPayload.items.map(item => ({ skuRef: item.sku, quantity: item.quantity, lotRef: item.lotNumber ?? null, expirationDate: item.expirationDate ?? null })),
|
|
143
|
+
deliveryKind: request.selection.deliveryKind, pickupPointRef: request.selection.pickupPoint?.id ?? null } });
|
|
144
|
+
return { body: command.body, evidence };
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
capabilities,
|
|
148
|
+
dispatch: {
|
|
149
|
+
async prepare(request) {
|
|
150
|
+
const failed = selectionFailure(request);
|
|
151
|
+
if (failed) return { kind: "refused", refusal: failed };
|
|
152
|
+
try { return { kind: "prepared", evidence: (await buildCommand(request)).evidence }; }
|
|
153
|
+
catch { return { kind: "refused", refusal: refusal("not_dispatchable", "prepare") }; }
|
|
154
|
+
},
|
|
155
|
+
async dispatch(request, evidence) {
|
|
156
|
+
let commandRequest: FulfillmentDispatchRequest;
|
|
157
|
+
let fingerprint: string;
|
|
158
|
+
let evidenceKey: string;
|
|
159
|
+
let requestCommandDigest: string;
|
|
160
|
+
try {
|
|
161
|
+
// Validate the closed evidence and its request-bound values without consulting preparation state.
|
|
162
|
+
if (Object.keys(evidence).length !== 6 || Object.keys(evidence.contactRevision).length !== 3
|
|
163
|
+
|| typeof evidence.commandFingerprint !== "string" || !/^[a-f0-9]{64}$/u.test(evidence.commandFingerprint)
|
|
164
|
+
|| !reference(evidence.providerRef)
|
|
165
|
+
|| !reference(evidence.carrierRef) || !reference(evidence.serviceRef)
|
|
166
|
+
|| !Number.isSafeInteger(evidence.contactRevision.schemaVersion) || evidence.contactRevision.schemaVersion < 1
|
|
167
|
+
|| !reference(evidence.contactRevision.source) || !Number.isSafeInteger(evidence.contactRevision.revision)
|
|
168
|
+
|| evidence.contactRevision.revision < 0
|
|
169
|
+
|| evidence.contactRevision.schemaVersion !== request.contactRevision.schemaVersion
|
|
170
|
+
|| evidence.contactRevision.source !== request.contactRevision.source
|
|
171
|
+
|| evidence.contactRevision.revision !== request.contactRevision.revision
|
|
172
|
+
|| evidence.providerRef !== request.selection.providerRef
|
|
173
|
+
|| evidence.carrierRef !== request.selection.carrierRef || evidence.serviceRef !== request.selection.serviceRef) throw Error();
|
|
174
|
+
evidenceKey = serializeOmnipackDispatchPayload(evidence).body;
|
|
175
|
+
if (serializeOmnipackDispatchPayload(dispatchEvidence(evidence)).body !== evidenceKey) throw Error();
|
|
176
|
+
requestCommandDigest = commandDigest(request);
|
|
177
|
+
const submitted = submissions.get(evidence.commandFingerprint);
|
|
178
|
+
if (submitted) {
|
|
179
|
+
// Replay is bound to this submission's evidence and command, never replaceable preparation.
|
|
180
|
+
if (submitted.evidenceKey !== evidenceKey || submitted.commandDigest !== requestCommandDigest) throw Error();
|
|
181
|
+
return structuredClone(await submitted.result);
|
|
182
|
+
}
|
|
183
|
+
if (selectionFailure(request)) throw Error();
|
|
184
|
+
commandRequest = structuredClone(request);
|
|
185
|
+
fingerprint = evidence.commandFingerprint;
|
|
186
|
+
} catch { return { kind: "not_dispatched", effectMayHaveSucceeded: false, refusal: refusal("not_dispatchable", "dispatch") }; }
|
|
187
|
+
// Reserve before host resolution too, so concurrent dispatches join the rebuild.
|
|
188
|
+
const result = Promise.resolve().then(async () => {
|
|
189
|
+
let command;
|
|
190
|
+
try {
|
|
191
|
+
command = await buildCommand(commandRequest);
|
|
192
|
+
if (command.evidence.commandFingerprint !== fingerprint || serializeOmnipackDispatchPayload(command.evidence).body !== evidenceKey) throw Error();
|
|
193
|
+
} catch {
|
|
194
|
+
// A stale or unresolved command has never reached credentials or HTTP.
|
|
195
|
+
submissions.delete(fingerprint);
|
|
196
|
+
return { kind: "not_dispatched" as const, effectMayHaveSucceeded: false as const, refusal: refusal("not_dispatchable", "dispatch") };
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const result = await client.createOrder(command.body);
|
|
200
|
+
const externalRef = reference(result.providerOrderId);
|
|
201
|
+
if (!externalRef) return { kind: "unknown" as const, effectMayHaveSucceeded: true as const, refusal: refusal("malformed", "acknowledge") };
|
|
202
|
+
// Opaque provider proof; the worker replaces these with authoritative host keys.
|
|
203
|
+
const proof = digest(JSON.stringify([externalRef, command.evidence.commandFingerprint]));
|
|
204
|
+
return { kind: "accepted" as const, externalRef, evidence: structuredClone(command.evidence),
|
|
205
|
+
providerAttemptKey: `attempt:${proof}`, labelKey: `label:${proof}` };
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const detail = operationError(error instanceof OmnipackProviderError ? error : { code: "provider_dispatch_failed", status: null, retryable: true, mayHaveSucceeded: true });
|
|
208
|
+
return !detail.mayHaveSucceeded
|
|
209
|
+
? { kind: "not_dispatched" as const, effectMayHaveSucceeded: false as const, refusal: refusal("io_failure", "dispatch", detail.retryable), error: detail }
|
|
210
|
+
: { kind: "unknown" as const, effectMayHaveSucceeded: true as const, refusal: refusal("io_failure", "dispatch", detail.retryable), error: detail };
|
|
211
|
+
} finally {
|
|
212
|
+
command.body = "";
|
|
213
|
+
submissions.get(fingerprint)!.completed = true;
|
|
214
|
+
evictCompleted();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
submissions.set(fingerprint, { evidenceKey, commandDigest: requestCommandDigest, completed: false, result });
|
|
218
|
+
evictCompleted();
|
|
219
|
+
return structuredClone(await result);
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
label: {
|
|
223
|
+
async create() { return { kind: "refused", refusal: refusal("unsupported", "dispatch") }; },
|
|
224
|
+
async read() { return { kind: "refused", refusal: refusal("unsupported", "dispatch") }; },
|
|
225
|
+
async void() { return { providerDeletion: "unconfirmed", labelDeleted: false, canProceed: false, refusal: refusal("unsupported", "dispatch") }; },
|
|
226
|
+
},
|
|
227
|
+
observation: {
|
|
228
|
+
async verifyAndNormalize(input) {
|
|
229
|
+
// Token check precedes every read of rawBody and every parse.
|
|
230
|
+
try {
|
|
231
|
+
if (!verifyOmnipackWebhookToken({ headers: { ...input.headers } }, await config.notification.token()))
|
|
232
|
+
return { kind: "refused", refusal: refusal("untrusted", "verify") };
|
|
233
|
+
} catch { return { kind: "refused", refusal: refusal("untrusted", "verify") }; }
|
|
234
|
+
try {
|
|
235
|
+
const rawBody = Buffer.from(input.rawBody);
|
|
236
|
+
const payload: unknown = JSON.parse(rawBody.toString("utf8"));
|
|
237
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return { kind: "refused", refusal: refusal("malformed", "normalize") };
|
|
238
|
+
if ("event" in payload && (typeof payload.event !== "string" || !eventKind(payload.event)))
|
|
239
|
+
return { kind: "refused", refusal: refusal("unsupported_event", "normalize") };
|
|
240
|
+
const original = payload as Record<string, unknown>;
|
|
241
|
+
// Validate calendar fields here only; keep compact offsets and local
|
|
242
|
+
// timestamps accepted by the base parser, without rewriting key bytes.
|
|
243
|
+
if (original.occurredAt !== undefined && original.occurredAt !== null
|
|
244
|
+
&& (typeof original.occurredAt !== "string" || !timestamp(original.occurredAt)
|
|
245
|
+
|| !z.iso.datetime({ offset: true, local: true }).safeParse(original.occurredAt.replace(/([+-]\d{2})(\d{2})$/u, "$1:$2")).success))
|
|
246
|
+
return { kind: "refused", refusal: refusal("malformed", "normalize"), diagnostics: { invalidTimestamp: true } };
|
|
247
|
+
const rejectedCorrelationRefs = [original.orderId, original.providerOrderId, original.orderNumber, original.fulfilmentNumber]
|
|
248
|
+
.filter(value => value !== undefined && value !== null && (typeof value !== "string" || !correlationReference(value, config.observationCodecs))).length;
|
|
249
|
+
if (rejectedCorrelationRefs)
|
|
250
|
+
return { kind: "refused", refusal: refusal("missing_correlation", "normalize"), diagnostics: { rejectedCorrelationRefs } };
|
|
251
|
+
const evidence = mapOmnipackWebhookPayload(payload, config.notification.expectedEvent, value => correlationReference(value, config.observationCodecs) !== null, {
|
|
252
|
+
service: value => correlationReference(value, config.observationCodecs) !== null,
|
|
253
|
+
url: value => publicTrackingUrl(value, config.approveTrackingUrl, config.observationCodecs?.trackingUrl) !== null,
|
|
254
|
+
});
|
|
255
|
+
const event = eventKind(evidence.event);
|
|
256
|
+
if (!event || event !== config.notification.expectedEvent) return { kind: "refused", refusal: refusal("unsupported_event", "normalize") };
|
|
257
|
+
if (![evidence.providerOrderId, evidence.orderNumber, evidence.fulfilmentNumber].some(value => correlationReference(value, config.observationCodecs)))
|
|
258
|
+
return { kind: "refused", refusal: refusal("missing_correlation", "normalize") };
|
|
259
|
+
return { kind: "verified", observation: observation({ evidence, event, receivedAt: input.receivedAt,
|
|
260
|
+
rawDigest: digest(rawBody), evidenceRef: config.mintEvidenceReference(), source: "provider_webhook", approveTrackingUrl: config.approveTrackingUrl, observationCodecs: config.observationCodecs }) };
|
|
261
|
+
} catch { return { kind: "refused", refusal: refusal("malformed", "normalize") }; }
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
readback: {
|
|
265
|
+
async read(input) {
|
|
266
|
+
if (input.providerRef !== capabilities.providerRef) return { kind: "unsupported", refusal: refusal("provider_mismatch", "readback") };
|
|
267
|
+
if (!capabilities.selected || !capabilities.readiness.ready) return { kind: "unsupported", refusal: refusal(capabilities.selected ? "not_ready" : "disabled", "readback") };
|
|
268
|
+
if (!reference(input.externalRef)) return { kind: "unsupported", refusal: refusal("unsupported", "readback") };
|
|
269
|
+
try {
|
|
270
|
+
const collection = await client.getFulfilmentsPage({ page: 0, size: 100 });
|
|
271
|
+
if (!collection.complete) return { kind: "unknown", refusal: refusal("local_missing", "readback") };
|
|
272
|
+
const matches = collection.rows.filter(row => input.mode === "parcel"
|
|
273
|
+
? row.fulfilmentNumber === input.externalRef : row.providerOrderId === input.externalRef);
|
|
274
|
+
if (matches.length !== 1) return { kind: "unknown", refusal: refusal("local_missing", "readback") };
|
|
275
|
+
const mapped = reconciliationObservation(matches[0]!, config, input.fulfillmentRef);
|
|
276
|
+
return mapped.kind === "unmapped" ? { kind: "unknown", refusal: refusal("unsupported_event", "readback") } : { ...mapped, externalRef: input.externalRef! };
|
|
277
|
+
} catch { return { kind: "unknown", refusal: refusal("io_failure", "readback") }; }
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
reconciliation: createReconciliationPagePort(client, config, () => !capabilities.selected || !capabilities.readiness.ready
|
|
281
|
+
? refusal(capabilities.selected ? "not_ready" : "disabled", "readback") : null),
|
|
282
|
+
stockSync: {
|
|
283
|
+
async getStock() {
|
|
284
|
+
requireSync();
|
|
285
|
+
try {
|
|
286
|
+
const identity = syncIdentity();
|
|
287
|
+
return (await client.getStock({ refuseRedirectRetry: true })).map((row, itemIndex) => {
|
|
288
|
+
const skuRef = reference(row.sku);
|
|
289
|
+
if (!skuRef) return { kind: "rejected" as const, itemIndex, reason: "invalid_sku_ref" as const };
|
|
290
|
+
return { ...identity, skuRef, quantityKind: "snapshot" as const, totalQuantity: row.totalQuantity,
|
|
291
|
+
forSaleQuantity: row.forSaleQuantity, reservedOrUnavailableQuantity: row.reservedOrUnavailableQuantity };
|
|
292
|
+
});
|
|
293
|
+
} catch (error) { throw stockFailure(error, "get_stock"); }
|
|
294
|
+
},
|
|
295
|
+
async getStockMovements(input) {
|
|
296
|
+
requireSync();
|
|
297
|
+
try {
|
|
298
|
+
const identity = syncIdentity();
|
|
299
|
+
return (await client.getStockMovements(input)).map((row, itemIndex) => {
|
|
300
|
+
const skuRef = reference(row.sku);
|
|
301
|
+
if (!skuRef) return { kind: "rejected" as const, itemIndex, reason: "invalid_sku_ref" as const };
|
|
302
|
+
return { ...identity, skuRef, quantityKind: "movement" as const, quantity: row.quantity, occurredAt: timestamp(row.occurredAt),
|
|
303
|
+
operationType: reference(row.operationType), documentRef: reference(row.warehouseDocumentNumber), lotRef: reference(row.lotNumber), expirationDate: /^\d{4}-\d\d-\d\d$/u.test(row.expirationDate ?? "") ? row.expirationDate : null };
|
|
304
|
+
});
|
|
305
|
+
} catch (error) { throw stockFailure(error, "get_stock_movements"); }
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
productSync: {
|
|
309
|
+
async ensureProduct(sku) {
|
|
310
|
+
requireSync();
|
|
311
|
+
try {
|
|
312
|
+
const metadata = await config.product?.resolvePushMetadata(sku);
|
|
313
|
+
if (!metadata) return null;
|
|
314
|
+
await client.createProduct(metadata, sku);
|
|
315
|
+
return { replayed: false };
|
|
316
|
+
} catch (error) { throw syncFailure("createProduct", error); }
|
|
317
|
+
},
|
|
318
|
+
async getProductPacks(sku) {
|
|
319
|
+
requireSync();
|
|
320
|
+
requireProductSku(sku);
|
|
321
|
+
try { return (await client.getProduct(sku)).packs.map(pack => ({ ean: pack.ean, quantity: pack.quantity })); }
|
|
322
|
+
catch (error) { throw syncFailure("getProduct", error); }
|
|
323
|
+
},
|
|
324
|
+
async addProductPack(sku, pack) {
|
|
325
|
+
requireSync();
|
|
326
|
+
if (!/^\d{8,14}$/u.test(pack.ean) || !Number.isSafeInteger(pack.quantity) || pack.quantity <= 0) throw new OmnipackSyncError("malformed", "sync");
|
|
327
|
+
try { await client.createProductPack(sku, { ean: pack.ean, quantity: pack.quantity }, pack.ean); }
|
|
328
|
+
catch (error) { throw syncFailure("createProductPack", error); }
|
|
329
|
+
},
|
|
330
|
+
async listStockedSkus() {
|
|
331
|
+
requireSync();
|
|
332
|
+
try { return [...new Set((await client.getStock()).flatMap(row => row.sku.trim() ? [row.sku] : []))]; }
|
|
333
|
+
catch (error) { throw syncFailure("getStock", error); }
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// buildOmnipackDispatchPayloadFromCandidate forwards only name/email/phone and
|
|
340
|
+
// line1/city/postalCode/country to buildOmnipackOutboundOrderPayload; neither
|
|
341
|
+
// codec reads line titles, recipient line2/deliveryInstructions/courierInstructions,
|
|
342
|
+
// or pickup-point provider/name/address (readPickUpPoint reads only its id).
|
|
343
|
+
// Exclude those descriptions; retain host request/order/fulfillment/parcel and
|
|
344
|
+
// product/pack identities as well as all wire/evidence-bearing request values.
|
|
345
|
+
// The submission retains only this SHA-256 hash in memory, never the raw values.
|
|
346
|
+
function commandDigest(request: FulfillmentDispatchRequest): string {
|
|
347
|
+
return stableDispatchHash({
|
|
348
|
+
requestKey: request.requestKey, fulfillmentRef: request.fulfillmentRef,
|
|
349
|
+
orderRef: request.orderRef, orderNumber: request.orderNumber, parcelRef: request.parcelRef,
|
|
350
|
+
contactRevision: { schemaVersion: request.contactRevision.schemaVersion,
|
|
351
|
+
source: request.contactRevision.source, revision: request.contactRevision.revision },
|
|
352
|
+
recipient: { name: request.recipient.name, email: request.recipient.email, phone: request.recipient.phone,
|
|
353
|
+
line1: request.recipient.line1, city: request.recipient.city, postalCode: request.recipient.postalCode, country: request.recipient.country },
|
|
354
|
+
lines: request.lines.map(line => ({ skuRef: line.skuRef, productRef: line.productRef, packRef: line.packRef, quantity: line.quantity })),
|
|
355
|
+
selection: { providerRef: request.selection.providerRef, carrierRef: request.selection.carrierRef,
|
|
356
|
+
serviceRef: request.selection.serviceRef, deliveryKind: request.selection.deliveryKind,
|
|
357
|
+
pickupPointId: request.selection.pickupPoint?.id ?? null },
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Translate only the contributed client's typed error; never copy a message or payload. */
|
|
362
|
+
function stockFailure(error: unknown, operation: "get_stock" | "get_stock_movements") {
|
|
363
|
+
const failure = syncFailure(operation === "get_stock" ? "getStock" : "getStockMovements", error);
|
|
364
|
+
if (!(error instanceof OmnipackProviderError)) return failure;
|
|
365
|
+
const retryable = error.code !== "redirect_refused" && (error.retryable || error.code === "network_error");
|
|
366
|
+
return Object.assign(failure, { retryable, providerError: {
|
|
367
|
+
operation, status: error.status, code: error.code, retryable,
|
|
368
|
+
} });
|
|
369
|
+
}
|