import { maskCustomerName, maskPhoneNumber, } from "./security.ts"; export type DeliveryInvitationAction = | "create_delivery_invitation" | "send_delivery_offer" | "send_delivery_reminder" | "request_new_link" | "confirm_delivery_choice" | "transfer_to_logistics" | "mark_paid_storage" | "mark_delivered"; export type DeliveryInvitationPublicState = | "awaiting_choice" | "opened" | "reminder_sent" | "transferred_to_logistics" | "paid_storage" | "delivered" | "agreed" | "default"; export const DEFAULT_AVAILABLE_SLOTS = ["Первая половина дня", "Вторая половина дня"]; export const getOrderUpdateForDeliveryInvitationAction = (action: DeliveryInvitationAction) => { switch (action) { case "create_delivery_invitation": case "send_delivery_offer": case "send_delivery_reminder": case "request_new_link": return { status: "Ожидает ответа клиента", deliveryAgreementStatus: "Отправлено клиенту", }; case "confirm_delivery_choice": return { status: "Доставка согласована", deliveryAgreementStatus: "Подтверждено клиентом", }; case "transfer_to_logistics": return { status: "Передан логисту", deliveryAgreementStatus: "Нет ответа", }; case "mark_paid_storage": return { status: "Платное хранение", deliveryAgreementStatus: "Нет ответа", }; case "mark_delivered": return { status: "Доставлен", deliveryAgreementStatus: "Подтверждено клиентом", }; default: return null; } }; export const getClientInvitationStateFromOrderStatus = ( status: string, ): DeliveryInvitationPublicState => { switch (status) { case "Ожидает ответа клиента": return "awaiting_choice"; case "Ожидает согласования доставки": return "opened"; case "Напоминание отправлено": case "Переход отправлен": return "reminder_sent"; case "Передан логисту": return "transferred_to_logistics"; case "Платное хранение": return "paid_storage"; case "Доставлен": return "delivered"; case "Доставка согласована": return "agreed"; default: return "default"; } }; export const getClientInvitationStateFromOrderGroupStatus = ( deliveryStatus: string | null | undefined, invitationState: string | null | undefined, ): DeliveryInvitationPublicState => { if (deliveryStatus === "agreed") { return "agreed"; } if (deliveryStatus === "delivered") { return "delivered"; } if (["awaiting_choice", "opened", "reminder_sent"].includes(String(invitationState || ""))) { return invitationState as DeliveryInvitationPublicState; } return "default"; }; export const isActiveInvitationState = (state: DeliveryInvitationPublicState) => state === "awaiting_choice" || state === "opened" || state === "reminder_sent"; export const generateInvitationToken = () => crypto.randomUUID().replaceAll("-", ""); export const hashInvitationToken = async (token: string) => { const bytes = new TextEncoder().encode(token); const digest = await crypto.subtle.digest("SHA-256", bytes); return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); }; export const normalizeAvailableSlots = (availableSlots?: string[] | null) => { const slots = availableSlots?.map((slot) => slot.trim()).filter(Boolean) || []; return slots.length > 0 ? Array.from(new Set(slots)) : [...DEFAULT_AVAILABLE_SLOTS]; }; export const buildDefaultDatedAvailableSlots = (now = new Date()) => { const CRIMEA_TZ = "Europe/Simferopol"; const formatCrimeaDate = (date: Date) => { return new Intl.DateTimeFormat("en-CA", { timeZone: CRIMEA_TZ, year: "numeric", month: "2-digit", day: "2-digit", }).format(date); }; const addDays = (date: Date, days: number) => { const next = new Date(date); next.setUTCDate(next.getUTCDate() + days); return next; }; const firstDay = formatCrimeaDate(addDays(now, 1)); const secondDay = formatCrimeaDate(addDays(now, 2)); return [ `${firstDay}, Первая половина дня`, `${firstDay}, Вторая половина дня`, `${secondDay}, Первая половина дня`, `${secondDay}, Вторая половина дня`, ]; }; export const resolvePublicAppUrl = ( request: Request, fallbackEnv?: string, ) => { const origin = request.headers.get("origin") || request.headers.get("referer") || ""; const envValue = fallbackEnv || (typeof Deno !== "undefined" ? Deno.env.get("PUBLIC_APP_URL") || Deno.env.get("APP_PUBLIC_URL") : ""); return (envValue || origin || "").replace(/\/$/, ""); }; export const buildInvitationUrl = (baseUrl: string, token: string) => `${baseUrl.replace(/\/$/, "")}/delivery/${token}`; export type DeliveryInvitationRecord = { id?: string; order_id?: string | null; order_group_id?: string | null; token_hash: string; state: string; order_number?: string | null; customer_name?: string | null; customer_phone?: string | null; customer_messenger?: string | null; available_slots?: string[] | null; expires_at?: string | null; revoked_at?: string | null; delivery_date?: string | null; delivery_time?: string | null; sent_at?: string | null; opened_at?: string | null; confirmed_at?: string | null; logistics_transferred_at?: string | null; paid_storage_at?: string | null; delivered_at?: string | null; updated_at?: string | null; }; export type OrderGroupInvitationSource = { id: string; group_key?: string | null; customer?: { name?: string | null; phone?: string | null; date?: string | null; } | null; customer_name?: string | null; customer_phone?: string | null; customer_date?: string | null; order_numbers?: string[] | null; delivery_status?: string | null; delivery_link?: string | null; source_orders?: unknown[] | null; }; export const isInvitationExpired = (invitation: DeliveryInvitationRecord, now = new Date()) => { if (invitation.revoked_at) { return true; } if (!invitation.expires_at) { return false; } return new Date(invitation.expires_at).getTime() <= now.getTime(); }; const parseGroupKey = (groupKey?: string | null) => { const [phone = "", date = ""] = String(groupKey || "").split("|"); return { phone: phone.trim(), date: date.trim(), }; }; const extractOrderItemsFromSourceOrders = (sourceOrders: unknown): Array<{ name: string; quantity: string; items?: unknown[] }> => { if (!Array.isArray(sourceOrders) || sourceOrders.length === 0) { return []; } const items: Array<{ name: string; quantity: string; items?: unknown[] }> = []; for (const source of sourceOrders) { if (!source || typeof source !== "object") { continue; } const record = source as Record; const nom = typeof record.nom === "string" ? record.nom : typeof record.name === "string" ? record.name : ""; const orderList = Array.isArray(record.orderList) ? record.orderList : Array.isArray(record.items) ? record.items : []; if (orderList.length > 0) { items.push({ name: nom || "Позиция", quantity: "", items: orderList.map((item: unknown) => { if (!item || typeof item !== "object") { return { name: String(item), quantity: "" }; } const row = item as Record; return { name: String(row.product_name || row.name || row.title || ""), quantity: String(row.product_quantity || row.quantity || row.count || row.amount || ""), }; }), }); } else if (nom) { items.push({ name: nom, quantity: "" }); } } return items; }; export const buildPublicOrderGroupInvitationView = ( invitation: DeliveryInvitationRecord, group: OrderGroupInvitationSource, ) => { const parsedKey = parseGroupKey(group.group_key); const customerName = group.customer_name || group.customer?.name || invitation.customer_name || null; const customerPhone = group.customer_phone || group.customer?.phone || invitation.customer_phone || parsedKey.phone || null; const orderNumbers = Array.isArray(group.order_numbers) ? group.order_numbers : []; const orderItemsFromSource = extractOrderItemsFromSourceOrders(group.source_orders); const orderItems = orderItemsFromSource.length > 0 ? orderItemsFromSource : orderNumbers.map((number) => ({ name: number, quantity: "" })); return { orderId: invitation.order_group_id || group.id, orderGroupId: invitation.order_group_id || group.id, state: invitation.state, token: "", orderNumber: invitation.order_number || orderNumbers[0] || group.group_key || null, customerName: maskCustomerName(customerName), customerPhone: maskPhoneNumber(customerPhone), orderItems, availableSlots: invitation.available_slots || [], deliveryDate: invitation.delivery_date || null, deliveryTime: invitation.delivery_time || null, orderStatus: null, deliveryAgreementStatus: null, }; }; export const buildPublicInvitationView = ( invitation: DeliveryInvitationRecord, order: { order_number?: string | null; customer?: { name?: string | null; phone?: string | null; items?: unknown }; status?: string | null; delivery_agreement_status?: string | null; }, ) => { const availableSlots = invitation.available_slots || []; const orderItems = Array.isArray(order.customer?.items) ? order.customer?.items : []; return { orderId: invitation.order_id, state: invitation.state, token: "", orderNumber: order.order_number || invitation.order_number || null, customerName: maskCustomerName(order.customer?.name || invitation.customer_name || null), customerPhone: maskPhoneNumber(order.customer?.phone || invitation.customer_phone || null), orderItems, availableSlots, deliveryDate: invitation.delivery_date || null, deliveryTime: invitation.delivery_time || null, orderStatus: null, deliveryAgreementStatus: null, }; };