axe/internal/models/models.go
2026-10-18 22:07:25 +03:00

255 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package models — предметные сущности API. Только структуры и теги JSON,
// без логики и без зависимостей: ими пользуются handlers, repository и docs.
package models
import "time"
// Допустимые значения полей заявки. Это третий по счёту источник тех же
// списков (после binding oneof при создании и CHECK-ограничений в 0001):
// он нужен фильтрам списка, чтобы мусор в ?status= получал 400, а не
// пустую страницу. Меняете значения — правьте все три места.
var (
// TicketStatuses включает 'confirmed': такой статус разрешён
// констрейнтом схемы, хотя боевая машина статусов его не выставляет.
TicketStatuses = []string{"new", "assigned", "in_progress", "done", "confirmed", "rejected", "closed"}
TicketCategories = []string{"electric", "plumbing", "climate", "elevator", "other"}
TicketPriorities = []string{"low", "normal", "high"}
)
// Profile — пользователь: клиент, мастер или админ.
// PasswordHash помечен json:"-" и наружу никогда не уходит.
type Profile struct {
ID string `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"-"`
Name string `json:"name"`
Phone string `json:"phone,omitempty"`
Role string `json:"role"`
IsActive bool `json:"is_active"`
// ConfirmedAt — момент подтверждения аккаунта; nil (null в JSON)
// — ожидает подтверждения, вход до этого запрещён (403
// account not confirmed). Существующие пользователи подтверждены
// бэкфиллом миграции 0010.
ConfirmedAt *time.Time `json:"confirmed_at"`
CreatedAt time.Time `json:"created_at"`
}
// Project — проект клиента: контур, внутри которого живут объекты
// (здания с адресами) и мастера. Проект создаёт клиент или админ.
type Project struct {
ID int64 `json:"id"`
ClientID string `json:"client_id"`
Name string `json:"name"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
// Денормализация для UI: чей проект (админ видит все сразу).
ClientName string `json:"client_name,omitempty"`
}
// Object — объект, входящий в проект: здание/корпус с адресом.
// Адрес заявки копируется из объекта на момент создания заявки.
type Object struct {
ID int64 `json:"id"`
ProjectID int64 `json:"project_id"`
Name string `json:"name"`
Address string `json:"address"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
// Денормализации для UI.
ProjectName string `json:"project_name,omitempty"`
// Служебное: активен ли проект-владелец — для проверки при создании
// заявки. В JSON не уходит.
ProjectActive bool `json:"-"`
}
// Ticket — заявка на обслуживание со всей её историей в одном ряду:
// какой-то статус машины состояний и таймстампы всех достигнутых этапов.
type Ticket struct {
ID int64 `json:"id"`
ClientID string `json:"client_id"`
WorkerID *string `json:"worker_id,omitempty"`
AssignedBy *string `json:"assigned_by,omitempty"`
Address string `json:"address"`
Floor string `json:"floor,omitempty"`
Category string `json:"category"`
Description string `json:"description"`
Priority string `json:"priority"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
AssignedAt *time.Time `json:"assigned_at,omitempty"`
StartedAt *time.Time `json:"started_at,omitempty"`
DoneAt *time.Time `json:"done_at,omitempty"`
ConfirmedAt *time.Time `json:"confirmed_at,omitempty"`
ClosedAt *time.Time `json:"closed_at,omitempty"`
// Принадлежность: заявка живёт в объекте проекта клиента.
// Проверяется составными FK в БД (миграция 0004).
ProjectID int64 `json:"project_id"`
ObjectID int64 `json:"object_id"`
// Закрытие цикла: rejected — это доработка, closed — финал.
RejectedAt *time.Time `json:"rejected_at,omitempty"`
RejectReason string `json:"reject_reason,omitempty"`
CancelReason string `json:"cancel_reason,omitempty"`
ClosedBy *string `json:"closed_by,omitempty"`
// Сколько раз клиент отклонял работу — чтобы доработки не бесконечны
// были незаметны.
ReworkCount int `json:"rework_count"`
// Денормализованные поля для UI — подтягиваются JOIN'ом
ClientName string `json:"client_name,omitempty"`
WorkerName string `json:"worker_name,omitempty"`
ProjectName string `json:"project_name,omitempty"`
ObjectName string `json:"object_name,omitempty"`
// Recurring — правило повторения, если заявка связана с ним:
// создана вместе с правилом (чекбокс «Новая заявка») или самим
// планировщиком. Заполняется только в детали (GetByID) — спискам
// блок на странице заявки не нужен.
Recurring *TicketRecurring `json:"recurring,omitempty"`
}
// TicketRecurring — что показать в блоке «Повторяющаяся заявка» на
// странице заявки: расписание правила, его состояние и ближайший запуск.
type TicketRecurring struct {
RuleID int64 `json:"rule_id"`
// Расписание: «каждые PeriodDays дней/недель от AnchorAt» —
// точка отсчёта плюс период в днях.
AnchorAt time.Time `json:"anchor_at"`
PeriodDays int `json:"period_days"`
IsActive bool `json:"is_active"`
// NextRunAt — ближайший запуск, вычисляется при чтении
// (см. repository.nextDue); у выключенного правила — когда включат.
NextRunAt *time.Time `json:"next_run_at,omitempty"`
}
// RecurringRule — правило повторяющейся заявки (миграции 0014/0016):
// шаблон будущей заявки (объект, категория, описание, приоритет) плюс
// расписание — точка отсчёта и период в днях. Фоновый планировщик сам
// создаёт заявку, когда срок наступил, — но только если прошлая по
// этому правилу уже закрыта (без дублей).
type RecurringRule struct {
ID int64 `json:"id"`
// ClientID — владелец будущих заявок (клиент проекта).
ClientID string `json:"client_id"`
ProjectID int64 `json:"project_id"`
ObjectID int64 `json:"object_id"`
// Шаблон заявки — те же поля, что у tickets.
Category string `json:"category"`
Description string `json:"description"`
Priority string `json:"priority"`
Floor string `json:"floor,omitempty"`
// Расписание: «каждые PeriodDays дней/недель от AnchorAt» —
// AnchorAt — дата-время первого запуска (точка отсчёта сетки
// сроков), PeriodDays — период в днях (1..3650, недели кратны 7).
AnchorAt time.Time `json:"anchor_at"`
PeriodDays int `json:"period_days"`
IsActive bool `json:"is_active"`
// LastRunAt — когда последний раз срабатывали; null (в JSON — null)
// означает «ещё ни разу», следующий запуск считается от created_at.
LastRunAt *time.Time `json:"last_run_at"`
CreatedAt time.Time `json:"created_at"`
// NextRunAt — ближайший запуск после последнего срабатывания
// (вычисляется при чтении; у выключенного правила — когда включат).
NextRunAt *time.Time `json:"next_run_at,omitempty"`
// CreatedBy — кто завёл правило (админ или клиент); в журнале
// созданной заявки это же лицо будет автором события «created».
CreatedBy string `json:"created_by,omitempty"`
// Денормализации для UI.
ProjectName string `json:"project_name,omitempty"`
ObjectName string `json:"object_name,omitempty"`
// Адрес объекта — снимок, как у заявки: правило показывает, куда едут.
Address string `json:"address,omitempty"`
ClientName string `json:"client_name,omitempty"`
// Кто завёл правило (админ или сам клиент) — для аудита, в JSON идёт имя.
CreatedByName string `json:"created_by_name,omitempty"`
}
// RecurringRun — история срабатывания правила (миграция 0014): какая
// заявка создана по какому правилу и когда. Аудит «кто и когда».
type RecurringRun struct {
ID int64 `json:"id"`
RuleID int64 `json:"rule_id"`
// TicketID — созданная заявка (её номер = id); null, если её удалили
// каскадом вместе с проектом.
TicketID *int64 `json:"ticket_id"`
RunAt time.Time `json:"run_at"`
// Адрес и категория — из заявки на момент создания (для списка истории).
Address string `json:"address,omitempty"`
Category string `json:"category,omitempty"`
}
// ProjectWorker — мастер в составе проекта: профиль плюс состояние
// членства (миграция 0005). invited — приглашение отправлено, но ещё
// не принято: такой мастер заявок не получает.
type ProjectWorker struct {
Profile
// invited | active | declined (см. 0005_project_invitations.sql).
Status string `json:"status"`
// RespondedAt — когда мастер принял/отклонил; пусто, пока не ответил.
RespondedAt *time.Time `json:"responded_at,omitempty"`
// ExpiresAt — крайний срок ответа; заполнен у приглашений.
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
// ProjectInvitation — приглашение в проект, ожидающее решения мастера
// (то, что видит сам мастер в GET /invitations).
type ProjectInvitation struct {
ProjectID int64 `json:"project_id"`
ProjectName string `json:"project_name"`
ClientName string `json:"client_name"`
InvitedBy string `json:"invited_by"` // id пригласившего
InvitedName string `json:"invited_by_name"` // его имя для UI
InvitedAt time.Time `json:"invited_at"`
ExpiresAt time.Time `json:"expires_at"`
}
// TicketEvent — запись журнала заявки (миграция 0006): кто, что сделал,
// от какого статуса к какому и когда. Пишется в той же SQL-операции,
// что и переход, поэтому история не может разойтись со статусом.
type TicketEvent struct {
ID int64 `json:"id"`
TicketID int64 `json:"ticket_id"`
// "" — автор удалён (событие сохранится, имя нет).
ActorID string `json:"actor_id,omitempty"`
ActorName string `json:"actor_name,omitempty"`
// created | assigned | started | done | confirmed | rejected |
// canceled | photo_added | photo_removed.
Action string `json:"action"`
// "" — у события «создана» не было предыдущего статуса.
// У photo_added/photo_removed статус не менялся: оба равны текущему.
FromStatus string `json:"from_status,omitempty"`
ToStatus string `json:"to_status"`
// Причина для rejected/canceled, имя файла для photo_*; иначе пусто.
Comment string `json:"comment,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// TicketPhoto — фотоотчёт к заявке (миграция 0008): в БД только
// метаданные, байты живут в объектном хранилище (S3-совместимом:
// локально MinIO, в проде — любой S3). Ссылка на чтение — временная
// (presigned), поэтому сам объект остаётся приватным.
type TicketPhoto struct {
ID int64 `json:"id"`
TicketID int64 `json:"ticket_id"`
// "" — аккаунт загрузившего удалён, файл остаётся.
UploaderID string `json:"uploader_id,omitempty"`
UploaderName string `json:"uploader_name,omitempty"`
ContentType string `json:"content_type"`
SizeBytes int64 `json:"size_bytes"`
// Имя, которое дал файл при загрузке: лежит и в журнале (comment).
OriginalName string `json:"original_name"`
CreatedAt time.Time `json:"created_at"`
// URL — presigned GET (срок жизни ограничен); заполняется только
// в ответах API. ObjectKey — внутренняя деталь, в JSON не уходит.
URL string `json:"url,omitempty"`
ObjectKey string `json:"-"`
}