diff --git a/public/service-worker.js b/public/service-worker.js
index cb6d884..adbfca9 100644
--- a/public/service-worker.js
+++ b/public/service-worker.js
@@ -1,8 +1,8 @@
const isLocalhost = self.location.hostname === "localhost" || self.location.hostname === "127.0.0.1";
if (!isLocalhost) {
- const STATIC_CACHE = "construction-delivery-static-v118";
- const RUNTIME_CACHE = "construction-delivery-runtime-v118";
+ const STATIC_CACHE = "construction-delivery-static-v119";
+ const RUNTIME_CACHE = "construction-delivery-runtime-v119";
const APP_SHELL_URLS = ["/", "/client", "/index.html", "/manifest.webmanifest", "/icons/icon-192.png", "/icons/icon-512.png"];
// Only these are treated as cacheable static assets. Everything else
diff --git a/scripts/sms_first_campaign.py b/scripts/sms_first_campaign.py
index 1b75414..9a56f73 100644
--- a/scripts/sms_first_campaign.py
+++ b/scripts/sms_first_campaign.py
@@ -68,10 +68,16 @@ LIMIT_ERROR_CODES = {"131", "132", "230", "231", "232"}
# Шаблоны живут в админке (sms_campaign_settings):
# sms_text_template — текст для ДОСТАВКИ
# sms_text_template_pickup — текст для САМОВЫВОЗА
-# {link} подставляется ссылкой согласования. Пустой шаблон → DEFAULT_TEXT_*.
+# Подстановки: {link}/{ссылка} — ссылка согласования,
+# {order}/{номер}/{номер_доставки} — номер доставки (010796),
+# {code}/{код}/{код_выдачи} — код выдачи (7598).
+# Пустой шаблон → DEFAULT_TEXT_*.
DEFAULT_TEXT_DELIVERY = "Ваш заказ готов. Согласуйте дату доставки по ссылке: {link}"
-DEFAULT_TEXT_PICKUP = "Ваш заказ готов. Согласуйте дату самовывоза по ссылке: {link}"
+DEFAULT_TEXT_PICKUP = (
+ "Ваш заказ готов. Номер доставки {order}, код выдачи {code}. "
+ "Согласуйте дату самовывоза по ссылке: {link}"
+)
DEFAULT_TEXT_NOTIFICATION = "Ваш заказ готов."
@@ -79,6 +85,35 @@ def is_pickup_group(group):
return (group.get("delivery_type") or "delivery").strip().lower() == "pickup"
+def first_order_digits(value):
+ """Номер доставки из order_numbers («ТП\\-010722» → «010722»)."""
+ items = value if isinstance(value, (list, tuple)) else [value]
+ for item in items:
+ digits = "".join(ch for ch in str(item or "").replace("\\", "") if ch.isdigit())
+ if digits:
+ return digits
+ return ""
+
+
+def apply_sms_placeholders(text, group, link):
+ """Подстановка переменных шаблона в текст SMS."""
+ if not text:
+ return text
+
+ order = first_order_digits(group.get("order_numbers"))
+ code = "".join(ch for ch in str(group.get("pickup_code") or "") if ch.isdigit())
+ replacements = (
+ ("{link}", link), ("{ссылка}", link),
+ ("{номер_доставки}", order), ("{номер}", order), ("{order}", order),
+ ("{код_выдачи}", code), ("{код}", code), ("{code}", code),
+ )
+
+ result = text
+ for token, value in replacements:
+ result = result.replace(token, value)
+ return result.strip()
+
+
def build_sms_text(group, settings):
"""Текст SMS по типу отгрузки: самовывоз → pickup-шаблон, доставка → обычный.
@@ -99,13 +134,13 @@ def build_sms_text(group, settings):
if mode == "notification":
if profile_text:
- return profile_text
+ return apply_sms_placeholders(profile_text, group, link)
# уведомление без ссылки: шаблон годится, только если в нём нет {link}
if template and "{link}" not in template:
- return template
- return DEFAULT_TEXT_NOTIFICATION
+ return apply_sms_placeholders(template, group, link)
+ return apply_sms_placeholders(DEFAULT_TEXT_NOTIFICATION, group, link)
- return template.replace("{link}", link).strip()
+ return apply_sms_placeholders(template, group, link)
# ─── Логирование ─────────────────────────────────────────────────────────────
@@ -227,6 +262,7 @@ def get_groups_to_send(conn, max_attempts):
SELECT og.id, og.group_key, og.customer_name, og.customer_phone,
og.customer_phone_normalized, og.delivery_link, og.notification_status,
og.delivery_type, og.town, og.sms_attempts,
+ og.order_numbers, og.pickup_code,
-- Профиль города (fallback на дефолтные значения если нет записи)
COALESCE(scp.delivery_mode, 'link') as delivery_mode,
COALESCE(scp.delivery_text, '') as delivery_text,
diff --git a/scripts/sms_paid_storage_campaign.py b/scripts/sms_paid_storage_campaign.py
index 64ba9da..877de44 100644
--- a/scripts/sms_paid_storage_campaign.py
+++ b/scripts/sms_paid_storage_campaign.py
@@ -55,18 +55,50 @@ LIMIT_ERROR_CODES = {"131", "132", "230", "231", "232"}
# Шаблоны живут в админке (sms_campaign_settings):
# sms_text_template — текст для ДОСТАВКИ
# sms_text_template_pickup — текст для САМОВЫВОЗА
-# {link} подставляется ссылкой согласования. Пустой шаблон → DEFAULT_TEXT_*.
+# Подстановки: {link}/{ссылка} — ссылка согласования,
+# {order}/{номер}/{номер_доставки} — номер доставки (010796),
+# {code}/{код}/{код_выдачи} — код выдачи (7598).
+# Пустой шаблон → DEFAULT_TEXT_*.
DEFAULT_TEXT_DELIVERY = (
"Ваш заказ переведён на платное хранение. Стоимость: 300 ₽/день. "
"Заберите заказ или согласуйте доставку: {link}"
)
DEFAULT_TEXT_PICKUP = (
- "Ваш заказ переведён на платное хранение. Стоимость: 300 ₽/день. "
- "Заберите заказ со склада или согласуйте дату самовывоза: {link}"
+ "Заказ переведён на платное хранение. Номер доставки {order}, код выдачи {code}. "
+ "Заберите со склада или согласуйте дату самовывоза: {link}"
)
+def first_order_digits(value):
+ """Номер доставки из order_numbers («ТП\\-010722» → «010722»)."""
+ items = value if isinstance(value, (list, tuple)) else [value]
+ for item in items:
+ digits = "".join(ch for ch in str(item or "").replace("\\", "") if ch.isdigit())
+ if digits:
+ return digits
+ return ""
+
+
+def apply_sms_placeholders(text, group, link):
+ """Подстановка переменных шаблона в текст SMS."""
+ if not text:
+ return text
+
+ order = first_order_digits(group.get("order_numbers"))
+ code = "".join(ch for ch in str(group.get("pickup_code") or "") if ch.isdigit())
+ replacements = (
+ ("{link}", link), ("{ссылка}", link),
+ ("{номер_доставки}", order), ("{номер}", order), ("{order}", order),
+ ("{код_выдачи}", code), ("{код}", code), ("{code}", code),
+ )
+
+ result = text
+ for token, value in replacements:
+ result = result.replace(token, value)
+ return result.strip()
+
+
def build_sms_text(group, settings):
"""Текст SMS о платном хранении по типу отгрузки."""
link = group.get("delivery_link") or ""
@@ -76,7 +108,7 @@ def build_sms_text(group, settings):
) or "").strip()
if not template:
template = DEFAULT_TEXT_PICKUP if pickup else DEFAULT_TEXT_DELIVERY
- return template.replace("{link}", link).strip()
+ return apply_sms_placeholders(template, group, link)
# ─── Логирование ─────────────────────────────────────────────────────────────
@@ -222,7 +254,8 @@ def get_groups_to_send(conn):
cur.execute("""
SELECT og.id, og.group_key, og.customer_name, og.customer_phone,
og.customer_phone_normalized, og.delivery_link, og.notification_status,
- og.paid_storage_at, og.delivery_type, og.town
+ og.paid_storage_at, og.delivery_type, og.town,
+ og.order_numbers, og.pickup_code
FROM order_groups og
LEFT JOIN sms_city_profiles scp ON scp.city = og.town
WHERE og.delivery_status = 'paid_storage'
diff --git a/scripts/sms_second_campaign.py b/scripts/sms_second_campaign.py
index 8fa3a0c..8efee5b 100644
--- a/scripts/sms_second_campaign.py
+++ b/scripts/sms_second_campaign.py
@@ -67,10 +67,45 @@ LIMIT_ERROR_CODES = {"131", "132", "230", "231", "232"}
# Шаблоны живут в админке (sms_campaign_settings):
# sms_text_template — текст для ДОСТАВКИ
# sms_text_template_pickup — текст для САМОВЫВОЗА
-# {link} подставляется ссылкой согласования. Пустой шаблон → DEFAULT_TEXT_*.
+# Подстановки: {link}/{ссылка} — ссылка согласования,
+# {order}/{номер}/{номер_доставки} — номер доставки (010796),
+# {code}/{код}/{код_выдачи} — код выдачи (7598).
+# Пустой шаблон → DEFAULT_TEXT_*.
DEFAULT_TEXT_DELIVERY = "Ваш заказ готов к доставке. Выберите дату доставки по ссылке: {link}"
-DEFAULT_TEXT_PICKUP = "Ваш заказ готов к самовывозу. Выберите дату самовывоза по ссылке: {link}"
+DEFAULT_TEXT_PICKUP = (
+ "Напоминаем: номер доставки {order}, код выдачи {code}. "
+ "Выберите дату самовывоза по ссылке: {link}"
+)
+
+
+def first_order_digits(value):
+ """Номер доставки из order_numbers («ТП\\-010722» → «010722»)."""
+ items = value if isinstance(value, (list, tuple)) else [value]
+ for item in items:
+ digits = "".join(ch for ch in str(item or "").replace("\\", "") if ch.isdigit())
+ if digits:
+ return digits
+ return ""
+
+
+def apply_sms_placeholders(text, group, link):
+ """Подстановка переменных шаблона в текст SMS."""
+ if not text:
+ return text
+
+ order = first_order_digits(group.get("order_numbers"))
+ code = "".join(ch for ch in str(group.get("pickup_code") or "") if ch.isdigit())
+ replacements = (
+ ("{link}", link), ("{ссылка}", link),
+ ("{номер_доставки}", order), ("{номер}", order), ("{order}", order),
+ ("{код_выдачи}", code), ("{код}", code), ("{code}", code),
+ )
+
+ result = text
+ for token, value in replacements:
+ result = result.replace(token, value)
+ return result.strip()
def build_sms_text(group, settings):
@@ -82,7 +117,7 @@ def build_sms_text(group, settings):
) or "").strip()
if not template:
template = DEFAULT_TEXT_PICKUP if pickup else DEFAULT_TEXT_DELIVERY
- return template.replace("{link}", link).strip()
+ return apply_sms_placeholders(template, group, link)
# ─── Логирование ─────────────────────────────────────────────────────────────
@@ -237,7 +272,7 @@ def get_groups_to_send(conn, max_attempts):
cur.execute("""
SELECT og.id, og.group_key, og.customer_name, og.customer_phone,
og.customer_phone_normalized, og.delivery_link, og.notification_status,
- og.delivery_type, og.town
+ og.delivery_type, og.town, og.order_numbers, og.pickup_code
FROM order_groups og
LEFT JOIN sms_city_profiles scp ON scp.city = og.town
WHERE og.delivery_status = 'pending_confirmation'
diff --git a/src/components/admin/SmsCampaignPanel.jsx b/src/components/admin/SmsCampaignPanel.jsx
index 8a80c86..77d54c4 100644
--- a/src/components/admin/SmsCampaignPanel.jsx
+++ b/src/components/admin/SmsCampaignPanel.jsx
@@ -24,13 +24,23 @@ const HAS_SETTINGS = ["first_sms", "second_sms", "manual", "paid_storage"];
// ── SMS text templates (доставка / самовывоз) ────────────────────────────────
const SMS_LINK_PREVIEW = "https://dost.supersamsev.ru/d/XXXXXX";
+// Образцы значений для предпросмотра (как будет выглядеть SMS у клиента)
+const SMS_PREVIEW_VALUES = [
+ ["{номер_доставки}", "010796"], ["{номер}", "010796"], ["{order}", "010796"],
+ ["{код_выдачи}", "7598"], ["{код}", "7598"], ["{code}", "7598"],
+ ["{ссылка}", SMS_LINK_PREVIEW], ["{link}", SMS_LINK_PREVIEW],
+];
-const renderSmsPreview = (template, emptyLabel) => {
- const text = (template || "").trim();
- if (!text) return emptyLabel;
- return text.replace("{link}", SMS_LINK_PREVIEW);
+const smsPreviewText = (template) => {
+ let text = (template || "").trim();
+ SMS_PREVIEW_VALUES.forEach(([token, value]) => {
+ text = text.split(token).join(value);
+ });
+ return text;
};
+const renderSmsPreview = (template, emptyLabel) => smsPreviewText(template) || emptyLabel;
+
// ── Days of week ─────────────────────────────────────────────────────────────
const DAYS = [
{ num: 1, short: "Пн" },
@@ -512,7 +522,7 @@ export const SmsCampaignPanel = () => {
placeholder="Ваш заказ готов. Согласуйте дату доставки по ссылке: {link}"
/>
- Текущая длина: {(settings.sms_text_template || "").replace("{link}", SMS_LINK_PREVIEW).length} символов
+ Текущая длина: {smsPreviewText(settings.sms_text_template).length} символов
@@ -525,12 +535,15 @@ export const SmsCampaignPanel = () => {
placeholder="Ваш заказ готов. Согласуйте дату самовывоза по ссылке: {link}"
/>
- Текущая длина: {(settings.sms_text_template_pickup || "").replace("{link}", SMS_LINK_PREVIEW).length} символов
+ Текущая длина: {smsPreviewText(settings.sms_text_template_pickup).length} символов
- Текст выбирается по типу отгрузки: самовывоз → правый шаблон, доставка → левый. {"{link}"} заменяется ссылкой согласования.
+ Текст выбирается по типу отгрузки: самовывоз → правый шаблон, доставка → левый.
+
+ Переменные: {"{link}"} — ссылка согласования, {"{order}"} — номер доставки (можно {"{номер}"}), {"{code}"} — код выдачи (можно {"{код}"}).
+
📦 Доставка: {renderSmsPreview(settings.sms_text_template, "— пусто, уйдёт стандартный текст кампании")}
🏪 Самовывоз: {renderSmsPreview(settings.sms_text_template_pickup, "— пусто, уйдёт стандартный текст кампании")}
Если у города в «SMS по городам» задан свой текст в режиме «Уведомление» — уходит он, и тогда шаблон применяется только если в нём нет {"{link}"}.
diff --git a/src/components/client/DeliveryChoiceFlow.test.jsx b/src/components/client/DeliveryChoiceFlow.test.jsx
index 0e29011..98fffc5 100644
--- a/src/components/client/DeliveryChoiceFlow.test.jsx
+++ b/src/components/client/DeliveryChoiceFlow.test.jsx
@@ -84,7 +84,7 @@ describe("DeliveryChoiceFlow", () => {
/>,
);
- expect(markup).toContain("Счет CD-240032");
+ expect(markup).toContain("Доставка CD-240032");
expect(markup).not.toContain("Александр Савин");
});
});
diff --git a/src/components/client/invitationReference.js b/src/components/client/invitationReference.js
index 13413b1..e64216c 100644
--- a/src/components/client/invitationReference.js
+++ b/src/components/client/invitationReference.js
@@ -20,20 +20,42 @@ const getOrderItemNames = (invitation) => {
.filter(Boolean);
};
+// Обмен с 1С экранирует дефис («ТП\-010796») — клиенту показываем «ТП-010796».
+const unescapeOncDashes = (value) => String(value || "").replace(/\\/g, "");
+
export const getInvitationReferenceLabel = (invitation) => {
- const invoiceNumbers = [...new Set(getOrderItemNames(invitation))];
+ const invoiceNumbers = [...new Set(getOrderItemNames(invitation))].map(unescapeOncDashes);
if (invoiceNumbers.length === 1) {
- return `Счет ${invoiceNumbers[0]}`;
+ return `Доставка ${invoiceNumbers[0]}`;
}
if (invoiceNumbers.length > 1) {
- return `Счета: ${invoiceNumbers.join(", ")}`;
+ return `Доставки: ${invoiceNumbers.join(", ")}`;
}
- const orderNumber = typeof invitation?.orderNumber === "string" ? invitation.orderNumber.trim() : "";
+ const orderNumber = typeof invitation?.orderNumber === "string"
+ ? unescapeOncDashes(invitation.orderNumber.trim())
+ : "";
if (orderNumber) {
- return `Счет ${orderNumber}`;
+ return `Доставка ${orderNumber}`;
}
- return "Счет —";
+ return "Доставка —";
+};
+
+/**
+ * Номер доставки для клиента: «ТП\-010796» → «010796».
+ * Сначала берём номер заказа из состава, затем orderNumber/orderNumbers приглашения.
+ */
+export const getDeliveryNumberDigits = (invitation) => {
+ const fromItems = getOrderItemNames(invitation)[0] || "";
+ const fromOrderNumbers = Array.isArray(invitation?.orderNumbers)
+ ? invitation.orderNumbers[0] || ""
+ : "";
+ const fromOrderNumber = typeof invitation?.orderNumber === "string"
+ ? invitation.orderNumber
+ : "";
+ const source = fromItems || fromOrderNumbers || fromOrderNumber;
+
+ return unescapeOncDashes(source).replace(/[^0-9]/g, "");
};
diff --git a/src/pages/ClientDeliveryPage.jsx b/src/pages/ClientDeliveryPage.jsx
index 545840c..c2daa3f 100644
--- a/src/pages/ClientDeliveryPage.jsx
+++ b/src/pages/ClientDeliveryPage.jsx
@@ -4,7 +4,7 @@ import { DeliveryChoiceFlow } from "../components/client/DeliveryChoiceFlow";
import { DeliverySlotsPicker } from "../components/client/DeliverySlotsPicker";
import { PickupSlotsPicker } from "../components/client/PickupSlotsPicker";
import { OrderCompositionPanel } from "../components/client/OrderCompositionPanel";
-import { getInvitationReferenceLabel } from "../components/client/invitationReference";
+import { getDeliveryNumberDigits, getInvitationReferenceLabel } from "../components/client/invitationReference";
import { DeliveryStateNotice } from "../components/client/DeliveryStateNotice";
import { PushSubscriptionBanner } from "../components/client/PushSubscriptionBanner";
import { PwaInstallCard } from "../components/client/PwaInstallCard";
@@ -299,13 +299,25 @@ export const buildSelectedSlotFromInvitation = (invitation, slots = []) => {
};
};
-export const getClientDeliveryHeroDescription = (isActiveState, isChoiceSaved) => {
+export const getClientDeliveryHeroDescription = (
+ isActiveState,
+ isChoiceSaved,
+ deliveryType = "delivery",
+) => {
if (isChoiceSaved) {
return "";
}
- return isActiveState
- ? "Вам предложены варианты доставки. Выберите удобную дату и время."
+ const isPickup = deliveryType === "pickup";
+
+ if (isActiveState) {
+ return isPickup
+ ? "Вам предложены варианты самовывоза. Выберите удобную дату и время."
+ : "Вам предложены варианты доставки. Выберите удобную дату и время.";
+ }
+
+ return isPickup
+ ? "По этому заказу согласование самовывоза завершено или передано логисту."
: "По этому заказу согласование доставки завершено или передано логисту.";
};
@@ -399,6 +411,10 @@ export const ClientDeliveryPage = () => {
const invitationState = invitation?.state || "awaiting_choice";
const isActiveState = ["awaiting_choice", "opened", "reminder_sent"].includes(invitationState);
+ // Тип отгрузки приходит из приглашения: для самовывоза страница говорит «самовывоз».
+ const orderIsPickup = invitation?.deliveryType === "pickup";
+ const deliveryNumberDigits = getDeliveryNumberDigits(invitation);
+ const orderTypeEyebrow = orderIsPickup ? "Самовывоз заказа" : "Доставка заказа";
const invitationSelectedSlot = isActiveState ? null : buildSelectedSlotFromInvitation(invitation, slots);
@@ -407,7 +423,11 @@ export const ClientDeliveryPage = () => {
const savedChoiceLabel = effectiveSelectedSlot
? `${formatDeliveryDate(effectiveSelectedSlot.date)} / ${effectiveSelectedSlot.time}`
: "";
- const heroDescription = getClientDeliveryHeroDescription(isActiveState, isChoiceSaved);
+ const heroDescription = getClientDeliveryHeroDescription(
+ isActiveState,
+ isChoiceSaved,
+ invitation?.deliveryType,
+ );
const handleSaveChoice = async () => {
if (!token) {
@@ -558,8 +578,10 @@ export const ClientDeliveryPage = () => {
{!isChoiceSaved ? (
- Доставка заказа
- Согласование доставки
+ {orderTypeEyebrow}
+
+ {orderIsPickup ? "Согласование самовывоза" : "Согласование доставки"}
+
{heroDescription ? (
{heroDescription}
@@ -591,7 +613,10 @@ export const ClientDeliveryPage = () => {
{(invitation?.pickupCode || invitation?.pickup_code) ? (
-
Код выдачи
+ {deliveryNumberDigits ? (
+
Ваш номер доставки {deliveryNumberDigits}
+ ) : null}
+
Ваш код выдачи
{invitation.pickupCode || invitation.pickup_code}
Покажите этот код при получении заказа
@@ -694,7 +719,10 @@ export const ClientDeliveryPage = () => {
{activeTab === TAB_PICKUP && (invitation?.pickupCode || invitation?.pickup_code) ? (
-
Код выдачи
+ {deliveryNumberDigits ? (
+
Ваш номер доставки {deliveryNumberDigits}
+ ) : null}
+
Ваш код выдачи
{invitation.pickupCode || invitation.pickup_code}
Покажите этот код при получении заказа
diff --git a/src/pages/ClientDeliveryPage.test.js b/src/pages/ClientDeliveryPage.test.js
index 8b7ab68..5206058 100644
--- a/src/pages/ClientDeliveryPage.test.js
+++ b/src/pages/ClientDeliveryPage.test.js
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
-import { getInvitationReferenceLabel } from "../components/client/invitationReference";
+import { getDeliveryNumberDigits, getInvitationReferenceLabel } from "../components/client/invitationReference";
import {
buildDeliveryConfirmationPayload,
buildSelectedSlotFromInvitation,
@@ -176,7 +176,16 @@ describe("ClientDeliveryPage helpers", () => {
{ name: "СФ Т\\ЕА-28700", quantity: "" },
],
}),
- ).toBe("Счета: СФ Т\\ЕА-28687, СФ Т\\ЕА-28700");
+ ).toBe("Доставки: СФ ТЕА-28687, СФ ТЕА-28700");
+ });
+
+ it("берёт номер доставки из состава заказа: «ТП\\-010796» → «010796»", () => {
+ expect(
+ getDeliveryNumberDigits({ orderItems: [{ name: "ТП\\-010796", quantity: "" }] }),
+ ).toBe("010796");
+ expect(getDeliveryNumberDigits({ orderNumbers: ["ТП\\-010557", "ТП\\-010599"] })).toBe("010557");
+ expect(getDeliveryNumberDigits({ orderNumber: "9788324113|28.09.2026" })).toBe("978832411328092026");
+ expect(getDeliveryNumberDigits({})).toBe("");
});
it("hides the hero helper text after the client saves the choice", () => {
@@ -189,6 +198,16 @@ describe("ClientDeliveryPage helpers", () => {
);
});
+ it("говорит про самовывоз, если отгрузка самовывозом", () => {
+ expect(getClientDeliveryHeroDescription(true, false, "pickup")).toBe(
+ "Вам предложены варианты самовывоза. Выберите удобную дату и время.",
+ );
+ expect(getClientDeliveryHeroDescription(false, false, "pickup")).toBe(
+ "По этому заказу согласование самовывоза завершено или передано логисту.",
+ );
+ expect(getClientDeliveryHeroDescription(true, true, "pickup")).toBe("");
+ });
+
it("skips Sunday when generating fallback delivery slots from Friday", () => {
// 2026-04-17 is Friday → next workday = Saturday 18, then Monday 20 (skip Sunday 19)
const slots = groupSlotsFromInvitation(
diff --git a/supabase/functions/_shared/delivery-invitations.ts b/supabase/functions/_shared/delivery-invitations.ts
index 1637dc0..33186eb 100644
--- a/supabase/functions/_shared/delivery-invitations.ts
+++ b/supabase/functions/_shared/delivery-invitations.ts
@@ -242,6 +242,8 @@ export type OrderGroupInvitationSource = {
delivery_status?: string | null;
delivery_link?: string | null;
source_orders?: unknown[] | null;
+ delivery_type?: string | null;
+ pickup_code?: string | null;
};
export const isInvitationExpired = (invitation: DeliveryInvitationRecord, now = new Date()) => {
if (invitation.revoked_at) {
@@ -325,6 +327,11 @@ export const buildPublicOrderGroupInvitationView = (
customerName: maskCustomerName(customerName),
customerPhone: maskPhoneNumber(customerPhone),
orderItems,
+ // Тип отгрузки и код выдачи — клиентская страница показывает «самовывоз»
+ // и номер доставки с кодом выдачи по этим полям.
+ deliveryType: group.delivery_type || null,
+ pickupCode: group.pickup_code || null,
+ orderNumbers,
availableSlots: invitation.available_slots || [],
deliveryDate: invitation.delivery_date || null,
deliveryTime: invitation.delivery_time || null,
diff --git a/supabase/sms_pickup_code_placeholders.sql b/supabase/sms_pickup_code_placeholders.sql
new file mode 100644
index 0000000..0f7ccc2
--- /dev/null
+++ b/supabase/sms_pickup_code_placeholders.sql
@@ -0,0 +1,33 @@
+-- Код выдачи и номер доставки в SMS для самовывоза
+-- 2026-09-28
+--
+-- Переменные шаблона (sms_text_template / sms_text_template_pickup):
+-- {link} / {ссылка} — ссылка согласования
+-- {order} / {номер} — номер доставки без префикса (010796)
+-- {code} / {код} — код выдачи (7598)
+-- Подстановку делает скрипт кампании (apply_sms_placeholders).
+
+UPDATE sms_campaign_settings SET
+ sms_text_template_pickup = 'Ваш заказ готов. Номер доставки {order}, код выдачи {code}. Согласуйте дату самовывоза по ссылке: {link}'
+WHERE campaign_type = 'first_sms';
+
+UPDATE sms_campaign_settings SET
+ sms_text_template_pickup = 'Напоминаем: номер доставки {order}, код выдачи {code}. Выберите дату самовывоза по ссылке: {link}'
+WHERE campaign_type = 'second_sms';
+
+UPDATE sms_campaign_settings SET
+ sms_text_template_pickup = 'Заказ переведён на платное хранение. Номер доставки {order}, код выдачи {code}. Заберите со склада или согласуйте дату самовывоза: {link}'
+WHERE campaign_type = 'paid_storage';
+
+-- Текст самовывоза для городов в режиме «Уведомление» (сейчас Ялта):
+-- переменные работают и в профиле города.
+UPDATE sms_city_profiles SET
+ pickup_text = 'Ваш заказ готов. Номер доставки {order}, код выдачи {code}. Заберите со склада.',
+ updated_at = NOW()
+WHERE pickup_text IS NOT NULL
+ AND pickup_text <> ''
+ AND pickup_text NOT LIKE '%{code}%';
+
+-- Проверка:
+-- SELECT campaign_type, sms_text_template_pickup FROM sms_campaign_settings ORDER BY 1;
+-- SELECT city, pickup_text FROM sms_city_profiles;