POST /api/v1/internal/profile/requisite-snapshot — Снапшот реквизитов для CRM (getRequisiteSnapshot + RegisterUsage)
Назначение
Выдать контексту crm иммутабельный расшифрованный снапшот реквизитов по шаблону инвестора в момент
оформления брони/сделки (контакты/реквизиты/документы переиспользуются как шаблон в бронировании и
оформлении сделок) и зарегистрировать использование шаблона (LastUsedAt = now, UsageCount + 1). Это
единственный канал, которым расшифрованные PII-реквизиты покидают контекст profile: Kafka не публикует PII
(ADR-0056), выдача синхронная. crm копирует снапшот в документы брони/сделки и далее владеет копией
(requisite-template.md, Delete behavior «Снапшот в CRM»). Операция
изменяет usage-счётчики шаблона и порождает обязательное аудит-событие каждой выдачи. Полный сквозной поток —
UC-PRF-004.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | profile |
| API area | internal (m2m, потребитель crm) |
| Feature group | requisites |
| Статус | draft |
| Документ | docs/06-services/profile/api/internal/get-requisite-snapshot.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | ServiceAccount (m2m; потребитель crm) — не user JWT |
| Auth policy / Permissions | m2m service token (client-credentials), policy InternalServiceOnly; allowlist потребителей internal-области (crm); user-permissions не применяются |
| Scope (RBAC) | internal (m2m); user/tenant scope не применяется |
| Record-level | владение проверяется явным ownerUserId в body: template.OwnerUserId == ownerUserId (запрос обязан нести инициатора сделки) |
| Tenant isolation | не применяется (ADR-0052; инвестор вне tenant) |
| Audit | read-sensitive: каждая выдача расшифрованных реквизитов аудируется RequisiteTemplateUsedEvent; в логах — только TemplateId/PersonId, без значений |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/internal/profile/requisite-snapshot |
| Success status | 200 |
| Idempotency header | не применяется (регистрация использования — статистический счётчик; повтор безопасен и допустим) |
| Correlation | OpenTelemetry trace (traceparent) |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TemplateId | body | Guid | Да | NotEmpty; активный шаблон (шаг 2) | — |
OwnerUserId | body | Guid | Да | NotEmpty; сверка владения template.OwnerUserId == OwnerUserId (шаг 2) | инициатор сделки; чужой шаблон → 403 |
RequestedByService | body | RequestedByService (enum строкой) | Да | ∈ { Crm }; сверка с аутентифицированным service-account (ActorId) | несоответствие сервису → 403 |
OwnerUserId/TenantId не выводятся из actor-контекста (m2m не несёт user-scope) — владение задаётся
явным OwnerUserId в body и сверяется с template.OwnerUserId (исключение для internal-области доверенного
m2m-вызова).
Модель ответа RequisiteSnapshotModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
TemplateId | Guid | Да | template.Id | — | Шаблон, по которому собран снапшот. |
TemplateVersion | long | Да | template.Version | — | Версия шаблона на момент выдачи (иммутабельность копии). |
PersonId | Guid | Да | person.Id | — | Персона состава. |
Person | PersonSnapshotModel | Да | PersonEntity (расшифровка) | значения — только внутри снапшота | Расшифрованные ФИО/дата рождения/гражданство. |
IdentityDocument | DocumentSnapshotModel | Да | документ Composition.IdentityDocumentId | payload — plaintext снапшота | Удостоверение личности (паспорт). |
InnDocument | DocumentSnapshotModel? | Нет | Composition.InnDocumentId | — | ИНН, если задан в шаблоне. |
AdditionalDocuments | List<DocumentSnapshotModel> | Да (может быть пустым) | Composition.AdditionalDocumentIds (≤ 10) | — | Доп. документы. Ограниченная вложенная коллекция состава (не постраничный список) — живёт внутри RequisiteSnapshotModel, поэтому List<>. |
ContactEmail | string? | Нет | Decrypt(ContactEmailCipher) или fallback Profile.ContactEmail | — | Оверрайд шаблона либо контакт учётки из реплики профиля. |
ContactPhone | string? | Нет | Decrypt(ContactPhoneCipher) или fallback Profile.ContactPhone | — | Аналогично. |
IssuedAt | DateTimeOffset | Да | IClock.UtcNow | — | Момент выдачи снапшота. |
PersonSnapshotModel—{ FirstName (string), LastName (string), MiddleName (string?), BirthDate (DateOnly?), CitizenshipCountryId (short? — справочник стран handbook), Gender (PersonGender?) }. Дом полей и правила шифрования — person.md.DocumentSnapshotModel—{ DocumentId (Guid), Type (PersonDocumentType строкой), CountryId (short — справочник стран handbook), CountryIsoCode2 (string char(2) — юридический след: код страны на момент снапшота, копия денормализацииPersonDocumentEntity.CountryIsoCode2, снятой при записи документа; резолв справочника на пути снапшота не выполняется), PayloadContractVersion (int), Payload (typed по Type: RfPassportModel | RbPassportModel | KzPassportModel | CisPassportModel | ForeignPassportModel | InnModel | CustomDocumentModel), Scans (List<DocumentScanModel>) }. Дом typed-моделей payload иDocumentScanModel { FileId, FileName, ContentType, SizeBytes, UploadedAt }— person-document.md; здесь — только состав снапшота. Голыхobject/jsonнет:Payloadдискриминируется поType+PayloadContractVersion.
Алгоритм
- Контекст и доступ. Вызывает m2m service-account потребителя
crm(не user JWT); policyInternalServiceOnly, allowlist internal-области; user-permissions и tenant не применяются (ADR-0052). Владелец задаётся явнымOwnerUserIdв body. - Проверка входа.
TemplateId/OwnerUserIdне пустые,RequestedByService ∈ { Crm }— иначеValidationError; несоответствиеRequestedByServiceаутентифицированному сервису →PROFILE_TEMPLATE_FOREIGN_REFERENCE(403). - Существование и владение. Шаблон должен существовать — иначе
PROFILE_TEMPLATE_NOT_FOUND(404).template.OwnerUserIdдолжен совпасть сOwnerUserIdиз body — иначеPROFILE_TEMPLATE_FOREIGN_REFERENCE(403; для доверенного m2m — сигнал рассинхрона данных потребителя, в отличие от 404-«без утечки» публичных методов). Шаблон активен (Status = Active) — иначеPROFILE_TEMPLATE_ARCHIVED(409, INV-RQT-8). - Что читаем. Персона состава (
template.PersonId) целиком с документами (≤ 20); профиль-владелец — для fallback контактов учётки (может отсутствовать → контактыnull). Документы состава разрешаются по составу шаблона:IdentityDocumentId,InnDocumentId?,AdditionalDocumentIds. - Проверка состава и расшифровка. Каждый документ состава обязан принадлежать персоне и иметь
Status = Active(INV-RQT-3); архивный/отсутствующий →PROFILE_TEMPLATE_REFERENCE_ARCHIVED(409 — инвестор обязан обновить шаблон). Значения PII расшифровываются fail-closed (недоступность ключей →PROFILE_CRYPTO_UNAVAILABLE, 503): ФИО, дата рождения, payload каждого документа (typed-модель поType), контакты-оверрайды. СобираетсяRequisiteSnapshotModel:Person— расшифрованные поля;IdentityDocument/InnDocument?/AdditionalDocuments—DocumentSnapshotModel(payload + сканы изScanList);ContactEmail/ContactPhone— оверрайды шаблона, приnull— контакты профиля;TemplateVersion = template.Version,IssuedAt = now. - Регистрация использования. Шаблон фиксирует использование:
LastUsedAt = now,UsageCount + 1; поднимается обязательное аудит-событиеRequisiteTemplateUsedEvent(template.Id, OwnerUserId, RequestedByService). Персона и документы не меняются (расшифровка — чтение). - Согласованность и события. Обновление usage-счётчиков и запись аудит-события фиксируются атомарно. Интеграционных/Kafka-событий нет (ADR-0056: PII в шину не выносится; снапшот — синхронная выдача).
- Результат.
RequisiteSnapshotModel→ HTTP200. Единственный канал выдачи расшифрованных реквизитов; ответ по внутренней сети/mTLS; в логах — толькоTemplateId/PersonId.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (Kafka не публикуется — ADR-0056; выдача синхронная, PII не выносится) |
| External calls | криптосервис (ключи Vault/k8s secrets) — расшифровка значений PII (fail-closed → 503) |
| Cache / projections / search | none (LastUsedAt/UsageCount — сортировка «недавние» в UI списка шаблонов) |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
| — (auth) | 401 | Нет/невалиден m2m service token | без тела | обновить токен |
| — (authz) | 403 | Токен вне allowlist internal-области | без тела | no retry |
ValidationError | 400 | Пустой TemplateId/OwnerUserId, неизвестный RequestedByService | RestApiValidationErrorResponse | no retry |
PROFILE_TEMPLATE_NOT_FOUND | 404 | Нет шаблона по TemplateId | «Шаблон реквизитов не найден» | no retry |
PROFILE_TEMPLATE_FOREIGN_REFERENCE | 403 | template.OwnerUserId != OwnerUserId или RequestedByService ≠ ActorId | «Шаблон не принадлежит инициатору» | no retry (исправить запрос) |
PROFILE_TEMPLATE_ARCHIVED | 409 | template.Status = Archived (INV-RQT-8) | «Шаблон архивирован — обновите реквизиты» | после восстановления шаблона |
PROFILE_TEMPLATE_REFERENCE_ARCHIVED | 409 | Документ состава архивирован/отсутствует (INV-RQT-3) | «Документ шаблона недоступен — обновите шаблон» | после правки шаблона (A3 UC-PRF-004) |
PROFILE_CRYPTO_UNAVAILABLE | 503 | Ключи PersonalDataCrypto недоступны (fail-closed) | «Сервис реквизитов временно недоступен» | retry |
Совместимость и наблюдаемость
- Семантика стабильна; расширения
RequisiteSnapshotModel/DocumentSnapshotModel— additive (optional-поля, ростPayloadContractVersion— lazy-миграция в памяти); удаление/переименование/смена типа — breaking (migration + согласованный rollout сcrm). Unknown enumPersonDocumentType/RequestedByService— fail-fast. - SLO: p99 < 200 мс (блокирует оформление брони — README profile, Runtime); при недоступности —
crmиспользует fallback ручного ввода реквизитов (A2/A5 UC-PRF-004). - Logs:
templateId,personId,requestedByService,result— без расшифрованных значений; metrics:profile_internal_operations_total{operation="getRequisiteSnapshot",result},profile_crypto_decrypt_errors_total, latency p99; traces — OTel; dashboardprofile-overview; alert —profile_crypto_decrypt_errors_total > 0(недоступность ключей).
Acceptance Criteria
- Happy path. Given активный шаблон владельца
OwnerUserId, m2m-вызовcrm, ключи доступны, состав активен; When snapshot; Then200, расшифрованныйRequisiteSnapshotModel(TemplateVersion,IssuedAt = now),LastUsedAt = now/UsageCount + 1, поднято аудит-событиеRequisiteTemplateUsedEvent; PII в Kafka не публикуется. - Чужой шаблон / сервис. Given
template.OwnerUserId != OwnerUserIdиз body илиRequestedByServiceне совпадает с аутентифицированным сервисом; When snapshot; Then403 PROFILE_TEMPLATE_FOREIGN_REFERENCE. - Архивный шаблон. Given
template.Status = Archived; When snapshot; Then409 PROFILE_TEMPLATE_ARCHIVED(INV-RQT-8); использование не регистрируется. - Архивный документ состава. Given документ состава архивирован/отсутствует; When snapshot; Then
409 PROFILE_TEMPLATE_REFERENCE_ARCHIVED(INV-RQT-3) — инвестор обязан обновить шаблон. - Ключи недоступны. Given хранилище ключей недоступно (fail-closed); When snapshot; Then
503 PROFILE_CRYPTO_UNAVAILABLE; расшифровка предшествует регистрации использования, поэтомуUsageCount/LastUsedAtне меняются. - Повтор допустим. Given
crmповторяет вызов (ретрай / повторное оформление); When snapshot дважды; Then обе выдачи успешны (usage — статистический счётчик), на каждую — отдельная аудит-запись. - Контакты-fallback. Given в шаблоне нет оверрайда контактов; When snapshot; Then
ContactEmail/ContactPhoneберутся из реплики профиля (илиnull, если профиль/контакты отсутствуют). - Деградация у потребителя. Given
profileнедоступен/отдал ошибку; Whencrmоформляет бронь; Thencrmпереходит на ручной ввод реквизитов (A2/A5 UC-PRF-004), не блокируя бронь.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: RequisiteTemplateEntity
- API (4): GET /api/v1/internal/profile/verification-status/{userId} — Статус верификации для m2m (getVerificationStatus), GET /api/v1/profile/requisite-templates — Список шаблонов реквизитов (listRequisiteTemplates), POST /api/v1/profile/requisite-templates — Создание шаблона реквизитов (createRequisiteTemplate), Profile API — реестр операций областей public / internal / admin
- Use Cases (1): UC-PRF-004. Создание шаблона реквизитов и его использование в бронировании (CRM)
- События (1): Event Contract: pin.profile.person-upserted (PersonUpsertedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: requisite-template.md (INV-RQT-8,
RegisterUsage,Composition,RequisiteTemplateUsedEvent), person.md (EncryptedPersonName,BirthDateCipher), person-document.md (typed payload-модели,DocumentScan), profile.md (реплика контактов для fallback). - Сценарий: UC-PRF-004 (создание шаблона и использование в брони, шаги 4–7).
- Потребитель
crm:docs/06-services/crm/(booking — копия снапшота в документы брони/сделки); ADR-0052 (tenancy), ADR-0056 (simplicity-first, синхронная выдача без Kafka).