Перейти к основному содержимому

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 areainternal (m2m, потребитель crm)
Feature grouprequisites
Статусdraft
Документdocs/06-services/profile/api/internal/get-requisite-snapshot.md

Актор и доступ

ПроверкаЗначение
Actor typeServiceAccount (m2m; потребитель crm) — не user JWT
Auth policy / Permissionsm2m 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)
Auditread-sensitive: каждая выдача расшифрованных реквизитов аудируется RequisiteTemplateUsedEvent; в логах — только TemplateId/PersonId, без значений

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/internal/profile/requisite-snapshot
Success status200
Idempotency headerне применяется (регистрация использования — статистический счётчик; повтор безопасен и допустим)
CorrelationOpenTelemetry trace (traceparent)

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TemplateIdbodyGuidДаNotEmpty; активный шаблон (шаг 2)
OwnerUserIdbodyGuidДаNotEmpty; сверка владения template.OwnerUserId == OwnerUserId (шаг 2)инициатор сделки; чужой шаблон → 403
RequestedByServicebodyRequestedByService (enum строкой)Да∈ { Crm }; сверка с аутентифицированным service-account (ActorId)несоответствие сервису → 403

OwnerUserId/TenantId не выводятся из actor-контекста (m2m не несёт user-scope) — владение задаётся явным OwnerUserId в body и сверяется с template.OwnerUserId (исключение для internal-области доверенного m2m-вызова).

Модель ответа RequisiteSnapshotModel

ПолеТипОбязательностьИсточникMaskingОписание
TemplateIdGuidДаtemplate.IdШаблон, по которому собран снапшот.
TemplateVersionlongДаtemplate.VersionВерсия шаблона на момент выдачи (иммутабельность копии).
PersonIdGuidДаperson.IdПерсона состава.
PersonPersonSnapshotModelДаPersonEntity (расшифровка)значения — только внутри снапшотаРасшифрованные ФИО/дата рождения/гражданство.
IdentityDocumentDocumentSnapshotModelДадокумент Composition.IdentityDocumentIdpayload — plaintext снапшотаУдостоверение личности (паспорт).
InnDocumentDocumentSnapshotModel?НетComposition.InnDocumentIdИНН, если задан в шаблоне.
AdditionalDocumentsList<DocumentSnapshotModel>Да (может быть пустым)Composition.AdditionalDocumentIds (≤ 10)Доп. документы. Ограниченная вложенная коллекция состава (не постраничный список) — живёт внутри RequisiteSnapshotModel, поэтому List<>.
ContactEmailstring?НетDecrypt(ContactEmailCipher) или fallback Profile.ContactEmailОверрайд шаблона либо контакт учётки из реплики профиля.
ContactPhonestring?НетDecrypt(ContactPhoneCipher) или fallback Profile.ContactPhoneАналогично.
IssuedAtDateTimeOffsetДа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.

Алгоритм

  1. Контекст и доступ. Вызывает m2m service-account потребителя crm (не user JWT); policy InternalServiceOnly, allowlist internal-области; user-permissions и tenant не применяются (ADR-0052). Владелец задаётся явным OwnerUserId в body.
  2. Проверка входа. TemplateId/OwnerUserId не пустые, RequestedByService ∈ { Crm } — иначе ValidationError; несоответствие RequestedByService аутентифицированному сервису → PROFILE_TEMPLATE_FOREIGN_REFERENCE (403).
  3. Существование и владение. Шаблон должен существовать — иначе 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).
  4. Что читаем. Персона состава (template.PersonId) целиком с документами (≤ 20); профиль-владелец — для fallback контактов учётки (может отсутствовать → контакты null). Документы состава разрешаются по составу шаблона: IdentityDocumentId, InnDocumentId?, AdditionalDocumentIds.
  5. Проверка состава и расшифровка. Каждый документ состава обязан принадлежать персоне и иметь Status = Active (INV-RQT-3); архивный/отсутствующий → PROFILE_TEMPLATE_REFERENCE_ARCHIVED (409 — инвестор обязан обновить шаблон). Значения PII расшифровываются fail-closed (недоступность ключей → PROFILE_CRYPTO_UNAVAILABLE, 503): ФИО, дата рождения, payload каждого документа (typed-модель по Type), контакты-оверрайды. Собирается RequisiteSnapshotModel: Person — расшифрованные поля; IdentityDocument/InnDocument?/AdditionalDocumentsDocumentSnapshotModel (payload + сканы из ScanList); ContactEmail/ContactPhone — оверрайды шаблона, при null — контакты профиля; TemplateVersion = template.Version, IssuedAt = now.
  6. Регистрация использования. Шаблон фиксирует использование: LastUsedAt = now, UsageCount + 1; поднимается обязательное аудит-событие RequisiteTemplateUsedEvent(template.Id, OwnerUserId, RequestedByService). Персона и документы не меняются (расшифровка — чтение).
  7. Согласованность и события. Обновление usage-счётчиков и запись аудит-события фиксируются атомарно. Интеграционных/Kafka-событий нет (ADR-0056: PII в шину не выносится; снапшот — синхронная выдача).
  8. Результат. RequisiteSnapshotModel → HTTP 200. Единственный канал выдачи расшифрованных реквизитов; ответ по внутренней сети/mTLS; в логах — только TemplateId/PersonId.

Диаграмма

Побочные эффекты

ТипДетали
Integration eventsnone (Kafka не публикуется — ADR-0056; выдача синхронная, PII не выносится)
External callsкриптосервис (ключи Vault/k8s secrets) — расшифровка значений PII (fail-closed → 503)
Cache / projections / searchnone (LastUsedAt/UsageCount — сортировка «недавние» в UI списка шаблонов)
Notificationsnone

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/невалиден m2m service tokenбез телаобновить токен
— (authz)403Токен вне allowlist internal-областибез телаno retry
ValidationError400Пустой TemplateId/OwnerUserId, неизвестный RequestedByServiceRestApiValidationErrorResponseno retry
PROFILE_TEMPLATE_NOT_FOUND404Нет шаблона по TemplateId«Шаблон реквизитов не найден»no retry
PROFILE_TEMPLATE_FOREIGN_REFERENCE403template.OwnerUserId != OwnerUserId или RequestedByService ≠ ActorId«Шаблон не принадлежит инициатору»no retry (исправить запрос)
PROFILE_TEMPLATE_ARCHIVED409template.Status = Archived (INV-RQT-8)«Шаблон архивирован — обновите реквизиты»после восстановления шаблона
PROFILE_TEMPLATE_REFERENCE_ARCHIVED409Документ состава архивирован/отсутствует (INV-RQT-3)«Документ шаблона недоступен — обновите шаблон»после правки шаблона (A3 UC-PRF-004)
PROFILE_CRYPTO_UNAVAILABLE503Ключи PersonalDataCrypto недоступны (fail-closed)«Сервис реквизитов временно недоступен»retry

Совместимость и наблюдаемость

  • Семантика стабильна; расширения RequisiteSnapshotModel/DocumentSnapshotModel — additive (optional-поля, рост PayloadContractVersion — lazy-миграция в памяти); удаление/переименование/смена типа — breaking (migration + согласованный rollout с crm). Unknown enum PersonDocumentType/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; dashboard profile-overview; alert — profile_crypto_decrypt_errors_total > 0 (недоступность ключей).

Acceptance Criteria

  • Happy path. Given активный шаблон владельца OwnerUserId, m2m-вызов crm, ключи доступны, состав активен; When snapshot; Then 200, расшифрованный RequisiteSnapshotModel (TemplateVersion, IssuedAt = now), LastUsedAt = now/UsageCount + 1, поднято аудит-событие RequisiteTemplateUsedEvent; PII в Kafka не публикуется.
  • Чужой шаблон / сервис. Given template.OwnerUserId != OwnerUserId из body или RequestedByService не совпадает с аутентифицированным сервисом; When snapshot; Then 403 PROFILE_TEMPLATE_FOREIGN_REFERENCE.
  • Архивный шаблон. Given template.Status = Archived; When snapshot; Then 409 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 недоступен/отдал ошибку; When crm оформляет бронь; Then crm переходит на ручной ввод реквизитов (A2/A5 UC-PRF-004), не блокируя бронь.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы

  • Сущности: 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).