39a979b1a7
backend phase 3: identity profiles, patients & nurse bank accounts Add the role-attached identity layer on top of the b2 auth spine: nurse seller profiles (guarded is_verified, read-only aggregates), thin customer payer profiles, first-class patients (tenancy-scoped), and nurse payout bank accounts hardened with an iban_hash uniqueness guard and an automated استعلام شبا IBAN-ownership inquiry. - Four usr tables via one migration (1:1 uniques, UNIQUE(iban_hash), filtered UNIQUE(nurse_id) WHERE is_primary=1, guarded is_verified, encrypted PII, soft-delete on nurse_profiles) - 15 CQRS slices + 4 role-scoped controllers; reads projected + paginated, IBAN masked (last-4); ownership-inquiry endpoints rate-limited - New IBankAccountOwnershipVerifier seam (mock deterministic شبا match) + per-domain repositories on IUnitOfWork + encrypted-PII value converters - Activate FluentValidation repo-wide (validators were never registered) - Handler unit tests + WebApplicationFactory integration tests (76 pass); contract identity-profiles.md + swagger snapshot; docs, handoff & report Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
5.2 KiB
5.2 KiB
Backend phase 3 report — Identity: profiles, patients & nurse bank accounts
What was built
- Four domain entities (
Baya.Domain/Entities/Identity/):NurseProfile,CustomerProfile,Patient,NurseBankAccount.NurseProfile.is_verifiedis write-guarded (private setter +MarkVerified()/MarkUnverified()— only b6 calls it);is_accepting_bookingstoggled via a domain method; the search aggregates are read-only. - One EF migration
IdentityProfilesPatientsBankAccounts(schemausr): 1:1 uniques onuser_id,UNIQUE(iban_hash), filteredUNIQUE(nurse_id) WHERE is_primary=1, soft-delete onNurseProfiles, encrypted PII columns, audit fields. No CUT columns. - 15 CQRS slices under
Features/Identity/{Commands|Queries}/+ 4sealed : BaseControllercontrollers (NurseProfilesController,CustomerProfilesController,PatientsController,NurseBankAccountsController). Reads project to DTOs; lists paginate; the ownership-inquiry endpoints are rate-limited (sensitivepolicy). - New seam
IBankAccountOwnershipVerifier(ApplicationContracts/Common) + mockMockBankAccountOwnershipVerifier(CrossCutting), registered inAddCrossCuttingSeams, config-selected. - Persistence: four per-domain repositories on
IUnitOfWork(NurseProfileRepository,CustomerProfileRepository,PatientRepository,NurseBankAccountRepository); encrypted-PII value converters for the new columns wired inApplicationDbContext.OnModelCreating; an atomicSetPrimaryAsync(clear-then-set in one transaction) so the single-primary index never trips. - Infra fix:
AddApplicationServicesnow registers everyAbstractValidator<T>asIValidator<T>so the pre-existingValidateCommandBehaviorandModelStateValidationAttributefilter actually validate (they had no validators registered before this phase — validation was silently inert).
What is now testable, and exactly how (mirrors the phase §7)
Log in as a nurse and a customer (b2 OTP flow), refreshing the token after select_role so the
role claim is present. Then:
- Nurse profile —
POST api/v1/nurse_profiles/upsert→ row createdis_verified=0,is_accepting_bookings=0;GET …/meshows aggregates at 0. No path setsis_verified. - Accepting-bookings —
POST …/set_accepting_bookingsflips it; verified untouched. - Customer profile —
POST api/v1/customer_profiles/upsertwith emergency contact →GET …/meround-trips it through the encrypted column. - Patient CRUD —
create(gender required) /list/get/{id}/update/{id}/archive/{id}. - Tenancy — customer B calling
get/updateon customer A's patient id → 404. - Bank account + inquiry —
POST api/v1/nurse_bank_accounts/add(normal IBAN) →matched_national_id=true, vendor ref recorded;listshows the IBAN masked. - Mismatch — add the mismatch IBAN →
matched_national_id=false. - Duplicate IBAN — re-add the same IBAN → clean
400viaiban_hashuniqueness. - Primary flip — add a 2nd account,
set_primary/{id2}→ account 2 primary, account 1 not; never two primaries.
Automated coverage: 15 handler unit tests (NSubstitute) covering profile upsert, role forbidden, patient
CRUD + cross-customer 404, bank add match/mismatch/duplicate + set-primary flip/not-owned; 13
WebApplicationFactory integration tests (one+ per controller: happy path, 401, validation 400, tenancy
404, mask, duplicate, primary flip, mismatch). dotnet build clean (0 new code warnings); dotnet test
green (75 pass).
What is mocked / waiting on a real service
IBankAccountOwnershipVerifier(🟡) — deterministic fake استعلام شبا. Make-it-real steps inreports/mocks-registry.md. Reused seams:IFieldEncryptor,ICurrentUser,IDateTimeProvider.
Contracts produced / consumed
- Produced:
dev/contracts/domains/identity-profiles.md;dev/contracts/openapi/swagger.v1.jsonrefreshed (adds all nurse-profile / customer-profile / patient / bank-account paths). - Consumed: b2 auth (login/roles), b0 seams (
IFieldEncryptor/ICurrentUser/IDateTimeProvider), b1IPlatformConfig(available; not needed this phase).
Follow-ups for later phases
- b4:
customer_addresses+nurse_service_areas(need geography + geocoder) — deferred here. - b6: the
is_verifiedflip (verification-confirm transaction); Shahkar/KYC populatenational_id; thebank_account_verificationstep couples toNurseBankAccounts. - b13: first-payout gate on
matched_national_id = true. - b9/b14: recompute
average_rating/total_reviews/total_completed_bookings(read-only here). - Chain-wide: validators are now active — new phases must ensure route-supplied ids aren't validated in the body command, and can rely on FluentValidation for input rejection.
Decisions taken (flagged for confirmation)
- A thin
customer_profilesrow is auto-provisioned on a customer's first patient (so patient registration needs no separate profile step). Recorded inproduct/data-model/01-identity-and-access.md. - IBAN is returned masked (last-4) on every read; first account added is primary by default.