39 KiB
39 KiB
Backend status log (append-only)
One block per completed backend phase. Newest at the top. Backend lane writes here; frontend reads.
refinement-phase-9 — Observability, ops hardening, docs honesty & scale-later — 2026-07-13
- Shipped: one OpenTelemetry stack (metrics scraped at
/metrics+ tracing ASP.NET Core/EF, opt-in OTLP viaOpenTelemetry:Otlp:Endpoint; prometheus-net removed) —ApiResult.requestId= W3C trace id. Health split/healthz/livevs/healthz/ready(app DB + logDb[deployed] + object-storage write probe);/HealthCheckaggregate kept. Prod logs Information+ with no PII (OTP code no longer logged) + dead Elasticsearch sink/package removed.AuditLogRetentionJob(IRecurringJob, two-tier legal retention).TicketMessage.Bodyencrypted at rest (IFieldEncryptor; column → nvarchar(max)). gRPC reflection Development-only. Docs reconciled (mocks-registry stale rows pruned; deferrals 9.7–9.11 recorded with pull- triggers). MigrationRefinementPhase9TicketBodyEncryptionAndAuditRetention(Body widen + 3 config seed rows). - Contracts: none changed — no wire/shape change (observability + docs + at-rest encryption only).
- Mocked: none new. Deferred (recorded, not gaps): Elasticsearch
INurseSearch, SMS/pushINotificationDispatcher, analytics pipeline, holiday feed, 8 product tables — each with a written pull-trigger. - Gate: build clean (0 new warnings) / 407 tests pass (402 prior + 5 new: audit-retention, ticket-body encryption, liveness).
- Handoff: backend/handoff/after-refinement-phase-9.md
- Notes for frontend: no client-facing change. Ticket message bodies are now encrypted at rest server-side
(the thread read still returns plaintext — unchanged wire). A request's
requestIdis a real trace id useful for support correlation.
refinement-phase-7 — Unattended ops: scheduler, locking & multi-instance readiness — 2026-07-13
- Shipped: one in-process
RecurringJobSchedulerHostedService+ theIRecurringJobseam (Persistence/Services/Scheduling/) replacing the twoPeriodicTimerhosted services and scheduling the previously admin-manual crons —booking_request_expiry,notification_retention,verification_expiry_scan,no_show_sweep,weekly_payout_generation— each reading its seeded cadence key; admin triggers stay overrides. Payout generation is scheduled (system-initiateddraft); processing stays admin-only (NursePayoutBatch.InitiatedByAdminIdnow nullable = system;SystemInitiatedflag is scheduler-only, controller-neutralized). Migrations split from boot:dotnet run -- migrateone-shot + deployed-boot schema check (EnsureSchemaUpToDateAsync); Dev keeps migrate-on-boot. No Redis/Hangfire added — documented as the1-instance scale-out gate; the scheduler's per-tick
IDistributedLockis that swap point. - Contracts:
PayoutBatchDto.initiatedByAdminIdnullable —dev/contracts/domains/payouts.md+ openapi snapshot refreshed (yes). - Mocked: none new. Redis = scale gate; Moadian/refund-settlement poll = Phase 8 jobs (see mocks-registry).
- Gate: build clean (0 new warnings) / 402 tests pass (396 prior + 6 new scheduling tests).
- Handoff: backend/handoff/after-refinement-phase-7.md
- Notes for frontend: admin payout batch
initiatedByAdminIdcan benull(system/scheduled batch) — render a "system"/"scheduled" label rather than assuming an admin id.
refinement-phase-1 — Database: local-dev story, demo seed & migration hygiene — 2026-07-13
- Shipped (no migration, no endpoint, no contract change): Development-gated demo-world seeder —
Persistence/Services/Seeding/DemoWorldSeeder.cs+DemoWorldDefinitions.cs, scoped-registered inAddPersistenceServices, invoked fromProgram.csvia newSeedDemoWorldAsync()only underIsDevelopment(). Idempotently (guarded on phone) creates 3 nurses (2 verified:MarkVerified()+nurse_verificationsapproved+ credentials +matched_national_idprimary bank + 2–3 IRR variants + Tehran areas incl. whole-citydistrict_id=NULL; 1 unverified), 2 customers (patients + coord addresses), and 1 cross-category required demo option group (شیفت / Shift Type). Search rows are driven through the realISearchIndexMaintainer.RebuildAsync— never hand-inserted, sois_searchablestays truthful (verified surface, unverified doesn't). ReferenceHasDataseeds + the 17 migrations untouched. - Local DB / migration hygiene: verified Phase 0's compose + placeholder connstrings + boot-migrate;
documented the explicit
dotnet ef database updatepath + demo-reset flow in the RUNBOOK; confirmed no pending model changes. Forward-dep FKs (Phase 6) and the boot-migrate multi-instance split (Phase 7) are out of scope by design. - Contracts: none produced; swagger snapshot not regenerated (no route/shape change).
- Mocked: none introduced (the seeder uses the real handlers/maintainer/converters).
- Gate: build clean (0 new warnings) / tests green (372: +3
DemoWorldSeederTestsproving over the real HTTP pipeline — verified nurse in search, unverified not, trust badge correct, idempotent). AddedInternalsVisibleTo("Baya.Test.Api")on Persistence for the seeder test. - Handoff: backend/handoff/after-refinement-phase-1.md
- Notes for frontend: demo accounts to log in as (phone-OTP) — nurses
09120000001(verified) /09120000002(verified) /09120000003(unverified); customers09120000010/09120000011. Tehrancity_id=101.GET /search/nurses?service_category_id=1&city_id=101now returns real verified nurses. NoUSE_*_MOCKflag flipped — de-mocking is Phase 4.
refinement-phase-0 — Local end-to-end bring-up & the integration seam — 2026-07-12
- Shipped (integration/plumbing — no business logic): CORS (
Baya.WebFramework/ServiceConfiguration/ CorsServiceExtension.cs→AddCorsPolicies, policyBalinyaarWebClientfromCors:AllowedOrigins, defaulthttp://localhost:3000;app.UseCorsafterUseRouting/ beforeUseRateLimiter; noAllowCredentials). Local DB story —server/docker-compose.ymlrewritten to SQL Server 2022 Developer onlocalhost:1433(dev-only SA password); committedappsettings*.jsonconnection strings replaced with non-working placeholders (real value viadotnet user-secrets); added<UserSecretsId>to the API csproj. Development-only OTP helperGET /api/v1/dev/last_otp/{phone}(DevController+DevOtpStore+DevCapturingSmsSender, wired only in Development viaAddDevelopmentOtpCapture; 404 outside Development). Wrotedev/post-phase/refinement/RUNBOOK.md. - Contracts: none produced; swagger snapshot not regenerated (only new path is the dev-only helper).
- Mocked: no new seam;
ISmsSender(LoggingSmsSender) row updated in mocks-registry (interim OTP channel- the Development-only capture affordance).
- Gate: build clean (0 new warnings) / tests green (369: 4 identity + 248 foundation + 117 API, incl. 3 new CORS/dev-otp integration tests + 8 dev-otp store/decorator unit tests).
- Handoff: backend/handoff/after-refinement-phase-0.md
- Notes for frontend: the client now really reaches the API cross-origin (CORS unblocked). No mock flag
was flipped —
authis still the only real domain (that's Phase 4).client/.env.developmentalready points athttps://localhost:5002; the API speaks HTTP/2 (browsers negotiate h2-over-TLS automatically). Read the OTP from the server console orGET /api/v1/dev/last_otp/{phone}(Development only).
backend-phase-15 — Messaging (tickets), partner centers & admin backoffice — 2026-07-10
- Shipped (FINAL backend phase): new
messagingschema —Tickets(UNIQUE(reference_code), status/ category, nullablebooking_id/refund_id),TicketParticipants(UNIQUE(ticket_id, user_id), soft-remove viaremoved_at),TicketMessages(is_internalhard boundary) — and newpartnerschema —PartnerCenters(IAuditable, encrypted+maskedsettlement_iban,commission_rateseparate fromplatform_fee_rate). Added thenurse_profiles.partner_center_idFK in place. One migration (MessagingAndPartnerCenters). CQRS: OpenTicket / AutoCreateCoordinationTicket / PostMessage / Add+RemoveParticipant / Close+ReopenTicket / LogEmergencyTicket / GetTicketThread (user vs admin view) / ListMyTickets / ListTicketsForAdmin; CreatePartnerCenter / UpdatePartnerCenter / VerifyPartnerCenter / SponsorNurse / GetCenterForBooking / ListPartnerCenters / GetPartnerCenterById / GetCenterDashboard. 5 new controllers (Tickets/AdminTickets/AdminPartnerCenters/Centers/ InternalCenters). Wired: b11IssueInvoicenow resolves issuer/settlement viaGetCenterForBooking; b11CreateRefundauto-opens arefundticket (sorefunds.ticket_idis always non-null); the card confirm + BNPL settle handlers auto-create the coordination ticket. Support-alert worklist + audit viewer reused from b1 (not rebuilt). The 4 DEFERRED tables were not created. - Contracts:
dev/contracts/domains/messaging-notifications-admin.md+ openapi snapshot refreshed (yes). - Mocked:
ILicenseVerificationService(eNamad / MoH permit — manual-approve at MVP) → 🟡 (see reports/mocks-registry.md). - Gate: build clean (0 new code warnings) / tests green (358 total: 4 identity + 240 foundation + 114 API, incl. 4 new merchant-of-record resolver tests + 8 new ticket/partner-center API tests).
- Handoff: backend/handoff/after-backend-phase-15.md
- Notes for frontend:
is_internalis stripped from the user thread view server-side (never trust the UI); no direct nurse↔customer channel / no phone numbers; ticket↔booking/refund links are optional (nullable); duplicate participant add = 409;settlement_ibanis only ever returned masked (last 4); merchant-of-record (invoice issuer + settlement) followspartner_centers, resolved byGET /internal/bookings/{id}/center.
backend-phase-14 — Reviews, ratings & patient care records — 2026-07-09
- Shipped: new
reviewsschema, 4 tables —Reviews(UNIQUE(booking_id),CHECK(rating 1–5), guardedmoderation_status,IAuditable),ReviewTagsMaster(seeded 5-tag vocab,UNIQUE(code)),ReviewTagLinks(UNIQUE(review_id, review_tag_master_id)),PatientCareRecords(encrypted, patient-scoped,(patient_id, recorded_at)index). One migration (ReviewsAndPatientCareRecords). CQRS: SubmitReview / ModerateReview / AttachReviewTags / ListReviewsForNurse / GetReviewModerationQueue / GetTagAggregates / WritePatientCareRecord / GetPatientHistory + theRecomputeNurseRatingfrom-source helper. 8 endpoints across 5 controllers. AddedNurseProfile.SetReviewAggregates(guarded aggregate write) +ICustomerProfileRepository.GetUserIdByProfileIdAsync. - Contracts:
dev/contracts/domains/reviews-records.md+ openapi snapshot refreshed (yes). - Mocked:
IReviewModerationService(AI pre-screen — keyword/pass-through) → 🟡 (see reports/mocks-registry.md). - Gate: build clean (0 new warnings) / tests green (346 total: 4 identity + 236 foundation + 106 API, incl. 13 new review/care-record handler tests + 4 new API tests).
- Handoff: backend/handoff/after-backend-phase-14.md
- Notes for frontend: publish gate — only
publishedreviews are ever public/counted; the nurse aggregate recomputes from source on every transition. Care records are patient-scoped + encrypted + strict access (owner/assigned-nurse/admin). Money-free domain. Support alerts stay internal (onlylowRatingAlertIdon the admin queue).
backend-phase-13 — Weekly nurse payouts (mocked PAYA/SATNA) — 2026-07-09
- Shipped: new
payoutsschema, 3 tables —NursePayoutBatches(holiday-shifted period/processing dates),NursePayouts(net-split CHECK, encryptediban_snapshot, forward-onlyPayoutStatus),NursePayoutBookingLinks(unconditionalUNIQUE(booking_id)= one-payout-per-booking-ever). One migration (NursePayoutEngine).Features/Payouts/*(compute-eligible / generate-batch / process / retry / mark-failed + admin batch-detail/list + nurse history; sharedPayoutSettlementstep).IPayoutRepository. ControllersAdminPayouts(admin, rate-limited)NursePayouts(nurse, tenancy-scoped). New seamIBankTransferProvider(mock PAYA/SATNA). SwappedINursePayoutStatusto the authoritative link-basedNursePayoutLinkStatusService(deleted the interim one). Added 2 config keys (payout_satna_threshold_irr,require_bnpl_settlement_for_payout).
- Contracts:
dev/contracts/domains/payouts.md+ openapi snapshot refreshed (yes — 7 payout paths). - Mocked:
IBankTransferProvider→ 🟡;INursePayoutStatus→ 🟢 (real link lookup). ReuseIHolidayCalendar,IFieldEncryptor,IDistributedLock,ICacheService. See reports/mocks-registry.md. - Gate: build clean (0 new code warnings) / tests green (329: 223 foundation + 102 api + 4 identity; +9 payout unit + 6 payout api). Migration builds; swagger serves all 7 payout paths.
- Handoff: backend/handoff/after-backend-phase-13.md
- Notes for frontend: f12-b13 = nurse
nurse_payouts/history(own payouts, masked IBAN, digit-string money) + admin payout console (admin_payouts/eligible|batches|batches/{id}|batches/{id}/process|{payoutId}/retry|mark_failed). Query params camelCase (page/pageSize/status/periodStart/periodEnd). Money is a digit string.
backend-phase-12 — BNPL: provider-financed installments (mocked) — 2026-07-09
- Shipped:
payments.BnplTransactions(1:1 withpayment_transaction,UNIQUE(payment_transaction_id), settle-split CHECK, forward-onlyBnplStatusmachine);Features/Bnpl/*(eligibility/initiate/verify/settle/ revert/callback/status);CheckoutBnplController(customer) +WebhooksBnplController(anon, signed) +AdminBnplController(admin), all rate-limited;LedgerPosting.BnplSettle(net-of-fee group w/bnpl_fee_expense); extracted sharedFeatures/Bookings/BookingConversion(used by b10 card + b12 settle). - Contracts: dev/contracts/domains/bnpl.md + openapi snapshot refreshed (yes).
- Mocked:
IBnplProvider(perprovider_codeviaIBnplProviderResolver) +ICurrencyNormalizer→ 🟡 (configurable mock commission %, non-instantsettled_at). See reports/mocks-registry.md. - Gate: build clean (0 new code warnings) / tests green (314 pass: 4 identity + 214 foundation + 96 api;
+14 new). Migration
BnplTransactionscreated (not applied to a live DB this session — SQLiteEnsureCreatedbuilds it for tests). - Handoff: backend/handoff/after-backend-phase-12.md
- Notes for frontend: f11-b12 = the "pay with installments" checkout (
checkout_bnpl/eligibility→initiate→ provider redirect; declined → fall back to card), the customer order view (checkout_bnpl/{id}), and the admin BNPL revert path with the ~7–10-day ETA. Money is IRR digit-strings;settledAtis nullable (not instant). A BNPL revert opens arefund_channel='bnpl_revert'refund (read viarefunds/{id}/status).
backend-phase-11 — Refunds, invoices & nurse clawbacks — 2026-07-09
- Shipped: the reversal leg via one migration in the
paymentsschema — 3 tablesRefunds(fee-leg decomposition +refund_channel+amount = fee_leg + payout_legCHECK + nullableticket_id, no FK until b15) /NurseClawbacks(nullableoriginal_payout_id/recovered_in_payout_iduntil b13) /Invoices(UNIQUEinvoice_numbersequential + UNIQUEbooking_id), plus theInvoiceNumberSequencescounter row. Features:CreateRefund(whole money-path underlock(booking:{id}:refund); decompose → Σ≤captured → channel → balanced ledger reversal via b10's helper; pre-payoutnurse_payablevs post-payoutnurse_clawback_receivablefork +pendingclawback + support alert),WriteOffClawback,ListRefunds,GetRefundStatus,IssueInvoice(VAT on commission only from config, sequential number, mocked مودیان),GetInvoice. Controllers: adminAdminRefunds/AdminClawbacks/AdminInvoices(rate-limited) + customerRefunds/Invoices. - Contracts: dev/contracts/domains/refunds-invoices.md + openapi snapshot refreshed — yes.
- Mocked:
IMoadianClient(new),IBnplProvider(thin pre-b12 stub),INursePayoutStatus(interim derivation; b13 owns real); reusedIPaymentProvider/IWebhookVerifier/IDistributedLock/INotificationDispatcher. See reports/mocks-registry.md. - Gate: build clean (0 new warnings) / tests green (300 pass: 205 Foundation + 91 Api + 4 Identity). Migration applied to the dev DB; swagger snapshot republished.
- Handoff: backend/handoff/after-backend-phase-11.md
- Notes for frontend: refunds are admin-only (no self-service); the only customer surfaces are
GET refunds/{id}/status(with the BNPLexpected_customer_refund_etadate + masked reference) andGET invoices/{booking_id}. Money is IRR digit-strings. Card refund = immediatesucceeded; BNPL =processing- ETA.
backend-phase-10 — Payments core: ledger, transactions, webhooks & card capture — 2026-07-06
- Shipped: the money core via one migration in a new
paymentsschema — 4 tablesPaymentGateways(encryptedconfig_json) /PaymentTransactions(the two filtered uniques —gateway_reference_codeWHERE NOT NULL,booking_idWHERE status='succeeded') /PaymentWebhookEvents(UNIQUE(provider_code, external_event_id)) / append-onlyLedgerEntries. FeaturesInitiatePayment,HandlePaymentWebhook,ConfirmPaymentAndPostLedger(internal),GetNursePayableBalance; controllersPOST bookings/{id}/payments, publicPOST webhooks/payments/{provider},GET nurses/{id}/payable_balance. ExtractedBookingFactoryso b10's real capture reuses b9's conversion (b9 unchanged). - Contracts:
dev/contracts/domains/payments.mdwritten; openapi snapshot refreshed (dev/contracts/openapi/swagger.v1.json— the three b10 paths + DTOs present). - Mocked:
IPaymentProvider,ISettlementSplitProvider,IWebhookVerifier,IDistributedLock→ 🟡 (see reports/mocks-registry.md).IPaymentCaptureSimulator(b9) retained for b9's Convert path/tests. - Gate: build clean (0 new warnings) / tests green (Foundation 198, Identity 4, Api 83; +12 Foundation +4 Api new).
- Handoff: backend/handoff/after-backend-phase-10.md
- Notes for frontend: money is an IRR digit-string; a booking exists only on capture (pay against the
accepted request id; the booking appears
confirmedafter the webhook); never expose internalaccount_types (checkout = gross + commission/VAT); payment idempotent end-to-end (Idempotency-Keyheader).
backend-phase-9 — Bookings, sessions, care instructions & EVV — 2026-07-06
- Shipped: the post-payment engine via one additive migration in the
bookingschema — 5 tablesBookings/BookingSessions/BookingCareInstructions/VisitVerifications/CancellationPolicies(seeded 4 tiers) + 2 config rows (no_show_threshold_minutes,no_show_scan_cadence_hours).Convert(mock-capture → booking 1:1, three-amount split, snapshots, N sessions), care-instructions submit + gated read, EVVcheck_in/check_out(advisory address match, dispute window on completion),transition,cancelbooking/session (policy snapshot + un-started refund),detect_no_shows, cancellation-policy CRUD. Controllers:Bookings/BookingSessions/AdminEvv/AdminCancellationPolicies. - Contracts: dev/contracts/domains/bookings-evv.md + openapi snapshot refreshed (yes).
- Mocked:
IPaymentCaptureSimulatorintroduced (🟡) — the temporary conversion trigger; reusesIGeocoder/IFieldEncryptor/INotificationDispatcher/ISupportAlertService. Real capture trigger = b10. - Gate: build clean (0 new code warnings) / tests green (269: 186 foundation + 4 identity + 79 API).
- Handoff: backend/handoff/after-backend-phase-9.md
- Notes for frontend (f8-b9): money is an IRR digit-string (three amounts reconcile). The nurse view
omits
addressSnapshotJson; care-instruction clinical fields are assigned-nurse/admin only, post-confirmation (GET bookings/care_instructions/{id}); raw EVV GPS is gated to owning nurse + admin. Routes are action-style (bookings/get/{id},booking_sessions/check_in/{id},booking_sessions/today,admin_evv/list). Payout eligibility comes fromdisputeWindowEndsAt/payoutEligibleAt, nevercompletedalone.
backend-phase-8 — Booking requests (pre-payment intent) — 2026-07-06
- Shipped: the money-free request lifecycle via one additive migration — new
bookingschema, 1 tableBookingRequests. The customer-side (CreateBookingRequest— tenancy invariant patient+address∈customer & variant∈nurse, bookability + same-gender match, response deadline frozen fromnurse_response_deadline_hours;CancelBookingRequest), the nurse-side (AcceptBookingRequest— freezespayment_deadline_atfrombooking_payment_deadline_minutes=30, self-guards the response deadline;RejectBookingRequest— reason required), the reads (ListBookingRequestsrole-scoped inbox +GetBookingRequestparty/admin, nurse view masks the full address, exposes onlycustomer_notes), and the forward-only status guard (BookingRequestTransitions) + the idempotent, paginatedExpireBookingRequestssweep behind a recurringBackgroundService(BookingRequestExpiryHostedService, reuses the b1IJobSchedulerseam) + an admin manual trigger. 2 controllers:BookingRequestsController(customer/nurse) +AdminBookingRequestsController(POST admin_booking_requests/expire). Inbox/expiry covering indexes(nurse_id,status)/(customer_id,status)/(status,response_deadline)/(status,payment_deadline). No money column, nobookingsrow, no snapshot, no price anywhere. - Contracts: dev/contracts/domains/booking-requests.md + openapi snapshot refreshed (yes — the 7 booking
paths +
BookingRequestDto/BookingRequestListItemDto/ExpireBookingRequestsResult). - Mocked: nothing new — reuses
IPlatformConfig/INotificationDispatcher/IJobScheduler/IDateTimeProvider/ICurrentUser. No newmocks-registry.mdrow (theIJobSchedulerrow was updated to note the second hosted job). - Gate: build clean (0 new code warnings) / tests green (215 pass: +20 booking = 14 handler-unit + the transition machine + 4 DB-backed SQLite + 5 API integration). Migration applied to the dev DB; API boots with all booking paths in Swagger; the expiry sweep runs on startup (verified against SQL Server).
- Handoff: backend/handoff/after-backend-phase-8.md
- Notes for frontend: f7-b8 = the request form (C4 →
booking_requests/create), the status tracker (C5 →booking_requests/get/{id}+ customer inbox with the response + 30-min payment countdowns +cancel), and the nurse inbox (booking_requests/list?role=nurse+accept/reject). Gender required (male/female/any); deadlines are server-frozen UTC timestamps; the nurse sees onlycustomer_notes+ a masked address.
backend-phase-7 — Search & matching (nurse search index) — 2026-07-05
- Shipped: the discovery layer via one additive migration — new
searchschema, 1 tableNurseSearchIndices(the denormalizednurse_search_index): one flat row per (bookable variant × covered area) with copied category/price/unit,city_id/district_id(NULL = whole city),nurse_gender- rating aggregates, and the single
is_searchablegate. It is a read-only projection, maintained inline in each source write's own transaction byISearchIndexMaintainer(SearchIndexMaintainer) wired into the b3/b4/b5/b6 handlers (ReindexVariant/ReindexNurse/FanOutServiceArea/RemoveServiceAreaRows+Rebuild). Read side is theINurseSearchseam — real MVP implSqlNurseSearch(reads onlyis_searchable=1; category/city/district(NULL-aware)/gender/price filters + rating sort + pagination). 2 controllers: publicSearchController(GET search/nurses) + adminAdminSearchController(POST admin_search/rebuild_index, idempotent convergence rebuild). Covering search index + filtered-unique(variant_id, city_id, district_id)pair (NULL participating) +nurse_idindex.
- rating aggregates, and the single
- Contracts: dev/contracts/domains/search.md + openapi snapshot refreshed (yes —
search/nurses+admin_search/rebuild_index+ DTOs). - Mocked:
INurseSearch→ 🟢 SQL is real (Elastic backend 🟡 deferred, configSearch:Backend);ISearchIndexMaintainerinline path real, outbox/feeder 🟡 deferred (see reports/mocks-registry.md). - Gate: build clean (0 new code warnings) / tests green (167 pass: +9 DB-backed search + 4 API integration; affected b3/b4/b5/b6 handler tests updated for the new dependency).
- Handoff: backend/handoff/after-backend-phase-7.md
- Notes for frontend: f6-b7 =
GET api/v1/search/nurses(public; snake_case paramsservice_category_id/city_idrequired, optionaldistrict_id/nurse_gender/min_price/max_price/price_unit;page/page_sizedefault 1/50 max 100). Returns only searchable nurses;districtId=nullresult = whole city;priceis an IRR digit string; sort is rating-desc only.required_caregiver_gendercapture into booking is b8.
backend-phase-6 — Nurse verification & credentials (mocked vendors) — 2026-07-02
- Shipped: the trust engine via one additive migration — new
verifschema, 5 tables:NurseVerifications(status= the single source of verification truth),VerificationStepTypes(seeded catalog — six stable codesidentity_kyc/shahkar_match/moh_competency_license/ino_membership/criminal_record/bank_account_verification),VerificationSteps(one per required step-type; snapshotsis_automated),VerificationDocuments(metadata only — bytes never in the DB),NurseCredentials(credential_numberencrypted, never serialized). 15 endpoints across 4 controllers —nurse_verification(submit/get/upload_url/documents + automatedidentity_kyc/shahkar_match/bank_account_verification/run),admin_verification_step_types(list/upsert/deactivate, dup code → 409),admin_verifications(queue/detail/decide/suspend/scan_expiring), publicnurses/{id}/trust_badge.nurse_profiles.is_verifiedis the only derived boolean, flipped only inside the finalize transaction (reversed transactionally on suspend/expiry). Three new mock vendor seams (IShahkarVerifier,IIdentityKycProvider,ICredentialVerifier); reuses b3IBankAccountOwnershipVerifier+ b0IObjectStorage/IFieldEncryptor. The expiry-scan logic ships asScanExpiringCredentialsCommand; the scheduled cron is deferred (adminscan_expiringis the entry point; configverification_expiry_scan_cadence_hours, default 24). - Contracts: dev/contracts/domains/verification.md + openapi snapshot refreshed (yes — all 15 b6 paths).
- Mocked:
IShahkarVerifier,IIdentityKycProvider,ICredentialVerifier→ 🟡 (deterministic mocks; see reports/mocks-registry.md). All vendor/money calls are mocked. - Gate: build clean (0 new code warnings) / tests green (153 pass). Swagger exposes all 15 b6 paths.
- Handoff: backend/handoff/after-backend-phase-6.md
- Notes for frontend:
isBookable/isVerifiedare read-only, server-derived — never infer verification client-side. Credential numbers never cross the wire (trust badge = types only).step.isAutomateddrives the UI (/runbutton vs upload flow). Prereqs enforced with 400 (Shahkar needs KYC; bank needs KYC + a primary b3 account). Shared-SIM / vendor fails are 200 withstepStatus:"failed"+failureReason, not HTTP errors. Documents are short-lived signed URLs. Routes are action-style POST; refresh afterselect_role. Public trust badge (f6) is[AllowAnonymous].
backend-phase-5 — Service catalog & nurse pricing variants — 2026-07-02
- Shipped: five tables via one additive migration (
ServiceCatalogAndNurseVariants) — newcatalogschemaServiceCategories/ServiceOptionGroups(nullableservice_category_id= cross-category) /ServiceOptionValues/NurseServiceVariants(PriceBIGINT IRR,PriceUnit,SessionCount?,DisplayName,OptionSetHash) /NurseServiceVariantOptions(UNIQUE(variant_id, option_group_id)); seed of 5 categories (nameFa+nameEn) viaHasData. 16 CQRS slices across 3 controllers (catalogpublic browse,admin_catalogCRUD + set-active,nurse_variantscreate/update/set-active/ list/get). Duplicate-listing guard =OptionSetHash+ filteredUNIQUE(nurse_id, service_category_id, option_set_hash) WHERE deleted_at IS NULL+ 409 pre-check. Public catalog reads cached behind aCatalogCachegeneration token (invalidate on any admin write). ShipsIVariantSnapshotSerializer(pure, for b8). No new seam. - Contracts: dev/contracts/domains/catalog.md + openapi snapshot refreshed (yes — 14 new catalog/admin_catalog/nurse_variants paths; 71 total).
- Mocked: none — this phase mocks nothing and adds no
reports/mocks-registry.mdrow. - Gate: build clean (0 new code warnings) / tests green (122 pass: +10 handler/serializer unit,
+11
Baya.Test.Apiintegration). Migration verified to apply on a real SQL Server; swagger exposes all b5 paths. Adversarial 4-dimension review: 0 confirmed findings. - Handoff: backend/handoff/after-backend-phase-5.md
- Notes for frontend: the variant is the bookable unit (not the nurse).
priceis a string of IRR digits; the total isprice+priceUnit+sessionCount, never price alone. A NULL-category option group is cross-category (render it under every category; required ones must be answered). Duplicate identical listing → 409; missing required dimension → 400.displayNameauto-generates (editable). Deactivate, never delete. Routes are action-style POST (admin_catalog/create_category,nurse_variants/create, …); groups/values are admin-authored (only categories are seeded).
backend-phase-4 — Geography, addresses & nurse service areas — 2026-07-02
- Shipped: five tables via one migration (
GeographyAddressesServiceAreas) — newgeoschemaProvinces1:NCities1:NDistricts(+NurseServiceAreas) andusr.CustomerAddresses; seed (31 provinces + capital cities + Tehran's 22 مناطق viaHasData); 20 CQRS slices across 4 controllers (geopublic lookups incl./tree,admin_geoCRUD + set_active,nurse_service_areas,customer_addresses); newIGeocoderseam (deterministic mock,Seams:Geocoding); per-domain repos onIUnitOfWork; enc value converters for the address PII columns;409 Conflictadded toOperationResult/BaseController. Whole-city (district_id NULL) uniqueness via a filtered-index pair; single-primary address via filteredUNIQUE(customer_id) WHERE is_primary=1; geo reads cached behind a generation-token scheme with invalidate-on-admin-write. - Contracts: dev/contracts/domains/geography-addresses.md + openapi snapshot refreshed (yes — 20 new geo/service-area/address paths).
- Mocked:
IGeocoder→ 🟡 (see reports/mocks-registry.md). - Gate: build clean (0 new code warnings) / tests green (103 pass: +16
Baya.Test.Apiintegration, +12 handler unit tests). MigrationGeographyAddressesServiceAreasapplies on startup; swagger exposes all b4 paths. - Handoff: backend/handoff/after-backend-phase-4.md
- Notes for frontend:
districtId=nullmeans whole city (a real choice) everywhere. Duplicate service area → 409. Addresses come back decrypted for the owner withlatitude/longitude(nullable when ungeocoded — geocoding is mocked). Routes are action-style (admin_geo/create_city,nurse_service_areas/add,customer_addresses/create, …). Admin geo needs an admin token.
backend-phase-3 — Identity: profiles, patients & nurse bank accounts — 2026-07-02
- Shipped: four
usrtables via one migration (IdentityProfilesPatientsBankAccounts) —NurseProfiles(1:1Users; guardedis_verifiedno public setter; read-only aggregates; soft-delete),CustomerProfiles(thin payer; enc emergency contact),Patients(care recipient, tenancy-scoped;is_activearchive; encinitial_medical_notes),NurseBankAccounts(enciban+UNIQUE(iban_hash)+ filteredUNIQUE(nurse_id) WHERE is_primary=1; استعلام شبا inquiry fields); 15 CQRS slices across 4 controllers (nurse_profiles,customer_profiles,patients,nurse_bank_accounts); newIBankAccountOwnershipVerifierseam (mock = deterministic شبا match); per-domain repositories onIUnitOfWork; enc value converters for the new PII columns. Also activated FluentValidation repo-wide (AddApplicationServicesnow registers everyAbstractValidator<T>— theValidateCommandBehavior/model-state filter were previously starved). - Contracts: dev/contracts/domains/identity-profiles.md + openapi snapshot refreshed (yes — new nurse/customer/patient/bank paths).
- Mocked:
IBankAccountOwnershipVerifier→ 🟡 (see reports/mocks-registry.md). - Gate: build clean (0 new code warnings) / tests green (75 pass: +13
Baya.Test.Apiintegration, +15 handler unit tests). Migration applied to the dev DB on startup; swagger exposes all b3 paths. - Handoff: backend/handoff/after-backend-phase-3.md
- Notes for frontend: role scoping needs the role claim in the token — refresh after
select_rolebefore calling these. IBAN comes back masked (last-4).isVerified/aggregates are read-only. Patientget/updateof another customer's id → 404. Addresses/service-areas are deferred to b4.
backend-phase-2 — Identity: phone-OTP auth, sessions & roles (REST) — 2026-07-02
- Shipped: the six-endpoint REST auth surface (
auth/request_otp,auth/verify_otp,auth/refresh,auth/logout,me,me/select_role) wrapping the existing JWE/TOTP/RBAC engine; newusr.UserSessions(refresh-token rotation + revoke-all on replayed token);usr.Usersextended (Gender,NationalIdenc NULL,ShahkarVerifiedAtauto-reset on phone change,PhoneHashUNIQUE,IsActive,DeletedAt+ soft-delete filter); phone/email/national-id encrypted at rest (EF value converter overIFieldEncryptor);usr.UserRolesgrant/revoke audit trail + revoked filter; 7 roles seeded;ISmsSenderseam (mock logs the code); 3 auth config keys;OperationResult/BaseControllerlearned 401/403. - Contracts: dev/contracts/domains/identity-auth.md + openapi snapshot refreshed (yes — 22 paths).
- Mocked:
ISmsSender→ 🟡 (see reports/mocks-registry.md). - Gate: build clean (0 new code warnings) / tests green (47 pass: 10 new
Baya.Test.Apiintegration + 14 new handler unit tests). MigrationIdentitySessionsAndUserExtensionsapplied to the dev DB; full §7 flow verified live (OTP in log, tokens, 401/403/429, rotation, replay-revoke, logout stamp-kill). - Handoff: backend/handoff/after-backend-phase-2.md
- Notes for frontend: exact paths are
request_otp/verify_otp/select_role(snake_case transformer — not theotp/requestsketch). Bodies camelCase. Fresh users:roles: []→ role router →me/select_role→ refresh tokens to pick up role claims./mephone is masked. SMS is mocked — read the OTP from the server log.
backend-phase-1 — Config, reference & platform signals — 2026-07-02
- Shipped: first marketplace migration baseline (
InitialMarketplaceBaseline, newopsschema) with 6 tables (PlatformConfigs,AuditLogs,SystemEvents,IranianHolidays,Notifications,SupportAlerts) + seed (12 config keys, 7 holidays); platform-signal facadesIPlatformConfig/IHolidayCalendar/IAnalyticsSink/IAuditLogger/INotificationService/ISupportAlertService(Persistence/Services/);AuditFieldInterceptorextended to write append-onlyaudit_logsrows forIAuditableentities; real in-appINotificationDispatcher(b0 stub removed); notification-retention hosted service; 5 controllers (admin config/holidays/audit/support-alerts + current-user notifications). - Contracts:
dev/contracts/domains/config-reference.md+ openapi snapshot refreshed (yes — 16 paths). - Mocked:
IHolidayCalendar,IAnalyticsSink, retentionIJobScheduler→ 🟡;INotificationDispatcherflipped to in-app-real 🟡 (SMS/push deferred). See reports/mocks-registry.md. - Gate: build clean (0 new code warnings) / tests green (22 pass: 4 identity + 18 foundation). Migration applied to the dev DB; API boots with all 16 paths in Swagger; retention job runs on startup.
- Handoff: backend/handoff/after-backend-phase-1.md
- Notes for frontend: f14 =
notifications/*(envelope unchanged; unread-first lists;data_jsonis a typed deep-link payload). f15 = adminplatform_config/*,holidays/*,audit/get_audit_trail,support_alerts/*(DynamicPermission). Paginationpage/page_size(default 50, max 100).
backend-phase-0 — Foundation, cross-cutting seams & starter cleanup — 2026-06-28
- Shipped: removed the
Orderdemo (entity/feature/repo/config/gRPC) + 3 old migrations; freshInitialBaselinemigration; REST surface (PingController+System/PingCQRS);ICurrentUser+AuditFieldInterceptor; five cross-cutting seams (IDateTimeProvider,IFieldEncryptor,ICacheService,IObjectStorage,INotificationDispatcher) with mocks;LoggingBehavior+ rate limiter (per-IP global +otp/auth/sensitive). - Contracts:
dev/contracts/openapi/swagger.v1.jsonpublished (envelope + ping schemas). - Mocked: the 5 seams above → 🟡 (see reports/mocks-registry.md).
- Gate: build clean (0 new warnings) / tests green (10 pass). Live API verified vs
192.168.100.14(migration applied + seeded; ping200; rate-limit429). - Handoff: backend/handoff/after-backend-phase-0.md
- Notes for frontend:
ApiResultenvelope is fixed (camelCase body, snake_case URLs);GET /api/v1/ping/get_statusis live to wire types against;429on over-limit.