@valentine-efagene/qshelter-common 2.0.94 → 2.0.96

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.
Files changed (51) hide show
  1. package/dist/generated/client/browser.d.ts +16 -16
  2. package/dist/generated/client/client.d.ts +16 -16
  3. package/dist/generated/client/commonInputTypes.d.ts +72 -72
  4. package/dist/generated/client/enums.d.ts +13 -13
  5. package/dist/generated/client/enums.js +10 -10
  6. package/dist/generated/client/internal/class.d.ts +32 -32
  7. package/dist/generated/client/internal/class.js +2 -2
  8. package/dist/generated/client/internal/prismaNamespace.d.ts +321 -321
  9. package/dist/generated/client/internal/prismaNamespace.js +50 -50
  10. package/dist/generated/client/internal/prismaNamespaceBrowser.d.ts +66 -66
  11. package/dist/generated/client/internal/prismaNamespaceBrowser.js +50 -50
  12. package/dist/generated/client/models/Application.d.ts +5439 -0
  13. package/dist/generated/client/models/Application.js +1 -0
  14. package/dist/generated/client/models/ApplicationDocument.d.ts +1409 -0
  15. package/dist/generated/client/models/ApplicationDocument.js +1 -0
  16. package/dist/generated/client/models/ApplicationEvent.d.ts +1254 -0
  17. package/dist/generated/client/models/ApplicationEvent.js +1 -0
  18. package/dist/generated/client/models/ApplicationPayment.d.ts +2030 -0
  19. package/dist/generated/client/models/ApplicationPayment.js +1 -0
  20. package/dist/generated/client/models/ApplicationPhase.d.ts +2243 -0
  21. package/dist/generated/client/models/ApplicationPhase.js +1 -0
  22. package/dist/generated/client/models/ApplicationRefund.d.ts +2560 -0
  23. package/dist/generated/client/models/ApplicationRefund.js +1 -0
  24. package/dist/generated/client/models/ApplicationTermination.d.ts +3446 -0
  25. package/dist/generated/client/models/ApplicationTermination.js +1 -0
  26. package/dist/generated/client/models/DocumentationPhase.d.ts +13 -13
  27. package/dist/generated/client/models/OfferLetter.d.ts +97 -97
  28. package/dist/generated/client/models/PaymentInstallment.d.ts +1660 -0
  29. package/dist/generated/client/models/PaymentInstallment.js +1 -0
  30. package/dist/generated/client/models/PaymentMethodChangeRequest.d.ts +103 -103
  31. package/dist/generated/client/models/PaymentPhase.d.ts +40 -40
  32. package/dist/generated/client/models/PropertyPaymentMethod.d.ts +77 -77
  33. package/dist/generated/client/models/PropertyTransferRequest.d.ts +213 -213
  34. package/dist/generated/client/models/PropertyUnit.d.ts +53 -53
  35. package/dist/generated/client/models/QuestionnairePhase.d.ts +11 -11
  36. package/dist/generated/client/models/Tenant.d.ts +323 -323
  37. package/dist/generated/client/models/User.d.ts +1329 -1329
  38. package/dist/generated/client/models/index.d.ts +8 -8
  39. package/dist/generated/client/models/index.js +8 -8
  40. package/dist/generated/client/models.d.ts +8 -8
  41. package/dist/src/auth/policy-evaluator.d.ts +5 -5
  42. package/dist/src/auth/policy-evaluator.js +7 -7
  43. package/dist/src/events/notifications/notification-enums.d.ts +5 -5
  44. package/dist/src/events/notifications/notification-enums.js +6 -6
  45. package/dist/src/events/payments/payment-event.d.ts +2 -2
  46. package/dist/src/events/payments/payment-publisher.d.ts +1 -1
  47. package/package.json +1 -1
  48. package/prisma/migrations/20260112080730_rename_contract_to_application/migration.sql +293 -0
  49. package/prisma/migrations/20260112081422_cleanup_rename_contract_to_application/migration.sql +14 -0
  50. package/prisma/schema.prisma +107 -107
  51. package/prisma/schema.prisma.backup +2601 -0
@@ -0,0 +1,2601 @@
1
+ // =============================================================================
2
+ // QSHELTER UNIFIED DATABASE SCHEMA
3
+ // =============================================================================
4
+ // This schema contains all database models for the QShelter platform
5
+ // Organized by domain for better readability
6
+ // =============================================================================
7
+
8
+ generator client {
9
+ provider = "prisma-client"
10
+ output = "../generated/client"
11
+ engineType = "client"
12
+ }
13
+
14
+ datasource db {
15
+ provider = "mysql"
16
+ }
17
+
18
+ // =============================================================================
19
+ // ENUMS - Database-enforced value constraints
20
+ // =============================================================================
21
+
22
+ enum PhaseCategory {
23
+ QUESTIONNAIRE // Configurable form fields with validation
24
+ DOCUMENTATION // Document upload and approval workflow
25
+ PAYMENT // Installment-based payment collection
26
+ }
27
+
28
+ enum PhaseType {
29
+ // QUESTIONNAIRE phases
30
+ PRE_APPROVAL // Eligibility questionnaire (income, employment, etc.)
31
+ UNDERWRITING // System evaluation of eligibility
32
+
33
+ // DOCUMENTATION phases
34
+ KYC
35
+ VERIFICATION
36
+
37
+ // PAYMENT phases
38
+ DOWNPAYMENT
39
+ MORTGAGE
40
+ BALLOON
41
+
42
+ // Generic
43
+ CUSTOM
44
+ }
45
+
46
+ enum PaymentFrequency {
47
+ MONTHLY
48
+ BIWEEKLY
49
+ WEEKLY
50
+ ONE_TIME
51
+ CUSTOM
52
+ }
53
+
54
+ enum ContractStatus {
55
+ DRAFT
56
+ PENDING
57
+ ACTIVE
58
+ COMPLETED
59
+ CANCELLED
60
+ TERMINATED
61
+ TRANSFERRED // Contract was transferred to a different property
62
+ }
63
+
64
+ enum TransferRequestStatus {
65
+ PENDING
66
+ APPROVED
67
+ REJECTED
68
+ IN_PROGRESS
69
+ COMPLETED
70
+ FAILED
71
+ }
72
+
73
+ enum PhaseStatus {
74
+ PENDING
75
+ IN_PROGRESS
76
+ AWAITING_APPROVAL
77
+ ACTIVE
78
+ COMPLETED
79
+ SKIPPED
80
+ FAILED
81
+ SUPERSEDED // Phase replaced by payment method change
82
+ }
83
+
84
+ enum StepType {
85
+ UPLOAD
86
+ REVIEW
87
+ SIGNATURE
88
+ APPROVAL
89
+ EXTERNAL_CHECK
90
+ WAIT
91
+ GENERATE_DOCUMENT // Triggers document generation (offer letters, contracts, etc.)
92
+ PRE_APPROVAL // Customer answers eligibility questionnaire
93
+ UNDERWRITING // System evaluates DTI, score, eligibility
94
+ }
95
+
96
+ enum StepStatus {
97
+ PENDING
98
+ IN_PROGRESS
99
+ COMPLETED
100
+ FAILED
101
+ SKIPPED
102
+ NEEDS_RESUBMISSION // User must re-upload or correct something (after rejection)
103
+ ACTION_REQUIRED // User action needed (generic - check actionReason)
104
+ AWAITING_REVIEW // Submitted, waiting for admin/system review
105
+ }
106
+
107
+ /// When a step event attachment should trigger
108
+ enum StepTrigger {
109
+ ON_COMPLETE // When step is approved/completed
110
+ ON_REJECT // When step is rejected
111
+ ON_SUBMIT // When step is submitted for review
112
+ ON_RESUBMIT // When step is resubmitted after rejection
113
+ ON_START // When step transitions to IN_PROGRESS
114
+ }
115
+
116
+ /// When a phase event attachment should trigger
117
+ enum PhaseTrigger {
118
+ ON_ACTIVATE // When phase is activated (becomes current)
119
+ ON_COMPLETE // When phase is completed (all steps/payments done)
120
+ ON_CANCEL // When phase is cancelled
121
+ ON_PAYMENT_RECEIVED // When any payment is received (for PAYMENT phases)
122
+ ON_ALL_PAYMENTS_RECEIVED // When all payments in phase are complete
123
+ }
124
+
125
+ enum InstallmentStatus {
126
+ PENDING
127
+ PAID
128
+ OVERDUE
129
+ PARTIALLY_PAID
130
+ WAIVED
131
+ }
132
+
133
+ enum PaymentStatus {
134
+ INITIATED
135
+ PENDING
136
+ COMPLETED
137
+ FAILED
138
+ REFUNDED
139
+ }
140
+
141
+ enum ApprovalDecision {
142
+ APPROVED
143
+ REJECTED
144
+ REQUEST_CHANGES
145
+ }
146
+
147
+ // =============================================================================
148
+ // CONTRACT TERMINATION / CANCELLATION ENUMS
149
+ // =============================================================================
150
+
151
+ enum TerminationType {
152
+ BUYER_WITHDRAWAL // Buyer wants to cancel (voluntary)
153
+ SELLER_WITHDRAWAL // Seller/developer cancels
154
+ MUTUAL_AGREEMENT // Both parties agree to terminate
155
+ PAYMENT_DEFAULT // Buyer failed payment obligations
156
+ DOCUMENT_FAILURE // Buyer failed to provide required documents
157
+ FRAUD // Fraudulent activity detected
158
+ FORCE_MAJEURE // External circumstances (disaster, etc.)
159
+ PROPERTY_UNAVAILABLE // Property no longer available
160
+ REGULATORY // Regulatory/legal requirement
161
+ OTHER // Other reasons (with notes)
162
+ }
163
+
164
+ enum TerminationStatus {
165
+ REQUESTED // Initial request submitted
166
+ PENDING_REVIEW // Awaiting admin review
167
+ PENDING_REFUND // Approved, awaiting refund processing
168
+ REFUND_IN_PROGRESS // Refund being processed
169
+ REFUND_COMPLETED // Refund completed
170
+ COMPLETED // Termination fully executed (no refund or refund done)
171
+ REJECTED // Termination request rejected
172
+ CANCELLED // Termination request was cancelled
173
+ }
174
+
175
+ enum TerminationInitiator {
176
+ BUYER
177
+ SELLER
178
+ ADMIN
179
+ SYSTEM
180
+ }
181
+
182
+ enum CompletionCriterion {
183
+ DOCUMENT_APPROVALS
184
+ PAYMENT_AMOUNT
185
+ STEPS_COMPLETED
186
+ }
187
+
188
+ enum DocumentStatus {
189
+ DRAFT
190
+ PENDING
191
+ PENDING_SIGNATURE
192
+ SENT
193
+ VIEWED
194
+ SIGNED
195
+ APPROVED
196
+ REJECTED
197
+ EXPIRED
198
+ CANCELLED
199
+ }
200
+
201
+ enum OfferLetterType {
202
+ PROVISIONAL
203
+ FINAL
204
+ }
205
+
206
+ enum OfferLetterStatus {
207
+ DRAFT
208
+ GENERATED
209
+ SENT
210
+ VIEWED
211
+ SIGNED
212
+ EXPIRED
213
+ CANCELLED
214
+ }
215
+
216
+ enum ContractEventType {
217
+ CONTRACT_CREATED
218
+ CONTRACT_STATE_CHANGED
219
+ PHASE_ACTIVATED
220
+ PHASE_COMPLETED
221
+ STEP_COMPLETED
222
+ STEP_REJECTED
223
+ DOCUMENT_SUBMITTED
224
+ DOCUMENT_APPROVED
225
+ DOCUMENT_REJECTED
226
+ PAYMENT_INITIATED
227
+ PAYMENT_COMPLETED
228
+ PAYMENT_FAILED
229
+ INSTALLMENTS_GENERATED
230
+ CONTRACT_SIGNED
231
+ CONTRACT_TERMINATED
232
+ CONTRACT_TRANSFERRED
233
+ UNDERWRITING_COMPLETED
234
+ OFFER_LETTER_GENERATED
235
+ }
236
+
237
+ enum ContractEventGroup {
238
+ STATE_CHANGE
239
+ PAYMENT
240
+ DOCUMENT
241
+ NOTIFICATION
242
+ WORKFLOW
243
+ }
244
+
245
+ enum EventActorType {
246
+ USER
247
+ SYSTEM
248
+ WEBHOOK
249
+ ADMIN
250
+ }
251
+
252
+ enum RefundStatus {
253
+ PENDING
254
+ APPROVED
255
+ REJECTED
256
+ PROCESSING
257
+ COMPLETED
258
+ FAILED
259
+ CANCELLED
260
+ }
261
+
262
+ // =============================================================================
263
+ // EVENT-DRIVEN WORKFLOW ENUMS
264
+ // =============================================================================
265
+
266
+ /// Handler Type - What kind of action the handler performs
267
+ /// These are business-friendly names that admins can understand
268
+ enum EventHandlerType {
269
+ SEND_EMAIL // Send an email notification to recipient(s)
270
+ SEND_SMS // Send an SMS text message
271
+ SEND_PUSH // Send a push notification
272
+ CALL_WEBHOOK // Call an external API/webhook
273
+ ADVANCE_WORKFLOW // Advance or complete a workflow step
274
+ RUN_AUTOMATION // Execute internal business logic
275
+ }
276
+
277
+ /// Actor Type - Who triggered an event
278
+ enum ActorType {
279
+ USER
280
+ API_KEY
281
+ SYSTEM
282
+ WEBHOOK
283
+ }
284
+
285
+ /// Workflow Event Status
286
+ enum WorkflowEventStatus {
287
+ PENDING
288
+ PROCESSING
289
+ COMPLETED
290
+ FAILED
291
+ SKIPPED
292
+ }
293
+
294
+ /// Handler Execution Status
295
+ enum ExecutionStatus {
296
+ PENDING
297
+ RUNNING
298
+ COMPLETED
299
+ FAILED
300
+ RETRYING
301
+ SKIPPED
302
+ }
303
+
304
+ /// Permission effect (Allow/Deny)
305
+ enum PermissionEffect {
306
+ ALLOW
307
+ DENY
308
+ }
309
+
310
+ // =============================================================================
311
+ // USER & AUTH DOMAIN
312
+ // =============================================================================
313
+
314
+ model User {
315
+ id String @id @default(cuid())
316
+ email String @unique
317
+ password String?
318
+ phone String? @unique
319
+ firstName String?
320
+ lastName String?
321
+ isActive Boolean @default(true)
322
+ isEmailVerified Boolean @default(false)
323
+ googleId String?
324
+ avatar String?
325
+ // Legacy: Optional direct tenant association (for backward compatibility)
326
+ // New: Use tenantMemberships for multi-tenant federation
327
+ tenantId String?
328
+ tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: SetNull)
329
+ // Federated: User can belong to multiple tenants with different roles
330
+ tenantMemberships TenantMembership[]
331
+ // Legacy: Support multiple roles via explicit join table `UserRole`
332
+ userRoles UserRole[]
333
+ walletId String? @unique
334
+ wallet Wallet? @relation(fields: [walletId], references: [id])
335
+ createdAt DateTime @default(now())
336
+ updatedAt DateTime @updatedAt
337
+ emailVerifiedAt DateTime?
338
+ emailVerificationToken String?
339
+ lastLoginAt DateTime?
340
+ refreshTokens RefreshToken[]
341
+ passwordResets PasswordReset[]
342
+ suspensions UserSuspension[]
343
+ emailPreferences EmailPreference[]
344
+ deviceEndpoints DeviceEndpoint[]
345
+ socials Social[]
346
+
347
+ // Relations to other domains
348
+ properties Property[]
349
+ contracts Contract[] @relation("ContractBuyer")
350
+ soldContracts Contract[] @relation("ContractSeller")
351
+ contractPayments ContractPayment[] @relation("ContractPayer")
352
+
353
+ // Documentation step assignments and approvals
354
+ assignedSteps DocumentationStep[] @relation("DocumentationStepAssignee")
355
+ stepApprovals DocumentationStepApproval[] @relation("DocumentationStepApprover")
356
+ uploadedDocs ContractDocument[] @relation("DocumentUploader")
357
+
358
+ // Payment method changes
359
+ paymentMethodChangeRequests PaymentMethodChangeRequest[] @relation("ChangeRequestor")
360
+ reviewedChangeRequests PaymentMethodChangeRequest[] @relation("ChangeReviewer")
361
+
362
+ // Contract terminations
363
+ initiatedTerminations ContractTermination[] @relation("TerminationInitiator")
364
+ reviewedTerminations ContractTermination[] @relation("TerminationReviewer")
365
+
366
+ // Offer letters
367
+ offerLettersGenerated OfferLetter[] @relation("OfferLetterGenerator")
368
+ offerLettersSent OfferLetter[] @relation("OfferLetterSender")
369
+
370
+ // Property transfer requests
371
+ transferRequestsSubmitted PropertyTransferRequest[] @relation("TransferRequestor")
372
+ transferRequestsReviewed PropertyTransferRequest[] @relation("TransferReviewer")
373
+
374
+ // Unified approval requests
375
+ approvalRequestsSubmitted ApprovalRequest[] @relation("ApprovalRequestor")
376
+ approvalRequestsAssigned ApprovalRequest[] @relation("ApprovalAssignee")
377
+ approvalRequestsReviewed ApprovalRequest[] @relation("ApprovalReviewer")
378
+
379
+ // Contract refunds
380
+ requestedRefunds ContractRefund[] @relation("RefundRequester")
381
+ approvedRefunds ContractRefund[] @relation("RefundApprover")
382
+ processedRefunds ContractRefund[] @relation("RefundProcessor")
383
+
384
+ @@index([email])
385
+ @@index([tenantId])
386
+ @@map("users")
387
+ }
388
+
389
+ model Role {
390
+ id String @id @default(cuid())
391
+ name String
392
+ description String?
393
+ // Tenant-scoping: NULL = global template, set = tenant-specific role
394
+ tenantId String?
395
+ tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade)
396
+ // System roles cannot be deleted (admin, user, etc.)
397
+ isSystem Boolean @default(false)
398
+ isActive Boolean @default(true)
399
+ // Legacy: UserRole for backward compatibility
400
+ userRoles UserRole[]
401
+ // New: TenantMembership for federated users
402
+ memberships TenantMembership[]
403
+ permissions RolePermission[]
404
+ createdAt DateTime @default(now())
405
+ updatedAt DateTime @updatedAt
406
+
407
+ @@unique([name, tenantId]) // Unique name per tenant (null tenantId = global)
408
+ @@index([tenantId])
409
+ @@map("roles")
410
+ }
411
+
412
+ /// Permission defines a path pattern + HTTP methods + effect
413
+ /// Supports path-based authorization matching the authorizer's policy structure
414
+ model Permission {
415
+ id String @id @default(cuid())
416
+ name String // Descriptive name: "Read Users", "Manage Properties"
417
+ description String?
418
+ // Path pattern: /users, /users/:id, /properties/*, etc.
419
+ path String
420
+ // HTTP methods: ["GET"], ["GET", "POST"], ["*"] - stored as JSON
421
+ methods Json @default("[]")
422
+ // Allow or Deny this path/methods
423
+ effect PermissionEffect @default(ALLOW)
424
+ // Tenant-scoping: NULL = global template, set = tenant-specific
425
+ tenantId String?
426
+ tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade)
427
+ // System permissions cannot be deleted
428
+ isSystem Boolean @default(false)
429
+ roles RolePermission[]
430
+ createdAt DateTime @default(now())
431
+ updatedAt DateTime @updatedAt
432
+
433
+ @@unique([path, tenantId]) // Unique path per tenant
434
+ @@index([tenantId])
435
+ @@map("permissions")
436
+ }
437
+
438
+ model RolePermission {
439
+ roleId String
440
+ permissionId String
441
+ role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
442
+ permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
443
+ createdAt DateTime @default(now())
444
+
445
+ @@id([roleId, permissionId])
446
+ @@map("role_permissions")
447
+ }
448
+
449
+ /// Legacy: Direct user-role assignment (global, not tenant-scoped)
450
+ /// @deprecated Use TenantMembership for tenant-scoped role assignments
451
+ model UserRole {
452
+ userId String
453
+ roleId String
454
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
455
+ role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
456
+ createdAt DateTime @default(now())
457
+
458
+ @@id([userId, roleId])
459
+ @@map("user_roles")
460
+ }
461
+
462
+ /// Tenant Membership: Links users to tenants with specific roles
463
+ /// Enables federated users across multiple tenants with different roles per tenant
464
+ model TenantMembership {
465
+ id String @id @default(cuid())
466
+ userId String
467
+ tenantId String
468
+ roleId String
469
+ // Whether this membership is active
470
+ isActive Boolean @default(true)
471
+ // Whether this is the user's default tenant (for login without specifying tenant)
472
+ isDefault Boolean @default(false)
473
+ createdAt DateTime @default(now())
474
+ updatedAt DateTime @updatedAt
475
+
476
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
477
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
478
+ role Role @relation(fields: [roleId], references: [id], onDelete: Restrict)
479
+
480
+ @@unique([userId, tenantId]) // User can only have one membership per tenant
481
+ @@index([tenantId])
482
+ @@index([userId])
483
+ @@map("tenant_memberships")
484
+ }
485
+
486
+ model Tenant {
487
+ id String @id @default(cuid())
488
+ name String
489
+ subdomain String @unique
490
+ isActive Boolean @default(true)
491
+ createdAt DateTime @default(now())
492
+ updatedAt DateTime @updatedAt
493
+
494
+ // Back-relations for multitenancy
495
+ users User[]
496
+ properties Property[]
497
+ paymentPlans PaymentPlan[]
498
+ paymentMethods PropertyPaymentMethod[]
499
+ contracts Contract[]
500
+
501
+ // RBAC: Tenant-scoped roles and permissions
502
+ roles Role[]
503
+ permissions Permission[]
504
+ // Federated user memberships
505
+ memberships TenantMembership[]
506
+
507
+ // Payment method changes
508
+ paymentMethodChangeRequests PaymentMethodChangeRequest[]
509
+ documentRequirementRules DocumentRequirementRule[]
510
+
511
+ // Contract terminations
512
+ contractTerminations ContractTermination[]
513
+
514
+ // Offer letters and templates
515
+ documentTemplates DocumentTemplate[]
516
+ offerLetters OfferLetter[]
517
+
518
+ // API keys for third-party integrations
519
+ apiKeys ApiKey[]
520
+
521
+ // Event-driven workflow
522
+ eventChannels EventChannel[]
523
+ eventTypes EventType[]
524
+ eventHandlers EventHandler[]
525
+ workflowEvents WorkflowEvent[]
526
+
527
+ // Property transfer requests
528
+ propertyTransferRequests PropertyTransferRequest[]
529
+
530
+ // Unified approval requests
531
+ approvalRequests ApprovalRequest[]
532
+
533
+ // Contract refunds
534
+ contractRefunds ContractRefund[]
535
+
536
+ @@index([subdomain])
537
+ @@map("tenants")
538
+ }
539
+
540
+ // =============================================================================
541
+ // API KEYS - Third-party integration credentials
542
+ // =============================================================================
543
+ // ApiKey enables partners/integrations to authenticate via token exchange.
544
+ //
545
+ // Flow:
546
+ // 1. Admin creates API key for a partner (POST /api-keys)
547
+ // 2. System generates secret, stores in Secrets Manager, returns id.secret ONCE
548
+ // 3. Partner calls token endpoint with id.secret (POST /api-keys/:id/token)
549
+ // 4. Token endpoint validates via Secrets Manager, returns short-lived JWT
550
+ // 5. Partner uses JWT for API requests; authorizer validates + resolves scopes
551
+ //
552
+ // Security:
553
+ // - Raw secret stored ONLY in AWS Secrets Manager (secretRef = ARN)
554
+ // - Secret returned only once at creation; admin must rotate if lost
555
+ // - Scopes define allowed operations (e.g., ["contract:read", "payment:read"])
556
+ // - Short-lived JWTs (5-15 min) minimize exposure on key compromise
557
+ // =============================================================================
558
+
559
+ model ApiKey {
560
+ id String @id @default(cuid())
561
+ tenantId String
562
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
563
+
564
+ // Identification
565
+ name String // Human-readable name (e.g., "Paystack Integration")
566
+ description String? @db.Text // Optional description
567
+ provider String // Partner/vendor name (e.g., "paystack", "flutterwave")
568
+
569
+ // Secret management (NEVER store raw secret in DB)
570
+ secretRef String // AWS Secrets Manager ARN or name
571
+
572
+ // Permissions - scopes this API key is allowed to request
573
+ // Examples: ["contract:read", "payment:*", "property:read"]
574
+ scopes Json // JSON array of scope strings
575
+
576
+ // Lifecycle
577
+ enabled Boolean @default(true)
578
+ expiresAt DateTime? // Optional expiration date
579
+ lastUsedAt DateTime? // Updated on each token exchange
580
+ revokedAt DateTime? // Set when key is revoked
581
+ revokedBy String? // User ID who revoked
582
+
583
+ // Audit
584
+ createdBy String? // User ID who created
585
+ createdAt DateTime @default(now())
586
+ updatedAt DateTime @updatedAt
587
+
588
+ @@index([tenantId])
589
+ @@index([provider])
590
+ @@index([enabled])
591
+ @@map("api_keys")
592
+ }
593
+
594
+ model RefreshToken {
595
+ id String @id @default(cuid())
596
+ // Use the JWT `jti` for indexed lookups and keep the raw JWT (optional)
597
+ jti String? @unique @db.VarChar(255)
598
+ token String? @db.LongText
599
+ userId String
600
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
601
+ expiresAt DateTime
602
+ createdAt DateTime @default(now())
603
+
604
+ @@index([userId])
605
+ @@index([expiresAt])
606
+ @@map("refresh_tokens")
607
+ }
608
+
609
+ model PasswordReset {
610
+ id String @id @default(cuid())
611
+ token String @unique
612
+ userId String
613
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
614
+ expiresAt DateTime
615
+ usedAt DateTime?
616
+ createdAt DateTime @default(now())
617
+
618
+ @@index([userId])
619
+ @@index([expiresAt])
620
+ @@map("password_resets")
621
+ }
622
+
623
+ model UserSuspension {
624
+ id String @id @default(cuid())
625
+ userId String
626
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
627
+ reason String
628
+ suspendedAt DateTime @default(now())
629
+ expiresAt DateTime?
630
+ liftedAt DateTime?
631
+
632
+ @@index([userId])
633
+ @@map("user_suspensions")
634
+ }
635
+
636
+ model EmailPreference {
637
+ id String @id @default(cuid())
638
+ userId String
639
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
640
+ marketingEmails Boolean @default(true)
641
+ transactionalEmails Boolean @default(true)
642
+ propertyAlerts Boolean @default(true)
643
+ paymentReminders Boolean @default(true)
644
+ createdAt DateTime @default(now())
645
+ updatedAt DateTime @updatedAt
646
+
647
+ @@index([userId])
648
+ @@map("email_preferences")
649
+ }
650
+
651
+ model DeviceEndpoint {
652
+ id String @id @default(cuid())
653
+ userId String
654
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
655
+ endpoint String // Push notification endpoint
656
+ platform String // ios, android, web
657
+ isActive Boolean @default(true)
658
+ createdAt DateTime @default(now())
659
+ updatedAt DateTime @updatedAt
660
+
661
+ @@index([userId])
662
+ @@map("device_endpoints")
663
+ }
664
+
665
+ model Social {
666
+ id String @id @default(cuid())
667
+ userId String
668
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
669
+ provider String // google, facebook, twitter, etc
670
+ socialId String // ID from the social provider
671
+ createdAt DateTime @default(now())
672
+ updatedAt DateTime @updatedAt
673
+
674
+ @@unique([provider, socialId])
675
+ @@index([userId])
676
+ @@map("socials")
677
+ }
678
+
679
+ model OAuthState {
680
+ id String @id @default(cuid())
681
+ state String @unique
682
+ expiresAt DateTime
683
+ createdAt DateTime @default(now())
684
+
685
+ @@index([state])
686
+ @@index([expiresAt])
687
+ @@map("oauth_states")
688
+ }
689
+
690
+ model Wallet {
691
+ id String @id @default(cuid())
692
+ balance Float @default(0)
693
+ currency String @default("USD")
694
+ user User?
695
+ transactions Transaction[]
696
+ createdAt DateTime @default(now())
697
+ updatedAt DateTime @updatedAt
698
+
699
+ @@map("wallets")
700
+ }
701
+
702
+ model Transaction {
703
+ id String @id @default(cuid())
704
+ walletId String
705
+ wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
706
+ amount Float
707
+ type String // CREDIT, DEBIT
708
+ status String // PENDING, COMPLETED, FAILED
709
+ reference String?
710
+ description String?
711
+ createdAt DateTime @default(now())
712
+ updatedAt DateTime @updatedAt
713
+
714
+ @@index([walletId])
715
+ @@map("transactions")
716
+ }
717
+
718
+ model Settings {
719
+ id String @id @default(cuid())
720
+ key String @unique
721
+ value String @db.Text
722
+ category String?
723
+ createdAt DateTime @default(now())
724
+ updatedAt DateTime @updatedAt
725
+
726
+ @@index([category])
727
+ @@map("settings")
728
+ }
729
+
730
+ // =============================================================================
731
+ // PROPERTY DOMAIN
732
+ // =============================================================================
733
+ // Property = listing/project (e.g., "Sunrise Estate")
734
+ // PropertyVariant = configuration with specs & price (e.g., "3-Bed Corner - Finished")
735
+ // PropertyUnit = individual sellable unit (e.g., "Unit A1")
736
+ // =============================================================================
737
+
738
+ model Property {
739
+ id String @id @default(cuid())
740
+ tenantId String
741
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
742
+ userId String
743
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
744
+ title String
745
+ category String // SALE, RENT, LEASE
746
+ propertyType String // APARTMENT, HOUSE, LAND, COMMERCIAL, ESTATE, TOWNHOUSE
747
+ country String
748
+ currency String // USD, NGN, etc
749
+ city String
750
+ district String?
751
+ zipCode String?
752
+ streetAddress String?
753
+ longitude Float?
754
+ latitude Float?
755
+ status String @default("DRAFT") // DRAFT, PUBLISHED, SOLD_OUT, ARCHIVED
756
+ description String? @db.Text
757
+ displayImageId String?
758
+ displayImage PropertyMedia? @relation("DisplayImage", fields: [displayImageId], references: [id], onDelete: SetNull)
759
+ isPublished Boolean @default(false)
760
+ publishedAt DateTime?
761
+ createdAt DateTime @default(now())
762
+ updatedAt DateTime @updatedAt
763
+
764
+ // Relations
765
+ documents PropertyDocument[]
766
+ media PropertyMedia[] @relation("PropertyMedia")
767
+ amenities PropertyAmenity[] // Shared amenities (gym, pool, security)
768
+ paymentMethods PropertyPaymentMethodLink[]
769
+ variants PropertyVariant[]
770
+
771
+ @@index([tenantId])
772
+ @@index([userId])
773
+ @@index([category])
774
+ @@index([propertyType])
775
+ @@index([city])
776
+ @@index([status])
777
+ @@map("properties")
778
+ }
779
+
780
+ model PropertyMedia {
781
+ id String @id @default(cuid())
782
+ propertyId String
783
+ property Property @relation("PropertyMedia", fields: [propertyId], references: [id], onDelete: Cascade)
784
+ url String
785
+ type String // IMAGE, VIDEO
786
+ caption String?
787
+ order Int @default(0)
788
+ createdAt DateTime @default(now())
789
+ updatedAt DateTime @updatedAt
790
+
791
+ displayForProperties Property[] @relation("DisplayImage")
792
+
793
+ @@index([propertyId])
794
+ @@map("property_media")
795
+ }
796
+
797
+ model PropertyDocument {
798
+ id String @id @default(cuid())
799
+ propertyId String
800
+ property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)
801
+ name String
802
+ url String
803
+ type String // TITLE_DEED, SURVEY_PLAN, etc
804
+ createdAt DateTime @default(now())
805
+ updatedAt DateTime @updatedAt
806
+
807
+ @@index([propertyId])
808
+ @@map("property_documents")
809
+ }
810
+
811
+ model Amenity {
812
+ id String @id @default(cuid())
813
+ name String @unique
814
+ category String? // PROPERTY, VARIANT, BOTH - helps filter which amenities to show
815
+ icon String? // Icon name/URL for UI
816
+ createdAt DateTime @default(now())
817
+ updatedAt DateTime @updatedAt
818
+ properties PropertyAmenity[]
819
+ variants PropertyVariantAmenity[]
820
+
821
+ @@index([category])
822
+ @@map("amenities")
823
+ }
824
+
825
+ // =============================================================================
826
+ // PROPERTY VARIANT & UNIT MODELS
827
+ // =============================================================================
828
+
829
+ // PropertyVariant = specific configuration with its own price and amenities
830
+ // e.g., "3-Bedroom Corner Piece - Fully Finished", "2-Bedroom Standard - Carcass"
831
+ model PropertyVariant {
832
+ id String @id @default(cuid())
833
+ propertyId String
834
+ property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)
835
+
836
+ name String // "Corner Piece - Finished", "Standard - Carcass"
837
+ description String? @db.Text
838
+
839
+ // Specifications
840
+ nBedrooms Int?
841
+ nBathrooms Int?
842
+ nParkingSpots Int?
843
+ area Float? // Square meters/feet
844
+
845
+ // Pricing
846
+ price Float
847
+ pricePerSqm Float? // Computed or set manually
848
+
849
+ // Inventory counters (denormalized for performance, updated via triggers/service)
850
+ totalUnits Int @default(1)
851
+ availableUnits Int @default(1)
852
+ reservedUnits Int @default(0)
853
+ soldUnits Int @default(0)
854
+
855
+ // Status
856
+ status String @default("AVAILABLE") // AVAILABLE, LOW_STOCK, SOLD_OUT, ARCHIVED
857
+ isActive Boolean @default(true)
858
+ createdAt DateTime @default(now())
859
+ updatedAt DateTime @updatedAt
860
+
861
+ // Relations
862
+ amenities PropertyVariantAmenity[]
863
+ units PropertyUnit[]
864
+ media PropertyVariantMedia[]
865
+
866
+ @@index([propertyId])
867
+ @@index([status])
868
+ @@index([price])
869
+ @@map("property_variants")
870
+ }
871
+
872
+ // PropertyVariantAmenity = amenities specific to a variant
873
+ // e.g., "Finished Kitchen", "Smart Home System", "Private Garden"
874
+ model PropertyVariantAmenity {
875
+ variantId String
876
+ amenityId String
877
+ variant PropertyVariant @relation(fields: [variantId], references: [id], onDelete: Cascade)
878
+ amenity Amenity @relation(fields: [amenityId], references: [id], onDelete: Cascade)
879
+ createdAt DateTime @default(now())
880
+
881
+ @@id([variantId, amenityId])
882
+ @@map("property_variant_amenities")
883
+ }
884
+
885
+ // PropertyVariantMedia = images/videos specific to a variant
886
+ model PropertyVariantMedia {
887
+ id String @id @default(cuid())
888
+ variantId String
889
+ variant PropertyVariant @relation(fields: [variantId], references: [id], onDelete: Cascade)
890
+ url String
891
+ type String // IMAGE, VIDEO, FLOOR_PLAN, 3D_TOUR
892
+ caption String?
893
+ order Int @default(0)
894
+ createdAt DateTime @default(now())
895
+ updatedAt DateTime @updatedAt
896
+
897
+ @@index([variantId])
898
+ @@map("property_variant_media")
899
+ }
900
+
901
+ // PropertyUnit = individual sellable/rentable unit within a variant
902
+ // e.g., "Unit A1", "Block B - Flat 3", "Plot 15"
903
+ model PropertyUnit {
904
+ id String @id @default(cuid())
905
+ variantId String
906
+ variant PropertyVariant @relation(fields: [variantId], references: [id], onDelete: Cascade)
907
+
908
+ unitNumber String // "A1", "B-3", "Plot 15"
909
+ floorNumber Int? // For apartments
910
+ blockName String? // "Block A", "Tower 1"
911
+
912
+ // Unit-specific overrides (if different from variant)
913
+ priceOverride Float? // If this specific unit has a different price
914
+ areaOverride Float? // If this specific unit has a different area
915
+ notes String? @db.Text // Internal notes about this unit
916
+
917
+ // Status tracking
918
+ status String @default("AVAILABLE") // AVAILABLE, RESERVED, SOLD, RENTED, UNAVAILABLE
919
+
920
+ // Reservation/hold
921
+ reservedAt DateTime?
922
+ reservedUntil DateTime?
923
+ reservedById String?
924
+
925
+ // Ownership tracking (once sold)
926
+ ownerId String?
927
+
928
+ createdAt DateTime @default(now())
929
+ updatedAt DateTime @updatedAt
930
+
931
+ // Relations
932
+ contracts Contract[]
933
+
934
+ // Transfer requests targeting this unit
935
+ transferRequests PropertyTransferRequest[]
936
+
937
+ @@unique([variantId, unitNumber])
938
+ @@index([variantId])
939
+ @@index([status])
940
+ @@map("property_units")
941
+ }
942
+
943
+ model PropertyAmenity {
944
+ propertyId String
945
+ amenityId String
946
+ property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)
947
+ amenity Amenity @relation(fields: [amenityId], references: [id], onDelete: Cascade)
948
+ createdAt DateTime @default(now())
949
+
950
+ @@id([propertyId, amenityId])
951
+ @@map("property_amenities")
952
+ }
953
+
954
+ // =============================================================================
955
+ // PAYMENT PLAN DOMAIN - Reusable installment structure templates
956
+ // =============================================================================
957
+
958
+ // PaymentPlan = reusable structure for how payments are scheduled
959
+ // Examples: "Monthly360" (360 monthly payments), "Weekly52", "OneTime"
960
+ model PaymentPlan {
961
+ id String @id @default(cuid())
962
+ tenantId String? // NULL = global template available to all tenants
963
+ tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade)
964
+ name String
965
+ description String? @db.Text
966
+ isActive Boolean @default(true)
967
+
968
+ // Structure configuration
969
+ paymentFrequency PaymentFrequency
970
+ customFrequencyDays Int?
971
+ numberOfInstallments Int? // Fixed number (1 for one-time, 360 for 30yr monthly) - NULL if flexible
972
+ calculateInterestDaily Boolean @default(false)
973
+ gracePeriodDays Int @default(0)
974
+
975
+ // Flexible term configuration (for user-selectable duration like mortgages)
976
+ // If these are set, numberOfInstallments is ignored and user selects within range
977
+ allowFlexibleTerm Boolean @default(false) // true = user can select term within range
978
+ minTermMonths Int? // e.g., 60 (5 years minimum)
979
+ maxTermMonths Int? // e.g., 360 (30 years maximum)
980
+ termStepMonths Int? // e.g., 12 (increments of 1 year) - NULL = any month allowed
981
+
982
+ // Age-based constraints (for mortgage eligibility)
983
+ maxAgeAtMaturity Int? // e.g., 65 - user's age + term cannot exceed this
984
+
985
+ // Fund collection behavior
986
+ // true = we collect funds via wallet/gateway (e.g., downpayment)
987
+ // false = external payment, we only track/reconcile (e.g., bank mortgage)
988
+ collectFunds Boolean @default(true)
989
+
990
+ createdAt DateTime @default(now())
991
+ updatedAt DateTime @updatedAt
992
+
993
+ // Used by property payment method phases (templates)
994
+ methodPhases PropertyPaymentMethodPhase[]
995
+ // Used by instantiated payment phases
996
+ paymentPhases PaymentPhase[]
997
+
998
+ @@unique([tenantId, name]) // Unique per tenant, or globally if tenantId is null
999
+ @@index([tenantId])
1000
+ @@map("payment_plans")
1001
+ }
1002
+
1003
+ // =============================================================================
1004
+ // PROPERTY PAYMENT METHOD DOMAIN - Product offerings per property
1005
+ // =============================================================================
1006
+
1007
+ // PropertyPaymentMethod = how a property can be purchased (e.g., "Standard Mortgage", "Cash", "Rent-to-Own")
1008
+ model PropertyPaymentMethod {
1009
+ id String @id @default(cuid())
1010
+ tenantId String
1011
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
1012
+ name String // "Standard Mortgage", "Flexible Payment", "Cash Purchase"
1013
+ description String? @db.Text
1014
+ isActive Boolean @default(true)
1015
+
1016
+ // Global method configuration
1017
+ allowEarlyPayoff Boolean @default(true)
1018
+ earlyPayoffPenaltyRate Float?
1019
+ autoActivatePhases Boolean @default(true)
1020
+ requiresManualApproval Boolean @default(false)
1021
+
1022
+ createdAt DateTime @default(now())
1023
+ updatedAt DateTime @updatedAt
1024
+
1025
+ // Many-to-many with properties
1026
+ properties PropertyPaymentMethodLink[]
1027
+ // Phases that make up this method (templates)
1028
+ phases PropertyPaymentMethodPhase[]
1029
+ // Contracts using this method
1030
+ contracts Contract[]
1031
+
1032
+ // Payment method change tracking
1033
+ changeRequestsFrom PaymentMethodChangeRequest[] @relation("ChangeFromMethod")
1034
+ changeRequestsTo PaymentMethodChangeRequest[] @relation("ChangeToMethod")
1035
+
1036
+ // Document requirement rules
1037
+ documentRules DocumentRequirementRule[] @relation("RulePaymentMethod")
1038
+ changeRulesFrom DocumentRequirementRule[] @relation("RuleFromMethod")
1039
+ changeRulesTo DocumentRequirementRule[] @relation("RuleToMethod")
1040
+
1041
+ @@unique([tenantId, name]) // Unique per tenant
1042
+ @@index([tenantId])
1043
+ @@map("property_payment_methods")
1044
+ }
1045
+
1046
+ // Many-to-many link between Property and PaymentMethod
1047
+ model PropertyPaymentMethodLink {
1048
+ propertyId String
1049
+ property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)
1050
+ paymentMethodId String
1051
+ paymentMethod PropertyPaymentMethod @relation(fields: [paymentMethodId], references: [id], onDelete: Cascade)
1052
+
1053
+ // Method-specific overrides for this property
1054
+ isDefault Boolean @default(false)
1055
+ isActive Boolean @default(true)
1056
+ createdAt DateTime @default(now())
1057
+
1058
+ @@id([propertyId, paymentMethodId])
1059
+ @@map("property_payment_method_links")
1060
+ }
1061
+
1062
+ // Phase template within a PropertyPaymentMethod (e.g., documentation, downpayment, mortgage)
1063
+ // phaseCategory determines the FSM type: DOCUMENTATION or PAYMENT
1064
+ model PropertyPaymentMethodPhase {
1065
+ id String @id @default(cuid())
1066
+ paymentMethodId String
1067
+ paymentMethod PropertyPaymentMethod @relation(fields: [paymentMethodId], references: [id], onDelete: Cascade)
1068
+ paymentPlanId String? // Only for PAYMENT phases
1069
+ paymentPlan PaymentPlan? @relation(fields: [paymentPlanId], references: [id])
1070
+
1071
+ name String
1072
+ description String? @db.Text
1073
+
1074
+ // Phase classification (DB-enforced enums)
1075
+ phaseCategory PhaseCategory
1076
+ phaseType PhaseType
1077
+ order Int
1078
+
1079
+ // Financial configuration (for PAYMENT phases)
1080
+ interestRate Float?
1081
+ percentOfPrice Float? // e.g., 10.0 for 10% downpayment
1082
+
1083
+ // Fund collection behavior (inherited from PaymentPlan if not set)
1084
+ // true = we collect funds via wallet/gateway (e.g., downpayment)
1085
+ // false = external payment, we only track/reconcile (e.g., bank mortgage)
1086
+ collectFunds Boolean? // null = inherit from PaymentPlan
1087
+
1088
+ // Activation rules
1089
+ requiresPreviousPhaseCompletion Boolean @default(true)
1090
+ minimumCompletionPercentage Float?
1091
+ completionCriterion CompletionCriterion?
1092
+
1093
+ // Snapshots for audit (original config at creation time)
1094
+ stepDefinitionsSnapshot Json?
1095
+ requiredDocumentSnapshot Json?
1096
+
1097
+ createdAt DateTime @default(now())
1098
+ updatedAt DateTime @updatedAt
1099
+
1100
+ // Normalized child tables (for DOCUMENTATION phases)
1101
+ steps PaymentMethodPhaseStep[]
1102
+ requiredDocuments PaymentMethodPhaseDocument[]
1103
+ // Normalized child tables (for QUESTIONNAIRE phases)
1104
+ questionnaireFields PaymentMethodPhaseField[]
1105
+ // Event attachments - handlers that fire on phase transitions
1106
+ eventAttachments PhaseEventAttachment[]
1107
+
1108
+ @@index([paymentMethodId])
1109
+ @@index([paymentPlanId])
1110
+ @@index([phaseCategory])
1111
+ @@map("property_payment_method_phases")
1112
+ }
1113
+
1114
+ /// Phase Event Attachment - Links event handlers to phase template triggers
1115
+ /// When a phase transitions (complete, payment received, etc.), attached handlers fire
1116
+ model PhaseEventAttachment {
1117
+ id String @id @default(cuid())
1118
+ phaseId String
1119
+ phase PropertyPaymentMethodPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
1120
+
1121
+ /// When this handler should fire
1122
+ trigger PhaseTrigger
1123
+
1124
+ /// The event handler to execute
1125
+ handlerId String
1126
+ handler EventHandler @relation(fields: [handlerId], references: [id], onDelete: Cascade)
1127
+
1128
+ /// Order of execution (lower = first)
1129
+ priority Int @default(100)
1130
+
1131
+ /// Whether this attachment is active
1132
+ enabled Boolean @default(true)
1133
+
1134
+ createdAt DateTime @default(now())
1135
+ updatedAt DateTime @updatedAt
1136
+
1137
+ @@unique([phaseId, handlerId, trigger])
1138
+ @@index([phaseId])
1139
+ @@index([handlerId])
1140
+ @@map("phase_event_attachments")
1141
+ }
1142
+
1143
+ // Step template within a DOCUMENTATION phase
1144
+ model PaymentMethodPhaseStep {
1145
+ id String @id @default(cuid())
1146
+ phaseId String
1147
+ phase PropertyPaymentMethodPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
1148
+
1149
+ name String
1150
+ stepType StepType
1151
+ order Int
1152
+
1153
+ metadata Json?
1154
+ createdAt DateTime @default(now())
1155
+ updatedAt DateTime @updatedAt
1156
+
1157
+ // Event attachments - handlers that fire on step transitions
1158
+ eventAttachments StepEventAttachment[]
1159
+
1160
+ @@index([phaseId])
1161
+ @@map("payment_method_phase_steps")
1162
+ }
1163
+
1164
+ /// Step Event Attachment - Links event handlers to step template triggers
1165
+ /// When a step transitions (complete, reject, etc.), attached handlers fire
1166
+ model StepEventAttachment {
1167
+ id String @id @default(cuid())
1168
+ stepId String
1169
+ step PaymentMethodPhaseStep @relation(fields: [stepId], references: [id], onDelete: Cascade)
1170
+
1171
+ /// When this handler should fire
1172
+ trigger StepTrigger
1173
+
1174
+ /// The event handler to execute
1175
+ handlerId String
1176
+ handler EventHandler @relation(fields: [handlerId], references: [id], onDelete: Cascade)
1177
+
1178
+ /// Order of execution (lower = first)
1179
+ priority Int @default(100)
1180
+
1181
+ /// Whether this attachment is active
1182
+ enabled Boolean @default(true)
1183
+
1184
+ createdAt DateTime @default(now())
1185
+ updatedAt DateTime @updatedAt
1186
+
1187
+ @@unique([stepId, handlerId, trigger])
1188
+ @@index([stepId])
1189
+ @@index([handlerId])
1190
+ @@map("step_event_attachments")
1191
+ }
1192
+
1193
+ // Required document within a DOCUMENTATION phase
1194
+ model PaymentMethodPhaseDocument {
1195
+ id String @id @default(cuid())
1196
+ phaseId String
1197
+ phase PropertyPaymentMethodPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
1198
+
1199
+ documentType String
1200
+ isRequired Boolean @default(true)
1201
+ description String? @db.Text
1202
+ allowedMimeTypes String? // CSV: application/pdf,image/jpeg
1203
+ maxSizeBytes Int?
1204
+
1205
+ metadata Json?
1206
+ createdAt DateTime @default(now())
1207
+
1208
+ @@index([phaseId, documentType])
1209
+ @@map("payment_method_phase_documents")
1210
+ }
1211
+
1212
+ // =============================================================================
1213
+ // QUESTIONNAIRE FIELD TYPES - For QUESTIONNAIRE phases
1214
+ // =============================================================================
1215
+
1216
+ enum FieldType {
1217
+ TEXT // Short text input
1218
+ TEXTAREA // Long text input
1219
+ NUMBER // Numeric input (with optional min/max)
1220
+ CURRENCY // Currency input (validated as money)
1221
+ EMAIL // Email validation
1222
+ PHONE // Phone number validation
1223
+ DATE // Date picker
1224
+ SELECT // Dropdown/single select
1225
+ MULTI_SELECT // Multiple selection
1226
+ CHECKBOX // Boolean yes/no
1227
+ RADIO // Radio button group
1228
+ FILE // File upload (single)
1229
+ }
1230
+
1231
+ // Questionnaire field template within a QUESTIONNAIRE phase
1232
+ model PaymentMethodPhaseField {
1233
+ id String @id @default(cuid())
1234
+ phaseId String
1235
+ phase PropertyPaymentMethodPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
1236
+
1237
+ // Field identification
1238
+ name String // Internal field name (e.g., "monthly_income")
1239
+ label String // Display label (e.g., "Monthly Income")
1240
+ description String? @db.Text // Help text for the field
1241
+ placeholder String? // Placeholder text
1242
+
1243
+ // Field configuration
1244
+ fieldType FieldType
1245
+ isRequired Boolean @default(true)
1246
+ order Int
1247
+
1248
+ // Validation rules (JSON schema-like)
1249
+ // Examples:
1250
+ // NUMBER: { "min": 0, "max": 1000000 }
1251
+ // TEXT: { "minLength": 2, "maxLength": 100, "pattern": "^[A-Za-z]+$" }
1252
+ // SELECT: { "options": [{"value": "employed", "label": "Employed"}, ...] }
1253
+ // DATE: { "minDate": "now", "maxDate": "+30d" }
1254
+ validation Json?
1255
+
1256
+ // For conditional display (e.g., show if another field has a certain value)
1257
+ // { "field": "employment_status", "operator": "equals", "value": "employed" }
1258
+ displayCondition Json?
1259
+
1260
+ // Default value (JSON to support any type)
1261
+ defaultValue Json?
1262
+
1263
+ createdAt DateTime @default(now())
1264
+ updatedAt DateTime @updatedAt
1265
+
1266
+ @@unique([phaseId, name])
1267
+ @@index([phaseId])
1268
+ @@map("payment_method_phase_fields")
1269
+ }
1270
+
1271
+ // =============================================================================
1272
+ // CONTRACT DOMAIN - Unified agreement model (replaces Mortgage, PurchasePlan, etc.)
1273
+ // =============================================================================
1274
+ // Contract is the canonical agreement. "Mortgage" is just a product configuration
1275
+ // that creates a Contract with specific phases (documentation, downpayment, long-term payment).
1276
+ // Phases can be QUESTIONNAIRE, DOCUMENTATION, or PAYMENT.
1277
+ // =============================================================================
1278
+
1279
+ model Contract {
1280
+ id String @id @default(cuid())
1281
+ tenantId String
1282
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
1283
+ // Link to specific unit being purchased/rented
1284
+ propertyUnitId String
1285
+ propertyUnit PropertyUnit @relation(fields: [propertyUnitId], references: [id], onDelete: Cascade)
1286
+ buyerId String
1287
+ buyer User @relation("ContractBuyer", fields: [buyerId], references: [id], onDelete: Cascade)
1288
+ sellerId String?
1289
+ seller User? @relation("ContractSeller", fields: [sellerId], references: [id])
1290
+ paymentMethodId String? // PropertyPaymentMethod used to create this contract
1291
+ paymentMethod PropertyPaymentMethod? @relation(fields: [paymentMethodId], references: [id])
1292
+
1293
+ // Contract identification
1294
+ contractNumber String @unique
1295
+ title String
1296
+ description String? @db.Text
1297
+ contractType String // Admin-defined: MORTGAGE, INSTALLMENT, RENT_TO_OWN, CASH, LEASE, etc.
1298
+
1299
+ // Contract value (negotiated from unit price)
1300
+ totalAmount Float
1301
+
1302
+ // FSM state (DB-enforced enum)
1303
+ status ContractStatus @default(DRAFT)
1304
+ currentPhaseId String?
1305
+ currentPhase ContractPhase? @relation("CurrentPhase", fields: [currentPhaseId], references: [id])
1306
+
1307
+ // Timing
1308
+ nextPaymentDueDate DateTime?
1309
+ lastReminderSentAt DateTime?
1310
+ startDate DateTime?
1311
+ endDate DateTime?
1312
+ signedAt DateTime?
1313
+ terminatedAt DateTime?
1314
+ createdAt DateTime @default(now())
1315
+ updatedAt DateTime @updatedAt
1316
+
1317
+ // Relations
1318
+ phases ContractPhase[]
1319
+ documents ContractDocument[]
1320
+ payments ContractPayment[]
1321
+ terminations ContractTermination[]
1322
+ offerLetters OfferLetter[]
1323
+
1324
+ // Payment method change requests for this contract
1325
+ paymentMethodChangeRequests PaymentMethodChangeRequest[]
1326
+
1327
+ // Transfer tracking - when a contract is transferred to a different property
1328
+ transferredFromId String? @unique // Source contract if this was created via transfer
1329
+ transferredFrom Contract? @relation("ContractTransfer", fields: [transferredFromId], references: [id])
1330
+ transferredTo Contract? @relation("ContractTransfer")
1331
+
1332
+ // Transfer requests where this contract is the source
1333
+ outgoingTransferRequests PropertyTransferRequest[] @relation("SourceContract")
1334
+ // Transfer requests where this contract is the target (created after approval)
1335
+ incomingTransferRequests PropertyTransferRequest[] @relation("TargetContract")
1336
+
1337
+ // Audit trail
1338
+ events ContractEvent[]
1339
+
1340
+ // Refund requests
1341
+ refunds ContractRefund[]
1342
+
1343
+ @@index([tenantId])
1344
+ @@index([propertyUnitId])
1345
+ @@index([buyerId])
1346
+ @@index([sellerId])
1347
+ @@index([paymentMethodId])
1348
+ @@index([status])
1349
+ @@index([currentPhaseId])
1350
+ @@map("contracts")
1351
+ }
1352
+
1353
+ // =============================================================================
1354
+ // CONTRACT REFUNDS - Track refund requests for overpayments or cancellations
1355
+ // =============================================================================
1356
+ model ContractRefund {
1357
+ id String @id @default(cuid())
1358
+ tenantId String
1359
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
1360
+
1361
+ contractId String
1362
+ contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
1363
+
1364
+ amount Float
1365
+ reason String @db.Text
1366
+ status RefundStatus @default(PENDING)
1367
+
1368
+ // Who requested the refund
1369
+ requestedById String
1370
+ requestedBy User @relation("RefundRequester", fields: [requestedById], references: [id])
1371
+
1372
+ // Who approved/rejected the refund
1373
+ approvedById String?
1374
+ approvedBy User? @relation("RefundApprover", fields: [approvedById], references: [id])
1375
+
1376
+ // Who processed the refund (finance team)
1377
+ processedById String?
1378
+ processedBy User? @relation("RefundProcessor", fields: [processedById], references: [id])
1379
+
1380
+ // Refund payment details
1381
+ paymentMethod String? // BANK_TRANSFER, CHEQUE, MOBILE_MONEY, etc.
1382
+ referenceNumber String? // Bank/payment reference
1383
+ recipientName String?
1384
+ recipientAccount String?
1385
+ recipientBank String?
1386
+
1387
+ // Timestamps
1388
+ requestedAt DateTime @default(now())
1389
+ approvedAt DateTime?
1390
+ rejectedAt DateTime?
1391
+ processedAt DateTime?
1392
+
1393
+ // Additional notes
1394
+ approvalNotes String? @db.Text
1395
+ rejectionNotes String? @db.Text
1396
+ processingNotes String? @db.Text
1397
+
1398
+ createdAt DateTime @default(now())
1399
+ updatedAt DateTime @updatedAt
1400
+
1401
+ @@index([contractId])
1402
+ @@index([status])
1403
+ @@index([tenantId])
1404
+ @@index([requestedById])
1405
+ @@map("contract_refunds")
1406
+ }
1407
+
1408
+ // Phase within a contract - can be DOCUMENTATION or PAYMENT type
1409
+ // Admin names phases freely (e.g., "KYC Documents", "Downpayment", "Monthly Mortgage")
1410
+ // =============================================================================
1411
+ // CONTRACT PHASE - Base model with polymorphic extensions
1412
+ // =============================================================================
1413
+ // ContractPhase is the base table with shared fields only.
1414
+ // Each phase has exactly ONE extension table based on phaseCategory:
1415
+ // - QUESTIONNAIRE → QuestionnairePhase
1416
+ // - DOCUMENTATION → DocumentationPhase
1417
+ // - PAYMENT → PaymentPhase
1418
+ // This eliminates nullable field pollution and makes the schema self-documenting.
1419
+ // =============================================================================
1420
+
1421
+ model ContractPhase {
1422
+ id String @id @default(cuid())
1423
+ contractId String
1424
+ contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
1425
+
1426
+ // Admin-defined naming
1427
+ name String
1428
+ description String? @db.Text
1429
+
1430
+ // Phase classification (DB-enforced enums)
1431
+ phaseCategory PhaseCategory
1432
+ phaseType PhaseType
1433
+ order Int
1434
+
1435
+ // FSM state for this phase (DB-enforced enum)
1436
+ status PhaseStatus @default(PENDING)
1437
+
1438
+ // Timing (shared across all phase types)
1439
+ dueDate DateTime?
1440
+ startDate DateTime?
1441
+ endDate DateTime?
1442
+ activatedAt DateTime?
1443
+ completedAt DateTime?
1444
+
1445
+ // Activation rules (shared)
1446
+ requiresPreviousPhaseCompletion Boolean @default(true)
1447
+
1448
+ createdAt DateTime @default(now())
1449
+ updatedAt DateTime @updatedAt
1450
+
1451
+ // Polymorphic extensions (exactly one will be populated based on phaseCategory)
1452
+ questionnairePhase QuestionnairePhase?
1453
+ documentationPhase DocumentationPhase?
1454
+ paymentPhase PaymentPhase?
1455
+
1456
+ // Payments can be linked to any phase type (for tracking)
1457
+ payments ContractPayment[]
1458
+
1459
+ // Back-relation for contracts where this is the current phase
1460
+ currentForContracts Contract[] @relation("CurrentPhase")
1461
+
1462
+ @@index([contractId])
1463
+ @@index([phaseCategory])
1464
+ @@index([status])
1465
+ @@index([order])
1466
+ @@map("contract_phases")
1467
+ }
1468
+
1469
+ // =============================================================================
1470
+ // QUESTIONNAIRE PHASE DATA - Extension for QUESTIONNAIRE phases
1471
+ // =============================================================================
1472
+ // Collects form data from users (eligibility, pre-approval, underwriting inputs)
1473
+ // =============================================================================
1474
+
1475
+ model QuestionnairePhase {
1476
+ id String @id @default(cuid())
1477
+ phaseId String @unique
1478
+ phase ContractPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
1479
+
1480
+ // Progress tracking
1481
+ completedFieldsCount Int @default(0)
1482
+ totalFieldsCount Int @default(0)
1483
+
1484
+ // Computed results (for UNDERWRITING phases)
1485
+ underwritingScore Float?
1486
+ debtToIncomeRatio Float?
1487
+ underwritingDecision String? // APPROVED, CONDITIONAL, DECLINED
1488
+ underwritingNotes String? @db.Text
1489
+
1490
+ // Snapshot of field definitions at creation
1491
+ fieldsSnapshot Json?
1492
+
1493
+ createdAt DateTime @default(now())
1494
+ updatedAt DateTime @updatedAt
1495
+
1496
+ // Child records
1497
+ fields QuestionnaireField[]
1498
+
1499
+ @@index([phaseId])
1500
+ @@map("questionnaire_phases")
1501
+ }
1502
+
1503
+ // =============================================================================
1504
+ // DOCUMENTATION PHASE DATA - Extension for DOCUMENTATION phases
1505
+ // =============================================================================
1506
+ // Manages document upload/approval workflow with FSM steps
1507
+ // =============================================================================
1508
+
1509
+ model DocumentationPhase {
1510
+ id String @id @default(cuid())
1511
+ phaseId String @unique
1512
+ phase ContractPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
1513
+
1514
+ // Current step pointer for UX and orchestration
1515
+ currentStepId String?
1516
+ currentStep DocumentationStep? @relation("CurrentStep", fields: [currentStepId], references: [id])
1517
+
1518
+ // Progress counters
1519
+ approvedDocumentsCount Int @default(0)
1520
+ requiredDocumentsCount Int @default(0)
1521
+ completedStepsCount Int @default(0)
1522
+ totalStepsCount Int @default(0)
1523
+
1524
+ // Completion criteria
1525
+ minimumCompletionPercentage Float?
1526
+ completionCriterion CompletionCriterion?
1527
+
1528
+ // Snapshots for audit
1529
+ stepDefinitionsSnapshot Json?
1530
+ requiredDocumentSnapshot Json?
1531
+
1532
+ createdAt DateTime @default(now())
1533
+ updatedAt DateTime @updatedAt
1534
+
1535
+ // Child records
1536
+ steps DocumentationStep[]
1537
+
1538
+ @@index([phaseId])
1539
+ @@index([currentStepId])
1540
+ @@map("documentation_phases")
1541
+ }
1542
+
1543
+ // =============================================================================
1544
+ // PAYMENT PHASE DATA - Extension for PAYMENT phases
1545
+ // =============================================================================
1546
+ // Manages installment-based payment collection
1547
+ // =============================================================================
1548
+
1549
+ model PaymentPhase {
1550
+ id String @id @default(cuid())
1551
+ phaseId String @unique
1552
+ phase ContractPhase @relation(fields: [phaseId], references: [id], onDelete: Cascade)
1553
+
1554
+ // Payment plan reference
1555
+ paymentPlanId String?
1556
+ paymentPlan PaymentPlan? @relation(fields: [paymentPlanId], references: [id])
1557
+
1558
+ // Financial details
1559
+ totalAmount Float
1560
+ paidAmount Float @default(0)
1561
+ interestRate Float @default(0)
1562
+
1563
+ // User-selected term (for flexible-term plans like mortgages)
1564
+ // These are set when user selects their preferred term at application time
1565
+ selectedTermMonths Int? // User's chosen term (e.g., 240 for 20 years)
1566
+ numberOfInstallments Int? // Calculated from selectedTermMonths and frequency
1567
+
1568
+ // Fund collection behavior
1569
+ // true = we collect funds via wallet/gateway (e.g., downpayment)
1570
+ // false = external payment, we only track/reconcile (e.g., bank mortgage)
1571
+ collectFunds Boolean @default(true)
1572
+
1573
+ // Completion criteria
1574
+ minimumCompletionPercentage Float?
1575
+
1576
+ // Snapshot for audit
1577
+ paymentPlanSnapshot Json?
1578
+
1579
+ createdAt DateTime @default(now())
1580
+ updatedAt DateTime @updatedAt
1581
+
1582
+ // Child records
1583
+ installments ContractInstallment[]
1584
+
1585
+ @@index([phaseId])
1586
+ @@index([paymentPlanId])
1587
+ @@map("payment_phases")
1588
+ }
1589
+
1590
+ // =============================================================================
1591
+ // QUESTIONNAIRE FIELDS - Instantiated fields within QUESTIONNAIRE phases
1592
+ // =============================================================================
1593
+ // When a contract is created from a payment method template, questionnaire field
1594
+ // templates are copied to QuestionnaireField records. Users submit answers here.
1595
+ // =============================================================================
1596
+
1597
+ model QuestionnaireField {
1598
+ id String @id @default(cuid())
1599
+ questionnairePhaseId String
1600
+ questionnairePhase QuestionnairePhase @relation(fields: [questionnairePhaseId], references: [id], onDelete: Cascade)
1601
+
1602
+ // Field identification (copied from template)
1603
+ name String // Internal field name (e.g., "monthly_income")
1604
+ label String // Display label (e.g., "Monthly Income")
1605
+ description String? @db.Text
1606
+ placeholder String?
1607
+
1608
+ // Field configuration (copied from template)
1609
+ fieldType FieldType
1610
+ isRequired Boolean @default(true)
1611
+ order Int
1612
+
1613
+ // Validation rules (copied from template)
1614
+ validation Json?
1615
+ displayCondition Json?
1616
+ defaultValue Json?
1617
+
1618
+ // User's submitted answer (JSON to support any type)
1619
+ // TEXT: "John Doe"
1620
+ // NUMBER/CURRENCY: 500000
1621
+ // SELECT: "employed"
1622
+ // MULTI_SELECT: ["option1", "option2"]
1623
+ // CHECKBOX: true
1624
+ // DATE: "2024-01-15"
1625
+ answer Json?
1626
+
1627
+ // Validation status
1628
+ isValid Boolean @default(false)
1629
+ validationErrors Json? // Array of error messages if invalid
1630
+
1631
+ // Timestamps
1632
+ submittedAt DateTime?
1633
+ createdAt DateTime @default(now())
1634
+ updatedAt DateTime @updatedAt
1635
+
1636
+ @@unique([questionnairePhaseId, name])
1637
+ @@index([questionnairePhaseId])
1638
+ @@map("questionnaire_fields")
1639
+ }
1640
+
1641
+ // =============================================================================
1642
+ // CONTRACT EVENTS - Audit trail for contract lifecycle
1643
+ // =============================================================================
1644
+ // Tracks all significant events in a contract's lifecycle for audit, compliance,
1645
+ // and debugging. Unlike DomainEvent (which is for inter-service communication),
1646
+ // ContractEvent is purely for historical tracking and state machine transitions.
1647
+ // =============================================================================
1648
+ model ContractEvent {
1649
+ id String @id @default(cuid())
1650
+ contractId String
1651
+ contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
1652
+
1653
+ // Event classification
1654
+ eventType ContractEventType
1655
+ eventGroup ContractEventGroup?
1656
+
1657
+ // For state transitions (optional - only populated for CONTRACT_STATE_CHANGED events)
1658
+ fromState String?
1659
+ toState String?
1660
+ trigger String?
1661
+
1662
+ // Event payload (all event-specific data)
1663
+ data Json?
1664
+
1665
+ // Actor tracking
1666
+ actorId String?
1667
+ actorType EventActorType?
1668
+
1669
+ // Timing
1670
+ occurredAt DateTime @default(now())
1671
+
1672
+ @@index([contractId])
1673
+ @@index([eventType])
1674
+ @@index([eventGroup])
1675
+ @@index([occurredAt])
1676
+ @@map("contract_events")
1677
+ }
1678
+
1679
+ // Steps within a DOCUMENTATION phase (FSM for document collection/approval)
1680
+ model DocumentationStep {
1681
+ id String @id @default(cuid())
1682
+ documentationPhaseId String
1683
+ documentationPhase DocumentationPhase @relation(fields: [documentationPhaseId], references: [id], onDelete: Cascade)
1684
+
1685
+ name String
1686
+ description String? @db.Text
1687
+ stepType StepType
1688
+ order Int
1689
+
1690
+ status StepStatus @default(PENDING)
1691
+
1692
+ // =========================================================================
1693
+ // USER ACTION TRACKING - For rejection/resubmission flows
1694
+ // =========================================================================
1695
+ // When status is NEEDS_RESUBMISSION or ACTION_REQUIRED, this explains why.
1696
+ // Populated from DocumentationStepApproval.comment on rejection.
1697
+ actionReason String? @db.Text
1698
+
1699
+ // Number of times this step has been submitted (for tracking resubmissions)
1700
+ submissionCount Int @default(0)
1701
+
1702
+ // Last submission timestamp (for tracking resubmission timing)
1703
+ lastSubmittedAt DateTime?
1704
+
1705
+ // Configuration metadata (for GENERATE_DOCUMENT steps, etc.)
1706
+ metadata Json?
1707
+
1708
+ // Assignment
1709
+ assigneeId String?
1710
+ assignee User? @relation("DocumentationStepAssignee", fields: [assigneeId], references: [id])
1711
+
1712
+ // Required document types for UPLOAD steps (normalized)
1713
+ requiredDocuments DocumentationStepDocument[]
1714
+
1715
+ // Timing
1716
+ dueDate DateTime?
1717
+ completedAt DateTime?
1718
+
1719
+ createdAt DateTime @default(now())
1720
+ updatedAt DateTime @updatedAt
1721
+
1722
+ approvals DocumentationStepApproval[]
1723
+ currentForPhase DocumentationPhase[] @relation("CurrentStep")
1724
+
1725
+ @@index([documentationPhaseId])
1726
+ @@index([status])
1727
+ @@index([order])
1728
+ @@map("documentation_steps")
1729
+ }
1730
+
1731
+ // Required documents for a step (normalized from CSV)
1732
+ model DocumentationStepDocument {
1733
+ id String @id @default(cuid())
1734
+ stepId String
1735
+ step DocumentationStep @relation(fields: [stepId], references: [id], onDelete: Cascade)
1736
+
1737
+ documentType String
1738
+ isRequired Boolean @default(true)
1739
+
1740
+ createdAt DateTime @default(now())
1741
+
1742
+ @@index([stepId, documentType])
1743
+ @@map("documentation_step_documents")
1744
+ }
1745
+
1746
+ // Approvals for documentation steps
1747
+ model DocumentationStepApproval {
1748
+ id String @id @default(cuid())
1749
+ stepId String
1750
+ step DocumentationStep @relation(fields: [stepId], references: [id], onDelete: Cascade)
1751
+ approverId String?
1752
+ approver User? @relation("DocumentationStepApprover", fields: [approverId], references: [id])
1753
+
1754
+ decision ApprovalDecision
1755
+ comment String? @db.Text
1756
+ decidedAt DateTime @default(now())
1757
+
1758
+ createdAt DateTime @default(now())
1759
+
1760
+ @@index([stepId])
1761
+ @@map("documentation_step_approvals")
1762
+ }
1763
+
1764
+ // Installments within a PAYMENT phase
1765
+ model ContractInstallment {
1766
+ id String @id @default(cuid())
1767
+ paymentPhaseId String
1768
+ paymentPhase PaymentPhase @relation(fields: [paymentPhaseId], references: [id], onDelete: Cascade)
1769
+
1770
+ installmentNumber Int
1771
+
1772
+ amount Float
1773
+ principalAmount Float @default(0)
1774
+ interestAmount Float @default(0)
1775
+
1776
+ dueDate DateTime
1777
+ status InstallmentStatus @default(PENDING)
1778
+
1779
+ paidAmount Float @default(0)
1780
+ paidDate DateTime?
1781
+
1782
+ lateFee Float @default(0)
1783
+ lateFeeWaived Boolean @default(false)
1784
+ gracePeriodDays Int @default(0)
1785
+ gracePeriodEndDate DateTime?
1786
+
1787
+ createdAt DateTime @default(now())
1788
+ updatedAt DateTime @updatedAt
1789
+
1790
+ payments ContractPayment[]
1791
+
1792
+ @@index([paymentPhaseId])
1793
+ @@index([dueDate])
1794
+ @@index([status])
1795
+ @@map("contract_installments")
1796
+ }
1797
+
1798
+ // Unified payment record for contracts
1799
+ model ContractPayment {
1800
+ id String @id @default(cuid())
1801
+ contractId String
1802
+ contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
1803
+ phaseId String?
1804
+ phase ContractPhase? @relation(fields: [phaseId], references: [id])
1805
+ installmentId String?
1806
+ installment ContractInstallment? @relation(fields: [installmentId], references: [id])
1807
+ payerId String?
1808
+ payer User? @relation("ContractPayer", fields: [payerId], references: [id])
1809
+
1810
+ amount Float
1811
+ principalAmount Float @default(0)
1812
+ interestAmount Float @default(0)
1813
+ lateFeeAmount Float @default(0)
1814
+
1815
+ paymentMethod String // BANK_TRANSFER, CREDIT_CARD, WALLET, CASH, CHECK
1816
+ status PaymentStatus @default(INITIATED)
1817
+
1818
+ reference String? @unique
1819
+ gatewayResponse String? @db.Text // JSON
1820
+
1821
+ processedAt DateTime?
1822
+ createdAt DateTime @default(now())
1823
+ updatedAt DateTime @updatedAt
1824
+
1825
+ @@index([contractId])
1826
+ @@index([phaseId])
1827
+ @@index([installmentId])
1828
+ @@index([payerId])
1829
+ @@index([status])
1830
+ @@index([reference])
1831
+ @@map("contract_payments")
1832
+ }
1833
+
1834
+ // Contract documents (owned by contract, linked to phases/steps as needed)
1835
+ model ContractDocument {
1836
+ id String @id @default(cuid())
1837
+ contractId String
1838
+ contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
1839
+ phaseId String? // Optional link to specific phase
1840
+ stepId String? // Optional link to specific step
1841
+
1842
+ name String
1843
+ url String
1844
+ type String // ID, BANK_STATEMENT, INCOME_PROOF, TITLE_DEED, SIGNATURE, etc.
1845
+ uploadedById String?
1846
+ uploadedBy User? @relation("DocumentUploader", fields: [uploadedById], references: [id])
1847
+
1848
+ status DocumentStatus @default(PENDING)
1849
+
1850
+ createdAt DateTime @default(now())
1851
+ updatedAt DateTime @updatedAt
1852
+
1853
+ @@index([contractId])
1854
+ @@index([phaseId])
1855
+ @@index([stepId])
1856
+ @@index([type])
1857
+ @@index([status])
1858
+ @@map("contract_documents")
1859
+ }
1860
+
1861
+ // =============================================================================
1862
+ // OFFER LETTERS - Provisional and Final offer documents
1863
+ // =============================================================================
1864
+
1865
+ model DocumentTemplate {
1866
+ id String @id @default(cuid())
1867
+ tenantId String
1868
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
1869
+
1870
+ name String // "Provisional Offer Letter", "Final Offer Letter"
1871
+ code String // PROVISIONAL_OFFER, FINAL_OFFER
1872
+ description String?
1873
+ version Int @default(1)
1874
+
1875
+ // Template content (Handlebars)
1876
+ htmlTemplate String @db.Text
1877
+ cssStyles String? @db.Text
1878
+
1879
+ // Merge field definitions for UI
1880
+ mergeFields Json? // [{name, type, required, description}]
1881
+
1882
+ isActive Boolean @default(true)
1883
+ isDefault Boolean @default(false)
1884
+
1885
+ createdAt DateTime @default(now())
1886
+ updatedAt DateTime @updatedAt
1887
+
1888
+ offerLetters OfferLetter[]
1889
+
1890
+ @@unique([tenantId, code, version])
1891
+ @@index([tenantId])
1892
+ @@index([code])
1893
+ @@map("document_templates")
1894
+ }
1895
+
1896
+ model OfferLetter {
1897
+ id String @id @default(cuid())
1898
+ tenantId String
1899
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
1900
+ contractId String
1901
+ contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
1902
+
1903
+ // Template used (optional - documents-service may handle default selection)
1904
+ templateId String?
1905
+ template DocumentTemplate? @relation(fields: [templateId], references: [id])
1906
+
1907
+ // Letter details
1908
+ letterNumber String @unique // OL-XXXXXX
1909
+ type OfferLetterType
1910
+ status OfferLetterStatus @default(DRAFT)
1911
+
1912
+ // Generated document
1913
+ htmlContent String? @db.Text // Rendered HTML
1914
+ pdfUrl String? // S3 URL of generated PDF
1915
+ pdfKey String? // S3 key for deletion/access
1916
+
1917
+ // Merge data used (snapshot for audit)
1918
+ mergeData Json? // All data merged into template
1919
+
1920
+ // Signing workflow
1921
+ sentAt DateTime?
1922
+ viewedAt DateTime?
1923
+ signedAt DateTime?
1924
+ signatureIp String?
1925
+ signatureData Json? // {method, timestamp, metadata}
1926
+
1927
+ // Validity
1928
+ expiresAt DateTime?
1929
+ expiredAt DateTime?
1930
+ cancelledAt DateTime?
1931
+ cancelReason String?
1932
+
1933
+ // Audit
1934
+ generatedById String?
1935
+ generatedBy User? @relation("OfferLetterGenerator", fields: [generatedById], references: [id])
1936
+ sentById String?
1937
+ sentBy User? @relation("OfferLetterSender", fields: [sentById], references: [id])
1938
+
1939
+ createdAt DateTime @default(now())
1940
+ updatedAt DateTime @updatedAt
1941
+
1942
+ @@index([tenantId])
1943
+ @@index([contractId])
1944
+ @@index([type])
1945
+ @@index([status])
1946
+ @@map("offer_letters")
1947
+ }
1948
+
1949
+ // =============================================================================
1950
+ // CONTRACT TERMINATION - Full lifecycle for cancellation/termination
1951
+ // =============================================================================
1952
+ // Tracks termination requests from initiation through refund completion.
1953
+ // Industry-standard flow:
1954
+ // 1. Request created (by buyer/seller/admin/system)
1955
+ // 2. Admin reviews (if required by policy)
1956
+ // 3. Financial settlement calculated (refunds, penalties, forfeitures)
1957
+ // 4. Refund processed (if applicable)
1958
+ // 5. Contract marked terminated, unit released
1959
+ // =============================================================================
1960
+
1961
+ model ContractTermination {
1962
+ id String @id @default(cuid())
1963
+ contractId String
1964
+ contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
1965
+ tenantId String
1966
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
1967
+
1968
+ // Request identification
1969
+ requestNumber String @unique // TRM-XXXXXX
1970
+
1971
+ // Who initiated and why
1972
+ initiatedBy TerminationInitiator
1973
+ initiatorId String? // userId if BUYER/SELLER/ADMIN
1974
+ initiator User? @relation("TerminationInitiator", fields: [initiatorId], references: [id])
1975
+ type TerminationType
1976
+ reason String? @db.Text
1977
+ supportingDocs Json? // [{type, url, uploadedAt}]
1978
+
1979
+ // Workflow status
1980
+ status TerminationStatus @default(REQUESTED)
1981
+ requiresApproval Boolean @default(true)
1982
+ autoApproveEligible Boolean @default(false) // Pre-signature, no payments
1983
+
1984
+ // Admin review
1985
+ reviewedBy String?
1986
+ reviewer User? @relation("TerminationReviewer", fields: [reviewedBy], references: [id])
1987
+ reviewedAt DateTime?
1988
+ reviewNotes String? @db.Text
1989
+ rejectionReason String? @db.Text
1990
+
1991
+ // Financial snapshot at time of request
1992
+ contractSnapshot Json // Full contract state snapshot
1993
+ totalContractAmount Float
1994
+ totalPaidToDate Float
1995
+ outstandingBalance Float
1996
+
1997
+ // Settlement calculation
1998
+ refundableAmount Float @default(0) // Amount eligible for refund
1999
+ penaltyAmount Float @default(0) // Penalties/fees to deduct
2000
+ forfeitedAmount Float @default(0) // Amount forfeited (non-refundable deposits)
2001
+ adminFeeAmount Float @default(0) // Processing fees
2002
+ netRefundAmount Float @default(0) // refundableAmount - penaltyAmount - adminFeeAmount
2003
+ settlementNotes String? @db.Text
2004
+
2005
+ // Refund processing
2006
+ refundStatus RefundStatus @default(PENDING)
2007
+ refundReference String? // Payment gateway reference
2008
+ refundMethod String? // ORIGINAL_METHOD, BANK_TRANSFER, CHECK, WALLET
2009
+ refundAccountDetails Json? // Encrypted bank details if needed
2010
+ refundInitiatedAt DateTime?
2011
+ refundCompletedAt DateTime?
2012
+ refundFailureReason String? @db.Text
2013
+
2014
+ // Property unit handling
2015
+ unitReleasedAt DateTime?
2016
+ unitReservedForId String? // If unit being held for another buyer
2017
+
2018
+ // Timing
2019
+ requestedAt DateTime @default(now())
2020
+ approvedAt DateTime?
2021
+ executedAt DateTime?
2022
+ completedAt DateTime?
2023
+ cancelledAt DateTime?
2024
+
2025
+ // Idempotency and audit
2026
+ idempotencyKey String? @unique
2027
+ metadata Json?
2028
+
2029
+ createdAt DateTime @default(now())
2030
+ updatedAt DateTime @updatedAt
2031
+
2032
+ @@index([contractId])
2033
+ @@index([tenantId])
2034
+ @@index([status])
2035
+ @@index([type])
2036
+ @@index([initiatorId])
2037
+ @@index([requestedAt])
2038
+ @@map("contract_terminations")
2039
+ }
2040
+
2041
+ // =============================================================================
2042
+ // PAYMENT METHOD CHANGE REQUEST - Mid-contract payment method changes
2043
+ // =============================================================================
2044
+ // When a user wants to change their payment method after contract creation,
2045
+ // this aggregate tracks the request, required documentation, approvals, and
2046
+ // final execution. Different from-to combinations may require different docs.
2047
+ // =============================================================================
2048
+
2049
+ enum PaymentMethodChangeStatus {
2050
+ PENDING_DOCUMENTS
2051
+ DOCUMENTS_SUBMITTED
2052
+ UNDER_REVIEW
2053
+ APPROVED
2054
+ REJECTED
2055
+ EXECUTED
2056
+ CANCELLED
2057
+ }
2058
+
2059
+ model PaymentMethodChangeRequest {
2060
+ id String @id @default(cuid())
2061
+ tenantId String
2062
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
2063
+ contractId String
2064
+ contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
2065
+
2066
+ // The change being requested
2067
+ fromPaymentMethodId String
2068
+ fromPaymentMethod PropertyPaymentMethod @relation("ChangeFromMethod", fields: [fromPaymentMethodId], references: [id])
2069
+ toPaymentMethodId String
2070
+ toPaymentMethod PropertyPaymentMethod @relation("ChangeToMethod", fields: [toPaymentMethodId], references: [id])
2071
+
2072
+ // Who requested and why
2073
+ requestorId String
2074
+ requestor User @relation("ChangeRequestor", fields: [requestorId], references: [id])
2075
+ reason String? @db.Text
2076
+
2077
+ // Documentation requirements (determined by DocumentRequirementRule)
2078
+ requiredDocumentTypes String? // CSV: BANK_STATEMENT,INCOME_PROOF,NEW_EMPLOYER_LETTER
2079
+ submittedDocuments Json? // [{type, s3Key, uploadedAt, status}]
2080
+
2081
+ // Financial impact assessment
2082
+ currentOutstanding Float? // Outstanding balance at time of request
2083
+ newTermMonths Int? // New term if applicable
2084
+ newInterestRate Float? // New rate if applicable
2085
+ newMonthlyPayment Float? // Projected new payment
2086
+ penaltyAmount Float? // Early change penalty if applicable
2087
+ financialImpactNotes String? @db.Text
2088
+
2089
+ // Status and workflow
2090
+ status PaymentMethodChangeStatus @default(PENDING_DOCUMENTS)
2091
+ reviewerId String?
2092
+ reviewer User? @relation("ChangeReviewer", fields: [reviewerId], references: [id])
2093
+ reviewNotes String? @db.Text
2094
+ reviewedAt DateTime?
2095
+
2096
+ // Execution details
2097
+ executedAt DateTime?
2098
+ previousPhaseData Json? // Snapshot of phases before change
2099
+ newPhaseData Json? // New phases created after change
2100
+
2101
+ createdAt DateTime @default(now())
2102
+ updatedAt DateTime @updatedAt
2103
+
2104
+ @@index([tenantId])
2105
+ @@index([contractId])
2106
+ @@index([status])
2107
+ @@index([requestorId])
2108
+ @@map("payment_method_change_requests")
2109
+ }
2110
+
2111
+ // =============================================================================
2112
+ // DOCUMENT REQUIREMENT RULES - Configurable document requirements
2113
+ // =============================================================================
2114
+ // Admins can configure which documents are required for specific scenarios:
2115
+ // - Prequalification for a payment method type
2116
+ // - Contract phases
2117
+ // - Payment method changes (from-to combinations)
2118
+ // This allows tenants to customize documentation workflows per product.
2119
+ // =============================================================================
2120
+
2121
+ enum DocumentRequirementContext {
2122
+ CONTRACT_PHASE // During a contract phase
2123
+ PAYMENT_METHOD_CHANGE // When changing payment method mid-contract
2124
+ }
2125
+
2126
+ model DocumentRequirementRule {
2127
+ id String @id @default(cuid())
2128
+ tenantId String
2129
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
2130
+
2131
+ // Rule context
2132
+ context DocumentRequirementContext
2133
+
2134
+ // Scoping (which situations this rule applies to)
2135
+ // For PREQUALIFICATION: paymentMethodId
2136
+ // For CONTRACT_PHASE: phaseType
2137
+ // For PAYMENT_METHOD_CHANGE: fromMethodId + toMethodId
2138
+ paymentMethodId String?
2139
+ paymentMethod PropertyPaymentMethod? @relation("RulePaymentMethod", fields: [paymentMethodId], references: [id])
2140
+ phaseType String? // KYC, VERIFICATION, DOWNPAYMENT, etc.
2141
+ fromPaymentMethodId String?
2142
+ fromPaymentMethod PropertyPaymentMethod? @relation("RuleFromMethod", fields: [fromPaymentMethodId], references: [id])
2143
+ toPaymentMethodId String?
2144
+ toPaymentMethod PropertyPaymentMethod? @relation("RuleToMethod", fields: [toPaymentMethodId], references: [id])
2145
+
2146
+ // Document requirements
2147
+ documentType String // ID_CARD, PASSPORT, BANK_STATEMENT, INCOME_PROOF, etc.
2148
+ isRequired Boolean @default(true)
2149
+ description String? // Instructions for the user
2150
+ maxSizeBytes Int? // Max file size allowed
2151
+ allowedMimeTypes String? // CSV: application/pdf,image/jpeg,image/png
2152
+
2153
+ // Validation rules
2154
+ expiryDays Int? // Document must not be older than X days
2155
+ requiresManualReview Boolean @default(false)
2156
+
2157
+ isActive Boolean @default(true)
2158
+ createdAt DateTime @default(now())
2159
+ updatedAt DateTime @updatedAt
2160
+
2161
+ @@index([tenantId])
2162
+ @@index([context])
2163
+ @@index([paymentMethodId])
2164
+ @@index([phaseType])
2165
+ @@index([fromPaymentMethodId, toPaymentMethodId])
2166
+ @@map("document_requirement_rules")
2167
+ }
2168
+
2169
+ // =============================================================================
2170
+ // EVENT-DRIVEN WORKFLOW CONFIGURATION
2171
+ // =============================================================================
2172
+ // This system allows admins to configure event channels, types, and handlers
2173
+ // for a flexible, configurable event-driven workflow system.
2174
+ //
2175
+ // Architecture:
2176
+ // 1. EventChannel - Logical grouping of events (e.g., "contracts", "payments")
2177
+ // 2. EventType - Specific event types (e.g., "DOCUMENT_UPLOADED", "STEP_COMPLETED")
2178
+ // 3. EventHandler - What to do when an event fires (webhook, internal call, etc.)
2179
+ // 4. WorkflowEvent - Actual event instances (audit log)
2180
+ // 5. EventHandlerExecution - Log of handler executions
2181
+ // =============================================================================
2182
+
2183
+ /// Event Channel - A logical grouping of events
2184
+ /// Channels help organize events and route them to appropriate handlers
2185
+ model EventChannel {
2186
+ id String @id @default(cuid())
2187
+ tenantId String
2188
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
2189
+
2190
+ /// Unique code for the channel (e.g., "CONTRACTS", "PAYMENTS")
2191
+ code String
2192
+ /// Human-readable name
2193
+ name String
2194
+ /// Description of what this channel handles
2195
+ description String? @db.Text
2196
+
2197
+ /// Whether this channel is active
2198
+ enabled Boolean @default(true)
2199
+
2200
+ /// Event types that belong to this channel
2201
+ eventTypes EventType[]
2202
+
2203
+ createdAt DateTime @default(now())
2204
+ updatedAt DateTime @updatedAt
2205
+
2206
+ @@unique([tenantId, code])
2207
+ @@index([tenantId])
2208
+ @@map("event_channels")
2209
+ }
2210
+
2211
+ /// Event Type - Defines a type of event that can occur
2212
+ /// Each event type belongs to a channel and can have multiple handlers
2213
+ model EventType {
2214
+ id String @id @default(cuid())
2215
+ tenantId String
2216
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
2217
+
2218
+ /// The channel this event type belongs to
2219
+ channelId String
2220
+ channel EventChannel @relation(fields: [channelId], references: [id], onDelete: Cascade)
2221
+
2222
+ /// Unique code for this event type (e.g., "DOCUMENT_UPLOADED")
2223
+ code String
2224
+ /// Human-readable name
2225
+ name String
2226
+ /// Description of when this event fires
2227
+ description String? @db.Text
2228
+
2229
+ /// JSON schema for event payload validation (optional)
2230
+ payloadSchema Json?
2231
+
2232
+ /// Whether this event type is active
2233
+ enabled Boolean @default(true)
2234
+
2235
+ /// Handlers subscribed to this event type
2236
+ handlers EventHandler[]
2237
+
2238
+ /// Actual event instances of this type
2239
+ events WorkflowEvent[]
2240
+
2241
+ createdAt DateTime @default(now())
2242
+ updatedAt DateTime @updatedAt
2243
+
2244
+ @@unique([tenantId, code])
2245
+ @@unique([channelId, code])
2246
+ @@index([tenantId])
2247
+ @@index([channelId])
2248
+ @@map("event_types")
2249
+ }
2250
+
2251
+ /// Event Handler - Defines what should happen when an event fires
2252
+ /// Handlers can be internal (call a service), external (webhook), or workflow triggers
2253
+ model EventHandler {
2254
+ id String @id @default(cuid())
2255
+ tenantId String
2256
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
2257
+
2258
+ /// The event type this handler responds to
2259
+ eventTypeId String
2260
+ eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
2261
+
2262
+ /// Human-readable name
2263
+ name String
2264
+ /// Description of what this handler does
2265
+ description String? @db.Text
2266
+
2267
+ /// Handler type determines how the event is processed
2268
+ handlerType EventHandlerType
2269
+
2270
+ /// Configuration for the handler (JSON, depends on handlerType)
2271
+ /// INTERNAL: { "service": "contract", "method": "completeStep" }
2272
+ /// WEBHOOK: { "url": "https://...", "method": "POST", "headers": {...} }
2273
+ /// WORKFLOW: { "workflowId": "...", "action": "advance" }
2274
+ /// NOTIFICATION: { "template": "...", "channels": ["email", "sms"] }
2275
+ config Json
2276
+
2277
+ /// Order of execution when multiple handlers exist (lower = first)
2278
+ priority Int @default(100)
2279
+
2280
+ /// Whether this handler is active
2281
+ enabled Boolean @default(true)
2282
+
2283
+ /// Retry configuration
2284
+ maxRetries Int @default(3)
2285
+ retryDelayMs Int @default(1000)
2286
+
2287
+ /// Filter condition (JSONPath expression) to conditionally run
2288
+ /// e.g., "$.payload.status == 'approved'"
2289
+ filterCondition String? @db.Text
2290
+
2291
+ /// Handler execution logs
2292
+ executions EventHandlerExecution[]
2293
+
2294
+ /// Step attachments - steps that have attached this handler
2295
+ stepAttachments StepEventAttachment[]
2296
+
2297
+ /// Phase attachments - phases that have attached this handler
2298
+ phaseAttachments PhaseEventAttachment[]
2299
+
2300
+ createdAt DateTime @default(now())
2301
+ updatedAt DateTime @updatedAt
2302
+
2303
+ @@index([tenantId])
2304
+ @@index([eventTypeId])
2305
+ @@index([handlerType])
2306
+ @@map("event_handlers")
2307
+ }
2308
+
2309
+ /// Workflow Event - An actual event instance that occurred
2310
+ /// This is the audit log of all events in the system
2311
+ model WorkflowEvent {
2312
+ id String @id @default(cuid())
2313
+ tenantId String
2314
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
2315
+
2316
+ /// The type of this event
2317
+ eventTypeId String
2318
+ eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
2319
+
2320
+ /// The event payload (actual data)
2321
+ payload Json
2322
+
2323
+ /// Optional correlation ID to link related events
2324
+ correlationId String?
2325
+
2326
+ /// Optional causation ID (which event caused this one)
2327
+ causationId String?
2328
+
2329
+ /// Source of the event (service name, user action, etc.)
2330
+ source String
2331
+
2332
+ /// Actor who triggered the event (user ID, API key ID, "system")
2333
+ actorId String?
2334
+ actorType ActorType @default(SYSTEM)
2335
+
2336
+ /// Event status
2337
+ status WorkflowEventStatus @default(PENDING)
2338
+
2339
+ /// Error message if processing failed
2340
+ error String? @db.Text
2341
+
2342
+ /// When the event was processed
2343
+ processedAt DateTime?
2344
+
2345
+ /// Handler executions for this event
2346
+ executions EventHandlerExecution[]
2347
+
2348
+ createdAt DateTime @default(now())
2349
+
2350
+ @@index([tenantId])
2351
+ @@index([eventTypeId])
2352
+ @@index([correlationId])
2353
+ @@index([causationId])
2354
+ @@index([status])
2355
+ @@index([createdAt])
2356
+ @@map("workflow_events")
2357
+ }
2358
+
2359
+ /// Event Handler Execution - Log of a handler processing an event
2360
+ model EventHandlerExecution {
2361
+ id String @id @default(cuid())
2362
+
2363
+ /// The event being processed
2364
+ eventId String
2365
+ event WorkflowEvent @relation(fields: [eventId], references: [id], onDelete: Cascade)
2366
+
2367
+ /// The handler that processed this event
2368
+ handlerId String
2369
+ handler EventHandler @relation(fields: [handlerId], references: [id], onDelete: Cascade)
2370
+
2371
+ /// Execution status
2372
+ status ExecutionStatus @default(PENDING)
2373
+
2374
+ /// Attempt number (1 for first try, increments on retry)
2375
+ attempt Int @default(1)
2376
+
2377
+ /// Input to the handler (may be transformed payload)
2378
+ input Json?
2379
+
2380
+ /// Output from the handler
2381
+ output Json?
2382
+
2383
+ /// Error details if failed
2384
+ error String? @db.Text
2385
+ errorCode String?
2386
+
2387
+ /// Timing
2388
+ startedAt DateTime?
2389
+ completedAt DateTime?
2390
+ durationMs Int?
2391
+
2392
+ createdAt DateTime @default(now())
2393
+
2394
+ @@index([eventId])
2395
+ @@index([handlerId])
2396
+ @@index([status])
2397
+ @@map("event_handler_executions")
2398
+ }
2399
+
2400
+ // =============================================================================
2401
+ // EVENT OUTBOX - For guaranteed event delivery to SQS queues
2402
+ // =============================================================================
2403
+
2404
+ model DomainEvent {
2405
+ id String @id @default(cuid())
2406
+
2407
+ // Event identification
2408
+ eventType String // MORTGAGE.CREATED, PHASE.ACTIVATED, PAYMENT.COMPLETED, etc
2409
+ aggregateType String // Mortgage, MortgagePhase, MortgagePayment, Property, etc
2410
+ aggregateId String
2411
+
2412
+ // Routing - which queue(s) should receive this
2413
+ queueName String // notifications, payments, mortgage-steps, accounting, etc
2414
+
2415
+ // Event payload (all data needed by consumers)
2416
+ payload String @db.Text // JSON
2417
+
2418
+ // Metadata
2419
+ occurredAt DateTime @default(now())
2420
+ actorId String? // User who triggered the event
2421
+ actorRole String? // Role of the actor
2422
+
2423
+ // Processing status
2424
+ status String @default("PENDING") // PENDING, PROCESSING, SENT, FAILED
2425
+ processedAt DateTime?
2426
+ sentAt DateTime?
2427
+ failureCount Int @default(0)
2428
+ lastError String? @db.Text
2429
+ nextRetryAt DateTime?
2430
+
2431
+ createdAt DateTime @default(now())
2432
+ updatedAt DateTime @updatedAt
2433
+
2434
+ @@index([status, nextRetryAt])
2435
+ @@index([eventType])
2436
+ @@index([aggregateType, aggregateId])
2437
+ @@index([queueName])
2438
+ @@index([occurredAt])
2439
+ @@map("domain_events")
2440
+ }
2441
+
2442
+ // =============================================================================
2443
+ // Property Transfer Request
2444
+ // =============================================================================
2445
+ // Allows a buyer to request transferring their contract to a different property.
2446
+ //
2447
+ // TRANSFER PAYMENT FLOW:
2448
+ // 1. All payments from source contract are refunded to buyer's wallet (single credit transaction)
2449
+ // 2. Source contract marked TRANSFERRED, unit released
2450
+ // 3. New contract created for target unit with zero payments
2451
+ // 4. Buyer can optionally apply wallet balance as equity during new mortgage application
2452
+ //
2453
+ // This ensures clean accounting: each contract has isolated payment history,
2454
+ // wallet serves as intermediary, and buyer retains full credit.
2455
+ // =============================================================================
2456
+
2457
+ model PropertyTransferRequest {
2458
+ id String @id @default(cuid())
2459
+ tenantId String
2460
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
2461
+
2462
+ // Source contract being transferred
2463
+ sourceContractId String
2464
+ sourceContract Contract @relation("SourceContract", fields: [sourceContractId], references: [id], onDelete: Cascade)
2465
+
2466
+ // Target property unit
2467
+ targetPropertyUnitId String
2468
+ targetPropertyUnit PropertyUnit @relation(fields: [targetPropertyUnitId], references: [id])
2469
+
2470
+ // Requestor (buyer) and reviewer (admin)
2471
+ requestedById String
2472
+ requestedBy User @relation("TransferRequestor", fields: [requestedById], references: [id])
2473
+ reviewedById String?
2474
+ reviewedBy User? @relation("TransferReviewer", fields: [reviewedById], references: [id])
2475
+
2476
+ // Status and workflow
2477
+ status TransferRequestStatus @default(PENDING)
2478
+
2479
+ // Request details
2480
+ reason String? @db.Text // Buyer's reason for transfer
2481
+
2482
+ // Review details
2483
+ reviewNotes String? @db.Text // Admin notes on decision
2484
+ priceAdjustmentHandling String? // How to handle price difference: ADD_TO_MORTGAGE, REQUIRE_PAYMENT, CREDIT_BUYER
2485
+
2486
+ // Computed values
2487
+ sourceTotalAmount Float? // Original contract total
2488
+ targetTotalAmount Float? // New contract total (based on target property)
2489
+ priceAdjustment Float? // Difference (positive = buyer owes more)
2490
+
2491
+ // Wallet refund tracking (payments refunded to wallet, NOT carried over)
2492
+ refundedAmount Float? // Total amount refunded to wallet
2493
+ refundTransactionId String? // Reference to wallet Transaction record
2494
+ refundedAt DateTime?
2495
+
2496
+ // Result - new contract created after approval
2497
+ targetContractId String?
2498
+ targetContract Contract? @relation("TargetContract", fields: [targetContractId], references: [id])
2499
+
2500
+ // Timestamps
2501
+ createdAt DateTime @default(now())
2502
+ reviewedAt DateTime?
2503
+ completedAt DateTime?
2504
+ updatedAt DateTime @updatedAt
2505
+
2506
+ @@index([tenantId])
2507
+ @@index([sourceContractId])
2508
+ @@index([targetPropertyUnitId])
2509
+ @@index([requestedById])
2510
+ @@index([status])
2511
+ @@map("property_transfer_requests")
2512
+ }
2513
+
2514
+ // =============================================================================
2515
+ // UNIFIED APPROVAL REQUESTS
2516
+ // =============================================================================
2517
+
2518
+ enum ApprovalRequestType {
2519
+ PROPERTY_TRANSFER // Property unit transfer between contracts
2520
+ PROPERTY_UPDATE // Property/unit listing update requiring approval
2521
+ USER_WORKFLOW // User workflow step approval
2522
+ CREDIT_CHECK // Credit check result review
2523
+ CONTRACT_TERMINATION // Contract termination approval
2524
+ REFUND_APPROVAL // Refund request approval
2525
+ }
2526
+
2527
+ enum ApprovalRequestStatus {
2528
+ PENDING // Awaiting review
2529
+ IN_REVIEW // Assigned to reviewer
2530
+ APPROVED // Approved by reviewer
2531
+ REJECTED // Rejected by reviewer
2532
+ CANCELLED // Cancelled by requestor
2533
+ EXPIRED // Auto-expired (if TTL configured)
2534
+ }
2535
+
2536
+ enum ApprovalRequestPriority {
2537
+ LOW
2538
+ NORMAL
2539
+ HIGH
2540
+ URGENT
2541
+ }
2542
+
2543
+ // Polymorphic approval request model for unified admin dashboard
2544
+ model ApprovalRequest {
2545
+ id String @id @default(cuid())
2546
+ tenantId String
2547
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
2548
+
2549
+ // Request type and status
2550
+ type ApprovalRequestType
2551
+ status ApprovalRequestStatus @default(PENDING)
2552
+ priority ApprovalRequestPriority @default(NORMAL)
2553
+
2554
+ // Polymorphic reference to the entity requiring approval
2555
+ entityType String // e.g., "PropertyTransferRequest", "PropertyUnit", "User"
2556
+ entityId String // ID of the referenced entity
2557
+
2558
+ // Request metadata
2559
+ title String @db.VarChar(255) // Human-readable title for the request
2560
+ description String? @db.Text // Detailed description
2561
+
2562
+ // Payload for any additional context (JSON)
2563
+ payload Json? // Flexible data storage for type-specific details
2564
+
2565
+ // Requestor - who created the request
2566
+ requestedById String
2567
+ requestedBy User @relation("ApprovalRequestor", fields: [requestedById], references: [id])
2568
+
2569
+ // Assignee - admin/reviewer assigned to handle this request
2570
+ assigneeId String?
2571
+ assignee User? @relation("ApprovalAssignee", fields: [assigneeId], references: [id])
2572
+
2573
+ // Reviewer - who made the final decision (may differ from assignee)
2574
+ reviewedById String?
2575
+ reviewedBy User? @relation("ApprovalReviewer", fields: [reviewedById], references: [id])
2576
+
2577
+ // Review details
2578
+ reviewNotes String? @db.Text // Reviewer's notes/comments
2579
+ decision ApprovalDecision? // APPROVED, REJECTED, REQUEST_CHANGES
2580
+
2581
+ // Expiration
2582
+ expiresAt DateTime? // Optional TTL for auto-expiration
2583
+
2584
+ // Timestamps
2585
+ createdAt DateTime @default(now())
2586
+ assignedAt DateTime? // When assigned to reviewer
2587
+ reviewedAt DateTime? // When decision was made
2588
+ completedAt DateTime? // When fully processed
2589
+ updatedAt DateTime @updatedAt
2590
+
2591
+ @@index([tenantId])
2592
+ @@index([type])
2593
+ @@index([status])
2594
+ @@index([priority])
2595
+ @@index([entityType, entityId])
2596
+ @@index([requestedById])
2597
+ @@index([assigneeId])
2598
+ @@index([createdAt])
2599
+ @@index([type, status]) // Efficient queries for approval dashboard
2600
+ @@map("approval_requests")
2601
+ }