import { createServiceClient } from "../_shared/chatbot.ts"; import { insertIntegrationEvent } from "../_shared/integration-events.ts"; import { getClientIp, getCorsHeaders, hashText, jsonResponse, preflightResponse, readJsonBody, requireRateLimit, } from "../_shared/security.ts"; const MAX_BODY_BYTES = 8 * 1024; const ALLOWED_ROLES = new Set(["manager", "logistician", "admin"]); const ALLOWED_DELIVERY_TIMES = new Set(["Первая половина дня", "Вторая половина дня"]); const DELIVERY_TIME_ALIASES = new Map([ ["До обеда", "Первая половина дня"], ["После обеда", "Вторая половина дня"], ]); const DELIVERY_TIMEZONE = "Europe/Simferopol"; type UpdateDeliveryChoiceBody = { orderGroupId?: string; deliveryDate?: string; deliveryTime?: string; }; const isValidDate = (value: string) => /^\d{4}-\d{2}-\d{2}$/.test(value); const getTodayKey = () => { const parts = new Intl.DateTimeFormat("en-CA", { timeZone: DELIVERY_TIMEZONE, year: "numeric", month: "2-digit", day: "2-digit", }).formatToParts(new Date()); const year = parts.find((part) => part.type === "year")?.value || ""; const month = parts.find((part) => part.type === "month")?.value || ""; const day = parts.find((part) => part.type === "day")?.value || ""; return `${year}-${month}-${day}`; }; const isWeekendDeliveryDate = (value: string) => { if (!isValidDate(value)) { return false; } const date = new Date(`${value}T12:00:00Z`); const weekday = date.getUTCDay(); return weekday === 0 || weekday === 6; }; const isAllowedDeliveryDate = (value: string) => isValidDate(value) && value > getTodayKey() && !isWeekendDeliveryDate(value); const normalizeDeliveryTime = (value: string) => DELIVERY_TIME_ALIASES.get(value) || value; const getBearerToken = (request: Request) => { const authorization = request.headers.get("authorization") || ""; return authorization.toLowerCase().startsWith("bearer ") ? authorization.slice(7).trim() : ""; }; const getUserRole = async ( supabase: ReturnType, accessToken: string, ) => { const { data: authData, error: authError } = await supabase.auth.getUser(accessToken); if (authError || !authData.user?.id) { return null; } const { data: profile, error: profileError } = await supabase .from("users") .select("id, role_info:roles(name)") .eq("id", authData.user.id) .single(); if (profileError) { throw profileError; } const roleInfo = Array.isArray(profile.role_info) ? profile.role_info[0] : profile.role_info; return { userId: authData.user.id, role: roleInfo?.name || "", }; }; Deno.serve(async (request) => { if (request.method === "OPTIONS") { return preflightResponse(request, "public"); } if (request.method !== "POST") { return jsonResponse({ ok: false, error: "Method not allowed" }, 405); } const corsHeaders = getCorsHeaders(request, "public"); if (!corsHeaders) { return jsonResponse({ ok: false, error: "Origin not allowed" }, 403); } try { const { body } = await readJsonBody(request, { maxBytes: MAX_BODY_BYTES, }); const orderGroupId = String(body.orderGroupId || "").trim(); const deliveryDate = String(body.deliveryDate || "").trim(); const deliveryTime = normalizeDeliveryTime(String(body.deliveryTime || "").trim()); if (!orderGroupId) { return jsonResponse({ ok: false, error: "orderGroupId is required" }, 400, corsHeaders); } if (!isAllowedDeliveryDate(deliveryDate)) { return jsonResponse({ ok: false, error: "Выберите будущий будний день доставки" }, 400, corsHeaders); } if (!ALLOWED_DELIVERY_TIMES.has(deliveryTime)) { return jsonResponse({ ok: false, error: "Выберите первую или вторую половину дня доставки" }, 400, corsHeaders); } const accessToken = getBearerToken(request); if (!accessToken) { return jsonResponse({ ok: false, error: "Authentication is required" }, 401, corsHeaders); } const supabase = createServiceClient(); const actor = await getUserRole(supabase, accessToken); if (!actor || !ALLOWED_ROLES.has(actor.role)) { return jsonResponse({ ok: false, error: "Forbidden" }, 403, corsHeaders); } const ipHash = await hashText(getClientIp(request)); await requireRateLimit(supabase, { scope: "order-group-manual-delivery-choice", key: `${actor.userId}:${ipHash}:${orderGroupId}`, maxCount: 20, windowSeconds: 600, blockSeconds: 1800, }); const { data: currentGroup, error: currentGroupError } = await supabase .from("order_groups") .select("id, delivery_status, delivery_invitation_id") .eq("id", orderGroupId) .single(); if (currentGroupError) { throw currentGroupError; } const { data: group, error: groupUpdateError } = await supabase .from("order_groups") .update({ delivery_status: "agreed", delivery_date: deliveryDate, delivery_time: deliveryTime, notification_status: "confirmed", updated_at: new Date().toISOString(), }) .eq("id", orderGroupId) .select("*") .single(); if (groupUpdateError) { throw groupUpdateError; } if (currentGroup.delivery_invitation_id) { const { error: invitationUpdateError } = await supabase .from("delivery_invitations") .update({ state: "agreed", delivery_date: deliveryDate, delivery_time: deliveryTime, confirmed_at: new Date().toISOString(), }) .eq("id", currentGroup.delivery_invitation_id); if (invitationUpdateError) { throw invitationUpdateError; } } await insertIntegrationEvent(supabase, { order_id: null, event_type: "order_group_manual_delivery_choice", direction: "internal", status: "success", payload: { order_group_id: orderGroupId, actor_user_id: actor.userId, actor_role: actor.role, old_delivery_status: currentGroup.delivery_status || null, new_delivery_status: "agreed", delivery_date: deliveryDate, delivery_time: deliveryTime, }, }); return jsonResponse( { ok: true, orderGroup: group, }, 200, corsHeaders, ); } catch (error) { if (error instanceof Error && "status" in error) { const httpError = error as { status: number; message: string }; return jsonResponse({ ok: false, error: httpError.message }, httpError.status, corsHeaders); } return jsonResponse( { ok: false, error: error instanceof Error ? error.message : "Unexpected error", }, 500, corsHeaders, ); } });