@openlup/platform 0.22.0 → 0.24.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 +440 -158
- package/README.md +141 -589
- package/dist/fulfillment/adapterConformance.js +1 -1
- package/dist/fulfillment/bindings.js +1 -1
- package/dist/fulfillment/contracts/conformance.d.ts +3 -1
- package/dist/fulfillment/contracts/dispatch.d.ts +45 -3
- package/dist/fulfillment/contracts/index.d.ts +3 -3
- package/dist/fulfillment/contracts/observation.d.ts +21 -1
- package/dist/fulfillment/contracts/readback.d.ts +21 -0
- package/dist/fulfillment/contracts/store.d.ts +3 -0
- package/dist/fulfillment/contracts/sync.d.ts +58 -2
- package/dist/fulfillment/dispatchProjection.d.ts +20 -1
- package/dist/fulfillment/dispatchProjection.js +97 -6
- package/dist/fulfillment/index.d.ts +3 -2
- package/dist/fulfillment/index.js +2 -2
- package/dist/fulfillment/migrations.d.ts +5 -0
- package/dist/fulfillment/migrations.js +1 -0
- package/dist/fulfillment/observationProjection.d.ts +24 -3
- package/dist/fulfillment/observationProjection.js +65 -12
- package/dist/fulfillment/omnipack/client.d.ts +3 -1
- package/dist/fulfillment/omnipack/client.js +31 -13
- package/dist/fulfillment/omnipack/index.d.ts +4 -1
- package/dist/fulfillment/omnipack/index.js +107 -45
- package/dist/fulfillment/omnipack/mappers.d.ts +7 -1
- package/dist/fulfillment/omnipack/mappers.js +24 -2
- package/dist/fulfillment/omnipack/observation.d.ts +11 -4
- package/dist/fulfillment/omnipack/observation.js +44 -29
- package/dist/fulfillment/omnipack/reconciliation.d.ts +15 -0
- package/dist/fulfillment/omnipack/reconciliation.js +55 -0
- package/dist/fulfillment/postgresDispatchStore.js +1 -1
- package/dist/fulfillment/postgresShipmentSpine.d.ts +8 -1
- package/dist/fulfillment/postgresShipmentSpine.js +17 -3
- package/dist/fulfillment/postgresStockSyncPort.js +6 -3
- package/dist/fulfillment/providerDispatchAcceptance.d.ts +1 -0
- package/dist/fulfillment/providerDispatchAcceptance.js +8 -2
- package/dist/fulfillment/providerDispatchWorker.d.ts +51 -0
- package/dist/fulfillment/providerDispatchWorker.js +65 -20
- package/dist/fulfillment/providerReconciliationWorker.d.ts +27 -19
- package/dist/fulfillment/providerReconciliationWorker.js +75 -19
- package/dist/fulfillment/providerStockSyncWorker.d.ts +5 -1
- package/dist/fulfillment/providerStockSyncWorker.js +229 -120
- package/dist/fulfillment/providerTrackingReferences.d.ts +3 -2
- package/dist/fulfillment/verifiedIngress.d.ts +38 -11
- package/dist/fulfillment/verifiedIngress.js +36 -8
- package/dist/invoicing/accountingCanonicalProviderLines.d.ts +5 -0
- package/dist/invoicing/accountingCanonicalProviderLines.js +104 -0
- package/dist/invoicing/accountingLineAllocation.d.ts +2 -0
- package/dist/invoicing/accountingLineAllocation.js +69 -0
- package/dist/invoicing/adapterConformance.d.ts +42 -0
- package/dist/invoicing/adapterConformance.js +296 -0
- package/dist/invoicing/artifacts.d.ts +7 -0
- package/dist/invoicing/artifacts.js +20 -0
- package/dist/invoicing/bindings.d.ts +46 -0
- package/dist/invoicing/bindings.js +69 -0
- package/dist/invoicing/capabilities.d.ts +11 -0
- package/dist/invoicing/capabilities.js +10 -0
- package/dist/invoicing/channelPolicy.d.ts +28 -0
- package/dist/invoicing/channelPolicy.js +27 -0
- package/dist/invoicing/continuity.d.ts +12 -0
- package/dist/invoicing/continuity.js +18 -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 +110 -0
- package/dist/invoicing/contracts/facts.js +1 -0
- package/dist/invoicing/contracts/host.d.ts +245 -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 +277 -0
- package/dist/invoicing/contracts/store.js +1 -0
- package/dist/invoicing/contribution.d.ts +21 -0
- package/dist/invoicing/contribution.js +19 -0
- package/dist/invoicing/correction.d.ts +42 -0
- package/dist/invoicing/correction.js +44 -0
- package/dist/invoicing/delivery.d.ts +85 -0
- package/dist/invoicing/delivery.js +79 -0
- package/dist/invoicing/documentHistory.d.ts +50 -0
- package/dist/invoicing/documentHistory.js +213 -0
- package/dist/invoicing/documentSync.d.ts +24 -0
- package/dist/invoicing/documentSync.js +26 -0
- package/dist/invoicing/dutyCollector.d.ts +15 -0
- package/dist/invoicing/dutyCollector.js +35 -0
- package/dist/invoicing/fulfillmentTrigger.d.ts +56 -0
- package/dist/invoicing/fulfillmentTrigger.js +90 -0
- package/dist/invoicing/handoff.d.ts +37 -0
- package/dist/invoicing/handoff.js +62 -0
- package/dist/invoicing/index.d.ts +39 -0
- package/dist/invoicing/index.js +37 -0
- package/dist/invoicing/issue.d.ts +39 -0
- package/dist/invoicing/issue.js +55 -0
- package/dist/invoicing/migrations.d.ts +48 -0
- package/dist/invoicing/migrations.js +25 -0
- package/dist/invoicing/nativeUnsupported.d.ts +9 -0
- package/dist/invoicing/nativeUnsupported.js +22 -0
- package/dist/invoicing/paymentReadback.d.ts +20 -0
- package/dist/invoicing/paymentReadback.js +47 -0
- package/dist/invoicing/postgresInvoiceStore.d.ts +55 -0
- package/dist/invoicing/postgresInvoiceStore.js +344 -0
- package/dist/invoicing/readModel.d.ts +74 -0
- package/dist/invoicing/readModel.js +90 -0
- package/dist/invoicing/readback.d.ts +4 -0
- package/dist/invoicing/readback.js +30 -0
- package/dist/invoicing/reversal.d.ts +37 -0
- package/dist/invoicing/reversal.js +28 -0
- package/dist/invoicing/sellerRevision.d.ts +19 -0
- package/dist/invoicing/sellerRevision.js +14 -0
- package/dist/invoicing/settlementSync.d.ts +18 -0
- package/dist/invoicing/settlementSync.js +32 -0
- package/dist/invoicing/snapshot.d.ts +52 -0
- package/dist/invoicing/snapshot.js +141 -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 +23 -3
- package/release-gates.json +74 -7
- package/sql/20260809210000_accounting_document_rail.sql +889 -0
- package/sql/20260811150000_accounting_payout_rail.sql +333 -0
- package/sql/20260812130000_accounting_document_lifecycle_rail.sql +460 -0
- package/sql/20261022160000_subscription_retry_lifecycle.sql +328 -0
- package/sql/20261022165000_fulfillment_stock_evidence_replay_reopen.sql +30 -0
- package/sql/20261022190000_invoicing_durable_duties.sql +494 -0
- package/sql/20261022191000_invoicing_document_effects.sql +249 -0
- package/sql/20261022192000_invoicing_payment_decoupling.sql +144 -0
- package/sql/20261022195000_invoicing_seller_revision.sql +65 -0
- package/src/fulfillment/adapterConformance.ts +1 -1
- package/src/fulfillment/bindings.ts +1 -1
- package/src/fulfillment/contracts/conformance.ts +3 -1
- package/src/fulfillment/contracts/dispatch.ts +24 -5
- package/src/fulfillment/contracts/index.ts +3 -3
- package/src/fulfillment/contracts/observation.ts +11 -3
- package/src/fulfillment/contracts/readback.ts +13 -0
- package/src/fulfillment/contracts/store.ts +1 -0
- package/src/fulfillment/contracts/sync.ts +13 -2
- package/src/fulfillment/dispatchProjection.ts +94 -7
- package/src/fulfillment/index.ts +3 -2
- package/src/fulfillment/migrations.ts +1 -0
- package/src/fulfillment/observationProjection.ts +64 -13
- package/src/fulfillment/omnipack/client.ts +32 -13
- package/src/fulfillment/omnipack/index.ts +99 -39
- package/src/fulfillment/omnipack/mappers.ts +23 -2
- package/src/fulfillment/omnipack/observation.ts +45 -27
- package/src/fulfillment/omnipack/reconciliation.ts +56 -0
- package/src/fulfillment/postgresDispatchStore.ts +1 -1
- package/src/fulfillment/postgresShipmentSpine.ts +21 -4
- package/src/fulfillment/postgresStockSyncPort.ts +6 -3
- package/src/fulfillment/providerDispatchAcceptance.ts +9 -3
- package/src/fulfillment/providerDispatchWorker.ts +61 -20
- package/src/fulfillment/providerReconciliationWorker.ts +81 -25
- package/src/fulfillment/providerStockSyncWorker.ts +222 -126
- package/src/fulfillment/providerTrackingReferences.ts +3 -2
- package/src/fulfillment/verifiedIngress.ts +44 -11
- package/src/invoicing/accountingCanonicalProviderLines.ts +134 -0
- package/src/invoicing/accountingLineAllocation.ts +69 -0
- package/src/invoicing/adapterConformance.ts +249 -0
- package/src/invoicing/artifacts.ts +19 -0
- package/src/invoicing/bindings.ts +80 -0
- package/src/invoicing/capabilities.ts +9 -0
- package/src/invoicing/channelPolicy.ts +50 -0
- package/src/invoicing/continuity.ts +15 -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 +71 -0
- package/src/invoicing/contracts/host.ts +84 -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 +102 -0
- package/src/invoicing/contribution.ts +28 -0
- package/src/invoicing/correction.ts +56 -0
- package/src/invoicing/delivery.ts +122 -0
- package/src/invoicing/documentHistory.ts +301 -0
- package/src/invoicing/documentSync.ts +51 -0
- package/src/invoicing/dutyCollector.ts +31 -0
- package/src/invoicing/fulfillmentTrigger.ts +144 -0
- package/src/invoicing/handoff.ts +71 -0
- package/src/invoicing/index.ts +39 -0
- package/src/invoicing/issue.ts +64 -0
- package/src/invoicing/migrations.ts +25 -0
- package/src/invoicing/nativeUnsupported.ts +24 -0
- package/src/invoicing/paymentReadback.ts +70 -0
- package/src/invoicing/postgresInvoiceStore.ts +334 -0
- package/src/invoicing/readModel.ts +80 -0
- package/src/invoicing/readback.ts +26 -0
- package/src/invoicing/reversal.ts +34 -0
- package/src/invoicing/sellerRevision.ts +20 -0
- package/src/invoicing/settlementSync.ts +37 -0
- package/src/invoicing/snapshot.ts +192 -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
|
@@ -24,11 +24,11 @@ export function createOmnipackClient(config, fetchImpl) {
|
|
|
24
24
|
const body = await requestJson(fetchImpl, config, "createProductPack", path, post(payload));
|
|
25
25
|
return mapOmnipackProductPackCreatedResponse(body, sku, ean);
|
|
26
26
|
},
|
|
27
|
-
async getStock() {
|
|
28
|
-
return mapOmnipackStockResponse(await requestJson(fetchImpl, config, "getStock", "/stock", { method: "GET" }));
|
|
27
|
+
async getStock(options) {
|
|
28
|
+
return mapOmnipackStockResponse(await requestJson(fetchImpl, config, "getStock", "/stock", { method: "GET" }, options?.refuseRedirectRetry));
|
|
29
29
|
},
|
|
30
30
|
async getStockMovements(params) {
|
|
31
|
-
return mapOmnipackStockMovementsResponse(await requestJson(fetchImpl, config, "getStockMovements", withQuery("/stock-movements", params), { method: "GET" }));
|
|
31
|
+
return mapOmnipackStockMovementsResponse(await requestJson(fetchImpl, config, "getStockMovements", withQuery("/stock-movements", params), { method: "GET" }, true));
|
|
32
32
|
},
|
|
33
33
|
async getFulfilmentsPage(params) {
|
|
34
34
|
const body = await requestJson(fetchImpl, config, "getFulfilments", withQuery("/fulfilments", params), { method: "GET" });
|
|
@@ -49,30 +49,41 @@ export function createOmnipackClient(config, fetchImpl) {
|
|
|
49
49
|
},
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
async function requestJson(fetchImpl, config, operation, path, init) {
|
|
52
|
+
async function requestJson(fetchImpl, config, operation, path, init, refuseRedirectRetry = false) {
|
|
53
53
|
const method = (init.method ?? "GET").toUpperCase();
|
|
54
54
|
const attempts = method === "GET" ? config.safeReadRetries + 1 : 1;
|
|
55
|
+
// Dispatch/readback diagnostics must not change product-sync or stock retry policy.
|
|
56
|
+
const diagnoseRedirectFailure = refuseRedirectRetry || operation === "createOrder"
|
|
57
|
+
|| operation === "getFulfilments" || operation === "getFulfilmentRequests";
|
|
55
58
|
let lastError = null;
|
|
56
59
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
57
60
|
let submitted = false;
|
|
61
|
+
let responseBodyRead;
|
|
62
|
+
const dispatchFacts = () => operation === "createOrder" ? { submissionAttempted: submitted,
|
|
63
|
+
...(responseBodyRead !== undefined ? { responseBodyRead } : {}) } : {};
|
|
58
64
|
try {
|
|
59
65
|
const base = new URL(config.baseUrl);
|
|
60
66
|
const url = new URL(path, base);
|
|
61
67
|
if (url.origin !== base.origin || url.username || url.password || url.protocol !== "https:")
|
|
62
68
|
throw new Error("Invalid fulfillment path");
|
|
63
69
|
const response = await fetchWithTimeout(fetchImpl, url, config, init, () => { submitted = true; });
|
|
70
|
+
responseBodyRead = false;
|
|
71
|
+
const body = await readJsonBody(response).then(value => { responseBodyRead = true; return value; }).catch(error => {
|
|
72
|
+
if (response.ok)
|
|
73
|
+
throw error;
|
|
74
|
+
return null;
|
|
75
|
+
});
|
|
64
76
|
if (response.ok)
|
|
65
|
-
return
|
|
77
|
+
return body;
|
|
66
78
|
const retryable = method === "GET" && isRetryableStatus(response.status);
|
|
67
79
|
// A failed body read cannot erase the rejection status already received.
|
|
68
|
-
|
|
69
|
-
lastError = new OmnipackProviderError({
|
|
80
|
+
lastError = Object.assign(new OmnipackProviderError({
|
|
70
81
|
operation,
|
|
71
82
|
status: response.status,
|
|
72
|
-
code: readProviderErrorCode(body) ?? `http_${response.status}`,
|
|
83
|
+
code: refuseRedirectRetry && response.status >= 300 && response.status < 400 ? "redirect_refused" : readProviderErrorCode(body) ?? `http_${response.status}`,
|
|
73
84
|
retryable,
|
|
74
85
|
mayHaveSucceeded: mayPostHaveSucceeded(method, response.status),
|
|
75
|
-
});
|
|
86
|
+
}), dispatchFacts());
|
|
76
87
|
if (!retryable || attempt === attempts)
|
|
77
88
|
throw lastError;
|
|
78
89
|
}
|
|
@@ -80,14 +91,14 @@ async function requestJson(fetchImpl, config, operation, path, init) {
|
|
|
80
91
|
const retryable = method === "GET" && attempt < attempts;
|
|
81
92
|
lastError = submitted && error instanceof OmnipackProviderError
|
|
82
93
|
? error
|
|
83
|
-
: new OmnipackProviderError({
|
|
94
|
+
: Object.assign(new OmnipackProviderError({
|
|
84
95
|
operation,
|
|
85
96
|
status: null,
|
|
86
|
-
code: "network_error",
|
|
97
|
+
code: diagnoseRedirectFailure && isRedirectRefusal(error) ? "redirect_refused" : "network_error",
|
|
87
98
|
retryable,
|
|
88
99
|
mayHaveSucceeded: submitted && mayPostHaveSucceeded(method, null),
|
|
89
|
-
});
|
|
90
|
-
if (!retryable)
|
|
100
|
+
}), dispatchFacts());
|
|
101
|
+
if (!retryable || (refuseRedirectRetry && lastError.code === "redirect_refused"))
|
|
91
102
|
throw lastError;
|
|
92
103
|
}
|
|
93
104
|
}
|
|
@@ -160,3 +171,10 @@ function readProviderErrorCode(body) {
|
|
|
160
171
|
function isRetryableStatus(status) {
|
|
161
172
|
return status === 408 || status === 429 || status >= 500;
|
|
162
173
|
}
|
|
174
|
+
// Fetch implementations expose redirect refusal as an exception, sometimes through cause.
|
|
175
|
+
// Inspect it only to select a closed diagnostic; no exception text leaves this adapter.
|
|
176
|
+
function isRedirectRefusal(error) {
|
|
177
|
+
if (!(error instanceof Error))
|
|
178
|
+
return false;
|
|
179
|
+
return /redirect/i.test(error.message) || (error.cause instanceof Error && /redirect/i.test(error.cause.message));
|
|
180
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { FulfillmentCapabilityDescriptor, FulfillmentDispatchPort, FulfillmentDispatchRequest, FulfillmentLabelPort, FulfillmentObservationPort, FulfillmentReadbackPort, FulfillmentStockSyncProvider, FulfillmentProductSyncProvider } from "../contracts/index.js";
|
|
1
|
+
import type { FulfillmentCapabilityDescriptor, FulfillmentDispatchPort, FulfillmentDispatchRequest, FulfillmentLabelPort, FulfillmentObservationPort, FulfillmentReadbackPort, FulfillmentObservationPagePort, FulfillmentStockSyncProvider, FulfillmentProductSyncProvider } from "../contracts/index.js";
|
|
2
2
|
import { type OmnipackWebhookRouteEvent } from "./omnipackWebhookPayload.js";
|
|
3
|
+
import { type OmnipackObservationCodecs } from "./observation.js";
|
|
3
4
|
export { omnipackFulfillmentCapabilities } from "./capabilities.js";
|
|
4
5
|
/** Node-only, explicit construction. Secrets are supplied per call, never retained as values. */
|
|
5
6
|
export interface OmnipackFulfillmentAdapterConfig {
|
|
@@ -35,6 +36,7 @@ export interface OmnipackFulfillmentAdapterConfig {
|
|
|
35
36
|
mintEvidenceReference: () => `evidence:${string}`;
|
|
36
37
|
mintSyncRunId: () => string;
|
|
37
38
|
approveTrackingUrl: (url: string) => boolean;
|
|
39
|
+
observationCodecs?: OmnipackObservationCodecs;
|
|
38
40
|
/** Writes occur only through deliberate calls; scheduled composition must remain pull-only. */
|
|
39
41
|
product?: {
|
|
40
42
|
resolvePushMetadata: (sku: string) => Promise<Record<string, unknown> | null>;
|
|
@@ -47,6 +49,7 @@ export declare function createOmnipackFulfillmentAdapter(config: OmnipackFulfill
|
|
|
47
49
|
label: FulfillmentLabelPort;
|
|
48
50
|
observation: FulfillmentObservationPort;
|
|
49
51
|
readback: FulfillmentReadbackPort;
|
|
52
|
+
reconciliation: FulfillmentObservationPagePort;
|
|
50
53
|
stockSync: FulfillmentStockSyncProvider;
|
|
51
54
|
productSync: FulfillmentProductSyncProvider;
|
|
52
55
|
};
|
|
@@ -1,11 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { publicTrackingUrl } from "../observationProjection.js";
|
|
1
3
|
import { createOmnipackClient, OmnipackProviderError } from "./client.js";
|
|
2
4
|
import { omnipackFulfillmentCapabilities } from "./capabilities.js";
|
|
3
5
|
import { buildOmnipackDispatchCommand, serializeOmnipackDispatchPayload, stableDispatchHash } from "./omnipackDispatchPayload.js";
|
|
4
6
|
import { mapOmnipackWebhookPayload } from "./mappers.js";
|
|
5
7
|
import { verifyOmnipackWebhookToken } from "./omnipackWebhookPayload.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
+
import { createReconciliationPagePort, reconciliationObservation } from "./reconciliation.js";
|
|
9
|
+
import { dispatchEvidence, operationError } from "../dispatchProjection.js";
|
|
10
|
+
import { correlationReference, digest, eventKind, observation, reference, refusal, timestamp } from "./observation.js";
|
|
8
11
|
export { omnipackFulfillmentCapabilities } from "./capabilities.js";
|
|
12
|
+
// Internal error projection: retain diagnostics without exposing a provider payload or cause.
|
|
13
|
+
class OmnipackSyncError extends Error {
|
|
14
|
+
reason;
|
|
15
|
+
phase = "sync";
|
|
16
|
+
retryable;
|
|
17
|
+
providerFailure;
|
|
18
|
+
status;
|
|
19
|
+
code;
|
|
20
|
+
constructor(reason, operation, error) {
|
|
21
|
+
super(reason === "io_failure" ? `OmniPack request failed: ${operation}` : `OmniPack sync refused: ${reason}`);
|
|
22
|
+
this.reason = reason;
|
|
23
|
+
this.name = "OmnipackSyncError";
|
|
24
|
+
this.providerFailure = reason === "io_failure";
|
|
25
|
+
this.retryable = error instanceof OmnipackProviderError ? error.retryable : false;
|
|
26
|
+
this.status = error instanceof OmnipackProviderError ? error.status : null;
|
|
27
|
+
this.code = error instanceof OmnipackProviderError ? error.code : null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function syncFailure(operation, error) {
|
|
31
|
+
return error instanceof OmnipackSyncError ? error : new OmnipackSyncError("io_failure", operation, error);
|
|
32
|
+
}
|
|
33
|
+
function requireProductSku(sku) {
|
|
34
|
+
// URL resolution collapses dot segments; encodeURIComponent rejects lone surrogates.
|
|
35
|
+
// Keep these SKUs in the listing so the worker counts a visible per-SKU failure.
|
|
36
|
+
try {
|
|
37
|
+
if (!sku.trim() || sku === "." || sku === "..")
|
|
38
|
+
throw Error();
|
|
39
|
+
encodeURIComponent(sku);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new OmnipackSyncError("malformed", "getProduct");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
9
45
|
/** No route, schedule, environment reader or default binding is installed. */
|
|
10
46
|
export function createOmnipackFulfillmentAdapter(config) {
|
|
11
47
|
const maxRetainedSubmissions = config.maxRetainedSubmissions === undefined ? 10_000 : config.maxRetainedSubmissions;
|
|
@@ -48,13 +84,13 @@ export function createOmnipackFulfillmentAdapter(config) {
|
|
|
48
84
|
};
|
|
49
85
|
const requireSync = () => {
|
|
50
86
|
if (!capabilities.selected || !capabilities.readiness.ready)
|
|
51
|
-
throw
|
|
87
|
+
throw new OmnipackSyncError(capabilities.selected ? "not_ready" : "disabled", "selection");
|
|
52
88
|
};
|
|
53
89
|
const syncIdentity = () => {
|
|
54
90
|
const syncRunId = reference(config.mintSyncRunId());
|
|
55
91
|
const collectedAt = timestamp(config.clock());
|
|
56
92
|
if (!syncRunId || !collectedAt)
|
|
57
|
-
throw
|
|
93
|
+
throw new OmnipackSyncError("malformed", "sync");
|
|
58
94
|
return { providerRef: capabilities.providerRef, syncRunId, collectedAt, sourceRevision: null, revisionSource: "collection" };
|
|
59
95
|
};
|
|
60
96
|
// Shared computation: host metadata and serialized bytes live only for this call.
|
|
@@ -81,9 +117,12 @@ export function createOmnipackFulfillmentAdapter(config) {
|
|
|
81
117
|
selectedDelivery: { providerKind: capabilities.providerRef, carrierKind: request.selection.carrierRef,
|
|
82
118
|
serviceCode: request.selection.serviceRef, deliveryKind: request.selection.deliveryKind, pickupPoint: request.selection.pickupPoint } }, lines,
|
|
83
119
|
});
|
|
84
|
-
const evidence = { commandFingerprint: command.requestFingerprint,
|
|
120
|
+
const evidence = dispatchEvidence({ commandFingerprint: command.requestFingerprint,
|
|
85
121
|
contactRevision: { schemaVersion: request.contactRevision.schemaVersion, source: request.contactRevision.source, revision: request.contactRevision.revision },
|
|
86
|
-
providerRef: capabilities.providerRef, carrierRef: request.selection.carrierRef, serviceRef: request.selection.serviceRef
|
|
122
|
+
providerRef: capabilities.providerRef, carrierRef: request.selection.carrierRef, serviceRef: request.selection.serviceRef,
|
|
123
|
+
transcript: { itemCount: command.providerPayload.items.length,
|
|
124
|
+
items: command.providerPayload.items.map(item => ({ skuRef: item.sku, quantity: item.quantity, lotRef: item.lotNumber ?? null, expirationDate: item.expirationDate ?? null })),
|
|
125
|
+
deliveryKind: request.selection.deliveryKind, pickupPointRef: request.selection.pickupPoint?.id ?? null } });
|
|
87
126
|
return { body: command.body, evidence };
|
|
88
127
|
};
|
|
89
128
|
return {
|
|
@@ -107,7 +146,7 @@ export function createOmnipackFulfillmentAdapter(config) {
|
|
|
107
146
|
let requestCommandDigest;
|
|
108
147
|
try {
|
|
109
148
|
// Validate the closed evidence and its request-bound values without consulting preparation state.
|
|
110
|
-
if (Object.keys(evidence).length !==
|
|
149
|
+
if (Object.keys(evidence).length !== 6 || Object.keys(evidence.contactRevision).length !== 3
|
|
111
150
|
|| typeof evidence.commandFingerprint !== "string" || !/^[a-f0-9]{64}$/u.test(evidence.commandFingerprint)
|
|
112
151
|
|| !reference(evidence.providerRef)
|
|
113
152
|
|| !reference(evidence.carrierRef) || !reference(evidence.serviceRef)
|
|
@@ -121,6 +160,8 @@ export function createOmnipackFulfillmentAdapter(config) {
|
|
|
121
160
|
|| evidence.carrierRef !== request.selection.carrierRef || evidence.serviceRef !== request.selection.serviceRef)
|
|
122
161
|
throw Error();
|
|
123
162
|
evidenceKey = serializeOmnipackDispatchPayload(evidence).body;
|
|
163
|
+
if (serializeOmnipackDispatchPayload(dispatchEvidence(evidence)).body !== evidenceKey)
|
|
164
|
+
throw Error();
|
|
124
165
|
requestCommandDigest = commandDigest(request);
|
|
125
166
|
const submitted = submissions.get(evidence.commandFingerprint);
|
|
126
167
|
if (submitted) {
|
|
@@ -142,7 +183,7 @@ export function createOmnipackFulfillmentAdapter(config) {
|
|
|
142
183
|
let command;
|
|
143
184
|
try {
|
|
144
185
|
command = await buildCommand(commandRequest);
|
|
145
|
-
if (command.evidence.commandFingerprint !== fingerprint)
|
|
186
|
+
if (command.evidence.commandFingerprint !== fingerprint || serializeOmnipackDispatchPayload(command.evidence).body !== evidenceKey)
|
|
146
187
|
throw Error();
|
|
147
188
|
}
|
|
148
189
|
catch {
|
|
@@ -161,9 +202,10 @@ export function createOmnipackFulfillmentAdapter(config) {
|
|
|
161
202
|
providerAttemptKey: `attempt:${proof}`, labelKey: `label:${proof}` };
|
|
162
203
|
}
|
|
163
204
|
catch (error) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
205
|
+
const detail = operationError(error instanceof OmnipackProviderError ? error : { code: "provider_dispatch_failed", status: null, retryable: true, mayHaveSucceeded: true });
|
|
206
|
+
return !detail.mayHaveSucceeded
|
|
207
|
+
? { kind: "not_dispatched", effectMayHaveSucceeded: false, refusal: refusal("io_failure", "dispatch", detail.retryable), error: detail }
|
|
208
|
+
: { kind: "unknown", effectMayHaveSucceeded: true, refusal: refusal("io_failure", "dispatch", detail.retryable), error: detail };
|
|
167
209
|
}
|
|
168
210
|
finally {
|
|
169
211
|
command.body = "";
|
|
@@ -198,14 +240,31 @@ export function createOmnipackFulfillmentAdapter(config) {
|
|
|
198
240
|
return { kind: "refused", refusal: refusal("malformed", "normalize") };
|
|
199
241
|
if ("event" in payload && (typeof payload.event !== "string" || !eventKind(payload.event)))
|
|
200
242
|
return { kind: "refused", refusal: refusal("unsupported_event", "normalize") };
|
|
201
|
-
const
|
|
243
|
+
const original = payload;
|
|
244
|
+
// Validate calendar fields here only; keep compact offsets and local
|
|
245
|
+
// timestamps and minute precision accepted by the base parser,
|
|
246
|
+
// without rewriting key bytes.
|
|
247
|
+
if (original.occurredAt !== undefined && original.occurredAt !== null
|
|
248
|
+
&& (typeof original.occurredAt !== "string" || !timestamp(original.occurredAt)
|
|
249
|
+
|| !z.iso.datetime({ offset: true, local: true })
|
|
250
|
+
.or(z.iso.datetime({ offset: true, local: true, precision: -1 }))
|
|
251
|
+
.safeParse(original.occurredAt.replace(/([+-]\d{2})(\d{2})$/u, "$1:$2")).success))
|
|
252
|
+
return { kind: "refused", refusal: refusal("malformed", "normalize"), diagnostics: { invalidTimestamp: true } };
|
|
253
|
+
const rejectedCorrelationRefs = [original.orderId, original.providerOrderId, original.orderNumber, original.fulfilmentNumber]
|
|
254
|
+
.filter(value => value !== undefined && value !== null && (typeof value !== "string" || !correlationReference(value, config.observationCodecs))).length;
|
|
255
|
+
if (rejectedCorrelationRefs)
|
|
256
|
+
return { kind: "refused", refusal: refusal("missing_correlation", "normalize"), diagnostics: { rejectedCorrelationRefs } };
|
|
257
|
+
const evidence = mapOmnipackWebhookPayload(payload, config.notification.expectedEvent, value => correlationReference(value, config.observationCodecs) !== null, {
|
|
258
|
+
service: value => correlationReference(value, config.observationCodecs) !== null,
|
|
259
|
+
url: value => publicTrackingUrl(value, config.approveTrackingUrl, config.observationCodecs?.trackingUrl) !== null,
|
|
260
|
+
});
|
|
202
261
|
const event = eventKind(evidence.event);
|
|
203
262
|
if (!event || event !== config.notification.expectedEvent)
|
|
204
263
|
return { kind: "refused", refusal: refusal("unsupported_event", "normalize") };
|
|
205
|
-
if (![evidence.providerOrderId, evidence.orderNumber, evidence.fulfilmentNumber].some(value =>
|
|
264
|
+
if (![evidence.providerOrderId, evidence.orderNumber, evidence.fulfilmentNumber].some(value => correlationReference(value, config.observationCodecs)))
|
|
206
265
|
return { kind: "refused", refusal: refusal("missing_correlation", "normalize") };
|
|
207
266
|
return { kind: "verified", observation: observation({ evidence, event, receivedAt: input.receivedAt,
|
|
208
|
-
rawDigest: digest(rawBody), evidenceRef: config.mintEvidenceReference(), source: "provider_webhook", approveTrackingUrl: config.approveTrackingUrl }) };
|
|
267
|
+
rawDigest: digest(rawBody), evidenceRef: config.mintEvidenceReference(), source: "provider_webhook", approveTrackingUrl: config.approveTrackingUrl, observationCodecs: config.observationCodecs }) };
|
|
209
268
|
}
|
|
210
269
|
catch {
|
|
211
270
|
return { kind: "refused", refusal: refusal("malformed", "normalize") };
|
|
@@ -228,55 +287,47 @@ export function createOmnipackFulfillmentAdapter(config) {
|
|
|
228
287
|
? row.fulfilmentNumber === input.externalRef : row.providerOrderId === input.externalRef);
|
|
229
288
|
if (matches.length !== 1)
|
|
230
289
|
return { kind: "unknown", refusal: refusal("local_missing", "readback") };
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
if (!normalized)
|
|
234
|
-
return { kind: "unknown", refusal: refusal("unsupported_event", "readback") };
|
|
235
|
-
const status = localStatusFor(normalized);
|
|
236
|
-
const event = { provider_received: "shipment.accepted", picking: "order.processing_started", packed: "order.picked", in_transit: "order.shipped", delivered: "order.delivered", exception: "parcel.exception" }[status];
|
|
237
|
-
const evidence = { provider: "omnipack", event, providerOrderId: row.providerOrderId, orderNumber: row.orderNumber,
|
|
238
|
-
fulfilmentNumber: row.fulfilmentNumber, occurredAt: row.updatedAt, trackingNumbers: row.trackingNumbers,
|
|
239
|
-
trackingReferences: row.trackingReferences, shippingMethods: [] };
|
|
240
|
-
return { kind: "found", externalRef: input.externalRef, createdAt: timestamp(row.createdAt), updatedAt: timestamp(row.updatedAt),
|
|
241
|
-
observation: observation({ evidence, event, status, fulfillmentRef: input.fulfillmentRef, receivedAt: config.clock(),
|
|
242
|
-
rawDigest: digest(JSON.stringify(evidence)), evidenceRef: config.mintEvidenceReference(), source: "provider_reconciliation", approveTrackingUrl: config.approveTrackingUrl }) };
|
|
290
|
+
const mapped = reconciliationObservation(matches[0], config, input.fulfillmentRef);
|
|
291
|
+
return mapped.kind === "unmapped" ? { kind: "unknown", refusal: refusal("unsupported_event", "readback") } : { ...mapped, externalRef: input.externalRef };
|
|
243
292
|
}
|
|
244
293
|
catch {
|
|
245
294
|
return { kind: "unknown", refusal: refusal("io_failure", "readback") };
|
|
246
295
|
}
|
|
247
296
|
},
|
|
248
297
|
},
|
|
298
|
+
reconciliation: createReconciliationPagePort(client, config, () => !capabilities.selected || !capabilities.readiness.ready
|
|
299
|
+
? refusal(capabilities.selected ? "not_ready" : "disabled", "readback") : null),
|
|
249
300
|
stockSync: {
|
|
250
301
|
async getStock() {
|
|
251
302
|
requireSync();
|
|
252
303
|
try {
|
|
253
304
|
const identity = syncIdentity();
|
|
254
|
-
return (await client.getStock()).map(row => {
|
|
305
|
+
return (await client.getStock({ refuseRedirectRetry: true })).map((row, itemIndex) => {
|
|
255
306
|
const skuRef = reference(row.sku);
|
|
256
307
|
if (!skuRef)
|
|
257
|
-
|
|
308
|
+
return { kind: "rejected", itemIndex, reason: "invalid_sku_ref" };
|
|
258
309
|
return { ...identity, skuRef, quantityKind: "snapshot", totalQuantity: row.totalQuantity,
|
|
259
310
|
forSaleQuantity: row.forSaleQuantity, reservedOrUnavailableQuantity: row.reservedOrUnavailableQuantity };
|
|
260
311
|
});
|
|
261
312
|
}
|
|
262
|
-
catch {
|
|
263
|
-
throw
|
|
313
|
+
catch (error) {
|
|
314
|
+
throw stockFailure(error, "get_stock");
|
|
264
315
|
}
|
|
265
316
|
},
|
|
266
317
|
async getStockMovements(input) {
|
|
267
318
|
requireSync();
|
|
268
319
|
try {
|
|
269
320
|
const identity = syncIdentity();
|
|
270
|
-
return (await client.getStockMovements(input)).map(row => {
|
|
321
|
+
return (await client.getStockMovements(input)).map((row, itemIndex) => {
|
|
271
322
|
const skuRef = reference(row.sku);
|
|
272
323
|
if (!skuRef)
|
|
273
|
-
|
|
324
|
+
return { kind: "rejected", itemIndex, reason: "invalid_sku_ref" };
|
|
274
325
|
return { ...identity, skuRef, quantityKind: "movement", quantity: row.quantity, occurredAt: timestamp(row.occurredAt),
|
|
275
326
|
operationType: reference(row.operationType), documentRef: reference(row.warehouseDocumentNumber), lotRef: reference(row.lotNumber), expirationDate: /^\d{4}-\d\d-\d\d$/u.test(row.expirationDate ?? "") ? row.expirationDate : null };
|
|
276
327
|
});
|
|
277
328
|
}
|
|
278
|
-
catch {
|
|
279
|
-
throw
|
|
329
|
+
catch (error) {
|
|
330
|
+
throw stockFailure(error, "get_stock_movements");
|
|
280
331
|
}
|
|
281
332
|
},
|
|
282
333
|
},
|
|
@@ -290,37 +341,38 @@ export function createOmnipackFulfillmentAdapter(config) {
|
|
|
290
341
|
await client.createProduct(metadata, sku);
|
|
291
342
|
return { replayed: false };
|
|
292
343
|
}
|
|
293
|
-
catch {
|
|
294
|
-
throw
|
|
344
|
+
catch (error) {
|
|
345
|
+
throw syncFailure("createProduct", error);
|
|
295
346
|
}
|
|
296
347
|
},
|
|
297
348
|
async getProductPacks(sku) {
|
|
298
349
|
requireSync();
|
|
350
|
+
requireProductSku(sku);
|
|
299
351
|
try {
|
|
300
|
-
return (await client.getProduct(sku)).packs.
|
|
352
|
+
return (await client.getProduct(sku)).packs.map(pack => ({ ean: pack.ean, quantity: pack.quantity }));
|
|
301
353
|
}
|
|
302
|
-
catch {
|
|
303
|
-
throw
|
|
354
|
+
catch (error) {
|
|
355
|
+
throw syncFailure("getProduct", error);
|
|
304
356
|
}
|
|
305
357
|
},
|
|
306
358
|
async addProductPack(sku, pack) {
|
|
307
359
|
requireSync();
|
|
308
360
|
if (!/^\d{8,14}$/u.test(pack.ean) || !Number.isSafeInteger(pack.quantity) || pack.quantity <= 0)
|
|
309
|
-
throw
|
|
361
|
+
throw new OmnipackSyncError("malformed", "sync");
|
|
310
362
|
try {
|
|
311
363
|
await client.createProductPack(sku, { ean: pack.ean, quantity: pack.quantity }, pack.ean);
|
|
312
364
|
}
|
|
313
|
-
catch {
|
|
314
|
-
throw
|
|
365
|
+
catch (error) {
|
|
366
|
+
throw syncFailure("createProductPack", error);
|
|
315
367
|
}
|
|
316
368
|
},
|
|
317
369
|
async listStockedSkus() {
|
|
318
370
|
requireSync();
|
|
319
371
|
try {
|
|
320
|
-
return [...new Set((await client.getStock()).flatMap(row =>
|
|
372
|
+
return [...new Set((await client.getStock()).flatMap(row => row.sku.trim() ? [row.sku] : []))];
|
|
321
373
|
}
|
|
322
|
-
catch {
|
|
323
|
-
throw
|
|
374
|
+
catch (error) {
|
|
375
|
+
throw syncFailure("getStock", error);
|
|
324
376
|
}
|
|
325
377
|
},
|
|
326
378
|
},
|
|
@@ -347,3 +399,13 @@ function commandDigest(request) {
|
|
|
347
399
|
pickupPointId: request.selection.pickupPoint?.id ?? null },
|
|
348
400
|
});
|
|
349
401
|
}
|
|
402
|
+
/** Translate only the contributed client's typed error; never copy a message or payload. */
|
|
403
|
+
function stockFailure(error, operation) {
|
|
404
|
+
const failure = syncFailure(operation === "get_stock" ? "getStock" : "getStockMovements", error);
|
|
405
|
+
if (!(error instanceof OmnipackProviderError))
|
|
406
|
+
return failure;
|
|
407
|
+
const retryable = error.code !== "redirect_refused" && (error.retryable || error.code === "network_error");
|
|
408
|
+
return Object.assign(failure, { retryable, providerError: {
|
|
409
|
+
operation, status: error.status, code: error.code, retryable,
|
|
410
|
+
} });
|
|
411
|
+
}
|
|
@@ -53,6 +53,9 @@ export interface OmnipackFulfilmentRequestEvidence {
|
|
|
53
53
|
state: string | null;
|
|
54
54
|
}
|
|
55
55
|
export interface OmnipackWebhookEvidence {
|
|
56
|
+
rejectedTrackingRefs?: number;
|
|
57
|
+
rejectedServices?: number;
|
|
58
|
+
rejectedTrackingUrls?: number;
|
|
56
59
|
provider: "omnipack";
|
|
57
60
|
event: string;
|
|
58
61
|
providerOrderId: string | null;
|
|
@@ -69,5 +72,8 @@ export declare function mapOmnipackStockResponse(body: unknown): OmnipackStockEv
|
|
|
69
72
|
export declare function mapOmnipackStockMovementsResponse(body: unknown): OmnipackStockMovementEvidence[];
|
|
70
73
|
export declare function mapOmnipackFulfilmentsResponse(body: unknown): OmnipackFulfilmentEvidence[];
|
|
71
74
|
export declare function mapOmnipackFulfilmentRequestsResponse(body: unknown): OmnipackFulfilmentRequestEvidence[];
|
|
72
|
-
export declare function mapOmnipackWebhookPayload(body: unknown, fallbackEvent?: string
|
|
75
|
+
export declare function mapOmnipackWebhookPayload(body: unknown, fallbackEvent?: string, admitTrackingNumber?: (value: string) => boolean, admitMetadata?: {
|
|
76
|
+
service(value: string): boolean;
|
|
77
|
+
url(value: string): boolean;
|
|
78
|
+
}): OmnipackWebhookEvidence;
|
|
73
79
|
export declare function sanitizeOmnipackPayload(input: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -102,13 +102,35 @@ export function mapOmnipackFulfilmentRequestsResponse(body) {
|
|
|
102
102
|
};
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
|
-
export function mapOmnipackWebhookPayload(body, fallbackEvent = "") {
|
|
105
|
+
export function mapOmnipackWebhookPayload(body, fallbackEvent = "", admitTrackingNumber, admitMetadata) {
|
|
106
106
|
const payload = asRecord(body);
|
|
107
107
|
const shipments = readArray(payload.shipments).map(asRecord);
|
|
108
|
-
|
|
108
|
+
let rejectedTrackingRefs = 0, rejectedServices = 0, rejectedTrackingUrls = 0;
|
|
109
|
+
// Count every supplied spelling before extraction, tracking refusal or first-wins deduplication.
|
|
110
|
+
if (admitMetadata)
|
|
111
|
+
for (const row of shipments) {
|
|
112
|
+
for (const value of [row.shippingMethod, row.service]) {
|
|
113
|
+
if (value != null && (typeof value !== "string" || !admitMetadata.service(value)))
|
|
114
|
+
rejectedServices++;
|
|
115
|
+
}
|
|
116
|
+
for (const value of [row.trackingUrl, row.trackingURL, row.trackingLink, row.publicTrackingUrl]) {
|
|
117
|
+
if (value != null && (typeof value !== "string" || !admitMetadata.url(value)))
|
|
118
|
+
rejectedTrackingUrls++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Admit and count original entries before the contributed extractor trims/deduplicates.
|
|
122
|
+
const admitted = admitTrackingNumber ? shipments.filter(row => {
|
|
123
|
+
if (admitTrackingNumber(readString(row.trackingNo) || readString(row.trackingNumber)))
|
|
124
|
+
return true;
|
|
125
|
+
rejectedTrackingRefs++;
|
|
126
|
+
return false;
|
|
127
|
+
}) : shipments;
|
|
128
|
+
const trackingReferences = trackingReferencesFrom({ shipments: admitted });
|
|
109
129
|
return {
|
|
110
130
|
provider: "omnipack",
|
|
111
131
|
event: readString(payload.event) || fallbackEvent,
|
|
132
|
+
...(rejectedTrackingRefs ? { rejectedTrackingRefs } : {}),
|
|
133
|
+
...(admitMetadata ? { rejectedServices, rejectedTrackingUrls } : {}),
|
|
112
134
|
providerOrderId: readNullableString(payload.orderId) ?? readNullableString(payload.providerOrderId),
|
|
113
135
|
orderNumber: readNullableString(payload.orderNumber),
|
|
114
136
|
fulfilmentNumber: readNullableString(payload.fulfilmentNumber),
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
export { reference } from "../dispatchProjection.js";
|
|
2
|
+
import type { CanonicalLocalFulfillmentStatus, FulfillmentObservation, FulfillmentObservationCodecs, FulfillmentRefusal } from "../contracts/index.js";
|
|
2
3
|
import type { OmnipackWebhookEvidence } from "./mappers.js";
|
|
3
4
|
import { type OmnipackWebhookRouteEvent } from "./omnipackWebhookPayload.js";
|
|
4
5
|
export declare const refusal: (reason: FulfillmentRefusal["reason"], phase: FulfillmentRefusal["phase"], retryable?: boolean) => FulfillmentRefusal;
|
|
5
6
|
export declare const digest: (bytes: string | Uint8Array) => string;
|
|
6
|
-
/**
|
|
7
|
-
export declare function
|
|
7
|
+
/** Share admission with the durable sink, including explicit deny allowlists. */
|
|
8
|
+
export declare function correlationReference(value: string | null | undefined, codecs?: OmnipackObservationCodecs): string | null;
|
|
9
|
+
export interface OmnipackObservationCodecs extends FulfillmentObservationCodecs {
|
|
10
|
+
normalizeCarrier?(input: {
|
|
11
|
+
carrierRef: string | null;
|
|
12
|
+
serviceRef: string | null;
|
|
13
|
+
}): string | null;
|
|
14
|
+
}
|
|
8
15
|
export declare function timestamp(value: string | null): string | null;
|
|
9
16
|
export declare function eventKind(value: string): OmnipackWebhookRouteEvent | null;
|
|
10
|
-
export declare function publicUrl(value: string | null, approve: (url: string) => boolean): string | null;
|
|
11
17
|
export declare function observation(input: {
|
|
12
18
|
evidence: OmnipackWebhookEvidence;
|
|
13
19
|
event: FulfillmentObservation["eventKind"];
|
|
@@ -18,4 +24,5 @@ export declare function observation(input: {
|
|
|
18
24
|
source: "provider_webhook" | "provider_reconciliation";
|
|
19
25
|
fulfillmentRef?: string;
|
|
20
26
|
approveTrackingUrl: (url: string) => boolean;
|
|
27
|
+
observationCodecs?: OmnipackObservationCodecs;
|
|
21
28
|
}): FulfillmentObservation;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { admittedReference, reference } from "../dispatchProjection.js";
|
|
2
|
+
import { projectObservation, publicTrackingUrl } from "../observationProjection.js";
|
|
3
|
+
export { reference } from "../dispatchProjection.js";
|
|
1
4
|
import { createHash } from "node:crypto";
|
|
2
5
|
import { parseNormalizedFulfillmentFact } from "@openlup/core/fulfillment";
|
|
3
|
-
import { EVENT_TO_STATUS } from "./omnipackWebhookPayload.js";
|
|
6
|
+
import { buildProviderEventId, EVENT_TO_STATUS } from "./omnipackWebhookPayload.js";
|
|
4
7
|
import { localStatusFor } from "./omnipackStatusVocabulary.js";
|
|
5
8
|
export const refusal = (reason, phase, retryable = false) => ({ reason, phase, retryable });
|
|
6
9
|
export const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
7
|
-
/**
|
|
8
|
-
export function
|
|
9
|
-
return
|
|
10
|
+
/** Share admission with the durable sink, including explicit deny allowlists. */
|
|
11
|
+
export function correlationReference(value, codecs = {}) {
|
|
12
|
+
return admittedReference(value, codecs.approveReference);
|
|
10
13
|
}
|
|
11
14
|
export function timestamp(value) {
|
|
12
15
|
return value && /^\d{4}-\d\d-\d\dT/u.test(value) && Number.isFinite(Date.parse(value)) ? new Date(value).toISOString() : null;
|
|
@@ -14,44 +17,56 @@ export function timestamp(value) {
|
|
|
14
17
|
export function eventKind(value) {
|
|
15
18
|
return Object.hasOwn(EVENT_TO_STATUS, value) ? value : null;
|
|
16
19
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
function tracking(refs, codecs, approve) {
|
|
21
|
+
let rejectedTrackingRefs = 0, rejectedServices = 0, rejectedTrackingUrls = 0;
|
|
22
|
+
const trackingRefs = [];
|
|
23
|
+
for (const item of refs) {
|
|
24
|
+
if (item.service !== null && !correlationReference(item.service, codecs))
|
|
25
|
+
rejectedServices++;
|
|
26
|
+
if (item.trackingUrl !== null && !publicTrackingUrl(item.trackingUrl, approve, codecs.trackingUrl))
|
|
27
|
+
rejectedTrackingUrls++;
|
|
28
|
+
const trackingNumber = correlationReference(item.trackingNumber, codecs);
|
|
29
|
+
if (!trackingNumber) {
|
|
30
|
+
rejectedTrackingRefs++;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Service admission precedes any derivation. A refused service cannot leak a prefix.
|
|
34
|
+
const serviceRef = correlationReference(item.service, codecs);
|
|
35
|
+
const carrierRef = item.service !== null && !serviceRef ? null : reference(codecs.normalizeCarrier
|
|
36
|
+
? codecs.normalizeCarrier({ carrierRef: item.carrier, serviceRef })
|
|
37
|
+
: serviceRef?.split("_")[0]?.toLowerCase());
|
|
38
|
+
trackingRefs.push({ trackingNumber, carrierRef, serviceRef: item.service, publicUrl: item.trackingUrl });
|
|
24
39
|
}
|
|
25
|
-
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function tracking(refs, approve) {
|
|
30
|
-
return refs.flatMap(item => {
|
|
31
|
-
const trackingNumber = reference(item.trackingNumber);
|
|
32
|
-
return trackingNumber ? [{ trackingNumber, carrierRef: reference(item.carrier), serviceRef: reference(item.service), publicUrl: publicUrl(item.trackingUrl, approve) }] : [];
|
|
33
|
-
});
|
|
40
|
+
return { trackingRefs, rejectedTrackingRefs, rejectedServices, rejectedTrackingUrls };
|
|
34
41
|
}
|
|
35
42
|
export function observation(input) {
|
|
36
43
|
const evidence = input.evidence;
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
44
|
+
const codecs = input.observationCodecs ?? {};
|
|
45
|
+
const externalRef = correlationReference(evidence.providerOrderId, codecs);
|
|
46
|
+
const providerFulfillmentRef = correlationReference(evidence.fulfilmentNumber, codecs);
|
|
47
|
+
const orderNumber = correlationReference(evidence.orderNumber, codecs);
|
|
40
48
|
const fulfillmentRef = reference(input.fulfillmentRef) ?? providerFulfillmentRef ?? externalRef ?? orderNumber;
|
|
41
|
-
if (!fulfillmentRef)
|
|
49
|
+
if (!fulfillmentRef || input.source === "provider_webhook" && [evidence.providerOrderId, evidence.orderNumber, evidence.fulfilmentNumber].some(value => value !== null && !correlationReference(value, codecs)))
|
|
42
50
|
throw refusal("missing_correlation", "normalize");
|
|
43
51
|
const occurredAt = timestamp(evidence.occurredAt);
|
|
44
52
|
if (evidence.occurredAt && !occurredAt)
|
|
45
53
|
throw refusal("malformed", "normalize");
|
|
46
|
-
|
|
54
|
+
// Compose the key with the provider's
|
|
55
|
+
// original bytes, including minute precision and offsets, rather than the UTC fact.
|
|
56
|
+
const key = buildProviderEventId(evidence);
|
|
57
|
+
const normalizedTracking = tracking(evidence.trackingReferences, codecs, input.approveTrackingUrl);
|
|
58
|
+
const rejectedTrackingRefs = normalizedTracking.rejectedTrackingRefs + (evidence.rejectedTrackingRefs ?? 0)
|
|
59
|
+
+ (evidence.rejectedServices ?? normalizedTracking.rejectedServices);
|
|
47
60
|
const status = input.status ?? localStatusFor(EVENT_TO_STATUS[input.event]);
|
|
48
61
|
const fact = parseNormalizedFulfillmentFact({
|
|
49
|
-
schemaVersion: 1, mapperVersion: "1", factId: key, providerKind: "omnipack", fulfillmentReference: fulfillmentRef,
|
|
62
|
+
schemaVersion: 1, mapperVersion: "1", factId: digest(key), providerKind: "omnipack", fulfillmentReference: reference(fulfillmentRef) ?? digest(fulfillmentRef),
|
|
50
63
|
canonicalStatus: status, provenance: { source: input.source, authority: "provider_asserted" },
|
|
51
64
|
occurredAt, receivedAt: input.receivedAt,
|
|
52
65
|
rawEvent: { reference: input.evidenceRef, digestAlgorithm: "sha256", digest: input.rawDigest },
|
|
53
66
|
}, { isCanonicalStatus: (value) => ["provider_received", "picking", "packed", "in_transit", "delivered", "exception", "cancelled"].includes(value) });
|
|
54
|
-
return { fact, eventKind: input.event, eventKey: key, firstReceivedAt: input.receivedAt,
|
|
55
|
-
correlation: { orderRef: null, fulfillmentRef: reference(input.fulfillmentRef), externalRef, orderNumber, providerFulfillmentRef, parcelRef: null },
|
|
56
|
-
trackingRefs:
|
|
67
|
+
return projectObservation({ fact, eventKind: input.event, eventKey: key, firstReceivedAt: input.receivedAt,
|
|
68
|
+
correlation: { orderRef: null, fulfillmentRef: reference(input.fulfillmentRef), externalRef: evidence.providerOrderId, orderNumber: evidence.orderNumber, providerFulfillmentRef: evidence.fulfilmentNumber, parcelRef: null },
|
|
69
|
+
trackingRefs: normalizedTracking.trackingRefs, diagnostics: { admission: {
|
|
70
|
+
rejectedTrackingRefs, rejectedTrackingUrls: evidence.rejectedTrackingUrls ?? normalizedTracking.rejectedTrackingUrls,
|
|
71
|
+
} } }, input.approveTrackingUrl, codecs);
|
|
57
72
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FulfillmentObservationPagePort, FulfillmentReconciliationObservation } from "../contracts/index.js";
|
|
2
|
+
import type { OmnipackClient } from "./client.js";
|
|
3
|
+
import type { OmnipackFulfilmentEvidence } from "./mappers.js";
|
|
4
|
+
import { refusal, type OmnipackObservationCodecs } from "./observation.js";
|
|
5
|
+
type Config = {
|
|
6
|
+
observationCodecs?: OmnipackObservationCodecs;
|
|
7
|
+
clock(): string;
|
|
8
|
+
mintEvidenceReference(): `evidence:${string}`;
|
|
9
|
+
approveTrackingUrl(url: string): boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function reconciliationObservation(row: OmnipackFulfilmentEvidence, config: Config, fulfillmentRef?: string): Extract<FulfillmentReconciliationObservation, {
|
|
12
|
+
kind: "found" | "unmapped";
|
|
13
|
+
}>;
|
|
14
|
+
export declare function createReconciliationPagePort(client: OmnipackClient, config: Config, ready: () => ReturnType<typeof refusal> | null): FulfillmentObservationPagePort;
|
|
15
|
+
export {};
|