@open-mercato/carrier-inpost 0.10.1-develop.8072.1.592356afda → 0.10.1-develop.8098.1.f4decc8a57
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/carrier_inpost/__integration__/TC-INPOST-005.spec.js +117 -0
- package/dist/modules/carrier_inpost/__integration__/TC-INPOST-005.spec.js.map +7 -0
- package/dist/modules/carrier_inpost/lib/status-map.js +2 -2
- package/dist/modules/carrier_inpost/lib/status-map.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/carrier_inpost/__integration__/TC-INPOST-005.spec.ts +155 -0
- package/src/modules/carrier_inpost/__tests__/status-map.test.ts +21 -1
- package/src/modules/carrier_inpost/lib/status-map.ts +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:carrier-inpost] found
|
|
1
|
+
[build:carrier-inpost] found 26 entry points
|
|
2
2
|
[build:carrier-inpost] built successfully
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createHmac, randomUUID } from "node:crypto";
|
|
2
|
+
import { expect, test } from "@playwright/test";
|
|
3
|
+
import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
|
|
4
|
+
import {
|
|
5
|
+
createOrganizationInDb,
|
|
6
|
+
deleteIntegrationCredentialsInDb,
|
|
7
|
+
deleteOrganizationInDb,
|
|
8
|
+
withClient
|
|
9
|
+
} from "@open-mercato/core/helpers/integration/dbFixtures";
|
|
10
|
+
import { getTokenScope } from "@open-mercato/core/helpers/integration/generalFixtures";
|
|
11
|
+
import { drainIntegrationQueue } from "@open-mercato/core/helpers/integration/queue";
|
|
12
|
+
const WEBHOOK_PATH = "/api/shipping-carriers/webhook/inpost";
|
|
13
|
+
const stamp = `${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
14
|
+
const webhookSecret = `tcinpost005_${stamp}`;
|
|
15
|
+
async function seedShipment(scope, carrierShipmentId, status) {
|
|
16
|
+
const id = randomUUID();
|
|
17
|
+
await withClient(async (client) => {
|
|
18
|
+
await client.query(
|
|
19
|
+
`insert into carrier_shipments
|
|
20
|
+
(id, order_id, provider_key, carrier_shipment_id, tracking_number, unified_status,
|
|
21
|
+
organization_id, tenant_id, created_at, updated_at)
|
|
22
|
+
values ($1, $2, 'inpost', $3, $3, $4, $5, $6, now(), now())`,
|
|
23
|
+
[id, randomUUID(), carrierShipmentId, status, scope.organizationId, scope.tenantId]
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
return id;
|
|
27
|
+
}
|
|
28
|
+
async function readShipment(id) {
|
|
29
|
+
return withClient(async (client) => {
|
|
30
|
+
const result = await client.query(
|
|
31
|
+
"select unified_status, carrier_status from carrier_shipments where id = $1",
|
|
32
|
+
[id]
|
|
33
|
+
);
|
|
34
|
+
return result.rows[0] ?? null;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function isWebhookProcessed(idempotencyKey) {
|
|
38
|
+
return withClient(async (client) => {
|
|
39
|
+
const result = await client.query(
|
|
40
|
+
"select 1 from carrier_webhook_events where provider_key = 'inpost' and idempotency_key = $1",
|
|
41
|
+
[idempotencyKey]
|
|
42
|
+
);
|
|
43
|
+
return (result.rowCount ?? 0) > 0;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function deliverStatus(request, carrierShipmentId, status) {
|
|
47
|
+
const eventTs = (/* @__PURE__ */ new Date()).toISOString();
|
|
48
|
+
const body = JSON.stringify({
|
|
49
|
+
event: "shipment_status_changed",
|
|
50
|
+
event_ts: eventTs,
|
|
51
|
+
organization_id: 1,
|
|
52
|
+
payload: { shipment_id: carrierShipmentId, status, tracking_number: carrierShipmentId }
|
|
53
|
+
});
|
|
54
|
+
const idempotencyKey = [carrierShipmentId, "shipment_status_changed", status, eventTs].join(":");
|
|
55
|
+
const response = await request.post(WEBHOOK_PATH, {
|
|
56
|
+
data: body,
|
|
57
|
+
headers: {
|
|
58
|
+
"content-type": "application/json",
|
|
59
|
+
"x-inpost-signature": createHmac("sha256", webhookSecret).update(body).digest("hex")
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
expect(response.status(), `webhook ${status} should be accepted`).toBe(202);
|
|
63
|
+
await drainIntegrationQueue("shipping-carriers-webhook");
|
|
64
|
+
await expect.poll(() => isWebhookProcessed(idempotencyKey), {
|
|
65
|
+
message: `the ${status} webhook should be processed by the worker`,
|
|
66
|
+
timeout: 3e4
|
|
67
|
+
}).toBe(true);
|
|
68
|
+
}
|
|
69
|
+
test.describe('TC-INPOST-005: InPost "claimed" is a failed delivery, not a cancellation', () => {
|
|
70
|
+
test("keeps a complained parcel trackable until InPost returns it to the sender", async ({ request }) => {
|
|
71
|
+
test.setTimeout(12e4);
|
|
72
|
+
const adminScope = getTokenScope(await getAuthToken(request, "admin"));
|
|
73
|
+
const tenantId = adminScope.tenantId;
|
|
74
|
+
let organizationId = null;
|
|
75
|
+
const shipmentIds = [];
|
|
76
|
+
try {
|
|
77
|
+
organizationId = await createOrganizationInDb({ name: `TC-INPOST-005 ${stamp}`, tenantId });
|
|
78
|
+
const scope = { organizationId, tenantId };
|
|
79
|
+
const superadminToken = await getAuthToken(request, "superadmin");
|
|
80
|
+
const saved = await apiRequest(request, "PUT", "/api/integrations/carrier_inpost/credentials", {
|
|
81
|
+
token: superadminToken,
|
|
82
|
+
headers: {
|
|
83
|
+
Cookie: `om_selected_tenant=${encodeURIComponent(tenantId)}; om_selected_org=${encodeURIComponent(organizationId)}`
|
|
84
|
+
},
|
|
85
|
+
data: { credentials: { apiToken: `tcinpost005_token_${stamp}`, organizationId: "1", webhookSecret } }
|
|
86
|
+
});
|
|
87
|
+
expect(saved.status(), await saved.text()).toBe(200);
|
|
88
|
+
const awaitingPickup = `tcinpost005a${stamp}`;
|
|
89
|
+
const awaitingPickupId = await seedShipment(scope, awaitingPickup, "out_for_delivery");
|
|
90
|
+
shipmentIds.push(awaitingPickupId);
|
|
91
|
+
const pickupExpired = `tcinpost005b${stamp}`;
|
|
92
|
+
const pickupExpiredId = await seedShipment(scope, pickupExpired, "failed_delivery");
|
|
93
|
+
shipmentIds.push(pickupExpiredId);
|
|
94
|
+
await deliverStatus(request, awaitingPickup, "claimed");
|
|
95
|
+
await expect.poll(async () => readShipment(awaitingPickupId), { timeout: 1e4 }).toEqual({ unified_status: "failed_delivery", carrier_status: "claimed" });
|
|
96
|
+
await deliverStatus(request, pickupExpired, "claimed");
|
|
97
|
+
expect(await readShipment(pickupExpiredId)).toEqual({ unified_status: "failed_delivery", carrier_status: null });
|
|
98
|
+
await deliverStatus(request, pickupExpired, "returned_to_sender");
|
|
99
|
+
await expect.poll(async () => (await readShipment(pickupExpiredId))?.unified_status, {
|
|
100
|
+
message: "the return after a complaint must still be applied",
|
|
101
|
+
timeout: 1e4
|
|
102
|
+
}).toBe("returned");
|
|
103
|
+
} finally {
|
|
104
|
+
await withClient(async (client) => {
|
|
105
|
+
if (organizationId) {
|
|
106
|
+
await client.query("delete from carrier_webhook_events where organization_id = $1", [organizationId]);
|
|
107
|
+
}
|
|
108
|
+
if (shipmentIds.length > 0) {
|
|
109
|
+
await client.query("delete from carrier_shipments where id = any($1::uuid[])", [shipmentIds]);
|
|
110
|
+
}
|
|
111
|
+
}).catch(() => void 0);
|
|
112
|
+
await deleteIntegrationCredentialsInDb(organizationId).catch(() => void 0);
|
|
113
|
+
await deleteOrganizationInDb(organizationId).catch(() => void 0);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
//# sourceMappingURL=TC-INPOST-005.spec.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/carrier_inpost/__integration__/TC-INPOST-005.spec.ts"],
|
|
4
|
+
"sourcesContent": ["import { createHmac, randomUUID } from 'node:crypto'\nimport { expect, test, type APIRequestContext } from '@playwright/test'\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'\nimport {\n createOrganizationInDb,\n deleteIntegrationCredentialsInDb,\n deleteOrganizationInDb,\n withClient,\n} from '@open-mercato/core/helpers/integration/dbFixtures'\nimport { getTokenScope } from '@open-mercato/core/helpers/integration/generalFixtures'\nimport { drainIntegrationQueue } from '@open-mercato/core/helpers/integration/queue'\n\n/**\n * TC-INPOST-005: An InPost parcel complaint (\"claimed\") does not cancel the shipment\n *\n * InPost reports `claimed` when the recipient files a complaint at the parcel machine. That is a\n * failed delivery, not a cancelled label: the shipment must stay non-terminal so the carrier's\n * later outcome (here `returned_to_sender`) is still applied.\n *\n * Shipments are seeded with SQL in a throwaway organization (creating a real InPost shipment needs\n * an InPost account). Signed webhooks are POSTed to the real `/api/shipping-carriers/webhook/inpost`\n * route; verification is local HMAC with the organization's saved webhook secret.\n *\n * ENVIRONMENT: API + raw `pg` against `DATABASE_URL`; run under a coherent app+DB stack\n * (`yarn test:integration` / `yarn test:integration:ephemeral`).\n */\n\nconst WEBHOOK_PATH = '/api/shipping-carriers/webhook/inpost'\nconst stamp = `${Date.now()}_${randomUUID().slice(0, 8)}`\nconst webhookSecret = `tcinpost005_${stamp}`\n\ntype Scope = { organizationId: string; tenantId: string }\ntype ShipmentRow = { unified_status: string; carrier_status: string | null }\n\nasync function seedShipment(scope: Scope, carrierShipmentId: string, status: string): Promise<string> {\n const id = randomUUID()\n await withClient(async (client) => {\n await client.query(\n `insert into carrier_shipments\n (id, order_id, provider_key, carrier_shipment_id, tracking_number, unified_status,\n organization_id, tenant_id, created_at, updated_at)\n values ($1, $2, 'inpost', $3, $3, $4, $5, $6, now(), now())`,\n [id, randomUUID(), carrierShipmentId, status, scope.organizationId, scope.tenantId],\n )\n })\n return id\n}\n\nasync function readShipment(id: string): Promise<ShipmentRow | null> {\n return withClient(async (client) => {\n const result = await client.query<ShipmentRow>(\n 'select unified_status, carrier_status from carrier_shipments where id = $1',\n [id],\n )\n return result.rows[0] ?? null\n })\n}\n\nasync function isWebhookProcessed(idempotencyKey: string): Promise<boolean> {\n return withClient(async (client) => {\n const result = await client.query(\n \"select 1 from carrier_webhook_events where provider_key = 'inpost' and idempotency_key = $1\",\n [idempotencyKey],\n )\n return (result.rowCount ?? 0) > 0\n })\n}\n\nasync function deliverStatus(\n request: APIRequestContext,\n carrierShipmentId: string,\n status: string,\n): Promise<void> {\n const eventTs = new Date().toISOString()\n const body = JSON.stringify({\n event: 'shipment_status_changed',\n event_ts: eventTs,\n organization_id: 1,\n payload: { shipment_id: carrierShipmentId, status, tracking_number: carrierShipmentId },\n })\n const idempotencyKey = [carrierShipmentId, 'shipment_status_changed', status, eventTs].join(':')\n const response = await request.post(WEBHOOK_PATH, {\n data: body,\n headers: {\n 'content-type': 'application/json',\n 'x-inpost-signature': createHmac('sha256', webhookSecret).update(body).digest('hex'),\n },\n })\n expect(response.status(), `webhook ${status} should be accepted`).toBe(202)\n await drainIntegrationQueue('shipping-carriers-webhook')\n await expect\n .poll(() => isWebhookProcessed(idempotencyKey), {\n message: `the ${status} webhook should be processed by the worker`,\n timeout: 30_000,\n })\n .toBe(true)\n}\n\ntest.describe('TC-INPOST-005: InPost \"claimed\" is a failed delivery, not a cancellation', () => {\n test('keeps a complained parcel trackable until InPost returns it to the sender', async ({ request }) => {\n test.setTimeout(120_000)\n const adminScope = getTokenScope(await getAuthToken(request, 'admin'))\n const tenantId = adminScope.tenantId\n let organizationId: string | null = null\n const shipmentIds: string[] = []\n\n try {\n organizationId = await createOrganizationInDb({ name: `TC-INPOST-005 ${stamp}`, tenantId })\n const scope: Scope = { organizationId, tenantId }\n const superadminToken = await getAuthToken(request, 'superadmin')\n const saved = await apiRequest(request, 'PUT', '/api/integrations/carrier_inpost/credentials', {\n token: superadminToken,\n headers: {\n Cookie: `om_selected_tenant=${encodeURIComponent(tenantId)}; om_selected_org=${encodeURIComponent(organizationId)}`,\n },\n data: { credentials: { apiToken: `tcinpost005_token_${stamp}`, organizationId: '1', webhookSecret } },\n })\n expect(saved.status(), await saved.text()).toBe(200)\n\n const awaitingPickup = `tcinpost005a${stamp}`\n const awaitingPickupId = await seedShipment(scope, awaitingPickup, 'out_for_delivery')\n shipmentIds.push(awaitingPickupId)\n const pickupExpired = `tcinpost005b${stamp}`\n const pickupExpiredId = await seedShipment(scope, pickupExpired, 'failed_delivery')\n shipmentIds.push(pickupExpiredId)\n\n await deliverStatus(request, awaitingPickup, 'claimed')\n await expect\n .poll(async () => readShipment(awaitingPickupId), { timeout: 10_000 })\n .toEqual({ unified_status: 'failed_delivery', carrier_status: 'claimed' })\n\n await deliverStatus(request, pickupExpired, 'claimed')\n expect(await readShipment(pickupExpiredId)).toEqual({ unified_status: 'failed_delivery', carrier_status: null })\n\n await deliverStatus(request, pickupExpired, 'returned_to_sender')\n await expect\n .poll(async () => (await readShipment(pickupExpiredId))?.unified_status, {\n message: 'the return after a complaint must still be applied',\n timeout: 10_000,\n })\n .toBe('returned')\n } finally {\n await withClient(async (client) => {\n if (organizationId) {\n await client.query('delete from carrier_webhook_events where organization_id = $1', [organizationId])\n }\n if (shipmentIds.length > 0) {\n await client.query('delete from carrier_shipments where id = any($1::uuid[])', [shipmentIds])\n }\n }).catch(() => undefined)\n await deleteIntegrationCredentialsInDb(organizationId).catch(() => undefined)\n await deleteOrganizationInDb(organizationId).catch(() => undefined)\n }\n })\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -44,11 +44,11 @@ const INPOST_STATUS_MAP = {
|
|
|
44
44
|
undelivered_wrong_address: "failed_delivery",
|
|
45
45
|
undelivered_cod_cash_receiver: "failed_delivery",
|
|
46
46
|
rejected_by_receiver: "failed_delivery",
|
|
47
|
+
claimed: "failed_delivery",
|
|
47
48
|
// Returned
|
|
48
49
|
returned_to_sender: "returned",
|
|
49
50
|
// Cancelled
|
|
50
|
-
canceled: "cancelled"
|
|
51
|
-
claimed: "cancelled"
|
|
51
|
+
canceled: "cancelled"
|
|
52
52
|
};
|
|
53
53
|
function mapInpostStatus(inpostStatus) {
|
|
54
54
|
return INPOST_STATUS_MAP[inpostStatus] ?? "unknown";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/carrier_inpost/lib/status-map.ts"],
|
|
4
|
-
"sourcesContent": ["import type { UnifiedShipmentStatus } from '@open-mercato/core/modules/shipping_carriers/lib/adapter'\n\nconst INPOST_STATUS_MAP: Record<string, UnifiedShipmentStatus> = {\n // Label / offer preparation\n created: 'label_created',\n offers_prepared: 'label_created',\n offer_selected: 'label_created',\n confirmed: 'label_created',\n\n // Picked up from sender\n dispatched_by_sender: 'picked_up',\n dispatched_by_sender_to_pok: 'picked_up',\n collected_from_sender: 'picked_up',\n taken_by_courier_from_customer_service_point: 'picked_up',\n taken_by_courier_from_pok: 'picked_up',\n\n // In transit / sorting\n taken_by_courier: 'in_transit',\n adopted_at_source_branch: 'in_transit',\n sent_from_source_branch: 'in_transit',\n adopted_at_sorting_center: 'in_transit',\n oversized: 'in_transit',\n readdressed: 'in_transit',\n delay_in_delivery: 'in_transit',\n unstack_from_customer_service_point: 'in_transit',\n unstack_from_box_machine: 'in_transit',\n redirect_to_box: 'in_transit',\n canceled_redirect_to_box: 'in_transit',\n\n // Out for delivery / awaiting pickup\n out_for_delivery: 'out_for_delivery',\n out_for_delivery_to_address: 'out_for_delivery',\n ready_to_pickup: 'out_for_delivery',\n ready_to_pickup_from_pok: 'out_for_delivery',\n ready_to_pickup_from_branch: 'out_for_delivery',\n stack_in_box_machine: 'out_for_delivery',\n stack_in_customer_service_point: 'out_for_delivery',\n pickup_reminder_sent: 'out_for_delivery',\n pickup_reminder_sent_address: 'out_for_delivery',\n courier_avizo_in_customer_service_point: 'out_for_delivery',\n\n // Delivered\n delivered: 'delivered',\n\n // Failed delivery\n pickup_time_expired: 'failed_delivery',\n stack_parcel_pickup_time_expired: 'failed_delivery',\n stack_parcel_in_box_machine_pickup_time_expired: 'failed_delivery',\n avizo: 'failed_delivery',\n undelivered: 'failed_delivery',\n undelivered_wrong_address: 'failed_delivery',\n undelivered_cod_cash_receiver: 'failed_delivery',\n rejected_by_receiver: 'failed_delivery',\n\n // Returned\n returned_to_sender: 'returned',\n\n // Cancelled\n canceled: 'cancelled',\n
|
|
5
|
-
"mappings": "AAEA,MAAM,oBAA2D;AAAA;AAAA,EAE/D,SAAS;AAAA,EACT,iBAAiB;AAAA,EACjB,gBAAgB;AAAA,EAChB,WAAW;AAAA;AAAA,EAGX,sBAAsB;AAAA,EACtB,6BAA6B;AAAA,EAC7B,uBAAuB;AAAA,EACvB,8CAA8C;AAAA,EAC9C,2BAA2B;AAAA;AAAA,EAG3B,kBAAkB;AAAA,EAClB,0BAA0B;AAAA,EAC1B,yBAAyB;AAAA,EACzB,2BAA2B;AAAA,EAC3B,WAAW;AAAA,EACX,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,qCAAqC;AAAA,EACrC,0BAA0B;AAAA,EAC1B,iBAAiB;AAAA,EACjB,0BAA0B;AAAA;AAAA,EAG1B,kBAAkB;AAAA,EAClB,6BAA6B;AAAA,EAC7B,iBAAiB;AAAA,EACjB,0BAA0B;AAAA,EAC1B,6BAA6B;AAAA,EAC7B,sBAAsB;AAAA,EACtB,iCAAiC;AAAA,EACjC,sBAAsB;AAAA,EACtB,8BAA8B;AAAA,EAC9B,yCAAyC;AAAA;AAAA,EAGzC,WAAW;AAAA;AAAA,EAGX,qBAAqB;AAAA,EACrB,kCAAkC;AAAA,EAClC,iDAAiD;AAAA,EACjD,OAAO;AAAA,EACP,aAAa;AAAA,EACb,2BAA2B;AAAA,EAC3B,+BAA+B;AAAA,EAC/B,sBAAsB;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["import type { UnifiedShipmentStatus } from '@open-mercato/core/modules/shipping_carriers/lib/adapter'\n\nconst INPOST_STATUS_MAP: Record<string, UnifiedShipmentStatus> = {\n // Label / offer preparation\n created: 'label_created',\n offers_prepared: 'label_created',\n offer_selected: 'label_created',\n confirmed: 'label_created',\n\n // Picked up from sender\n dispatched_by_sender: 'picked_up',\n dispatched_by_sender_to_pok: 'picked_up',\n collected_from_sender: 'picked_up',\n taken_by_courier_from_customer_service_point: 'picked_up',\n taken_by_courier_from_pok: 'picked_up',\n\n // In transit / sorting\n taken_by_courier: 'in_transit',\n adopted_at_source_branch: 'in_transit',\n sent_from_source_branch: 'in_transit',\n adopted_at_sorting_center: 'in_transit',\n oversized: 'in_transit',\n readdressed: 'in_transit',\n delay_in_delivery: 'in_transit',\n unstack_from_customer_service_point: 'in_transit',\n unstack_from_box_machine: 'in_transit',\n redirect_to_box: 'in_transit',\n canceled_redirect_to_box: 'in_transit',\n\n // Out for delivery / awaiting pickup\n out_for_delivery: 'out_for_delivery',\n out_for_delivery_to_address: 'out_for_delivery',\n ready_to_pickup: 'out_for_delivery',\n ready_to_pickup_from_pok: 'out_for_delivery',\n ready_to_pickup_from_branch: 'out_for_delivery',\n stack_in_box_machine: 'out_for_delivery',\n stack_in_customer_service_point: 'out_for_delivery',\n pickup_reminder_sent: 'out_for_delivery',\n pickup_reminder_sent_address: 'out_for_delivery',\n courier_avizo_in_customer_service_point: 'out_for_delivery',\n\n // Delivered\n delivered: 'delivered',\n\n // Failed delivery\n pickup_time_expired: 'failed_delivery',\n stack_parcel_pickup_time_expired: 'failed_delivery',\n stack_parcel_in_box_machine_pickup_time_expired: 'failed_delivery',\n avizo: 'failed_delivery',\n undelivered: 'failed_delivery',\n undelivered_wrong_address: 'failed_delivery',\n undelivered_cod_cash_receiver: 'failed_delivery',\n rejected_by_receiver: 'failed_delivery',\n claimed: 'failed_delivery',\n\n // Returned\n returned_to_sender: 'returned',\n\n // Cancelled\n canceled: 'cancelled',\n}\n\nexport function mapInpostStatus(inpostStatus: string): UnifiedShipmentStatus {\n return INPOST_STATUS_MAP[inpostStatus] ?? 'unknown'\n}\n\nexport const SERVICE_CODE_MAP: Record<string, string> = {\n locker_standard: 'inpost_locker_standard',\n courier_standard: 'inpost_courier_standard',\n courier_c2c: 'inpost_courier_c2c',\n}\n\n// Locker services use template-based parcel sizing (small/medium/large).\n// courier_c2c also uses templates (small/medium/large/xlarge) but is NOT\n// included here because isLockerService is used to add locker-specific\n// API fields (e.g. target_point, receiver email) \u2014 courier_c2c does not need those.\nconst LOCKER_SERVICE_CODES = new Set([\n 'locker_standard',\n 'inpost_locker_standard',\n 'inpost_locker_allegro',\n 'inpost_locker_pass_thru',\n 'inpost_locker_standard_smart',\n 'inpost_locker_allegro_smart',\n])\n\nexport function isLockerService(serviceCode: string): boolean {\n return LOCKER_SERVICE_CODES.has(serviceCode)\n}\n\nexport function mapServiceCodeToInpost(serviceCode: string): string {\n return SERVICE_CODE_MAP[serviceCode] ?? serviceCode\n}\n"],
|
|
5
|
+
"mappings": "AAEA,MAAM,oBAA2D;AAAA;AAAA,EAE/D,SAAS;AAAA,EACT,iBAAiB;AAAA,EACjB,gBAAgB;AAAA,EAChB,WAAW;AAAA;AAAA,EAGX,sBAAsB;AAAA,EACtB,6BAA6B;AAAA,EAC7B,uBAAuB;AAAA,EACvB,8CAA8C;AAAA,EAC9C,2BAA2B;AAAA;AAAA,EAG3B,kBAAkB;AAAA,EAClB,0BAA0B;AAAA,EAC1B,yBAAyB;AAAA,EACzB,2BAA2B;AAAA,EAC3B,WAAW;AAAA,EACX,aAAa;AAAA,EACb,mBAAmB;AAAA,EACnB,qCAAqC;AAAA,EACrC,0BAA0B;AAAA,EAC1B,iBAAiB;AAAA,EACjB,0BAA0B;AAAA;AAAA,EAG1B,kBAAkB;AAAA,EAClB,6BAA6B;AAAA,EAC7B,iBAAiB;AAAA,EACjB,0BAA0B;AAAA,EAC1B,6BAA6B;AAAA,EAC7B,sBAAsB;AAAA,EACtB,iCAAiC;AAAA,EACjC,sBAAsB;AAAA,EACtB,8BAA8B;AAAA,EAC9B,yCAAyC;AAAA;AAAA,EAGzC,WAAW;AAAA;AAAA,EAGX,qBAAqB;AAAA,EACrB,kCAAkC;AAAA,EAClC,iDAAiD;AAAA,EACjD,OAAO;AAAA,EACP,aAAa;AAAA,EACb,2BAA2B;AAAA,EAC3B,+BAA+B;AAAA,EAC/B,sBAAsB;AAAA,EACtB,SAAS;AAAA;AAAA,EAGT,oBAAoB;AAAA;AAAA,EAGpB,UAAU;AACZ;AAEO,SAAS,gBAAgB,cAA6C;AAC3E,SAAO,kBAAkB,YAAY,KAAK;AAC5C;AAEO,MAAM,mBAA2C;AAAA,EACtD,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,aAAa;AACf;AAMA,MAAM,uBAAuB,oBAAI,IAAI;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,SAAS,gBAAgB,aAA8B;AAC5D,SAAO,qBAAqB,IAAI,WAAW;AAC7C;AAEO,SAAS,uBAAuB,aAA6B;AAClE,SAAO,iBAAiB,WAAW,KAAK;AAC1C;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/carrier-inpost",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.8098.1.f4decc8a57",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -62,21 +62,21 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@open-mercato/core": "0.10.1-develop.
|
|
66
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
65
|
+
"@open-mercato/core": "0.10.1-develop.8098.1.f4decc8a57",
|
|
66
|
+
"@open-mercato/ui": "0.10.1-develop.8098.1.f4decc8a57",
|
|
67
67
|
"ts-pattern": "^5.0.0"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@mikro-orm/postgresql": "^7.0.14",
|
|
71
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
72
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
71
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.8098.1.f4decc8a57",
|
|
72
|
+
"@open-mercato/shared": "0.10.1-develop.8098.1.f4decc8a57",
|
|
73
73
|
"react": "^19.0.0",
|
|
74
74
|
"react-dom": "^19.0.0",
|
|
75
75
|
"react-is": ">=18.0.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
79
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
78
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.8098.1.f4decc8a57",
|
|
79
|
+
"@open-mercato/shared": "0.10.1-develop.8098.1.f4decc8a57",
|
|
80
80
|
"@types/chance": "^1.1.8",
|
|
81
81
|
"@types/jest": "^30.0.0",
|
|
82
82
|
"@types/react": "^19.2.18",
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createHmac, randomUUID } from 'node:crypto'
|
|
2
|
+
import { expect, test, type APIRequestContext } from '@playwright/test'
|
|
3
|
+
import { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api'
|
|
4
|
+
import {
|
|
5
|
+
createOrganizationInDb,
|
|
6
|
+
deleteIntegrationCredentialsInDb,
|
|
7
|
+
deleteOrganizationInDb,
|
|
8
|
+
withClient,
|
|
9
|
+
} from '@open-mercato/core/helpers/integration/dbFixtures'
|
|
10
|
+
import { getTokenScope } from '@open-mercato/core/helpers/integration/generalFixtures'
|
|
11
|
+
import { drainIntegrationQueue } from '@open-mercato/core/helpers/integration/queue'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* TC-INPOST-005: An InPost parcel complaint ("claimed") does not cancel the shipment
|
|
15
|
+
*
|
|
16
|
+
* InPost reports `claimed` when the recipient files a complaint at the parcel machine. That is a
|
|
17
|
+
* failed delivery, not a cancelled label: the shipment must stay non-terminal so the carrier's
|
|
18
|
+
* later outcome (here `returned_to_sender`) is still applied.
|
|
19
|
+
*
|
|
20
|
+
* Shipments are seeded with SQL in a throwaway organization (creating a real InPost shipment needs
|
|
21
|
+
* an InPost account). Signed webhooks are POSTed to the real `/api/shipping-carriers/webhook/inpost`
|
|
22
|
+
* route; verification is local HMAC with the organization's saved webhook secret.
|
|
23
|
+
*
|
|
24
|
+
* ENVIRONMENT: API + raw `pg` against `DATABASE_URL`; run under a coherent app+DB stack
|
|
25
|
+
* (`yarn test:integration` / `yarn test:integration:ephemeral`).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const WEBHOOK_PATH = '/api/shipping-carriers/webhook/inpost'
|
|
29
|
+
const stamp = `${Date.now()}_${randomUUID().slice(0, 8)}`
|
|
30
|
+
const webhookSecret = `tcinpost005_${stamp}`
|
|
31
|
+
|
|
32
|
+
type Scope = { organizationId: string; tenantId: string }
|
|
33
|
+
type ShipmentRow = { unified_status: string; carrier_status: string | null }
|
|
34
|
+
|
|
35
|
+
async function seedShipment(scope: Scope, carrierShipmentId: string, status: string): Promise<string> {
|
|
36
|
+
const id = randomUUID()
|
|
37
|
+
await withClient(async (client) => {
|
|
38
|
+
await client.query(
|
|
39
|
+
`insert into carrier_shipments
|
|
40
|
+
(id, order_id, provider_key, carrier_shipment_id, tracking_number, unified_status,
|
|
41
|
+
organization_id, tenant_id, created_at, updated_at)
|
|
42
|
+
values ($1, $2, 'inpost', $3, $3, $4, $5, $6, now(), now())`,
|
|
43
|
+
[id, randomUUID(), carrierShipmentId, status, scope.organizationId, scope.tenantId],
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
return id
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function readShipment(id: string): Promise<ShipmentRow | null> {
|
|
50
|
+
return withClient(async (client) => {
|
|
51
|
+
const result = await client.query<ShipmentRow>(
|
|
52
|
+
'select unified_status, carrier_status from carrier_shipments where id = $1',
|
|
53
|
+
[id],
|
|
54
|
+
)
|
|
55
|
+
return result.rows[0] ?? null
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function isWebhookProcessed(idempotencyKey: string): Promise<boolean> {
|
|
60
|
+
return withClient(async (client) => {
|
|
61
|
+
const result = await client.query(
|
|
62
|
+
"select 1 from carrier_webhook_events where provider_key = 'inpost' and idempotency_key = $1",
|
|
63
|
+
[idempotencyKey],
|
|
64
|
+
)
|
|
65
|
+
return (result.rowCount ?? 0) > 0
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function deliverStatus(
|
|
70
|
+
request: APIRequestContext,
|
|
71
|
+
carrierShipmentId: string,
|
|
72
|
+
status: string,
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
const eventTs = new Date().toISOString()
|
|
75
|
+
const body = JSON.stringify({
|
|
76
|
+
event: 'shipment_status_changed',
|
|
77
|
+
event_ts: eventTs,
|
|
78
|
+
organization_id: 1,
|
|
79
|
+
payload: { shipment_id: carrierShipmentId, status, tracking_number: carrierShipmentId },
|
|
80
|
+
})
|
|
81
|
+
const idempotencyKey = [carrierShipmentId, 'shipment_status_changed', status, eventTs].join(':')
|
|
82
|
+
const response = await request.post(WEBHOOK_PATH, {
|
|
83
|
+
data: body,
|
|
84
|
+
headers: {
|
|
85
|
+
'content-type': 'application/json',
|
|
86
|
+
'x-inpost-signature': createHmac('sha256', webhookSecret).update(body).digest('hex'),
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
expect(response.status(), `webhook ${status} should be accepted`).toBe(202)
|
|
90
|
+
await drainIntegrationQueue('shipping-carriers-webhook')
|
|
91
|
+
await expect
|
|
92
|
+
.poll(() => isWebhookProcessed(idempotencyKey), {
|
|
93
|
+
message: `the ${status} webhook should be processed by the worker`,
|
|
94
|
+
timeout: 30_000,
|
|
95
|
+
})
|
|
96
|
+
.toBe(true)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
test.describe('TC-INPOST-005: InPost "claimed" is a failed delivery, not a cancellation', () => {
|
|
100
|
+
test('keeps a complained parcel trackable until InPost returns it to the sender', async ({ request }) => {
|
|
101
|
+
test.setTimeout(120_000)
|
|
102
|
+
const adminScope = getTokenScope(await getAuthToken(request, 'admin'))
|
|
103
|
+
const tenantId = adminScope.tenantId
|
|
104
|
+
let organizationId: string | null = null
|
|
105
|
+
const shipmentIds: string[] = []
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
organizationId = await createOrganizationInDb({ name: `TC-INPOST-005 ${stamp}`, tenantId })
|
|
109
|
+
const scope: Scope = { organizationId, tenantId }
|
|
110
|
+
const superadminToken = await getAuthToken(request, 'superadmin')
|
|
111
|
+
const saved = await apiRequest(request, 'PUT', '/api/integrations/carrier_inpost/credentials', {
|
|
112
|
+
token: superadminToken,
|
|
113
|
+
headers: {
|
|
114
|
+
Cookie: `om_selected_tenant=${encodeURIComponent(tenantId)}; om_selected_org=${encodeURIComponent(organizationId)}`,
|
|
115
|
+
},
|
|
116
|
+
data: { credentials: { apiToken: `tcinpost005_token_${stamp}`, organizationId: '1', webhookSecret } },
|
|
117
|
+
})
|
|
118
|
+
expect(saved.status(), await saved.text()).toBe(200)
|
|
119
|
+
|
|
120
|
+
const awaitingPickup = `tcinpost005a${stamp}`
|
|
121
|
+
const awaitingPickupId = await seedShipment(scope, awaitingPickup, 'out_for_delivery')
|
|
122
|
+
shipmentIds.push(awaitingPickupId)
|
|
123
|
+
const pickupExpired = `tcinpost005b${stamp}`
|
|
124
|
+
const pickupExpiredId = await seedShipment(scope, pickupExpired, 'failed_delivery')
|
|
125
|
+
shipmentIds.push(pickupExpiredId)
|
|
126
|
+
|
|
127
|
+
await deliverStatus(request, awaitingPickup, 'claimed')
|
|
128
|
+
await expect
|
|
129
|
+
.poll(async () => readShipment(awaitingPickupId), { timeout: 10_000 })
|
|
130
|
+
.toEqual({ unified_status: 'failed_delivery', carrier_status: 'claimed' })
|
|
131
|
+
|
|
132
|
+
await deliverStatus(request, pickupExpired, 'claimed')
|
|
133
|
+
expect(await readShipment(pickupExpiredId)).toEqual({ unified_status: 'failed_delivery', carrier_status: null })
|
|
134
|
+
|
|
135
|
+
await deliverStatus(request, pickupExpired, 'returned_to_sender')
|
|
136
|
+
await expect
|
|
137
|
+
.poll(async () => (await readShipment(pickupExpiredId))?.unified_status, {
|
|
138
|
+
message: 'the return after a complaint must still be applied',
|
|
139
|
+
timeout: 10_000,
|
|
140
|
+
})
|
|
141
|
+
.toBe('returned')
|
|
142
|
+
} finally {
|
|
143
|
+
await withClient(async (client) => {
|
|
144
|
+
if (organizationId) {
|
|
145
|
+
await client.query('delete from carrier_webhook_events where organization_id = $1', [organizationId])
|
|
146
|
+
}
|
|
147
|
+
if (shipmentIds.length > 0) {
|
|
148
|
+
await client.query('delete from carrier_shipments where id = any($1::uuid[])', [shipmentIds])
|
|
149
|
+
}
|
|
150
|
+
}).catch(() => undefined)
|
|
151
|
+
await deleteIntegrationCredentialsInDb(organizationId).catch(() => undefined)
|
|
152
|
+
await deleteOrganizationInDb(organizationId).catch(() => undefined)
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
})
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import Chance from 'chance'
|
|
2
|
+
import {
|
|
3
|
+
getTerminalShippingEvent,
|
|
4
|
+
syncShipmentStatus,
|
|
5
|
+
TERMINAL_SHIPPING_STATUSES,
|
|
6
|
+
} from '@open-mercato/core/modules/shipping_carriers/lib/status-sync'
|
|
2
7
|
import { mapInpostStatus, mapServiceCodeToInpost, isLockerService } from '../lib/status-map'
|
|
3
8
|
|
|
4
9
|
const chance = new Chance()
|
|
@@ -55,13 +60,13 @@ describe('mapInpostStatus', () => {
|
|
|
55
60
|
['undelivered_wrong_address', 'failed_delivery'],
|
|
56
61
|
['undelivered_cod_cash_receiver', 'failed_delivery'],
|
|
57
62
|
['rejected_by_receiver', 'failed_delivery'],
|
|
63
|
+
['claimed', 'failed_delivery'],
|
|
58
64
|
|
|
59
65
|
// Returned
|
|
60
66
|
['returned_to_sender', 'returned'],
|
|
61
67
|
|
|
62
68
|
// Cancelled
|
|
63
69
|
['canceled', 'cancelled'],
|
|
64
|
-
['claimed', 'cancelled'],
|
|
65
70
|
]
|
|
66
71
|
|
|
67
72
|
it.each(cases)('maps InPost status "%s" to unified "%s"', (inpostStatus, expected) => {
|
|
@@ -77,6 +82,21 @@ describe('mapInpostStatus', () => {
|
|
|
77
82
|
it('returns "unknown" for sent_from_sorting_center (not in official status list)', () => {
|
|
78
83
|
expect(mapInpostStatus('sent_from_sorting_center')).toBe('unknown')
|
|
79
84
|
})
|
|
85
|
+
|
|
86
|
+
it('does not treat a parcel complaint ("claimed") as a terminal cancellation', () => {
|
|
87
|
+
const mapped = mapInpostStatus('claimed')
|
|
88
|
+
expect(mapped).not.toBe(mapInpostStatus('canceled'))
|
|
89
|
+
expect(TERMINAL_SHIPPING_STATUSES.has(mapped)).toBe(false)
|
|
90
|
+
expect(getTerminalShippingEvent(mapped)).toBeNull()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('keeps syncing a shipment after a complaint so the parcel return is still recorded', () => {
|
|
94
|
+
const shipment = { unifiedStatus: 'failed_delivery' } as Parameters<typeof syncShipmentStatus>[0]
|
|
95
|
+
expect(syncShipmentStatus(shipment, mapInpostStatus('claimed'))).toBe(false)
|
|
96
|
+
expect(shipment.unifiedStatus).toBe('failed_delivery')
|
|
97
|
+
expect(syncShipmentStatus(shipment, mapInpostStatus('returned_to_sender'))).toBe(true)
|
|
98
|
+
expect(shipment.unifiedStatus).toBe('returned')
|
|
99
|
+
})
|
|
80
100
|
})
|
|
81
101
|
|
|
82
102
|
describe('mapServiceCodeToInpost', () => {
|
|
@@ -51,13 +51,13 @@ const INPOST_STATUS_MAP: Record<string, UnifiedShipmentStatus> = {
|
|
|
51
51
|
undelivered_wrong_address: 'failed_delivery',
|
|
52
52
|
undelivered_cod_cash_receiver: 'failed_delivery',
|
|
53
53
|
rejected_by_receiver: 'failed_delivery',
|
|
54
|
+
claimed: 'failed_delivery',
|
|
54
55
|
|
|
55
56
|
// Returned
|
|
56
57
|
returned_to_sender: 'returned',
|
|
57
58
|
|
|
58
59
|
// Cancelled
|
|
59
60
|
canceled: 'cancelled',
|
|
60
|
-
claimed: 'cancelled',
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export function mapInpostStatus(inpostStatus: string): UnifiedShipmentStatus {
|