@openlup/platform 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +272 -278
- package/README.md +195 -6
- package/dist/composition/composeBundle.d.ts +13 -0
- package/dist/composition/composeBundle.js +4 -0
- package/dist/composition/fulfillmentContribution.d.ts +21 -0
- package/dist/composition/fulfillmentContribution.js +19 -0
- package/dist/composition/index.d.ts +1 -0
- package/dist/composition/index.js +2 -0
- package/dist/fulfillment/adapterConformance.d.ts +3 -0
- package/dist/fulfillment/adapterConformance.js +223 -0
- package/dist/fulfillment/bindings.d.ts +3 -0
- package/dist/fulfillment/bindings.js +29 -0
- package/dist/fulfillment/commerceFulfillmentHandlers.d.ts +29 -0
- package/dist/fulfillment/commerceFulfillmentHandlers.js +184 -0
- package/dist/fulfillment/commerceFulfillmentReadModel.d.ts +86 -0
- package/dist/fulfillment/commerceFulfillmentReadModel.js +131 -0
- package/dist/fulfillment/contracts/alignment.d.ts +25 -0
- package/dist/fulfillment/contracts/alignment.js +1 -0
- package/dist/fulfillment/contracts/bindings.d.ts +51 -0
- package/dist/fulfillment/contracts/bindings.js +1 -0
- package/dist/fulfillment/contracts/capabilities.d.ts +29 -0
- package/dist/fulfillment/contracts/capabilities.js +1 -0
- package/dist/fulfillment/contracts/conformance.d.ts +93 -0
- package/dist/fulfillment/contracts/conformance.js +1 -0
- package/dist/fulfillment/contracts/customerAlignment.d.ts +6 -0
- package/dist/fulfillment/contracts/customerAlignment.js +1 -0
- package/dist/fulfillment/contracts/dispatch.d.ts +105 -0
- package/dist/fulfillment/contracts/dispatch.js +1 -0
- package/dist/fulfillment/contracts/index.d.ts +18 -0
- package/dist/fulfillment/contracts/index.js +1 -0
- package/dist/fulfillment/contracts/label.d.ts +35 -0
- package/dist/fulfillment/contracts/label.js +1 -0
- package/dist/fulfillment/contracts/nativeUnsupported.d.ts +6 -0
- package/dist/fulfillment/contracts/nativeUnsupported.js +1 -0
- package/dist/fulfillment/contracts/observation.d.ts +52 -0
- package/dist/fulfillment/contracts/observation.js +1 -0
- package/dist/fulfillment/contracts/operator.d.ts +64 -0
- package/dist/fulfillment/contracts/operator.js +1 -0
- package/dist/fulfillment/contracts/read.d.ts +110 -0
- package/dist/fulfillment/contracts/read.js +1 -0
- package/dist/fulfillment/contracts/readback.d.ts +38 -0
- package/dist/fulfillment/contracts/readback.js +1 -0
- package/dist/fulfillment/contracts/replacement.d.ts +34 -0
- package/dist/fulfillment/contracts/replacement.js +1 -0
- package/dist/fulfillment/contracts/store.d.ts +86 -0
- package/dist/fulfillment/contracts/store.js +1 -0
- package/dist/fulfillment/contracts/sync.d.ts +170 -0
- package/dist/fulfillment/contracts/sync.js +1 -0
- package/dist/fulfillment/currentFulfillmentParcel.d.ts +2 -0
- package/dist/fulfillment/currentFulfillmentParcel.js +1 -0
- package/dist/fulfillment/delayReadModel.d.ts +11 -0
- package/dist/fulfillment/delayReadModel.js +45 -0
- package/dist/fulfillment/deliveryAlignmentReadModel.d.ts +4 -0
- package/dist/fulfillment/deliveryAlignmentReadModel.js +12 -0
- package/dist/fulfillment/dispatchProjection.d.ts +7 -0
- package/dist/fulfillment/dispatchProjection.js +18 -0
- package/dist/fulfillment/fulfillmentObservationEffects.d.ts +66 -0
- package/dist/fulfillment/fulfillmentObservationEffects.js +36 -0
- package/dist/fulfillment/handlerSchemas.d.ts +647 -0
- package/dist/fulfillment/handlerSchemas.js +213 -0
- package/dist/fulfillment/handlerSupport.d.ts +33 -0
- package/dist/fulfillment/handlerSupport.js +25 -0
- package/dist/fulfillment/index.d.ts +37 -0
- package/dist/fulfillment/index.js +37 -0
- package/dist/fulfillment/labelLifecycle.d.ts +24 -0
- package/dist/fulfillment/labelLifecycle.js +9 -0
- package/dist/fulfillment/lowStockEvidenceHandler.d.ts +36 -0
- package/dist/fulfillment/lowStockEvidenceHandler.js +46 -0
- package/dist/fulfillment/migrations.d.ts +27 -0
- package/dist/fulfillment/migrations.js +8 -0
- package/dist/fulfillment/nativeUnsupported.d.ts +10 -0
- package/dist/fulfillment/nativeUnsupported.js +18 -0
- package/dist/fulfillment/normalizedFulfillmentFacts.d.ts +2 -0
- package/dist/fulfillment/normalizedFulfillmentFacts.js +1 -0
- package/dist/fulfillment/observationProjection.d.ts +11 -0
- package/dist/fulfillment/observationProjection.js +37 -0
- package/dist/fulfillment/omnipack/capabilities.d.ts +3 -0
- package/dist/fulfillment/omnipack/capabilities.js +16 -0
- package/dist/fulfillment/omnipack/client.d.ts +37 -0
- package/dist/fulfillment/omnipack/client.js +162 -0
- package/dist/fulfillment/omnipack/index.d.ts +52 -0
- package/dist/fulfillment/omnipack/index.js +349 -0
- package/dist/fulfillment/omnipack/mappers.d.ts +73 -0
- package/dist/fulfillment/omnipack/mappers.js +177 -0
- package/dist/fulfillment/omnipack/observation.d.ts +21 -0
- package/dist/fulfillment/omnipack/observation.js +57 -0
- package/dist/fulfillment/omnipack/omnipackDispatchPayload.d.ts +36 -0
- package/dist/fulfillment/omnipack/omnipackDispatchPayload.js +63 -0
- package/dist/fulfillment/omnipack/omnipackOutboundOrderPayload.d.ts +83 -0
- package/dist/fulfillment/omnipack/omnipackOutboundOrderPayload.js +113 -0
- package/dist/fulfillment/omnipack/omnipackProductPackPayload.d.ts +11 -0
- package/dist/fulfillment/omnipack/omnipackProductPackPayload.js +38 -0
- package/dist/fulfillment/omnipack/omnipackProductPayload.d.ts +18 -0
- package/dist/fulfillment/omnipack/omnipackProductPayload.js +51 -0
- package/dist/fulfillment/omnipack/omnipackReconciliationPayload.d.ts +1 -0
- package/dist/fulfillment/omnipack/omnipackReconciliationPayload.js +3 -0
- package/dist/fulfillment/omnipack/omnipackStatusVocabulary.d.ts +4 -0
- package/dist/fulfillment/omnipack/omnipackStatusVocabulary.js +87 -0
- package/dist/fulfillment/omnipack/omnipackWebhookPayload.d.ts +9 -0
- package/dist/fulfillment/omnipack/omnipackWebhookPayload.js +76 -0
- package/dist/fulfillment/omnipack/productSyncMappers.d.ts +18 -0
- package/dist/fulfillment/omnipack/productSyncMappers.js +43 -0
- package/dist/fulfillment/omnipack/providerError.d.ts +25 -0
- package/dist/fulfillment/omnipack/providerError.js +47 -0
- package/dist/fulfillment/omnipack/trackingReferences.d.ts +16 -0
- package/dist/fulfillment/omnipack/trackingReferences.js +52 -0
- package/dist/fulfillment/paidFulfillmentRecovery.d.ts +67 -0
- package/dist/fulfillment/paidFulfillmentRecovery.js +154 -0
- package/dist/fulfillment/paidHandoff.d.ts +27 -0
- package/dist/fulfillment/paidHandoff.js +86 -0
- package/dist/fulfillment/paidHandoffPorts.d.ts +24 -0
- package/dist/fulfillment/paidHandoffPorts.js +3 -0
- package/dist/fulfillment/postgresDeliveryAlignment.d.ts +4 -0
- package/dist/fulfillment/postgresDeliveryAlignment.js +12 -0
- package/dist/fulfillment/postgresDispatchStore.d.ts +16 -0
- package/dist/fulfillment/postgresDispatchStore.js +44 -0
- package/dist/fulfillment/postgresEvidenceReadPort.d.ts +25 -0
- package/dist/fulfillment/postgresEvidenceReadPort.js +45 -0
- package/dist/fulfillment/postgresPaidFulfillmentRecovery.d.ts +25 -0
- package/dist/fulfillment/postgresPaidFulfillmentRecovery.js +26 -0
- package/dist/fulfillment/postgresPaidHandoffStore.d.ts +4 -0
- package/dist/fulfillment/postgresPaidHandoffStore.js +10 -0
- package/dist/fulfillment/postgresShipmentSpine.d.ts +63 -0
- package/dist/fulfillment/postgresShipmentSpine.js +232 -0
- package/dist/fulfillment/postgresStockSyncPort.d.ts +8 -0
- package/dist/fulfillment/postgresStockSyncPort.js +63 -0
- package/dist/fulfillment/productReconciliationEvidenceHandler.d.ts +36 -0
- package/dist/fulfillment/productReconciliationEvidenceHandler.js +50 -0
- package/dist/fulfillment/providerCapabilities.d.ts +9 -0
- package/dist/fulfillment/providerCapabilities.js +17 -0
- package/dist/fulfillment/providerCommandLedger.d.ts +143 -0
- package/dist/fulfillment/providerCommandLedger.js +14 -0
- package/dist/fulfillment/providerDispatchAcceptance.d.ts +26 -0
- package/dist/fulfillment/providerDispatchAcceptance.js +23 -0
- package/dist/fulfillment/providerDispatchError.d.ts +6 -0
- package/dist/fulfillment/providerDispatchError.js +5 -0
- package/dist/fulfillment/providerDispatchWorker.d.ts +31 -0
- package/dist/fulfillment/providerDispatchWorker.js +172 -0
- package/dist/fulfillment/providerProductSyncWorker.d.ts +28 -0
- package/dist/fulfillment/providerProductSyncWorker.js +122 -0
- package/dist/fulfillment/providerReconciliationError.d.ts +9 -0
- package/dist/fulfillment/providerReconciliationError.js +17 -0
- package/dist/fulfillment/providerReconciliationTerminalSkip.d.ts +2 -0
- package/dist/fulfillment/providerReconciliationTerminalSkip.js +6 -0
- package/dist/fulfillment/providerReconciliationTimestamp.d.ts +1 -0
- package/dist/fulfillment/providerReconciliationTimestamp.js +27 -0
- package/dist/fulfillment/providerReconciliationWorker.d.ts +82 -0
- package/dist/fulfillment/providerReconciliationWorker.js +151 -0
- package/dist/fulfillment/providerStockAvailability.d.ts +13 -0
- package/dist/fulfillment/providerStockAvailability.js +51 -0
- package/dist/fulfillment/providerStockSyncWorker.d.ts +37 -0
- package/dist/fulfillment/providerStockSyncWorker.js +186 -0
- package/dist/fulfillment/providerTrackingReferences.d.ts +15 -0
- package/dist/fulfillment/providerTrackingReferences.js +23 -0
- package/dist/fulfillment/replacementRefusals.d.ts +5 -0
- package/dist/fulfillment/replacementRefusals.js +38 -0
- package/dist/fulfillment/replacementShipmentHandler.d.ts +5 -0
- package/dist/fulfillment/replacementShipmentHandler.js +46 -0
- package/dist/fulfillment/schemaCapabilities.d.ts +13 -0
- package/dist/fulfillment/schemaCapabilities.js +7 -0
- package/dist/fulfillment/shipmentsOverviewHandler.d.ts +16 -0
- package/dist/fulfillment/shipmentsOverviewHandler.js +54 -0
- package/dist/fulfillment/statusMap.d.ts +2 -0
- package/dist/fulfillment/statusMap.js +1 -0
- package/dist/fulfillment/trackingProgress.d.ts +1 -0
- package/dist/fulfillment/trackingProgress.js +1 -0
- package/dist/fulfillment/verifiedIngress.d.ts +52 -0
- package/dist/fulfillment/verifiedIngress.js +66 -0
- package/dist/payments/adapterConformance.d.ts +8 -0
- package/dist/payments/adapterConformance.js +109 -0
- package/dist/payments/bindings.d.ts +3 -0
- package/dist/payments/bindings.js +17 -0
- package/dist/payments/checkoutActivePaymentActionResolver.d.ts +7 -0
- package/dist/payments/checkoutActivePaymentActionResolver.js +77 -0
- package/dist/payments/checkoutRecoveryPaymentResolver.d.ts +8 -0
- package/dist/payments/checkoutRecoveryPaymentResolver.js +87 -0
- package/dist/payments/contracts/adapter.d.ts +113 -0
- package/dist/payments/contracts/adapter.js +1 -0
- package/dist/payments/contracts/conformance.d.ts +155 -0
- package/dist/payments/contracts/conformance.js +1 -0
- package/dist/payments/contracts/evidence.d.ts +157 -0
- package/dist/payments/contracts/evidence.js +1 -0
- package/dist/payments/contracts/index.d.ts +10 -0
- package/dist/payments/contracts/index.js +1 -0
- package/dist/payments/contracts/preparedAttempt.d.ts +69 -0
- package/dist/payments/contracts/preparedAttempt.js +1 -0
- package/dist/payments/contracts/reconciliation.d.ts +303 -0
- package/dist/payments/contracts/reconciliation.js +1 -0
- package/dist/payments/contracts/repair.d.ts +92 -0
- package/dist/payments/contracts/repair.js +1 -0
- package/dist/payments/contracts/truth.d.ts +92 -0
- package/dist/payments/contracts/truth.js +1 -0
- package/dist/payments/eventConformanceRunner.d.ts +12 -0
- package/dist/payments/eventConformanceRunner.js +125 -0
- package/dist/payments/index.d.ts +16 -0
- package/dist/payments/index.js +16 -0
- package/dist/payments/paymentAttemptSilenceWindow.d.ts +10 -0
- package/dist/payments/paymentAttemptSilenceWindow.js +19 -0
- package/dist/payments/paymentMethodLifecycle.d.ts +11 -0
- package/dist/payments/paymentMethodLifecycle.js +37 -0
- package/dist/payments/paymentProviderReconciliationAbsenceReopen.d.ts +7 -0
- package/dist/payments/paymentProviderReconciliationAbsenceReopen.js +100 -0
- package/dist/payments/paymentProviderReconciliationEvidence.d.ts +36 -0
- package/dist/payments/paymentProviderReconciliationEvidence.js +91 -0
- package/dist/payments/paymentProviderReconciliationWorker.d.ts +4 -0
- package/dist/payments/paymentProviderReconciliationWorker.js +245 -0
- package/dist/payments/paymentTruth.d.ts +8 -0
- package/dist/payments/paymentTruth.js +125 -0
- package/dist/payments/paymentVerifyNowHandler.d.ts +27 -0
- package/dist/payments/paymentVerifyNowHandler.js +70 -0
- package/dist/payments/paymentVerifyNowService.d.ts +23 -0
- package/dist/payments/paymentVerifyNowService.js +93 -0
- package/dist/payments/paymentWebhookPostProcessing.d.ts +22 -0
- package/dist/payments/paymentWebhookPostProcessing.js +15 -0
- package/dist/payments/postgresPaymentTruth.d.ts +10 -0
- package/dist/payments/postgresPaymentTruth.js +126 -0
- package/dist/payments/preparedAttempt.d.ts +6 -0
- package/dist/payments/preparedAttempt.js +70 -0
- package/dist/payments/preparedAttemptFinalization.d.ts +5 -0
- package/dist/payments/preparedAttemptFinalization.js +12 -0
- package/dist/payments/schemaCapabilities.d.ts +10 -0
- package/dist/payments/schemaCapabilities.js +10 -0
- package/dist/payments/sql.d.ts +20 -0
- package/dist/payments/sql.js +6 -0
- package/dist/payments/stripe/classifyDecline.d.ts +46 -0
- package/dist/payments/stripe/classifyDecline.js +58 -0
- package/dist/payments/stripe/declineFailureHints.d.ts +46 -0
- package/dist/payments/stripe/declineFailureHints.js +87 -0
- package/dist/payments/stripe/donorContracts.d.ts +120 -0
- package/dist/payments/stripe/donorContracts.js +1 -0
- package/dist/payments/stripe/executionDecline.d.ts +53 -0
- package/dist/payments/stripe/executionDecline.js +107 -0
- package/dist/payments/stripe/index.d.ts +41 -0
- package/dist/payments/stripe/index.js +264 -0
- package/dist/payments/stripe/intentMapper.d.ts +26 -0
- package/dist/payments/stripe/intentMapper.js +51 -0
- package/dist/payments/stripe/methodFacts.d.ts +10 -0
- package/dist/payments/stripe/methodFacts.js +12 -0
- package/dist/payments/stripe/paymentFailureEvidence.d.ts +26 -0
- package/dist/payments/stripe/paymentFailureEvidence.js +33 -0
- package/dist/payments/stripe/stripeFailureEvidence.d.ts +41 -0
- package/dist/payments/stripe/stripeFailureEvidence.js +82 -0
- package/dist/payments/stripe/stripeIntentConfirmationEvidence.d.ts +28 -0
- package/dist/payments/stripe/stripeIntentConfirmationEvidence.js +32 -0
- package/dist/payments/stripe/stripePaymentReconciliationProvider.d.ts +35 -0
- package/dist/payments/stripe/stripePaymentReconciliationProvider.js +222 -0
- package/dist/payments/stripe/stripeRefundEvidence.d.ts +8 -0
- package/dist/payments/stripe/stripeRefundEvidence.js +17 -0
- package/dist/payments/stripe/stripeSandboxPaymentExecutionAdapter.d.ts +45 -0
- package/dist/payments/stripe/stripeSandboxPaymentExecutionAdapter.js +220 -0
- package/dist/payments/stripe/stripeWebhookNormalizer.d.ts +14 -0
- package/dist/payments/stripe/stripeWebhookNormalizer.js +243 -0
- package/dist/payments/stripe/webhookSignature.d.ts +14 -0
- package/dist/payments/stripe/webhookSignature.js +43 -0
- package/dist/payments/tpay/blikRecurringMapper.d.ts +37 -0
- package/dist/payments/tpay/blikRecurringMapper.js +24 -0
- package/dist/payments/tpay/capabilities.d.ts +3 -0
- package/dist/payments/tpay/capabilities.js +18 -0
- package/dist/payments/tpay/classifyDecline.d.ts +46 -0
- package/dist/payments/tpay/classifyDecline.js +58 -0
- package/dist/payments/tpay/declineFailureHints.d.ts +72 -0
- package/dist/payments/tpay/declineFailureHints.js +37 -0
- package/dist/payments/tpay/donorContracts.d.ts +140 -0
- package/dist/payments/tpay/donorContracts.js +1 -0
- package/dist/payments/tpay/index.d.ts +73 -0
- package/dist/payments/tpay/index.js +294 -0
- package/dist/payments/tpay/paymentFailureEvidence.d.ts +26 -0
- package/dist/payments/tpay/paymentFailureEvidence.js +33 -0
- package/dist/payments/tpay/preparedProviderAttempt.d.ts +22 -0
- package/dist/payments/tpay/preparedProviderAttempt.js +24 -0
- package/dist/payments/tpay/providerAttemptFailureDiagnostic.d.ts +3 -0
- package/dist/payments/tpay/providerAttemptFailureDiagnostic.js +95 -0
- package/dist/payments/tpay/tpayCreateTransactionInput.d.ts +25 -0
- package/dist/payments/tpay/tpayCreateTransactionInput.js +128 -0
- package/dist/payments/tpay/tpayDeclineEvidence.d.ts +126 -0
- package/dist/payments/tpay/tpayDeclineEvidence.js +177 -0
- package/dist/payments/tpay/tpayDispatchFailure.d.ts +60 -0
- package/dist/payments/tpay/tpayDispatchFailure.js +121 -0
- package/dist/payments/tpay/tpayFailureEvidence.d.ts +20 -0
- package/dist/payments/tpay/tpayFailureEvidence.js +61 -0
- package/dist/payments/tpay/tpayHttpClient.d.ts +52 -0
- package/dist/payments/tpay/tpayHttpClient.js +185 -0
- package/dist/payments/tpay/tpayHttpFailure.d.ts +57 -0
- package/dist/payments/tpay/tpayHttpFailure.js +143 -0
- package/dist/payments/tpay/tpayPaymentExecutionAdapter.d.ts +20 -0
- package/dist/payments/tpay/tpayPaymentExecutionAdapter.js +249 -0
- package/dist/payments/tpay/tpayPaymentReconciliationProvider.d.ts +5 -0
- package/dist/payments/tpay/tpayPaymentReconciliationProvider.js +244 -0
- package/dist/payments/tpay/tpayTransactionPayload.d.ts +120 -0
- package/dist/payments/tpay/tpayTransactionPayload.js +96 -0
- package/dist/payments/tpay/wallClockTime.d.ts +31 -0
- package/dist/payments/tpay/wallClockTime.js +57 -0
- package/dist/payments/tpay/webhookJwsVerifier.d.ts +17 -0
- package/dist/payments/tpay/webhookJwsVerifier.js +51 -0
- package/dist/payments/tpay/webhookNormalizer.d.ts +54 -0
- package/dist/payments/tpay/webhookNormalizer.js +200 -0
- package/dist/payments/verifiedEvidenceProjection.d.ts +12 -0
- package/dist/payments/verifiedEvidenceProjection.js +166 -0
- package/dist/payments/verifiedIngress.d.ts +119 -0
- package/dist/payments/verifiedIngress.js +220 -0
- package/package.json +37 -2
- package/release-gates.json +105 -3
- package/sql/20260808210000_fulfillment_shipment_rail.sql +810 -0
- package/sql/20260810180000_payment_settlement_rail.sql +574 -0
- package/sql/20260815150000_payment_truth_reconciliation.sql +479 -0
- package/sql/20260819150000_fulfillment_shipment_sequence.sql +98 -0
- package/sql/20260826100000_late_payment_shifts_the_subscription_cycle.sql +215 -0
- package/sql/20261022080000_fulfillment_command_ledger.sql +786 -0
- package/sql/20261022090000_fulfillment_delivery_alignment.sql +1106 -0
- package/src/composition/composeBundle.ts +13 -0
- package/src/composition/fulfillmentContribution.ts +28 -0
- package/src/composition/index.ts +2 -0
- package/src/fulfillment/adapterConformance.ts +195 -0
- package/src/fulfillment/bindings.ts +26 -0
- package/src/fulfillment/commerceFulfillmentHandlers.ts +263 -0
- package/src/fulfillment/commerceFulfillmentReadModel.ts +257 -0
- package/src/fulfillment/contracts/alignment.ts +13 -0
- package/src/fulfillment/contracts/bindings.ts +30 -0
- package/src/fulfillment/contracts/capabilities.ts +15 -0
- package/src/fulfillment/contracts/conformance.ts +61 -0
- package/src/fulfillment/contracts/customerAlignment.ts +4 -0
- package/src/fulfillment/contracts/dispatch.ts +58 -0
- package/src/fulfillment/contracts/index.ts +18 -0
- package/src/fulfillment/contracts/label.ts +9 -0
- package/src/fulfillment/contracts/nativeUnsupported.ts +6 -0
- package/src/fulfillment/contracts/observation.ts +18 -0
- package/src/fulfillment/contracts/operator.ts +31 -0
- package/src/fulfillment/contracts/read.ts +42 -0
- package/src/fulfillment/contracts/readback.ts +11 -0
- package/src/fulfillment/contracts/replacement.ts +15 -0
- package/src/fulfillment/contracts/store.ts +26 -0
- package/src/fulfillment/contracts/sync.ts +61 -0
- package/src/fulfillment/currentFulfillmentParcel.ts +2 -0
- package/src/fulfillment/delayReadModel.ts +60 -0
- package/src/fulfillment/deliveryAlignmentReadModel.ts +15 -0
- package/src/fulfillment/dispatchProjection.ts +20 -0
- package/src/fulfillment/fulfillmentObservationEffects.ts +86 -0
- package/src/fulfillment/handlerSchemas.ts +255 -0
- package/src/fulfillment/handlerSupport.ts +25 -0
- package/src/fulfillment/index.ts +38 -0
- package/src/fulfillment/labelLifecycle.ts +11 -0
- package/src/fulfillment/lowStockEvidenceHandler.ts +60 -0
- package/src/fulfillment/migrations.ts +8 -0
- package/src/fulfillment/nativeUnsupported.ts +23 -0
- package/src/fulfillment/normalizedFulfillmentFacts.ts +2 -0
- package/src/fulfillment/observationProjection.ts +41 -0
- package/src/fulfillment/omnipack/capabilities.ts +18 -0
- package/src/fulfillment/omnipack/client.ts +258 -0
- package/src/fulfillment/omnipack/index.ts +312 -0
- package/src/fulfillment/omnipack/mappers.ts +261 -0
- package/src/fulfillment/omnipack/observation.ts +61 -0
- package/src/fulfillment/omnipack/omnipackDispatchPayload.ts +93 -0
- package/src/fulfillment/omnipack/omnipackOutboundOrderPayload.ts +194 -0
- package/src/fulfillment/omnipack/omnipackProductPackPayload.ts +47 -0
- package/src/fulfillment/omnipack/omnipackProductPayload.ts +67 -0
- package/src/fulfillment/omnipack/omnipackReconciliationPayload.ts +7 -0
- package/src/fulfillment/omnipack/omnipackStatusVocabulary.ts +103 -0
- package/src/fulfillment/omnipack/omnipackWebhookPayload.ts +73 -0
- package/src/fulfillment/omnipack/productSyncMappers.ts +74 -0
- package/src/fulfillment/omnipack/providerError.ts +64 -0
- package/src/fulfillment/omnipack/trackingReferences.ts +77 -0
- package/src/fulfillment/paidFulfillmentRecovery.ts +252 -0
- package/src/fulfillment/paidHandoff.ts +122 -0
- package/src/fulfillment/paidHandoffPorts.ts +21 -0
- package/src/fulfillment/postgresDeliveryAlignment.ts +14 -0
- package/src/fulfillment/postgresDispatchStore.ts +55 -0
- package/src/fulfillment/postgresEvidenceReadPort.ts +59 -0
- package/src/fulfillment/postgresPaidFulfillmentRecovery.ts +29 -0
- package/src/fulfillment/postgresPaidHandoffStore.ts +11 -0
- package/src/fulfillment/postgresShipmentSpine.ts +321 -0
- package/src/fulfillment/postgresStockSyncPort.ts +65 -0
- package/src/fulfillment/productReconciliationEvidenceHandler.ts +64 -0
- package/src/fulfillment/providerCapabilities.ts +18 -0
- package/src/fulfillment/providerCommandLedger.ts +53 -0
- package/src/fulfillment/providerDispatchAcceptance.ts +31 -0
- package/src/fulfillment/providerDispatchError.ts +7 -0
- package/src/fulfillment/providerDispatchWorker.ts +131 -0
- package/src/fulfillment/providerProductSyncWorker.ts +154 -0
- package/src/fulfillment/providerReconciliationError.ts +20 -0
- package/src/fulfillment/providerReconciliationTerminalSkip.ts +8 -0
- package/src/fulfillment/providerReconciliationTimestamp.ts +34 -0
- package/src/fulfillment/providerReconciliationWorker.ts +150 -0
- package/src/fulfillment/providerStockAvailability.ts +60 -0
- package/src/fulfillment/providerStockSyncWorker.ts +222 -0
- package/src/fulfillment/providerTrackingReferences.ts +31 -0
- package/src/fulfillment/replacementRefusals.ts +43 -0
- package/src/fulfillment/replacementShipmentHandler.ts +47 -0
- package/src/fulfillment/schemaCapabilities.ts +7 -0
- package/src/fulfillment/shipmentsOverviewHandler.ts +73 -0
- package/src/fulfillment/statusMap.ts +2 -0
- package/src/fulfillment/trackingProgress.ts +1 -0
- package/src/fulfillment/verifiedIngress.ts +80 -0
- package/src/payments/adapterConformance.ts +118 -0
- package/src/payments/bindings.ts +17 -0
- package/src/payments/checkoutActivePaymentActionResolver.ts +97 -0
- package/src/payments/checkoutRecoveryPaymentResolver.ts +108 -0
- package/src/payments/contracts/adapter.ts +96 -0
- package/src/payments/contracts/conformance.ts +171 -0
- package/src/payments/contracts/evidence.ts +142 -0
- package/src/payments/contracts/index.ts +54 -0
- package/src/payments/contracts/preparedAttempt.ts +86 -0
- package/src/payments/contracts/reconciliation.ts +296 -0
- package/src/payments/contracts/repair.ts +95 -0
- package/src/payments/contracts/truth.ts +108 -0
- package/src/payments/eventConformanceRunner.ts +149 -0
- package/src/payments/index.ts +21 -0
- package/src/payments/paymentAttemptSilenceWindow.ts +26 -0
- package/src/payments/paymentMethodLifecycle.ts +50 -0
- package/src/payments/paymentProviderReconciliationAbsenceReopen.ts +130 -0
- package/src/payments/paymentProviderReconciliationEvidence.ts +117 -0
- package/src/payments/paymentProviderReconciliationWorker.ts +291 -0
- package/src/payments/paymentTruth.ts +131 -0
- package/src/payments/paymentVerifyNowHandler.ts +94 -0
- package/src/payments/paymentVerifyNowService.ts +125 -0
- package/src/payments/paymentWebhookPostProcessing.ts +36 -0
- package/src/payments/postgresPaymentTruth.ts +149 -0
- package/src/payments/preparedAttempt.ts +90 -0
- package/src/payments/preparedAttemptFinalization.ts +13 -0
- package/src/payments/schemaCapabilities.ts +10 -0
- package/src/payments/sql.ts +6 -0
- package/src/payments/stripe/classifyDecline.ts +62 -0
- package/src/payments/stripe/declineFailureHints.ts +89 -0
- package/src/payments/stripe/donorContracts.ts +87 -0
- package/src/payments/stripe/executionDecline.ts +126 -0
- package/src/payments/stripe/index.ts +265 -0
- package/src/payments/stripe/intentMapper.ts +58 -0
- package/src/payments/stripe/methodFacts.ts +21 -0
- package/src/payments/stripe/paymentFailureEvidence.ts +58 -0
- package/src/payments/stripe/stripeFailureEvidence.ts +115 -0
- package/src/payments/stripe/stripeIntentConfirmationEvidence.ts +34 -0
- package/src/payments/stripe/stripePaymentReconciliationProvider.ts +263 -0
- package/src/payments/stripe/stripeRefundEvidence.ts +19 -0
- package/src/payments/stripe/stripeSandboxPaymentExecutionAdapter.ts +283 -0
- package/src/payments/stripe/stripeWebhookNormalizer.ts +277 -0
- package/src/payments/stripe/webhookSignature.ts +55 -0
- package/src/payments/tpay/blikRecurringMapper.ts +42 -0
- package/src/payments/tpay/capabilities.ts +21 -0
- package/src/payments/tpay/classifyDecline.ts +62 -0
- package/src/payments/tpay/declineFailureHints.ts +90 -0
- package/src/payments/tpay/donorContracts.ts +104 -0
- package/src/payments/tpay/index.ts +293 -0
- package/src/payments/tpay/paymentFailureEvidence.ts +58 -0
- package/src/payments/tpay/preparedProviderAttempt.ts +38 -0
- package/src/payments/tpay/providerAttemptFailureDiagnostic.ts +99 -0
- package/src/payments/tpay/tpayCreateTransactionInput.ts +158 -0
- package/src/payments/tpay/tpayDeclineEvidence.ts +233 -0
- package/src/payments/tpay/tpayDispatchFailure.ts +186 -0
- package/src/payments/tpay/tpayFailureEvidence.ts +75 -0
- package/src/payments/tpay/tpayHttpClient.ts +290 -0
- package/src/payments/tpay/tpayHttpFailure.ts +186 -0
- package/src/payments/tpay/tpayPaymentExecutionAdapter.ts +297 -0
- package/src/payments/tpay/tpayPaymentReconciliationProvider.ts +285 -0
- package/src/payments/tpay/tpayTransactionPayload.ts +207 -0
- package/src/payments/tpay/wallClockTime.ts +57 -0
- package/src/payments/tpay/webhookJwsVerifier.ts +82 -0
- package/src/payments/tpay/webhookNormalizer.ts +279 -0
- package/src/payments/verifiedEvidenceProjection.ts +179 -0
- package/src/payments/verifiedIngress.ts +317 -0
|
@@ -0,0 +1,1106 @@
|
|
|
1
|
+
-- D13 native translation of the effective managed delivery-alignment rail.
|
|
2
|
+
-- New invoker objects only; default off, deployment owner execution, no browser grants.
|
|
3
|
+
-- Existing history, payment lifecycle and managed graph remain unchanged.
|
|
4
|
+
-- Donor activation cycle 1 ↔ native activation ordinal 0;
|
|
5
|
+
-- donor renewal n+1 ↔ native renewal n (renewal operation numbers remain >= 1).
|
|
6
|
+
-- Activation requires no renewal operation and its active activation-order link.
|
|
7
|
+
-- Other unnumbered cycles remain unordered: paid ones refuse due admission for
|
|
8
|
+
-- manual review; any other unordered cycle prevents automatic arrival alignment.
|
|
9
|
+
-- D13 native translation note: only NEW cycle admission/INSERT is guarded.
|
|
10
|
+
-- Existing-cycle renewal/dunning retries bypass admission and one-use receipts.
|
|
11
|
+
-- confirm_replacement remains a note-only operator attestation. Never invoke it
|
|
12
|
+
-- for a replacement that did not arrive. No automatic caller is installed.
|
|
13
|
+
-- Roll back compatible code or turn new cases off; repair accepted effects forward.
|
|
14
|
+
|
|
15
|
+
CREATE TABLE public.subscription_delivery_alignment_control (
|
|
16
|
+
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
|
17
|
+
mode text NOT NULL DEFAULT 'off'
|
|
18
|
+
CHECK (mode IN ('off', 'shadow', 'protect', 'auto_align')),
|
|
19
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
INSERT INTO public.subscription_delivery_alignment_control (singleton, mode)
|
|
23
|
+
VALUES (true, 'off')
|
|
24
|
+
ON CONFLICT (singleton) DO NOTHING;
|
|
25
|
+
|
|
26
|
+
COMMENT ON TABLE public.subscription_delivery_alignment_control IS
|
|
27
|
+
'Singleton DB-side rollout control for new delivery-alignment cases. Default off. Existing protected cases retain their captured authority after this row changes.';
|
|
28
|
+
|
|
29
|
+
CREATE TABLE public.subscription_delivery_alignment_cases (
|
|
30
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
31
|
+
shipment_id uuid REFERENCES public.fulfillment_shipments(id) ON DELETE CASCADE,
|
|
32
|
+
predecessor_order_id uuid NOT NULL REFERENCES public.commerce_orders(id) ON DELETE CASCADE,
|
|
33
|
+
subscription_id uuid NOT NULL REFERENCES public.subscriptions(id) ON DELETE CASCADE,
|
|
34
|
+
predecessor_cycle_id uuid NOT NULL REFERENCES public.subscription_cycles(id) ON DELETE CASCADE,
|
|
35
|
+
predecessor_cycle_number integer CHECK (predecessor_cycle_number >= 0),
|
|
36
|
+
captured_mode text NOT NULL CHECK (captured_mode IN ('shadow', 'protect', 'auto_align')),
|
|
37
|
+
state text NOT NULL CHECK (state IN ('shadow', 'protected', 'aligned', 'released', 'manual_review')),
|
|
38
|
+
evidence_kind text NOT NULL CHECK (evidence_kind IN ('fulfillment_exception', 'overdue_renewal', 'missing_fulfillment')),
|
|
39
|
+
observed_next_cycle_at timestamptz,
|
|
40
|
+
delivered_at timestamptz,
|
|
41
|
+
aligned_next_cycle_at timestamptz,
|
|
42
|
+
manual_review_reason text,
|
|
43
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
44
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
45
|
+
aligned_at timestamptz,
|
|
46
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
47
|
+
CHECK (
|
|
48
|
+
(state = 'manual_review' AND manual_review_reason IS NOT NULL)
|
|
49
|
+
OR state <> 'manual_review'
|
|
50
|
+
),
|
|
51
|
+
CHECK (
|
|
52
|
+
(state = 'aligned' AND aligned_next_cycle_at IS NOT NULL AND aligned_at IS NOT NULL)
|
|
53
|
+
OR state <> 'aligned'
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
-- An allowed renewal needs a durable, one-time proof that the same schedule
|
|
58
|
+
-- passed admission in an enforcing mode. A rejected INSERT cannot retain a new
|
|
59
|
+
-- protected case (the transaction rolls it back), so this receipt is the safe
|
|
60
|
+
-- DB boundary for callers that otherwise bypass the service preflight.
|
|
61
|
+
CREATE TABLE public.subscription_delivery_alignment_admission_receipts (
|
|
62
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
63
|
+
subscription_id uuid NOT NULL REFERENCES public.subscriptions(id) ON DELETE CASCADE,
|
|
64
|
+
scheduled_at timestamptz NOT NULL,
|
|
65
|
+
observed_next_cycle_at timestamptz NOT NULL,
|
|
66
|
+
decision text NOT NULL DEFAULT 'allowed' CHECK (decision = 'allowed'),
|
|
67
|
+
source text NOT NULL CHECK (source = 'renewal_admission'),
|
|
68
|
+
admitted_at timestamptz NOT NULL DEFAULT now(),
|
|
69
|
+
consumed_at timestamptz,
|
|
70
|
+
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
71
|
+
UNIQUE (subscription_id, scheduled_at),
|
|
72
|
+
CHECK (consumed_at IS NULL OR consumed_at >= admitted_at)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
-- The ordinary shape is exactly one case per fulfillment order. The nullable
|
|
78
|
+
-- fallback is intentionally only for the fail-closed "paid but no fulfillment
|
|
79
|
+
-- row exists" corruption/manual-review case; it is unique per predecessor order
|
|
80
|
+
-- and can never stand in for two physical fulfillment records.
|
|
81
|
+
CREATE UNIQUE INDEX subscription_delivery_alignment_case_fulfillment_key
|
|
82
|
+
ON public.subscription_delivery_alignment_cases (shipment_id)
|
|
83
|
+
WHERE shipment_id IS NOT NULL;
|
|
84
|
+
CREATE UNIQUE INDEX subscription_delivery_alignment_case_missing_fulfillment_key
|
|
85
|
+
ON public.subscription_delivery_alignment_cases (predecessor_order_id)
|
|
86
|
+
WHERE shipment_id IS NULL;
|
|
87
|
+
CREATE INDEX idx_subscription_delivery_alignment_cases_blocking
|
|
88
|
+
ON public.subscription_delivery_alignment_cases (subscription_id, state, predecessor_cycle_number)
|
|
89
|
+
WHERE state IN ('protected', 'manual_review');
|
|
90
|
+
CREATE INDEX idx_subscription_delivery_alignment_cases_order
|
|
91
|
+
ON public.subscription_delivery_alignment_cases (predecessor_order_id, shipment_id);
|
|
92
|
+
|
|
93
|
+
COMMENT ON TABLE public.subscription_delivery_alignment_cases IS
|
|
94
|
+
'Durable delivery-alignment ledger. Customer mapping exposes only protected and aligned. shadow, released and manual_review remain operator/server facts.';
|
|
95
|
+
COMMENT ON COLUMN public.subscription_delivery_alignment_cases.delivered_at IS
|
|
96
|
+
'Trusted durable fulfillment chronology copied only from fulfillment_shipments.delivered_at; status text or outbox occurredAt never qualifies.';
|
|
97
|
+
COMMENT ON TABLE public.subscription_delivery_alignment_admission_receipts IS
|
|
98
|
+
'One-time, schedule-bound authorization consumed by a protecting-mode payment_pending cycle insert. Created only by a successful due renewal admission.';
|
|
99
|
+
|
|
100
|
+
CREATE VIEW public.subscription_delivery_alignment_cycles AS
|
|
101
|
+
SELECT cycle.*,
|
|
102
|
+
CASE
|
|
103
|
+
WHEN operation.cycle_number >= 1 THEN operation.cycle_number
|
|
104
|
+
WHEN cycle.renewal_operation_id IS NULL AND EXISTS (
|
|
105
|
+
SELECT 1 FROM public.subscription_activations activation
|
|
106
|
+
WHERE activation.state = 'active'
|
|
107
|
+
AND activation.subscription_id = cycle.subscription_id
|
|
108
|
+
AND activation.order_id = order_row.id
|
|
109
|
+
) THEN 0
|
|
110
|
+
ELSE NULL
|
|
111
|
+
END AS cycle_number,
|
|
112
|
+
order_row.id AS order_id
|
|
113
|
+
FROM public.subscription_cycles cycle
|
|
114
|
+
LEFT JOIN public.subscription_renewal_operations operation ON operation.id = cycle.renewal_operation_id
|
|
115
|
+
LEFT JOIN public.commerce_orders order_row ON order_row.subscription_cycle_id = cycle.id;
|
|
116
|
+
CREATE FUNCTION public.subscription_delivery_alignment_get_mode()
|
|
117
|
+
RETURNS text
|
|
118
|
+
LANGUAGE sql
|
|
119
|
+
SECURITY INVOKER
|
|
120
|
+
SET search_path = pg_catalog
|
|
121
|
+
AS $$
|
|
122
|
+
SELECT mode
|
|
123
|
+
FROM public.subscription_delivery_alignment_control
|
|
124
|
+
WHERE singleton = true
|
|
125
|
+
$$;
|
|
126
|
+
|
|
127
|
+
-- Enforcing modes (protect, auto_align) require the host renewal path to call
|
|
128
|
+
-- subscription_delivery_alignment_admit_renewal before inserting a cycle.
|
|
129
|
+
-- The native renewal store does not yet call it: switching to an enforcing mode
|
|
130
|
+
-- halts every autonomous renewal with subscription_delivery_alignment_admission_required.
|
|
131
|
+
-- Native installs must keep mode off until that wiring lands. Before switching,
|
|
132
|
+
-- check for paid cycles without an activation or renewal link: enforcing modes
|
|
133
|
+
-- hold every due renewal on such subscriptions for manual review.
|
|
134
|
+
CREATE FUNCTION public.subscription_delivery_alignment_set_mode(
|
|
135
|
+
p_mode text
|
|
136
|
+
)
|
|
137
|
+
RETURNS jsonb
|
|
138
|
+
LANGUAGE plpgsql
|
|
139
|
+
SECURITY INVOKER
|
|
140
|
+
SET search_path = pg_catalog
|
|
141
|
+
AS $$
|
|
142
|
+
BEGIN
|
|
143
|
+
IF p_mode IS NULL OR p_mode NOT IN ('off', 'shadow', 'protect', 'auto_align') THEN
|
|
144
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_invalid_mode' USING ERRCODE = '22023';
|
|
145
|
+
END IF;
|
|
146
|
+
|
|
147
|
+
UPDATE public.subscription_delivery_alignment_control
|
|
148
|
+
SET mode = p_mode,
|
|
149
|
+
updated_at = now()
|
|
150
|
+
WHERE singleton = true;
|
|
151
|
+
|
|
152
|
+
RETURN jsonb_build_object('mode', p_mode, 'updatedAt', now());
|
|
153
|
+
END;
|
|
154
|
+
$$;
|
|
155
|
+
|
|
156
|
+
CREATE FUNCTION public.subscription_delivery_alignment_record_allowed_receipt(
|
|
157
|
+
p_subscription_id uuid,
|
|
158
|
+
p_scheduled_at timestamptz,
|
|
159
|
+
p_observed_next_cycle_at timestamptz,
|
|
160
|
+
p_source text
|
|
161
|
+
)
|
|
162
|
+
RETURNS void
|
|
163
|
+
LANGUAGE plpgsql
|
|
164
|
+
SECURITY INVOKER
|
|
165
|
+
SET search_path = pg_catalog
|
|
166
|
+
AS $$
|
|
167
|
+
BEGIN
|
|
168
|
+
IF p_subscription_id IS NULL
|
|
169
|
+
OR p_scheduled_at IS NULL
|
|
170
|
+
OR p_observed_next_cycle_at IS NULL
|
|
171
|
+
OR p_source <> 'renewal_admission'
|
|
172
|
+
THEN
|
|
173
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_invalid_receipt' USING ERRCODE = '22023';
|
|
174
|
+
END IF;
|
|
175
|
+
|
|
176
|
+
-- A subscription can hold at most one future admission token. The caller
|
|
177
|
+
-- already owns the subscription lock, so clearing an obsolete unconsumed
|
|
178
|
+
-- schedule cannot race a valid creator and bounds replay/stale buildup.
|
|
179
|
+
DELETE FROM public.subscription_delivery_alignment_admission_receipts
|
|
180
|
+
WHERE subscription_id = p_subscription_id
|
|
181
|
+
AND consumed_at IS NULL
|
|
182
|
+
AND scheduled_at <> p_scheduled_at;
|
|
183
|
+
|
|
184
|
+
INSERT INTO public.subscription_delivery_alignment_admission_receipts (
|
|
185
|
+
subscription_id, scheduled_at, observed_next_cycle_at, source, metadata
|
|
186
|
+
) VALUES (
|
|
187
|
+
p_subscription_id, p_scheduled_at, p_observed_next_cycle_at, p_source,
|
|
188
|
+
jsonb_build_object('source', 'subscription_delivery_alignment')
|
|
189
|
+
)
|
|
190
|
+
ON CONFLICT (subscription_id, scheduled_at) DO UPDATE
|
|
191
|
+
SET observed_next_cycle_at = EXCLUDED.observed_next_cycle_at,
|
|
192
|
+
source = EXCLUDED.source,
|
|
193
|
+
admitted_at = now(),
|
|
194
|
+
metadata = EXCLUDED.metadata
|
|
195
|
+
WHERE public.subscription_delivery_alignment_admission_receipts.consumed_at IS NULL;
|
|
196
|
+
END;
|
|
197
|
+
$$;
|
|
198
|
+
|
|
199
|
+
CREATE FUNCTION public.subscription_delivery_alignment_upsert_case(
|
|
200
|
+
p_shipment_id uuid,
|
|
201
|
+
p_predecessor_order_id uuid,
|
|
202
|
+
p_subscription_id uuid,
|
|
203
|
+
p_predecessor_cycle_id uuid,
|
|
204
|
+
p_predecessor_cycle_number integer,
|
|
205
|
+
p_mode text,
|
|
206
|
+
p_evidence_kind text,
|
|
207
|
+
p_observed_next_cycle_at timestamptz,
|
|
208
|
+
p_manual_review_reason text DEFAULT NULL
|
|
209
|
+
)
|
|
210
|
+
RETURNS void
|
|
211
|
+
LANGUAGE plpgsql
|
|
212
|
+
SECURITY INVOKER
|
|
213
|
+
SET search_path = pg_catalog
|
|
214
|
+
AS $$
|
|
215
|
+
DECLARE
|
|
216
|
+
v_state text;
|
|
217
|
+
BEGIN
|
|
218
|
+
IF p_mode NOT IN ('shadow', 'protect', 'auto_align') THEN
|
|
219
|
+
RETURN;
|
|
220
|
+
END IF;
|
|
221
|
+
|
|
222
|
+
IF p_mode = 'shadow' THEN
|
|
223
|
+
v_state := 'shadow';
|
|
224
|
+
ELSE
|
|
225
|
+
-- An unordered non-activation cycle cannot authorize automatic alignment.
|
|
226
|
+
IF p_predecessor_cycle_number IS NULL THEN
|
|
227
|
+
p_manual_review_reason := 'missing_renewal_correlation';
|
|
228
|
+
END IF;
|
|
229
|
+
v_state := CASE
|
|
230
|
+
WHEN p_manual_review_reason IS NOT NULL THEN 'manual_review'
|
|
231
|
+
ELSE 'protected'
|
|
232
|
+
END;
|
|
233
|
+
END IF;
|
|
234
|
+
|
|
235
|
+
IF p_shipment_id IS NULL THEN
|
|
236
|
+
INSERT INTO public.subscription_delivery_alignment_cases (
|
|
237
|
+
shipment_id, predecessor_order_id, subscription_id,
|
|
238
|
+
predecessor_cycle_id, predecessor_cycle_number, captured_mode, state,
|
|
239
|
+
evidence_kind, observed_next_cycle_at, manual_review_reason, metadata
|
|
240
|
+
) VALUES (
|
|
241
|
+
NULL, p_predecessor_order_id, p_subscription_id,
|
|
242
|
+
p_predecessor_cycle_id, p_predecessor_cycle_number, p_mode, v_state,
|
|
243
|
+
p_evidence_kind, p_observed_next_cycle_at,
|
|
244
|
+
p_manual_review_reason,
|
|
245
|
+
jsonb_build_object('source', 'subscription_delivery_alignment')
|
|
246
|
+
)
|
|
247
|
+
ON CONFLICT (predecessor_order_id) WHERE shipment_id IS NULL DO UPDATE
|
|
248
|
+
SET updated_at = now(),
|
|
249
|
+
observed_next_cycle_at = EXCLUDED.observed_next_cycle_at,
|
|
250
|
+
evidence_kind = EXCLUDED.evidence_kind,
|
|
251
|
+
state = CASE
|
|
252
|
+
WHEN public.subscription_delivery_alignment_cases.state IN ('aligned', 'released')
|
|
253
|
+
THEN public.subscription_delivery_alignment_cases.state
|
|
254
|
+
ELSE EXCLUDED.state
|
|
255
|
+
END,
|
|
256
|
+
captured_mode = CASE
|
|
257
|
+
WHEN public.subscription_delivery_alignment_cases.state IN ('aligned', 'released')
|
|
258
|
+
THEN public.subscription_delivery_alignment_cases.captured_mode
|
|
259
|
+
ELSE EXCLUDED.captured_mode
|
|
260
|
+
END,
|
|
261
|
+
manual_review_reason = COALESCE(
|
|
262
|
+
public.subscription_delivery_alignment_cases.manual_review_reason,
|
|
263
|
+
EXCLUDED.manual_review_reason
|
|
264
|
+
);
|
|
265
|
+
RETURN;
|
|
266
|
+
END IF;
|
|
267
|
+
|
|
268
|
+
INSERT INTO public.subscription_delivery_alignment_cases (
|
|
269
|
+
shipment_id, predecessor_order_id, subscription_id,
|
|
270
|
+
predecessor_cycle_id, predecessor_cycle_number, captured_mode, state,
|
|
271
|
+
evidence_kind, observed_next_cycle_at, manual_review_reason, metadata
|
|
272
|
+
) VALUES (
|
|
273
|
+
p_shipment_id, p_predecessor_order_id, p_subscription_id,
|
|
274
|
+
p_predecessor_cycle_id, p_predecessor_cycle_number, p_mode, v_state,
|
|
275
|
+
p_evidence_kind, p_observed_next_cycle_at, p_manual_review_reason,
|
|
276
|
+
jsonb_build_object('source', 'subscription_delivery_alignment')
|
|
277
|
+
)
|
|
278
|
+
ON CONFLICT (shipment_id) WHERE shipment_id IS NOT NULL DO UPDATE
|
|
279
|
+
SET updated_at = now(),
|
|
280
|
+
observed_next_cycle_at = EXCLUDED.observed_next_cycle_at,
|
|
281
|
+
evidence_kind = EXCLUDED.evidence_kind,
|
|
282
|
+
state = CASE
|
|
283
|
+
WHEN public.subscription_delivery_alignment_cases.state IN ('aligned', 'released')
|
|
284
|
+
THEN public.subscription_delivery_alignment_cases.state
|
|
285
|
+
WHEN EXCLUDED.state = 'manual_review' THEN 'manual_review'
|
|
286
|
+
WHEN EXCLUDED.state = 'protected' THEN 'protected'
|
|
287
|
+
ELSE public.subscription_delivery_alignment_cases.state
|
|
288
|
+
END,
|
|
289
|
+
captured_mode = CASE
|
|
290
|
+
WHEN public.subscription_delivery_alignment_cases.state IN ('aligned', 'released')
|
|
291
|
+
THEN public.subscription_delivery_alignment_cases.captured_mode
|
|
292
|
+
WHEN EXCLUDED.state IN ('protected', 'manual_review') THEN EXCLUDED.captured_mode
|
|
293
|
+
ELSE public.subscription_delivery_alignment_cases.captured_mode
|
|
294
|
+
END,
|
|
295
|
+
manual_review_reason = COALESCE(
|
|
296
|
+
public.subscription_delivery_alignment_cases.manual_review_reason,
|
|
297
|
+
EXCLUDED.manual_review_reason
|
|
298
|
+
);
|
|
299
|
+
END;
|
|
300
|
+
$$;
|
|
301
|
+
|
|
302
|
+
-- Observe an existing active exception hold; never create a hold or an event.
|
|
303
|
+
-- Local cancellation remains a local state change. An undelivered, unsuperseded
|
|
304
|
+
-- cancelled parcel still counts as outstanding when renewal admission is enabled.
|
|
305
|
+
CREATE FUNCTION public.subscription_delivery_alignment_open_exception_case()
|
|
306
|
+
RETURNS trigger
|
|
307
|
+
LANGUAGE plpgsql
|
|
308
|
+
SECURITY INVOKER
|
|
309
|
+
SET search_path = pg_catalog
|
|
310
|
+
AS $$
|
|
311
|
+
DECLARE
|
|
312
|
+
v_mode text;
|
|
313
|
+
v_order public.commerce_orders%ROWTYPE;
|
|
314
|
+
v_cycle public.subscription_delivery_alignment_cycles%ROWTYPE;
|
|
315
|
+
v_subscription public.subscriptions%ROWTYPE;
|
|
316
|
+
v_fulfillment public.fulfillment_shipments%ROWTYPE;
|
|
317
|
+
v_found boolean := false;
|
|
318
|
+
BEGIN
|
|
319
|
+
IF NEW.reason <> 'fulfillment_exception' OR NEW.status <> 'active' THEN
|
|
320
|
+
RETURN NEW;
|
|
321
|
+
END IF;
|
|
322
|
+
IF TG_OP = 'UPDATE' AND OLD.reason = 'fulfillment_exception' AND OLD.status = 'active' THEN
|
|
323
|
+
RETURN NEW;
|
|
324
|
+
END IF;
|
|
325
|
+
|
|
326
|
+
SELECT mode INTO v_mode FROM public.subscription_delivery_alignment_control WHERE singleton = true;
|
|
327
|
+
IF COALESCE(v_mode, 'off') = 'off' THEN
|
|
328
|
+
RETURN NEW;
|
|
329
|
+
END IF;
|
|
330
|
+
|
|
331
|
+
SELECT * INTO v_order FROM public.commerce_orders WHERE id = NEW.order_id;
|
|
332
|
+
IF NOT FOUND OR v_order.subscription_cycle_id IS NULL THEN
|
|
333
|
+
RETURN NEW;
|
|
334
|
+
END IF;
|
|
335
|
+
SELECT * INTO v_cycle FROM public.subscription_delivery_alignment_cycles WHERE id = v_order.subscription_cycle_id;
|
|
336
|
+
SELECT * INTO v_subscription FROM public.subscriptions WHERE id = v_cycle.subscription_id FOR UPDATE;
|
|
337
|
+
IF NOT FOUND OR v_subscription.status <> 'active' OR v_cycle.status IS DISTINCT FROM 'paid' THEN
|
|
338
|
+
RETURN NEW;
|
|
339
|
+
END IF;
|
|
340
|
+
|
|
341
|
+
FOR v_fulfillment IN
|
|
342
|
+
SELECT * FROM public.fulfillment_shipments
|
|
343
|
+
WHERE order_id = v_order.id
|
|
344
|
+
ORDER BY created_at ASC, id ASC
|
|
345
|
+
LOOP
|
|
346
|
+
v_found := true;
|
|
347
|
+
PERFORM public.subscription_delivery_alignment_upsert_case(
|
|
348
|
+
v_fulfillment.id, v_order.id, v_subscription.id, v_cycle.id,
|
|
349
|
+
v_cycle.cycle_number, v_mode, 'fulfillment_exception', v_subscription.next_cycle_at
|
|
350
|
+
);
|
|
351
|
+
END LOOP;
|
|
352
|
+
IF NOT v_found THEN
|
|
353
|
+
PERFORM public.subscription_delivery_alignment_upsert_case(
|
|
354
|
+
NULL, v_order.id, v_subscription.id, v_cycle.id,
|
|
355
|
+
v_cycle.cycle_number, v_mode, 'missing_fulfillment',
|
|
356
|
+
v_subscription.next_cycle_at,
|
|
357
|
+
CASE WHEN v_mode = 'shadow' THEN NULL ELSE 'fulfillment_exception_without_fulfillment' END
|
|
358
|
+
);
|
|
359
|
+
END IF;
|
|
360
|
+
RETURN NEW;
|
|
361
|
+
END;
|
|
362
|
+
$$;
|
|
363
|
+
|
|
364
|
+
CREATE FUNCTION public.subscription_delivery_alignment_on_delivery()
|
|
365
|
+
RETURNS trigger
|
|
366
|
+
LANGUAGE plpgsql
|
|
367
|
+
SECURITY INVOKER
|
|
368
|
+
SET search_path = pg_catalog
|
|
369
|
+
AS $$
|
|
370
|
+
DECLARE
|
|
371
|
+
v_case public.subscription_delivery_alignment_cases%ROWTYPE;
|
|
372
|
+
v_inherited_case_id uuid;
|
|
373
|
+
v_order public.commerce_orders%ROWTYPE;
|
|
374
|
+
v_subscription public.subscriptions%ROWTYPE;
|
|
375
|
+
v_open_dunning boolean;
|
|
376
|
+
v_artifact_reason text;
|
|
377
|
+
v_latest_delivered_at timestamptz;
|
|
378
|
+
v_target timestamptz;
|
|
379
|
+
BEGIN
|
|
380
|
+
IF NEW.delivered_at IS NULL
|
|
381
|
+
OR (TG_OP = 'UPDATE' AND OLD.delivered_at IS NOT NULL)
|
|
382
|
+
THEN
|
|
383
|
+
RETURN NEW;
|
|
384
|
+
END IF;
|
|
385
|
+
|
|
386
|
+
-- Derive the aggregate from the fulfillment's order, then lock it before
|
|
387
|
+
-- examining its ledger. This fence is required even when there is no case
|
|
388
|
+
-- yet: otherwise delivery could return on an empty lookup while concurrent
|
|
389
|
+
-- admission creates protection from the not-yet-committed delivery.
|
|
390
|
+
SELECT * INTO v_order
|
|
391
|
+
FROM public.commerce_orders
|
|
392
|
+
WHERE id = NEW.order_id
|
|
393
|
+
AND subscription_cycle_id IS NOT NULL;
|
|
394
|
+
IF NOT FOUND THEN
|
|
395
|
+
RETURN NEW;
|
|
396
|
+
END IF;
|
|
397
|
+
|
|
398
|
+
SELECT * INTO v_subscription
|
|
399
|
+
FROM public.subscriptions
|
|
400
|
+
WHERE id = (SELECT subscription_id FROM public.subscription_cycles WHERE id = v_order.subscription_cycle_id)
|
|
401
|
+
FOR UPDATE;
|
|
402
|
+
IF NOT FOUND THEN
|
|
403
|
+
RETURN NEW;
|
|
404
|
+
END IF;
|
|
405
|
+
|
|
406
|
+
-- Admission or exception evidence may have changed the case while this
|
|
407
|
+
-- transaction waited for the aggregate, so take the case lock only now.
|
|
408
|
+
SELECT * INTO v_case
|
|
409
|
+
FROM public.subscription_delivery_alignment_cases
|
|
410
|
+
WHERE shipment_id = NEW.id
|
|
411
|
+
FOR UPDATE;
|
|
412
|
+
|
|
413
|
+
-- A replacement parcel is created after the case was already open, so it never
|
|
414
|
+
-- carries a case of its own. It is the arrival the customer is actually
|
|
415
|
+
-- waiting for, so it settles the case its predecessor opened on the same
|
|
416
|
+
-- order. An open case is preferred because that is the one holding the
|
|
417
|
+
-- renewal. Ordinary parcels never enter this branch: they replace nothing.
|
|
418
|
+
IF NOT FOUND AND NEW.replaces_shipment_id IS NOT NULL THEN
|
|
419
|
+
SELECT id INTO v_inherited_case_id
|
|
420
|
+
FROM public.subscription_delivery_alignment_cases
|
|
421
|
+
WHERE predecessor_order_id = NEW.order_id
|
|
422
|
+
ORDER BY (state IN ('protected', 'manual_review')) DESC, created_at ASC, id ASC
|
|
423
|
+
LIMIT 1;
|
|
424
|
+
IF v_inherited_case_id IS NOT NULL THEN
|
|
425
|
+
SELECT * INTO v_case
|
|
426
|
+
FROM public.subscription_delivery_alignment_cases
|
|
427
|
+
WHERE id = v_inherited_case_id
|
|
428
|
+
FOR UPDATE;
|
|
429
|
+
END IF;
|
|
430
|
+
END IF;
|
|
431
|
+
IF v_case.id IS NULL THEN
|
|
432
|
+
RETURN NEW;
|
|
433
|
+
END IF;
|
|
434
|
+
|
|
435
|
+
UPDATE public.subscription_delivery_alignment_cases
|
|
436
|
+
SET delivered_at = LEAST(COALESCE(delivered_at, NEW.delivered_at), NEW.delivered_at),
|
|
437
|
+
updated_at = now()
|
|
438
|
+
WHERE id = v_case.id
|
|
439
|
+
RETURNING * INTO v_case;
|
|
440
|
+
|
|
441
|
+
-- Outstanding means the customer is still waiting for it. A parcel that
|
|
442
|
+
-- another row supersedes through replaces_shipment_id has been
|
|
443
|
+
-- answered by that replacement, and when it was lost its delivered_at stays
|
|
444
|
+
-- null for ever - counting it would hold this case open for ever with it.
|
|
445
|
+
IF EXISTS (
|
|
446
|
+
SELECT 1 FROM public.fulfillment_shipments outstanding
|
|
447
|
+
WHERE outstanding.order_id = v_case.predecessor_order_id
|
|
448
|
+
AND outstanding.delivered_at IS NULL
|
|
449
|
+
AND NOT EXISTS (
|
|
450
|
+
SELECT 1 FROM public.fulfillment_shipments successor
|
|
451
|
+
WHERE successor.replaces_shipment_id = outstanding.id
|
|
452
|
+
)
|
|
453
|
+
) THEN
|
|
454
|
+
RETURN NEW;
|
|
455
|
+
END IF;
|
|
456
|
+
|
|
457
|
+
IF v_subscription.status <> 'active' THEN
|
|
458
|
+
UPDATE public.subscription_delivery_alignment_cases
|
|
459
|
+
SET state = CASE WHEN state = 'protected' THEN 'released' ELSE state END,
|
|
460
|
+
updated_at = now()
|
|
461
|
+
WHERE predecessor_order_id = v_case.predecessor_order_id;
|
|
462
|
+
RETURN NEW;
|
|
463
|
+
END IF;
|
|
464
|
+
|
|
465
|
+
SELECT EXISTS (
|
|
466
|
+
SELECT 1 FROM public.subscription_dunning_cases
|
|
467
|
+
WHERE subscription_id = v_subscription.id
|
|
468
|
+
AND status IN ('open', 'expired')
|
|
469
|
+
) INTO v_open_dunning;
|
|
470
|
+
IF v_open_dunning THEN
|
|
471
|
+
UPDATE public.subscription_delivery_alignment_cases
|
|
472
|
+
SET state = CASE WHEN state = 'protected' THEN 'manual_review' ELSE state END,
|
|
473
|
+
manual_review_reason = CASE WHEN state = 'protected' THEN 'open_dunning' ELSE manual_review_reason END,
|
|
474
|
+
updated_at = now()
|
|
475
|
+
WHERE predecessor_order_id = v_case.predecessor_order_id;
|
|
476
|
+
RETURN NEW;
|
|
477
|
+
END IF;
|
|
478
|
+
|
|
479
|
+
SELECT CASE
|
|
480
|
+
WHEN EXISTS (
|
|
481
|
+
SELECT 1 FROM public.subscription_delivery_alignment_cycles successor
|
|
482
|
+
JOIN public.subscription_renewal_operations operation ON operation.id = successor.renewal_operation_id
|
|
483
|
+
WHERE successor.subscription_id = v_subscription.id
|
|
484
|
+
AND (successor.cycle_number > v_case.predecessor_cycle_number OR (successor.cycle_number IS NULL AND successor.id <> v_case.predecessor_cycle_id))
|
|
485
|
+
AND operation.attempt_prepared_at IS NOT NULL
|
|
486
|
+
) THEN 'existing_provider_attempt'
|
|
487
|
+
WHEN EXISTS (
|
|
488
|
+
SELECT 1 FROM public.subscription_delivery_alignment_cycles successor
|
|
489
|
+
WHERE successor.subscription_id = v_subscription.id
|
|
490
|
+
AND (successor.cycle_number > v_case.predecessor_cycle_number
|
|
491
|
+
OR (successor.cycle_number IS NULL AND successor.id <> v_case.predecessor_cycle_id))
|
|
492
|
+
) THEN 'renewal_cycle_already_created'
|
|
493
|
+
ELSE NULL
|
|
494
|
+
END INTO v_artifact_reason;
|
|
495
|
+
|
|
496
|
+
IF v_artifact_reason IS NOT NULL THEN
|
|
497
|
+
UPDATE public.subscription_delivery_alignment_cases
|
|
498
|
+
SET state = CASE WHEN state = 'protected' THEN 'manual_review' ELSE state END,
|
|
499
|
+
manual_review_reason = CASE WHEN state = 'protected' THEN v_artifact_reason ELSE manual_review_reason END,
|
|
500
|
+
updated_at = now()
|
|
501
|
+
WHERE predecessor_order_id = v_case.predecessor_order_id;
|
|
502
|
+
RETURN NEW;
|
|
503
|
+
END IF;
|
|
504
|
+
|
|
505
|
+
IF v_case.state <> 'protected' THEN
|
|
506
|
+
RETURN NEW;
|
|
507
|
+
END IF;
|
|
508
|
+
IF v_case.captured_mode = 'protect' THEN
|
|
509
|
+
UPDATE public.subscription_delivery_alignment_cases
|
|
510
|
+
SET state = CASE WHEN state = 'protected' THEN 'released' ELSE state END,
|
|
511
|
+
updated_at = now()
|
|
512
|
+
WHERE predecessor_order_id = v_case.predecessor_order_id;
|
|
513
|
+
RETURN NEW;
|
|
514
|
+
END IF;
|
|
515
|
+
IF v_case.captured_mode <> 'auto_align' THEN
|
|
516
|
+
RETURN NEW;
|
|
517
|
+
END IF;
|
|
518
|
+
|
|
519
|
+
-- With a split shipment, delivery events can arrive in either chronology.
|
|
520
|
+
-- Preserve the interval from the last parcel delivered, not necessarily from
|
|
521
|
+
-- the final webhook processed by this transaction.
|
|
522
|
+
SELECT max(delivered_at) INTO v_latest_delivered_at
|
|
523
|
+
FROM public.fulfillment_shipments
|
|
524
|
+
WHERE order_id = v_case.predecessor_order_id;
|
|
525
|
+
v_target := v_latest_delivered_at + make_interval(days => v_subscription.cadence_days);
|
|
526
|
+
UPDATE public.subscriptions
|
|
527
|
+
SET next_cycle_at = CASE
|
|
528
|
+
WHEN next_cycle_at IS NULL THEN v_target
|
|
529
|
+
ELSE GREATEST(next_cycle_at, v_target)
|
|
530
|
+
END,
|
|
531
|
+
updated_at = now()
|
|
532
|
+
WHERE id = v_subscription.id
|
|
533
|
+
RETURNING next_cycle_at INTO v_target;
|
|
534
|
+
|
|
535
|
+
UPDATE public.subscription_delivery_alignment_cases
|
|
536
|
+
SET state = CASE WHEN state = 'protected' THEN 'aligned' ELSE state END,
|
|
537
|
+
aligned_next_cycle_at = CASE WHEN state = 'protected' THEN v_target ELSE aligned_next_cycle_at END,
|
|
538
|
+
aligned_at = CASE WHEN state = 'protected' THEN now() ELSE aligned_at END,
|
|
539
|
+
updated_at = now()
|
|
540
|
+
WHERE predecessor_order_id = v_case.predecessor_order_id;
|
|
541
|
+
RETURN NEW;
|
|
542
|
+
END;
|
|
543
|
+
$$;
|
|
544
|
+
|
|
545
|
+
CREATE FUNCTION public.subscription_delivery_alignment_admit_renewal(
|
|
546
|
+
p_subscription_id uuid,
|
|
547
|
+
p_scheduled_at timestamptz,
|
|
548
|
+
p_as_of timestamptz DEFAULT now()
|
|
549
|
+
)
|
|
550
|
+
RETURNS jsonb
|
|
551
|
+
LANGUAGE plpgsql
|
|
552
|
+
SECURITY INVOKER
|
|
553
|
+
SET search_path = pg_catalog
|
|
554
|
+
AS $$
|
|
555
|
+
DECLARE
|
|
556
|
+
v_subscription public.subscriptions%ROWTYPE;
|
|
557
|
+
v_predecessor public.subscription_delivery_alignment_cycles%ROWTYPE;
|
|
558
|
+
v_order public.commerce_orders%ROWTYPE;
|
|
559
|
+
v_mode text;
|
|
560
|
+
v_fulfillment public.fulfillment_shipments%ROWTYPE;
|
|
561
|
+
v_has_fulfillment boolean := false;
|
|
562
|
+
v_has_undelivered boolean := false;
|
|
563
|
+
v_has_paid_predecessor boolean := false;
|
|
564
|
+
v_existing_state text;
|
|
565
|
+
v_ledger_state text;
|
|
566
|
+
BEGIN
|
|
567
|
+
IF p_subscription_id IS NULL OR p_scheduled_at IS NULL OR p_as_of IS NULL THEN
|
|
568
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_invalid_admission' USING ERRCODE = '22023';
|
|
569
|
+
END IF;
|
|
570
|
+
|
|
571
|
+
SELECT * INTO v_subscription
|
|
572
|
+
FROM public.subscriptions
|
|
573
|
+
WHERE id = p_subscription_id
|
|
574
|
+
FOR UPDATE;
|
|
575
|
+
IF NOT FOUND THEN
|
|
576
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_subscription_not_found' USING ERRCODE = '22023';
|
|
577
|
+
END IF;
|
|
578
|
+
|
|
579
|
+
IF v_subscription.status <> 'active' THEN
|
|
580
|
+
RETURN jsonb_build_object('allowed', false, 'state', 'not_active', 'reason', 'subscription_not_active');
|
|
581
|
+
END IF;
|
|
582
|
+
|
|
583
|
+
-- D13 native translation note: this entry admits NEW cycles only.
|
|
584
|
+
-- Existing renewal/dunning retries bypass admission and receipts structurally.
|
|
585
|
+
SELECT * INTO v_predecessor
|
|
586
|
+
FROM public.subscription_delivery_alignment_cycles
|
|
587
|
+
WHERE subscription_id = p_subscription_id
|
|
588
|
+
AND status = 'paid'
|
|
589
|
+
AND cycle_number >= 0
|
|
590
|
+
ORDER BY cycle_number DESC NULLS LAST, scheduled_at DESC
|
|
591
|
+
LIMIT 1;
|
|
592
|
+
v_has_paid_predecessor := FOUND;
|
|
593
|
+
|
|
594
|
+
-- A later paid cycle is authoritative proof that an older collision no
|
|
595
|
+
-- longer controls the next renewal. Release it atomically under the same
|
|
596
|
+
-- subscription lock so a completed retry cannot strand future normal cycles.
|
|
597
|
+
IF v_has_paid_predecessor THEN
|
|
598
|
+
UPDATE public.subscription_delivery_alignment_cases
|
|
599
|
+
SET state = 'released',
|
|
600
|
+
updated_at = now()
|
|
601
|
+
WHERE subscription_id = p_subscription_id
|
|
602
|
+
AND state IN ('protected', 'manual_review')
|
|
603
|
+
AND predecessor_cycle_number < v_predecessor.cycle_number;
|
|
604
|
+
END IF;
|
|
605
|
+
|
|
606
|
+
SELECT state INTO v_existing_state
|
|
607
|
+
FROM public.subscription_delivery_alignment_cases
|
|
608
|
+
WHERE subscription_id = p_subscription_id
|
|
609
|
+
AND state IN ('protected', 'manual_review')
|
|
610
|
+
AND (NOT v_has_paid_predecessor OR predecessor_cycle_number >= v_predecessor.cycle_number OR predecessor_cycle_number IS NULL)
|
|
611
|
+
ORDER BY predecessor_cycle_number DESC, created_at DESC, id DESC
|
|
612
|
+
LIMIT 1;
|
|
613
|
+
IF v_existing_state IS NOT NULL THEN
|
|
614
|
+
RETURN jsonb_build_object(
|
|
615
|
+
'allowed', false,
|
|
616
|
+
'state', v_existing_state,
|
|
617
|
+
'reason', CASE WHEN v_existing_state = 'protected'
|
|
618
|
+
THEN 'delivery_alignment_protected' ELSE 'delivery_alignment_manual_review' END
|
|
619
|
+
);
|
|
620
|
+
END IF;
|
|
621
|
+
|
|
622
|
+
SELECT mode INTO v_mode
|
|
623
|
+
FROM public.subscription_delivery_alignment_control
|
|
624
|
+
WHERE singleton = true;
|
|
625
|
+
IF COALESCE(v_mode, 'off') = 'off' THEN
|
|
626
|
+
RETURN jsonb_build_object('allowed', true, 'state', 'off', 'reason', 'mode_off');
|
|
627
|
+
END IF;
|
|
628
|
+
-- Shadow is observation-only. A stale caller is useful telemetry but it must
|
|
629
|
+
-- never turn this rollout mode into an implicit admission gate or rewrite a
|
|
630
|
+
-- case using a schedule we no longer own.
|
|
631
|
+
IF COALESCE(v_mode, 'off') = 'shadow'
|
|
632
|
+
AND v_subscription.next_cycle_at IS DISTINCT FROM p_scheduled_at THEN
|
|
633
|
+
RETURN jsonb_build_object('allowed', true, 'state', 'shadow', 'reason', 'stale_shadow');
|
|
634
|
+
END IF;
|
|
635
|
+
IF v_subscription.next_cycle_at IS DISTINCT FROM p_scheduled_at THEN
|
|
636
|
+
RETURN jsonb_build_object('allowed', false, 'state', 'stale', 'reason', 'stale_next_cycle_at');
|
|
637
|
+
END IF;
|
|
638
|
+
IF p_scheduled_at > p_as_of THEN
|
|
639
|
+
RETURN jsonb_build_object('allowed', true, 'state', 'none', 'reason', 'not_due');
|
|
640
|
+
END IF;
|
|
641
|
+
|
|
642
|
+
-- Unknown paid history cannot outrank a known predecessor, but ignoring it
|
|
643
|
+
-- could admit a charge while an uncorrelated parcel is still outstanding.
|
|
644
|
+
-- Refuse without a receipt; shadow remains observation-only. Do not invent
|
|
645
|
+
-- an ordinal from scheduled_at or silently classify it as activation.
|
|
646
|
+
IF EXISTS (
|
|
647
|
+
SELECT 1 FROM public.subscription_delivery_alignment_cycles
|
|
648
|
+
WHERE subscription_id = p_subscription_id
|
|
649
|
+
AND status = 'paid' AND cycle_number IS NULL
|
|
650
|
+
) THEN
|
|
651
|
+
RETURN jsonb_build_object(
|
|
652
|
+
'allowed', v_mode = 'shadow',
|
|
653
|
+
'state', CASE WHEN v_mode = 'shadow' THEN 'shadow' ELSE 'manual_review' END,
|
|
654
|
+
'reason', 'missing_renewal_correlation'
|
|
655
|
+
);
|
|
656
|
+
END IF;
|
|
657
|
+
|
|
658
|
+
IF NOT v_has_paid_predecessor OR v_predecessor.order_id IS NULL THEN
|
|
659
|
+
PERFORM public.subscription_delivery_alignment_record_allowed_receipt(
|
|
660
|
+
p_subscription_id, p_scheduled_at, v_subscription.next_cycle_at, 'renewal_admission'
|
|
661
|
+
);
|
|
662
|
+
RETURN jsonb_build_object('allowed', true, 'state', 'none', 'reason', 'no_paid_predecessor');
|
|
663
|
+
END IF;
|
|
664
|
+
|
|
665
|
+
SELECT * INTO v_order
|
|
666
|
+
FROM public.commerce_orders
|
|
667
|
+
WHERE id = v_predecessor.order_id;
|
|
668
|
+
IF NOT FOUND OR v_order.subscription_cycle_id IS DISTINCT FROM v_predecessor.id THEN
|
|
669
|
+
PERFORM public.subscription_delivery_alignment_record_allowed_receipt(
|
|
670
|
+
p_subscription_id, p_scheduled_at, v_subscription.next_cycle_at, 'renewal_admission'
|
|
671
|
+
);
|
|
672
|
+
RETURN jsonb_build_object('allowed', true, 'state', 'none', 'reason', 'predecessor_not_subscription_cycle');
|
|
673
|
+
END IF;
|
|
674
|
+
|
|
675
|
+
FOR v_fulfillment IN
|
|
676
|
+
SELECT *
|
|
677
|
+
FROM public.fulfillment_shipments
|
|
678
|
+
WHERE order_id = v_order.id
|
|
679
|
+
ORDER BY created_at ASC, id ASC
|
|
680
|
+
LOOP
|
|
681
|
+
v_has_fulfillment := true;
|
|
682
|
+
-- Superseded parcels do not count. Without this the lost original would
|
|
683
|
+
-- keep this admission answering `protected` long after its replacement
|
|
684
|
+
-- reached the customer, and the subscription would never renew again.
|
|
685
|
+
IF v_fulfillment.delivered_at IS NULL
|
|
686
|
+
AND NOT EXISTS (
|
|
687
|
+
SELECT 1 FROM public.fulfillment_shipments successor
|
|
688
|
+
WHERE successor.replaces_shipment_id = v_fulfillment.id
|
|
689
|
+
)
|
|
690
|
+
THEN
|
|
691
|
+
v_has_undelivered := true;
|
|
692
|
+
END IF;
|
|
693
|
+
END LOOP;
|
|
694
|
+
|
|
695
|
+
-- Do not open a delivery case merely because a normal parcel exists. It is
|
|
696
|
+
-- delay evidence only once the autonomous renewal is actually overdue.
|
|
697
|
+
IF NOT v_has_fulfillment THEN
|
|
698
|
+
PERFORM public.subscription_delivery_alignment_upsert_case(
|
|
699
|
+
NULL, v_order.id, p_subscription_id, v_predecessor.id,
|
|
700
|
+
v_predecessor.cycle_number, v_mode, 'missing_fulfillment',
|
|
701
|
+
v_subscription.next_cycle_at,
|
|
702
|
+
CASE WHEN v_mode = 'shadow' THEN NULL ELSE 'missing_fulfillment' END
|
|
703
|
+
);
|
|
704
|
+
IF v_mode = 'shadow' THEN
|
|
705
|
+
RETURN jsonb_build_object('allowed', true, 'state', 'shadow', 'reason', 'delivery_alignment_shadow');
|
|
706
|
+
END IF;
|
|
707
|
+
RETURN jsonb_build_object('allowed', false, 'state', 'manual_review', 'reason', 'delivery_alignment_manual_review');
|
|
708
|
+
END IF;
|
|
709
|
+
IF NOT v_has_undelivered THEN
|
|
710
|
+
PERFORM public.subscription_delivery_alignment_record_allowed_receipt(
|
|
711
|
+
p_subscription_id, p_scheduled_at, v_subscription.next_cycle_at, 'renewal_admission'
|
|
712
|
+
);
|
|
713
|
+
RETURN jsonb_build_object('allowed', true, 'state', 'none', 'reason', 'predecessor_delivered');
|
|
714
|
+
END IF;
|
|
715
|
+
|
|
716
|
+
FOR v_fulfillment IN
|
|
717
|
+
SELECT *
|
|
718
|
+
FROM public.fulfillment_shipments
|
|
719
|
+
WHERE order_id = v_order.id
|
|
720
|
+
ORDER BY created_at ASC, id ASC
|
|
721
|
+
LOOP
|
|
722
|
+
PERFORM public.subscription_delivery_alignment_upsert_case(
|
|
723
|
+
v_fulfillment.id, v_order.id, p_subscription_id, v_predecessor.id,
|
|
724
|
+
v_predecessor.cycle_number, v_mode, 'overdue_renewal',
|
|
725
|
+
v_subscription.next_cycle_at
|
|
726
|
+
);
|
|
727
|
+
END LOOP;
|
|
728
|
+
|
|
729
|
+
IF v_mode = 'shadow' THEN
|
|
730
|
+
RETURN jsonb_build_object('allowed', true, 'state', 'shadow', 'reason', 'delivery_alignment_shadow');
|
|
731
|
+
END IF;
|
|
732
|
+
|
|
733
|
+
-- The ledger owns this answer. The upsert above preserves a state an operator
|
|
734
|
+
-- or the delivery path already settled, so reading the row is the only way for
|
|
735
|
+
-- the response to agree with it. A blocking state still refuses; a settled one
|
|
736
|
+
-- is admitted and takes the same one-time receipt the other admitted paths
|
|
737
|
+
-- take, because the cycle-insert backstop would otherwise reject a renewal
|
|
738
|
+
-- this call has just declared allowed. A missing row cannot happen here (the
|
|
739
|
+
-- loop above wrote one for every fulfillment) and is treated as blocking.
|
|
740
|
+
SELECT state INTO v_ledger_state
|
|
741
|
+
FROM public.subscription_delivery_alignment_cases
|
|
742
|
+
WHERE subscription_id = p_subscription_id
|
|
743
|
+
AND predecessor_order_id = v_order.id
|
|
744
|
+
ORDER BY (state IN ('protected', 'manual_review')) DESC, updated_at DESC, id DESC
|
|
745
|
+
LIMIT 1;
|
|
746
|
+
v_ledger_state := COALESCE(v_ledger_state, 'protected');
|
|
747
|
+
IF v_ledger_state IN ('protected', 'manual_review') THEN
|
|
748
|
+
RETURN jsonb_build_object(
|
|
749
|
+
'allowed', false,
|
|
750
|
+
'state', v_ledger_state,
|
|
751
|
+
'reason', CASE WHEN v_ledger_state = 'protected'
|
|
752
|
+
THEN 'delivery_alignment_protected' ELSE 'delivery_alignment_manual_review' END
|
|
753
|
+
);
|
|
754
|
+
END IF;
|
|
755
|
+
PERFORM public.subscription_delivery_alignment_record_allowed_receipt(
|
|
756
|
+
p_subscription_id, p_scheduled_at, v_subscription.next_cycle_at, 'renewal_admission'
|
|
757
|
+
);
|
|
758
|
+
RETURN jsonb_build_object(
|
|
759
|
+
'allowed', true,
|
|
760
|
+
'state', v_ledger_state,
|
|
761
|
+
'reason', 'delivery_alignment_case_settled'
|
|
762
|
+
);
|
|
763
|
+
END;
|
|
764
|
+
$$;
|
|
765
|
+
|
|
766
|
+
CREATE FUNCTION public.subscription_delivery_alignment_resolve_case(
|
|
767
|
+
p_case_id uuid,
|
|
768
|
+
p_resolution text,
|
|
769
|
+
p_note text
|
|
770
|
+
)
|
|
771
|
+
RETURNS jsonb
|
|
772
|
+
LANGUAGE plpgsql
|
|
773
|
+
SECURITY INVOKER
|
|
774
|
+
SET search_path = pg_catalog
|
|
775
|
+
AS $$
|
|
776
|
+
DECLARE
|
|
777
|
+
v_case public.subscription_delivery_alignment_cases%ROWTYPE;
|
|
778
|
+
v_subscription_id uuid;
|
|
779
|
+
v_subscription_status text;
|
|
780
|
+
BEGIN
|
|
781
|
+
IF p_case_id IS NULL
|
|
782
|
+
OR p_resolution NOT IN ('operator_confirmed', 'retry_succeeded', 'false_positive')
|
|
783
|
+
OR NULLIF(btrim(p_note), '') IS NULL
|
|
784
|
+
THEN
|
|
785
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_invalid_resolution' USING ERRCODE = '22023';
|
|
786
|
+
END IF;
|
|
787
|
+
|
|
788
|
+
-- Discover identity without locking the ledger first; all mutable alignment
|
|
789
|
+
-- paths own subscription -> ledger case to avoid delivery/admission deadlock.
|
|
790
|
+
SELECT subscription_id INTO v_subscription_id
|
|
791
|
+
FROM public.subscription_delivery_alignment_cases
|
|
792
|
+
WHERE id = p_case_id;
|
|
793
|
+
IF NOT FOUND THEN
|
|
794
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_case_not_found' USING ERRCODE = '22023';
|
|
795
|
+
END IF;
|
|
796
|
+
|
|
797
|
+
SELECT status INTO v_subscription_status
|
|
798
|
+
FROM public.subscriptions
|
|
799
|
+
WHERE id = v_subscription_id
|
|
800
|
+
FOR UPDATE;
|
|
801
|
+
IF NOT FOUND THEN
|
|
802
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_subscription_not_found' USING ERRCODE = '22023';
|
|
803
|
+
END IF;
|
|
804
|
+
|
|
805
|
+
SELECT * INTO v_case
|
|
806
|
+
FROM public.subscription_delivery_alignment_cases
|
|
807
|
+
WHERE id = p_case_id
|
|
808
|
+
FOR UPDATE;
|
|
809
|
+
IF v_case.state NOT IN ('protected', 'manual_review') THEN
|
|
810
|
+
RETURN jsonb_build_object('caseId', v_case.id, 'state', v_case.state, 'resolved', false, 'reason', 'not_open');
|
|
811
|
+
END IF;
|
|
812
|
+
|
|
813
|
+
-- Release is now real permission for the next autonomous charge, so it may not
|
|
814
|
+
-- be spent on a shipment that has not arrived. Releasing here would take money
|
|
815
|
+
-- for goods the customer never received and, because every open-case
|
|
816
|
+
-- projection keys on the open states, would take the case off the operator
|
|
817
|
+
-- queue, the overdue counter and the customer banner at the same time. The
|
|
818
|
+
-- honest exits are the durable delivery, a confirmed replacement, or ending
|
|
819
|
+
-- the subscription first. A case with no fulfillment row at all is untouched:
|
|
820
|
+
-- it has no delivery fact to wait for, and operator judgement is the only tool
|
|
821
|
+
-- that corruption case has.
|
|
822
|
+
IF v_subscription_status = 'active'
|
|
823
|
+
AND EXISTS (
|
|
824
|
+
SELECT 1
|
|
825
|
+
FROM public.fulfillment_shipments outstanding
|
|
826
|
+
WHERE outstanding.order_id = v_case.predecessor_order_id
|
|
827
|
+
AND outstanding.delivered_at IS NULL
|
|
828
|
+
AND NOT EXISTS (
|
|
829
|
+
SELECT 1 FROM public.fulfillment_shipments successor
|
|
830
|
+
WHERE successor.replaces_shipment_id = outstanding.id
|
|
831
|
+
)
|
|
832
|
+
)
|
|
833
|
+
THEN
|
|
834
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_release_would_strand' USING ERRCODE = '22023';
|
|
835
|
+
END IF;
|
|
836
|
+
|
|
837
|
+
UPDATE public.subscription_delivery_alignment_cases
|
|
838
|
+
SET state = 'released',
|
|
839
|
+
updated_at = now(),
|
|
840
|
+
metadata = metadata || jsonb_build_object(
|
|
841
|
+
'manualResolution',
|
|
842
|
+
jsonb_build_object('code', p_resolution, 'note', p_note, 'resolvedAt', now())
|
|
843
|
+
)
|
|
844
|
+
WHERE id = v_case.id
|
|
845
|
+
RETURNING * INTO v_case;
|
|
846
|
+
|
|
847
|
+
RETURN jsonb_build_object('caseId', v_case.id, 'state', v_case.state, 'resolved', true, 'resolution', p_resolution);
|
|
848
|
+
END;
|
|
849
|
+
$$;
|
|
850
|
+
|
|
851
|
+
CREATE FUNCTION public.subscription_delivery_alignment_confirm_replacement(
|
|
852
|
+
p_case_id uuid,
|
|
853
|
+
p_note text
|
|
854
|
+
)
|
|
855
|
+
RETURNS jsonb
|
|
856
|
+
LANGUAGE plpgsql
|
|
857
|
+
SECURITY INVOKER
|
|
858
|
+
SET search_path = pg_catalog
|
|
859
|
+
AS $$
|
|
860
|
+
DECLARE
|
|
861
|
+
v_case public.subscription_delivery_alignment_cases%ROWTYPE;
|
|
862
|
+
v_subscription public.subscriptions%ROWTYPE;
|
|
863
|
+
v_subscription_id uuid;
|
|
864
|
+
v_target timestamptz;
|
|
865
|
+
BEGIN
|
|
866
|
+
IF p_case_id IS NULL OR NULLIF(btrim(p_note), '') IS NULL THEN
|
|
867
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_invalid_replacement' USING ERRCODE = '22023';
|
|
868
|
+
END IF;
|
|
869
|
+
|
|
870
|
+
-- Identity first, then the aggregate lock, then the case lock: the same order
|
|
871
|
+
-- admission, delivery and resolution use, so this cannot deadlock against them.
|
|
872
|
+
SELECT subscription_id INTO v_subscription_id
|
|
873
|
+
FROM public.subscription_delivery_alignment_cases
|
|
874
|
+
WHERE id = p_case_id;
|
|
875
|
+
IF NOT FOUND THEN
|
|
876
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_case_not_found' USING ERRCODE = '22023';
|
|
877
|
+
END IF;
|
|
878
|
+
|
|
879
|
+
SELECT * INTO v_subscription
|
|
880
|
+
FROM public.subscriptions
|
|
881
|
+
WHERE id = v_subscription_id
|
|
882
|
+
FOR UPDATE;
|
|
883
|
+
IF NOT FOUND THEN
|
|
884
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_subscription_not_found' USING ERRCODE = '22023';
|
|
885
|
+
END IF;
|
|
886
|
+
|
|
887
|
+
SELECT * INTO v_case
|
|
888
|
+
FROM public.subscription_delivery_alignment_cases
|
|
889
|
+
WHERE id = p_case_id
|
|
890
|
+
FOR UPDATE;
|
|
891
|
+
|
|
892
|
+
-- A case this call already aligned answers with its stored schedule instead of
|
|
893
|
+
-- shifting again. Without this the second call would add another cadence.
|
|
894
|
+
IF v_case.state = 'aligned' THEN
|
|
895
|
+
RETURN jsonb_build_object(
|
|
896
|
+
'caseId', v_case.id,
|
|
897
|
+
'state', v_case.state,
|
|
898
|
+
'confirmed', true,
|
|
899
|
+
'nextCycleAt', v_case.aligned_next_cycle_at
|
|
900
|
+
);
|
|
901
|
+
END IF;
|
|
902
|
+
IF v_case.state NOT IN ('protected', 'manual_review') THEN
|
|
903
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_case_not_open' USING ERRCODE = '22023';
|
|
904
|
+
END IF;
|
|
905
|
+
|
|
906
|
+
-- A non-active subscription has no autonomous renewal to align. Confirming a
|
|
907
|
+
-- replacement there would move a schedule nobody is going to read, and would
|
|
908
|
+
-- hide the case from the operator queue on the strength of a schedule shift
|
|
909
|
+
-- that changes nothing.
|
|
910
|
+
IF v_subscription.status <> 'active' THEN
|
|
911
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_replacement_requires_active_subscription' USING ERRCODE = '22023';
|
|
912
|
+
END IF;
|
|
913
|
+
|
|
914
|
+
-- The confirmation instant plus the subscription's own cadence, and never
|
|
915
|
+
-- earlier than the schedule already promised. `GREATEST` is what makes this
|
|
916
|
+
-- monotonic: the same one-way rule the delivery path applies.
|
|
917
|
+
v_target := now() + make_interval(days => v_subscription.cadence_days);
|
|
918
|
+
UPDATE public.subscriptions
|
|
919
|
+
SET next_cycle_at = CASE
|
|
920
|
+
WHEN next_cycle_at IS NULL THEN v_target
|
|
921
|
+
ELSE GREATEST(next_cycle_at, v_target)
|
|
922
|
+
END,
|
|
923
|
+
updated_at = now()
|
|
924
|
+
WHERE id = v_subscription.id
|
|
925
|
+
RETURNING next_cycle_at INTO v_target;
|
|
926
|
+
|
|
927
|
+
-- Settle every open case of the same predecessor order, not only the targeted
|
|
928
|
+
-- row. One replacement answers for the whole undelivered order, and a sibling
|
|
929
|
+
-- left open would keep the cycle-insert backstop rejecting the renewal this
|
|
930
|
+
-- confirmation just scheduled. Matches how the delivery path settles siblings.
|
|
931
|
+
UPDATE public.subscription_delivery_alignment_cases
|
|
932
|
+
SET state = 'aligned',
|
|
933
|
+
aligned_next_cycle_at = v_target,
|
|
934
|
+
aligned_at = now(),
|
|
935
|
+
updated_at = now(),
|
|
936
|
+
metadata = metadata || jsonb_build_object(
|
|
937
|
+
'replacementConfirmation',
|
|
938
|
+
jsonb_build_object('note', p_note, 'confirmedAt', now(), 'nextCycleAt', v_target)
|
|
939
|
+
)
|
|
940
|
+
WHERE predecessor_order_id = v_case.predecessor_order_id
|
|
941
|
+
AND state IN ('protected', 'manual_review');
|
|
942
|
+
|
|
943
|
+
SELECT * INTO v_case
|
|
944
|
+
FROM public.subscription_delivery_alignment_cases
|
|
945
|
+
WHERE id = v_case.id;
|
|
946
|
+
|
|
947
|
+
RETURN jsonb_build_object(
|
|
948
|
+
'caseId', v_case.id,
|
|
949
|
+
'state', v_case.state,
|
|
950
|
+
'confirmed', true,
|
|
951
|
+
'nextCycleAt', v_case.aligned_next_cycle_at
|
|
952
|
+
);
|
|
953
|
+
END;
|
|
954
|
+
$$;
|
|
955
|
+
|
|
956
|
+
CREATE FUNCTION public.subscription_delivery_alignment_guard_cycle_insert()
|
|
957
|
+
RETURNS trigger
|
|
958
|
+
LANGUAGE plpgsql
|
|
959
|
+
SECURITY INVOKER
|
|
960
|
+
SET search_path = pg_catalog
|
|
961
|
+
AS $$
|
|
962
|
+
DECLARE
|
|
963
|
+
v_subscription public.subscriptions%ROWTYPE;
|
|
964
|
+
v_mode text;
|
|
965
|
+
v_receipt_id uuid;
|
|
966
|
+
BEGIN
|
|
967
|
+
IF NEW.status <> 'payment_pending' THEN
|
|
968
|
+
RETURN NEW;
|
|
969
|
+
END IF;
|
|
970
|
+
|
|
971
|
+
SELECT * INTO v_subscription
|
|
972
|
+
FROM public.subscriptions
|
|
973
|
+
WHERE id = NEW.subscription_id
|
|
974
|
+
FOR UPDATE;
|
|
975
|
+
IF NOT FOUND OR v_subscription.status <> 'active' THEN
|
|
976
|
+
RETURN NEW;
|
|
977
|
+
END IF;
|
|
978
|
+
|
|
979
|
+
-- Expired-dunning recovery owns its fresh cycle through a separate tokenized
|
|
980
|
+
-- workflow. It retains its own authority rather than becoming alignment dunning.
|
|
981
|
+
IF EXISTS (
|
|
982
|
+
SELECT 1 FROM public.subscription_dunning_cases
|
|
983
|
+
WHERE subscription_id = NEW.subscription_id
|
|
984
|
+
AND status = 'expired'
|
|
985
|
+
) THEN
|
|
986
|
+
RETURN NEW;
|
|
987
|
+
END IF;
|
|
988
|
+
|
|
989
|
+
IF EXISTS (
|
|
990
|
+
SELECT 1 FROM public.subscription_delivery_alignment_cases
|
|
991
|
+
WHERE subscription_id = NEW.subscription_id
|
|
992
|
+
AND state IN ('protected', 'manual_review')
|
|
993
|
+
) THEN
|
|
994
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_protected' USING ERRCODE = 'P0001';
|
|
995
|
+
END IF;
|
|
996
|
+
|
|
997
|
+
SELECT mode INTO v_mode
|
|
998
|
+
FROM public.subscription_delivery_alignment_control
|
|
999
|
+
WHERE singleton = true;
|
|
1000
|
+
|
|
1001
|
+
-- Only enforcing modes own the stale-schedule gate. Shadow observes without
|
|
1002
|
+
-- blocking, and an off rollout must be behaviorally inert for new cases.
|
|
1003
|
+
IF COALESCE(v_mode, 'off') IN ('off', 'shadow') THEN
|
|
1004
|
+
-- Consuming an aligned scheduled instant is a durable lifecycle transition,
|
|
1005
|
+
-- even when an operator turned new-case creation off after alignment.
|
|
1006
|
+
UPDATE public.subscription_delivery_alignment_cases
|
|
1007
|
+
SET state = 'released',
|
|
1008
|
+
updated_at = now()
|
|
1009
|
+
WHERE subscription_id = NEW.subscription_id
|
|
1010
|
+
AND state = 'aligned'
|
|
1011
|
+
AND aligned_next_cycle_at = NEW.scheduled_at;
|
|
1012
|
+
RETURN NEW;
|
|
1013
|
+
END IF;
|
|
1014
|
+
|
|
1015
|
+
IF NEW.scheduled_at IS DISTINCT FROM v_subscription.next_cycle_at THEN
|
|
1016
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_stale_schedule' USING ERRCODE = '40001';
|
|
1017
|
+
END IF;
|
|
1018
|
+
|
|
1019
|
+
-- Claim exactly one admission proof in the same transaction as the new
|
|
1020
|
+
-- cycle. A uniqueness conflict or later failure rolls this consumption back,
|
|
1021
|
+
-- allowing the runtime's normal idempotent retry without a second preflight.
|
|
1022
|
+
UPDATE public.subscription_delivery_alignment_admission_receipts
|
|
1023
|
+
SET consumed_at = now()
|
|
1024
|
+
WHERE subscription_id = NEW.subscription_id
|
|
1025
|
+
AND scheduled_at = NEW.scheduled_at
|
|
1026
|
+
AND observed_next_cycle_at = v_subscription.next_cycle_at
|
|
1027
|
+
AND decision = 'allowed'
|
|
1028
|
+
AND consumed_at IS NULL
|
|
1029
|
+
RETURNING id INTO v_receipt_id;
|
|
1030
|
+
IF v_receipt_id IS NULL THEN
|
|
1031
|
+
RAISE EXCEPTION 'subscription_delivery_alignment_admission_required' USING ERRCODE = 'P0001';
|
|
1032
|
+
END IF;
|
|
1033
|
+
|
|
1034
|
+
UPDATE public.subscription_delivery_alignment_cases
|
|
1035
|
+
SET state = 'released',
|
|
1036
|
+
updated_at = now()
|
|
1037
|
+
WHERE subscription_id = NEW.subscription_id
|
|
1038
|
+
AND state = 'aligned'
|
|
1039
|
+
AND aligned_next_cycle_at = NEW.scheduled_at;
|
|
1040
|
+
|
|
1041
|
+
RETURN NEW;
|
|
1042
|
+
END;
|
|
1043
|
+
$$;
|
|
1044
|
+
CREATE TRIGGER trg_subscription_delivery_alignment_exception_case
|
|
1045
|
+
AFTER INSERT OR UPDATE OF status, reason ON public.commerce_order_holds
|
|
1046
|
+
FOR EACH ROW EXECUTE FUNCTION public.subscription_delivery_alignment_open_exception_case();
|
|
1047
|
+
|
|
1048
|
+
CREATE TRIGGER trg_subscription_delivery_alignment_on_delivery
|
|
1049
|
+
AFTER INSERT OR UPDATE OF delivered_at ON public.fulfillment_shipments
|
|
1050
|
+
FOR EACH ROW EXECUTE FUNCTION public.subscription_delivery_alignment_on_delivery();
|
|
1051
|
+
|
|
1052
|
+
CREATE TRIGGER trg_subscription_delivery_alignment_guard_cycle_insert
|
|
1053
|
+
BEFORE INSERT ON public.subscription_cycles
|
|
1054
|
+
FOR EACH ROW EXECUTE FUNCTION public.subscription_delivery_alignment_guard_cycle_insert();
|
|
1055
|
+
|
|
1056
|
+
REVOKE ALL ON FUNCTION public.subscription_delivery_alignment_get_mode() FROM PUBLIC, anon, authenticated;
|
|
1057
|
+
REVOKE ALL ON FUNCTION public.subscription_delivery_alignment_set_mode(text) FROM PUBLIC, anon, authenticated;
|
|
1058
|
+
REVOKE ALL ON FUNCTION public.subscription_delivery_alignment_record_allowed_receipt(uuid, timestamptz, timestamptz, text) FROM PUBLIC, anon, authenticated;
|
|
1059
|
+
REVOKE ALL ON FUNCTION public.subscription_delivery_alignment_upsert_case(uuid, uuid, uuid, uuid, integer, text, text, timestamptz, text) FROM PUBLIC, anon, authenticated;
|
|
1060
|
+
REVOKE ALL ON FUNCTION public.subscription_delivery_alignment_open_exception_case() FROM PUBLIC, anon, authenticated;
|
|
1061
|
+
REVOKE ALL ON FUNCTION public.subscription_delivery_alignment_on_delivery() FROM PUBLIC, anon, authenticated;
|
|
1062
|
+
REVOKE ALL ON FUNCTION public.subscription_delivery_alignment_admit_renewal(uuid, timestamptz, timestamptz) FROM PUBLIC, anon, authenticated;
|
|
1063
|
+
REVOKE ALL ON FUNCTION public.subscription_delivery_alignment_resolve_case(uuid, text, text) FROM PUBLIC, anon, authenticated;
|
|
1064
|
+
REVOKE ALL ON FUNCTION public.subscription_delivery_alignment_confirm_replacement(uuid, text) FROM PUBLIC, anon, authenticated;
|
|
1065
|
+
REVOKE ALL ON FUNCTION public.subscription_delivery_alignment_guard_cycle_insert() FROM PUBLIC, anon, authenticated;
|
|
1066
|
+
REVOKE ALL ON public.subscription_delivery_alignment_control FROM PUBLIC, anon, authenticated;
|
|
1067
|
+
REVOKE ALL ON public.subscription_delivery_alignment_cases FROM PUBLIC, anon, authenticated;
|
|
1068
|
+
REVOKE ALL ON public.subscription_delivery_alignment_admission_receipts FROM PUBLIC, anon, authenticated;
|
|
1069
|
+
REVOKE ALL ON public.subscription_delivery_alignment_cycles FROM PUBLIC, anon, authenticated;
|
|
1070
|
+
|
|
1071
|
+
CREATE TABLE public.subscription_delivery_alignment_arrivals (
|
|
1072
|
+
request_key text PRIMARY KEY,
|
|
1073
|
+
shipment_id uuid NOT NULL REFERENCES public.fulfillment_shipments(id),
|
|
1074
|
+
delivered_at timestamptz NOT NULL,
|
|
1075
|
+
evidence_ref text NOT NULL CHECK (evidence_ref LIKE 'evidence:%')
|
|
1076
|
+
);
|
|
1077
|
+
CREATE FUNCTION public.fulfillment_record_alignment_arrival(p_key text,p_shipment uuid,p_arrived timestamptz,p_evidence text)
|
|
1078
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog AS $$
|
|
1079
|
+
DECLARE v_arrival public.subscription_delivery_alignment_arrivals%ROWTYPE; v_replayed boolean; v_subscription uuid;
|
|
1080
|
+
BEGIN
|
|
1081
|
+
IF btrim(COALESCE(p_key,''))='' OR p_arrived IS NULL OR p_evidence NOT LIKE 'evidence:%' OR p_evidence IS NULL THEN RAISE EXCEPTION 'fulfillment_arrival_invalid' USING ERRCODE='22023'; END IF;
|
|
1082
|
+
-- Arrival commands lock subscription before shipment, then the delivery trigger
|
|
1083
|
+
-- locks the case. Direct shipment writes already hold the shipment lock when
|
|
1084
|
+
-- their AFTER trigger acquires the subscription; callers must use this command
|
|
1085
|
+
-- or acquire the subscription first to preserve the same lock order.
|
|
1086
|
+
SELECT cycle.subscription_id INTO v_subscription FROM public.fulfillment_shipments shipment
|
|
1087
|
+
JOIN public.commerce_orders order_row ON order_row.id=shipment.order_id
|
|
1088
|
+
JOIN public.subscription_cycles cycle ON cycle.id=order_row.subscription_cycle_id WHERE shipment.id=p_shipment;
|
|
1089
|
+
IF v_subscription IS NOT NULL THEN PERFORM id FROM public.subscriptions WHERE id=v_subscription FOR UPDATE; END IF;
|
|
1090
|
+
PERFORM id FROM public.fulfillment_shipments WHERE id=p_shipment FOR UPDATE;
|
|
1091
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'fulfillment_arrival_shipment_missing' USING ERRCODE='22023'; END IF;
|
|
1092
|
+
SELECT * INTO v_arrival FROM public.subscription_delivery_alignment_arrivals WHERE request_key=p_key;
|
|
1093
|
+
v_replayed := FOUND;
|
|
1094
|
+
IF FOUND THEN
|
|
1095
|
+
IF v_arrival.shipment_id<>p_shipment OR v_arrival.delivered_at<>p_arrived OR v_arrival.evidence_ref<>p_evidence THEN RAISE EXCEPTION 'fulfillment_arrival_key_conflict' USING ERRCODE='23505'; END IF;
|
|
1096
|
+
ELSE
|
|
1097
|
+
INSERT INTO public.subscription_delivery_alignment_arrivals VALUES(p_key,p_shipment,p_arrived,p_evidence);
|
|
1098
|
+
-- Arrival evidence is supplied by the trusted selected binding, never inferred
|
|
1099
|
+
-- from status/receipt time. Preserve the first durable arrival on replay.
|
|
1100
|
+
UPDATE public.fulfillment_shipments SET delivered_at=p_arrived,updated_at=now() WHERE id=p_shipment AND delivered_at IS NULL;
|
|
1101
|
+
END IF;
|
|
1102
|
+
RETURN jsonb_build_object('replayed',v_replayed);
|
|
1103
|
+
END;
|
|
1104
|
+
$$;
|
|
1105
|
+
REVOKE ALL ON public.subscription_delivery_alignment_arrivals FROM PUBLIC, anon, authenticated;
|
|
1106
|
+
REVOKE ALL ON FUNCTION public.fulfillment_record_alignment_arrival(text,uuid,timestamptz,text) FROM PUBLIC, anon, authenticated;
|