Files
aura-crm/production

production

Модуль платформы сервисного центра (Севастополь): приём в ремонт — клиенты (физ/юр), заказы, статусы, гарантия, публичный трекинг, фото/комментарии по заказу. Часть модульной архитектуры — see core (service-center repo) для реестра модулей и общей staff-идентичности.

Этот сервис не имеет своего логина. Staff JWT выпускает core; production только верифицирует токен тем же JWT_SECRET (HS256, общий секрет).

Домены

  • clients — физ/юр клиенты
  • orders — заказы в ремонт: статус (new → diagnosing → in_repair → waiting_parts → ready → completed, либо cancelled), гарантия (warranty_until + original_order_id — связь повторного ремонта с исходным), назначенный мастер, оценка/итоговая цена
  • order_events — таймлайн заказа: смена статуса (всегда публична), комментарии и фото (публичность — is_public, по умолчанию комментарии приватные, фото публичные)

API

Staff-эндпоинты (Authorization: Bearer <JWT из core>):

POST   /api/clients
GET    /api/clients?q=
GET    /api/clients/:id          — + история заказов

POST   /api/orders
GET    /api/orders?status=&assigned_master_id=&client_id=   — Kanban-лента (плоский список, группировка на фронте)
GET    /api/orders/:id           — + полный таймлайн
PATCH  /api/orders/:id           — мастер/гарантия/цены, без записи в таймлайн
PATCH  /api/orders/:id/status    — пишет событие в таймлайн
POST   /api/orders/:id/comments  — {body, is_public}
POST   /api/orders/:id/photos    — multipart, {file, is_public}

Публичный (без авторизации, токен = ключ доступа):

GET /api/track/:token   — статус, устройство, гарантия, только публичные события

GET /api/files/:key (staff-authenticated) отдаёт файл целиком (не стримом — SendStream/SetBodyStream в fasthttp читает тело уже после возврата хендлера, так что defer obj.Close() гонялся бы с чтением; файлы ≤20MB, буферизация в память безопасна). <img src> не может послать Bearer-токен, поэтому фронтенд грузит фото через fetch и URL.createObjectURL.

GET /api/orders/:id/invoice.pdf   — Счёт на оплату
GET /api/orders/:id/act.pdf       — Акт выполненных работ

Оба — staff-authenticated, PDF генерируется на лету через internal/pdfgen (github.com/go-pdf/fpdf, кириллица — встроенные через go:embed шрифты DejaVu Sans, не читаются с диска в рантайме). Реквизиты бизнеса — из BUSINESS_* env (все опциональны — юрлицо ещё не зарегистрировано, пустые поля рендерятся как «_______», а не падают/пустая строка). Различают физ/юр клиента (ИНН/КПП/адрес только для type=company). Номер документа детерминированно из первых 8 hex символов order id (нет БД-последовательности). <a href> не может послать Bearer-токен — фронтенд фетчит PDF как blob и открывает в новой вкладке (api.orders.openDocument, тот же приём, что и для фото). Лимиты длины на все поля, которые попадают в PDF (client/order Create/Update) — иначе MultiCell заворачивает произвольно длинный текст в произвольно длинный документ (реальный DoS-вектор, найден security-reviewer).

Фронтенд (web/)

React + Vite, UI перенесён из Glass CRM (Kanban на @dnd-kit, карточки, модалки) и адаптирован под fetch вместо supabase.from(...). Два бэкенда: core (только /api/auth/login) и production (всё остальное) — оба URL через VITE_CORE_URL/VITE_PRODUCTION_URL (compile-time, Vite их запекает в бандл).

cd web && cp .env.example .env && npm install && npm run dev

Страницы: /login, /kanban, /clients, /track/:token (публичная). JWT хранится в localStorage, auth — React Context (AuthContext.jsx), не голый хук — иначе каждый вызывающий компонент завёл бы свою копию состояния.

Регистрация в core

# на core, как owner:
curl -X POST $CORE_URL/api/modules -H "Authorization: Bearer $OWNER_JWT" \
  -d '{"name":"production","base_url":"http://production:3000","health_path":"/health"}'
# → сохранить token в MODULE_TOKEN здесь

После старта сервис сам шлёт heartbeat в core каждые 30с (internal/coreclient). Без CORE_URL/MODULE_NAME/MODULE_TOKEN просто не регистрируется — сервис работает автономно, core не жёсткая зависимость.

Разработка

cp .env.example .env   # JWT_SECRET должен совпадать с core/.env!
docker compose up -d --build
curl http://127.0.0.1:18091/health

backend подключён к внешней сети platform_net (создаётся один раз: docker network create platform_net) — так production достаёт core по алиасу core, а core, если понадобится, достанет production по production. Порт наружу — 127.0.0.1:18091, как и у core, через хостовый Caddy.

Пока не сделано

  • Заправка картриджей (Фаза 3) — отдельный модуль или расширение этого
  • Публичный трекинг не показывает сами фото (только факт «добавлено») — /api/files/:key требует staff JWT, публичного скоупа под токен трекинга нет
  • Kanban во фронтенде без realtime — поллинг раз в 20с (нет WebSocket в Фазе 1)
  • Документы: реальных реквизитов бизнеса ещё нет (юрлицо не зарегистрировано, BUSINESS_* пустые), нет ролевого разграничения — любой авторизованный staff может сгенерировать документ по любому заказу (существующая во всём API норма, не регрессия этой фичи — auth.RequireRole есть в коде, но нигде не подключён; если нужно разделение ролей — отдельная задача на весь API)