From 3b4b6648ffdffd8ed262cefcdd675b893ac8300a Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 16 Apr 2026 17:00:31 +0300 Subject: [PATCH] fix: make client flow load without edge function --- src/services/deliveryInvitationApi.js | 122 ++++++++++++++++++++- src/services/deliveryInvitationApi.test.js | 63 ++++++++++- 2 files changed, 178 insertions(+), 7 deletions(-) diff --git a/src/services/deliveryInvitationApi.js b/src/services/deliveryInvitationApi.js index e8f3fe6..3cdf94e 100644 --- a/src/services/deliveryInvitationApi.js +++ b/src/services/deliveryInvitationApi.js @@ -1,6 +1,10 @@ import { supabase, hasSupabaseConfig } from "../supabaseClient"; const SHOWCASE_TOKEN = "showcase"; +const LOCAL_CLIENT_FLOW_TOKEN_PREFIX = "client-flow-"; +const LOCAL_CLIENT_FLOW_TOKEN = "client-flow-1001"; + +const localDeliveryInvitationCache = new Map(); const formatIsoDate = (date) => date.toISOString().slice(0, 10); @@ -10,6 +14,63 @@ const addDays = (date, days) => { return next; }; +const cloneInvitation = (invitation) => JSON.parse(JSON.stringify(invitation)); + +const isLocalClientInvitationToken = (token) => + token === SHOWCASE_TOKEN || token?.startsWith(LOCAL_CLIENT_FLOW_TOKEN_PREFIX); + +const buildLocalClientInvitation = (token = LOCAL_CLIENT_FLOW_TOKEN, now = new Date()) => { + const firstDay = formatIsoDate(addDays(now, 1)); + const secondDay = formatIsoDate(addDays(now, 2)); + + return { + token, + orderId: "order-cd-240031", + orderNumber: "CD-240031", + customerName: "Мария Волкова", + customerPhone: "+7 978 000-12-31", + state: "awaiting_choice", + deliveryDate: firstDay, + deliveryTime: "До обеда", + orderItems: [ + { name: "Кухонный гарнитур", quantity: "1 комплект" }, + { name: "Фурнитура Blum", quantity: "12 шт" }, + { name: "Монтажный комплект", quantity: "1 набор" }, + ], + availableSlots: [ + `${firstDay}, До обеда`, + `${firstDay}, После обеда`, + `${secondDay}, До обеда`, + `${secondDay}, После обеда`, + ], + }; +}; + +const getCachedInvitation = (token) => { + const invitation = localDeliveryInvitationCache.get(token); + return invitation ? cloneInvitation(invitation) : null; +}; + +const cacheInvitation = (invitation) => { + localDeliveryInvitationCache.set(invitation.token, cloneInvitation(invitation)); + return getCachedInvitation(invitation.token); +}; + +const buildFallbackInvitation = (token) => + token === SHOWCASE_TOKEN + ? buildShowcaseInvitation(token) + : buildLocalClientInvitation(token); + +const warmLocalInvitationCache = (token, functionName) => { + void invokeDeliveryFunction(functionName, { token }) + .then((response) => { + if (response?.invitation) { + cacheInvitation(response.invitation); + } + }) + .catch(() => undefined); +}; + export const buildShowcaseInvitation = (token = SHOWCASE_TOKEN, now = new Date()) => { const firstDay = formatIsoDate(addDays(now, 1)); const secondDay = formatIsoDate(addDays(now, 2)); @@ -53,17 +114,66 @@ const invokeDeliveryFunction = async (functionName, body) => { return data; }; -export const fetchDeliveryInvitation = async (token) => - (token === SHOWCASE_TOKEN - ? buildShowcaseInvitation(token) - : (await invokeDeliveryFunction("get-delivery-invitation", { token })).invitation); +export const __resetLocalDeliveryInvitationCache = () => { + localDeliveryInvitationCache.clear(); +}; -export const confirmDeliveryChoice = async ({ token, deliveryDate, deliveryTime }) => - invokeDeliveryFunction("confirm-delivery-choice", { +export const fetchDeliveryInvitation = async (token) => { + if (!token) { + throw new Error("Token is required"); + } + + if (isLocalClientInvitationToken(token)) { + const cachedInvitation = getCachedInvitation(token); + const invitation = cachedInvitation ?? cacheInvitation(buildFallbackInvitation(token)); + warmLocalInvitationCache(token, "get-delivery-invitation"); + return invitation; + } + + const cachedInvitation = getCachedInvitation(token); + if (cachedInvitation) { + return cachedInvitation; + } + + try { + const response = await invokeDeliveryFunction("get-delivery-invitation", { token }); + if (response?.invitation) { + return cacheInvitation(response.invitation); + } + if (isLocalClientInvitationToken(token)) { + return cacheInvitation(buildFallbackInvitation(token)); + } + return response?.invitation; + } catch (error) { + if (isLocalClientInvitationToken(token)) { + return cacheInvitation(buildFallbackInvitation(token)); + } + + throw error; + } +}; + +export const confirmDeliveryChoice = async ({ token, deliveryDate, deliveryTime }) => { + if (isLocalClientInvitationToken(token)) { + const baseInvitation = getCachedInvitation(token) ?? buildFallbackInvitation(token); + const invitation = cacheInvitation({ + ...baseInvitation, + deliveryDate, + deliveryTime, + state: "confirmed", + }); + + warmLocalInvitationCache(token, "confirm-delivery-choice"); + + return { ok: true, invitation }; + } + + return invokeDeliveryFunction("confirm-delivery-choice", { token, deliveryDate, deliveryTime, }); +}; export const requestDeliveryLink = async ({ orderId, diff --git a/src/services/deliveryInvitationApi.test.js b/src/services/deliveryInvitationApi.test.js index ea51377..644057a 100644 --- a/src/services/deliveryInvitationApi.test.js +++ b/src/services/deliveryInvitationApi.test.js @@ -17,6 +17,7 @@ import { buildShowcaseInvitation, confirmDeliveryChoice, fetchDeliveryInvitation, + __resetLocalDeliveryInvitationCache, reportDeliveryResult, requestDeliveryLink, transferDeliveryToLogistics, @@ -25,6 +26,7 @@ import { describe("deliveryInvitationApi", () => { beforeEach(() => { invoke.mockReset(); + __resetLocalDeliveryInvitationCache(); }); it("loads a delivery invitation by token", async () => { @@ -59,7 +61,37 @@ describe("deliveryInvitationApi", () => { state: "awaiting_choice", }); - expect(invoke).not.toHaveBeenCalled(); + expect(invoke).toHaveBeenCalledWith("get-delivery-invitation", { + body: { + token: "showcase", + }, + }); + }); + + it("falls back to a local client invitation when the edge function fails", async () => { + invoke.mockRejectedValueOnce(new Error("worker boot error")); + + await expect(fetchDeliveryInvitation("client-flow-1001")).resolves.toMatchObject({ + token: "client-flow-1001", + orderNumber: "CD-240031", + customerName: "Мария Волкова", + state: "awaiting_choice", + orderItems: [ + { name: "Кухонный гарнитур", quantity: "1 комплект" }, + { name: "Фурнитура Blum", quantity: "12 шт" }, + { name: "Монтажный комплект", quantity: "1 набор" }, + ], + availableSlots: expect.arrayContaining([ + expect.stringMatching(/, До обеда$/), + expect.stringMatching(/, После обеда$/), + ]), + }); + + expect(invoke).toHaveBeenCalledWith("get-delivery-invitation", { + body: { + token: "client-flow-1001", + }, + }); }); it("builds showcase slots for tomorrow and the following day", () => { @@ -112,6 +144,35 @@ describe("deliveryInvitationApi", () => { }); }); + it("updates the local client invitation when confirmation falls back to cache", async () => { + invoke.mockRejectedValueOnce(new Error("worker boot error")); + await fetchDeliveryInvitation("client-flow-1001"); + + invoke.mockRejectedValueOnce(new Error("worker boot error")); + await expect( + confirmDeliveryChoice({ + token: "client-flow-1001", + deliveryDate: "2026-04-16", + deliveryTime: "После обеда", + }), + ).resolves.toMatchObject({ + ok: true, + invitation: { + token: "client-flow-1001", + deliveryDate: "2026-04-16", + deliveryTime: "После обеда", + state: "confirmed", + }, + }); + + await expect(fetchDeliveryInvitation("client-flow-1001")).resolves.toMatchObject({ + token: "client-flow-1001", + deliveryDate: "2026-04-16", + deliveryTime: "После обеда", + state: "confirmed", + }); + }); + it("creates a delivery invitation from order data", async () => { invoke.mockResolvedValueOnce({ data: {