From 314763f7641d9ee03d5ea1d9cf3b11f79a0b4d41 Mon Sep 17 00:00:00 2001 From: hamid Date: Mon, 13 Jul 2026 11:26:39 +0330 Subject: [PATCH] refinement phase 3 --- dev/contracts/conventions/api-conventions.md | 4 +- dev/contracts/domains/_TEMPLATE.md | 2 +- dev/contracts/domains/bnpl.md | 14 + dev/contracts/domains/booking-requests.md | 18 + dev/contracts/domains/bookings-evv.md | 6 +- dev/contracts/domains/catalog.md | 6 +- dev/contracts/domains/config-reference.md | 14 +- dev/contracts/domains/geography-addresses.md | 14 +- dev/contracts/domains/identity-auth.md | 11 + dev/contracts/domains/identity-profiles.md | 13 + .../domains/messaging-notifications-admin.md | 20 + dev/contracts/domains/messaging.md | 13 + dev/contracts/domains/payments.md | 8 + dev/contracts/domains/payouts.md | 13 + dev/contracts/domains/refunds-invoices.md | 21 + dev/contracts/domains/reviews-records.md | 16 + dev/contracts/domains/search.md | 16 +- dev/contracts/domains/verification.md | 16 +- dev/contracts/openapi/swagger.v1.json | 2860 +++++++- .../frontend/requests/for-backend.md | 71 +- .../V1/AdminPartnerCentersController.cs | 7 + .../V1/BookingRequestsController.cs | 7 + .../V1/BookingReviewsController.cs | 14 + .../Controllers/V1/BookingsController.cs | 16 + .../Controllers/V1/CheckoutBnplController.cs | 9 +- .../V1/CustomerProfilesController.cs | 17 + .../Controllers/V1/NursePayoutsController.cs | 21 + .../Controllers/V1/NurseProfilesController.cs | 17 + .../V1/NurseVerificationController.cs | 8 + .../Controllers/V1/NursesController.cs | 8 + .../V1/PatientCareRecordsController.cs | 33 +- .../Controllers/V1/RefundsController.cs | 7 + .../BaseController/BaseController.cs | 8 + .../Baya.Application/Common/AvatarUpload.cs | 30 + .../Baya.Application/Common/JsonCodeList.cs | 29 + .../Contracts/Audit/IAuditLogger.cs | 8 +- .../Contracts/Persistence/IBnplRepository.cs | 4 + .../Persistence/IBookingRequestRepository.cs | 5 + .../Persistence/INurseProfileRepository.cs | 6 + .../IPatientCareRecordRepository.cs | 7 + .../Persistence/IPayoutRepository.cs | 12 + .../Persistence/IRefundRepository.cs | 4 + .../Persistence/IReviewRepository.cs | 4 + .../Persistence/ITicketRepository.cs | 6 +- .../Persistence/IUserAccountRepository.cs | 3 + .../Persistence/IVerificationRepository.cs | 4 + .../CreateAddressCommand.Handler.cs | 25 +- .../CreateAddressCommand.Validator.cs | 5 + .../CreateAddress/CreateAddressCommand.cs | 4 +- .../UpdateAddressCommand.Handler.cs | 11 +- .../UpdateAddressCommand.Validator.cs | 5 + .../UpdateAddress/UpdateAddressCommand.cs | 4 +- .../GetAuditTrailQuery.Handler.cs | 4 +- .../GetAuditTrail/GetAuditTrailQuery.cs | 12 +- .../SettleBnplOrderCommand.Handler.cs | 5 + .../CheckBnplEligibilityQuery.Handler.cs | 5 +- .../CheckBnplEligibilityQuery.Validator.cs | 7 +- .../CheckBnplEligibilityQuery.cs | 13 +- .../GetBnplOrderByRequestQuery.Handler.cs | 25 + .../GetBnplOrderByRequestQuery.cs | 9 + .../Features/Booking/BookingRequestMapper.cs | 6 +- .../GetCheckoutSummaryQuery.Handler.cs | 73 + .../GetCheckoutSummaryQuery.cs | 12 + .../CreatePatientCommand.Handler.cs | 7 +- .../CreatePatientCommand.Validator.cs | 6 + .../CreatePatient/CreatePatientCommand.cs | 4 +- .../RequestOtp/RequestOtpCommand.Handler.cs | 6 +- .../UpdatePatientCommand.Handler.cs | 7 +- .../UpdatePatientCommand.Validator.cs | 6 + .../UpdatePatient/UpdatePatientCommand.cs | 4 +- .../UploadCustomerAvatarCommand.Handler.cs | 53 + .../UploadCustomerAvatarCommand.cs | 14 + .../UploadNurseAvatarCommand.Handler.cs | 46 + .../UploadNurseAvatarCommand.cs | 14 + .../UpsertCustomerProfileCommand.Handler.cs | 19 +- .../UpsertCustomerProfileCommand.cs | 8 +- .../VerifyOtp/VerifyOtpCommand.Handler.cs | 12 +- .../Features/Identity/IdentityDefaults.cs | 8 + .../Features/Identity/PatientRules.cs | 8 + .../Features/Invoices/InvoiceDtoFactory.cs | 1 + .../PostMessage/PostMessageCommand.Handler.cs | 13 +- .../PostMessage/PostMessageCommand.cs | 2 +- .../GetTicketThreadQuery.Handler.cs | 15 +- .../ListMyTicketsQuery.Handler.cs | 2 +- .../ListMyTickets/ListMyTicketsQuery.cs | 1 + .../GetNursePublicProfileQuery.Handler.cs | 19 + .../GetNursePublicProfileQuery.cs | 12 + .../SetPartnerCenterActiveCommand.Handler.cs | 24 + .../SetPartnerCenterActiveCommand.cs | 11 + .../CarePlanSerialization.cs | 61 + .../UpsertCarePlanCommand.Handler.cs | 51 + .../UpsertCarePlan/UpsertCarePlanCommand.cs | 14 + .../WritePatientCareRecordCommand.Handler.cs | 2 + .../WritePatientCareRecordCommand.cs | 11 +- .../PatientCareRecords/PatientAccess.cs | 51 + .../GetCarePlan/GetCarePlanQuery.Handler.cs | 35 + .../Queries/GetCarePlan/GetCarePlanQuery.cs | 9 + .../GetPatientHistoryQuery.Handler.cs | 18 +- .../GetRecordAccessQuery.Handler.cs | 25 + .../GetRecordAccess/GetRecordAccessQuery.cs | 9 + ...firmPaymentAndPostLedgerCommand.Handler.cs | 6 + .../GetNurseEarningsQuery.Handler.cs | 48 + .../GetNurseEarnings/GetNurseEarningsQuery.cs | 9 + .../GetNurseEarningsBalanceQuery.Handler.cs | 53 + .../GetNurseEarningsBalanceQuery.cs | 8 + .../GetNursePayoutDetailQuery.Handler.cs | 33 + .../GetNursePayoutDetailQuery.cs | 9 + .../CancelBookingAndRefundCommand.Handler.cs | 70 + .../CancelBookingAndRefundCommand.cs | 18 + ...tCancellationPolicyPreviewQuery.Handler.cs | 91 + .../GetCancellationPolicyPreviewQuery.cs | 9 + .../GetRefundByBookingQuery.Handler.cs | 25 + .../GetRefundByBookingQuery.cs | 9 + .../GetMyReview/GetMyReviewQuery.Handler.cs | 29 + .../Queries/GetMyReview/GetMyReviewQuery.cs | 9 + .../GetReviewEligibilityQuery.Handler.cs | 38 + .../GetReviewEligibilityQuery.cs | 9 + .../SubmitCredentialDetailsCommand.Handler.cs | 84 + ...ubmitCredentialDetailsCommand.Validator.cs | 17 + .../SubmitCredentialDetailsCommand.cs | 21 + .../Models/Addresses/CustomerAddressDto.cs | 1 + .../Models/ApiResult/ApiResult.cs | 6 + .../Booking/BookingRequestDetailProjection.cs | 5 +- .../Models/Booking/BookingRequestDto.cs | 9 +- .../Booking/BookingRequestListItemDto.cs | 6 +- .../Models/Booking/CheckoutContext.cs | 18 + .../Models/Booking/CheckoutSummaryDto.cs | 32 + .../Models/Common/OperationResult.cs | 20 + .../Models/Configuration/PlatformConfigDto.cs | 9 +- .../Models/Identity/AvatarUploadResult.cs | 4 + .../Models/Identity/CustomerProfileDto.cs | 4 +- .../Models/Identity/NurseProfileDto.cs | 3 +- .../Models/Identity/PatientDto.cs | 7 +- .../Models/Identity/RequestOtpResult.cs | 3 +- .../Models/Invoices/InvoiceProjections.cs | 2 + .../Models/Messaging/MessagingProjections.cs | 10 +- .../Models/Nurses/NursePublicProfileDto.cs | 39 + .../Models/Patients/CarePlanDtos.cs | 22 + .../Models/Payouts/PayoutProjections.cs | 58 +- .../Refunds/CancellationPolicyPreviewDto.cs | 35 + .../Models/Refunds/RefundProjections.cs | 12 +- .../Models/Reviews/CareRecordProjections.cs | 6 +- .../Models/Reviews/ReviewGateDtos.cs | 29 + .../Models/Reviews/ReviewProjections.cs | 1 + .../Models/Search/NurseSearchResultDto.cs | 8 +- .../Models/Verification/VerificationDtos.cs | 4 +- .../Entities/Bnpl/BnplProviderCodes.cs | 7 +- .../Entities/Identity/CustomerAddress.cs | 5 + .../Entities/Identity/CustomerProfile.cs | 8 + .../Entities/Identity/GeocodeSources.cs | 11 + .../Entities/Identity/NurseProfile.cs | 4 + .../Baya.Domain/Entities/Identity/Patient.cs | 9 + .../Entities/Identity/PatientCarePlan.cs | 27 + .../Entities/Messaging/TicketMessage.cs | 4 + .../Entities/Messaging/TicketParticipant.cs | 6 + .../Entities/Reviews/PatientCareRecord.cs | 5 + .../Entities/Search/NurseSearchIndex.cs | 8 + .../IdentityConfig/CustomerAddressConfig.cs | 1 + .../IdentityConfig/CustomerProfileConfig.cs | 3 + .../IdentityConfig/NurseProfileConfig.cs | 1 + .../IdentityConfig/PatientCarePlanConfig.cs | 26 + .../IdentityConfig/PatientConfig.cs | 1 + .../MessagingConfig/TicketMessageConfig.cs | 7 + .../SearchConfig/NurseSearchIndexConfig.cs | 4 +- ...12223125_RefinementPhase3TierA.Designer.cs | 5966 ++++++++++++++++ .../20260712223125_RefinementPhase3TierA.cs | 145 + ...13065646_RefinementPhase3TierB.Designer.cs | 6023 ++++++++++++++++ .../20260713065646_RefinementPhase3TierB.cs | 72 + ...13072137_RefinementPhase3TierC.Designer.cs | 6035 +++++++++++++++++ .../20260713072137_RefinementPhase3TierC.cs | 57 + .../ApplicationDbContextModelSnapshot.cs | 102 +- .../Repositories/BnplRepository.cs | 11 + .../Repositories/BookingRequestRepository.cs | 85 +- .../Repositories/CustomerAddressRepository.cs | 1 + .../Repositories/CustomerProfileRepository.cs | 3 +- .../Repositories/InvoiceRepository.cs | 1 + .../Repositories/NurseProfileRepository.cs | 66 +- .../PatientCareRecordRepository.cs | 10 +- .../Repositories/PatientRepository.cs | 48 +- .../Repositories/PayoutRepository.cs | 94 +- .../Repositories/RefundRepository.cs | 58 +- .../Repositories/ReviewRepository.cs | 24 + .../Repositories/TicketRepository.cs | 53 +- .../Repositories/UserAccountRepository.cs | 3 + .../Repositories/VerificationRepository.cs | 8 +- .../Services/Audit/AuditLogger.cs | 30 +- .../Configuration/PlatformConfigService.cs | 4 +- .../Services/Search/SearchIndexMaintainer.cs | 37 +- .../Services/Search/SqlNurseSearch.cs | 10 +- .../CreateBookingRequestHandlerTests.cs | 3 +- .../Booking/GetCheckoutSummaryHandlerTests.cs | 80 + .../RespondBookingRequestHandlerTests.cs | 3 +- .../Identity/NurseProfileHandlersTests.cs | 2 +- .../GetReviewEligibilityHandlerTests.cs | 83 + 194 files changed, 24211 insertions(+), 274 deletions(-) create mode 100644 dev/contracts/domains/messaging.md create mode 100644 server/src/Core/Baya.Application/Common/AvatarUpload.cs create mode 100644 server/src/Core/Baya.Application/Common/JsonCodeList.cs create mode 100644 server/src/Core/Baya.Application/Features/Bnpl/Queries/GetBnplOrderByRequest/GetBnplOrderByRequestQuery.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/Bnpl/Queries/GetBnplOrderByRequest/GetBnplOrderByRequestQuery.cs create mode 100644 server/src/Core/Baya.Application/Features/Booking/Queries/GetCheckoutSummary/GetCheckoutSummaryQuery.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/Booking/Queries/GetCheckoutSummary/GetCheckoutSummaryQuery.cs create mode 100644 server/src/Core/Baya.Application/Features/Identity/Commands/UploadCustomerAvatar/UploadCustomerAvatarCommand.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/Identity/Commands/UploadCustomerAvatar/UploadCustomerAvatarCommand.cs create mode 100644 server/src/Core/Baya.Application/Features/Identity/Commands/UploadNurseAvatar/UploadNurseAvatarCommand.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/Identity/Commands/UploadNurseAvatar/UploadNurseAvatarCommand.cs create mode 100644 server/src/Core/Baya.Application/Features/Nurses/Queries/GetNursePublicProfile/GetNursePublicProfileQuery.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/Nurses/Queries/GetNursePublicProfile/GetNursePublicProfileQuery.cs create mode 100644 server/src/Core/Baya.Application/Features/PartnerCenters/Commands/SetPartnerCenterActive/SetPartnerCenterActiveCommand.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/PartnerCenters/Commands/SetPartnerCenterActive/SetPartnerCenterActiveCommand.cs create mode 100644 server/src/Core/Baya.Application/Features/PatientCareRecords/CarePlanSerialization.cs create mode 100644 server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/UpsertCarePlan/UpsertCarePlanCommand.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/UpsertCarePlan/UpsertCarePlanCommand.cs create mode 100644 server/src/Core/Baya.Application/Features/PatientCareRecords/PatientAccess.cs create mode 100644 server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetCarePlan/GetCarePlanQuery.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetCarePlan/GetCarePlanQuery.cs create mode 100644 server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetRecordAccess/GetRecordAccessQuery.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetRecordAccess/GetRecordAccessQuery.cs create mode 100644 server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarnings/GetNurseEarningsQuery.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarnings/GetNurseEarningsQuery.cs create mode 100644 server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarningsBalance/GetNurseEarningsBalanceQuery.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarningsBalance/GetNurseEarningsBalanceQuery.cs create mode 100644 server/src/Core/Baya.Application/Features/Payouts/Queries/GetNursePayoutDetail/GetNursePayoutDetailQuery.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/Payouts/Queries/GetNursePayoutDetail/GetNursePayoutDetailQuery.cs create mode 100644 server/src/Core/Baya.Application/Features/Refunds/Commands/CancelBookingAndRefund/CancelBookingAndRefundCommand.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/Refunds/Commands/CancelBookingAndRefund/CancelBookingAndRefundCommand.cs create mode 100644 server/src/Core/Baya.Application/Features/Refunds/Queries/GetCancellationPolicyPreview/GetCancellationPolicyPreviewQuery.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/Refunds/Queries/GetCancellationPolicyPreview/GetCancellationPolicyPreviewQuery.cs create mode 100644 server/src/Core/Baya.Application/Features/Refunds/Queries/GetRefundByBooking/GetRefundByBookingQuery.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/Refunds/Queries/GetRefundByBooking/GetRefundByBookingQuery.cs create mode 100644 server/src/Core/Baya.Application/Features/Reviews/Queries/GetMyReview/GetMyReviewQuery.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/Reviews/Queries/GetMyReview/GetMyReviewQuery.cs create mode 100644 server/src/Core/Baya.Application/Features/Reviews/Queries/GetReviewEligibility/GetReviewEligibilityQuery.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/Reviews/Queries/GetReviewEligibility/GetReviewEligibilityQuery.cs create mode 100644 server/src/Core/Baya.Application/Features/Verification/Commands/SubmitCredentialDetails/SubmitCredentialDetailsCommand.Handler.cs create mode 100644 server/src/Core/Baya.Application/Features/Verification/Commands/SubmitCredentialDetails/SubmitCredentialDetailsCommand.Validator.cs create mode 100644 server/src/Core/Baya.Application/Features/Verification/Commands/SubmitCredentialDetails/SubmitCredentialDetailsCommand.cs create mode 100644 server/src/Core/Baya.Application/Models/Booking/CheckoutContext.cs create mode 100644 server/src/Core/Baya.Application/Models/Booking/CheckoutSummaryDto.cs create mode 100644 server/src/Core/Baya.Application/Models/Identity/AvatarUploadResult.cs create mode 100644 server/src/Core/Baya.Application/Models/Nurses/NursePublicProfileDto.cs create mode 100644 server/src/Core/Baya.Application/Models/Patients/CarePlanDtos.cs create mode 100644 server/src/Core/Baya.Application/Models/Refunds/CancellationPolicyPreviewDto.cs create mode 100644 server/src/Core/Baya.Application/Models/Reviews/ReviewGateDtos.cs create mode 100644 server/src/Core/Baya.Domain/Entities/Identity/GeocodeSources.cs create mode 100644 server/src/Core/Baya.Domain/Entities/Identity/PatientCarePlan.cs create mode 100644 server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/PatientCarePlanConfig.cs create mode 100644 server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260712223125_RefinementPhase3TierA.Designer.cs create mode 100644 server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260712223125_RefinementPhase3TierA.cs create mode 100644 server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713065646_RefinementPhase3TierB.Designer.cs create mode 100644 server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713065646_RefinementPhase3TierB.cs create mode 100644 server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713072137_RefinementPhase3TierC.Designer.cs create mode 100644 server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713072137_RefinementPhase3TierC.cs create mode 100644 server/src/Tests/Baya.Test.Foundation/Booking/GetCheckoutSummaryHandlerTests.cs create mode 100644 server/src/Tests/Baya.Test.Foundation/Reviews/GetReviewEligibilityHandlerTests.cs diff --git a/dev/contracts/conventions/api-conventions.md b/dev/contracts/conventions/api-conventions.md index 5e23d0d..eb7760c 100644 --- a/dev/contracts/conventions/api-conventions.md +++ b/dev/contracts/conventions/api-conventions.md @@ -35,8 +35,8 @@ server — mirror that, don't invent a new envelope. the client picks by locale. ## Pagination (mandatory on lists) -- Query params: `page` (1-based) + `page_size` (cap it server-side, e.g. ≤100). Response payload carries - `items` + `total` (+ `page`/`page_size`). Document the default and max `page_size` per endpoint. +- Query params: `page` (1-based) + `pageSize` (cap it server-side, e.g. ≤100). Response payload carries + `items` + `total` (+ `page`/`pageSize`). Document the default and max `pageSize` per endpoint. ## Idempotency (money & side-effecting POSTs) - Where stated, the client sends an idempotency key (header or body field) and the server dedups. Webhook diff --git a/dev/contracts/domains/_TEMPLATE.md b/dev/contracts/domains/_TEMPLATE.md index 6464e6b..0b97336 100644 --- a/dev/contracts/domains/_TEMPLATE.md +++ b/dev/contracts/domains/_TEMPLATE.md @@ -15,7 +15,7 @@ ### ` api/v1//` - **Purpose:** … - **Auth:** none | authenticated | policy/role … · **Rate-limited:** yes/no · **Idempotency key:** yes/no -- **Path/query params:** `name` (type) — meaning; pagination `page`/`page_size` (default/max) for lists. +- **Path/query params:** `name` (type) — meaning; pagination `page`/`pageSize` (default/max) for lists. - **Request body:** ```json { "field": "example" } diff --git a/dev/contracts/domains/bnpl.md b/dev/contracts/domains/bnpl.md index e17b2c2..34bb6c9 100644 --- a/dev/contracts/domains/bnpl.md +++ b/dev/contracts/domains/bnpl.md @@ -148,3 +148,17 @@ customer's repayment schedule — `installment_count` is informational (default ## Changelog - b12 — initial contract (eligibility, initiate, customer/admin status, webhook, admin verify/settle/revert). + +--- + +## Refinement phase 3 additions (REQ-022/023/024) + +- `balinyaar` added to the `provider_code` enum (in-house plan; identical net-of-fee mechanics, resolves to the + same adapter). The set is now `snapppay|digipay|tara|torobpay|balinyaar`. +- `POST checkout_bnpl/eligibility` accepts optional `{ nationalId, mobile, consent }` (consent required when the + KYC inputs are present; a supplied mobile drives the provider inquiry, else the account mobile). +- `GET api/v1/checkout_bnpl/by_request/{bookingRequestId}` (owner-scoped) → `BnplOrderStatusDto`; `bookingId` on + the settled order was already present on the DTO. +- **DEFERRED:** `checkout_bnpl/options/{id}` + `schedule` + `wallet_installments` — b12 deliberately does not model + the customer repayment schedule / per-installment status, and there is no installment ledger to serve them from. + Keep the D1/D2/D4/D5 plan visualization mocked until a provider-schedule integration (or a schedule table) lands. diff --git a/dev/contracts/domains/booking-requests.md b/dev/contracts/domains/booking-requests.md index c63f0ce..681c9d4 100644 --- a/dev/contracts/domains/booking-requests.md +++ b/dev/contracts/domains/booking-requests.md @@ -146,3 +146,21 @@ ## Changelog - b8 — initial contract (create/accept/reject/cancel + role-scoped list + single get + admin expire). + +--- + +## Refinement phase 3 additions (REQ-013/014/016/017) + +- **`BookingRequestDto`** gains `variantPrice` (IRR digit-string — the chosen variant's *display rate*, not + an engagement total; the request stays money-free), `nurseAvatarUrl` (nullable), and `bookingId` + (nullable — the booking created once the request is `converted`, for the confirmation deep-link). +- **`BookingRequestListItemDto`** gains `variantLabel` (self-describing inbox row) and `patientAge` + (nullable coarse triage age). +- **`GET api/v1/booking_requests/checkout_summary/{id}`** (owner-scoped) — the C6 money breakdown: + `{ bookingRequestId, requestStatus, nurseName, patientName, variantLabel, variantPriceUnit, sessionCount, + requestedDate, requestedTimeStart, requestedTimeEnd, paymentDeadlineAt, serviceCostIrr, commissionIrr, + vatIrr, vatRate, totalIrr, grossPriceIrr, balinyaarCommissionIrr, nursePayoutAmount }`. All IRR + digit-strings, computed server-side. **Canonical rates:** `platform_fee_rate = 0.15`, `vat_rate = 0.10`. + VAT is **carved out of the commission** so `serviceCostIrr + commissionIrr + vatIrr = totalIrr = gross` + (the captured amount); `commissionIrr` is the commission **net of VAT**, and the raw b10 amounts + (`grossPriceIrr = balinyaarCommissionIrr + nursePayoutAmount`) are surfaced alongside. diff --git a/dev/contracts/domains/bookings-evv.md b/dev/contracts/domains/bookings-evv.md index 21a1932..647ff3b 100644 --- a/dev/contracts/domains/bookings-evv.md +++ b/dev/contracts/domains/bookings-evv.md @@ -38,7 +38,7 @@ ISO-8601. Enums cross as their stable string codes. (customer own / nurse assigned / admin all). The **nurse** view omits `addressSnapshotJson`. Never includes care-instruction clinical fields. **Failure:** `401`, `404` (not found / not a party → no leak). -### `GET api/v1/bookings/list?role=customer|nurse|all&status=&page=&page_size=` +### `GET api/v1/bookings/list?role=customer|nurse|all&status=&page=&pageSize=` - **Purpose:** role-scoped "My bookings" (paginated, projected). `role=all` is admin-only (`403` otherwise). **Success:** `PagedResult`. @@ -76,7 +76,7 @@ ISO-8601. Enums cross as their stable string codes. `payout_eligible_at`, and — when all sessions are settled — completes the booking + sets `dispute_window_ends_at`. **Failure:** `400` no open check-in, `409` not checkout-able. -### `GET api/v1/booking_sessions/today?date=&page=&page_size=` +### `GET api/v1/booking_sessions/today?date=&page=&pageSize=` - **Purpose:** the nurse's sessions for a day (default all), with check-in/out CTA state. **Auth:** nurse, tenancy-scoped. **Success:** `PagedResult`. @@ -87,7 +87,7 @@ ISO-8601. Enums cross as their stable string codes. - **Purpose:** cancel a single un-started session · **Rate-limited:** yes. Snapshots the policy + computes the session's refundable share. **Failure:** `409` if the session already started. -### `GET api/v1/admin_evv/list?type=mismatch|no_show&page=&page_size=` +### `GET api/v1/admin_evv/list?type=mismatch|no_show&page=&pageSize=` - **Purpose:** admin EVV-review queue. **Auth:** admin policy · **Rate-limited:** yes. **Success:** `PagedResult`. diff --git a/dev/contracts/domains/catalog.md b/dev/contracts/domains/catalog.md index 9d8547c..74123d7 100644 --- a/dev/contracts/domains/catalog.md +++ b/dev/contracts/domains/catalog.md @@ -38,8 +38,8 @@ ## Public catalog browse — `CatalogController` (no auth) -### `GET api/v1/catalog/categories?page=&page_size=` -- Active categories ordered by `sortOrder`, **paginated** (default `page_size` 50, max 100). Cached. `data`: +### `GET api/v1/catalog/categories?page=&pageSize=` +- Active categories ordered by `sortOrder`, **paginated** (default `pageSize` 50, max 100). Cached. `data`: `PagedResult`. ### `GET api/v1/catalog/option_groups?category_id={id}` @@ -89,7 +89,7 @@ Every write **invalidates the catalog cache**. Both labels required (`nameFa`/`n ### `POST api/v1/nurse_variants/set_active/{id}` - **Body:** `{ isActive }`. Deactivate/reactivate — **never hard-delete**. `data`: `true`. `404` if not owned. -### `GET api/v1/nurse_variants/list?page=&page_size=` +### `GET api/v1/nurse_variants/list?page=&pageSize=` - The nurse's own offerings — **active and inactive**, active-first, paginated. `data`: `PagedResult`. diff --git a/dev/contracts/domains/config-reference.md b/dev/contracts/domains/config-reference.md index 69e2b27..e7a88ce 100644 --- a/dev/contracts/domains/config-reference.md +++ b/dev/contracts/domains/config-reference.md @@ -8,7 +8,7 @@ **Status:** live as of backend-phase-1 · **Frontend consumer:** frontend-phase-f14 (notification center) / frontend-phase-f15 (admin config/holidays/audit/alerts) All responses are the standard `OperationResult`→`ApiResult` envelope (camelCase body, snake_case URLs). -Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1-based) + `page_size` +Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1-based) + `pageSize` (default 50, max 100) — bound from the query string; derive exact casing from `swagger.v1.json`. ## Enums used @@ -25,7 +25,7 @@ Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1- ### `GET api/v1/platform_config/get_platform_configs` - **Purpose:** list config rows. **Auth:** admin (DynamicPermission). **Rate-limited:** no. -- **Query:** `page`, `page_size`. +- **Query:** `page`, `pageSize`. - **200 `data`:** `PagedResult` — `{ items:[{ key, value, dataType, description }], total, page, pageSize }`. ### `POST api/v1/platform_config/update_platform_config` @@ -36,7 +36,7 @@ Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1- ### `GET api/v1/platform_config/get_config_change_history` - **Purpose:** the audited change history for one key (from the append-only trail). **Auth:** admin. -- **Query:** `key` (required), `page`, `page_size`. +- **Query:** `key` (required), `page`, `pageSize`. - **200 `data`:** `PagedResult` — `{ items:[{ id, action, changedFieldsJson, actorUserId, occurredAt }], … }`, newest first. --- @@ -44,7 +44,7 @@ Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1- ## Admin — Holidays (`holidays` controller, `[Authorize(DynamicPermission)]`) ### `GET api/v1/holidays/get_holidays` -- **Query:** `from` (date, optional), `to` (date, optional), `page`, `page_size`. +- **Query:** `from` (date, optional), `to` (date, optional), `page`, `pageSize`. - **200 `data`:** `PagedResult` — `{ items:[{ id, holidayDate, nameFa, type, isBankClosed }], … }`, by date. ### `POST api/v1/holidays/upsert_holiday` @@ -60,7 +60,7 @@ Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1- ### `GET api/v1/audit/get_audit_trail` - **Purpose:** the immutable trail for one entity. **Auth:** admin. -- **Query:** `entity_type` (e.g. `PlatformConfig`), `entity_id` (string), `page`, `page_size`. +- **Query:** `entity_type` (e.g. `PlatformConfig`), `entity_id` (string), `page`, `pageSize`. - **200 `data`:** `PagedResult` — `{ items:[{ id, entityType, entityId, action, changedFieldsJson, actorUserId, occurredAt }], … }`, newest first. **Notes:** read-only; there is no write/update/delete endpoint for audit rows. --- @@ -68,7 +68,7 @@ Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1- ## Admin — Support alerts (`support_alerts` controller, `[Authorize(DynamicPermission)]`, never user-facing) ### `GET api/v1/support_alerts/get_support_alerts` -- **Query:** `type?`, `status?`, `owner_user_id?`, `page`, `page_size`. +- **Query:** `type?`, `status?`, `owner_user_id?`, `page`, `pageSize`. - **200 `data`:** `PagedResult` — `{ items:[{ id, type, severity, status, entityType, entityId, bookingId, reviewId, ownerUserId, resolutionNote, resolvedAt, createdAt }], … }`. ### `POST api/v1/support_alerts/assign_support_alert` @@ -84,7 +84,7 @@ Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1- Every endpoint is scoped to the signed-in caller (`ICurrentUser`) — never a body-supplied user id. ### `GET api/v1/notifications/get_notifications` -- **Query:** `page`, `page_size`. +- **Query:** `page`, `pageSize`. - **200 `data`:** `PagedResult` — `{ items:[{ id, type, title, body, dataJson, isRead, readAt, createdAt }], … }`, **unread-first** then newest-first. ### `GET api/v1/notifications/get_unread_count` diff --git a/dev/contracts/domains/geography-addresses.md b/dev/contracts/domains/geography-addresses.md index 6409ff3..a94243b 100644 --- a/dev/contracts/domains/geography-addresses.md +++ b/dev/contracts/domains/geography-addresses.md @@ -72,7 +72,7 @@ Every write **invalidates the geo cache**. Names required (`nameFa`/`nameEn`); ` ### `DELETE api/v1/nurse_service_areas/remove/{id}` - Soft-removes the nurse's own area. `data`: `true`. `404` if not owned/absent (existence not leaked). -### `GET api/v1/nurse_service_areas/list?page=&page_size=` +### `GET api/v1/nurse_service_areas/list?page=&pageSize=` - The nurse's own areas, whole-city first, paginated. `data`: `PagedResult`. ## Customer addresses — `CustomerAddressesController` (authenticated; customer-scoped in handler) @@ -97,7 +97,7 @@ Every write **invalidates the geo cache**. Names required (`nameFa`/`nameEn`); ` ### `DELETE api/v1/customer_addresses/delete/{id}` - Soft-deletes the owned address. `data`: `true`. `404` if not owned. -### `GET api/v1/customer_addresses/list?page=&page_size=` +### `GET api/v1/customer_addresses/list?page=&pageSize=` - The customer's own addresses, **primary first**, paginated, with PII **decrypted for the owner**. `data`: `PagedResult`. @@ -123,3 +123,13 @@ Tehran city id `101`, Tehran districts `1001…1022`; other cities have no distr ## Changelog - b4 — initial contract: public geo lookups, admin geo CRUD + set_active, nurse service areas, customer addresses; `IGeocoder` seam; `409` conflict added to the envelope. + +--- + +## Refinement phase 3 additions (REQ-008/009) + +- **`CustomerAddressDto`** gains `provinceId` (joined from `cities.province_id`) so the edit form can + prefill the province → city cascade from a server-loaded address. +- **`customer_addresses/create` + `update/{id}`** now accept optional `latitude`/`longitude` (both or + neither). When present, the user's dropped pin is stored (`geocode_source = user_pin`, preferred for the + EVV distance check); when absent the server geocodes as before (`geocode_source = geocoder`). diff --git a/dev/contracts/domains/identity-auth.md b/dev/contracts/domains/identity-auth.md index 93956b7..e591aea 100644 --- a/dev/contracts/domains/identity-auth.md +++ b/dev/contracts/domains/identity-auth.md @@ -128,3 +128,14 @@ ## Changelog - b2 — initial contract (phone-OTP auth, sessions, `/me`, role selection). + +--- + +## Refinement phase 3 additions (REQ-002/003) + +- **`RequestOtpResult`** gains `codeLength` (6) and `expiresInSeconds` (60) so the OTP box count + expiry + hint are contract-driven. +- **`verify_otp` failures** now carry a stable machine `code` on the envelope: `otp_invalid` (wrong **or** + expired — collapsed for anti-enumeration) and `otp_locked` with `data: { retryAfterSeconds }` on lockout. + The coded-error envelope is `{ isSuccess: false, statusCode: 400, message, code, data? }` (the optional + `code` is omitted from every other response). diff --git a/dev/contracts/domains/identity-profiles.md b/dev/contracts/domains/identity-profiles.md index cf0c4d3..ca53ce5 100644 --- a/dev/contracts/domains/identity-profiles.md +++ b/dev/contracts/domains/identity-profiles.md @@ -85,3 +85,16 @@ segments are snake_case; responses use the standard `OperationResult`→`ApiResu ## Changelog - b3 — initial contract (nurse/customer profiles, patients, nurse bank accounts + ownership inquiry). + +--- + +## Refinement phase 3 additions (REQ-005/006/007) + +- **`PatientDto` + create/update** gain `relation` (`parent|spouse|child|self`, nullable) and `conditions` + (`string[]` of stable codes; empty, never null). Stored as a nullable code + a JSON array column. +- **`NurseProfileDto`** and **`CustomerProfileDto`** gain `avatarUrl` (nullable). `CustomerProfileDto` also + gains `preferredLanguage` (nullable); the customer `upsert` body now accepts `firstName`/`lastName` + (persisted on the base `users` row) and `preferredLanguage`. +- **Avatar upload (multipart):** `POST api/v1/nurse_profiles/avatar` and + `POST api/v1/customer_profiles/avatar` — `multipart/form-data` field `file` (JPEG/PNG/WebP, ≤ 5 MB), + stored via `IObjectStorage`, returns `{ url }` and persists it on the profile. diff --git a/dev/contracts/domains/messaging-notifications-admin.md b/dev/contracts/domains/messaging-notifications-admin.md index 64431d6..6d0d36a 100644 --- a/dev/contracts/domains/messaging-notifications-admin.md +++ b/dev/contracts/domains/messaging-notifications-admin.md @@ -173,3 +173,23 @@ rebuild). All are `[Authorize(DynamicPermission)]` (admin role passes; other sta | Verification queue / refunds / payouts / moderation / config / holidays | their own phase routes | b6/b11/b13/b14/b1 | `support_alerts` are internal-only and must never appear in a user-facing response or join. + +--- + +## Refinement phase 3 additions (REQ-029/030/031/032/033/034/035/036/037) + +**Delivered:** +- **REQ-029** `PlatformConfigDto` gains `updatedAt` + `updatedBy` (from the entity audit fields). +- **REQ-030** `GET audit/get_audit_trail` filters also by `actorId`, `action`, `from`, `to` (all optional; `entityType`/ + `entityId` now optional too). **Query params bind camelCase** (`actorId`/`from`/`to`), not `actor_id`. +- **REQ-037** `tagCodes: string[]` on `ModerationQueueItemDto`. **REQ-033** `totalIrr` on `InvoiceDto` + (= platform commission + BNPL commission + VAT). +- **REQ-032** activate/suspend toggle `POST admin/partner-centers/{id}/set-active { isActive }`. **Route casing + pinned:** the admin partner-center routes are **kebab-case** (`admin/partner-centers`, `.../set-active`) — an + intentional b15 divergence from the `snake_case` convention; the frontend's kebab-case client is CORRECT. + +**Deferred (admin-console polish, documented in the tracker):** REQ-031 (RBAC `admin_roles/list|grant|revoke`), +REQ-032 `centers/me` + split portal reads (need the user↔center admin association REQ-038 deferred), REQ-033 +center-scoped invoice list, REQ-034 verification nurse-queue/signed-url/whole-approve, REQ-035 refund admin +preview+approve/reject (the customer preview REQ-020 IS delivered), REQ-036 payout admin preview/holidayShifted/ +transfer-reference. diff --git a/dev/contracts/domains/messaging.md b/dev/contracts/domains/messaging.md new file mode 100644 index 0000000..7c7709e --- /dev/null +++ b/dev/contracts/domains/messaging.md @@ -0,0 +1,13 @@ + +--- + +## Refinement phase 3 additions (REQ-028) + +- **`TicketSummaryDto`** gains `lastMessageAt` (last non-internal activity) + `unreadCount` (the caller's unread + non-internal messages from others; 0 on the admin queue). Unread is computed against the participant's + `last_read_at`, **stamped when the participant fetches the user-facing thread**. +- **`GET /tickets`** gains a `bookingId` query filter (jump to a booking's coordination ticket). +- **`POST /tickets/{id}/messages`** accepts an optional `clientMessageId` — a retried send with the same key is + deduplicated (returns the original) and the key is echoed on `PostMessageResult`. +- **Message author = role label, not a name** (confirmed intentional, privacy): the DTO carries `senderId`; the + client derives the author label from the participant role. No raw identity/name is exposed. diff --git a/dev/contracts/domains/payments.md b/dev/contracts/domains/payments.md index e9fa507..5452507 100644 --- a/dev/contracts/domains/payments.md +++ b/dev/contracts/domains/payments.md @@ -69,3 +69,11 @@ DEBIT escrow_held gross_price_irr (e.g. 23300000) - **The checkout shows gross + commission/VAT breakdown only** — never the internal `account_type`s. - **Payment is idempotent end-to-end**: a retried `initiate` (same `Idempotency-Key`) reuses the attempt; a replayed webhook is a no-op; a repeat `initiate` after capture is a `409`. + +--- + +## Refinement phase 3 additions (REQ-018) + +- **Invoice auto-issue on capture/settle:** the commission invoice is now issued automatically when a card + capture (`ConfirmPaymentAndPostLedger`) or a BNPL settle first creates the booking — idempotent per + booking — so a paying customer's `GET api/v1/invoices/{bookingId}` resolves right away (was admin-only). diff --git a/dev/contracts/domains/payouts.md b/dev/contracts/domains/payouts.md index 52e38f1..4433874 100644 --- a/dev/contracts/domains/payouts.md +++ b/dev/contracts/domains/payouts.md @@ -97,3 +97,16 @@ The response envelope is the standard `{ data, … }`; the shapes below are the ## Changelog - b13 — initial contract. + +--- + +## Refinement phase 3 additions (REQ-025 — nurse earnings) + +- **`GET api/v1/nurse_payouts/earnings_balance`** → `{ pendingTotalIrr, eligibleTotalIrr, paidTotalIrr, + clawbackOutstandingIrr, netPayableBalanceIrr }`. `netPayableBalanceIrr` is the **ledger-derived, SIGNED** + nurse_payable balance (may be negative = "owed back"; never clamped); `paidTotalIrr` is lifetime, not in the net. +- **`GET api/v1/nurse_payouts/earnings?state=&page=&pageSize=`** → `PagedResult`; `state` + (`pending|eligible|paid|clawback_applied`) is **derived server-side** from `bookings.status` + + `dispute_window_ends_at < now` + the payout link + any clawback. Filterable by `state`. +- **`GET api/v1/nurse_payouts/{id}`** → nurse-scoped payout detail (batch window + covered bookings). +- **`NursePayoutHistoryDto`** gains `failureReason`. diff --git a/dev/contracts/domains/refunds-invoices.md b/dev/contracts/domains/refunds-invoices.md index 831a995..bf68b74 100644 --- a/dev/contracts/domains/refunds-invoices.md +++ b/dev/contracts/domains/refunds-invoices.md @@ -125,3 +125,24 @@ ISO-8601; `expected_customer_refund_eta` is a **date** (`"2026-08-24"`). ## Changelog - b11 — initial contract (create refund, list refunds, write-off clawback, issue invoice, refund status, get invoice). + +--- + +## Refinement phase 3 additions (REQ-019/020/021 — customer refunds) + +- **`POST api/v1/bookings/{id}/cancel`** (customer) — cancels the booking (freezing the policy snapshot) AND + opens its refund in one call → `RefundStatusDto`. Body `{ reasonCategory, reasonNotes?, sessionIds? }` + (MVP cancels all un-started sessions; `sessionIds` is accepted for forward-compat). +- **`GET api/v1/bookings/{id}/cancellation_policy`** (customer) — pre-cancel disclosure: resolves the + applicable policy by **current** lead time + per-session refundability → + `{ bookingId, cancellable, cancellationPolicyCode, refundPercentageApplied, feePercentage, refundAmountIrr, + feeAmountIrr, refundableAmountIrr, platformFeeRefundedIrr, nursePayoutRefundedIrr, appliesTo, leadTimeLabel, + refundChannel, expectedCustomerRefundEta (null in preview), sessions: [{ bookingSessionId, sessionIndex, + scheduledDate, refundable, reasonCode }] }`. `refundAmountIrr + feeAmountIrr = refundableAmountIrr`. +- **`GET api/v1/refunds/by_booking/{bookingId}`** (customer) — the booking's latest refund status (404 if none). +- **`RefundStatusDto`** gains `platformFeeRefundedIrr`, `nursePayoutRefundedIrr`, `refundPercentageApplied`, + `cancellationPolicyCode`, `createdAt`, `completedAt` (the fee-leg transparency split). +- **Canonical `cancellation_policy_code` set** (seeded, stable — the frontend's `free_24h`/`partial_under_24h`/ + `customer_no_show` were invented): **`standard_24h`** (customer ≥24h → full refund), **`standard_inside_24h`** + (customer <24h → partial), **`nurse_no_show`** (nurse-initiated → full refund + penalty), **`admin_cancellation`** + (admin → full refund). Per-session `reasonCode`: **`un_started`** when refundable, else the blocking session status. diff --git a/dev/contracts/domains/reviews-records.md b/dev/contracts/domains/reviews-records.md index 7514b3a..ab111a5 100644 --- a/dev/contracts/domains/reviews-records.md +++ b/dev/contracts/domains/reviews-records.md @@ -119,3 +119,19 @@ access rule is enforced in the handler, not just the route policy. - **Failure cases:** `401`; `403` no clinical access; `404` patient not found. - **Notes:** The record is **patient-scoped, not booking-scoped** — a new nurse taking over reads the whole history (not just their own booking's notes). + +--- + +## Refinement phase 3 additions (REQ-026/027) + +- **`GET api/v1/bookings/{bookingId}/review_eligibility`** → `{ canReview, reason?: + not_completed|already_reviewed|not_owner|not_found }`. +- **`GET api/v1/bookings/{bookingId}/my_review`** → `{ moderationStatus: + pending_moderation|published|hidden|rejected|none, rating?, body?, tagCodes[], createdAt? }`. Masked-author + omission on the public list is **intentional** (privacy). +- **Family-owned care plan (new entity `usr.PatientCarePlans`):** `GET/PUT api/v1/patients/{patientId}/care_record` + → `{ patientId, medications:[{id,name,dosage?,frequency,timingNote?}], routine:[{id,label,timeOfDay?,note?}], + tasks:[{id,label,done}] }`. Read = owner/nurse-with-booking/admin; write = owning customer only. +- **`GET api/v1/patients/{patientId}/record_access`** → `{ canView, canEdit, canAppendNote, deniedReason? }` + (always 200; non-leaking `not_found`/`not_authorized`). +- **Structured `taskResults`** (`[{ label, done }]`) added to the visit-note write body + the history DTO. diff --git a/dev/contracts/domains/search.md b/dev/contracts/domains/search.md index 87e980b..624b005 100644 --- a/dev/contracts/domains/search.md +++ b/dev/contracts/domains/search.md @@ -49,7 +49,7 @@ - `nurse_gender` (`male`|`female`, optional) — the same-gender facet. - `min_price` / `max_price` (long IRR, optional) — inclusive range over the copied `price`. - `price_unit` (enum, optional) — compare like-for-like listings (e.g. only `per_day`). - - `page` (int, default 1), `page_size` (int, default 50, max 100). + - `page` (int, default 1), `pageSize` (int, default 50, max 100). - **Success `200` payload (`data` = `PagedResultOfNurseSearchResultDto`):** ```json { @@ -74,7 +74,7 @@ } ``` - **Failure cases:** `400` — `service_category_id`/`city_id` missing or ≤ 0, `nurse_gender` not `male`/`female`, - `min_price > max_price`, invalid `price_unit`, or `page_size > 100`. + `min_price > max_price`, invalid `price_unit`, or `pageSize > 100`. - **Notes:** returns only `is_searchable = 1` rows. `districtId = null` in a result row means the nurse covers the whole city. No entity is hydrated — the read is a projected, paginated, `AsNoTracking` index scan. @@ -103,3 +103,15 @@ ## Changelog - b7 — initial contract: public `search/nurses`, admin `admin_search/rebuild_index`. + +--- + +## Refinement phase 3 additions (REQ-012) + +- **`NurseSearchResultDto`** gains `nurseName` + `avatarUrl` (denormalized onto `nurse_search_index`, so no + per-row join) and `distanceKm` (nullable — the covering index carries no coordinate, so it is null today). +- **`GET api/v1/nurses/{id}/profile`** (public) — the aggregated discovery detail: + `{ nurseId, nurseName, avatarUrl, bio, yearsExperience, averageRating, totalReviews, + totalCompletedBookings, isVerified, inoMembership, attributeChips[], services: [{ variantId, displayName, + priceIrr, priceUnit, sessionCount? }], latestReview?: { rating, body, authorMasked (null by design), + createdAt } }`. No encrypted credential number is ever exposed. diff --git a/dev/contracts/domains/verification.md b/dev/contracts/domains/verification.md index 03496cd..05434d8 100644 --- a/dev/contracts/domains/verification.md +++ b/dev/contracts/domains/verification.md @@ -124,9 +124,9 @@ ## Admin review queue — `AdminVerificationsController` (`[Authorize(DynamicPermission)]`, rate-limited `sensitive`) -### `GET api/v1/admin_verifications?status=&page=&page_size=` +### `GET api/v1/admin_verifications?status=&page=&pageSize=` - **Purpose:** the review queue — one row per step awaiting attention. -- **Params:** `status` (default `in_review`) + pagination `page`/`page_size`. +- **Params:** `status` (default `in_review`) + pagination `page`/`pageSize`. - **`data`:** `PagedResult`. Documents carry **signed GET URLs**. ### `GET api/v1/admin_verifications/{nurseVerificationId}` @@ -255,3 +255,15 @@ GET /api/v1/nurses/{nurseId}/trust_badge -> { isVerified: true, approvedAt, cr `verification_status` / `verification_step_status` / `credential_type` / `verification_method` enums; transactional `is_verified` flip; encrypted-never-serialized `credential_number`; three new mocked vendor seams (`IShahkarVerifier`, `IIdentityKycProvider`, `ICredentialVerifier`). Scheduled expiry cron deferred. + +--- + +## Refinement phase 3 additions (REQ-011) + +- **`VerificationStepDto`** gains `isRequired` (mirrors the step-type catalog; an optional step never blocks + bookability). +- **`POST api/v1/nurse_verification/credential_details`** (nurse) — captures the structured credential + fields collected with the uploads: `{ inoNumber (required), specialties: string[], licenseNumber?, + issuingAuthority?, holderName?, issuedAt?, expiresAt? }` → `VerificationStatusDto`. Upserts an + `ino_membership` (and, if a license number is sent, `moh_competency_license`) `nurse_credentials` row + (unverified — admin still decides) and persists `specialties` on the profile. The INO number is encrypted. diff --git a/dev/contracts/openapi/swagger.v1.json b/dev/contracts/openapi/swagger.v1.json index 8ab880f..386e0d1 100644 --- a/dev/contracts/openapi/swagger.v1.json +++ b/dev/contracts/openapi/swagger.v1.json @@ -2722,6 +2722,95 @@ ] } }, + "/api/v1/admin/partner-centers/{id}/set-active": { + "post": { + "tags": [ + "AdminPartnerCenters" + ], + "operationId": "AdminPartnerCenters_SetActive", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "x-position": 1 + } + ], + "requestBody": { + "x-name": "command", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetPartnerCenterActiveCommand" + } + } + }, + "required": true, + "x-position": 2 + }, + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfPartnerCenterDetailDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, "/api/v1/admin_payouts/eligible": { "get": { "tags": [ @@ -4565,6 +4654,45 @@ }, "x-position": 2 }, + { + "name": "ActorId", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "x-position": 3 + }, + { + "name": "Action", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 4 + }, + { + "name": "From", + "in": "query", + "schema": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "x-position": 5 + }, + { + "name": "To", + "in": "query", + "schema": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "x-position": 6 + }, { "name": "Page", "in": "query", @@ -4572,7 +4700,7 @@ "type": "integer", "format": "int32" }, - "x-position": 3 + "x-position": 7 }, { "name": "PageSize", @@ -4581,7 +4709,7 @@ "type": "integer", "format": "int32" }, - "x-position": 4 + "x-position": 8 } ], "responses": { @@ -5445,6 +5573,83 @@ ] } }, + "/api/v1/booking_requests/checkout_summary/{id}": { + "get": { + "tags": [ + "BookingRequests" + ], + "operationId": "BookingRequests_CheckoutSummary", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "x-position": 1 + } + ], + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfCheckoutSummaryDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, "/api/v1/bookings/{bookingId}/review": { "post": { "tags": [ @@ -5534,6 +5739,160 @@ ] } }, + "/api/v1/bookings/{bookingId}/review_eligibility": { + "get": { + "tags": [ + "BookingReviews" + ], + "operationId": "BookingReviews_ReviewEligibility", + "parameters": [ + { + "name": "bookingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "x-position": 1 + } + ], + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfReviewEligibilityDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, + "/api/v1/bookings/{bookingId}/my_review": { + "get": { + "tags": [ + "BookingReviews" + ], + "operationId": "BookingReviews_MyReview", + "parameters": [ + { + "name": "bookingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "x-position": 1 + } + ], + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfMyReviewDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, "/api/v1/bookings/convert": { "post": { "tags": [ @@ -5971,6 +6330,172 @@ ] } }, + "/api/v1/bookings/{id}/cancel": { + "post": { + "tags": [ + "Bookings" + ], + "operationId": "Bookings_CancelAndRefund", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "x-position": 1 + } + ], + "requestBody": { + "x-name": "command", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelBookingAndRefundCommand" + } + } + }, + "required": true, + "x-position": 2 + }, + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfRefundStatusDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, + "/api/v1/bookings/{id}/cancellation_policy": { + "get": { + "tags": [ + "Bookings" + ], + "operationId": "Bookings_CancellationPolicy", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "x-position": 1 + } + ], + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfCancellationPolicyPreviewDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, "/api/v1/bookings/submit_care_instructions/{id}": { "post": { "tags": [ @@ -7038,6 +7563,83 @@ ] } }, + "/api/v1/checkout_bnpl/by_request/{bookingRequestId}": { + "get": { + "tags": [ + "CheckoutBnpl" + ], + "operationId": "CheckoutBnpl_ByRequest", + "parameters": [ + { + "name": "bookingRequestId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "x-position": 1 + } + ], + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfBnplOrderStatusDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, "/api/v1/customer_addresses/create": { "post": { "tags": [ @@ -7591,6 +8193,159 @@ ] } }, + "/api/v1/customer_profiles/avatar": { + "post": { + "tags": [ + "CustomerProfiles" + ], + "operationId": "CustomerProfiles_Avatar", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary", + "nullable": true + } + } + } + } + } + }, + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfAvatarUploadResult" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, + "/api/v1/dev/last_otp/{phone}": { + "get": { + "tags": [ + "Dev" + ], + "summary": "Development-only: returns the most recent OTP for phone so a browser or an\nautomated end-to-end flow can complete phone-OTP login without an SMS gateway (the code is otherwise\nonly written to the server log by LoggingSmsSender). Returns 404 in every non-Development\nenvironment \u2014 the capture is not even wired there \u2014 so it can never leak a code in staging/production.\nIt does not touch the OTP rate-limit or the per-phone resend window. Superseded by the real SMS gateway\nin refinement Phase 8.", + "operationId": "Dev_LastOtp", + "parameters": [ + { + "name": "phone", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "x-position": 1 + } + ], + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDevLastOtpResult" + } + } + } + } + } + } + }, "/api/v1/geo/provinces": { "get": { "tags": [ @@ -8344,7 +9099,7 @@ "tags": [ "Me" ], - "description": "Role claims live inside the access token — after selecting a role the client should\n refresh its tokens to pick the new role up.", + "description": "Role claims live inside the access token \u2014 after selecting a role the client should\n refresh its tokens to pick the new role up.", "operationId": "Me_SelectRole", "requestBody": { "x-name": "command", @@ -9168,6 +9923,242 @@ ] } }, + "/api/v1/nurse_payouts/earnings_balance": { + "get": { + "tags": [ + "NursePayouts" + ], + "operationId": "NursePayouts_EarningsBalance", + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfNurseEarningsBalanceDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, + "/api/v1/nurse_payouts/earnings": { + "get": { + "tags": [ + "NursePayouts" + ], + "operationId": "NursePayouts_Earnings", + "parameters": [ + { + "name": "State", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + }, + { + "name": "Page", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 2 + }, + { + "name": "PageSize", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 3 + } + ], + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfPagedResultOfNurseEarningsItemDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, + "/api/v1/nurse_payouts/{id}": { + "get": { + "tags": [ + "NursePayouts" + ], + "operationId": "NursePayouts_Detail", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "x-position": 1 + } + ], + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfNursePayoutDetailDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, "/api/v1/nurse_profiles/upsert": { "post": { "tags": [ @@ -9387,6 +10378,87 @@ ] } }, + "/api/v1/nurse_profiles/avatar": { + "post": { + "tags": [ + "NurseProfiles" + ], + "operationId": "NurseProfiles_Avatar", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary", + "nullable": true + } + } + } + } + } + }, + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfAvatarUploadResult" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, "/api/v1/nurses/{nurseId}/trust_badge": { "get": { "tags": [ @@ -9459,6 +10531,78 @@ } } }, + "/api/v1/nurses/{nurseId}/profile": { + "get": { + "tags": [ + "Nurses" + ], + "operationId": "Nurses_Profile", + "parameters": [ + { + "name": "nurseId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "x-position": 1 + } + ], + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfNursePublicProfileDto" + } + } + } + } + } + } + }, "/api/v1/nurses/{nurseProfileId}/reviews": { "get": { "tags": [ @@ -10591,6 +11735,83 @@ ] } }, + "/api/v1/nurse_verification/credential_details": { + "post": { + "tags": [ + "NurseVerification" + ], + "operationId": "NurseVerification_CredentialDetails", + "requestBody": { + "x-name": "command", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmitCredentialDetailsCommand" + } + } + }, + "required": true, + "x-position": 1 + }, + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfVerificationStatusDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, "/api/v1/nurse_verification/steps/identity_kyc/run": { "post": { "tags": [ @@ -10982,6 +12203,247 @@ ] } }, + "/api/v1/patients/{patientId}/care_record": { + "get": { + "tags": [ + "PatientCareRecords" + ], + "operationId": "PatientCareRecords_CarePlan", + "parameters": [ + { + "name": "patientId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "x-position": 1 + } + ], + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfCarePlanDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + }, + "put": { + "tags": [ + "PatientCareRecords" + ], + "operationId": "PatientCareRecords_UpsertCarePlan", + "parameters": [ + { + "name": "patientId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "x-position": 1 + } + ], + "requestBody": { + "x-name": "body", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpsertCarePlanBody" + } + } + }, + "required": true, + "x-position": 2 + }, + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfCarePlanDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, + "/api/v1/patients/{patientId}/record_access": { + "get": { + "tags": [ + "PatientCareRecords" + ], + "operationId": "PatientCareRecords_RecordAccess", + "parameters": [ + { + "name": "patientId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "x-position": 1 + } + ], + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfRecordAccessDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, "/api/v1/patients/create": { "post": { "tags": [ @@ -11926,6 +13388,83 @@ ] } }, + "/api/v1/refunds/by_booking/{bookingId}": { + "get": { + "tags": [ + "Refunds" + ], + "operationId": "Refunds_ByBooking", + "parameters": [ + { + "name": "bookingId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + }, + "x-position": 1 + } + ], + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfDictionaryOfStringAndListOfString" + } + } + } + }, + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "403": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResult" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResultOfRefundStatusDto" + } + } + } + } + }, + "security": [ + { + "Bearer": [] + } + ] + } + }, "/api/v1/reviews/{reviewId}/tags": { "post": { "tags": [ @@ -12626,6 +14165,16 @@ }, "x-position": 2 }, + { + "name": "BookingId", + "in": "query", + "schema": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "x-position": 3 + }, { "name": "Page", "in": "query", @@ -12633,7 +14182,7 @@ "type": "integer", "format": "int32" }, - "x-position": 3 + "x-position": 4 }, { "name": "PageSize", @@ -12642,7 +14191,7 @@ "type": "integer", "format": "int32" }, - "x-position": 4 + "x-position": 5 } ], "responses": { @@ -13462,6 +15011,10 @@ "requestId": { "type": "string", "nullable": true + }, + "code": { + "type": "string", + "nullable": true } } }, @@ -14661,6 +16214,9 @@ "vatIrr": { "type": "string" }, + "totalIrr": { + "type": "string" + }, "moadianReferenceNumber": { "type": "string", "nullable": true @@ -14886,6 +16442,19 @@ } } }, + "SetPartnerCenterActiveCommand": { + "type": "object", + "additionalProperties": false, + "properties": { + "isActive": { + "type": "boolean" + }, + "id": { + "type": "integer", + "format": "int64" + } + } + }, "ApiResultOfPagedResultOfPartnerCenterListItemDto": { "allOf": [ { @@ -15706,6 +17275,12 @@ "format": "int64", "nullable": true }, + "tagCodes": { + "type": "array", + "items": { + "type": "string" + } + }, "createdAt": { "type": "string", "format": "date-time" @@ -15827,6 +17402,15 @@ "createdAt": { "type": "string", "format": "date-time" + }, + "lastMessageAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "unreadCount": { + "type": "integer", + "format": "int32" } } }, @@ -16546,6 +18130,14 @@ "resendAvailableInSeconds": { "type": "integer", "format": "int32" + }, + "codeLength": { + "type": "integer", + "format": "int32" + }, + "expiresInSeconds": { + "type": "integer", + "format": "int32" } } }, @@ -16796,6 +18388,18 @@ "createdAt": { "type": "string", "format": "date-time" + }, + "variantPrice": { + "type": "string" + }, + "nurseAvatarUrl": { + "type": "string", + "nullable": true + }, + "bookingId": { + "type": "integer", + "format": "int64", + "nullable": true } } }, @@ -16947,6 +18551,107 @@ "customerNotes": { "type": "string", "nullable": true + }, + "variantLabel": { + "type": "string" + }, + "patientAge": { + "type": "integer", + "format": "int32", + "nullable": true + } + } + }, + "ApiResultOfCheckoutSummaryDto": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResult" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/CheckoutSummaryDto" + } + ] + } + } + } + ] + }, + "CheckoutSummaryDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "bookingRequestId": { + "type": "integer", + "format": "int64" + }, + "requestStatus": { + "type": "string" + }, + "nurseName": { + "type": "string" + }, + "patientName": { + "type": "string" + }, + "variantLabel": { + "type": "string" + }, + "variantPriceUnit": { + "type": "string" + }, + "sessionCount": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requestedDate": { + "type": "string", + "format": "date" + }, + "requestedTimeStart": { + "type": "string", + "format": "time" + }, + "requestedTimeEnd": { + "type": "string", + "format": "time" + }, + "paymentDeadlineAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "serviceCostIrr": { + "type": "string" + }, + "commissionIrr": { + "type": "string" + }, + "vatIrr": { + "type": "string" + }, + "vatRate": { + "type": "number", + "format": "decimal" + }, + "totalIrr": { + "type": "string" + }, + "grossPriceIrr": { + "type": "string" + }, + "balinyaarCommissionIrr": { + "type": "string" + }, + "nursePayoutAmount": { + "type": "string" } } }, @@ -17009,6 +18714,90 @@ } } }, + "ApiResultOfReviewEligibilityDto": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResult" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/ReviewEligibilityDto" + } + ] + } + } + } + ] + }, + "ReviewEligibilityDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "canReview": { + "type": "boolean" + }, + "reason": { + "type": "string", + "nullable": true + } + } + }, + "ApiResultOfMyReviewDto": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResult" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/MyReviewDto" + } + ] + } + } + } + ] + }, + "MyReviewDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "moderationStatus": { + "type": "string" + }, + "rating": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "body": { + "type": "string", + "nullable": true + }, + "tagCodes": { + "type": "array", + "items": { + "type": "string" + } + }, + "createdAt": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, "ApiResultOfBookingDetailDto": { "allOf": [ { @@ -17384,6 +19173,216 @@ } } }, + "ApiResultOfRefundStatusDto": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResult" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/RefundStatusDto" + } + ] + } + } + } + ] + }, + "RefundStatusDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "bookingId": { + "type": "integer", + "format": "int64" + }, + "status": { + "type": "string" + }, + "refundChannel": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "expectedCustomerRefundEta": { + "type": "string", + "format": "date", + "nullable": true + }, + "reference": { + "type": "string", + "nullable": true + }, + "platformFeeRefundedIrr": { + "type": "string" + }, + "nursePayoutRefundedIrr": { + "type": "string" + }, + "refundPercentageApplied": { + "type": "number", + "format": "decimal", + "nullable": true + }, + "cancellationPolicyCode": { + "type": "string", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "completedAt": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "CancelBookingAndRefundCommand": { + "type": "object", + "additionalProperties": false, + "properties": { + "reasonCategory": { + "type": "string", + "nullable": true + }, + "reasonNotes": { + "type": "string", + "nullable": true + }, + "sessionIds": { + "type": "array", + "nullable": true, + "items": { + "type": "integer", + "format": "int64" + } + }, + "bookingId": { + "type": "integer", + "format": "int64" + } + } + }, + "ApiResultOfCancellationPolicyPreviewDto": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResult" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/CancellationPolicyPreviewDto" + } + ] + } + } + } + ] + }, + "CancellationPolicyPreviewDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "bookingId": { + "type": "integer", + "format": "int64" + }, + "cancellable": { + "type": "boolean" + }, + "cancellationPolicyCode": { + "type": "string", + "nullable": true + }, + "refundPercentageApplied": { + "type": "number", + "format": "decimal", + "nullable": true + }, + "feePercentage": { + "type": "number", + "format": "decimal", + "nullable": true + }, + "refundAmountIrr": { + "type": "string" + }, + "feeAmountIrr": { + "type": "string" + }, + "refundableAmountIrr": { + "type": "string" + }, + "platformFeeRefundedIrr": { + "type": "string" + }, + "nursePayoutRefundedIrr": { + "type": "string" + }, + "appliesTo": { + "type": "string" + }, + "leadTimeLabel": { + "type": "string" + }, + "refundChannel": { + "type": "string" + }, + "expectedCustomerRefundEta": { + "type": "string", + "format": "date", + "nullable": true + }, + "sessions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CancellationPreviewSessionDto" + } + } + } + }, + "CancellationPreviewSessionDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "bookingSessionId": { + "type": "integer", + "format": "int64" + }, + "sessionIndex": { + "type": "integer", + "format": "int32" + }, + "scheduledDate": { + "type": "string", + "format": "date" + }, + "refundable": { + "type": "boolean" + }, + "reasonCode": { + "type": "string" + } + } + }, "ApiResultOfCareInstructionsDto": { "allOf": [ { @@ -17897,6 +19896,18 @@ "providerCode": { "type": "string", "nullable": true + }, + "nationalId": { + "type": "string", + "nullable": true + }, + "mobile": { + "type": "string", + "nullable": true + }, + "consent": { + "type": "boolean", + "nullable": true } } }, @@ -17992,6 +20003,10 @@ "type": "string", "nullable": true }, + "provinceId": { + "type": "integer", + "format": "int64" + }, "cityId": { "type": "integer", "format": "int64" @@ -18083,6 +20098,16 @@ }, "isPrimary": { "type": "boolean" + }, + "latitude": { + "type": "number", + "format": "decimal", + "nullable": true + }, + "longitude": { + "type": "number", + "format": "decimal", + "nullable": true } } }, @@ -18122,6 +20147,16 @@ "recipientPhone": { "type": "string", "nullable": true + }, + "latitude": { + "type": "number", + "format": "decimal", + "nullable": true + }, + "longitude": { + "type": "number", + "format": "decimal", + "nullable": true } } }, @@ -18207,6 +20242,14 @@ "defaultEmergencyContactPhone": { "type": "string", "nullable": true + }, + "avatarUrl": { + "type": "string", + "nullable": true + }, + "preferredLanguage": { + "type": "string", + "nullable": true } } }, @@ -18221,6 +20264,85 @@ "defaultEmergencyContactPhone": { "type": "string", "nullable": true + }, + "firstName": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "string", + "nullable": true + }, + "preferredLanguage": { + "type": "string", + "nullable": true + } + } + }, + "ApiResultOfAvatarUploadResult": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResult" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/AvatarUploadResult" + } + ] + } + } + } + ] + }, + "AvatarUploadResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "nullable": true + } + } + }, + "ApiResultOfDevLastOtpResult": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResult" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/DevLastOtpResult" + } + ] + } + } + } + ] + }, + "DevLastOtpResult": { + "type": "object", + "description": "The most recent OTP captured for a phone (Development only).", + "additionalProperties": false, + "properties": { + "phone": { + "type": "string", + "nullable": true + }, + "code": { + "type": "string", + "nullable": true } } }, @@ -18918,6 +21040,240 @@ "periodEnd": { "type": "string", "format": "date" + }, + "failureReason": { + "type": "string", + "nullable": true + } + } + }, + "ApiResultOfNurseEarningsBalanceDto": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResult" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/NurseEarningsBalanceDto" + } + ] + } + } + } + ] + }, + "NurseEarningsBalanceDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "pendingTotalIrr": { + "type": "string" + }, + "eligibleTotalIrr": { + "type": "string" + }, + "paidTotalIrr": { + "type": "string" + }, + "clawbackOutstandingIrr": { + "type": "string" + }, + "netPayableBalanceIrr": { + "type": "string" + } + } + }, + "ApiResultOfPagedResultOfNurseEarningsItemDto": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResult" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/PagedResultOfNurseEarningsItemDto" + } + ] + } + } + } + ] + }, + "PagedResultOfNurseEarningsItemDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "items": { + "type": "array", + "nullable": true, + "items": { + "$ref": "#/components/schemas/NurseEarningsItemDto" + } + }, + "total": { + "type": "integer", + "format": "int32" + }, + "page": { + "type": "integer", + "format": "int32" + }, + "pageSize": { + "type": "integer", + "format": "int32" + } + } + }, + "NurseEarningsItemDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "bookingId": { + "type": "integer", + "format": "int64" + }, + "patientName": { + "type": "string", + "nullable": true + }, + "scheduledDate": { + "type": "string", + "format": "date" + }, + "grossPriceIrr": { + "type": "string" + }, + "balinyaarCommissionIrr": { + "type": "string" + }, + "nursePayoutAmount": { + "type": "string" + }, + "state": { + "type": "string" + }, + "disputeWindowEndsAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "payoutEligibleAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "paidAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "transferReference": { + "type": "string", + "nullable": true + }, + "nursePayoutId": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "batchId": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "clawbackAppliedIrr": { + "type": "string", + "nullable": true + }, + "netAmountIrr": { + "type": "string", + "nullable": true + } + } + }, + "ApiResultOfNursePayoutDetailDto": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResult" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/NursePayoutDetailDto" + } + ] + } + } + } + ] + }, + "NursePayoutDetailDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "batchId": { + "type": "integer", + "format": "int64" + }, + "status": { + "type": "string" + }, + "grossEarningsIrr": { + "type": "string" + }, + "clawbackAppliedIrr": { + "type": "string" + }, + "netAmountIrr": { + "type": "string" + }, + "amountIrr": { + "type": "string" + }, + "maskedIban": { + "type": "string" + }, + "transferReference": { + "type": "string", + "nullable": true + }, + "paidAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "failureReason": { + "type": "string", + "nullable": true + }, + "batch": { + "$ref": "#/components/schemas/PayoutBatchDto" + }, + "bookings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PayoutBookingLinkDto" + } } } }, @@ -18987,6 +21343,10 @@ "totalCompletedBookings": { "type": "integer", "format": "int32" + }, + "avatarUrl": { + "type": "string", + "nullable": true } } }, @@ -19070,6 +21430,135 @@ } } }, + "ApiResultOfNursePublicProfileDto": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResult" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/NursePublicProfileDto" + } + ] + } + } + } + ] + }, + "NursePublicProfileDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "nurseId": { + "type": "integer", + "format": "int64" + }, + "nurseName": { + "type": "string" + }, + "avatarUrl": { + "type": "string", + "nullable": true + }, + "bio": { + "type": "string" + }, + "yearsExperience": { + "type": "integer", + "format": "int32" + }, + "averageRating": { + "type": "number", + "format": "decimal" + }, + "totalReviews": { + "type": "integer", + "format": "int32" + }, + "totalCompletedBookings": { + "type": "integer", + "format": "int32" + }, + "isVerified": { + "type": "boolean" + }, + "inoMembership": { + "type": "boolean" + }, + "attributeChips": { + "type": "array", + "items": { + "type": "string" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NursePublicServiceDto" + } + }, + "latestReview": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/NursePublicLatestReviewDto" + } + ] + } + } + }, + "NursePublicServiceDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "variantId": { + "type": "integer", + "format": "int64" + }, + "displayName": { + "type": "string" + }, + "priceIrr": { + "type": "string" + }, + "priceUnit": { + "type": "string" + }, + "sessionCount": { + "type": "integer", + "format": "int32", + "nullable": true + } + } + }, + "NursePublicLatestReviewDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "rating": { + "type": "integer", + "format": "int32" + }, + "body": { + "type": "string", + "nullable": true + }, + "authorMasked": { + "type": "string", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + }, "ApiResultOfNurseReviewsResult": { "allOf": [ { @@ -19647,6 +22136,9 @@ "isAutomated": { "type": "boolean" }, + "isRequired": { + "type": "boolean" + }, "expiresAt": { "type": "string", "format": "date-time", @@ -19769,6 +22261,45 @@ } } }, + "SubmitCredentialDetailsCommand": { + "type": "object", + "additionalProperties": false, + "properties": { + "inoNumber": { + "type": "string", + "nullable": true + }, + "specialties": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "licenseNumber": { + "type": "string", + "nullable": true + }, + "issuingAuthority": { + "type": "string", + "nullable": true + }, + "holderName": { + "type": "string", + "nullable": true + }, + "issuedAt": { + "type": "string", + "format": "date", + "nullable": true + }, + "expiresAt": { + "type": "string", + "format": "date", + "nullable": true + } + } + }, "ApiResultOfRunStepResult": { "allOf": [ { @@ -19872,6 +22403,25 @@ "body": { "type": "string", "nullable": true + }, + "taskResults": { + "type": "array", + "nullable": true, + "items": { + "$ref": "#/components/schemas/TaskResultDto" + } + } + } + }, + "TaskResultDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "label": { + "type": "string" + }, + "done": { + "type": "boolean" } } }, @@ -19949,12 +22499,196 @@ "body": { "type": "string" }, + "taskResults": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskResultDto" + } + }, "recordedAt": { "type": "string", "format": "date-time" } } }, + "ApiResultOfCarePlanDto": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResult" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/CarePlanDto" + } + ] + } + } + } + ] + }, + "CarePlanDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "patientId": { + "type": "integer", + "format": "int64" + }, + "medications": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MedicationDto" + } + }, + "routine": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutineItemDto" + } + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CareTaskDto" + } + } + } + }, + "MedicationDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "dosage": { + "type": "string", + "nullable": true + }, + "frequency": { + "type": "string" + }, + "timingNote": { + "type": "string", + "nullable": true + } + } + }, + "RoutineItemDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "label": { + "type": "string" + }, + "timeOfDay": { + "type": "string", + "nullable": true + }, + "note": { + "type": "string", + "nullable": true + } + } + }, + "CareTaskDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "label": { + "type": "string" + }, + "done": { + "type": "boolean" + } + } + }, + "UpsertCarePlanBody": { + "type": "object", + "description": "The family care-plan body (the patient id comes from the route).", + "additionalProperties": false, + "properties": { + "medications": { + "type": "array", + "nullable": true, + "items": { + "$ref": "#/components/schemas/MedicationDto" + } + }, + "routine": { + "type": "array", + "nullable": true, + "items": { + "$ref": "#/components/schemas/RoutineItemDto" + } + }, + "tasks": { + "type": "array", + "nullable": true, + "items": { + "$ref": "#/components/schemas/CareTaskDto" + } + } + } + }, + "ApiResultOfRecordAccessDto": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiResult" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/RecordAccessDto" + } + ] + } + } + } + ] + }, + "RecordAccessDto": { + "type": "object", + "additionalProperties": false, + "properties": { + "canView": { + "type": "boolean" + }, + "canEdit": { + "type": "boolean" + }, + "canAppendNote": { + "type": "boolean" + }, + "deniedReason": { + "type": "string", + "nullable": true + } + } + }, "ApiResultOfPatientDto": { "allOf": [ { @@ -20014,6 +22748,17 @@ }, "isActive": { "type": "boolean" + }, + "relation": { + "type": "string", + "nullable": true + }, + "conditions": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } } } }, @@ -20048,6 +22793,17 @@ "initialMedicalNotes": { "type": "string", "nullable": true + }, + "relation": { + "type": "string", + "nullable": true + }, + "conditions": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } } } }, @@ -20132,6 +22888,17 @@ "initialMedicalNotes": { "type": "string", "nullable": true + }, + "relation": { + "type": "string", + "nullable": true + }, + "conditions": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } } } }, @@ -20275,6 +23042,15 @@ "description": { "type": "string", "nullable": true + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "updatedBy": { + "type": "integer", + "format": "int32", + "nullable": true } } }, @@ -20364,59 +23140,6 @@ } } }, - "ApiResultOfRefundStatusDto": { - "allOf": [ - { - "$ref": "#/components/schemas/ApiResult" - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "nullable": true, - "oneOf": [ - { - "$ref": "#/components/schemas/RefundStatusDto" - } - ] - } - } - } - ] - }, - "RefundStatusDto": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "bookingId": { - "type": "integer", - "format": "int64" - }, - "status": { - "type": "string" - }, - "refundChannel": { - "type": "string" - }, - "amount": { - "type": "string" - }, - "expectedCustomerRefundEta": { - "type": "string", - "format": "date", - "nullable": true - }, - "reference": { - "type": "string", - "nullable": true - } - } - }, "ApiResultOfReviewTagsResult": { "allOf": [ { @@ -20614,6 +23337,19 @@ "type": "integer", "format": "int64", "nullable": true + }, + "nurseName": { + "type": "string", + "nullable": true + }, + "avatarUrl": { + "type": "string", + "nullable": true + }, + "distanceKm": { + "type": "number", + "format": "double", + "nullable": true } } }, @@ -20864,6 +23600,10 @@ "sentAt": { "type": "string", "format": "date-time" + }, + "clientMessageId": { + "type": "string", + "nullable": true } } }, @@ -20880,6 +23620,10 @@ }, "isInternal": { "type": "boolean" + }, + "clientMessageId": { + "type": "string", + "nullable": true } } }, diff --git a/dev/shared-working-context/frontend/requests/for-backend.md b/dev/shared-working-context/frontend/requests/for-backend.md index fa10d41..2578251 100644 --- a/dev/shared-working-context/frontend/requests/for-backend.md +++ b/dev/shared-working-context/frontend/requests/for-backend.md @@ -29,7 +29,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a pattern every later frontend phase inherits. - **Proposed shape:** `{ isSuccess: boolean, statusCode: number, message?: string, requestId?: string, data?: T }` and `data: { items: T[], total: number, page: number, pageSize: number }` for lists. -- **Status:** open +- **Status:** confirmed in refinement-phase-3 — the `ApiResult` envelope (payload under `data`, camelCase body, integer `statusCode`) and `PagedResult` `{ items, total, page, pageSize }` are the intended shapes for all endpoints. **Note:** the list query param binds camelCase **`pageSize`** (case-insensitive); the `page_size` doc occurrences were swept (REQ-010). ## REQ-002 — OTP length + expiry in RequestOtpResult — filed by frontend-phase-1-b2 — 2026-07-02 - **Need:** Add `codeLength` (int) and `expiresInSeconds` (int) to `RequestOtpResult`. @@ -38,7 +38,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a (`OTP_CODE_LENGTH = 6`, inferred from the live 6-digit verify example, not the 4-box wireframe). Surfacing the length makes the box count contract-driven; the expiry lets us show "code expires in …". - **Proposed shape:** `{ otpSent: boolean, resendAvailableInSeconds: number, codeLength: number, expiresInSeconds: number }` -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-003 — Machine-readable error codes for verify_otp failures — filed by frontend-phase-1-b2 — 2026-07-02 - **Need:** A stable `code` on the 400 envelope for verify_otp that distinguishes wrong code vs expired code @@ -50,7 +50,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a otherwise degrades to a generic "incorrect or expired" message. A stable machine code (kept generic enough to avoid account enumeration) would let the UI render the precise state + the unlock countdown. - **Proposed shape:** `{ isSuccess: false, statusCode: 400, message: "…", code: "otp_locked", data: { retryAfterSeconds: 60 } }` -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-005 — Patient `relation` + `conditions` fields — filed by frontend-phase-2-b3 — 2026-07-02 - **Need:** Add two fields to `PatientDto` and the `patients/create` + `patients/update` bodies: @@ -62,7 +62,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a and drops them on the real path. Adding the columns lets the client flip the flag to the live endpoints. - **Proposed shape:** `PatientDto { …, relation: string|null, conditions: string[] }`; same fields accepted on create/update. Enum for `relation`; `conditions` a stable code list (could also be a normalized child table). -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-006 — Avatar / object-storage upload route (nurse & customer) — filed by frontend-phase-2-b3 — 2026-07-02 - **Need:** A multipart image-upload endpoint backed by `IObjectStorage` that returns a stored URL, plus an @@ -72,7 +72,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a has no avatar field or upload route, and the client fetch layer is JSON-only (can't send multipart). The client mocks this behind the `services/profiles` seam (`uploadAvatar` returns an object URL). The real `profilesClientApi.uploadAvatar` throws `501` until this lands. -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-007 — Customer name + preferred-language update — filed by frontend-phase-2-b3 — 2026-07-02 - **Need:** Either add `firstName`/`lastName`/`preferredLanguage` to the `customer_profiles/upsert` body + @@ -83,7 +83,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a contact. Absent a wire field/endpoint, the client augments name/language behind the `services/profiles` seam (mock-persisted; the real upsert sends only the emergency contact). Confirm the intended home for these so the client stops augmenting. -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-004 — Confirm multi-role disambiguation (activeRole?) — filed by frontend-phase-1-b2 — 2026-07-02 - **Need:** Confirm whether `MeResult` will gain an `activeRole` (the user's currently-selected actor) for a @@ -112,7 +112,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a - **Proposed shape:** create/update body gains `latitude?: number, longitude?: number`; when both present, store them (and mark the geocode source as "user-pin"); when absent, geocode as today. `CustomerAddressDto` already returns `latitude`/`longitude`. -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-009 — Add `provinceId` to `CustomerAddressDto` — filed by frontend-phase-3-b4 — 2026-07-02 - **Need:** Add `provinceId` (long) to `CustomerAddressDto` (the province that owns the address's `cityId`). @@ -124,7 +124,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a the server** can't prefill the province until this lands. `cityId` still implies the province server-side — this is purely to prefill the client cascade. - **Proposed shape:** `CustomerAddressDto { …, provinceId: long }` (join from `cities.province_id`). -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-010 — Confirm/align the list pagination query-param name (catalog + all lists) — filed by frontend-phase-4-b5 — 2026-07-05 - **Need:** Confirm the exact query-param name the paginated list endpoints bind for page size. The @@ -136,7 +136,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a the server truly binds `pageSize`, please update the `page_size` occurrences in the contract docs to match; if it binds `page_size`, tell us and we'll switch the client (one line per list call). - **Proposed shape:** list query = `?page={1-based}&pageSize={≤100}`; response `data` = `{ items, total, page, pageSize }`. -- **Status:** open +- **Status:** delivered in refinement-phase-3 — server binds **`pageSize`**; the `page_size` occurrences in `dev/contracts/domains/*.md` + `conventions/api-conventions.md` were swept to `pageSize`. (One generated swagger endpoint still shows a `page_size` query name with `x-originalName: pageSize`; it binds `pageSize` case-insensitively.) ## REQ-011 — Nurse-facing endpoint for structured professional-credential details — filed by frontend-phase-5-b6 — 2026-07-09 - **Need:** A nurse-facing command to submit the **structured** credential fields B5 collects alongside the @@ -154,7 +154,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a → `VerificationStatusDto`. Alternatively, extend the manual-step `documents` confirm body with these fields. - **Also (minor):** the contract's `VerificationStepDto` has no `isRequired` — the client treats **every** seeded step as required (the "X از Y" meter Y = `steps.length`). Confirm that holds, or add `isRequired`. -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-012 — Search result + nurse-profile enrichment for discovery (C2/C3) — filed by frontend-phase-6-b7 — 2026-07-09 - **Need:** Two extra read surfaces the discovery UI renders but b7/b6/b5 don't yet expose: @@ -175,7 +175,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a When both land, the swap is a single config flip (no hook/component change). - **Proposed shape:** enrich `NurseSearchResultDto` with `{ nurseName, avatarUrl, distanceKm? }`; add `GET api/v1/nurses/{id}/profile` returning the object above. `price`/`priceIrr` stay IRR digit-strings. -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-013 — Variant price on `BookingRequestDto` — filed by frontend-phase-7-b8 — 2026-07-09 - **Need:** Add the variant's **price** (IRR digit-string) to `BookingRequestDto` (and ideally the nurse's @@ -188,7 +188,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a lets the summary price the service once the domain flips to the real endpoint. - **Proposed shape:** `BookingRequestDto { …, variantPrice: string (IRR digits), nurseAvatarUrl?: string }`. (Money-free rule intact — this is the *rate* of the chosen variant for display, not an engagement total.) -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-014 — Enrich the nurse-inbox list item (variant label + patient age) — filed by frontend-phase-7-b8 — 2026-07-09 - **Need:** Add `variantLabel` (and optionally the patient's **age/age-band**) to @@ -199,7 +199,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a the nurse must open the detail (`get/{id}`, which *does* carry `variantLabel`) to see the service. Surfacing `variantLabel` on the row makes the inbox self-describing; a coarse age is a nice-to-have for triage. - **Proposed shape:** `BookingRequestListItemDto { …, variantLabel: string, patientAge?: int }`. -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-015 — Confirm the booking/session/EVV enum codes + `checkInAddressMatch` tri-state — filed by frontend-phase-8-b9 — 2026-07-10 - **Need:** Two confirmations so the f8 `services/bookings/types.ts` client unions stay wire-accurate: @@ -218,7 +218,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a - **Why:** f8 renders the status timeline, per-session chips, and the EVV banner strictly off these codes; a casing/int drift or a `false`-vs-`null` conflation would mislabel a visit. Low-risk (mock-primary now), but worth locking before f9/f13 consume the same shapes. -- **Status:** open +- **Status:** confirmed in refinement-phase-3 — booking/session/EVV statuses serialize as the exact snake_case string codes the client unions expect (verified in code); `checkInAddressMatch` is `bool?` = `null` when GPS was absent **or** the frozen address has no resolvable coordinate, `false` = advisory out-of-range (never a block), `true` = in range. ## REQ-016 — Checkout summary for C6 (served gross/commission/VAT breakdown) — filed by frontend-phase-9-b10 — 2026-07-10 - **Need:** A customer-facing read that serves the C6 «خلاصه و پرداخت» money rows for an @@ -237,7 +237,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a serviceCostIrr, commissionIrr, vatIrr, vatRate, totalIrr, grossPriceIrr, balinyaarCommissionIrr, nursePayoutAmount }` — the client's real `paymentClientApi.getCheckoutSummary` already targets this slug and unwraps this exact shape (`client/src/services/payment/types.ts: CheckoutSummaryDto`). -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-017 — Client-readable payment outcome + `bookingId` on a converted request — filed by frontend-phase-9-b10 — 2026-07-10 - **Need:** After the gateway redirect returns, the client needs to learn (a) the payment transaction's @@ -256,7 +256,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a - **Proposed shape:** add `bookingId: long?` to `BookingRequestDto` (null until converted) **and/or** `GET api/v1/bookings/{bookingRequestId}/payments/latest` → `{ transactionId, status, gatewayReferenceCode, bookingId? }`. -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-018 — Customer invoice availability after capture (auto-issue or owner-issue) — filed by frontend-phase-9-b10 — 2026-07-10 - **Need:** Make the b11 invoice reachable by the paying customer right after capture: auto-issue the @@ -267,7 +267,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a admin acts. The UI handles the 404 as a "فاکتور هنوز صادر نشده است" state (and the mock auto-issues at capture to demo the full flow), but on the real rails every fresh payment would land on that empty state. -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-019 — Customer-initiated booking cancellation command — filed by frontend-phase-10-b11 — 2026-07-10 - **Need:** A **customer-facing** command to cancel a booking (post-payment) and open its refund, e.g. @@ -286,7 +286,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a server resolves the snapshotted policy, enforces the outside-policy/state rules (`409`), posts the balanced reversal, and (per the admin-only rule) may route the refund through an admin/ticket step — the customer surface just needs to *create* the cancellation request and read the resulting refund. -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-020 — Cancellation-policy preview (pre-cancel, per-session) — filed by frontend-phase-10-b11 — 2026-07-10 - **Need:** A read that **resolves the applicable cancellation policy by current lead time** *before* the @@ -308,7 +308,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a set so the client maps the real codes. - **Proposed shape:** as above. The `cancellationPolicyCode` set + the per-session `reasonCode` set (`un_started` / the blocking session status) should be documented as stable enum codes → i18n keys. -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-021 — Customer refund lookup-by-booking + fee-leg decomposition on the customer status — filed by frontend-phase-10-b11 — 2026-07-10 - **Need:** Two additions to the customer refund surface: @@ -327,7 +327,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a commission on a revert) is **nullable** on the refund shape and reconciled from the provider response — the b12 `IBnplProvider` mock echoes it as nullable and the client treats any provider-commission figure as opaque/never customer-facing. -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-022 — BNPL provider/plan options + repayment schedule (D1/D2/D4) — filed by frontend-phase-11-b12 — 2026-07-10 - **Need:** Two customer-facing reads the installment checkout renders that b12 serves **nothing** for: @@ -349,7 +349,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a - **Note on provider set:** the wireframe includes an **in-house `balinyaar`** plan not in the b12 `provider_code` enum (`snapppay|digipay|tara|torobpay`). Please add `balinyaar` (or state how the in-house plan is modelled) so `providerCode` stays a closed set. -- **Status:** open +- **Status:** partially delivered in refinement-phase-3 — `balinyaar` added to the `provider_code` enum. **DEFERRED:** `checkout_bnpl/options/{id}` + `schedule` (per-plan monthly/down-payment split + due-dated repayment table) — b12 deliberately does not model the customer repayment schedule and there is no installment ledger to serve it from; keep D1/D2/D4 mocked until a provider-schedule integration or schedule table lands. ## REQ-023 — BNPL eligibility should accept the D3 credit-check inputs (national ID / mobile / consent) — filed by frontend-phase-11-b12 — 2026-07-10 - **Need:** Either extend `POST api/v1/checkout_bnpl/eligibility` to accept `{ nationalId, mobile, consent }` @@ -359,7 +359,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a the extra fields today (ignored server-side until the KYC step exists) and the mock uses them for the deterministic declined-path demo. The response already carries `eligibilityStatus` + `creditCeilingIrr`, which D3 renders — only the request inputs are the gap. -- **Status:** open +- **Status:** delivered in refinement-phase-3 — `checkout_bnpl/eligibility` now accepts `{ nationalId, mobile, consent }` (consent required when the KYC inputs are present; supplied mobile drives the inquiry, else the account mobile). Mock still uses only the mobile until the real KYC step exists. ## REQ-024 — BNPL provider-reported installment status for the Wallet (D5) + customer bookingId link — filed by frontend-phase-11-b12 — 2026-07-10 - **Need:** Two additions for the Wallet installment view and the confirmation deep-link: @@ -383,7 +383,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a surface, and b12 models none of the per-installment schedule/status. The client mocks the whole D5 read behind the `services/bnpl` seam (seeded plan + a plan pushed on each settled checkout). When it lands the swap is one config flip. -- **Status:** open +- **Status:** partially delivered in refinement-phase-3 — (2) `bookingId` on the settled order **already present** on `BnplOrderStatusDto` (confirmed); (3) `GET checkout_bnpl/by_request/{bookingRequestId}` added (owner-scoped). **DEFERRED:** (1) `checkout_bnpl/wallet_installments` — per-installment provider-reported status Balinyaar does not own/track (no installment ledger in b12); needs provider integration. Keep D5 mocked. ## REQ-025 — Nurse-read earnings surface: four-bucket balance + per-booking earnings list + nurse payout detail — filed by frontend-phase-12-b13 — 2026-07-10 - **Need:** b13 serves the nurse exactly one endpoint (`GET api/v1/nurse_payouts/history` → `NursePayoutHistoryDto`). @@ -417,7 +417,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a - **Note (money invariants the server owns):** `gross_price_irr = balinyaar_commission_irr + nurse_payout_amount`; `net_amount = gross_earnings − clawback_applied`; a payout's booking-link `payout_amount_irr` sum = its `gross_earnings_irr`; the nurse amount is **payment-method-invariant** (BNPL provider commission never deducted). -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-026 — Review eligibility + my-review-for-booking reads (+ masked author confirmation) — filed by frontend-phase-13-b14 — 2026-07-10 - **Need:** Three customer-facing additions the leave-a-review flow renders that b14 does not serve: @@ -437,7 +437,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a reads the shared f8 bookings store for completed-booking eligibility, tracks the submission for the under-review state, and seeds a published list per nurse. The real `reviewsClientApi` maps `getNurseReviews`/`createReview` 1:1 and targets the two proposed slugs for the gaps — one config flip when they land. -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-027 — Family-owned care record (medications/routine/tasks) + record access + structured task results — filed by frontend-phase-13-b14 — 2026-07-10 - **Need:** The b14 `care_records` GET/POST serve the **nurse-authored visit-note history** (سوابق) — that half @@ -460,7 +460,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a methods target the proposed slugs (REQ-027) and the domain is **mock-primary** (`USE_PATIENT_RECORDS_MOCK = true`) until they land. **Note:** the wireframe's four-tab E2 record is not in the data model — please confirm whether the family-owned record is a real MVP entity or a future addition (the client treats it as forward-looking). -- **Status:** open +- **Status:** delivered in refinement-phase-3 ## REQ-028 — Ticket inbox enrichment (unread + last-activity), message author name, by-booking lookup, optimistic idempotency — filed by frontend-phase-14-b15 — 2026-07-10 - **Need:** Four additions the f14 messaging UI renders that the b15 `TicketSummaryDto`/`TicketThreadDto`/message @@ -492,7 +492,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a optimistic composer render these; the domain is mock-primary precisely because (1)/(3) aren't served and the linked bookings are themselves mock-primary. When they land the swap is a single `USE_TICKETS_MOCK = false` flip (no hook/component change) — `ticketsClientApi` already maps the live routes. -- **Status:** open +- **Status:** delivered in refinement-phase-3 — (1) `unreadCount` + `lastMessageAt` on `TicketSummaryDto` (unread = non-internal messages from others after the caller's `last_read_at`, stamped when the participant fetches the thread; admin queue = 0). (3) `bookingId` query param on `GET /tickets`. (4) optional `clientMessageId` on `POST /tickets/{id}/messages` (deduped, echoed on `PostMessageResult`). (2) **Confirmed:** the role-label approach is intended — no raw identity/name is added (privacy). ## REQ-029 — Config `updatedAt`/`updatedBy` on `PlatformConfigDto` — filed by frontend-phase-15-b15 — 2026-07-10 - **Need:** the f15 config editor shows each row's last-changed meta ("updated {date} by {actor}"), but the b1 @@ -501,14 +501,14 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a latest on the row without opening the drawer). Mock supplies both; the real row degrades gracefully without. - **Why:** finance needs the effective value + who last touched it at a glance. `services/admin` is mock-primary (`USE_ADMIN_MOCK = true`); `adminClientApi.listConfigs` maps the live route 1:1 and leaves these undefined. -- **Status:** open +- **Status:** delivered in refinement-phase-3 — `updatedAt` + `updatedBy` on `PlatformConfigDto` (from the entity's audit fields; falls back to creation). ## REQ-030 — Audit-trail filters: actor / action / date-range — filed by frontend-phase-15-b15 — 2026-07-10 - **Need:** `GET audit/get_audit_trail` filters only by `entity_type` + `entity_id`. The f15 audit viewer offers actor, action, and from/to date filters. Proposed: add `actor_id`, `action`, `from`, `to` query params (the mock honours all four). Until then the real client passes only the supported two and the rest degrade. - **Why:** ops audits by actor and by time window, not only by a single entity. Mock-primary. -- **Status:** open +- **Status:** delivered in refinement-phase-3 — `GET audit/get_audit_trail` now also filters by `actorId`, `action`, `from`, `to` (all optional; `entityType`/`entityId` are now optional too). **Note:** query params bind camelCase (`actorId`/`from`/`to`), like `pageSize` — not `actor_id`. ## REQ-031 — RBAC role grant/revoke/list endpoints — filed by frontend-phase-15-b15 — 2026-07-10 - **Need:** the b15 contract exposes no role-management endpoints. The (optional, **DEFERRED-IF-MISSING**) admin @@ -517,7 +517,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a { userId, role }`, where `role` is one of `super_admin|admin|support|finance|moderation`. The screen is built against the mock and flagged DEFERRED-IF-MISSING; swap `USE_ADMIN_MOCK=false` once the routes land. - **Why:** to manage which users hold which admin scopes. Not on the testable acceptance path. -- **Status:** open +- **Status:** deferred in refinement-phase-3 — the RBAC `admin_roles/list|grant|revoke` console. The admin sub-role vocabulary + phone-OTP admins are seeded (refinement-phase-2), but a full grant/revoke management surface is admin-console tooling not on the frontend acceptance path (flagged DEFERRED-IF-MISSING); keep `USE_ADMIN_MOCK` for `/admin/roles`. To deliver: 3 endpoints over `user_roles` (grant/revoke audited) + a `RoleGrant[]` read. ## REQ-032 — Partner-portal split reads + activate/suspend + IBAN write-then-masked — filed by frontend-phase-15-b15 — 2026-07-10 - **Need:** the b15 contract has admin partner-center CRUD/verify/sponsor + a single `GET /centers/{id}/dashboard` @@ -529,7 +529,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a full `settlementIban`, GET returns only `settlementIbanMasked` last-4). Also the admin **roster read** (`GET admin/partner-centers/{id}/nurses`). `services/partnerCenter` is mock-primary (`USE_PARTNER_MOCK`). - **Why:** the portal (separate authz scope, own-center tenancy) + the admin management screens render these. -- **Status:** open +- **Status:** partially delivered in refinement-phase-3 — (3) activate/suspend toggle `POST admin/partner-centers/{id}/set-active { isActive }` added; (4) **confirmed** the write-then-masked IBAN contract (PATCH accepts full `settlementIban`; reads return only masked last-4). **Route casing confirmed:** the b15 admin partner-center routes are **kebab-case** (`admin/partner-centers`, `.../set-active`) — an intentional b15 divergence, so the frontend's kebab-case guess is CORRECT (no change needed). **DEFERRED:** (1) `centers/me` + (2) the split portal reads (`centers/me/nurses|bookings|settlement`) — these need the user↔partner-center admin association that REQ-038 deferred; `/partner` stays reachable by direct nav + the partnerCenter mock until that seed + `/me` signal land. ## REQ-033 — Partner settlement: per-booking commission invoices + invoice `total` — filed by frontend-phase-15-b15 — 2026-07-10 - **Need:** the merchant-of-record settlement view lists per-booking **commission invoices** (b11 `Invoice` @@ -539,7 +539,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a Proposed: serve `totalIrr` on the invoice (= commission + bnplCommission + vat), plus a center-scoped list. - **Why:** the settlement/invoice view (rendered only when `is_merchant_of_record`) needs a reconciling total. VAT stays on the commission line only; the rate is config-driven (`vat_rate`), never hardcoded. -- **Status:** open +- **Status:** partially delivered in refinement-phase-3 — `totalIrr` (= platform commission + BNPL commission + VAT) added to `InvoiceDto`. **DEFERRED:** the center-scoped invoice list (depends on the partner portal split reads, REQ-032). ## REQ-034 — Verification admin: nurse-level queue + on-demand document URL + whole-verification approve/reject — filed by frontend-phase-15-b15 — 2026-07-10 - **Need:** three gaps in the b6 admin surface for the f15 review queue: (1) `GET admin_verifications` returns @@ -552,7 +552,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a Reject action. `services/verification` is mock-primary. - **Why:** the queue + per-nurse case + signed-URL document viewer render these. The client never writes `is_verified` — the server flips it transactionally (§5). -- **Status:** open +- **Status:** deferred in refinement-phase-3 — verification admin polish (nurse-grouped queue, on-demand signed document URL, explicit whole-verification approve/reject). The per-step admin surface + the transactional `is_verified` flip already exist (b6); these are ergonomic refinements to the admin queue, not on the frontend acceptance path. ## REQ-035 — Refund preview + explicit approve/reject — filed by frontend-phase-15-b15 — 2026-07-10 - **Need:** b11 `POST admin_refunds` **creates and executes** in one call, so there is no way to render a @@ -563,7 +563,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a (today the single POST both creates + executes). `services/refunds` admin methods are mock-primary; `initiateRefund` maps the live `POST admin_refunds`. - **Why:** the ticket-linked refund panel shows the preview, then initiate → (retry on provider failure) / reject. -- **Status:** open +- **Status:** deferred in refinement-phase-3 — refund admin preview + explicit approve/reject (the single `POST admin_refunds` creates+executes today). The customer preview (REQ-020) IS delivered and serves the same decomposition; the admin-side preview/approve/reject split is admin-console tooling. ## REQ-036 — Payout single preview endpoint + `holidayShifted` flag + record-transfer-reference — filed by frontend-phase-15-b15 — 2026-07-10 - **Need:** the f15 payout dashboard wants (1) a **single preview** call returning eligible + skipped + the @@ -576,7 +576,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a (b13 has `mark_failed` but no reconcile-reference write). `services/payouts` admin methods are mock-primary; the run/retry map the live routes with the `Idempotency-Key` header. - **Why:** preview → run (idempotency-keyed, no double-pay) → detail with per-nurse retry + transfer-ref reconcile. -- **Status:** open +- **Status:** deferred in refinement-phase-3 — payout admin single-preview endpoint + `holidayShifted` flag + record-transfer-reference route. The eligible/skipped data is already returned by the batch generate/`GET admin_payouts/eligible`; a consolidated dry-run preview + reconcile-reference write are admin-console refinements. ## REQ-037 — Moderation queue `tagCodes` on `ModerationQueueItemDto` — filed by frontend-phase-15-b15 — 2026-07-10 - **Need:** the f15 moderation queue renders each review's tag chips, but `ModerationQueueItemDto` (b14) doesn't @@ -584,6 +584,7 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a DTO. The client defaults to `[]` meanwhile. `services/reviews` moderation methods are mock-primary; `moderateReview` maps the live `PATCH reviews/{id}/status` and the queue maps `GET admin/reviews/moderation_queue`. - **Why:** moderators see the tags a review carries before publishing/hiding. +- **Status:** delivered in refinement-phase-3 ## REQ-038 — Signal on `/me` that the caller administers a partner center (partner auto-routing) — filed by refinement-phase-2 — 2026-07-13 - **Need:** add a boolean/id on `MeResult` — e.g. `administersPartnerCenterId: number | null` (or a plain diff --git a/server/src/API/Baya.Web.Api/Controllers/V1/AdminPartnerCentersController.cs b/server/src/API/Baya.Web.Api/Controllers/V1/AdminPartnerCentersController.cs index 412a435..1e09a0f 100644 --- a/server/src/API/Baya.Web.Api/Controllers/V1/AdminPartnerCentersController.cs +++ b/server/src/API/Baya.Web.Api/Controllers/V1/AdminPartnerCentersController.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using Asp.Versioning; using Baya.Application.Features.PartnerCenters.Commands.CreatePartnerCenter; +using Baya.Application.Features.PartnerCenters.Commands.SetPartnerCenterActive; using Baya.Application.Features.PartnerCenters.Commands.SponsorNurse; using Baya.Application.Features.PartnerCenters.Commands.UpdatePartnerCenter; using Baya.Application.Features.PartnerCenters.Commands.VerifyPartnerCenter; @@ -48,6 +49,12 @@ public sealed class AdminPartnerCentersController(ISender sender) : BaseControll public async Task SponsorNurse(long id, SponsorNurseCommand command, CancellationToken cancellationToken) => OperationResult(await sender.Send(command with { CenterId = id }, cancellationToken)); + // Activate/suspend toggle (distinct from verify, which records licensing approval) — REQ-032. + [HttpPost("{id}/set-active")] + [ProducesOkApiResponseType] + public async Task SetActive(long id, SetPartnerCenterActiveCommand command, CancellationToken cancellationToken) + => OperationResult(await sender.Send(command with { Id = id }, cancellationToken)); + [HttpGet] [ProducesOkApiResponseType>] public async Task List([FromQuery] ListPartnerCentersQuery query, CancellationToken cancellationToken) diff --git a/server/src/API/Baya.Web.Api/Controllers/V1/BookingRequestsController.cs b/server/src/API/Baya.Web.Api/Controllers/V1/BookingRequestsController.cs index 25a050f..ad129d9 100644 --- a/server/src/API/Baya.Web.Api/Controllers/V1/BookingRequestsController.cs +++ b/server/src/API/Baya.Web.Api/Controllers/V1/BookingRequestsController.cs @@ -5,6 +5,7 @@ using Baya.Application.Features.Booking.Commands.CancelBookingRequest; using Baya.Application.Features.Booking.Commands.CreateBookingRequest; using Baya.Application.Features.Booking.Commands.RejectBookingRequest; using Baya.Application.Features.Booking.Queries.GetBookingRequest; +using Baya.Application.Features.Booking.Queries.GetCheckoutSummary; using Baya.Application.Features.Booking.Queries.ListBookingRequests; using Baya.Application.Models.Booking; using Baya.Application.Models.Common; @@ -57,4 +58,10 @@ public sealed class BookingRequestsController(ISender sender) : BaseController [ProducesOkApiResponseType] public async Task Get(long id, CancellationToken cancellationToken) => OperationResult(await sender.Send(new GetBookingRequestQuery(id), cancellationToken)); + + // The C6 money breakdown for an accepted-awaiting-payment request (owner-scoped, server-computed). + [HttpGet("[action]/{id}")] + [ProducesOkApiResponseType] + public async Task CheckoutSummary(long id, CancellationToken cancellationToken) + => OperationResult(await sender.Send(new GetCheckoutSummaryQuery(id), cancellationToken)); } diff --git a/server/src/API/Baya.Web.Api/Controllers/V1/BookingReviewsController.cs b/server/src/API/Baya.Web.Api/Controllers/V1/BookingReviewsController.cs index 70b018e..4de7206 100644 --- a/server/src/API/Baya.Web.Api/Controllers/V1/BookingReviewsController.cs +++ b/server/src/API/Baya.Web.Api/Controllers/V1/BookingReviewsController.cs @@ -1,6 +1,8 @@ using System.ComponentModel.DataAnnotations; using Asp.Versioning; using Baya.Application.Features.Reviews.Commands.SubmitReview; +using Baya.Application.Features.Reviews.Queries.GetMyReview; +using Baya.Application.Features.Reviews.Queries.GetReviewEligibility; using Baya.Application.Models.Reviews; using Baya.WebFramework.Attributes; using Baya.WebFramework.BaseController; @@ -27,6 +29,18 @@ public sealed class BookingReviewsController(ISender sender) : BaseController => OperationResult(await sender.Send( new SubmitReviewCommand(bookingId, body.Rating, body.Body, body.TagCodes), cancellationToken)); + // Can the caller review this booking? (completed/closed AND not already reviewed) — REQ-026. + [HttpGet("{bookingId}/review_eligibility")] + [ProducesOkApiResponseType] + public async Task ReviewEligibility(long bookingId, CancellationToken cancellationToken) + => OperationResult(await sender.Send(new GetReviewEligibilityQuery(bookingId), cancellationToken)); + + // The caller's own review for the booking (persistent "under review" state across sessions) — REQ-026. + [HttpGet("{bookingId}/my_review")] + [ProducesOkApiResponseType] + public async Task MyReview(long bookingId, CancellationToken cancellationToken) + => OperationResult(await sender.Send(new GetMyReviewQuery(bookingId), cancellationToken)); + /// The review body (the booking id comes from the route). public record SubmitReviewBody(int Rating, string? Body, IReadOnlyList? TagCodes); } diff --git a/server/src/API/Baya.Web.Api/Controllers/V1/BookingsController.cs b/server/src/API/Baya.Web.Api/Controllers/V1/BookingsController.cs index 5f2786e..78670dd 100644 --- a/server/src/API/Baya.Web.Api/Controllers/V1/BookingsController.cs +++ b/server/src/API/Baya.Web.Api/Controllers/V1/BookingsController.cs @@ -7,8 +7,11 @@ using Baya.Application.Features.Bookings.Commands.TransitionBookingStatus; using Baya.Application.Features.Bookings.Queries.GetBookingDetail; using Baya.Application.Features.Bookings.Queries.GetCareInstructions; using Baya.Application.Features.Bookings.Queries.ListBookings; +using Baya.Application.Features.Refunds.Commands.CancelBookingAndRefund; +using Baya.Application.Features.Refunds.Queries.GetCancellationPolicyPreview; using Baya.Application.Models.Booking; using Baya.Application.Models.Common; +using Baya.Application.Models.Refunds; using Baya.WebFramework.Attributes; using Baya.WebFramework.BaseController; using Baya.WebFramework.ServiceConfiguration; @@ -60,6 +63,19 @@ public sealed class BookingsController(ISender sender) : BaseController public async Task Cancel(long id, CancelBookingCommand command, CancellationToken cancellationToken) => OperationResult(await sender.Send(command with { BookingId = id }, cancellationToken)); + // Customer-initiated cancel: cancels the booking AND opens its refund in one call (REQ-019). + [HttpPost("{id}/cancel")] + [EnableRateLimiting(RateLimitingServiceExtension.SensitivePolicy)] + [ProducesOkApiResponseType] + public async Task CancelAndRefund(long id, CancelBookingAndRefundCommand command, CancellationToken cancellationToken) + => OperationResult(await sender.Send(command with { BookingId = id }, cancellationToken)); + + // Pre-cancel disclosure: the applicable policy + per-session refundability, resolved by current lead time (REQ-020). + [HttpGet("{id}/cancellation_policy")] + [ProducesOkApiResponseType] + public async Task CancellationPolicy(long id, CancellationToken cancellationToken) + => OperationResult(await sender.Send(new GetCancellationPolicyPreviewQuery(id), cancellationToken)); + [HttpPost("[action]/{id}")] [ProducesOkApiResponseType] public async Task SubmitCareInstructions(long id, SubmitCareInstructionsCommand command, CancellationToken cancellationToken) diff --git a/server/src/API/Baya.Web.Api/Controllers/V1/CheckoutBnplController.cs b/server/src/API/Baya.Web.Api/Controllers/V1/CheckoutBnplController.cs index 8c2be1d..7bff7e4 100644 --- a/server/src/API/Baya.Web.Api/Controllers/V1/CheckoutBnplController.cs +++ b/server/src/API/Baya.Web.Api/Controllers/V1/CheckoutBnplController.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using Asp.Versioning; using Baya.Application.Features.Bnpl.Commands.InitiateBnplOrder; using Baya.Application.Features.Bnpl.Queries.CheckBnplEligibility; +using Baya.Application.Features.Bnpl.Queries.GetBnplOrderByRequest; using Baya.Application.Features.Bnpl.Queries.GetBnplOrderStatus; using Baya.Application.Models.Bnpl; using Baya.WebFramework.Attributes; @@ -42,11 +43,17 @@ public sealed class CheckoutBnplController(ISender sender) : BaseController new InitiateBnplOrderCommand(body.BookingRequestId, body.ProviderCode, idempotencyKey), cancellationToken)); } - [HttpGet("{id}")] + [HttpGet("{id:long}")] [ProducesOkApiResponseType] public async Task Get(long id, CancellationToken cancellationToken) => OperationResult(await sender.Send(new GetBnplOrderStatusQuery(id, AdminView: false), cancellationToken)); + // Reach the BNPL order from the booking request id (the return-poll holds the request id, not the order id). + [HttpGet("by_request/{bookingRequestId}")] + [ProducesOkApiResponseType] + public async Task ByRequest(long bookingRequestId, CancellationToken cancellationToken) + => OperationResult(await sender.Send(new GetBnplOrderByRequestQuery(bookingRequestId), cancellationToken)); + /// The initiate body (the idempotency key comes from the Idempotency-Key header). public record InitiateBnplBody(long BookingRequestId, string ProviderCode); } diff --git a/server/src/API/Baya.Web.Api/Controllers/V1/CustomerProfilesController.cs b/server/src/API/Baya.Web.Api/Controllers/V1/CustomerProfilesController.cs index ba59c4b..f041e4c 100644 --- a/server/src/API/Baya.Web.Api/Controllers/V1/CustomerProfilesController.cs +++ b/server/src/API/Baya.Web.Api/Controllers/V1/CustomerProfilesController.cs @@ -1,12 +1,15 @@ using System.ComponentModel.DataAnnotations; using Asp.Versioning; +using Baya.Application.Features.Identity.Commands.UploadCustomerAvatar; using Baya.Application.Features.Identity.Commands.UpsertCustomerProfile; using Baya.Application.Features.Identity.Queries.GetMyCustomerProfile; +using Baya.Application.Models.Common; using Baya.Application.Models.Identity; using Baya.WebFramework.Attributes; using Baya.WebFramework.BaseController; using Mediator; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Baya.Web.Api.Controllers.V1; @@ -27,4 +30,18 @@ public sealed class CustomerProfilesController(ISender sender) : BaseController [ProducesOkApiResponseType] public async Task Me(CancellationToken cancellationToken) => OperationResult(await sender.Send(new GetMyCustomerProfileQuery(), cancellationToken)); + + // Multipart image upload → stored via IObjectStorage; the returned URL is persisted on the profile. + [HttpPost("[action]")] + [ProducesOkApiResponseType] + public async Task Avatar(IFormFile file, CancellationToken cancellationToken) + { + if (file is null || file.Length == 0) + return OperationResult(Baya.Application.Models.Common.OperationResult.FailureResult("No file uploaded.")); + + using var buffer = new MemoryStream(); + await file.CopyToAsync(buffer, cancellationToken); + var command = new UploadCustomerAvatarCommand(buffer.ToArray(), file.ContentType, file.Length); + return OperationResult(await sender.Send(command, cancellationToken)); + } } diff --git a/server/src/API/Baya.Web.Api/Controllers/V1/NursePayoutsController.cs b/server/src/API/Baya.Web.Api/Controllers/V1/NursePayoutsController.cs index 43029ea..9322f7e 100644 --- a/server/src/API/Baya.Web.Api/Controllers/V1/NursePayoutsController.cs +++ b/server/src/API/Baya.Web.Api/Controllers/V1/NursePayoutsController.cs @@ -1,5 +1,8 @@ using System.ComponentModel.DataAnnotations; using Asp.Versioning; +using Baya.Application.Features.Payouts.Queries.GetNurseEarnings; +using Baya.Application.Features.Payouts.Queries.GetNurseEarningsBalance; +using Baya.Application.Features.Payouts.Queries.GetNursePayoutDetail; using Baya.Application.Features.Payouts.Queries.GetNursePayoutHistory; using Baya.Application.Models.Common; using Baya.Application.Models.Payouts; @@ -25,4 +28,22 @@ public sealed class NursePayoutsController(ISender sender) : BaseController [ProducesOkApiResponseType>] public async Task History([FromQuery] GetNursePayoutHistoryQuery query, CancellationToken cancellationToken) => OperationResult(await sender.Send(query, cancellationToken)); + + // The four-bucket balance + ledger-derived signed net payable (REQ-025). + [HttpGet("earnings_balance")] + [ProducesOkApiResponseType] + public async Task EarningsBalance(CancellationToken cancellationToken) + => OperationResult(await sender.Send(new GetNurseEarningsBalanceQuery(), cancellationToken)); + + // The per-booking earnings list with server-derived money-state, filterable by state (REQ-025). + [HttpGet("earnings")] + [ProducesOkApiResponseType>] + public async Task Earnings([FromQuery] GetNurseEarningsQuery query, CancellationToken cancellationToken) + => OperationResult(await sender.Send(query, cancellationToken)); + + // The nurse's own payout detail — batch window + covered bookings for reconciliation (REQ-025). + [HttpGet("{id:long}")] + [ProducesOkApiResponseType] + public async Task Detail(long id, CancellationToken cancellationToken) + => OperationResult(await sender.Send(new GetNursePayoutDetailQuery(id), cancellationToken)); } diff --git a/server/src/API/Baya.Web.Api/Controllers/V1/NurseProfilesController.cs b/server/src/API/Baya.Web.Api/Controllers/V1/NurseProfilesController.cs index 8df2257..58762b4 100644 --- a/server/src/API/Baya.Web.Api/Controllers/V1/NurseProfilesController.cs +++ b/server/src/API/Baya.Web.Api/Controllers/V1/NurseProfilesController.cs @@ -1,13 +1,16 @@ using System.ComponentModel.DataAnnotations; using Asp.Versioning; using Baya.Application.Features.Identity.Commands.SetNurseAcceptingBookings; +using Baya.Application.Features.Identity.Commands.UploadNurseAvatar; using Baya.Application.Features.Identity.Commands.UpsertNurseProfile; using Baya.Application.Features.Identity.Queries.GetMyNurseProfile; +using Baya.Application.Models.Common; using Baya.Application.Models.Identity; using Baya.WebFramework.Attributes; using Baya.WebFramework.BaseController; using Mediator; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Baya.Web.Api.Controllers.V1; @@ -33,4 +36,18 @@ public sealed class NurseProfilesController(ISender sender) : BaseController [ProducesOkApiResponseType] public async Task Me(CancellationToken cancellationToken) => OperationResult(await sender.Send(new GetMyNurseProfileQuery(), cancellationToken)); + + // Multipart image upload → stored via IObjectStorage; the returned URL is persisted on the profile. + [HttpPost("[action]")] + [ProducesOkApiResponseType] + public async Task Avatar(IFormFile file, CancellationToken cancellationToken) + { + if (file is null || file.Length == 0) + return OperationResult(Baya.Application.Models.Common.OperationResult.FailureResult("No file uploaded.")); + + using var buffer = new MemoryStream(); + await file.CopyToAsync(buffer, cancellationToken); + var command = new UploadNurseAvatarCommand(buffer.ToArray(), file.ContentType, file.Length); + return OperationResult(await sender.Send(command, cancellationToken)); + } } diff --git a/server/src/API/Baya.Web.Api/Controllers/V1/NurseVerificationController.cs b/server/src/API/Baya.Web.Api/Controllers/V1/NurseVerificationController.cs index 8573472..937058f 100644 --- a/server/src/API/Baya.Web.Api/Controllers/V1/NurseVerificationController.cs +++ b/server/src/API/Baya.Web.Api/Controllers/V1/NurseVerificationController.cs @@ -5,6 +5,7 @@ using Baya.Application.Features.Verification.Commands.RequestDocumentUploadUrl; using Baya.Application.Features.Verification.Commands.RunBankAccountVerification; using Baya.Application.Features.Verification.Commands.RunIdentityKyc; using Baya.Application.Features.Verification.Commands.RunShahkarMatch; +using Baya.Application.Features.Verification.Commands.SubmitCredentialDetails; using Baya.Application.Features.Verification.Commands.SubmitVerification; using Baya.Application.Features.Verification.Queries.GetStatus; using Baya.Application.Models.Verification; @@ -43,6 +44,13 @@ public sealed class NurseVerificationController(ISender sender) : BaseController public async Task ConfirmDocument(long stepId, ConfirmDocumentUploadCommand command, CancellationToken cancellationToken) => OperationResult(await sender.Send(command with { StepId = stepId }, cancellationToken)); + // Captures the structured credential fields (INO number, specialties, optional license details) B5 + // collects — the real path used to drop them silently. + [HttpPost("[action]")] + [ProducesOkApiResponseType] + public async Task CredentialDetails(SubmitCredentialDetailsCommand command, CancellationToken cancellationToken) + => OperationResult(await sender.Send(command, cancellationToken)); + [HttpPost("steps/identity_kyc/run")] [ProducesOkApiResponseType] public async Task RunIdentityKyc(RunIdentityKycCommand command, CancellationToken cancellationToken) diff --git a/server/src/API/Baya.Web.Api/Controllers/V1/NursesController.cs b/server/src/API/Baya.Web.Api/Controllers/V1/NursesController.cs index 744b932..b219a2e 100644 --- a/server/src/API/Baya.Web.Api/Controllers/V1/NursesController.cs +++ b/server/src/API/Baya.Web.Api/Controllers/V1/NursesController.cs @@ -1,8 +1,10 @@ using System.ComponentModel.DataAnnotations; using Asp.Versioning; +using Baya.Application.Features.Nurses.Queries.GetNursePublicProfile; using Baya.Application.Features.Reviews.Queries.GetTagAggregates; using Baya.Application.Features.Reviews.Queries.ListReviewsForNurse; using Baya.Application.Features.Verification.Queries.GetTrustBadge; +using Baya.Application.Models.Nurses; using Baya.Application.Models.Reviews; using Baya.Application.Models.Verification; using Baya.WebFramework.Attributes; @@ -26,6 +28,12 @@ public sealed class NursesController(ISender sender) : BaseController public async Task TrustBadge(long nurseId, CancellationToken cancellationToken) => OperationResult(await sender.Send(new GetVerifiedTrustBadgeQuery(nurseId), cancellationToken)); + // Public: the aggregated discovery detail (identity + aggregates + verification + services + latest review). + [HttpGet("{nurseId}/[action]")] + [ProducesOkApiResponseType] + public async Task Profile(long nurseId, CancellationToken cancellationToken) + => OperationResult(await sender.Send(new GetNursePublicProfileQuery(nurseId), cancellationToken)); + // Public: published reviews only (the publish gate is enforced in the query) + the cached rating aggregate. [HttpGet("{nurseProfileId}/reviews")] [ProducesOkApiResponseType] diff --git a/server/src/API/Baya.Web.Api/Controllers/V1/PatientCareRecordsController.cs b/server/src/API/Baya.Web.Api/Controllers/V1/PatientCareRecordsController.cs index fc8ed16..5371a08 100644 --- a/server/src/API/Baya.Web.Api/Controllers/V1/PatientCareRecordsController.cs +++ b/server/src/API/Baya.Web.Api/Controllers/V1/PatientCareRecordsController.cs @@ -1,8 +1,12 @@ using System.ComponentModel.DataAnnotations; using Asp.Versioning; +using Baya.Application.Features.PatientCareRecords.Commands.UpsertCarePlan; using Baya.Application.Features.PatientCareRecords.Commands.WritePatientCareRecord; +using Baya.Application.Features.PatientCareRecords.Queries.GetCarePlan; using Baya.Application.Features.PatientCareRecords.Queries.GetPatientHistory; +using Baya.Application.Features.PatientCareRecords.Queries.GetRecordAccess; using Baya.Application.Models.Common; +using Baya.Application.Models.Patients; using Baya.Application.Models.Reviews; using Baya.WebFramework.Attributes; using Baya.WebFramework.BaseController; @@ -29,13 +33,38 @@ public sealed class PatientCareRecordsController(ISender sender) : BaseControlle [ProducesOkApiResponseType] public async Task Write(long patientId, WriteCareRecordBody body, CancellationToken cancellationToken) => OperationResult(await sender.Send( - new WritePatientCareRecordCommand(patientId, body.BookingId, body.Body), cancellationToken)); + new WritePatientCareRecordCommand(patientId, body.BookingId, body.Body, body.TaskResults), cancellationToken)); [HttpGet("{patientId}/care_records")] [ProducesOkApiResponseType>] public async Task History(long patientId, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken cancellationToken = default) => OperationResult(await sender.Send(new GetPatientHistoryQuery(patientId, page, pageSize), cancellationToken)); + // The family-owned care plan (medications/routine/tasks) — read (owner/nurse/admin), REQ-027. + [HttpGet("{patientId}/care_record")] + [ProducesOkApiResponseType] + public async Task CarePlan(long patientId, CancellationToken cancellationToken) + => OperationResult(await sender.Send(new GetCarePlanQuery(patientId), cancellationToken)); + + // Replace the family-owned care plan (owning customer only), REQ-027. + [HttpPut("{patientId}/care_record")] + [ProducesOkApiResponseType] + public async Task UpsertCarePlan(long patientId, UpsertCarePlanBody body, CancellationToken cancellationToken) + => OperationResult(await sender.Send( + new UpsertCarePlanCommand(patientId, body.Medications, body.Routine, body.Tasks), cancellationToken)); + + // The caller's access to this patient's records (view/edit/append-note + non-leaking denied), REQ-027. + [HttpGet("{patientId}/record_access")] + [ProducesOkApiResponseType] + public async Task RecordAccess(long patientId, CancellationToken cancellationToken) + => OperationResult(await sender.Send(new GetRecordAccessQuery(patientId), cancellationToken)); + /// The care-record body (the patient id comes from the route). - public record WriteCareRecordBody(long? BookingId, string Body); + public record WriteCareRecordBody(long? BookingId, string Body, IReadOnlyList? TaskResults); + + /// The family care-plan body (the patient id comes from the route). + public record UpsertCarePlanBody( + IReadOnlyList? Medications, + IReadOnlyList? Routine, + IReadOnlyList? Tasks); } diff --git a/server/src/API/Baya.Web.Api/Controllers/V1/RefundsController.cs b/server/src/API/Baya.Web.Api/Controllers/V1/RefundsController.cs index d64a418..c7fc420 100644 --- a/server/src/API/Baya.Web.Api/Controllers/V1/RefundsController.cs +++ b/server/src/API/Baya.Web.Api/Controllers/V1/RefundsController.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using Asp.Versioning; +using Baya.Application.Features.Refunds.Queries.GetRefundByBooking; using Baya.Application.Features.Refunds.Queries.GetRefundStatus; using Baya.Application.Models.Refunds; using Baya.WebFramework.Attributes; @@ -23,4 +24,10 @@ public sealed class RefundsController(ISender sender) : BaseController [ProducesOkApiResponseType] public async Task Status(long id, CancellationToken cancellationToken) => OperationResult(await sender.Send(new GetRefundStatusQuery(id), cancellationToken)); + + // Reach the refund from its booking id (the id the customer holds) — 404 when none exists (REQ-021). + [HttpGet("by_booking/{bookingId}")] + [ProducesOkApiResponseType] + public async Task ByBooking(long bookingId, CancellationToken cancellationToken) + => OperationResult(await sender.Send(new GetRefundByBookingQuery(bookingId), cancellationToken)); } diff --git a/server/src/API/Baya.WebFramework/BaseController/BaseController.cs b/server/src/API/Baya.WebFramework/BaseController/BaseController.cs index f1be015..7709732 100644 --- a/server/src/API/Baya.WebFramework/BaseController/BaseController.cs +++ b/server/src/API/Baya.WebFramework/BaseController/BaseController.cs @@ -50,6 +50,14 @@ public class BaseController : ControllerBase return new JsonResult(new ApiResult(false, ApiResultStatusCode.Conflict, FirstErrorMessage(result))) { StatusCode = StatusCodes.Status409Conflict }; + // A coded failure (e.g. otp_locked) is written as the full envelope so the client sees the stable + // `code` (+ optional `data`), rather than the bare ModelState errors of an ordinary 400. + if (result.ErrorCode is not null) + return new JsonResult( + new ApiResult(false, ApiResultStatusCode.BadRequest, result.ErrorData, FirstErrorMessage(result)) + { Code = result.ErrorCode }) + { StatusCode = StatusCodes.Status400BadRequest }; + AddErrors(result); var badRequestErrors = new ValidationProblemDetails(ModelState); diff --git a/server/src/Core/Baya.Application/Common/AvatarUpload.cs b/server/src/Core/Baya.Application/Common/AvatarUpload.cs new file mode 100644 index 0000000..6484eef --- /dev/null +++ b/server/src/Core/Baya.Application/Common/AvatarUpload.cs @@ -0,0 +1,30 @@ +#nullable enable +namespace Baya.Application.Common; + +/// +/// Shared validation + storage-key rules for avatar (profile photo) uploads. Keeps the nurse and customer +/// upload handlers in lock-step on the accepted content types, the size cap, and the key scheme. +/// +public static class AvatarUpload +{ + public const long MaxSizeBytes = 5 * 1024 * 1024; + + private static readonly IReadOnlyDictionary AllowedTypes = new Dictionary + { + ["image/jpeg"] = ".jpg", + ["image/png"] = ".png", + ["image/webp"] = ".webp" + }; + + public static bool IsAllowedContentType(string? contentType) + => contentType is not null && AllowedTypes.ContainsKey(contentType.ToLowerInvariant()); + + public static bool IsWithinSizeLimit(long length) => length > 0 && length <= MaxSizeBytes; + + /// Opaque, collision-free storage key: avatars/{scope}/{ownerId}/{token}{ext}. + public static string BuildKey(string scope, long ownerId, string contentType, string token) + { + var ext = AllowedTypes.TryGetValue(contentType.ToLowerInvariant(), out var e) ? e : ".bin"; + return $"avatars/{scope}/{ownerId}/{token}{ext}"; + } +} diff --git a/server/src/Core/Baya.Application/Common/JsonCodeList.cs b/server/src/Core/Baya.Application/Common/JsonCodeList.cs new file mode 100644 index 0000000..b322913 --- /dev/null +++ b/server/src/Core/Baya.Application/Common/JsonCodeList.cs @@ -0,0 +1,29 @@ +using System.Text.Json; + +namespace Baya.Application.Common; + +/// +/// Shared (de)serialization for a small list of stable string codes persisted as a JSON string array +/// (e.g. patient conditions_json, nurse specializations_json). A malformed or empty value +/// parses to an empty list; an empty list serializes back to null (a clean NULL column). +/// +public static class JsonCodeList +{ + public static IReadOnlyList Parse(string json) + { + if (string.IsNullOrWhiteSpace(json)) + return []; + + try + { + return JsonSerializer.Deserialize>(json) ?? []; + } + catch (JsonException) + { + return []; + } + } + + public static string Serialize(IReadOnlyList codes) + => codes is null || codes.Count == 0 ? null : JsonSerializer.Serialize(codes); +} diff --git a/server/src/Core/Baya.Application/Contracts/Audit/IAuditLogger.cs b/server/src/Core/Baya.Application/Contracts/Audit/IAuditLogger.cs index 1dd2c93..e4b9828 100644 --- a/server/src/Core/Baya.Application/Contracts/Audit/IAuditLogger.cs +++ b/server/src/Core/Baya.Application/Contracts/Audit/IAuditLogger.cs @@ -19,8 +19,12 @@ public interface IAuditLogger CancellationToken cancellationToken = default); ValueTask> GetTrailAsync( - string entityType, - string entityId, + string? entityType, + string? entityId, + int? actorUserId, + string? action, + System.DateTimeOffset? from, + System.DateTimeOffset? to, int page, int pageSize, CancellationToken cancellationToken = default); diff --git a/server/src/Core/Baya.Application/Contracts/Persistence/IBnplRepository.cs b/server/src/Core/Baya.Application/Contracts/Persistence/IBnplRepository.cs index 7dcb341..fca54bc 100644 --- a/server/src/Core/Baya.Application/Contracts/Persistence/IBnplRepository.cs +++ b/server/src/Core/Baya.Application/Contracts/Persistence/IBnplRepository.cs @@ -40,4 +40,8 @@ public interface IBnplRepository /// The BNPL order view + the owning customer's user id for tenancy, and the linked refund's ETA /// when reverted. Null when absent. Task GetStatusAsync(long id, CancellationToken cancellationToken); + + /// The BNPL order for a booking request (the id the return-poll surface holds), latest first, with the + /// owning customer's user id for tenancy. Null when the request has no BNPL order. + Task GetStatusByRequestAsync(long bookingRequestId, CancellationToken cancellationToken); } diff --git a/server/src/Core/Baya.Application/Contracts/Persistence/IBookingRequestRepository.cs b/server/src/Core/Baya.Application/Contracts/Persistence/IBookingRequestRepository.cs index 49af701..89c02be 100644 --- a/server/src/Core/Baya.Application/Contracts/Persistence/IBookingRequestRepository.cs +++ b/server/src/Core/Baya.Application/Contracts/Persistence/IBookingRequestRepository.cs @@ -54,4 +54,9 @@ public interface IBookingRequestRepository /// The facts b10's InitiatePayment needs to validate a card attempt (owning customer, /// status, frozen payment window, gross to charge). NULL when absent. Task GetPaymentContextAsync(long id, CancellationToken cancellationToken); + + /// Owner-scoped read for the checkout summary — the request's schedule/participant labels + the + /// variant price/session-count needed to compute the money decomposition. NULL when the request is absent + /// or not the caller's (existence not leaked). + Task GetCheckoutContextAsync(long id, long customerId, CancellationToken cancellationToken); } diff --git a/server/src/Core/Baya.Application/Contracts/Persistence/INurseProfileRepository.cs b/server/src/Core/Baya.Application/Contracts/Persistence/INurseProfileRepository.cs index 65e5c8a..5a6ad69 100644 --- a/server/src/Core/Baya.Application/Contracts/Persistence/INurseProfileRepository.cs +++ b/server/src/Core/Baya.Application/Contracts/Persistence/INurseProfileRepository.cs @@ -1,5 +1,6 @@ #nullable enable using Baya.Application.Models.Identity; +using Baya.Application.Models.Nurses; using Baya.Domain.Entities.Identity; namespace Baya.Application.Contracts.Persistence; @@ -31,4 +32,9 @@ public interface INurseProfileRepository /// read — what a booking-request create needs to notify the nurse and run the bookability + same-gender /// checks. NULL when no such nurse profile exists. Task GetBookingContextByIdAsync(long nurseProfileId, CancellationToken cancellationToken); + + /// The aggregated public nurse profile for the discovery detail (C3): identity + aggregates + + /// verification signal + specialty chips + active bookable services + latest published review. NULL when + /// no such nurse profile exists. Exposes no encrypted credential number. + Task GetPublicProfileAsync(long nurseProfileId, CancellationToken cancellationToken); } diff --git a/server/src/Core/Baya.Application/Contracts/Persistence/IPatientCareRecordRepository.cs b/server/src/Core/Baya.Application/Contracts/Persistence/IPatientCareRecordRepository.cs index 5fc771a..95cffc8 100644 --- a/server/src/Core/Baya.Application/Contracts/Persistence/IPatientCareRecordRepository.cs +++ b/server/src/Core/Baya.Application/Contracts/Persistence/IPatientCareRecordRepository.cs @@ -1,6 +1,7 @@ #nullable enable using Baya.Application.Models.Common; using Baya.Application.Models.Reviews; +using Baya.Domain.Entities.Identity; using Baya.Domain.Entities.Reviews; namespace Baya.Application.Contracts.Persistence; @@ -25,4 +26,10 @@ public interface IPatientCareRecordRepository /// Patient-scoped longitudinal history, paginated, newest first — ciphertext bodies; the /// handler decrypts post-check. Task> GetPatientHistoryAsync(long patientId, int page, int pageSize, CancellationToken cancellationToken); + + /// The tracked family-owned care plan for a patient (for read + upsert). Null when none exists yet. + Task GetCarePlanAsync(long patientId, CancellationToken cancellationToken); + + /// Adds a new family-owned care plan (first PUT for a patient). + Task AddCarePlanAsync(PatientCarePlan plan, CancellationToken cancellationToken); } diff --git a/server/src/Core/Baya.Application/Contracts/Persistence/IPayoutRepository.cs b/server/src/Core/Baya.Application/Contracts/Persistence/IPayoutRepository.cs index b1cab37..47e1d5e 100644 --- a/server/src/Core/Baya.Application/Contracts/Persistence/IPayoutRepository.cs +++ b/server/src/Core/Baya.Application/Contracts/Persistence/IPayoutRepository.cs @@ -70,6 +70,18 @@ public interface IPayoutRepository /// The nurse's own payouts (tenancy-scoped), most recent first, projected + paginated. Masked IBAN. Task> GetNurseHistoryAsync(long nurseId, int page, int pageSize, CancellationToken cancellationToken); + + /// Every completed/closed booking the nurse earned, each with its server-derived money-state + /// (pending|eligible|paid|clawback_applied) from bookings.status + dispute_window_ends_at + /// + the payout link + any clawback. The handler filters by state, sums the buckets, and paginates. + Task> GetNurseEarningsAsync(long nurseId, DateTime now, CancellationToken cancellationToken); + + /// Lifetime total (IRR) of the nurse's paid payouts' net — the paidTotalIrr bucket. + Task GetPaidNetTotalAsync(long nurseId, CancellationToken cancellationToken); + + /// A nurse-scoped payout detail (payout + batch window + covered bookings). Null when the payout is + /// absent or not the nurse's — never leak another nurse's payout. + Task GetNursePayoutDetailAsync(long payoutId, long nurseId, CancellationToken cancellationToken); } /// The verified primary account a payout snapshots — the account id and the decrypted IBAN (frozen into diff --git a/server/src/Core/Baya.Application/Contracts/Persistence/IRefundRepository.cs b/server/src/Core/Baya.Application/Contracts/Persistence/IRefundRepository.cs index 87e0334..66308df 100644 --- a/server/src/Core/Baya.Application/Contracts/Persistence/IRefundRepository.cs +++ b/server/src/Core/Baya.Application/Contracts/Persistence/IRefundRepository.cs @@ -35,6 +35,10 @@ public interface IRefundRepository /// Null when absent. Task GetStatusAsync(long id, CancellationToken cancellationToken); + /// The customer-facing status of a booking's (latest) refund, with the owning customer's user id + /// for tenancy. Null when the booking has no refund. Lets the customer reach a refund from its booking. + Task GetStatusByBookingAsync(long bookingId, CancellationToken cancellationToken); + /// The provider revert reference on a bnpl_revert refund — the BNPL revert path records it /// as revert_transaction_id on the bnpl_transactions row. Null when absent. Task GetExternalRevertReferenceAsync(long refundId, CancellationToken cancellationToken); diff --git a/server/src/Core/Baya.Application/Contracts/Persistence/IReviewRepository.cs b/server/src/Core/Baya.Application/Contracts/Persistence/IReviewRepository.cs index 24fa5f1..2c2e42b 100644 --- a/server/src/Core/Baya.Application/Contracts/Persistence/IReviewRepository.cs +++ b/server/src/Core/Baya.Application/Contracts/Persistence/IReviewRepository.cs @@ -20,6 +20,10 @@ public interface IReviewRepository /// True if a (non-deleted) review already exists for the booking — the 1:1 pre-check. Task ExistsForBookingAsync(long bookingId, CancellationToken cancellationToken); + /// The caller's own review for a booking (with tags + moderation status) plus the owning + /// customer's user id for tenancy. Null when the booking has no review. + Task GetMyReviewForBookingAsync(long bookingId, CancellationToken cancellationToken); + /// Tracked review for a moderation transition. Null if absent. Task GetTrackedAsync(long reviewId, CancellationToken cancellationToken); diff --git a/server/src/Core/Baya.Application/Contracts/Persistence/ITicketRepository.cs b/server/src/Core/Baya.Application/Contracts/Persistence/ITicketRepository.cs index 7cad63b..01bec43 100644 --- a/server/src/Core/Baya.Application/Contracts/Persistence/ITicketRepository.cs +++ b/server/src/Core/Baya.Application/Contracts/Persistence/ITicketRepository.cs @@ -59,7 +59,11 @@ public interface ITicketRepository /// Paginated tickets the user participates in (active membership), filterable by status and /// reference code, newest first. - Task> ListMyTicketsAsync(int userId, string? status, string? referenceCode, int page, int pageSize, CancellationToken cancellationToken); + Task> ListMyTicketsAsync(int userId, string? status, string? referenceCode, long? bookingId, int page, int pageSize, CancellationToken cancellationToken); + + /// An existing message on the ticket carrying this client message id — the optimistic-send + /// idempotency lookup. Null when none (a fresh send). Ignores internal notes. + Task GetMessageByClientIdAsync(long ticketId, string clientMessageId, CancellationToken cancellationToken); /// The admin global queue — paginated, filter by status/category, search by reference code, optional /// booking/refund link, newest first. diff --git a/server/src/Core/Baya.Application/Contracts/Persistence/IUserAccountRepository.cs b/server/src/Core/Baya.Application/Contracts/Persistence/IUserAccountRepository.cs index 473bbc9..98f931c 100644 --- a/server/src/Core/Baya.Application/Contracts/Persistence/IUserAccountRepository.cs +++ b/server/src/Core/Baya.Application/Contracts/Persistence/IUserAccountRepository.cs @@ -10,6 +10,9 @@ public interface IUserAccountRepository /// The phone comes back decrypted and unmasked — masking is the handler's job. Task GetAccountSnapshotAsync(int userId, CancellationToken cancellationToken); + /// Tracked user row for an in-place edit of the base identity (e.g. name). + Task GetTrackedByIdAsync(int userId, CancellationToken cancellationToken); + Task GetRoleByNameAsync(string roleName, CancellationToken cancellationToken); /// Tracked user-role lookup that bypasses the revoked-filter, so a revoked grant can be diff --git a/server/src/Core/Baya.Application/Contracts/Persistence/IVerificationRepository.cs b/server/src/Core/Baya.Application/Contracts/Persistence/IVerificationRepository.cs index eba4101..78d8f2f 100644 --- a/server/src/Core/Baya.Application/Contracts/Persistence/IVerificationRepository.cs +++ b/server/src/Core/Baya.Application/Contracts/Persistence/IVerificationRepository.cs @@ -38,6 +38,10 @@ public interface IVerificationRepository Task AddDocumentAsync(VerificationDocument document, CancellationToken cancellationToken); Task AddCredentialAsync(NurseCredential credential, CancellationToken cancellationToken); + /// Tracked credential for the nurse of a given type — lets the nurse-facing credential-details + /// capture upsert (rather than duplicate) its registry row. Null when none exists yet. + Task GetTrackedCredentialAsync(long nurseId, string credentialType, CancellationToken cancellationToken); + // --- Projected reads --- Task GetStatusForNurseAsync(long nurseId, CancellationToken cancellationToken); Task> ListPendingStepsAsync( diff --git a/server/src/Core/Baya.Application/Features/Addresses/Commands/CreateAddress/CreateAddressCommand.Handler.cs b/server/src/Core/Baya.Application/Features/Addresses/Commands/CreateAddress/CreateAddressCommand.Handler.cs index abdeb76..6a9d3f4 100644 --- a/server/src/Core/Baya.Application/Features/Addresses/Commands/CreateAddress/CreateAddressCommand.Handler.cs +++ b/server/src/Core/Baya.Application/Features/Addresses/Commands/CreateAddress/CreateAddressCommand.Handler.cs @@ -42,7 +42,24 @@ internal sealed class CreateAddressCommandHandler( var isFirst = customerId is not { } existing || !await unitOfWork.CustomerAddressRepository.HasAnyAsync(existing, cancellationToken); var isPrimary = request.IsPrimary || isFirst; - var geo = await geocoder.GeocodeAsync(request.AddressLine, city.NameEn, district?.NameEn, cancellationToken); + // Prefer the client's dropped pin (the customer's exact door — best for the EVV distance check); + // fall back to the server geocode only when no pin was sent. + decimal? latitude; + decimal? longitude; + string geocodeSource; + if (request is { Latitude: { } pinLat, Longitude: { } pinLng }) + { + latitude = pinLat; + longitude = pinLng; + geocodeSource = GeocodeSources.UserPin; + } + else + { + var geo = await geocoder.GeocodeAsync(request.AddressLine, city.NameEn, district?.NameEn, cancellationToken); + latitude = geo.Latitude; + longitude = geo.Longitude; + geocodeSource = GeocodeSources.Geocoder; + } // Values are set as plaintext; the EF value converter encrypts the PII columns at rest. var address = new CustomerAddress @@ -54,8 +71,9 @@ internal sealed class CreateAddressCommandHandler( PostalCode = request.PostalCode, RecipientName = request.RecipientName, RecipientPhone = request.RecipientPhone, - Latitude = geo.Latitude, - Longitude = geo.Longitude, + Latitude = latitude, + Longitude = longitude, + GeocodeSource = geocodeSource, IsPrimary = isPrimary }; @@ -84,6 +102,7 @@ internal sealed class CreateAddressCommandHandler( new( address.Id, address.Title, + city.ProvinceId, city.Id, city.NameFa, city.NameEn, diff --git a/server/src/Core/Baya.Application/Features/Addresses/Commands/CreateAddress/CreateAddressCommand.Validator.cs b/server/src/Core/Baya.Application/Features/Addresses/Commands/CreateAddress/CreateAddressCommand.Validator.cs index f2ac32c..2a4c4c9 100644 --- a/server/src/Core/Baya.Application/Features/Addresses/Commands/CreateAddress/CreateAddressCommand.Validator.cs +++ b/server/src/Core/Baya.Application/Features/Addresses/Commands/CreateAddress/CreateAddressCommand.Validator.cs @@ -14,5 +14,10 @@ public sealed class CreateAddressCommandValidator : AbstractValidator !string.IsNullOrWhiteSpace(x.PostalCode)) .WithMessage("Postal code must be 10 digits."); + RuleFor(x => x.Latitude).InclusiveBetween(-90m, 90m).When(x => x.Latitude.HasValue); + RuleFor(x => x.Longitude).InclusiveBetween(-180m, 180m).When(x => x.Longitude.HasValue); + RuleFor(x => x) + .Must(x => x.Latitude.HasValue == x.Longitude.HasValue) + .WithMessage("Latitude and longitude must be supplied together."); } } diff --git a/server/src/Core/Baya.Application/Features/Addresses/Commands/CreateAddress/CreateAddressCommand.cs b/server/src/Core/Baya.Application/Features/Addresses/Commands/CreateAddress/CreateAddressCommand.cs index 000d5f5..cc4e7f2 100644 --- a/server/src/Core/Baya.Application/Features/Addresses/Commands/CreateAddress/CreateAddressCommand.cs +++ b/server/src/Core/Baya.Application/Features/Addresses/Commands/CreateAddress/CreateAddressCommand.cs @@ -17,4 +17,6 @@ public record CreateAddressCommand( string PostalCode, string RecipientName, string RecipientPhone, - bool IsPrimary) : IRequest>; + bool IsPrimary, + decimal? Latitude = null, + decimal? Longitude = null) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Addresses/Commands/UpdateAddress/UpdateAddressCommand.Handler.cs b/server/src/Core/Baya.Application/Features/Addresses/Commands/UpdateAddress/UpdateAddressCommand.Handler.cs index 41754b4..091a810 100644 --- a/server/src/Core/Baya.Application/Features/Addresses/Commands/UpdateAddress/UpdateAddressCommand.Handler.cs +++ b/server/src/Core/Baya.Application/Features/Addresses/Commands/UpdateAddress/UpdateAddressCommand.Handler.cs @@ -58,11 +58,19 @@ internal sealed class UpdateAddressCommandHandler( address.RecipientName = request.RecipientName; address.RecipientPhone = request.RecipientPhone; - if (locationChanged) + // A freshly-dropped pin always wins; otherwise re-geocode only when the location text changed. + if (request is { Latitude: { } pinLat, Longitude: { } pinLng }) + { + address.Latitude = pinLat; + address.Longitude = pinLng; + address.GeocodeSource = GeocodeSources.UserPin; + } + else if (locationChanged) { var geo = await geocoder.GeocodeAsync(request.AddressLine, city.NameEn, district?.NameEn, cancellationToken); address.Latitude = geo.Latitude; address.Longitude = geo.Longitude; + address.GeocodeSource = GeocodeSources.Geocoder; } await unitOfWork.CommitAsync(); @@ -70,6 +78,7 @@ internal sealed class UpdateAddressCommandHandler( return OperationResult.SuccessResult(new CustomerAddressDto( address.Id, address.Title, + city.ProvinceId, city.Id, city.NameFa, city.NameEn, diff --git a/server/src/Core/Baya.Application/Features/Addresses/Commands/UpdateAddress/UpdateAddressCommand.Validator.cs b/server/src/Core/Baya.Application/Features/Addresses/Commands/UpdateAddress/UpdateAddressCommand.Validator.cs index b3de833..5775430 100644 --- a/server/src/Core/Baya.Application/Features/Addresses/Commands/UpdateAddress/UpdateAddressCommand.Validator.cs +++ b/server/src/Core/Baya.Application/Features/Addresses/Commands/UpdateAddress/UpdateAddressCommand.Validator.cs @@ -14,5 +14,10 @@ public sealed class UpdateAddressCommandValidator : AbstractValidator !string.IsNullOrWhiteSpace(x.PostalCode)) .WithMessage("Postal code must be 10 digits."); + RuleFor(x => x.Latitude).InclusiveBetween(-90m, 90m).When(x => x.Latitude.HasValue); + RuleFor(x => x.Longitude).InclusiveBetween(-180m, 180m).When(x => x.Longitude.HasValue); + RuleFor(x => x) + .Must(x => x.Latitude.HasValue == x.Longitude.HasValue) + .WithMessage("Latitude and longitude must be supplied together."); } } diff --git a/server/src/Core/Baya.Application/Features/Addresses/Commands/UpdateAddress/UpdateAddressCommand.cs b/server/src/Core/Baya.Application/Features/Addresses/Commands/UpdateAddress/UpdateAddressCommand.cs index c9beda9..d4a5d14 100644 --- a/server/src/Core/Baya.Application/Features/Addresses/Commands/UpdateAddress/UpdateAddressCommand.cs +++ b/server/src/Core/Baya.Application/Features/Addresses/Commands/UpdateAddress/UpdateAddressCommand.cs @@ -14,4 +14,6 @@ public record UpdateAddressCommand( string AddressLine, string PostalCode, string RecipientName, - string RecipientPhone) : IRequest>; + string RecipientPhone, + decimal? Latitude = null, + decimal? Longitude = null) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Audit/Queries/GetAuditTrail/GetAuditTrailQuery.Handler.cs b/server/src/Core/Baya.Application/Features/Audit/Queries/GetAuditTrail/GetAuditTrailQuery.Handler.cs index 055a72c..88ba109 100644 --- a/server/src/Core/Baya.Application/Features/Audit/Queries/GetAuditTrail/GetAuditTrailQuery.Handler.cs +++ b/server/src/Core/Baya.Application/Features/Audit/Queries/GetAuditTrail/GetAuditTrailQuery.Handler.cs @@ -12,7 +12,9 @@ internal sealed class GetAuditTrailQueryHandler(IAuditLogger auditLogger) public async ValueTask>> Handle(GetAuditTrailQuery request, CancellationToken cancellationToken) { var (page, pageSize) = Pagination.Normalize(request.Page, request.PageSize); - var result = await auditLogger.GetTrailAsync(request.EntityType, request.EntityId, page, pageSize, cancellationToken); + var result = await auditLogger.GetTrailAsync( + request.EntityType, request.EntityId, request.ActorId, request.Action, request.From, request.To, + page, pageSize, cancellationToken); return OperationResult>.SuccessResult(result); } } diff --git a/server/src/Core/Baya.Application/Features/Audit/Queries/GetAuditTrail/GetAuditTrailQuery.cs b/server/src/Core/Baya.Application/Features/Audit/Queries/GetAuditTrail/GetAuditTrailQuery.cs index c6cdcc5..7b685e2 100644 --- a/server/src/Core/Baya.Application/Features/Audit/Queries/GetAuditTrail/GetAuditTrailQuery.cs +++ b/server/src/Core/Baya.Application/Features/Audit/Queries/GetAuditTrail/GetAuditTrailQuery.cs @@ -1,8 +1,16 @@ +using System; using Baya.Application.Models.Audit; using Baya.Application.Models.Common; using Mediator; namespace Baya.Application.Features.Audit.Queries.GetAuditTrail; -public record GetAuditTrailQuery(string EntityType, string EntityId, int Page = 1, int PageSize = 50) - : IRequest>>; +public record GetAuditTrailQuery( + string? EntityType = null, + string? EntityId = null, + int? ActorId = null, + string? Action = null, + DateTimeOffset? From = null, + DateTimeOffset? To = null, + int Page = 1, + int PageSize = 50) : IRequest>>; diff --git a/server/src/Core/Baya.Application/Features/Bnpl/Commands/SettleBnplOrder/SettleBnplOrderCommand.Handler.cs b/server/src/Core/Baya.Application/Features/Bnpl/Commands/SettleBnplOrder/SettleBnplOrderCommand.Handler.cs index dc03313..43b1160 100644 --- a/server/src/Core/Baya.Application/Features/Bnpl/Commands/SettleBnplOrder/SettleBnplOrderCommand.Handler.cs +++ b/server/src/Core/Baya.Application/Features/Bnpl/Commands/SettleBnplOrder/SettleBnplOrderCommand.Handler.cs @@ -124,8 +124,13 @@ internal sealed class SettleBnplOrderCommandHandler( // Open the booking-coordination ticket once the booking is confirmed (idempotent, one per booking) — b15. if (conversion.Created) + { await sender.Send(new Messaging.Commands.AutoCreateCoordinationTicket.AutoCreateCoordinationTicketCommand(conversion.BookingId), cancellationToken); + // Auto-issue the commission invoice so the customer can reach it right after settlement (idempotent). + await sender.Send(new Invoices.Commands.IssueInvoice.IssueInvoiceCommand(conversion.BookingId), cancellationToken); + } + return OperationResult.SuccessResult(true); } diff --git a/server/src/Core/Baya.Application/Features/Bnpl/Queries/CheckBnplEligibility/CheckBnplEligibilityQuery.Handler.cs b/server/src/Core/Baya.Application/Features/Bnpl/Queries/CheckBnplEligibility/CheckBnplEligibilityQuery.Handler.cs index 34c88bc..c8d2f09 100644 --- a/server/src/Core/Baya.Application/Features/Bnpl/Queries/CheckBnplEligibility/CheckBnplEligibilityQuery.Handler.cs +++ b/server/src/Core/Baya.Application/Features/Bnpl/Queries/CheckBnplEligibility/CheckBnplEligibilityQuery.Handler.cs @@ -46,7 +46,10 @@ internal sealed class CheckBnplEligibilityQueryHandler( if (gatewayId is null) return OperationResult.FailureResult("No active BNPL gateway is configured."); - var eligibility = await provider.CheckEligibilityAsync(ctx.CustomerMobile, ctx.GrossIrr, cancellationToken); + // The D3 inputs (national id / mobile / consent) feed the provider credit inquiry; the mock uses only the + // mobile today (the KYC step is deferred). Prefer the supplied mobile, else the account mobile. + var inquiryMobile = string.IsNullOrWhiteSpace(request.Mobile) ? ctx.CustomerMobile : request.Mobile; + var eligibility = await provider.CheckEligibilityAsync(inquiryMobile, ctx.GrossIrr, cancellationToken); var merchantOfRecord = await platformConfig.GetConfig("bnpl_merchant_of_record", cancellationToken); var bnpl = await BnplOrderInitializer.EnsureAsync( diff --git a/server/src/Core/Baya.Application/Features/Bnpl/Queries/CheckBnplEligibility/CheckBnplEligibilityQuery.Validator.cs b/server/src/Core/Baya.Application/Features/Bnpl/Queries/CheckBnplEligibility/CheckBnplEligibilityQuery.Validator.cs index e3e6b21..af4a3c6 100644 --- a/server/src/Core/Baya.Application/Features/Bnpl/Queries/CheckBnplEligibility/CheckBnplEligibilityQuery.Validator.cs +++ b/server/src/Core/Baya.Application/Features/Bnpl/Queries/CheckBnplEligibility/CheckBnplEligibilityQuery.Validator.cs @@ -11,6 +11,11 @@ public sealed class CheckBnplEligibilityQueryValidator : AbstractValidator x.ProviderCode) .NotEmpty() .Must(BnplProviderCodes.IsKnown) - .WithMessage("provider_code must be one of snapppay, digipay, tara, torobpay."); + .WithMessage("provider_code must be one of snapppay, digipay, tara, torobpay, balinyaar."); + // The D3 credit inquiry is consented: when national id / mobile are supplied, consent must be granted. + RuleFor(x => x.Consent) + .Equal(true) + .When(x => !string.IsNullOrWhiteSpace(x.NationalId) || !string.IsNullOrWhiteSpace(x.Mobile)) + .WithMessage("Consent is required to run the credit inquiry."); } } diff --git a/server/src/Core/Baya.Application/Features/Bnpl/Queries/CheckBnplEligibility/CheckBnplEligibilityQuery.cs b/server/src/Core/Baya.Application/Features/Bnpl/Queries/CheckBnplEligibility/CheckBnplEligibilityQuery.cs index 686e0d6..79f98ce 100644 --- a/server/src/Core/Baya.Application/Features/Bnpl/Queries/CheckBnplEligibility/CheckBnplEligibilityQuery.cs +++ b/server/src/Core/Baya.Application/Features/Bnpl/Queries/CheckBnplEligibility/CheckBnplEligibilityQuery.cs @@ -11,6 +11,13 @@ namespace Baya.Application.Features.Bnpl.Queries.CheckBnplEligibility; /// client shows the plan summary on eligible or falls back to card. Owned by the requesting customer. /// /// The accepted request to finance (a b9 bookings row exists only on settle). -/// Which BNPL provider to check (snapppay/digipay/tara/torobpay). -public record CheckBnplEligibilityQuery(long BookingRequestId, string ProviderCode) - : IRequest>; +/// Which BNPL provider to check (snapppay/digipay/tara/torobpay/balinyaar). +/// The D3 credit-inquiry national id (optional; used for the provider KYC inquiry). +/// The D3 credit-inquiry mobile (optional; falls back to the account mobile). +/// The D3 consent checkbox — must be true when the KYC inputs are supplied. +public record CheckBnplEligibilityQuery( + long BookingRequestId, + string ProviderCode, + string NationalId = null, + string Mobile = null, + bool? Consent = null) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Bnpl/Queries/GetBnplOrderByRequest/GetBnplOrderByRequestQuery.Handler.cs b/server/src/Core/Baya.Application/Features/Bnpl/Queries/GetBnplOrderByRequest/GetBnplOrderByRequestQuery.Handler.cs new file mode 100644 index 0000000..ab5131a --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Bnpl/Queries/GetBnplOrderByRequest/GetBnplOrderByRequestQuery.Handler.cs @@ -0,0 +1,25 @@ +#nullable enable +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Bnpl; +using Baya.Application.Models.Common; +using Mediator; + +namespace Baya.Application.Features.Bnpl.Queries.GetBnplOrderByRequest; + +internal sealed class GetBnplOrderByRequestQueryHandler( + ICurrentUser currentUser, + IUnitOfWork unitOfWork) + : IRequestHandler> +{ + public async ValueTask> Handle(GetBnplOrderByRequestQuery request, CancellationToken cancellationToken) + { + var projection = await unitOfWork.BnplRepository.GetStatusByRequestAsync(request.BookingRequestId, cancellationToken); + + // Cross-customer access is indistinguishable from "no order" — never leak another customer's order. + if (projection is null || projection.CustomerUserId != currentUser.UserId) + return OperationResult.NotFoundResult("BNPL order not found."); + + return OperationResult.SuccessResult(projection.Order); + } +} diff --git a/server/src/Core/Baya.Application/Features/Bnpl/Queries/GetBnplOrderByRequest/GetBnplOrderByRequestQuery.cs b/server/src/Core/Baya.Application/Features/Bnpl/Queries/GetBnplOrderByRequest/GetBnplOrderByRequestQuery.cs new file mode 100644 index 0000000..4f43379 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Bnpl/Queries/GetBnplOrderByRequest/GetBnplOrderByRequestQuery.cs @@ -0,0 +1,9 @@ +using Baya.Application.Models.Bnpl; +using Baya.Application.Models.Common; +using Mediator; + +namespace Baya.Application.Features.Bnpl.Queries.GetBnplOrderByRequest; + +/// Reads the BNPL order for a booking request (the id the return-poll surface holds, not the order id), +/// owner-scoped. 404 when the request has no BNPL order. +public record GetBnplOrderByRequestQuery(long BookingRequestId) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Booking/BookingRequestMapper.cs b/server/src/Core/Baya.Application/Features/Booking/BookingRequestMapper.cs index 2c6770f..dc35dcf 100644 --- a/server/src/Core/Baya.Application/Features/Booking/BookingRequestMapper.cs +++ b/server/src/Core/Baya.Application/Features/Booking/BookingRequestMapper.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Globalization; using Baya.Application.Models.Booking; namespace Baya.Application.Features.Booking; @@ -43,5 +44,8 @@ internal static class BookingRequestMapper p.NurseResponseDeadlineAt, p.PaymentDeadlineAt, p.NurseRejectionReason, - p.CreatedAt); + p.CreatedAt, + p.VariantPrice.ToString(CultureInfo.InvariantCulture), + p.NurseAvatarUrl, + p.BookingId); } diff --git a/server/src/Core/Baya.Application/Features/Booking/Queries/GetCheckoutSummary/GetCheckoutSummaryQuery.Handler.cs b/server/src/Core/Baya.Application/Features/Booking/Queries/GetCheckoutSummary/GetCheckoutSummaryQuery.Handler.cs new file mode 100644 index 0000000..83709a6 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Booking/Queries/GetCheckoutSummary/GetCheckoutSummaryQuery.Handler.cs @@ -0,0 +1,73 @@ +#nullable enable +using System.Globalization; +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Configuration; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Booking; +using Baya.Application.Models.Common; +using Baya.Domain.Entities.Booking; +using Baya.Domain.Entities.User; +using Mediator; + +namespace Baya.Application.Features.Booking.Queries.GetCheckoutSummary; + +internal sealed class GetCheckoutSummaryQueryHandler( + ICurrentUser currentUser, + IUnitOfWork unitOfWork, + IPlatformConfig platformConfig) + : IRequestHandler> +{ + public async ValueTask> Handle(GetCheckoutSummaryQuery request, CancellationToken cancellationToken) + { + if (currentUser.UserId is not { } userId) + return OperationResult.UnauthorizedResult("Not authenticated."); + + if (currentUser.Roles?.Contains(RoleNames.Customer) != true) + return OperationResult.ForbiddenResult("Only a customer can read a checkout summary."); + + var customerId = await unitOfWork.CustomerProfileRepository.GetProfileIdByUserIdAsync(userId, cancellationToken); + if (customerId is not { } cid) + return OperationResult.NotFoundResult("Booking request not found."); + + var ctx = await unitOfWork.BookingRequestRepository.GetCheckoutContextAsync(request.BookingRequestId, cid, cancellationToken); + if (ctx is null) + return OperationResult.NotFoundResult("Booking request not found."); + + var sessions = ctx.SessionCount is > 0 ? ctx.SessionCount.Value : 1; + var gross = ctx.VariantPrice * sessions; + + var feeRate = await platformConfig.GetConfig("platform_fee_rate", cancellationToken); + var vatRate = await platformConfig.GetConfig("vat_rate", cancellationToken); + + // b10 split (VAT-inclusive commission), then carve VAT out of the commission so the display + // decomposition reconciles to the captured total: serviceCost + commissionNet + vat = gross. + var (commission, payout) = BookingAmounts.Split(gross, feeRate); + var commissionNet = (long)decimal.Round(commission / (1 + vatRate), MidpointRounding.AwayFromZero); + var vat = commission - commissionNet; + + var dto = new CheckoutSummaryDto( + ctx.Id, + ctx.Status, + ctx.NurseName, + ctx.PatientName, + ctx.VariantLabel, + ctx.VariantPriceUnit, + ctx.SessionCount, + ctx.RequestedDate, + ctx.RequestedTimeStart, + ctx.RequestedTimeEnd, + ctx.PaymentDeadlineAt, + Str(payout), + Str(commissionNet), + Str(vat), + vatRate, + Str(gross), + Str(gross), + Str(commission), + Str(payout)); + + return OperationResult.SuccessResult(dto); + } + + private static string Str(long value) => value.ToString(CultureInfo.InvariantCulture); +} diff --git a/server/src/Core/Baya.Application/Features/Booking/Queries/GetCheckoutSummary/GetCheckoutSummaryQuery.cs b/server/src/Core/Baya.Application/Features/Booking/Queries/GetCheckoutSummary/GetCheckoutSummaryQuery.cs new file mode 100644 index 0000000..4b93653 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Booking/Queries/GetCheckoutSummary/GetCheckoutSummaryQuery.cs @@ -0,0 +1,12 @@ +using Baya.Application.Models.Booking; +using Baya.Application.Models.Common; +using Mediator; + +namespace Baya.Application.Features.Booking.Queries.GetCheckoutSummary; + +/// +/// Owner-scoped checkout money summary for a booking request (C6). Serves the reconciling gross / commission +/// (net of VAT) / VAT / total decomposition computed server-side from config; the client renders, never +/// derives. A foreign or absent request is a clean not-found. +/// +public record GetCheckoutSummaryQuery(long BookingRequestId) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Identity/Commands/CreatePatient/CreatePatientCommand.Handler.cs b/server/src/Core/Baya.Application/Features/Identity/Commands/CreatePatient/CreatePatientCommand.Handler.cs index 487e0ea..8086220 100644 --- a/server/src/Core/Baya.Application/Features/Identity/Commands/CreatePatient/CreatePatientCommand.Handler.cs +++ b/server/src/Core/Baya.Application/Features/Identity/Commands/CreatePatient/CreatePatientCommand.Handler.cs @@ -1,4 +1,5 @@ #nullable enable +using Baya.Application.Common; using Baya.Application.Contracts.Common; using Baya.Application.Contracts.Persistence; using Baya.Application.Models.Common; @@ -29,6 +30,8 @@ internal sealed class CreatePatientCommandHandler(ICurrentUser currentUser, IUni Gender = request.Gender, BloodType = request.BloodType, InitialMedicalNotes = request.InitialMedicalNotes, + Relation = request.Relation, + ConditionsJson = JsonCodeList.Serialize(request.Conditions), IsActive = true }; @@ -58,6 +61,8 @@ internal sealed class CreatePatientCommandHandler(ICurrentUser currentUser, IUni patient.Gender, patient.BloodType, patient.InitialMedicalNotes, - patient.IsActive)); + patient.IsActive, + patient.Relation, + JsonCodeList.Parse(patient.ConditionsJson))); } } diff --git a/server/src/Core/Baya.Application/Features/Identity/Commands/CreatePatient/CreatePatientCommand.Validator.cs b/server/src/Core/Baya.Application/Features/Identity/Commands/CreatePatient/CreatePatientCommand.Validator.cs index 47ce234..7245040 100644 --- a/server/src/Core/Baya.Application/Features/Identity/Commands/CreatePatient/CreatePatientCommand.Validator.cs +++ b/server/src/Core/Baya.Application/Features/Identity/Commands/CreatePatient/CreatePatientCommand.Validator.cs @@ -17,5 +17,11 @@ public sealed class CreatePatientCommandValidator : AbstractValidator x.Relation) + .Must(PatientRules.IsValidRelation) + .WithMessage("Relation must be one of parent, spouse, child, self."); + RuleFor(x => x.Conditions) + .Must(PatientRules.AreValidConditions) + .WithMessage("Each condition must be a non-empty code up to 40 characters."); } } diff --git a/server/src/Core/Baya.Application/Features/Identity/Commands/CreatePatient/CreatePatientCommand.cs b/server/src/Core/Baya.Application/Features/Identity/Commands/CreatePatient/CreatePatientCommand.cs index 41c898b..dcda340 100644 --- a/server/src/Core/Baya.Application/Features/Identity/Commands/CreatePatient/CreatePatientCommand.cs +++ b/server/src/Core/Baya.Application/Features/Identity/Commands/CreatePatient/CreatePatientCommand.cs @@ -16,4 +16,6 @@ public record CreatePatientCommand( DateOnly BirthDate, string Gender, string BloodType, - string InitialMedicalNotes) : IRequest>; + string InitialMedicalNotes, + string Relation = null, + IReadOnlyList Conditions = null) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Identity/Commands/RequestOtp/RequestOtpCommand.Handler.cs b/server/src/Core/Baya.Application/Features/Identity/Commands/RequestOtp/RequestOtpCommand.Handler.cs index 3d21820..1446b2c 100644 --- a/server/src/Core/Baya.Application/Features/Identity/Commands/RequestOtp/RequestOtpCommand.Handler.cs +++ b/server/src/Core/Baya.Application/Features/Identity/Commands/RequestOtp/RequestOtpCommand.Handler.cs @@ -33,7 +33,8 @@ internal sealed class RequestOtpCommandHandler( var now = clock.UtcNow; if (windowEndsAt > now) return OperationResult.SuccessResult( - new RequestOtpResult(false, (int)Math.Ceiling((windowEndsAt - now).TotalSeconds))); + new RequestOtpResult(false, (int)Math.Ceiling((windowEndsAt - now).TotalSeconds), + IdentityDefaults.OtpCodeLength, IdentityDefaults.OtpExpirySeconds)); var user = await userManager.GetUserByPhoneNumber(phone); if (user is null) @@ -64,6 +65,7 @@ internal sealed class RequestOtpCommandHandler( await cache.SetAsync(resendKey, now.AddSeconds(resendSeconds), TimeSpan.FromSeconds(resendSeconds), cancellationToken); - return OperationResult.SuccessResult(new RequestOtpResult(true, resendSeconds)); + return OperationResult.SuccessResult( + new RequestOtpResult(true, resendSeconds, IdentityDefaults.OtpCodeLength, IdentityDefaults.OtpExpirySeconds)); } } \ No newline at end of file diff --git a/server/src/Core/Baya.Application/Features/Identity/Commands/UpdatePatient/UpdatePatientCommand.Handler.cs b/server/src/Core/Baya.Application/Features/Identity/Commands/UpdatePatient/UpdatePatientCommand.Handler.cs index e3c569f..2cab865 100644 --- a/server/src/Core/Baya.Application/Features/Identity/Commands/UpdatePatient/UpdatePatientCommand.Handler.cs +++ b/server/src/Core/Baya.Application/Features/Identity/Commands/UpdatePatient/UpdatePatientCommand.Handler.cs @@ -1,4 +1,5 @@ #nullable enable +using Baya.Application.Common; using Baya.Application.Contracts.Common; using Baya.Application.Contracts.Persistence; using Baya.Application.Models.Common; @@ -34,6 +35,8 @@ internal sealed class UpdatePatientCommandHandler(ICurrentUser currentUser, IUni patient.Gender = request.Gender; patient.BloodType = request.BloodType; patient.InitialMedicalNotes = request.InitialMedicalNotes; + patient.Relation = request.Relation; + patient.ConditionsJson = JsonCodeList.Serialize(request.Conditions); await unitOfWork.CommitAsync(); @@ -46,6 +49,8 @@ internal sealed class UpdatePatientCommandHandler(ICurrentUser currentUser, IUni patient.Gender, patient.BloodType, patient.InitialMedicalNotes, - patient.IsActive)); + patient.IsActive, + patient.Relation, + JsonCodeList.Parse(patient.ConditionsJson))); } } diff --git a/server/src/Core/Baya.Application/Features/Identity/Commands/UpdatePatient/UpdatePatientCommand.Validator.cs b/server/src/Core/Baya.Application/Features/Identity/Commands/UpdatePatient/UpdatePatientCommand.Validator.cs index cc15bcc..c7c01cd 100644 --- a/server/src/Core/Baya.Application/Features/Identity/Commands/UpdatePatient/UpdatePatientCommand.Validator.cs +++ b/server/src/Core/Baya.Application/Features/Identity/Commands/UpdatePatient/UpdatePatientCommand.Validator.cs @@ -18,5 +18,11 @@ public sealed class UpdatePatientCommandValidator : AbstractValidator x.Relation) + .Must(PatientRules.IsValidRelation) + .WithMessage("Relation must be one of parent, spouse, child, self."); + RuleFor(x => x.Conditions) + .Must(PatientRules.AreValidConditions) + .WithMessage("Each condition must be a non-empty code up to 40 characters."); } } diff --git a/server/src/Core/Baya.Application/Features/Identity/Commands/UpdatePatient/UpdatePatientCommand.cs b/server/src/Core/Baya.Application/Features/Identity/Commands/UpdatePatient/UpdatePatientCommand.cs index 8aaaef3..452480e 100644 --- a/server/src/Core/Baya.Application/Features/Identity/Commands/UpdatePatient/UpdatePatientCommand.cs +++ b/server/src/Core/Baya.Application/Features/Identity/Commands/UpdatePatient/UpdatePatientCommand.cs @@ -13,4 +13,6 @@ public record UpdatePatientCommand( DateOnly BirthDate, string Gender, string BloodType, - string InitialMedicalNotes) : IRequest>; + string InitialMedicalNotes, + string Relation = null, + IReadOnlyList Conditions = null) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Identity/Commands/UploadCustomerAvatar/UploadCustomerAvatarCommand.Handler.cs b/server/src/Core/Baya.Application/Features/Identity/Commands/UploadCustomerAvatar/UploadCustomerAvatarCommand.Handler.cs new file mode 100644 index 0000000..845f07e --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Identity/Commands/UploadCustomerAvatar/UploadCustomerAvatarCommand.Handler.cs @@ -0,0 +1,53 @@ +#nullable enable +using Baya.Application.Common; +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Common; +using Baya.Application.Models.Identity; +using Baya.Domain.Entities.Identity; +using Baya.Domain.Entities.User; +using Mediator; + +namespace Baya.Application.Features.Identity.Commands.UploadCustomerAvatar; + +internal sealed class UploadCustomerAvatarCommandHandler( + ICurrentUser currentUser, + IUnitOfWork unitOfWork, + IObjectStorage objectStorage) + : IRequestHandler> +{ + public async ValueTask> Handle(UploadCustomerAvatarCommand request, CancellationToken cancellationToken) + { + if (currentUser.UserId is not { } userId) + return OperationResult.UnauthorizedResult("Not authenticated."); + + if (currentUser.Roles?.Contains(RoleNames.Customer) != true) + return OperationResult.ForbiddenResult("Only a customer can upload a customer avatar."); + + if (!AvatarUpload.IsAllowedContentType(request.ContentType)) + return OperationResult.FailureResult("Unsupported image type. Use JPEG, PNG, or WebP."); + + if (!AvatarUpload.IsWithinSizeLimit(request.Length)) + return OperationResult.FailureResult("Image is empty or exceeds the 5 MB limit."); + + var profile = await unitOfWork.CustomerProfileRepository.GetByUserIdAsync(userId, cancellationToken); + if (profile is null) + { + // Mirror the customer-profile upsert: provision the thin payer row so an avatar-first customer + // still gets a profile to attach the URL to. + profile = new CustomerProfile { UserId = userId }; + await unitOfWork.CustomerProfileRepository.AddAsync(profile, cancellationToken); + await unitOfWork.CommitAsync(); + } + + var key = AvatarUpload.BuildKey("customer", profile.Id, request.ContentType, Guid.NewGuid().ToString("N")); + using (var stream = new MemoryStream(request.Content)) + await objectStorage.PutAsync(key, stream, request.ContentType, cancellationToken); + + var url = objectStorage.GetUrl(key); + profile.AvatarUrl = url; + await unitOfWork.CommitAsync(); + + return OperationResult.SuccessResult(new AvatarUploadResult(url)); + } +} diff --git a/server/src/Core/Baya.Application/Features/Identity/Commands/UploadCustomerAvatar/UploadCustomerAvatarCommand.cs b/server/src/Core/Baya.Application/Features/Identity/Commands/UploadCustomerAvatar/UploadCustomerAvatarCommand.cs new file mode 100644 index 0000000..6bf3db1 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Identity/Commands/UploadCustomerAvatar/UploadCustomerAvatarCommand.cs @@ -0,0 +1,14 @@ +using Baya.Application.Models.Common; +using Baya.Application.Models.Identity; +using Mediator; + +namespace Baya.Application.Features.Identity.Commands.UploadCustomerAvatar; + +/// +/// Stores the signed-in customer's profile photo via IObjectStorage and persists the returned URL on +/// the customer profile. The controller reads the multipart file; the command carries the bytes + metadata. +/// +public record UploadCustomerAvatarCommand( + byte[] Content, + string ContentType, + long Length) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Identity/Commands/UploadNurseAvatar/UploadNurseAvatarCommand.Handler.cs b/server/src/Core/Baya.Application/Features/Identity/Commands/UploadNurseAvatar/UploadNurseAvatarCommand.Handler.cs new file mode 100644 index 0000000..6ed8623 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Identity/Commands/UploadNurseAvatar/UploadNurseAvatarCommand.Handler.cs @@ -0,0 +1,46 @@ +#nullable enable +using Baya.Application.Common; +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Common; +using Baya.Application.Models.Identity; +using Baya.Domain.Entities.User; +using Mediator; + +namespace Baya.Application.Features.Identity.Commands.UploadNurseAvatar; + +internal sealed class UploadNurseAvatarCommandHandler( + ICurrentUser currentUser, + IUnitOfWork unitOfWork, + IObjectStorage objectStorage) + : IRequestHandler> +{ + public async ValueTask> Handle(UploadNurseAvatarCommand request, CancellationToken cancellationToken) + { + if (currentUser.UserId is not { } userId) + return OperationResult.UnauthorizedResult("Not authenticated."); + + if (currentUser.Roles?.Contains(RoleNames.Nurse) != true) + return OperationResult.ForbiddenResult("Only a nurse can upload a nurse avatar."); + + if (!AvatarUpload.IsAllowedContentType(request.ContentType)) + return OperationResult.FailureResult("Unsupported image type. Use JPEG, PNG, or WebP."); + + if (!AvatarUpload.IsWithinSizeLimit(request.Length)) + return OperationResult.FailureResult("Image is empty or exceeds the 5 MB limit."); + + var profile = await unitOfWork.NurseProfileRepository.GetByUserIdAsync(userId, cancellationToken); + if (profile is null) + return OperationResult.NotFoundResult("No nurse profile exists yet."); + + var key = AvatarUpload.BuildKey("nurse", profile.Id, request.ContentType, Guid.NewGuid().ToString("N")); + using (var stream = new MemoryStream(request.Content)) + await objectStorage.PutAsync(key, stream, request.ContentType, cancellationToken); + + var url = objectStorage.GetUrl(key); + profile.AvatarUrl = url; + await unitOfWork.CommitAsync(); + + return OperationResult.SuccessResult(new AvatarUploadResult(url)); + } +} diff --git a/server/src/Core/Baya.Application/Features/Identity/Commands/UploadNurseAvatar/UploadNurseAvatarCommand.cs b/server/src/Core/Baya.Application/Features/Identity/Commands/UploadNurseAvatar/UploadNurseAvatarCommand.cs new file mode 100644 index 0000000..acd4381 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Identity/Commands/UploadNurseAvatar/UploadNurseAvatarCommand.cs @@ -0,0 +1,14 @@ +using Baya.Application.Models.Common; +using Baya.Application.Models.Identity; +using Mediator; + +namespace Baya.Application.Features.Identity.Commands.UploadNurseAvatar; + +/// +/// Stores the signed-in nurse's profile photo via IObjectStorage and persists the returned URL on +/// the nurse profile. The controller reads the multipart file; the command carries the bytes + metadata. +/// +public record UploadNurseAvatarCommand( + byte[] Content, + string ContentType, + long Length) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Identity/Commands/UpsertCustomerProfile/UpsertCustomerProfileCommand.Handler.cs b/server/src/Core/Baya.Application/Features/Identity/Commands/UpsertCustomerProfile/UpsertCustomerProfileCommand.Handler.cs index f79d712..b4961c5 100644 --- a/server/src/Core/Baya.Application/Features/Identity/Commands/UpsertCustomerProfile/UpsertCustomerProfileCommand.Handler.cs +++ b/server/src/Core/Baya.Application/Features/Identity/Commands/UpsertCustomerProfile/UpsertCustomerProfileCommand.Handler.cs @@ -29,7 +29,8 @@ internal sealed class UpsertCustomerProfileCommandHandler(ICurrentUser currentUs { UserId = userId, DefaultEmergencyContactName = request.DefaultEmergencyContactName, - DefaultEmergencyContactPhone = phone + DefaultEmergencyContactPhone = phone, + PreferredLanguage = request.PreferredLanguage }; await unitOfWork.CustomerProfileRepository.AddAsync(profile, cancellationToken); } @@ -37,6 +38,22 @@ internal sealed class UpsertCustomerProfileCommandHandler(ICurrentUser currentUs { profile.DefaultEmergencyContactName = request.DefaultEmergencyContactName; profile.DefaultEmergencyContactPhone = phone; + if (request.PreferredLanguage is not null) + profile.PreferredLanguage = request.PreferredLanguage; + } + + // The customer's display name lives on the base identity row, not the profile — update it in the + // same unit of work when supplied. + if (request.FirstName is not null || request.LastName is not null) + { + var user = await unitOfWork.UserAccountRepository.GetTrackedByIdAsync(userId, cancellationToken); + if (user is not null) + { + if (request.FirstName is not null) + user.Name = request.FirstName; + if (request.LastName is not null) + user.FamilyName = request.LastName; + } } await unitOfWork.CommitAsync(); diff --git a/server/src/Core/Baya.Application/Features/Identity/Commands/UpsertCustomerProfile/UpsertCustomerProfileCommand.cs b/server/src/Core/Baya.Application/Features/Identity/Commands/UpsertCustomerProfile/UpsertCustomerProfileCommand.cs index 15783a8..25669f3 100644 --- a/server/src/Core/Baya.Application/Features/Identity/Commands/UpsertCustomerProfile/UpsertCustomerProfileCommand.cs +++ b/server/src/Core/Baya.Application/Features/Identity/Commands/UpsertCustomerProfile/UpsertCustomerProfileCommand.cs @@ -6,8 +6,12 @@ namespace Baya.Application.Features.Identity.Commands.UpsertCustomerProfile; /// /// Creates (first call) or updates the signed-in customer's payer profile and its default emergency -/// contact (encrypted at rest). Idempotent on the owning user. +/// contact (encrypted at rest). Optionally updates the customer's display name (persisted on the base +/// users row) and preferred UI language (persisted on the profile). Idempotent on the owning user. /// public record UpsertCustomerProfileCommand( string DefaultEmergencyContactName, - string DefaultEmergencyContactPhone) : IRequest>; + string DefaultEmergencyContactPhone, + string FirstName = null, + string LastName = null, + string PreferredLanguage = null) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Identity/Commands/VerifyOtp/VerifyOtpCommand.Handler.cs b/server/src/Core/Baya.Application/Features/Identity/Commands/VerifyOtp/VerifyOtpCommand.Handler.cs index 8cb5873..d809d45 100644 --- a/server/src/Core/Baya.Application/Features/Identity/Commands/VerifyOtp/VerifyOtpCommand.Handler.cs +++ b/server/src/Core/Baya.Application/Features/Identity/Commands/VerifyOtp/VerifyOtpCommand.Handler.cs @@ -35,7 +35,13 @@ internal sealed class VerifyOtpCommandHandler( var maxAttempts = await platformConfig.GetConfig(IdentityDefaults.OtpMaxAttemptsKey, cancellationToken); if (user.AccessFailedCount >= maxAttempts) - return OperationResult.FailureResult("Too many failed attempts. Request a new code."); + { + // Lockout is the one machine-distinguishable state (safe — it reveals nothing about the code or + // account). The client shows the unlock countdown; a fresh code is gated by the resend window. + var retryAfterSeconds = await platformConfig.GetConfig(IdentityDefaults.OtpResendSecondsKey, cancellationToken); + return OperationResult.CodedFailureResult( + "otp_locked", "Too many failed attempts. Request a new code.", new { retryAfterSeconds }); + } // First-ever verify confirms the phone (ChangePhoneNumber to the same number); afterwards the // passwordless TOTP path applies. Both rotate the security stamp, so the token is minted after. @@ -47,7 +53,9 @@ internal sealed class VerifyOtpCommandHandler( if (!verifyResult.Succeeded) { await userManager.IncrementAccessFailedCountAsync(user); - return OperationResult.FailureResult(InvalidCodeMessage); + // Wrong and expired stay collapsed behind one code + message (anti-enumeration); only lockout is + // distinguished (above). + return OperationResult.CodedFailureResult("otp_invalid", InvalidCodeMessage); } var now = clock.UtcNow; diff --git a/server/src/Core/Baya.Application/Features/Identity/IdentityDefaults.cs b/server/src/Core/Baya.Application/Features/Identity/IdentityDefaults.cs index c12802f..c4279bd 100644 --- a/server/src/Core/Baya.Application/Features/Identity/IdentityDefaults.cs +++ b/server/src/Core/Baya.Application/Features/Identity/IdentityDefaults.cs @@ -21,6 +21,14 @@ internal static class IdentityDefaults /// Refresh-token session lifetime, in days. public const string SessionTtlDaysKey = "auth_session_ttl_days"; + /// Number of digits in the OTP code — mirrors the TOTP token provider (6). Surfaced on + /// RequestOtpResult so the client renders the correct number of input boxes contract-driven. + public const int OtpCodeLength = 6; + + /// How long an OTP stays valid, in seconds — mirrors the passwordless TOTP + /// TokenLifespan (1 minute). Surfaced so the client can show a "code expires in …" hint. + public const int OtpExpirySeconds = 60; + /// Cache key of the per-phone resend window (keyed by phone hash, never the raw phone). public static string OtpResendCacheKey(string phoneHash) => $"auth:otp:resend:{phoneHash}"; diff --git a/server/src/Core/Baya.Application/Features/Identity/PatientRules.cs b/server/src/Core/Baya.Application/Features/Identity/PatientRules.cs index 04685f1..dc5754b 100644 --- a/server/src/Core/Baya.Application/Features/Identity/PatientRules.cs +++ b/server/src/Core/Baya.Application/Features/Identity/PatientRules.cs @@ -6,4 +6,12 @@ internal static class PatientRules public static bool IsValidGender(string gender) => gender is "male" or "female"; public static bool IsNotFuture(DateOnly birthDate) => birthDate <= DateOnly.FromDateTime(DateTime.UtcNow.Date); + + /// The care-recipient relation-to-payer code set. Null/empty means "unspecified". + public static bool IsValidRelation(string relation) + => string.IsNullOrEmpty(relation) || relation is "parent" or "spouse" or "child" or "self"; + + /// Each condition is a short stable code; the list itself may be null/empty. + public static bool AreValidConditions(IReadOnlyList conditions) + => conditions is null || conditions.All(c => !string.IsNullOrWhiteSpace(c) && c.Length <= 40); } diff --git a/server/src/Core/Baya.Application/Features/Invoices/InvoiceDtoFactory.cs b/server/src/Core/Baya.Application/Features/Invoices/InvoiceDtoFactory.cs index f853d81..25f0dea 100644 --- a/server/src/Core/Baya.Application/Features/Invoices/InvoiceDtoFactory.cs +++ b/server/src/Core/Baya.Application/Features/Invoices/InvoiceDtoFactory.cs @@ -18,6 +18,7 @@ internal static class InvoiceDtoFactory invoice.BnplCommissionIrr?.ToString(), invoice.VatRate, invoice.VatIrr.ToString(), + (invoice.PlatformCommissionIrr + (invoice.BnplCommissionIrr ?? 0) + invoice.VatIrr).ToString(), invoice.MoadianReferenceNumber, invoice.MoadianStatus, pdfUrl, diff --git a/server/src/Core/Baya.Application/Features/Messaging/Commands/PostMessage/PostMessageCommand.Handler.cs b/server/src/Core/Baya.Application/Features/Messaging/Commands/PostMessage/PostMessageCommand.Handler.cs index 750b401..14d2153 100644 --- a/server/src/Core/Baya.Application/Features/Messaging/Commands/PostMessage/PostMessageCommand.Handler.cs +++ b/server/src/Core/Baya.Application/Features/Messaging/Commands/PostMessage/PostMessageCommand.Handler.cs @@ -41,6 +41,16 @@ internal sealed class PostMessageCommandHandler( if (header.Status == TicketStatus.Closed && !isStaff) return OperationResult.ForbiddenResult("This ticket is closed."); + // Optimistic-send idempotency: a retried post with the same client message id returns the original + // message instead of a duplicate. + if (!string.IsNullOrWhiteSpace(request.ClientMessageId)) + { + var existing = await unitOfWork.TicketRepository.GetMessageByClientIdAsync(request.TicketId, request.ClientMessageId, cancellationToken); + if (existing is not null) + return OperationResult.SuccessResult( + new PostMessageResult(existing.Id, request.TicketId, existing.SentAt, request.ClientMessageId)); + } + var now = dateTimeProvider.UtcNow; var message = new TicketMessage { @@ -48,6 +58,7 @@ internal sealed class PostMessageCommandHandler( SenderId = userId, Body = request.Body, IsInternal = request.IsInternal, + ClientMessageId = string.IsNullOrWhiteSpace(request.ClientMessageId) ? null : request.ClientMessageId, SentAt = now }; @@ -67,6 +78,6 @@ internal sealed class PostMessageCommandHandler( } return OperationResult.SuccessResult( - new PostMessageResult(message.Id, request.TicketId, message.SentAt)); + new PostMessageResult(message.Id, request.TicketId, message.SentAt, message.ClientMessageId)); } } diff --git a/server/src/Core/Baya.Application/Features/Messaging/Commands/PostMessage/PostMessageCommand.cs b/server/src/Core/Baya.Application/Features/Messaging/Commands/PostMessage/PostMessageCommand.cs index be55e22..fe24769 100644 --- a/server/src/Core/Baya.Application/Features/Messaging/Commands/PostMessage/PostMessageCommand.cs +++ b/server/src/Core/Baya.Application/Features/Messaging/Commands/PostMessage/PostMessageCommand.cs @@ -8,5 +8,5 @@ namespace Baya.Application.Features.Messaging.Commands.PostMessage; /// Appends a message to a ticket. Only an active participant (or staff) may post. /// (an admin-only note) can be set only by staff; a non-staff caller can neither set it nor post to a /// closed ticket. -public record PostMessageCommand(long TicketId, string Body, bool IsInternal = false) +public record PostMessageCommand(long TicketId, string Body, bool IsInternal = false, string? ClientMessageId = null) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Messaging/Queries/GetTicketThread/GetTicketThreadQuery.Handler.cs b/server/src/Core/Baya.Application/Features/Messaging/Queries/GetTicketThread/GetTicketThreadQuery.Handler.cs index 802fd90..73982e7 100644 --- a/server/src/Core/Baya.Application/Features/Messaging/Queries/GetTicketThread/GetTicketThreadQuery.Handler.cs +++ b/server/src/Core/Baya.Application/Features/Messaging/Queries/GetTicketThread/GetTicketThreadQuery.Handler.cs @@ -15,7 +15,8 @@ namespace Baya.Application.Features.Messaging.Queries.GetTicketThread; /// internal sealed class GetTicketThreadQueryHandler( ICurrentUser currentUser, - IUnitOfWork unitOfWork) + IUnitOfWork unitOfWork, + IDateTimeProvider dateTimeProvider) : IRequestHandler> { public async ValueTask> Handle(GetTicketThreadQuery request, CancellationToken cancellationToken) @@ -42,6 +43,18 @@ internal sealed class GetTicketThreadQueryHandler( var participants = await unitOfWork.TicketRepository.GetActiveParticipantsAsync(request.TicketId, cancellationToken); var messages = await unitOfWork.TicketRepository.GetMessagesAsync(request.TicketId, includeInternal, cancellationToken); + // Fetching the user-facing thread marks it read for the caller (drives the inbox unread count) — admins + // reading the staff view do not consume a participant's read state. + if (!request.AsAdmin) + { + var participant = await unitOfWork.TicketRepository.GetParticipantAsync(request.TicketId, userId, cancellationToken); + if (participant is { IsActive: true }) + { + participant.MarkRead(dateTimeProvider.UtcNow); + await unitOfWork.CommitAsync(); + } + } + return OperationResult.SuccessResult(new TicketThreadDto( header.Id, header.ReferenceCode, header.Subject, header.Status, header.Category, header.BookingId, header.RefundId, header.OpenedById, header.ClosedAt, participants, messages)); diff --git a/server/src/Core/Baya.Application/Features/Messaging/Queries/ListMyTickets/ListMyTicketsQuery.Handler.cs b/server/src/Core/Baya.Application/Features/Messaging/Queries/ListMyTickets/ListMyTicketsQuery.Handler.cs index 559c57c..fa628da 100644 --- a/server/src/Core/Baya.Application/Features/Messaging/Queries/ListMyTickets/ListMyTicketsQuery.Handler.cs +++ b/server/src/Core/Baya.Application/Features/Messaging/Queries/ListMyTickets/ListMyTicketsQuery.Handler.cs @@ -19,7 +19,7 @@ internal sealed class ListMyTicketsQueryHandler( return OperationResult>.UnauthorizedResult("Not authenticated."); var (page, pageSize) = Pagination.Normalize(request.Page, request.PageSize); - var result = await unitOfWork.TicketRepository.ListMyTicketsAsync(userId, request.Status, request.ReferenceCode, page, pageSize, cancellationToken); + var result = await unitOfWork.TicketRepository.ListMyTicketsAsync(userId, request.Status, request.ReferenceCode, request.BookingId, page, pageSize, cancellationToken); return OperationResult>.SuccessResult(result); } } diff --git a/server/src/Core/Baya.Application/Features/Messaging/Queries/ListMyTickets/ListMyTicketsQuery.cs b/server/src/Core/Baya.Application/Features/Messaging/Queries/ListMyTickets/ListMyTicketsQuery.cs index cd31ebe..adf6bb8 100644 --- a/server/src/Core/Baya.Application/Features/Messaging/Queries/ListMyTickets/ListMyTicketsQuery.cs +++ b/server/src/Core/Baya.Application/Features/Messaging/Queries/ListMyTickets/ListMyTicketsQuery.cs @@ -9,5 +9,6 @@ namespace Baya.Application.Features.Messaging.Queries.ListMyTickets; public record ListMyTicketsQuery( string? Status = null, string? ReferenceCode = null, + long? BookingId = null, int Page = 1, int PageSize = 50) : IRequest>>; diff --git a/server/src/Core/Baya.Application/Features/Nurses/Queries/GetNursePublicProfile/GetNursePublicProfileQuery.Handler.cs b/server/src/Core/Baya.Application/Features/Nurses/Queries/GetNursePublicProfile/GetNursePublicProfileQuery.Handler.cs new file mode 100644 index 0000000..fa31853 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Nurses/Queries/GetNursePublicProfile/GetNursePublicProfileQuery.Handler.cs @@ -0,0 +1,19 @@ +#nullable enable +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Common; +using Baya.Application.Models.Nurses; +using Mediator; + +namespace Baya.Application.Features.Nurses.Queries.GetNursePublicProfile; + +internal sealed class GetNursePublicProfileQueryHandler(IUnitOfWork unitOfWork) + : IRequestHandler> +{ + public async ValueTask> Handle(GetNursePublicProfileQuery request, CancellationToken cancellationToken) + { + var dto = await unitOfWork.NurseProfileRepository.GetPublicProfileAsync(request.NurseId, cancellationToken); + return dto is null + ? OperationResult.NotFoundResult("Nurse not found.") + : OperationResult.SuccessResult(dto); + } +} diff --git a/server/src/Core/Baya.Application/Features/Nurses/Queries/GetNursePublicProfile/GetNursePublicProfileQuery.cs b/server/src/Core/Baya.Application/Features/Nurses/Queries/GetNursePublicProfile/GetNursePublicProfileQuery.cs new file mode 100644 index 0000000..21b8d4d --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Nurses/Queries/GetNursePublicProfile/GetNursePublicProfileQuery.cs @@ -0,0 +1,12 @@ +using Baya.Application.Models.Common; +using Baya.Application.Models.Nurses; +using Mediator; + +namespace Baya.Application.Features.Nurses.Queries.GetNursePublicProfile; + +/// +/// The aggregated public nurse profile for the C3 discovery detail. Anonymous — it exposes only public, +/// non-PII facts (identity name/avatar/bio, aggregates, verification signal, specialty chips, active +/// services, the latest published review). No encrypted credential number is ever returned. +/// +public record GetNursePublicProfileQuery(long NurseId) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/PartnerCenters/Commands/SetPartnerCenterActive/SetPartnerCenterActiveCommand.Handler.cs b/server/src/Core/Baya.Application/Features/PartnerCenters/Commands/SetPartnerCenterActive/SetPartnerCenterActiveCommand.Handler.cs new file mode 100644 index 0000000..0da1a73 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/PartnerCenters/Commands/SetPartnerCenterActive/SetPartnerCenterActiveCommand.Handler.cs @@ -0,0 +1,24 @@ +#nullable enable +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Common; +using Baya.Application.Models.PartnerCenters; +using Mediator; + +namespace Baya.Application.Features.PartnerCenters.Commands.SetPartnerCenterActive; + +internal sealed class SetPartnerCenterActiveCommandHandler(IUnitOfWork unitOfWork) + : IRequestHandler> +{ + public async ValueTask> Handle(SetPartnerCenterActiveCommand request, CancellationToken cancellationToken) + { + var center = await unitOfWork.PartnerCenterRepository.GetTrackedAsync(request.Id, cancellationToken); + if (center is null) + return OperationResult.NotFoundResult("Partner center not found."); + + center.SetActive(request.IsActive); + await unitOfWork.CommitAsync(); + + var detail = await unitOfWork.PartnerCenterRepository.GetDetailAsync(center.Id, cancellationToken); + return OperationResult.SuccessResult(detail!); + } +} diff --git a/server/src/Core/Baya.Application/Features/PartnerCenters/Commands/SetPartnerCenterActive/SetPartnerCenterActiveCommand.cs b/server/src/Core/Baya.Application/Features/PartnerCenters/Commands/SetPartnerCenterActive/SetPartnerCenterActiveCommand.cs new file mode 100644 index 0000000..d7db115 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/PartnerCenters/Commands/SetPartnerCenterActive/SetPartnerCenterActiveCommand.cs @@ -0,0 +1,11 @@ +#nullable enable +using Baya.Application.Models.Common; +using Baya.Application.Models.PartnerCenters; +using Mediator; + +namespace Baya.Application.Features.PartnerCenters.Commands.SetPartnerCenterActive; + +/// Activates or suspends a partner center (admin). Distinct from verify (verify records the licensing +/// approval + activates); this is the standalone activate/suspend toggle. The id comes from the route. +public record SetPartnerCenterActiveCommand(bool IsActive, long Id = 0) + : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/PatientCareRecords/CarePlanSerialization.cs b/server/src/Core/Baya.Application/Features/PatientCareRecords/CarePlanSerialization.cs new file mode 100644 index 0000000..d7b0ed4 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/PatientCareRecords/CarePlanSerialization.cs @@ -0,0 +1,61 @@ +#nullable enable +using System.Text.Json; +using Baya.Application.Models.Patients; + +namespace Baya.Application.Features.PatientCareRecords; + +/// (De)serializes the family care plan's three JSON lists and assigns stable ids on write: a +/// caller-supplied id (> 0) is preserved, a new item (id 0) gets the next free id — so ids stay stable +/// across edits within a plan. +internal static class CarePlanSerialization +{ + public static IReadOnlyList ParseMedications(string? json) + => Parse(json); + + public static IReadOnlyList ParseRoutine(string? json) + => Parse(json); + + public static IReadOnlyList ParseTasks(string? json) + => Parse(json); + + public static (string Json, IReadOnlyList Items) AssignMedications(IReadOnlyList? items) + { + var assigned = AssignIds(items, (m, id) => m with { Id = id }, m => m.Id); + return (JsonSerializer.Serialize(assigned), assigned); + } + + public static (string Json, IReadOnlyList Items) AssignRoutine(IReadOnlyList? items) + { + var assigned = AssignIds(items, (r, id) => r with { Id = id }, r => r.Id); + return (JsonSerializer.Serialize(assigned), assigned); + } + + public static (string Json, IReadOnlyList Items) AssignTasks(IReadOnlyList? items) + { + var assigned = AssignIds(items, (t, id) => t with { Id = id }, t => t.Id); + return (JsonSerializer.Serialize(assigned), assigned); + } + + private static IReadOnlyList Parse(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + return []; + try + { + return JsonSerializer.Deserialize>(json) ?? []; + } + catch (JsonException) + { + return []; + } + } + + private static IReadOnlyList AssignIds(IReadOnlyList? items, Func withId, Func getId) + { + if (items is null || items.Count == 0) + return []; + + var next = items.Select(getId).DefaultIfEmpty(0).Max() + 1; + return items.Select(i => getId(i) > 0 ? i : withId(i, next++)).ToList(); + } +} diff --git a/server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/UpsertCarePlan/UpsertCarePlanCommand.Handler.cs b/server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/UpsertCarePlan/UpsertCarePlanCommand.Handler.cs new file mode 100644 index 0000000..e38a31a --- /dev/null +++ b/server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/UpsertCarePlan/UpsertCarePlanCommand.Handler.cs @@ -0,0 +1,51 @@ +#nullable enable +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Common; +using Baya.Application.Models.Patients; +using Baya.Domain.Entities.Identity; +using Mediator; + +namespace Baya.Application.Features.PatientCareRecords.Commands.UpsertCarePlan; + +internal sealed class UpsertCarePlanCommandHandler( + ICurrentUser currentUser, + IUnitOfWork unitOfWork) + : IRequestHandler> +{ + public async ValueTask> Handle(UpsertCarePlanCommand request, CancellationToken cancellationToken) + { + if (currentUser.UserId is null) + return OperationResult.UnauthorizedResult("Not authenticated."); + + var (owner, access) = await PatientAccess.ResolveAsync(currentUser, unitOfWork, request.PatientId, cancellationToken); + if (owner is null) + return OperationResult.NotFoundResult("Patient not found."); + if (!access.CanEdit) + return OperationResult.ForbiddenResult("Only the owning customer can edit the care plan."); + + var (medsJson, meds) = CarePlanSerialization.AssignMedications(request.Medications); + var (routineJson, routine) = CarePlanSerialization.AssignRoutine(request.Routine); + var (tasksJson, tasks) = CarePlanSerialization.AssignTasks(request.Tasks); + + var plan = await unitOfWork.PatientCareRecordRepository.GetCarePlanAsync(request.PatientId, cancellationToken); + if (plan is null) + { + plan = new PatientCarePlan { PatientId = request.PatientId }; + plan.MedicationsJson = medsJson; + plan.RoutineJson = routineJson; + plan.TasksJson = tasksJson; + await unitOfWork.PatientCareRecordRepository.AddCarePlanAsync(plan, cancellationToken); + } + else + { + plan.MedicationsJson = medsJson; + plan.RoutineJson = routineJson; + plan.TasksJson = tasksJson; + } + + await unitOfWork.CommitAsync(); + + return OperationResult.SuccessResult(new CarePlanDto(request.PatientId, meds, routine, tasks)); + } +} diff --git a/server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/UpsertCarePlan/UpsertCarePlanCommand.cs b/server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/UpsertCarePlan/UpsertCarePlanCommand.cs new file mode 100644 index 0000000..545edb9 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/UpsertCarePlan/UpsertCarePlanCommand.cs @@ -0,0 +1,14 @@ +#nullable enable +using Baya.Application.Models.Common; +using Baya.Application.Models.Patients; +using Mediator; + +namespace Baya.Application.Features.PatientCareRecords.Commands.UpsertCarePlan; + +/// Replaces the family-owned care plan for a patient (owning customer only). New items (id 0) are +/// assigned stable ids on save. The patient id comes from the route. +public record UpsertCarePlanCommand( + long PatientId, + IReadOnlyList? Medications, + IReadOnlyList? Routine, + IReadOnlyList? Tasks) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/WritePatientCareRecord/WritePatientCareRecordCommand.Handler.cs b/server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/WritePatientCareRecord/WritePatientCareRecordCommand.Handler.cs index 97e96ed..a249b36 100644 --- a/server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/WritePatientCareRecord/WritePatientCareRecordCommand.Handler.cs +++ b/server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/WritePatientCareRecord/WritePatientCareRecordCommand.Handler.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Text.Json; using Baya.Application.Contracts.Common; using Baya.Application.Contracts.Persistence; using Baya.Application.Models.Common; @@ -47,6 +48,7 @@ internal sealed class WritePatientCareRecordCommandHandler( BookingId = request.BookingId, NurseProfileId = nurseId, BodyEncrypted = fieldEncryptor.Encrypt(request.Body.Trim()), + TaskResultsJson = request.TaskResults is { Count: > 0 } tr ? JsonSerializer.Serialize(tr) : null, RecordedAt = dateTimeProvider.UtcNow.UtcDateTime }; diff --git a/server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/WritePatientCareRecord/WritePatientCareRecordCommand.cs b/server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/WritePatientCareRecord/WritePatientCareRecordCommand.cs index faea15a..aa334b4 100644 --- a/server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/WritePatientCareRecord/WritePatientCareRecordCommand.cs +++ b/server/src/Core/Baya.Application/Features/PatientCareRecords/Commands/WritePatientCareRecord/WritePatientCareRecordCommand.cs @@ -1,11 +1,16 @@ #nullable enable using Baya.Application.Models.Common; +using Baya.Application.Models.Patients; using Baya.Application.Models.Reviews; using Mediator; namespace Baya.Application.Features.PatientCareRecords.Commands.WritePatientCareRecord; /// A nurse authors a clinical note for a patient (optionally tagged with the booking that produced -/// it). The patient id comes from the route; the body is encrypted at rest before persisting. -public record WritePatientCareRecordCommand(long PatientId, long? BookingId, string Body) - : IRequest>; +/// it) plus the visit's ticked task checklist. The patient id comes from the route; the body is encrypted at +/// rest before persisting. +public record WritePatientCareRecordCommand( + long PatientId, + long? BookingId, + string Body, + IReadOnlyList? TaskResults = null) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/PatientCareRecords/PatientAccess.cs b/server/src/Core/Baya.Application/Features/PatientCareRecords/PatientAccess.cs new file mode 100644 index 0000000..c5af422 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/PatientCareRecords/PatientAccess.cs @@ -0,0 +1,51 @@ +#nullable enable +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Patients; +using Baya.Domain.Entities.User; + +namespace Baya.Application.Features.PatientCareRecords; + +/// +/// The single resolver for a caller's access to a patient's records: edit = the owning customer, +/// append-note = a nurse with a confirmed booking for the patient, view = either of those or an +/// admin. Centralized so the family care-plan get/put, the visit-note write/history, and the explicit +/// record_access read all apply the exact same rule. +/// +internal static class PatientAccess +{ + private static readonly string[] AdminRoles = [RoleNames.Admin, RoleNames.SuperAdmin]; + + public const string DeniedNotFound = "not_found"; + public const string DeniedNotAuthorized = "not_authorized"; + + /// Resolves access. OwnerCustomerId is null when the patient does not exist (a + /// access with everything false). + public static async Task<(long? OwnerCustomerId, RecordAccessDto Access)> ResolveAsync( + ICurrentUser currentUser, IUnitOfWork unitOfWork, long patientId, CancellationToken cancellationToken) + { + var ownerCustomerId = await unitOfWork.PatientCareRecordRepository.GetPatientOwnerCustomerIdAsync(patientId, cancellationToken); + if (ownerCustomerId is null) + return (null, new RecordAccessDto(false, false, false, DeniedNotFound)); + + var isAdmin = currentUser.Roles?.Any(AdminRoles.Contains) == true; + + var userId = currentUser.UserId; + var customerProfileId = userId is null + ? null + : await unitOfWork.CustomerProfileRepository.GetProfileIdByUserIdAsync(userId.Value, cancellationToken); + var canEdit = customerProfileId is { } cid && cid == ownerCustomerId; + + var canAppendNote = false; + if (userId is not null) + { + var nurseProfileId = await unitOfWork.NurseProfileRepository.GetProfileIdByUserIdAsync(userId.Value, cancellationToken); + if (nurseProfileId is { } nurseId) + canAppendNote = await unitOfWork.PatientCareRecordRepository + .NurseHasQualifyingBookingForPatientAsync(nurseId, patientId, cancellationToken); + } + + var canView = canEdit || canAppendNote || isAdmin; + return (ownerCustomerId, new RecordAccessDto(canView, canEdit, canAppendNote, canView ? null : DeniedNotAuthorized)); + } +} diff --git a/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetCarePlan/GetCarePlanQuery.Handler.cs b/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetCarePlan/GetCarePlanQuery.Handler.cs new file mode 100644 index 0000000..981b830 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetCarePlan/GetCarePlanQuery.Handler.cs @@ -0,0 +1,35 @@ +#nullable enable +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Common; +using Baya.Application.Models.Patients; +using Mediator; + +namespace Baya.Application.Features.PatientCareRecords.Queries.GetCarePlan; + +internal sealed class GetCarePlanQueryHandler( + ICurrentUser currentUser, + IUnitOfWork unitOfWork) + : IRequestHandler> +{ + public async ValueTask> Handle(GetCarePlanQuery request, CancellationToken cancellationToken) + { + if (currentUser.UserId is null) + return OperationResult.UnauthorizedResult("Not authenticated."); + + var (owner, access) = await PatientAccess.ResolveAsync(currentUser, unitOfWork, request.PatientId, cancellationToken); + if (owner is null) + return OperationResult.NotFoundResult("Patient not found."); + if (!access.CanView) + return OperationResult.ForbiddenResult("You do not have access to this patient's care plan."); + + var plan = await unitOfWork.PatientCareRecordRepository.GetCarePlanAsync(request.PatientId, cancellationToken); + return OperationResult.SuccessResult(plan is null + ? new CarePlanDto(request.PatientId, [], [], []) + : new CarePlanDto( + request.PatientId, + CarePlanSerialization.ParseMedications(plan.MedicationsJson), + CarePlanSerialization.ParseRoutine(plan.RoutineJson), + CarePlanSerialization.ParseTasks(plan.TasksJson))); + } +} diff --git a/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetCarePlan/GetCarePlanQuery.cs b/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetCarePlan/GetCarePlanQuery.cs new file mode 100644 index 0000000..5feff3f --- /dev/null +++ b/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetCarePlan/GetCarePlanQuery.cs @@ -0,0 +1,9 @@ +using Baya.Application.Models.Common; +using Baya.Application.Models.Patients; +using Mediator; + +namespace Baya.Application.Features.PatientCareRecords.Queries.GetCarePlan; + +/// Reads the family-owned care plan for a patient, under the same clinical access rule as the history +/// (owner / nurse-with-booking / admin). The patient id comes from the route. +public record GetCarePlanQuery(long PatientId) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetPatientHistory/GetPatientHistoryQuery.Handler.cs b/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetPatientHistory/GetPatientHistoryQuery.Handler.cs index 352e60e..76c573e 100644 --- a/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetPatientHistory/GetPatientHistoryQuery.Handler.cs +++ b/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetPatientHistory/GetPatientHistoryQuery.Handler.cs @@ -1,7 +1,9 @@ #nullable enable +using System.Text.Json; using Baya.Application.Contracts.Common; using Baya.Application.Contracts.Persistence; using Baya.Application.Models.Common; +using Baya.Application.Models.Patients; using Baya.Application.Models.Reviews; using Baya.Domain.Entities.User; using Mediator; @@ -60,10 +62,24 @@ internal sealed class GetPatientHistoryQueryHandler( var items = cipher.Items .Select(r => new CareRecordDto( r.Id, r.PatientId, r.BookingId, r.NurseProfileId, r.NurseName, - fieldEncryptor.Decrypt(r.BodyEncrypted), r.RecordedAt)) + fieldEncryptor.Decrypt(r.BodyEncrypted), ParseTaskResults(r.TaskResultsJson), r.RecordedAt)) .ToList(); return OperationResult>.SuccessResult( new PagedResult(items, cipher.Total, cipher.Page, cipher.PageSize)); } + + private static IReadOnlyList ParseTaskResults(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + return []; + try + { + return JsonSerializer.Deserialize>(json) ?? []; + } + catch (JsonException) + { + return []; + } + } } diff --git a/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetRecordAccess/GetRecordAccessQuery.Handler.cs b/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetRecordAccess/GetRecordAccessQuery.Handler.cs new file mode 100644 index 0000000..b0ed364 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetRecordAccess/GetRecordAccessQuery.Handler.cs @@ -0,0 +1,25 @@ +#nullable enable +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Common; +using Baya.Application.Models.Patients; +using Mediator; + +namespace Baya.Application.Features.PatientCareRecords.Queries.GetRecordAccess; + +internal sealed class GetRecordAccessQueryHandler( + ICurrentUser currentUser, + IUnitOfWork unitOfWork) + : IRequestHandler> +{ + public async ValueTask> Handle(GetRecordAccessQuery request, CancellationToken cancellationToken) + { + if (currentUser.UserId is null) + return OperationResult.UnauthorizedResult("Not authenticated."); + + // Always 200 with the access flags (incl. the non-leaking not_found / not_authorized denied states) so + // the client can render the access-denied card without probing a 403/404. + var (_, access) = await PatientAccess.ResolveAsync(currentUser, unitOfWork, request.PatientId, cancellationToken); + return OperationResult.SuccessResult(access); + } +} diff --git a/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetRecordAccess/GetRecordAccessQuery.cs b/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetRecordAccess/GetRecordAccessQuery.cs new file mode 100644 index 0000000..64a88ab --- /dev/null +++ b/server/src/Core/Baya.Application/Features/PatientCareRecords/Queries/GetRecordAccess/GetRecordAccessQuery.cs @@ -0,0 +1,9 @@ +using Baya.Application.Models.Common; +using Baya.Application.Models.Patients; +using Mediator; + +namespace Baya.Application.Features.PatientCareRecords.Queries.GetRecordAccess; + +/// The caller's access to a patient's records (view/edit/append-note) — so the UI shows the right +/// affordances + a non-leaking access-denied state. The patient id comes from the route. +public record GetRecordAccessQuery(long PatientId) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Payments/Commands/ConfirmPaymentAndPostLedger/ConfirmPaymentAndPostLedgerCommand.Handler.cs b/server/src/Core/Baya.Application/Features/Payments/Commands/ConfirmPaymentAndPostLedger/ConfirmPaymentAndPostLedgerCommand.Handler.cs index 5bba9e3..b08a06d 100644 --- a/server/src/Core/Baya.Application/Features/Payments/Commands/ConfirmPaymentAndPostLedger/ConfirmPaymentAndPostLedgerCommand.Handler.cs +++ b/server/src/Core/Baya.Application/Features/Payments/Commands/ConfirmPaymentAndPostLedger/ConfirmPaymentAndPostLedgerCommand.Handler.cs @@ -90,8 +90,14 @@ internal sealed class ConfirmPaymentAndPostLedgerCommandHandler( // Open the booking-coordination ticket (nurse + customer) once the booking is confirmed. Idempotent — // one coordination ticket per booking; a replayed confirm is a no-op (b15). if (conversion.Created) + { await sender.Send(new Messaging.Commands.AutoCreateCoordinationTicket.AutoCreateCoordinationTicketCommand(booking), cancellationToken); + // Auto-issue the commission invoice so the customer can reach «دانلود فاکتور» right after capture + // (was admin-only). Idempotent per booking — a replayed confirm returns the existing invoice. + await sender.Send(new Invoices.Commands.IssueInvoice.IssueInvoiceCommand(booking), cancellationToken); + } + return OperationResult.SuccessResult(true); } diff --git a/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarnings/GetNurseEarningsQuery.Handler.cs b/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarnings/GetNurseEarningsQuery.Handler.cs new file mode 100644 index 0000000..1fb03eb --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarnings/GetNurseEarningsQuery.Handler.cs @@ -0,0 +1,48 @@ +#nullable enable +using Baya.Application.Common; +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Common; +using Baya.Application.Models.Payouts; +using Baya.Domain.Entities.User; +using Mediator; + +namespace Baya.Application.Features.Payouts.Queries.GetNurseEarnings; + +internal sealed class GetNurseEarningsQueryHandler( + ICurrentUser currentUser, + IUnitOfWork unitOfWork, + IDateTimeProvider dateTimeProvider) + : IRequestHandler>> +{ + public async ValueTask>> Handle(GetNurseEarningsQuery request, CancellationToken cancellationToken) + { + if (currentUser.UserId is not { } userId) + return OperationResult>.UnauthorizedResult("Not authenticated."); + + if (currentUser.Roles?.Contains(RoleNames.Nurse) != true) + return OperationResult>.ForbiddenResult("Only a nurse can read their earnings."); + + var nurseId = await unitOfWork.NurseProfileRepository.GetProfileIdByUserIdAsync(userId, cancellationToken); + if (nurseId is not { } nid) + return OperationResult>.NotFoundResult("No nurse profile exists yet."); + + var (page, pageSize) = Pagination.Normalize(request.Page, request.PageSize); + var now = dateTimeProvider.UtcNow.UtcDateTime; + + var all = await unitOfWork.PayoutRepository.GetNurseEarningsAsync(nid, now, cancellationToken); + var filtered = string.IsNullOrWhiteSpace(request.State) + ? all + : all.Where(e => e.State == request.State).ToList(); + + var total = filtered.Count; + var items = filtered + .OrderByDescending(e => e.ScheduledDate).ThenByDescending(e => e.BookingId) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToList(); + + return OperationResult>.SuccessResult( + new PagedResult(items, total, page, pageSize)); + } +} diff --git a/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarnings/GetNurseEarningsQuery.cs b/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarnings/GetNurseEarningsQuery.cs new file mode 100644 index 0000000..5a4250a --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarnings/GetNurseEarningsQuery.cs @@ -0,0 +1,9 @@ +using Baya.Application.Models.Common; +using Baya.Application.Models.Payouts; +using Mediator; + +namespace Baya.Application.Features.Payouts.Queries.GetNurseEarnings; + +/// The signed-in nurse's per-booking earnings list, optionally filtered by money-state, paginated. +public record GetNurseEarningsQuery(string State = null, int Page = 1, int PageSize = 20) + : IRequest>>; diff --git a/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarningsBalance/GetNurseEarningsBalanceQuery.Handler.cs b/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarningsBalance/GetNurseEarningsBalanceQuery.Handler.cs new file mode 100644 index 0000000..bb4aa61 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarningsBalance/GetNurseEarningsBalanceQuery.Handler.cs @@ -0,0 +1,53 @@ +#nullable enable +using System.Globalization; +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Common; +using Baya.Application.Models.Payouts; +using Baya.Domain.Entities.User; +using Mediator; + +namespace Baya.Application.Features.Payouts.Queries.GetNurseEarningsBalance; + +internal sealed class GetNurseEarningsBalanceQueryHandler( + ICurrentUser currentUser, + IUnitOfWork unitOfWork, + IDateTimeProvider dateTimeProvider) + : IRequestHandler> +{ + public async ValueTask> Handle(GetNurseEarningsBalanceQuery request, CancellationToken cancellationToken) + { + if (currentUser.UserId is not { } userId) + return OperationResult.UnauthorizedResult("Not authenticated."); + + if (currentUser.Roles?.Contains(RoleNames.Nurse) != true) + return OperationResult.ForbiddenResult("Only a nurse can read their earnings."); + + var nurseId = await unitOfWork.NurseProfileRepository.GetProfileIdByUserIdAsync(userId, cancellationToken); + if (nurseId is not { } nid) + return OperationResult.NotFoundResult("No nurse profile exists yet."); + + var now = dateTimeProvider.UtcNow.UtcDateTime; + var earnings = await unitOfWork.PayoutRepository.GetNurseEarningsAsync(nid, now, cancellationToken); + + long pending = 0, eligible = 0; + foreach (var e in earnings) + { + var amount = long.Parse(e.NursePayoutAmount, CultureInfo.InvariantCulture); + if (e.State == NurseEarningsState.Pending) pending += amount; + else if (e.State == NurseEarningsState.Eligible) eligible += amount; + } + + var paidTotal = await unitOfWork.PayoutRepository.GetPaidNetTotalAsync(nid, cancellationToken); + var clawbackOutstanding = await unitOfWork.PayoutRepository.GetPendingClawbackSumAsync(nid, cancellationToken); + // The authoritative, SIGNED net payable — the ledger sum over nurse_payable (may be negative). Never clamped. + var net = await unitOfWork.PaymentRepository.GetNursePayableBalanceAsync(nid, cancellationToken); + + return OperationResult.SuccessResult(new NurseEarningsBalanceDto( + pending.ToString(CultureInfo.InvariantCulture), + eligible.ToString(CultureInfo.InvariantCulture), + paidTotal.ToString(CultureInfo.InvariantCulture), + clawbackOutstanding.ToString(CultureInfo.InvariantCulture), + net.ToString(CultureInfo.InvariantCulture))); + } +} diff --git a/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarningsBalance/GetNurseEarningsBalanceQuery.cs b/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarningsBalance/GetNurseEarningsBalanceQuery.cs new file mode 100644 index 0000000..a0bf372 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNurseEarningsBalance/GetNurseEarningsBalanceQuery.cs @@ -0,0 +1,8 @@ +using Baya.Application.Models.Common; +using Baya.Application.Models.Payouts; +using Mediator; + +namespace Baya.Application.Features.Payouts.Queries.GetNurseEarningsBalance; + +/// The signed-in nurse's four-bucket earnings balance + the ledger-derived signed net payable. +public record GetNurseEarningsBalanceQuery : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNursePayoutDetail/GetNursePayoutDetailQuery.Handler.cs b/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNursePayoutDetail/GetNursePayoutDetailQuery.Handler.cs new file mode 100644 index 0000000..05777f5 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNursePayoutDetail/GetNursePayoutDetailQuery.Handler.cs @@ -0,0 +1,33 @@ +#nullable enable +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Common; +using Baya.Application.Models.Payouts; +using Baya.Domain.Entities.User; +using Mediator; + +namespace Baya.Application.Features.Payouts.Queries.GetNursePayoutDetail; + +internal sealed class GetNursePayoutDetailQueryHandler( + ICurrentUser currentUser, + IUnitOfWork unitOfWork) + : IRequestHandler> +{ + public async ValueTask> Handle(GetNursePayoutDetailQuery request, CancellationToken cancellationToken) + { + if (currentUser.UserId is not { } userId) + return OperationResult.UnauthorizedResult("Not authenticated."); + + if (currentUser.Roles?.Contains(RoleNames.Nurse) != true) + return OperationResult.ForbiddenResult("Only a nurse can read their payout."); + + var nurseId = await unitOfWork.NurseProfileRepository.GetProfileIdByUserIdAsync(userId, cancellationToken); + if (nurseId is not { } nid) + return OperationResult.NotFoundResult("Payout not found."); + + var detail = await unitOfWork.PayoutRepository.GetNursePayoutDetailAsync(request.PayoutId, nid, cancellationToken); + return detail is null + ? OperationResult.NotFoundResult("Payout not found.") + : OperationResult.SuccessResult(detail); + } +} diff --git a/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNursePayoutDetail/GetNursePayoutDetailQuery.cs b/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNursePayoutDetail/GetNursePayoutDetailQuery.cs new file mode 100644 index 0000000..32872bb --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Payouts/Queries/GetNursePayoutDetail/GetNursePayoutDetailQuery.cs @@ -0,0 +1,9 @@ +using Baya.Application.Models.Common; +using Baya.Application.Models.Payouts; +using Mediator; + +namespace Baya.Application.Features.Payouts.Queries.GetNursePayoutDetail; + +/// The signed-in nurse's own payout detail (batch window + covered bookings). A payout that isn't the +/// nurse's is a clean not-found. The payout id comes from the route. +public record GetNursePayoutDetailQuery(long PayoutId) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Refunds/Commands/CancelBookingAndRefund/CancelBookingAndRefundCommand.Handler.cs b/server/src/Core/Baya.Application/Features/Refunds/Commands/CancelBookingAndRefund/CancelBookingAndRefundCommand.Handler.cs new file mode 100644 index 0000000..46d604f --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Refunds/Commands/CancelBookingAndRefund/CancelBookingAndRefundCommand.Handler.cs @@ -0,0 +1,70 @@ +#nullable enable +using Baya.Application.Contracts.Common; +using Baya.Application.Features.Bookings.Commands.CancelBooking; +using Baya.Application.Features.Refunds.Commands.CreateRefund; +using Baya.Application.Features.Refunds.Queries.GetRefundStatus; +using Baya.Application.Models.Common; +using Baya.Application.Models.Refunds; +using Baya.Domain.Entities.User; +using Mediator; + +namespace Baya.Application.Features.Refunds.Commands.CancelBookingAndRefund; + +internal sealed class CancelBookingAndRefundCommandHandler( + ICurrentUser currentUser, + ISender sender) + : IRequestHandler> +{ + public async ValueTask> Handle(CancelBookingAndRefundCommand request, CancellationToken cancellationToken) + { + if (currentUser.UserId is null) + return OperationResult.UnauthorizedResult("Not authenticated."); + + if (currentUser.Roles?.Contains(RoleNames.Customer) != true) + return OperationResult.ForbiddenResult("Only a customer can cancel their own booking."); + + var reason = string.IsNullOrWhiteSpace(request.ReasonNotes) + ? request.ReasonCategory + : $"{request.ReasonCategory}: {request.ReasonNotes}"; + + // 1) Cancel the booking — resolves + freezes the customer cancellation policy snapshot (owner-scoped + // inside the handler; a foreign booking is a clean 404, a terminal one a 409). + var cancel = await sender.Send(new CancelBookingCommand(reason, request.BookingId), cancellationToken); + if (!cancel.IsSuccess) + return Propagate(cancel); + + // 2) Open the refund off the just-frozen snapshot (decomposes both fee legs, posts the reversal, + // auto-opens a refund ticket). The customer never sets the money — the snapshot drives it. + var refund = await sender.Send( + new CreateRefundCommand( + BookingId: request.BookingId, + TicketId: null, + RefundPercentage: null, + PlatformFeeRefundedIrr: null, + NursePayoutRefundedIrr: null, + ReasonCategory: request.ReasonCategory, + ReasonNotes: request.ReasonNotes, + AdminNotes: null, + ManualBankReference: null), + cancellationToken); + if (!refund.IsSuccess) + return Propagate(refund); + + // Return the full customer status (incl. the decomposition + policy snapshot) — owner-scoped read. + return await sender.Send(new GetRefundStatusQuery(refund.Result.RefundId), cancellationToken); + } + + private static OperationResult Propagate(OperationResult failed) + { + var message = failed.ErrorMessages.Count > 0 ? failed.ErrorMessages[0].Value : "The cancellation could not be completed."; + if (failed.IsNotFound) + return OperationResult.NotFoundResult(message); + if (failed.IsConflict) + return OperationResult.ConflictResult(message); + if (failed.IsForbidden) + return OperationResult.ForbiddenResult(message); + if (failed.IsUnauthorized) + return OperationResult.UnauthorizedResult(message); + return OperationResult.FailureResult(message); + } +} diff --git a/server/src/Core/Baya.Application/Features/Refunds/Commands/CancelBookingAndRefund/CancelBookingAndRefundCommand.cs b/server/src/Core/Baya.Application/Features/Refunds/Commands/CancelBookingAndRefund/CancelBookingAndRefundCommand.cs new file mode 100644 index 0000000..11f703f --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Refunds/Commands/CancelBookingAndRefund/CancelBookingAndRefundCommand.cs @@ -0,0 +1,18 @@ +using Baya.Application.Models.Common; +using Baya.Application.Models.Refunds; +using Mediator; + +namespace Baya.Application.Features.Refunds.Commands.CancelBookingAndRefund; + +/// +/// The customer-initiated cancel: cancels the booking (freezing the resolved cancellation policy snapshot) +/// and opens the resulting refund in one call, returning the refund status. The customer *requests*; an +/// admin/ticket step still governs the money (the refund auto-opens a refund ticket). MVP cancels all +/// un-started sessions (the whole remaining engagement); is accepted for +/// forward-compatibility. The booking id comes from the route. +/// +public record CancelBookingAndRefundCommand( + string ReasonCategory, + string ReasonNotes = null, + IReadOnlyList SessionIds = null, + long BookingId = 0) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Refunds/Queries/GetCancellationPolicyPreview/GetCancellationPolicyPreviewQuery.Handler.cs b/server/src/Core/Baya.Application/Features/Refunds/Queries/GetCancellationPolicyPreview/GetCancellationPolicyPreviewQuery.Handler.cs new file mode 100644 index 0000000..5ac03a9 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Refunds/Queries/GetCancellationPolicyPreview/GetCancellationPolicyPreviewQuery.Handler.cs @@ -0,0 +1,91 @@ +#nullable enable +using System.Globalization; +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Features.Bookings; +using Baya.Application.Models.Common; +using Baya.Application.Models.Refunds; +using Baya.Domain.Entities.Booking; +using Baya.Domain.Entities.Payments; +using Baya.Domain.Entities.Refunds; +using Baya.Domain.Entities.User; +using Mediator; + +namespace Baya.Application.Features.Refunds.Queries.GetCancellationPolicyPreview; + +internal sealed class GetCancellationPolicyPreviewQueryHandler( + ICurrentUser currentUser, + IUnitOfWork unitOfWork, + IDateTimeProvider dateTimeProvider) + : IRequestHandler> +{ + public async ValueTask> Handle(GetCancellationPolicyPreviewQuery request, CancellationToken cancellationToken) + { + if (currentUser.UserId is not { } userId) + return OperationResult.UnauthorizedResult("Not authenticated."); + + if (currentUser.Roles?.Contains(RoleNames.Customer) != true) + return OperationResult.ForbiddenResult("Only a customer can preview their own cancellation."); + + var customerId = await unitOfWork.CustomerProfileRepository.GetProfileIdByUserIdAsync(userId, cancellationToken); + var booking = await unitOfWork.BookingRepository.GetTrackedWithSessionsAsync(request.BookingId, cancellationToken); + if (booking is null || customerId is not { } cid || booking.CustomerId != cid) + return OperationResult.NotFoundResult("Booking not found."); + + var now = dateTimeProvider.UtcNow.UtcDateTime; + var cancellable = booking.CanTransitionTo(BookingStatus.Cancelled); + + var grossShares = BookingAmounts.SplitPayout(booking.GrossPriceIrr, booking.SessionCount); + long refundableBase = 0; + var sessions = booking.Sessions + .OrderBy(s => s.SessionIndex) + .Select(s => + { + var refundable = s.Status == BookingSessionStatus.Scheduled; + if (refundable) + refundableBase += grossShares[s.SessionIndex - 1]; + return new CancellationPreviewSessionDto( + s.Id, s.SessionIndex, s.ScheduledDate, refundable, refundable ? "un_started" : s.Status); + }) + .ToList(); + + var policies = await unitOfWork.CancellationPolicyRepository.GetActiveForActorAsync(CancellationActor.Customer, cancellationToken); + var hoursBefore = CancellationHelper.HoursBeforeStart(booking.ScheduledDate, booking.ScheduledTimeStart, now); + var policy = CancellationHelper.ResolvePolicy(policies, hoursBefore); + + var refundAmount = policy is null ? 0 : CancellationHelper.Refundable(refundableBase, policy.RefundPercentage); + var feeAmount = refundableBase - refundAmount; + + // Split the refunded amount across the booking's frozen fee legs pro-rata (never client-derived). + long platformFeeRefunded = 0; + if (refundAmount > 0 && booking.GrossPriceIrr > 0) + platformFeeRefunded = (long)decimal.Round( + (decimal)refundAmount * booking.BalinyaarCommissionIrr / booking.GrossPriceIrr, MidpointRounding.AwayFromZero); + var nursePayoutRefunded = refundAmount - platformFeeRefunded; + + var channelContext = await unitOfWork.RefundRepository.GetRefundContextAsync(request.BookingId, cancellationToken); + var channel = channelContext?.GatewayType == PaymentGatewayType.Bnpl ? RefundChannel.BnplRevert : RefundChannel.PspCard; + + var dto = new CancellationPolicyPreviewDto( + booking.Id, + cancellable, + policy?.Code, + policy?.RefundPercentage, + policy is null ? null : 100m - policy.RefundPercentage, + Str(refundAmount), + Str(feeAmount), + Str(refundableBase), + Str(platformFeeRefunded), + Str(nursePayoutRefunded), + CancellationActor.Customer, + hoursBefore >= 24 ? "at_least_24h" : "less_than_24h", + channel, + // The BNPL ~7–10-business-day customer ETA is stamped on the actual refund; the preview leaves it null. + null, + sessions); + + return OperationResult.SuccessResult(dto); + } + + private static string Str(long value) => value.ToString(CultureInfo.InvariantCulture); +} diff --git a/server/src/Core/Baya.Application/Features/Refunds/Queries/GetCancellationPolicyPreview/GetCancellationPolicyPreviewQuery.cs b/server/src/Core/Baya.Application/Features/Refunds/Queries/GetCancellationPolicyPreview/GetCancellationPolicyPreviewQuery.cs new file mode 100644 index 0000000..0d73f95 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Refunds/Queries/GetCancellationPolicyPreview/GetCancellationPolicyPreviewQuery.cs @@ -0,0 +1,9 @@ +using Baya.Application.Models.Common; +using Baya.Application.Models.Refunds; +using Mediator; + +namespace Baya.Application.Features.Refunds.Queries.GetCancellationPolicyPreview; + +/// Owner-scoped pre-cancel preview: resolves the applicable customer policy by current lead time and +/// the per-session refundability, without mutating anything. The booking id comes from the route. +public record GetCancellationPolicyPreviewQuery(long BookingId) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Refunds/Queries/GetRefundByBooking/GetRefundByBookingQuery.Handler.cs b/server/src/Core/Baya.Application/Features/Refunds/Queries/GetRefundByBooking/GetRefundByBookingQuery.Handler.cs new file mode 100644 index 0000000..72a106f --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Refunds/Queries/GetRefundByBooking/GetRefundByBookingQuery.Handler.cs @@ -0,0 +1,25 @@ +#nullable enable +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Common; +using Baya.Application.Models.Refunds; +using Mediator; + +namespace Baya.Application.Features.Refunds.Queries.GetRefundByBooking; + +internal sealed class GetRefundByBookingQueryHandler( + IUnitOfWork unitOfWork, + ICurrentUser currentUser) + : IRequestHandler> +{ + public async ValueTask> Handle(GetRefundByBookingQuery request, CancellationToken cancellationToken) + { + var projection = await unitOfWork.RefundRepository.GetStatusByBookingAsync(request.BookingId, cancellationToken); + + // Cross-customer access is indistinguishable from "no refund" — never confirm existence. + if (projection is null || projection.CustomerUserId != currentUser.UserId) + return OperationResult.NotFoundResult("Refund not found."); + + return OperationResult.SuccessResult(projection.Refund); + } +} diff --git a/server/src/Core/Baya.Application/Features/Refunds/Queries/GetRefundByBooking/GetRefundByBookingQuery.cs b/server/src/Core/Baya.Application/Features/Refunds/Queries/GetRefundByBooking/GetRefundByBookingQuery.cs new file mode 100644 index 0000000..ef38421 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Refunds/Queries/GetRefundByBooking/GetRefundByBookingQuery.cs @@ -0,0 +1,9 @@ +using Baya.Application.Models.Common; +using Baya.Application.Models.Refunds; +using Mediator; + +namespace Baya.Application.Features.Refunds.Queries.GetRefundByBooking; + +/// Reaches the customer's own refund from its booking id (owner-scoped). 404 when the booking has no +/// refund — the customer holds the booking id, not the refund id. +public record GetRefundByBookingQuery(long BookingId) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Reviews/Queries/GetMyReview/GetMyReviewQuery.Handler.cs b/server/src/Core/Baya.Application/Features/Reviews/Queries/GetMyReview/GetMyReviewQuery.Handler.cs new file mode 100644 index 0000000..8723b11 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Reviews/Queries/GetMyReview/GetMyReviewQuery.Handler.cs @@ -0,0 +1,29 @@ +#nullable enable +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Common; +using Baya.Application.Models.Reviews; +using Mediator; + +namespace Baya.Application.Features.Reviews.Queries.GetMyReview; + +internal sealed class GetMyReviewQueryHandler( + ICurrentUser currentUser, + IUnitOfWork unitOfWork) + : IRequestHandler> +{ + public async ValueTask> Handle(GetMyReviewQuery request, CancellationToken cancellationToken) + { + if (currentUser.UserId is not { } userId) + return OperationResult.UnauthorizedResult("Not authenticated."); + + var projection = await unitOfWork.ReviewRepository.GetMyReviewForBookingAsync(request.BookingId, cancellationToken); + + // No review yet → a clean "none" state (only for a booking the caller could own). A review owned by a + // different customer is indistinguishable from "none" — never leak another customer's review. + if (projection is null || projection.CustomerUserId != userId) + return OperationResult.SuccessResult(new MyReviewDto(MyReviewDto.StatusNone, null, null, [], null)); + + return OperationResult.SuccessResult(projection.Review); + } +} diff --git a/server/src/Core/Baya.Application/Features/Reviews/Queries/GetMyReview/GetMyReviewQuery.cs b/server/src/Core/Baya.Application/Features/Reviews/Queries/GetMyReview/GetMyReviewQuery.cs new file mode 100644 index 0000000..8e92d2b --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Reviews/Queries/GetMyReview/GetMyReviewQuery.cs @@ -0,0 +1,9 @@ +using Baya.Application.Models.Common; +using Baya.Application.Models.Reviews; +using Mediator; + +namespace Baya.Application.Features.Reviews.Queries.GetMyReview; + +/// Owner-scoped: the caller's own review for a booking (its persistent moderation state), or a +/// none status when they have not reviewed it. The booking id comes from the route. +public record GetMyReviewQuery(long BookingId) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Reviews/Queries/GetReviewEligibility/GetReviewEligibilityQuery.Handler.cs b/server/src/Core/Baya.Application/Features/Reviews/Queries/GetReviewEligibility/GetReviewEligibilityQuery.Handler.cs new file mode 100644 index 0000000..c37e7b0 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Reviews/Queries/GetReviewEligibility/GetReviewEligibilityQuery.Handler.cs @@ -0,0 +1,38 @@ +#nullable enable +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Common; +using Baya.Application.Models.Reviews; +using Baya.Domain.Entities.Booking; +using Mediator; + +namespace Baya.Application.Features.Reviews.Queries.GetReviewEligibility; + +internal sealed class GetReviewEligibilityQueryHandler( + ICurrentUser currentUser, + IUnitOfWork unitOfWork) + : IRequestHandler> +{ + public async ValueTask> Handle(GetReviewEligibilityQuery request, CancellationToken cancellationToken) + { + if (currentUser.UserId is not { } userId) + return OperationResult.UnauthorizedResult("Not authenticated."); + + var customerId = await unitOfWork.CustomerProfileRepository.GetProfileIdByUserIdAsync(userId, cancellationToken); + var booking = await unitOfWork.ReviewRepository.GetReviewableBookingAsync(request.BookingId, cancellationToken); + + if (booking is null) + return Result(false, ReviewEligibilityDto.ReasonNotFound); + if (customerId is not { } cid || booking.CustomerProfileId != cid) + return Result(false, ReviewEligibilityDto.ReasonNotOwner); + if (booking.Status is not (BookingStatus.Completed or BookingStatus.Closed)) + return Result(false, ReviewEligibilityDto.ReasonNotCompleted); + if (await unitOfWork.ReviewRepository.ExistsForBookingAsync(request.BookingId, cancellationToken)) + return Result(false, ReviewEligibilityDto.ReasonAlreadyReviewed); + + return Result(true, null); + } + + private static OperationResult Result(bool canReview, string? reason) + => OperationResult.SuccessResult(new ReviewEligibilityDto(canReview, reason)); +} diff --git a/server/src/Core/Baya.Application/Features/Reviews/Queries/GetReviewEligibility/GetReviewEligibilityQuery.cs b/server/src/Core/Baya.Application/Features/Reviews/Queries/GetReviewEligibility/GetReviewEligibilityQuery.cs new file mode 100644 index 0000000..322066e --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Reviews/Queries/GetReviewEligibility/GetReviewEligibilityQuery.cs @@ -0,0 +1,9 @@ +using Baya.Application.Models.Common; +using Baya.Application.Models.Reviews; +using Mediator; + +namespace Baya.Application.Features.Reviews.Queries.GetReviewEligibility; + +/// Owner-scoped: can the caller review this booking? Returns a stable reason when not, so the CTA/form +/// gates without probing the 1:1 conflict. The booking id comes from the route. +public record GetReviewEligibilityQuery(long BookingId) : IRequest>; diff --git a/server/src/Core/Baya.Application/Features/Verification/Commands/SubmitCredentialDetails/SubmitCredentialDetailsCommand.Handler.cs b/server/src/Core/Baya.Application/Features/Verification/Commands/SubmitCredentialDetails/SubmitCredentialDetailsCommand.Handler.cs new file mode 100644 index 0000000..b9b9dcc --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Verification/Commands/SubmitCredentialDetails/SubmitCredentialDetailsCommand.Handler.cs @@ -0,0 +1,84 @@ +#nullable enable +using Baya.Application.Common; +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Models.Common; +using Baya.Application.Models.Verification; +using Baya.Domain.Entities.User; +using Baya.Domain.Entities.Verification; +using Mediator; + +namespace Baya.Application.Features.Verification.Commands.SubmitCredentialDetails; + +internal sealed class SubmitCredentialDetailsCommandHandler( + ICurrentUser currentUser, + IUnitOfWork unitOfWork) + : IRequestHandler> +{ + public async ValueTask> Handle(SubmitCredentialDetailsCommand request, CancellationToken cancellationToken) + { + if (currentUser.UserId is not { } userId) + return OperationResult.UnauthorizedResult("Not authenticated."); + + if (currentUser.Roles?.Contains(RoleNames.Nurse) != true) + return OperationResult.ForbiddenResult("Only a nurse can submit credential details."); + + var profile = await unitOfWork.NurseProfileRepository.GetByUserIdAsync(userId, cancellationToken); + if (profile is null) + return OperationResult.NotFoundResult("No nurse profile exists yet."); + + var nurseId = profile.Id; + var holderName = string.IsNullOrWhiteSpace(request.HolderName) + ? await unitOfWork.VerificationRepository.GetNurseIdentityNameAsync(nurseId, cancellationToken) ?? string.Empty + : request.HolderName; + + // Specialties are the nurse's professional facets — persisted on the profile (they feed the public + // profile + search facets), not on the credential registry. + if (request.Specialties is not null) + profile.SpecializationsJson = JsonCodeList.Serialize(request.Specialties) ?? "[]"; + + await UpsertCredentialAsync(nurseId, CredentialTypes.InoMembership, request.InoNumber, holderName, request, cancellationToken); + + if (!string.IsNullOrWhiteSpace(request.LicenseNumber)) + await UpsertCredentialAsync(nurseId, CredentialTypes.MohCompetencyLicense, request.LicenseNumber, holderName, request, cancellationToken); + + await unitOfWork.CommitAsync(); + + var status = await unitOfWork.VerificationRepository.GetStatusForNurseAsync(nurseId, cancellationToken); + return status is null + ? OperationResult.NotFoundResult("No verification exists yet.") + : OperationResult.SuccessResult(status); + } + + private async Task UpsertCredentialAsync( + long nurseId, string credentialType, string number, string holderName, + SubmitCredentialDetailsCommand request, CancellationToken cancellationToken) + { + var existing = await unitOfWork.VerificationRepository.GetTrackedCredentialAsync(nurseId, credentialType, cancellationToken); + if (existing is null) + { + await unitOfWork.VerificationRepository.AddCredentialAsync(new NurseCredential + { + NurseId = nurseId, + CredentialType = credentialType, + CredentialNumber = number, + HolderNameSnapshot = holderName, + IssuingAuthority = request.IssuingAuthority ?? string.Empty, + IssuedAt = request.IssuedAt, + ExpiresAt = request.ExpiresAt, + VerificationMethod = VerificationMethods.Manual + }, cancellationToken); + return; + } + + // The nurse resubmitted — refresh the structured details; admin verification is unchanged. + existing.CredentialNumber = number; + existing.HolderNameSnapshot = holderName; + if (request.IssuingAuthority is not null) + existing.IssuingAuthority = request.IssuingAuthority; + if (request.IssuedAt is not null) + existing.IssuedAt = request.IssuedAt; + if (request.ExpiresAt is not null) + existing.ExpiresAt = request.ExpiresAt; + } +} diff --git a/server/src/Core/Baya.Application/Features/Verification/Commands/SubmitCredentialDetails/SubmitCredentialDetailsCommand.Validator.cs b/server/src/Core/Baya.Application/Features/Verification/Commands/SubmitCredentialDetails/SubmitCredentialDetailsCommand.Validator.cs new file mode 100644 index 0000000..3c36ec0 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Verification/Commands/SubmitCredentialDetails/SubmitCredentialDetailsCommand.Validator.cs @@ -0,0 +1,17 @@ +using FluentValidation; + +namespace Baya.Application.Features.Verification.Commands.SubmitCredentialDetails; + +public sealed class SubmitCredentialDetailsCommandValidator : AbstractValidator +{ + public SubmitCredentialDetailsCommandValidator() + { + RuleFor(x => x.InoNumber).NotEmpty().MaximumLength(50); + RuleFor(x => x.LicenseNumber).MaximumLength(50); + RuleFor(x => x.IssuingAuthority).MaximumLength(150); + RuleFor(x => x.HolderName).MaximumLength(200); + RuleFor(x => x.Specialties) + .Must(s => s is null || s.All(c => !string.IsNullOrWhiteSpace(c) && c.Length <= 50)) + .WithMessage("Each specialty must be a non-empty code up to 50 characters."); + } +} diff --git a/server/src/Core/Baya.Application/Features/Verification/Commands/SubmitCredentialDetails/SubmitCredentialDetailsCommand.cs b/server/src/Core/Baya.Application/Features/Verification/Commands/SubmitCredentialDetails/SubmitCredentialDetailsCommand.cs new file mode 100644 index 0000000..8bdb996 --- /dev/null +++ b/server/src/Core/Baya.Application/Features/Verification/Commands/SubmitCredentialDetails/SubmitCredentialDetailsCommand.cs @@ -0,0 +1,21 @@ +using Baya.Application.Models.Common; +using Baya.Application.Models.Verification; +using Mediator; + +namespace Baya.Application.Features.Verification.Commands.SubmitCredentialDetails; + +/// +/// The nurse-facing capture of the structured credential fields collected alongside the document +/// uploads (B5): the INO (nursing-system) number, professional specialties, and optional +/// license/authority/holder/date details. Without this the real path silently drops the nurse's INO number +/// and specialties until an admin re-enters them. The submitted credential is unverified — an admin +/// still decides it; the specialties feed the nurse's public profile/search facets. +/// +public record SubmitCredentialDetailsCommand( + string InoNumber, + IReadOnlyList Specialties, + string LicenseNumber = null, + string IssuingAuthority = null, + string HolderName = null, + DateOnly? IssuedAt = null, + DateOnly? ExpiresAt = null) : IRequest>; diff --git a/server/src/Core/Baya.Application/Models/Addresses/CustomerAddressDto.cs b/server/src/Core/Baya.Application/Models/Addresses/CustomerAddressDto.cs index 803ee77..9090993 100644 --- a/server/src/Core/Baya.Application/Models/Addresses/CustomerAddressDto.cs +++ b/server/src/Core/Baya.Application/Models/Addresses/CustomerAddressDto.cs @@ -9,6 +9,7 @@ namespace Baya.Application.Models.Addresses; public record CustomerAddressDto( long Id, string Title, + long ProvinceId, long CityId, string CityNameFa, string CityNameEn, diff --git a/server/src/Core/Baya.Application/Models/ApiResult/ApiResult.cs b/server/src/Core/Baya.Application/Models/ApiResult/ApiResult.cs index fd82e52..c3be2ce 100644 --- a/server/src/Core/Baya.Application/Models/ApiResult/ApiResult.cs +++ b/server/src/Core/Baya.Application/Models/ApiResult/ApiResult.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Text.Json.Serialization; using Baya.SharedKernel.Extensions; namespace Baya.Application.Models.ApiResult; @@ -11,6 +12,11 @@ public class ApiResult public string Message { get; set; } public string RequestId { get; } + /// Optional stable machine-readable error code (e.g. otp_locked) for a failure the client + /// must branch on. Omitted from the wire when null, so success/normal responses are unchanged. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Code { get; set; } + public ApiResult(bool isSuccess, ApiResultStatusCode statusCode, string message = null) { IsSuccess = isSuccess; diff --git a/server/src/Core/Baya.Application/Models/Booking/BookingRequestDetailProjection.cs b/server/src/Core/Baya.Application/Models/Booking/BookingRequestDetailProjection.cs index a38f95e..edf69f5 100644 --- a/server/src/Core/Baya.Application/Models/Booking/BookingRequestDetailProjection.cs +++ b/server/src/Core/Baya.Application/Models/Booking/BookingRequestDetailProjection.cs @@ -41,4 +41,7 @@ public record BookingRequestDetailProjection( DateTime NurseResponseDeadlineAt, DateTime? PaymentDeadlineAt, string? NurseRejectionReason, - DateTimeOffset CreatedAt); + DateTimeOffset CreatedAt, + long VariantPrice, + string? NurseAvatarUrl, + long? BookingId); diff --git a/server/src/Core/Baya.Application/Models/Booking/BookingRequestDto.cs b/server/src/Core/Baya.Application/Models/Booking/BookingRequestDto.cs index 540d219..f5a8dd4 100644 --- a/server/src/Core/Baya.Application/Models/Booking/BookingRequestDto.cs +++ b/server/src/Core/Baya.Application/Models/Booking/BookingRequestDto.cs @@ -39,4 +39,11 @@ public record BookingRequestDto( DateTime NurseResponseDeadlineAt, DateTime? PaymentDeadlineAt, string? NurseRejectionReason, - DateTimeOffset CreatedAt); + DateTimeOffset CreatedAt, + // The chosen variant's display rate (IRR digit-string) — for the summary card. This is the variant's + // rate, NOT an engagement total: a booking_request stays money-free. + string VariantPrice, + string? NurseAvatarUrl, + // The booking created once this request is converted (paid); null until then. Lets the confirmation + // deep-link the booking + invoice. + long? BookingId); diff --git a/server/src/Core/Baya.Application/Models/Booking/BookingRequestListItemDto.cs b/server/src/Core/Baya.Application/Models/Booking/BookingRequestListItemDto.cs index c7a50bc..d569d77 100644 --- a/server/src/Core/Baya.Application/Models/Booking/BookingRequestListItemDto.cs +++ b/server/src/Core/Baya.Application/Models/Booking/BookingRequestListItemDto.cs @@ -18,4 +18,8 @@ public record BookingRequestListItemDto( TimeOnly RequestedTimeEnd, DateTime NurseResponseDeadlineAt, DateTime? PaymentDeadlineAt, - string? CustomerNotes); + string? CustomerNotes, + // The requested service variant — makes the inbox row self-describing without opening the detail. + string VariantLabel, + // Coarse patient age for triage (nurse inbox); null when the birth date is unset. + int? PatientAge); diff --git a/server/src/Core/Baya.Application/Models/Booking/CheckoutContext.cs b/server/src/Core/Baya.Application/Models/Booking/CheckoutContext.cs new file mode 100644 index 0000000..fbbdc63 --- /dev/null +++ b/server/src/Core/Baya.Application/Models/Booking/CheckoutContext.cs @@ -0,0 +1,18 @@ +#nullable enable +namespace Baya.Application.Models.Booking; + +/// The owner-scoped facts the checkout-summary handler needs: participant labels, the schedule, +/// the frozen payment window, and the variant price + session count to compute the money split. +public record CheckoutContext( + long Id, + string Status, + string NurseName, + string PatientName, + string VariantLabel, + string VariantPriceUnit, + int? SessionCount, + long VariantPrice, + DateOnly RequestedDate, + TimeOnly RequestedTimeStart, + TimeOnly RequestedTimeEnd, + DateTime? PaymentDeadlineAt); diff --git a/server/src/Core/Baya.Application/Models/Booking/CheckoutSummaryDto.cs b/server/src/Core/Baya.Application/Models/Booking/CheckoutSummaryDto.cs new file mode 100644 index 0000000..fe404ef --- /dev/null +++ b/server/src/Core/Baya.Application/Models/Booking/CheckoutSummaryDto.cs @@ -0,0 +1,32 @@ +#nullable enable +namespace Baya.Application.Models.Booking; + +/// +/// The served money breakdown for the C6 checkout of an accepted_awaiting_payment request. All money +/// is IRR digit-strings, computed server-side from config (the client renders, never derives). The display +/// decomposition reconciles to the captured total: serviceCostIrr + commissionIrr + vatIrr = totalIrr. +/// VAT is carved out of the platform commission (VAT-on-commission-only), so commissionIrr is +/// the commission net of VAT and the customer's total equals grossPriceIrr — the amount +/// captured. The raw b10 amounts (grossPriceIrr/balinyaarCommissionIrr/nursePayoutAmount) +/// are surfaced alongside for reference: grossPriceIrr = balinyaarCommissionIrr + nursePayoutAmount. +/// +public record CheckoutSummaryDto( + long BookingRequestId, + string RequestStatus, + string NurseName, + string PatientName, + string VariantLabel, + string VariantPriceUnit, + int? SessionCount, + DateOnly RequestedDate, + TimeOnly RequestedTimeStart, + TimeOnly RequestedTimeEnd, + DateTime? PaymentDeadlineAt, + string ServiceCostIrr, + string CommissionIrr, + string VatIrr, + decimal VatRate, + string TotalIrr, + string GrossPriceIrr, + string BalinyaarCommissionIrr, + string NursePayoutAmount); diff --git a/server/src/Core/Baya.Application/Models/Common/OperationResult.cs b/server/src/Core/Baya.Application/Models/Common/OperationResult.cs index 653b58e..46bb9ca 100644 --- a/server/src/Core/Baya.Application/Models/Common/OperationResult.cs +++ b/server/src/Core/Baya.Application/Models/Common/OperationResult.cs @@ -30,6 +30,14 @@ public class OperationResult : IOperationResult /// (backend-phase-4). Distinct from a validation 400 so callers can react to the collision. public bool IsConflict { get; set; } + /// Optional stable machine-readable error code (e.g. otp_locked) surfaced on the failure + /// envelope so the client can branch on the exact condition. Null for ordinary failures. + public string ErrorCode { get; set; } + + /// Optional structured payload that accompanies (e.g. a lockout's + /// retryAfterSeconds). Serialized under the envelope's data when present. + public object ErrorData { get; set; } + public static OperationResult SuccessResult(TResult result) { return new OperationResult { Result = result, IsSuccess = true }; @@ -87,6 +95,18 @@ public class OperationResult : IOperationResult return operationResult; } + /// A validation-style failure (HTTP 400) that also carries a stable machine + /// (and optional structured ) on the envelope — for a condition the client must + /// distinguish, e.g. an OTP lockout. + public static OperationResult CodedFailureResult(string code, string message, object data = null) + { + var operationResult = new OperationResult { IsSuccess = false, ErrorCode = code, ErrorData = data }; + + operationResult.ErrorMessages.Add(new("GeneralError", message)); + + return operationResult; + } + public void AddError(string propertyName, string message) { IsSuccess = false; diff --git a/server/src/Core/Baya.Application/Models/Configuration/PlatformConfigDto.cs b/server/src/Core/Baya.Application/Models/Configuration/PlatformConfigDto.cs index a0019fd..c871356 100644 --- a/server/src/Core/Baya.Application/Models/Configuration/PlatformConfigDto.cs +++ b/server/src/Core/Baya.Application/Models/Configuration/PlatformConfigDto.cs @@ -2,7 +2,14 @@ namespace Baya.Application.Models.Configuration; /// A runtime config row as returned to admins. Value is the raw string; parse per DataType. -public record PlatformConfigDto(string Key, string Value, string DataType, string? Description); +public record PlatformConfigDto( + string Key, + string Value, + string DataType, + string? Description, + // Last-changed meta for the editor row (from the entity's audit fields; falls back to creation). + System.DateTimeOffset UpdatedAt, + int? UpdatedBy); /// One audited change to a config key (from the append-only audit trail). public record ConfigChangeDto( diff --git a/server/src/Core/Baya.Application/Models/Identity/AvatarUploadResult.cs b/server/src/Core/Baya.Application/Models/Identity/AvatarUploadResult.cs new file mode 100644 index 0000000..b42fd68 --- /dev/null +++ b/server/src/Core/Baya.Application/Models/Identity/AvatarUploadResult.cs @@ -0,0 +1,4 @@ +namespace Baya.Application.Models.Identity; + +/// The stored, retrievable URL of an uploaded avatar. Also persisted on the owning profile. +public record AvatarUploadResult(string Url); diff --git a/server/src/Core/Baya.Application/Models/Identity/CustomerProfileDto.cs b/server/src/Core/Baya.Application/Models/Identity/CustomerProfileDto.cs index 975c683..353aa5a 100644 --- a/server/src/Core/Baya.Application/Models/Identity/CustomerProfileDto.cs +++ b/server/src/Core/Baya.Application/Models/Identity/CustomerProfileDto.cs @@ -7,4 +7,6 @@ namespace Baya.Application.Models.Identity; public record CustomerProfileDto( long Id, string DefaultEmergencyContactName, - string DefaultEmergencyContactPhone); + string DefaultEmergencyContactPhone, + string AvatarUrl, + string PreferredLanguage); diff --git a/server/src/Core/Baya.Application/Models/Identity/NurseProfileDto.cs b/server/src/Core/Baya.Application/Models/Identity/NurseProfileDto.cs index a172d7a..4e3fcf7 100644 --- a/server/src/Core/Baya.Application/Models/Identity/NurseProfileDto.cs +++ b/server/src/Core/Baya.Application/Models/Identity/NurseProfileDto.cs @@ -15,4 +15,5 @@ public record NurseProfileDto( bool IsAcceptingBookings, decimal AverageRating, int TotalReviews, - int TotalCompletedBookings); + int TotalCompletedBookings, + string AvatarUrl); diff --git a/server/src/Core/Baya.Application/Models/Identity/PatientDto.cs b/server/src/Core/Baya.Application/Models/Identity/PatientDto.cs index f3dac0f..4c19f1d 100644 --- a/server/src/Core/Baya.Application/Models/Identity/PatientDto.cs +++ b/server/src/Core/Baya.Application/Models/Identity/PatientDto.cs @@ -2,7 +2,8 @@ namespace Baya.Application.Models.Identity; /// /// A care recipient owned by the signed-in customer. InitialMedicalNotes is decrypted and -/// returned only to the owning customer. +/// returned only to the owning customer. Relation is a stable code (nullable); +/// Conditions is the patient's care-condition code list (empty, never null). /// public record PatientDto( long Id, @@ -13,4 +14,6 @@ public record PatientDto( string Gender, string BloodType, string InitialMedicalNotes, - bool IsActive); + bool IsActive, + string Relation, + IReadOnlyList Conditions); diff --git a/server/src/Core/Baya.Application/Models/Identity/RequestOtpResult.cs b/server/src/Core/Baya.Application/Models/Identity/RequestOtpResult.cs index 6707b98..4fbc893 100644 --- a/server/src/Core/Baya.Application/Models/Identity/RequestOtpResult.cs +++ b/server/src/Core/Baya.Application/Models/Identity/RequestOtpResult.cs @@ -3,5 +3,6 @@ namespace Baya.Application.Models.Identity; /// /// Deliberately non-enumerating: the same shape comes back whether or not the phone already had an /// account. is false only when the per-phone resend window is still open. +/// drives the OTP input-box count; the expiry hint. /// -public record RequestOtpResult(bool OtpSent, int ResendAvailableInSeconds); \ No newline at end of file +public record RequestOtpResult(bool OtpSent, int ResendAvailableInSeconds, int CodeLength, int ExpiresInSeconds); \ No newline at end of file diff --git a/server/src/Core/Baya.Application/Models/Invoices/InvoiceProjections.cs b/server/src/Core/Baya.Application/Models/Invoices/InvoiceProjections.cs index 174f6e7..b1f0f76 100644 --- a/server/src/Core/Baya.Application/Models/Invoices/InvoiceProjections.cs +++ b/server/src/Core/Baya.Application/Models/Invoices/InvoiceProjections.cs @@ -22,6 +22,8 @@ public record InvoiceDto( string? BnplCommissionIrr, decimal VatRate, string VatIrr, + // The reconciling settlement total = platform commission + BNPL commission + VAT (IRR digit-string). + string TotalIrr, string? MoadianReferenceNumber, string? MoadianStatus, string? PdfUrl, diff --git a/server/src/Core/Baya.Application/Models/Messaging/MessagingProjections.cs b/server/src/Core/Baya.Application/Models/Messaging/MessagingProjections.cs index 70b66a3..921ac59 100644 --- a/server/src/Core/Baya.Application/Models/Messaging/MessagingProjections.cs +++ b/server/src/Core/Baya.Application/Models/Messaging/MessagingProjections.cs @@ -1,7 +1,9 @@ #nullable enable namespace Baya.Application.Models.Messaging; -/// A ticket row in a list (no messages). Used by ListMyTickets and the admin queue. +/// A ticket row in a list (no messages). Used by ListMyTickets and the admin queue. +/// LastMessageAt (last non-internal activity) drives inbox sorting; UnreadCount is the caller's +/// unread non-internal messages from others (0 for the admin queue). public record TicketSummaryDto( long Id, string ReferenceCode, @@ -10,7 +12,9 @@ public record TicketSummaryDto( string Category, long? BookingId, long? RefundId, - DateTimeOffset CreatedAt); + DateTimeOffset CreatedAt, + DateTimeOffset? LastMessageAt, + int UnreadCount); /// One message in a thread. IsInternal is only ever true in the admin view — the user /// view never contains an internal message (it is stripped in the query projection). @@ -55,4 +59,4 @@ public record TicketHeaderDto( public record OpenTicketResult(long TicketId, string ReferenceCode, string Status, string Category); -public record PostMessageResult(long MessageId, long TicketId, DateTimeOffset SentAt); +public record PostMessageResult(long MessageId, long TicketId, DateTimeOffset SentAt, string? ClientMessageId); diff --git a/server/src/Core/Baya.Application/Models/Nurses/NursePublicProfileDto.cs b/server/src/Core/Baya.Application/Models/Nurses/NursePublicProfileDto.cs new file mode 100644 index 0000000..ffd84b7 --- /dev/null +++ b/server/src/Core/Baya.Application/Models/Nurses/NursePublicProfileDto.cs @@ -0,0 +1,39 @@ +#nullable enable +namespace Baya.Application.Models.Nurses; + +/// +/// The aggregated public nurse profile behind the C3 discovery detail — composed from identity (name, +/// avatar, bio, experience), the quality aggregates, the verification signal (isVerified + +/// inoMembership), the nurse's specialty chips, their active bookable services, and the latest +/// published review. Prices are IRR digit-strings; no encrypted credential number is ever exposed. +/// +public record NursePublicProfileDto( + long NurseId, + string NurseName, + string? AvatarUrl, + string Bio, + int YearsExperience, + decimal AverageRating, + int TotalReviews, + int TotalCompletedBookings, + bool IsVerified, + bool InoMembership, + IReadOnlyList AttributeChips, + IReadOnlyList Services, + NursePublicLatestReviewDto? LatestReview); + +/// One bookable service on the public profile. PriceIrr is IRR Rials as a digit string. +public record NursePublicServiceDto( + long VariantId, + string DisplayName, + string PriceIrr, + string PriceUnit, + int? SessionCount); + +/// The latest published review teaser. AuthorMasked is intentionally null — the author name +/// is never exposed (privacy); the client renders a generic masked label. +public record NursePublicLatestReviewDto( + int Rating, + string? Body, + string? AuthorMasked, + DateTimeOffset CreatedAt); diff --git a/server/src/Core/Baya.Application/Models/Patients/CarePlanDtos.cs b/server/src/Core/Baya.Application/Models/Patients/CarePlanDtos.cs new file mode 100644 index 0000000..d4eddf9 --- /dev/null +++ b/server/src/Core/Baya.Application/Models/Patients/CarePlanDtos.cs @@ -0,0 +1,22 @@ +#nullable enable +namespace Baya.Application.Models.Patients; + +/// The family-owned care plan for a patient — the customer's editable medications / routine / tasks. +public record CarePlanDto( + long PatientId, + IReadOnlyList Medications, + IReadOnlyList Routine, + IReadOnlyList Tasks); + +public record MedicationDto(long Id, string Name, string? Dosage, string Frequency, string? TimingNote); + +public record RoutineItemDto(long Id, string Label, string? TimeOfDay, string? Note); + +public record CareTaskDto(long Id, string Label, bool Done); + +/// Whether the caller may view/edit the plan or append a clinical note — so the UI renders the right +/// affordances (and a non-leaking access-denied state) without probing the 403. +public record RecordAccessDto(bool CanView, bool CanEdit, bool CanAppendNote, string? DeniedReason); + +/// One ticked item from the nurse's visit checklist, preserved structurally on the visit note. +public record TaskResultDto(string Label, bool Done); diff --git a/server/src/Core/Baya.Application/Models/Payouts/PayoutProjections.cs b/server/src/Core/Baya.Application/Models/Payouts/PayoutProjections.cs index 927da3d..db5063b 100644 --- a/server/src/Core/Baya.Application/Models/Payouts/PayoutProjections.cs +++ b/server/src/Core/Baya.Application/Models/Payouts/PayoutProjections.cs @@ -84,4 +84,60 @@ public record NursePayoutHistoryDto( string? TransferReference, DateTime? PaidAt, DateOnly PeriodStart, - DateOnly PeriodEnd); + DateOnly PeriodEnd, + string? FailureReason); + +/// The nurse's four-bucket earnings balance + the ledger-derived signed net payable (may be +/// negative = "owed back"; never clamped). paidTotalIrr is a lifetime total and does NOT enter the net. +/// All money is IRR digit-strings. +public record NurseEarningsBalanceDto( + string PendingTotalIrr, + string EligibleTotalIrr, + string PaidTotalIrr, + string ClawbackOutstandingIrr, + string NetPayableBalanceIrr); + +/// One per-booking earnings row with its server-derived money-state +/// (pending|eligible|paid|clawback_applied). Money is IRR digit-strings. +public record NurseEarningsItemDto( + long BookingId, + string? PatientName, + DateOnly ScheduledDate, + string GrossPriceIrr, + string BalinyaarCommissionIrr, + string NursePayoutAmount, + string State, + DateTime? DisputeWindowEndsAt, + DateTime? PayoutEligibleAt, + DateTime? PaidAt, + string? TransferReference, + long? NursePayoutId, + long? BatchId, + string? ClawbackAppliedIrr, + string? NetAmountIrr); + +/// A nurse-scoped payout detail — the payout + its batch window + the bookings it covers, for +/// reconciliation. Money is IRR digit-strings. +public record NursePayoutDetailDto( + long Id, + long BatchId, + string Status, + string GrossEarningsIrr, + string ClawbackAppliedIrr, + string NetAmountIrr, + string AmountIrr, + string MaskedIban, + string? TransferReference, + DateTime? PaidAt, + string? FailureReason, + PayoutBatchDto Batch, + IReadOnlyList Bookings); + +/// The stable nurse-facing earnings-state codes. +public static class NurseEarningsState +{ + public const string Pending = "pending"; + public const string Eligible = "eligible"; + public const string Paid = "paid"; + public const string ClawbackApplied = "clawback_applied"; +} diff --git a/server/src/Core/Baya.Application/Models/Refunds/CancellationPolicyPreviewDto.cs b/server/src/Core/Baya.Application/Models/Refunds/CancellationPolicyPreviewDto.cs new file mode 100644 index 0000000..22ca2fe --- /dev/null +++ b/server/src/Core/Baya.Application/Models/Refunds/CancellationPolicyPreviewDto.cs @@ -0,0 +1,35 @@ +#nullable enable +namespace Baya.Application.Models.Refunds; + +/// +/// The pre-cancel disclosure (REQ-020): resolves the applicable customer cancellation policy by the +/// current lead time and enumerates per-session refundability, so the fee/refund split is shown before +/// the customer confirms — an outside-policy fee is never a surprise. All IRR fields are digit-strings and +/// server-computed. refundAmountIrr + feeAmountIrr = refundableAmountIrr; the fee-leg split +/// (platformFeeRefundedIrr/nursePayoutRefundedIrr) is served, never client-derived. +/// +public record CancellationPolicyPreviewDto( + long BookingId, + bool Cancellable, + string? CancellationPolicyCode, + decimal? RefundPercentageApplied, + decimal? FeePercentage, + string RefundAmountIrr, + string FeeAmountIrr, + string RefundableAmountIrr, + string PlatformFeeRefundedIrr, + string NursePayoutRefundedIrr, + string AppliesTo, + string LeadTimeLabel, + string RefundChannel, + DateOnly? ExpectedCustomerRefundEta, + IReadOnlyList Sessions); + +/// One session's refundability in the preview. ReasonCode is un_started when refundable, +/// else the blocking session status (e.g. completed/in_progress). +public record CancellationPreviewSessionDto( + long BookingSessionId, + int SessionIndex, + DateOnly ScheduledDate, + bool Refundable, + string ReasonCode); diff --git a/server/src/Core/Baya.Application/Models/Refunds/RefundProjections.cs b/server/src/Core/Baya.Application/Models/Refunds/RefundProjections.cs index 7a1faca..41cce3b 100644 --- a/server/src/Core/Baya.Application/Models/Refunds/RefundProjections.cs +++ b/server/src/Core/Baya.Application/Models/Refunds/RefundProjections.cs @@ -44,7 +44,9 @@ public record RefundListItemDto( DateTimeOffset CreatedAt); /// The customer-facing status of their own refund — the reference is masked, money is a digit string, -/// and the BNPL 7–10-business-day window is surfaced so the UI can show "on its way, ~N days". +/// and the BNPL 7–10-business-day window is surfaced so the UI can show "on its way, ~N days". The fee-leg +/// decomposition (platformFeeRefundedIrr/nursePayoutRefundedIrr) + policy snapshot let the UI +/// render the transparency split without re-deriving it. public record RefundStatusDto( long Id, long BookingId, @@ -52,7 +54,13 @@ public record RefundStatusDto( string RefundChannel, string Amount, DateOnly? ExpectedCustomerRefundEta, - string? Reference); + string? Reference, + string PlatformFeeRefundedIrr, + string NursePayoutRefundedIrr, + decimal? RefundPercentageApplied, + string? CancellationPolicyCode, + DateTimeOffset CreatedAt, + DateTime? CompletedAt); /// The tenancy envelope for the customer refund-status read: the owning customer's user id (compared /// to ICurrentUser) plus the DTO. A cross-customer access is a clean not-found, never a leak. diff --git a/server/src/Core/Baya.Application/Models/Reviews/CareRecordProjections.cs b/server/src/Core/Baya.Application/Models/Reviews/CareRecordProjections.cs index 3c9164d..57505aa 100644 --- a/server/src/Core/Baya.Application/Models/Reviews/CareRecordProjections.cs +++ b/server/src/Core/Baya.Application/Models/Reviews/CareRecordProjections.cs @@ -1,4 +1,6 @@ #nullable enable +using Baya.Application.Models.Patients; + namespace Baya.Application.Models.Reviews; /// A patient-care-record row as it leaves the repository — the clinical body is still ciphertext @@ -11,10 +13,11 @@ public record CareRecordCipherRow( long NurseProfileId, string? NurseName, string BodyEncrypted, + string? TaskResultsJson, DateTime RecordedAt); /// A decrypted clinical note returned to an authorized reader (owning customer / nurse with a -/// confirmed booking / admin). Newest first. +/// confirmed booking / admin). Newest first. is the visit's ticked checklist. public record CareRecordDto( long Id, long PatientId, @@ -22,6 +25,7 @@ public record CareRecordDto( long NurseProfileId, string? NurseName, string Body, + IReadOnlyList TaskResults, DateTime RecordedAt); /// What WritePatientCareRecordCommand returns. diff --git a/server/src/Core/Baya.Application/Models/Reviews/ReviewGateDtos.cs b/server/src/Core/Baya.Application/Models/Reviews/ReviewGateDtos.cs new file mode 100644 index 0000000..3276e4d --- /dev/null +++ b/server/src/Core/Baya.Application/Models/Reviews/ReviewGateDtos.cs @@ -0,0 +1,29 @@ +#nullable enable +namespace Baya.Application.Models.Reviews; + +/// Whether the caller may review a booking, and if not, the stable machine reason so the CTA/form +/// can gate without probing the 1:1 409. +public record ReviewEligibilityDto(bool CanReview, string? Reason) +{ + public const string ReasonNotCompleted = "not_completed"; + public const string ReasonAlreadyReviewed = "already_reviewed"; + public const string ReasonNotOwner = "not_owner"; + public const string ReasonNotFound = "not_found"; +} + +/// The caller's own review for a booking (across sessions), so a returning customer sees the +/// persistent "under review" state and never a second form. ModerationStatus is none when the +/// customer has not reviewed the booking. +public record MyReviewDto( + string ModerationStatus, + int? Rating, + string? Body, + IReadOnlyList TagCodes, + DateTimeOffset? CreatedAt) +{ + public const string StatusNone = "none"; +} + +/// Owner-scoped envelope for the my-review read: the owning customer's user id (compared to the +/// caller) + the review. A foreign booking is a clean not-found. +public record MyReviewProjection(int CustomerUserId, MyReviewDto Review); diff --git a/server/src/Core/Baya.Application/Models/Reviews/ReviewProjections.cs b/server/src/Core/Baya.Application/Models/Reviews/ReviewProjections.cs index 2c407bb..508bed9 100644 --- a/server/src/Core/Baya.Application/Models/Reviews/ReviewProjections.cs +++ b/server/src/Core/Baya.Application/Models/Reviews/ReviewProjections.cs @@ -44,6 +44,7 @@ public record ModerationQueueItemDto( string ModerationStatus, string? ModerationReason, long? LowRatingAlertId, + IReadOnlyList TagCodes, DateTimeOffset CreatedAt); /// One tag's rollup for a nurse — the count of published reviews carrying it and that as a percentage diff --git a/server/src/Core/Baya.Application/Models/Search/NurseSearchResultDto.cs b/server/src/Core/Baya.Application/Models/Search/NurseSearchResultDto.cs index 48c5959..c013bdd 100644 --- a/server/src/Core/Baya.Application/Models/Search/NurseSearchResultDto.cs +++ b/server/src/Core/Baya.Application/Models/Search/NurseSearchResultDto.cs @@ -3,7 +3,8 @@ namespace Baya.Application.Models.Search; /// /// One family-facing search hit — a bookable variant matched in a covered area. Price is IRR Rials /// as a digit string (BIGINT on the wire, never a float); DistrictId == null means the nurse covers -/// the whole city. +/// the whole city. NurseName/AvatarUrl are the card's identity (denormalized on the index); +/// DistanceKm is optional — null until the index carries the searched coordinate. /// public record NurseSearchResultDto( long VariantId, @@ -16,4 +17,7 @@ public record NurseSearchResultDto( int TotalReviews, int TotalCompletedBookings, long CityId, - long? DistrictId); + long? DistrictId, + string NurseName, + string AvatarUrl, + double? DistanceKm); diff --git a/server/src/Core/Baya.Application/Models/Verification/VerificationDtos.cs b/server/src/Core/Baya.Application/Models/Verification/VerificationDtos.cs index 6745018..7359f0b 100644 --- a/server/src/Core/Baya.Application/Models/Verification/VerificationDtos.cs +++ b/server/src/Core/Baya.Application/Models/Verification/VerificationDtos.cs @@ -13,13 +13,15 @@ public record VerificationStepTypeDto( int SortOrder, bool IsActive); -/// One step in the nurse's checklist. Status is a snake_case code. +/// One step in the nurse's checklist. Status is a snake_case code. IsRequired mirrors +/// the step-type catalog — an optional step never blocks bookability. public record VerificationStepDto( long Id, string Code, string DisplayName, string Status, bool IsAutomated, + bool IsRequired, DateTimeOffset? ExpiresAt, string? FailureReason); diff --git a/server/src/Core/Baya.Domain/Entities/Bnpl/BnplProviderCodes.cs b/server/src/Core/Baya.Domain/Entities/Bnpl/BnplProviderCodes.cs index 8fbd6bb..e54acf9 100644 --- a/server/src/Core/Baya.Domain/Entities/Bnpl/BnplProviderCodes.cs +++ b/server/src/Core/Baya.Domain/Entities/Bnpl/BnplProviderCodes.cs @@ -13,7 +13,12 @@ public static class BnplProviderCodes public const string Tara = "tara"; public const string TorobPay = "torobpay"; - public static readonly IReadOnlyCollection All = [SnappPay, Digipay, Tara, TorobPay]; + /// Balinyaar's in-house installment plan. Modelled identically to the external providers + /// (net-of-fee card payment, provider-financed) so it selects the same mock adapter; the distinction is the + /// financing entity, not the money mechanics. + public const string Balinyaar = "balinyaar"; + + public static readonly IReadOnlyCollection All = [SnappPay, Digipay, Tara, TorobPay, Balinyaar]; public static bool IsKnown(string providerCode) => All.Contains(providerCode); } diff --git a/server/src/Core/Baya.Domain/Entities/Identity/CustomerAddress.cs b/server/src/Core/Baya.Domain/Entities/Identity/CustomerAddress.cs index 5ea3b5c..050dc5b 100644 --- a/server/src/Core/Baya.Domain/Entities/Identity/CustomerAddress.cs +++ b/server/src/Core/Baya.Domain/Entities/Identity/CustomerAddress.cs @@ -32,6 +32,11 @@ public class CustomerAddress : BaseEntity public decimal? Latitude { get; set; } public decimal? Longitude { get; set; } + /// How the stored coordinate was obtained: when the + /// customer dropped a pin (their exact door, preferred for the EVV distance check) or + /// when the server geocoded the street address. + public string GeocodeSource { get; set; } + public bool IsPrimary { get; set; } /// Encrypted at rest. diff --git a/server/src/Core/Baya.Domain/Entities/Identity/CustomerProfile.cs b/server/src/Core/Baya.Domain/Entities/Identity/CustomerProfile.cs index 64d6edb..80f1dda 100644 --- a/server/src/Core/Baya.Domain/Entities/Identity/CustomerProfile.cs +++ b/server/src/Core/Baya.Domain/Entities/Identity/CustomerProfile.cs @@ -18,5 +18,13 @@ public class CustomerProfile : BaseEntity /// Encrypted at rest. public string DefaultEmergencyContactPhone { get; set; } + /// Public URL of the customer's profile photo, stored via IObjectStorage. NULL until + /// one is uploaded. + public string AvatarUrl { get; set; } + + /// BCP-47-ish UI language code the customer prefers (e.g. fa, en). NULL falls + /// back to the platform default. + public string PreferredLanguage { get; set; } + public ICollection Patients { get; set; } } diff --git a/server/src/Core/Baya.Domain/Entities/Identity/GeocodeSources.cs b/server/src/Core/Baya.Domain/Entities/Identity/GeocodeSources.cs new file mode 100644 index 0000000..8e4318e --- /dev/null +++ b/server/src/Core/Baya.Domain/Entities/Identity/GeocodeSources.cs @@ -0,0 +1,11 @@ +namespace Baya.Domain.Entities.Identity; + +/// Stable codes for customer_addresses.geocode_source. +public static class GeocodeSources +{ + /// The customer dropped a pin — their exact door coordinate (preferred for EVV). + public const string UserPin = "user_pin"; + + /// The server geocoded the street address (a coarser centroid). + public const string Geocoder = "geocoder"; +} diff --git a/server/src/Core/Baya.Domain/Entities/Identity/NurseProfile.cs b/server/src/Core/Baya.Domain/Entities/Identity/NurseProfile.cs index 7fb86b1..4da3b09 100644 --- a/server/src/Core/Baya.Domain/Entities/Identity/NurseProfile.cs +++ b/server/src/Core/Baya.Domain/Entities/Identity/NurseProfile.cs @@ -24,6 +24,10 @@ public class NurseProfile : BaseEntity public string EducationField { get; set; } public string SpecializationsJson { get; set; } + /// Public URL of the nurse's profile photo, stored via IObjectStorage. NULL until the + /// nurse uploads one. Denormalized into nurse_search_index for the discovery card. + public string AvatarUrl { get; set; } + /// Write-guarded. Flipped ONLY inside the b6 verification-confirm transaction once every /// required verification step has passed — never from a profile command in this phase. A nurse is /// not bookable until this is true. diff --git a/server/src/Core/Baya.Domain/Entities/Identity/Patient.cs b/server/src/Core/Baya.Domain/Entities/Identity/Patient.cs index 10a9818..867ac82 100644 --- a/server/src/Core/Baya.Domain/Entities/Identity/Patient.cs +++ b/server/src/Core/Baya.Domain/Entities/Identity/Patient.cs @@ -23,6 +23,15 @@ public class Patient : BaseEntity public string BloodType { get; set; } + /// The care recipient's relation to the paying customer — a stable code + /// (parent/spouse/child/self). NULL when unspecified. + public string Relation { get; set; } + + /// The patient's care conditions as a JSON array of stable codes + /// (e.g. ["elderly","post_surgery"]). Stored as JSON rather than a child table because it is + /// only ever read/written whole with the patient (no server-side filtering on conditions at MVP). + public string ConditionsJson { get; set; } + /// Encrypted at rest. public string InitialMedicalNotes { get; set; } diff --git a/server/src/Core/Baya.Domain/Entities/Identity/PatientCarePlan.cs b/server/src/Core/Baya.Domain/Entities/Identity/PatientCarePlan.cs new file mode 100644 index 0000000..1dc0719 --- /dev/null +++ b/server/src/Core/Baya.Domain/Entities/Identity/PatientCarePlan.cs @@ -0,0 +1,27 @@ +using Baya.Domain.Common; + +namespace Baya.Domain.Entities.Identity; + +/// +/// The family-owned care plan for a patient — the customer's editable medications / daily routine / +/// task checklist. Distinct from the nurse-authored, encrypted clinical visit notes (PatientCareRecord): +/// this is patient-scoped (one row per patient, persists across nurse changes) and owned/edited by the paying +/// customer. The three lists are stored as JSON because they are always read/written whole with the plan (no +/// server-side querying into them). Not encrypted — it is the family's own working list, not clinical PII the +/// nurse authored. +/// +public class PatientCarePlan : BaseEntity +{ + public long PatientId { get; set; } + + /// JSON array of { id, name, dosage?, frequency, timingNote? }. + public string MedicationsJson { get; set; } = "[]"; + + /// JSON array of { id, label, timeOfDay?, note? }. + public string RoutineJson { get; set; } = "[]"; + + /// JSON array of { id, label, done }. + public string TasksJson { get; set; } = "[]"; + + public DateTimeOffset? DeletedAt { get; set; } +} diff --git a/server/src/Core/Baya.Domain/Entities/Messaging/TicketMessage.cs b/server/src/Core/Baya.Domain/Entities/Messaging/TicketMessage.cs index 4d2c23b..2285ad7 100644 --- a/server/src/Core/Baya.Domain/Entities/Messaging/TicketMessage.cs +++ b/server/src/Core/Baya.Domain/Entities/Messaging/TicketMessage.cs @@ -23,6 +23,10 @@ public class TicketMessage : BaseEntity /// user-facing thread read; only the admin view returns it. public bool IsInternal { get; set; } + /// Optional client-generated idempotency key for the optimistic composer — a retried send (network + /// flap) carrying the same key is deduplicated rather than posted twice. Unique per ticket when present. + public string? ClientMessageId { get; set; } + public DateTimeOffset SentAt { get; set; } public DateTimeOffset? DeletedAt { get; set; } diff --git a/server/src/Core/Baya.Domain/Entities/Messaging/TicketParticipant.cs b/server/src/Core/Baya.Domain/Entities/Messaging/TicketParticipant.cs index 258116d..b86aa63 100644 --- a/server/src/Core/Baya.Domain/Entities/Messaging/TicketParticipant.cs +++ b/server/src/Core/Baya.Domain/Entities/Messaging/TicketParticipant.cs @@ -22,6 +22,10 @@ public class TicketParticipant : BaseEntity public int? AddedById { get; set; } + /// When this participant last read the thread — drives the inbox unread count (messages after it, + /// internal excluded). Stamped when the participant fetches the thread. Null = never read. + public DateTimeOffset? LastReadAt { get; private set; } + /// Soft-remove stamp. null = an active participant; non-null = detached from the thread. public DateTimeOffset? RemovedAt { get; private set; } @@ -29,6 +33,8 @@ public class TicketParticipant : BaseEntity public void Remove(DateTimeOffset now) => RemovedAt = now; + public void MarkRead(DateTimeOffset now) => LastReadAt = now; + /// Re-attaches a previously-removed participant (the same UNIQUE row), clearing the removal stamp. public void Restore(int? addedById) { diff --git a/server/src/Core/Baya.Domain/Entities/Reviews/PatientCareRecord.cs b/server/src/Core/Baya.Domain/Entities/Reviews/PatientCareRecord.cs index e5f0249..677fcd0 100644 --- a/server/src/Core/Baya.Domain/Entities/Reviews/PatientCareRecord.cs +++ b/server/src/Core/Baya.Domain/Entities/Reviews/PatientCareRecord.cs @@ -29,6 +29,11 @@ public class PatientCareRecord : BaseEntity /// The clinical note, encrypted at rest via IFieldEncryptor. Never plaintext, never logged. public string BodyEncrypted { get; set; } = string.Empty; + /// The nurse's ticked task checklist for this visit, as a JSON array of + /// { label, done } — structural provenance of what was done (rendered as chips in the history). + /// NULL when the visit had no checklist. + public string? TaskResultsJson { get; set; } + /// When the note was recorded (UTC). Stored as datetime2 so the history read can order by it /// on both SQL Server and the SQLite test provider (which cannot translate DateTimeOffset ordering). public DateTime RecordedAt { get; set; } diff --git a/server/src/Core/Baya.Domain/Entities/Search/NurseSearchIndex.cs b/server/src/Core/Baya.Domain/Entities/Search/NurseSearchIndex.cs index d661503..f725eb4 100644 --- a/server/src/Core/Baya.Domain/Entities/Search/NurseSearchIndex.cs +++ b/server/src/Core/Baya.Domain/Entities/Search/NurseSearchIndex.cs @@ -49,6 +49,14 @@ public class NurseSearchIndex : BaseEntity /// Copied from users.gender via the nurse, for the first-class same-gender filter. public string NurseGender { get; set; } + /// The nurse's display name, denormalized from users so the discovery card renders a + /// real, named person without a per-row join. Recomputed on every nurse-level reindex. + public string NurseName { get; set; } + + /// The nurse's avatar URL, denormalized from nurse_profiles. NULL when the nurse has + /// not uploaded a photo (the card falls back to initials). + public string AvatarUrl { get; set; } + public decimal AverageRating { get; set; } public int TotalReviews { get; set; } public int TotalCompletedBookings { get; set; } diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/CustomerAddressConfig.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/CustomerAddressConfig.cs index 34a99e3..555f1a8 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/CustomerAddressConfig.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/CustomerAddressConfig.cs @@ -17,6 +17,7 @@ internal sealed class CustomerAddressConfig : IEntityTypeConfiguration a.Latitude).HasPrecision(9, 6); builder.Property(a => a.Longitude).HasPrecision(9, 6); + builder.Property(a => a.GeocodeSource).HasMaxLength(20); builder.Property(a => a.IsPrimary).HasDefaultValue(false); // Exactly one primary address per customer — the authoritative DB backstop the set-primary diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/CustomerProfileConfig.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/CustomerProfileConfig.cs index c1a7047..f8337b4 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/CustomerProfileConfig.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/CustomerProfileConfig.cs @@ -13,6 +13,9 @@ internal sealed class CustomerProfileConfig : IEntityTypeConfiguration c.AvatarUrl).HasMaxLength(2048); + builder.Property(c => c.PreferredLanguage).HasMaxLength(10); + builder.HasIndex(c => c.UserId).IsUnique(); builder.HasOne(c => c.User) .WithOne() diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/NurseProfileConfig.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/NurseProfileConfig.cs index 8ed05be..3544dcb 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/NurseProfileConfig.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/NurseProfileConfig.cs @@ -14,6 +14,7 @@ internal sealed class NurseProfileConfig : IEntityTypeConfiguration p.Bio).HasMaxLength(2000); builder.Property(p => p.EducationLevel).HasMaxLength(100); builder.Property(p => p.EducationField).HasMaxLength(150); + builder.Property(p => p.AvatarUrl).HasMaxLength(2048); builder.Property(p => p.IsVerified).HasDefaultValue(false); builder.Property(p => p.IsAcceptingBookings).HasDefaultValue(false); diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/PatientCarePlanConfig.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/PatientCarePlanConfig.cs new file mode 100644 index 0000000..2b9f087 --- /dev/null +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/PatientCarePlanConfig.cs @@ -0,0 +1,26 @@ +using Baya.Domain.Entities.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Baya.Infrastructure.Persistence.Configuration.IdentityConfig; + +internal sealed class PatientCarePlanConfig : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("PatientCarePlans", "usr"); + + // One family-owned plan per patient — the upsert target. + builder.HasIndex(p => p.PatientId) + .IsUnique() + .HasFilter("[DeletedAt] IS NULL") + .HasDatabaseName("UX_PatientCarePlans_Patient"); + + builder.HasOne() + .WithMany() + .HasForeignKey(p => p.PatientId) + .IsRequired(); + + builder.HasQueryFilter(p => p.DeletedAt == null); + } +} diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/PatientConfig.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/PatientConfig.cs index 01e95ae..3c20cfc 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/PatientConfig.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/IdentityConfig/PatientConfig.cs @@ -15,6 +15,7 @@ internal sealed class PatientConfig : IEntityTypeConfiguration builder.Property(p => p.LastName).HasMaxLength(100); builder.Property(p => p.Gender).HasMaxLength(10).IsRequired(); builder.Property(p => p.BloodType).HasMaxLength(10); + builder.Property(p => p.Relation).HasMaxLength(20); builder.Property(p => p.IsActive).HasDefaultValue(true); // initial_medical_notes is encrypted at rest (converter wired in ApplicationDbContext). diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/MessagingConfig/TicketMessageConfig.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/MessagingConfig/TicketMessageConfig.cs index 07294b5..68728b6 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/MessagingConfig/TicketMessageConfig.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/MessagingConfig/TicketMessageConfig.cs @@ -17,9 +17,16 @@ internal sealed class TicketMessageConfig : IEntityTypeConfiguration m.Body).HasMaxLength(4000).IsRequired(); builder.Property(m => m.IsInternal).HasDefaultValue(false); + builder.Property(m => m.ClientMessageId).HasMaxLength(100); builder.HasIndex(m => new { m.TicketId, m.SentAt }); + // Optimistic-send idempotency backstop: at most one message per (ticket, client_message_id). + builder.HasIndex(m => new { m.TicketId, m.ClientMessageId }) + .IsUnique() + .HasFilter("[ClientMessageId] IS NOT NULL AND [DeletedAt] IS NULL") + .HasDatabaseName("UX_TicketMessages_Ticket_ClientMessageId"); + builder.HasOne().WithMany().HasForeignKey(m => m.SenderId).IsRequired(); builder.HasQueryFilter(m => m.DeletedAt == null); diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/SearchConfig/NurseSearchIndexConfig.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/SearchConfig/NurseSearchIndexConfig.cs index cb8a447..a104315 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/SearchConfig/NurseSearchIndexConfig.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Configuration/SearchConfig/NurseSearchIndexConfig.cs @@ -15,13 +15,15 @@ internal sealed class NurseSearchIndexConfig : IEntityTypeConfiguration x.PriceUnit).HasMaxLength(20).IsRequired(); builder.Property(x => x.NurseGender).HasMaxLength(10); + builder.Property(x => x.NurseName).HasMaxLength(200); + builder.Property(x => x.AvatarUrl).HasMaxLength(2048); builder.Property(x => x.AverageRating).HasPrecision(3, 2); // The hot search path: filter on (is_searchable, category, city, district) then rating-sort + page. // INCLUDE the columns the projection reads so the filtered, sorted page is served straight from the // index with no key lookups (SQL Server; the INCLUDE annotation is ignored by other providers). builder.HasIndex(x => new { x.IsSearchable, x.ServiceCategoryId, x.CityId, x.DistrictId }) - .IncludeProperties(x => new { x.Price, x.NurseGender, x.AverageRating, x.TotalReviews, x.NurseId, x.VariantId }) + .IncludeProperties(x => new { x.Price, x.NurseGender, x.AverageRating, x.TotalReviews, x.NurseId, x.VariantId, x.NurseName }) .HasDatabaseName("IX_NurseSearchIndex_Search"); // Exactly one live row per (variant × area) — the upsert target and anti-duplication backstop. diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260712223125_RefinementPhase3TierA.Designer.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260712223125_RefinementPhase3TierA.Designer.cs new file mode 100644 index 0000000..609ed6c --- /dev/null +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260712223125_RefinementPhase3TierA.Designer.cs @@ -0,0 +1,5966 @@ +// +using System; +using Baya.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Baya.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260712223125_RefinementPhase3TierA")] + partial class RefinementPhase3TierA + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Baya.Domain.Entities.Analytics.SystemEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("OccurredAt") + .HasColumnType("datetimeoffset"); + + b.Property("PropsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("OccurredAt"); + + b.HasIndex("UserId"); + + b.ToTable("SystemEvents", "ops"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Audit.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ActorUserId") + .HasColumnType("int"); + + b.Property("ChangedFieldsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("OccurredAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("ActorUserId"); + + b.HasIndex("OccurredAt"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("AuditLogs", "ops"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Bnpl.BnplTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BnplCommissionIrr") + .HasColumnType("bigint"); + + b.Property("CallbackPayloadJson") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("nvarchar(5)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("EligibilityStatus") + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ExternalPaymentToken") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ExternalTransactionId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InstallmentCount") + .HasColumnType("tinyint"); + + b.Property("MerchantOfRecord") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("OrderAmountIrr") + .HasColumnType("bigint"); + + b.Property("PaymentTransactionId") + .HasColumnType("bigint"); + + b.Property("ProviderCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProviderCommissionReversedAmount") + .HasColumnType("bigint"); + + b.Property("RefundChannel") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RevertTransactionId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RevertedAmountIrr") + .HasColumnType("bigint"); + + b.Property("RevertedAt") + .HasColumnType("datetime2"); + + b.Property("SettledAmountIrr") + .HasColumnType("bigint"); + + b.Property("SettledAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.HasKey("Id"); + + b.HasIndex("ExternalPaymentToken") + .HasFilter("[ExternalPaymentToken] IS NOT NULL"); + + b.HasIndex("PaymentTransactionId") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("BnplTransactions", "payments", t => + { + t.HasCheckConstraint("CK_BnplTransactions_SettleSplit", "([SettledAmountIrr] IS NULL AND [BnplCommissionIrr] IS NULL) OR ([SettledAmountIrr] = [OrderAmountIrr] - [BnplCommissionIrr] AND [SettledAmountIrr] >= 0 AND [BnplCommissionIrr] >= 0)"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.Booking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AddressSnapshotJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BalinyaarCommissionIrr") + .HasColumnType("bigint"); + + b.Property("BookingRequestId") + .HasColumnType("bigint"); + + b.Property("CancellationPolicyCode") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CancellationReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("CancellationRefundPercentage") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("CancelledAt") + .HasColumnType("datetime2"); + + b.Property("CancelledBy") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("CompletedAt") + .HasColumnType("datetime2"); + + b.Property("ConfirmedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerAddressId") + .HasColumnType("bigint"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DisputeWindowEndsAt") + .HasColumnType("datetime2"); + + b.Property("GrossPriceIrr") + .HasColumnType("bigint"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("NursePayoutAmount") + .HasColumnType("bigint"); + + b.Property("PartnerCenterId") + .HasColumnType("bigint"); + + b.Property("PatientId") + .HasColumnType("bigint"); + + b.Property("PlatformFeeRate") + .HasPrecision(5, 4) + .HasColumnType("decimal(5,4)"); + + b.Property("PspFeeAmount") + .HasColumnType("bigint"); + + b.Property("RefundableAmountIrr") + .HasColumnType("bigint"); + + b.Property("ScheduledDate") + .HasColumnType("date"); + + b.Property("ScheduledTimeEnd") + .HasColumnType("time"); + + b.Property("ScheduledTimeStart") + .HasColumnType("time"); + + b.Property("SessionCount") + .HasColumnType("smallint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("VariantId") + .HasColumnType("bigint"); + + b.Property("VariantSnapshotJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BookingRequestId") + .IsUnique(); + + b.HasIndex("CustomerAddressId"); + + b.HasIndex("DisputeWindowEndsAt"); + + b.HasIndex("PatientId"); + + b.HasIndex("VariantId"); + + b.HasIndex("CustomerId", "Status"); + + b.HasIndex("NurseId", "Status"); + + b.ToTable("Bookings", "booking", t => + { + t.HasCheckConstraint("CK_Bookings_AmountSplit", "[GrossPriceIrr] = [BalinyaarCommissionIrr] + [NursePayoutAmount] AND [GrossPriceIrr] >= 0 AND [BalinyaarCommissionIrr] >= 0 AND [NursePayoutAmount] >= 0"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingCareInstruction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Allergies") + .HasColumnType("nvarchar(max)"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CurrentConditions") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("EmergencyContactName") + .HasColumnType("nvarchar(max)"); + + b.Property("EmergencyContactPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("Medications") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("SpecialInstructions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique(); + + b.ToTable("BookingCareInstructions", "booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerAddressId") + .HasColumnType("bigint"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("CustomerNotes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("NurseRejectionReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("NurseResponseDeadlineAt") + .HasColumnType("datetime2"); + + b.Property("PatientId") + .HasColumnType("bigint"); + + b.Property("PaymentDeadlineAt") + .HasColumnType("datetime2"); + + b.Property("RequestedDate") + .HasColumnType("date"); + + b.Property("RequestedTimeEnd") + .HasColumnType("time"); + + b.Property("RequestedTimeStart") + .HasColumnType("time"); + + b.Property("RequiredCaregiverGender") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("VariantId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CustomerAddressId"); + + b.HasIndex("PatientId"); + + b.HasIndex("VariantId"); + + b.HasIndex("CustomerId", "Status"); + + b.HasIndex("NurseId", "Status"); + + b.HasIndex("Status", "NurseResponseDeadlineAt"); + + b.HasIndex("Status", "PaymentDeadlineAt"); + + b.ToTable("BookingRequests", "booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CancellationEventId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PayoutEligibleAt") + .HasColumnType("datetime2"); + + b.Property("ScheduledDate") + .HasColumnType("date"); + + b.Property("ScheduledTimeEnd") + .HasColumnType("time"); + + b.Property("ScheduledTimeStart") + .HasColumnType("time"); + + b.Property("SessionIndex") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("VisitPayoutAmount") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("BookingId", "SessionIndex"); + + b.HasIndex("Status", "ScheduledDate"); + + b.ToTable("BookingSessions", "booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.CancellationPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppliesTo") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("FeeAmountIrr") + .HasColumnType("bigint"); + + b.Property("FeeRate") + .HasPrecision(5, 4) + .HasColumnType("decimal(5,4)"); + + b.Property("HoursBeforeStartMax") + .HasColumnType("int"); + + b.Property("HoursBeforeStartMin") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("RefundPercentage") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("AppliesTo", "IsActive"); + + b.ToTable("CancellationPolicies", "booking"); + + b.HasData( + new + { + Id = 1L, + AppliesTo = "customer", + Code = "standard_24h", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + FeeAmountIrr = 0L, + HoursBeforeStartMin = 24, + IsActive = true, + RefundPercentage = 100m + }, + new + { + Id = 2L, + AppliesTo = "customer", + Code = "standard_inside_24h", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + FeeAmountIrr = 0L, + HoursBeforeStartMax = 24, + IsActive = true, + RefundPercentage = 50m + }, + new + { + Id = 3L, + AppliesTo = "nurse", + Code = "nurse_no_show", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + FeeAmountIrr = 0L, + FeeRate = 0m, + IsActive = true, + RefundPercentage = 100m + }, + new + { + Id = 4L, + AppliesTo = "admin", + Code = "admin_cancellation", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + FeeAmountIrr = 0L, + IsActive = true, + RefundPercentage = 100m + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.VisitVerification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingSessionId") + .HasColumnType("bigint"); + + b.Property("CheckInAddressMatch") + .HasColumnType("bit"); + + b.Property("CheckInAt") + .HasColumnType("datetime2"); + + b.Property("CheckInDistanceMeters") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("CheckInLat") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("CheckInLng") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("CheckOutAt") + .HasColumnType("datetime2"); + + b.Property("CheckOutLat") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("CheckOutLng") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("BookingSessionId") + .IsUnique(); + + b.HasIndex("CheckInAddressMatch"); + + b.ToTable("VisitVerifications", "booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("OptionSetHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("PriceUnit") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ServiceCategoryId") + .HasColumnType("bigint"); + + b.Property("SessionCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ServiceCategoryId"); + + b.HasIndex("NurseId", "IsActive"); + + b.HasIndex("NurseId", "ServiceCategoryId", "OptionSetHash") + .IsUnique() + .HasDatabaseName("UX_NurseServiceVariants_Nurse_Category_OptionSet") + .HasFilter("[DeletedAt] IS NULL"); + + b.ToTable("NurseServiceVariants", "catalog"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariantOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("OptionGroupId") + .HasColumnType("bigint"); + + b.Property("OptionValueId") + .HasColumnType("bigint"); + + b.Property("VariantId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OptionGroupId"); + + b.HasIndex("OptionValueId"); + + b.HasIndex("VariantId", "OptionGroupId") + .IsUnique() + .HasDatabaseName("UX_NurseServiceVariantOptions_Variant_Group"); + + b.ToTable("NurseServiceVariantOptions", "catalog"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DescriptionEn") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DescriptionFa") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IconKey") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("IsActive", "SortOrder"); + + b.ToTable("ServiceCategories", "catalog"); + + b.HasData( + new + { + Id = 1L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Elderly Care", + NameFa = "مراقبت از سالمند", + SortOrder = 1 + }, + new + { + Id = 2L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Post-Surgery Recovery", + NameFa = "مراقبت پس از جراحی", + SortOrder = 2 + }, + new + { + Id = 3L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Infant Care", + NameFa = "مراقبت از نوزاد", + SortOrder = 3 + }, + new + { + Id = 4L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Chronic Illness Management", + NameFa = "مدیریت بیماری مزمن", + SortOrder = 4 + }, + new + { + Id = 5L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Companionship", + NameFa = "همراهی و مراقبت روزمره", + SortOrder = 5 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsRequired") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("ServiceCategoryId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("ServiceCategoryId", "SortOrder"); + + b.ToTable("ServiceOptionGroups", "catalog"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("OptionGroupId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("OptionGroupId", "SortOrder"); + + b.ToTable("ServiceOptionValues", "catalog"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Configuration.PlatformConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DataType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformConfigs", "ops"); + + b.HasData( + new + { + Id = 1L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "Balinyaar commission rate on the booking gross (fraction).", + Key = "platform_fee_rate", + Value = "0.15" + }, + new + { + Id = 2L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "VAT rate applied to the commission line only (fraction).", + Key = "vat_rate", + Value = "0.10" + }, + new + { + Id = 3L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Hours after check-out a booking can be disputed.", + Key = "dispute_window_hours", + Value = "72" + }, + new + { + Id = 4L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Minutes a family has to pay before a pending booking expires.", + Key = "booking_payment_deadline_minutes", + Value = "30" + }, + new + { + Id = 5L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Hours a nurse has to accept/decline a booking request.", + Key = "nurse_response_deadline_hours", + Value = "24" + }, + new + { + Id = 6L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Weekly payout cadence in days.", + Key = "nurse_payout_interval_days", + Value = "7" + }, + new + { + Id = 7L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Allowed EVV check-in distance from the care address.", + Key = "evv_location_tolerance_meters", + Value = "200" + }, + new + { + Id = 8L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "A review at or below this rating raises a support alert.", + Key = "min_rating_for_support_alert", + Value = "2" + }, + new + { + Id = 9L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "string", + Description = "Who is merchant of record for BNPL orders (platform|nurse).", + Key = "bnpl_merchant_of_record", + Value = "platform" + }, + new + { + Id = 10L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "BNPL provider commission rate (fraction).", + Key = "bnpl_provider_commission_rate", + Value = "0.07" + }, + new + { + Id = 11L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "string", + Description = "When BNPL settles funds to the platform (immediate|deferred).", + Key = "bnpl_settlement_timing", + Value = "immediate" + }, + new + { + Id = 12L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "json", + Description = "Tiered cancellation refund policy: refund_percent by hours before the visit.", + Key = "cancellation_tiers", + Value = "[{\"min_hours_before\":48,\"refund_percent\":100},{\"min_hours_before\":24,\"refund_percent\":50},{\"min_hours_before\":0,\"refund_percent\":0}]" + }, + new + { + Id = 13L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Seconds a phone must wait before another OTP can be requested.", + Key = "auth_otp_resend_seconds", + Value = "120" + }, + new + { + Id = 14L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Wrong-code attempts allowed before OTP verification is refused until a fresh code.", + Key = "auth_otp_max_attempts", + Value = "5" + }, + new + { + Id = 15L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Refresh-token session lifetime in days.", + Key = "auth_session_ttl_days", + Value = "30" + }, + new + { + Id = 16L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Hours between credential-expiry scans (the scheduled cron is deferred; the scan is admin-triggered today).", + Key = "verification_expiry_scan_cadence_hours", + Value = "24" + }, + new + { + Id = 17L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Minutes after a session's scheduled start with no EVV check-in before it is flagged a no-show.", + Key = "no_show_threshold_minutes", + Value = "60" + }, + new + { + Id = 18L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Hours between no-show sweeps (the scheduled cron is deferred; the sweep is admin-triggered today).", + Key = "no_show_scan_cadence_hours", + Value = "1" + }, + new + { + Id = 19L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "bool", + Description = "Whether a refund must link a support ticket (b11). Off until b15 ships the tickets table.", + Key = "refund_ticket_required", + Value = "false" + }, + new + { + Id = 20L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Business days shown as the customer BNPL refund ETA (b11).", + Key = "bnpl_refund_eta_business_days", + Value = "10" + }, + new + { + Id = 21L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "bool", + Description = "Ops/testing override that forces the post-payout clawback path for refunds (b11); b13 replaces the derivation.", + Key = "refund_assume_nurse_paid", + Value = "false" + }, + new + { + Id = 22L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "IRR net-amount threshold above which a payout is routed via SATNA (real-time) instead of PAYA (batch) (b13).", + Key = "payout_satna_threshold_irr", + Value = "1000000000" + }, + new + { + Id = 23L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "bool", + Description = "When on, a BNPL-paid booking is payout-eligible only after its provider settlement is received (b13; default off — the DEFERRED settled_at guard).", + Key = "require_bnpl_settlement_for_payout", + Value = "false" + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("ProvinceId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("ProvinceId", "SortOrder"); + + b.ToTable("Cities", "geo"); + + b.HasData( + new + { + Id = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Tehran", + NameFa = "تهران", + ProvinceId = 1L, + SortOrder = 1 + }, + new + { + Id = 102L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Karaj", + NameFa = "کرج", + ProvinceId = 2L, + SortOrder = 1 + }, + new + { + Id = 103L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Isfahan", + NameFa = "اصفهان", + ProvinceId = 3L, + SortOrder = 1 + }, + new + { + Id = 104L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Shiraz", + NameFa = "شیراز", + ProvinceId = 4L, + SortOrder = 1 + }, + new + { + Id = 105L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Mashhad", + NameFa = "مشهد", + ProvinceId = 5L, + SortOrder = 1 + }, + new + { + Id = 106L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Tabriz", + NameFa = "تبریز", + ProvinceId = 6L, + SortOrder = 1 + }, + new + { + Id = 107L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Urmia", + NameFa = "ارومیه", + ProvinceId = 7L, + SortOrder = 1 + }, + new + { + Id = 108L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ahvaz", + NameFa = "اهواز", + ProvinceId = 8L, + SortOrder = 1 + }, + new + { + Id = 109L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Qom", + NameFa = "قم", + ProvinceId = 9L, + SortOrder = 1 + }, + new + { + Id = 110L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kerman", + NameFa = "کرمان", + ProvinceId = 10L, + SortOrder = 1 + }, + new + { + Id = 111L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Rasht", + NameFa = "رشت", + ProvinceId = 11L, + SortOrder = 1 + }, + new + { + Id = 112L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Sari", + NameFa = "ساری", + ProvinceId = 12L, + SortOrder = 1 + }, + new + { + Id = 113L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Arak", + NameFa = "اراک", + ProvinceId = 13L, + SortOrder = 1 + }, + new + { + Id = 114L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ardabil", + NameFa = "اردبیل", + ProvinceId = 14L, + SortOrder = 1 + }, + new + { + Id = 115L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Qazvin", + NameFa = "قزوین", + ProvinceId = 15L, + SortOrder = 1 + }, + new + { + Id = 116L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kermanshah", + NameFa = "کرمانشاه", + ProvinceId = 16L, + SortOrder = 1 + }, + new + { + Id = 117L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Bojnord", + NameFa = "بجنورد", + ProvinceId = 17L, + SortOrder = 1 + }, + new + { + Id = 118L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Birjand", + NameFa = "بیرجند", + ProvinceId = 18L, + SortOrder = 1 + }, + new + { + Id = 119L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Hamadan", + NameFa = "همدان", + ProvinceId = 19L, + SortOrder = 1 + }, + new + { + Id = 120L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Sanandaj", + NameFa = "سنندج", + ProvinceId = 20L, + SortOrder = 1 + }, + new + { + Id = 121L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Khorramabad", + NameFa = "خرم‌آباد", + ProvinceId = 21L, + SortOrder = 1 + }, + new + { + Id = 122L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Gorgan", + NameFa = "گرگان", + ProvinceId = 22L, + SortOrder = 1 + }, + new + { + Id = 123L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Bandar Abbas", + NameFa = "بندرعباس", + ProvinceId = 23L, + SortOrder = 1 + }, + new + { + Id = 124L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Bushehr", + NameFa = "بوشهر", + ProvinceId = 24L, + SortOrder = 1 + }, + new + { + Id = 125L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Zanjan", + NameFa = "زنجان", + ProvinceId = 25L, + SortOrder = 1 + }, + new + { + Id = 126L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Semnan", + NameFa = "سمنان", + ProvinceId = 26L, + SortOrder = 1 + }, + new + { + Id = 127L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Yazd", + NameFa = "یزد", + ProvinceId = 27L, + SortOrder = 1 + }, + new + { + Id = 128L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Zahedan", + NameFa = "زاهدان", + ProvinceId = 28L, + SortOrder = 1 + }, + new + { + Id = 129L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Shahrekord", + NameFa = "شهرکرد", + ProvinceId = 29L, + SortOrder = 1 + }, + new + { + Id = 130L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Yasuj", + NameFa = "یاسوج", + ProvinceId = 30L, + SortOrder = 1 + }, + new + { + Id = 131L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ilam", + NameFa = "ایلام", + ProvinceId = 31L, + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.District", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CityId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("CityId", "SortOrder"); + + b.ToTable("Districts", "geo"); + + b.HasData( + new + { + Id = 1001L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 1", + NameFa = "منطقه ۱", + SortOrder = 1 + }, + new + { + Id = 1002L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 2", + NameFa = "منطقه ۲", + SortOrder = 2 + }, + new + { + Id = 1003L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 3", + NameFa = "منطقه ۳", + SortOrder = 3 + }, + new + { + Id = 1004L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 4", + NameFa = "منطقه ۴", + SortOrder = 4 + }, + new + { + Id = 1005L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 5", + NameFa = "منطقه ۵", + SortOrder = 5 + }, + new + { + Id = 1006L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 6", + NameFa = "منطقه ۶", + SortOrder = 6 + }, + new + { + Id = 1007L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 7", + NameFa = "منطقه ۷", + SortOrder = 7 + }, + new + { + Id = 1008L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 8", + NameFa = "منطقه ۸", + SortOrder = 8 + }, + new + { + Id = 1009L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 9", + NameFa = "منطقه ۹", + SortOrder = 9 + }, + new + { + Id = 1010L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 10", + NameFa = "منطقه ۱۰", + SortOrder = 10 + }, + new + { + Id = 1011L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 11", + NameFa = "منطقه ۱۱", + SortOrder = 11 + }, + new + { + Id = 1012L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 12", + NameFa = "منطقه ۱۲", + SortOrder = 12 + }, + new + { + Id = 1013L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 13", + NameFa = "منطقه ۱۳", + SortOrder = 13 + }, + new + { + Id = 1014L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 14", + NameFa = "منطقه ۱۴", + SortOrder = 14 + }, + new + { + Id = 1015L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 15", + NameFa = "منطقه ۱۵", + SortOrder = 15 + }, + new + { + Id = 1016L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 16", + NameFa = "منطقه ۱۶", + SortOrder = 16 + }, + new + { + Id = 1017L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 17", + NameFa = "منطقه ۱۷", + SortOrder = 17 + }, + new + { + Id = 1018L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 18", + NameFa = "منطقه ۱۸", + SortOrder = 18 + }, + new + { + Id = 1019L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 19", + NameFa = "منطقه ۱۹", + SortOrder = 19 + }, + new + { + Id = 1020L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 20", + NameFa = "منطقه ۲۰", + SortOrder = 20 + }, + new + { + Id = 1021L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 21", + NameFa = "منطقه ۲۱", + SortOrder = 21 + }, + new + { + Id = 1022L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 22", + NameFa = "منطقه ۲۲", + SortOrder = 22 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.NurseServiceArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CityId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DistrictId") + .HasColumnType("bigint"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.HasIndex("DistrictId"); + + b.HasIndex("NurseId", "CityId") + .IsUnique() + .HasDatabaseName("UX_NurseServiceAreas_Nurse_City_WholeCity") + .HasFilter("[DistrictId] IS NULL AND [DeletedAt] IS NULL"); + + b.HasIndex("NurseId", "CityId", "DistrictId") + .IsUnique() + .HasDatabaseName("UX_NurseServiceAreas_Nurse_City_District") + .HasFilter("[DistrictId] IS NOT NULL AND [DeletedAt] IS NULL"); + + b.ToTable("NurseServiceAreas", "geo"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.Province", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("SortOrder"); + + b.ToTable("Provinces", "geo"); + + b.HasData( + new + { + Id = 1L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Tehran", + NameFa = "تهران", + SortOrder = 1 + }, + new + { + Id = 2L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Alborz", + NameFa = "البرز", + SortOrder = 2 + }, + new + { + Id = 3L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Isfahan", + NameFa = "اصفهان", + SortOrder = 3 + }, + new + { + Id = 4L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Fars", + NameFa = "فارس", + SortOrder = 4 + }, + new + { + Id = 5L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Razavi Khorasan", + NameFa = "خراسان رضوی", + SortOrder = 5 + }, + new + { + Id = 6L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "East Azerbaijan", + NameFa = "آذربایجان شرقی", + SortOrder = 6 + }, + new + { + Id = 7L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "West Azerbaijan", + NameFa = "آذربایجان غربی", + SortOrder = 7 + }, + new + { + Id = 8L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Khuzestan", + NameFa = "خوزستان", + SortOrder = 8 + }, + new + { + Id = 9L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Qom", + NameFa = "قم", + SortOrder = 9 + }, + new + { + Id = 10L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kerman", + NameFa = "کرمان", + SortOrder = 10 + }, + new + { + Id = 11L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Gilan", + NameFa = "گیلان", + SortOrder = 11 + }, + new + { + Id = 12L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Mazandaran", + NameFa = "مازندران", + SortOrder = 12 + }, + new + { + Id = 13L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Markazi", + NameFa = "مرکزی", + SortOrder = 13 + }, + new + { + Id = 14L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ardabil", + NameFa = "اردبیل", + SortOrder = 14 + }, + new + { + Id = 15L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Qazvin", + NameFa = "قزوین", + SortOrder = 15 + }, + new + { + Id = 16L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kermanshah", + NameFa = "کرمانشاه", + SortOrder = 16 + }, + new + { + Id = 17L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "North Khorasan", + NameFa = "خراسان شمالی", + SortOrder = 17 + }, + new + { + Id = 18L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "South Khorasan", + NameFa = "خراسان جنوبی", + SortOrder = 18 + }, + new + { + Id = 19L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Hamadan", + NameFa = "همدان", + SortOrder = 19 + }, + new + { + Id = 20L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kurdistan", + NameFa = "کردستان", + SortOrder = 20 + }, + new + { + Id = 21L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Lorestan", + NameFa = "لرستان", + SortOrder = 21 + }, + new + { + Id = 22L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Golestan", + NameFa = "گلستان", + SortOrder = 22 + }, + new + { + Id = 23L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Hormozgan", + NameFa = "هرمزگان", + SortOrder = 23 + }, + new + { + Id = 24L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Bushehr", + NameFa = "بوشهر", + SortOrder = 24 + }, + new + { + Id = 25L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Zanjan", + NameFa = "زنجان", + SortOrder = 25 + }, + new + { + Id = 26L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Semnan", + NameFa = "سمنان", + SortOrder = 26 + }, + new + { + Id = 27L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Yazd", + NameFa = "یزد", + SortOrder = 27 + }, + new + { + Id = 28L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Sistan and Baluchestan", + NameFa = "سیستان و بلوچستان", + SortOrder = 28 + }, + new + { + Id = 29L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Chaharmahal and Bakhtiari", + NameFa = "چهارمحال و بختیاری", + SortOrder = 29 + }, + new + { + Id = 30L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kohgiluyeh and Boyer-Ahmad", + NameFa = "کهگیلویه و بویراحمد", + SortOrder = 30 + }, + new + { + Id = 31L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ilam", + NameFa = "ایلام", + SortOrder = 31 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Holidays.IranianHoliday", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("HolidayDate") + .HasColumnType("date"); + + b.Property("IsBankClosed") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("HolidayDate") + .IsUnique(); + + b.ToTable("IranianHolidays", "ops"); + + b.HasData( + new + { + Id = 1L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 2, 11), + IsBankClosed = true, + NameFa = "پیروزی انقلاب اسلامی", + Type = "national" + }, + new + { + Id = 2L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 3, 21), + IsBankClosed = true, + NameFa = "نوروز", + Type = "national" + }, + new + { + Id = 3L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 3, 22), + IsBankClosed = true, + NameFa = "نوروز", + Type = "national" + }, + new + { + Id = 4L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 3, 23), + IsBankClosed = true, + NameFa = "نوروز", + Type = "national" + }, + new + { + Id = 5L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 3, 24), + IsBankClosed = true, + NameFa = "نوروز", + Type = "national" + }, + new + { + Id = 6L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 4, 1), + IsBankClosed = true, + NameFa = "روز طبیعت (سیزده‌به‌در)", + Type = "official" + }, + new + { + Id = 7L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 6, 26), + IsBankClosed = true, + NameFa = "عید سعید قربان", + Type = "religious" + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AddressLine") + .HasColumnType("nvarchar(max)"); + + b.Property("CityId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DistrictId") + .HasColumnType("bigint"); + + b.Property("GeocodeSource") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PostalCode") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipientName") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipientPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.HasIndex("CustomerId") + .IsUnique() + .HasDatabaseName("UX_CustomerAddresses_Customer_Primary") + .HasFilter("[IsPrimary] = 1 AND [DeletedAt] IS NULL"); + + b.HasIndex("DistrictId"); + + b.ToTable("CustomerAddresses", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DefaultEmergencyContactName") + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultEmergencyContactPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PreferredLanguage") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("CustomerProfiles", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseBankAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountHolderFromBank") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("AccountHolderName") + .HasColumnType("nvarchar(max)"); + + b.Property("BankName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("Iban") + .HasColumnType("nvarchar(max)"); + + b.Property("IbanHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsVerified") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("MatchedNationalId") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("OwnershipVendorRef") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("VerifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("VerifiedByAdminId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IbanHash") + .IsUnique(); + + b.HasIndex("NurseId") + .IsUnique() + .HasDatabaseName("UX_NurseBankAccounts_NurseId_Primary") + .HasFilter("[IsPrimary] = 1"); + + b.HasIndex("VerifiedByAdminId"); + + b.ToTable("NurseBankAccounts", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("AverageRating") + .ValueGeneratedOnAdd() + .HasPrecision(3, 2) + .HasColumnType("decimal(3,2)") + .HasDefaultValue(0m); + + b.Property("Bio") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("EducationField") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("EducationLevel") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsAcceptingBookings") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsVerified") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PartnerCenterId") + .HasColumnType("bigint"); + + b.Property("SpecializationsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("TotalCompletedBookings") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("TotalReviews") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("YearsOfExperience") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PartnerCenterId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("NurseProfiles", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.Patient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("BloodType") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("ConditionsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("FirstName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Gender") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("InitialMedicalNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("LastName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Relation") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Patients", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Invoices.Invoice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BnplCommissionIrr") + .HasColumnType("bigint"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("GrossIrr") + .HasColumnType("bigint"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("IssuedAt") + .HasColumnType("datetime2"); + + b.Property("IssuingEntityType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("MoadianReferenceNumber") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("MoadianStatus") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PartnerCenterId") + .HasColumnType("bigint"); + + b.Property("PdfStorageKey") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("PlatformCommissionIrr") + .HasColumnType("bigint"); + + b.Property("VatIrr") + .HasColumnType("bigint"); + + b.Property("VatRate") + .HasPrecision(5, 4) + .HasColumnType("decimal(5,4)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique(); + + b.HasIndex("InvoiceNumber") + .IsUnique(); + + b.ToTable("Invoices", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Invoices.InvoiceNumberSequence", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("NextValue") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("InvoiceNumberSequences", "payments"); + + b.HasData( + new + { + Id = 1, + NextValue = 1L + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ClosedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ClosedById") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("OpenedById") + .HasColumnType("int"); + + b.Property("ReferenceCode") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("RefundId") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Subject") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("ClosedById"); + + b.HasIndex("OpenedById"); + + b.HasIndex("ReferenceCode") + .IsUnique(); + + b.HasIndex("RefundId"); + + b.HasIndex("Status"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("Tickets", "messaging"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.TicketMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsInternal") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("SenderId") + .HasColumnType("int"); + + b.Property("SentAt") + .HasColumnType("datetimeoffset"); + + b.Property("TicketId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SenderId"); + + b.HasIndex("TicketId", "SentAt"); + + b.ToTable("TicketMessages", "messaging"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.TicketParticipant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AddedById") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("RemovedAt") + .HasColumnType("datetimeoffset"); + + b.Property("RoleOnTicket") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("TicketId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AddedById"); + + b.HasIndex("TicketId", "UserId") + .IsUnique(); + + b.HasIndex("UserId", "TicketId"); + + b.ToTable("TicketParticipants", "messaging"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Notifications.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Body") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DataJson") + .HasColumnType("nvarchar(max)"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ReadAt") + .HasColumnType("datetimeoffset"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "IsRead", "CreatedAt"); + + b.ToTable("Notifications", "ops"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.PartnerCenters.PartnerCenter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminUserId") + .HasColumnType("int"); + + b.Property("CommissionRate") + .HasPrecision(5, 4) + .HasColumnType("decimal(5,4)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("EnamadCode") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsMerchantOfRecord") + .HasColumnType("bit"); + + b.Property("LegalEntityType") + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("MohEstablishmentPermitNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("SettlementIban") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TechnicalDirectorLicenseNo") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("TechnicalDirectorNurseUserId") + .HasColumnType("int"); + + b.Property("VerifiedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("AdminUserId"); + + b.HasIndex("IsActive"); + + b.HasIndex("TechnicalDirectorNurseUserId"); + + b.ToTable("PartnerCenters", "partner"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.LedgerEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountType") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("AmountIrr") + .HasColumnType("bigint"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Direction") + .IsRequired() + .HasMaxLength(6) + .HasColumnType("nvarchar(6)"); + + b.Property("Memo") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("SourceRefId") + .HasColumnType("bigint"); + + b.Property("SourceRefType") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("TransactionGroupId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("NurseId"); + + b.HasIndex("TransactionGroupId"); + + b.HasIndex("AccountType", "NurseId"); + + b.HasIndex("SourceRefType", "SourceRefId"); + + b.ToTable("LedgerEntries", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("ProviderCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Type", "IsActive", "Priority"); + + b.ToTable("PaymentGateways", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.PaymentTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("BookingRequestId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("GatewayId") + .HasColumnType("bigint"); + + b.Property("GatewayReferenceCode") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("GatewayResponseCode") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GatewayResponseJson") + .HasColumnType("nvarchar(max)"); + + b.Property("GatewayTransactionId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("IsInstallment") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("UserAgent") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique() + .HasFilter("[Status] = 'succeeded' AND [BookingId] IS NOT NULL"); + + b.HasIndex("CustomerId"); + + b.HasIndex("GatewayId"); + + b.HasIndex("GatewayReferenceCode") + .IsUnique() + .HasFilter("[GatewayReferenceCode] IS NOT NULL"); + + b.HasIndex("BookingId", "Status"); + + b.HasIndex("BookingRequestId", "Status"); + + b.ToTable("PaymentTransactions", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.PaymentWebhookEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("nvarchar(80)"); + + b.Property("ExternalEventId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProcessedAt") + .HasColumnType("datetime2"); + + b.Property("ProcessingStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ProviderCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ReceivedAt") + .HasColumnType("datetime2"); + + b.Property("RelatedPaymentTransactionId") + .HasColumnType("bigint"); + + b.Property("SignatureValid") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("ProviderCode", "ExternalEventId") + .IsUnique(); + + b.ToTable("PaymentWebhookEvents", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("BankAccountId") + .HasColumnType("bigint"); + + b.Property("BatchId") + .HasColumnType("bigint"); + + b.Property("BookingCount") + .HasColumnType("int"); + + b.Property("ClawbackAppliedIrr") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("FailureReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("GrossEarningsIrr") + .HasColumnType("bigint"); + + b.Property("IbanSnapshot") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NetAmountIrr") + .HasColumnType("bigint"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("PaidAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("TransferReference") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("BankAccountId"); + + b.HasIndex("BatchId"); + + b.HasIndex("NurseId"); + + b.HasIndex("Status"); + + b.ToTable("NursePayouts", "payouts", t => + { + t.HasCheckConstraint("CK_NursePayouts_NetSplit", "[NetAmountIrr] = [GrossEarningsIrr] - [ClawbackAppliedIrr] AND [GrossEarningsIrr] >= 0 AND [ClawbackAppliedIrr] >= 0 AND [NetAmountIrr] >= 0 AND [Amount] >= 0"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("FailureNotes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("InitiatedByAdminId") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PayoutCount") + .HasColumnType("int"); + + b.Property("PeriodEnd") + .HasColumnType("date"); + + b.Property("PeriodStart") + .HasColumnType("date"); + + b.Property("ProcessedAt") + .HasColumnType("datetime2"); + + b.Property("ProcessingDate") + .HasColumnType("date"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("TotalAmount") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InitiatedByAdminId"); + + b.HasIndex("ProcessingDate"); + + b.HasIndex("Status"); + + b.ToTable("NursePayoutBatches", "payouts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBookingLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PayoutAmountIrr") + .HasColumnType("bigint"); + + b.Property("PayoutId") + .HasColumnType("bigint"); + + b.Property("SessionId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique(); + + b.HasIndex("PayoutId"); + + b.HasIndex("SessionId"); + + b.ToTable("NursePayoutBookingLinks", "payouts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Refunds.NurseClawback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AmountIrr") + .HasColumnType("bigint"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("OriginalPayoutId") + .HasColumnType("bigint"); + + b.Property("RecoveredInPayoutId") + .HasColumnType("bigint"); + + b.Property("RefundId") + .HasColumnType("bigint"); + + b.Property("ResolutionNotes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ResolvedAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("NurseId"); + + b.HasIndex("OriginalPayoutId"); + + b.HasIndex("RecoveredInPayoutId"); + + b.HasIndex("RefundId") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("NurseClawbacks", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Refunds.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminNotes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("ApprovedByAdminId") + .HasColumnType("int"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CancellationPolicyCode") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ExpectedCustomerRefundEta") + .HasColumnType("date"); + + b.Property("ExternalRevertReference") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("GatewayRefundReference") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NursePayoutRefundedIrr") + .HasColumnType("bigint"); + + b.Property("PaymentTransactionId") + .HasColumnType("bigint"); + + b.Property("PlatformFeeRefundedIrr") + .HasColumnType("bigint"); + + b.Property("ProcessedAt") + .HasColumnType("datetime2"); + + b.Property("ReasonCategory") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ReasonNotes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RefundChannel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RefundPercentage") + .HasPrecision(6, 4) + .HasColumnType("decimal(6,4)"); + + b.Property("RefundPercentageApplied") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("RejectedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RequestedByCustomerId") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("TicketId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("PaymentTransactionId"); + + b.HasIndex("RequestedByCustomerId"); + + b.HasIndex("Status"); + + b.HasIndex("TicketId"); + + b.ToTable("Refunds", "payments", t => + { + t.HasCheckConstraint("CK_Refunds_LegSplit", "[Amount] = [PlatformFeeRefundedIrr] + [NursePayoutRefundedIrr] AND [Amount] >= 0 AND [PlatformFeeRefundedIrr] >= 0 AND [NursePayoutRefundedIrr] >= 0"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.PatientCareRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BodyEncrypted") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseProfileId") + .HasColumnType("bigint"); + + b.Property("PatientId") + .HasColumnType("bigint"); + + b.Property("RecordedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("NurseProfileId"); + + b.HasIndex("PatientId", "RecordedAt") + .HasDatabaseName("IX_PatientCareRecords_Patient_RecordedAt"); + + b.ToTable("PatientCareRecords", "reviews"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Body") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerProfileId") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModeratedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModeratedById") + .HasColumnType("int"); + + b.Property("ModerationReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ModerationStatus") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseProfileId") + .HasColumnType("bigint"); + + b.Property("Rating") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique(); + + b.HasIndex("CustomerProfileId"); + + b.HasIndex("ModerationStatus"); + + b.HasIndex("NurseProfileId", "ModerationStatus"); + + b.ToTable("Reviews", "reviews", t => + { + t.HasCheckConstraint("CK_Reviews_Rating", "[Rating] BETWEEN 1 AND 5"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.ReviewTagLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("ReviewId") + .HasColumnType("bigint"); + + b.Property("ReviewTagMasterId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ReviewTagMasterId"); + + b.HasIndex("ReviewId", "ReviewTagMasterId") + .IsUnique() + .HasDatabaseName("UX_ReviewTagLinks_Review_Tag"); + + b.ToTable("ReviewTagLinks", "reviews"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.ReviewTagMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("LabelEn") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LabelFa") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsActive", "SortOrder"); + + b.ToTable("ReviewTagsMasters", "reviews"); + + b.HasData( + new + { + Id = 1L, + Code = "punctual", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Punctual", + LabelFa = "وقت‌شناس", + SortOrder = 1 + }, + new + { + Id = 2L, + Code = "professional", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Professional", + LabelFa = "حرفه‌ای", + SortOrder = 2 + }, + new + { + Id = 3L, + Code = "clean", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Clean", + LabelFa = "تمیز و بهداشتی", + SortOrder = 3 + }, + new + { + Id = 4L, + Code = "kind", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Kind", + LabelFa = "مهربان", + SortOrder = 4 + }, + new + { + Id = 5L, + Code = "communicative", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Communicative", + LabelFa = "خوش‌برخورد", + SortOrder = 5 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Search.NurseSearchIndex", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("AverageRating") + .HasPrecision(3, 2) + .HasColumnType("decimal(3,2)"); + + b.Property("CityId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DistrictId") + .HasColumnType("bigint"); + + b.Property("IsSearchable") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseGender") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("NurseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("PriceUnit") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ServiceCategoryId") + .HasColumnType("bigint"); + + b.Property("TotalCompletedBookings") + .HasColumnType("int"); + + b.Property("TotalReviews") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("VariantId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("NurseId"); + + b.HasIndex("VariantId", "CityId") + .IsUnique() + .HasDatabaseName("UX_NurseSearchIndex_Variant_City_WholeCity") + .HasFilter("[DistrictId] IS NULL AND [DeletedAt] IS NULL"); + + b.HasIndex("VariantId", "CityId", "DistrictId") + .IsUnique() + .HasDatabaseName("UX_NurseSearchIndex_Variant_City_District") + .HasFilter("[DistrictId] IS NOT NULL AND [DeletedAt] IS NULL"); + + b.HasIndex("IsSearchable", "ServiceCategoryId", "CityId", "DistrictId") + .HasDatabaseName("IX_NurseSearchIndex_Search"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("IsSearchable", "ServiceCategoryId", "CityId", "DistrictId"), new[] { "Price", "NurseGender", "AverageRating", "TotalReviews", "NurseId", "VariantId", "NurseName" }); + + b.ToTable("NurseSearchIndices", "search"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.SupportAlerts.SupportAlert", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("OwnerUserId") + .HasColumnType("int"); + + b.Property("ResolutionNote") + .HasColumnType("nvarchar(max)"); + + b.Property("ResolvedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ReviewId") + .HasColumnType("bigint"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.HasKey("Id"); + + b.HasIndex("OwnerUserId"); + + b.HasIndex("Status"); + + b.HasIndex("Type"); + + b.ToTable("SupportAlerts", "ops"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedClaim") + .HasColumnType("datetime2"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("UserId"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FamilyName") + .HasColumnType("nvarchar(max)"); + + b.Property("Gender") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("GeneratedCode") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NationalId") + .HasColumnType("nvarchar(max)"); + + b.Property("NationalIdVerifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("PhoneVerifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("ShahkarVerifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("PhoneHash") + .IsUnique() + .HasFilter("[PhoneHash] IS NOT NULL"); + + b.ToTable("Users", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("LoggedOn") + .HasColumnType("datetime2"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("IsValid") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRefreshTokens", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.Property("CreatedUserRoleDate") + .HasColumnType("datetime2"); + + b.Property("GrantedAt") + .HasColumnType("datetimeoffset"); + + b.Property("GrantedById") + .HasColumnType("int"); + + b.Property("RevokedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("GrantedById"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeviceInfo") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("ExpiresAt") + .HasColumnType("datetimeoffset"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("RefreshTokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("RevokedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RefreshTokenHash") + .IsUnique(); + + b.HasIndex("UserId", "IsRevoked"); + + b.ToTable("UserSessions", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("GeneratedTime") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseCredential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CredentialNumber") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("CredentialType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ExpiresAt") + .HasColumnType("date"); + + b.Property("HolderNameSnapshot") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IssuedAt") + .HasColumnType("date"); + + b.Property("IssuingAuthority") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("VerificationMethod") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("VerificationSource") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("VerifiedByAdminId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("VerifiedByAdminId"); + + b.HasIndex("NurseId", "CredentialType"); + + b.ToTable("NurseCredentials", "verif"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseVerification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApprovedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("InternalNotes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("RejectedAt") + .HasColumnType("datetimeoffset"); + + b.Property("RejectionReason") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ReviewedByAdminId") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("SubmittedAt") + .HasColumnType("datetimeoffset"); + + b.Property("SuspendedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("NurseId") + .IsUnique(); + + b.HasIndex("ReviewedByAdminId"); + + b.ToTable("NurseVerifications", "verif"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("IntegrityHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("ObjectStorageKey") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("OriginalFileName") + .HasMaxLength(260) + .HasColumnType("nvarchar(260)"); + + b.Property("StepId") + .HasColumnType("bigint"); + + b.Property("UploadedByUserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("StepId"); + + b.HasIndex("UploadedByUserId"); + + b.ToTable("VerificationDocuments", "verif"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("ExpiresAt") + .HasColumnType("datetimeoffset"); + + b.Property("ExternalResponseJson") + .HasColumnType("nvarchar(max)"); + + b.Property("FailureReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsAutomated") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseVerificationId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("StepTypeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("StepTypeId"); + + b.HasIndex("NurseVerificationId", "StepTypeId") + .IsUnique() + .HasDatabaseName("UX_VerificationSteps_Verification_StepType"); + + b.ToTable("VerificationSteps", "verif"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStepType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AutomationProvider") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsAutomated") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsRequired") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsActive", "SortOrder"); + + b.ToTable("VerificationStepTypes", "verif"); + + b.HasData( + new + { + Id = 1L, + AutomationProvider = "identity_kyc_vendor", + Code = "identity_kyc", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "National-ID validity, name match and photo/video liveness via an Iranian e-KYC vendor.", + DisplayName = "Identity Verification (KYC)", + IsActive = true, + IsAutomated = true, + IsRequired = true, + SortOrder = 1 + }, + new + { + Id = 2L, + AutomationProvider = "shahkar", + Code = "shahkar_match", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Confirms the login SIM is registered to the nurse's own national ID (شاهکار).", + DisplayName = "Shahkar Phone Binding", + IsActive = true, + IsAutomated = true, + IsRequired = true, + SortOrder = 2 + }, + new + { + Id = 3L, + Code = "moh_competency_license", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "پروانه صلاحیت حرفه‌ای — the MoH-mandated in-home nursing licence (bundles the criminal-record screen). Manual today.", + DisplayName = "MoH Professional Competency License", + IsActive = true, + IsAutomated = false, + IsRequired = true, + SortOrder = 3 + }, + new + { + Id = 4L, + Code = "ino_membership", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "نظام پرستاری membership cross-check (ino.ir). Manual today.", + DisplayName = "Nursing Organization (INO) Membership", + IsActive = true, + IsAutomated = false, + IsRequired = true, + SortOrder = 4 + }, + new + { + Id = 5L, + Code = "criminal_record", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "عدم سوء پیشینه — consent-gated, nurse-uploaded, time-limited (reverts on expiry).", + DisplayName = "Criminal Record Certificate", + IsActive = true, + IsAutomated = false, + IsRequired = true, + SortOrder = 5 + }, + new + { + Id = 6L, + AutomationProvider = "sheba", + Code = "bank_account_verification", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "استعلام شبا — the payout IBAN owner's national ID must equal the verified nurse national ID.", + DisplayName = "Bank Account (IBAN) Ownership", + IsActive = true, + IsAutomated = true, + IsRequired = true, + SortOrder = 6 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Analytics.SystemEvent", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Audit.AuditLog", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("ActorUserId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Bnpl.BnplTransaction", b => + { + b.HasOne("Baya.Domain.Entities.Payments.PaymentTransaction", null) + .WithMany() + .HasForeignKey("PaymentTransactionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.Booking", b => + { + b.HasOne("Baya.Domain.Entities.Booking.BookingRequest", null) + .WithOne() + .HasForeignKey("Baya.Domain.Entities.Booking.Booking", "BookingRequestId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerAddress", null) + .WithMany() + .HasForeignKey("CustomerAddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", null) + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.Patient", null) + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.NurseServiceVariant", null) + .WithMany() + .HasForeignKey("VariantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingCareInstruction", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", "Booking") + .WithOne("CareInstructions") + .HasForeignKey("Baya.Domain.Entities.Booking.BookingCareInstruction", "BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingRequest", b => + { + b.HasOne("Baya.Domain.Entities.Identity.CustomerAddress", "CustomerAddress") + .WithMany() + .HasForeignKey("CustomerAddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse") + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.Patient", "Patient") + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.NurseServiceVariant", "Variant") + .WithMany() + .HasForeignKey("VariantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("CustomerAddress"); + + b.Navigation("Nurse"); + + b.Navigation("Patient"); + + b.Navigation("Variant"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingSession", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", "Booking") + .WithMany("Sessions") + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.VisitVerification", b => + { + b.HasOne("Baya.Domain.Entities.Booking.BookingSession", "Session") + .WithOne("Verification") + .HasForeignKey("Baya.Domain.Entities.Booking.VisitVerification", "BookingSessionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariant", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse") + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.ServiceCategory", "ServiceCategory") + .WithMany("Variants") + .HasForeignKey("ServiceCategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Nurse"); + + b.Navigation("ServiceCategory"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariantOption", b => + { + b.HasOne("Baya.Domain.Entities.Catalog.ServiceOptionGroup", "OptionGroup") + .WithMany() + .HasForeignKey("OptionGroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.ServiceOptionValue", "OptionValue") + .WithMany() + .HasForeignKey("OptionValueId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.NurseServiceVariant", "Variant") + .WithMany("Options") + .HasForeignKey("VariantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("OptionGroup"); + + b.Navigation("OptionValue"); + + b.Navigation("Variant"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionGroup", b => + { + b.HasOne("Baya.Domain.Entities.Catalog.ServiceCategory", "ServiceCategory") + .WithMany("OptionGroups") + .HasForeignKey("ServiceCategoryId"); + + b.Navigation("ServiceCategory"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionValue", b => + { + b.HasOne("Baya.Domain.Entities.Catalog.ServiceOptionGroup", "OptionGroup") + .WithMany("Values") + .HasForeignKey("OptionGroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("OptionGroup"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.City", b => + { + b.HasOne("Baya.Domain.Entities.Geography.Province", "Province") + .WithMany("Cities") + .HasForeignKey("ProvinceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Province"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.District", b => + { + b.HasOne("Baya.Domain.Entities.Geography.City", "City") + .WithMany("Districts") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("City"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.NurseServiceArea", b => + { + b.HasOne("Baya.Domain.Entities.Geography.City", "City") + .WithMany() + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Geography.District", "District") + .WithMany() + .HasForeignKey("DistrictId"); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("City"); + + b.Navigation("District"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerAddress", b => + { + b.HasOne("Baya.Domain.Entities.Geography.City", "City") + .WithMany() + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Geography.District", "District") + .WithMany() + .HasForeignKey("DistrictId"); + + b.Navigation("City"); + + b.Navigation("Customer"); + + b.Navigation("District"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerProfile", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithOne() + .HasForeignKey("Baya.Domain.Entities.Identity.CustomerProfile", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseBankAccount", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse") + .WithMany("BankAccounts") + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("VerifiedByAdminId"); + + b.Navigation("Nurse"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseProfile", b => + { + b.HasOne("Baya.Domain.Entities.PartnerCenters.PartnerCenter", null) + .WithMany() + .HasForeignKey("PartnerCenterId"); + + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithOne() + .HasForeignKey("Baya.Domain.Entities.Identity.NurseProfile", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.Patient", b => + { + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", "Customer") + .WithMany("Patients") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Invoices.Invoice", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.Ticket", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId"); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("ClosedById"); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("OpenedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Refunds.Refund", null) + .WithMany() + .HasForeignKey("RefundId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.TicketMessage", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Messaging.Ticket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.TicketParticipant", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("AddedById"); + + b.HasOne("Baya.Domain.Entities.Messaging.Ticket", "Ticket") + .WithMany("Participants") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Notifications.Notification", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.PartnerCenters.PartnerCenter", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("AdminUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("TechnicalDirectorNurseUserId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.LedgerEntry", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId"); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.PaymentTransaction", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId"); + + b.HasOne("Baya.Domain.Entities.Booking.BookingRequest", null) + .WithMany() + .HasForeignKey("BookingRequestId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", null) + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Payments.PaymentGateway", null) + .WithMany() + .HasForeignKey("GatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayout", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseBankAccount", null) + .WithMany() + .HasForeignKey("BankAccountId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Payouts.NursePayoutBatch", "Batch") + .WithMany("Payouts") + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Batch"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBatch", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("InitiatedByAdminId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBookingLink", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Payouts.NursePayout", null) + .WithMany("BookingLinks") + .HasForeignKey("PayoutId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Booking.BookingSession", null) + .WithMany() + .HasForeignKey("SessionId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Refunds.NurseClawback", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Refunds.Refund", null) + .WithMany() + .HasForeignKey("RefundId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Refunds.Refund", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Payments.PaymentTransaction", null) + .WithMany() + .HasForeignKey("PaymentTransactionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", null) + .WithMany() + .HasForeignKey("RequestedByCustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.PatientCareRecord", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId"); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseProfileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.Patient", null) + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.Review", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", null) + .WithMany() + .HasForeignKey("CustomerProfileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseProfileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.ReviewTagLink", b => + { + b.HasOne("Baya.Domain.Entities.Reviews.Review", "Review") + .WithMany("TagLinks") + .HasForeignKey("ReviewId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Reviews.ReviewTagMaster", "Tag") + .WithMany("Links") + .HasForeignKey("ReviewTagMasterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Review"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Search.NurseSearchIndex", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.NurseServiceVariant", "Variant") + .WithMany() + .HasForeignKey("VariantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Variant"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.SupportAlerts.SupportAlert", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("OwnerUserId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.RoleClaim", b => + { + b.HasOne("Baya.Domain.Entities.User.Role", "Role") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserClaim", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserLogin", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserRefreshToken", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("UserRefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserRole", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("GrantedById"); + + b.HasOne("Baya.Domain.Entities.User.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserSession", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("Sessions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserToken", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseCredential", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("VerifiedByAdminId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseVerification", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse") + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("ReviewedByAdminId"); + + b.Navigation("Nurse"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationDocument", b => + { + b.HasOne("Baya.Domain.Entities.Verification.VerificationStep", "Step") + .WithMany("Documents") + .HasForeignKey("StepId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("UploadedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStep", b => + { + b.HasOne("Baya.Domain.Entities.Verification.NurseVerification", "NurseVerification") + .WithMany("Steps") + .HasForeignKey("NurseVerificationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Verification.VerificationStepType", "StepType") + .WithMany("Steps") + .HasForeignKey("StepTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("NurseVerification"); + + b.Navigation("StepType"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.Booking", b => + { + b.Navigation("CareInstructions"); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingSession", b => + { + b.Navigation("Verification"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariant", b => + { + b.Navigation("Options"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceCategory", b => + { + b.Navigation("OptionGroups"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionGroup", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.City", b => + { + b.Navigation("Districts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.Province", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerProfile", b => + { + b.Navigation("Patients"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseProfile", b => + { + b.Navigation("BankAccounts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.Ticket", b => + { + b.Navigation("Messages"); + + b.Navigation("Participants"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayout", b => + { + b.Navigation("BookingLinks"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBatch", b => + { + b.Navigation("Payouts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.Review", b => + { + b.Navigation("TagLinks"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.ReviewTagMaster", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.Role", b => + { + b.Navigation("Claims"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.User", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("Sessions"); + + b.Navigation("Tokens"); + + b.Navigation("UserRefreshTokens"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseVerification", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStep", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStepType", b => + { + b.Navigation("Steps"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260712223125_RefinementPhase3TierA.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260712223125_RefinementPhase3TierA.cs new file mode 100644 index 0000000..b186959 --- /dev/null +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260712223125_RefinementPhase3TierA.cs @@ -0,0 +1,145 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Baya.Infrastructure.Persistence.Migrations +{ + /// + public partial class RefinementPhase3TierA : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_NurseSearchIndex_Search", + schema: "search", + table: "NurseSearchIndices"); + + migrationBuilder.AddColumn( + name: "ConditionsJson", + schema: "usr", + table: "Patients", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "Relation", + schema: "usr", + table: "Patients", + type: "nvarchar(20)", + maxLength: 20, + nullable: true); + + migrationBuilder.AddColumn( + name: "AvatarUrl", + schema: "search", + table: "NurseSearchIndices", + type: "nvarchar(2048)", + maxLength: 2048, + nullable: true); + + migrationBuilder.AddColumn( + name: "NurseName", + schema: "search", + table: "NurseSearchIndices", + type: "nvarchar(200)", + maxLength: 200, + nullable: true); + + migrationBuilder.AddColumn( + name: "AvatarUrl", + schema: "usr", + table: "NurseProfiles", + type: "nvarchar(2048)", + maxLength: 2048, + nullable: true); + + migrationBuilder.AddColumn( + name: "AvatarUrl", + schema: "usr", + table: "CustomerProfiles", + type: "nvarchar(2048)", + maxLength: 2048, + nullable: true); + + migrationBuilder.AddColumn( + name: "PreferredLanguage", + schema: "usr", + table: "CustomerProfiles", + type: "nvarchar(10)", + maxLength: 10, + nullable: true); + + migrationBuilder.AddColumn( + name: "GeocodeSource", + schema: "usr", + table: "CustomerAddresses", + type: "nvarchar(20)", + maxLength: 20, + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_NurseSearchIndex_Search", + schema: "search", + table: "NurseSearchIndices", + columns: new[] { "IsSearchable", "ServiceCategoryId", "CityId", "DistrictId" }) + .Annotation("SqlServer:Include", new[] { "Price", "NurseGender", "AverageRating", "TotalReviews", "NurseId", "VariantId", "NurseName" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_NurseSearchIndex_Search", + schema: "search", + table: "NurseSearchIndices"); + + migrationBuilder.DropColumn( + name: "ConditionsJson", + schema: "usr", + table: "Patients"); + + migrationBuilder.DropColumn( + name: "Relation", + schema: "usr", + table: "Patients"); + + migrationBuilder.DropColumn( + name: "AvatarUrl", + schema: "search", + table: "NurseSearchIndices"); + + migrationBuilder.DropColumn( + name: "NurseName", + schema: "search", + table: "NurseSearchIndices"); + + migrationBuilder.DropColumn( + name: "AvatarUrl", + schema: "usr", + table: "NurseProfiles"); + + migrationBuilder.DropColumn( + name: "AvatarUrl", + schema: "usr", + table: "CustomerProfiles"); + + migrationBuilder.DropColumn( + name: "PreferredLanguage", + schema: "usr", + table: "CustomerProfiles"); + + migrationBuilder.DropColumn( + name: "GeocodeSource", + schema: "usr", + table: "CustomerAddresses"); + + migrationBuilder.CreateIndex( + name: "IX_NurseSearchIndex_Search", + schema: "search", + table: "NurseSearchIndices", + columns: new[] { "IsSearchable", "ServiceCategoryId", "CityId", "DistrictId" }) + .Annotation("SqlServer:Include", new[] { "Price", "NurseGender", "AverageRating", "TotalReviews", "NurseId", "VariantId" }); + } + } +} diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713065646_RefinementPhase3TierB.Designer.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713065646_RefinementPhase3TierB.Designer.cs new file mode 100644 index 0000000..19d4c35 --- /dev/null +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713065646_RefinementPhase3TierB.Designer.cs @@ -0,0 +1,6023 @@ +// +using System; +using Baya.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Baya.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260713065646_RefinementPhase3TierB")] + partial class RefinementPhase3TierB + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Baya.Domain.Entities.Analytics.SystemEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("OccurredAt") + .HasColumnType("datetimeoffset"); + + b.Property("PropsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("OccurredAt"); + + b.HasIndex("UserId"); + + b.ToTable("SystemEvents", "ops"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Audit.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ActorUserId") + .HasColumnType("int"); + + b.Property("ChangedFieldsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("OccurredAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("ActorUserId"); + + b.HasIndex("OccurredAt"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("AuditLogs", "ops"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Bnpl.BnplTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BnplCommissionIrr") + .HasColumnType("bigint"); + + b.Property("CallbackPayloadJson") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("nvarchar(5)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("EligibilityStatus") + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ExternalPaymentToken") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ExternalTransactionId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InstallmentCount") + .HasColumnType("tinyint"); + + b.Property("MerchantOfRecord") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("OrderAmountIrr") + .HasColumnType("bigint"); + + b.Property("PaymentTransactionId") + .HasColumnType("bigint"); + + b.Property("ProviderCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProviderCommissionReversedAmount") + .HasColumnType("bigint"); + + b.Property("RefundChannel") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RevertTransactionId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RevertedAmountIrr") + .HasColumnType("bigint"); + + b.Property("RevertedAt") + .HasColumnType("datetime2"); + + b.Property("SettledAmountIrr") + .HasColumnType("bigint"); + + b.Property("SettledAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.HasKey("Id"); + + b.HasIndex("ExternalPaymentToken") + .HasFilter("[ExternalPaymentToken] IS NOT NULL"); + + b.HasIndex("PaymentTransactionId") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("BnplTransactions", "payments", t => + { + t.HasCheckConstraint("CK_BnplTransactions_SettleSplit", "([SettledAmountIrr] IS NULL AND [BnplCommissionIrr] IS NULL) OR ([SettledAmountIrr] = [OrderAmountIrr] - [BnplCommissionIrr] AND [SettledAmountIrr] >= 0 AND [BnplCommissionIrr] >= 0)"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.Booking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AddressSnapshotJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BalinyaarCommissionIrr") + .HasColumnType("bigint"); + + b.Property("BookingRequestId") + .HasColumnType("bigint"); + + b.Property("CancellationPolicyCode") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CancellationReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("CancellationRefundPercentage") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("CancelledAt") + .HasColumnType("datetime2"); + + b.Property("CancelledBy") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("CompletedAt") + .HasColumnType("datetime2"); + + b.Property("ConfirmedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerAddressId") + .HasColumnType("bigint"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DisputeWindowEndsAt") + .HasColumnType("datetime2"); + + b.Property("GrossPriceIrr") + .HasColumnType("bigint"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("NursePayoutAmount") + .HasColumnType("bigint"); + + b.Property("PartnerCenterId") + .HasColumnType("bigint"); + + b.Property("PatientId") + .HasColumnType("bigint"); + + b.Property("PlatformFeeRate") + .HasPrecision(5, 4) + .HasColumnType("decimal(5,4)"); + + b.Property("PspFeeAmount") + .HasColumnType("bigint"); + + b.Property("RefundableAmountIrr") + .HasColumnType("bigint"); + + b.Property("ScheduledDate") + .HasColumnType("date"); + + b.Property("ScheduledTimeEnd") + .HasColumnType("time"); + + b.Property("ScheduledTimeStart") + .HasColumnType("time"); + + b.Property("SessionCount") + .HasColumnType("smallint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("VariantId") + .HasColumnType("bigint"); + + b.Property("VariantSnapshotJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BookingRequestId") + .IsUnique(); + + b.HasIndex("CustomerAddressId"); + + b.HasIndex("DisputeWindowEndsAt"); + + b.HasIndex("PatientId"); + + b.HasIndex("VariantId"); + + b.HasIndex("CustomerId", "Status"); + + b.HasIndex("NurseId", "Status"); + + b.ToTable("Bookings", "booking", t => + { + t.HasCheckConstraint("CK_Bookings_AmountSplit", "[GrossPriceIrr] = [BalinyaarCommissionIrr] + [NursePayoutAmount] AND [GrossPriceIrr] >= 0 AND [BalinyaarCommissionIrr] >= 0 AND [NursePayoutAmount] >= 0"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingCareInstruction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Allergies") + .HasColumnType("nvarchar(max)"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CurrentConditions") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("EmergencyContactName") + .HasColumnType("nvarchar(max)"); + + b.Property("EmergencyContactPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("Medications") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("SpecialInstructions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique(); + + b.ToTable("BookingCareInstructions", "booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerAddressId") + .HasColumnType("bigint"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("CustomerNotes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("NurseRejectionReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("NurseResponseDeadlineAt") + .HasColumnType("datetime2"); + + b.Property("PatientId") + .HasColumnType("bigint"); + + b.Property("PaymentDeadlineAt") + .HasColumnType("datetime2"); + + b.Property("RequestedDate") + .HasColumnType("date"); + + b.Property("RequestedTimeEnd") + .HasColumnType("time"); + + b.Property("RequestedTimeStart") + .HasColumnType("time"); + + b.Property("RequiredCaregiverGender") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("VariantId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CustomerAddressId"); + + b.HasIndex("PatientId"); + + b.HasIndex("VariantId"); + + b.HasIndex("CustomerId", "Status"); + + b.HasIndex("NurseId", "Status"); + + b.HasIndex("Status", "NurseResponseDeadlineAt"); + + b.HasIndex("Status", "PaymentDeadlineAt"); + + b.ToTable("BookingRequests", "booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CancellationEventId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PayoutEligibleAt") + .HasColumnType("datetime2"); + + b.Property("ScheduledDate") + .HasColumnType("date"); + + b.Property("ScheduledTimeEnd") + .HasColumnType("time"); + + b.Property("ScheduledTimeStart") + .HasColumnType("time"); + + b.Property("SessionIndex") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("VisitPayoutAmount") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("BookingId", "SessionIndex"); + + b.HasIndex("Status", "ScheduledDate"); + + b.ToTable("BookingSessions", "booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.CancellationPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppliesTo") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("FeeAmountIrr") + .HasColumnType("bigint"); + + b.Property("FeeRate") + .HasPrecision(5, 4) + .HasColumnType("decimal(5,4)"); + + b.Property("HoursBeforeStartMax") + .HasColumnType("int"); + + b.Property("HoursBeforeStartMin") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("RefundPercentage") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("AppliesTo", "IsActive"); + + b.ToTable("CancellationPolicies", "booking"); + + b.HasData( + new + { + Id = 1L, + AppliesTo = "customer", + Code = "standard_24h", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + FeeAmountIrr = 0L, + HoursBeforeStartMin = 24, + IsActive = true, + RefundPercentage = 100m + }, + new + { + Id = 2L, + AppliesTo = "customer", + Code = "standard_inside_24h", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + FeeAmountIrr = 0L, + HoursBeforeStartMax = 24, + IsActive = true, + RefundPercentage = 50m + }, + new + { + Id = 3L, + AppliesTo = "nurse", + Code = "nurse_no_show", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + FeeAmountIrr = 0L, + FeeRate = 0m, + IsActive = true, + RefundPercentage = 100m + }, + new + { + Id = 4L, + AppliesTo = "admin", + Code = "admin_cancellation", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + FeeAmountIrr = 0L, + IsActive = true, + RefundPercentage = 100m + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.VisitVerification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingSessionId") + .HasColumnType("bigint"); + + b.Property("CheckInAddressMatch") + .HasColumnType("bit"); + + b.Property("CheckInAt") + .HasColumnType("datetime2"); + + b.Property("CheckInDistanceMeters") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("CheckInLat") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("CheckInLng") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("CheckOutAt") + .HasColumnType("datetime2"); + + b.Property("CheckOutLat") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("CheckOutLng") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("BookingSessionId") + .IsUnique(); + + b.HasIndex("CheckInAddressMatch"); + + b.ToTable("VisitVerifications", "booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("OptionSetHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("PriceUnit") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ServiceCategoryId") + .HasColumnType("bigint"); + + b.Property("SessionCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ServiceCategoryId"); + + b.HasIndex("NurseId", "IsActive"); + + b.HasIndex("NurseId", "ServiceCategoryId", "OptionSetHash") + .IsUnique() + .HasDatabaseName("UX_NurseServiceVariants_Nurse_Category_OptionSet") + .HasFilter("[DeletedAt] IS NULL"); + + b.ToTable("NurseServiceVariants", "catalog"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariantOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("OptionGroupId") + .HasColumnType("bigint"); + + b.Property("OptionValueId") + .HasColumnType("bigint"); + + b.Property("VariantId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OptionGroupId"); + + b.HasIndex("OptionValueId"); + + b.HasIndex("VariantId", "OptionGroupId") + .IsUnique() + .HasDatabaseName("UX_NurseServiceVariantOptions_Variant_Group"); + + b.ToTable("NurseServiceVariantOptions", "catalog"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DescriptionEn") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DescriptionFa") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IconKey") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("IsActive", "SortOrder"); + + b.ToTable("ServiceCategories", "catalog"); + + b.HasData( + new + { + Id = 1L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Elderly Care", + NameFa = "مراقبت از سالمند", + SortOrder = 1 + }, + new + { + Id = 2L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Post-Surgery Recovery", + NameFa = "مراقبت پس از جراحی", + SortOrder = 2 + }, + new + { + Id = 3L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Infant Care", + NameFa = "مراقبت از نوزاد", + SortOrder = 3 + }, + new + { + Id = 4L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Chronic Illness Management", + NameFa = "مدیریت بیماری مزمن", + SortOrder = 4 + }, + new + { + Id = 5L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Companionship", + NameFa = "همراهی و مراقبت روزمره", + SortOrder = 5 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsRequired") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("ServiceCategoryId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("ServiceCategoryId", "SortOrder"); + + b.ToTable("ServiceOptionGroups", "catalog"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("OptionGroupId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("OptionGroupId", "SortOrder"); + + b.ToTable("ServiceOptionValues", "catalog"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Configuration.PlatformConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DataType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformConfigs", "ops"); + + b.HasData( + new + { + Id = 1L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "Balinyaar commission rate on the booking gross (fraction).", + Key = "platform_fee_rate", + Value = "0.15" + }, + new + { + Id = 2L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "VAT rate applied to the commission line only (fraction).", + Key = "vat_rate", + Value = "0.10" + }, + new + { + Id = 3L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Hours after check-out a booking can be disputed.", + Key = "dispute_window_hours", + Value = "72" + }, + new + { + Id = 4L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Minutes a family has to pay before a pending booking expires.", + Key = "booking_payment_deadline_minutes", + Value = "30" + }, + new + { + Id = 5L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Hours a nurse has to accept/decline a booking request.", + Key = "nurse_response_deadline_hours", + Value = "24" + }, + new + { + Id = 6L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Weekly payout cadence in days.", + Key = "nurse_payout_interval_days", + Value = "7" + }, + new + { + Id = 7L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Allowed EVV check-in distance from the care address.", + Key = "evv_location_tolerance_meters", + Value = "200" + }, + new + { + Id = 8L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "A review at or below this rating raises a support alert.", + Key = "min_rating_for_support_alert", + Value = "2" + }, + new + { + Id = 9L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "string", + Description = "Who is merchant of record for BNPL orders (platform|nurse).", + Key = "bnpl_merchant_of_record", + Value = "platform" + }, + new + { + Id = 10L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "BNPL provider commission rate (fraction).", + Key = "bnpl_provider_commission_rate", + Value = "0.07" + }, + new + { + Id = 11L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "string", + Description = "When BNPL settles funds to the platform (immediate|deferred).", + Key = "bnpl_settlement_timing", + Value = "immediate" + }, + new + { + Id = 12L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "json", + Description = "Tiered cancellation refund policy: refund_percent by hours before the visit.", + Key = "cancellation_tiers", + Value = "[{\"min_hours_before\":48,\"refund_percent\":100},{\"min_hours_before\":24,\"refund_percent\":50},{\"min_hours_before\":0,\"refund_percent\":0}]" + }, + new + { + Id = 13L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Seconds a phone must wait before another OTP can be requested.", + Key = "auth_otp_resend_seconds", + Value = "120" + }, + new + { + Id = 14L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Wrong-code attempts allowed before OTP verification is refused until a fresh code.", + Key = "auth_otp_max_attempts", + Value = "5" + }, + new + { + Id = 15L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Refresh-token session lifetime in days.", + Key = "auth_session_ttl_days", + Value = "30" + }, + new + { + Id = 16L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Hours between credential-expiry scans (the scheduled cron is deferred; the scan is admin-triggered today).", + Key = "verification_expiry_scan_cadence_hours", + Value = "24" + }, + new + { + Id = 17L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Minutes after a session's scheduled start with no EVV check-in before it is flagged a no-show.", + Key = "no_show_threshold_minutes", + Value = "60" + }, + new + { + Id = 18L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Hours between no-show sweeps (the scheduled cron is deferred; the sweep is admin-triggered today).", + Key = "no_show_scan_cadence_hours", + Value = "1" + }, + new + { + Id = 19L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "bool", + Description = "Whether a refund must link a support ticket (b11). Off until b15 ships the tickets table.", + Key = "refund_ticket_required", + Value = "false" + }, + new + { + Id = 20L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Business days shown as the customer BNPL refund ETA (b11).", + Key = "bnpl_refund_eta_business_days", + Value = "10" + }, + new + { + Id = 21L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "bool", + Description = "Ops/testing override that forces the post-payout clawback path for refunds (b11); b13 replaces the derivation.", + Key = "refund_assume_nurse_paid", + Value = "false" + }, + new + { + Id = 22L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "IRR net-amount threshold above which a payout is routed via SATNA (real-time) instead of PAYA (batch) (b13).", + Key = "payout_satna_threshold_irr", + Value = "1000000000" + }, + new + { + Id = 23L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "bool", + Description = "When on, a BNPL-paid booking is payout-eligible only after its provider settlement is received (b13; default off — the DEFERRED settled_at guard).", + Key = "require_bnpl_settlement_for_payout", + Value = "false" + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("ProvinceId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("ProvinceId", "SortOrder"); + + b.ToTable("Cities", "geo"); + + b.HasData( + new + { + Id = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Tehran", + NameFa = "تهران", + ProvinceId = 1L, + SortOrder = 1 + }, + new + { + Id = 102L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Karaj", + NameFa = "کرج", + ProvinceId = 2L, + SortOrder = 1 + }, + new + { + Id = 103L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Isfahan", + NameFa = "اصفهان", + ProvinceId = 3L, + SortOrder = 1 + }, + new + { + Id = 104L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Shiraz", + NameFa = "شیراز", + ProvinceId = 4L, + SortOrder = 1 + }, + new + { + Id = 105L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Mashhad", + NameFa = "مشهد", + ProvinceId = 5L, + SortOrder = 1 + }, + new + { + Id = 106L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Tabriz", + NameFa = "تبریز", + ProvinceId = 6L, + SortOrder = 1 + }, + new + { + Id = 107L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Urmia", + NameFa = "ارومیه", + ProvinceId = 7L, + SortOrder = 1 + }, + new + { + Id = 108L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ahvaz", + NameFa = "اهواز", + ProvinceId = 8L, + SortOrder = 1 + }, + new + { + Id = 109L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Qom", + NameFa = "قم", + ProvinceId = 9L, + SortOrder = 1 + }, + new + { + Id = 110L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kerman", + NameFa = "کرمان", + ProvinceId = 10L, + SortOrder = 1 + }, + new + { + Id = 111L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Rasht", + NameFa = "رشت", + ProvinceId = 11L, + SortOrder = 1 + }, + new + { + Id = 112L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Sari", + NameFa = "ساری", + ProvinceId = 12L, + SortOrder = 1 + }, + new + { + Id = 113L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Arak", + NameFa = "اراک", + ProvinceId = 13L, + SortOrder = 1 + }, + new + { + Id = 114L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ardabil", + NameFa = "اردبیل", + ProvinceId = 14L, + SortOrder = 1 + }, + new + { + Id = 115L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Qazvin", + NameFa = "قزوین", + ProvinceId = 15L, + SortOrder = 1 + }, + new + { + Id = 116L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kermanshah", + NameFa = "کرمانشاه", + ProvinceId = 16L, + SortOrder = 1 + }, + new + { + Id = 117L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Bojnord", + NameFa = "بجنورد", + ProvinceId = 17L, + SortOrder = 1 + }, + new + { + Id = 118L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Birjand", + NameFa = "بیرجند", + ProvinceId = 18L, + SortOrder = 1 + }, + new + { + Id = 119L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Hamadan", + NameFa = "همدان", + ProvinceId = 19L, + SortOrder = 1 + }, + new + { + Id = 120L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Sanandaj", + NameFa = "سنندج", + ProvinceId = 20L, + SortOrder = 1 + }, + new + { + Id = 121L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Khorramabad", + NameFa = "خرم‌آباد", + ProvinceId = 21L, + SortOrder = 1 + }, + new + { + Id = 122L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Gorgan", + NameFa = "گرگان", + ProvinceId = 22L, + SortOrder = 1 + }, + new + { + Id = 123L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Bandar Abbas", + NameFa = "بندرعباس", + ProvinceId = 23L, + SortOrder = 1 + }, + new + { + Id = 124L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Bushehr", + NameFa = "بوشهر", + ProvinceId = 24L, + SortOrder = 1 + }, + new + { + Id = 125L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Zanjan", + NameFa = "زنجان", + ProvinceId = 25L, + SortOrder = 1 + }, + new + { + Id = 126L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Semnan", + NameFa = "سمنان", + ProvinceId = 26L, + SortOrder = 1 + }, + new + { + Id = 127L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Yazd", + NameFa = "یزد", + ProvinceId = 27L, + SortOrder = 1 + }, + new + { + Id = 128L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Zahedan", + NameFa = "زاهدان", + ProvinceId = 28L, + SortOrder = 1 + }, + new + { + Id = 129L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Shahrekord", + NameFa = "شهرکرد", + ProvinceId = 29L, + SortOrder = 1 + }, + new + { + Id = 130L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Yasuj", + NameFa = "یاسوج", + ProvinceId = 30L, + SortOrder = 1 + }, + new + { + Id = 131L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ilam", + NameFa = "ایلام", + ProvinceId = 31L, + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.District", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CityId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("CityId", "SortOrder"); + + b.ToTable("Districts", "geo"); + + b.HasData( + new + { + Id = 1001L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 1", + NameFa = "منطقه ۱", + SortOrder = 1 + }, + new + { + Id = 1002L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 2", + NameFa = "منطقه ۲", + SortOrder = 2 + }, + new + { + Id = 1003L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 3", + NameFa = "منطقه ۳", + SortOrder = 3 + }, + new + { + Id = 1004L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 4", + NameFa = "منطقه ۴", + SortOrder = 4 + }, + new + { + Id = 1005L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 5", + NameFa = "منطقه ۵", + SortOrder = 5 + }, + new + { + Id = 1006L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 6", + NameFa = "منطقه ۶", + SortOrder = 6 + }, + new + { + Id = 1007L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 7", + NameFa = "منطقه ۷", + SortOrder = 7 + }, + new + { + Id = 1008L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 8", + NameFa = "منطقه ۸", + SortOrder = 8 + }, + new + { + Id = 1009L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 9", + NameFa = "منطقه ۹", + SortOrder = 9 + }, + new + { + Id = 1010L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 10", + NameFa = "منطقه ۱۰", + SortOrder = 10 + }, + new + { + Id = 1011L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 11", + NameFa = "منطقه ۱۱", + SortOrder = 11 + }, + new + { + Id = 1012L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 12", + NameFa = "منطقه ۱۲", + SortOrder = 12 + }, + new + { + Id = 1013L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 13", + NameFa = "منطقه ۱۳", + SortOrder = 13 + }, + new + { + Id = 1014L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 14", + NameFa = "منطقه ۱۴", + SortOrder = 14 + }, + new + { + Id = 1015L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 15", + NameFa = "منطقه ۱۵", + SortOrder = 15 + }, + new + { + Id = 1016L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 16", + NameFa = "منطقه ۱۶", + SortOrder = 16 + }, + new + { + Id = 1017L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 17", + NameFa = "منطقه ۱۷", + SortOrder = 17 + }, + new + { + Id = 1018L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 18", + NameFa = "منطقه ۱۸", + SortOrder = 18 + }, + new + { + Id = 1019L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 19", + NameFa = "منطقه ۱۹", + SortOrder = 19 + }, + new + { + Id = 1020L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 20", + NameFa = "منطقه ۲۰", + SortOrder = 20 + }, + new + { + Id = 1021L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 21", + NameFa = "منطقه ۲۱", + SortOrder = 21 + }, + new + { + Id = 1022L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 22", + NameFa = "منطقه ۲۲", + SortOrder = 22 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.NurseServiceArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CityId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DistrictId") + .HasColumnType("bigint"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.HasIndex("DistrictId"); + + b.HasIndex("NurseId", "CityId") + .IsUnique() + .HasDatabaseName("UX_NurseServiceAreas_Nurse_City_WholeCity") + .HasFilter("[DistrictId] IS NULL AND [DeletedAt] IS NULL"); + + b.HasIndex("NurseId", "CityId", "DistrictId") + .IsUnique() + .HasDatabaseName("UX_NurseServiceAreas_Nurse_City_District") + .HasFilter("[DistrictId] IS NOT NULL AND [DeletedAt] IS NULL"); + + b.ToTable("NurseServiceAreas", "geo"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.Province", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("SortOrder"); + + b.ToTable("Provinces", "geo"); + + b.HasData( + new + { + Id = 1L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Tehran", + NameFa = "تهران", + SortOrder = 1 + }, + new + { + Id = 2L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Alborz", + NameFa = "البرز", + SortOrder = 2 + }, + new + { + Id = 3L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Isfahan", + NameFa = "اصفهان", + SortOrder = 3 + }, + new + { + Id = 4L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Fars", + NameFa = "فارس", + SortOrder = 4 + }, + new + { + Id = 5L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Razavi Khorasan", + NameFa = "خراسان رضوی", + SortOrder = 5 + }, + new + { + Id = 6L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "East Azerbaijan", + NameFa = "آذربایجان شرقی", + SortOrder = 6 + }, + new + { + Id = 7L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "West Azerbaijan", + NameFa = "آذربایجان غربی", + SortOrder = 7 + }, + new + { + Id = 8L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Khuzestan", + NameFa = "خوزستان", + SortOrder = 8 + }, + new + { + Id = 9L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Qom", + NameFa = "قم", + SortOrder = 9 + }, + new + { + Id = 10L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kerman", + NameFa = "کرمان", + SortOrder = 10 + }, + new + { + Id = 11L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Gilan", + NameFa = "گیلان", + SortOrder = 11 + }, + new + { + Id = 12L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Mazandaran", + NameFa = "مازندران", + SortOrder = 12 + }, + new + { + Id = 13L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Markazi", + NameFa = "مرکزی", + SortOrder = 13 + }, + new + { + Id = 14L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ardabil", + NameFa = "اردبیل", + SortOrder = 14 + }, + new + { + Id = 15L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Qazvin", + NameFa = "قزوین", + SortOrder = 15 + }, + new + { + Id = 16L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kermanshah", + NameFa = "کرمانشاه", + SortOrder = 16 + }, + new + { + Id = 17L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "North Khorasan", + NameFa = "خراسان شمالی", + SortOrder = 17 + }, + new + { + Id = 18L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "South Khorasan", + NameFa = "خراسان جنوبی", + SortOrder = 18 + }, + new + { + Id = 19L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Hamadan", + NameFa = "همدان", + SortOrder = 19 + }, + new + { + Id = 20L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kurdistan", + NameFa = "کردستان", + SortOrder = 20 + }, + new + { + Id = 21L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Lorestan", + NameFa = "لرستان", + SortOrder = 21 + }, + new + { + Id = 22L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Golestan", + NameFa = "گلستان", + SortOrder = 22 + }, + new + { + Id = 23L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Hormozgan", + NameFa = "هرمزگان", + SortOrder = 23 + }, + new + { + Id = 24L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Bushehr", + NameFa = "بوشهر", + SortOrder = 24 + }, + new + { + Id = 25L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Zanjan", + NameFa = "زنجان", + SortOrder = 25 + }, + new + { + Id = 26L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Semnan", + NameFa = "سمنان", + SortOrder = 26 + }, + new + { + Id = 27L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Yazd", + NameFa = "یزد", + SortOrder = 27 + }, + new + { + Id = 28L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Sistan and Baluchestan", + NameFa = "سیستان و بلوچستان", + SortOrder = 28 + }, + new + { + Id = 29L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Chaharmahal and Bakhtiari", + NameFa = "چهارمحال و بختیاری", + SortOrder = 29 + }, + new + { + Id = 30L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kohgiluyeh and Boyer-Ahmad", + NameFa = "کهگیلویه و بویراحمد", + SortOrder = 30 + }, + new + { + Id = 31L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ilam", + NameFa = "ایلام", + SortOrder = 31 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Holidays.IranianHoliday", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("HolidayDate") + .HasColumnType("date"); + + b.Property("IsBankClosed") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("HolidayDate") + .IsUnique(); + + b.ToTable("IranianHolidays", "ops"); + + b.HasData( + new + { + Id = 1L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 2, 11), + IsBankClosed = true, + NameFa = "پیروزی انقلاب اسلامی", + Type = "national" + }, + new + { + Id = 2L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 3, 21), + IsBankClosed = true, + NameFa = "نوروز", + Type = "national" + }, + new + { + Id = 3L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 3, 22), + IsBankClosed = true, + NameFa = "نوروز", + Type = "national" + }, + new + { + Id = 4L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 3, 23), + IsBankClosed = true, + NameFa = "نوروز", + Type = "national" + }, + new + { + Id = 5L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 3, 24), + IsBankClosed = true, + NameFa = "نوروز", + Type = "national" + }, + new + { + Id = 6L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 4, 1), + IsBankClosed = true, + NameFa = "روز طبیعت (سیزده‌به‌در)", + Type = "official" + }, + new + { + Id = 7L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 6, 26), + IsBankClosed = true, + NameFa = "عید سعید قربان", + Type = "religious" + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AddressLine") + .HasColumnType("nvarchar(max)"); + + b.Property("CityId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DistrictId") + .HasColumnType("bigint"); + + b.Property("GeocodeSource") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PostalCode") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipientName") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipientPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.HasIndex("CustomerId") + .IsUnique() + .HasDatabaseName("UX_CustomerAddresses_Customer_Primary") + .HasFilter("[IsPrimary] = 1 AND [DeletedAt] IS NULL"); + + b.HasIndex("DistrictId"); + + b.ToTable("CustomerAddresses", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DefaultEmergencyContactName") + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultEmergencyContactPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PreferredLanguage") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("CustomerProfiles", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseBankAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountHolderFromBank") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("AccountHolderName") + .HasColumnType("nvarchar(max)"); + + b.Property("BankName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("Iban") + .HasColumnType("nvarchar(max)"); + + b.Property("IbanHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsVerified") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("MatchedNationalId") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("OwnershipVendorRef") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("VerifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("VerifiedByAdminId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IbanHash") + .IsUnique(); + + b.HasIndex("NurseId") + .IsUnique() + .HasDatabaseName("UX_NurseBankAccounts_NurseId_Primary") + .HasFilter("[IsPrimary] = 1"); + + b.HasIndex("VerifiedByAdminId"); + + b.ToTable("NurseBankAccounts", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("AverageRating") + .ValueGeneratedOnAdd() + .HasPrecision(3, 2) + .HasColumnType("decimal(3,2)") + .HasDefaultValue(0m); + + b.Property("Bio") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("EducationField") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("EducationLevel") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsAcceptingBookings") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsVerified") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PartnerCenterId") + .HasColumnType("bigint"); + + b.Property("SpecializationsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("TotalCompletedBookings") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("TotalReviews") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("YearsOfExperience") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PartnerCenterId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("NurseProfiles", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.Patient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("BloodType") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("ConditionsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("FirstName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Gender") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("InitialMedicalNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("LastName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Relation") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Patients", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.PatientCarePlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("MedicationsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PatientId") + .HasColumnType("bigint"); + + b.Property("RoutineJson") + .HasColumnType("nvarchar(max)"); + + b.Property("TasksJson") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PatientId") + .IsUnique() + .HasDatabaseName("UX_PatientCarePlans_Patient") + .HasFilter("[DeletedAt] IS NULL"); + + b.ToTable("PatientCarePlans", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Invoices.Invoice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BnplCommissionIrr") + .HasColumnType("bigint"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("GrossIrr") + .HasColumnType("bigint"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("IssuedAt") + .HasColumnType("datetime2"); + + b.Property("IssuingEntityType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("MoadianReferenceNumber") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("MoadianStatus") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PartnerCenterId") + .HasColumnType("bigint"); + + b.Property("PdfStorageKey") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("PlatformCommissionIrr") + .HasColumnType("bigint"); + + b.Property("VatIrr") + .HasColumnType("bigint"); + + b.Property("VatRate") + .HasPrecision(5, 4) + .HasColumnType("decimal(5,4)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique(); + + b.HasIndex("InvoiceNumber") + .IsUnique(); + + b.ToTable("Invoices", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Invoices.InvoiceNumberSequence", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("NextValue") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("InvoiceNumberSequences", "payments"); + + b.HasData( + new + { + Id = 1, + NextValue = 1L + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ClosedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ClosedById") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("OpenedById") + .HasColumnType("int"); + + b.Property("ReferenceCode") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("RefundId") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Subject") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("ClosedById"); + + b.HasIndex("OpenedById"); + + b.HasIndex("ReferenceCode") + .IsUnique(); + + b.HasIndex("RefundId"); + + b.HasIndex("Status"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("Tickets", "messaging"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.TicketMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsInternal") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("SenderId") + .HasColumnType("int"); + + b.Property("SentAt") + .HasColumnType("datetimeoffset"); + + b.Property("TicketId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SenderId"); + + b.HasIndex("TicketId", "SentAt"); + + b.ToTable("TicketMessages", "messaging"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.TicketParticipant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AddedById") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("RemovedAt") + .HasColumnType("datetimeoffset"); + + b.Property("RoleOnTicket") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("TicketId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AddedById"); + + b.HasIndex("TicketId", "UserId") + .IsUnique(); + + b.HasIndex("UserId", "TicketId"); + + b.ToTable("TicketParticipants", "messaging"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Notifications.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Body") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DataJson") + .HasColumnType("nvarchar(max)"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ReadAt") + .HasColumnType("datetimeoffset"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "IsRead", "CreatedAt"); + + b.ToTable("Notifications", "ops"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.PartnerCenters.PartnerCenter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminUserId") + .HasColumnType("int"); + + b.Property("CommissionRate") + .HasPrecision(5, 4) + .HasColumnType("decimal(5,4)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("EnamadCode") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsMerchantOfRecord") + .HasColumnType("bit"); + + b.Property("LegalEntityType") + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("MohEstablishmentPermitNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("SettlementIban") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TechnicalDirectorLicenseNo") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("TechnicalDirectorNurseUserId") + .HasColumnType("int"); + + b.Property("VerifiedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("AdminUserId"); + + b.HasIndex("IsActive"); + + b.HasIndex("TechnicalDirectorNurseUserId"); + + b.ToTable("PartnerCenters", "partner"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.LedgerEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountType") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("AmountIrr") + .HasColumnType("bigint"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Direction") + .IsRequired() + .HasMaxLength(6) + .HasColumnType("nvarchar(6)"); + + b.Property("Memo") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("SourceRefId") + .HasColumnType("bigint"); + + b.Property("SourceRefType") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("TransactionGroupId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("NurseId"); + + b.HasIndex("TransactionGroupId"); + + b.HasIndex("AccountType", "NurseId"); + + b.HasIndex("SourceRefType", "SourceRefId"); + + b.ToTable("LedgerEntries", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("ProviderCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Type", "IsActive", "Priority"); + + b.ToTable("PaymentGateways", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.PaymentTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("BookingRequestId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("GatewayId") + .HasColumnType("bigint"); + + b.Property("GatewayReferenceCode") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("GatewayResponseCode") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GatewayResponseJson") + .HasColumnType("nvarchar(max)"); + + b.Property("GatewayTransactionId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("IsInstallment") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("UserAgent") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique() + .HasFilter("[Status] = 'succeeded' AND [BookingId] IS NOT NULL"); + + b.HasIndex("CustomerId"); + + b.HasIndex("GatewayId"); + + b.HasIndex("GatewayReferenceCode") + .IsUnique() + .HasFilter("[GatewayReferenceCode] IS NOT NULL"); + + b.HasIndex("BookingId", "Status"); + + b.HasIndex("BookingRequestId", "Status"); + + b.ToTable("PaymentTransactions", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.PaymentWebhookEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("nvarchar(80)"); + + b.Property("ExternalEventId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProcessedAt") + .HasColumnType("datetime2"); + + b.Property("ProcessingStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ProviderCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ReceivedAt") + .HasColumnType("datetime2"); + + b.Property("RelatedPaymentTransactionId") + .HasColumnType("bigint"); + + b.Property("SignatureValid") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("ProviderCode", "ExternalEventId") + .IsUnique(); + + b.ToTable("PaymentWebhookEvents", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("BankAccountId") + .HasColumnType("bigint"); + + b.Property("BatchId") + .HasColumnType("bigint"); + + b.Property("BookingCount") + .HasColumnType("int"); + + b.Property("ClawbackAppliedIrr") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("FailureReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("GrossEarningsIrr") + .HasColumnType("bigint"); + + b.Property("IbanSnapshot") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NetAmountIrr") + .HasColumnType("bigint"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("PaidAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("TransferReference") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("BankAccountId"); + + b.HasIndex("BatchId"); + + b.HasIndex("NurseId"); + + b.HasIndex("Status"); + + b.ToTable("NursePayouts", "payouts", t => + { + t.HasCheckConstraint("CK_NursePayouts_NetSplit", "[NetAmountIrr] = [GrossEarningsIrr] - [ClawbackAppliedIrr] AND [GrossEarningsIrr] >= 0 AND [ClawbackAppliedIrr] >= 0 AND [NetAmountIrr] >= 0 AND [Amount] >= 0"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("FailureNotes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("InitiatedByAdminId") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PayoutCount") + .HasColumnType("int"); + + b.Property("PeriodEnd") + .HasColumnType("date"); + + b.Property("PeriodStart") + .HasColumnType("date"); + + b.Property("ProcessedAt") + .HasColumnType("datetime2"); + + b.Property("ProcessingDate") + .HasColumnType("date"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("TotalAmount") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InitiatedByAdminId"); + + b.HasIndex("ProcessingDate"); + + b.HasIndex("Status"); + + b.ToTable("NursePayoutBatches", "payouts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBookingLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PayoutAmountIrr") + .HasColumnType("bigint"); + + b.Property("PayoutId") + .HasColumnType("bigint"); + + b.Property("SessionId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique(); + + b.HasIndex("PayoutId"); + + b.HasIndex("SessionId"); + + b.ToTable("NursePayoutBookingLinks", "payouts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Refunds.NurseClawback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AmountIrr") + .HasColumnType("bigint"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("OriginalPayoutId") + .HasColumnType("bigint"); + + b.Property("RecoveredInPayoutId") + .HasColumnType("bigint"); + + b.Property("RefundId") + .HasColumnType("bigint"); + + b.Property("ResolutionNotes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ResolvedAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("NurseId"); + + b.HasIndex("OriginalPayoutId"); + + b.HasIndex("RecoveredInPayoutId"); + + b.HasIndex("RefundId") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("NurseClawbacks", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Refunds.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminNotes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("ApprovedByAdminId") + .HasColumnType("int"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CancellationPolicyCode") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ExpectedCustomerRefundEta") + .HasColumnType("date"); + + b.Property("ExternalRevertReference") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("GatewayRefundReference") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NursePayoutRefundedIrr") + .HasColumnType("bigint"); + + b.Property("PaymentTransactionId") + .HasColumnType("bigint"); + + b.Property("PlatformFeeRefundedIrr") + .HasColumnType("bigint"); + + b.Property("ProcessedAt") + .HasColumnType("datetime2"); + + b.Property("ReasonCategory") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ReasonNotes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RefundChannel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RefundPercentage") + .HasPrecision(6, 4) + .HasColumnType("decimal(6,4)"); + + b.Property("RefundPercentageApplied") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("RejectedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RequestedByCustomerId") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("TicketId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("PaymentTransactionId"); + + b.HasIndex("RequestedByCustomerId"); + + b.HasIndex("Status"); + + b.HasIndex("TicketId"); + + b.ToTable("Refunds", "payments", t => + { + t.HasCheckConstraint("CK_Refunds_LegSplit", "[Amount] = [PlatformFeeRefundedIrr] + [NursePayoutRefundedIrr] AND [Amount] >= 0 AND [PlatformFeeRefundedIrr] >= 0 AND [NursePayoutRefundedIrr] >= 0"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.PatientCareRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BodyEncrypted") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseProfileId") + .HasColumnType("bigint"); + + b.Property("PatientId") + .HasColumnType("bigint"); + + b.Property("RecordedAt") + .HasColumnType("datetime2"); + + b.Property("TaskResultsJson") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("NurseProfileId"); + + b.HasIndex("PatientId", "RecordedAt") + .HasDatabaseName("IX_PatientCareRecords_Patient_RecordedAt"); + + b.ToTable("PatientCareRecords", "reviews"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Body") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerProfileId") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModeratedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModeratedById") + .HasColumnType("int"); + + b.Property("ModerationReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ModerationStatus") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseProfileId") + .HasColumnType("bigint"); + + b.Property("Rating") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique(); + + b.HasIndex("CustomerProfileId"); + + b.HasIndex("ModerationStatus"); + + b.HasIndex("NurseProfileId", "ModerationStatus"); + + b.ToTable("Reviews", "reviews", t => + { + t.HasCheckConstraint("CK_Reviews_Rating", "[Rating] BETWEEN 1 AND 5"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.ReviewTagLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("ReviewId") + .HasColumnType("bigint"); + + b.Property("ReviewTagMasterId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ReviewTagMasterId"); + + b.HasIndex("ReviewId", "ReviewTagMasterId") + .IsUnique() + .HasDatabaseName("UX_ReviewTagLinks_Review_Tag"); + + b.ToTable("ReviewTagLinks", "reviews"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.ReviewTagMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("LabelEn") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LabelFa") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsActive", "SortOrder"); + + b.ToTable("ReviewTagsMasters", "reviews"); + + b.HasData( + new + { + Id = 1L, + Code = "punctual", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Punctual", + LabelFa = "وقت‌شناس", + SortOrder = 1 + }, + new + { + Id = 2L, + Code = "professional", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Professional", + LabelFa = "حرفه‌ای", + SortOrder = 2 + }, + new + { + Id = 3L, + Code = "clean", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Clean", + LabelFa = "تمیز و بهداشتی", + SortOrder = 3 + }, + new + { + Id = 4L, + Code = "kind", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Kind", + LabelFa = "مهربان", + SortOrder = 4 + }, + new + { + Id = 5L, + Code = "communicative", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Communicative", + LabelFa = "خوش‌برخورد", + SortOrder = 5 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Search.NurseSearchIndex", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("AverageRating") + .HasPrecision(3, 2) + .HasColumnType("decimal(3,2)"); + + b.Property("CityId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DistrictId") + .HasColumnType("bigint"); + + b.Property("IsSearchable") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseGender") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("NurseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("PriceUnit") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ServiceCategoryId") + .HasColumnType("bigint"); + + b.Property("TotalCompletedBookings") + .HasColumnType("int"); + + b.Property("TotalReviews") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("VariantId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("NurseId"); + + b.HasIndex("VariantId", "CityId") + .IsUnique() + .HasDatabaseName("UX_NurseSearchIndex_Variant_City_WholeCity") + .HasFilter("[DistrictId] IS NULL AND [DeletedAt] IS NULL"); + + b.HasIndex("VariantId", "CityId", "DistrictId") + .IsUnique() + .HasDatabaseName("UX_NurseSearchIndex_Variant_City_District") + .HasFilter("[DistrictId] IS NOT NULL AND [DeletedAt] IS NULL"); + + b.HasIndex("IsSearchable", "ServiceCategoryId", "CityId", "DistrictId") + .HasDatabaseName("IX_NurseSearchIndex_Search"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("IsSearchable", "ServiceCategoryId", "CityId", "DistrictId"), new[] { "Price", "NurseGender", "AverageRating", "TotalReviews", "NurseId", "VariantId", "NurseName" }); + + b.ToTable("NurseSearchIndices", "search"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.SupportAlerts.SupportAlert", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("OwnerUserId") + .HasColumnType("int"); + + b.Property("ResolutionNote") + .HasColumnType("nvarchar(max)"); + + b.Property("ResolvedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ReviewId") + .HasColumnType("bigint"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.HasKey("Id"); + + b.HasIndex("OwnerUserId"); + + b.HasIndex("Status"); + + b.HasIndex("Type"); + + b.ToTable("SupportAlerts", "ops"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedClaim") + .HasColumnType("datetime2"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("UserId"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FamilyName") + .HasColumnType("nvarchar(max)"); + + b.Property("Gender") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("GeneratedCode") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NationalId") + .HasColumnType("nvarchar(max)"); + + b.Property("NationalIdVerifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("PhoneVerifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("ShahkarVerifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("PhoneHash") + .IsUnique() + .HasFilter("[PhoneHash] IS NOT NULL"); + + b.ToTable("Users", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("LoggedOn") + .HasColumnType("datetime2"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("IsValid") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRefreshTokens", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.Property("CreatedUserRoleDate") + .HasColumnType("datetime2"); + + b.Property("GrantedAt") + .HasColumnType("datetimeoffset"); + + b.Property("GrantedById") + .HasColumnType("int"); + + b.Property("RevokedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("GrantedById"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeviceInfo") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("ExpiresAt") + .HasColumnType("datetimeoffset"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("RefreshTokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("RevokedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RefreshTokenHash") + .IsUnique(); + + b.HasIndex("UserId", "IsRevoked"); + + b.ToTable("UserSessions", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("GeneratedTime") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseCredential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CredentialNumber") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("CredentialType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ExpiresAt") + .HasColumnType("date"); + + b.Property("HolderNameSnapshot") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IssuedAt") + .HasColumnType("date"); + + b.Property("IssuingAuthority") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("VerificationMethod") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("VerificationSource") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("VerifiedByAdminId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("VerifiedByAdminId"); + + b.HasIndex("NurseId", "CredentialType"); + + b.ToTable("NurseCredentials", "verif"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseVerification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApprovedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("InternalNotes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("RejectedAt") + .HasColumnType("datetimeoffset"); + + b.Property("RejectionReason") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ReviewedByAdminId") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("SubmittedAt") + .HasColumnType("datetimeoffset"); + + b.Property("SuspendedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("NurseId") + .IsUnique(); + + b.HasIndex("ReviewedByAdminId"); + + b.ToTable("NurseVerifications", "verif"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("IntegrityHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("ObjectStorageKey") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("OriginalFileName") + .HasMaxLength(260) + .HasColumnType("nvarchar(260)"); + + b.Property("StepId") + .HasColumnType("bigint"); + + b.Property("UploadedByUserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("StepId"); + + b.HasIndex("UploadedByUserId"); + + b.ToTable("VerificationDocuments", "verif"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("ExpiresAt") + .HasColumnType("datetimeoffset"); + + b.Property("ExternalResponseJson") + .HasColumnType("nvarchar(max)"); + + b.Property("FailureReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsAutomated") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseVerificationId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("StepTypeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("StepTypeId"); + + b.HasIndex("NurseVerificationId", "StepTypeId") + .IsUnique() + .HasDatabaseName("UX_VerificationSteps_Verification_StepType"); + + b.ToTable("VerificationSteps", "verif"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStepType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AutomationProvider") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsAutomated") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsRequired") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsActive", "SortOrder"); + + b.ToTable("VerificationStepTypes", "verif"); + + b.HasData( + new + { + Id = 1L, + AutomationProvider = "identity_kyc_vendor", + Code = "identity_kyc", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "National-ID validity, name match and photo/video liveness via an Iranian e-KYC vendor.", + DisplayName = "Identity Verification (KYC)", + IsActive = true, + IsAutomated = true, + IsRequired = true, + SortOrder = 1 + }, + new + { + Id = 2L, + AutomationProvider = "shahkar", + Code = "shahkar_match", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Confirms the login SIM is registered to the nurse's own national ID (شاهکار).", + DisplayName = "Shahkar Phone Binding", + IsActive = true, + IsAutomated = true, + IsRequired = true, + SortOrder = 2 + }, + new + { + Id = 3L, + Code = "moh_competency_license", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "پروانه صلاحیت حرفه‌ای — the MoH-mandated in-home nursing licence (bundles the criminal-record screen). Manual today.", + DisplayName = "MoH Professional Competency License", + IsActive = true, + IsAutomated = false, + IsRequired = true, + SortOrder = 3 + }, + new + { + Id = 4L, + Code = "ino_membership", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "نظام پرستاری membership cross-check (ino.ir). Manual today.", + DisplayName = "Nursing Organization (INO) Membership", + IsActive = true, + IsAutomated = false, + IsRequired = true, + SortOrder = 4 + }, + new + { + Id = 5L, + Code = "criminal_record", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "عدم سوء پیشینه — consent-gated, nurse-uploaded, time-limited (reverts on expiry).", + DisplayName = "Criminal Record Certificate", + IsActive = true, + IsAutomated = false, + IsRequired = true, + SortOrder = 5 + }, + new + { + Id = 6L, + AutomationProvider = "sheba", + Code = "bank_account_verification", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "استعلام شبا — the payout IBAN owner's national ID must equal the verified nurse national ID.", + DisplayName = "Bank Account (IBAN) Ownership", + IsActive = true, + IsAutomated = true, + IsRequired = true, + SortOrder = 6 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Analytics.SystemEvent", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Audit.AuditLog", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("ActorUserId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Bnpl.BnplTransaction", b => + { + b.HasOne("Baya.Domain.Entities.Payments.PaymentTransaction", null) + .WithMany() + .HasForeignKey("PaymentTransactionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.Booking", b => + { + b.HasOne("Baya.Domain.Entities.Booking.BookingRequest", null) + .WithOne() + .HasForeignKey("Baya.Domain.Entities.Booking.Booking", "BookingRequestId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerAddress", null) + .WithMany() + .HasForeignKey("CustomerAddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", null) + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.Patient", null) + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.NurseServiceVariant", null) + .WithMany() + .HasForeignKey("VariantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingCareInstruction", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", "Booking") + .WithOne("CareInstructions") + .HasForeignKey("Baya.Domain.Entities.Booking.BookingCareInstruction", "BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingRequest", b => + { + b.HasOne("Baya.Domain.Entities.Identity.CustomerAddress", "CustomerAddress") + .WithMany() + .HasForeignKey("CustomerAddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse") + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.Patient", "Patient") + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.NurseServiceVariant", "Variant") + .WithMany() + .HasForeignKey("VariantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("CustomerAddress"); + + b.Navigation("Nurse"); + + b.Navigation("Patient"); + + b.Navigation("Variant"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingSession", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", "Booking") + .WithMany("Sessions") + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.VisitVerification", b => + { + b.HasOne("Baya.Domain.Entities.Booking.BookingSession", "Session") + .WithOne("Verification") + .HasForeignKey("Baya.Domain.Entities.Booking.VisitVerification", "BookingSessionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariant", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse") + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.ServiceCategory", "ServiceCategory") + .WithMany("Variants") + .HasForeignKey("ServiceCategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Nurse"); + + b.Navigation("ServiceCategory"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariantOption", b => + { + b.HasOne("Baya.Domain.Entities.Catalog.ServiceOptionGroup", "OptionGroup") + .WithMany() + .HasForeignKey("OptionGroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.ServiceOptionValue", "OptionValue") + .WithMany() + .HasForeignKey("OptionValueId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.NurseServiceVariant", "Variant") + .WithMany("Options") + .HasForeignKey("VariantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("OptionGroup"); + + b.Navigation("OptionValue"); + + b.Navigation("Variant"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionGroup", b => + { + b.HasOne("Baya.Domain.Entities.Catalog.ServiceCategory", "ServiceCategory") + .WithMany("OptionGroups") + .HasForeignKey("ServiceCategoryId"); + + b.Navigation("ServiceCategory"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionValue", b => + { + b.HasOne("Baya.Domain.Entities.Catalog.ServiceOptionGroup", "OptionGroup") + .WithMany("Values") + .HasForeignKey("OptionGroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("OptionGroup"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.City", b => + { + b.HasOne("Baya.Domain.Entities.Geography.Province", "Province") + .WithMany("Cities") + .HasForeignKey("ProvinceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Province"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.District", b => + { + b.HasOne("Baya.Domain.Entities.Geography.City", "City") + .WithMany("Districts") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("City"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.NurseServiceArea", b => + { + b.HasOne("Baya.Domain.Entities.Geography.City", "City") + .WithMany() + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Geography.District", "District") + .WithMany() + .HasForeignKey("DistrictId"); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("City"); + + b.Navigation("District"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerAddress", b => + { + b.HasOne("Baya.Domain.Entities.Geography.City", "City") + .WithMany() + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Geography.District", "District") + .WithMany() + .HasForeignKey("DistrictId"); + + b.Navigation("City"); + + b.Navigation("Customer"); + + b.Navigation("District"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerProfile", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithOne() + .HasForeignKey("Baya.Domain.Entities.Identity.CustomerProfile", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseBankAccount", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse") + .WithMany("BankAccounts") + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("VerifiedByAdminId"); + + b.Navigation("Nurse"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseProfile", b => + { + b.HasOne("Baya.Domain.Entities.PartnerCenters.PartnerCenter", null) + .WithMany() + .HasForeignKey("PartnerCenterId"); + + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithOne() + .HasForeignKey("Baya.Domain.Entities.Identity.NurseProfile", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.Patient", b => + { + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", "Customer") + .WithMany("Patients") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.PatientCarePlan", b => + { + b.HasOne("Baya.Domain.Entities.Identity.Patient", null) + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Invoices.Invoice", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.Ticket", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId"); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("ClosedById"); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("OpenedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Refunds.Refund", null) + .WithMany() + .HasForeignKey("RefundId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.TicketMessage", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Messaging.Ticket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.TicketParticipant", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("AddedById"); + + b.HasOne("Baya.Domain.Entities.Messaging.Ticket", "Ticket") + .WithMany("Participants") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Notifications.Notification", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.PartnerCenters.PartnerCenter", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("AdminUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("TechnicalDirectorNurseUserId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.LedgerEntry", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId"); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.PaymentTransaction", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId"); + + b.HasOne("Baya.Domain.Entities.Booking.BookingRequest", null) + .WithMany() + .HasForeignKey("BookingRequestId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", null) + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Payments.PaymentGateway", null) + .WithMany() + .HasForeignKey("GatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayout", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseBankAccount", null) + .WithMany() + .HasForeignKey("BankAccountId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Payouts.NursePayoutBatch", "Batch") + .WithMany("Payouts") + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Batch"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBatch", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("InitiatedByAdminId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBookingLink", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Payouts.NursePayout", null) + .WithMany("BookingLinks") + .HasForeignKey("PayoutId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Booking.BookingSession", null) + .WithMany() + .HasForeignKey("SessionId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Refunds.NurseClawback", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Refunds.Refund", null) + .WithMany() + .HasForeignKey("RefundId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Refunds.Refund", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Payments.PaymentTransaction", null) + .WithMany() + .HasForeignKey("PaymentTransactionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", null) + .WithMany() + .HasForeignKey("RequestedByCustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.PatientCareRecord", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId"); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseProfileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.Patient", null) + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.Review", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", null) + .WithMany() + .HasForeignKey("CustomerProfileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseProfileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.ReviewTagLink", b => + { + b.HasOne("Baya.Domain.Entities.Reviews.Review", "Review") + .WithMany("TagLinks") + .HasForeignKey("ReviewId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Reviews.ReviewTagMaster", "Tag") + .WithMany("Links") + .HasForeignKey("ReviewTagMasterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Review"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Search.NurseSearchIndex", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.NurseServiceVariant", "Variant") + .WithMany() + .HasForeignKey("VariantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Variant"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.SupportAlerts.SupportAlert", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("OwnerUserId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.RoleClaim", b => + { + b.HasOne("Baya.Domain.Entities.User.Role", "Role") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserClaim", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserLogin", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserRefreshToken", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("UserRefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserRole", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("GrantedById"); + + b.HasOne("Baya.Domain.Entities.User.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserSession", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("Sessions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserToken", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseCredential", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("VerifiedByAdminId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseVerification", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse") + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("ReviewedByAdminId"); + + b.Navigation("Nurse"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationDocument", b => + { + b.HasOne("Baya.Domain.Entities.Verification.VerificationStep", "Step") + .WithMany("Documents") + .HasForeignKey("StepId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("UploadedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStep", b => + { + b.HasOne("Baya.Domain.Entities.Verification.NurseVerification", "NurseVerification") + .WithMany("Steps") + .HasForeignKey("NurseVerificationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Verification.VerificationStepType", "StepType") + .WithMany("Steps") + .HasForeignKey("StepTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("NurseVerification"); + + b.Navigation("StepType"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.Booking", b => + { + b.Navigation("CareInstructions"); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingSession", b => + { + b.Navigation("Verification"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariant", b => + { + b.Navigation("Options"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceCategory", b => + { + b.Navigation("OptionGroups"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionGroup", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.City", b => + { + b.Navigation("Districts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.Province", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerProfile", b => + { + b.Navigation("Patients"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseProfile", b => + { + b.Navigation("BankAccounts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.Ticket", b => + { + b.Navigation("Messages"); + + b.Navigation("Participants"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayout", b => + { + b.Navigation("BookingLinks"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBatch", b => + { + b.Navigation("Payouts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.Review", b => + { + b.Navigation("TagLinks"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.ReviewTagMaster", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.Role", b => + { + b.Navigation("Claims"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.User", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("Sessions"); + + b.Navigation("Tokens"); + + b.Navigation("UserRefreshTokens"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseVerification", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStep", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStepType", b => + { + b.Navigation("Steps"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713065646_RefinementPhase3TierB.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713065646_RefinementPhase3TierB.cs new file mode 100644 index 0000000..c1bac09 --- /dev/null +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713065646_RefinementPhase3TierB.cs @@ -0,0 +1,72 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Baya.Infrastructure.Persistence.Migrations +{ + /// + public partial class RefinementPhase3TierB : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TaskResultsJson", + schema: "reviews", + table: "PatientCareRecords", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.CreateTable( + name: "PatientCarePlans", + schema: "usr", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + PatientId = table.Column(type: "bigint", nullable: false), + MedicationsJson = table.Column(type: "nvarchar(max)", nullable: true), + RoutineJson = table.Column(type: "nvarchar(max)", nullable: true), + TasksJson = table.Column(type: "nvarchar(max)", nullable: true), + DeletedAt = table.Column(type: "datetimeoffset", nullable: true), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false), + ModifiedAt = table.Column(type: "datetimeoffset", nullable: true), + CreatedById = table.Column(type: "int", nullable: true), + ModifiedById = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PatientCarePlans", x => x.Id); + table.ForeignKey( + name: "FK_PatientCarePlans_Patients_PatientId", + column: x => x.PatientId, + principalSchema: "usr", + principalTable: "Patients", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "UX_PatientCarePlans_Patient", + schema: "usr", + table: "PatientCarePlans", + column: "PatientId", + unique: true, + filter: "[DeletedAt] IS NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PatientCarePlans", + schema: "usr"); + + migrationBuilder.DropColumn( + name: "TaskResultsJson", + schema: "reviews", + table: "PatientCareRecords"); + } + } +} diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713072137_RefinementPhase3TierC.Designer.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713072137_RefinementPhase3TierC.Designer.cs new file mode 100644 index 0000000..b6ed2d0 --- /dev/null +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713072137_RefinementPhase3TierC.Designer.cs @@ -0,0 +1,6035 @@ +// +using System; +using Baya.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Baya.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260713072137_RefinementPhase3TierC")] + partial class RefinementPhase3TierC + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Baya.Domain.Entities.Analytics.SystemEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("OccurredAt") + .HasColumnType("datetimeoffset"); + + b.Property("PropsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("OccurredAt"); + + b.HasIndex("UserId"); + + b.ToTable("SystemEvents", "ops"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Audit.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ActorUserId") + .HasColumnType("int"); + + b.Property("ChangedFieldsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("OccurredAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("ActorUserId"); + + b.HasIndex("OccurredAt"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("AuditLogs", "ops"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Bnpl.BnplTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BnplCommissionIrr") + .HasColumnType("bigint"); + + b.Property("CallbackPayloadJson") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("nvarchar(5)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("EligibilityStatus") + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ExternalPaymentToken") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ExternalTransactionId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("InstallmentCount") + .HasColumnType("tinyint"); + + b.Property("MerchantOfRecord") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("OrderAmountIrr") + .HasColumnType("bigint"); + + b.Property("PaymentTransactionId") + .HasColumnType("bigint"); + + b.Property("ProviderCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProviderCommissionReversedAmount") + .HasColumnType("bigint"); + + b.Property("RefundChannel") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RevertTransactionId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RevertedAmountIrr") + .HasColumnType("bigint"); + + b.Property("RevertedAt") + .HasColumnType("datetime2"); + + b.Property("SettledAmountIrr") + .HasColumnType("bigint"); + + b.Property("SettledAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.HasKey("Id"); + + b.HasIndex("ExternalPaymentToken") + .HasFilter("[ExternalPaymentToken] IS NOT NULL"); + + b.HasIndex("PaymentTransactionId") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("BnplTransactions", "payments", t => + { + t.HasCheckConstraint("CK_BnplTransactions_SettleSplit", "([SettledAmountIrr] IS NULL AND [BnplCommissionIrr] IS NULL) OR ([SettledAmountIrr] = [OrderAmountIrr] - [BnplCommissionIrr] AND [SettledAmountIrr] >= 0 AND [BnplCommissionIrr] >= 0)"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.Booking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AddressSnapshotJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BalinyaarCommissionIrr") + .HasColumnType("bigint"); + + b.Property("BookingRequestId") + .HasColumnType("bigint"); + + b.Property("CancellationPolicyCode") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CancellationReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("CancellationRefundPercentage") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("CancelledAt") + .HasColumnType("datetime2"); + + b.Property("CancelledBy") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("CompletedAt") + .HasColumnType("datetime2"); + + b.Property("ConfirmedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerAddressId") + .HasColumnType("bigint"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DisputeWindowEndsAt") + .HasColumnType("datetime2"); + + b.Property("GrossPriceIrr") + .HasColumnType("bigint"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("NursePayoutAmount") + .HasColumnType("bigint"); + + b.Property("PartnerCenterId") + .HasColumnType("bigint"); + + b.Property("PatientId") + .HasColumnType("bigint"); + + b.Property("PlatformFeeRate") + .HasPrecision(5, 4) + .HasColumnType("decimal(5,4)"); + + b.Property("PspFeeAmount") + .HasColumnType("bigint"); + + b.Property("RefundableAmountIrr") + .HasColumnType("bigint"); + + b.Property("ScheduledDate") + .HasColumnType("date"); + + b.Property("ScheduledTimeEnd") + .HasColumnType("time"); + + b.Property("ScheduledTimeStart") + .HasColumnType("time"); + + b.Property("SessionCount") + .HasColumnType("smallint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("VariantId") + .HasColumnType("bigint"); + + b.Property("VariantSnapshotJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BookingRequestId") + .IsUnique(); + + b.HasIndex("CustomerAddressId"); + + b.HasIndex("DisputeWindowEndsAt"); + + b.HasIndex("PatientId"); + + b.HasIndex("VariantId"); + + b.HasIndex("CustomerId", "Status"); + + b.HasIndex("NurseId", "Status"); + + b.ToTable("Bookings", "booking", t => + { + t.HasCheckConstraint("CK_Bookings_AmountSplit", "[GrossPriceIrr] = [BalinyaarCommissionIrr] + [NursePayoutAmount] AND [GrossPriceIrr] >= 0 AND [BalinyaarCommissionIrr] >= 0 AND [NursePayoutAmount] >= 0"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingCareInstruction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Allergies") + .HasColumnType("nvarchar(max)"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CurrentConditions") + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("EmergencyContactName") + .HasColumnType("nvarchar(max)"); + + b.Property("EmergencyContactPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("Medications") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("SpecialInstructions") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique(); + + b.ToTable("BookingCareInstructions", "booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerAddressId") + .HasColumnType("bigint"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("CustomerNotes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("NurseRejectionReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("NurseResponseDeadlineAt") + .HasColumnType("datetime2"); + + b.Property("PatientId") + .HasColumnType("bigint"); + + b.Property("PaymentDeadlineAt") + .HasColumnType("datetime2"); + + b.Property("RequestedDate") + .HasColumnType("date"); + + b.Property("RequestedTimeEnd") + .HasColumnType("time"); + + b.Property("RequestedTimeStart") + .HasColumnType("time"); + + b.Property("RequiredCaregiverGender") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("VariantId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CustomerAddressId"); + + b.HasIndex("PatientId"); + + b.HasIndex("VariantId"); + + b.HasIndex("CustomerId", "Status"); + + b.HasIndex("NurseId", "Status"); + + b.HasIndex("Status", "NurseResponseDeadlineAt"); + + b.HasIndex("Status", "PaymentDeadlineAt"); + + b.ToTable("BookingRequests", "booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CancellationEventId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PayoutEligibleAt") + .HasColumnType("datetime2"); + + b.Property("ScheduledDate") + .HasColumnType("date"); + + b.Property("ScheduledTimeEnd") + .HasColumnType("time"); + + b.Property("ScheduledTimeStart") + .HasColumnType("time"); + + b.Property("SessionIndex") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("VisitPayoutAmount") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("BookingId", "SessionIndex"); + + b.HasIndex("Status", "ScheduledDate"); + + b.ToTable("BookingSessions", "booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.CancellationPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AppliesTo") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("FeeAmountIrr") + .HasColumnType("bigint"); + + b.Property("FeeRate") + .HasPrecision(5, 4) + .HasColumnType("decimal(5,4)"); + + b.Property("HoursBeforeStartMax") + .HasColumnType("int"); + + b.Property("HoursBeforeStartMin") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("RefundPercentage") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("AppliesTo", "IsActive"); + + b.ToTable("CancellationPolicies", "booking"); + + b.HasData( + new + { + Id = 1L, + AppliesTo = "customer", + Code = "standard_24h", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + FeeAmountIrr = 0L, + HoursBeforeStartMin = 24, + IsActive = true, + RefundPercentage = 100m + }, + new + { + Id = 2L, + AppliesTo = "customer", + Code = "standard_inside_24h", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + FeeAmountIrr = 0L, + HoursBeforeStartMax = 24, + IsActive = true, + RefundPercentage = 50m + }, + new + { + Id = 3L, + AppliesTo = "nurse", + Code = "nurse_no_show", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + FeeAmountIrr = 0L, + FeeRate = 0m, + IsActive = true, + RefundPercentage = 100m + }, + new + { + Id = 4L, + AppliesTo = "admin", + Code = "admin_cancellation", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + FeeAmountIrr = 0L, + IsActive = true, + RefundPercentage = 100m + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.VisitVerification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingSessionId") + .HasColumnType("bigint"); + + b.Property("CheckInAddressMatch") + .HasColumnType("bit"); + + b.Property("CheckInAt") + .HasColumnType("datetime2"); + + b.Property("CheckInDistanceMeters") + .HasPrecision(10, 2) + .HasColumnType("decimal(10,2)"); + + b.Property("CheckInLat") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("CheckInLng") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("CheckOutAt") + .HasColumnType("datetime2"); + + b.Property("CheckOutLat") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("CheckOutLng") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("BookingSessionId") + .IsUnique(); + + b.HasIndex("CheckInAddressMatch"); + + b.ToTable("VisitVerifications", "booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("OptionSetHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("PriceUnit") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ServiceCategoryId") + .HasColumnType("bigint"); + + b.Property("SessionCount") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ServiceCategoryId"); + + b.HasIndex("NurseId", "IsActive"); + + b.HasIndex("NurseId", "ServiceCategoryId", "OptionSetHash") + .IsUnique() + .HasDatabaseName("UX_NurseServiceVariants_Nurse_Category_OptionSet") + .HasFilter("[DeletedAt] IS NULL"); + + b.ToTable("NurseServiceVariants", "catalog"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariantOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("OptionGroupId") + .HasColumnType("bigint"); + + b.Property("OptionValueId") + .HasColumnType("bigint"); + + b.Property("VariantId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("OptionGroupId"); + + b.HasIndex("OptionValueId"); + + b.HasIndex("VariantId", "OptionGroupId") + .IsUnique() + .HasDatabaseName("UX_NurseServiceVariantOptions_Variant_Group"); + + b.ToTable("NurseServiceVariantOptions", "catalog"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DescriptionEn") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DescriptionFa") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IconKey") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("IsActive", "SortOrder"); + + b.ToTable("ServiceCategories", "catalog"); + + b.HasData( + new + { + Id = 1L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Elderly Care", + NameFa = "مراقبت از سالمند", + SortOrder = 1 + }, + new + { + Id = 2L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Post-Surgery Recovery", + NameFa = "مراقبت پس از جراحی", + SortOrder = 2 + }, + new + { + Id = 3L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Infant Care", + NameFa = "مراقبت از نوزاد", + SortOrder = 3 + }, + new + { + Id = 4L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Chronic Illness Management", + NameFa = "مدیریت بیماری مزمن", + SortOrder = 4 + }, + new + { + Id = 5L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Companionship", + NameFa = "همراهی و مراقبت روزمره", + SortOrder = 5 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsRequired") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("ServiceCategoryId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("ServiceCategoryId", "SortOrder"); + + b.ToTable("ServiceOptionGroups", "catalog"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionValue", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("OptionGroupId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("OptionGroupId", "SortOrder"); + + b.ToTable("ServiceOptionValues", "catalog"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Configuration.PlatformConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DataType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("PlatformConfigs", "ops"); + + b.HasData( + new + { + Id = 1L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "Balinyaar commission rate on the booking gross (fraction).", + Key = "platform_fee_rate", + Value = "0.15" + }, + new + { + Id = 2L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "VAT rate applied to the commission line only (fraction).", + Key = "vat_rate", + Value = "0.10" + }, + new + { + Id = 3L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Hours after check-out a booking can be disputed.", + Key = "dispute_window_hours", + Value = "72" + }, + new + { + Id = 4L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Minutes a family has to pay before a pending booking expires.", + Key = "booking_payment_deadline_minutes", + Value = "30" + }, + new + { + Id = 5L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Hours a nurse has to accept/decline a booking request.", + Key = "nurse_response_deadline_hours", + Value = "24" + }, + new + { + Id = 6L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Weekly payout cadence in days.", + Key = "nurse_payout_interval_days", + Value = "7" + }, + new + { + Id = 7L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Allowed EVV check-in distance from the care address.", + Key = "evv_location_tolerance_meters", + Value = "200" + }, + new + { + Id = 8L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "A review at or below this rating raises a support alert.", + Key = "min_rating_for_support_alert", + Value = "2" + }, + new + { + Id = 9L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "string", + Description = "Who is merchant of record for BNPL orders (platform|nurse).", + Key = "bnpl_merchant_of_record", + Value = "platform" + }, + new + { + Id = 10L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "BNPL provider commission rate (fraction).", + Key = "bnpl_provider_commission_rate", + Value = "0.07" + }, + new + { + Id = 11L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "string", + Description = "When BNPL settles funds to the platform (immediate|deferred).", + Key = "bnpl_settlement_timing", + Value = "immediate" + }, + new + { + Id = 12L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "json", + Description = "Tiered cancellation refund policy: refund_percent by hours before the visit.", + Key = "cancellation_tiers", + Value = "[{\"min_hours_before\":48,\"refund_percent\":100},{\"min_hours_before\":24,\"refund_percent\":50},{\"min_hours_before\":0,\"refund_percent\":0}]" + }, + new + { + Id = 13L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Seconds a phone must wait before another OTP can be requested.", + Key = "auth_otp_resend_seconds", + Value = "120" + }, + new + { + Id = 14L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Wrong-code attempts allowed before OTP verification is refused until a fresh code.", + Key = "auth_otp_max_attempts", + Value = "5" + }, + new + { + Id = 15L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Refresh-token session lifetime in days.", + Key = "auth_session_ttl_days", + Value = "30" + }, + new + { + Id = 16L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Hours between credential-expiry scans (the scheduled cron is deferred; the scan is admin-triggered today).", + Key = "verification_expiry_scan_cadence_hours", + Value = "24" + }, + new + { + Id = 17L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Minutes after a session's scheduled start with no EVV check-in before it is flagged a no-show.", + Key = "no_show_threshold_minutes", + Value = "60" + }, + new + { + Id = 18L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Hours between no-show sweeps (the scheduled cron is deferred; the sweep is admin-triggered today).", + Key = "no_show_scan_cadence_hours", + Value = "1" + }, + new + { + Id = 19L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "bool", + Description = "Whether a refund must link a support ticket (b11). Off until b15 ships the tickets table.", + Key = "refund_ticket_required", + Value = "false" + }, + new + { + Id = 20L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "int", + Description = "Business days shown as the customer BNPL refund ETA (b11).", + Key = "bnpl_refund_eta_business_days", + Value = "10" + }, + new + { + Id = 21L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "bool", + Description = "Ops/testing override that forces the post-payout clawback path for refunds (b11); b13 replaces the derivation.", + Key = "refund_assume_nurse_paid", + Value = "false" + }, + new + { + Id = 22L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "decimal", + Description = "IRR net-amount threshold above which a payout is routed via SATNA (real-time) instead of PAYA (batch) (b13).", + Key = "payout_satna_threshold_irr", + Value = "1000000000" + }, + new + { + Id = 23L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DataType = "bool", + Description = "When on, a BNPL-paid booking is payout-eligible only after its provider settlement is received (b13; default off — the DEFERRED settled_at guard).", + Key = "require_bnpl_settlement_for_payout", + Value = "false" + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("ProvinceId") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("ProvinceId", "SortOrder"); + + b.ToTable("Cities", "geo"); + + b.HasData( + new + { + Id = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Tehran", + NameFa = "تهران", + ProvinceId = 1L, + SortOrder = 1 + }, + new + { + Id = 102L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Karaj", + NameFa = "کرج", + ProvinceId = 2L, + SortOrder = 1 + }, + new + { + Id = 103L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Isfahan", + NameFa = "اصفهان", + ProvinceId = 3L, + SortOrder = 1 + }, + new + { + Id = 104L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Shiraz", + NameFa = "شیراز", + ProvinceId = 4L, + SortOrder = 1 + }, + new + { + Id = 105L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Mashhad", + NameFa = "مشهد", + ProvinceId = 5L, + SortOrder = 1 + }, + new + { + Id = 106L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Tabriz", + NameFa = "تبریز", + ProvinceId = 6L, + SortOrder = 1 + }, + new + { + Id = 107L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Urmia", + NameFa = "ارومیه", + ProvinceId = 7L, + SortOrder = 1 + }, + new + { + Id = 108L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ahvaz", + NameFa = "اهواز", + ProvinceId = 8L, + SortOrder = 1 + }, + new + { + Id = 109L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Qom", + NameFa = "قم", + ProvinceId = 9L, + SortOrder = 1 + }, + new + { + Id = 110L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kerman", + NameFa = "کرمان", + ProvinceId = 10L, + SortOrder = 1 + }, + new + { + Id = 111L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Rasht", + NameFa = "رشت", + ProvinceId = 11L, + SortOrder = 1 + }, + new + { + Id = 112L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Sari", + NameFa = "ساری", + ProvinceId = 12L, + SortOrder = 1 + }, + new + { + Id = 113L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Arak", + NameFa = "اراک", + ProvinceId = 13L, + SortOrder = 1 + }, + new + { + Id = 114L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ardabil", + NameFa = "اردبیل", + ProvinceId = 14L, + SortOrder = 1 + }, + new + { + Id = 115L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Qazvin", + NameFa = "قزوین", + ProvinceId = 15L, + SortOrder = 1 + }, + new + { + Id = 116L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kermanshah", + NameFa = "کرمانشاه", + ProvinceId = 16L, + SortOrder = 1 + }, + new + { + Id = 117L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Bojnord", + NameFa = "بجنورد", + ProvinceId = 17L, + SortOrder = 1 + }, + new + { + Id = 118L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Birjand", + NameFa = "بیرجند", + ProvinceId = 18L, + SortOrder = 1 + }, + new + { + Id = 119L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Hamadan", + NameFa = "همدان", + ProvinceId = 19L, + SortOrder = 1 + }, + new + { + Id = 120L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Sanandaj", + NameFa = "سنندج", + ProvinceId = 20L, + SortOrder = 1 + }, + new + { + Id = 121L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Khorramabad", + NameFa = "خرم‌آباد", + ProvinceId = 21L, + SortOrder = 1 + }, + new + { + Id = 122L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Gorgan", + NameFa = "گرگان", + ProvinceId = 22L, + SortOrder = 1 + }, + new + { + Id = 123L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Bandar Abbas", + NameFa = "بندرعباس", + ProvinceId = 23L, + SortOrder = 1 + }, + new + { + Id = 124L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Bushehr", + NameFa = "بوشهر", + ProvinceId = 24L, + SortOrder = 1 + }, + new + { + Id = 125L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Zanjan", + NameFa = "زنجان", + ProvinceId = 25L, + SortOrder = 1 + }, + new + { + Id = 126L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Semnan", + NameFa = "سمنان", + ProvinceId = 26L, + SortOrder = 1 + }, + new + { + Id = 127L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Yazd", + NameFa = "یزد", + ProvinceId = 27L, + SortOrder = 1 + }, + new + { + Id = 128L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Zahedan", + NameFa = "زاهدان", + ProvinceId = 28L, + SortOrder = 1 + }, + new + { + Id = 129L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Shahrekord", + NameFa = "شهرکرد", + ProvinceId = 29L, + SortOrder = 1 + }, + new + { + Id = 130L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Yasuj", + NameFa = "یاسوج", + ProvinceId = 30L, + SortOrder = 1 + }, + new + { + Id = 131L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ilam", + NameFa = "ایلام", + ProvinceId = 31L, + SortOrder = 1 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.District", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CityId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("CityId", "SortOrder"); + + b.ToTable("Districts", "geo"); + + b.HasData( + new + { + Id = 1001L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 1", + NameFa = "منطقه ۱", + SortOrder = 1 + }, + new + { + Id = 1002L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 2", + NameFa = "منطقه ۲", + SortOrder = 2 + }, + new + { + Id = 1003L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 3", + NameFa = "منطقه ۳", + SortOrder = 3 + }, + new + { + Id = 1004L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 4", + NameFa = "منطقه ۴", + SortOrder = 4 + }, + new + { + Id = 1005L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 5", + NameFa = "منطقه ۵", + SortOrder = 5 + }, + new + { + Id = 1006L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 6", + NameFa = "منطقه ۶", + SortOrder = 6 + }, + new + { + Id = 1007L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 7", + NameFa = "منطقه ۷", + SortOrder = 7 + }, + new + { + Id = 1008L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 8", + NameFa = "منطقه ۸", + SortOrder = 8 + }, + new + { + Id = 1009L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 9", + NameFa = "منطقه ۹", + SortOrder = 9 + }, + new + { + Id = 1010L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 10", + NameFa = "منطقه ۱۰", + SortOrder = 10 + }, + new + { + Id = 1011L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 11", + NameFa = "منطقه ۱۱", + SortOrder = 11 + }, + new + { + Id = 1012L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 12", + NameFa = "منطقه ۱۲", + SortOrder = 12 + }, + new + { + Id = 1013L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 13", + NameFa = "منطقه ۱۳", + SortOrder = 13 + }, + new + { + Id = 1014L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 14", + NameFa = "منطقه ۱۴", + SortOrder = 14 + }, + new + { + Id = 1015L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 15", + NameFa = "منطقه ۱۵", + SortOrder = 15 + }, + new + { + Id = 1016L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 16", + NameFa = "منطقه ۱۶", + SortOrder = 16 + }, + new + { + Id = 1017L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 17", + NameFa = "منطقه ۱۷", + SortOrder = 17 + }, + new + { + Id = 1018L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 18", + NameFa = "منطقه ۱۸", + SortOrder = 18 + }, + new + { + Id = 1019L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 19", + NameFa = "منطقه ۱۹", + SortOrder = 19 + }, + new + { + Id = 1020L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 20", + NameFa = "منطقه ۲۰", + SortOrder = 20 + }, + new + { + Id = 1021L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 21", + NameFa = "منطقه ۲۱", + SortOrder = 21 + }, + new + { + Id = 1022L, + CityId = 101L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "District 22", + NameFa = "منطقه ۲۲", + SortOrder = 22 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.NurseServiceArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CityId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DistrictId") + .HasColumnType("bigint"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.HasIndex("DistrictId"); + + b.HasIndex("NurseId", "CityId") + .IsUnique() + .HasDatabaseName("UX_NurseServiceAreas_Nurse_City_WholeCity") + .HasFilter("[DistrictId] IS NULL AND [DeletedAt] IS NULL"); + + b.HasIndex("NurseId", "CityId", "DistrictId") + .IsUnique() + .HasDatabaseName("UX_NurseServiceAreas_Nurse_City_District") + .HasFilter("[DistrictId] IS NOT NULL AND [DeletedAt] IS NULL"); + + b.ToTable("NurseServiceAreas", "geo"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.Province", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameEn") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("SortOrder"); + + b.ToTable("Provinces", "geo"); + + b.HasData( + new + { + Id = 1L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Tehran", + NameFa = "تهران", + SortOrder = 1 + }, + new + { + Id = 2L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Alborz", + NameFa = "البرز", + SortOrder = 2 + }, + new + { + Id = 3L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Isfahan", + NameFa = "اصفهان", + SortOrder = 3 + }, + new + { + Id = 4L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Fars", + NameFa = "فارس", + SortOrder = 4 + }, + new + { + Id = 5L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Razavi Khorasan", + NameFa = "خراسان رضوی", + SortOrder = 5 + }, + new + { + Id = 6L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "East Azerbaijan", + NameFa = "آذربایجان شرقی", + SortOrder = 6 + }, + new + { + Id = 7L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "West Azerbaijan", + NameFa = "آذربایجان غربی", + SortOrder = 7 + }, + new + { + Id = 8L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Khuzestan", + NameFa = "خوزستان", + SortOrder = 8 + }, + new + { + Id = 9L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Qom", + NameFa = "قم", + SortOrder = 9 + }, + new + { + Id = 10L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kerman", + NameFa = "کرمان", + SortOrder = 10 + }, + new + { + Id = 11L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Gilan", + NameFa = "گیلان", + SortOrder = 11 + }, + new + { + Id = 12L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Mazandaran", + NameFa = "مازندران", + SortOrder = 12 + }, + new + { + Id = 13L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Markazi", + NameFa = "مرکزی", + SortOrder = 13 + }, + new + { + Id = 14L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ardabil", + NameFa = "اردبیل", + SortOrder = 14 + }, + new + { + Id = 15L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Qazvin", + NameFa = "قزوین", + SortOrder = 15 + }, + new + { + Id = 16L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kermanshah", + NameFa = "کرمانشاه", + SortOrder = 16 + }, + new + { + Id = 17L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "North Khorasan", + NameFa = "خراسان شمالی", + SortOrder = 17 + }, + new + { + Id = 18L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "South Khorasan", + NameFa = "خراسان جنوبی", + SortOrder = 18 + }, + new + { + Id = 19L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Hamadan", + NameFa = "همدان", + SortOrder = 19 + }, + new + { + Id = 20L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kurdistan", + NameFa = "کردستان", + SortOrder = 20 + }, + new + { + Id = 21L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Lorestan", + NameFa = "لرستان", + SortOrder = 21 + }, + new + { + Id = 22L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Golestan", + NameFa = "گلستان", + SortOrder = 22 + }, + new + { + Id = 23L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Hormozgan", + NameFa = "هرمزگان", + SortOrder = 23 + }, + new + { + Id = 24L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Bushehr", + NameFa = "بوشهر", + SortOrder = 24 + }, + new + { + Id = 25L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Zanjan", + NameFa = "زنجان", + SortOrder = 25 + }, + new + { + Id = 26L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Semnan", + NameFa = "سمنان", + SortOrder = 26 + }, + new + { + Id = 27L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Yazd", + NameFa = "یزد", + SortOrder = 27 + }, + new + { + Id = 28L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Sistan and Baluchestan", + NameFa = "سیستان و بلوچستان", + SortOrder = 28 + }, + new + { + Id = 29L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Chaharmahal and Bakhtiari", + NameFa = "چهارمحال و بختیاری", + SortOrder = 29 + }, + new + { + Id = 30L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Kohgiluyeh and Boyer-Ahmad", + NameFa = "کهگیلویه و بویراحمد", + SortOrder = 30 + }, + new + { + Id = 31L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + NameEn = "Ilam", + NameFa = "ایلام", + SortOrder = 31 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Holidays.IranianHoliday", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("HolidayDate") + .HasColumnType("date"); + + b.Property("IsBankClosed") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NameFa") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("HolidayDate") + .IsUnique(); + + b.ToTable("IranianHolidays", "ops"); + + b.HasData( + new + { + Id = 1L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 2, 11), + IsBankClosed = true, + NameFa = "پیروزی انقلاب اسلامی", + Type = "national" + }, + new + { + Id = 2L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 3, 21), + IsBankClosed = true, + NameFa = "نوروز", + Type = "national" + }, + new + { + Id = 3L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 3, 22), + IsBankClosed = true, + NameFa = "نوروز", + Type = "national" + }, + new + { + Id = 4L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 3, 23), + IsBankClosed = true, + NameFa = "نوروز", + Type = "national" + }, + new + { + Id = 5L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 3, 24), + IsBankClosed = true, + NameFa = "نوروز", + Type = "national" + }, + new + { + Id = 6L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 4, 1), + IsBankClosed = true, + NameFa = "روز طبیعت (سیزده‌به‌در)", + Type = "official" + }, + new + { + Id = 7L, + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + HolidayDate = new DateOnly(2026, 6, 26), + IsBankClosed = true, + NameFa = "عید سعید قربان", + Type = "religious" + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AddressLine") + .HasColumnType("nvarchar(max)"); + + b.Property("CityId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DistrictId") + .HasColumnType("bigint"); + + b.Property("GeocodeSource") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("Latitude") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("Longitude") + .HasPrecision(9, 6) + .HasColumnType("decimal(9,6)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PostalCode") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipientName") + .HasColumnType("nvarchar(max)"); + + b.Property("RecipientPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.HasIndex("CustomerId") + .IsUnique() + .HasDatabaseName("UX_CustomerAddresses_Customer_Primary") + .HasFilter("[IsPrimary] = 1 AND [DeletedAt] IS NULL"); + + b.HasIndex("DistrictId"); + + b.ToTable("CustomerAddresses", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DefaultEmergencyContactName") + .HasColumnType("nvarchar(max)"); + + b.Property("DefaultEmergencyContactPhone") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PreferredLanguage") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("CustomerProfiles", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseBankAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountHolderFromBank") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("AccountHolderName") + .HasColumnType("nvarchar(max)"); + + b.Property("BankName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("Iban") + .HasColumnType("nvarchar(max)"); + + b.Property("IbanHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("IsPrimary") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsVerified") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("MatchedNationalId") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("OwnershipVendorRef") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("VerifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("VerifiedByAdminId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IbanHash") + .IsUnique(); + + b.HasIndex("NurseId") + .IsUnique() + .HasDatabaseName("UX_NurseBankAccounts_NurseId_Primary") + .HasFilter("[IsPrimary] = 1"); + + b.HasIndex("VerifiedByAdminId"); + + b.ToTable("NurseBankAccounts", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("AverageRating") + .ValueGeneratedOnAdd() + .HasPrecision(3, 2) + .HasColumnType("decimal(3,2)") + .HasDefaultValue(0m); + + b.Property("Bio") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("EducationField") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("EducationLevel") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsAcceptingBookings") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsVerified") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PartnerCenterId") + .HasColumnType("bigint"); + + b.Property("SpecializationsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("TotalCompletedBookings") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("TotalReviews") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("YearsOfExperience") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PartnerCenterId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("NurseProfiles", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.Patient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("BloodType") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("ConditionsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("FirstName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Gender") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("InitialMedicalNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("LastName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Relation") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Patients", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.PatientCarePlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("MedicationsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PatientId") + .HasColumnType("bigint"); + + b.Property("RoutineJson") + .HasColumnType("nvarchar(max)"); + + b.Property("TasksJson") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PatientId") + .IsUnique() + .HasDatabaseName("UX_PatientCarePlans_Patient") + .HasFilter("[DeletedAt] IS NULL"); + + b.ToTable("PatientCarePlans", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Invoices.Invoice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BnplCommissionIrr") + .HasColumnType("bigint"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("GrossIrr") + .HasColumnType("bigint"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("IssuedAt") + .HasColumnType("datetime2"); + + b.Property("IssuingEntityType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("MoadianReferenceNumber") + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("MoadianStatus") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PartnerCenterId") + .HasColumnType("bigint"); + + b.Property("PdfStorageKey") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("PlatformCommissionIrr") + .HasColumnType("bigint"); + + b.Property("VatIrr") + .HasColumnType("bigint"); + + b.Property("VatRate") + .HasPrecision(5, 4) + .HasColumnType("decimal(5,4)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique(); + + b.HasIndex("InvoiceNumber") + .IsUnique(); + + b.ToTable("Invoices", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Invoices.InvoiceNumberSequence", b => + { + b.Property("Id") + .HasColumnType("int"); + + b.Property("NextValue") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("InvoiceNumberSequences", "payments"); + + b.HasData( + new + { + Id = 1, + NextValue = 1L + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ClosedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ClosedById") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("OpenedById") + .HasColumnType("int"); + + b.Property("ReferenceCode") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("RefundId") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Subject") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("ClosedById"); + + b.HasIndex("OpenedById"); + + b.HasIndex("ReferenceCode") + .IsUnique(); + + b.HasIndex("RefundId"); + + b.HasIndex("Status"); + + b.HasIndex("Status", "CreatedAt"); + + b.ToTable("Tickets", "messaging"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.TicketMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Body") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ClientMessageId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsInternal") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("SenderId") + .HasColumnType("int"); + + b.Property("SentAt") + .HasColumnType("datetimeoffset"); + + b.Property("TicketId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("SenderId"); + + b.HasIndex("TicketId", "ClientMessageId") + .IsUnique() + .HasDatabaseName("UX_TicketMessages_Ticket_ClientMessageId") + .HasFilter("[ClientMessageId] IS NOT NULL AND [DeletedAt] IS NULL"); + + b.HasIndex("TicketId", "SentAt"); + + b.ToTable("TicketMessages", "messaging"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.TicketParticipant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AddedById") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("LastReadAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("RemovedAt") + .HasColumnType("datetimeoffset"); + + b.Property("RoleOnTicket") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("TicketId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AddedById"); + + b.HasIndex("TicketId", "UserId") + .IsUnique(); + + b.HasIndex("UserId", "TicketId"); + + b.ToTable("TicketParticipants", "messaging"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Notifications.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Body") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DataJson") + .HasColumnType("nvarchar(max)"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ReadAt") + .HasColumnType("datetimeoffset"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "IsRead", "CreatedAt"); + + b.ToTable("Notifications", "ops"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.PartnerCenters.PartnerCenter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminUserId") + .HasColumnType("int"); + + b.Property("CommissionRate") + .HasPrecision(5, 4) + .HasColumnType("decimal(5,4)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("EnamadCode") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsMerchantOfRecord") + .HasColumnType("bit"); + + b.Property("LegalEntityType") + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("MohEstablishmentPermitNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("SettlementIban") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("TechnicalDirectorLicenseNo") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("TechnicalDirectorNurseUserId") + .HasColumnType("int"); + + b.Property("VerifiedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("AdminUserId"); + + b.HasIndex("IsActive"); + + b.HasIndex("TechnicalDirectorNurseUserId"); + + b.ToTable("PartnerCenters", "partner"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.LedgerEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccountType") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("AmountIrr") + .HasColumnType("bigint"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Direction") + .IsRequired() + .HasMaxLength(6) + .HasColumnType("nvarchar(6)"); + + b.Property("Memo") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("SourceRefId") + .HasColumnType("bigint"); + + b.Property("SourceRefType") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("TransactionGroupId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("NurseId"); + + b.HasIndex("TransactionGroupId"); + + b.HasIndex("AccountType", "NurseId"); + + b.HasIndex("SourceRefType", "SourceRefId"); + + b.ToTable("LedgerEntries", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.PaymentGateway", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("ProviderCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Type", "IsActive", "Priority"); + + b.ToTable("PaymentGateways", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.PaymentTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("BookingRequestId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + + b.Property("CustomerId") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("GatewayId") + .HasColumnType("bigint"); + + b.Property("GatewayReferenceCode") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("GatewayResponseCode") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GatewayResponseJson") + .HasColumnType("nvarchar(max)"); + + b.Property("GatewayTransactionId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("IsInstallment") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("UserAgent") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique() + .HasFilter("[Status] = 'succeeded' AND [BookingId] IS NOT NULL"); + + b.HasIndex("CustomerId"); + + b.HasIndex("GatewayId"); + + b.HasIndex("GatewayReferenceCode") + .IsUnique() + .HasFilter("[GatewayReferenceCode] IS NOT NULL"); + + b.HasIndex("BookingId", "Status"); + + b.HasIndex("BookingRequestId", "Status"); + + b.ToTable("PaymentTransactions", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.PaymentWebhookEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("nvarchar(80)"); + + b.Property("ExternalEventId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProcessedAt") + .HasColumnType("datetime2"); + + b.Property("ProcessingStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ProviderCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ReceivedAt") + .HasColumnType("datetime2"); + + b.Property("RelatedPaymentTransactionId") + .HasColumnType("bigint"); + + b.Property("SignatureValid") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("ProviderCode", "ExternalEventId") + .IsUnique(); + + b.ToTable("PaymentWebhookEvents", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("BankAccountId") + .HasColumnType("bigint"); + + b.Property("BatchId") + .HasColumnType("bigint"); + + b.Property("BookingCount") + .HasColumnType("int"); + + b.Property("ClawbackAppliedIrr") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("FailureReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("GrossEarningsIrr") + .HasColumnType("bigint"); + + b.Property("IbanSnapshot") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NetAmountIrr") + .HasColumnType("bigint"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("PaidAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("TransferReference") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("BankAccountId"); + + b.HasIndex("BatchId"); + + b.HasIndex("NurseId"); + + b.HasIndex("Status"); + + b.ToTable("NursePayouts", "payouts", t => + { + t.HasCheckConstraint("CK_NursePayouts_NetSplit", "[NetAmountIrr] = [GrossEarningsIrr] - [ClawbackAppliedIrr] AND [GrossEarningsIrr] >= 0 AND [ClawbackAppliedIrr] >= 0 AND [NetAmountIrr] >= 0 AND [Amount] >= 0"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("FailureNotes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("InitiatedByAdminId") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PayoutCount") + .HasColumnType("int"); + + b.Property("PeriodEnd") + .HasColumnType("date"); + + b.Property("PeriodStart") + .HasColumnType("date"); + + b.Property("ProcessedAt") + .HasColumnType("datetime2"); + + b.Property("ProcessingDate") + .HasColumnType("date"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("TotalAmount") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InitiatedByAdminId"); + + b.HasIndex("ProcessingDate"); + + b.HasIndex("Status"); + + b.ToTable("NursePayoutBatches", "payouts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBookingLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PayoutAmountIrr") + .HasColumnType("bigint"); + + b.Property("PayoutId") + .HasColumnType("bigint"); + + b.Property("SessionId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique(); + + b.HasIndex("PayoutId"); + + b.HasIndex("SessionId"); + + b.ToTable("NursePayoutBookingLinks", "payouts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Refunds.NurseClawback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AmountIrr") + .HasColumnType("bigint"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("OriginalPayoutId") + .HasColumnType("bigint"); + + b.Property("RecoveredInPayoutId") + .HasColumnType("bigint"); + + b.Property("RefundId") + .HasColumnType("bigint"); + + b.Property("ResolutionNotes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ResolvedAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("NurseId"); + + b.HasIndex("OriginalPayoutId"); + + b.HasIndex("RecoveredInPayoutId"); + + b.HasIndex("RefundId") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("NurseClawbacks", "payments"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Refunds.Refund", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdminNotes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Amount") + .HasColumnType("bigint"); + + b.Property("ApprovedByAdminId") + .HasColumnType("int"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CancellationPolicyCode") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ExpectedCustomerRefundEta") + .HasColumnType("date"); + + b.Property("ExternalRevertReference") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("GatewayRefundReference") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NursePayoutRefundedIrr") + .HasColumnType("bigint"); + + b.Property("PaymentTransactionId") + .HasColumnType("bigint"); + + b.Property("PlatformFeeRefundedIrr") + .HasColumnType("bigint"); + + b.Property("ProcessedAt") + .HasColumnType("datetime2"); + + b.Property("ReasonCategory") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ReasonNotes") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RefundChannel") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RefundPercentage") + .HasPrecision(6, 4) + .HasColumnType("decimal(6,4)"); + + b.Property("RefundPercentageApplied") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("RejectedReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RequestedByCustomerId") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("TicketId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("PaymentTransactionId"); + + b.HasIndex("RequestedByCustomerId"); + + b.HasIndex("Status"); + + b.HasIndex("TicketId"); + + b.ToTable("Refunds", "payments", t => + { + t.HasCheckConstraint("CK_Refunds_LegSplit", "[Amount] = [PlatformFeeRefundedIrr] + [NursePayoutRefundedIrr] AND [Amount] >= 0 AND [PlatformFeeRefundedIrr] >= 0 AND [NursePayoutRefundedIrr] >= 0"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.PatientCareRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BodyEncrypted") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseProfileId") + .HasColumnType("bigint"); + + b.Property("PatientId") + .HasColumnType("bigint"); + + b.Property("RecordedAt") + .HasColumnType("datetime2"); + + b.Property("TaskResultsJson") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("BookingId"); + + b.HasIndex("NurseProfileId"); + + b.HasIndex("PatientId", "RecordedAt") + .HasDatabaseName("IX_PatientCareRecords_Patient_RecordedAt"); + + b.ToTable("PatientCareRecords", "reviews"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.Review", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Body") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CustomerProfileId") + .HasColumnType("bigint"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModeratedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModeratedById") + .HasColumnType("int"); + + b.Property("ModerationReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ModerationStatus") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseProfileId") + .HasColumnType("bigint"); + + b.Property("Rating") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BookingId") + .IsUnique(); + + b.HasIndex("CustomerProfileId"); + + b.HasIndex("ModerationStatus"); + + b.HasIndex("NurseProfileId", "ModerationStatus"); + + b.ToTable("Reviews", "reviews", t => + { + t.HasCheckConstraint("CK_Reviews_Rating", "[Rating] BETWEEN 1 AND 5"); + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.ReviewTagLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("ReviewId") + .HasColumnType("bigint"); + + b.Property("ReviewTagMasterId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ReviewTagMasterId"); + + b.HasIndex("ReviewId", "ReviewTagMasterId") + .IsUnique() + .HasDatabaseName("UX_ReviewTagLinks_Review_Tag"); + + b.ToTable("ReviewTagLinks", "reviews"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.ReviewTagMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("LabelEn") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LabelFa") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsActive", "SortOrder"); + + b.ToTable("ReviewTagsMasters", "reviews"); + + b.HasData( + new + { + Id = 1L, + Code = "punctual", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Punctual", + LabelFa = "وقت‌شناس", + SortOrder = 1 + }, + new + { + Id = 2L, + Code = "professional", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Professional", + LabelFa = "حرفه‌ای", + SortOrder = 2 + }, + new + { + Id = 3L, + Code = "clean", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Clean", + LabelFa = "تمیز و بهداشتی", + SortOrder = 3 + }, + new + { + Id = 4L, + Code = "kind", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Kind", + LabelFa = "مهربان", + SortOrder = 4 + }, + new + { + Id = 5L, + Code = "communicative", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + IsActive = true, + LabelEn = "Communicative", + LabelFa = "خوش‌برخورد", + SortOrder = 5 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Search.NurseSearchIndex", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("AverageRating") + .HasPrecision(3, 2) + .HasColumnType("decimal(3,2)"); + + b.Property("CityId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("DistrictId") + .HasColumnType("bigint"); + + b.Property("IsSearchable") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseGender") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("NurseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Price") + .HasColumnType("bigint"); + + b.Property("PriceUnit") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ServiceCategoryId") + .HasColumnType("bigint"); + + b.Property("TotalCompletedBookings") + .HasColumnType("int"); + + b.Property("TotalReviews") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("VariantId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("NurseId"); + + b.HasIndex("VariantId", "CityId") + .IsUnique() + .HasDatabaseName("UX_NurseSearchIndex_Variant_City_WholeCity") + .HasFilter("[DistrictId] IS NULL AND [DeletedAt] IS NULL"); + + b.HasIndex("VariantId", "CityId", "DistrictId") + .IsUnique() + .HasDatabaseName("UX_NurseSearchIndex_Variant_City_District") + .HasFilter("[DistrictId] IS NOT NULL AND [DeletedAt] IS NULL"); + + b.HasIndex("IsSearchable", "ServiceCategoryId", "CityId", "DistrictId") + .HasDatabaseName("IX_NurseSearchIndex_Search"); + + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("IsSearchable", "ServiceCategoryId", "CityId", "DistrictId"), new[] { "Price", "NurseGender", "AverageRating", "TotalReviews", "NurseId", "VariantId", "NurseName" }); + + b.ToTable("NurseSearchIndices", "search"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.SupportAlerts.SupportAlert", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BookingId") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("OwnerUserId") + .HasColumnType("int"); + + b.Property("ResolutionNote") + .HasColumnType("nvarchar(max)"); + + b.Property("ResolvedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ReviewId") + .HasColumnType("bigint"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.HasKey("Id"); + + b.HasIndex("OwnerUserId"); + + b.HasIndex("Status"); + + b.HasIndex("Type"); + + b.ToTable("SupportAlerts", "ops"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedClaim") + .HasColumnType("datetime2"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("UserId"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FamilyName") + .HasColumnType("nvarchar(max)"); + + b.Property("Gender") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("GeneratedCode") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NationalId") + .HasColumnType("nvarchar(max)"); + + b.Property("NationalIdVerifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneHash") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("PhoneVerifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("ShahkarVerifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.HasIndex("PhoneHash") + .IsUnique() + .HasFilter("[PhoneHash] IS NOT NULL"); + + b.ToTable("Users", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("LoggedOn") + .HasColumnType("datetime2"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("IsValid") + .HasColumnType("bit"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserRefreshTokens", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.Property("CreatedUserRoleDate") + .HasColumnType("datetime2"); + + b.Property("GrantedAt") + .HasColumnType("datetimeoffset"); + + b.Property("GrantedById") + .HasColumnType("int"); + + b.Property("RevokedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("GrantedById"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeviceInfo") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("ExpiresAt") + .HasColumnType("datetimeoffset"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("RefreshTokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("RevokedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RefreshTokenHash") + .IsUnique(); + + b.HasIndex("UserId", "IsRevoked"); + + b.ToTable("UserSessions", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("GeneratedTime") + .HasColumnType("datetime2"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", "usr"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseCredential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("CredentialNumber") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("CredentialType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ExpiresAt") + .HasColumnType("date"); + + b.Property("HolderNameSnapshot") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IssuedAt") + .HasColumnType("date"); + + b.Property("IssuingAuthority") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("VerificationMethod") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("VerificationSource") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("VerifiedByAdminId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("VerifiedByAdminId"); + + b.HasIndex("NurseId", "CredentialType"); + + b.ToTable("NurseCredentials", "verif"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseVerification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApprovedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("InternalNotes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseId") + .HasColumnType("bigint"); + + b.Property("RejectedAt") + .HasColumnType("datetimeoffset"); + + b.Property("RejectionReason") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ReviewedByAdminId") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("SubmittedAt") + .HasColumnType("datetimeoffset"); + + b.Property("SuspendedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("NurseId") + .IsUnique(); + + b.HasIndex("ReviewedByAdminId"); + + b.ToTable("NurseVerifications", "verif"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("IntegrityHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("ObjectStorageKey") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("OriginalFileName") + .HasMaxLength(260) + .HasColumnType("nvarchar(260)"); + + b.Property("StepId") + .HasColumnType("bigint"); + + b.Property("UploadedByUserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("StepId"); + + b.HasIndex("UploadedByUserId"); + + b.ToTable("VerificationDocuments", "verif"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("ExpiresAt") + .HasColumnType("datetimeoffset"); + + b.Property("ExternalResponseJson") + .HasColumnType("nvarchar(max)"); + + b.Property("FailureReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsAutomated") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("NurseVerificationId") + .HasColumnType("bigint"); + + b.Property("StartedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("StepTypeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("StepTypeId"); + + b.HasIndex("NurseVerificationId", "StepTypeId") + .IsUnique() + .HasDatabaseName("UX_VerificationSteps_Verification_StepType"); + + b.ToTable("VerificationSteps", "verif"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStepType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AutomationProvider") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsAutomated") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("IsRequired") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsActive", "SortOrder"); + + b.ToTable("VerificationStepTypes", "verif"); + + b.HasData( + new + { + Id = 1L, + AutomationProvider = "identity_kyc_vendor", + Code = "identity_kyc", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "National-ID validity, name match and photo/video liveness via an Iranian e-KYC vendor.", + DisplayName = "Identity Verification (KYC)", + IsActive = true, + IsAutomated = true, + IsRequired = true, + SortOrder = 1 + }, + new + { + Id = 2L, + AutomationProvider = "shahkar", + Code = "shahkar_match", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "Confirms the login SIM is registered to the nurse's own national ID (شاهکار).", + DisplayName = "Shahkar Phone Binding", + IsActive = true, + IsAutomated = true, + IsRequired = true, + SortOrder = 2 + }, + new + { + Id = 3L, + Code = "moh_competency_license", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "پروانه صلاحیت حرفه‌ای — the MoH-mandated in-home nursing licence (bundles the criminal-record screen). Manual today.", + DisplayName = "MoH Professional Competency License", + IsActive = true, + IsAutomated = false, + IsRequired = true, + SortOrder = 3 + }, + new + { + Id = 4L, + Code = "ino_membership", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "نظام پرستاری membership cross-check (ino.ir). Manual today.", + DisplayName = "Nursing Organization (INO) Membership", + IsActive = true, + IsAutomated = false, + IsRequired = true, + SortOrder = 4 + }, + new + { + Id = 5L, + Code = "criminal_record", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "عدم سوء پیشینه — consent-gated, nurse-uploaded, time-limited (reverts on expiry).", + DisplayName = "Criminal Record Certificate", + IsActive = true, + IsAutomated = false, + IsRequired = true, + SortOrder = 5 + }, + new + { + Id = 6L, + AutomationProvider = "sheba", + Code = "bank_account_verification", + CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + Description = "استعلام شبا — the payout IBAN owner's national ID must equal the verified nurse national ID.", + DisplayName = "Bank Account (IBAN) Ownership", + IsActive = true, + IsAutomated = true, + IsRequired = true, + SortOrder = 6 + }); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Analytics.SystemEvent", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Audit.AuditLog", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("ActorUserId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Bnpl.BnplTransaction", b => + { + b.HasOne("Baya.Domain.Entities.Payments.PaymentTransaction", null) + .WithMany() + .HasForeignKey("PaymentTransactionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.Booking", b => + { + b.HasOne("Baya.Domain.Entities.Booking.BookingRequest", null) + .WithOne() + .HasForeignKey("Baya.Domain.Entities.Booking.Booking", "BookingRequestId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerAddress", null) + .WithMany() + .HasForeignKey("CustomerAddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", null) + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.Patient", null) + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.NurseServiceVariant", null) + .WithMany() + .HasForeignKey("VariantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingCareInstruction", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", "Booking") + .WithOne("CareInstructions") + .HasForeignKey("Baya.Domain.Entities.Booking.BookingCareInstruction", "BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingRequest", b => + { + b.HasOne("Baya.Domain.Entities.Identity.CustomerAddress", "CustomerAddress") + .WithMany() + .HasForeignKey("CustomerAddressId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse") + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.Patient", "Patient") + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.NurseServiceVariant", "Variant") + .WithMany() + .HasForeignKey("VariantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("CustomerAddress"); + + b.Navigation("Nurse"); + + b.Navigation("Patient"); + + b.Navigation("Variant"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingSession", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", "Booking") + .WithMany("Sessions") + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Booking"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.VisitVerification", b => + { + b.HasOne("Baya.Domain.Entities.Booking.BookingSession", "Session") + .WithOne("Verification") + .HasForeignKey("Baya.Domain.Entities.Booking.VisitVerification", "BookingSessionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariant", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse") + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.ServiceCategory", "ServiceCategory") + .WithMany("Variants") + .HasForeignKey("ServiceCategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Nurse"); + + b.Navigation("ServiceCategory"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariantOption", b => + { + b.HasOne("Baya.Domain.Entities.Catalog.ServiceOptionGroup", "OptionGroup") + .WithMany() + .HasForeignKey("OptionGroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.ServiceOptionValue", "OptionValue") + .WithMany() + .HasForeignKey("OptionValueId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.NurseServiceVariant", "Variant") + .WithMany("Options") + .HasForeignKey("VariantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("OptionGroup"); + + b.Navigation("OptionValue"); + + b.Navigation("Variant"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionGroup", b => + { + b.HasOne("Baya.Domain.Entities.Catalog.ServiceCategory", "ServiceCategory") + .WithMany("OptionGroups") + .HasForeignKey("ServiceCategoryId"); + + b.Navigation("ServiceCategory"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionValue", b => + { + b.HasOne("Baya.Domain.Entities.Catalog.ServiceOptionGroup", "OptionGroup") + .WithMany("Values") + .HasForeignKey("OptionGroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("OptionGroup"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.City", b => + { + b.HasOne("Baya.Domain.Entities.Geography.Province", "Province") + .WithMany("Cities") + .HasForeignKey("ProvinceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Province"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.District", b => + { + b.HasOne("Baya.Domain.Entities.Geography.City", "City") + .WithMany("Districts") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("City"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.NurseServiceArea", b => + { + b.HasOne("Baya.Domain.Entities.Geography.City", "City") + .WithMany() + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Geography.District", "District") + .WithMany() + .HasForeignKey("DistrictId"); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("City"); + + b.Navigation("District"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerAddress", b => + { + b.HasOne("Baya.Domain.Entities.Geography.City", "City") + .WithMany() + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Geography.District", "District") + .WithMany() + .HasForeignKey("DistrictId"); + + b.Navigation("City"); + + b.Navigation("Customer"); + + b.Navigation("District"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerProfile", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithOne() + .HasForeignKey("Baya.Domain.Entities.Identity.CustomerProfile", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseBankAccount", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse") + .WithMany("BankAccounts") + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("VerifiedByAdminId"); + + b.Navigation("Nurse"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseProfile", b => + { + b.HasOne("Baya.Domain.Entities.PartnerCenters.PartnerCenter", null) + .WithMany() + .HasForeignKey("PartnerCenterId"); + + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithOne() + .HasForeignKey("Baya.Domain.Entities.Identity.NurseProfile", "UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.Patient", b => + { + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", "Customer") + .WithMany("Patients") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.PatientCarePlan", b => + { + b.HasOne("Baya.Domain.Entities.Identity.Patient", null) + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Invoices.Invoice", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.Ticket", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId"); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("ClosedById"); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("OpenedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Refunds.Refund", null) + .WithMany() + .HasForeignKey("RefundId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.TicketMessage", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Messaging.Ticket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.TicketParticipant", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("AddedById"); + + b.HasOne("Baya.Domain.Entities.Messaging.Ticket", "Ticket") + .WithMany("Participants") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Notifications.Notification", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.PartnerCenters.PartnerCenter", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("AdminUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("TechnicalDirectorNurseUserId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.LedgerEntry", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId"); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payments.PaymentTransaction", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId"); + + b.HasOne("Baya.Domain.Entities.Booking.BookingRequest", null) + .WithMany() + .HasForeignKey("BookingRequestId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", null) + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Payments.PaymentGateway", null) + .WithMany() + .HasForeignKey("GatewayId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayout", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseBankAccount", null) + .WithMany() + .HasForeignKey("BankAccountId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Payouts.NursePayoutBatch", "Batch") + .WithMany("Payouts") + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Batch"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBatch", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("InitiatedByAdminId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBookingLink", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Payouts.NursePayout", null) + .WithMany("BookingLinks") + .HasForeignKey("PayoutId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Booking.BookingSession", null) + .WithMany() + .HasForeignKey("SessionId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Refunds.NurseClawback", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Refunds.Refund", null) + .WithMany() + .HasForeignKey("RefundId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Refunds.Refund", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Payments.PaymentTransaction", null) + .WithMany() + .HasForeignKey("PaymentTransactionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", null) + .WithMany() + .HasForeignKey("RequestedByCustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.PatientCareRecord", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId"); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseProfileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.Patient", null) + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.Review", b => + { + b.HasOne("Baya.Domain.Entities.Booking.Booking", null) + .WithMany() + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", null) + .WithMany() + .HasForeignKey("CustomerProfileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseProfileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.ReviewTagLink", b => + { + b.HasOne("Baya.Domain.Entities.Reviews.Review", "Review") + .WithMany("TagLinks") + .HasForeignKey("ReviewId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Reviews.ReviewTagMaster", "Tag") + .WithMany("Links") + .HasForeignKey("ReviewTagMasterId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Review"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Search.NurseSearchIndex", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Catalog.NurseServiceVariant", "Variant") + .WithMany() + .HasForeignKey("VariantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Variant"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.SupportAlerts.SupportAlert", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("OwnerUserId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.RoleClaim", b => + { + b.HasOne("Baya.Domain.Entities.User.Role", "Role") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserClaim", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserLogin", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserRefreshToken", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("UserRefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserRole", b => + { + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("GrantedById"); + + b.HasOne("Baya.Domain.Entities.User.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserSession", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("Sessions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.UserToken", b => + { + b.HasOne("Baya.Domain.Entities.User.User", "User") + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseCredential", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null) + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("VerifiedByAdminId"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseVerification", b => + { + b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse") + .WithMany() + .HasForeignKey("NurseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("ReviewedByAdminId"); + + b.Navigation("Nurse"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationDocument", b => + { + b.HasOne("Baya.Domain.Entities.Verification.VerificationStep", "Step") + .WithMany("Documents") + .HasForeignKey("StepId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.User.User", null) + .WithMany() + .HasForeignKey("UploadedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStep", b => + { + b.HasOne("Baya.Domain.Entities.Verification.NurseVerification", "NurseVerification") + .WithMany("Steps") + .HasForeignKey("NurseVerificationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Baya.Domain.Entities.Verification.VerificationStepType", "StepType") + .WithMany("Steps") + .HasForeignKey("StepTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("NurseVerification"); + + b.Navigation("StepType"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.Booking", b => + { + b.Navigation("CareInstructions"); + + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingSession", b => + { + b.Navigation("Verification"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariant", b => + { + b.Navigation("Options"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceCategory", b => + { + b.Navigation("OptionGroups"); + + b.Navigation("Variants"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Catalog.ServiceOptionGroup", b => + { + b.Navigation("Values"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.City", b => + { + b.Navigation("Districts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Geography.Province", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerProfile", b => + { + b.Navigation("Patients"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseProfile", b => + { + b.Navigation("BankAccounts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Messaging.Ticket", b => + { + b.Navigation("Messages"); + + b.Navigation("Participants"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayout", b => + { + b.Navigation("BookingLinks"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Payouts.NursePayoutBatch", b => + { + b.Navigation("Payouts"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.Review", b => + { + b.Navigation("TagLinks"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Reviews.ReviewTagMaster", b => + { + b.Navigation("Links"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.Role", b => + { + b.Navigation("Claims"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.User.User", b => + { + b.Navigation("Claims"); + + b.Navigation("Logins"); + + b.Navigation("Sessions"); + + b.Navigation("Tokens"); + + b.Navigation("UserRefreshTokens"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.NurseVerification", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStep", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Baya.Domain.Entities.Verification.VerificationStepType", b => + { + b.Navigation("Steps"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713072137_RefinementPhase3TierC.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713072137_RefinementPhase3TierC.cs new file mode 100644 index 0000000..57cbabc --- /dev/null +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/20260713072137_RefinementPhase3TierC.cs @@ -0,0 +1,57 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Baya.Infrastructure.Persistence.Migrations +{ + /// + public partial class RefinementPhase3TierC : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastReadAt", + schema: "messaging", + table: "TicketParticipants", + type: "datetimeoffset", + nullable: true); + + migrationBuilder.AddColumn( + name: "ClientMessageId", + schema: "messaging", + table: "TicketMessages", + type: "nvarchar(100)", + maxLength: 100, + nullable: true); + + migrationBuilder.CreateIndex( + name: "UX_TicketMessages_Ticket_ClientMessageId", + schema: "messaging", + table: "TicketMessages", + columns: new[] { "TicketId", "ClientMessageId" }, + unique: true, + filter: "[ClientMessageId] IS NOT NULL AND [DeletedAt] IS NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "UX_TicketMessages_Ticket_ClientMessageId", + schema: "messaging", + table: "TicketMessages"); + + migrationBuilder.DropColumn( + name: "LastReadAt", + schema: "messaging", + table: "TicketParticipants"); + + migrationBuilder.DropColumn( + name: "ClientMessageId", + schema: "messaging", + table: "TicketMessages"); + } + } +} diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 8d3e788..72b6f67 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -2477,6 +2477,10 @@ namespace Baya.Infrastructure.Persistence.Migrations b.Property("DistrictId") .HasColumnType("bigint"); + b.Property("GeocodeSource") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + b.Property("IsPrimary") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -2532,6 +2536,10 @@ namespace Baya.Infrastructure.Persistence.Migrations SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + b.Property("CreatedAt") .HasColumnType("datetimeoffset"); @@ -2550,6 +2558,10 @@ namespace Baya.Infrastructure.Persistence.Migrations b.Property("ModifiedById") .HasColumnType("int"); + b.Property("PreferredLanguage") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + b.Property("UserId") .HasColumnType("int"); @@ -2649,6 +2661,10 @@ namespace Baya.Infrastructure.Persistence.Migrations SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + b.Property("AverageRating") .ValueGeneratedOnAdd() .HasPrecision(3, 2) @@ -2739,6 +2755,9 @@ namespace Baya.Infrastructure.Persistence.Migrations .HasMaxLength(10) .HasColumnType("nvarchar(10)"); + b.Property("ConditionsJson") + .HasColumnType("nvarchar(max)"); + b.Property("CreatedAt") .HasColumnType("datetimeoffset"); @@ -2779,6 +2798,10 @@ namespace Baya.Infrastructure.Persistence.Migrations b.Property("ModifiedById") .HasColumnType("int"); + b.Property("Relation") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + b.HasKey("Id"); b.HasIndex("CustomerId"); @@ -2786,6 +2809,51 @@ namespace Baya.Infrastructure.Persistence.Migrations b.ToTable("Patients", "usr"); }); + modelBuilder.Entity("Baya.Domain.Entities.Identity.PatientCarePlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("MedicationsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("ModifiedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ModifiedById") + .HasColumnType("int"); + + b.Property("PatientId") + .HasColumnType("bigint"); + + b.Property("RoutineJson") + .HasColumnType("nvarchar(max)"); + + b.Property("TasksJson") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PatientId") + .IsUnique() + .HasDatabaseName("UX_PatientCarePlans_Patient") + .HasFilter("[DeletedAt] IS NULL"); + + b.ToTable("PatientCarePlans", "usr"); + }); + modelBuilder.Entity("Baya.Domain.Entities.Invoices.Invoice", b => { b.Property("Id") @@ -2977,6 +3045,10 @@ namespace Baya.Infrastructure.Persistence.Migrations .HasMaxLength(4000) .HasColumnType("nvarchar(4000)"); + b.Property("ClientMessageId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + b.Property("CreatedAt") .HasColumnType("datetimeoffset"); @@ -3010,6 +3082,11 @@ namespace Baya.Infrastructure.Persistence.Migrations b.HasIndex("SenderId"); + b.HasIndex("TicketId", "ClientMessageId") + .IsUnique() + .HasDatabaseName("UX_TicketMessages_Ticket_ClientMessageId") + .HasFilter("[ClientMessageId] IS NOT NULL AND [DeletedAt] IS NULL"); + b.HasIndex("TicketId", "SentAt"); b.ToTable("TicketMessages", "messaging"); @@ -3032,6 +3109,9 @@ namespace Baya.Infrastructure.Persistence.Migrations b.Property("CreatedById") .HasColumnType("int"); + b.Property("LastReadAt") + .HasColumnType("datetimeoffset"); + b.Property("ModifiedAt") .HasColumnType("datetimeoffset"); @@ -3882,6 +3962,9 @@ namespace Baya.Infrastructure.Persistence.Migrations b.Property("RecordedAt") .HasColumnType("datetime2"); + b.Property("TaskResultsJson") + .HasColumnType("nvarchar(max)"); + b.HasKey("Id"); b.HasIndex("BookingId"); @@ -4117,6 +4200,10 @@ namespace Baya.Infrastructure.Persistence.Migrations SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + b.Property("AvatarUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + b.Property("AverageRating") .HasPrecision(3, 2) .HasColumnType("decimal(3,2)"); @@ -4152,6 +4239,10 @@ namespace Baya.Infrastructure.Persistence.Migrations b.Property("NurseId") .HasColumnType("bigint"); + b.Property("NurseName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + b.Property("Price") .HasColumnType("bigint"); @@ -4192,7 +4283,7 @@ namespace Baya.Infrastructure.Persistence.Migrations b.HasIndex("IsSearchable", "ServiceCategoryId", "CityId", "DistrictId") .HasDatabaseName("IX_NurseSearchIndex_Search"); - SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("IsSearchable", "ServiceCategoryId", "CityId", "DistrictId"), new[] { "Price", "NurseGender", "AverageRating", "TotalReviews", "NurseId", "VariantId" }); + SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("IsSearchable", "ServiceCategoryId", "CityId", "DistrictId"), new[] { "Price", "NurseGender", "AverageRating", "TotalReviews", "NurseId", "VariantId", "NurseName" }); b.ToTable("NurseSearchIndices", "search"); }); @@ -5359,6 +5450,15 @@ namespace Baya.Infrastructure.Persistence.Migrations b.Navigation("Customer"); }); + modelBuilder.Entity("Baya.Domain.Entities.Identity.PatientCarePlan", b => + { + b.HasOne("Baya.Domain.Entities.Identity.Patient", null) + .WithMany() + .HasForeignKey("PatientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + modelBuilder.Entity("Baya.Domain.Entities.Invoices.Invoice", b => { b.HasOne("Baya.Domain.Entities.Booking.Booking", null) diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/BnplRepository.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/BnplRepository.cs index 60f6ecf..72d23aa 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/BnplRepository.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/BnplRepository.cs @@ -57,6 +57,17 @@ internal sealed class BnplRepository : BaseAsyncRepository, IBn e => e.SourceRefType == LedgerSourceRefType.BnplTransaction && e.SourceRefId == bnplTransactionId, cancellationToken); + public async Task GetStatusByRequestAsync(long bookingRequestId, CancellationToken cancellationToken) + { + var id = await TableNoTracking + .Where(t => DbContext.Set().Any(pt => pt.Id == t.PaymentTransactionId && pt.BookingRequestId == bookingRequestId)) + .OrderByDescending(t => t.Id) + .Select(t => (long?)t.Id) + .FirstOrDefaultAsync(cancellationToken); + + return id is null ? null : await GetStatusAsync(id.Value, cancellationToken); + } + public async Task GetStatusAsync(long id, CancellationToken cancellationToken) { var row = await (from t in TableNoTracking diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/BookingRequestRepository.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/BookingRequestRepository.cs index 18576ed..7718583 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/BookingRequestRepository.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/BookingRequestRepository.cs @@ -7,6 +7,7 @@ using Baya.Application.Models.Payments; using Baya.Domain.Entities.Booking; using Baya.Infrastructure.Persistence.Repositories.Common; using Microsoft.EntityFrameworkCore; +using BookingEntity = Baya.Domain.Entities.Booking.Booking; namespace Baya.Infrastructure.Persistence.Repositories; @@ -68,14 +69,17 @@ internal sealed class BookingRequestRepository : BaseAsyncRepository new BookingRequestListItemDto( r.Id, r.Status, ComposeName(r.NurseName, r.NurseFamily), r.NurseRating, r.RequiredCaregiverGender, r.RequestedDate, r.RequestedTimeStart, r.RequestedTimeEnd, - r.NurseResponseDeadlineAt, r.PaymentDeadlineAt, CustomerNotes: null)) + r.NurseResponseDeadlineAt, r.PaymentDeadlineAt, CustomerNotes: null, + r.VariantLabel, AgeFrom(r.PatientBirthDate))) .ToList(); return new PagedResult(items, total, page, pageSize); @@ -89,25 +93,35 @@ internal sealed class BookingRequestRepository : BaseAsyncRepository r.Status == status); var total = await query.CountAsync(cancellationToken); - var items = await OrderActionableFirst(query) + // Nurse inbox: counterparty is the patient, and stage-1 customer_notes is the ONLY clinical context + // surfaced. No encrypted/care field is projected here, ever. Age is folded in memory (DateOnly + // arithmetic is not translatable), so fetch a flat row first. + var rows = await OrderActionableFirst(query) .Skip((page - 1) * pageSize) .Take(pageSize) - // Nurse inbox: counterparty is the patient, and stage-1 customer_notes is the ONLY clinical - // context surfaced. No encrypted/care field is projected here, ever. - .Select(r => new BookingRequestListItemDto( + .Select(r => new NurseRow( r.Id, r.Status, r.Patient.DisplayName, - null, r.RequiredCaregiverGender, r.RequestedDate, r.RequestedTimeStart, r.RequestedTimeEnd, r.NurseResponseDeadlineAt, r.PaymentDeadlineAt, - r.CustomerNotes)) + r.CustomerNotes, + r.Variant.DisplayName, + r.Patient.BirthDate)) .ToListAsync(cancellationToken); + var items = rows + .Select(r => new BookingRequestListItemDto( + r.Id, r.Status, r.PatientName, null, r.RequiredCaregiverGender, + r.RequestedDate, r.RequestedTimeStart, r.RequestedTimeEnd, + r.NurseResponseDeadlineAt, r.PaymentDeadlineAt, r.CustomerNotes, + r.VariantLabel, AgeFrom(r.PatientBirthDate))) + .ToList(); + return new PagedResult(items, total, page, pageSize); } @@ -149,7 +163,13 @@ internal sealed class BookingRequestRepository : BaseAsyncRepository() + .Where(b => b.BookingRequestId == r.Id) + .Select(b => (long?)b.Id) + .FirstOrDefault())) .FirstOrDefaultAsync(cancellationToken); if (row is null) @@ -165,7 +185,8 @@ internal sealed class BookingRequestRepository : BaseAsyncRepository GetTrackedByIdAsync(long id, CancellationToken cancellationToken) @@ -234,6 +255,24 @@ internal sealed class BookingRequestRepository : BaseAsyncRepository GetCheckoutContextAsync(long id, long customerId, CancellationToken cancellationToken) + => TableNoTracking + .Where(r => r.Id == id && r.CustomerId == customerId) + .Select(r => new CheckoutContext( + r.Id, + r.Status, + ((r.Nurse.User.Name ?? "") + " " + (r.Nurse.User.FamilyName ?? "")).Trim(), + r.Patient.DisplayName, + r.Variant.DisplayName, + r.Variant.PriceUnit, + r.Variant.SessionCount, + r.Variant.Price, + r.RequestedDate, + r.RequestedTimeStart, + r.RequestedTimeEnd, + r.PaymentDeadlineAt)) + .FirstOrDefaultAsync(cancellationToken); + // Actionable (awaiting a party's action) rows float above terminal ones, then most-recent first. Recency // (id) rather than the deadline is the secondary key: for a given status the deadline tracks creation // time anyway, and DateTimeOffset is not sortable on the SQLite test provider. @@ -246,10 +285,31 @@ internal sealed class BookingRequestRepository : BaseAsyncRepository string.Join(' ', new[] { name, familyName }.Where(s => !string.IsNullOrWhiteSpace(s))).Trim(); + // Coarse display age; null when the birth date is unset (default). Uses the UTC calendar date — a + // one-day boundary drift is immaterial for a triage age band. + private static int? AgeFrom(DateOnly birthDate) + { + if (birthDate == default) + return null; + + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var age = today.Year - birthDate.Year; + if (birthDate > today.AddYears(-age)) + age--; + return age < 0 ? null : age; + } + private sealed record CustomerRow( long Id, string Status, string? NurseName, string? NurseFamily, decimal NurseRating, string? RequiredCaregiverGender, DateOnly RequestedDate, TimeOnly RequestedTimeStart, TimeOnly RequestedTimeEnd, - DateTime NurseResponseDeadlineAt, DateTime? PaymentDeadlineAt); + DateTime NurseResponseDeadlineAt, DateTime? PaymentDeadlineAt, + string VariantLabel, DateOnly PatientBirthDate); + + private sealed record NurseRow( + long Id, string Status, string PatientName, string? RequiredCaregiverGender, + DateOnly RequestedDate, TimeOnly RequestedTimeStart, TimeOnly RequestedTimeEnd, + DateTime NurseResponseDeadlineAt, DateTime? PaymentDeadlineAt, string? CustomerNotes, + string VariantLabel, DateOnly PatientBirthDate); private sealed record DetailRow( long Id, string Status, long CustomerId, long NurseId, @@ -261,5 +321,6 @@ internal sealed class BookingRequestRepository : BaseAsyncRepository new CustomerAddressDto( a.Id, a.Title, + a.City.ProvinceId, a.CityId, a.City.NameFa, a.City.NameEn, diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/CustomerProfileRepository.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/CustomerProfileRepository.cs index 552f7a6..37ac9e8 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/CustomerProfileRepository.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/CustomerProfileRepository.cs @@ -21,7 +21,8 @@ internal sealed class CustomerProfileRepository : BaseAsyncRepository GetMineAsync(int userId, CancellationToken cancellationToken) => TableNoTracking .Where(c => c.UserId == userId) - .Select(c => new CustomerProfileDto(c.Id, c.DefaultEmergencyContactName, c.DefaultEmergencyContactPhone)) + .Select(c => new CustomerProfileDto( + c.Id, c.DefaultEmergencyContactName, c.DefaultEmergencyContactPhone, c.AvatarUrl, c.PreferredLanguage)) .FirstOrDefaultAsync(cancellationToken); public Task GetProfileIdByUserIdAsync(int userId, CancellationToken cancellationToken) diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/InvoiceRepository.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/InvoiceRepository.cs index d7bf364..c4ef500 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/InvoiceRepository.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/InvoiceRepository.cs @@ -47,6 +47,7 @@ internal sealed class InvoiceRepository : BaseAsyncRepository, IInvoice row.Invoice.Id, row.Invoice.BookingId, row.Invoice.InvoiceNumber, row.Invoice.IssuingEntityType, row.Invoice.GrossIrr.ToString(), row.Invoice.PlatformCommissionIrr.ToString(), row.Invoice.BnplCommissionIrr?.ToString(), row.Invoice.VatRate, row.Invoice.VatIrr.ToString(), + (row.Invoice.PlatformCommissionIrr + (row.Invoice.BnplCommissionIrr ?? 0) + row.Invoice.VatIrr).ToString(), row.Invoice.MoadianReferenceNumber, row.Invoice.MoadianStatus, PdfUrl: null, row.Invoice.IssuedAt); return new InvoiceProjection(row.CustomerUserId, row.Invoice.PdfStorageKey, dto); diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/NurseProfileRepository.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/NurseProfileRepository.cs index 5bd3a7b..c5d27ee 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/NurseProfileRepository.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/NurseProfileRepository.cs @@ -1,6 +1,12 @@ +using System.Globalization; +using Baya.Application.Common; using Baya.Application.Contracts.Persistence; using Baya.Application.Models.Identity; +using Baya.Application.Models.Nurses; +using Baya.Domain.Entities.Catalog; using Baya.Domain.Entities.Identity; +using Baya.Domain.Entities.Reviews; +using Baya.Domain.Entities.Verification; using Baya.Infrastructure.Persistence.Repositories.Common; using Microsoft.EntityFrameworkCore; @@ -35,7 +41,8 @@ internal sealed class NurseProfileRepository : BaseAsyncRepository p.IsAcceptingBookings, p.AverageRating, p.TotalReviews, - p.TotalCompletedBookings)) + p.TotalCompletedBookings, + p.AvatarUrl)) .FirstOrDefaultAsync(cancellationToken); public Task GetProfileIdByUserIdAsync(int userId, CancellationToken cancellationToken) @@ -55,4 +62,61 @@ internal sealed class NurseProfileRepository : BaseAsyncRepository .Where(p => p.Id == nurseProfileId) .Select(p => new NurseBookingContext(p.UserId, p.User.Gender, p.IsVerified, p.IsAcceptingBookings)) .FirstOrDefaultAsync(cancellationToken); + + public async Task GetPublicProfileAsync(long nurseProfileId, CancellationToken cancellationToken) + { + var core = await TableNoTracking + .Where(p => p.Id == nurseProfileId) + .Select(p => new + { + p.Id, + Name = ((p.User.Name ?? "") + " " + (p.User.FamilyName ?? "")).Trim(), + p.AvatarUrl, + p.Bio, + p.YearsOfExperience, + p.AverageRating, + p.TotalReviews, + p.TotalCompletedBookings, + p.IsVerified, + p.SpecializationsJson + }) + .FirstOrDefaultAsync(cancellationToken); + + if (core is null) + return null; + + var services = await DbContext.Set() + .AsNoTracking() + .Where(v => v.NurseId == nurseProfileId && v.IsActive) + .OrderBy(v => v.Id) + .Select(v => new { v.Id, v.DisplayName, v.Price, v.PriceUnit, v.SessionCount }) + .ToListAsync(cancellationToken); + + var inoMembership = await DbContext.Set() + .AsNoTracking() + .AnyAsync(c => c.NurseId == nurseProfileId && c.CredentialType == CredentialTypes.InoMembership, cancellationToken); + + var latest = await DbContext.Set() + .AsNoTracking() + .Where(r => r.NurseProfileId == nurseProfileId && r.ModerationStatus == ReviewModerationStatus.Published) + .OrderByDescending(r => r.Id) + .Select(r => new { r.Rating, r.Body, r.CreatedAt }) + .FirstOrDefaultAsync(cancellationToken); + + return new NursePublicProfileDto( + core.Id, + core.Name, + core.AvatarUrl, + core.Bio, + core.YearsOfExperience, + core.AverageRating, + core.TotalReviews, + core.TotalCompletedBookings, + core.IsVerified, + inoMembership, + JsonCodeList.Parse(core.SpecializationsJson), + services.Select(s => new NursePublicServiceDto( + s.Id, s.DisplayName, s.Price.ToString(CultureInfo.InvariantCulture), s.PriceUnit, s.SessionCount)).ToList(), + latest is null ? null : new NursePublicLatestReviewDto(latest.Rating, latest.Body, null, latest.CreatedAt)); + } } diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/PatientCareRecordRepository.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/PatientCareRecordRepository.cs index f855560..2d8ad5e 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/PatientCareRecordRepository.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/PatientCareRecordRepository.cs @@ -51,7 +51,7 @@ internal sealed class PatientCareRecordRepository : BaseAsyncRepository new { - r.Id, r.PatientId, r.BookingId, r.NurseProfileId, r.BodyEncrypted, r.RecordedAt, + r.Id, r.PatientId, r.BookingId, r.NurseProfileId, r.BodyEncrypted, r.TaskResultsJson, r.RecordedAt, NurseName = DbContext.Set() .Where(n => n.Id == r.NurseProfileId) .Select(n => n.User.Name + " " + n.User.FamilyName) @@ -63,9 +63,15 @@ internal sealed class PatientCareRecordRepository : BaseAsyncRepository new CareRecordCipherRow( r.Id, r.PatientId, r.BookingId, r.NurseProfileId, string.IsNullOrWhiteSpace(r.NurseName) ? null : r.NurseName.Trim(), - r.BodyEncrypted, r.RecordedAt)) + r.BodyEncrypted, r.TaskResultsJson, r.RecordedAt)) .ToList(); return new PagedResult(items, total, page, pageSize); } + + public Task GetCarePlanAsync(long patientId, CancellationToken cancellationToken) + => DbContext.Set().FirstOrDefaultAsync(p => p.PatientId == patientId, cancellationToken); + + public async Task AddCarePlanAsync(PatientCarePlan plan, CancellationToken cancellationToken) + => await DbContext.Set().AddAsync(plan, cancellationToken); } diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/PatientRepository.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/PatientRepository.cs index 47dc851..e6435fb 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/PatientRepository.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/PatientRepository.cs @@ -1,3 +1,4 @@ +using Baya.Application.Common; using Baya.Application.Contracts.Persistence; using Baya.Application.Models.Common; using Baya.Application.Models.Identity; @@ -24,37 +25,38 @@ internal sealed class PatientRepository : BaseAsyncRepository, IPatient var query = TableNoTracking.Where(p => p.CustomerId == customerId); var total = await query.CountAsync(cancellationToken); - var items = await query + var rows = await query .OrderByDescending(p => p.Id) .Skip((page - 1) * pageSize) .Take(pageSize) - .Select(p => new PatientDto( - p.Id, - p.DisplayName, - p.FirstName, - p.LastName, - p.BirthDate, - p.Gender, - p.BloodType, - p.InitialMedicalNotes, - p.IsActive)) + .Select(p => new PatientRow( + p.Id, p.DisplayName, p.FirstName, p.LastName, p.BirthDate, + p.Gender, p.BloodType, p.InitialMedicalNotes, p.IsActive, p.Relation, p.ConditionsJson)) .ToListAsync(cancellationToken); + var items = rows.Select(ToDto).ToList(); return new PagedResult(items, total, page, pageSize); } - public Task GetOwnedProjectedAsync(long id, long customerId, CancellationToken cancellationToken) - => TableNoTracking + public async Task GetOwnedProjectedAsync(long id, long customerId, CancellationToken cancellationToken) + { + var row = await TableNoTracking .Where(p => p.Id == id && p.CustomerId == customerId) - .Select(p => new PatientDto( - p.Id, - p.DisplayName, - p.FirstName, - p.LastName, - p.BirthDate, - p.Gender, - p.BloodType, - p.InitialMedicalNotes, - p.IsActive)) + .Select(p => new PatientRow( + p.Id, p.DisplayName, p.FirstName, p.LastName, p.BirthDate, + p.Gender, p.BloodType, p.InitialMedicalNotes, p.IsActive, p.Relation, p.ConditionsJson)) .FirstOrDefaultAsync(cancellationToken); + + return row is null ? null : ToDto(row); + } + + private static PatientDto ToDto(PatientRow r) => new( + r.Id, r.DisplayName, r.FirstName, r.LastName, r.BirthDate, + r.Gender, r.BloodType, r.InitialMedicalNotes, r.IsActive, + r.Relation, JsonCodeList.Parse(r.ConditionsJson)); + + private sealed record PatientRow( + long Id, string DisplayName, string FirstName, string LastName, DateOnly BirthDate, + string Gender, string BloodType, string InitialMedicalNotes, bool IsActive, + string Relation, string ConditionsJson); } diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/PayoutRepository.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/PayoutRepository.cs index 972a388..1032999 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/PayoutRepository.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/PayoutRepository.cs @@ -214,7 +214,7 @@ internal sealed class PayoutRepository : BaseAsyncRepository, select new { p.Id, p.BatchId, p.Status, p.GrossEarningsIrr, p.ClawbackAppliedIrr, p.NetAmountIrr, - p.IbanSnapshot, p.TransferReference, p.PaidAt, b.PeriodStart, b.PeriodEnd + p.IbanSnapshot, p.TransferReference, p.PaidAt, p.FailureReason, b.PeriodStart, b.PeriodEnd }; var total = await query.CountAsync(cancellationToken); @@ -223,12 +223,102 @@ internal sealed class PayoutRepository : BaseAsyncRepository, var items = rows.Select(p => new NursePayoutHistoryDto( p.Id, p.BatchId, p.Status, p.GrossEarningsIrr.ToString(), p.ClawbackAppliedIrr.ToString(), p.NetAmountIrr.ToString(), MaskIban(p.IbanSnapshot), p.TransferReference, p.PaidAt, - p.PeriodStart, p.PeriodEnd)) + p.PeriodStart, p.PeriodEnd, p.FailureReason)) .ToList(); return new PagedResult(items, total, page, pageSize); } + public async Task> GetNurseEarningsAsync(long nurseId, DateTime now, CancellationToken cancellationToken) + { + // The nurse's terminal (completed/closed) bookings are the earnings-bearing set + the payout link (if any). + var rows = await (from b in DbContext.Set().AsNoTracking() + where b.NurseId == nurseId + && (b.Status == BookingStatus.Completed || b.Status == BookingStatus.Closed) + join p in DbContext.Set() on b.PatientId equals p.Id + select new + { + b.Id, + PatientName = p.DisplayName, + b.ScheduledDate, + b.GrossPriceIrr, + b.BalinyaarCommissionIrr, + b.NursePayoutAmount, + b.DisputeWindowEndsAt, + PayoutId = DbContext.Set() + .Where(l => l.BookingId == b.Id).Select(l => (long?)l.PayoutId).FirstOrDefault(), + ClawbackIrr = DbContext.Set() + .Where(c => c.BookingId == b.Id && c.NurseId == nurseId).Select(c => (long?)c.AmountIrr).FirstOrDefault() + }) + .ToListAsync(cancellationToken); + + var payoutIds = rows.Where(r => r.PayoutId is not null).Select(r => r.PayoutId!.Value).Distinct().ToList(); + var payouts = payoutIds.Count == 0 + ? new Dictionary() + : await DbContext.Set().AsNoTracking() + .Where(p => payoutIds.Contains(p.Id)) + .Select(p => new { p.Id, p.BatchId, p.Status, p.TransferReference, p.PaidAt, p.NetAmountIrr }) + .ToDictionaryAsync(p => p.Id, p => new PayoutFacts(p.BatchId, p.Status, p.TransferReference, p.PaidAt), cancellationToken); + + return rows.Select(r => + { + PayoutFacts? payout = r.PayoutId is { } pid && payouts.TryGetValue(pid, out var f) ? f : null; + var state = DeriveEarningsState(r.ClawbackIrr, payout, r.DisputeWindowEndsAt, now); + return new NurseEarningsItemDto( + r.Id, r.PatientName, r.ScheduledDate, + r.GrossPriceIrr.ToString(), r.BalinyaarCommissionIrr.ToString(), r.NursePayoutAmount.ToString(), + state, r.DisputeWindowEndsAt, null, payout?.PaidAt, payout?.TransferReference, + r.PayoutId, payout?.BatchId, r.ClawbackIrr?.ToString(), null); + }).ToList(); + } + + private static string DeriveEarningsState(long? clawbackIrr, PayoutFacts? payout, DateTime? disputeWindowEndsAt, DateTime now) + { + if (clawbackIrr is not null) + return NurseEarningsState.ClawbackApplied; + if (payout is not null) + return NurseEarningsState.Paid; + if (disputeWindowEndsAt is { } ends && ends < now) + return NurseEarningsState.Eligible; + return NurseEarningsState.Pending; + } + + public async Task GetPaidNetTotalAsync(long nurseId, CancellationToken cancellationToken) + => await DbContext.Set().AsNoTracking() + .Where(p => p.NurseId == nurseId && p.Status == PayoutStatus.Paid) + .SumAsync(p => (long?)p.NetAmountIrr, cancellationToken) ?? 0; + + public async Task GetNursePayoutDetailAsync(long payoutId, long nurseId, CancellationToken cancellationToken) + { + var p = await DbContext.Set().AsNoTracking() + .Where(x => x.Id == payoutId && x.NurseId == nurseId) + .Select(x => new + { + x.Id, x.BatchId, x.Status, x.GrossEarningsIrr, x.ClawbackAppliedIrr, x.NetAmountIrr, x.Amount, + x.IbanSnapshot, x.TransferReference, x.PaidAt, x.FailureReason, + Links = x.BookingLinks.Select(l => new { l.BookingId, l.SessionId, l.PayoutAmountIrr }).ToList() + }) + .FirstOrDefaultAsync(cancellationToken); + + if (p is null) + return null; + + var batch = await DbContext.Set().AsNoTracking() + .Where(b => b.Id == p.BatchId) + .Select(b => new PayoutBatchDto( + b.Id, b.PeriodStart, b.PeriodEnd, b.ProcessingDate, b.TotalAmount.ToString(), b.PayoutCount, + b.Status, b.InitiatedByAdminId, b.ProcessedAt, b.FailureNotes, b.CreatedAt)) + .FirstOrDefaultAsync(cancellationToken); + + return new NursePayoutDetailDto( + p.Id, p.BatchId, p.Status, p.GrossEarningsIrr.ToString(), p.ClawbackAppliedIrr.ToString(), + p.NetAmountIrr.ToString(), p.Amount.ToString(), MaskIban(p.IbanSnapshot), p.TransferReference, p.PaidAt, + p.FailureReason, batch!, + p.Links.Select(l => new PayoutBookingLinkDto(l.BookingId, l.SessionId, l.PayoutAmountIrr.ToString())).ToList()); + } + + private readonly record struct PayoutFacts(long BatchId, string Status, string? TransferReference, DateTime? PaidAt); + // Show only the last 4 digits of the IBAN — the plaintext snapshot never leaves the server. private static string MaskIban(string iban) { diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/RefundRepository.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/RefundRepository.cs index 425c490..1ffbc17 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/RefundRepository.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/RefundRepository.cs @@ -103,32 +103,48 @@ internal sealed class RefundRepository : BaseAsyncRepository, IRefundRep return new PagedResult(items, total, page, pageSize); } - public async Task GetStatusAsync(long id, CancellationToken cancellationToken) - { - var row = await (from r in TableNoTracking - where r.Id == id - join b in DbContext.Set() on r.BookingId equals b.Id - join c in DbContext.Set() on b.CustomerId equals c.Id - select new - { - c.UserId, - r.Id, - r.BookingId, - r.Status, - r.RefundChannel, - r.Amount, - r.ExpectedCustomerRefundEta, - r.GatewayRefundReference, - r.ExternalRevertReference - }) - .FirstOrDefaultAsync(cancellationToken); + public Task GetStatusAsync(long id, CancellationToken cancellationToken) + => StatusProjection(r => r.Id == id, cancellationToken); + public Task GetStatusByBookingAsync(long bookingId, CancellationToken cancellationToken) + // Newest refund for the booking (a re-refund is rare, but keep it deterministic). + => StatusProjection(r => r.BookingId == bookingId, cancellationToken, latestFirst: true); + + private async Task StatusProjection( + System.Linq.Expressions.Expression> predicate, CancellationToken cancellationToken, bool latestFirst = false) + { + var query = from r in TableNoTracking.Where(predicate) + join b in DbContext.Set() on r.BookingId equals b.Id + join c in DbContext.Set() on b.CustomerId equals c.Id + select new + { + c.UserId, + r.Id, + r.BookingId, + r.Status, + r.RefundChannel, + r.Amount, + r.ExpectedCustomerRefundEta, + r.GatewayRefundReference, + r.ExternalRevertReference, + r.PlatformFeeRefundedIrr, + r.NursePayoutRefundedIrr, + r.RefundPercentageApplied, + r.CancellationPolicyCode, + r.CreatedAt, + r.ProcessedAt + }; + + var row = await (latestFirst ? query.OrderByDescending(x => x.Id) : query).FirstOrDefaultAsync(cancellationToken); if (row is null) return null; var reference = Mask(row.GatewayRefundReference ?? row.ExternalRevertReference); - var dto = new RefundStatusDto(row.Id, row.BookingId, row.Status, row.RefundChannel, row.Amount.ToString(), - row.ExpectedCustomerRefundEta, reference); + var dto = new RefundStatusDto( + row.Id, row.BookingId, row.Status, row.RefundChannel, row.Amount.ToString(), + row.ExpectedCustomerRefundEta, reference, + row.PlatformFeeRefundedIrr.ToString(), row.NursePayoutRefundedIrr.ToString(), + row.RefundPercentageApplied, row.CancellationPolicyCode, row.CreatedAt, row.ProcessedAt); return new RefundStatusProjection(row.UserId, dto); } diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/ReviewRepository.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/ReviewRepository.cs index 27c3211..ac3190e 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/ReviewRepository.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/ReviewRepository.cs @@ -28,6 +28,29 @@ internal sealed class ReviewRepository : BaseAsyncRepository, IReviewRep public Task ExistsForBookingAsync(long bookingId, CancellationToken cancellationToken) => Entities.AnyAsync(r => r.BookingId == bookingId, cancellationToken); + public async Task GetMyReviewForBookingAsync(long bookingId, CancellationToken cancellationToken) + { + var row = await (from r in Entities.AsNoTracking().Where(r => r.BookingId == bookingId) + join b in DbContext.Set() on r.BookingId equals b.Id + join c in DbContext.Set() on b.CustomerId equals c.Id + select new + { + c.UserId, + r.ModerationStatus, + r.Rating, + r.Body, + TagCodes = r.TagLinks.Select(l => l.Tag.Code).ToList(), + r.CreatedAt + }) + .FirstOrDefaultAsync(cancellationToken); + + if (row is null) + return null; + + return new MyReviewProjection(row.UserId, + new MyReviewDto(row.ModerationStatus, row.Rating, row.Body, row.TagCodes, row.CreatedAt)); + } + public Task GetTrackedAsync(long reviewId, CancellationToken cancellationToken) => Entities.FirstOrDefaultAsync(r => r.Id == reviewId, cancellationToken); @@ -107,6 +130,7 @@ internal sealed class ReviewRepository : BaseAsyncRepository, IReviewRep .Where(a => a.ReviewId == r.Id && a.Type == SupportAlertType.LowRating) .Select(a => (long?)a.Id) .FirstOrDefault(), + r.TagLinks.Select(l => l.Tag.Code).ToList(), r.CreatedAt)) .ToListAsync(cancellationToken); diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/TicketRepository.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/TicketRepository.cs index e6e3ba1..e8726a5 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/TicketRepository.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/TicketRepository.cs @@ -17,6 +17,12 @@ internal sealed class TicketRepository : BaseAsyncRepository, ITicketRep public Task AddAsync(Ticket ticket, CancellationToken cancellationToken) => base.AddAsync(ticket); + public Task GetMessageByClientIdAsync(long ticketId, string clientMessageId, CancellationToken cancellationToken) + => DbContext.Set().AsNoTracking() + .Where(m => m.TicketId == ticketId && m.ClientMessageId == clientMessageId) + .Select(m => new TicketMessageDto(m.Id, m.SenderId, m.Body, m.IsInternal, m.SentAt)) + .FirstOrDefaultAsync(cancellationToken); + public async Task AddMessageAsync(TicketMessage message, CancellationToken cancellationToken) => await DbContext.Set().AddAsync(message, cancellationToken); @@ -103,7 +109,7 @@ internal sealed class TicketRepository : BaseAsyncRepository, ITicketRep } public async Task> ListMyTicketsAsync( - int userId, string? status, string? referenceCode, int page, int pageSize, CancellationToken cancellationToken) + int userId, string? status, string? referenceCode, long? bookingId, int page, int pageSize, CancellationToken cancellationToken) { var participantTickets = DbContext.Set().AsNoTracking() .Where(p => p.UserId == userId && p.RemovedAt == null) @@ -111,8 +117,10 @@ internal sealed class TicketRepository : BaseAsyncRepository, ITicketRep var query = Entities.AsNoTracking().Where(t => participantTickets.Contains(t.Id)); query = ApplyTicketFilters(query, status, referenceCode); + if (bookingId is { } b) + query = query.Where(t => t.BookingId == b); - return await PageAsync(query, page, pageSize, cancellationToken); + return await PageAsync(query, page, pageSize, userId, cancellationToken); } public async Task> ListForAdminAsync( @@ -129,7 +137,7 @@ internal sealed class TicketRepository : BaseAsyncRepository, ITicketRep if (refundId is { } r) query = query.Where(t => t.RefundId == r); - return await PageAsync(query, page, pageSize, cancellationToken); + return await PageAsync(query, page, pageSize, forUserId: null, cancellationToken); } private static IQueryable ApplyTicketFilters(IQueryable query, string? status, string? referenceCode) @@ -141,18 +149,47 @@ internal sealed class TicketRepository : BaseAsyncRepository, ITicketRep return query; } - private static async Task> PageAsync( - IQueryable query, int page, int pageSize, CancellationToken cancellationToken) + private async Task> PageAsync( + IQueryable query, int page, int pageSize, int? forUserId, CancellationToken cancellationToken) { var total = await query.CountAsync(cancellationToken); - var items = await query + var headers = await query .OrderByDescending(t => t.Id) .Skip((page - 1) * pageSize) .Take(pageSize) - .Select(t => new TicketSummaryDto( - t.Id, t.ReferenceCode, t.Subject, t.Status, t.Category, t.BookingId, t.RefundId, t.CreatedAt)) + .Select(t => new { t.Id, t.ReferenceCode, t.Subject, t.Status, t.Category, t.BookingId, t.RefundId, t.CreatedAt }) .ToListAsync(cancellationToken); + var ids = headers.Select(h => h.Id).ToList(); + + // Non-internal messages only feed activity + unread (an internal note never touches a user-facing count). + var msgs = await DbContext.Set().AsNoTracking() + .Where(m => ids.Contains(m.TicketId) && !m.IsInternal) + .Select(m => new { m.TicketId, m.SenderId, m.SentAt }) + .ToListAsync(cancellationToken); + + var lastRead = forUserId is { } uid + ? await DbContext.Set().AsNoTracking() + .Where(p => ids.Contains(p.TicketId) && p.UserId == uid) + .Select(p => new { p.TicketId, p.LastReadAt }) + .ToDictionaryAsync(x => x.TicketId, x => x.LastReadAt, cancellationToken) + : []; + + var byTicket = msgs.GroupBy(m => m.TicketId).ToDictionary(g => g.Key, g => g.ToList()); + + var items = headers.Select(h => + { + var list = byTicket.GetValueOrDefault(h.Id) ?? []; + DateTimeOffset? lastMessageAt = list.Count == 0 ? null : list.Max(m => m.SentAt); + var read = lastRead.GetValueOrDefault(h.Id); + var unread = forUserId is { } u + ? list.Count(m => m.SenderId != u && (read is null || m.SentAt > read)) + : 0; + return new TicketSummaryDto( + h.Id, h.ReferenceCode, h.Subject, h.Status, h.Category, h.BookingId, h.RefundId, h.CreatedAt, + lastMessageAt, unread); + }).ToList(); + return new PagedResult(items, total, page, pageSize); } } diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/UserAccountRepository.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/UserAccountRepository.cs index 4a3e51e..66730df 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/UserAccountRepository.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/UserAccountRepository.cs @@ -28,6 +28,9 @@ internal class UserAccountRepository : BaseAsyncRepository, IUserAccountRe .FirstOrDefaultAsync(cancellationToken); } + public Task GetTrackedByIdAsync(int userId, CancellationToken cancellationToken) + => Table.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); + public Task GetRoleByNameAsync(string roleName, CancellationToken cancellationToken) { return DbContext.Set() diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/VerificationRepository.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/VerificationRepository.cs index 48c0a41..aa98215 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/VerificationRepository.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/VerificationRepository.cs @@ -93,6 +93,10 @@ internal sealed class VerificationRepository : BaseAsyncRepository await DbContext.Set().AddAsync(credential, cancellationToken); + public Task GetTrackedCredentialAsync(long nurseId, string credentialType, CancellationToken cancellationToken) + => DbContext.Set() + .FirstOrDefaultAsync(c => c.NurseId == nurseId && c.CredentialType == credentialType, cancellationToken); + // --- Projected reads --- public async Task GetStatusForNurseAsync(long nurseId, CancellationToken cancellationToken) @@ -116,6 +120,7 @@ internal sealed class VerificationRepository : BaseAsyncRepository new VerificationStepDto(s.Id, s.Code, s.Display, s.Status.ToCode(), s.IsAutomated, s.ExpiresAt, s.FailureReason)) + .Select(s => new VerificationStepDto(s.Id, s.Code, s.Display, s.Status.ToCode(), s.IsAutomated, s.IsRequired, s.ExpiresAt, s.FailureReason)) .ToList(); var blocking = steps @@ -209,6 +214,7 @@ internal sealed class VerificationRepository : BaseAsyncRepository> GetTrailAsync( - string entityType, - string entityId, + string? entityType, + string? entityId, + int? actorUserId, + string? action, + System.DateTimeOffset? from, + System.DateTimeOffset? to, int page, int pageSize, CancellationToken cancellationToken = default) { - var query = db.Set() - .AsNoTracking() - .Where(a => a.EntityType == entityType && a.EntityId == entityId) - // Id is a monotonic identity → newest-first and deterministic (no timestamp ties). - .OrderByDescending(a => a.Id); + var filtered = db.Set().AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(entityType)) + filtered = filtered.Where(a => a.EntityType == entityType); + if (!string.IsNullOrWhiteSpace(entityId)) + filtered = filtered.Where(a => a.EntityId == entityId); + if (actorUserId is { } actor) + filtered = filtered.Where(a => a.ActorUserId == actor); + if (!string.IsNullOrWhiteSpace(action)) + filtered = filtered.Where(a => a.Action == action); + if (from is { } f) + filtered = filtered.Where(a => a.OccurredAt >= f); + if (to is { } t) + filtered = filtered.Where(a => a.OccurredAt <= t); + + // Id is a monotonic identity → newest-first and deterministic (no timestamp ties). + var query = filtered.OrderByDescending(a => a.Id); var total = await query.CountAsync(cancellationToken); var items = await query diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Services/Configuration/PlatformConfigService.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Services/Configuration/PlatformConfigService.cs index 6b2f9e3..7ff6dd5 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Services/Configuration/PlatformConfigService.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Services/Configuration/PlatformConfigService.cs @@ -29,7 +29,7 @@ internal sealed class PlatformConfigService(ApplicationDbContext db, ICacheServi async ct => await db.Set() .AsNoTracking() .Where(c => c.Key == key) - .Select(c => new PlatformConfigDto(c.Key, c.Value, c.DataType, c.Description)) + .Select(c => new PlatformConfigDto(c.Key, c.Value, c.DataType, c.Description, c.ModifiedAt ?? c.CreatedAt, c.ModifiedById ?? c.CreatedById)) .FirstOrDefaultAsync(ct), CacheTtl, cancellationToken); @@ -61,7 +61,7 @@ internal sealed class PlatformConfigService(ApplicationDbContext db, ICacheServi var items = await query .Skip((page - 1) * pageSize) .Take(pageSize) - .Select(c => new PlatformConfigDto(c.Key, c.Value, c.DataType, c.Description)) + .Select(c => new PlatformConfigDto(c.Key, c.Value, c.DataType, c.Description, c.ModifiedAt ?? c.CreatedAt, c.ModifiedById ?? c.CreatedById)) .ToListAsync(cancellationToken); return new PagedResult(items, total, page, pageSize); diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Services/Search/SearchIndexMaintainer.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Services/Search/SearchIndexMaintainer.cs index 935b201..a005c6c 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Services/Search/SearchIndexMaintainer.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Services/Search/SearchIndexMaintainer.cs @@ -54,13 +54,14 @@ internal sealed class SearchIndexMaintainer(ApplicationDbContext db, IDateTimePr public async Task ReindexNurseAsync(NurseProfile profile, VerificationStatus? verificationStatus, CancellationToken cancellationToken) { var status = verificationStatus ?? await LoadStatusAsync(profile.Id, cancellationToken); - var gender = await LoadGenderAsync(profile.Id, cancellationToken); + var identity = await LoadIdentityAsync(profile.Id, cancellationToken); - // Bookability + aggregates come from the tracked profile (its just-changed values); gender is stable - // for these triggers so it is read from the database. + // Bookability + aggregates come from the tracked profile (its just-changed values); gender + name are + // stable for these triggers so they are read from the database. The avatar is on the tracked profile. var ctx = new NurseContext( - profile.IsVerified, profile.IsAcceptingBookings, gender, - profile.AverageRating, profile.TotalReviews, profile.TotalCompletedBookings); + profile.IsVerified, profile.IsAcceptingBookings, identity.Gender, + profile.AverageRating, profile.TotalReviews, profile.TotalCompletedBookings, + identity.Name, profile.AvatarUrl); var bookable = NurseBookable(profile.IsVerified, profile.IsAcceptingBookings, status); var areas = await LoadActiveAreasAsync(profile.Id, cancellationToken); @@ -197,6 +198,8 @@ internal sealed class SearchIndexMaintainer(ApplicationDbContext db, IDateTimePr existing.Price = variant.Price; existing.PriceUnit = variant.PriceUnit; existing.NurseGender = ctx.Gender; + existing.NurseName = ctx.NurseName; + existing.AvatarUrl = ctx.AvatarUrl; existing.AverageRating = ctx.AverageRating; existing.TotalReviews = ctx.TotalReviews; existing.TotalCompletedBookings = ctx.TotalCompletedBookings; @@ -218,6 +221,8 @@ internal sealed class SearchIndexMaintainer(ApplicationDbContext db, IDateTimePr CityId = area.CityId, DistrictId = area.DistrictId, NurseGender = ctx.Gender, + NurseName = ctx.NurseName, + AvatarUrl = ctx.AvatarUrl, AverageRating = ctx.AverageRating, TotalReviews = ctx.TotalReviews, TotalCompletedBookings = ctx.TotalCompletedBookings, @@ -248,14 +253,21 @@ internal sealed class SearchIndexMaintainer(ApplicationDbContext db, IDateTimePr .Where(p => p.Id == nurseId) .Select(p => new NurseContext( p.IsVerified, p.IsAcceptingBookings, p.User.Gender, - p.AverageRating, p.TotalReviews, p.TotalCompletedBookings)) + p.AverageRating, p.TotalReviews, p.TotalCompletedBookings, + ((p.User.Name ?? "") + " " + (p.User.FamilyName ?? "")).Trim(), p.AvatarUrl)) .FirstOrDefaultAsync(cancellationToken); - private async Task LoadGenderAsync(long nurseId, CancellationToken cancellationToken) - => await db.Set() + private async Task LoadIdentityAsync(long nurseId, CancellationToken cancellationToken) + { + var identity = await db.Set() .Where(p => p.Id == nurseId) - .Select(p => p.User.Gender) - .FirstOrDefaultAsync(cancellationToken) ?? string.Empty; + .Select(p => new NurseIdentity( + p.User.Gender ?? string.Empty, + ((p.User.Name ?? "") + " " + (p.User.FamilyName ?? "")).Trim())) + .FirstOrDefaultAsync(cancellationToken); + // NurseIdentity is a struct — FirstOrDefaultAsync yields default (null fields) when no row exists. + return new NurseIdentity(identity.Gender ?? string.Empty, identity.Name ?? string.Empty); + } private Task LoadStatusAsync(long nurseId, CancellationToken cancellationToken) => db.Set() @@ -281,5 +293,8 @@ internal sealed class SearchIndexMaintainer(ApplicationDbContext db, IDateTimePr private sealed record NurseContext( bool IsVerified, bool IsAcceptingBookings, string Gender, - decimal AverageRating, int TotalReviews, int TotalCompletedBookings); + decimal AverageRating, int TotalReviews, int TotalCompletedBookings, + string NurseName, string AvatarUrl); + + private readonly record struct NurseIdentity(string Gender, string Name); } diff --git a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Services/Search/SqlNurseSearch.cs b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Services/Search/SqlNurseSearch.cs index e9ef692..d5e10d6 100644 --- a/server/src/Infrastructure/Baya.Infrastructure.Persistence/Services/Search/SqlNurseSearch.cs +++ b/server/src/Infrastructure/Baya.Infrastructure.Persistence/Services/Search/SqlNurseSearch.cs @@ -54,19 +54,23 @@ internal sealed class SqlNurseSearch(ApplicationDbContext db) : INurseSearch .Take(criteria.PageSize) .Select(r => new Row( r.VariantId, r.NurseId, r.ServiceCategoryId, r.Price, r.PriceUnit, r.NurseGender, - r.AverageRating, r.TotalReviews, r.TotalCompletedBookings, r.CityId, r.DistrictId)) + r.AverageRating, r.TotalReviews, r.TotalCompletedBookings, r.CityId, r.DistrictId, + r.NurseName, r.AvatarUrl)) .ToListAsync(cancellationToken); // Format price to a digit string in memory (no long.ToString translation required in SQL). + // DistanceKm is null: the covering index carries no coordinate, so distance is not derivable here. var items = rows.Select(r => new NurseSearchResultDto( r.VariantId, r.NurseId, r.ServiceCategoryId, r.Price.ToString(CultureInfo.InvariantCulture), r.PriceUnit, r.NurseGender, - r.AverageRating, r.TotalReviews, r.TotalCompletedBookings, r.CityId, r.DistrictId)).ToList(); + r.AverageRating, r.TotalReviews, r.TotalCompletedBookings, r.CityId, r.DistrictId, + r.NurseName, r.AvatarUrl, null)).ToList(); return new PagedResult(items, total, criteria.Page, criteria.PageSize); } private sealed record Row( long VariantId, long NurseId, long ServiceCategoryId, long Price, string PriceUnit, string NurseGender, - decimal AverageRating, int TotalReviews, int TotalCompletedBookings, long CityId, long? DistrictId); + decimal AverageRating, int TotalReviews, int TotalCompletedBookings, long CityId, long? DistrictId, + string NurseName, string AvatarUrl); } diff --git a/server/src/Tests/Baya.Test.Foundation/Booking/CreateBookingRequestHandlerTests.cs b/server/src/Tests/Baya.Test.Foundation/Booking/CreateBookingRequestHandlerTests.cs index f8af65b..6624c32 100644 --- a/server/src/Tests/Baya.Test.Foundation/Booking/CreateBookingRequestHandlerTests.cs +++ b/server/src/Tests/Baya.Test.Foundation/Booking/CreateBookingRequestHandlerTests.cs @@ -188,5 +188,6 @@ public class CreateBookingRequestHandlerTests AddressId, "خانه", 1, "تهران", "Tehran", null, null, null, "line", "postal", "recipient", "phone", "female", new DateOnly(2026, 8, 1), new TimeOnly(9, 0), new TimeOnly(13, 0), - "notes", Now.AddHours(24).UtcDateTime, null, null, Now); + "notes", Now.AddHours(24).UtcDateTime, null, null, Now, + 1_000_000, null, null); } diff --git a/server/src/Tests/Baya.Test.Foundation/Booking/GetCheckoutSummaryHandlerTests.cs b/server/src/Tests/Baya.Test.Foundation/Booking/GetCheckoutSummaryHandlerTests.cs new file mode 100644 index 0000000..6280aa6 --- /dev/null +++ b/server/src/Tests/Baya.Test.Foundation/Booking/GetCheckoutSummaryHandlerTests.cs @@ -0,0 +1,80 @@ +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Configuration; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Features.Booking.Queries.GetCheckoutSummary; +using Baya.Application.Models.Booking; +using Baya.Domain.Entities.Booking; +using Baya.Domain.Entities.User; +using NSubstitute; + +namespace Baya.Test.Foundation.Booking; + +public class GetCheckoutSummaryHandlerTests +{ + private readonly ICurrentUser _currentUser = Substitute.For(); + private readonly IUnitOfWork _unitOfWork = Substitute.For(); + private readonly IPlatformConfig _config = Substitute.For(); + private readonly ICustomerProfileRepository _customers = Substitute.For(); + private readonly IBookingRequestRepository _requests = Substitute.For(); + + private const int CustomerUserId = 7; + private const long CustomerId = 100; + private const long RequestId = 1; + + public GetCheckoutSummaryHandlerTests() + { + _unitOfWork.CustomerProfileRepository.Returns(_customers); + _unitOfWork.BookingRequestRepository.Returns(_requests); + _currentUser.UserId.Returns(CustomerUserId); + _currentUser.Roles.Returns([RoleNames.Customer]); + _customers.GetProfileIdByUserIdAsync(CustomerUserId, Arg.Any()).Returns(CustomerId); + _config.GetConfig("platform_fee_rate", Arg.Any()).Returns(0.15m); + _config.GetConfig("vat_rate", Arg.Any()).Returns(0.10m); + } + + [Fact] + public async Task CheckoutSummary_Reconciles_ServicePlusCommissionPlusVatEqualsTotal() + { + // gross = 1,000,000 × 2 = 2,000,000; commission = 15% = 300,000; payout = 1,700,000. + // VAT carved out of the commission: net = round(300,000 / 1.10) = 272,727; vat = 27,273. + _requests.GetCheckoutContextAsync(RequestId, CustomerId, Arg.Any()) + .Returns(Context(1_000_000, sessionCount: 2)); + var handler = new GetCheckoutSummaryQueryHandler(_currentUser, _unitOfWork, _config); + + var result = await handler.Handle(new GetCheckoutSummaryQuery(RequestId), CancellationToken.None); + + Assert.True(result.IsSuccess); + var dto = result.Result; + Assert.Equal("2000000", dto.TotalIrr); + Assert.Equal("2000000", dto.GrossPriceIrr); + Assert.Equal("300000", dto.BalinyaarCommissionIrr); + Assert.Equal("1700000", dto.NursePayoutAmount); + Assert.Equal("1700000", dto.ServiceCostIrr); + Assert.Equal("272727", dto.CommissionIrr); + Assert.Equal("27273", dto.VatIrr); + // The load-bearing invariant: service + commission(net) + vat == total, to the rial. + Assert.Equal( + long.Parse(dto.TotalIrr), + long.Parse(dto.ServiceCostIrr) + long.Parse(dto.CommissionIrr) + long.Parse(dto.VatIrr)); + } + + [Fact] + public async Task CheckoutSummary_ForeignRequest_IsNotFound() + { + _requests.GetCheckoutContextAsync(RequestId, CustomerId, Arg.Any()) + .Returns((CheckoutContext)null); + var handler = new GetCheckoutSummaryQueryHandler(_currentUser, _unitOfWork, _config); + + var result = await handler.Handle(new GetCheckoutSummaryQuery(RequestId), CancellationToken.None); + + Assert.False(result.IsSuccess); + Assert.True(result.IsNotFound); + } + + private static CheckoutContext Context(long price, int sessionCount) + => new( + RequestId, BookingRequestStatus.AcceptedAwaitingPayment, "پرستار", "پدر", + "خدمت", "per_session", sessionCount, price, + new DateOnly(2026, 8, 1), new TimeOnly(9, 0), new TimeOnly(13, 0), + new DateTime(2026, 7, 6, 10, 30, 0, DateTimeKind.Utc)); +} diff --git a/server/src/Tests/Baya.Test.Foundation/Booking/RespondBookingRequestHandlerTests.cs b/server/src/Tests/Baya.Test.Foundation/Booking/RespondBookingRequestHandlerTests.cs index 3afdc6c..3c5b4f4 100644 --- a/server/src/Tests/Baya.Test.Foundation/Booking/RespondBookingRequestHandlerTests.cs +++ b/server/src/Tests/Baya.Test.Foundation/Booking/RespondBookingRequestHandlerTests.cs @@ -195,5 +195,6 @@ public class RespondBookingRequestHandlerTests 6, "خانه", 1, "تهران", "Tehran", null, null, null, "line", "postal", "recipient", "phone", "any", new DateOnly(2026, 8, 1), new TimeOnly(9, 0), new TimeOnly(13, 0), - null, Now.AddHours(24).UtcDateTime, null, null, Now); + null, Now.AddHours(24).UtcDateTime, null, null, Now, + 1_000_000, null, null); } diff --git a/server/src/Tests/Baya.Test.Foundation/Identity/NurseProfileHandlersTests.cs b/server/src/Tests/Baya.Test.Foundation/Identity/NurseProfileHandlersTests.cs index 88267fa..5973aa2 100644 --- a/server/src/Tests/Baya.Test.Foundation/Identity/NurseProfileHandlersTests.cs +++ b/server/src/Tests/Baya.Test.Foundation/Identity/NurseProfileHandlersTests.cs @@ -30,7 +30,7 @@ public class NurseProfileHandlersTests { _repo.GetByUserIdAsync(7, Arg.Any()).ReturnsNull(); _repo.GetMineAsync(7, Arg.Any()) - .Returns(new NurseProfileDto(1, "bio", 3, "BSc", "Nursing", "[]", false, false, 0m, 0, 0)); + .Returns(new NurseProfileDto(1, "bio", 3, "BSc", "Nursing", "[]", false, false, 0m, 0, 0, null)); var handler = new UpsertNurseProfileCommandHandler(_currentUser, _unitOfWork); var result = await handler.Handle(new UpsertNurseProfileCommand("bio", 3, "BSc", "Nursing", "[]"), CancellationToken.None); diff --git a/server/src/Tests/Baya.Test.Foundation/Reviews/GetReviewEligibilityHandlerTests.cs b/server/src/Tests/Baya.Test.Foundation/Reviews/GetReviewEligibilityHandlerTests.cs new file mode 100644 index 0000000..188a9d6 --- /dev/null +++ b/server/src/Tests/Baya.Test.Foundation/Reviews/GetReviewEligibilityHandlerTests.cs @@ -0,0 +1,83 @@ +using Baya.Application.Contracts.Common; +using Baya.Application.Contracts.Persistence; +using Baya.Application.Features.Reviews.Queries.GetReviewEligibility; +using Baya.Application.Models.Reviews; +using Baya.Domain.Entities.Booking; +using NSubstitute; + +namespace Baya.Test.Foundation.Reviews; + +public class GetReviewEligibilityHandlerTests +{ + private readonly ICurrentUser _currentUser = Substitute.For(); + private readonly IUnitOfWork _unitOfWork = Substitute.For(); + private readonly ICustomerProfileRepository _customers = Substitute.For(); + private readonly IReviewRepository _reviews = Substitute.For(); + + private const int UserId = 7; + private const long CustomerId = 100; + private const long BookingId = 5; + + public GetReviewEligibilityHandlerTests() + { + _currentUser.UserId.Returns(UserId); + _unitOfWork.CustomerProfileRepository.Returns(_customers); + _unitOfWork.ReviewRepository.Returns(_reviews); + _customers.GetProfileIdByUserIdAsync(UserId, Arg.Any()).Returns(CustomerId); + } + + [Fact] + public async Task Eligibility_CompletedOwnedUnreviewed_CanReview() + { + _reviews.GetReviewableBookingAsync(BookingId, Arg.Any()) + .Returns(new ReviewableBooking(BookingId, CustomerId, 1, BookingStatus.Completed)); + _reviews.ExistsForBookingAsync(BookingId, Arg.Any()).Returns(false); + + var result = await Handle(); + + Assert.True(result.IsSuccess); + Assert.True(result.Result.CanReview); + Assert.Null(result.Result.Reason); + } + + [Fact] + public async Task Eligibility_NotCompleted_ReasonNotCompleted() + { + _reviews.GetReviewableBookingAsync(BookingId, Arg.Any()) + .Returns(new ReviewableBooking(BookingId, CustomerId, 1, BookingStatus.InProgress)); + + var result = await Handle(); + + Assert.False(result.Result.CanReview); + Assert.Equal(ReviewEligibilityDto.ReasonNotCompleted, result.Result.Reason); + } + + [Fact] + public async Task Eligibility_AlreadyReviewed_ReasonAlreadyReviewed() + { + _reviews.GetReviewableBookingAsync(BookingId, Arg.Any()) + .Returns(new ReviewableBooking(BookingId, CustomerId, 1, BookingStatus.Closed)); + _reviews.ExistsForBookingAsync(BookingId, Arg.Any()).Returns(true); + + var result = await Handle(); + + Assert.False(result.Result.CanReview); + Assert.Equal(ReviewEligibilityDto.ReasonAlreadyReviewed, result.Result.Reason); + } + + [Fact] + public async Task Eligibility_ForeignBooking_ReasonNotOwner() + { + _reviews.GetReviewableBookingAsync(BookingId, Arg.Any()) + .Returns(new ReviewableBooking(BookingId, CustomerProfileId: 999, 1, BookingStatus.Completed)); + + var result = await Handle(); + + Assert.False(result.Result.CanReview); + Assert.Equal(ReviewEligibilityDto.ReasonNotOwner, result.Result.Reason); + } + + private ValueTask> Handle() + => new GetReviewEligibilityQueryHandler(_currentUser, _unitOfWork) + .Handle(new GetReviewEligibilityQuery(BookingId), CancellationToken.None); +}