@valentine-efagene/qshelter-common 2.0.67 → 2.0.69

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.
@@ -165,6 +165,7 @@ export declare const EventHandlerType: {
165
165
  readonly WEBHOOK: "WEBHOOK";
166
166
  readonly WORKFLOW: "WORKFLOW";
167
167
  readonly NOTIFICATION: "NOTIFICATION";
168
+ readonly SNS: "SNS";
168
169
  readonly SCRIPT: "SCRIPT";
169
170
  };
170
171
  export type EventHandlerType = (typeof EventHandlerType)[keyof typeof EventHandlerType];
@@ -156,6 +156,7 @@ export const EventHandlerType = {
156
156
  WEBHOOK: 'WEBHOOK',
157
157
  WORKFLOW: 'WORKFLOW',
158
158
  NOTIFICATION: 'NOTIFICATION',
159
+ SNS: 'SNS',
159
160
  SCRIPT: 'SCRIPT'
160
161
  };
161
162
  export const ActorType = {
@@ -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}\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\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 RefundStatus {\n NOT_APPLICABLE // No refund needed (no payments made)\n PENDING // Refund not yet initiated\n INITIATED // Refund request sent to payment gateway\n PROCESSING // Gateway processing refund\n PARTIAL_COMPLETED // Some refund completed (penalties deducted)\n COMPLETED // Full refund completed\n FAILED // Refund failed (needs manual intervention)\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\n// =============================================================================\n// EVENT-DRIVEN WORKFLOW ENUMS\n// =============================================================================\n\n/// Handler Type - What kind of action the handler performs\nenum EventHandlerType {\n INTERNAL // Call an internal service method\n WEBHOOK // Send a webhook to an external URL\n WORKFLOW // Trigger or advance a workflow\n NOTIFICATION // Send a notification (email, SMS, push)\n SCRIPT // Execute a custom script/expression\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 @@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 @@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 @@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 @@index([phaseId])\n @@map(\"payment_method_phase_steps\")\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 events ContractEvent[]\n terminations ContractTermination[]\n offerLetters OfferLetter[]\n\n // Payment method change requests for this contract\n paymentMethodChangeRequests PaymentMethodChangeRequest[]\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// 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// 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// Contract domain events (FSM transitions, payments, documents, etc.)\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 String // STATE.TRANSITION, PHASE.ACTIVATED, PAYMENT.COMPLETED, etc.\n eventGroup String? // STATE_CHANGE, PAYMENT, DOCUMENT, NOTIFICATION (for filtering)\n\n // For state transitions (optional - only populated for STATE.TRANSITION 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 String? // USER, SYSTEM, WEBHOOK\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// =============================================================================\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(NOT_APPLICABLE)\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 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",
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}\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\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 RefundStatus {\n NOT_APPLICABLE // No refund needed (no payments made)\n PENDING // Refund not yet initiated\n INITIATED // Refund request sent to payment gateway\n PROCESSING // Gateway processing refund\n PARTIAL_COMPLETED // Some refund completed (penalties deducted)\n COMPLETED // Full refund completed\n FAILED // Refund failed (needs manual intervention)\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\n// =============================================================================\n// EVENT-DRIVEN WORKFLOW ENUMS\n// =============================================================================\n\n/// Handler Type - What kind of action the handler performs\nenum EventHandlerType {\n INTERNAL // Call an internal service method\n WEBHOOK // Send a webhook to an external URL\n WORKFLOW // Trigger or advance a workflow\n NOTIFICATION // Send a notification (email, SMS, push) - internal\n SNS // Publish to SNS topic (for notification-service)\n SCRIPT // Execute a custom script/expression\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 @@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 @@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 @@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 @@index([phaseId])\n @@map(\"payment_method_phase_steps\")\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 events ContractEvent[]\n terminations ContractTermination[]\n offerLetters OfferLetter[]\n\n // Payment method change requests for this contract\n paymentMethodChangeRequests PaymentMethodChangeRequest[]\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// 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// 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// Contract domain events (FSM transitions, payments, documents, etc.)\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 String // STATE.TRANSITION, PHASE.ACTIVATED, PAYMENT.COMPLETED, etc.\n eventGroup String? // STATE_CHANGE, PAYMENT, DOCUMENT, NOTIFICATION (for filtering)\n\n // For state transitions (optional - only populated for STATE.TRANSITION 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 String? // USER, SYSTEM, WEBHOOK\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// =============================================================================\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(NOT_APPLICABLE)\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 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",
19
19
  "runtimeDataModel": {
20
20
  "models": {},
21
21
  "enums": {},
@@ -29,6 +29,8 @@ export declare enum NotificationType {
29
29
  UNDERWRITING_APPROVED = "underwritingApproved",
30
30
  UNDERWRITING_REJECTED = "underwritingRejected",
31
31
  UNDERWRITING_CONDITIONAL = "underwritingConditional",
32
+ DOCUMENT_APPROVED = "documentApproved",
33
+ DOCUMENT_REJECTED = "documentRejected",
32
34
  ADMIN_CONTRIBUTION_RECEIVED = "adminContributionReceived",
33
35
  ADMIN_PROPERTY_ALLOCATION = "adminPropertyAllocation",
34
36
  ADMIN_INVITE = "adminInviteAdmin",
@@ -38,6 +38,9 @@ export var NotificationType;
38
38
  NotificationType["UNDERWRITING_APPROVED"] = "underwritingApproved";
39
39
  NotificationType["UNDERWRITING_REJECTED"] = "underwritingRejected";
40
40
  NotificationType["UNDERWRITING_CONDITIONAL"] = "underwritingConditional";
41
+ // Documents
42
+ NotificationType["DOCUMENT_APPROVED"] = "documentApproved";
43
+ NotificationType["DOCUMENT_REJECTED"] = "documentRejected";
41
44
  // Admin
42
45
  NotificationType["ADMIN_CONTRIBUTION_RECEIVED"] = "adminContributionReceived";
43
46
  NotificationType["ADMIN_PROPERTY_ALLOCATION"] = "adminPropertyAllocation";
@@ -33,6 +33,7 @@
33
33
  */
34
34
  import { PrismaClient } from '../../generated/client/client';
35
35
  import type { EmitEventInput, WorkflowEventData, ProcessEventResult } from './workflow-types';
36
+ import { EventPublisher } from './event-publisher';
36
37
  /**
37
38
  * Service registry interface for internal handlers
38
39
  */
@@ -43,7 +44,8 @@ export interface ServiceRegistry {
43
44
  export declare class WorkflowEventService {
44
45
  private prisma;
45
46
  private serviceRegistry;
46
- constructor(prisma: PrismaClient, serviceRegistry?: ServiceRegistry);
47
+ private eventPublisher;
48
+ constructor(prisma: PrismaClient, serviceRegistry?: ServiceRegistry, eventPublisher?: EventPublisher);
47
49
  /**
48
50
  * Register a service for internal event handlers
49
51
  *
@@ -166,6 +168,20 @@ export declare class WorkflowEventService {
166
168
  * Returns what would be sent for logging purposes.
167
169
  */
168
170
  private executeNotificationHandler;
171
+ /**
172
+ * Execute an SNS handler
173
+ *
174
+ * Publishes to SNS topic using the EventPublisher, which triggers
175
+ * the notification-service via SNS->SQS subscription.
176
+ *
177
+ * The config specifies the notification type, channel, and how to
178
+ * map the event payload to the notification payload.
179
+ */
180
+ private executeSnsHandler;
181
+ /**
182
+ * Build the notification payload for SNS from config and event payload
183
+ */
184
+ private buildSnsPayload;
169
185
  /**
170
186
  * Evaluate a filter condition against the payload
171
187
  */
@@ -31,6 +31,8 @@
31
31
  * const result = await eventService.processEvent(event.id);
32
32
  * ```
33
33
  */
34
+ import { EventPublisher } from './event-publisher';
35
+ import { NotificationType, NotificationChannel } from './notification-enums';
34
36
  /**
35
37
  * Simple in-memory service registry
36
38
  */
@@ -46,9 +48,11 @@ class InMemoryServiceRegistry {
46
48
  export class WorkflowEventService {
47
49
  prisma;
48
50
  serviceRegistry;
49
- constructor(prisma, serviceRegistry) {
51
+ eventPublisher;
52
+ constructor(prisma, serviceRegistry, eventPublisher) {
50
53
  this.prisma = prisma;
51
54
  this.serviceRegistry = serviceRegistry || new InMemoryServiceRegistry();
55
+ this.eventPublisher = eventPublisher || new EventPublisher('workflow-event-service');
52
56
  }
53
57
  /**
54
58
  * Register a service for internal event handlers
@@ -357,6 +361,8 @@ export class WorkflowEventService {
357
361
  return this.executeWorkflowHandler(config, payload, tenantId);
358
362
  case 'NOTIFICATION':
359
363
  return this.executeNotificationHandler(config, payload, tenantId);
364
+ case 'SNS':
365
+ return this.executeSnsHandler(config, payload, tenantId);
360
366
  case 'SCRIPT':
361
367
  // TODO: Implement script execution (sandboxed)
362
368
  throw new Error('Script handlers not yet implemented');
@@ -457,6 +463,72 @@ export class WorkflowEventService {
457
463
  tenantId,
458
464
  };
459
465
  }
466
+ /**
467
+ * Execute an SNS handler
468
+ *
469
+ * Publishes to SNS topic using the EventPublisher, which triggers
470
+ * the notification-service via SNS->SQS subscription.
471
+ *
472
+ * The config specifies the notification type, channel, and how to
473
+ * map the event payload to the notification payload.
474
+ */
475
+ async executeSnsHandler(config, payload, tenantId) {
476
+ // Build the notification payload from config
477
+ const notificationPayload = this.buildSnsPayload(config, payload);
478
+ // Map channel string to NotificationChannel enum
479
+ const channelMap = {
480
+ email: NotificationChannel.EMAIL,
481
+ sms: NotificationChannel.SMS,
482
+ push: NotificationChannel.PUSH,
483
+ };
484
+ const channel = channelMap[config.channel] || NotificationChannel.EMAIL;
485
+ // Validate notification type is a valid enum value
486
+ const notificationType = config.notificationType;
487
+ if (!Object.values(NotificationType).includes(notificationType)) {
488
+ throw new Error(`Invalid notification type: ${config.notificationType}`);
489
+ }
490
+ // Publish to SNS via EventPublisher
491
+ const messageId = await this.eventPublisher.publish(notificationType, channel, notificationPayload, {
492
+ tenantId,
493
+ correlationId: payload.correlationId || undefined,
494
+ userId: payload.userId || payload.actorId || undefined,
495
+ });
496
+ return {
497
+ success: true,
498
+ messageId,
499
+ notificationType: config.notificationType,
500
+ channel: config.channel,
501
+ payload: notificationPayload,
502
+ tenantId,
503
+ };
504
+ }
505
+ /**
506
+ * Build the notification payload for SNS from config and event payload
507
+ */
508
+ buildSnsPayload(config, payload) {
509
+ const result = {};
510
+ // Apply static payload first
511
+ if (config.staticPayload) {
512
+ Object.assign(result, config.staticPayload);
513
+ }
514
+ // Apply payload mapping (map from event payload to notification payload)
515
+ if (config.payloadMapping) {
516
+ for (const [targetField, sourcePath] of Object.entries(config.payloadMapping)) {
517
+ const value = this.resolvePath(payload, sourcePath.replace(/^\$\./, ''));
518
+ if (value !== undefined) {
519
+ result[targetField] = value;
520
+ }
521
+ }
522
+ }
523
+ // If recipientPath is specified, extract the recipient email
524
+ if (config.recipientPath) {
525
+ const email = this.resolvePath(payload, config.recipientPath.replace(/^\$\./, ''));
526
+ if (email && typeof email === 'string') {
527
+ result.to_email = email;
528
+ }
529
+ }
530
+ return result;
531
+ }
460
532
  // ==========================================
461
533
  // UTILITY METHODS
462
534
  // ==========================================
@@ -61,7 +61,7 @@ export interface WorkflowHandlerConfig {
61
61
  }
62
62
  /**
63
63
  * Configuration for NOTIFICATION handler type
64
- * Sends notifications via various channels
64
+ * Sends notifications via various channels (internal handling)
65
65
  */
66
66
  export interface NotificationHandlerConfig {
67
67
  type: 'NOTIFICATION';
@@ -78,6 +78,33 @@ export interface NotificationHandlerConfig {
78
78
  /** Priority */
79
79
  priority?: 'low' | 'normal' | 'high' | 'urgent';
80
80
  }
81
+ /**
82
+ * Configuration for SNS handler type
83
+ * Publishes to SNS topic, which triggers the notification-service via SQS
84
+ * Uses the NotificationEvent format expected by notification-service
85
+ */
86
+ export interface SnsHandlerConfig {
87
+ type: 'SNS';
88
+ /** SNS Topic ARN (optional - defaults to notifications topic) */
89
+ topicArn?: string;
90
+ /** Notification type (from NotificationType enum) */
91
+ notificationType: string;
92
+ /** Notification channel (email, sms, push) */
93
+ channel: 'email' | 'sms' | 'push';
94
+ /**
95
+ * Payload mapping - maps event payload to notification payload
96
+ * Uses JSONPath-like expressions (e.g., $.user.email -> to_email)
97
+ */
98
+ payloadMapping?: Record<string, string>;
99
+ /**
100
+ * Static payload fields to merge with mapped payload
101
+ */
102
+ staticPayload?: Record<string, unknown>;
103
+ /**
104
+ * Email recipient field path in the event payload (e.g., $.user.email)
105
+ */
106
+ recipientPath?: string;
107
+ }
81
108
  /**
82
109
  * Configuration for SCRIPT handler type
83
110
  * Executes custom logic
@@ -94,7 +121,7 @@ export interface ScriptHandlerConfig {
94
121
  /**
95
122
  * Union type for all handler configurations
96
123
  */
97
- export type HandlerConfig = InternalHandlerConfig | WebhookHandlerConfig | WorkflowHandlerConfig | NotificationHandlerConfig | ScriptHandlerConfig;
124
+ export type HandlerConfig = InternalHandlerConfig | WebhookHandlerConfig | WorkflowHandlerConfig | NotificationHandlerConfig | SnsHandlerConfig | ScriptHandlerConfig;
98
125
  /**
99
126
  * Input for emitting an event
100
127
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valentine-efagene/qshelter-common",
3
- "version": "2.0.67",
3
+ "version": "2.0.69",
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",
@@ -0,0 +1,2 @@
1
+ -- AlterTable
2
+ ALTER TABLE `event_handlers` MODIFY `handlerType` ENUM('INTERNAL', 'WEBHOOK', 'WORKFLOW', 'NOTIFICATION', 'SNS', 'SCRIPT') NOT NULL;
@@ -194,7 +194,8 @@ enum EventHandlerType {
194
194
  INTERNAL // Call an internal service method
195
195
  WEBHOOK // Send a webhook to an external URL
196
196
  WORKFLOW // Trigger or advance a workflow
197
- NOTIFICATION // Send a notification (email, SMS, push)
197
+ NOTIFICATION // Send a notification (email, SMS, push) - internal
198
+ SNS // Publish to SNS topic (for notification-service)
198
199
  SCRIPT // Execute a custom script/expression
199
200
  }
200
201