Tenant, роли и тикеты
Создайте REST helpdesk для нескольких компаний. Локальный seed создаёт customer, agent, team_lead и tenant_admin в двух tenant; фиксированные токены читаются только из Authorization header, actor и tenant определяются по токену. Каждые customer, team, agent, ticket, message и SLA calendar имеют tenant_id. Роли customer, agent, team_lead и tenant_admin определяют действия: customer создаёт тикет и читает свои тикеты и публичные сообщения; agent читает тикеты назначенной команды, меняет статус и пишет customer-visible или internal сообщение; team_lead управляет назначением, приоритетом и командным календарём; tenant_admin настраивает участников, SLA и календарь компании. Любая выборка, счётчик и вложенный маршрут фильтруются по проверенному tenant и доступу пользователя. Чужой или скрытый ресурс отвечает одинаковым 404.
Ticket содержит subject 1–160 символов, priority low/normal/high/urgent, status open/pending/resolved/closed, requester, team, assignee, created_at и revision. Сообщение содержит body 1–5000 символов, visibility public/internal, автора и время; HTML хранится как обычный текст и не исполняется. Customer не может запросить internal сообщения, даже задав visibility в query. Правила переходов: open -> pending/resolved; pending -> open/resolved; resolved -> closed/open; closed терминален. Новое публичное сообщение клиента переводит resolved в open, но не меняет closed. PATCH тикета требует expected_revision; при несовпадении возвращается 409 stale_revision с текущим номером. GET/POST /api/v1/tickets, GET/PATCH /tickets/{id}, GET/POST /tickets/{id}/messages и GET /api/v1/reports/sla реализуют эти действия.
SLA и календарь
Календарь задаётся IANA timezone, рабочими днями и локальными интервалами. SLA first_response равен 4 рабочим часам для urgent, 8 для high и 24 для normal/low. Решение о выполнении — время первого публичного сообщения агента. Пауза ticket pending останавливает SLA, возврат в open продлевает deadline на точную длительность паузы. Остаток рассчитывается по снимку календаря, который действовал при открытии тикета. Выходные и DST учитываются согласно timezone tenant; несуществующее локальное время пропускается, неоднозначный час использует более ранний offset. Изменение календаря действует только для тикетов, открытых после изменения; deadline не пересчитывается по новому календарю. GET ticket показывает UTC deadline и исходную timezone.
Напоминание о просрочке создаётся один раз на ticket и SLA deadline. Исполнитель (worker) сохраняет контрольную позицию и запись outbox в PostgreSQL; локальная имитация почтового получателя принимает ключ идемпотентности. Максимум 200 просрочек за партию; после перезапуска исполнитель повторяет незавершённую партию. Внешняя доставка может повториться, exactly-once не обещается. Все SQL операции имеют context timeout 3 секунды.
Структура программы
Предметные правила задают видимость сообщений, переходы тикета, календарь и паузу SLA; HTTP и SQL отдельно отвечают за транспорт и хранение. main.go только загружает конфигурацию, связывает зависимости и управляет запуском. Пакеты без циклов, общих utils и интерфейсов без потребителя. README показывает зависимости; тесты ядра без HTTP/БД, интеграционные отдельно.
Аудит и приёмка
Изменения статуса, назначения, priority, календаря, участников и видимости сообщения атомарно создают audit row с actor, tenant, объектом и временем. PostgreSQL, ручной SQL и миграции обязательны. Тестовая база имеет две компании, разных команд и клиентов; тесты просят customer прочитать internal сообщение, чужой ticket, отчёт и счётчик. Другие проверки покрывают все переходы, stale revision, business-hours расчёт через DST, pause/resume SLA, смену календаря и остановку исполнителя до и после deadline. Перезапуск не теряет долг и не создаёт второй внутренний факт напоминания. Критерии готовности: customer никогда не видит internal текст; неправильный tenant не раскрывается; дедлайн для фиксированных часов соответствует календарю; аудит и изменение тикета фиксируются одной транзакцией.