fix: make client flow load without edge function

This commit is contained in:
Codex 2026-04-16 17:00:31 +03:00
parent ffc7f3a97d
commit 3b4b6648ff
2 changed files with 178 additions and 7 deletions

View File

@ -1,6 +1,10 @@
import { supabase, hasSupabaseConfig } from "../supabaseClient"; import { supabase, hasSupabaseConfig } from "../supabaseClient";
const SHOWCASE_TOKEN = "showcase"; 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); const formatIsoDate = (date) => date.toISOString().slice(0, 10);
@ -10,6 +14,63 @@ const addDays = (date, days) => {
return next; 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()) => { export const buildShowcaseInvitation = (token = SHOWCASE_TOKEN, now = new Date()) => {
const firstDay = formatIsoDate(addDays(now, 1)); const firstDay = formatIsoDate(addDays(now, 1));
const secondDay = formatIsoDate(addDays(now, 2)); const secondDay = formatIsoDate(addDays(now, 2));
@ -53,17 +114,66 @@ const invokeDeliveryFunction = async (functionName, body) => {
return data; return data;
}; };
export const fetchDeliveryInvitation = async (token) => export const __resetLocalDeliveryInvitationCache = () => {
(token === SHOWCASE_TOKEN localDeliveryInvitationCache.clear();
? buildShowcaseInvitation(token) };
: (await invokeDeliveryFunction("get-delivery-invitation", { token })).invitation);
export const confirmDeliveryChoice = async ({ token, deliveryDate, deliveryTime }) => export const fetchDeliveryInvitation = async (token) => {
invokeDeliveryFunction("confirm-delivery-choice", { 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, token,
deliveryDate, deliveryDate,
deliveryTime, deliveryTime,
}); });
};
export const requestDeliveryLink = async ({ export const requestDeliveryLink = async ({
orderId, orderId,

View File

@ -17,6 +17,7 @@ import {
buildShowcaseInvitation, buildShowcaseInvitation,
confirmDeliveryChoice, confirmDeliveryChoice,
fetchDeliveryInvitation, fetchDeliveryInvitation,
__resetLocalDeliveryInvitationCache,
reportDeliveryResult, reportDeliveryResult,
requestDeliveryLink, requestDeliveryLink,
transferDeliveryToLogistics, transferDeliveryToLogistics,
@ -25,6 +26,7 @@ import {
describe("deliveryInvitationApi", () => { describe("deliveryInvitationApi", () => {
beforeEach(() => { beforeEach(() => {
invoke.mockReset(); invoke.mockReset();
__resetLocalDeliveryInvitationCache();
}); });
it("loads a delivery invitation by token", async () => { it("loads a delivery invitation by token", async () => {
@ -59,7 +61,37 @@ describe("deliveryInvitationApi", () => {
state: "awaiting_choice", 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", () => { 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 () => { it("creates a delivery invitation from order data", async () => {
invoke.mockResolvedValueOnce({ invoke.mockResolvedValueOnce({
data: { data: {