@valentine-efagene/qshelter-common 2.0.119 → 2.0.121

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 (35) hide show
  1. package/dist/generated/client/browser.d.ts +11 -0
  2. package/dist/generated/client/client.d.ts +11 -0
  3. package/dist/generated/client/commonInputTypes.d.ts +260 -0
  4. package/dist/generated/client/enums.d.ts +79 -0
  5. package/dist/generated/client/enums.js +71 -1
  6. package/dist/generated/client/internal/class.d.ts +22 -0
  7. package/dist/generated/client/internal/class.js +2 -2
  8. package/dist/generated/client/internal/prismaNamespace.d.ts +277 -2
  9. package/dist/generated/client/internal/prismaNamespace.js +109 -4
  10. package/dist/generated/client/internal/prismaNamespaceBrowser.d.ts +110 -1
  11. package/dist/generated/client/internal/prismaNamespaceBrowser.js +109 -4
  12. package/dist/generated/client/models/DocumentationPhase.d.ts +243 -1
  13. package/dist/generated/client/models/DocumentationPlanStep.d.ts +241 -1
  14. package/dist/generated/client/models/DocumentationStep.d.ts +1017 -1
  15. package/dist/generated/client/models/Organization.d.ts +1930 -0
  16. package/dist/generated/client/models/Organization.js +1 -0
  17. package/dist/generated/client/models/OrganizationMember.d.ts +1598 -0
  18. package/dist/generated/client/models/OrganizationMember.js +1 -0
  19. package/dist/generated/client/models/QuestionnairePhase.d.ts +165 -0
  20. package/dist/generated/client/models/QuestionnairePlanQuestion.d.ts +25 -22
  21. package/dist/generated/client/models/Tenant.d.ts +555 -0
  22. package/dist/generated/client/models/User.d.ts +776 -0
  23. package/dist/generated/client/models.d.ts +2 -0
  24. package/dist/src/middleware/auth-context.d.ts +30 -1
  25. package/dist/src/middleware/auth-context.js +34 -1
  26. package/dist/src/prisma/tenant.js +2 -0
  27. package/dist/src/utils/condition-operators.d.ts +5 -23
  28. package/dist/src/utils/condition-operators.js +6 -19
  29. package/dist/src/utils/documentation-enums.d.ts +6 -0
  30. package/dist/src/utils/documentation-enums.js +6 -0
  31. package/package.json +1 -1
  32. package/prisma/migrations/20260115123703_add_organizations/migration.sql +69 -0
  33. package/prisma/migrations/20260115125519_add_requires_manual_review_to_step/migration.sql +2 -0
  34. package/prisma/migrations/20260115134106_add_conditional_step_support/migration.sql +11 -0
  35. package/prisma/schema.prisma +191 -1
@@ -4,6 +4,8 @@ export type * from './models/Permission.js';
4
4
  export type * from './models/RolePermission.js';
5
5
  export type * from './models/UserRole.js';
6
6
  export type * from './models/TenantMembership.js';
7
+ export type * from './models/Organization.js';
8
+ export type * from './models/OrganizationMember.js';
7
9
  export type * from './models/Tenant.js';
8
10
  export type * from './models/ApiKey.js';
9
11
  export type * from './models/RefreshToken.js';
@@ -71,7 +71,7 @@ export declare function getAuthContext(req: Request): AuthContext;
71
71
  * .send({ name: 'John' });
72
72
  * ```
73
73
  *
74
- * @deprecated Use Authorization header directly with JWT token.
74
+ * @deprecated Use `mockAuthHeaders` for E2E tests or pass Authorization header directly with JWT token.
75
75
  * This helper is kept for backward compatibility.
76
76
  */
77
77
  export declare function authHeaders(userId: string, tenantId: string, extras?: {
@@ -79,6 +79,29 @@ export declare function authHeaders(userId: string, tenantId: string, extras?: {
79
79
  roles?: string[];
80
80
  token?: string;
81
81
  }): Record<string, string>;
82
+ /**
83
+ * Generate mock authorization headers for E2E tests.
84
+ *
85
+ * This is the preferred helper for E2E tests that bypass JWT authentication
86
+ * and instead use mock headers that simulate what the Lambda authorizer would set.
87
+ *
88
+ * @param userId - The user ID to set in the mock headers
89
+ * @param tenantId - The tenant ID to set in the mock headers
90
+ * @param options - Optional additional header values
91
+ * @returns Headers object to pass to supertest .set()
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const response = await api
96
+ * .post('/applications')
97
+ * .set(mockAuthHeaders(userId, tenantId, { roles: [ROLES.CUSTOMER] }))
98
+ * .send({ ... });
99
+ * ```
100
+ */
101
+ export declare function mockAuthHeaders(userId: string, tenantId: string, options?: {
102
+ email?: string;
103
+ roles?: string[];
104
+ }): Record<string, string>;
82
105
  /**
83
106
  * Standard role names used across the platform.
84
107
  */
@@ -88,6 +111,12 @@ export declare const ROLES: {
88
111
  readonly LOAN_OFFICER: "LOAN_OFFICER";
89
112
  readonly CUSTOMER: "CUSTOMER";
90
113
  readonly VIEWER: "VIEWER";
114
+ /** Property developers who list properties and upload sales offer letters */
115
+ readonly DEVELOPER: "DEVELOPER";
116
+ /** Bank/financial institution representatives who upload preapproval and mortgage offer letters */
117
+ readonly LENDER: "LENDER";
118
+ /** Legal officers who upload final offer letters and handle legal documentation */
119
+ readonly LEGAL: "LEGAL";
91
120
  };
92
121
  export type RoleName = (typeof ROLES)[keyof typeof ROLES];
93
122
  /**
@@ -128,7 +128,7 @@ export function getAuthContext(req) {
128
128
  * .send({ name: 'John' });
129
129
  * ```
130
130
  *
131
- * @deprecated Use Authorization header directly with JWT token.
131
+ * @deprecated Use `mockAuthHeaders` for E2E tests or pass Authorization header directly with JWT token.
132
132
  * This helper is kept for backward compatibility.
133
133
  */
134
134
  export function authHeaders(userId, tenantId, extras) {
@@ -145,6 +145,33 @@ export function authHeaders(userId, tenantId, extras) {
145
145
  ...(extras?.roles && { 'x-authorizer-roles': JSON.stringify(extras.roles) }),
146
146
  };
147
147
  }
148
+ /**
149
+ * Generate mock authorization headers for E2E tests.
150
+ *
151
+ * This is the preferred helper for E2E tests that bypass JWT authentication
152
+ * and instead use mock headers that simulate what the Lambda authorizer would set.
153
+ *
154
+ * @param userId - The user ID to set in the mock headers
155
+ * @param tenantId - The tenant ID to set in the mock headers
156
+ * @param options - Optional additional header values
157
+ * @returns Headers object to pass to supertest .set()
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * const response = await api
162
+ * .post('/applications')
163
+ * .set(mockAuthHeaders(userId, tenantId, { roles: [ROLES.CUSTOMER] }))
164
+ * .send({ ... });
165
+ * ```
166
+ */
167
+ export function mockAuthHeaders(userId, tenantId, options) {
168
+ return {
169
+ 'x-authorizer-user-id': userId,
170
+ 'x-authorizer-tenant-id': tenantId,
171
+ ...(options?.email && { 'x-authorizer-email': options.email }),
172
+ ...(options?.roles && { 'x-authorizer-roles': JSON.stringify(options.roles) }),
173
+ };
174
+ }
148
175
  /**
149
176
  * Standard role names used across the platform.
150
177
  */
@@ -154,6 +181,12 @@ export const ROLES = {
154
181
  LOAN_OFFICER: 'LOAN_OFFICER',
155
182
  CUSTOMER: 'CUSTOMER',
156
183
  VIEWER: 'VIEWER',
184
+ /** Property developers who list properties and upload sales offer letters */
185
+ DEVELOPER: 'DEVELOPER',
186
+ /** Bank/financial institution representatives who upload preapproval and mortgage offer letters */
187
+ LENDER: 'LENDER',
188
+ /** Legal officers who upload final offer letters and handle legal documentation */
189
+ LEGAL: 'LEGAL',
157
190
  };
158
191
  /**
159
192
  * Roles that have admin privileges (can manage resources).
@@ -20,6 +20,8 @@ const GLOBAL_MODELS = [
20
20
  // User preferences/devices (linked to user, not tenant)
21
21
  "emailPreference",
22
22
  "deviceEndpoint",
23
+ // Organization members (tenant access via organization.tenantId)
24
+ "organizationMember",
23
25
  ];
24
26
  /**
25
27
  * Models that have OPTIONAL tenant scoping (nullable tenantId).
@@ -2,30 +2,12 @@
2
2
  * Condition operators for evaluating step conditions against questionnaire answers.
3
3
  * Used in DocumentationPlanStep.condition to conditionally require documents
4
4
  * based on prequalification answers.
5
+ *
6
+ * ConditionOperator is a Prisma enum - this file provides TypeScript interfaces
7
+ * for building condition objects.
5
8
  */
6
- /**
7
- * Supported condition operators for evaluating document step conditions.
8
- */
9
- export declare const CONDITION_OPERATORS: {
10
- /** Value equals the specified value */
11
- readonly EQUALS: "EQUALS";
12
- /** Value does not equal the specified value */
13
- readonly NOT_EQUALS: "NOT_EQUALS";
14
- /** Value is in the specified array of values */
15
- readonly IN: "IN";
16
- /** Value is not in the specified array of values */
17
- readonly NOT_IN: "NOT_IN";
18
- /** Numeric value is greater than the specified value */
19
- readonly GREATER_THAN: "GREATER_THAN";
20
- /** Numeric value is less than the specified value */
21
- readonly LESS_THAN: "LESS_THAN";
22
- /** Value exists (is not null/undefined) */
23
- readonly EXISTS: "EXISTS";
24
- };
25
- /**
26
- * Type representing a valid condition operator.
27
- */
28
- export type ConditionOperator = keyof typeof CONDITION_OPERATORS;
9
+ import { ConditionOperator } from '../../generated/client/enums';
10
+ export { ConditionOperator };
29
11
  /**
30
12
  * Interface for a simple condition that checks a single question answer.
31
13
  */
@@ -2,23 +2,10 @@
2
2
  * Condition operators for evaluating step conditions against questionnaire answers.
3
3
  * Used in DocumentationPlanStep.condition to conditionally require documents
4
4
  * based on prequalification answers.
5
+ *
6
+ * ConditionOperator is a Prisma enum - this file provides TypeScript interfaces
7
+ * for building condition objects.
5
8
  */
6
- /**
7
- * Supported condition operators for evaluating document step conditions.
8
- */
9
- export const CONDITION_OPERATORS = {
10
- /** Value equals the specified value */
11
- EQUALS: 'EQUALS',
12
- /** Value does not equal the specified value */
13
- NOT_EQUALS: 'NOT_EQUALS',
14
- /** Value is in the specified array of values */
15
- IN: 'IN',
16
- /** Value is not in the specified array of values */
17
- NOT_IN: 'NOT_IN',
18
- /** Numeric value is greater than the specified value */
19
- GREATER_THAN: 'GREATER_THAN',
20
- /** Numeric value is less than the specified value */
21
- LESS_THAN: 'LESS_THAN',
22
- /** Value exists (is not null/undefined) */
23
- EXISTS: 'EXISTS',
24
- };
9
+ import { ConditionOperator } from '../../generated/client/enums';
10
+ // Re-export for convenience (the Prisma enum is the source of truth)
11
+ export { ConditionOperator };
@@ -10,8 +10,14 @@ export declare const UPLOADED_BY: {
10
10
  readonly CUSTOMER: "CUSTOMER";
11
11
  /** Document is uploaded by an admin/staff member */
12
12
  readonly ADMIN: "ADMIN";
13
+ /** Document is uploaded by a lender/bank representative */
14
+ readonly LENDER: "LENDER";
15
+ /** Document is uploaded by the developer */
16
+ readonly DEVELOPER: "DEVELOPER";
13
17
  /** Document is uploaded by the system (auto-generated) */
14
18
  readonly SYSTEM: "SYSTEM";
19
+ /** Document is uploaded by a legal officer */
20
+ readonly LEGAL: "LEGAL";
15
21
  };
16
22
  /**
17
23
  * Type representing a valid uploadedBy value.
@@ -10,6 +10,12 @@ export const UPLOADED_BY = {
10
10
  CUSTOMER: 'CUSTOMER',
11
11
  /** Document is uploaded by an admin/staff member */
12
12
  ADMIN: 'ADMIN',
13
+ /** Document is uploaded by a lender/bank representative */
14
+ LENDER: 'LENDER',
15
+ /** Document is uploaded by the developer */
16
+ DEVELOPER: 'DEVELOPER',
13
17
  /** Document is uploaded by the system (auto-generated) */
14
18
  SYSTEM: 'SYSTEM',
19
+ /** Document is uploaded by a legal officer */
20
+ LEGAL: 'LEGAL',
15
21
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valentine-efagene/qshelter-common",
3
- "version": "2.0.119",
3
+ "version": "2.0.121",
4
4
  "description": "Shared database schemas and utilities for QShelter services",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -0,0 +1,69 @@
1
+ -- CreateTable
2
+ CREATE TABLE `organizations` (
3
+ `id` VARCHAR(191) NOT NULL,
4
+ `tenantId` VARCHAR(191) NOT NULL,
5
+ `name` VARCHAR(191) NOT NULL,
6
+ `type` ENUM('BANK', 'DEVELOPER') NOT NULL,
7
+ `status` ENUM('PENDING', 'ACTIVE', 'SUSPENDED', 'INACTIVE') NOT NULL DEFAULT 'PENDING',
8
+ `email` VARCHAR(191) NULL,
9
+ `phone` VARCHAR(191) NULL,
10
+ `address` VARCHAR(191) NULL,
11
+ `city` VARCHAR(191) NULL,
12
+ `state` VARCHAR(191) NULL,
13
+ `country` VARCHAR(191) NULL DEFAULT 'Nigeria',
14
+ `website` VARCHAR(191) NULL,
15
+ `logoUrl` VARCHAR(191) NULL,
16
+ `description` TEXT NULL,
17
+ `bankCode` VARCHAR(191) NULL,
18
+ `bankLicenseNo` VARCHAR(191) NULL,
19
+ `swiftCode` VARCHAR(191) NULL,
20
+ `sortCode` VARCHAR(191) NULL,
21
+ `cacNumber` VARCHAR(191) NULL,
22
+ `cacCertificateUrl` VARCHAR(191) NULL,
23
+ `taxId` VARCHAR(191) NULL,
24
+ `approvedAt` DATETIME(3) NULL,
25
+ `approvedById` VARCHAR(191) NULL,
26
+ `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
27
+ `updatedAt` DATETIME(3) NOT NULL,
28
+
29
+ INDEX `organizations_tenantId_idx`(`tenantId`),
30
+ INDEX `organizations_type_idx`(`type`),
31
+ INDEX `organizations_status_idx`(`status`),
32
+ UNIQUE INDEX `organizations_tenantId_bankCode_key`(`tenantId`, `bankCode`),
33
+ UNIQUE INDEX `organizations_tenantId_cacNumber_key`(`tenantId`, `cacNumber`),
34
+ PRIMARY KEY (`id`)
35
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
36
+
37
+ -- CreateTable
38
+ CREATE TABLE `organization_members` (
39
+ `id` VARCHAR(191) NOT NULL,
40
+ `organizationId` VARCHAR(191) NOT NULL,
41
+ `userId` VARCHAR(191) NOT NULL,
42
+ `role` ENUM('ADMIN', 'MANAGER', 'OFFICER', 'VIEWER') NOT NULL DEFAULT 'OFFICER',
43
+ `title` VARCHAR(191) NULL,
44
+ `department` VARCHAR(191) NULL,
45
+ `employeeId` VARCHAR(191) NULL,
46
+ `isActive` BOOLEAN NOT NULL DEFAULT true,
47
+ `canApprove` BOOLEAN NOT NULL DEFAULT false,
48
+ `approvalLimit` DECIMAL(65, 30) NULL,
49
+ `invitedAt` DATETIME(3) NULL,
50
+ `acceptedAt` DATETIME(3) NULL,
51
+ `invitedBy` VARCHAR(191) NULL,
52
+ `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
53
+ `updatedAt` DATETIME(3) NOT NULL,
54
+
55
+ INDEX `organization_members_userId_idx`(`userId`),
56
+ INDEX `organization_members_organizationId_idx`(`organizationId`),
57
+ INDEX `organization_members_role_idx`(`role`),
58
+ UNIQUE INDEX `organization_members_organizationId_userId_key`(`organizationId`, `userId`),
59
+ PRIMARY KEY (`id`)
60
+ ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
61
+
62
+ -- AddForeignKey
63
+ ALTER TABLE `organizations` ADD CONSTRAINT `organizations_tenantId_fkey` FOREIGN KEY (`tenantId`) REFERENCES `tenants`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
64
+
65
+ -- AddForeignKey
66
+ ALTER TABLE `organization_members` ADD CONSTRAINT `organization_members_organizationId_fkey` FOREIGN KEY (`organizationId`) REFERENCES `organizations`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
67
+
68
+ -- AddForeignKey
69
+ ALTER TABLE `organization_members` ADD CONSTRAINT `organization_members_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
1
+ -- AlterTable
2
+ ALTER TABLE `documentation_steps` ADD COLUMN `requiresManualReview` BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,11 @@
1
+ -- AlterTable
2
+ ALTER TABLE `documentation_phases` ADD COLUMN `sourceQuestionnairePhaseId` VARCHAR(191) NULL;
3
+
4
+ -- AlterTable
5
+ ALTER TABLE `documentation_steps` ADD COLUMN `condition` JSON NULL;
6
+
7
+ -- CreateIndex
8
+ CREATE INDEX `documentation_phases_sourceQuestionnairePhaseId_idx` ON `documentation_phases`(`sourceQuestionnairePhaseId`);
9
+
10
+ -- AddForeignKey
11
+ ALTER TABLE `documentation_phases` ADD CONSTRAINT `documentation_phases_sourceQuestionnairePhaseId_fkey` FOREIGN KEY (`sourceQuestionnairePhaseId`) REFERENCES `questionnaire_phases`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
@@ -62,6 +62,18 @@ enum ApplicationStatus {
62
62
  SUPERSEDED // Another buyer locked the unit - this application was outbid
63
63
  }
64
64
 
65
+ /// Triggers that cause application state transitions
66
+ enum ApplicationTrigger {
67
+ SUBMIT // Submit application for review (DRAFT -> PENDING)
68
+ APPROVE // Approve pending application (PENDING -> ACTIVE)
69
+ REJECT // Reject pending application (PENDING -> CANCELLED)
70
+ CANCEL // Cancel application
71
+ COMPLETE // Complete application (all phases done)
72
+ TERMINATE // Terminate active application
73
+ TRANSFER // Transfer to different unit
74
+ SUPERSEDE // Another buyer locked the unit
75
+ }
76
+
65
77
  enum TransferRequestStatus {
66
78
  PENDING
67
79
  APPROVED
@@ -129,6 +141,20 @@ enum StepStatus {
129
141
  AWAITING_REVIEW // Submitted, waiting for admin/system review
130
142
  }
131
143
 
144
+ /// Operators for conditional step evaluation
145
+ enum ConditionOperator {
146
+ EQUALS // Field value equals expected value
147
+ NOT_EQUALS // Field value does not equal expected value
148
+ IN // Field value is in a list of values
149
+ NOT_IN // Field value is not in a list of values
150
+ GREATER_THAN // Numeric comparison
151
+ LESS_THAN // Numeric comparison
152
+ GREATER_THAN_OR_EQUAL
153
+ LESS_THAN_OR_EQUAL
154
+ EXISTS // Field has any value (not null/undefined)
155
+ NOT_EXISTS // Field is null/undefined
156
+ }
157
+
132
158
  /// When a step event attachment should trigger
133
159
  enum StepTrigger {
134
160
  ON_COMPLETE // When step is approved/completed
@@ -169,6 +195,32 @@ enum ApprovalDecision {
169
195
  REQUEST_CHANGES
170
196
  }
171
197
 
198
+ // =============================================================================
199
+ // ORGANIZATION ENUMS (Banks, Developers, etc.)
200
+ // =============================================================================
201
+
202
+ /// Type of organization on the platform
203
+ enum OrganizationType {
204
+ BANK // Financial institution providing mortgages (e.g., Access Bank, GTBank)
205
+ DEVELOPER // Property developer building and selling properties
206
+ }
207
+
208
+ /// Status of an organization
209
+ enum OrganizationStatus {
210
+ PENDING // Awaiting approval to operate on platform
211
+ ACTIVE // Fully operational
212
+ SUSPENDED // Temporarily suspended
213
+ INACTIVE // No longer active
214
+ }
215
+
216
+ /// Role of a member within an organization
217
+ enum OrganizationRole {
218
+ ADMIN // Can manage org settings, members, and full operations
219
+ MANAGER // Can approve transactions within limits (checker)
220
+ OFFICER // Regular employee, can initiate actions (maker)
221
+ VIEWER // Read-only access to organization data
222
+ }
223
+
172
224
  // =============================================================================
173
225
  // CONTRACT TERMINATION / CANCELLATION ENUMS
174
226
  // =============================================================================
@@ -408,6 +460,9 @@ model User {
408
460
  approvedRefunds ApplicationRefund[] @relation("RefundApprover")
409
461
  processedRefunds ApplicationRefund[] @relation("RefundProcessor")
410
462
 
463
+ // Organization memberships (user can be employee of banks/developers)
464
+ organizationMemberships OrganizationMember[]
465
+
411
466
  @@index([email])
412
467
  @@index([tenantId])
413
468
  @@map("users")
@@ -510,6 +565,103 @@ model TenantMembership {
510
565
  @@map("tenant_memberships")
511
566
  }
512
567
 
568
+ // =============================================================================
569
+ // ORGANIZATIONS (Banks, Developers, etc.)
570
+ // =============================================================================
571
+ // Organizations represent external entities that participate in the platform.
572
+ // Banks provide mortgage financing; Developers build and sell properties.
573
+ // Users can be employees (members) of organizations with specific roles.
574
+ // =============================================================================
575
+
576
+ /// Organization: Banks, Developers, and other partner entities
577
+ model Organization {
578
+ id String @id @default(cuid())
579
+ tenantId String
580
+ name String // e.g., "Access Bank PLC", "Lekki Gardens Ltd"
581
+ type OrganizationType // BANK or DEVELOPER
582
+ status OrganizationStatus @default(PENDING)
583
+
584
+ // Common fields
585
+ email String? // Primary contact email
586
+ phone String? // Primary contact phone
587
+ address String?
588
+ city String?
589
+ state String?
590
+ country String? @default("Nigeria")
591
+ website String?
592
+ logoUrl String?
593
+ description String? @db.Text
594
+
595
+ // Bank-specific fields
596
+ bankCode String? // CBN bank code (e.g., "044" for Access Bank)
597
+ bankLicenseNo String? // Banking license number
598
+ swiftCode String? // SWIFT/BIC code for international transfers
599
+ sortCode String? // Sort code for local transfers
600
+
601
+ // Developer-specific fields
602
+ cacNumber String? // CAC registration number
603
+ cacCertificateUrl String? // URL to CAC certificate document
604
+ taxId String? // Tax Identification Number
605
+
606
+ // Approval workflow
607
+ approvedAt DateTime?
608
+ approvedById String?
609
+
610
+ createdAt DateTime @default(now())
611
+ updatedAt DateTime @updatedAt
612
+
613
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
614
+ members OrganizationMember[]
615
+
616
+ // Properties developed by this organization (for DEVELOPERs)
617
+ // developedProperties Property[] @relation("PropertyDeveloper")
618
+
619
+ @@unique([tenantId, bankCode]) // Bank codes unique within tenant
620
+ @@unique([tenantId, cacNumber]) // CAC numbers unique within tenant
621
+ @@index([tenantId])
622
+ @@index([type])
623
+ @@index([status])
624
+ @@map("organizations")
625
+ }
626
+
627
+ /// OrganizationMember: Links users to organizations with roles and permissions
628
+ /// Supports maker-checker workflows via canApprove and approvalLimit
629
+ model OrganizationMember {
630
+ id String @id @default(cuid())
631
+ organizationId String
632
+ userId String
633
+ role OrganizationRole @default(OFFICER)
634
+
635
+ // Employee details
636
+ title String? // Job title (e.g., "Loan Officer", "Project Manager")
637
+ department String? // Department within organization
638
+ employeeId String? // Internal employee ID
639
+
640
+ // Status
641
+ isActive Boolean @default(true)
642
+
643
+ // Maker-Checker workflow permissions
644
+ canApprove Boolean @default(false) // Can this member approve transactions?
645
+ approvalLimit Decimal? // Maximum amount this member can approve (null = unlimited)
646
+
647
+ // Invitation/onboarding tracking
648
+ invitedAt DateTime?
649
+ acceptedAt DateTime?
650
+ invitedBy String? // User ID who invited this member
651
+
652
+ createdAt DateTime @default(now())
653
+ updatedAt DateTime @updatedAt
654
+
655
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
656
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
657
+
658
+ @@unique([organizationId, userId]) // User can only be member once per org
659
+ @@index([userId])
660
+ @@index([organizationId])
661
+ @@index([role])
662
+ @@map("organization_members")
663
+ }
664
+
513
665
  model Tenant {
514
666
  id String @id @default(cuid())
515
667
  name String
@@ -596,6 +748,9 @@ model Tenant {
596
748
  workflowBlockers WorkflowBlocker[]
597
749
  questionnairePlans QuestionnairePlan[]
598
750
 
751
+ // Organizations (Banks, Developers) operating on this tenant
752
+ organizations Organization[]
753
+
599
754
  @@index([subdomain])
600
755
  @@map("tenants")
601
756
  }
@@ -1209,7 +1364,7 @@ model QuestionnairePlanQuestion {
1209
1364
  showIf Json?
1210
1365
 
1211
1366
  // Metadata for grouping
1212
- category String? // Group questions (e.g., "income", "employment", "property")
1367
+ category QuestionCategory? // Group questions by topic
1213
1368
 
1214
1369
  createdAt DateTime @default(now())
1215
1370
  updatedAt DateTime @updatedAt
@@ -1568,6 +1723,23 @@ enum QuestionnaireCategory {
1568
1723
  CUSTOM
1569
1724
  }
1570
1725
 
1726
+ /// Category for individual questions within a questionnaire
1727
+ /// Used to group questions by topic for UI/UX and scoring
1728
+ enum QuestionCategory {
1729
+ ELIGIBILITY // Age, citizenship, legal status
1730
+ EMPLOYMENT // Employment status, employer details
1731
+ INCOME // Salary, bonuses, other income
1732
+ AFFORDABILITY // Income vs expenses, debt-to-income
1733
+ EXPENSES // Monthly expenses, existing debts
1734
+ APPLICATION_TYPE // Type of mortgage/purchase (single, joint)
1735
+ PERSONAL // Marital status, dependents, contact info
1736
+ PREFERENCES // Preferred terms, property type preferences
1737
+ PROPERTY // Property-related questions
1738
+ CREDIT // Credit history, existing loans
1739
+ ASSETS // Savings, investments, other assets
1740
+ CUSTOM // Custom/uncategorized questions
1741
+ }
1742
+
1571
1743
  // Questionnaire field template within a QUESTIONNAIRE phase
1572
1744
  model PaymentMethodPhaseField {
1573
1745
  id String @id @default(cuid())
@@ -1867,6 +2039,9 @@ model QuestionnairePhase {
1867
2039
  // Child records
1868
2040
  fields QuestionnaireField[]
1869
2041
 
2042
+ // Back-relation: Documentation phases that use this questionnaire's answers for conditions
2043
+ dependentDocumentationPhases DocumentationPhase[] @relation("SourceQuestionnaire")
2044
+
1870
2045
  @@index([tenantId])
1871
2046
  @@index([phaseId])
1872
2047
  @@index([questionnairePlanId])
@@ -1890,6 +2065,11 @@ model DocumentationPhase {
1890
2065
  documentationPlanId String?
1891
2066
  documentationPlan DocumentationPlan? @relation(fields: [documentationPlanId], references: [id])
1892
2067
 
2068
+ // Source questionnaire for conditional step evaluation
2069
+ // Links to the questionnaire phase whose answers determine which steps apply
2070
+ sourceQuestionnairePhaseId String?
2071
+ sourceQuestionnairePhase QuestionnairePhase? @relation("SourceQuestionnaire", fields: [sourceQuestionnairePhaseId], references: [id])
2072
+
1893
2073
  // Current step pointer for UX and orchestration
1894
2074
  currentStepId String?
1895
2075
  currentStep DocumentationStep? @relation("CurrentStep", fields: [currentStepId], references: [id])
@@ -1917,6 +2097,7 @@ model DocumentationPhase {
1917
2097
  @@index([tenantId])
1918
2098
  @@index([phaseId])
1919
2099
  @@index([currentStepId])
2100
+ @@index([sourceQuestionnairePhaseId])
1920
2101
  @@map("documentation_phases")
1921
2102
  }
1922
2103
 
@@ -2096,6 +2277,15 @@ model DocumentationStep {
2096
2277
  // Configuration metadata (for GENERATE_DOCUMENT steps, etc.)
2097
2278
  metadata Json?
2098
2279
 
2280
+ // Document validation rules (copied from plan for UPLOAD steps)
2281
+ requiresManualReview Boolean @default(false)
2282
+
2283
+ // Conditional step logic (copied from plan)
2284
+ // When set, this step is only applicable if the condition evaluates to true
2285
+ // Format: { "questionKey": "mortgage_type", "operator": "EQUALS", "value": "JOINT" }
2286
+ // Or: { "questionKey": "employment_status", "operator": "IN", "values": ["SELF_EMPLOYED", "BUSINESS_OWNER"] }
2287
+ condition Json?
2288
+
2099
2289
  // Assignment
2100
2290
  assigneeId String?
2101
2291
  assignee User? @relation("DocumentationStepAssignee", fields: [assigneeId], references: [id])