324 lines
10 KiB
TypeScript
324 lines
10 KiB
TypeScript
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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,
|
|
};
|
|
};
|