Данные и доступ
Реализуйте REST CRM с организациями, пользователями, клиентами и сделками. Организация tenant имеет неизменяемый ID. Членство пользователя в организации содержит одну роль: owner, manager или reader. Owner управляет членствами и настройками, создаёт, читает, меняет, архивирует и экспортирует записи. Manager создаёт и меняет клиентов и сделки, читает записи и аудит, но не меняет членства, квоты и настройки. Reader читает записи и аудит, не меняя их и не экспортируя. Каждая запись содержит tenant_id; сервер берёт текущий tenant из проверенного членства, а не из тела запроса. Для каждой операции, включая get-by-ID, поиск, count, вложенные ресурсы и экспорт, tenant-фильтр обязателен. Чужая запись и отсутствующая запись одинаково отвечают 404.
REST-контракт
POST /api/v1/organizations создаёт организацию и её владельца. POST/GET /api/v1/organizations/{org_id}/members и PATCH/DELETE для member ID управляют членствами только owner. POST/GET /api/v1/organizations/{org_id}/clients, GET/PATCH/DELETE /clients/{client_id}, и соответствующие /deals маршруты управляют клиентами и сделками. GET /clients/{id}/deals показывает только связанные записи текущего tenant. GET /audit и POST /exports доступны owner; manager читает audit. Все списки имеют limit 1–100, курсор по ключу; сортировка по created_at DESC, id DESC. Клиент содержит display_name (1–160 символов), email optional и status active/archived. Сделка содержит client_id, title (1–160), amount_minor, currency RUB/USD/EUR, stage lead/qualified/proposal/won/lost и expected_close_date. Сумма неотрицательна, валюта не меняется после won, client должен быть из той же организации.
Квоты, аудит и транзакции
По умолчанию tenant ограничен 10 активными пользователями и 10 000 незакрытыми клиентами плюс сделками вместе; owner может видеть usage и текущие пределы через GET /settings/usage. При превышении лимита возвращается 409 quota_exceeded. Параллельное создание последнего разрешённого объекта должно сериализовать проверку и изменение квоты так, чтобы успешное число не превышало предел. Архивация освобождает quota slot, но не удаляет историю. Удаление клиента с незакрытой сделкой возвращает 409 open_deals_exist; после закрытия сделки клиент архивируется. Экспорт включает только активные поля текущего tenant, максимум 5000 строк, и создаёт запись аудита.
Каждое изменение пишет actor, tenant, действие, объект, время UTC и список изменённых имён полей в audit_log. Данные и аудит фиксируются одной транзакцией. Полные email и другие значения не копируются в аудит. PostgreSQL используется через SQL без ORM и миграции; FK включают tenant ID, индексы начинаются с tenant_id. Контекст каждого запроса задаёт SQL timeout 3 секунды. Локальный seed создаёт owner, manager и reader для каждой организации с фиксированными токенами; token принимается только из Authorization header, actor и tenant выводятся из него, секреты не логируются. Ошибка БД превращается в безопасный 500.
Структура программы
Предметные правила проверяют роли, квоты, связи и аудит отдельно от HTTP-обработчиков и SQL-запросов; обе организации проходят через единые правила доступа. main.go только загружает конфигурацию, связывает зависимости и управляет запуском. Пакеты без циклов, общих utils и интерфейсов без потребителя. README показывает зависимости; тесты ядра без HTTP/БД, интеграционные отдельно.
Приёмка
Офлайн-тесты создают две организации, повторяющиеся display name и пользователей всех ролей. Они пытаются читать, менять, удалять, считать и экспортировать чужие записи через каждый маршрут. Тесты подтверждают матрицу ролей, 404 без утечки существования, привязку клиента и сделки к одному tenant, атомарность аудита и квоты в конкурентном создании. Критерии готовности: ни одно чужое значение не появляется в API/export; каждое успешное изменение имеет одну запись аудита; при гонке квоты не превышены и отказ не оставляет частичную запись.