Data Model: Validação e escopo por CRP¶
ProfessionalProfile (existing, extended)¶
ownerUserIdunique;cpf,cnpjnormalized and independently nullable, with at least one for completion.profileCompletedAt; existing users may remain grandfathered during rollout.- Partial unique indexes for non-null CPF/CNPJ.
ProfessionalRegistration¶
- IDs:
id,ownerUserId,profileId. - CRP:
region,number,canonicalKey. - Document:
linkedDocumentType: cpf|cnpj,linkedDocumentValuesnapshot. - Validation:
validationState,officialStatus,source,stateVersion. - Times:
graceStartedAt,graceExpiresAt,lastConfirmedAt,lastAttemptAt,nextAttemptAt, timestamps. - Error: stable
lastErrorCode, never raw provider payload. - Unique owner/canonical/document association; cross-owner conflict resolved neutrally.
ValidationState¶
pending → provisional → validated | invalid_inactive | expired; temporary failures preserve last confirmed state. Success after expiry transitions to validated. Grace dates never renew.
CrpValidationAttempt¶
registrationId,attemptKey, start/completion, outcome, provider status/error code.- Unique
attemptKey; no CPF/CNPJ or raw response.
Patient (existing, extended)¶
professionalRegistrationIdnullable FK during migration.- New patients require a registration; legacy null is visible only in “Todos”.
- Owner and registration owner must match. Changing registration does not affect quota.
- Index owner/registration/status and owner/registration/name.
UserPlan (existing)¶
- Tier
planKey:free|standard|future-key. free.activePatientsMax=5; usage always counted by owner without CRP filter.
OperationDecision (read model)¶
action,allowed,blockers[].- Blocker:
code,source: profile|crp|plan|policy, optional registration scope, safe message, optional resolution. - Decisions are calculated; source states are persisted, not a global user status.
Invariants¶
- Every query first restricts owner; CRP can only narrow.
- New patient has one registration; dependents derive via patient.
- Absence of CRP filter never means absence of owner authorization.
- Expiry is
graceExpiresAt <= now, independent of worker execution.