@valentine-efagene/qshelter-common 2.0.82 → 2.0.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -15,7 +15,7 @@ const config = {
|
|
|
15
15
|
"clientVersion": "7.2.0",
|
|
16
16
|
"engineVersion": "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3",
|
|
17
17
|
"activeProvider": "mysql",
|
|
18
|
-
"inlineSchema": "// =============================================================================\n// QSHELTER UNIFIED DATABASE SCHEMA\n// =============================================================================\n// This schema contains all database models for the QShelter platform\n// Organized by domain for better readability\n// =============================================================================\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated/client\"\n engineType = \"client\"\n}\n\ndatasource db {\n provider = \"mysql\"\n}\n\n// =============================================================================\n// ENUMS - Database-enforced value constraints\n// =============================================================================\n\nenum PhaseCategory {\n DOCUMENTATION\n PAYMENT\n}\n\nenum PhaseType {\n KYC\n VERIFICATION\n DOWNPAYMENT\n MORTGAGE\n BALLOON\n CUSTOM\n}\n\nenum PaymentFrequency {\n MONTHLY\n BIWEEKLY\n WEEKLY\n ONE_TIME\n CUSTOM\n}\n\nenum ContractStatus {\n DRAFT\n PENDING\n ACTIVE\n COMPLETED\n CANCELLED\n TERMINATED\n TRANSFERRED // Contract was transferred to a different property\n}\n\nenum TransferRequestStatus {\n PENDING\n APPROVED\n REJECTED\n IN_PROGRESS\n COMPLETED\n FAILED\n}\n\nenum PhaseStatus {\n PENDING\n IN_PROGRESS\n AWAITING_APPROVAL\n ACTIVE\n COMPLETED\n SKIPPED\n FAILED\n SUPERSEDED // Phase replaced by payment method change\n}\n\nenum StepType {\n UPLOAD\n REVIEW\n SIGNATURE\n APPROVAL\n EXTERNAL_CHECK\n WAIT\n GENERATE_DOCUMENT // Triggers document generation (offer letters, contracts, etc.)\n PRE_APPROVAL // Customer answers eligibility questionnaire\n UNDERWRITING // System evaluates DTI, score, eligibility\n}\n\nenum StepStatus {\n PENDING\n IN_PROGRESS\n COMPLETED\n FAILED\n SKIPPED\n NEEDS_RESUBMISSION // User must re-upload or correct something (after rejection)\n ACTION_REQUIRED // User action needed (generic - check actionReason)\n AWAITING_REVIEW // Submitted, waiting for admin/system review\n}\n\n/// When a step event attachment should trigger\nenum StepTrigger {\n ON_COMPLETE // When step is approved/completed\n ON_REJECT // When step is rejected\n ON_SUBMIT // When step is submitted for review\n ON_RESUBMIT // When step is resubmitted after rejection\n ON_START // When step transitions to IN_PROGRESS\n}\n\nenum InstallmentStatus {\n PENDING\n PAID\n OVERDUE\n PARTIALLY_PAID\n WAIVED\n}\n\nenum PaymentStatus {\n INITIATED\n PENDING\n COMPLETED\n FAILED\n REFUNDED\n}\n\nenum ApprovalDecision {\n APPROVED\n REJECTED\n REQUEST_CHANGES\n}\n\n// =============================================================================\n// CONTRACT TERMINATION / CANCELLATION ENUMS\n// =============================================================================\n\nenum TerminationType {\n BUYER_WITHDRAWAL // Buyer wants to cancel (voluntary)\n SELLER_WITHDRAWAL // Seller/developer cancels\n MUTUAL_AGREEMENT // Both parties agree to terminate\n PAYMENT_DEFAULT // Buyer failed payment obligations\n DOCUMENT_FAILURE // Buyer failed to provide required documents\n FRAUD // Fraudulent activity detected\n FORCE_MAJEURE // External circumstances (disaster, etc.)\n PROPERTY_UNAVAILABLE // Property no longer available\n REGULATORY // Regulatory/legal requirement\n OTHER // Other reasons (with notes)\n}\n\nenum TerminationStatus {\n REQUESTED // Initial request submitted\n PENDING_REVIEW // Awaiting admin review\n PENDING_REFUND // Approved, awaiting refund processing\n REFUND_IN_PROGRESS // Refund being processed\n REFUND_COMPLETED // Refund completed\n COMPLETED // Termination fully executed (no refund or refund done)\n REJECTED // Termination request rejected\n CANCELLED // Termination request was cancelled\n}\n\nenum TerminationInitiator {\n BUYER\n SELLER\n ADMIN\n SYSTEM\n}\n\nenum CompletionCriterion {\n DOCUMENT_APPROVALS\n PAYMENT_AMOUNT\n STEPS_COMPLETED\n}\n\nenum DocumentStatus {\n DRAFT\n PENDING\n PENDING_SIGNATURE\n SENT\n VIEWED\n SIGNED\n APPROVED\n REJECTED\n EXPIRED\n CANCELLED\n}\n\nenum OfferLetterType {\n PROVISIONAL\n FINAL\n}\n\nenum OfferLetterStatus {\n DRAFT\n GENERATED\n SENT\n VIEWED\n SIGNED\n EXPIRED\n CANCELLED\n}\n\nenum ContractEventType {\n CONTRACT_CREATED\n CONTRACT_STATE_CHANGED\n PHASE_ACTIVATED\n PHASE_COMPLETED\n STEP_COMPLETED\n STEP_REJECTED\n DOCUMENT_SUBMITTED\n DOCUMENT_APPROVED\n DOCUMENT_REJECTED\n PAYMENT_INITIATED\n PAYMENT_COMPLETED\n PAYMENT_FAILED\n INSTALLMENTS_GENERATED\n CONTRACT_SIGNED\n CONTRACT_TERMINATED\n CONTRACT_TRANSFERRED\n UNDERWRITING_COMPLETED\n OFFER_LETTER_GENERATED\n}\n\nenum ContractEventGroup {\n STATE_CHANGE\n PAYMENT\n DOCUMENT\n NOTIFICATION\n WORKFLOW\n}\n\nenum EventActorType {\n USER\n SYSTEM\n WEBHOOK\n ADMIN\n}\n\nenum RefundStatus {\n PENDING\n APPROVED\n REJECTED\n PROCESSING\n COMPLETED\n FAILED\n CANCELLED\n}\n\n// =============================================================================\n// EVENT-DRIVEN WORKFLOW ENUMS\n// =============================================================================\n\n/// Handler Type - What kind of action the handler performs\n/// These are business-friendly names that admins can understand\nenum EventHandlerType {\n SEND_EMAIL // Send an email notification to recipient(s)\n SEND_SMS // Send an SMS text message\n SEND_PUSH // Send a push notification\n CALL_WEBHOOK // Call an external API/webhook\n ADVANCE_WORKFLOW // Advance or complete a workflow step\n RUN_AUTOMATION // Execute internal business logic\n}\n\n/// Actor Type - Who triggered an event\nenum ActorType {\n USER\n API_KEY\n SYSTEM\n WEBHOOK\n}\n\n/// Workflow Event Status\nenum WorkflowEventStatus {\n PENDING\n PROCESSING\n COMPLETED\n FAILED\n SKIPPED\n}\n\n/// Handler Execution Status\nenum ExecutionStatus {\n PENDING\n RUNNING\n COMPLETED\n FAILED\n RETRYING\n SKIPPED\n}\n\n// =============================================================================\n// USER & AUTH DOMAIN\n// =============================================================================\n\nmodel User {\n id String @id @default(cuid())\n email String @unique\n password String?\n phone String? @unique\n firstName String?\n lastName String?\n isActive Boolean @default(true)\n isEmailVerified Boolean @default(false)\n googleId String?\n avatar String?\n tenantId String?\n tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: SetNull)\n // Support multiple roles via explicit join table `UserRole`\n userRoles UserRole[]\n walletId String? @unique\n wallet Wallet? @relation(fields: [walletId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n emailVerifiedAt DateTime?\n emailVerificationToken String?\n lastLoginAt DateTime?\n refreshTokens RefreshToken[]\n passwordResets PasswordReset[]\n suspensions UserSuspension[]\n emailPreferences EmailPreference[]\n deviceEndpoints DeviceEndpoint[]\n socials Social[]\n\n // Relations to other domains\n properties Property[]\n contracts Contract[] @relation(\"ContractBuyer\")\n soldContracts Contract[] @relation(\"ContractSeller\")\n contractPayments ContractPayment[] @relation(\"ContractPayer\")\n\n // Documentation step assignments and approvals\n assignedSteps DocumentationStep[] @relation(\"DocumentationStepAssignee\")\n stepApprovals DocumentationStepApproval[] @relation(\"DocumentationStepApprover\")\n uploadedDocs ContractDocument[] @relation(\"DocumentUploader\")\n\n // Payment method changes\n paymentMethodChangeRequests PaymentMethodChangeRequest[] @relation(\"ChangeRequestor\")\n reviewedChangeRequests PaymentMethodChangeRequest[] @relation(\"ChangeReviewer\")\n\n // Contract terminations\n initiatedTerminations ContractTermination[] @relation(\"TerminationInitiator\")\n reviewedTerminations ContractTermination[] @relation(\"TerminationReviewer\")\n\n // Offer letters\n offerLettersGenerated OfferLetter[] @relation(\"OfferLetterGenerator\")\n offerLettersSent OfferLetter[] @relation(\"OfferLetterSender\")\n\n // Property transfer requests\n transferRequestsSubmitted PropertyTransferRequest[] @relation(\"TransferRequestor\")\n transferRequestsReviewed PropertyTransferRequest[] @relation(\"TransferReviewer\")\n\n // Unified approval requests\n approvalRequestsSubmitted ApprovalRequest[] @relation(\"ApprovalRequestor\")\n approvalRequestsAssigned ApprovalRequest[] @relation(\"ApprovalAssignee\")\n approvalRequestsReviewed ApprovalRequest[] @relation(\"ApprovalReviewer\")\n\n // Contract refunds\n requestedRefunds ContractRefund[] @relation(\"RefundRequester\")\n approvedRefunds ContractRefund[] @relation(\"RefundApprover\")\n processedRefunds ContractRefund[] @relation(\"RefundProcessor\")\n\n @@index([email])\n @@index([tenantId])\n @@map(\"users\")\n}\n\nmodel Role {\n id String @id @default(cuid())\n name String @unique\n description String?\n userRoles UserRole[]\n permissions RolePermission[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@map(\"roles\")\n}\n\nmodel Permission {\n id String @id @default(cuid())\n name String @unique\n description String?\n resource String\n action String\n roles RolePermission[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([resource, action])\n @@index([resource])\n @@map(\"permissions\")\n}\n\nmodel RolePermission {\n roleId String\n permissionId String\n role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)\n permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n\n @@id([roleId, permissionId])\n @@map(\"role_permissions\")\n}\n\nmodel UserRole {\n userId String\n roleId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n\n @@id([userId, roleId])\n @@map(\"user_roles\")\n}\n\nmodel Tenant {\n id String @id @default(cuid())\n name String\n subdomain String @unique\n isActive Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Back-relations for multitenancy\n users User[]\n properties Property[]\n paymentPlans PaymentPlan[]\n paymentMethods PropertyPaymentMethod[]\n contracts Contract[]\n\n // Payment method changes\n paymentMethodChangeRequests PaymentMethodChangeRequest[]\n documentRequirementRules DocumentRequirementRule[]\n\n // Contract terminations\n contractTerminations ContractTermination[]\n\n // Offer letters and templates\n documentTemplates DocumentTemplate[]\n offerLetters OfferLetter[]\n\n // API keys for third-party integrations\n apiKeys ApiKey[]\n\n // Event-driven workflow\n eventChannels EventChannel[]\n eventTypes EventType[]\n eventHandlers EventHandler[]\n workflowEvents WorkflowEvent[]\n\n // Property transfer requests\n propertyTransferRequests PropertyTransferRequest[]\n\n // Unified approval requests\n approvalRequests ApprovalRequest[]\n\n // Contract refunds\n contractRefunds ContractRefund[]\n\n @@index([subdomain])\n @@map(\"tenants\")\n}\n\n// =============================================================================\n// API KEYS - Third-party integration credentials\n// =============================================================================\n// ApiKey enables partners/integrations to authenticate via token exchange.\n// \n// Flow:\n// 1. Admin creates API key for a partner (POST /api-keys)\n// 2. System generates secret, stores in Secrets Manager, returns id.secret ONCE\n// 3. Partner calls token endpoint with id.secret (POST /api-keys/:id/token)\n// 4. Token endpoint validates via Secrets Manager, returns short-lived JWT\n// 5. Partner uses JWT for API requests; authorizer validates + resolves scopes\n//\n// Security:\n// - Raw secret stored ONLY in AWS Secrets Manager (secretRef = ARN)\n// - Secret returned only once at creation; admin must rotate if lost\n// - Scopes define allowed operations (e.g., [\"contract:read\", \"payment:read\"])\n// - Short-lived JWTs (5-15 min) minimize exposure on key compromise\n// =============================================================================\n\nmodel ApiKey {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n // Identification\n name String // Human-readable name (e.g., \"Paystack Integration\")\n description String? @db.Text // Optional description\n provider String // Partner/vendor name (e.g., \"paystack\", \"flutterwave\")\n\n // Secret management (NEVER store raw secret in DB)\n secretRef String // AWS Secrets Manager ARN or name\n\n // Permissions - scopes this API key is allowed to request\n // Examples: [\"contract:read\", \"payment:*\", \"property:read\"]\n scopes Json // JSON array of scope strings\n\n // Lifecycle\n enabled Boolean @default(true)\n expiresAt DateTime? // Optional expiration date\n lastUsedAt DateTime? // Updated on each token exchange\n revokedAt DateTime? // Set when key is revoked\n revokedBy String? // User ID who revoked\n\n // Audit\n createdBy String? // User ID who created\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([tenantId])\n @@index([provider])\n @@index([enabled])\n @@map(\"api_keys\")\n}\n\nmodel RefreshToken {\n id String @id @default(cuid())\n // Use the JWT `jti` for indexed lookups and keep the raw JWT (optional)\n jti String? @unique @db.VarChar(255)\n token String? @db.LongText\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n expiresAt DateTime\n createdAt DateTime @default(now())\n\n @@index([userId])\n @@index([expiresAt])\n @@map(\"refresh_tokens\")\n}\n\nmodel PasswordReset {\n id String @id @default(cuid())\n token String @unique\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n expiresAt DateTime\n usedAt DateTime?\n createdAt DateTime @default(now())\n\n @@index([userId])\n @@index([expiresAt])\n @@map(\"password_resets\")\n}\n\nmodel UserSuspension {\n id String @id @default(cuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n reason String\n suspendedAt DateTime @default(now())\n expiresAt DateTime?\n liftedAt DateTime?\n\n @@index([userId])\n @@map(\"user_suspensions\")\n}\n\nmodel EmailPreference {\n id String @id @default(cuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n marketingEmails Boolean @default(true)\n transactionalEmails Boolean @default(true)\n propertyAlerts Boolean @default(true)\n paymentReminders Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@map(\"email_preferences\")\n}\n\nmodel DeviceEndpoint {\n id String @id @default(cuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n endpoint String // Push notification endpoint\n platform String // ios, android, web\n isActive Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@map(\"device_endpoints\")\n}\n\nmodel Social {\n id String @id @default(cuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n provider String // google, facebook, twitter, etc\n socialId String // ID from the social provider\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([provider, socialId])\n @@index([userId])\n @@map(\"socials\")\n}\n\nmodel OAuthState {\n id String @id @default(cuid())\n state String @unique\n expiresAt DateTime\n createdAt DateTime @default(now())\n\n @@index([state])\n @@index([expiresAt])\n @@map(\"oauth_states\")\n}\n\nmodel Wallet {\n id String @id @default(cuid())\n balance Float @default(0)\n currency String @default(\"USD\")\n user User?\n transactions Transaction[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@map(\"wallets\")\n}\n\nmodel Transaction {\n id String @id @default(cuid())\n walletId String\n wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)\n amount Float\n type String // CREDIT, DEBIT\n status String // PENDING, COMPLETED, FAILED\n reference String?\n description String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([walletId])\n @@map(\"transactions\")\n}\n\nmodel Settings {\n id String @id @default(cuid())\n key String @unique\n value String @db.Text\n category String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([category])\n @@map(\"settings\")\n}\n\n// =============================================================================\n// PROPERTY DOMAIN\n// =============================================================================\n// Property = listing/project (e.g., \"Sunrise Estate\")\n// PropertyVariant = configuration with specs & price (e.g., \"3-Bed Corner - Finished\")\n// PropertyUnit = individual sellable unit (e.g., \"Unit A1\")\n// =============================================================================\n\nmodel Property {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n title String\n category String // SALE, RENT, LEASE\n propertyType String // APARTMENT, HOUSE, LAND, COMMERCIAL, ESTATE, TOWNHOUSE\n country String\n currency String // USD, NGN, etc\n city String\n district String?\n zipCode String?\n streetAddress String?\n longitude Float?\n latitude Float?\n status String @default(\"DRAFT\") // DRAFT, PUBLISHED, SOLD_OUT, ARCHIVED\n description String? @db.Text\n displayImageId String?\n displayImage PropertyMedia? @relation(\"DisplayImage\", fields: [displayImageId], references: [id], onDelete: SetNull)\n isPublished Boolean @default(false)\n publishedAt DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n documents PropertyDocument[]\n media PropertyMedia[] @relation(\"PropertyMedia\")\n amenities PropertyAmenity[] // Shared amenities (gym, pool, security)\n paymentMethods PropertyPaymentMethodLink[]\n variants PropertyVariant[]\n\n @@index([tenantId])\n @@index([userId])\n @@index([category])\n @@index([propertyType])\n @@index([city])\n @@index([status])\n @@map(\"properties\")\n}\n\nmodel PropertyMedia {\n id String @id @default(cuid())\n propertyId String\n property Property @relation(\"PropertyMedia\", fields: [propertyId], references: [id], onDelete: Cascade)\n url String\n type String // IMAGE, VIDEO\n caption String?\n order Int @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n displayForProperties Property[] @relation(\"DisplayImage\")\n\n @@index([propertyId])\n @@map(\"property_media\")\n}\n\nmodel PropertyDocument {\n id String @id @default(cuid())\n propertyId String\n property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)\n name String\n url String\n type String // TITLE_DEED, SURVEY_PLAN, etc\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([propertyId])\n @@map(\"property_documents\")\n}\n\nmodel Amenity {\n id String @id @default(cuid())\n name String @unique\n category String? // PROPERTY, VARIANT, BOTH - helps filter which amenities to show\n icon String? // Icon name/URL for UI\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n properties PropertyAmenity[]\n variants PropertyVariantAmenity[]\n\n @@index([category])\n @@map(\"amenities\")\n}\n\n// =============================================================================\n// PROPERTY VARIANT & UNIT MODELS\n// =============================================================================\n\n// PropertyVariant = specific configuration with its own price and amenities\n// e.g., \"3-Bedroom Corner Piece - Fully Finished\", \"2-Bedroom Standard - Carcass\"\nmodel PropertyVariant {\n id String @id @default(cuid())\n propertyId String\n property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)\n\n name String // \"Corner Piece - Finished\", \"Standard - Carcass\"\n description String? @db.Text\n\n // Specifications\n nBedrooms Int?\n nBathrooms Int?\n nParkingSpots Int?\n area Float? // Square meters/feet\n\n // Pricing\n price Float\n pricePerSqm Float? // Computed or set manually\n\n // Inventory counters (denormalized for performance, updated via triggers/service)\n totalUnits Int @default(1)\n availableUnits Int @default(1)\n reservedUnits Int @default(0)\n soldUnits Int @default(0)\n\n // Status\n status String @default(\"AVAILABLE\") // AVAILABLE, LOW_STOCK, SOLD_OUT, ARCHIVED\n isActive Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n amenities PropertyVariantAmenity[]\n units PropertyUnit[]\n media PropertyVariantMedia[]\n\n @@index([propertyId])\n @@index([status])\n @@index([price])\n @@map(\"property_variants\")\n}\n\n// PropertyVariantAmenity = amenities specific to a variant\n// e.g., \"Finished Kitchen\", \"Smart Home System\", \"Private Garden\"\nmodel PropertyVariantAmenity {\n variantId String\n amenityId String\n variant PropertyVariant @relation(fields: [variantId], references: [id], onDelete: Cascade)\n amenity Amenity @relation(fields: [amenityId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n\n @@id([variantId, amenityId])\n @@map(\"property_variant_amenities\")\n}\n\n// PropertyVariantMedia = images/videos specific to a variant\nmodel PropertyVariantMedia {\n id String @id @default(cuid())\n variantId String\n variant PropertyVariant @relation(fields: [variantId], references: [id], onDelete: Cascade)\n url String\n type String // IMAGE, VIDEO, FLOOR_PLAN, 3D_TOUR\n caption String?\n order Int @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([variantId])\n @@map(\"property_variant_media\")\n}\n\n// PropertyUnit = individual sellable/rentable unit within a variant\n// e.g., \"Unit A1\", \"Block B - Flat 3\", \"Plot 15\"\nmodel PropertyUnit {\n id String @id @default(cuid())\n variantId String\n variant PropertyVariant @relation(fields: [variantId], references: [id], onDelete: Cascade)\n\n unitNumber String // \"A1\", \"B-3\", \"Plot 15\"\n floorNumber Int? // For apartments\n blockName String? // \"Block A\", \"Tower 1\"\n\n // Unit-specific overrides (if different from variant)\n priceOverride Float? // If this specific unit has a different price\n areaOverride Float? // If this specific unit has a different area\n notes String? @db.Text // Internal notes about this unit\n\n // Status tracking\n status String @default(\"AVAILABLE\") // AVAILABLE, RESERVED, SOLD, RENTED, UNAVAILABLE\n\n // Reservation/hold\n reservedAt DateTime?\n reservedUntil DateTime?\n reservedById String?\n\n // Ownership tracking (once sold)\n ownerId String?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n contracts Contract[]\n\n // Transfer requests targeting this unit\n transferRequests PropertyTransferRequest[]\n\n @@unique([variantId, unitNumber])\n @@index([variantId])\n @@index([status])\n @@map(\"property_units\")\n}\n\nmodel PropertyAmenity {\n propertyId String\n amenityId String\n property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)\n amenity Amenity @relation(fields: [amenityId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n\n @@id([propertyId, amenityId])\n @@map(\"property_amenities\")\n}\n\n// =============================================================================\n// PAYMENT PLAN DOMAIN - Reusable installment structure templates\n// =============================================================================\n\n// PaymentPlan = reusable structure for how payments are scheduled\n// Examples: \"Monthly360\" (360 monthly payments), \"Weekly52\", \"OneTime\"\nmodel PaymentPlan {\n id String @id @default(cuid())\n tenantId String? // NULL = global template available to all tenants\n tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n name String\n description String? @db.Text\n isActive Boolean @default(true)\n\n // Structure configuration\n paymentFrequency PaymentFrequency\n customFrequencyDays Int?\n numberOfInstallments Int // 1 for one-time, 360 for 30yr monthly, etc\n calculateInterestDaily Boolean @default(false)\n gracePeriodDays Int @default(0)\n\n // Fund collection behavior\n // true = we collect funds via wallet/gateway (e.g., downpayment)\n // false = external payment, we only track/reconcile (e.g., bank mortgage)\n collectFunds Boolean @default(true)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Used by property payment method phases (templates)\n methodPhases PropertyPaymentMethodPhase[]\n // Used by instantiated contract phases\n contractPhases ContractPhase[]\n\n @@unique([tenantId, name]) // Unique per tenant, or globally if tenantId is null\n @@index([tenantId])\n @@map(\"payment_plans\")\n}\n\n// =============================================================================\n// PROPERTY PAYMENT METHOD DOMAIN - Product offerings per property\n// =============================================================================\n\n// PropertyPaymentMethod = how a property can be purchased (e.g., \"Standard Mortgage\", \"Cash\", \"Rent-to-Own\")\nmodel PropertyPaymentMethod {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n name String // \"Standard Mortgage\", \"Flexible Payment\", \"Cash Purchase\"\n description String? @db.Text\n isActive Boolean @default(true)\n\n // Global method configuration\n allowEarlyPayoff Boolean @default(true)\n earlyPayoffPenaltyRate Float?\n autoActivatePhases Boolean @default(true)\n requiresManualApproval Boolean @default(false)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Many-to-many with properties\n properties PropertyPaymentMethodLink[]\n // Phases that make up this method (templates)\n phases PropertyPaymentMethodPhase[]\n // Contracts using this method\n contracts Contract[]\n\n // Payment method change tracking\n changeRequestsFrom PaymentMethodChangeRequest[] @relation(\"ChangeFromMethod\")\n changeRequestsTo PaymentMethodChangeRequest[] @relation(\"ChangeToMethod\")\n\n // Document requirement rules\n documentRules DocumentRequirementRule[] @relation(\"RulePaymentMethod\")\n changeRulesFrom DocumentRequirementRule[] @relation(\"RuleFromMethod\")\n changeRulesTo DocumentRequirementRule[] @relation(\"RuleToMethod\")\n\n @@unique([tenantId, name]) // Unique per tenant\n @@index([tenantId])\n @@map(\"property_payment_methods\")\n}\n\n// Many-to-many link between Property and PaymentMethod\nmodel PropertyPaymentMethodLink {\n propertyId String\n property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)\n paymentMethodId String\n paymentMethod PropertyPaymentMethod @relation(fields: [paymentMethodId], references: [id], onDelete: Cascade)\n\n // Method-specific overrides for this property\n isDefault Boolean @default(false)\n isActive Boolean @default(true)\n createdAt DateTime @default(now())\n\n @@id([propertyId, paymentMethodId])\n @@map(\"property_payment_method_links\")\n}\n\n// Phase template within a PropertyPaymentMethod (e.g., documentation, downpayment, mortgage)\n// phaseCategory determines the FSM type: DOCUMENTATION or PAYMENT\nmodel PropertyPaymentMethodPhase {\n id String @id @default(cuid())\n paymentMethodId String\n paymentMethod PropertyPaymentMethod @relation(fields: [paymentMethodId], references: [id], onDelete: Cascade)\n paymentPlanId String? // Only for PAYMENT phases\n paymentPlan PaymentPlan? @relation(fields: [paymentPlanId], references: [id])\n\n name String\n description String? @db.Text\n\n // Phase classification (DB-enforced enums)\n phaseCategory PhaseCategory\n phaseType PhaseType\n order Int\n\n // Financial configuration (for PAYMENT phases)\n interestRate Float?\n percentOfPrice Float? // e.g., 10.0 for 10% downpayment\n\n // Fund collection behavior (inherited from PaymentPlan if not set)\n // true = we collect funds via wallet/gateway (e.g., downpayment)\n // false = external payment, we only track/reconcile (e.g., bank mortgage)\n collectFunds Boolean? // null = inherit from PaymentPlan\n\n // Activation rules\n requiresPreviousPhaseCompletion Boolean @default(true)\n minimumCompletionPercentage Float?\n completionCriterion CompletionCriterion?\n\n // Snapshots for audit (original config at creation time)\n stepDefinitionsSnapshot Json?\n requiredDocumentSnapshot Json?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Normalized child tables (for DOCUMENTATION phases)\n steps PaymentMethodPhaseStep[]\n requiredDocuments PaymentMethodPhaseDocument[]\n\n @@index([paymentMethodId])\n @@index([paymentPlanId])\n @@index([phaseCategory])\n @@map(\"property_payment_method_phases\")\n}\n\n// Step template within a DOCUMENTATION phase\nmodel PaymentMethodPhaseStep {\n id String @id @default(cuid())\n phaseId String\n phase PropertyPaymentMethodPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)\n\n name String\n stepType StepType\n order Int\n\n metadata Json?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Event attachments - handlers that fire on step transitions\n eventAttachments StepEventAttachment[]\n\n @@index([phaseId])\n @@map(\"payment_method_phase_steps\")\n}\n\n/// Step Event Attachment - Links event handlers to step template triggers\n/// When a step transitions (complete, reject, etc.), attached handlers fire\nmodel StepEventAttachment {\n id String @id @default(cuid())\n stepId String\n step PaymentMethodPhaseStep @relation(fields: [stepId], references: [id], onDelete: Cascade)\n\n /// When this handler should fire\n trigger StepTrigger\n\n /// The event handler to execute\n handlerId String\n handler EventHandler @relation(fields: [handlerId], references: [id], onDelete: Cascade)\n\n /// Order of execution (lower = first)\n priority Int @default(100)\n\n /// Whether this attachment is active\n enabled Boolean @default(true)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([stepId, handlerId, trigger])\n @@index([stepId])\n @@index([handlerId])\n @@map(\"step_event_attachments\")\n}\n\n// Required document within a DOCUMENTATION phase\nmodel PaymentMethodPhaseDocument {\n id String @id @default(cuid())\n phaseId String\n phase PropertyPaymentMethodPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)\n\n documentType String\n isRequired Boolean @default(true)\n description String? @db.Text\n allowedMimeTypes String? // CSV: application/pdf,image/jpeg\n maxSizeBytes Int?\n\n metadata Json?\n createdAt DateTime @default(now())\n\n @@index([phaseId, documentType])\n @@map(\"payment_method_phase_documents\")\n}\n\n// =============================================================================\n// CONTRACT DOMAIN - Unified agreement model (replaces Mortgage, PurchasePlan, etc.)\n// =============================================================================\n// Contract is the canonical agreement. \"Mortgage\" is just a product configuration\n// that creates a Contract with specific phases (documentation, downpayment, long-term payment).\n// Phases can be DOCUMENTATION (FSM for approvals) or PAYMENT (PaymentPlan-driven installments).\n// =============================================================================\n\nmodel Contract {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n // Link to specific unit being purchased/rented\n propertyUnitId String\n propertyUnit PropertyUnit @relation(fields: [propertyUnitId], references: [id], onDelete: Cascade)\n buyerId String\n buyer User @relation(\"ContractBuyer\", fields: [buyerId], references: [id], onDelete: Cascade)\n sellerId String?\n seller User? @relation(\"ContractSeller\", fields: [sellerId], references: [id])\n paymentMethodId String? // PropertyPaymentMethod used to create this contract\n paymentMethod PropertyPaymentMethod? @relation(fields: [paymentMethodId], references: [id])\n\n // Contract identification\n contractNumber String @unique\n title String\n description String? @db.Text\n contractType String // Admin-defined: MORTGAGE, INSTALLMENT, RENT_TO_OWN, CASH, LEASE, etc.\n\n // Financial summary (computed from phases)\n totalAmount Float // Total contract value (from unit price or negotiated)\n downPayment Float @default(0)\n downPaymentPaid Float @default(0)\n principal Float? // Financed amount (if applicable)\n interestRate Float? // Overall interest rate (if applicable)\n termMonths Int? // Total term (if applicable)\n periodicPayment Float? // Computed periodic payment (if applicable)\n totalPaidToDate Float @default(0)\n totalInterestPaid Float @default(0)\n\n // Pre-approval and underwriting data (moved from prequalification)\n monthlyIncome Float? // Buyer's monthly income\n monthlyExpenses Float? // Buyer's monthly expenses\n preApprovalAnswers Json? // Questionnaire answers from PRE_APPROVAL step\n underwritingScore Float? // Aggregate score from underwriting evaluation\n debtToIncomeRatio Float? // Calculated DTI ratio\n\n // FSM state (DB-enforced enums)\n status ContractStatus @default(DRAFT)\n state ContractStatus @default(DRAFT) // FSM state for workflow\n currentPhaseId String?\n\n // Timing\n nextPaymentDueDate DateTime?\n lastReminderSentAt DateTime?\n startDate DateTime?\n endDate DateTime?\n signedAt DateTime?\n terminatedAt DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n phases ContractPhase[]\n documents ContractDocument[]\n payments ContractPayment[]\n terminations ContractTermination[]\n offerLetters OfferLetter[]\n\n // Payment method change requests for this contract\n paymentMethodChangeRequests PaymentMethodChangeRequest[]\n\n // Transfer tracking - when a contract is transferred to a different property\n transferredFromId String? @unique // Source contract if this was created via transfer\n transferredFrom Contract? @relation(\"ContractTransfer\", fields: [transferredFromId], references: [id])\n transferredTo Contract? @relation(\"ContractTransfer\")\n\n // Transfer requests where this contract is the source\n outgoingTransferRequests PropertyTransferRequest[] @relation(\"SourceContract\")\n // Transfer requests where this contract is the target (created after approval)\n incomingTransferRequests PropertyTransferRequest[] @relation(\"TargetContract\")\n\n // Audit trail\n events ContractEvent[]\n\n // Refund requests\n refunds ContractRefund[]\n\n @@index([tenantId])\n @@index([propertyUnitId])\n @@index([buyerId])\n @@index([sellerId])\n @@index([paymentMethodId])\n @@index([status])\n @@index([state])\n @@map(\"contracts\")\n}\n\n// =============================================================================\n// CONTRACT REFUNDS - Track refund requests for overpayments or cancellations\n// =============================================================================\nmodel ContractRefund {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n\n amount Float\n reason String @db.Text\n status RefundStatus @default(PENDING)\n\n // Who requested the refund\n requestedById String\n requestedBy User @relation(\"RefundRequester\", fields: [requestedById], references: [id])\n\n // Who approved/rejected the refund\n approvedById String?\n approvedBy User? @relation(\"RefundApprover\", fields: [approvedById], references: [id])\n\n // Who processed the refund (finance team)\n processedById String?\n processedBy User? @relation(\"RefundProcessor\", fields: [processedById], references: [id])\n\n // Refund payment details\n paymentMethod String? // BANK_TRANSFER, CHEQUE, MOBILE_MONEY, etc.\n referenceNumber String? // Bank/payment reference\n recipientName String?\n recipientAccount String?\n recipientBank String?\n\n // Timestamps\n requestedAt DateTime @default(now())\n approvedAt DateTime?\n rejectedAt DateTime?\n processedAt DateTime?\n\n // Additional notes\n approvalNotes String? @db.Text\n rejectionNotes String? @db.Text\n processingNotes String? @db.Text\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([contractId])\n @@index([status])\n @@index([tenantId])\n @@index([requestedById])\n @@map(\"contract_refunds\")\n}\n\n// Phase within a contract - can be DOCUMENTATION or PAYMENT type\n// Admin names phases freely (e.g., \"KYC Documents\", \"Downpayment\", \"Monthly Mortgage\")\nmodel ContractPhase {\n id String @id @default(cuid())\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n paymentPlanId String? // Only for PAYMENT phases\n paymentPlan PaymentPlan? @relation(fields: [paymentPlanId], references: [id])\n\n // Admin-defined naming\n name String\n description String? @db.Text\n\n // Phase classification (DB-enforced enums)\n phaseCategory PhaseCategory\n phaseType PhaseType\n order Int\n\n // FSM state for this phase (DB-enforced enum)\n status PhaseStatus @default(PENDING)\n\n // =========================================================================\n // WORKFLOW TRACKING - Current step pointer for UX and orchestration\n // =========================================================================\n // Canonical pointer to the step currently requiring attention.\n // Updated by service when: phase activates (→ first step), step completes (→ next),\n // step rejected (→ same step with NEEDS_RESUBMISSION), or phase completes (→ null).\n currentStepId String?\n currentStep DocumentationStep? @relation(\"CurrentStep\", fields: [currentStepId], references: [id])\n\n // Financial details (for PAYMENT phases)\n totalAmount Float?\n paidAmount Float @default(0)\n remainingAmount Float?\n interestRate Float?\n\n // Fund collection behavior (snapshotted from template at contract creation)\n // true = we collect funds via wallet/gateway (e.g., downpayment)\n // false = external payment, we only track/reconcile (e.g., bank mortgage)\n collectFunds Boolean @default(true)\n\n // Progress counters (for efficient activation checks)\n approvedDocumentsCount Int @default(0)\n requiredDocumentsCount Int @default(0)\n completedStepsCount Int @default(0)\n totalStepsCount Int @default(0)\n\n // Timing\n dueDate DateTime?\n startDate DateTime?\n endDate DateTime?\n activatedAt DateTime?\n completedAt DateTime?\n\n // Activation rules\n requiresPreviousPhaseCompletion Boolean @default(true)\n minimumCompletionPercentage Float?\n completionCriterion CompletionCriterion?\n\n // Snapshots for audit (effective config at contract creation)\n paymentPlanSnapshot Json?\n stepDefinitionsSnapshot Json?\n requiredDocumentSnapshot Json?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n installments ContractInstallment[]\n payments ContractPayment[]\n steps DocumentationStep[] // For DOCUMENTATION phases (FSM steps)\n\n @@index([contractId])\n @@index([paymentPlanId])\n @@index([phaseCategory])\n @@index([status])\n @@index([order])\n @@index([currentStepId])\n @@map(\"contract_phases\")\n}\n\n// =============================================================================\n// CONTRACT EVENTS - Audit trail for contract lifecycle\n// =============================================================================\n// Tracks all significant events in a contract's lifecycle for audit, compliance,\n// and debugging. Unlike DomainEvent (which is for inter-service communication),\n// ContractEvent is purely for historical tracking and state machine transitions.\n// =============================================================================\nmodel ContractEvent {\n id String @id @default(cuid())\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n\n // Event classification\n eventType ContractEventType\n eventGroup ContractEventGroup?\n\n // For state transitions (optional - only populated for CONTRACT_STATE_CHANGED events)\n fromState String?\n toState String?\n trigger String?\n\n // Event payload (all event-specific data)\n data Json?\n\n // Actor tracking\n actorId String?\n actorType EventActorType?\n\n // Timing\n occurredAt DateTime @default(now())\n\n @@index([contractId])\n @@index([eventType])\n @@index([eventGroup])\n @@index([occurredAt])\n @@map(\"contract_events\")\n}\n\n// Steps within a DOCUMENTATION phase (FSM for document collection/approval)\nmodel DocumentationStep {\n id String @id @default(cuid())\n phaseId String\n phase ContractPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)\n\n name String\n description String? @db.Text\n stepType StepType\n order Int\n\n status StepStatus @default(PENDING)\n\n // =========================================================================\n // USER ACTION TRACKING - For rejection/resubmission flows\n // =========================================================================\n // When status is NEEDS_RESUBMISSION or ACTION_REQUIRED, this explains why.\n // Populated from DocumentationStepApproval.comment on rejection.\n actionReason String? @db.Text\n\n // Number of times this step has been submitted (for tracking resubmissions)\n submissionCount Int @default(0)\n\n // Last submission timestamp (for tracking resubmission timing)\n lastSubmittedAt DateTime?\n\n // Configuration metadata (for GENERATE_DOCUMENT steps, etc.)\n metadata Json?\n\n // For PRE_APPROVAL steps: store questionnaire answers\n preApprovalAnswers Json?\n\n // For UNDERWRITING steps: store evaluation results\n underwritingScore Float?\n debtToIncomeRatio Float?\n underwritingDecision String? // APPROVED, CONDITIONAL, DECLINED\n underwritingNotes String? @db.Text\n\n // Assignment\n assigneeId String?\n assignee User? @relation(\"DocumentationStepAssignee\", fields: [assigneeId], references: [id])\n\n // Required document types for UPLOAD steps (normalized)\n requiredDocuments DocumentationStepDocument[]\n\n // Timing\n dueDate DateTime?\n completedAt DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n approvals DocumentationStepApproval[]\n currentForPhase ContractPhase[] @relation(\"CurrentStep\")\n\n @@index([phaseId])\n @@index([status])\n @@index([order])\n @@map(\"contract_phase_steps\")\n}\n\n// Required documents for a step (normalized from CSV)\nmodel DocumentationStepDocument {\n id String @id @default(cuid())\n stepId String\n step DocumentationStep @relation(fields: [stepId], references: [id], onDelete: Cascade)\n\n documentType String\n isRequired Boolean @default(true)\n\n createdAt DateTime @default(now())\n\n @@index([stepId, documentType])\n @@map(\"contract_phase_step_documents\")\n}\n\n// Approvals for documentation steps\nmodel DocumentationStepApproval {\n id String @id @default(cuid())\n stepId String\n step DocumentationStep @relation(fields: [stepId], references: [id], onDelete: Cascade)\n approverId String?\n approver User? @relation(\"DocumentationStepApprover\", fields: [approverId], references: [id])\n\n decision ApprovalDecision\n comment String? @db.Text\n decidedAt DateTime @default(now())\n\n createdAt DateTime @default(now())\n\n @@index([stepId])\n @@map(\"contract_phase_step_approvals\")\n}\n\n// Installments within a PAYMENT phase\nmodel ContractInstallment {\n id String @id @default(cuid())\n phaseId String\n phase ContractPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)\n\n installmentNumber Int\n\n amount Float\n principalAmount Float @default(0)\n interestAmount Float @default(0)\n\n dueDate DateTime\n status InstallmentStatus @default(PENDING)\n\n paidAmount Float @default(0)\n paidDate DateTime?\n\n lateFee Float @default(0)\n lateFeeWaived Boolean @default(false)\n gracePeriodDays Int @default(0)\n gracePeriodEndDate DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n payments ContractPayment[]\n\n @@index([phaseId])\n @@index([dueDate])\n @@index([status])\n @@map(\"contract_installments\")\n}\n\n// Unified payment record for contracts\nmodel ContractPayment {\n id String @id @default(cuid())\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n phaseId String?\n phase ContractPhase? @relation(fields: [phaseId], references: [id])\n installmentId String?\n installment ContractInstallment? @relation(fields: [installmentId], references: [id])\n payerId String?\n payer User? @relation(\"ContractPayer\", fields: [payerId], references: [id])\n\n amount Float\n principalAmount Float @default(0)\n interestAmount Float @default(0)\n lateFeeAmount Float @default(0)\n\n paymentMethod String // BANK_TRANSFER, CREDIT_CARD, WALLET, CASH, CHECK\n status PaymentStatus @default(INITIATED)\n\n reference String? @unique\n gatewayResponse String? @db.Text // JSON\n\n processedAt DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([contractId])\n @@index([phaseId])\n @@index([installmentId])\n @@index([payerId])\n @@index([status])\n @@index([reference])\n @@map(\"contract_payments\")\n}\n\n// Contract documents (owned by contract, linked to phases/steps as needed)\nmodel ContractDocument {\n id String @id @default(cuid())\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n phaseId String? // Optional link to specific phase\n stepId String? // Optional link to specific step\n\n name String\n url String\n type String // ID, BANK_STATEMENT, INCOME_PROOF, TITLE_DEED, SIGNATURE, etc.\n uploadedById String?\n uploadedBy User? @relation(\"DocumentUploader\", fields: [uploadedById], references: [id])\n\n status DocumentStatus @default(PENDING)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([contractId])\n @@index([phaseId])\n @@index([stepId])\n @@index([type])\n @@index([status])\n @@map(\"contract_documents\")\n}\n\n// =============================================================================\n// OFFER LETTERS - Provisional and Final offer documents\n// =============================================================================\n\nmodel DocumentTemplate {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n name String // \"Provisional Offer Letter\", \"Final Offer Letter\"\n code String // PROVISIONAL_OFFER, FINAL_OFFER\n description String?\n version Int @default(1)\n\n // Template content (Handlebars)\n htmlTemplate String @db.Text\n cssStyles String? @db.Text\n\n // Merge field definitions for UI\n mergeFields Json? // [{name, type, required, description}]\n\n isActive Boolean @default(true)\n isDefault Boolean @default(false)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n offerLetters OfferLetter[]\n\n @@unique([tenantId, code, version])\n @@index([tenantId])\n @@index([code])\n @@map(\"document_templates\")\n}\n\nmodel OfferLetter {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n\n // Template used (optional - documents-service may handle default selection)\n templateId String?\n template DocumentTemplate? @relation(fields: [templateId], references: [id])\n\n // Letter details\n letterNumber String @unique // OL-XXXXXX\n type OfferLetterType\n status OfferLetterStatus @default(DRAFT)\n\n // Generated document\n htmlContent String? @db.Text // Rendered HTML\n pdfUrl String? // S3 URL of generated PDF\n pdfKey String? // S3 key for deletion/access\n\n // Merge data used (snapshot for audit)\n mergeData Json? // All data merged into template\n\n // Signing workflow\n sentAt DateTime?\n viewedAt DateTime?\n signedAt DateTime?\n signatureIp String?\n signatureData Json? // {method, timestamp, metadata}\n\n // Validity\n expiresAt DateTime?\n expiredAt DateTime?\n cancelledAt DateTime?\n cancelReason String?\n\n // Audit\n generatedById String?\n generatedBy User? @relation(\"OfferLetterGenerator\", fields: [generatedById], references: [id])\n sentById String?\n sentBy User? @relation(\"OfferLetterSender\", fields: [sentById], references: [id])\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([tenantId])\n @@index([contractId])\n @@index([type])\n @@index([status])\n @@map(\"offer_letters\")\n}\n\n// =============================================================================\n// CONTRACT TERMINATION - Full lifecycle for cancellation/termination\n// =============================================================================\n// Tracks termination requests from initiation through refund completion.\n// Industry-standard flow:\n// 1. Request created (by buyer/seller/admin/system)\n// 2. Admin reviews (if required by policy)\n// 3. Financial settlement calculated (refunds, penalties, forfeitures)\n// 4. Refund processed (if applicable)\n// 5. Contract marked terminated, unit released\n// =============================================================================\n\nmodel ContractTermination {\n id String @id @default(cuid())\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n // Request identification\n requestNumber String @unique // TRM-XXXXXX\n\n // Who initiated and why\n initiatedBy TerminationInitiator\n initiatorId String? // userId if BUYER/SELLER/ADMIN\n initiator User? @relation(\"TerminationInitiator\", fields: [initiatorId], references: [id])\n type TerminationType\n reason String? @db.Text\n supportingDocs Json? // [{type, url, uploadedAt}]\n\n // Workflow status\n status TerminationStatus @default(REQUESTED)\n requiresApproval Boolean @default(true)\n autoApproveEligible Boolean @default(false) // Pre-signature, no payments\n\n // Admin review\n reviewedBy String?\n reviewer User? @relation(\"TerminationReviewer\", fields: [reviewedBy], references: [id])\n reviewedAt DateTime?\n reviewNotes String? @db.Text\n rejectionReason String? @db.Text\n\n // Financial snapshot at time of request\n contractSnapshot Json // Full contract state snapshot\n totalContractAmount Float\n totalPaidToDate Float\n outstandingBalance Float\n\n // Settlement calculation\n refundableAmount Float @default(0) // Amount eligible for refund\n penaltyAmount Float @default(0) // Penalties/fees to deduct\n forfeitedAmount Float @default(0) // Amount forfeited (non-refundable deposits)\n adminFeeAmount Float @default(0) // Processing fees\n netRefundAmount Float @default(0) // refundableAmount - penaltyAmount - adminFeeAmount\n settlementNotes String? @db.Text\n\n // Refund processing\n refundStatus RefundStatus @default(PENDING)\n refundReference String? // Payment gateway reference\n refundMethod String? // ORIGINAL_METHOD, BANK_TRANSFER, CHECK, WALLET\n refundAccountDetails Json? // Encrypted bank details if needed\n refundInitiatedAt DateTime?\n refundCompletedAt DateTime?\n refundFailureReason String? @db.Text\n\n // Property unit handling\n unitReleasedAt DateTime?\n unitReservedForId String? // If unit being held for another buyer\n\n // Timing\n requestedAt DateTime @default(now())\n approvedAt DateTime?\n executedAt DateTime?\n completedAt DateTime?\n cancelledAt DateTime?\n\n // Idempotency and audit\n idempotencyKey String? @unique\n metadata Json?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([contractId])\n @@index([tenantId])\n @@index([status])\n @@index([type])\n @@index([initiatorId])\n @@index([requestedAt])\n @@map(\"contract_terminations\")\n}\n\n// =============================================================================\n// PAYMENT METHOD CHANGE REQUEST - Mid-contract payment method changes\n// =============================================================================\n// When a user wants to change their payment method after contract creation,\n// this aggregate tracks the request, required documentation, approvals, and\n// final execution. Different from-to combinations may require different docs.\n// =============================================================================\n\nenum PaymentMethodChangeStatus {\n PENDING_DOCUMENTS\n DOCUMENTS_SUBMITTED\n UNDER_REVIEW\n APPROVED\n REJECTED\n EXECUTED\n CANCELLED\n}\n\nmodel PaymentMethodChangeRequest {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n\n // The change being requested\n fromPaymentMethodId String\n fromPaymentMethod PropertyPaymentMethod @relation(\"ChangeFromMethod\", fields: [fromPaymentMethodId], references: [id])\n toPaymentMethodId String\n toPaymentMethod PropertyPaymentMethod @relation(\"ChangeToMethod\", fields: [toPaymentMethodId], references: [id])\n\n // Who requested and why\n requestorId String\n requestor User @relation(\"ChangeRequestor\", fields: [requestorId], references: [id])\n reason String? @db.Text\n\n // Documentation requirements (determined by DocumentRequirementRule)\n requiredDocumentTypes String? // CSV: BANK_STATEMENT,INCOME_PROOF,NEW_EMPLOYER_LETTER\n submittedDocuments Json? // [{type, s3Key, uploadedAt, status}]\n\n // Financial impact assessment\n currentOutstanding Float? // Outstanding balance at time of request\n newTermMonths Int? // New term if applicable\n newInterestRate Float? // New rate if applicable\n newMonthlyPayment Float? // Projected new payment\n penaltyAmount Float? // Early change penalty if applicable\n financialImpactNotes String? @db.Text\n\n // Status and workflow\n status PaymentMethodChangeStatus @default(PENDING_DOCUMENTS)\n reviewerId String?\n reviewer User? @relation(\"ChangeReviewer\", fields: [reviewerId], references: [id])\n reviewNotes String? @db.Text\n reviewedAt DateTime?\n\n // Execution details\n executedAt DateTime?\n previousPhaseData Json? // Snapshot of phases before change\n newPhaseData Json? // New phases created after change\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([tenantId])\n @@index([contractId])\n @@index([status])\n @@index([requestorId])\n @@map(\"payment_method_change_requests\")\n}\n\n// =============================================================================\n// DOCUMENT REQUIREMENT RULES - Configurable document requirements\n// =============================================================================\n// Admins can configure which documents are required for specific scenarios:\n// - Prequalification for a payment method type\n// - Contract phases\n// - Payment method changes (from-to combinations)\n// This allows tenants to customize documentation workflows per product.\n// =============================================================================\n\nenum DocumentRequirementContext {\n CONTRACT_PHASE // During a contract phase\n PAYMENT_METHOD_CHANGE // When changing payment method mid-contract\n}\n\nmodel DocumentRequirementRule {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n // Rule context\n context DocumentRequirementContext\n\n // Scoping (which situations this rule applies to)\n // For PREQUALIFICATION: paymentMethodId\n // For CONTRACT_PHASE: phaseType\n // For PAYMENT_METHOD_CHANGE: fromMethodId + toMethodId\n paymentMethodId String?\n paymentMethod PropertyPaymentMethod? @relation(\"RulePaymentMethod\", fields: [paymentMethodId], references: [id])\n phaseType String? // KYC, VERIFICATION, DOWNPAYMENT, etc.\n fromPaymentMethodId String?\n fromPaymentMethod PropertyPaymentMethod? @relation(\"RuleFromMethod\", fields: [fromPaymentMethodId], references: [id])\n toPaymentMethodId String?\n toPaymentMethod PropertyPaymentMethod? @relation(\"RuleToMethod\", fields: [toPaymentMethodId], references: [id])\n\n // Document requirements\n documentType String // ID_CARD, PASSPORT, BANK_STATEMENT, INCOME_PROOF, etc.\n isRequired Boolean @default(true)\n description String? // Instructions for the user\n maxSizeBytes Int? // Max file size allowed\n allowedMimeTypes String? // CSV: application/pdf,image/jpeg,image/png\n\n // Validation rules\n expiryDays Int? // Document must not be older than X days\n requiresManualReview Boolean @default(false)\n\n isActive Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([tenantId])\n @@index([context])\n @@index([paymentMethodId])\n @@index([phaseType])\n @@index([fromPaymentMethodId, toPaymentMethodId])\n @@map(\"document_requirement_rules\")\n}\n\n// =============================================================================\n// EVENT-DRIVEN WORKFLOW CONFIGURATION\n// =============================================================================\n// This system allows admins to configure event channels, types, and handlers\n// for a flexible, configurable event-driven workflow system.\n//\n// Architecture:\n// 1. EventChannel - Logical grouping of events (e.g., \"contracts\", \"payments\")\n// 2. EventType - Specific event types (e.g., \"DOCUMENT_UPLOADED\", \"STEP_COMPLETED\")\n// 3. EventHandler - What to do when an event fires (webhook, internal call, etc.)\n// 4. WorkflowEvent - Actual event instances (audit log)\n// 5. EventHandlerExecution - Log of handler executions\n// =============================================================================\n\n/// Event Channel - A logical grouping of events\n/// Channels help organize events and route them to appropriate handlers\nmodel EventChannel {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n /// Unique code for the channel (e.g., \"CONTRACTS\", \"PAYMENTS\")\n code String\n /// Human-readable name\n name String\n /// Description of what this channel handles\n description String? @db.Text\n\n /// Whether this channel is active\n enabled Boolean @default(true)\n\n /// Event types that belong to this channel\n eventTypes EventType[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([tenantId, code])\n @@index([tenantId])\n @@map(\"event_channels\")\n}\n\n/// Event Type - Defines a type of event that can occur\n/// Each event type belongs to a channel and can have multiple handlers\nmodel EventType {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n /// The channel this event type belongs to\n channelId String\n channel EventChannel @relation(fields: [channelId], references: [id], onDelete: Cascade)\n\n /// Unique code for this event type (e.g., \"DOCUMENT_UPLOADED\")\n code String\n /// Human-readable name\n name String\n /// Description of when this event fires\n description String? @db.Text\n\n /// JSON schema for event payload validation (optional)\n payloadSchema Json?\n\n /// Whether this event type is active\n enabled Boolean @default(true)\n\n /// Handlers subscribed to this event type\n handlers EventHandler[]\n\n /// Actual event instances of this type\n events WorkflowEvent[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([tenantId, code])\n @@unique([channelId, code])\n @@index([tenantId])\n @@index([channelId])\n @@map(\"event_types\")\n}\n\n/// Event Handler - Defines what should happen when an event fires\n/// Handlers can be internal (call a service), external (webhook), or workflow triggers\nmodel EventHandler {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n /// The event type this handler responds to\n eventTypeId String\n eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)\n\n /// Human-readable name\n name String\n /// Description of what this handler does\n description String? @db.Text\n\n /// Handler type determines how the event is processed\n handlerType EventHandlerType\n\n /// Configuration for the handler (JSON, depends on handlerType)\n /// INTERNAL: { \"service\": \"contract\", \"method\": \"completeStep\" }\n /// WEBHOOK: { \"url\": \"https://...\", \"method\": \"POST\", \"headers\": {...} }\n /// WORKFLOW: { \"workflowId\": \"...\", \"action\": \"advance\" }\n /// NOTIFICATION: { \"template\": \"...\", \"channels\": [\"email\", \"sms\"] }\n config Json\n\n /// Order of execution when multiple handlers exist (lower = first)\n priority Int @default(100)\n\n /// Whether this handler is active\n enabled Boolean @default(true)\n\n /// Retry configuration\n maxRetries Int @default(3)\n retryDelayMs Int @default(1000)\n\n /// Filter condition (JSONPath expression) to conditionally run\n /// e.g., \"$.payload.status == 'approved'\"\n filterCondition String? @db.Text\n\n /// Handler execution logs\n executions EventHandlerExecution[]\n\n /// Step attachments - steps that have attached this handler\n stepAttachments StepEventAttachment[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([tenantId])\n @@index([eventTypeId])\n @@index([handlerType])\n @@map(\"event_handlers\")\n}\n\n/// Workflow Event - An actual event instance that occurred\n/// This is the audit log of all events in the system\nmodel WorkflowEvent {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n /// The type of this event\n eventTypeId String\n eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)\n\n /// The event payload (actual data)\n payload Json\n\n /// Optional correlation ID to link related events\n correlationId String?\n\n /// Optional causation ID (which event caused this one)\n causationId String?\n\n /// Source of the event (service name, user action, etc.)\n source String\n\n /// Actor who triggered the event (user ID, API key ID, \"system\")\n actorId String?\n actorType ActorType @default(SYSTEM)\n\n /// Event status\n status WorkflowEventStatus @default(PENDING)\n\n /// Error message if processing failed\n error String? @db.Text\n\n /// When the event was processed\n processedAt DateTime?\n\n /// Handler executions for this event\n executions EventHandlerExecution[]\n\n createdAt DateTime @default(now())\n\n @@index([tenantId])\n @@index([eventTypeId])\n @@index([correlationId])\n @@index([causationId])\n @@index([status])\n @@index([createdAt])\n @@map(\"workflow_events\")\n}\n\n/// Event Handler Execution - Log of a handler processing an event\nmodel EventHandlerExecution {\n id String @id @default(cuid())\n\n /// The event being processed\n eventId String\n event WorkflowEvent @relation(fields: [eventId], references: [id], onDelete: Cascade)\n\n /// The handler that processed this event\n handlerId String\n handler EventHandler @relation(fields: [handlerId], references: [id], onDelete: Cascade)\n\n /// Execution status\n status ExecutionStatus @default(PENDING)\n\n /// Attempt number (1 for first try, increments on retry)\n attempt Int @default(1)\n\n /// Input to the handler (may be transformed payload)\n input Json?\n\n /// Output from the handler\n output Json?\n\n /// Error details if failed\n error String? @db.Text\n errorCode String?\n\n /// Timing\n startedAt DateTime?\n completedAt DateTime?\n durationMs Int?\n\n createdAt DateTime @default(now())\n\n @@index([eventId])\n @@index([handlerId])\n @@index([status])\n @@map(\"event_handler_executions\")\n}\n\n// =============================================================================\n// EVENT OUTBOX - For guaranteed event delivery to SQS queues\n// =============================================================================\n\nmodel DomainEvent {\n id String @id @default(cuid())\n\n // Event identification\n eventType String // MORTGAGE.CREATED, PHASE.ACTIVATED, PAYMENT.COMPLETED, etc\n aggregateType String // Mortgage, MortgagePhase, MortgagePayment, Property, etc\n aggregateId String\n\n // Routing - which queue(s) should receive this\n queueName String // notifications, payments, mortgage-steps, accounting, etc\n\n // Event payload (all data needed by consumers)\n payload String @db.Text // JSON\n\n // Metadata\n occurredAt DateTime @default(now())\n actorId String? // User who triggered the event\n actorRole String? // Role of the actor\n\n // Processing status\n status String @default(\"PENDING\") // PENDING, PROCESSING, SENT, FAILED\n processedAt DateTime?\n sentAt DateTime?\n failureCount Int @default(0)\n lastError String? @db.Text\n nextRetryAt DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([status, nextRetryAt])\n @@index([eventType])\n @@index([aggregateType, aggregateId])\n @@index([queueName])\n @@index([occurredAt])\n @@map(\"domain_events\")\n}\n\n// =============================================================================\n// Property Transfer Request\n// =============================================================================\n// Allows a buyer to request transferring their contract to a different property\n// while preserving payments, completed workflow steps, and progress.\n// =============================================================================\n\nmodel PropertyTransferRequest {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n // Source contract being transferred\n sourceContractId String\n sourceContract Contract @relation(\"SourceContract\", fields: [sourceContractId], references: [id], onDelete: Cascade)\n\n // Target property unit\n targetPropertyUnitId String\n targetPropertyUnit PropertyUnit @relation(fields: [targetPropertyUnitId], references: [id])\n\n // Requestor (buyer) and reviewer (admin)\n requestedById String\n requestedBy User @relation(\"TransferRequestor\", fields: [requestedById], references: [id])\n reviewedById String?\n reviewedBy User? @relation(\"TransferReviewer\", fields: [reviewedById], references: [id])\n\n // Status and workflow\n status TransferRequestStatus @default(PENDING)\n\n // Request details\n reason String? @db.Text // Buyer's reason for transfer\n\n // Review details\n reviewNotes String? @db.Text // Admin notes on decision\n priceAdjustmentHandling String? // How to handle price difference: ADD_TO_MORTGAGE, REQUIRE_PAYMENT, CREDIT_BUYER\n\n // Computed values\n sourceTotalAmount Float? // Original contract total\n targetTotalAmount Float? // New contract total (based on target property)\n priceAdjustment Float? // Difference (positive = buyer owes more)\n paymentsMigrated Int? // Number of payments migrated\n\n // Result - new contract created after approval\n targetContractId String?\n targetContract Contract? @relation(\"TargetContract\", fields: [targetContractId], references: [id])\n\n // Timestamps\n createdAt DateTime @default(now())\n reviewedAt DateTime?\n completedAt DateTime?\n updatedAt DateTime @updatedAt\n\n @@index([tenantId])\n @@index([sourceContractId])\n @@index([targetPropertyUnitId])\n @@index([requestedById])\n @@index([status])\n @@map(\"property_transfer_requests\")\n}\n\n// =============================================================================\n// UNIFIED APPROVAL REQUESTS\n// =============================================================================\n\nenum ApprovalRequestType {\n PROPERTY_TRANSFER // Property unit transfer between contracts\n PROPERTY_UPDATE // Property/unit listing update requiring approval\n USER_WORKFLOW // User workflow step approval\n CREDIT_CHECK // Credit check result review\n CONTRACT_TERMINATION // Contract termination approval\n REFUND_APPROVAL // Refund request approval\n}\n\nenum ApprovalRequestStatus {\n PENDING // Awaiting review\n IN_REVIEW // Assigned to reviewer\n APPROVED // Approved by reviewer\n REJECTED // Rejected by reviewer\n CANCELLED // Cancelled by requestor\n EXPIRED // Auto-expired (if TTL configured)\n}\n\nenum ApprovalRequestPriority {\n LOW\n NORMAL\n HIGH\n URGENT\n}\n\n// Polymorphic approval request model for unified admin dashboard\nmodel ApprovalRequest {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n // Request type and status\n type ApprovalRequestType\n status ApprovalRequestStatus @default(PENDING)\n priority ApprovalRequestPriority @default(NORMAL)\n\n // Polymorphic reference to the entity requiring approval\n entityType String // e.g., \"PropertyTransferRequest\", \"PropertyUnit\", \"User\"\n entityId String // ID of the referenced entity\n\n // Request metadata\n title String @db.VarChar(255) // Human-readable title for the request\n description String? @db.Text // Detailed description\n\n // Payload for any additional context (JSON)\n payload Json? // Flexible data storage for type-specific details\n\n // Requestor - who created the request\n requestedById String\n requestedBy User @relation(\"ApprovalRequestor\", fields: [requestedById], references: [id])\n\n // Assignee - admin/reviewer assigned to handle this request\n assigneeId String?\n assignee User? @relation(\"ApprovalAssignee\", fields: [assigneeId], references: [id])\n\n // Reviewer - who made the final decision (may differ from assignee)\n reviewedById String?\n reviewedBy User? @relation(\"ApprovalReviewer\", fields: [reviewedById], references: [id])\n\n // Review details\n reviewNotes String? @db.Text // Reviewer's notes/comments\n decision ApprovalDecision? // APPROVED, REJECTED, REQUEST_CHANGES\n\n // Expiration\n expiresAt DateTime? // Optional TTL for auto-expiration\n\n // Timestamps\n createdAt DateTime @default(now())\n assignedAt DateTime? // When assigned to reviewer\n reviewedAt DateTime? // When decision was made\n completedAt DateTime? // When fully processed\n updatedAt DateTime @updatedAt\n\n @@index([tenantId])\n @@index([type])\n @@index([status])\n @@index([priority])\n @@index([entityType, entityId])\n @@index([requestedById])\n @@index([assigneeId])\n @@index([createdAt])\n @@map(\"approval_requests\")\n}\n",
|
|
18
|
+
"inlineSchema": "// =============================================================================\n// QSHELTER UNIFIED DATABASE SCHEMA\n// =============================================================================\n// This schema contains all database models for the QShelter platform\n// Organized by domain for better readability\n// =============================================================================\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../generated/client\"\n engineType = \"client\"\n}\n\ndatasource db {\n provider = \"mysql\"\n}\n\n// =============================================================================\n// ENUMS - Database-enforced value constraints\n// =============================================================================\n\nenum PhaseCategory {\n DOCUMENTATION\n PAYMENT\n}\n\nenum PhaseType {\n KYC\n VERIFICATION\n DOWNPAYMENT\n MORTGAGE\n BALLOON\n CUSTOM\n}\n\nenum PaymentFrequency {\n MONTHLY\n BIWEEKLY\n WEEKLY\n ONE_TIME\n CUSTOM\n}\n\nenum ContractStatus {\n DRAFT\n PENDING\n ACTIVE\n COMPLETED\n CANCELLED\n TERMINATED\n TRANSFERRED // Contract was transferred to a different property\n}\n\nenum TransferRequestStatus {\n PENDING\n APPROVED\n REJECTED\n IN_PROGRESS\n COMPLETED\n FAILED\n}\n\nenum PhaseStatus {\n PENDING\n IN_PROGRESS\n AWAITING_APPROVAL\n ACTIVE\n COMPLETED\n SKIPPED\n FAILED\n SUPERSEDED // Phase replaced by payment method change\n}\n\nenum StepType {\n UPLOAD\n REVIEW\n SIGNATURE\n APPROVAL\n EXTERNAL_CHECK\n WAIT\n GENERATE_DOCUMENT // Triggers document generation (offer letters, contracts, etc.)\n PRE_APPROVAL // Customer answers eligibility questionnaire\n UNDERWRITING // System evaluates DTI, score, eligibility\n}\n\nenum StepStatus {\n PENDING\n IN_PROGRESS\n COMPLETED\n FAILED\n SKIPPED\n NEEDS_RESUBMISSION // User must re-upload or correct something (after rejection)\n ACTION_REQUIRED // User action needed (generic - check actionReason)\n AWAITING_REVIEW // Submitted, waiting for admin/system review\n}\n\n/// When a step event attachment should trigger\nenum StepTrigger {\n ON_COMPLETE // When step is approved/completed\n ON_REJECT // When step is rejected\n ON_SUBMIT // When step is submitted for review\n ON_RESUBMIT // When step is resubmitted after rejection\n ON_START // When step transitions to IN_PROGRESS\n}\n\nenum InstallmentStatus {\n PENDING\n PAID\n OVERDUE\n PARTIALLY_PAID\n WAIVED\n}\n\nenum PaymentStatus {\n INITIATED\n PENDING\n COMPLETED\n FAILED\n REFUNDED\n}\n\nenum ApprovalDecision {\n APPROVED\n REJECTED\n REQUEST_CHANGES\n}\n\n// =============================================================================\n// CONTRACT TERMINATION / CANCELLATION ENUMS\n// =============================================================================\n\nenum TerminationType {\n BUYER_WITHDRAWAL // Buyer wants to cancel (voluntary)\n SELLER_WITHDRAWAL // Seller/developer cancels\n MUTUAL_AGREEMENT // Both parties agree to terminate\n PAYMENT_DEFAULT // Buyer failed payment obligations\n DOCUMENT_FAILURE // Buyer failed to provide required documents\n FRAUD // Fraudulent activity detected\n FORCE_MAJEURE // External circumstances (disaster, etc.)\n PROPERTY_UNAVAILABLE // Property no longer available\n REGULATORY // Regulatory/legal requirement\n OTHER // Other reasons (with notes)\n}\n\nenum TerminationStatus {\n REQUESTED // Initial request submitted\n PENDING_REVIEW // Awaiting admin review\n PENDING_REFUND // Approved, awaiting refund processing\n REFUND_IN_PROGRESS // Refund being processed\n REFUND_COMPLETED // Refund completed\n COMPLETED // Termination fully executed (no refund or refund done)\n REJECTED // Termination request rejected\n CANCELLED // Termination request was cancelled\n}\n\nenum TerminationInitiator {\n BUYER\n SELLER\n ADMIN\n SYSTEM\n}\n\nenum CompletionCriterion {\n DOCUMENT_APPROVALS\n PAYMENT_AMOUNT\n STEPS_COMPLETED\n}\n\nenum DocumentStatus {\n DRAFT\n PENDING\n PENDING_SIGNATURE\n SENT\n VIEWED\n SIGNED\n APPROVED\n REJECTED\n EXPIRED\n CANCELLED\n}\n\nenum OfferLetterType {\n PROVISIONAL\n FINAL\n}\n\nenum OfferLetterStatus {\n DRAFT\n GENERATED\n SENT\n VIEWED\n SIGNED\n EXPIRED\n CANCELLED\n}\n\nenum ContractEventType {\n CONTRACT_CREATED\n CONTRACT_STATE_CHANGED\n PHASE_ACTIVATED\n PHASE_COMPLETED\n STEP_COMPLETED\n STEP_REJECTED\n DOCUMENT_SUBMITTED\n DOCUMENT_APPROVED\n DOCUMENT_REJECTED\n PAYMENT_INITIATED\n PAYMENT_COMPLETED\n PAYMENT_FAILED\n INSTALLMENTS_GENERATED\n CONTRACT_SIGNED\n CONTRACT_TERMINATED\n CONTRACT_TRANSFERRED\n UNDERWRITING_COMPLETED\n OFFER_LETTER_GENERATED\n}\n\nenum ContractEventGroup {\n STATE_CHANGE\n PAYMENT\n DOCUMENT\n NOTIFICATION\n WORKFLOW\n}\n\nenum EventActorType {\n USER\n SYSTEM\n WEBHOOK\n ADMIN\n}\n\nenum RefundStatus {\n PENDING\n APPROVED\n REJECTED\n PROCESSING\n COMPLETED\n FAILED\n CANCELLED\n}\n\n// =============================================================================\n// EVENT-DRIVEN WORKFLOW ENUMS\n// =============================================================================\n\n/// Handler Type - What kind of action the handler performs\n/// These are business-friendly names that admins can understand\nenum EventHandlerType {\n SEND_EMAIL // Send an email notification to recipient(s)\n SEND_SMS // Send an SMS text message\n SEND_PUSH // Send a push notification\n CALL_WEBHOOK // Call an external API/webhook\n ADVANCE_WORKFLOW // Advance or complete a workflow step\n RUN_AUTOMATION // Execute internal business logic\n}\n\n/// Actor Type - Who triggered an event\nenum ActorType {\n USER\n API_KEY\n SYSTEM\n WEBHOOK\n}\n\n/// Workflow Event Status\nenum WorkflowEventStatus {\n PENDING\n PROCESSING\n COMPLETED\n FAILED\n SKIPPED\n}\n\n/// Handler Execution Status\nenum ExecutionStatus {\n PENDING\n RUNNING\n COMPLETED\n FAILED\n RETRYING\n SKIPPED\n}\n\n// =============================================================================\n// USER & AUTH DOMAIN\n// =============================================================================\n\nmodel User {\n id String @id @default(cuid())\n email String @unique\n password String?\n phone String? @unique\n firstName String?\n lastName String?\n isActive Boolean @default(true)\n isEmailVerified Boolean @default(false)\n googleId String?\n avatar String?\n tenantId String?\n tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: SetNull)\n // Support multiple roles via explicit join table `UserRole`\n userRoles UserRole[]\n walletId String? @unique\n wallet Wallet? @relation(fields: [walletId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n emailVerifiedAt DateTime?\n emailVerificationToken String?\n lastLoginAt DateTime?\n refreshTokens RefreshToken[]\n passwordResets PasswordReset[]\n suspensions UserSuspension[]\n emailPreferences EmailPreference[]\n deviceEndpoints DeviceEndpoint[]\n socials Social[]\n\n // Relations to other domains\n properties Property[]\n contracts Contract[] @relation(\"ContractBuyer\")\n soldContracts Contract[] @relation(\"ContractSeller\")\n contractPayments ContractPayment[] @relation(\"ContractPayer\")\n\n // Documentation step assignments and approvals\n assignedSteps DocumentationStep[] @relation(\"DocumentationStepAssignee\")\n stepApprovals DocumentationStepApproval[] @relation(\"DocumentationStepApprover\")\n uploadedDocs ContractDocument[] @relation(\"DocumentUploader\")\n\n // Payment method changes\n paymentMethodChangeRequests PaymentMethodChangeRequest[] @relation(\"ChangeRequestor\")\n reviewedChangeRequests PaymentMethodChangeRequest[] @relation(\"ChangeReviewer\")\n\n // Contract terminations\n initiatedTerminations ContractTermination[] @relation(\"TerminationInitiator\")\n reviewedTerminations ContractTermination[] @relation(\"TerminationReviewer\")\n\n // Offer letters\n offerLettersGenerated OfferLetter[] @relation(\"OfferLetterGenerator\")\n offerLettersSent OfferLetter[] @relation(\"OfferLetterSender\")\n\n // Property transfer requests\n transferRequestsSubmitted PropertyTransferRequest[] @relation(\"TransferRequestor\")\n transferRequestsReviewed PropertyTransferRequest[] @relation(\"TransferReviewer\")\n\n // Unified approval requests\n approvalRequestsSubmitted ApprovalRequest[] @relation(\"ApprovalRequestor\")\n approvalRequestsAssigned ApprovalRequest[] @relation(\"ApprovalAssignee\")\n approvalRequestsReviewed ApprovalRequest[] @relation(\"ApprovalReviewer\")\n\n // Contract refunds\n requestedRefunds ContractRefund[] @relation(\"RefundRequester\")\n approvedRefunds ContractRefund[] @relation(\"RefundApprover\")\n processedRefunds ContractRefund[] @relation(\"RefundProcessor\")\n\n @@index([email])\n @@index([tenantId])\n @@map(\"users\")\n}\n\nmodel Role {\n id String @id @default(cuid())\n name String @unique\n description String?\n userRoles UserRole[]\n permissions RolePermission[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@map(\"roles\")\n}\n\nmodel Permission {\n id String @id @default(cuid())\n name String @unique\n description String?\n resource String\n action String\n roles RolePermission[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([resource, action])\n @@index([resource])\n @@map(\"permissions\")\n}\n\nmodel RolePermission {\n roleId String\n permissionId String\n role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)\n permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n\n @@id([roleId, permissionId])\n @@map(\"role_permissions\")\n}\n\nmodel UserRole {\n userId String\n roleId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n\n @@id([userId, roleId])\n @@map(\"user_roles\")\n}\n\nmodel Tenant {\n id String @id @default(cuid())\n name String\n subdomain String @unique\n isActive Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Back-relations for multitenancy\n users User[]\n properties Property[]\n paymentPlans PaymentPlan[]\n paymentMethods PropertyPaymentMethod[]\n contracts Contract[]\n\n // Payment method changes\n paymentMethodChangeRequests PaymentMethodChangeRequest[]\n documentRequirementRules DocumentRequirementRule[]\n\n // Contract terminations\n contractTerminations ContractTermination[]\n\n // Offer letters and templates\n documentTemplates DocumentTemplate[]\n offerLetters OfferLetter[]\n\n // API keys for third-party integrations\n apiKeys ApiKey[]\n\n // Event-driven workflow\n eventChannels EventChannel[]\n eventTypes EventType[]\n eventHandlers EventHandler[]\n workflowEvents WorkflowEvent[]\n\n // Property transfer requests\n propertyTransferRequests PropertyTransferRequest[]\n\n // Unified approval requests\n approvalRequests ApprovalRequest[]\n\n // Contract refunds\n contractRefunds ContractRefund[]\n\n @@index([subdomain])\n @@map(\"tenants\")\n}\n\n// =============================================================================\n// API KEYS - Third-party integration credentials\n// =============================================================================\n// ApiKey enables partners/integrations to authenticate via token exchange.\n// \n// Flow:\n// 1. Admin creates API key for a partner (POST /api-keys)\n// 2. System generates secret, stores in Secrets Manager, returns id.secret ONCE\n// 3. Partner calls token endpoint with id.secret (POST /api-keys/:id/token)\n// 4. Token endpoint validates via Secrets Manager, returns short-lived JWT\n// 5. Partner uses JWT for API requests; authorizer validates + resolves scopes\n//\n// Security:\n// - Raw secret stored ONLY in AWS Secrets Manager (secretRef = ARN)\n// - Secret returned only once at creation; admin must rotate if lost\n// - Scopes define allowed operations (e.g., [\"contract:read\", \"payment:read\"])\n// - Short-lived JWTs (5-15 min) minimize exposure on key compromise\n// =============================================================================\n\nmodel ApiKey {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n // Identification\n name String // Human-readable name (e.g., \"Paystack Integration\")\n description String? @db.Text // Optional description\n provider String // Partner/vendor name (e.g., \"paystack\", \"flutterwave\")\n\n // Secret management (NEVER store raw secret in DB)\n secretRef String // AWS Secrets Manager ARN or name\n\n // Permissions - scopes this API key is allowed to request\n // Examples: [\"contract:read\", \"payment:*\", \"property:read\"]\n scopes Json // JSON array of scope strings\n\n // Lifecycle\n enabled Boolean @default(true)\n expiresAt DateTime? // Optional expiration date\n lastUsedAt DateTime? // Updated on each token exchange\n revokedAt DateTime? // Set when key is revoked\n revokedBy String? // User ID who revoked\n\n // Audit\n createdBy String? // User ID who created\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([tenantId])\n @@index([provider])\n @@index([enabled])\n @@map(\"api_keys\")\n}\n\nmodel RefreshToken {\n id String @id @default(cuid())\n // Use the JWT `jti` for indexed lookups and keep the raw JWT (optional)\n jti String? @unique @db.VarChar(255)\n token String? @db.LongText\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n expiresAt DateTime\n createdAt DateTime @default(now())\n\n @@index([userId])\n @@index([expiresAt])\n @@map(\"refresh_tokens\")\n}\n\nmodel PasswordReset {\n id String @id @default(cuid())\n token String @unique\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n expiresAt DateTime\n usedAt DateTime?\n createdAt DateTime @default(now())\n\n @@index([userId])\n @@index([expiresAt])\n @@map(\"password_resets\")\n}\n\nmodel UserSuspension {\n id String @id @default(cuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n reason String\n suspendedAt DateTime @default(now())\n expiresAt DateTime?\n liftedAt DateTime?\n\n @@index([userId])\n @@map(\"user_suspensions\")\n}\n\nmodel EmailPreference {\n id String @id @default(cuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n marketingEmails Boolean @default(true)\n transactionalEmails Boolean @default(true)\n propertyAlerts Boolean @default(true)\n paymentReminders Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@map(\"email_preferences\")\n}\n\nmodel DeviceEndpoint {\n id String @id @default(cuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n endpoint String // Push notification endpoint\n platform String // ios, android, web\n isActive Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@map(\"device_endpoints\")\n}\n\nmodel Social {\n id String @id @default(cuid())\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n provider String // google, facebook, twitter, etc\n socialId String // ID from the social provider\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([provider, socialId])\n @@index([userId])\n @@map(\"socials\")\n}\n\nmodel OAuthState {\n id String @id @default(cuid())\n state String @unique\n expiresAt DateTime\n createdAt DateTime @default(now())\n\n @@index([state])\n @@index([expiresAt])\n @@map(\"oauth_states\")\n}\n\nmodel Wallet {\n id String @id @default(cuid())\n balance Float @default(0)\n currency String @default(\"USD\")\n user User?\n transactions Transaction[]\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@map(\"wallets\")\n}\n\nmodel Transaction {\n id String @id @default(cuid())\n walletId String\n wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)\n amount Float\n type String // CREDIT, DEBIT\n status String // PENDING, COMPLETED, FAILED\n reference String?\n description String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([walletId])\n @@map(\"transactions\")\n}\n\nmodel Settings {\n id String @id @default(cuid())\n key String @unique\n value String @db.Text\n category String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([category])\n @@map(\"settings\")\n}\n\n// =============================================================================\n// PROPERTY DOMAIN\n// =============================================================================\n// Property = listing/project (e.g., \"Sunrise Estate\")\n// PropertyVariant = configuration with specs & price (e.g., \"3-Bed Corner - Finished\")\n// PropertyUnit = individual sellable unit (e.g., \"Unit A1\")\n// =============================================================================\n\nmodel Property {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n userId String\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n title String\n category String // SALE, RENT, LEASE\n propertyType String // APARTMENT, HOUSE, LAND, COMMERCIAL, ESTATE, TOWNHOUSE\n country String\n currency String // USD, NGN, etc\n city String\n district String?\n zipCode String?\n streetAddress String?\n longitude Float?\n latitude Float?\n status String @default(\"DRAFT\") // DRAFT, PUBLISHED, SOLD_OUT, ARCHIVED\n description String? @db.Text\n displayImageId String?\n displayImage PropertyMedia? @relation(\"DisplayImage\", fields: [displayImageId], references: [id], onDelete: SetNull)\n isPublished Boolean @default(false)\n publishedAt DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n documents PropertyDocument[]\n media PropertyMedia[] @relation(\"PropertyMedia\")\n amenities PropertyAmenity[] // Shared amenities (gym, pool, security)\n paymentMethods PropertyPaymentMethodLink[]\n variants PropertyVariant[]\n\n @@index([tenantId])\n @@index([userId])\n @@index([category])\n @@index([propertyType])\n @@index([city])\n @@index([status])\n @@map(\"properties\")\n}\n\nmodel PropertyMedia {\n id String @id @default(cuid())\n propertyId String\n property Property @relation(\"PropertyMedia\", fields: [propertyId], references: [id], onDelete: Cascade)\n url String\n type String // IMAGE, VIDEO\n caption String?\n order Int @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n displayForProperties Property[] @relation(\"DisplayImage\")\n\n @@index([propertyId])\n @@map(\"property_media\")\n}\n\nmodel PropertyDocument {\n id String @id @default(cuid())\n propertyId String\n property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)\n name String\n url String\n type String // TITLE_DEED, SURVEY_PLAN, etc\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([propertyId])\n @@map(\"property_documents\")\n}\n\nmodel Amenity {\n id String @id @default(cuid())\n name String @unique\n category String? // PROPERTY, VARIANT, BOTH - helps filter which amenities to show\n icon String? // Icon name/URL for UI\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n properties PropertyAmenity[]\n variants PropertyVariantAmenity[]\n\n @@index([category])\n @@map(\"amenities\")\n}\n\n// =============================================================================\n// PROPERTY VARIANT & UNIT MODELS\n// =============================================================================\n\n// PropertyVariant = specific configuration with its own price and amenities\n// e.g., \"3-Bedroom Corner Piece - Fully Finished\", \"2-Bedroom Standard - Carcass\"\nmodel PropertyVariant {\n id String @id @default(cuid())\n propertyId String\n property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)\n\n name String // \"Corner Piece - Finished\", \"Standard - Carcass\"\n description String? @db.Text\n\n // Specifications\n nBedrooms Int?\n nBathrooms Int?\n nParkingSpots Int?\n area Float? // Square meters/feet\n\n // Pricing\n price Float\n pricePerSqm Float? // Computed or set manually\n\n // Inventory counters (denormalized for performance, updated via triggers/service)\n totalUnits Int @default(1)\n availableUnits Int @default(1)\n reservedUnits Int @default(0)\n soldUnits Int @default(0)\n\n // Status\n status String @default(\"AVAILABLE\") // AVAILABLE, LOW_STOCK, SOLD_OUT, ARCHIVED\n isActive Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n amenities PropertyVariantAmenity[]\n units PropertyUnit[]\n media PropertyVariantMedia[]\n\n @@index([propertyId])\n @@index([status])\n @@index([price])\n @@map(\"property_variants\")\n}\n\n// PropertyVariantAmenity = amenities specific to a variant\n// e.g., \"Finished Kitchen\", \"Smart Home System\", \"Private Garden\"\nmodel PropertyVariantAmenity {\n variantId String\n amenityId String\n variant PropertyVariant @relation(fields: [variantId], references: [id], onDelete: Cascade)\n amenity Amenity @relation(fields: [amenityId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n\n @@id([variantId, amenityId])\n @@map(\"property_variant_amenities\")\n}\n\n// PropertyVariantMedia = images/videos specific to a variant\nmodel PropertyVariantMedia {\n id String @id @default(cuid())\n variantId String\n variant PropertyVariant @relation(fields: [variantId], references: [id], onDelete: Cascade)\n url String\n type String // IMAGE, VIDEO, FLOOR_PLAN, 3D_TOUR\n caption String?\n order Int @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([variantId])\n @@map(\"property_variant_media\")\n}\n\n// PropertyUnit = individual sellable/rentable unit within a variant\n// e.g., \"Unit A1\", \"Block B - Flat 3\", \"Plot 15\"\nmodel PropertyUnit {\n id String @id @default(cuid())\n variantId String\n variant PropertyVariant @relation(fields: [variantId], references: [id], onDelete: Cascade)\n\n unitNumber String // \"A1\", \"B-3\", \"Plot 15\"\n floorNumber Int? // For apartments\n blockName String? // \"Block A\", \"Tower 1\"\n\n // Unit-specific overrides (if different from variant)\n priceOverride Float? // If this specific unit has a different price\n areaOverride Float? // If this specific unit has a different area\n notes String? @db.Text // Internal notes about this unit\n\n // Status tracking\n status String @default(\"AVAILABLE\") // AVAILABLE, RESERVED, SOLD, RENTED, UNAVAILABLE\n\n // Reservation/hold\n reservedAt DateTime?\n reservedUntil DateTime?\n reservedById String?\n\n // Ownership tracking (once sold)\n ownerId String?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n contracts Contract[]\n\n // Transfer requests targeting this unit\n transferRequests PropertyTransferRequest[]\n\n @@unique([variantId, unitNumber])\n @@index([variantId])\n @@index([status])\n @@map(\"property_units\")\n}\n\nmodel PropertyAmenity {\n propertyId String\n amenityId String\n property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)\n amenity Amenity @relation(fields: [amenityId], references: [id], onDelete: Cascade)\n createdAt DateTime @default(now())\n\n @@id([propertyId, amenityId])\n @@map(\"property_amenities\")\n}\n\n// =============================================================================\n// PAYMENT PLAN DOMAIN - Reusable installment structure templates\n// =============================================================================\n\n// PaymentPlan = reusable structure for how payments are scheduled\n// Examples: \"Monthly360\" (360 monthly payments), \"Weekly52\", \"OneTime\"\nmodel PaymentPlan {\n id String @id @default(cuid())\n tenantId String? // NULL = global template available to all tenants\n tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n name String\n description String? @db.Text\n isActive Boolean @default(true)\n\n // Structure configuration\n paymentFrequency PaymentFrequency\n customFrequencyDays Int?\n numberOfInstallments Int // 1 for one-time, 360 for 30yr monthly, etc\n calculateInterestDaily Boolean @default(false)\n gracePeriodDays Int @default(0)\n\n // Fund collection behavior\n // true = we collect funds via wallet/gateway (e.g., downpayment)\n // false = external payment, we only track/reconcile (e.g., bank mortgage)\n collectFunds Boolean @default(true)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Used by property payment method phases (templates)\n methodPhases PropertyPaymentMethodPhase[]\n // Used by instantiated contract phases\n contractPhases ContractPhase[]\n\n @@unique([tenantId, name]) // Unique per tenant, or globally if tenantId is null\n @@index([tenantId])\n @@map(\"payment_plans\")\n}\n\n// =============================================================================\n// PROPERTY PAYMENT METHOD DOMAIN - Product offerings per property\n// =============================================================================\n\n// PropertyPaymentMethod = how a property can be purchased (e.g., \"Standard Mortgage\", \"Cash\", \"Rent-to-Own\")\nmodel PropertyPaymentMethod {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n name String // \"Standard Mortgage\", \"Flexible Payment\", \"Cash Purchase\"\n description String? @db.Text\n isActive Boolean @default(true)\n\n // Global method configuration\n allowEarlyPayoff Boolean @default(true)\n earlyPayoffPenaltyRate Float?\n autoActivatePhases Boolean @default(true)\n requiresManualApproval Boolean @default(false)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Many-to-many with properties\n properties PropertyPaymentMethodLink[]\n // Phases that make up this method (templates)\n phases PropertyPaymentMethodPhase[]\n // Contracts using this method\n contracts Contract[]\n\n // Payment method change tracking\n changeRequestsFrom PaymentMethodChangeRequest[] @relation(\"ChangeFromMethod\")\n changeRequestsTo PaymentMethodChangeRequest[] @relation(\"ChangeToMethod\")\n\n // Document requirement rules\n documentRules DocumentRequirementRule[] @relation(\"RulePaymentMethod\")\n changeRulesFrom DocumentRequirementRule[] @relation(\"RuleFromMethod\")\n changeRulesTo DocumentRequirementRule[] @relation(\"RuleToMethod\")\n\n @@unique([tenantId, name]) // Unique per tenant\n @@index([tenantId])\n @@map(\"property_payment_methods\")\n}\n\n// Many-to-many link between Property and PaymentMethod\nmodel PropertyPaymentMethodLink {\n propertyId String\n property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)\n paymentMethodId String\n paymentMethod PropertyPaymentMethod @relation(fields: [paymentMethodId], references: [id], onDelete: Cascade)\n\n // Method-specific overrides for this property\n isDefault Boolean @default(false)\n isActive Boolean @default(true)\n createdAt DateTime @default(now())\n\n @@id([propertyId, paymentMethodId])\n @@map(\"property_payment_method_links\")\n}\n\n// Phase template within a PropertyPaymentMethod (e.g., documentation, downpayment, mortgage)\n// phaseCategory determines the FSM type: DOCUMENTATION or PAYMENT\nmodel PropertyPaymentMethodPhase {\n id String @id @default(cuid())\n paymentMethodId String\n paymentMethod PropertyPaymentMethod @relation(fields: [paymentMethodId], references: [id], onDelete: Cascade)\n paymentPlanId String? // Only for PAYMENT phases\n paymentPlan PaymentPlan? @relation(fields: [paymentPlanId], references: [id])\n\n name String\n description String? @db.Text\n\n // Phase classification (DB-enforced enums)\n phaseCategory PhaseCategory\n phaseType PhaseType\n order Int\n\n // Financial configuration (for PAYMENT phases)\n interestRate Float?\n percentOfPrice Float? // e.g., 10.0 for 10% downpayment\n\n // Fund collection behavior (inherited from PaymentPlan if not set)\n // true = we collect funds via wallet/gateway (e.g., downpayment)\n // false = external payment, we only track/reconcile (e.g., bank mortgage)\n collectFunds Boolean? // null = inherit from PaymentPlan\n\n // Activation rules\n requiresPreviousPhaseCompletion Boolean @default(true)\n minimumCompletionPercentage Float?\n completionCriterion CompletionCriterion?\n\n // Snapshots for audit (original config at creation time)\n stepDefinitionsSnapshot Json?\n requiredDocumentSnapshot Json?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Normalized child tables (for DOCUMENTATION phases)\n steps PaymentMethodPhaseStep[]\n requiredDocuments PaymentMethodPhaseDocument[]\n\n @@index([paymentMethodId])\n @@index([paymentPlanId])\n @@index([phaseCategory])\n @@map(\"property_payment_method_phases\")\n}\n\n// Step template within a DOCUMENTATION phase\nmodel PaymentMethodPhaseStep {\n id String @id @default(cuid())\n phaseId String\n phase PropertyPaymentMethodPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)\n\n name String\n stepType StepType\n order Int\n\n metadata Json?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Event attachments - handlers that fire on step transitions\n eventAttachments StepEventAttachment[]\n\n @@index([phaseId])\n @@map(\"payment_method_phase_steps\")\n}\n\n/// Step Event Attachment - Links event handlers to step template triggers\n/// When a step transitions (complete, reject, etc.), attached handlers fire\nmodel StepEventAttachment {\n id String @id @default(cuid())\n stepId String\n step PaymentMethodPhaseStep @relation(fields: [stepId], references: [id], onDelete: Cascade)\n\n /// When this handler should fire\n trigger StepTrigger\n\n /// The event handler to execute\n handlerId String\n handler EventHandler @relation(fields: [handlerId], references: [id], onDelete: Cascade)\n\n /// Order of execution (lower = first)\n priority Int @default(100)\n\n /// Whether this attachment is active\n enabled Boolean @default(true)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([stepId, handlerId, trigger])\n @@index([stepId])\n @@index([handlerId])\n @@map(\"step_event_attachments\")\n}\n\n// Required document within a DOCUMENTATION phase\nmodel PaymentMethodPhaseDocument {\n id String @id @default(cuid())\n phaseId String\n phase PropertyPaymentMethodPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)\n\n documentType String\n isRequired Boolean @default(true)\n description String? @db.Text\n allowedMimeTypes String? // CSV: application/pdf,image/jpeg\n maxSizeBytes Int?\n\n metadata Json?\n createdAt DateTime @default(now())\n\n @@index([phaseId, documentType])\n @@map(\"payment_method_phase_documents\")\n}\n\n// =============================================================================\n// CONTRACT DOMAIN - Unified agreement model (replaces Mortgage, PurchasePlan, etc.)\n// =============================================================================\n// Contract is the canonical agreement. \"Mortgage\" is just a product configuration\n// that creates a Contract with specific phases (documentation, downpayment, long-term payment).\n// Phases can be DOCUMENTATION (FSM for approvals) or PAYMENT (PaymentPlan-driven installments).\n// =============================================================================\n\nmodel Contract {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n // Link to specific unit being purchased/rented\n propertyUnitId String\n propertyUnit PropertyUnit @relation(fields: [propertyUnitId], references: [id], onDelete: Cascade)\n buyerId String\n buyer User @relation(\"ContractBuyer\", fields: [buyerId], references: [id], onDelete: Cascade)\n sellerId String?\n seller User? @relation(\"ContractSeller\", fields: [sellerId], references: [id])\n paymentMethodId String? // PropertyPaymentMethod used to create this contract\n paymentMethod PropertyPaymentMethod? @relation(fields: [paymentMethodId], references: [id])\n\n // Contract identification\n contractNumber String @unique\n title String\n description String? @db.Text\n contractType String // Admin-defined: MORTGAGE, INSTALLMENT, RENT_TO_OWN, CASH, LEASE, etc.\n\n // Financial summary (computed from phases)\n totalAmount Float // Total contract value (from unit price or negotiated)\n downPayment Float @default(0)\n downPaymentPaid Float @default(0)\n principal Float? // Financed amount (if applicable)\n interestRate Float? // Overall interest rate (if applicable)\n termMonths Int? // Total term (if applicable)\n periodicPayment Float? // Computed periodic payment (if applicable)\n totalPaidToDate Float @default(0)\n totalInterestPaid Float @default(0)\n\n // Pre-approval and underwriting data (moved from prequalification)\n monthlyIncome Float? // Buyer's monthly income\n monthlyExpenses Float? // Buyer's monthly expenses\n preApprovalAnswers Json? // Questionnaire answers from PRE_APPROVAL step\n underwritingScore Float? // Aggregate score from underwriting evaluation\n debtToIncomeRatio Float? // Calculated DTI ratio\n\n // FSM state (DB-enforced enums)\n status ContractStatus @default(DRAFT)\n state ContractStatus @default(DRAFT) // FSM state for workflow\n currentPhaseId String?\n\n // Timing\n nextPaymentDueDate DateTime?\n lastReminderSentAt DateTime?\n startDate DateTime?\n endDate DateTime?\n signedAt DateTime?\n terminatedAt DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n phases ContractPhase[]\n documents ContractDocument[]\n payments ContractPayment[]\n terminations ContractTermination[]\n offerLetters OfferLetter[]\n\n // Payment method change requests for this contract\n paymentMethodChangeRequests PaymentMethodChangeRequest[]\n\n // Transfer tracking - when a contract is transferred to a different property\n transferredFromId String? @unique // Source contract if this was created via transfer\n transferredFrom Contract? @relation(\"ContractTransfer\", fields: [transferredFromId], references: [id])\n transferredTo Contract? @relation(\"ContractTransfer\")\n\n // Transfer requests where this contract is the source\n outgoingTransferRequests PropertyTransferRequest[] @relation(\"SourceContract\")\n // Transfer requests where this contract is the target (created after approval)\n incomingTransferRequests PropertyTransferRequest[] @relation(\"TargetContract\")\n\n // Audit trail\n events ContractEvent[]\n\n // Refund requests\n refunds ContractRefund[]\n\n @@index([tenantId])\n @@index([propertyUnitId])\n @@index([buyerId])\n @@index([sellerId])\n @@index([paymentMethodId])\n @@index([status])\n @@index([state])\n @@map(\"contracts\")\n}\n\n// =============================================================================\n// CONTRACT REFUNDS - Track refund requests for overpayments or cancellations\n// =============================================================================\nmodel ContractRefund {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n\n amount Float\n reason String @db.Text\n status RefundStatus @default(PENDING)\n\n // Who requested the refund\n requestedById String\n requestedBy User @relation(\"RefundRequester\", fields: [requestedById], references: [id])\n\n // Who approved/rejected the refund\n approvedById String?\n approvedBy User? @relation(\"RefundApprover\", fields: [approvedById], references: [id])\n\n // Who processed the refund (finance team)\n processedById String?\n processedBy User? @relation(\"RefundProcessor\", fields: [processedById], references: [id])\n\n // Refund payment details\n paymentMethod String? // BANK_TRANSFER, CHEQUE, MOBILE_MONEY, etc.\n referenceNumber String? // Bank/payment reference\n recipientName String?\n recipientAccount String?\n recipientBank String?\n\n // Timestamps\n requestedAt DateTime @default(now())\n approvedAt DateTime?\n rejectedAt DateTime?\n processedAt DateTime?\n\n // Additional notes\n approvalNotes String? @db.Text\n rejectionNotes String? @db.Text\n processingNotes String? @db.Text\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([contractId])\n @@index([status])\n @@index([tenantId])\n @@index([requestedById])\n @@map(\"contract_refunds\")\n}\n\n// Phase within a contract - can be DOCUMENTATION or PAYMENT type\n// Admin names phases freely (e.g., \"KYC Documents\", \"Downpayment\", \"Monthly Mortgage\")\nmodel ContractPhase {\n id String @id @default(cuid())\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n paymentPlanId String? // Only for PAYMENT phases\n paymentPlan PaymentPlan? @relation(fields: [paymentPlanId], references: [id])\n\n // Admin-defined naming\n name String\n description String? @db.Text\n\n // Phase classification (DB-enforced enums)\n phaseCategory PhaseCategory\n phaseType PhaseType\n order Int\n\n // FSM state for this phase (DB-enforced enum)\n status PhaseStatus @default(PENDING)\n\n // =========================================================================\n // WORKFLOW TRACKING - Current step pointer for UX and orchestration\n // =========================================================================\n // Canonical pointer to the step currently requiring attention.\n // Updated by service when: phase activates (→ first step), step completes (→ next),\n // step rejected (→ same step with NEEDS_RESUBMISSION), or phase completes (→ null).\n currentStepId String?\n currentStep DocumentationStep? @relation(\"CurrentStep\", fields: [currentStepId], references: [id])\n\n // Financial details (for PAYMENT phases)\n totalAmount Float?\n paidAmount Float @default(0)\n remainingAmount Float?\n interestRate Float?\n\n // Fund collection behavior (snapshotted from template at contract creation)\n // true = we collect funds via wallet/gateway (e.g., downpayment)\n // false = external payment, we only track/reconcile (e.g., bank mortgage)\n collectFunds Boolean @default(true)\n\n // Progress counters (for efficient activation checks)\n approvedDocumentsCount Int @default(0)\n requiredDocumentsCount Int @default(0)\n completedStepsCount Int @default(0)\n totalStepsCount Int @default(0)\n\n // Timing\n dueDate DateTime?\n startDate DateTime?\n endDate DateTime?\n activatedAt DateTime?\n completedAt DateTime?\n\n // Activation rules\n requiresPreviousPhaseCompletion Boolean @default(true)\n minimumCompletionPercentage Float?\n completionCriterion CompletionCriterion?\n\n // Snapshots for audit (effective config at contract creation)\n paymentPlanSnapshot Json?\n stepDefinitionsSnapshot Json?\n requiredDocumentSnapshot Json?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n installments ContractInstallment[]\n payments ContractPayment[]\n steps DocumentationStep[] // For DOCUMENTATION phases (FSM steps)\n\n @@index([contractId])\n @@index([paymentPlanId])\n @@index([phaseCategory])\n @@index([status])\n @@index([order])\n @@index([currentStepId])\n @@map(\"contract_phases\")\n}\n\n// =============================================================================\n// CONTRACT EVENTS - Audit trail for contract lifecycle\n// =============================================================================\n// Tracks all significant events in a contract's lifecycle for audit, compliance,\n// and debugging. Unlike DomainEvent (which is for inter-service communication),\n// ContractEvent is purely for historical tracking and state machine transitions.\n// =============================================================================\nmodel ContractEvent {\n id String @id @default(cuid())\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n\n // Event classification\n eventType ContractEventType\n eventGroup ContractEventGroup?\n\n // For state transitions (optional - only populated for CONTRACT_STATE_CHANGED events)\n fromState String?\n toState String?\n trigger String?\n\n // Event payload (all event-specific data)\n data Json?\n\n // Actor tracking\n actorId String?\n actorType EventActorType?\n\n // Timing\n occurredAt DateTime @default(now())\n\n @@index([contractId])\n @@index([eventType])\n @@index([eventGroup])\n @@index([occurredAt])\n @@map(\"contract_events\")\n}\n\n// Steps within a DOCUMENTATION phase (FSM for document collection/approval)\nmodel DocumentationStep {\n id String @id @default(cuid())\n phaseId String\n phase ContractPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)\n\n name String\n description String? @db.Text\n stepType StepType\n order Int\n\n status StepStatus @default(PENDING)\n\n // =========================================================================\n // USER ACTION TRACKING - For rejection/resubmission flows\n // =========================================================================\n // When status is NEEDS_RESUBMISSION or ACTION_REQUIRED, this explains why.\n // Populated from DocumentationStepApproval.comment on rejection.\n actionReason String? @db.Text\n\n // Number of times this step has been submitted (for tracking resubmissions)\n submissionCount Int @default(0)\n\n // Last submission timestamp (for tracking resubmission timing)\n lastSubmittedAt DateTime?\n\n // Configuration metadata (for GENERATE_DOCUMENT steps, etc.)\n metadata Json?\n\n // For PRE_APPROVAL steps: store questionnaire answers\n preApprovalAnswers Json?\n\n // For UNDERWRITING steps: store evaluation results\n underwritingScore Float?\n debtToIncomeRatio Float?\n underwritingDecision String? // APPROVED, CONDITIONAL, DECLINED\n underwritingNotes String? @db.Text\n\n // Assignment\n assigneeId String?\n assignee User? @relation(\"DocumentationStepAssignee\", fields: [assigneeId], references: [id])\n\n // Required document types for UPLOAD steps (normalized)\n requiredDocuments DocumentationStepDocument[]\n\n // Timing\n dueDate DateTime?\n completedAt DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n approvals DocumentationStepApproval[]\n currentForPhase ContractPhase[] @relation(\"CurrentStep\")\n\n @@index([phaseId])\n @@index([status])\n @@index([order])\n @@map(\"contract_phase_steps\")\n}\n\n// Required documents for a step (normalized from CSV)\nmodel DocumentationStepDocument {\n id String @id @default(cuid())\n stepId String\n step DocumentationStep @relation(fields: [stepId], references: [id], onDelete: Cascade)\n\n documentType String\n isRequired Boolean @default(true)\n\n createdAt DateTime @default(now())\n\n @@index([stepId, documentType])\n @@map(\"contract_phase_step_documents\")\n}\n\n// Approvals for documentation steps\nmodel DocumentationStepApproval {\n id String @id @default(cuid())\n stepId String\n step DocumentationStep @relation(fields: [stepId], references: [id], onDelete: Cascade)\n approverId String?\n approver User? @relation(\"DocumentationStepApprover\", fields: [approverId], references: [id])\n\n decision ApprovalDecision\n comment String? @db.Text\n decidedAt DateTime @default(now())\n\n createdAt DateTime @default(now())\n\n @@index([stepId])\n @@map(\"contract_phase_step_approvals\")\n}\n\n// Installments within a PAYMENT phase\nmodel ContractInstallment {\n id String @id @default(cuid())\n phaseId String\n phase ContractPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)\n\n installmentNumber Int\n\n amount Float\n principalAmount Float @default(0)\n interestAmount Float @default(0)\n\n dueDate DateTime\n status InstallmentStatus @default(PENDING)\n\n paidAmount Float @default(0)\n paidDate DateTime?\n\n lateFee Float @default(0)\n lateFeeWaived Boolean @default(false)\n gracePeriodDays Int @default(0)\n gracePeriodEndDate DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n payments ContractPayment[]\n\n @@index([phaseId])\n @@index([dueDate])\n @@index([status])\n @@map(\"contract_installments\")\n}\n\n// Unified payment record for contracts\nmodel ContractPayment {\n id String @id @default(cuid())\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n phaseId String?\n phase ContractPhase? @relation(fields: [phaseId], references: [id])\n installmentId String?\n installment ContractInstallment? @relation(fields: [installmentId], references: [id])\n payerId String?\n payer User? @relation(\"ContractPayer\", fields: [payerId], references: [id])\n\n amount Float\n principalAmount Float @default(0)\n interestAmount Float @default(0)\n lateFeeAmount Float @default(0)\n\n paymentMethod String // BANK_TRANSFER, CREDIT_CARD, WALLET, CASH, CHECK\n status PaymentStatus @default(INITIATED)\n\n reference String? @unique\n gatewayResponse String? @db.Text // JSON\n\n processedAt DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([contractId])\n @@index([phaseId])\n @@index([installmentId])\n @@index([payerId])\n @@index([status])\n @@index([reference])\n @@map(\"contract_payments\")\n}\n\n// Contract documents (owned by contract, linked to phases/steps as needed)\nmodel ContractDocument {\n id String @id @default(cuid())\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n phaseId String? // Optional link to specific phase\n stepId String? // Optional link to specific step\n\n name String\n url String\n type String // ID, BANK_STATEMENT, INCOME_PROOF, TITLE_DEED, SIGNATURE, etc.\n uploadedById String?\n uploadedBy User? @relation(\"DocumentUploader\", fields: [uploadedById], references: [id])\n\n status DocumentStatus @default(PENDING)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([contractId])\n @@index([phaseId])\n @@index([stepId])\n @@index([type])\n @@index([status])\n @@map(\"contract_documents\")\n}\n\n// =============================================================================\n// OFFER LETTERS - Provisional and Final offer documents\n// =============================================================================\n\nmodel DocumentTemplate {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n name String // \"Provisional Offer Letter\", \"Final Offer Letter\"\n code String // PROVISIONAL_OFFER, FINAL_OFFER\n description String?\n version Int @default(1)\n\n // Template content (Handlebars)\n htmlTemplate String @db.Text\n cssStyles String? @db.Text\n\n // Merge field definitions for UI\n mergeFields Json? // [{name, type, required, description}]\n\n isActive Boolean @default(true)\n isDefault Boolean @default(false)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n offerLetters OfferLetter[]\n\n @@unique([tenantId, code, version])\n @@index([tenantId])\n @@index([code])\n @@map(\"document_templates\")\n}\n\nmodel OfferLetter {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n\n // Template used (optional - documents-service may handle default selection)\n templateId String?\n template DocumentTemplate? @relation(fields: [templateId], references: [id])\n\n // Letter details\n letterNumber String @unique // OL-XXXXXX\n type OfferLetterType\n status OfferLetterStatus @default(DRAFT)\n\n // Generated document\n htmlContent String? @db.Text // Rendered HTML\n pdfUrl String? // S3 URL of generated PDF\n pdfKey String? // S3 key for deletion/access\n\n // Merge data used (snapshot for audit)\n mergeData Json? // All data merged into template\n\n // Signing workflow\n sentAt DateTime?\n viewedAt DateTime?\n signedAt DateTime?\n signatureIp String?\n signatureData Json? // {method, timestamp, metadata}\n\n // Validity\n expiresAt DateTime?\n expiredAt DateTime?\n cancelledAt DateTime?\n cancelReason String?\n\n // Audit\n generatedById String?\n generatedBy User? @relation(\"OfferLetterGenerator\", fields: [generatedById], references: [id])\n sentById String?\n sentBy User? @relation(\"OfferLetterSender\", fields: [sentById], references: [id])\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([tenantId])\n @@index([contractId])\n @@index([type])\n @@index([status])\n @@map(\"offer_letters\")\n}\n\n// =============================================================================\n// CONTRACT TERMINATION - Full lifecycle for cancellation/termination\n// =============================================================================\n// Tracks termination requests from initiation through refund completion.\n// Industry-standard flow:\n// 1. Request created (by buyer/seller/admin/system)\n// 2. Admin reviews (if required by policy)\n// 3. Financial settlement calculated (refunds, penalties, forfeitures)\n// 4. Refund processed (if applicable)\n// 5. Contract marked terminated, unit released\n// =============================================================================\n\nmodel ContractTermination {\n id String @id @default(cuid())\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n // Request identification\n requestNumber String @unique // TRM-XXXXXX\n\n // Who initiated and why\n initiatedBy TerminationInitiator\n initiatorId String? // userId if BUYER/SELLER/ADMIN\n initiator User? @relation(\"TerminationInitiator\", fields: [initiatorId], references: [id])\n type TerminationType\n reason String? @db.Text\n supportingDocs Json? // [{type, url, uploadedAt}]\n\n // Workflow status\n status TerminationStatus @default(REQUESTED)\n requiresApproval Boolean @default(true)\n autoApproveEligible Boolean @default(false) // Pre-signature, no payments\n\n // Admin review\n reviewedBy String?\n reviewer User? @relation(\"TerminationReviewer\", fields: [reviewedBy], references: [id])\n reviewedAt DateTime?\n reviewNotes String? @db.Text\n rejectionReason String? @db.Text\n\n // Financial snapshot at time of request\n contractSnapshot Json // Full contract state snapshot\n totalContractAmount Float\n totalPaidToDate Float\n outstandingBalance Float\n\n // Settlement calculation\n refundableAmount Float @default(0) // Amount eligible for refund\n penaltyAmount Float @default(0) // Penalties/fees to deduct\n forfeitedAmount Float @default(0) // Amount forfeited (non-refundable deposits)\n adminFeeAmount Float @default(0) // Processing fees\n netRefundAmount Float @default(0) // refundableAmount - penaltyAmount - adminFeeAmount\n settlementNotes String? @db.Text\n\n // Refund processing\n refundStatus RefundStatus @default(PENDING)\n refundReference String? // Payment gateway reference\n refundMethod String? // ORIGINAL_METHOD, BANK_TRANSFER, CHECK, WALLET\n refundAccountDetails Json? // Encrypted bank details if needed\n refundInitiatedAt DateTime?\n refundCompletedAt DateTime?\n refundFailureReason String? @db.Text\n\n // Property unit handling\n unitReleasedAt DateTime?\n unitReservedForId String? // If unit being held for another buyer\n\n // Timing\n requestedAt DateTime @default(now())\n approvedAt DateTime?\n executedAt DateTime?\n completedAt DateTime?\n cancelledAt DateTime?\n\n // Idempotency and audit\n idempotencyKey String? @unique\n metadata Json?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([contractId])\n @@index([tenantId])\n @@index([status])\n @@index([type])\n @@index([initiatorId])\n @@index([requestedAt])\n @@map(\"contract_terminations\")\n}\n\n// =============================================================================\n// PAYMENT METHOD CHANGE REQUEST - Mid-contract payment method changes\n// =============================================================================\n// When a user wants to change their payment method after contract creation,\n// this aggregate tracks the request, required documentation, approvals, and\n// final execution. Different from-to combinations may require different docs.\n// =============================================================================\n\nenum PaymentMethodChangeStatus {\n PENDING_DOCUMENTS\n DOCUMENTS_SUBMITTED\n UNDER_REVIEW\n APPROVED\n REJECTED\n EXECUTED\n CANCELLED\n}\n\nmodel PaymentMethodChangeRequest {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n contractId String\n contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)\n\n // The change being requested\n fromPaymentMethodId String\n fromPaymentMethod PropertyPaymentMethod @relation(\"ChangeFromMethod\", fields: [fromPaymentMethodId], references: [id])\n toPaymentMethodId String\n toPaymentMethod PropertyPaymentMethod @relation(\"ChangeToMethod\", fields: [toPaymentMethodId], references: [id])\n\n // Who requested and why\n requestorId String\n requestor User @relation(\"ChangeRequestor\", fields: [requestorId], references: [id])\n reason String? @db.Text\n\n // Documentation requirements (determined by DocumentRequirementRule)\n requiredDocumentTypes String? // CSV: BANK_STATEMENT,INCOME_PROOF,NEW_EMPLOYER_LETTER\n submittedDocuments Json? // [{type, s3Key, uploadedAt, status}]\n\n // Financial impact assessment\n currentOutstanding Float? // Outstanding balance at time of request\n newTermMonths Int? // New term if applicable\n newInterestRate Float? // New rate if applicable\n newMonthlyPayment Float? // Projected new payment\n penaltyAmount Float? // Early change penalty if applicable\n financialImpactNotes String? @db.Text\n\n // Status and workflow\n status PaymentMethodChangeStatus @default(PENDING_DOCUMENTS)\n reviewerId String?\n reviewer User? @relation(\"ChangeReviewer\", fields: [reviewerId], references: [id])\n reviewNotes String? @db.Text\n reviewedAt DateTime?\n\n // Execution details\n executedAt DateTime?\n previousPhaseData Json? // Snapshot of phases before change\n newPhaseData Json? // New phases created after change\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([tenantId])\n @@index([contractId])\n @@index([status])\n @@index([requestorId])\n @@map(\"payment_method_change_requests\")\n}\n\n// =============================================================================\n// DOCUMENT REQUIREMENT RULES - Configurable document requirements\n// =============================================================================\n// Admins can configure which documents are required for specific scenarios:\n// - Prequalification for a payment method type\n// - Contract phases\n// - Payment method changes (from-to combinations)\n// This allows tenants to customize documentation workflows per product.\n// =============================================================================\n\nenum DocumentRequirementContext {\n CONTRACT_PHASE // During a contract phase\n PAYMENT_METHOD_CHANGE // When changing payment method mid-contract\n}\n\nmodel DocumentRequirementRule {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n // Rule context\n context DocumentRequirementContext\n\n // Scoping (which situations this rule applies to)\n // For PREQUALIFICATION: paymentMethodId\n // For CONTRACT_PHASE: phaseType\n // For PAYMENT_METHOD_CHANGE: fromMethodId + toMethodId\n paymentMethodId String?\n paymentMethod PropertyPaymentMethod? @relation(\"RulePaymentMethod\", fields: [paymentMethodId], references: [id])\n phaseType String? // KYC, VERIFICATION, DOWNPAYMENT, etc.\n fromPaymentMethodId String?\n fromPaymentMethod PropertyPaymentMethod? @relation(\"RuleFromMethod\", fields: [fromPaymentMethodId], references: [id])\n toPaymentMethodId String?\n toPaymentMethod PropertyPaymentMethod? @relation(\"RuleToMethod\", fields: [toPaymentMethodId], references: [id])\n\n // Document requirements\n documentType String // ID_CARD, PASSPORT, BANK_STATEMENT, INCOME_PROOF, etc.\n isRequired Boolean @default(true)\n description String? // Instructions for the user\n maxSizeBytes Int? // Max file size allowed\n allowedMimeTypes String? // CSV: application/pdf,image/jpeg,image/png\n\n // Validation rules\n expiryDays Int? // Document must not be older than X days\n requiresManualReview Boolean @default(false)\n\n isActive Boolean @default(true)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([tenantId])\n @@index([context])\n @@index([paymentMethodId])\n @@index([phaseType])\n @@index([fromPaymentMethodId, toPaymentMethodId])\n @@map(\"document_requirement_rules\")\n}\n\n// =============================================================================\n// EVENT-DRIVEN WORKFLOW CONFIGURATION\n// =============================================================================\n// This system allows admins to configure event channels, types, and handlers\n// for a flexible, configurable event-driven workflow system.\n//\n// Architecture:\n// 1. EventChannel - Logical grouping of events (e.g., \"contracts\", \"payments\")\n// 2. EventType - Specific event types (e.g., \"DOCUMENT_UPLOADED\", \"STEP_COMPLETED\")\n// 3. EventHandler - What to do when an event fires (webhook, internal call, etc.)\n// 4. WorkflowEvent - Actual event instances (audit log)\n// 5. EventHandlerExecution - Log of handler executions\n// =============================================================================\n\n/// Event Channel - A logical grouping of events\n/// Channels help organize events and route them to appropriate handlers\nmodel EventChannel {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n /// Unique code for the channel (e.g., \"CONTRACTS\", \"PAYMENTS\")\n code String\n /// Human-readable name\n name String\n /// Description of what this channel handles\n description String? @db.Text\n\n /// Whether this channel is active\n enabled Boolean @default(true)\n\n /// Event types that belong to this channel\n eventTypes EventType[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([tenantId, code])\n @@index([tenantId])\n @@map(\"event_channels\")\n}\n\n/// Event Type - Defines a type of event that can occur\n/// Each event type belongs to a channel and can have multiple handlers\nmodel EventType {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n /// The channel this event type belongs to\n channelId String\n channel EventChannel @relation(fields: [channelId], references: [id], onDelete: Cascade)\n\n /// Unique code for this event type (e.g., \"DOCUMENT_UPLOADED\")\n code String\n /// Human-readable name\n name String\n /// Description of when this event fires\n description String? @db.Text\n\n /// JSON schema for event payload validation (optional)\n payloadSchema Json?\n\n /// Whether this event type is active\n enabled Boolean @default(true)\n\n /// Handlers subscribed to this event type\n handlers EventHandler[]\n\n /// Actual event instances of this type\n events WorkflowEvent[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([tenantId, code])\n @@unique([channelId, code])\n @@index([tenantId])\n @@index([channelId])\n @@map(\"event_types\")\n}\n\n/// Event Handler - Defines what should happen when an event fires\n/// Handlers can be internal (call a service), external (webhook), or workflow triggers\nmodel EventHandler {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n /// The event type this handler responds to\n eventTypeId String\n eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)\n\n /// Human-readable name\n name String\n /// Description of what this handler does\n description String? @db.Text\n\n /// Handler type determines how the event is processed\n handlerType EventHandlerType\n\n /// Configuration for the handler (JSON, depends on handlerType)\n /// INTERNAL: { \"service\": \"contract\", \"method\": \"completeStep\" }\n /// WEBHOOK: { \"url\": \"https://...\", \"method\": \"POST\", \"headers\": {...} }\n /// WORKFLOW: { \"workflowId\": \"...\", \"action\": \"advance\" }\n /// NOTIFICATION: { \"template\": \"...\", \"channels\": [\"email\", \"sms\"] }\n config Json\n\n /// Order of execution when multiple handlers exist (lower = first)\n priority Int @default(100)\n\n /// Whether this handler is active\n enabled Boolean @default(true)\n\n /// Retry configuration\n maxRetries Int @default(3)\n retryDelayMs Int @default(1000)\n\n /// Filter condition (JSONPath expression) to conditionally run\n /// e.g., \"$.payload.status == 'approved'\"\n filterCondition String? @db.Text\n\n /// Handler execution logs\n executions EventHandlerExecution[]\n\n /// Step attachments - steps that have attached this handler\n stepAttachments StepEventAttachment[]\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([tenantId])\n @@index([eventTypeId])\n @@index([handlerType])\n @@map(\"event_handlers\")\n}\n\n/// Workflow Event - An actual event instance that occurred\n/// This is the audit log of all events in the system\nmodel WorkflowEvent {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n /// The type of this event\n eventTypeId String\n eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)\n\n /// The event payload (actual data)\n payload Json\n\n /// Optional correlation ID to link related events\n correlationId String?\n\n /// Optional causation ID (which event caused this one)\n causationId String?\n\n /// Source of the event (service name, user action, etc.)\n source String\n\n /// Actor who triggered the event (user ID, API key ID, \"system\")\n actorId String?\n actorType ActorType @default(SYSTEM)\n\n /// Event status\n status WorkflowEventStatus @default(PENDING)\n\n /// Error message if processing failed\n error String? @db.Text\n\n /// When the event was processed\n processedAt DateTime?\n\n /// Handler executions for this event\n executions EventHandlerExecution[]\n\n createdAt DateTime @default(now())\n\n @@index([tenantId])\n @@index([eventTypeId])\n @@index([correlationId])\n @@index([causationId])\n @@index([status])\n @@index([createdAt])\n @@map(\"workflow_events\")\n}\n\n/// Event Handler Execution - Log of a handler processing an event\nmodel EventHandlerExecution {\n id String @id @default(cuid())\n\n /// The event being processed\n eventId String\n event WorkflowEvent @relation(fields: [eventId], references: [id], onDelete: Cascade)\n\n /// The handler that processed this event\n handlerId String\n handler EventHandler @relation(fields: [handlerId], references: [id], onDelete: Cascade)\n\n /// Execution status\n status ExecutionStatus @default(PENDING)\n\n /// Attempt number (1 for first try, increments on retry)\n attempt Int @default(1)\n\n /// Input to the handler (may be transformed payload)\n input Json?\n\n /// Output from the handler\n output Json?\n\n /// Error details if failed\n error String? @db.Text\n errorCode String?\n\n /// Timing\n startedAt DateTime?\n completedAt DateTime?\n durationMs Int?\n\n createdAt DateTime @default(now())\n\n @@index([eventId])\n @@index([handlerId])\n @@index([status])\n @@map(\"event_handler_executions\")\n}\n\n// =============================================================================\n// EVENT OUTBOX - For guaranteed event delivery to SQS queues\n// =============================================================================\n\nmodel DomainEvent {\n id String @id @default(cuid())\n\n // Event identification\n eventType String // MORTGAGE.CREATED, PHASE.ACTIVATED, PAYMENT.COMPLETED, etc\n aggregateType String // Mortgage, MortgagePhase, MortgagePayment, Property, etc\n aggregateId String\n\n // Routing - which queue(s) should receive this\n queueName String // notifications, payments, mortgage-steps, accounting, etc\n\n // Event payload (all data needed by consumers)\n payload String @db.Text // JSON\n\n // Metadata\n occurredAt DateTime @default(now())\n actorId String? // User who triggered the event\n actorRole String? // Role of the actor\n\n // Processing status\n status String @default(\"PENDING\") // PENDING, PROCESSING, SENT, FAILED\n processedAt DateTime?\n sentAt DateTime?\n failureCount Int @default(0)\n lastError String? @db.Text\n nextRetryAt DateTime?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([status, nextRetryAt])\n @@index([eventType])\n @@index([aggregateType, aggregateId])\n @@index([queueName])\n @@index([occurredAt])\n @@map(\"domain_events\")\n}\n\n// =============================================================================\n// Property Transfer Request\n// =============================================================================\n// Allows a buyer to request transferring their contract to a different property\n// while preserving payments, completed workflow steps, and progress.\n// =============================================================================\n\nmodel PropertyTransferRequest {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n // Source contract being transferred\n sourceContractId String\n sourceContract Contract @relation(\"SourceContract\", fields: [sourceContractId], references: [id], onDelete: Cascade)\n\n // Target property unit\n targetPropertyUnitId String\n targetPropertyUnit PropertyUnit @relation(fields: [targetPropertyUnitId], references: [id])\n\n // Requestor (buyer) and reviewer (admin)\n requestedById String\n requestedBy User @relation(\"TransferRequestor\", fields: [requestedById], references: [id])\n reviewedById String?\n reviewedBy User? @relation(\"TransferReviewer\", fields: [reviewedById], references: [id])\n\n // Status and workflow\n status TransferRequestStatus @default(PENDING)\n\n // Request details\n reason String? @db.Text // Buyer's reason for transfer\n\n // Review details\n reviewNotes String? @db.Text // Admin notes on decision\n priceAdjustmentHandling String? // How to handle price difference: ADD_TO_MORTGAGE, REQUIRE_PAYMENT, CREDIT_BUYER\n\n // Computed values\n sourceTotalAmount Float? // Original contract total\n targetTotalAmount Float? // New contract total (based on target property)\n priceAdjustment Float? // Difference (positive = buyer owes more)\n paymentsMigrated Int? // Number of payments migrated\n\n // Result - new contract created after approval\n targetContractId String?\n targetContract Contract? @relation(\"TargetContract\", fields: [targetContractId], references: [id])\n\n // Timestamps\n createdAt DateTime @default(now())\n reviewedAt DateTime?\n completedAt DateTime?\n updatedAt DateTime @updatedAt\n\n @@index([tenantId])\n @@index([sourceContractId])\n @@index([targetPropertyUnitId])\n @@index([requestedById])\n @@index([status])\n @@map(\"property_transfer_requests\")\n}\n\n// =============================================================================\n// UNIFIED APPROVAL REQUESTS\n// =============================================================================\n\nenum ApprovalRequestType {\n PROPERTY_TRANSFER // Property unit transfer between contracts\n PROPERTY_UPDATE // Property/unit listing update requiring approval\n USER_WORKFLOW // User workflow step approval\n CREDIT_CHECK // Credit check result review\n CONTRACT_TERMINATION // Contract termination approval\n REFUND_APPROVAL // Refund request approval\n}\n\nenum ApprovalRequestStatus {\n PENDING // Awaiting review\n IN_REVIEW // Assigned to reviewer\n APPROVED // Approved by reviewer\n REJECTED // Rejected by reviewer\n CANCELLED // Cancelled by requestor\n EXPIRED // Auto-expired (if TTL configured)\n}\n\nenum ApprovalRequestPriority {\n LOW\n NORMAL\n HIGH\n URGENT\n}\n\n// Polymorphic approval request model for unified admin dashboard\nmodel ApprovalRequest {\n id String @id @default(cuid())\n tenantId String\n tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)\n\n // Request type and status\n type ApprovalRequestType\n status ApprovalRequestStatus @default(PENDING)\n priority ApprovalRequestPriority @default(NORMAL)\n\n // Polymorphic reference to the entity requiring approval\n entityType String // e.g., \"PropertyTransferRequest\", \"PropertyUnit\", \"User\"\n entityId String // ID of the referenced entity\n\n // Request metadata\n title String @db.VarChar(255) // Human-readable title for the request\n description String? @db.Text // Detailed description\n\n // Payload for any additional context (JSON)\n payload Json? // Flexible data storage for type-specific details\n\n // Requestor - who created the request\n requestedById String\n requestedBy User @relation(\"ApprovalRequestor\", fields: [requestedById], references: [id])\n\n // Assignee - admin/reviewer assigned to handle this request\n assigneeId String?\n assignee User? @relation(\"ApprovalAssignee\", fields: [assigneeId], references: [id])\n\n // Reviewer - who made the final decision (may differ from assignee)\n reviewedById String?\n reviewedBy User? @relation(\"ApprovalReviewer\", fields: [reviewedById], references: [id])\n\n // Review details\n reviewNotes String? @db.Text // Reviewer's notes/comments\n decision ApprovalDecision? // APPROVED, REJECTED, REQUEST_CHANGES\n\n // Expiration\n expiresAt DateTime? // Optional TTL for auto-expiration\n\n // Timestamps\n createdAt DateTime @default(now())\n assignedAt DateTime? // When assigned to reviewer\n reviewedAt DateTime? // When decision was made\n completedAt DateTime? // When fully processed\n updatedAt DateTime @updatedAt\n\n @@index([tenantId])\n @@index([type])\n @@index([status])\n @@index([priority])\n @@index([entityType, entityId])\n @@index([requestedById])\n @@index([assigneeId])\n @@index([createdAt])\n @@index([type, status]) // Efficient queries for approval dashboard\n @@map(\"approval_requests\")\n}\n",
|
|
19
19
|
"runtimeDataModel": {
|
|
20
20
|
"models": {},
|
|
21
21
|
"enums": {},
|
package/package.json
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valentine-efagene/qshelter-common",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.83",
|
|
4
4
|
"description": "Shared database schemas and utilities for QShelter services",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"scripts": {
|
|
9
|
-
"build": "tsc",
|
|
10
|
-
"dev": "tsc --watch",
|
|
11
|
-
"generate:prisma": "prisma generate && node scripts/generate-models-index.mjs",
|
|
12
|
-
"postgenerate": "node scripts/generate-models-index.mjs",
|
|
13
|
-
"migrate:dev": "prisma migrate dev",
|
|
14
|
-
"patch": "npm version patch && npm run build && npm publish --access public",
|
|
15
|
-
"prepublishOnly": "npm run build",
|
|
16
|
-
"publish:public": "npm publish --access public"
|
|
17
|
-
},
|
|
18
8
|
"keywords": [
|
|
19
9
|
"qshelter",
|
|
20
10
|
"common",
|
|
@@ -57,5 +47,14 @@
|
|
|
57
47
|
"@types/node": "^25.0.3",
|
|
58
48
|
"typescript": "^5.7.3",
|
|
59
49
|
"zod": "^4.0.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc",
|
|
53
|
+
"dev": "tsc --watch",
|
|
54
|
+
"generate:prisma": "prisma generate && node scripts/generate-models-index.mjs",
|
|
55
|
+
"postgenerate": "node scripts/generate-models-index.mjs",
|
|
56
|
+
"migrate:dev": "prisma migrate dev",
|
|
57
|
+
"patch": "npm version patch && npm run build && npm publish --access public",
|
|
58
|
+
"publish:public": "npm publish --access public"
|
|
60
59
|
}
|
|
61
|
-
}
|
|
60
|
+
}
|
package/prisma/schema.prisma
CHANGED