# Улучшение системы оповещений — План реализации

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Исправить дублирование, добавить недостающие лейблы, расширить текст оповещений и починить авторизационный код в Telegram.

**Architecture:** Изменения затрагивают shared-константы, серверные шаблоны и роуты, а также клиентский UI. Новая миграция Prisma. Новое событие `order:deliveryFeeAdjusted`.

**Tech Stack:** Fastify + Prisma + SQLite, React + Vite + MUI, shared-константы.

**Build order (из AGENTS.md):**
```bash
cd server && npm run db:migrate   # если schema.prisma изменён
cd server && npm test             # серверные тесты сначала
cd client && npm run lint && npm run format:check && npm test  # потом клиент
cd client && npm run build        # полная проверка типов + сборка
```

---

### Task 1: Починить dispatchNotification для AUTH_CODE_REQUESTED

**Файлы:**
- Modify: `server/src/index.js`

- [ ] **Step 1: Изменить dispatchNotification для AUTH_CODE_REQUESTED**

В `server/src/index.js`, внутри `dispatchNotification`, добавить отдельную ветку для `AUTH_CODE_REQUESTED`, использующую `resolveAuthCodeTargets`:

```js
async function dispatchNotification(eventType, payload) {
  if (eventType === AUTH_CODE_REQUESTED) {
    const targets = await resolveAuthCodeTargets(eventType, payload);
    for (const target of targets) {
      const log = await prisma.notificationLog.create({
        data: {
          userId: payload.userId,
          eventType,
          channel: target.channel,
          status: 'pending',
          payload: JSON.stringify(payload),
        },
      });
      notificationQueue.enqueue({ ...target, eventType, payload, logId: log.id });
    }
    return;
  }

  const userTargets = await resolveUserNotificationTargets(eventType, payload);
  // ... остальной код без изменений
```

- [ ] **Step 2: Запустить серверные тесты**

Run: `cd server && npm test`
Expected: все тесты проходят.

- [ ] **Step 3: Проверить линтер**

Run: `cd server && npm run lint`
Expected: no errors (линтера на сервере может не быть, просто проверить что файл валидный JS)

---

### Task 2: Убрать дублирование new order для админа

**Файлы:**
- Modify: `server/src/lib/notifications/preferences.js`

- [ ] **Step 1: Удалить ORDER_CREATED из adminEventFieldMap**

```js
const adminEventFieldMap = {
  [ORDER_MESSAGE_SENT]: 'newOrderMessage',
  'review:created': 'newReview',
};
```

- [ ] **Step 2: Запустить тесты**

Run: `cd server && npm test`

---

### Task 3: Добавить PAID в лейблы статусов

**Файлы:**
- Modify: `server/src/lib/notifications/templates/telegram-templates.js`
- Modify: `server/src/lib/notifications/templates/email-templates.js`

- [ ] **Step 1: Добавить PAID в telegram-templates.js**

```js
export function renderOrderStatusChangedTg({ orderId, oldStatus, newStatus }) {
  const labels = {
    DRAFT: 'Черновик', PENDING_PAYMENT: 'Ожидает оплаты', PAID: 'Оплачен', IN_PROGRESS: 'В работе',
    READY_FOR_PICKUP: 'Готов к выдаче', SHIPPED: 'Отправлен', DONE: 'Выполнен', CANCELLED: 'Отменён',
  }
  return `🔄 Заказ #${orderId.slice(0, 8)}\n${labels[oldStatus] || oldStatus} → <b>${labels[newStatus] || newStatus}</b>`
}
```

- [ ] **Step 2: Добавить PAID в email-templates.js**

```js
const statusLabels = {
  DRAFT: "Черновик",
  PENDING_PAYMENT: "Ожидает оплаты",
  PAID: "Оплачен",
  IN_PROGRESS: "В работе",
  READY_FOR_PICKUP: "Готов к выдаче",
  SHIPPED: "Отправлен",
  DONE: "Выполнен",
  CANCELLED: "Отменён",
};
```

- [ ] **Step 3: Запустить тесты**

Run: `cd server && npm test`

---

### Task 4: Добавить deliveryType в ORDER_CREATED payload и обновить шаблоны пользователя

**Файлы:**
- Modify: `server/src/routes/user-orders.js`
- Modify: `server/src/lib/notifications/templates/telegram-templates.js`
- Modify: `server/src/lib/notifications/templates/email-templates.js`

- [ ] **Step 1: Добавить deliveryType в ORDER_CREATED payload**

```js
request.server.eventBus.emit(NOTIFICATION_EVENTS.ORDER_CREATED, {
  orderId: created.id,
  userId,
  totalCents: created.totalCents,
  itemsCount: cartItems.length,
  deliveryType: created.deliveryType,
});
```

- [ ] **Step 2: Обновить renderOrderCreatedTg**

```js
export function renderOrderCreatedTg({ orderId, totalCents, itemsCount, deliveryType }) {
  const total = (totalCents / 100).toLocaleString('ru-RU')
  const nextAction = deliveryType === 'delivery'
    ? 'Оплата будет доступна после уточнения стоимости доставки.'
    : 'Ожидает оплаты.'
  return `📦 <b>Новый заказ</b> #${orderId.slice(0, 8)}\nТоваров: ${itemsCount} | Сумма: ${total} ₽\n${nextAction}`
}
```

- [ ] **Step 3: Обновить renderOrderCreatedEmail**

```js
export function renderOrderCreatedEmail({ orderId, totalCents, itemsCount, deliveryType }) {
  const total = (totalCents / 100).toLocaleString("ru-RU");
  const nextAction = deliveryType === "delivery"
    ? "Оплата будет доступна после уточнения стоимости доставки."
    : "Ожидает оплаты.";
  const body = `
    <p>Ваш заказ <b>#${orderId.slice(0, 8)}</b> успешно создан.</p>
    <p>Товаров: ${itemsCount} | Сумма: <b>${total} ₽</b></p>
    <p>${nextAction}</p>
  `;
  return { subject: "Заказ создан", html: baseLayout("Заказ создан", body) };
}
```

- [ ] **Step 4: Запустить тесты**

Run: `cd server && npm test`

---

### Task 5: Добавить deliveryType в order:created:admin payload и обновить шаблоны админа

**Файлы:**
- Modify: `server/src/routes/user-orders.js`
- Modify: `server/src/lib/notifications/templates/telegram-templates.js`
- Modify: `server/src/lib/notifications/templates/email-templates.js`

- [ ] **Step 1: Добавить deliveryType в order:created:admin payload**

```js
request.server.eventBus.emit('order:created:admin', {
  orderId: created.id,
  userId,
  userEmail: request.user.email || '',
  totalCents: created.totalCents,
  itemsCount: cartItems.length,
  deliveryType: created.deliveryType,
});
```

- [ ] **Step 2: Обновить renderAdminOrderCreatedTg**

```js
export function renderAdminOrderCreatedTg({ orderId, userEmail, totalCents, itemsCount, deliveryType }) {
  const total = (totalCents / 100).toLocaleString('ru-RU')
  const note = deliveryType === 'delivery' ? '\n\n⚠️ Скорректируйте стоимость доставки' : ''
  return `🛒 <b>Новый заказ</b> #${orderId.slice(0, 8)}\nОт: ${userEmail}\nТоваров: ${itemsCount} | Сумма: ${total} ₽${note}`
}
```

- [ ] **Step 3: Обновить renderAdminOrderCreatedEmail**

```js
export function renderAdminOrderCreatedEmail({
  orderId,
  userEmail,
  totalCents,
  itemsCount,
  deliveryType,
}) {
  const total = (totalCents / 100).toLocaleString("ru-RU");
  const note = deliveryType === "delivery"
    ? '<p>⚠️ <b>Скорректируйте стоимость доставки</b> в админ-панели.</p>'
    : "";
  const body = `
    <p>Новый заказ <b>#${orderId.slice(0, 8)}</b> от <b>${userEmail}</b>.</p>
    <p>Товаров: ${itemsCount} | Сумма: <b>${total} ₽</b></p>
    ${note}
  `;
  return { subject: "Новый заказ", html: baseLayout("Новый заказ", body) };
}
```

- [ ] **Step 4: Запустить тесты**

Run: `cd server && npm test`

---

### Task 6: Добавить новое событие order:deliveryFeeAdjusted — Shared и Prisma

**Файлы:**
- Modify: `shared/constants/notification-events.js`
- Modify: `shared/constants/notification-events.d.ts`
- Modify: `server/prisma/schema.prisma`
- Modify: `server/src/lib/notifications/preferences.js`
- Modify: `server/src/routes/user/notifications.js`
- Modify: `server/src/index.js`
- Modify: `server/src/routes/api/admin-orders.js`

- [ ] **Step 1: Добавить DELIVERY_FEE_ADJUSTED в shared/constants/notification-events.js**

```js
export const NOTIFICATION_EVENTS = {
  ORDER_CREATED: 'order:created',
  ORDER_STATUS_CHANGED: 'order:statusChanged',
  ORDER_MESSAGE_SENT: 'orderMessage:sent',
  ORDER_MESSAGE_ADMIN_REPLY: 'orderMessage:adminReply',
  PAYMENT_STATUS_CHANGED: 'payment:statusChanged',
  AUTH_CODE_REQUESTED: 'auth:codeRequested',
  DELIVERY_FEE_ADJUSTED: 'order:deliveryFeeAdjusted',
}
```

- [ ] **Step 2: Добавить тип в shared/constants/notification-events.d.ts**

```ts
export type NotificationEventType =
  | 'order:created'
  | 'order:statusChanged'
  | 'orderMessage:sent'
  | 'orderMessage:adminReply'
  | 'payment:statusChanged'
  | 'auth:codeRequested'
  | 'order:deliveryFeeAdjusted'
```

И добавить в объект:
```ts
export const NOTIFICATION_EVENTS: {
  ORDER_CREATED: NotificationEventType
  ORDER_STATUS_CHANGED: NotificationEventType
  ORDER_MESSAGE_SENT: NotificationEventType
  ORDER_MESSAGE_ADMIN_REPLY: NotificationEventType
  PAYMENT_STATUS_CHANGED: NotificationEventType
  AUTH_CODE_REQUESTED: NotificationEventType
  DELIVERY_FEE_ADJUSTED: NotificationEventType
}
```

- [ ] **Step 3: Добавить поле deliveryFeeAdjusted в Prisma schema**

```prisma
model NotificationPreference {
  id                     String   @id @default(cuid())
  userId                 String   @unique
  globalEnabled          Boolean  @default(true)
  orderCreated           Boolean  @default(true)
  orderStatusChanged     Boolean  @default(true)
  orderMessageReceived   Boolean  @default(true)
  paymentStatusChanged   Boolean  @default(true)
  deliveryFeeAdjusted    Boolean  @default(true)
  createdAt              DateTime @default(now())
  updatedAt              DateTime @updatedAt

  user                   User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}
```

- [ ] **Step 4: Создать миграцию Prisma**

Run: `cd server && npx prisma migrate dev --name add_delivery_fee_adjusted --create-only`

Затем применить: `cd server && npx prisma migrate deploy`

- [ ] **Step 5: Добавить DELIVERY_FEE_ADJUSTED в userEventFieldMap**

```js
const userEventFieldMap = {
  [ORDER_CREATED]: 'orderCreated',
  [ORDER_STATUS_CHANGED]: 'orderStatusChanged',
  [ORDER_MESSAGE_ADMIN_REPLY]: 'orderMessageReceived',
  [PAYMENT_STATUS_CHANGED]: 'paymentStatusChanged',
  [DELIVERY_FEE_ADJUSTED]: 'deliveryFeeAdjusted',
};
```

Импорт в `preferences.js`:
```js
const {
  ORDER_CREATED,
  ORDER_STATUS_CHANGED,
  ORDER_MESSAGE_SENT,
  ORDER_MESSAGE_ADMIN_REPLY,
  PAYMENT_STATUS_CHANGED,
  AUTH_CODE_REQUESTED,
  DELIVERY_FEE_ADJUSTED,
} = NOTIFICATION_EVENTS;
```

- [ ] **Step 6: Добавить поле deliveryFeeAdjusted в API пользователя**

В `server/src/routes/user/notifications.js`:
```js
if ('deliveryFeeAdjusted' in body) data.deliveryFeeAdjusted = Boolean(body.deliveryFeeAdjusted)
```

- [ ] **Step 7: Добавить эмит события при корректировке стоимости доставки**

В `server/src/routes/api/admin-orders.js`, в конце `PATCH /:id/delivery-fee`:

```js
request.server.eventBus.emit(NOTIFICATION_EVENTS.DELIVERY_FEE_ADJUSTED, {
  orderId: updated.id,
  userId: existing.userId,
  totalCents: updated.totalCents,
});
```

Добавить импорт `NOTIFICATION_EVENTS` (уже есть в файле).

- [ ] **Step 8: Зарегистрировать событие в dispatchNotification**

В `server/src/index.js`, в секцию eventBus.on:

```js
eventBus.on(DELIVERY_FEE_ADJUSTED, (payload) => dispatchNotification(DELIVERY_FEE_ADJUSTED, payload));
```

Добавить в деструктуризацию:
```js
const {
  ORDER_CREATED,
  ORDER_STATUS_CHANGED,
  ORDER_MESSAGE_SENT,
  ORDER_MESSAGE_ADMIN_REPLY,
  PAYMENT_STATUS_CHANGED,
  AUTH_CODE_REQUESTED,
  DELIVERY_FEE_ADJUSTED,
} = NOTIFICATION_EVENTS;
```

- [ ] **Step 9: Запустить тесты**

Run: `cd server && npm test`

---

### Task 7: Добавить рендереры для deliveryFeeAdjusted

**Файлы:**
- Modify: `server/src/lib/notifications/templates/telegram-templates.js`
- Modify: `server/src/lib/notifications/templates/email-templates.js`
- Modify: `server/src/lib/notifications/channels/telegram-channel.js`
- Modify: `server/src/lib/notifications/channels/email-channel.js`

- [ ] **Step 1: Добавить renderDeliveryFeeAdjustedTg**

В `telegram-templates.js`:
```js
export function renderDeliveryFeeAdjustedTg({ orderId, totalCents }) {
  const total = (totalCents / 100).toLocaleString('ru-RU')
  return `💰 <b>Стоимость доставки скорректирована</b> для заказа #${orderId.slice(0, 8)}\nНовая сумма: ${total} ₽\n\nОжидает оплаты.`
}
```

- [ ] **Step 2: Зарегистрировать в telegram-channel.js**

```js
import {
  renderOrderCreatedTg,
  renderOrderStatusChangedTg,
  renderOrderMessageTg,
  renderPaymentStatusChangedTg,
  renderAdminOrderCreatedTg,
  renderAdminNewReviewTg,
  renderAuthCodeTg,
  renderDeliveryFeeAdjustedTg,
} from '../templates/telegram-templates.js'

const templateRenderers = {
  'order:created': renderOrderCreatedTg,
  'order:statusChanged': renderOrderStatusChangedTg,
  'orderMessage:adminReply': renderOrderMessageTg,
  'payment:statusChanged': renderPaymentStatusChangedTg,
  'order:created:admin': renderAdminOrderCreatedTg,
  'orderMessage:sent': renderOrderMessageTg,
  'review:created': renderAdminNewReviewTg,
  'auth:codeRequested': renderAuthCodeTg,
  'order:deliveryFeeAdjusted': renderDeliveryFeeAdjustedTg,
}
```

- [ ] **Step 3: Добавить renderDeliveryFeeAdjustedEmail**

В `email-templates.js`:
```js
export function renderDeliveryFeeAdjustedEmail({ orderId, totalCents }) {
  const total = (totalCents / 100).toLocaleString("ru-RU");
  const body = `
    <p>Стоимость доставки заказа <b>#${orderId.slice(0, 8)}</b> скорректирована.</p>
    <p>Новая сумма: <b>${total} ₽</b></p>
    <p>Ожидает оплаты. Проверьте статус заказа в личном кабинете.</p>
  `;
  return {
    subject: "Стоимость доставки скорректирована",
    html: baseLayout("Стоимость доставки скорректирована", body),
  };
}
```

- [ ] **Step 4: Зарегистрировать в email-channel.js**

```js
import {
  renderOrderCreatedEmail,
  renderOrderStatusChangedEmail,
  renderOrderMessageEmail,
  renderPaymentStatusChangedEmail,
  renderAdminOrderCreatedEmail,
  renderAdminNewReviewEmail,
  renderAuthCodeEmail,
  renderDeliveryFeeAdjustedEmail,
} from '../templates/email-templates.js'

const templateRenderers = {
  'order:created': renderOrderCreatedEmail,
  'order:statusChanged': renderOrderStatusChangedEmail,
  'orderMessage:adminReply': renderOrderMessageEmail,
  'payment:statusChanged': renderPaymentStatusChangedEmail,
  'order:created:admin': renderAdminOrderCreatedEmail,
  'orderMessage:sent': renderOrderMessageEmail,
  'review:created': renderAdminNewReviewEmail,
  'auth:codeRequested': renderAuthCodeEmail,
  'order:deliveryFeeAdjusted': renderDeliveryFeeAdjustedEmail,
}
```

- [ ] **Step 5: Запустить тесты**

Run: `cd server && npm test`

---

### Task 8: Добавить переключатель deliveryFeeAdjusted в UI пользователя

**Файлы:**
- Modify: `client/src/pages/me/ui/sections/NotificationsPage.tsx`

- [ ] **Step 1: Добавить переключатель в NotificationsPage.tsx**

```tsx
const eventFields = [
  { key: 'orderCreated' as const, label: 'Заказ создан' },
  { key: 'orderStatusChanged' as const, label: 'Изменение статуса заказа' },
  { key: 'orderMessageReceived' as const, label: 'Сообщение в чате заказа' },
  { key: 'paymentStatusChanged' as const, label: 'Изменение статуса оплаты' },
  { key: 'deliveryFeeAdjusted' as const, label: 'Корректировка стоимости доставки' },
]
```

- [ ] **Step 2: Проверить линтер и формат**

Run: `cd client && npm run lint && npm run format:check`

---

### Task 9: Финальная проверка

- [ ] **Step 1: Запустить все серверные тесты**

Run: `cd server && npm test`

- [ ] **Step 2: Запустить все клиентские проверки**

Run: `cd client && npm run lint && npm run format:check && npm test`

- [ ] **Step 3: Полная сборка клиента**

Run: `cd client && npm run build`
