-- 0005_project_invitations — приглашения мастеров в проект. -- -- Раньше POST /projects/:id/workers сразу давало членство. Теперь это -- ровно два шага и без обходных путей: -- 1) клиент/админ отправляет приглашение — строка получает 'invited' -- и срок жизни expires_at (7 дней); -- 2) мастер сам решает: accept → 'active' (и только тогда получает -- заявки проекта) или decline → 'declined' (повторное приглашение -- перезапускает ту же строку). -- Принудительного членства (force) нет: активным мастер становится -- только своим решением. -- -- Статусы строки project_workers: -- invited — ждём решения мастера (expires_at обязателен, см. CHECK); -- active — мастер принял: назначение заявок разрешено; -- declined — мастер отказал; строка остаётся как история отказа -- и допускает повторное приглашение. -- -- Строки, созданные до этой миграции, получают DEFAULT 'active' — -- членство существующих мастеров не ломается. -- -- Принцип прежний: переходы проверяются в самом SQL: -- * назначение заявки требует членства status='active' — приглашённый -- не получает заявки, пока не принял (см. Assign в repository/ticket.go); -- * accept/decline двигают только строку в статусе 'invited'; -- * уход мастера (DELETE) снимает строку любого статуса — так же -- отзывается и не принятое приглашение. -- -- Применённый файл не редактировать (checksum в schema_migrations). ALTER TABLE project_workers ADD COLUMN status text NOT NULL DEFAULT 'active', ADD COLUMN invited_by uuid REFERENCES profiles(id) ON DELETE SET NULL, ADD COLUMN invited_at timestamptz, ADD COLUMN responded_at timestamptz, ADD COLUMN expires_at timestamptz; ALTER TABLE project_workers ADD CONSTRAINT project_workers_status_check CHECK (status IN ('invited', 'active', 'declined')); -- У приглашения всегда есть пригласивший и срок; у обычного членства -- (включая строки до миграции) этих полей может не быть. ALTER TABLE project_workers ADD CONSTRAINT project_workers_invited_shape CHECK (status <> 'invited' OR (invited_at IS NOT NULL AND invited_by IS NOT NULL AND expires_at IS NOT NULL AND responded_at IS NULL)); -- Принятое/отклонённое приглашение фиксирует момент ответа. ALTER TABLE project_workers ADD CONSTRAINT project_workers_responded_shape CHECK (status = 'invited' OR responded_at IS NOT NULL OR invited_at IS NULL); -- строки до миграции без ответа -- Списки: мастера проекта (по статусу) и приглашения мастера (worker_id + срок). CREATE INDEX IF NOT EXISTS idx_project_workers_project_status ON project_workers (project_id, status); CREATE INDEX IF NOT EXISTS idx_project_workers_worker_status ON project_workers (worker_id, status, expires_at);