Перейти к содержанию

04 kyc state machine

Машина состояний KYC-верификации: от загрузки документов до fully_verified.

Диаграмма состояний

stateDiagram-v2
    [*] --> pending: initializeKyc()
    pending --> in_review: submitForReview()
    in_review --> ocr_complete: OCR успешно (kyc-service)
    in_review --> ocr_failed: OCR ошибка
    ocr_failed --> in_review: retryKycProcessing()
    ocr_complete --> pending_operator_review: confirmKycData()
    pending_operator_review --> auto_approved: faceMatchScore >= 0.85
    pending_operator_review --> manual_review: faceMatchScore < 0.85
    auto_approved --> approved: operatorApprove() [или авто]
    manual_review --> approved: operatorApprove()
    approved --> pending: operatorReject()
    approved --> fully_verified: finalizeVerification()

Описание статусов

Статус Кто устанавливает Что происходит Следующий шаг
pending initializeKyc() Запись KYC создана, документы ещё не загружены Пользователь загружает фото документов
in_review submitForReview() Документы отправлены в kyc-service через Redis Stream OCR обработка
ocr_failed kyc-service callback OCR не смог распознать документы retryKycProcessing()in_review
ocr_complete kyc-service callback OCR успешно вернул данные + faceMatchScore Пользователь подтверждает данные
pending_operator_review confirmKycData() PII сохранён, кошелёк создан; ждёт auto-verify или оператора autoVerify() или оператор
auto_approved autoVerify() faceMatchScore >= 0.85; переходит без оператора operatorApprove() (авто)
manual_review autoVerify() faceMatchScore < 0.85; требует ручной проверки operatorApprove() оператором
approved operatorApprove() Оператор одобрил; ждёт финализации finalizeVerification()
fully_verified finalizeVerification() KYC завершён; user.status → active, профиль заблокирован Нет

Файл модели: lib/src/models/kyc_verification.spy.yaml

autoVerify

Метод KycService.autoVerify() вызывается после pending_operator_review:

Условие Переход Примечание
faceMatchScore >= 0.85 auto_approved Tier повышается до STANDARD если не переопределён админом
faceMatchScore < 0.85 manual_review submitForManualReview() — уведомление операторам

Порог 0.85 зашит как константа в kyc_service.dart строка 301: if (faceMatchScore >= 0.85).
Файл: lib/src/services/kyc_service.dart

confirmKycData()

Выполняется атомарно в несколько этапов:

  1. Валидация PiiInputfullName, phone, nationalId, address не пустые
  2. F-1 Guardprofile.isLocked == true → отклонить (профиль уже финализирован)
  3. Upsert user_profile через PiiProfileWriter (вне транзакции — отдельный DB call с hash/mask/blob)
  4. Атомарная транзакция:
  5. users.termsAcceptedAt = now
  6. kyc_verifications.status = pending_operator_review
  7. users.status = pending_operator_review
  8. WalletCreationService.ensureWallet() → TigerBeetle account (через PM POST /accounts)
  9. На ошибку создания кошелькаPendingWalletCreationCall (retry: 2 мин → 10 мин → 1 ч × 21, max 24 попытки, ~21 ч суммарно)

Файл: lib/src/services/kyc_service.dart, строки 135–285

finalizeVerification()

Вызывается оператором после approved → переводит в fully_verified:

  1. kyc_verifications.status → fully_verified
  2. users.status → active
  3. user_profiles.isLocked → true (F-1: PII становится иммутабельным)
  4. accountTier → STANDARD (если !accountTierOverriddenByAdmin)
  5. Audit log: action: 'profile_locked'
  6. Push-уведомление «KYC подтверждён» (локаль по профилю)
  7. [PLAN] IPPS Stage 2 — регистрация внешнего кошелька через PM POST /accounts/external/ipps (не реализовано; TODO в строке 482)

Файл: lib/src/services/kyc_service.dart, строки 396–520