@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,207 @@
|
|
|
1
|
+
import { durableStrings, localFailureReason, safeDiagnosticCode, operationError } from "./dispatchProjection.js";
|
|
2
|
+
import { applyFulfillmentObservationEffects } from "./fulfillmentObservationEffects.js";
|
|
3
|
+
import { observationStatusPolicy, projectObservation, projectPollEvidence, projectTrackingReference } from "./observationProjection.js";
|
|
4
|
+
import { quarantineReconciliationStateConflict } from "./providerReconciliationError.js";
|
|
5
|
+
import { isOffTrackFulfillmentStatus } from "./providerReconciliationTerminalSkip.js";
|
|
6
|
+
import { normalizeReconciliationTimestamp } from "./providerReconciliationTimestamp.js";
|
|
7
|
+
import { enrichTrackingReference, trackingReferenceComplete, uniqueTrackingReferences } from "./providerTrackingReferences.js";
|
|
8
|
+
export async function reconcileFulfillmentObservations(input) {
|
|
9
|
+
const privacy = durableStrings();
|
|
10
|
+
const failure = (stage, error) => localFailureReason(stage, error, privacy);
|
|
11
|
+
const result = {
|
|
12
|
+
ok: true, checked: 0, updated: 0, trackingRefs: 0, quarantined: 0, stateConflicts: 0, exceptions: 0,
|
|
13
|
+
stale: 0, offTrackSkips: 0, replayed: 0, failures: 0, invoiceIssueFailures: 0, invoiceIssueRefused: 0,
|
|
14
|
+
invoiceIssueRefusals: [], providerCalls: 0, readBacks: 0, skipped: false,
|
|
15
|
+
};
|
|
16
|
+
const page = await input.readPage({ page: input.page ?? 0, size: input.batchSize });
|
|
17
|
+
result.providerCalls += 1;
|
|
18
|
+
result.checked = page.length;
|
|
19
|
+
const quarantine = async (observation, reason) => {
|
|
20
|
+
const evidence = await input.lifecycle.recordQuarantine(observation, reason);
|
|
21
|
+
if (evidence.replayed)
|
|
22
|
+
result.replayed += 1;
|
|
23
|
+
result.quarantined += evidence.replayed ? 0 : 1;
|
|
24
|
+
};
|
|
25
|
+
const invoice = async (ref) => {
|
|
26
|
+
try {
|
|
27
|
+
await input.lifecycle.issueAccountingInvoice?.(ref);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const refusal = input.lifecycle.invoiceFailure(error);
|
|
31
|
+
if (refusal.permanent) {
|
|
32
|
+
result.invoiceIssueRefused += 1;
|
|
33
|
+
const identifier = privacy.token(refusal.identifier, safeDiagnosticCode), sqlstate = privacy.token(refusal.sqlstate, safeDiagnosticCode);
|
|
34
|
+
if (result.invoiceIssueRefusals.length < 10)
|
|
35
|
+
result.invoiceIssueRefusals.push({ fulfillmentRef: ref,
|
|
36
|
+
identifier, sqlstate: sqlstate ?? "22023", benign: refusal.benign });
|
|
37
|
+
result.reason ??= failure("reconciliation_invoice_issue_refused", { causeMessage: identifier, causeCode: sqlstate, message: error instanceof Error ? error.message : undefined });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
result.ok = false;
|
|
41
|
+
result.failures += 1;
|
|
42
|
+
result.invoiceIssueFailures += 1;
|
|
43
|
+
result.reason ??= failure("reconciliation_invoice_issue_failed", error);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
for (const readback of page) {
|
|
47
|
+
let observation = null;
|
|
48
|
+
try {
|
|
49
|
+
if (readback.kind === "unknown") {
|
|
50
|
+
result.ok = false;
|
|
51
|
+
result.failures += 1;
|
|
52
|
+
result.reason ??= "fulfillment_reconciliation_provider_read_failed";
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (readback.kind === "unmapped") {
|
|
56
|
+
// An unknown primary status still correlates/ACKs, but an off-track row
|
|
57
|
+
// must never trade a benign skip for a quarantine alarm.
|
|
58
|
+
const evidence = projectPollEvidence(readback.evidence);
|
|
59
|
+
const ref = await input.lifecycle.findDispatchRef(evidence);
|
|
60
|
+
if (!ref) {
|
|
61
|
+
await quarantine(evidence, "missing_correlation");
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const localState = await input.localPersistence.readLocalFulfillmentStatus(ref.fulfillmentRef);
|
|
65
|
+
if (isOffTrackFulfillmentStatus(localState)) {
|
|
66
|
+
result.offTrackSkips += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!(readback.externalRef?.trim() || ref.externalRef?.trim())) {
|
|
70
|
+
await quarantine(evidence, "missing_external_ref");
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
await input.lifecycle.acknowledgeDispatchAcceptance(ref, evidence);
|
|
74
|
+
await quarantine(evidence, "unknown_status");
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (readback.kind !== "found") {
|
|
78
|
+
// Neither unsupported nor a provider absence is a local transition.
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
observation = projectObservation(readback.observation, input.lifecycle.trackingCodecs.approveTrackingUrl, input.lifecycle.trackingCodecs);
|
|
82
|
+
if (observation.diagnostics?.redactedFieldCount) {
|
|
83
|
+
result.redactedFieldCount = (result.redactedFieldCount ?? 0) + observation.diagnostics.redactedFieldCount;
|
|
84
|
+
}
|
|
85
|
+
const ref = await input.lifecycle.findDispatchRef(observation);
|
|
86
|
+
if (!ref) {
|
|
87
|
+
await quarantine(observation, "missing_correlation");
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const localState = await input.localPersistence.readLocalFulfillmentStatus(ref.fulfillmentRef);
|
|
91
|
+
const offTrack = isOffTrackFulfillmentStatus(localState);
|
|
92
|
+
if (offTrack)
|
|
93
|
+
result.offTrackSkips += 1;
|
|
94
|
+
if (!offTrack) {
|
|
95
|
+
if (!(readback.externalRef.trim() || ref.externalRef?.trim())) {
|
|
96
|
+
await quarantine(observation, "missing_external_ref");
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
await input.lifecycle.acknowledgeDispatchAcceptance(ref, observation);
|
|
100
|
+
}
|
|
101
|
+
const occurredAt = normalizeReconciliationTimestamp(readback.updatedAt);
|
|
102
|
+
const localStatus = observation.fact.canonicalStatus;
|
|
103
|
+
const latest = await input.localPersistence.latestStatusOccurredAt(ref.fulfillmentRef);
|
|
104
|
+
if (latest && occurredAt && Date.parse(occurredAt) < Date.parse(latest)) {
|
|
105
|
+
result.stale += 1;
|
|
106
|
+
if (!offTrack && input.lifecycle.issueAccountingInvoice && await input.localPersistence.readHandedOverAt(ref.fulfillmentRef))
|
|
107
|
+
await invoice(ref.fulfillmentRef);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// The normalized fact carries the same nullable occurrence used by dedupe.
|
|
111
|
+
observation = { ...observation, fact: { ...observation.fact, occurredAt } };
|
|
112
|
+
const status = await input.lifecycle.recordStatusEvidence(ref, observation);
|
|
113
|
+
if (status.replayed)
|
|
114
|
+
result.replayed += 1;
|
|
115
|
+
if (offTrack)
|
|
116
|
+
continue;
|
|
117
|
+
result.updated += status.replayed ? 0 : 1;
|
|
118
|
+
if (localStatus === "exception") {
|
|
119
|
+
const exception = await input.lifecycle.recordProviderException(ref, observation);
|
|
120
|
+
if (exception.replayed)
|
|
121
|
+
result.replayed += 1;
|
|
122
|
+
result.exceptions += exception.replayed ? 0 : 1;
|
|
123
|
+
if (input.lifecycle.issueAccountingInvoice && await input.localPersistence.readHandedOverAt(ref.fulfillmentRef))
|
|
124
|
+
await invoice(ref.fulfillmentRef);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const trackingRefs = [];
|
|
128
|
+
if (observationStatusPolicy.handsOver(localStatus)) {
|
|
129
|
+
let carrier;
|
|
130
|
+
for (const tracking of uniqueTrackingReferences(observation.trackingRefs)) {
|
|
131
|
+
if (!trackingReferenceComplete(tracking) && carrier === undefined)
|
|
132
|
+
carrier = await input.lifecycle.readDeliveryCarrier(ref.fulfillmentRef);
|
|
133
|
+
const codecs = input.lifecycle.trackingCodecs;
|
|
134
|
+
const candidate = enrichTrackingReference(tracking, carrier ?? null, { ...codecs, publicUrl: () => null });
|
|
135
|
+
const enriched = projectTrackingReference(candidate, privacy, codecs.approveTrackingUrl, codecs);
|
|
136
|
+
if (!enriched)
|
|
137
|
+
continue;
|
|
138
|
+
// Host-built links use admitted parts and pass through unchanged, including queries.
|
|
139
|
+
enriched.publicUrl ??= codecs.publicUrl(enriched.carrierRef, enriched.trackingNumber);
|
|
140
|
+
trackingRefs.push({ trackingNumber: enriched.trackingNumber, trackingUrl: enriched.publicUrl, carrierKind: enriched.carrierRef, service: enriched.serviceRef });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
await applyFulfillmentObservationEffects({
|
|
144
|
+
port: input.lifecycle.effects, localStatus, ids: { orderId: ref.orderRef, fulfillmentOrderId: ref.fulfillmentRef }, observation,
|
|
145
|
+
trackingRefs, stockIdempotencyKey: input.lifecycle.keys.stock(ref.fulfillmentRef), handoffIdempotencyKey: input.lifecycle.keys.handoff(ref.fulfillmentRef),
|
|
146
|
+
trackingIdempotencyKey: number => input.lifecycle.keys.tracking(observation.eventKey, number),
|
|
147
|
+
issueAccountingInvoice: input.lifecycle.issueAccountingInvoice ? () => invoice(ref.fulfillmentRef) : undefined,
|
|
148
|
+
onReplayed: () => { result.replayed += 1; },
|
|
149
|
+
onTrackingRecorded: tracking => {
|
|
150
|
+
if (tracking.replayed)
|
|
151
|
+
result.replayed += 1;
|
|
152
|
+
if (tracking.readBack)
|
|
153
|
+
result.readBacks += 1;
|
|
154
|
+
result.trackingRefs += tracking.replayed ? 0 : 1;
|
|
155
|
+
},
|
|
156
|
+
}, observationStatusPolicy);
|
|
157
|
+
}
|
|
158
|
+
catch (caught) {
|
|
159
|
+
const error = observation ? await quarantineReconciliationStateConflict(caught, observation, input.lifecycle.classifyStateConflict, input.lifecycle.recordQuarantine, result) : caught;
|
|
160
|
+
if (error === null)
|
|
161
|
+
continue;
|
|
162
|
+
result.ok = false;
|
|
163
|
+
result.failures += 1;
|
|
164
|
+
result.reason ??= failure("reconciliation_local_operation_failed", error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { ...result, ...(privacy.redactedFieldCount ? { redactedFieldCount: (result.redactedFieldCount ?? 0) + privacy.redactedFieldCount } : {}) };
|
|
168
|
+
}
|
|
169
|
+
/** Host owns scheduling, HTTP and durable job-ledger writes. This operation only projects them. */
|
|
170
|
+
export async function runFulfillmentReconciliationPage(input) {
|
|
171
|
+
if (!Number.isSafeInteger(input.pageSize) || input.pageSize <= 0)
|
|
172
|
+
throw new Error("reconciliation_page_size_invalid");
|
|
173
|
+
const isPage = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
174
|
+
const checkpoint = input.checkpoint;
|
|
175
|
+
const saved = checkpoint?.reconciliationPagination;
|
|
176
|
+
const page = saved?.version === 1 && saved.pageSize === input.pageSize && isPage(saved.page) && isPage(saved.nextPage)
|
|
177
|
+
&& (saved.nextPage === 0 || saved.nextPage === saved.page || saved.page < Number.MAX_SAFE_INTEGER && saved.nextPage === saved.page + 1) ? saved.nextPage : 0;
|
|
178
|
+
const privacy = durableStrings();
|
|
179
|
+
let result;
|
|
180
|
+
let pageRead = false;
|
|
181
|
+
const empty = () => ({ ok: true, checked: 0, updated: 0, trackingRefs: 0, quarantined: 0, stateConflicts: 0, exceptions: 0,
|
|
182
|
+
stale: 0, offTrackSkips: 0, replayed: 0, failures: 0, invoiceIssueFailures: 0, invoiceIssueRefused: 0, invoiceIssueRefusals: [], providerCalls: 1, readBacks: 0, skipped: false });
|
|
183
|
+
try {
|
|
184
|
+
const response = await input.provider.readPage({ page, size: input.pageSize });
|
|
185
|
+
if (response.kind === "page_exhausted")
|
|
186
|
+
result = { ...empty(), reason: "page_exhausted" };
|
|
187
|
+
else if (response.kind === "failed") {
|
|
188
|
+
const providerError = response.error ? operationError(response.error) : undefined;
|
|
189
|
+
result = { ...empty(), ok: false, failures: 1, ...(providerError ? { providerError } : {}),
|
|
190
|
+
reason: providerError?.code ?? privacy.token(response.refusal.reason) ?? "redacted" };
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
pageRead = true;
|
|
194
|
+
result = await reconcileFulfillmentObservations({ ...input, batchSize: input.pageSize, page, readPage: async () => response.rows });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
result = { ...empty(), ok: false, failures: 1, reason: localFailureReason(pageRead ? "reconciliation_page_failed" : "reconciliation_provider_read_failed", error, privacy) };
|
|
199
|
+
}
|
|
200
|
+
if (privacy.redactedFieldCount)
|
|
201
|
+
result.redactedFieldCount = (result.redactedFieldCount ?? 0) + privacy.redactedFieldCount;
|
|
202
|
+
// A pre-traversal abort must not skip a page. Item failures return normally.
|
|
203
|
+
const nextPage = pageRead && result.checked === 0 && !result.ok ? page
|
|
204
|
+
: result.checked === input.pageSize && page < Number.MAX_SAFE_INTEGER ? page + 1 : 0;
|
|
205
|
+
return { status: result.ok ? 200 : 502, ledgerStatus: result.ok ? "succeeded" : "failed", result,
|
|
206
|
+
metadata: { reconciliationPagination: { version: 1, page, pageSize: input.pageSize, nextPage } } };
|
|
207
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FulfillmentStockSyncPort, LocalStock } from "./contracts/index.js";
|
|
2
|
+
type StockLine = Awaited<ReturnType<FulfillmentStockSyncPort["readLocalInventoryStock"]>>[number];
|
|
3
|
+
export declare function aggregateAtpCompatibleStock(lines: readonly StockLine[], now: Date): Map<string, LocalStock>;
|
|
4
|
+
/** Cap the host-local movement window at UTC today, preserving donor overlap. */
|
|
5
|
+
export declare function stockMovementWindow(cursor: {
|
|
6
|
+
lastMovementOccurredAt: string | null;
|
|
7
|
+
lastStockSyncedAt: string | null;
|
|
8
|
+
} | null, now: Date, timeZone: string): {
|
|
9
|
+
startLocalDate: string;
|
|
10
|
+
endLocalDate: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function emptyLocal(sku: string): LocalStock;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function aggregateAtpCompatibleStock(lines, now) {
|
|
2
|
+
const requestedAt = now.toISOString();
|
|
3
|
+
const balances = new Map();
|
|
4
|
+
for (const line of lines.filter((item) => isAtpUsable(item, requestedAt))) {
|
|
5
|
+
const current = balances.get(line.skuRef) ?? emptyLocal(line.skuRef);
|
|
6
|
+
current.onHand += line.quantity.onHand;
|
|
7
|
+
current.reserved += line.quantity.reserved;
|
|
8
|
+
current.unavailable += line.quantity.unavailable;
|
|
9
|
+
current.safetyStock += line.quantity.safetyStock;
|
|
10
|
+
balances.set(line.skuRef, current);
|
|
11
|
+
}
|
|
12
|
+
return balances;
|
|
13
|
+
}
|
|
14
|
+
/** Cap the host-local movement window at UTC today, preserving donor overlap. */
|
|
15
|
+
export function stockMovementWindow(cursor, now, timeZone) {
|
|
16
|
+
const startAt = new Date(cursor?.lastMovementOccurredAt ?? cursor?.lastStockSyncedAt ?? now.toISOString());
|
|
17
|
+
const safeStartAt = Number.isNaN(startAt.getTime()) ? now : startAt;
|
|
18
|
+
const endLocalDate = minDate(localDate(now, timeZone), utcDate(now));
|
|
19
|
+
return {
|
|
20
|
+
startLocalDate: minDate(localDate(safeStartAt, timeZone), endLocalDate),
|
|
21
|
+
endLocalDate,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function utcDate(date) {
|
|
25
|
+
return date.toISOString().slice(0, 10);
|
|
26
|
+
}
|
|
27
|
+
function minDate(a, b) {
|
|
28
|
+
return a <= b ? a : b;
|
|
29
|
+
}
|
|
30
|
+
function isAtpUsable(line, requestedAt) {
|
|
31
|
+
if (!line.fulfillable || line.locationStatus !== "active")
|
|
32
|
+
return false;
|
|
33
|
+
if (line.lotStatus && line.lotStatus !== "available")
|
|
34
|
+
return false;
|
|
35
|
+
if (line.expiresAt && Date.parse(line.expiresAt) <= Date.parse(requestedAt))
|
|
36
|
+
return false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
export function emptyLocal(sku) {
|
|
40
|
+
return { skuRef: sku, onHand: 0, reserved: 0, unavailable: 0, safetyStock: 0 };
|
|
41
|
+
}
|
|
42
|
+
function localDate(date, timeZone) {
|
|
43
|
+
const parts = new Intl.DateTimeFormat("en", {
|
|
44
|
+
timeZone,
|
|
45
|
+
year: "numeric",
|
|
46
|
+
month: "2-digit",
|
|
47
|
+
day: "2-digit",
|
|
48
|
+
}).formatToParts(date);
|
|
49
|
+
const byType = new Map(parts.map((part) => [part.type, part.value]));
|
|
50
|
+
return `${byType.get("year")}-${byType.get("month")}-${byType.get("day")}`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { FulfillmentStockSyncPort, FulfillmentStockSyncProvider, SyncCursor } from "./contracts/index.js";
|
|
2
|
+
type Threshold = Parameters<FulfillmentStockSyncPort["resolveLowStockEvidence"]>[0]["activeThresholdKinds"][number];
|
|
3
|
+
interface StockSyncResult {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
checked: number;
|
|
6
|
+
updated: number;
|
|
7
|
+
mismatches: number;
|
|
8
|
+
reservationCoverage: number;
|
|
9
|
+
unclassified: number;
|
|
10
|
+
lowStock: number;
|
|
11
|
+
movements: number;
|
|
12
|
+
replayed: number;
|
|
13
|
+
failures: number;
|
|
14
|
+
providerCalls: number;
|
|
15
|
+
readBacks: number;
|
|
16
|
+
skipped: boolean;
|
|
17
|
+
syncRunId: string;
|
|
18
|
+
lastMovementOccurredAt: string | null;
|
|
19
|
+
reason?: string;
|
|
20
|
+
rejected: number;
|
|
21
|
+
items: NonNullable<SyncCursor["items"]>[number][];
|
|
22
|
+
providerError?: NonNullable<SyncCursor["error"]>["providerError"];
|
|
23
|
+
refusal?: NonNullable<SyncCursor["refusal"]>;
|
|
24
|
+
}
|
|
25
|
+
export declare function runFulfillmentStockSyncWorker(input: {
|
|
26
|
+
port: FulfillmentStockSyncPort;
|
|
27
|
+
provider: FulfillmentStockSyncProvider;
|
|
28
|
+
movementBatchSize: number;
|
|
29
|
+
staleSyncThresholdHours?: number;
|
|
30
|
+
now?: () => Date;
|
|
31
|
+
compatibility: {
|
|
32
|
+
syncRunId(now: Date): string;
|
|
33
|
+
staleEvidenceSku: string;
|
|
34
|
+
timeZone: string;
|
|
35
|
+
currentKey(run: string, sku: string): string;
|
|
36
|
+
snapshotKey(run: string, sku: string): string;
|
|
37
|
+
lowStockKey(sku: string, threshold: Threshold): string;
|
|
38
|
+
staleKey(lastSyncedAt: string): string;
|
|
39
|
+
};
|
|
40
|
+
}): Promise<StockSyncResult>;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { aggregateAtpCompatibleStock, emptyLocal, stockMovementWindow } from "./providerStockAvailability.js";
|
|
2
|
+
export async function runFulfillmentStockSyncWorker(input) {
|
|
3
|
+
const now = input.now?.() ?? new Date();
|
|
4
|
+
const staleThresholdHours = input.staleSyncThresholdHours && input.staleSyncThresholdHours > 0
|
|
5
|
+
? input.staleSyncThresholdHours
|
|
6
|
+
: 6;
|
|
7
|
+
const syncRunId = input.compatibility.syncRunId(now);
|
|
8
|
+
const result = {
|
|
9
|
+
ok: true,
|
|
10
|
+
rejected: 0,
|
|
11
|
+
items: [],
|
|
12
|
+
checked: 0,
|
|
13
|
+
updated: 0,
|
|
14
|
+
mismatches: 0,
|
|
15
|
+
reservationCoverage: 0,
|
|
16
|
+
unclassified: 0,
|
|
17
|
+
lowStock: 0,
|
|
18
|
+
movements: 0,
|
|
19
|
+
replayed: 0,
|
|
20
|
+
failures: 0,
|
|
21
|
+
providerCalls: 0,
|
|
22
|
+
readBacks: 0,
|
|
23
|
+
skipped: false,
|
|
24
|
+
syncRunId,
|
|
25
|
+
lastMovementOccurredAt: null,
|
|
26
|
+
};
|
|
27
|
+
const alerts = [];
|
|
28
|
+
let stage = "cursor_read_failed";
|
|
29
|
+
let operationalReason;
|
|
30
|
+
let staleSync;
|
|
31
|
+
let recoverStaleSync = false;
|
|
32
|
+
let previousCursor = null;
|
|
33
|
+
let rejectedStockResponse = false;
|
|
34
|
+
const currentCoverageGapSkus = new Set();
|
|
35
|
+
try {
|
|
36
|
+
previousCursor = await input.port.readCursor();
|
|
37
|
+
// Stale-sync alert: if the last successful sync aged past the threshold, open a stale_sync
|
|
38
|
+
// low-stock evidence row (evidence/warn — never blocks sales); a healthy run resolves it
|
|
39
|
+
// below. A never-synced state (no prior cursor) is not "stale".
|
|
40
|
+
const lastSyncedMs = previousCursor?.lastStockSyncedAt ? Date.parse(previousCursor.lastStockSyncedAt) : null;
|
|
41
|
+
const staleGapHours = lastSyncedMs !== null && !Number.isNaN(lastSyncedMs) ? (now.getTime() - lastSyncedMs) / 3_600_000 : null;
|
|
42
|
+
const syncIsStale = staleGapHours !== null && staleGapHours >= staleThresholdHours;
|
|
43
|
+
recoverStaleSync = !syncIsStale;
|
|
44
|
+
if (syncIsStale) {
|
|
45
|
+
staleSync = { gapHours: Math.round(staleGapHours * 10) / 10, thresholdHours: staleThresholdHours, detectedAt: now.toISOString() };
|
|
46
|
+
stage = "stock_evidence_write_failed";
|
|
47
|
+
alerts.push({ skuRef: input.compatibility.staleEvidenceSku, thresholdKind: "stale_sync", severity: "warning",
|
|
48
|
+
providerForSaleQuantity: 0, localAvailableQuantity: 0, ...staleSync });
|
|
49
|
+
await input.port.recordLowStockEvidence({
|
|
50
|
+
requestKey: input.compatibility.staleKey(previousCursor.lastStockSyncedAt),
|
|
51
|
+
skuRef: input.compatibility.staleEvidenceSku,
|
|
52
|
+
thresholdKind: "stale_sync",
|
|
53
|
+
severity: "warning",
|
|
54
|
+
providerForSaleQuantity: 0,
|
|
55
|
+
localAvailableQuantity: 0,
|
|
56
|
+
syncRunId,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
stage = "cursor_write_failed";
|
|
60
|
+
await input.port.recordCursor({
|
|
61
|
+
syncRunId,
|
|
62
|
+
status: "running",
|
|
63
|
+
lastStockSyncedAt: null,
|
|
64
|
+
lastMovementOccurredAt: null,
|
|
65
|
+
continuation: { nextRef: null, startLocalDate: null, endLocalDate: null },
|
|
66
|
+
coverageGapSkus: previousCursor?.coverageGapSkus ?? [], refusal: null,
|
|
67
|
+
});
|
|
68
|
+
stage = "stock_read_failed";
|
|
69
|
+
result.providerCalls += 1;
|
|
70
|
+
const [localStock, providerStock] = await Promise.all([
|
|
71
|
+
input.port.readLocalInventoryStock(now.toISOString()),
|
|
72
|
+
input.provider.getStock(),
|
|
73
|
+
]);
|
|
74
|
+
const stockItems = [];
|
|
75
|
+
for (const [itemIndex, item] of providerStock.entries()) {
|
|
76
|
+
if ("kind" in item && item.kind === "rejected") {
|
|
77
|
+
result.rejected += 1;
|
|
78
|
+
result.failures += 1;
|
|
79
|
+
result.items.push({ kind: "stock", itemIndex: item.itemIndex, skuRef: null, status: "rejected", reason: item.reason });
|
|
80
|
+
}
|
|
81
|
+
else if ("skuRef" in item && item.skuRef.trim()) {
|
|
82
|
+
stockItems.push({ stock: item, itemIndex });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const skus = [...new Set(stockItems.map(({ stock }) => stock.skuRef))];
|
|
86
|
+
rejectedStockResponse = providerStock.length > 0 && stockItems.length === 0;
|
|
87
|
+
stage = "stock_inventory_read_failed";
|
|
88
|
+
const [inventoryClasses, activeReservations] = await Promise.all([
|
|
89
|
+
input.port.readSkuInventoryClasses(skus),
|
|
90
|
+
input.port.readActiveReservations(skus, now.toISOString()),
|
|
91
|
+
]);
|
|
92
|
+
stage = "stock_movements_read_failed";
|
|
93
|
+
result.providerCalls += 1;
|
|
94
|
+
const movementRows = await input.provider.getStockMovements({ ...stockMovementWindow(previousCursor, now, input.compatibility.timeZone), size: input.movementBatchSize });
|
|
95
|
+
const movements = [];
|
|
96
|
+
for (const item of movementRows) {
|
|
97
|
+
if ("kind" in item && item.kind === "rejected") {
|
|
98
|
+
result.rejected += 1;
|
|
99
|
+
result.failures += 1;
|
|
100
|
+
result.items.push({ kind: "movement", itemIndex: item.itemIndex, skuRef: null, status: "rejected", reason: item.reason });
|
|
101
|
+
}
|
|
102
|
+
else if ("skuRef" in item) {
|
|
103
|
+
movements.push(item);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (rejectedStockResponse || (result.rejected > 0 && stockItems.length + movements.length === 0)) {
|
|
107
|
+
result.ok = false;
|
|
108
|
+
result.reason = "invalid_sku_ref";
|
|
109
|
+
}
|
|
110
|
+
result.movements = movements.length;
|
|
111
|
+
result.lastMovementOccurredAt = latestOccurredAt(movements);
|
|
112
|
+
const localBySku = aggregateAtpCompatibleStock(localStock, now);
|
|
113
|
+
const coverageCandidates = [];
|
|
114
|
+
const packagingSkus = new Set();
|
|
115
|
+
const previousCoverageGapSkus = previousCursor?.status === "succeeded"
|
|
116
|
+
? readStringSet(previousCursor.coverageGapSkus)
|
|
117
|
+
: new Set();
|
|
118
|
+
for (const { stock, itemIndex } of stockItems) {
|
|
119
|
+
try {
|
|
120
|
+
const local = localBySku.get(stock.skuRef) ?? emptyLocal(stock.skuRef);
|
|
121
|
+
const inventoryClass = inventoryClasses.get(stock.skuRef) ?? null;
|
|
122
|
+
const activeReserved = activeReservations.get(stock.skuRef) ?? 0;
|
|
123
|
+
const staleAfter = new Date(now.getTime() + staleThresholdHours * 3_600_000).toISOString();
|
|
124
|
+
stage = "stock_current_write_failed";
|
|
125
|
+
const current = await input.port.recordProviderStockCurrent({
|
|
126
|
+
requestKey: input.compatibility.currentKey(syncRunId, stock.skuRef),
|
|
127
|
+
snapshot: stock, inventoryClass, lastSyncedAt: now.toISOString(), staleAfter,
|
|
128
|
+
});
|
|
129
|
+
stage = "snapshot_write_failed";
|
|
130
|
+
const snapshot = await input.port.recordStockSnapshot({
|
|
131
|
+
requestKey: input.compatibility.snapshotKey(syncRunId, stock.skuRef),
|
|
132
|
+
snapshot: stock, local, mismatchKind: "none", inventoryClass,
|
|
133
|
+
});
|
|
134
|
+
result.items.push({ itemIndex, skuRef: stock.skuRef, status: "recorded", activeReserved,
|
|
135
|
+
sellableNow: inventoryClass === "sellable" ? Math.max(0, stock.forSaleQuantity - activeReserved - local.safetyStock) : null,
|
|
136
|
+
coverageGapQuantity: Math.max(0, activeReserved - stock.forSaleQuantity) });
|
|
137
|
+
result.checked += 1;
|
|
138
|
+
result.updated += (snapshot.replayed ? 0 : 1) + (current.replayed ? 0 : 1);
|
|
139
|
+
if (snapshot.replayed)
|
|
140
|
+
result.replayed += 1;
|
|
141
|
+
if (current.replayed)
|
|
142
|
+
result.replayed += 1;
|
|
143
|
+
if (snapshot.readBack)
|
|
144
|
+
result.readBacks += 1;
|
|
145
|
+
if (current.readBack)
|
|
146
|
+
result.readBacks += 1;
|
|
147
|
+
if (inventoryClass === null)
|
|
148
|
+
result.unclassified += 1;
|
|
149
|
+
if (inventoryClass === "packaging")
|
|
150
|
+
packagingSkus.add(stock.skuRef);
|
|
151
|
+
if (inventoryClass === "sellable") {
|
|
152
|
+
coverageCandidates.push({ stock, local, activeReserved });
|
|
153
|
+
if (activeReserved > stock.forSaleQuantity)
|
|
154
|
+
currentCoverageGapSkus.add(stock.skuRef);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
result.ok = false;
|
|
159
|
+
result.failures += 1;
|
|
160
|
+
const reason = safeReason(error, stage);
|
|
161
|
+
operationalReason ??= reason;
|
|
162
|
+
result.items.push({ itemIndex, skuRef: stock.skuRef, status: "failed", reason });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (result.ok) {
|
|
166
|
+
for (const { stock, local, activeReserved } of coverageCandidates) {
|
|
167
|
+
const localAvailable = availableQuantity(local);
|
|
168
|
+
const confirmedCoverageGap = currentCoverageGapSkus.has(stock.skuRef) && previousCoverageGapSkus.has(stock.skuRef);
|
|
169
|
+
const lowStockSignals = lowStockSignalsFor(stock, local, activeReserved, confirmedCoverageGap);
|
|
170
|
+
const activeThresholdKinds = lowStockSignals.map((signal) => signal.thresholdKind);
|
|
171
|
+
if (currentCoverageGapSkus.has(stock.skuRef) && !activeThresholdKinds.includes("reservation_coverage")) {
|
|
172
|
+
activeThresholdKinds.push("reservation_coverage");
|
|
173
|
+
}
|
|
174
|
+
for (const signal of lowStockSignals) {
|
|
175
|
+
alerts.push({ skuRef: stock.skuRef, thresholdKind: signal.thresholdKind, severity: signal.severity,
|
|
176
|
+
providerForSaleQuantity: stock.forSaleQuantity, localAvailableQuantity: localAvailable, detectedAt: now.toISOString(),
|
|
177
|
+
activeReserved, coverageGapQuantity: Math.max(0, activeReserved - stock.forSaleQuantity) });
|
|
178
|
+
stage = "stock_evidence_write_failed";
|
|
179
|
+
const evidence = await input.port.recordLowStockEvidence({
|
|
180
|
+
requestKey: input.compatibility.lowStockKey(stock.skuRef, signal.thresholdKind),
|
|
181
|
+
skuRef: stock.skuRef, thresholdKind: signal.thresholdKind, severity: signal.severity,
|
|
182
|
+
providerForSaleQuantity: stock.forSaleQuantity, localAvailableQuantity: localAvailable, syncRunId,
|
|
183
|
+
});
|
|
184
|
+
if (evidence.replayed)
|
|
185
|
+
result.replayed += 1;
|
|
186
|
+
result.lowStock += evidence.replayed ? 0 : 1;
|
|
187
|
+
if (signal.thresholdKind === "reservation_coverage")
|
|
188
|
+
result.reservationCoverage += 1;
|
|
189
|
+
}
|
|
190
|
+
stage = "stock_evidence_resolve_failed";
|
|
191
|
+
await input.port.resolveLowStockEvidence({
|
|
192
|
+
skuRef: stock.skuRef,
|
|
193
|
+
activeThresholdKinds,
|
|
194
|
+
syncRunId,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
for (const sku of packagingSkus) {
|
|
198
|
+
stage = "stock_evidence_resolve_failed";
|
|
199
|
+
await input.port.resolveLowStockEvidence({
|
|
200
|
+
skuRef: sku,
|
|
201
|
+
activeThresholdKinds: [],
|
|
202
|
+
syncRunId,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
result.ok = false;
|
|
209
|
+
result.failures += 1;
|
|
210
|
+
result.providerError = providerDiagnostic(error);
|
|
211
|
+
if (stage === "stock_read_failed" || stage === "stock_movements_read_failed")
|
|
212
|
+
result.refusal = admissionRefusal(error);
|
|
213
|
+
operationalReason ??= result.providerError ? `provider_${result.providerError.operation}_failed:${result.providerError.code}` : result.refusal?.reason ?? safeReason(error, stage);
|
|
214
|
+
}
|
|
215
|
+
result.reason = operationalReason ?? result.reason;
|
|
216
|
+
if (!result.ok)
|
|
217
|
+
result.refusal ??= { reason: result.failures === result.rejected ? "malformed" : "io_failure", phase: "sync", retryable: result.providerError?.retryable ?? result.failures > result.rejected };
|
|
218
|
+
await input.port.recordCursor({
|
|
219
|
+
status: result.ok ? "succeeded" : "failed", syncRunId,
|
|
220
|
+
lastStockSyncedAt: result.ok ? now.toISOString() : null,
|
|
221
|
+
lastMovementOccurredAt: result.lastMovementOccurredAt,
|
|
222
|
+
continuation: { nextRef: null, startLocalDate: null, endLocalDate: null },
|
|
223
|
+
coverageGapSkus: result.ok ? [...currentCoverageGapSkus].sort() : rejectedStockResponse ? previousCursor?.coverageGapSkus ?? [] : [],
|
|
224
|
+
refusal: result.refusal ?? null,
|
|
225
|
+
error: result.ok ? null : { reason: result.reason, ...(result.providerError ? { providerError: result.providerError } : {}) },
|
|
226
|
+
counts: { checked: result.checked, updated: result.updated, movements: result.movements, rejected: result.rejected,
|
|
227
|
+
failures: result.failures, providerCalls: result.providerCalls, readBacks: result.readBacks },
|
|
228
|
+
items: result.items, alerts, staleSync,
|
|
229
|
+
});
|
|
230
|
+
// Recovery follows the durable succeeded cursor; a failed cleanup leaves the completed run intact.
|
|
231
|
+
if (result.ok && recoverStaleSync) {
|
|
232
|
+
try {
|
|
233
|
+
await input.port.resolveLowStockEvidence({
|
|
234
|
+
skuRef: input.compatibility.staleEvidenceSku,
|
|
235
|
+
activeThresholdKinds: [],
|
|
236
|
+
syncRunId,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch { /* Best-effort cleanup must not undo the completed run. */ }
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
function lowStockSignalsFor(stock, local, activeReserved, confirmedCoverageGap) {
|
|
244
|
+
const signals = [];
|
|
245
|
+
const remainingAfterReservations = Math.max(0, stock.forSaleQuantity - activeReserved);
|
|
246
|
+
if (local.safetyStock > 0 && remainingAfterReservations <= local.safetyStock) {
|
|
247
|
+
signals.push({ thresholdKind: "safety_stock", severity: remainingAfterReservations <= 0 ? "critical" : "warning" });
|
|
248
|
+
}
|
|
249
|
+
if (confirmedCoverageGap)
|
|
250
|
+
signals.push({ thresholdKind: "reservation_coverage", severity: "critical" });
|
|
251
|
+
return signals;
|
|
252
|
+
}
|
|
253
|
+
function readStringSet(value) {
|
|
254
|
+
return new Set(Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0) : []);
|
|
255
|
+
}
|
|
256
|
+
function availableQuantity(balance) {
|
|
257
|
+
return Math.max(0, balance.onHand - balance.reserved - balance.unavailable - balance.safetyStock);
|
|
258
|
+
}
|
|
259
|
+
function latestOccurredAt(movements) {
|
|
260
|
+
return movements
|
|
261
|
+
.map((movement) => movement.occurredAt)
|
|
262
|
+
.filter((value) => Boolean(value))
|
|
263
|
+
.sort((a, b) => Date.parse(b) - Date.parse(a))[0] ?? null;
|
|
264
|
+
}
|
|
265
|
+
/** Keep the operation's closed identity and SQLSTATE only, never arbitrary exception prose. */
|
|
266
|
+
function safeReason(error, operation) {
|
|
267
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : null;
|
|
268
|
+
if (typeof code === "string" && /^[0-9A-Z]{5}$/.test(code))
|
|
269
|
+
return `${operation}:${code}`;
|
|
270
|
+
const known = error instanceof Error ? error.message.match(new RegExp(`^${operation}:([0-9A-Z]{5})$`)) : null;
|
|
271
|
+
return known ? `${operation}:${known[1]}` : operation;
|
|
272
|
+
}
|
|
273
|
+
function providerDiagnostic(error) {
|
|
274
|
+
if (!error || typeof error !== "object" || !("providerError" in error))
|
|
275
|
+
return undefined;
|
|
276
|
+
const detail = error.providerError;
|
|
277
|
+
if (!detail || typeof detail !== "object" || !("operation" in detail) || !("code" in detail) || !("status" in detail) || !("retryable" in detail))
|
|
278
|
+
return undefined;
|
|
279
|
+
if (detail.operation !== "get_stock" && detail.operation !== "get_stock_movements")
|
|
280
|
+
return undefined;
|
|
281
|
+
if (typeof detail.code !== "string" || !/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(detail.code))
|
|
282
|
+
return undefined;
|
|
283
|
+
return { operation: detail.operation, code: detail.code,
|
|
284
|
+
status: typeof detail.status === "number" && Number.isInteger(detail.status) && detail.status >= 100 && detail.status <= 599 ? detail.status : null,
|
|
285
|
+
retryable: detail.retryable === true };
|
|
286
|
+
}
|
|
287
|
+
function admissionRefusal(error) {
|
|
288
|
+
if (!error || typeof error !== "object" || !("reason" in error) || !("phase" in error) || !("retryable" in error))
|
|
289
|
+
return undefined;
|
|
290
|
+
if (error.phase !== "sync" || typeof error.retryable !== "boolean")
|
|
291
|
+
return undefined;
|
|
292
|
+
if (error.reason !== "disabled" && error.reason !== "not_ready" && error.reason !== "malformed")
|
|
293
|
+
return undefined;
|
|
294
|
+
return { reason: error.reason, phase: error.phase, retryable: error.retryable };
|
|
295
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FulfillmentTrackingReference, FulfillmentObservationCodecs } from "./contracts/index.js";
|
|
2
|
+
export declare function trackingReferenceComplete(ref: FulfillmentTrackingReference): boolean;
|
|
3
|
+
/** Provider evidence wins as a unit; private carrier/URL codecs stay injected. */
|
|
4
|
+
export declare function enrichTrackingReference(ref: FulfillmentTrackingReference, deliveryCarrier: {
|
|
5
|
+
carrierRef: string | null;
|
|
6
|
+
serviceRef: string | null;
|
|
7
|
+
} | null, codecs: FulfillmentObservationCodecs & {
|
|
8
|
+
approveTrackingUrl?(url: string): boolean;
|
|
9
|
+
normalizeCarrier(input: {
|
|
10
|
+
carrierRef: string | null;
|
|
11
|
+
serviceRef: string | null;
|
|
12
|
+
}): string | null;
|
|
13
|
+
publicUrl(carrierRef: string | null, trackingNumber: string): string | null;
|
|
14
|
+
}): FulfillmentTrackingReference;
|
|
15
|
+
/** First occurrence wins, including its selection, after trimming the number. */
|
|
16
|
+
export declare function uniqueTrackingReferences(refs: readonly FulfillmentTrackingReference[]): FulfillmentTrackingReference[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function trackingReferenceComplete(ref) {
|
|
2
|
+
return Boolean(ref.carrierRef && ref.publicUrl && ref.serviceRef);
|
|
3
|
+
}
|
|
4
|
+
/** Provider evidence wins as a unit; private carrier/URL codecs stay injected. */
|
|
5
|
+
export function enrichTrackingReference(ref, deliveryCarrier, codecs) {
|
|
6
|
+
const providerCarrier = ref.carrierRef ?? codecs.normalizeCarrier(ref);
|
|
7
|
+
const carrierRef = providerCarrier ?? codecs.normalizeCarrier({ carrierRef: deliveryCarrier?.carrierRef ?? null, serviceRef: deliveryCarrier?.serviceRef ?? null });
|
|
8
|
+
return { trackingNumber: ref.trackingNumber, publicUrl: ref.publicUrl ?? codecs.publicUrl(carrierRef, ref.trackingNumber),
|
|
9
|
+
carrierRef, serviceRef: ref.serviceRef ?? (providerCarrier ? null : deliveryCarrier?.serviceRef ?? null) };
|
|
10
|
+
}
|
|
11
|
+
/** First occurrence wins, including its selection, after trimming the number. */
|
|
12
|
+
export function uniqueTrackingReferences(refs) {
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
const result = [];
|
|
15
|
+
for (const ref of refs) {
|
|
16
|
+
const trackingNumber = ref.trackingNumber.trim();
|
|
17
|
+
if (!trackingNumber || seen.has(trackingNumber))
|
|
18
|
+
continue;
|
|
19
|
+
seen.add(trackingNumber);
|
|
20
|
+
result.push({ trackingNumber, publicUrl: ref.publicUrl ?? null, carrierRef: ref.carrierRef ?? null, serviceRef: ref.serviceRef ?? null });
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const FULFILLMENT_REPLACEMENT_COMMAND_REFUSALS: readonly ["invalid_input", "idempotency_conflict", "order_not_found", "order_not_replaceable", "missing_client", "missing_shipping_address", "shipping_address_not_found", "payment_not_succeeded", "subscription_cycle_not_paid", "no_parcel_to_replace", "undispatched_replacement_exists", "missing_order_items", "missing_catalog_sku", "missing_inventory_reservation", "blocked_by_payment_not_succeeded_hold", "blocked_by_inventory_review_hold", "blocked_by_risk_review_hold", "blocked_by_address_review_hold", "blocked_by_manual_support_hold"];
|
|
2
|
+
export declare const FULFILLMENT_REPLACEMENT_RESERVATION_REFUSALS: readonly ["inventory_external_provider_insufficient_available_stock", "inventory_external_provider_stock_stale", "inventory_external_provider_stock_missing", "inventory_external_provider_sku_not_found", "inventory_external_provider_location_missing", "inventory_reservation_insufficient_available_stock", "inventory_reservation_allocation_incomplete", "inventory_reservation_payment_status_not_reservable", "inventory_reservation_invalid_quantity"];
|
|
3
|
+
export declare const UNSPECIFIED_FULFILLMENT_REPLACEMENT_REFUSAL = "unspecified_refusal";
|
|
4
|
+
export declare const FULFILLMENT_REPLACEMENT_REFUSALS: readonly ["invalid_input", "idempotency_conflict", "order_not_found", "order_not_replaceable", "missing_client", "missing_shipping_address", "shipping_address_not_found", "payment_not_succeeded", "subscription_cycle_not_paid", "no_parcel_to_replace", "undispatched_replacement_exists", "missing_order_items", "missing_catalog_sku", "missing_inventory_reservation", "blocked_by_payment_not_succeeded_hold", "blocked_by_inventory_review_hold", "blocked_by_risk_review_hold", "blocked_by_address_review_hold", "blocked_by_manual_support_hold", "inventory_external_provider_insufficient_available_stock", "inventory_external_provider_stock_stale", "inventory_external_provider_stock_missing", "inventory_external_provider_sku_not_found", "inventory_external_provider_location_missing", "inventory_reservation_insufficient_available_stock", "inventory_reservation_allocation_incomplete", "inventory_reservation_payment_status_not_reservable", "inventory_reservation_invalid_quantity", "unspecified_refusal"];
|
|
5
|
+
export type FulfillmentReplacementRefusal = (typeof FULFILLMENT_REPLACEMENT_REFUSALS)[number];
|