GET /api/v1/investor/calendar/appointments — Мои записи на встречи
Назначение
Отдаёт инвестору список его записей к агентам и консультантам ПИН (appointment.md):
предстоящие и прошедшие, с их статусами, временем и правилами отмены. Инвестор видит только собственные
записи (record-level по InvestorUserId) — включая созданные анонимно по его телефону через
book-slot. Документ — дом модели MyAppointmentModel: остальные investor-операции
записей ссылаются сюда.
| Поле | Значение |
|---|
| Сервис/контекст | calendar |
| API area | investor |
| Feature group | appointments |
| Статус | draft |
| Документ | docs/06-services/calendar/api/investor/list-my-appointments.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (Инвестор с JWT) |
| Auth policy / Permissions | none (достаточно аутентификации инвестора) |
| Scope (RBAC) | не применяется — инвестор вне tenant (ADR-0052) |
| Record-level | InvestorUserId = ActorUserId из токена (sub), не из query — чужие записи в выборку не попадают (fail-closed) |
| Tenant isolation | не применяется: выборка пересекает tenants агентов — изоляция обеспечивается record-level фильтром |
| Audit | read: актор и фильтры фиксируются в trace; Comment в логи не пишется |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/investor/calendar/appointments |
| Success status | 200 |
| Idempotency header | нет (безопасное чтение) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
Все фильтры комбинируются по «И».
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
ActorUserId | токен (sub) | Guid | Да | Required; не принимается из query | record-level фильтр InvestorUserId |
Status | query | AppointmentStatus? | Нет | enum-строка Pending/Confirmed/Declined/Cancelled/Expired/Completed/NoShow (appointment.md) | — |
ActiveOnly | query | bool? | Нет | default false; true — только активные (Pending/Confirmed); несовместим с Status (задать можно что-то одно) | — |
Kind | query | EventKind? | Нет | enum-строка Showing/Consultation | — |
From / To | query | DateTimeOffset? | Нет | UTC; окно по StartAt; To > From, To − From ≤ 366 дней | — |
Page / PageSize | query | int | Нет | Page ≥ 1; PageSize 1..200, default 50 | — |
Модель ответа — страница MyAppointmentModel
Ответ — страница с полями Items, Page, PageSize, TotalCount; сортировка — StartAt по убыванию,
затем Id (ближайшие и недавние сверху). Дом модели — этот документ; поля — investor-проекция
appointment.md + display-данные агента и место встречи из слота. Внутренние
поля (TenantId, SlotId, CalendarEventId, Version) инвестору не отдаются.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Id | Guid | Да | entity | — | Идентификатор записи. |
AgentUserId | Guid | Да | entity | — | Агент/консультант. |
AgentDisplayName | string? | Нет | display-реплика identity | без контактов | Отображаемое имя агента; null, если реплика ещё не получила пользователя. |
Kind | EventKind | Да | entity | — | Вид встречи: Showing / Consultation. |
Status | AppointmentStatus | Да | entity | — | Текущий статус записи. |
StartAt | DateTimeOffset | Да | entity (снапшот слота) | — | Начало встречи, UTC. |
EndAt | DateTimeOffset | Да | entity (снапшот слота) | — | Конец встречи, UTC. |
TimeZone | string | Да | entity (снапшот слота) | — | IANA-таймзона — локальное отображение. |
Comment | string? | Нет | entity | в логи не пишется | Собственный комментарий инвестора, оставленный при записи. |
PendingExpiresAt | DateTimeOffset? | Нет | entity | — | Для Pending: до какого момента агент примет решение (после — запись истечёт). |
ConfirmedAt | DateTimeOffset? | Нет | entity | — | Момент подтверждения. |
CompletedAt | DateTimeOffset? | Нет | entity | — | Момент фиксации исхода (Completed/NoShow). |
CancelReason | string? | Нет | entity | — | Причина отклонения/отмены (заполнена при Declined/Cancelled). |
CancelledBy | AppointmentCancelledBy? | Нет | entity | — | Кто отменил (Investor/Agent/System). |
RescheduledFromAppointmentId | Guid? | Нет | entity | — | Запись, из которой выполнен перенос. |
RescheduledToAppointmentId | Guid? | Нет | entity | — | Запись, в которую выполнен перенос (у отменённой при переносе). |
ProjectId | Guid? | Нет | слот | — | Проект застройщика (логическая ссылка). |
PropertyObjectId | Guid? | Нет | слот | — | Объект недвижимости (логическая ссылка). |
Location | string? | Нет | слот | — | Место встречи — свободный текст. |
CancellationCutoffMinutes | int | Да | параметр слота | — | Правило отмены (INV-APT-3). |
CancellableUntil | DateTimeOffset | Да | computed: StartAt − CancellationCutoffMinutes | — | До какого момента доступны отмена и перенос; для терминальных статусов носит справочный характер. |
CreatedAt | DateTimeOffset | Да | entity | — | Момент бронирования. |
Алгоритм
- Контекст и доступ. Актор — аутентифицированный инвестор;
InvestorUserId берётся из токена, не из
запроса.
- Проверка входа. Enum-значения
Status/Kind (если заданы) допустимы; ActiveOnly = true и
Status одновременно не заданы; To > From, окно ≤ 366 дней; paging в границах — иначе
VALIDATION_FAILED.
- Что читаем. Страница записей
InvestorUserId = actor с наложением фильтров по «И»
(при ActiveOnly = true — Status ∈ {Pending, Confirmed}), сортировка StartAt убыв., затем Id;
display-имена агентов и место встречи (из слота) дополняются одним заходом.
- Результат. Страница
MyAppointmentModel → HTTP 200; пустая выборка — 200 с Items = [],
не 404. Данные — из source of truth calendar, актуальны на момент чтения.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | enum вне значений; ActiveOnly вместе со Status; To ≤ From; окно > 366 дней; paging вне границ | «Некорректные параметры выборки» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | — | no retry |
Совместимость и наблюдаемость
- Новые поля
MyAppointmentModel — additive optional; удаление/переименование/смена типа — breaking.
Новые значения AppointmentStatus/EventKind в ответе клиент обязан переживать (forward-compatible
отображение «запись»).
- Logs:
InvestorUserId, применённые фильтры, TotalCount (без Comment); metrics:
calendar_api_request_duration_seconds{route}; traces — сквозной traceparent.
Acceptance Criteria
- AC-1 (happy). Дано: у инвестора 2 активные и 3 завершённые записи. Когда:
GET без фильтров.
Тогда: 200, все 5 записей, сортировка StartAt убыв., у каждой — CancellableUntil и
AgentDisplayName без контактов.
- AC-2 (только активные). Дано:
ActiveOnly = true. Тогда: только Pending/Confirmed.
- AC-3 (record-level). Дано: в системе есть записи других инвесторов. Тогда: в выборку они не
попадают; параметры, пытающиеся подменить
InvestorUserId, игнорируются (ключ — из токена).
- AC-4 (анонимная бронь видна). Дано: запись создана анонимно по телефону инвестора через book-slot.
Когда: инвестор вошёл по этому телефону и запросил список. Тогда: запись в выборке.
- AC-5 (несовместимые фильтры). Дано:
ActiveOnly = true и Status = Completed. Тогда:
400 VALIDATION_FAILED.
- AC-6 (пустая выборка). Дано: записей нет. Тогда:
200, Items = [], TotalCount = 0.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы