diff --git a/apps/shopify-app/app/features/delivery/route-tracking.js b/apps/shopify-app/app/features/delivery/route-tracking.js index 98dff86..4e2ea50 100644 --- a/apps/shopify-app/app/features/delivery/route-tracking.js +++ b/apps/shopify-app/app/features/delivery/route-tracking.js @@ -285,7 +285,7 @@ function mergeRouteTrackingSnapshot(currentSnapshot, serverSnapshot) { const mergedBase = normalizeRouteTrackingSnapshot({ ...historyBase, policy: incomingSnapshot.policy ?? current.policy, - progress: incomingSnapshot.progress, + progress: mergeTrackingProgressSnapshot(current.progress, incomingSnapshot.progress), roadMatchedPath: getNewestRoadMatchedPath(current.roadMatchedPath, incomingSnapshot.roadMatchedPath), routePlanId: incomingSnapshot.routePlanId ?? current.routePlanId, schemaVersion: incomingSnapshot.schemaVersion, @@ -317,6 +317,48 @@ function mergeRouteTrackingSnapshot(currentSnapshot, serverSnapshot) { return mergedSnapshot; } +function mergeTrackingProgressSnapshot(currentProgress, incomingProgress) { + const currentLatestEvent = currentProgress?.latestEvent ?? null; + const incomingLatestEvent = incomingProgress?.latestEvent ?? null; + const currentLatestTimestamp = getPositionTimestamp(currentLatestEvent); + const incomingLatestTimestamp = getPositionTimestamp(incomingLatestEvent); + const latestEvent = incomingLatestEvent && incomingLatestTimestamp >= currentLatestTimestamp + ? incomingLatestEvent + : currentLatestEvent; + const completedStopIds = new Set(currentProgress?.completedStopIds ?? []); + const failedStopIds = new Set(currentProgress?.failedStopIds ?? []); + + for (const deliveryStopId of incomingProgress?.completedStopIds ?? []) { + completedStopIds.add(deliveryStopId); + failedStopIds.delete(deliveryStopId); + } + for (const deliveryStopId of incomingProgress?.failedStopIds ?? []) { + failedStopIds.add(deliveryStopId); + completedStopIds.delete(deliveryStopId); + } + + if (latestEvent?.deliveryStopId && latestEvent.eventType === "STOP_DELIVERED") { + completedStopIds.add(latestEvent.deliveryStopId); + failedStopIds.delete(latestEvent.deliveryStopId); + } + if (latestEvent?.deliveryStopId && latestEvent.eventType === "STOP_FAILED") { + failedStopIds.add(latestEvent.deliveryStopId); + completedStopIds.delete(latestEvent.deliveryStopId); + } + + return { + completedStopIds: [...completedStopIds], + currentStage: latestEvent + ? getProgressStage(latestEvent.eventType) + : incomingProgress?.currentStage ?? currentProgress?.currentStage ?? "READY", + currentStopId: latestEvent + ? latestEvent.eventType === "STOP_ARRIVED" ? latestEvent.deliveryStopId : null + : incomingProgress?.currentStopId ?? currentProgress?.currentStopId ?? null, + failedStopIds: [...failedStopIds], + latestEvent, + }; +} + function mergeTrackingStopArrivals(currentArrivals, incomingArrivals) { const arrivalsByEventId = new Map(); for (const arrival of currentArrivals) arrivalsByEventId.set(arrival.eventId, arrival); diff --git a/apps/shopify-app/app/features/orders/orders-page.jsx b/apps/shopify-app/app/features/orders/orders-page.jsx index 1ecceff..769fc4b 100644 --- a/apps/shopify-app/app/features/orders/orders-page.jsx +++ b/apps/shopify-app/app/features/orders/orders-page.jsx @@ -1273,17 +1273,28 @@ const noteCardStyle = { padding: "8px 10px", }; -const noteListStyle = { - margin: 0, - paddingLeft: "18px", +const noteStackStyle = { + display: "grid", + gap: "8px", +}; + +const noteLabelStyle = { + color: "#657080", + fontSize: "10px", + fontWeight: 700, + letterSpacing: "0.06em", + marginBottom: "4px", + textAlign: "left", + textTransform: "uppercase", }; -const noteListItemStyle = { +const noteTextStyle = { color: "#303030", fontSize: "12px", lineHeight: 1.4, overflowWrap: "anywhere", textAlign: "left", + whiteSpace: "pre-wrap", }; const itemPopoverTitleStyle = { @@ -5168,12 +5179,20 @@ function OrdersPageContent({ loaderData }) { width: `${Math.round(notePopoverPosition.width)}px`, }} > -
Order Note
-
- +
Notes
+
+ {orderNote ? ( +
+
Order Note
+
{orderNote}
+
+ ) : null} + {customerNote ? ( +
+
Customer Note
+
{customerNote}
+
+ ) : null}
, document.body) : null} diff --git a/apps/shopify-app/tests/orders-page.test.mjs b/apps/shopify-app/tests/orders-page.test.mjs index df97a21..0280a65 100644 --- a/apps/shopify-app/tests/orders-page.test.mjs +++ b/apps/shopify-app/tests/orders-page.test.mjs @@ -424,12 +424,17 @@ test("Orders ID stays centered while Note uses a separate headerless column", () assert.match(ordersPageSource, /createPortal\([\s\S]*?ref=\{notePopoverRef\}[\s\S]*?notePopoverPosition\.left/); assert.match(ordersPageSource, /data-order-notes-popover-root="true"/); assert.match(ordersPageSource, /NotesOrder NoteCustomer Note/); - assert.match(ordersPageSource, /
  • \{orderNote\}<\/li>/); - assert.match(ordersPageSource, /
  • \{customerNote\}<\/li>/); + assert.match(ordersPageSource, /const noteStackStyle = \{/); + assert.match(ordersPageSource, /const noteLabelStyle = \{/); + assert.match(ordersPageSource, /const noteTextStyle = \{/); + assert.match(ordersPageSource, /\{orderNote \? \([\s\S]*?
    Order Note<\/div>[\s\S]*?
    \{orderNote\}<\/div>/); + assert.match(ordersPageSource, /\{customerNote \? \([\s\S]*?
    Customer Note<\/div>[\s\S]*?
    \{customerNote\}<\/div>/); + assert.doesNotMatch(ordersPageSource, /const noteListStyle = \{/); + assert.doesNotMatch(ordersPageSource, /
      /); }); test("Ordered pill exposes order timing and delivery-cycle sequence on hover", () => { diff --git a/apps/shopify-app/tests/route-tracking-contract.test.mjs b/apps/shopify-app/tests/route-tracking-contract.test.mjs index 3755535..ad81cd4 100644 --- a/apps/shopify-app/tests/route-tracking-contract.test.mjs +++ b/apps/shopify-app/tests/route-tracking-contract.test.mjs @@ -665,6 +665,56 @@ test("tracking progress keeps the current driver stage and completed stop ids", assert.deepEqual(delivered.progress.completedStopIds, ["stop-1", "stop-2"]); }); +test("reconnect snapshots without latest events cannot regress newer live progress", () => { + const liveSnapshot = normalizeRouteTrackingSnapshot({ + policy, + progress: { + completedStopIds: ["stop-live-earlier", "stop-durable-failed"], + currentStage: "DRIVING", + currentStopId: null, + failedStopIds: ["stop-live-failed", "stop-durable-completed"], + latestEvent: { + deliveryStopId: "stop-live-delivered", + driverId: "driver-1", + eventId: "progress-live-delivered", + eventType: "STOP_DELIVERED", + occurredAt: "2026-07-20T04:05:00.000Z", + receivedAt: "2026-07-20T04:05:01.000Z", + routePlanId: "route-1", + }, + }, + recentPositions: [], + routePlanId: "route-1", + }); + const reconnectSnapshot = { + policy, + progress: { + completedStopIds: ["stop-durable-completed"], + currentStage: "READY", + currentStopId: null, + failedStopIds: ["stop-durable-failed"], + latestEvent: null, + }, + recentPositions: [], + routePlanId: "route-1", + status: "NO_DATA", + }; + + const merged = mergeRouteTrackingSnapshot(liveSnapshot, reconnectSnapshot); + + assert.equal(merged.progress.currentStage, "DRIVING"); + assert.equal(merged.progress.latestEvent.eventId, "progress-live-delivered"); + assert.deepEqual(new Set(merged.progress.completedStopIds), new Set([ + "stop-durable-completed", + "stop-live-delivered", + "stop-live-earlier", + ])); + assert.deepEqual(new Set(merged.progress.failedStopIds), new Set([ + "stop-durable-failed", + "stop-live-failed", + ])); +}); + test("tracking proxy forwards authentication and streams the upstream body without buffering", async () => { const abortController = new AbortController(); const request = new Request("https://app.test/app/route-tracking/route-1", {