65 Commits
Author SHA1 Message Date
Syed Waseem khadri Rafai ca446e42b8 Merge pull request 'feat: implement global HTTP request logging and audit interception for entity creation' (#34) from waseem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/34
2026-09-10 06:44:32 +00:00
Syed waseem 4fcbc4b925 feat: implement global HTTP request logging and audit interception for entity creation 2026-09-10 12:08:08 +05:30
Syed Waseem khadri Rafai 7cb8a7ea84 Merge pull request 'feat: implement core authentication system with JWT guards, RBAC, and modular controller scaffolding' (#33) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/33
2026-09-09 05:05:02 +00:00
azeeee05 07845f2af5 feat: implement core authentication system with JWT guards, RBAC, and modular controller scaffolding 2026-09-08 14:44:09 +05:30
Syed Waseem khadri Rafai bf3f987dce Merge pull request 'feat: implement policy applicability and rule evaluation services for policy engine engine' (#31) from waseem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/31
2026-08-25 07:22:06 +00:00
Syed Waseem cf458536fb feat: implement policy applicability and rule evaluation services for policy engine engine 2026-08-25 12:50:16 +05:30
Syed Waseem khadri Rafai 2dc4faa410 Merge pull request 'feat: add recovery incident module entities and DTOs and implement policy engine controller endpoints' (#29) from waseem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/29
2026-08-19 11:04:21 +00:00
Syed Waseem 5d83099957 feat: add recovery incident module entities and DTOs and implement policy engine controller endpoints 2026-08-19 16:32:39 +05:30
Syed Waseem khadri Rafai 61d5edb7a2 Merge pull request 'feat: implement PolicyApplicabilityService to evaluate policy eligibility based on jurisdiction and cohort matches' (#28) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/28
2026-08-19 09:37:40 +00:00
azeeee05 b8510d9902 feat: implement PolicyApplicabilityService to evaluate policy eligibility based on jurisdiction and cohort matches 2026-08-19 12:31:23 +05:30
Syed Waseem khadri Rafai 6c1a80587e Merge pull request 'waseem' (#26) from waseem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/26
2026-08-18 03:58:41 +00:00
Syed Waseem 5780b5cd47 feat: implement policy engine and recovery incident modules with evaluation logic and database schema 2026-08-17 16:38:18 +05:30
Syed Waseem 1d96290618 Merge branch 'development' of https://gitea.maskantech.in/gitea_admin/aeroresolve_backend into waseem 2026-08-17 13:03:40 +05:30
Syed Waseem khadri Rafai b83ec24268 Merge pull request 'feat: implement action builder field configurations and add master data service service layer' (#25) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/25
2026-08-17 06:52:04 +00:00
azeeee05 0a06a3856f feat: implement action builder field configurations and add master data service service layer 2026-08-17 12:12:40 +05:30
Syed Waseem 46770d4b2b feat: implement dashboard module with metrics, exposure, and incident breakdown services 2026-08-13 11:09:01 +05:30
Syed Waseem khadri Rafai e7a9ee911e Merge pull request 'azeem' (#24) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/24
2026-08-12 10:21:35 +00:00
azeeee05 4ee9e36e2b feat: implement MasterDataService with comprehensive entity repositories for application reference data 2026-08-12 15:14:21 +05:30
azeeee05 5779501812 feat: implement master data module with initial entities, action builder configurations, and SQL seed script 2026-08-12 12:46:22 +05:30
Syed Waseem khadri Rafai fd54fc6a0e Merge pull request 'azeem' (#23) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/23
2026-08-11 09:42:22 +00:00
azeeee05 8d11cf85ea feat: implement MasterDataService to manage comprehensive application entities and metadata repositories 2026-08-11 15:08:17 +05:30
azeeee05 9d386580c8 feat: initialize master data entities and seed lookup tables for the rule engine 2026-08-11 14:59:01 +05:30
azeeee05 67756b88bd masters data done 2026-08-11 14:37:42 +05:30
Syed Waseem khadri Rafai 5e4e37c948 Merge pull request 'waseem' (#22) from waseem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/22
2026-08-11 07:31:05 +00:00
Syed Waseem dfce163ca0 feat: implement master-data module with core entity definitions, schema migration, and CRUD endpoints 2026-08-11 12:59:01 +05:30
azeeee05 e9f5fd2e7b feat: add RoomType and TaxType entities and update master data module and service seeders 2026-08-11 12:24:53 +05:30
azeeee05 1f8c29d850 feat: initialize master data module with seed script and service structure 2026-08-10 19:23:26 +05:30
azeeee05 689be81442 feat: initialize master data service, entities, and SQL seed script for compensation basis and related parameters 2026-08-10 19:21:45 +05:30
azeeee05 ad90a5b082 feat: extend master-data module with new preference and category entities and initialization script 2026-08-10 19:13:22 +05:30
azeeee05 7f529d8ae4 feat: implement VehicleCategory entity and master data module support 2026-08-10 17:46:23 +05:30
azeeee05 5f95a5c341 feat: initialize master data module with new entity definitions, cohort module, and SQL seeding script 2026-08-10 17:17:06 +05:30
Syed Waseem 06a1ee518e feat: implement recovery incident service and controller with metric aggregation and CRUD operations 2026-08-10 15:18:20 +05:30
azeeee05 e920c9e3b5 Merge branch 'development' of https://gitea.maskantech.in/gitea_admin/aeroresolve_backend into azeem 2026-08-07 16:41:18 +05:30
azeeee05 dbfce10983 feat: initialize backend database schema and implement audit logging system with core service modules 2026-08-07 16:27:15 +05:30
Syed Waseem khadri Rafai a7733ad28f Merge pull request 'feat: add recovery incident module DTO and entity definition' (#21) from waseem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/21
2026-08-07 10:57:03 +00:00
Syed Waseem 98df6f18b4 feat: add recovery incident module DTO and entity definition 2026-08-07 16:25:38 +05:30
Syed Waseem khadri Rafai 345d04407a Merge pull request 'feat: implement RecoveryIncident entity and CreateRecoveryIncident DTO' (#20) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/20
2026-08-06 06:44:35 +00:00
azeeee05 e3edd7a6c4 feat: implement RecoveryIncident entity and CreateRecoveryIncident DTO 2026-08-06 11:47:35 +05:30
Syed Waseem khadri Rafai ee7796e6d3 Merge pull request 'feat: implement policy engine entity layer and initial database schema definition' (#19) from waseem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/19
2026-08-04 10:28:10 +00:00
Syed Waseem 0d8a6228f4 feat: implement policy engine entity layer and initial database schema definition 2026-08-04 15:45:42 +05:30
Syed Waseem khadri Rafai 03990c71fe Merge pull request 'feat: configure database module with schema initialization and implement recovery incident entity and DTO' (#18) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/18
2026-08-04 10:10:41 +00:00
azeeee05 7413167f79 feat: configure database module with schema initialization and implement recovery incident entity and DTO 2026-08-04 15:39:38 +05:30
Syed Waseem khadri Rafai 2c0cad7093 Merge pull request 'recovery incidents' (#17) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/17
2026-08-04 09:16:34 +00:00
azeeee05 86f435500a recovery incidents 2026-08-03 14:26:19 +05:30
Syed Waseem khadri Rafai a8c8b11ba5 Merge pull request 'feat: implement master data entities and initial database schema for the policy engine module' (#16) from waseem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/16
2026-08-03 06:20:06 +00:00
Syed Waseem 9f3b3ad4fd feat: implement master data entities and initial database schema for the policy engine module 2026-08-03 11:45:26 +05:30
Syed Waseem khadri Rafai 7afd58c178 Merge pull request 'feat: implement master data service and seed scripts for reference table management' (#15) from waseem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/15
2026-07-29 10:39:33 +00:00
Syed Waseem 1fe403cb11 feat: implement master data service and seed scripts for reference table management 2026-07-29 16:07:11 +05:30
Syed Waseem khadri Rafai e209541475 Merge pull request 'waseem' (#14) from waseem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/14
2026-07-29 09:48:50 +00:00
Syed Waseem 046d36d069 feat: implement comprehensive master data module with CRUD services, controllers, and entities 2026-07-29 15:13:41 +05:30
maskantech 393963a982 Merge pull request 'Update README.md' (#13) from deploy into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/13
2026-07-21 17:49:16 +00:00
maskantech 5cd42c914f Update README.md 2026-07-21 17:48:55 +00:00
maskantech c90c96b8e5 Merge pull request 'Update README.md' (#12) from deploy into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/12
2026-07-21 17:45:10 +00:00
maskantech 9cf49bc05e Update README.md 2026-07-21 17:44:52 +00:00
maskantech 894660b865 Merge pull request 'Update README.md' (#11) from deploy into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/11
2026-07-21 17:34:22 +00:00
maskantech 1d4b160660 Update README.md 2026-07-21 17:34:07 +00:00
maskantech a5770d1e0f Merge pull request 'Update README.md' (#10) from deploy into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/10
2026-07-21 17:26:31 +00:00
maskantech 2cdd921012 Update README.md 2026-07-21 17:25:55 +00:00
maskantech bef7aa98c1 Merge pull request 'Update README.md' (#9) from deploy into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/9
2026-07-21 11:25:30 +00:00
maskantech dfd2ff8127 Update README.md 2026-07-21 11:25:05 +00:00
Syed Waseem e8e6da31be feat: update entity properties to use definite assignment assertion and add unique indexes for tenant-specific codes 2026-07-21 13:17:08 +05:30
Syed Waseem khadri Rafai de7527e1bd Merge pull request 'waseem' (#8) from waseem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/8
2026-07-21 05:54:34 +00:00
Syed Waseem f3acfd2181 feat(master-data): add jurisdiction and rule category entities with CRUD operations
- Introduced Jurisdiction and RuleCategory entities along with their respective values.
- Implemented service methods for managing rule categories and values.
- Updated MasterDataService to include seeding logic for jurisdictions and rule categories.
- Added DTOs for creating and updating rule categories and values.

feat(policy-engine): enhance policy management with new entities and operations

- Created Policy, PolicyRule, PolicyAction, PolicyTargetAudience, and RuleCondition entities.
- Implemented create, update, and delete operations for policies and their associated rules and actions.
- Added enums for PolicyStatus and AudienceType to manage policy states and target audiences.
- Developed DTOs for creating and updating policies with nested structures for rules and audiences.
2026-07-21 11:22:54 +05:30
Syed Waseem 4fd1058dfe Merge branch 'development' of https://gitea.maskantech.in/gitea_admin/aeroresolve_backend into waseem 2026-07-18 11:13:00 +05:30
waseem khadri fd196f559b feat: initialize policy-engine module and integrate into AppModule 2026-07-15 09:04:42 +05:30
172 changed files with 12946 additions and 312 deletions
+4
View File
@@ -8,3 +8,7 @@ DB_SSL=false
PORT=9501
CORS_ORIGIN=https://ardev.maskantech.in
NODE_ENV=dev
JWT_SECRET=aeroresolve_jwt_access_secret_key_dev_2026_@#!
JWT_REFRESH_SECRET=aeroresolve_jwt_refresh_secret_key_dev_2026_@#!
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
+4
View File
@@ -8,3 +8,7 @@ DB_SSL=false
PORT=9502
CORS_ORIGIN=https://artest.maskantech.in
NODE_ENV=test
JWT_SECRET=aeroresolve_jwt_access_secret_key_test_2026_@#!
JWT_REFRESH_SECRET=aeroresolve_jwt_refresh_secret_key_test_2026_@#!
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
+4
View File
@@ -8,3 +8,7 @@ DB_SSL=false
PORT=9501
CORS_ORIGIN=https://aruat.maskantech.in
NODE_ENV=uat
JWT_SECRET=aeroresolve_jwt_access_secret_key_uat_2026_@#!
JWT_REFRESH_SECRET=aeroresolve_jwt_refresh_secret_key_uat_2026_@#!
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
+19 -18
View File
@@ -1,15 +1,16 @@
# AeroResolve Backend
test1
NestJS REST API for AeroResolve.
## Current Modules
| Module | Status | Description |
|--------|--------|-------------|
| `health` | Implemented | Health check |
| `database` | Implemented | PostgreSQL / TypeORM setup |
| Module | Status | Description |
| ------------- | ----------- | ---------------------------------------- |
| `health` | Implemented | Health check |
| `database` | Implemented | PostgreSQL / TypeORM setup |
| `master-data` | Implemented | CRUD and seed data for lookup categories |
| `cohort` | Implemented | Cohort CRUD, pagination, status update |
| `cohort` | Implemented | Cohort CRUD, pagination, status update |
## Prerequisites
@@ -80,20 +81,20 @@ http://localhost:3001/docs
## Endpoints
| Method | Path |
|--------|------|
| `GET` | `/api/health` |
| `GET` | `/api/master-data/:category` |
| `POST` | `/api/master-data/:category` |
| `GET` | `/api/master-data/:category/:id` |
| `PUT` | `/api/master-data/:category/:id` |
| Method | Path |
| -------- | -------------------------------- |
| `GET` | `/api/health` |
| `GET` | `/api/master-data/:category` |
| `POST` | `/api/master-data/:category` |
| `GET` | `/api/master-data/:category/:id` |
| `PUT` | `/api/master-data/:category/:id` |
| `DELETE` | `/api/master-data/:category/:id` |
| `GET` | `/api/cohorts?page=1&limit=10` |
| `POST` | `/api/cohorts` |
| `GET` | `/api/cohorts/:id` |
| `PUT` | `/api/cohorts/:id` |
| `PATCH` | `/api/cohorts/:id/status` |
| `DELETE` | `/api/cohorts/:id` |
| `GET` | `/api/cohorts?page=1&limit=10` |
| `POST` | `/api/cohorts` |
| `GET` | `/api/cohorts/:id` |
| `PUT` | `/api/cohorts/:id` |
| `PATCH` | `/api/cohorts/:id/status` |
| `DELETE` | `/api/cohorts/:id` |
## Seed Master Data
+136
View File
@@ -0,0 +1,136 @@
[
{
"actionTypeCode": "HOTEL_ACCOMMODATION",
"fields": [
{
"fieldCode": "hotel_tier",
"fieldName": "Hotel Tier",
"fieldType": "dropdown",
"lookupSource": "hotel-tier",
"isRequired": true,
"width": "half",
"section": "Hotel Details",
"displayOrder": 1
},
{
"fieldCode": "room_type",
"fieldName": "Room Type",
"fieldType": "dropdown",
"lookupSource": "room-type",
"isRequired": true,
"width": "half",
"section": "Hotel Details",
"displayOrder": 2
},
{
"fieldCode": "number_of_rooms",
"fieldName": "Number of Rooms",
"fieldType": "number",
"isRequired": true,
"width": "half",
"section": "Hotel Details",
"displayOrder": 3
},
{
"fieldCode": "number_of_nights",
"fieldName": "Number of Nights",
"fieldType": "number",
"isRequired": true,
"width": "half",
"section": "Hotel Details",
"displayOrder": 4
},
{
"fieldCode": "meal_included",
"fieldName": "Meal Included",
"fieldType": "multi_select",
"lookupSource": "meal-type",
"isRequired": false,
"width": "full",
"section": "Hotel Details",
"displayOrder": 5
},
{
"fieldCode": "check_in_date",
"fieldName": "Check-in Date",
"fieldType": "date",
"isRequired": true,
"width": "half",
"section": "Schedule",
"displayOrder": 6
},
{
"fieldCode": "check_out_date",
"fieldName": "Check-out Date",
"fieldType": "date",
"isRequired": true,
"width": "half",
"section": "Schedule",
"displayOrder": 7
},
{
"fieldCode": "preferred_hotel_chain",
"fieldName": "Preferred Hotel Chain",
"fieldType": "dropdown",
"lookupSource": "hotel-chain",
"isRequired": false,
"width": "full",
"section": "Preferences",
"displayOrder": 8
},
{
"fieldCode": "maximum_budget",
"fieldName": "Maximum Budget",
"fieldType": "currency",
"isRequired": false,
"width": "half",
"section": "Preferences",
"displayOrder": 9,
"lookupSource": "currency"
},
{
"fieldCode": "approval_required",
"fieldName": "Approval Required",
"fieldType": "switch",
"isRequired": false,
"width": "half",
"section": "Preferences",
"displayOrder": 10
}
]
},
{
"actionTypeCode": "AIRPORT_HOTEL",
"fields": [
{
"fieldCode": "hotel_category",
"fieldName": "Hotel Category",
"fieldType": "dropdown",
"lookupSource": "hotel-category",
"isRequired": true,
"width": "full",
"section": "General Information",
"displayOrder": 1
},
{
"fieldCode": "duration_hours",
"fieldName": "Duration (Hours)",
"fieldType": "number",
"isRequired": true,
"width": "half",
"section": "General Information",
"displayOrder": 2
},
{
"fieldCode": "maximum_budget",
"fieldName": "Maximum Budget",
"fieldType": "currency",
"isRequired": false,
"width": "half",
"section": "General Information",
"displayOrder": 3,
"lookupSource": "currency"
}
]
}
]
@@ -0,0 +1,115 @@
[
{
"actionTypeCode": "REGULATORY_COMPENSATION",
"fields": [
{
"fieldCode": "compensation_rule",
"fieldName": "Compensation Rule",
"fieldType": "dropdown",
"lookupSource": "compensation-rule",
"isRequired": true,
"width": "half",
"section": "Compensation Rules",
"displayOrder": 1
},
{
"fieldCode": "amount_type",
"fieldName": "Amount Type",
"fieldType": "dropdown",
"lookupSource": "amount-type",
"isRequired": true,
"width": "half",
"section": "Compensation Rules",
"displayOrder": 2
},
{
"fieldCode": "compensation_amount",
"fieldName": "Compensation Amount",
"fieldType": "currency",
"isRequired": true,
"width": "half",
"section": "Finance",
"displayOrder": 3,
"lookupSource": "currency"
},
{
"fieldCode": "payment_method",
"fieldName": "Payment Method",
"fieldType": "dropdown",
"lookupSource": "refund-method",
"isRequired": true,
"width": "half",
"section": "Finance",
"displayOrder": 4
},
{
"fieldCode": "payment_due_within_days",
"fieldName": "Payment Due Within (Days)",
"fieldType": "number",
"isRequired": false,
"width": "half",
"section": "Finance",
"displayOrder": 5
},
{
"fieldCode": "approval_required",
"fieldName": "Approval Required",
"fieldType": "switch",
"isRequired": false,
"width": "full",
"section": "Approval",
"displayOrder": 6
}
]
},
{
"actionTypeCode": "AIRLINE_GOODWILL_CASH",
"fields": [
{
"fieldCode": "goodwill_amount",
"fieldName": "Goodwill Amount",
"fieldType": "currency",
"isRequired": true,
"width": "half",
"section": "Goodwill Details",
"displayOrder": 1,
"lookupSource": "currency"
},
{
"fieldCode": "reason_code",
"fieldName": "Reason Code",
"fieldType": "dropdown",
"lookupSource": "goodwill-reason",
"isRequired": true,
"width": "full",
"section": "Goodwill Details",
"displayOrder": 2
}
]
},
{
"actionTypeCode": "EX_GRATIA_COMPENSATION",
"fields": [
{
"fieldCode": "amount",
"fieldName": "Amount",
"fieldType": "currency",
"isRequired": true,
"width": "half",
"section": "Finance",
"displayOrder": 1,
"lookupSource": "currency"
},
{
"fieldCode": "approval_level",
"fieldName": "Approval Level",
"fieldType": "dropdown",
"lookupSource": "approval-level",
"isRequired": true,
"width": "full",
"section": "Approval",
"displayOrder": 2
}
]
}
]
@@ -0,0 +1,106 @@
[
{
"actionTypeCode": "AWARD_MILES",
"fields": [
{
"fieldCode": "miles_to_credit",
"fieldName": "Miles to Credit",
"fieldType": "number",
"isRequired": true,
"width": "half",
"section": "Loyalty",
"displayOrder": 1
},
{
"fieldCode": "expiry_date",
"fieldName": "Expiry",
"fieldType": "date",
"isRequired": false,
"width": "half",
"section": "Loyalty",
"displayOrder": 2
},
{
"fieldCode": "loyalty_program",
"fieldName": "Loyalty Program",
"fieldType": "dropdown",
"lookupSource": "loyalty-program",
"isRequired": true,
"width": "full",
"section": "Loyalty",
"displayOrder": 3
}
]
},
{
"actionTypeCode": "BONUS_MILES",
"fields": [
{
"fieldCode": "bonus_miles",
"fieldName": "Bonus Miles",
"fieldType": "number",
"isRequired": true,
"width": "half",
"section": "Loyalty",
"displayOrder": 1
},
{
"fieldCode": "promotion_code",
"fieldName": "Promotion Code",
"fieldType": "dropdown",
"lookupSource": "promotion-code",
"isRequired": false,
"width": "half",
"section": "Loyalty",
"displayOrder": 2
}
]
},
{
"actionTypeCode": "TIER_POINTS",
"fields": [
{
"fieldCode": "tier_points",
"fieldName": "Tier Points",
"fieldType": "number",
"isRequired": true,
"width": "half",
"section": "Loyalty",
"displayOrder": 1
},
{
"fieldCode": "validity_date",
"fieldName": "Validity",
"fieldType": "date",
"isRequired": false,
"width": "half",
"section": "Loyalty",
"displayOrder": 2
}
]
},
{
"actionTypeCode": "TIER_UPGRADE",
"fields": [
{
"fieldCode": "target_tier",
"fieldName": "Target Tier",
"fieldType": "dropdown",
"lookupSource": "membership-tier",
"isRequired": true,
"width": "half",
"section": "Loyalty",
"displayOrder": 1
},
{
"fieldCode": "upgrade_duration_months",
"fieldName": "Upgrade Duration (Months)",
"fieldType": "number",
"isRequired": true,
"width": "half",
"section": "Loyalty",
"displayOrder": 2
}
]
}
]
+94
View File
@@ -0,0 +1,94 @@
[
{
"actionTypeCode": "MEAL_VOUCHER",
"fields": [
{
"fieldCode": "voucher_amount",
"fieldName": "Voucher Amount",
"fieldType": "currency",
"isRequired": true,
"width": "half",
"section": "General Information",
"displayOrder": 1,
"lookupSource": "currency"
},
{
"fieldCode": "meal_type",
"fieldName": "Meal Type",
"fieldType": "multi_select",
"lookupSource": "meal-type",
"isRequired": false,
"width": "full",
"section": "General Information",
"displayOrder": 2
},
{
"fieldCode": "validity_hours",
"fieldName": "Validity (Hours)",
"fieldType": "number",
"isRequired": true,
"width": "half",
"section": "Restrictions",
"displayOrder": 3
},
{
"fieldCode": "airport_restriction",
"fieldName": "Airport Restriction",
"fieldType": "multi_select",
"lookupSource": "airport-restriction",
"isRequired": false,
"width": "full",
"section": "Restrictions",
"displayOrder": 4
}
]
},
{
"actionTypeCode": "RESTAURANT_VOUCHER",
"fields": [
{
"fieldCode": "partner_restaurant",
"fieldName": "Partner Restaurant",
"fieldType": "dropdown",
"isRequired": true,
"width": "full",
"section": "General Information",
"displayOrder": 1
},
{
"fieldCode": "voucher_value",
"fieldName": "Voucher Value",
"fieldType": "currency",
"isRequired": true,
"width": "half",
"section": "General Information",
"displayOrder": 2,
"lookupSource": "currency"
},
{
"fieldCode": "expiry_date",
"fieldName": "Expiry",
"fieldType": "date",
"isRequired": true,
"width": "half",
"section": "Restrictions",
"displayOrder": 3
}
]
},
{
"actionTypeCode": "REFRESHMENT_COUPON",
"fields": [
{
"fieldCode": "coupon_amount",
"fieldName": "Coupon Amount",
"fieldType": "currency",
"isRequired": true,
"width": "half",
"section": "General Information",
"displayOrder": 1,
"lookupSource": "currency"
}
]
}
]
+311
View File
@@ -0,0 +1,311 @@
[
{
"actionTypeCode": "FULL_TICKET_REFUND",
"fields": [
{
"fieldCode": "refund_basis",
"fieldName": "Refund Basis",
"fieldType": "dropdown",
"lookupSource": "refund-basis",
"isRequired": true,
"width": "half",
"section": "Finance / Payment Gateway",
"displayOrder": 1
},
{
"fieldCode": "amount_type",
"fieldName": "Amount Type",
"fieldType": "dropdown",
"lookupSource": "amount-type",
"isRequired": true,
"width": "half",
"section": "Finance / Payment Gateway",
"displayOrder": 2
},
{
"fieldCode": "refund_amount",
"fieldName": "Refund Amount",
"fieldType": "currency",
"isRequired": true,
"width": "half",
"section": "Finance / Payment Gateway",
"displayOrder": 3,
"lookupSource": "currency"
},
{
"fieldCode": "maximum_refund_limit",
"fieldName": "Maximum Refund Limit",
"fieldType": "currency",
"isRequired": false,
"width": "half",
"section": "Finance / Payment Gateway",
"displayOrder": 4,
"lookupSource": "currency"
},
{
"fieldCode": "refund_method",
"fieldName": "Refund Method",
"fieldType": "dropdown",
"lookupSource": "refund-method",
"isRequired": true,
"width": "half",
"section": "Finance / Payment Gateway",
"displayOrder": 5
},
{
"fieldCode": "approval_required",
"fieldName": "Approval Required",
"fieldType": "switch",
"isRequired": false,
"width": "full",
"section": "Finance / Payment Gateway",
"displayOrder": 6
}
]
},
{
"actionTypeCode": "PARTIAL_TICKET_REFUND",
"fields": [
{
"fieldCode": "refund_component",
"fieldName": "Refund Component",
"fieldType": "multi_select",
"lookupSource": "refund-component",
"isRequired": true,
"width": "full",
"section": "Finance",
"displayOrder": 1
},
{
"fieldCode": "amount_type",
"fieldName": "Amount Type",
"fieldType": "dropdown",
"lookupSource": "amount-type",
"isRequired": true,
"width": "half",
"section": "Finance",
"displayOrder": 2
},
{
"fieldCode": "refund_percentage",
"fieldName": "Refund Percentage",
"fieldType": "number",
"isRequired": false,
"width": "half",
"section": "Finance",
"displayOrder": 3
},
{
"fieldCode": "refund_amount",
"fieldName": "Refund Amount",
"fieldType": "currency",
"isRequired": false,
"width": "half",
"section": "Finance",
"displayOrder": 4,
"lookupSource": "currency"
},
{
"fieldCode": "maximum_refund_limit",
"fieldName": "Maximum Refund Limit",
"fieldType": "currency",
"isRequired": false,
"width": "half",
"section": "Finance",
"displayOrder": 5,
"lookupSource": "currency"
},
{
"fieldCode": "refund_method",
"fieldName": "Refund Method",
"fieldType": "dropdown",
"lookupSource": "refund-method",
"isRequired": true,
"width": "half",
"section": "Finance",
"displayOrder": 6
},
{
"fieldCode": "approval_required",
"fieldName": "Approval Required",
"fieldType": "switch",
"isRequired": false,
"width": "full",
"section": "Finance",
"displayOrder": 7
}
]
},
{
"actionTypeCode": "TAX_REFUND",
"fields": [
{
"fieldCode": "tax_type",
"fieldName": "Tax Type",
"fieldType": "multi_select",
"lookupSource": "tax-type",
"isRequired": true,
"width": "full",
"section": "Amount Type",
"displayOrder": 1
},
{
"fieldCode": "amount_type",
"fieldName": "Amount Type",
"fieldType": "dropdown",
"lookupSource": "amount-type",
"isRequired": true,
"width": "half",
"section": "Refund Amount",
"displayOrder": 2
},
{
"fieldCode": "refund_amount",
"fieldName": "Refund Amount",
"fieldType": "currency",
"isRequired": true,
"width": "half",
"section": "Refund Amount",
"displayOrder": 3,
"lookupSource": "currency"
},
{
"fieldCode": "refund_method",
"fieldName": "Refund Method",
"fieldType": "dropdown",
"lookupSource": "refund-method",
"isRequired": true,
"width": "half",
"section": "Refund Method",
"displayOrder": 4
},
{
"fieldCode": "approval_required",
"fieldName": "Approval Required",
"fieldType": "switch",
"isRequired": false,
"width": "full",
"section": "Approval Required",
"displayOrder": 5
}
]
},
{
"actionTypeCode": "ANCILLARY_REFUND",
"fields": [
{
"fieldCode": "ancillary_type",
"fieldName": "Ancillary Type",
"fieldType": "multi_select",
"lookupSource": "ancillary-purchase",
"isRequired": true,
"width": "full",
"section": "Amount Type",
"displayOrder": 1
},
{
"fieldCode": "amount_type",
"fieldName": "Amount Type",
"fieldType": "dropdown",
"lookupSource": "amount-type",
"isRequired": true,
"width": "half",
"section": "Refund Amount",
"displayOrder": 2
},
{
"fieldCode": "refund_amount",
"fieldName": "Refund Amount",
"fieldType": "currency",
"isRequired": true,
"width": "half",
"section": "Refund Amount",
"displayOrder": 3,
"lookupSource": "currency"
},
{
"fieldCode": "refund_method",
"fieldName": "Refund Method",
"fieldType": "dropdown",
"lookupSource": "refund-method",
"isRequired": true,
"width": "half",
"section": "Refund Method",
"displayOrder": 4
},
{
"fieldCode": "approval_required",
"fieldName": "Approval Required",
"fieldType": "switch",
"isRequired": false,
"width": "full",
"section": "Approval Required",
"displayOrder": 5
}
]
},
{
"actionTypeCode": "BAGGAGE_FEE_REFUND",
"fields": [
{
"fieldCode": "baggage_type",
"fieldName": "Baggage Type",
"fieldType": "dropdown",
"lookupSource": "baggage-type",
"isRequired": true,
"width": "half",
"section": "Refund Basis",
"displayOrder": 1
},
{
"fieldCode": "refund_basis",
"fieldName": "Refund Basis",
"fieldType": "dropdown",
"lookupSource": "refund-basis",
"isRequired": true,
"width": "half",
"section": "Refund Basis",
"displayOrder": 2
},
{
"fieldCode": "amount_type",
"fieldName": "Amount Type",
"fieldType": "dropdown",
"lookupSource": "amount-type",
"isRequired": true,
"width": "half",
"section": "Refund Amount",
"displayOrder": 3
},
{
"fieldCode": "refund_amount",
"fieldName": "Refund Amount",
"fieldType": "currency",
"isRequired": true,
"width": "half",
"section": "Refund Amount",
"displayOrder": 4,
"lookupSource": "currency"
},
{
"fieldCode": "refund_method",
"fieldName": "Refund Method",
"fieldType": "dropdown",
"lookupSource": "refund-method",
"isRequired": true,
"width": "half",
"section": "Refund Method",
"displayOrder": 5
},
{
"fieldCode": "approval_required",
"fieldName": "Approval Required",
"fieldType": "switch",
"isRequired": false,
"width": "full",
"section": "Approval Required",
"displayOrder": 6
}
]
}
]
+133
View File
@@ -0,0 +1,133 @@
[
{
"actionTypeCode": "TAXI",
"fields": [
{
"fieldCode": "maximum_fare",
"fieldName": "Maximum Fare",
"fieldType": "currency",
"isRequired": true,
"width": "half",
"section": "Ground Transport",
"displayOrder": 1,
"lookupSource": "currency"
},
{
"fieldCode": "pickup_radius_km",
"fieldName": "Pickup Radius (Kilometers)",
"fieldType": "number",
"isRequired": true,
"width": "half",
"section": "Ground Transport",
"displayOrder": 2
},
{
"fieldCode": "destination_type",
"fieldName": "Destination Type",
"fieldType": "dropdown",
"lookupSource": "destination-type",
"isRequired": true,
"width": "half",
"section": "Ground Transport",
"displayOrder": 3
}
]
},
{
"actionTypeCode": "AIRPORT_TRANSFER",
"fields": [
{
"fieldCode": "vehicle_type",
"fieldName": "Vehicle Type",
"fieldType": "dropdown",
"lookupSource": "vehicle-category",
"isRequired": true,
"width": "half",
"section": "Ground Transport",
"displayOrder": 1
},
{
"fieldCode": "maximum_distance_km",
"fieldName": "Maximum Distance (Kilometers)",
"fieldType": "number",
"isRequired": true,
"width": "half",
"section": "Ground Transport",
"displayOrder": 2
}
]
},
{
"actionTypeCode": "BUS_TRANSFER",
"fields": [
{
"fieldCode": "route",
"fieldName": "Route",
"fieldType": "dropdown",
"lookupSource": "route",
"isRequired": true,
"width": "half",
"section": "Ground Transport",
"displayOrder": 1
},
{
"fieldCode": "number_of_passengers",
"fieldName": "Number of Passengers",
"fieldType": "number",
"isRequired": true,
"width": "half",
"section": "Ground Transport",
"displayOrder": 2
}
]
},
{
"actionTypeCode": "TRAIN_TICKET",
"fields": [
{
"fieldCode": "ticket_class",
"fieldName": "Ticket Class",
"fieldType": "dropdown",
"lookupSource": "cabin-class",
"isRequired": true,
"width": "half",
"section": "Partner API",
"displayOrder": 1
},
{
"fieldCode": "maximum_fare",
"fieldName": "Maximum Fare",
"fieldType": "currency",
"isRequired": true,
"width": "half",
"section": "Partner API",
"displayOrder": 2,
"lookupSource": "currency"
}
]
},
{
"actionTypeCode": "CHAUFFEUR_SERVICE",
"fields": [
{
"fieldCode": "vehicle_category",
"fieldName": "Vehicle Category",
"fieldType": "dropdown",
"lookupSource": "vehicle-category",
"isRequired": true,
"width": "half",
"section": "Partner API",
"displayOrder": 1
},
{
"fieldCode": "service_radius_km",
"fieldName": "Service Radius (Kilometers)",
"fieldType": "number",
"isRequired": true,
"width": "half",
"section": "Partner API",
"displayOrder": 2
}
]
}
]
+155
View File
@@ -0,0 +1,155 @@
[
{
"actionTypeCode": "TRAVEL_VOUCHER",
"fields": [
{
"fieldCode": "voucher_value",
"fieldName": "Voucher Value",
"fieldType": "currency",
"isRequired": true,
"width": "half",
"section": "Voucher Details",
"displayOrder": 1,
"lookupSource": "currency"
},
{
"fieldCode": "validity_months",
"fieldName": "Validity (Months)",
"fieldType": "number",
"isRequired": true,
"width": "half",
"section": "Voucher Rules",
"displayOrder": 2
},
{
"fieldCode": "transferable",
"fieldName": "Transferable",
"fieldType": "switch",
"isRequired": false,
"width": "half",
"section": "Voucher Rules",
"displayOrder": 3
},
{
"fieldCode": "applicable_routes",
"fieldName": "Applicable Routes",
"fieldType": "multi_select",
"lookupSource": "flight-type",
"isRequired": false,
"width": "full",
"section": "Voucher Rules",
"displayOrder": 4
},
{
"fieldCode": "fare_classes",
"fieldName": "Fare Classes",
"fieldType": "multi_select",
"lookupSource": "cabin-class",
"isRequired": false,
"width": "full",
"section": "Voucher Rules",
"displayOrder": 5
}
]
},
{
"actionTypeCode": "FUTURE_TRAVEL_CREDIT",
"fields": [
{
"fieldCode": "credit_amount",
"fieldName": "Credit Amount",
"fieldType": "currency",
"isRequired": true,
"width": "half",
"section": "Credit Details",
"displayOrder": 1,
"lookupSource": "currency"
},
{
"fieldCode": "expiry_date",
"fieldName": "Expiry Date",
"fieldType": "date",
"isRequired": true,
"width": "half",
"section": "Credit Rules",
"displayOrder": 2
},
{
"fieldCode": "eligible_fare_types",
"fieldName": "Eligible Fare Types",
"fieldType": "multi_select",
"lookupSource": "eligible-fare-type",
"isRequired": false,
"width": "full",
"section": "Credit Rules",
"displayOrder": 3
}
]
},
{
"actionTypeCode": "PROMO_CODE",
"fields": [
{
"fieldCode": "code_value",
"fieldName": "Code Value",
"fieldType": "text",
"isRequired": true,
"width": "full",
"section": "Promo Details",
"displayOrder": 1
},
{
"fieldCode": "validity_date",
"fieldName": "Validity",
"fieldType": "date",
"isRequired": true,
"width": "half",
"section": "Promo Rules",
"displayOrder": 2
},
{
"fieldCode": "applicable_products",
"fieldName": "Applicable Products",
"fieldType": "multi_select",
"lookupSource": "applicable-product",
"isRequired": false,
"width": "full",
"section": "Promo Rules",
"displayOrder": 3
}
]
},
{
"actionTypeCode": "DISCOUNT_COUPON",
"fields": [
{
"fieldCode": "discount_percentage",
"fieldName": "Discount Percentage",
"fieldType": "number",
"isRequired": true,
"width": "half",
"section": "Discount Details",
"displayOrder": 1
},
{
"fieldCode": "maximum_discount",
"fieldName": "Maximum Discount",
"fieldType": "currency",
"isRequired": false,
"width": "half",
"section": "Discount Details",
"displayOrder": 2,
"lookupSource": "currency"
},
{
"fieldCode": "expiry_date",
"fieldName": "Expiry",
"fieldType": "date",
"isRequired": true,
"width": "half",
"section": "Discount Rules",
"displayOrder": 3
}
]
}
]
+480 -42
View File
@@ -3,23 +3,44 @@
-- PostgreSQL 14+
-- Generated from TypeORM entities (aeroresolve_backend)
-- =============================================================================
--
-- Usage:
-- psql -h <host> -U <user> -d <database> -f database/schema.sql
--
-- Notes:
-- - Column names use camelCase to match TypeORM entity field names.
-- - In local/dev, TypeORM can also sync via DB_SYNCHRONIZE=true.
-- - Run scripts/seed-master-data.ts after schema creation for demo data.
-- =============================================================================
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ─── Schemas ────────────────────────────────────────────────────────────────
CREATE SCHEMA IF NOT EXISTS tenant;
CREATE SCHEMA IF NOT EXISTS masters;
CREATE SCHEMA IF NOT EXISTS masters_rule_engine;
CREATE SCHEMA IF NOT EXISTS masters_action_builder;
CREATE SCHEMA IF NOT EXISTS masters_lookup;
CREATE SCHEMA IF NOT EXISTS cohort;
CREATE SCHEMA IF NOT EXISTS policy_engine;
CREATE SCHEMA IF NOT EXISTS audit;
CREATE SCHEMA IF NOT EXISTS recovery_incident;
-- ─── Audit Logs ──────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS audit.tbl_audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id VARCHAR(255) NOT NULL,
module VARCHAR(100) NOT NULL,
action VARCHAR(50) NOT NULL,
entity_id VARCHAR(255),
entity_label VARCHAR(255),
before JSONB,
after JSONB,
performed_by VARCHAR(255),
ip_address VARCHAR(50),
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_audit_tenant ON audit.tbl_audit_logs (tenant_id);
CREATE INDEX IF NOT EXISTS idx_audit_module ON audit.tbl_audit_logs (module);
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit.tbl_audit_logs (action);
CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit.tbl_audit_logs (entity_id);
CREATE INDEX IF NOT EXISTS idx_audit_createdat ON audit.tbl_audit_logs (created_at DESC);
-- ─── Enum Types ─────────────────────────────────────────────────────────────
@@ -56,95 +77,437 @@ CREATE TABLE IF NOT EXISTS tenant.tbl_tenants (
CONSTRAINT uq_tbl_tenants_slug UNIQUE (slug)
);
-- ─── Master Data ────────────────────────────────────────────────────────────
-- ─── 1. Masters Lookup Schema (Domain Master Tables) ─────────────────────────
CREATE TABLE IF NOT EXISTS masters.tbl_membership_tiers (
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_membership_tiers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenantId" UUID NOT NULL REFERENCES tenant.tbl_tenants(id) ON DELETE CASCADE,
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_tbl_membership_tiers_tenant ON masters.tbl_membership_tiers("tenantId");
CREATE TABLE IF NOT EXISTS masters.tbl_customer_values (
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_customer_values (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenantId" UUID NOT NULL REFERENCES tenant.tbl_tenants(id) ON DELETE CASCADE,
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_tbl_customer_values_tenant ON masters.tbl_customer_values("tenantId");
CREATE TABLE IF NOT EXISTS masters.tbl_regions (
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_regions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenantId" UUID NOT NULL REFERENCES tenant.tbl_tenants(id) ON DELETE CASCADE,
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_tbl_regions_tenant ON masters.tbl_regions("tenantId");
CREATE TABLE IF NOT EXISTS masters.tbl_trip_purposes (
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_trip_purposes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenantId" UUID NOT NULL REFERENCES tenant.tbl_tenants(id) ON DELETE CASCADE,
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_tbl_trip_purposes_tenant ON masters.tbl_trip_purposes("tenantId");
CREATE TABLE IF NOT EXISTS masters.tbl_cabin_classes (
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_cabin_classes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenantId" UUID NOT NULL REFERENCES tenant.tbl_tenants(id) ON DELETE CASCADE,
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_tbl_cabin_classes_tenant ON masters.tbl_cabin_classes("tenantId");
CREATE TABLE IF NOT EXISTS masters.tbl_passenger_types (
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_passenger_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenantId" UUID NOT NULL REFERENCES tenant.tbl_tenants(id) ON DELETE CASCADE,
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_tbl_passenger_types_tenant ON masters.tbl_passenger_types("tenantId");
CREATE TABLE IF NOT EXISTS masters.tbl_ancillary_purchases (
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_ancillary_purchases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenantId" UUID NOT NULL REFERENCES tenant.tbl_tenants(id) ON DELETE CASCADE,
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_tbl_ancillary_purchases_tenant ON masters.tbl_ancillary_purchases("tenantId");
CREATE TABLE IF NOT EXISTS masters.tbl_revenue_segments (
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_revenue_segments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenantId" UUID NOT NULL REFERENCES tenant.tbl_tenants(id) ON DELETE CASCADE,
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_tbl_revenue_segments_tenant ON masters.tbl_revenue_segments("tenantId");
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_jurisdictions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_booking_channels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_flight_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_journey_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_fare_flexibilities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_carrier_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_special_assistance_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_delay_reasons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_delay_durations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_extraordinary_circumstances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_cancellation_reasons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_diversion_reasons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_missed_connection_reasons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_compensation_eligibilities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_compensation_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_refund_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_flight_disruption_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_airline_responsibilities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_weather_conditions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_atc_restrictions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_technical_fault_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_refund_bases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_currencies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
symbol VARCHAR,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_refund_methods (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_lookup.tbl_amount_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
label VARCHAR NOT NULL,
value VARCHAR NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
-- ─── 2. Masters Rule Engine Schema (Condition & Category Metadata) ───────────
CREATE TABLE IF NOT EXISTS masters_rule_engine.tbl_rules_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(150) NOT NULL,
"tableName" VARCHAR,
description TEXT,
"displayOrder" INT DEFAULT 0,
"isActive" BOOLEAN DEFAULT TRUE,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_rule_engine.tbl_condition_groups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(150) NOT NULL,
"displayOrder" INT DEFAULT 0,
"isActive" BOOLEAN DEFAULT TRUE,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_rule_engine.tbl_rule_category_groups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"ruleCategoryId" UUID NOT NULL REFERENCES masters_rule_engine.tbl_rules_categories(id) ON DELETE CASCADE,
"conditionGroupId" UUID NOT NULL REFERENCES masters_rule_engine.tbl_condition_groups(id) ON DELETE CASCADE,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT uq_rule_category_group UNIQUE ("ruleCategoryId", "conditionGroupId")
);
CREATE TABLE IF NOT EXISTS masters_rule_engine.tbl_condition_fields (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"groupId" UUID NOT NULL REFERENCES masters_rule_engine.tbl_condition_groups(id) ON DELETE CASCADE,
code VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(150) NOT NULL,
"lookupTable" VARCHAR,
"dataType" VARCHAR NOT NULL DEFAULT 'STRING',
"operatorType" VARCHAR NOT NULL DEFAULT 'COMPARISON',
"displayOrder" INT DEFAULT 0,
"isActive" BOOLEAN DEFAULT TRUE,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_rule_engine.tbl_operators (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
symbol VARCHAR(20),
"dataTypes" TEXT[],
"isActive" BOOLEAN DEFAULT TRUE,
"displayOrder" INT DEFAULT 0,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
-- ─── 3. Masters Action Builder Schema ───────────────────────────────────────
CREATE TABLE IF NOT EXISTS masters_action_builder.tbl_action_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR NOT NULL UNIQUE,
name VARCHAR NOT NULL,
description TEXT,
"displayOrder" INT DEFAULT 0,
"isActive" BOOLEAN DEFAULT TRUE,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_action_builder.tbl_action_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"categoryId" UUID NOT NULL REFERENCES masters_action_builder.tbl_action_categories(id) ON DELETE CASCADE,
code VARCHAR NOT NULL UNIQUE,
name VARCHAR NOT NULL,
description TEXT,
"displayOrder" INT DEFAULT 0,
"isActive" BOOLEAN DEFAULT TRUE,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_action_builder.tbl_field_definitions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"actionTypeId" UUID NOT NULL REFERENCES masters_action_builder.tbl_action_types(id) ON DELETE CASCADE,
"fieldCode" VARCHAR NOT NULL,
"fieldName" VARCHAR NOT NULL,
"fieldType" VARCHAR NOT NULL,
"lookupSource" VARCHAR,
"isRequired" BOOLEAN NOT NULL DEFAULT false,
"defaultValue" TEXT,
placeholder VARCHAR,
"helpText" TEXT,
width VARCHAR NOT NULL DEFAULT 'full',
section VARCHAR,
"displayOrder" INT NOT NULL DEFAULT 0,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"validationJson" JSONB,
"visibilityConditionJson" JSONB,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_field_definition_action_type_code
ON masters_action_builder.tbl_field_definitions("actionTypeId", "fieldCode");
CREATE TABLE IF NOT EXISTS masters_action_builder.tbl_action_submissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"categoryId" UUID NOT NULL REFERENCES masters_action_builder.tbl_action_categories(id) ON DELETE CASCADE,
"actionTypeId" UUID NOT NULL REFERENCES masters_action_builder.tbl_action_types(id) ON DELETE CASCADE,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS masters_action_builder.tbl_action_submission_values (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"submissionId" UUID NOT NULL REFERENCES masters_action_builder.tbl_action_submissions(id) ON DELETE CASCADE,
"fieldDefinitionId" UUID REFERENCES masters_action_builder.tbl_field_definitions(id) ON DELETE CASCADE,
"fieldCode" VARCHAR,
"valueIndex" INT NOT NULL DEFAULT 0,
"selectedValueId" VARCHAR,
"textValue" TEXT,
"numberValue" NUMERIC,
"booleanValue" BOOLEAN,
"dateValue" DATE,
"timeValue" TIME,
"timestampValue" TIMESTAMP WITH TIME ZONE,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
-- ─── Cohorts ────────────────────────────────────────────────────────────────
@@ -168,42 +531,117 @@ CREATE INDEX IF NOT EXISTS idx_tbl_cohorts_tenant ON cohort.tbl_cohorts("tenantI
CREATE TABLE IF NOT EXISTS cohort.tbl_cohort_cabin_classes (
"cohortId" UUID NOT NULL REFERENCES cohort.tbl_cohorts(id) ON DELETE CASCADE,
"cabinClassId" UUID NOT NULL REFERENCES masters.tbl_cabin_classes(id) ON DELETE CASCADE,
"cabinClassId" UUID NOT NULL REFERENCES masters_lookup.tbl_cabin_classes(id) ON DELETE CASCADE,
PRIMARY KEY ("cohortId", "cabinClassId")
);
CREATE TABLE IF NOT EXISTS cohort.tbl_cohort_passenger_types (
"cohortId" UUID NOT NULL REFERENCES cohort.tbl_cohorts(id) ON DELETE CASCADE,
"passengerTypeId" UUID NOT NULL REFERENCES masters.tbl_passenger_types(id) ON DELETE CASCADE,
"passengerTypeId" UUID NOT NULL REFERENCES masters_lookup.tbl_passenger_types(id) ON DELETE CASCADE,
PRIMARY KEY ("cohortId", "passengerTypeId")
);
CREATE TABLE IF NOT EXISTS cohort.tbl_cohort_ancillary_purchases (
"cohortId" UUID NOT NULL REFERENCES cohort.tbl_cohorts(id) ON DELETE CASCADE,
"ancillaryPurchaseId" UUID NOT NULL REFERENCES masters.tbl_ancillary_purchases(id) ON DELETE CASCADE,
"ancillaryPurchaseId" UUID NOT NULL REFERENCES masters_lookup.tbl_ancillary_purchases(id) ON DELETE CASCADE,
PRIMARY KEY ("cohortId", "ancillaryPurchaseId")
);
CREATE TABLE IF NOT EXISTS cohort.tbl_cohort_loyalty_tiers (
"cohortId" UUID NOT NULL REFERENCES cohort.tbl_cohorts(id) ON DELETE CASCADE,
"membershipTierId" UUID NOT NULL REFERENCES masters.tbl_membership_tiers(id) ON DELETE CASCADE,
"membershipTierId" UUID NOT NULL REFERENCES masters_lookup.tbl_membership_tiers(id) ON DELETE CASCADE,
PRIMARY KEY ("cohortId", "membershipTierId")
);
CREATE TABLE IF NOT EXISTS cohort.tbl_cohort_revenue_segments (
"cohortId" UUID NOT NULL REFERENCES cohort.tbl_cohorts(id) ON DELETE CASCADE,
"revenueSegmentId" UUID NOT NULL REFERENCES masters.tbl_revenue_segments(id) ON DELETE CASCADE,
"revenueSegmentId" UUID NOT NULL REFERENCES masters_lookup.tbl_revenue_segments(id) ON DELETE CASCADE,
PRIMARY KEY ("cohortId", "revenueSegmentId")
);
CREATE TABLE IF NOT EXISTS cohort.tbl_cohort_regions (
"cohortId" UUID NOT NULL REFERENCES cohort.tbl_cohorts(id) ON DELETE CASCADE,
"regionId" UUID NOT NULL REFERENCES masters.tbl_regions(id) ON DELETE CASCADE,
"regionId" UUID NOT NULL REFERENCES masters_lookup.tbl_regions(id) ON DELETE CASCADE,
PRIMARY KEY ("cohortId", "regionId")
);
CREATE TABLE IF NOT EXISTS cohort.tbl_cohort_trip_purposes (
"cohortId" UUID NOT NULL REFERENCES cohort.tbl_cohorts(id) ON DELETE CASCADE,
"tripPurposeId" UUID NOT NULL REFERENCES masters.tbl_trip_purposes(id) ON DELETE CASCADE,
"tripPurposeId" UUID NOT NULL REFERENCES masters_lookup.tbl_trip_purposes(id) ON DELETE CASCADE,
PRIMARY KEY ("cohortId", "tripPurposeId")
);
-- ─── Policy Engine ──────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS policy_engine.tbl_policies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenantId" UUID NOT NULL REFERENCES tenant.tbl_tenants(id) ON DELETE CASCADE,
policy_name VARCHAR(200) NOT NULL,
description TEXT,
status VARCHAR NOT NULL DEFAULT 'draft',
version INT NOT NULL DEFAULT 1,
audience_type VARCHAR NOT NULL DEFAULT 'ALL',
created_by UUID,
updated_by UUID,
created_at TIMESTAMP NOT NULL DEFAULT now(),
updated_at TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS policy_engine.tbl_policy_jurisdictions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
policy_id UUID NOT NULL REFERENCES policy_engine.tbl_policies(id) ON DELETE CASCADE,
jurisdiction_id UUID NOT NULL REFERENCES masters_lookup.tbl_jurisdictions(id) ON DELETE CASCADE,
created_at TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS policy_engine.tbl_policy_target_audiences (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
policy_id UUID NOT NULL REFERENCES policy_engine.tbl_policies(id) ON DELETE CASCADE,
target_type VARCHAR NOT NULL,
target_id UUID
);
CREATE TABLE IF NOT EXISTS policy_engine.tbl_policy_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
policy_id UUID NOT NULL REFERENCES policy_engine.tbl_policies(id) ON DELETE CASCADE,
rule_category_id UUID REFERENCES masters_rule_engine.tbl_rules_categories(id) ON DELETE SET NULL,
priority INT NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS policy_engine.tbl_policy_rule_conditions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rule_id UUID NOT NULL REFERENCES policy_engine.tbl_policy_rules(id) ON DELETE CASCADE,
field_id UUID NOT NULL,
operator_id UUID NOT NULL,
value_text TEXT,
logical_operator VARCHAR NOT NULL DEFAULT 'AND',
sequence INT NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS policy_engine.tbl_policy_actions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
rule_id UUID NOT NULL REFERENCES policy_engine.tbl_policy_rules(id) ON DELETE CASCADE,
action_type_id UUID NOT NULL REFERENCES masters_action_builder.tbl_action_types(id) ON DELETE CASCADE,
sequence INT NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS policy_engine.tbl_policy_action_values (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
action_id UUID NOT NULL REFERENCES policy_engine.tbl_policy_actions(id) ON DELETE CASCADE,
field_definition_id UUID REFERENCES masters_action_builder.tbl_field_definitions(id) ON DELETE SET NULL,
field_code VARCHAR NOT NULL,
value_index INT NOT NULL DEFAULT 0,
selected_value_id VARCHAR,
text_value TEXT,
number_value NUMERIC,
boolean_value BOOLEAN,
date_value DATE,
time_value TIME,
timestamp_value TIMESTAMP WITH TIME ZONE,
currency_code_id UUID,
created_at TIMESTAMP NOT NULL DEFAULT now(),
updated_at TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_tbl_policy_action_values_action
ON policy_engine.tbl_policy_action_values(action_id);
+939
View File
@@ -0,0 +1,939 @@
-- =============================================================================
-- AeroResolve Master Data Seed Script
-- PostgreSQL 14+
-- =============================================================================
CREATE SCHEMA IF NOT EXISTS masters_lookup;
CREATE SCHEMA IF NOT EXISTS masters_rule_engine;
CREATE SCHEMA IF NOT EXISTS masters_action_builder;
-- 1. Action Categories
INSERT INTO masters_action_builder.tbl_action_categories (code, name, "displayOrder", "isActive")
SELECT code, name, displayOrder, true FROM (VALUES
('REFUNDS', 'Refunds', 1),
('CASH_COMPENSATION', 'Cash Compensation', 2),
('TRAVEL_CREDITS', 'Travel Credits', 3),
('LOYALTY_BENEFITS', 'Loyalty Benefits', 4),
('ACCOMMODATION', 'Accommodation', 5),
('MEALS', 'Meals', 6),
('TRANSPORTATION', 'Transportation', 7),
('REBOOKING_TRAVEL', 'Rebooking & Travel', 8),
('UPGRADES', 'Upgrades', 9),
('ANCILLARY_RECOVERY', 'Ancillary Recovery', 10),
('COMMUNICATION', 'Communication', 11),
('WORKFLOW', 'Workflow', 12),
('FINANCE', 'Finance', 13),
('SYSTEM_INTEGRATION', 'System Integration', 14)
) AS t(code, name, displayOrder)
WHERE NOT EXISTS (SELECT 1 FROM masters_action_builder.tbl_action_categories WHERE masters_action_builder.tbl_action_categories.code = t.code);
-- 2. Action Types
INSERT INTO masters_action_builder.tbl_action_types (code, "categoryId", name, "displayOrder", "isActive")
SELECT v.code, c.id, v.name, v.display_order, true
FROM (VALUES
-- Refunds
('FULL_TICKET_REFUND', 'REFUNDS', 'Full Ticket Refund', 1),
('PARTIAL_TICKET_REFUND', 'REFUNDS', 'Partial Ticket Refund', 2),
('TAX_REFUND', 'REFUNDS', 'Tax Refund', 3),
('ANCILLARY_REFUND', 'REFUNDS', 'Ancillary Refund', 4),
('BAGGAGE_FEE_REFUND', 'REFUNDS', 'Baggage Fee Refund', 5),
('CABIN_DOWNGRADE_REFUND', 'REFUNDS', 'Cabin Downgrade Refund', 6),
-- Cash Compensation
('REGULATORY_COMPENSATION', 'CASH_COMPENSATION', 'Regulatory Compensation', 1),
('AIRLINE_GOODWILL_CASH', 'CASH_COMPENSATION', 'Airline Goodwill Cash', 2),
('EX_GRATIA_COMPENSATION', 'CASH_COMPENSATION', 'Ex-Gratia Compensation', 3),
-- Travel Credits
('TRAVEL_VOUCHER', 'TRAVEL_CREDITS', 'Travel Voucher', 1),
('FUTURE_TRAVEL_CREDIT', 'TRAVEL_CREDITS', 'Future Travel Credit', 2),
('PROMO_CODE', 'TRAVEL_CREDITS', 'Promo Code', 3),
('DISCOUNT_COUPON', 'TRAVEL_CREDITS', 'Discount Coupon', 4),
-- Loyalty Benefits
('AWARD_MILES', 'LOYALTY_BENEFITS', 'Award Miles', 1),
('BONUS_MILES', 'LOYALTY_BENEFITS', 'Bonus Miles', 2),
('TIER_POINTS', 'LOYALTY_BENEFITS', 'Tier Points', 3),
('TIER_UPGRADE', 'LOYALTY_BENEFITS', 'Tier Upgrade', 4),
-- Accommodation
('HOTEL_ACCOMMODATION', 'ACCOMMODATION', 'Hotel Accommodation', 1),
('AIRPORT_HOTEL', 'ACCOMMODATION', 'Airport Hotel', 2),
-- Meals
('MEAL_VOUCHER', 'MEALS', 'Meal Voucher', 1),
('RESTAURANT_VOUCHER', 'MEALS', 'Restaurant Voucher', 2),
('REFRESHMENT_COUPON', 'MEALS', 'Refreshment Coupon', 3),
-- Transportation
('TAXI', 'TRANSPORTATION', 'Taxi', 1),
('AIRPORT_TRANSFER', 'TRANSPORTATION', 'Airport Transfer', 2),
('BUS_TRANSFER', 'TRANSPORTATION', 'Bus Transfer', 3),
('TRAIN_TICKET', 'TRANSPORTATION', 'Train Ticket', 4),
('CHAUFFEUR_SERVICE', 'TRANSPORTATION', 'Chauffeur Service', 5),
-- Rebooking & Travel
('AUTO_REBOOK', 'REBOOKING_TRAVEL', 'Auto Rebook', 1),
('PRIORITY_REBOOKING', 'REBOOKING_TRAVEL', 'Priority Rebooking', 2),
('OPEN_TICKET', 'REBOOKING_TRAVEL', 'Open Ticket', 3),
('ALTERNATE_AIRLINE', 'REBOOKING_TRAVEL', 'Alternate Airline', 4),
-- Upgrades
('CABIN_UPGRADE', 'UPGRADES', 'Cabin Upgrade', 1),
('SEAT_UPGRADE', 'UPGRADES', 'Seat Upgrade', 2),
('LOUNGE_ACCESS', 'UPGRADES', 'Lounge Access', 3),
('FAST_TRACK_SECURITY', 'UPGRADES', 'Fast Track Security', 4),
('PRIORITY_BOARDING', 'UPGRADES', 'Priority Boarding', 5),
-- Ancillary Recovery
('COMPLIMENTARY_WIFI', 'ANCILLARY_RECOVERY', 'Complimentary Wi-Fi', 1),
('FREE_BAGGAGE', 'ANCILLARY_RECOVERY', 'Free Baggage', 2),
('COMPLIMENTARY_MEAL', 'ANCILLARY_RECOVERY', 'Complimentary Meal', 3),
('SEAT_SELECTION', 'ANCILLARY_RECOVERY', 'Seat Selection', 4),
('CARBON_OFFSET_CREDIT', 'ANCILLARY_RECOVERY', 'Carbon Offset Credit', 5),
-- Communication
('PASSENGER_NOTIFICATION', 'COMMUNICATION', 'Passenger Notification', 1),
('AGENT_NOTIFICATION', 'COMMUNICATION', 'Agent Notification', 2),
('MANAGEMENT_ALERT', 'COMMUNICATION', 'Management Alert', 3),
-- Workflow
('AUTO_APPROVE', 'WORKFLOW', 'Auto Approve', 1),
('MANUAL_REVIEW', 'WORKFLOW', 'Manual Review', 2),
('ESCALATE', 'WORKFLOW', 'Escalate', 3),
('CREATE_CASE', 'WORKFLOW', 'Create Case', 4),
('HOLD_FOR_INVESTIGATION', 'WORKFLOW', 'Hold for Investigation', 5),
-- Finance
('TRIGGER_PAYMENT', 'FINANCE', 'Trigger Payment', 1),
('GENERATE_CREDIT_NOTE', 'FINANCE', 'Generate Credit Note', 2),
('GENERATE_INVOICE', 'FINANCE', 'Generate Invoice', 3),
('WRITE_OFF', 'FINANCE', 'Write-off', 4),
-- System Integration
('UPDATE_CRM', 'SYSTEM_INTEGRATION', 'Update CRM', 1),
('UPDATE_LOYALTY', 'SYSTEM_INTEGRATION', 'Update Loyalty', 2),
('UPDATE_PSS', 'SYSTEM_INTEGRATION', 'Update PSS', 3),
('CREATE_AUDIT_RECORD', 'SYSTEM_INTEGRATION', 'Create Audit Record', 4),
('TRIGGER_WEBHOOK_API', 'SYSTEM_INTEGRATION', 'Trigger Webhook / API', 5)
) AS v(code, cat_code, name, display_order)
JOIN masters_action_builder.tbl_action_categories c ON c.code = v.cat_code
WHERE NOT EXISTS (SELECT 1 FROM masters_action_builder.tbl_action_types WHERE masters_action_builder.tbl_action_types.code = v.code);
-- 3. Membership Tiers
INSERT INTO masters_lookup.tbl_membership_tiers (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Platinum', 'platinum'),
('Gold', 'gold'),
('Silver', 'silver'),
('Bronze', 'bronze'),
('Basic', 'basic'),
('Non-Member', 'non-member')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_membership_tiers WHERE masters_lookup.tbl_membership_tiers.value = t.value);
-- 4. Customer Values
INSERT INTO masters_lookup.tbl_customer_values (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('High Value', 'high'),
('Medium Value', 'medium'),
('Low Value', 'low')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_customer_values WHERE masters_lookup.tbl_customer_values.value = t.value);
-- 5. Regions
INSERT INTO masters_lookup.tbl_regions (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Global', 'global'),
('Americas', 'americas'),
('Europe', 'europe'),
('Middle East', 'middle-east'),
('Asia Pacific', 'asia-pacific'),
('Africa', 'africa')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_regions WHERE masters_lookup.tbl_regions.value = t.value);
-- 6. Trip Purposes
INSERT INTO masters_lookup.tbl_trip_purposes (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Business', 'business'),
('Leisure', 'leisure'),
('Corporate', 'corporate'),
('Government', 'government')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_trip_purposes WHERE masters_lookup.tbl_trip_purposes.value = t.value);
-- 7. Cabin Classes
INSERT INTO masters_lookup.tbl_cabin_classes (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('First Class', 'first-class'),
('Business Class', 'business-class'),
('Premium Economy', 'premium-economy'),
('Economy', 'economy')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_cabin_classes WHERE masters_lookup.tbl_cabin_classes.value = t.value);
-- 8. Passenger Types
INSERT INTO masters_lookup.tbl_passenger_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Adult', 'adult'),
('Child', 'child'),
('Infant', 'infant'),
('Senior Citizen', 'senior')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_passenger_types WHERE masters_lookup.tbl_passenger_types.value = t.value);
-- 9. Ancillary Purchases
INSERT INTO masters_lookup.tbl_ancillary_purchases (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Preferred Seat', 'preferred-seat'),
('Extra Legroom', 'extra-legroom'),
('Wi-Fi', 'wi-fi'),
('Lounge Access', 'lounge-access'),
('Priority Boarding', 'priority-boarding'),
('Fast Track', 'fast-track'),
('Paid Meal', 'paid-meal'),
('Special Meal', 'special-meal'),
('Extra Baggage', 'extra-baggage'),
('Upgrade Purchase', 'upgrade-purchase'),
('Airport Transfer', 'airport-transfer'),
('Chauffeur Service', 'chauffeur-service'),
('Sports Equipment', 'sports-equipment'),
('Musical Instrument', 'musical-instrument'),
('In-flight Entertainment', 'in-flight-entertainment'),
('Power Outlet', 'power-outlet'),
('Carbon Offset', 'carbon-offset')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_ancillary_purchases WHERE masters_lookup.tbl_ancillary_purchases.value = t.value);
-- 10. Revenue Segments
INSERT INTO masters_lookup.tbl_revenue_segments (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('High Value', 'high'),
('Medium Value', 'medium'),
('Low Value', 'low')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_revenue_segments WHERE masters_lookup.tbl_revenue_segments.value = t.value);
-- 11. Jurisdictions
INSERT INTO masters_lookup.tbl_jurisdictions (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('United Arab Emirates', 'uae'),
('European Union', 'eu'),
('United States', 'us'),
('United Kingdom', 'uk'),
('India', 'india'),
('Asia Pacific', 'apac'),
('Global', 'global')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_jurisdictions WHERE masters_lookup.tbl_jurisdictions.value = t.value);
-- 12. Rule Categories (Business Rule Categories)
DELETE FROM masters_rule_engine.tbl_rules_categories
WHERE code NOT IN (
'FLIGHT_OPERATIONS', 'BAGGAGE_ISSUES', 'CABIN_DOWNGRADES',
'ANCILLARY_FAILURES', 'DENIED_BOARDING', 'MISSED_CONNECTIONS'
);
INSERT INTO masters_rule_engine.tbl_rules_categories (code, name, description, "displayOrder", "isActive")
SELECT code, name, description, displayOrder, true FROM (VALUES
('FLIGHT_OPERATIONS', 'Flight Operations', 'Rules for flight disruptions, delays, cancellations, and diversions', 1),
('BAGGAGE_ISSUES', 'Baggage Issues', 'Rules for delayed, damaged, or lost baggage recovery', 2),
('CABIN_DOWNGRADES', 'Cabin Downgrades', 'Rules for involuntary cabin downgrades and seat reassignments', 3),
('ANCILLARY_FAILURES', 'Ancillary Failures', 'Rules for unfulfilled ancillary services like Wi-Fi, meals, seats', 4),
('DENIED_BOARDING', 'Denied Boarding', 'Rules for overbooking and involuntary denied boarding', 5),
('MISSED_CONNECTIONS', 'Missed Connections', 'Rules for tight connection failures and rebooking compensation', 6)
) AS t(code, name, description, displayOrder)
ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description;
-- 13. Booking Channels
INSERT INTO masters_lookup.tbl_booking_channels (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Airline Website', 'airline-website'),
('Airline Mobile App', 'airline-mobile-app'),
('Airport Ticket Counter', 'airport-ticket-counter'),
('Call Center', 'call-center'),
('Corporate Booking Tool', 'corporate-booking-tool'),
('Global Distribution System', 'global-distribution-system'),
('Online Travel Agency', 'online-travel-agency'),
('Travel Agent', 'travel-agent')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_booking_channels WHERE masters_lookup.tbl_booking_channels.value = t.value);
-- 14. Flight Types
INSERT INTO masters_lookup.tbl_flight_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Domestic', 'domestic'),
('International', 'international')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_flight_types WHERE masters_lookup.tbl_flight_types.value = t.value);
-- 15. Journey Types
INSERT INTO masters_lookup.tbl_journey_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('One Way', 'one-way'),
('Round Trip', 'round-trip'),
('Multi City', 'multi-city')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_journey_types WHERE masters_lookup.tbl_journey_types.value = t.value);
-- 16. Fare Flexibilities
INSERT INTO masters_lookup.tbl_fare_flexibilities (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Non Refundable', 'non-refundable'),
('Partially Refundable', 'partially-refundable'),
('Refundable', 'refundable'),
('Exchangeable', 'exchangeable'),
('Non Changeable', 'non-changeable')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_fare_flexibilities WHERE masters_lookup.tbl_fare_flexibilities.value = t.value);
-- 17. Carrier Types
INSERT INTO masters_lookup.tbl_carrier_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Operating Carrier', 'operating-carrier'),
('Marketing Carrier', 'marketing-carrier'),
('Partner Carrier', 'partner-carrier'),
('Regional Carrier', 'regional-carrier'),
('Low Cost Carrier', 'low-cost-carrier'),
('Full Service Carrier', 'full-service-carrier'),
('Charter Carrier', 'charter-carrier')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_carrier_types WHERE masters_lookup.tbl_carrier_types.value = t.value);
-- 18. Special Assistance Types
INSERT INTO masters_lookup.tbl_special_assistance_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Wheelchair Assistance', 'wheelchair-assistance'),
('Wheelchair Ramp', 'wheelchair-ramp'),
('Wheelchair Steps', 'wheelchair-steps'),
('Wheelchair Cabin', 'wheelchair-cabin'),
('Blind Passenger', 'blind-passenger'),
('Deaf Passenger', 'deaf-passenger'),
('Medical Assistance', 'medical-assistance'),
('Oxygen Required', 'oxygen-required'),
('Stretcher', 'stretcher'),
('Unaccompanied Minor', 'unaccompanied-minor'),
('Service Animal', 'service-animal'),
('Pregnant Passenger', 'pregnant-passenger'),
('Elderly Passenger', 'elderly-passenger'),
('Other', 'other')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_special_assistance_types WHERE masters_lookup.tbl_special_assistance_types.value = t.value);
-- 19. Delay Reasons
INSERT INTO masters_lookup.tbl_delay_reasons (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Air Traffic Control Restriction', 'air-traffic-control-restriction'),
('Aircraft Rotation', 'aircraft-rotation'),
('Airport Congestion', 'airport-congestion'),
('Crew Availability', 'crew-availability'),
('Customs Delay', 'customs-delay'),
('Fueling Delay', 'fueling-delay'),
('Late Arrival of Aircraft', 'late-arrival-of-aircraft'),
('Operational Decision', 'operational-decision'),
('Passenger Handling', 'passenger-handling'),
('Runway Closure', 'runway-closure'),
('Security', 'security'),
('Severe Weather', 'severe-weather'),
('Technical Fault', 'technical-fault'),
('Other', 'other')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_delay_reasons WHERE masters_lookup.tbl_delay_reasons.value = t.value);
-- 20. Delay Durations
INSERT INTO masters_lookup.tbl_delay_durations (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Less than 1 Hour', 'less-than-1-hour'),
('1-2 Hours', '1-2-hours'),
('2-3 Hours', '2-3-hours'),
('3-4 Hours', '3-4-hours'),
('More than 4 Hours', 'more-than-4-hours')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_delay_durations WHERE masters_lookup.tbl_delay_durations.value = t.value);
-- 21. Extraordinary Circumstances
INSERT INTO masters_lookup.tbl_extraordinary_circumstances (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Air Traffic Management Decision', 'air-traffic-management-decision'),
('Airport Closure', 'airport-closure'),
('Bird Strike', 'bird-strike'),
('Civil Unrest', 'civil-unrest'),
('Medical Emergency', 'medical-emergency'),
('Political Instability', 'political-instability'),
('Security Threat', 'security-threat'),
('Severe Weather', 'severe-weather'),
('Strike (External)', 'strike-external-'),
('War', 'war'),
('Other', 'other')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_extraordinary_circumstances WHERE masters_lookup.tbl_extraordinary_circumstances.value = t.value);
-- 22. Cancellation Reasons
INSERT INTO masters_lookup.tbl_cancellation_reasons (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Air Traffic Control Restriction', 'air-traffic-control-restriction'),
('Airport Closure', 'airport-closure'),
('Commercial Decision', 'commercial-decision'),
('Crew Availability', 'crew-availability'),
('Operational Decision', 'operational-decision'),
('Overbooking', 'overbooking'),
('Security', 'security'),
('Severe Weather', 'severe-weather'),
('Strike', 'strike'),
('Technical Fault', 'technical-fault')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_cancellation_reasons WHERE masters_lookup.tbl_cancellation_reasons.value = t.value);
-- 23. Diversion Reasons
INSERT INTO masters_lookup.tbl_diversion_reasons (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Airport Closure', 'airport-closure'),
('Destination Weather', 'destination-weather'),
('Fuel Emergency', 'fuel-emergency'),
('Medical Emergency', 'medical-emergency'),
('Runway Obstruction', 'runway-obstruction'),
('Security Threat', 'security-threat'),
('Technical Fault', 'technical-fault')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_diversion_reasons WHERE masters_lookup.tbl_diversion_reasons.value = t.value);
-- 24. Missed Connection Reasons
INSERT INTO masters_lookup.tbl_missed_connection_reasons (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Customs Delay', 'customs-delay'),
('Flight Delay', 'flight-delay'),
('Immigration Delay', 'immigration-delay'),
('Passenger Delay', 'passenger-delay'),
('Security Screening Delay', 'security-screening-delay')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_missed_connection_reasons WHERE masters_lookup.tbl_missed_connection_reasons.value = t.value);
-- 25. Compensation Eligibilities
INSERT INTO masters_lookup.tbl_compensation_eligibilities (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Eligible', 'eligible'),
('Not Eligible', 'not-eligible'),
('Requires Manual Review', 'requires-manual-review')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_compensation_eligibilities WHERE masters_lookup.tbl_compensation_eligibilities.value = t.value);
-- 26. Compensation Types
INSERT INTO masters_lookup.tbl_compensation_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Cash', 'cash'),
('Cheque', 'cheque'),
('Flight Voucher', 'flight-voucher'),
('Loyalty Miles', 'loyalty-miles'),
('Meal Voucher', 'meal-voucher'),
('Hotel Accommodation', 'hotel-accommodation'),
('Ground Transport', 'ground-transport')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_compensation_types WHERE masters_lookup.tbl_compensation_types.value = t.value);
-- 27. Refund Types
INSERT INTO masters_lookup.tbl_refund_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Full Refund', 'full-refund'),
('Partial Refund', 'partial-refund'),
('Future Travel Credit', 'future-travel-credit'),
('Travel Voucher', 'travel-voucher'),
('Tax Refund Only', 'tax-refund-only'),
('Telephone Reimbursement', 'telephone-reimbursement')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_refund_types WHERE masters_lookup.tbl_refund_types.value = t.value);
-- 28. Flight Disruption Types
INSERT INTO masters_lookup.tbl_flight_disruption_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Cancellation', 'cancellation'),
('Delay', 'delay'),
('Denied Boarding', 'denied-boarding'),
('Diversion', 'diversion'),
('Missed Connection', 'missed-connection')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_flight_disruption_types WHERE masters_lookup.tbl_flight_disruption_types.value = t.value);
-- 29. Airline Responsibilities
INSERT INTO masters_lookup.tbl_airline_responsibilities (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Airline Responsible', 'airline-responsible'),
('Airport Responsible', 'airport-responsible'),
('ATC Responsible', 'atc-responsible'),
('Passenger Responsible', 'passenger-responsible'),
('Shared Responsibility', 'shared-responsibility'),
('Third Party Responsible', 'third-party-responsible'),
('Snow', 'snow')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_airline_responsibilities WHERE masters_lookup.tbl_airline_responsibilities.value = t.value);
-- 30. Weather Conditions
INSERT INTO masters_lookup.tbl_weather_conditions (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Fog', 'fog'),
('Heavy Rain', 'heavy-rain'),
('Hurricane', 'hurricane'),
('Ice', 'ice'),
('Lightning', 'lightning'),
('Sandstorm', 'sandstorm'),
('Thunderstorm', 'thunderstorm')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_weather_conditions WHERE masters_lookup.tbl_weather_conditions.value = t.value);
-- 31. ATC Restrictions
INSERT INTO masters_lookup.tbl_atc_restrictions (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Airspace Closure', 'airspace-closure'),
('Flow Control', 'flow-control'),
('Ground Stop', 'ground-stop'),
('Slot Restriction', 'slot-restriction'),
('Traffic Congestion', 'traffic-congestion'),
('Navigation System', 'navigation-system')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_atc_restrictions WHERE masters_lookup.tbl_atc_restrictions.value = t.value);
-- 32. Technical Fault Categories
INSERT INTO masters_lookup.tbl_technical_fault_categories (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Aircraft Damage', 'aircraft-damage'),
('Avionics', 'avionics'),
('Cabin Systems', 'cabin-systems'),
('Engine', 'engine'),
('Hydraulic System', 'hydraulic-system'),
('Landing Gear', 'landing-gear'),
('Volcanic Ash', 'volcanic-ash'),
('Wind Shear', 'wind-shear')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_technical_fault_categories WHERE masters_lookup.tbl_technical_fault_categories.value = t.value);
-- 33. Operators
INSERT INTO masters_rule_engine.tbl_operators (code, name, symbol, "displayOrder", "isActive")
SELECT code, name, symbol, displayOrder, true FROM (VALUES
('EQ', 'Equals', '=', 1),
('NE', 'Not Equals', '!=', 2),
('GT', 'Greater Than', '>', 3),
('GTE', 'Greater Than or Equal', '>=', 4),
('LT', 'Less Than', '<', 5),
('LTE', 'Less Than or Equal', '<=', 6),
('BETWEEN', 'Between', 'BETWEEN', 7),
('CONTAINS', 'Contains', 'CONTAINS', 8),
('IN', 'In', 'IN', 9),
('IS_EMPTY', 'Is Empty', 'IS EMPTY', 10)
) AS t(code, name, symbol, displayOrder)
WHERE NOT EXISTS (SELECT 1 FROM masters_rule_engine.tbl_operators WHERE masters_rule_engine.tbl_operators.code = t.code);
-- 34. Refund Bases
INSERT INTO masters_lookup.tbl_refund_bases (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Base Fare', 'base-fare'),
('Taxes', 'taxes'),
('Total Fare', 'total-fare')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_refund_bases WHERE masters_lookup.tbl_refund_bases.value = t.value);
-- 35. Currencies
INSERT INTO masters_lookup.tbl_currencies (label, value, symbol, "isActive")
SELECT label, value, symbol, true FROM (VALUES
('United States Dollar', 'USD', '$'),
('Euro', 'EUR', ''),
('British Pound', 'GBP', '£'),
('United Arab Emirates Dirham', 'AED', 'AED'),
('Saudi Riyal', 'SAR', 'SAR'),
('Indian Rupee', 'INR', ''),
('Japanese Yen', 'JPY', '¥'),
('Canadian Dollar', 'CAD', '$'),
('Australian Dollar', 'AUD', '$'),
('Swiss Franc', 'CHF', 'CHF'),
('Singapore Dollar', 'SGD', '$'),
('Qatari Riyal', 'QAR', 'QAR'),
('Kuwaiti Dinar', 'KWD', 'KWD'),
('Bahraini Dinar', 'BHD', 'BHD'),
('Omani Rial', 'OMR', 'OMR')
) AS t(label, value, symbol)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_currencies WHERE masters_lookup.tbl_currencies.value = t.value);
-- 36. Refund Methods
INSERT INTO masters_lookup.tbl_refund_methods (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Original Payment Method', 'original-payment-method'),
('Wallet', 'wallet'),
('Bank Transfer', 'bank-transfer'),
('Voucher', 'voucher')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_refund_methods WHERE masters_lookup.tbl_refund_methods.value = t.value);
-- 37. Amount Types
INSERT INTO masters_lookup.tbl_amount_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Fixed', 'fixed'),
('Percentage', 'percentage'),
('Formula', 'formula')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_amount_types WHERE masters_lookup.tbl_amount_types.value = t.value);
-- =============================================================================
-- 38. Condition Groups (Level 2)
-- =============================================================================
INSERT INTO masters_rule_engine.tbl_condition_groups (code, name, "displayOrder", "isActive")
SELECT code, name, displayOrder, true FROM (VALUES
('FLIGHT', 'Flight Details', 1),
('DISRUPTION', 'Disruption Details', 2),
('PASSENGER', 'Passenger Profile', 3),
('JOURNEY', 'Journey & Itinerary', 4),
('BOOKING', 'Booking & Fare', 5),
('BAGGAGE', 'Baggage Details', 6),
('CABIN', 'Cabin & Seating', 7),
('ANCILLARY', 'Ancillary Service Details', 8)
) AS t(code, name, displayOrder)
ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name;
-- =============================================================================
-- 40. Rule Category Groups Mapping (Level 1 -> Level 2)
-- =============================================================================
INSERT INTO masters_rule_engine.tbl_rule_category_groups ("ruleCategoryId", "conditionGroupId")
SELECT c.id, g.id
FROM (VALUES
('FLIGHT_OPERATIONS', 'FLIGHT'),
('FLIGHT_OPERATIONS', 'DISRUPTION'),
('FLIGHT_OPERATIONS', 'PASSENGER'),
('FLIGHT_OPERATIONS', 'JOURNEY'),
('FLIGHT_OPERATIONS', 'BOOKING'),
('BAGGAGE_ISSUES', 'PASSENGER'),
('BAGGAGE_ISSUES', 'JOURNEY'),
('BAGGAGE_ISSUES', 'BAGGAGE'),
('CABIN_DOWNGRADES', 'PASSENGER'),
('CABIN_DOWNGRADES', 'JOURNEY'),
('CABIN_DOWNGRADES', 'CABIN'),
('CABIN_DOWNGRADES', 'BOOKING'),
('ANCILLARY_FAILURES', 'PASSENGER'),
('ANCILLARY_FAILURES', 'ANCILLARY'),
('ANCILLARY_FAILURES', 'BOOKING'),
('DENIED_BOARDING', 'FLIGHT'),
('DENIED_BOARDING', 'PASSENGER'),
('DENIED_BOARDING', 'JOURNEY'),
('DENIED_BOARDING', 'BOOKING'),
('MISSED_CONNECTIONS', 'FLIGHT'),
('MISSED_CONNECTIONS', 'PASSENGER'),
('MISSED_CONNECTIONS', 'JOURNEY'),
('MISSED_CONNECTIONS', 'BOOKING')
) AS v(cat_code, group_code)
JOIN masters_rule_engine.tbl_rules_categories c ON c.code = v.cat_code
JOIN masters_rule_engine.tbl_condition_groups g ON g.code = v.group_code
WHERE NOT EXISTS (
SELECT 1 FROM masters_rule_engine.tbl_rule_category_groups rcg
WHERE rcg."ruleCategoryId" = c.id AND rcg."conditionGroupId" = g.id
);
-- =============================================================================
-- 41. Condition Fields (Level 3)
-- =============================================================================
INSERT INTO masters_rule_engine.tbl_condition_fields ("groupId", code, name, "lookupTable", "dataType", "operatorType", "displayOrder", "isActive")
SELECT g.id, v.code, v.name, v.lookup_table, v.data_type, v.operator_type, v.display_order, true
FROM (VALUES
-- FLIGHT
('FLIGHT', 'flight_type', 'Flight Type', 'tbl_flight_types', 'ENUM', 'COMPARISON', 1),
('FLIGHT', 'origin_airport', 'Origin Airport', NULL, 'STRING', 'TEXT', 2),
('FLIGHT', 'destination_airport', 'Destination Airport', NULL, 'STRING', 'TEXT', 3),
('FLIGHT', 'operating_carrier', 'Operating Carrier', 'tbl_carrier_types', 'ENUM', 'COMPARISON', 4),
('FLIGHT', 'marketing_carrier', 'Marketing Carrier', 'tbl_carrier_types', 'ENUM', 'COMPARISON', 5),
('FLIGHT', 'flight_distance', 'Flight Distance (km)', NULL, 'NUMBER', 'COMPARISON', 6),
-- DISRUPTION
('DISRUPTION', 'delay_duration', 'Delay Duration (mins)', 'tbl_delay_durations', 'NUMBER', 'COMPARISON', 1),
('DISRUPTION', 'delay_reason', 'Delay Reason', 'tbl_delay_reasons', 'ENUM', 'SET', 2),
('DISRUPTION', 'cancellation_reason', 'Cancellation Reason', 'tbl_cancellation_reasons', 'ENUM', 'SET', 3),
('DISRUPTION', 'diversion_reason', 'Diversion Reason', 'tbl_diversion_reasons', 'ENUM', 'SET', 4),
('DISRUPTION', 'weather_condition', 'Weather Condition', 'tbl_weather_conditions', 'ENUM', 'SET', 5),
('DISRUPTION', 'atc_restriction', 'ATC Restriction', 'tbl_atc_restrictions', 'ENUM', 'SET', 6),
('DISRUPTION', 'technical_fault', 'Technical Fault', 'tbl_technical_fault_categories', 'ENUM', 'SET', 7),
('DISRUPTION', 'extraordinary_circumstance', 'Extraordinary Circumstance', 'tbl_extraordinary_circumstances', 'ENUM', 'SET', 8),
('DISRUPTION', 'airline_responsibility', 'Airline Responsibility', 'tbl_airline_responsibilities', 'ENUM', 'COMPARISON', 9),
-- PASSENGER
('PASSENGER', 'passenger_type', 'Passenger Type', 'tbl_passenger_types', 'ENUM', 'SET', 1),
('PASSENGER', 'cabin_class', 'Cabin Class', 'tbl_cabin_classes', 'ENUM', 'SET', 2),
('PASSENGER', 'membership_tier', 'Membership Tier', 'tbl_membership_tiers', 'ENUM', 'SET', 3),
('PASSENGER', 'loyalty_tier', 'Loyalty Tier', 'tbl_membership_tiers', 'ENUM', 'SET', 4),
('PASSENGER', 'customer_value', 'Customer Value', 'tbl_customer_values', 'ENUM', 'COMPARISON', 5),
('PASSENGER', 'corporate_customer', 'Corporate Customer', NULL, 'BOOLEAN', 'BOOLEAN', 6),
('PASSENGER', 'group_booking', 'Group Booking', NULL, 'BOOLEAN', 'BOOLEAN', 7),
('PASSENGER', 'special_assistance', 'Special Assistance Type', 'tbl_special_assistance_types', 'ENUM', 'SET', 8),
-- JOURNEY
('JOURNEY', 'journey_type', 'Journey Type', 'tbl_journey_types', 'ENUM', 'COMPARISON', 1),
('JOURNEY', 'protected_connection', 'Protected Connection', NULL, 'BOOLEAN', 'BOOLEAN', 2),
('JOURNEY', 'self_transfer', 'Self Transfer', NULL, 'BOOLEAN', 'BOOLEAN', 3),
('JOURNEY', 'number_of_segments', 'Number Of Segments', NULL, 'NUMBER', 'COMPARISON', 4),
('JOURNEY', 'final_destination_delay', 'Final Destination Delay (mins)', NULL, 'NUMBER', 'COMPARISON', 5),
-- BOOKING
('BOOKING', 'booking_channel', 'Booking Channel', 'tbl_booking_channels', 'ENUM', 'SET', 1),
('BOOKING', 'refundable_ticket', 'Refundable Ticket', NULL, 'BOOLEAN', 'BOOLEAN', 2),
('BOOKING', 'fare_flexibility', 'Fare Flexibility', 'tbl_fare_flexibilities', 'ENUM', 'COMPARISON', 3),
('BOOKING', 'ticket_value', 'Ticket Value', NULL, 'NUMBER', 'COMPARISON', 4),
('BOOKING', 'ancillary_purchased', 'Ancillary Purchased', 'tbl_ancillary_purchases', 'ENUM', 'SET', 5),
-- BAGGAGE
('BAGGAGE', 'bag_status', 'Bag Status', NULL, 'STRING', 'TEXT', 1),
('BAGGAGE', 'bag_type', 'Bag Type', NULL, 'STRING', 'TEXT', 2),
('BAGGAGE', 'bag_delay', 'Bag Delay Duration (hrs)', NULL, 'NUMBER', 'COMPARISON', 3),
('BAGGAGE', 'bag_value', 'Bag Value', NULL, 'NUMBER', 'COMPARISON', 4),
('BAGGAGE', 'pir_created', 'PIR Created', NULL, 'BOOLEAN', 'BOOLEAN', 5),
-- CABIN
('CABIN', 'original_cabin', 'Original Cabin', 'tbl_cabin_classes', 'ENUM', 'COMPARISON', 1),
('CABIN', 'assigned_cabin', 'Assigned Cabin', 'tbl_cabin_classes', 'ENUM', 'COMPARISON', 2),
('CABIN', 'downgrade_level', 'Downgrade Level', NULL, 'NUMBER', 'COMPARISON', 3),
('CABIN', 'seat_type', 'Seat Type', NULL, 'STRING', 'TEXT', 4),
-- ANCILLARY
('ANCILLARY', 'ancillary_type', 'Ancillary Type', 'tbl_ancillary_purchases', 'ENUM', 'SET', 1),
('ANCILLARY', 'ancillary_delivered', 'Ancillary Delivered', NULL, 'BOOLEAN', 'BOOLEAN', 2),
('ANCILLARY', 'service_value', 'Service Value', NULL, 'NUMBER', 'COMPARISON', 3)
) AS v(group_code, code, name, lookup_table, data_type, operator_type, display_order)
JOIN masters_rule_engine.tbl_condition_groups g ON g.code = v.group_code
ON CONFLICT (code) DO UPDATE SET
name = EXCLUDED.name,
"lookupTable" = EXCLUDED."lookupTable",
"dataType" = EXCLUDED."dataType",
"operatorType" = EXCLUDED."operatorType",
"displayOrder" = EXCLUDED."displayOrder";
-- 38. Baggage Types
INSERT INTO masters_lookup.tbl_baggage_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Checked Bag', 'checked-bag'),
('Cabin Bag', 'cabin-bag'),
('Oversized Baggage', 'oversized-baggage'),
('Sports Equipment', 'sports-equipment'),
('Special Baggage', 'special-baggage')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_baggage_types WHERE masters_lookup.tbl_baggage_types.value = t.value);
-- 39. Compensation Rules
INSERT INTO masters_lookup.tbl_compensation_rules (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('EU261', 'eu261'),
('UK261', 'uk261'),
('APPR', 'appr'),
('DGCA', 'dgca'),
('DOT', 'dot'),
('Airline Policy', 'airline-policy')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_compensation_rules WHERE masters_lookup.tbl_compensation_rules.value = t.value);
-- 40. Approval Levels
INSERT INTO masters_lookup.tbl_approval_levels (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Supervisor', 'supervisor'),
('Manager', 'manager'),
('Director', 'director')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_approval_levels WHERE masters_lookup.tbl_approval_levels.value = t.value);
-- 41. Eligible Fare Types
INSERT INTO masters_lookup.tbl_eligible_fare_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Published Fares', 'published-fares'),
('Promo Fares', 'promo-fares')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_eligible_fare_types WHERE masters_lookup.tbl_eligible_fare_types.value = t.value);
-- 42. Applicable Products
INSERT INTO masters_lookup.tbl_applicable_products (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Flights', 'flights'),
('Ancillary', 'ancillary'),
('Lounge', 'lounge')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_applicable_products WHERE masters_lookup.tbl_applicable_products.value = t.value);
-- 43. Meal Types
INSERT INTO masters_lookup.tbl_meal_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Breakfast', 'breakfast'),
('Lunch', 'lunch'),
('Dinner', 'dinner'),
('Snack', 'snack'),
('Any', 'any')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_meal_types WHERE masters_lookup.tbl_meal_types.value = t.value);
-- 44. Airport Restrictions
INSERT INTO masters_lookup.tbl_airport_restrictions (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Departure', 'departure'),
('Transit', 'transit'),
('Arrival', 'arrival'),
('Any Airport', 'any-airport')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_airport_restrictions WHERE masters_lookup.tbl_airport_restrictions.value = t.value);
-- 45. Destination Types
INSERT INTO masters_lookup.tbl_destination_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Home', 'home'),
('Hotel', 'hotel'),
('Airport', 'airport'),
('City Center', 'city-center')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_destination_types WHERE masters_lookup.tbl_destination_types.value = t.value);
-- 46. Vehicle Categories
INSERT INTO masters_lookup.tbl_vehicle_categories (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Executive Sedan', 'executive-sedan'),
('Luxury SUV', 'luxury-suv'),
('Premium Van', 'premium-van')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_vehicle_categories WHERE masters_lookup.tbl_vehicle_categories.value = t.value);
-- 47. Cabin Preferences
INSERT INTO masters_lookup.tbl_cabin_preferences (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Same Cabin', 'same-cabin'),
('Upgrade if Available', 'upgrade-if-available'),
('Lowest Available', 'lowest-available')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_cabin_preferences WHERE masters_lookup.tbl_cabin_preferences.value = t.value);
-- 48. Airline Preferences
INSERT INTO masters_lookup.tbl_airline_preferences (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Same Airline', 'same-airline'),
('Alliance Airline', 'alliance-airline'),
('Any Partner', 'any-partner')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_airline_preferences WHERE masters_lookup.tbl_airline_preferences.value = t.value);
-- 49. Meal Categories
INSERT INTO masters_lookup.tbl_meal_categories (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Vegetarian', 'vegetarian'),
('Non-Vegetarian', 'non-vegetarian'),
('Vegan', 'vegan'),
('Child Meal', 'child-meal')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_meal_categories WHERE masters_lookup.tbl_meal_categories.value = t.value);
-- 50. Seat Categories
INSERT INTO masters_lookup.tbl_seat_categories (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Standard', 'standard'),
('Preferred', 'preferred'),
('Exit Row', 'exit-row'),
('Extra Legroom', 'extra-legroom')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_seat_categories WHERE masters_lookup.tbl_seat_categories.value = t.value);
-- 51. Compensation Bases
INSERT INTO masters_lookup.tbl_compensation_bases (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Regulatory Rule', 'regulatory-rule'),
('Airline Policy', 'airline-policy'),
('Percentage Difference', 'percentage-difference')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_compensation_bases WHERE masters_lookup.tbl_compensation_bases.value = t.value);
-- 52. Tax Types
INSERT INTO masters_lookup.tbl_tax_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Airport Tax', 'airport-tax'),
('Fuel Surcharge', 'fuel-surcharge'),
('Government Tax', 'government-tax'),
('Service Tax', 'service-tax'),
('VAT', 'vat'),
('GST', 'gst'),
('Airport Development Fee', 'airport-development-fee')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_tax_types WHERE masters_lookup.tbl_tax_types.value = t.value);
-- 53. Room Types
INSERT INTO masters_lookup.tbl_room_types (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Single', 'single'),
('Double', 'double'),
('Twin', 'twin'),
('Family', 'family')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_room_types WHERE masters_lookup.tbl_room_types.value = t.value);
-- 54. Hotel Tiers
INSERT INTO masters_lookup.tbl_hotel_tiers (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('3 Star', '3-star'),
('4 Star', '4-star'),
('5 Star', '5-star')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_hotel_tiers WHERE masters_lookup.tbl_hotel_tiers.value = t.value);
-- 55. Hotel Categories
INSERT INTO masters_lookup.tbl_hotel_categories (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Airport Hotel', 'airport-hotel'),
('Transit Hotel', 'transit-hotel')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_hotel_categories WHERE masters_lookup.tbl_hotel_categories.value = t.value);
-- 56. Hotel Chains
INSERT INTO masters_lookup.tbl_hotel_chains (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Hilton', 'hilton'),
('Marriott', 'marriott'),
('IHG', 'ihg'),
('Hyatt', 'hyatt'),
('Radisson', 'radisson')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_hotel_chains WHERE masters_lookup.tbl_hotel_chains.value = t.value);
-- 57. Loyalty Programs
INSERT INTO masters_lookup.tbl_loyalty_programs (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('AeroMiles', 'aeromiles'),
('SkyRewards', 'skyrewards'),
('StarAlliance', 'staralliance')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_loyalty_programs WHERE masters_lookup.tbl_loyalty_programs.value = t.value);
-- 58. Promotion Codes
INSERT INTO masters_lookup.tbl_promotion_codes (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('SUMMER2026', 'summer2026'),
('APOLOGY10', 'apology10'),
('FESTIVE25', 'festive25')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_promotion_codes WHERE masters_lookup.tbl_promotion_codes.value = t.value);
-- 59. Goodwill Reasons
INSERT INTO masters_lookup.tbl_goodwill_reasons (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Service Failure', 'service-failure'),
('Customer Retention', 'customer-retention'),
('Operational Delay', 'operational-delay')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_goodwill_reasons WHERE masters_lookup.tbl_goodwill_reasons.value = t.value);
-- 60. Routes
INSERT INTO masters_lookup.tbl_routes (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Airport to City Center', 'airport-city'),
('City Center to Airport', 'city-airport'),
('Airport to Terminal 2', 'airport-t2')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_routes WHERE masters_lookup.tbl_routes.value = t.value);
-- 61. Refund Components
INSERT INTO masters_lookup.tbl_refund_components (label, value, "isActive")
SELECT label, value, true FROM (VALUES
('Base Fare', 'base-fare'),
('Taxes', 'taxes'),
('Ancillary', 'ancillary'),
('Seat Fee', 'seat-fee'),
('Baggage Fee', 'baggage-fee'),
('Service Fee', 'service-fee')
) AS t(label, value)
WHERE NOT EXISTS (SELECT 1 FROM masters_lookup.tbl_refund_components WHERE masters_lookup.tbl_refund_components.value = t.value);
+282 -10
View File
@@ -12,11 +12,17 @@
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^12.0.1",
"@nestjs/passport": "^12.0.0",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.0",
"@nestjs/typeorm": "^11.0.3",
"bcryptjs": "^3.0.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"cookie-parser": "^1.4.7",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.22.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
@@ -28,9 +34,12 @@
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.0.0",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^7.0.0",
"cross-env": "^10.1.0",
"eslint": "^9.18.0",
@@ -2373,6 +2382,19 @@
}
}
},
"node_modules/@nestjs/jwt": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-12.0.1.tgz",
"integrity": "sha512-AOvjo0JIuVMNr+6JgbEz4fP+4afclGR5ffZnHtjOFs4+HhzerUYKcIVUL06u5uQYQi/ra8WHO+NZR7OFdQeGvA==",
"license": "MIT",
"dependencies": {
"@types/jsonwebtoken": "9.0.10",
"jsonwebtoken": "9.0.3"
},
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0"
}
},
"node_modules/@nestjs/mapped-types": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.1.tgz",
@@ -2393,15 +2415,25 @@
}
}
},
"node_modules/@nestjs/passport": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-12.0.0.tgz",
"integrity": "sha512-FM9ioo2Dyj+cC7aeO4g19ijAXoRaHlW/GQZFbgXsM4Z9pyQedA8yOseb7ivfqlPwUKJlH9PFgeWniu6c0vb6dw==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^11.0.0 || ^12.0.0",
"passport": "^0.5.0 || ^0.6.0 || ^0.7.0"
}
},
"node_modules/@nestjs/platform-express": {
"version": "11.1.27",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.27.tgz",
"integrity": "sha512-0ZFhz6H6EdGh4xQVbUNwjoAwBuz73P7FvUAl67h9CTdMqQlJDaQYJApBv8pKfVZ1fGjMCbl0m9DcC6pXaZPWSQ==",
"version": "11.1.28",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.28.tgz",
"integrity": "sha512-hU+9Sz4m+onHrR5AmelI59QKmY/Re546bPnygnpqqeQdHDiJpBgjWbL4t6Jr73CBpS60cpyng7WzjgphNB9iwA==",
"license": "MIT",
"dependencies": {
"cors": "2.8.6",
"express": "5.2.1",
"multer": "2.1.1",
"multer": "2.2.0",
"path-to-regexp": "8.4.2",
"tslib": "2.8.1"
},
@@ -2786,6 +2818,13 @@
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/bcryptjs": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/body-parser": {
"version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
@@ -2807,6 +2846,16 @@
"@types/node": "*"
}
},
"node_modules/@types/cookie-parser": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz",
"integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/express": "*"
}
},
"node_modules/@types/cookiejar": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
@@ -2920,6 +2969,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/methods": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@@ -2927,16 +2986,53 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.13.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/@types/passport": {
"version": "1.0.17",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
"integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/passport-jwt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz",
"integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/jsonwebtoken": "*",
"@types/passport-strategy": "*"
}
},
"node_modules/@types/passport-strategy": {
"version": "0.2.38",
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz",
"integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/passport": "*"
}
},
"node_modules/@types/qs": {
"version": "6.15.1",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
@@ -4202,6 +4298,15 @@
"node": ">=6.0.0"
}
},
"node_modules/bcryptjs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -4344,6 +4449,12 @@
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -4780,6 +4891,25 @@
"node": ">= 0.6"
}
},
"node_modules/cookie-parser": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cookie-signature": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
@@ -5057,6 +5187,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -7364,6 +7503,49 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"license": "MIT",
"dependencies": {
"jws": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -7466,6 +7648,42 @@
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -7480,6 +7698,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/log-symbols": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -7873,9 +8097,9 @@
"license": "MIT"
},
"node_modules/multer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
"integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz",
"integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
@@ -8230,6 +8454,42 @@
"node": ">= 0.8"
}
},
"node_modules/passport": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
"license": "MIT",
"dependencies": {
"passport-strategy": "1.x.x",
"pause": "0.0.1",
"utils-merge": "^1.0.1"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-jwt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
"integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
"passport-strategy": "^1.0.0"
}
},
"node_modules/passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -8307,6 +8567,11 @@
"node": ">=8"
}
},
"node_modules/pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"node_modules/pg": {
"version": "8.22.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
@@ -8893,7 +9158,6 @@
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -10242,7 +10506,6 @@
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"devOptional": true,
"license": "MIT"
},
"node_modules/universalify": {
@@ -10349,6 +10612,15 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+9
View File
@@ -30,11 +30,17 @@
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^12.0.1",
"@nestjs/passport": "^12.0.0",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.0",
"@nestjs/typeorm": "^11.0.3",
"bcryptjs": "^3.0.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"cookie-parser": "^1.4.7",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.22.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
@@ -46,9 +52,12 @@
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.0.0",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^7.0.0",
"cross-env": "^10.1.0",
"eslint": "^9.18.0",
+1 -5
View File
@@ -15,11 +15,7 @@ async function bootstrap() {
const tenants = await tenantService.seedDemoTenants();
logger.log(`Seeded ${tenants.length} demo tenant(s)`);
for (const tenant of tenants) {
await masterDataService.seedData(tenant.id);
logger.log(`Master data ready for tenant: ${tenant.slug}`);
}
logger.log('Master data ready (common across all tenants).');
logger.log('Seeding complete. Exiting...');
+25
View File
@@ -1,11 +1,20 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module';
import { HealthModule } from './modules/health/health.module';
import { AuthModule } from './modules/auth/auth.module';
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
import { MasterDataModule } from './modules/master-data/master-data.module';
import { CohortModule } from './modules/cohort/cohort.module';
import { TenantModule } from './modules/tenant/tenant.module';
import { TenantMiddleware } from './common/tenant/tenant.middleware';
import { HttpLoggerMiddleware } from './common/middleware/http-logger.middleware';
import { PolicyEngineModule } from './modules/policy-engine/policy-engine.module';
import { RecoveryIncidentModule } from './modules/recovery-incident/recovery-incident.module';
import { AuditLogModule } from './modules/audit-log/audit-log.module';
import { DashboardModule } from './modules/dashboard/dashboard.module';
import { AuditInterceptor } from './common/interceptors/audit.interceptor';
const env = process.env.NODE_ENV;
const envFilePath = env ? [`.env.${env}`, '.env.local', '.env'] : ['.env.local', '.env'];
@@ -17,14 +26,30 @@ const envFilePath = env ? [`.env.${env}`, '.env.local', '.env'] : ['.env.local',
envFilePath,
}),
DatabaseModule,
AuthModule,
HealthModule,
TenantModule,
MasterDataModule,
CohortModule,
PolicyEngineModule,
RecoveryIncidentModule,
AuditLogModule,
DashboardModule,
],
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
{
provide: APP_INTERCEPTOR,
useClass: AuditInterceptor,
},
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(HttpLoggerMiddleware).forRoutes('*');
consumer.apply(TenantMiddleware).forRoutes('*');
}
}
+2 -2
View File
@@ -4,9 +4,9 @@ import { Tenant } from '../../modules/tenant/tenant.entity';
export abstract class TenantOwnedEntity {
@Column({ type: 'uuid' })
@Index()
tenantId: string;
tenantId!: string;
@ManyToOne(() => Tenant, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'tenantId' })
tenant: Tenant;
tenant!: Tenant;
}
@@ -0,0 +1,120 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Request } from 'express';
import { AuditLogService } from '../../modules/audit-log/audit-log.service';
// Methods that mutate data — only these are audited
const MUTABLE_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
// Map: [HTTP method, URL pattern] → { module, action }
// The interceptor only handles CREATE (POST) since UPDATE/DELETE
// need before-snapshots which are captured directly in each service.
const MODULE_MAP: Array<{ pattern: RegExp; module: string }> = [
{ pattern: /policy-engine/, module: 'policy-engine' },
{ pattern: /recovery-incidents/, module: 'recovery-incident' },
{ pattern: /cohort/, module: 'cohort' },
{ pattern: /master-data/, module: 'master-data' },
];
// Routes that should never trigger an entity creation audit log
const EXCLUDED_PATTERNS: RegExp[] = [/\/auth\b/, /\/evaluate\b/];
function resolveModule(url: string): string | null {
for (const entry of MODULE_MAP) {
if (entry.pattern.test(url)) return entry.module;
}
return null;
}
function resolveAction(method: string, url: string): string {
if (method === 'POST') return 'CREATE';
if (method === 'DELETE') return 'DELETE';
if (method === 'PATCH' && url.includes('/status')) return 'STATUS_CHANGE';
if (method === 'PATCH' || method === 'PUT') return 'UPDATE';
return method;
}
@Injectable()
export class AuditInterceptor implements NestInterceptor {
private readonly logger = new Logger(AuditInterceptor.name);
constructor(private readonly auditLogService: AuditLogService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest<Request>();
const { method, ip, headers } = req;
const url = req.originalUrl || req.url;
// Only intercept CREATE (POST) — UPDATE/DELETE handled in services
if (!MUTABLE_METHODS.includes(method) || method !== 'POST') {
return next.handle();
}
// Never audit authentication endpoints or non-persisting evaluation endpoints
if (EXCLUDED_PATTERNS.some((pattern) => pattern.test(url))) {
return next.handle();
}
const module = resolveModule(url);
if (!module) {
return next.handle();
}
const action = resolveAction(method, url);
const userAgent = headers['user-agent'] ?? '';
const tenantId = (headers['x-tenant-id'] as string) ?? 'unknown';
this.logger.debug(
`Intercepted ${method} ${url} -> Module: ${module}, Action: ${action}`,
);
return next.handle().pipe(
tap({
next: (responseData: unknown) => {
this.logger.debug(`Writing audit log for ${module} ${action}...`);
const resObj =
typeof responseData === 'object' && responseData !== null
? (responseData as Record<string, unknown>)
: undefined;
const entityId =
typeof resObj?.id === 'string' ? resObj.id : undefined;
const entityLabel =
typeof resObj?.name === 'string'
? resObj.name
: typeof resObj?.recoveryCode === 'string'
? resObj.recoveryCode
: typeof resObj?.flightNumber === 'string'
? resObj.flightNumber
: undefined;
void this.auditLogService
.log({
module,
action,
entityId,
entityLabel,
before: undefined, // no before on CREATE
after: resObj as Record<string, any> | undefined,
performedBy: tenantId,
ipAddress: ip,
userAgent: String(userAgent),
})
.catch((err: unknown) => {
// Never let audit failure break the main request
this.logger.warn(
`Audit log failed for ${method} ${url}: ${String(err)}`,
);
});
},
}),
);
}
}
@@ -0,0 +1,42 @@
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class HttpLoggerMiddleware implements NestMiddleware {
private readonly logger = new Logger('HTTP');
use(req: Request, res: Response, next: NextFunction): void {
const { method, originalUrl, ip } = req;
const startTime = Date.now();
res.on('finish', () => {
const { statusCode } = res;
const duration = Date.now() - startTime;
const rawTenant = req.headers['x-tenant-id'];
const tenantHeader = Array.isArray(rawTenant) ? rawTenant[0] : rawTenant;
const tenantInfo = tenantHeader ? ` [tenant: ${tenantHeader}]` : '';
const clientIp = typeof ip === 'string' ? ip : 'unknown';
const message = `${method} ${originalUrl} ${statusCode} +${duration}ms - ${clientIp}${tenantInfo}`;
if (statusCode >= 500) {
this.logger.error(message);
} else if (statusCode >= 400) {
this.logger.warn(message);
} else {
this.logger.log(message);
}
});
res.on('close', () => {
if (!res.writableEnded) {
const duration = Date.now() - startTime;
const clientIp = typeof ip === 'string' ? ip : 'unknown';
this.logger.warn(
`${method} ${originalUrl} CLOSED_PREMATURELY +${duration}ms - ${clientIp}`,
);
}
});
next();
}
}
+13 -7
View File
@@ -17,9 +17,9 @@ export class TenantMiddleware implements NestMiddleware {
return next();
}
const tenantHeader = req.headers['x-tenant-id'];
let tenantHeader = req.headers['x-tenant-id'];
if (!tenantHeader || Array.isArray(tenantHeader)) {
throw new BadRequestException('Missing X-Tenant-Id header');
tenantHeader = 'demo-airline';
}
try {
@@ -29,11 +29,9 @@ export class TenantMiddleware implements NestMiddleware {
{ tenantId: tenant.id, tenantSlug: tenant.slug },
() => next(),
);
} catch (error) {
if (error instanceof NotFoundException) {
throw error;
}
throw new BadRequestException('Invalid X-Tenant-Id header');
} catch {
// Fallback gracefully without tenant requirement
return next();
}
}
@@ -41,6 +39,14 @@ export class TenantMiddleware implements NestMiddleware {
return (
path === '/api/health' ||
path === '/health' ||
path.startsWith('/api/auth') ||
path.startsWith('/auth') ||
path.startsWith('/api/users') ||
path.startsWith('/users') ||
path.startsWith('/api/roles') ||
path.startsWith('/roles') ||
path.startsWith('/api/permissions') ||
path.startsWith('/permissions') ||
path.startsWith('/api/tenants') ||
path.startsWith('/tenants') ||
path.startsWith('/docs') ||
+12 -6
View File
@@ -28,20 +28,26 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
// Initialize without syncing first
const dataSource = new DataSource({ ...options, synchronize: false });
await dataSource.initialize();
// Create schemas if they don't exist
// Ensure schemas exist safely without dropping user created cohorts or policies
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "tenant";`);
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "masters";`);
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "auth";`);
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "masters_rule_engine";`);
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "masters_action_builder";`);
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "masters_lookup";`);
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "cohort";`);
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "policy_engine";`);
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "recovery_incident";`);
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "audit";`);
// Run synchronization manually if it was enabled
if (options.synchronize) {
await dataSource.synchronize();
}
return dataSource;
},
}),
],
})
export class DatabaseModule {}
export class DatabaseModule { }
+6 -1
View File
@@ -6,6 +6,8 @@ import { AppModule } from './app.module';
import { Logger } from '@nestjs/common';
import cookieParser from 'cookie-parser';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: ['log', 'warn', 'error'],
@@ -16,10 +18,13 @@ async function bootstrap() {
const corsOrigin = configService.get<string>('CORS_ORIGIN') ?? 'http://localhost:5174';
const corsOrigins = corsOrigin.split(',').map(origin => origin.trim()).filter(Boolean);
app.use(cookieParser());
app.enableCors({
origin: corsOrigins.length > 1 ? corsOrigins : corsOrigins[0],
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization', 'X-Tenant-Id'],
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Tenant-Id', 'X-Refresh-Token'],
});
app.useGlobalPipes(
@@ -0,0 +1,54 @@
import {
Controller,
Get,
Param,
Query,
NotFoundException,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { AuditLogService } from './audit-log.service';
import { PermissionsGuard } from '../auth/guards/permissions.guard';
import { RequirePermissions } from '../auth/decorators/permissions.decorator';
@ApiTags('Audit Logs')
@Controller('audit-logs')
@UseGuards(PermissionsGuard)
export class AuditLogController {
constructor(private readonly auditLogService: AuditLogService) {}
@Get()
@RequirePermissions('audit_logs:view')
@ApiOperation({ summary: 'Get paginated audit logs with optional filters' })
findAll(
@Query('page') page: string = '1',
@Query('limit') limit: string = '20',
@Query('module') module?: string,
@Query('action') action?: string,
@Query('entityId') entityId?: string,
@Query('dateFrom') dateFrom?: string,
@Query('dateTo') dateTo?: string,
) {
return this.auditLogService.findAll({
page: Number(page),
limit: Number(limit),
module,
action,
entityId,
dateFrom,
dateTo,
});
}
@Get(':id')
@RequirePermissions('audit_logs:view')
@ApiOperation({ summary: 'Get a single audit log entry by ID' })
async findOne(@Param('id') id: string) {
const log = await this.auditLogService.findOne(id);
if (!log) {
throw new NotFoundException(`Audit log entry ${id} not found`);
}
return log;
}
}
+13
View File
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuditLog } from './entities/audit-log.entity';
import { AuditLogService } from './audit-log.service';
import { AuditLogController } from './audit-log.controller';
@Module({
imports: [TypeOrmModule.forFeature([AuditLog])],
controllers: [AuditLogController],
providers: [AuditLogService],
exports: [AuditLogService], // exported so other modules can inject it
})
export class AuditLogModule {}
@@ -0,0 +1,80 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOptionsWhere, Between, MoreThanOrEqual, LessThanOrEqual } from 'typeorm';
import { AuditLog } from './entities/audit-log.entity';
import { CreateAuditLogDto } from './dto/create-audit-log.dto';
import { getTenantIdOrNull } from '../../common/tenant/tenant.context';
@Injectable()
export class AuditLogService {
constructor(
@InjectRepository(AuditLog)
private readonly auditLogRepository: Repository<AuditLog>,
) {}
// ─── Write a single audit entry ──────────────────────────────────────────
// Called directly from services (for UPDATE/DELETE with before snapshots)
// and from the HTTP interceptor (for CREATE).
async log(dto: CreateAuditLogDto): Promise<void> {
const tenantId = getTenantIdOrNull() ?? dto.performedBy ?? 'system';
const entry = this.auditLogRepository.create({
...dto,
tenantId,
performedBy: dto.performedBy ?? tenantId,
});
await this.auditLogRepository.save(entry);
}
// ─── Paginated query with filters ────────────────────────────────────────
async findAll(options: {
page?: number;
limit?: number;
module?: string;
action?: string;
entityId?: string;
dateFrom?: string;
dateTo?: string;
}) {
const { page = 1, limit = 20, module, action, entityId, dateFrom, dateTo } = options;
const skip = (page - 1) * limit;
const tenantId = getTenantIdOrNull();
const where: FindOptionsWhere<AuditLog> = {};
if (tenantId) where.tenantId = tenantId;
if (module) where.module = module;
if (action) where.action = action;
if (entityId) where.entityId = entityId;
if (dateFrom && dateTo) {
where.createdAt = Between(new Date(dateFrom), new Date(dateTo)) as any;
} else if (dateFrom) {
where.createdAt = MoreThanOrEqual(new Date(dateFrom)) as any;
} else if (dateTo) {
where.createdAt = LessThanOrEqual(new Date(dateTo)) as any;
}
const [data, total] = await this.auditLogRepository.findAndCount({
where,
skip,
take: limit,
order: { createdAt: 'DESC' },
});
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
// ─── Single entry lookup ──────────────────────────────────────────────────
async findOne(id: string): Promise<AuditLog | null> {
const tenantId = getTenantIdOrNull();
const where: FindOptionsWhere<AuditLog> = { id };
if (tenantId) where.tenantId = tenantId;
return this.auditLogRepository.findOne({ where });
}
}
@@ -0,0 +1,39 @@
import { IsString, IsNotEmpty, IsOptional, IsObject } from 'class-validator';
export class CreateAuditLogDto {
@IsString()
@IsNotEmpty()
module: string;
@IsString()
@IsNotEmpty()
action: string;
@IsString()
@IsOptional()
entityId?: string;
@IsString()
@IsOptional()
entityLabel?: string;
@IsObject()
@IsOptional()
before?: Record<string, any>;
@IsObject()
@IsOptional()
after?: Record<string, any>;
@IsString()
@IsOptional()
performedBy?: string;
@IsString()
@IsOptional()
ipAddress?: string;
@IsString()
@IsOptional()
userAgent?: string;
}
@@ -0,0 +1,45 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_audit_logs', schema: 'audit' })
export class AuditLog {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ name: 'tenant_id', type: 'varchar', length: 255 })
tenantId!: string;
@Column({ type: 'varchar', length: 100 })
module!: string; // 'policy-engine' | 'recovery-incident' | 'cohort' | 'master-data'
@Column({ type: 'varchar', length: 50 })
action!: string; // 'CREATE' | 'UPDATE' | 'DELETE' | 'STATUS_CHANGE'
@Column({ name: 'entity_id', type: 'varchar', length: 255, nullable: true })
entityId?: string;
@Column({ name: 'entity_label', type: 'varchar', length: 255, nullable: true })
entityLabel?: string; // human-readable name (policy name, flight #, etc.)
@Column({ type: 'jsonb', nullable: true })
before?: Record<string, any>; // snapshot before change (null for CREATE)
@Column({ type: 'jsonb', nullable: true })
after?: Record<string, any>; // snapshot after change (null for DELETE)
@Column({ name: 'performed_by', type: 'varchar', length: 255, nullable: true })
performedBy?: string; // tenantId for now; will be userId when auth is added
@Column({ name: 'ip_address', type: 'varchar', length: 50, nullable: true })
ipAddress?: string;
@Column({ name: 'user_agent', type: 'text', nullable: true })
userAgent?: string;
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
}
+81
View File
@@ -0,0 +1,81 @@
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Req,
Res,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import type { Request, Response } from 'express';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { ChangePasswordDto } from './dto/change-password.dto';
import { Public } from './decorators/public.decorator';
import { CurrentUser } from './decorators/current-user.decorator';
@ApiTags('Authentication')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Public()
@Post('login')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Login user with email & password, sets secure HTTP-only cookies' })
@ApiResponse({ status: 200, description: 'Authentication successful' })
@ApiResponse({ status: 401, description: 'Invalid credentials or inactive account' })
async login(
@Body() loginDto: LoginDto,
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
) {
return this.authService.login(loginDto, req, res);
}
@Public()
@Post('refresh')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Rotate refresh token and issue new access token via cookies' })
@ApiResponse({ status: 200, description: 'Session refreshed successfully' })
@ApiResponse({ status: 401, description: 'Invalid or revoked refresh token' })
async refresh(
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
) {
return this.authService.refreshSession(req, res);
}
@Public()
@Post('logout')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Revoke active refresh token and clear authentication cookies' })
async logout(
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
) {
return this.authService.logout(req, res);
}
@Get('me')
@ApiOperation({ summary: 'Get current authenticated user profile and permissions' })
@ApiResponse({ status: 200, description: 'Current profile retrieved' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async getMe(@CurrentUser('id') userId: string) {
return this.authService.getMe(userId);
}
@Post('change-password')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Change password for currently authenticated user' })
@ApiResponse({ status: 200, description: 'Password changed successfully' })
@ApiResponse({ status: 400, description: 'Current password invalid' })
async changePassword(
@CurrentUser('id') userId: string,
@Body() dto: ChangePasswordDto,
) {
return this.authService.changePassword(userId, dto);
}
}
+66
View File
@@ -0,0 +1,66 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { User } from './entities/user.entity';
import { Role } from './entities/role.entity';
import { Permission } from './entities/permission.entity';
import { RefreshToken } from './entities/refresh-token.entity';
import { Tenant } from '../tenant/tenant.entity';
import { AuthService } from './auth.service';
import { UsersService } from './users.service';
import { RolesService } from './roles.service';
import { AuthController } from './auth.controller';
import { UsersController } from './users.controller';
import { PermissionsController, RolesController } from './roles.controller';
import { JwtStrategy } from './strategies/jwt.strategy';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { PermissionsGuard } from './guards/permissions.guard';
@Module({
imports: [
TypeOrmModule.forFeature([User, Role, Permission, RefreshToken, Tenant]),
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret:
configService.get<string>('JWT_SECRET') ||
'aeroresolve_ultra_secure_jwt_secret_key_2026_@#!',
signOptions: {
expiresIn: (configService.get<string>('JWT_EXPIRES_IN') || '15m') as any,
},
}),
}),
],
controllers: [
AuthController,
UsersController,
RolesController,
PermissionsController,
],
providers: [
AuthService,
UsersService,
RolesService,
JwtStrategy,
JwtAuthGuard,
PermissionsGuard,
],
exports: [
AuthService,
UsersService,
RolesService,
JwtAuthGuard,
PermissionsGuard,
PassportModule,
JwtModule,
],
})
export class AuthModule {}
+549
View File
@@ -0,0 +1,549 @@
import {
BadRequestException,
ConflictException,
ForbiddenException,
Injectable,
Logger,
NotFoundException,
OnModuleInit,
UnauthorizedException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import type { Request, Response } from 'express';
import * as bcrypt from 'bcryptjs';
import * as crypto from 'crypto';
import { User } from './entities/user.entity';
import { Role } from './entities/role.entity';
import { Permission } from './entities/permission.entity';
import { RefreshToken } from './entities/refresh-token.entity';
import { Tenant } from '../tenant/tenant.entity';
import { LoginDto } from './dto/login.dto';
import { ChangePasswordDto } from './dto/change-password.dto';
import { SYSTEM_PERMISSIONS } from './constants/permissions.constant';
@Injectable()
export class AuthService implements OnModuleInit {
private readonly logger = new Logger(AuthService.name);
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@InjectRepository(Role)
private readonly roleRepository: Repository<Role>,
@InjectRepository(Permission)
private readonly permissionRepository: Repository<Permission>,
@InjectRepository(RefreshToken)
private readonly refreshTokenRepository: Repository<RefreshToken>,
@InjectRepository(Tenant)
private readonly tenantRepository: Repository<Tenant>,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}
async onModuleInit() {
await this.seedPermissionsAndSuperAdmin();
}
/**
* Automatically initializes system permissions, default roles, and super admin user
*/
async seedPermissionsAndSuperAdmin() {
try {
this.logger.log('Verifying & seeding authentication permissions and default roles...');
// 0. Ensure default demo tenant exists
let defaultTenant = await this.tenantRepository.findOne({
where: { slug: 'demo-airline' },
});
if (!defaultTenant) {
defaultTenant = this.tenantRepository.create({
slug: 'demo-airline',
name: 'Demo Airline',
tier: 'enterprise',
isActive: true,
});
defaultTenant = await this.tenantRepository.save(defaultTenant);
this.logger.log('Created default tenant: demo-airline');
}
// 1. Seed / update system permissions
const existingPermissions = await this.permissionRepository.find();
const existingPermCodeMap = new Map(existingPermissions.map((p) => [p.code, p]));
const permissionsToSave: Permission[] = [];
for (const def of SYSTEM_PERMISSIONS) {
if (!existingPermCodeMap.has(def.code)) {
const perm = this.permissionRepository.create({
code: def.code,
name: def.name,
module: def.module,
action: def.action,
groupName: def.groupName,
description: def.description,
});
permissionsToSave.push(perm);
}
}
if (permissionsToSave.length > 0) {
await this.permissionRepository.save(permissionsToSave);
this.logger.log(`Seeded ${permissionsToSave.length} new system permissions.`);
}
const allPermissions = await this.permissionRepository.find();
// 2. Seed Super Admin Role (isSystem: true with ALL permissions)
let superAdminRole = await this.roleRepository.findOne({
where: { slug: 'super_admin' },
relations: { permissions: true },
});
if (!superAdminRole) {
superAdminRole = this.roleRepository.create({
name: 'Super Admin',
slug: 'super_admin',
description: 'Full system privileges across all modules. Immutable system role.',
isSystem: true,
tenantId: defaultTenant.id,
permissions: allPermissions,
});
await this.roleRepository.save(superAdminRole);
this.logger.log('Created default Super Admin role with all permissions.');
} else {
// Ensure Super Admin always has all permissions
superAdminRole.permissions = allPermissions;
superAdminRole.isSystem = true;
if (!superAdminRole.tenantId) {
superAdminRole.tenantId = defaultTenant.id;
}
await this.roleRepository.save(superAdminRole);
}
// 3. Seed Starter Role: Operations Manager
let opsManagerRole = await this.roleRepository.findOne({
where: { slug: 'operations_manager' },
});
if (!opsManagerRole) {
const opsPermissions = allPermissions.filter(
(p) =>
p.module === 'dashboard' ||
p.module === 'simulation' ||
p.module === 'recovery' ||
p.module === 'cohorts' ||
p.module === 'policy_engine' ||
p.module === 'config' ||
p.module === 'audit_logs',
);
opsManagerRole = this.roleRepository.create({
name: 'Operations Manager',
slug: 'operations_manager',
description: 'Manages flight operations, policies, cohorts, and recovery workflows',
isSystem: false,
tenantId: defaultTenant.id,
permissions: opsPermissions,
});
await this.roleRepository.save(opsManagerRole);
}
// 4. Seed Starter Role: Disruption Agent
let agentRole = await this.roleRepository.findOne({
where: { slug: 'disruption_agent' },
});
if (!agentRole) {
const agentPermissions = allPermissions.filter(
(p) =>
p.code === 'dashboard:view' ||
p.code === 'simulation:view' ||
p.code === 'simulation:execute' ||
p.code === 'recovery:view' ||
p.code === 'recovery:create' ||
p.code === 'recovery:edit' ||
p.code === 'recovery:evaluate',
);
agentRole = this.roleRepository.create({
name: 'Disruption Agent',
slug: 'disruption_agent',
description: 'Operational agent handling passenger incidents and simulations',
isSystem: false,
tenantId: defaultTenant.id,
permissions: agentPermissions,
});
await this.roleRepository.save(agentRole);
}
// 5. Seed Starter Role: Read-Only Auditor
let auditorRole = await this.roleRepository.findOne({
where: { slug: 'auditor' },
});
if (!auditorRole) {
const viewPermissions = allPermissions.filter((p) => p.action === 'view' || p.action === 'export');
auditorRole = this.roleRepository.create({
name: 'Auditor',
slug: 'auditor',
description: 'Read-only access to audit logs and system analytics',
isSystem: false,
tenantId: defaultTenant.id,
permissions: viewPermissions,
});
await this.roleRepository.save(auditorRole);
}
// 6. Seed Default Super Admin User
const defaultEmail = 'admin@aeroresolve.com';
let adminUser = await this.userRepository.findOne({
where: { email: defaultEmail },
});
if (!adminUser) {
const salt = await bcrypt.genSalt(12);
const hashedPassword = await bcrypt.hash('Password@123', salt);
adminUser = this.userRepository.create({
email: defaultEmail,
passwordHash: hashedPassword,
firstName: 'System',
lastName: 'Administrator',
roleId: superAdminRole.id,
role: superAdminRole,
tenantId: defaultTenant.id,
tenant: defaultTenant,
isActive: true,
isSystem: true,
});
await this.userRepository.save(adminUser);
this.logger.log(`Seeded default Super Admin user (${defaultEmail} / Password@123) for tenant "${defaultTenant.name}"`);
} else {
// Ensure admin user points to superAdminRole and defaultTenant
let needsSave = false;
if (!adminUser.isSystem || adminUser.roleId !== superAdminRole.id) {
adminUser.isSystem = true;
adminUser.roleId = superAdminRole.id;
needsSave = true;
}
if (!adminUser.tenantId) {
adminUser.tenantId = defaultTenant.id;
needsSave = true;
}
if (needsSave) {
await this.userRepository.save(adminUser);
}
}
} catch (error) {
this.logger.error('Failed to seed authentication data:', error);
}
}
/**
* User login with credential validation, token generation, and secure cookie setting
*/
async login(loginDto: LoginDto, req: Request, res: Response) {
const { email, password } = loginDto;
const normalizedEmail = email.toLowerCase().trim();
const user = await this.userRepository
.createQueryBuilder('user')
.addSelect('user.passwordHash')
.leftJoinAndSelect('user.role', 'role')
.leftJoinAndSelect('role.permissions', 'permissions')
.where('LOWER(user.email) = :email', { email: normalizedEmail })
.getOne();
if (!user) {
throw new UnauthorizedException('Invalid email or password');
}
if (!user.isActive) {
throw new UnauthorizedException('Account is deactivated. Please contact an administrator.');
}
const isMatch = await bcrypt.compare(password, user.passwordHash);
if (!isMatch) {
throw new UnauthorizedException('Invalid email or password');
}
// Generate Tokens
const { accessToken, refreshToken, refreshTokenExpiresAt } = await this.generateTokens(user);
// Store hashed refresh token in DB
const tokenHash = this.hashToken(refreshToken);
const ipAddress = req.ip || req.socket.remoteAddress || 'unknown';
const deviceInfo = req.headers['user-agent'] || 'unknown';
const tokenEntity = this.refreshTokenRepository.create({
userId: user.id,
tokenHash,
ipAddress: String(ipAddress).slice(0, 45),
deviceInfo: String(deviceInfo).slice(0, 500),
expiresAt: refreshTokenExpiresAt,
isRevoked: false,
});
await this.refreshTokenRepository.save(tokenEntity);
// Update last login
user.lastLoginAt = new Date();
await this.userRepository.save(user);
// Set secure HTTP-only cookies
this.setAuthCookies(res, accessToken, refreshToken);
return {
accessToken,
user: this.sanitizeUser(user),
};
}
/**
* Refresh Token rotation flow
*/
async refreshSession(req: Request, res: Response) {
let rawRefreshToken = req.cookies?.['aero_refresh_token'];
if (!rawRefreshToken && req.headers['x-refresh-token']) {
rawRefreshToken = req.headers['x-refresh-token'] as string;
}
if (!rawRefreshToken) {
throw new UnauthorizedException('Refresh token missing');
}
let payload: any;
try {
const refreshSecret =
this.configService.get<string>('JWT_REFRESH_SECRET') ||
'aeroresolve_ultra_refresh_secret_key_2026_@#!';
payload = this.jwtService.verify(rawRefreshToken, { secret: refreshSecret });
} catch {
this.clearAuthCookies(res);
throw new UnauthorizedException('Invalid or expired refresh token');
}
const user = await this.userRepository.findOne({
where: { id: payload.sub },
relations: { role: { permissions: true } },
});
if (!user || !user.isActive) {
this.clearAuthCookies(res);
throw new UnauthorizedException('User account invalid or deactivated');
}
const tokenHash = this.hashToken(rawRefreshToken);
const existingTokenRecord = await this.refreshTokenRepository.findOne({
where: {
userId: user.id,
tokenHash,
isRevoked: false,
},
});
if (!existingTokenRecord || existingTokenRecord.expiresAt < new Date()) {
// Possible reuse detection / attack -> revoke all user sessions
if (existingTokenRecord?.isRevoked) {
await this.refreshTokenRepository.update({ userId: user.id }, { isRevoked: true });
}
this.clearAuthCookies(res);
throw new UnauthorizedException('Refresh token revoked or expired');
}
// Revoke old refresh token (rotation)
existingTokenRecord.isRevoked = true;
await this.refreshTokenRepository.save(existingTokenRecord);
// Issue new pair
const { accessToken, refreshToken: newRefreshToken, refreshTokenExpiresAt } =
await this.generateTokens(user);
const newTokenHash = this.hashToken(newRefreshToken);
const ipAddress = req.ip || req.socket.remoteAddress || 'unknown';
const deviceInfo = req.headers['user-agent'] || 'unknown';
const newTokenEntity = this.refreshTokenRepository.create({
userId: user.id,
tokenHash: newTokenHash,
ipAddress: String(ipAddress).slice(0, 45),
deviceInfo: String(deviceInfo).slice(0, 500),
expiresAt: refreshTokenExpiresAt,
isRevoked: false,
});
await this.refreshTokenRepository.save(newTokenEntity);
// Set updated cookies
this.setAuthCookies(res, accessToken, newRefreshToken);
return {
accessToken,
user: this.sanitizeUser(user),
};
}
/**
* User logout
*/
async logout(req: Request, res: Response) {
const rawRefreshToken = req.cookies?.['aero_refresh_token'];
if (rawRefreshToken) {
const tokenHash = this.hashToken(rawRefreshToken);
await this.refreshTokenRepository.update({ tokenHash }, { isRevoked: true });
}
this.clearAuthCookies(res);
return { success: true, message: 'Logged out successfully' };
}
/**
* Get authenticated user profile with permissions
*/
async getMe(userId: string) {
const user = await this.userRepository.findOne({
where: { id: userId },
relations: { role: { permissions: true } },
});
if (!user) {
throw new NotFoundException('User profile not found');
}
return this.sanitizeUser(user);
}
/**
* Change user password
*/
async changePassword(userId: string, dto: ChangePasswordDto) {
const user = await this.userRepository
.createQueryBuilder('user')
.addSelect('user.passwordHash')
.where('user.id = :userId', { userId })
.getOne();
if (!user) {
throw new NotFoundException('User not found');
}
const isMatch = await bcrypt.compare(dto.currentPassword, user.passwordHash);
if (!isMatch) {
throw new BadRequestException('Current password does not match');
}
const salt = await bcrypt.genSalt(12);
user.passwordHash = await bcrypt.hash(dto.newPassword, salt);
await this.userRepository.save(user);
// Invalidate all active refresh tokens on password change
await this.refreshTokenRepository.update({ userId: user.id }, { isRevoked: true });
return { success: true, message: 'Password updated successfully' };
}
// Helper: Generate JWT access and refresh tokens
private async generateTokens(user: User) {
const accessSecret =
this.configService.get<string>('JWT_SECRET') ||
'aeroresolve_ultra_secure_jwt_secret_key_2026_@#!';
const refreshSecret =
this.configService.get<string>('JWT_REFRESH_SECRET') ||
'aeroresolve_ultra_refresh_secret_key_2026_@#!';
const accessExpiresIn =
(this.configService.get<string>('JWT_EXPIRES_IN') || '15m') as any;
const refreshExpiresIn =
(this.configService.get<string>('JWT_REFRESH_EXPIRES_IN') || '7d') as any;
const payload = {
sub: user.id,
email: user.email,
roleId: user.roleId,
roleSlug: user.role?.slug || 'unknown',
};
const accessToken = this.jwtService.sign(payload, {
secret: accessSecret,
expiresIn: accessExpiresIn,
});
const refreshToken = this.jwtService.sign(payload, {
secret: refreshSecret,
expiresIn: refreshExpiresIn,
});
const refreshTokenExpiresAt = new Date();
refreshTokenExpiresAt.setDate(refreshTokenExpiresAt.getDate() + 7);
return { accessToken, refreshToken, refreshTokenExpiresAt };
}
// Helper: SHA256 hash for stored refresh tokens
private hashToken(token: string): string {
return crypto.createHash('sha256').update(token).digest('hex');
}
// Helper: Set secure HTTP-only cookies
private setAuthCookies(res: Response, accessToken: string, refreshToken: string) {
const isProd = this.configService.get<string>('NODE_ENV') === 'production';
res.cookie('aero_access_token', accessToken, {
httpOnly: true,
secure: isProd,
sameSite: isProd ? 'strict' : 'lax',
path: '/',
maxAge: 15 * 60 * 1000, // 15 minutes
});
res.cookie('aero_refresh_token', refreshToken, {
httpOnly: true,
secure: isProd,
sameSite: isProd ? 'strict' : 'lax',
path: '/',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});
}
// Helper: Clear cookies
private clearAuthCookies(res: Response) {
const isProd = this.configService.get<string>('NODE_ENV') === 'production';
res.cookie('aero_access_token', '', {
httpOnly: true,
secure: isProd,
sameSite: isProd ? 'strict' : 'lax',
path: '/',
maxAge: 0,
});
res.cookie('aero_refresh_token', '', {
httpOnly: true,
secure: isProd,
sameSite: isProd ? 'strict' : 'lax',
path: '/',
maxAge: 0,
});
}
// Helper: Format user object for client consumption
private sanitizeUser(user: User) {
const permissions = user.role?.permissions?.map((p) => p.code) || [];
return {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
fullName: `${user.firstName} ${user.lastName || ''}`.trim(),
roleId: user.roleId,
tenantId: user.tenantId,
role: {
id: user.role?.id,
name: user.role?.name,
slug: user.role?.slug,
description: user.role?.description,
isSystem: user.role?.isSystem,
},
isActive: user.isActive,
isSystem: user.isSystem,
permissions,
lastLoginAt: user.lastLoginAt,
createdAt: user.createdAt,
};
}
}
@@ -0,0 +1,300 @@
export interface SystemPermissionDef {
code: string;
name: string;
module: string;
action: string;
groupName: string;
description: string;
}
export const SYSTEM_PERMISSIONS: SystemPermissionDef[] = [
// Dashboard
{
code: 'dashboard:view',
name: 'View Dashboard',
module: 'dashboard',
action: 'view',
groupName: 'Dashboard',
description: 'Allows viewing dashboard metrics, recovery status overview, and KPIs',
},
{
code: 'dashboard:export',
name: 'Export Dashboard Metrics',
module: 'dashboard',
action: 'export',
groupName: 'Dashboard',
description: 'Allows exporting aggregated dashboard statistics and operational summaries',
},
// Simulation Engine
{
code: 'simulation:view',
name: 'Access Simulation Terminal',
module: 'simulation',
action: 'view',
groupName: 'Simulation Engine',
description: 'Allows accessing manifest simulation workspace and previewing policy outcomes',
},
{
code: 'simulation:execute',
name: 'Execute Simulation Run',
module: 'simulation',
action: 'execute',
groupName: 'Simulation Engine',
description: 'Allows running real-time impact simulations against live or test passenger manifests',
},
// Recovery Incidents
{
code: 'recovery:view',
name: 'View Recovery Incidents',
module: 'recovery',
action: 'view',
groupName: 'Recovery Incidents',
description: 'Allows viewing flight disruption incidents and affected passenger lists',
},
{
code: 'recovery:create',
name: 'Create Recovery Incident',
module: 'recovery',
action: 'create',
groupName: 'Recovery Incidents',
description: 'Allows registering and initiating a new flight recovery incident',
},
{
code: 'recovery:edit',
name: 'Update Recovery Incident',
module: 'recovery',
action: 'edit',
groupName: 'Recovery Incidents',
description: 'Allows modifying incident parameters, status, and notes',
},
{
code: 'recovery:evaluate',
name: 'Evaluate Incident Policies',
module: 'recovery',
action: 'execute',
groupName: 'Recovery Incidents',
description: 'Allows running policy engine evaluations and generating compensation offers',
},
{
code: 'recovery:override',
name: 'Manual Action Override',
module: 'recovery',
action: 'approve',
groupName: 'Recovery Incidents',
description: 'Allows overriding automated decisions and granting exceptional goodwill gestures',
},
{
code: 'recovery:delete',
name: 'Delete Recovery Incident',
module: 'recovery',
action: 'delete',
groupName: 'Recovery Incidents',
description: 'Allows removing incident records from the system',
},
// Cohort Management
{
code: 'cohorts:view',
name: 'View Cohorts',
module: 'cohorts',
action: 'view',
groupName: 'Cohort Management',
description: 'Allows viewing passenger segmentation cohorts and classification rules',
},
{
code: 'cohorts:create',
name: 'Create Cohort',
module: 'cohorts',
action: 'create',
groupName: 'Cohort Management',
description: 'Allows designing and creating new passenger segmentation cohorts',
},
{
code: 'cohorts:edit',
name: 'Edit Cohort',
module: 'cohorts',
action: 'edit',
groupName: 'Cohort Management',
description: 'Allows updating cohort criteria, priority, and definitions',
},
{
code: 'cohorts:delete',
name: 'Delete Cohort',
module: 'cohorts',
action: 'delete',
groupName: 'Cohort Management',
description: 'Allows deleting passenger cohorts',
},
// Policy Engine
{
code: 'policy_engine:view',
name: 'View Policy Frameworks',
module: 'policy_engine',
action: 'view',
groupName: 'Policy Engine',
description: 'Allows viewing policy frameworks, rules, and condition sets',
},
{
code: 'policy_engine:create',
name: 'Create Policy Framework',
module: 'policy_engine',
action: 'create',
groupName: 'Policy Engine',
description: 'Allows authoring new policy frameworks and rules',
},
{
code: 'policy_engine:edit',
name: 'Edit Policy Framework',
module: 'policy_engine',
action: 'edit',
groupName: 'Policy Engine',
description: 'Allows modifying existing policy conditions, rules, and actions',
},
{
code: 'policy_engine:publish',
name: 'Publish/Activate Policy',
module: 'policy_engine',
action: 'publish',
groupName: 'Policy Engine',
description: 'Allows promoting draft policies to active production status',
},
{
code: 'policy_engine:delete',
name: 'Delete Policy Framework',
module: 'policy_engine',
action: 'delete',
groupName: 'Policy Engine',
description: 'Allows removing policy frameworks from the registry',
},
// Configuration & Action Builder & Master Data
{
code: 'config:view',
name: 'View Master Configurations',
module: 'config',
action: 'view',
groupName: 'Configuration & Masters',
description: 'Allows viewing system master data lookups and action builder templates',
},
{
code: 'config:create',
name: 'Create Master Data Item',
module: 'config',
action: 'create',
groupName: 'Configuration & Masters',
description: 'Allows creating new master lookup entries, categories, and field definitions',
},
{
code: 'config:edit',
name: 'Edit Master Data Item',
module: 'config',
action: 'edit',
groupName: 'Configuration & Masters',
description: 'Allows editing master lookup values, action types, and metadata schemas',
},
{
code: 'config:delete',
name: 'Delete Master Data Item',
module: 'config',
action: 'delete',
groupName: 'Configuration & Masters',
description: 'Allows deleting master lookup records',
},
// Audit Logs
{
code: 'audit_logs:view',
name: 'View Audit Logs',
module: 'audit_logs',
action: 'view',
groupName: 'Audit Logs',
description: 'Allows inspecting immutable compliance and event logs across the system',
},
{
code: 'audit_logs:export',
name: 'Export Audit Logs',
module: 'audit_logs',
action: 'export',
groupName: 'Audit Logs',
description: 'Allows exporting regulatory audit trails and security event logs',
},
// User Management
{
code: 'users:view',
name: 'View Users',
module: 'users',
action: 'view',
groupName: 'Access Control',
description: 'Allows viewing system users, profile details, and account statuses',
},
{
code: 'users:create',
name: 'Create User',
module: 'users',
action: 'create',
groupName: 'Access Control',
description: 'Allows provisioning new user accounts with role assignment',
},
{
code: 'users:edit',
name: 'Edit User',
module: 'users',
action: 'edit',
groupName: 'Access Control',
description: 'Allows updating user profiles, roles, and status (active/inactive)',
},
{
code: 'users:reset_password',
name: 'Reset User Password',
module: 'users',
action: 'edit',
groupName: 'Access Control',
description: 'Allows resetting credentials for user accounts',
},
{
code: 'users:delete',
name: 'Delete User',
module: 'users',
action: 'delete',
groupName: 'Access Control',
description: 'Allows removing non-system user accounts',
},
// Role Management
{
code: 'roles:view',
name: 'View Roles & Permissions',
module: 'roles',
action: 'view',
groupName: 'Access Control',
description: 'Allows viewing defined roles and assigned permission matrices',
},
{
code: 'roles:create',
name: 'Create Custom Role',
module: 'roles',
action: 'create',
groupName: 'Access Control',
description: 'Allows defining custom roles with granular permission mappings',
},
{
code: 'roles:edit',
name: 'Edit Role Permissions',
module: 'roles',
action: 'edit',
groupName: 'Access Control',
description: 'Allows updating name, description, and permissions of custom roles',
},
{
code: 'roles:delete',
name: 'Delete Role',
module: 'roles',
action: 'delete',
groupName: 'Access Control',
description: 'Allows deleting non-system roles that have no active users',
},
];
@@ -0,0 +1,9 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
);
@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
export const PERMISSIONS_KEY = 'permissions';
export const RequirePermissions = (...permissions: string[]) =>
SetMetadata(PERMISSIONS_KEY, permissions);
@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
export class ChangePasswordDto {
@ApiProperty({ example: 'OldPassword@123' })
@IsString()
@IsNotEmpty({ message: 'Current password is required' })
currentPassword!: string;
@ApiProperty({ example: 'NewSecretPass@2026' })
@IsString()
@IsNotEmpty({ message: 'New password is required' })
@MinLength(8, { message: 'New password must be at least 8 characters long' })
newPassword!: string;
}
+15
View File
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
export class LoginDto {
@ApiProperty({ example: 'admin@aeroresolve.com' })
@IsEmail({}, { message: 'Please provide a valid email address' })
@IsNotEmpty({ message: 'Email is required' })
email!: string;
@ApiProperty({ example: 'Password@123' })
@IsString()
@IsNotEmpty({ message: 'Password is required' })
@MinLength(6, { message: 'Password must be at least 6 characters long' })
password!: string;
}
+51
View File
@@ -0,0 +1,51 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
ArrayNotEmpty,
IsArray,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
export class CreateRoleDto {
@ApiProperty({ example: 'Flight Disruption Lead' })
@IsString()
@IsNotEmpty({ message: 'Role name is required' })
name!: string;
@ApiPropertyOptional({ example: 'Manages flight recovery incident workflows and exceptions' })
@IsString()
@IsOptional()
description?: string;
@ApiProperty({
example: ['11111111-1111-1111-1111-111111111111', '22222222-2222-2222-2222-222222222222'],
description: 'Array of Permission UUIDs to assign to this role',
})
@IsArray()
@ArrayNotEmpty({ message: 'At least one permission must be assigned to the role' })
@IsUUID('all', { each: true, message: 'Each permission ID must be a valid UUID' })
permissionIds!: string[];
}
export class UpdateRoleDto {
@ApiPropertyOptional({ example: 'Flight Disruption Lead' })
@IsString()
@IsOptional()
name?: string;
@ApiPropertyOptional({ example: 'Updated description of role responsibilities' })
@IsString()
@IsOptional()
description?: string;
@ApiPropertyOptional({
example: ['11111111-1111-1111-1111-111111111111'],
description: 'Updated array of Permission UUIDs for this role',
})
@IsArray()
@IsOptional()
@IsUUID('all', { each: true, message: 'Each permission ID must be a valid UUID' })
permissionIds?: string[];
}
+73
View File
@@ -0,0 +1,73 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsBoolean,
IsEmail,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
MinLength,
} from 'class-validator';
export class CreateUserDto {
@ApiProperty({ example: 'john.doe@aeroresolve.com' })
@IsEmail({}, { message: 'Please provide a valid email address' })
@IsNotEmpty({ message: 'Email is required' })
email!: string;
@ApiProperty({ example: 'Password@123' })
@IsString()
@IsNotEmpty({ message: 'Password is required' })
@MinLength(8, { message: 'Password must be at least 8 characters long' })
password!: string;
@ApiProperty({ example: 'John' })
@IsString()
@IsNotEmpty({ message: 'First name is required' })
firstName!: string;
@ApiPropertyOptional({ example: 'Doe' })
@IsString()
@IsOptional()
lastName?: string;
@ApiProperty({ example: '11111111-1111-1111-1111-111111111111' })
@IsUUID('all', { message: 'Valid Role ID (UUID) is required' })
@IsNotEmpty({ message: 'Role ID is required' })
roleId!: string;
@ApiPropertyOptional({ example: true, default: true })
@IsBoolean()
@IsOptional()
isActive?: boolean;
}
export class UpdateUserDto {
@ApiPropertyOptional({ example: 'John' })
@IsString()
@IsOptional()
firstName?: string;
@ApiPropertyOptional({ example: 'Doe' })
@IsString()
@IsOptional()
lastName?: string;
@ApiPropertyOptional({ example: '11111111-1111-1111-1111-111111111111' })
@IsUUID('all', { message: 'Valid Role ID (UUID) is required' })
@IsOptional()
roleId?: string;
@ApiPropertyOptional({ example: true })
@IsBoolean()
@IsOptional()
isActive?: boolean;
}
export class ResetUserPasswordDto {
@ApiProperty({ example: 'NewSecretPass@2026' })
@IsString()
@IsNotEmpty({ message: 'New password is required' })
@MinLength(8, { message: 'Password must be at least 8 characters long' })
newPassword!: string;
}
+4
View File
@@ -0,0 +1,4 @@
export * from './permission.entity';
export * from './role.entity';
export * from './user.entity';
export * from './refresh-token.entity';
@@ -0,0 +1,33 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity({ name: 'tbl_permissions', schema: 'auth' })
export class Permission {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ type: 'varchar', length: 100 })
module!: string; // e.g., 'dashboard', 'simulation', 'recovery', 'cohorts', 'policy_engine', 'config', 'audit_logs', 'users', 'roles'
@Column({ type: 'varchar', length: 50 })
action!: string; // e.g., 'view', 'create', 'edit', 'delete', 'export', 'execute', 'publish'
@Column({ type: 'varchar', length: 100, unique: true })
code!: string; // e.g., 'policy_engine:create', 'users:delete'
@Column({ type: 'varchar', length: 150 })
name!: string; // e.g., 'Create Policy'
@Column({ type: 'text', nullable: true })
description!: string;
@Column({ name: 'group_name', type: 'varchar', length: 100, default: 'General' })
groupName!: string;
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
}
@@ -0,0 +1,40 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from './user.entity';
@Entity({ name: 'tbl_refresh_tokens', schema: 'auth' })
export class RefreshToken {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ name: 'user_id', type: 'uuid' })
userId!: string;
@ManyToOne(() => User, (user) => user.refreshTokens, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user!: User;
@Column({ name: 'token_hash', type: 'varchar', length: 255 })
tokenHash!: string;
@Column({ name: 'device_info', type: 'text', nullable: true })
deviceInfo!: string;
@Column({ name: 'ip_address', type: 'varchar', length: 45, nullable: true })
ipAddress!: string;
@Column({ name: 'is_revoked', type: 'boolean', default: false })
isRevoked!: boolean;
@Column({ name: 'expires_at', type: 'timestamp with time zone' })
expiresAt!: Date;
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
}
+58
View File
@@ -0,0 +1,58 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Permission } from './permission.entity';
import { User } from './user.entity';
import { Tenant } from '../../tenant/tenant.entity';
@Entity({ name: 'tbl_roles', schema: 'auth' })
export class Role {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ name: 'tenant_id', type: 'uuid', nullable: true })
tenantId!: string | null;
@ManyToOne(() => Tenant, { eager: false, nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'tenant_id' })
tenant!: Tenant | null;
@Column({ type: 'varchar', length: 100 })
name!: string;
@Column({ type: 'varchar', length: 100, unique: true })
slug!: string;
@Column({ type: 'text', nullable: true })
description!: string;
@Column({ name: 'is_system', type: 'boolean', default: false })
isSystem!: boolean; // System roles (like Super Admin) cannot be edited or deleted
@ManyToMany(() => Permission, { eager: true, cascade: true })
@JoinTable({
name: 'tbl_role_permissions',
schema: 'auth',
joinColumn: { name: 'role_id', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'permission_id', referencedColumnName: 'id' },
})
permissions!: Permission[];
@OneToMany(() => User, (user) => user.role)
users!: User[];
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date;
}
+63
View File
@@ -0,0 +1,63 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Role } from './role.entity';
import { RefreshToken } from './refresh-token.entity';
import { Tenant } from '../../tenant/tenant.entity';
@Entity({ name: 'tbl_users', schema: 'auth' })
export class User {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ name: 'tenant_id', type: 'uuid', nullable: true })
tenantId!: string | null;
@ManyToOne(() => Tenant, { eager: false, nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'tenant_id' })
tenant!: Tenant | null;
@Column({ type: 'varchar', length: 255, unique: true })
email!: string;
@Column({ name: 'password_hash', type: 'varchar', length: 255, select: false })
passwordHash!: string;
@Column({ name: 'first_name', type: 'varchar', length: 100 })
firstName!: string;
@Column({ name: 'last_name', type: 'varchar', length: 100, nullable: true })
lastName!: string;
@Column({ name: 'role_id', type: 'uuid' })
roleId!: string;
@ManyToOne(() => Role, (role) => role.users, { eager: true, onDelete: 'RESTRICT' })
@JoinColumn({ name: 'role_id' })
role!: Role;
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive!: boolean;
@Column({ name: 'is_system', type: 'boolean', default: false })
isSystem!: boolean; // System default admin user cannot be deleted
@Column({ name: 'last_login_at', type: 'timestamp with time zone', nullable: true })
lastLoginAt!: Date | null;
@OneToMany(() => RefreshToken, (token) => token.user, { cascade: true })
refreshTokens!: RefreshToken[];
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date;
}
+35
View File
@@ -0,0 +1,35 @@
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
handleRequest(err: any, user: any, info: any) {
if (err || !user) {
throw err || new UnauthorizedException('Authentication required');
}
return user;
}
}
@@ -0,0 +1,49 @@
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PERMISSIONS_KEY } from '../decorators/permissions.decorator';
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
PERMISSIONS_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredPermissions || requiredPermissions.length === 0) {
return true;
}
const { user } = context.switchToHttp().getRequest();
if (!user) {
throw new ForbiddenException('User is not authenticated');
}
// Super Admin has full unrestricted access across all endpoints
if (user.role?.slug === 'super_admin' || user.isSystem) {
return true;
}
const userPermissions: string[] = user.permissions || [];
const hasPermission = requiredPermissions.every((permission) =>
userPermissions.includes(permission),
);
if (!hasPermission) {
throw new ForbiddenException(
`Insufficient privileges. Required permission(s): ${requiredPermissions.join(', ')}`,
);
}
return true;
}
}
+71
View File
@@ -0,0 +1,71 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { RolesService } from './roles.service';
import { CreateRoleDto, UpdateRoleDto } from './dto/role.dto';
import { RequirePermissions } from './decorators/permissions.decorator';
import { PermissionsGuard } from './guards/permissions.guard';
@ApiTags('Roles & Permissions')
@Controller('roles')
@UseGuards(PermissionsGuard)
export class RolesController {
constructor(private readonly rolesService: RolesService) {}
@Get()
@RequirePermissions('roles:view')
@ApiOperation({ summary: 'List all roles with assigned user and permission counts' })
async findAll() {
return this.rolesService.findAll();
}
@Get(':id')
@RequirePermissions('roles:view')
@ApiOperation({ summary: 'Get role details with all assigned permissions' })
async findOne(@Param('id') id: string) {
return this.rolesService.findOne(id);
}
@Post()
@RequirePermissions('roles:create')
@ApiOperation({ summary: 'Create a new custom role with assigned permissions' })
async create(@Body() dto: CreateRoleDto) {
return this.rolesService.create(dto);
}
@Patch(':id')
@RequirePermissions('roles:edit')
@ApiOperation({ summary: 'Update role details and assigned permissions (System roles protected)' })
async update(@Param('id') id: string, @Body() dto: UpdateRoleDto) {
return this.rolesService.update(id, dto);
}
@Delete(':id')
@RequirePermissions('roles:delete')
@ApiOperation({ summary: 'Delete custom role (System roles & roles with active users protected)' })
async remove(@Param('id') id: string) {
return this.rolesService.remove(id);
}
}
@ApiTags('Permissions')
@Controller('permissions')
@UseGuards(PermissionsGuard)
export class PermissionsController {
constructor(private readonly rolesService: RolesService) {}
@Get()
@RequirePermissions('roles:view')
@ApiOperation({ summary: 'List all available system permissions categorized by module and group' })
async findAll() {
return this.rolesService.findAllPermissions();
}
}
+209
View File
@@ -0,0 +1,209 @@
import {
BadRequestException,
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { Role } from './entities/role.entity';
import { Permission } from './entities/permission.entity';
import { User } from './entities/user.entity';
import { CreateRoleDto, UpdateRoleDto } from './dto/role.dto';
@Injectable()
export class RolesService {
constructor(
@InjectRepository(Role)
private readonly roleRepository: Repository<Role>,
@InjectRepository(Permission)
private readonly permissionRepository: Repository<Permission>,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
/**
* List all roles with assigned user count
*/
async findAll() {
const roles = await this.roleRepository.find({
relations: { permissions: true },
order: { isSystem: 'DESC', name: 'ASC' },
});
const userCounts = await this.userRepository
.createQueryBuilder('user')
.select('user.roleId', 'roleId')
.addSelect('COUNT(user.id)', 'count')
.groupBy('user.roleId')
.getRawMany();
const countMap = new Map<string, number>(
userCounts.map((uc) => [uc.roleId, parseInt(uc.count, 10)]),
);
return roles.map((role) => ({
id: role.id,
name: role.name,
slug: role.slug,
description: role.description,
isSystem: role.isSystem,
permissionCount: role.permissions?.length || 0,
userCount: countMap.get(role.id) || 0,
createdAt: role.createdAt,
updatedAt: role.updatedAt,
permissions: role.permissions,
}));
}
/**
* Find role by ID
*/
async findOne(id: string) {
const role = await this.roleRepository.findOne({
where: { id },
relations: { permissions: true },
});
if (!role) {
throw new NotFoundException(`Role with ID "${id}" not found`);
}
const userCount = await this.userRepository.count({ where: { roleId: id } });
return {
id: role.id,
name: role.name,
slug: role.slug,
description: role.description,
isSystem: role.isSystem,
userCount,
permissions: role.permissions,
createdAt: role.createdAt,
updatedAt: role.updatedAt,
};
}
/**
* Create custom role
*/
async create(dto: CreateRoleDto) {
const baseSlug = dto.name
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '');
const existingSlug = await this.roleRepository.findOne({ where: { slug: baseSlug } });
if (existingSlug) {
throw new ConflictException(`A role with name "${dto.name}" already exists`);
}
const permissions = await this.permissionRepository.findBy({
id: In(dto.permissionIds),
});
if (permissions.length === 0) {
throw new BadRequestException('At least one valid permission must be selected');
}
const role = this.roleRepository.create({
name: dto.name.trim(),
slug: baseSlug,
description: dto.description?.trim(),
isSystem: false,
permissions,
});
return this.roleRepository.save(role);
}
/**
* Update role details and permissions
*/
async update(id: string, dto: UpdateRoleDto) {
const role = await this.roleRepository.findOne({
where: { id },
relations: { permissions: true },
});
if (!role) {
throw new NotFoundException(`Role with ID "${id}" not found`);
}
// System roles (Super Admin) cannot be modified or edited
if (role.isSystem) {
throw new ForbiddenException('System default roles (Super Admin) are immutable and cannot be edited');
}
if (dto.name) {
role.name = dto.name.trim();
}
if (dto.description !== undefined) {
role.description = dto.description?.trim() || '';
}
if (dto.permissionIds) {
const permissions = await this.permissionRepository.findBy({
id: In(dto.permissionIds),
});
if (permissions.length === 0) {
throw new BadRequestException('A role must have at least one permission');
}
role.permissions = permissions;
}
return this.roleRepository.save(role);
}
/**
* Delete role (guarded against system roles and roles in use)
*/
async remove(id: string) {
const role = await this.roleRepository.findOne({ where: { id } });
if (!role) {
throw new NotFoundException(`Role with ID "${id}" not found`);
}
if (role.isSystem) {
throw new ForbiddenException('System default roles (Super Admin) cannot be deleted');
}
const userCount = await this.userRepository.count({ where: { roleId: id } });
if (userCount > 0) {
throw new ConflictException(
`Cannot delete role "${role.name}" because it is currently assigned to ${userCount} user(s). Reassign them first.`,
);
}
await this.roleRepository.remove(role);
return { success: true, message: `Role "${role.name}" removed successfully` };
}
/**
* Get all system permissions organized by module / group
*/
async findAllPermissions() {
const permissions = await this.permissionRepository.find({
order: { groupName: 'ASC', name: 'ASC' },
});
// Group permissions for matrix UI
const grouped: Record<string, Permission[]> = {};
for (const perm of permissions) {
const group = perm.groupName || perm.module;
if (!grouped[group]) {
grouped[group] = [];
}
grouped[group].push(perm);
}
return {
all: permissions,
grouped,
};
}
}
@@ -0,0 +1,80 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import type { Request } from 'express';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../entities/user.entity';
export interface JwtPayload {
sub: string;
email: string;
roleId: string;
roleSlug: string;
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
private readonly configService: ConfigService,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => {
let token = null;
if (request && request.cookies) {
token = request.cookies['aero_access_token'];
}
if (!token && request?.headers?.authorization) {
const parts = request.headers.authorization.split(' ');
if (parts.length === 2 && parts[0] === 'Bearer') {
token = parts[1];
}
}
return token;
},
]),
ignoreExpiration: false,
secretOrKey:
configService.get<string>('JWT_SECRET') ||
'aeroresolve_ultra_secure_jwt_secret_key_2026_@#!',
});
}
async validate(payload: JwtPayload) {
const user = await this.userRepository.findOne({
where: { id: payload.sub },
relations: { role: { permissions: true } },
});
if (!user) {
throw new UnauthorizedException('User account no longer exists');
}
if (!user.isActive) {
throw new UnauthorizedException('User account has been deactivated');
}
const permissionCodes = user.role?.permissions?.map((p) => p.code) || [];
return {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
fullName: `${user.firstName} ${user.lastName || ''}`.trim(),
roleId: user.roleId,
role: {
id: user.role?.id,
name: user.role?.name,
slug: user.role?.slug,
isSystem: user.role?.isSystem,
},
isSystem: user.isSystem,
permissions: permissionCodes,
};
}
}
+77
View File
@@ -0,0 +1,77 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { UsersService } from './users.service';
import { CreateUserDto, ResetUserPasswordDto, UpdateUserDto } from './dto/user.dto';
import { RequirePermissions } from './decorators/permissions.decorator';
import { PermissionsGuard } from './guards/permissions.guard';
import { CurrentUser } from './decorators/current-user.decorator';
@ApiTags('Users')
@Controller('users')
@UseGuards(PermissionsGuard)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
@RequirePermissions('users:view')
@ApiOperation({ summary: 'List users with optional search and role filtering' })
@ApiQuery({ name: 'search', required: false })
@ApiQuery({ name: 'roleId', required: false })
async findAll(
@Query('search') search?: string,
@Query('roleId') roleId?: string,
) {
return this.usersService.findAll(search, roleId);
}
@Get(':id')
@RequirePermissions('users:view')
@ApiOperation({ summary: 'Get single user details by ID' })
async findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
@Post()
@RequirePermissions('users:create')
@ApiOperation({ summary: 'Create a new user with role assignment' })
async create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
@Patch(':id')
@RequirePermissions('users:edit')
@ApiOperation({ summary: 'Update user profile, status, or role' })
async update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
return this.usersService.update(id, dto);
}
@Post(':id/reset-password')
@RequirePermissions('users:reset_password')
@ApiOperation({ summary: 'Reset password for a user account' })
async resetPassword(
@Param('id') id: string,
@Body() dto: ResetUserPasswordDto,
) {
return this.usersService.resetPassword(id, dto);
}
@Delete(':id')
@RequirePermissions('users:delete')
@ApiOperation({ summary: 'Delete a non-system user' })
async remove(
@Param('id') id: string,
@CurrentUser('id') currentUserId: string,
) {
return this.usersService.remove(id, currentUserId);
}
}
+220
View File
@@ -0,0 +1,220 @@
import {
BadRequestException,
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcryptjs';
import { User } from './entities/user.entity';
import { Role } from './entities/role.entity';
import { RefreshToken } from './entities/refresh-token.entity';
import { CreateUserDto, ResetUserPasswordDto, UpdateUserDto } from './dto/user.dto';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@InjectRepository(Role)
private readonly roleRepository: Repository<Role>,
@InjectRepository(RefreshToken)
private readonly refreshTokenRepository: Repository<RefreshToken>,
) {}
/**
* List all users with optional search and role filtering
*/
async findAll(search?: string, roleId?: string) {
const qb = this.userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.role', 'role')
.leftJoinAndSelect('role.permissions', 'permissions')
.orderBy('user.isSystem', 'DESC')
.addOrderBy('user.createdAt', 'DESC');
if (search) {
const term = `%${search.toLowerCase().trim()}%`;
qb.andWhere(
'(LOWER(user.email) LIKE :term OR LOWER(user.firstName) LIKE :term OR LOWER(user.lastName) LIKE :term)',
{ term },
);
}
if (roleId) {
qb.andWhere('user.roleId = :roleId', { roleId });
}
const users = await qb.getMany();
return users.map((u) => this.sanitizeUser(u));
}
/**
* Find single user by ID
*/
async findOne(id: string) {
const user = await this.userRepository.findOne({
where: { id },
relations: { role: { permissions: true } },
});
if (!user) {
throw new NotFoundException(`User with ID "${id}" not found`);
}
return this.sanitizeUser(user);
}
/**
* Create new user
*/
async create(dto: CreateUserDto) {
const normalizedEmail = dto.email.toLowerCase().trim();
const existingUser = await this.userRepository.findOne({
where: { email: normalizedEmail },
});
if (existingUser) {
throw new ConflictException(`User with email "${normalizedEmail}" already exists`);
}
const role = await this.roleRepository.findOne({ where: { id: dto.roleId } });
if (!role) {
throw new BadRequestException(`Role with ID "${dto.roleId}" not found`);
}
const salt = await bcrypt.genSalt(12);
const passwordHash = await bcrypt.hash(dto.password, salt);
const user = this.userRepository.create({
email: normalizedEmail,
passwordHash,
firstName: dto.firstName.trim(),
lastName: dto.lastName?.trim(),
roleId: role.id,
role,
tenantId: role.tenantId,
isActive: dto.isActive !== undefined ? dto.isActive : true,
isSystem: false,
});
const saved = await this.userRepository.save(user);
return this.findOne(saved.id);
}
/**
* Update user details
*/
async update(id: string, dto: UpdateUserDto) {
const user = await this.userRepository.findOne({
where: { id },
relations: { role: true },
});
if (!user) {
throw new NotFoundException(`User with ID "${id}" not found`);
}
// System user safety: cannot deactivate default admin
if (user.isSystem && dto.isActive === false) {
throw new ForbiddenException('The default System Administrator account cannot be deactivated');
}
if (dto.firstName) {
user.firstName = dto.firstName.trim();
}
if (dto.lastName !== undefined) {
user.lastName = dto.lastName?.trim();
}
if (dto.roleId && dto.roleId !== user.roleId) {
const newRole = await this.roleRepository.findOne({ where: { id: dto.roleId } });
if (!newRole) {
throw new BadRequestException(`Role with ID "${dto.roleId}" not found`);
}
user.roleId = newRole.id;
user.role = newRole;
}
if (dto.isActive !== undefined) {
user.isActive = dto.isActive;
}
await this.userRepository.save(user);
return this.findOne(user.id);
}
/**
* Reset user password (by admin)
*/
async resetPassword(id: string, dto: ResetUserPasswordDto) {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`User with ID "${id}" not found`);
}
const salt = await bcrypt.genSalt(12);
user.passwordHash = await bcrypt.hash(dto.newPassword, salt);
await this.userRepository.save(user);
// Invalidate active sessions
await this.refreshTokenRepository.update({ userId: user.id }, { isRevoked: true });
return { success: true, message: `Password for "${user.email}" was reset successfully` };
}
/**
* Delete user
*/
async remove(id: string, currentUserId: string) {
if (id === currentUserId) {
throw new ForbiddenException('You cannot delete your own active account');
}
const user = await this.userRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`User with ID "${id}" not found`);
}
if (user.isSystem) {
throw new ForbiddenException('The default System Administrator account cannot be deleted');
}
// Revoke all refresh tokens
await this.refreshTokenRepository.delete({ userId: user.id });
await this.userRepository.remove(user);
return { success: true, message: `User "${user.email}" removed successfully` };
}
private sanitizeUser(user: User) {
const permissions = user.role?.permissions?.map((p) => p.code) || [];
return {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
fullName: `${user.firstName} ${user.lastName || ''}`.trim(),
roleId: user.roleId,
tenantId: user.tenantId,
role: {
id: user.role?.id,
name: user.role?.name,
slug: user.role?.slug,
description: user.role?.description,
isSystem: user.role?.isSystem,
},
isActive: user.isActive,
isSystem: user.isSystem,
permissions,
lastLoginAt: user.lastLoginAt,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
};
}
}
+11 -1
View File
@@ -1,18 +1,23 @@
import { Controller, Get, Post, Body, Param, Put, Delete, Patch, Query } from '@nestjs/common';
import { Controller, Get, Post, Body, Param, Put, Delete, Patch, Query, UseGuards } from '@nestjs/common';
import { CohortService } from './cohort.service';
import { CreateCohortDto } from './dto/create-cohort.dto';
import { UpdateCohortDto } from './dto/update-cohort.dto';
import { PermissionsGuard } from '../auth/guards/permissions.guard';
import { RequirePermissions } from '../auth/decorators/permissions.decorator';
@Controller('cohorts')
@UseGuards(PermissionsGuard)
export class CohortController {
constructor(private readonly cohortService: CohortService) {}
@Post()
@RequirePermissions('cohorts:create')
async create(@Body() createCohortDto: CreateCohortDto) {
return this.cohortService.create(createCohortDto);
}
@Get()
@RequirePermissions('cohorts:view')
async findAll(
@Query('page') page: string = '1',
@Query('limit') limit: string = '10',
@@ -21,22 +26,27 @@ export class CohortController {
}
@Get(':id')
@RequirePermissions('cohorts:view')
async findOne(@Param('id') id: string) {
return this.cohortService.findOne(id);
}
@Put(':id')
@RequirePermissions('cohorts:edit')
async update(@Param('id') id: string, @Body() updateCohortDto: UpdateCohortDto) {
return this.cohortService.update(id, updateCohortDto);
}
@Patch(':id/status')
@RequirePermissions('cohorts:edit')
async updateStatus(@Param('id') id: string, @Body('status') status: string) {
return this.cohortService.updateStatus(id, status);
}
@Delete(':id')
@RequirePermissions('cohorts:delete')
async remove(@Param('id') id: string) {
return this.cohortService.remove(id);
}
}
+25 -25
View File
@@ -11,101 +11,101 @@ import { TripPurpose } from '../master-data/entities/trip-purpose.entity';
@Entity({ name: 'tbl_cohorts', schema: 'cohort' })
export class Cohort extends TenantOwnedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
id!: string;
@Column({ nullable: true })
scopeType: string;
scopeType!: string;
@Column()
name: string;
name!: string;
@Column({ nullable: true })
description: string;
description!: string;
@Column({ default: 'Draft' })
status: string; // Draft, Active, Inactive
status!: string; // Draft, Active, Inactive
// Passenger Attributes
@ManyToMany(() => CabinClass)
@JoinTable({
@JoinTable({
name: 'tbl_cohort_cabin_classes',
joinColumn: { name: 'cohortId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'cabinClassId', referencedColumnName: 'id' }
})
cabinClasses: CabinClass[];
cabinClasses!: CabinClass[];
@ManyToMany(() => PassengerType)
@JoinTable({
@JoinTable({
name: 'tbl_cohort_passenger_types',
joinColumn: { name: 'cohortId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'passengerTypeId', referencedColumnName: 'id' }
})
passengerTypes: PassengerType[];
passengerTypes!: PassengerType[];
@ManyToMany(() => AncillaryPurchase)
@JoinTable({
@JoinTable({
name: 'tbl_cohort_ancillary_purchases',
joinColumn: { name: 'cohortId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'ancillaryPurchaseId', referencedColumnName: 'id' }
})
ancillaryPurchases: AncillaryPurchase[];
ancillaryPurchases!: AncillaryPurchase[];
// Customer Value
@ManyToMany(() => MembershipTier)
@JoinTable({
@JoinTable({
name: 'tbl_cohort_loyalty_tiers',
joinColumn: { name: 'cohortId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'membershipTierId', referencedColumnName: 'id' }
})
loyaltyTiers: MembershipTier[];
loyaltyTiers!: MembershipTier[];
@ManyToMany(() => RevenueSegment)
@JoinTable({
@JoinTable({
name: 'tbl_cohort_revenue_segments',
joinColumn: { name: 'cohortId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'revenueSegmentId', referencedColumnName: 'id' }
})
revenueSegments: RevenueSegment[];
revenueSegments!: RevenueSegment[];
@Column({
type: 'enum',
enum: ['Any', 'Yes(VIP/Strategic)', 'No'],
default: 'Any'
})
highValuePassenger: string;
highValuePassenger!: string;
@Column({
type: 'enum',
enum: ['Domestic Only', 'International Only', 'Both (All)'],
nullable: true
})
flightType: string;
flightType!: string;
@ManyToMany(() => Region)
@JoinTable({
@JoinTable({
name: 'tbl_cohort_regions',
joinColumn: { name: 'cohortId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'regionId', referencedColumnName: 'id' }
})
regions: Region[];
regions!: Region[];
@ManyToMany(() => TripPurpose)
@JoinTable({
@JoinTable({
name: 'tbl_cohort_trip_purposes',
joinColumn: { name: 'cohortId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'tripPurposeId', referencedColumnName: 'id' }
})
tripPurposes: TripPurpose[];
tripPurposes!: TripPurpose[];
@Column('simple-array', { nullable: true })
originAirport: string[];
originAirport!: string[];
@Column('simple-array', { nullable: true })
destinationAirport: string[];
destinationAirport!: string[];
@CreateDateColumn()
createdAt: Date;
createdAt!: Date;
@UpdateDateColumn()
updatedAt: Date;
updatedAt!: Date;
}
+3 -1
View File
@@ -1,11 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CohortController } from './cohort.controller';
import { CohortService } from './cohort.service';
import { Cohort } from './cohort.entity';
import { AuditLogModule } from '../audit-log/audit-log.module';
@Module({
imports: [TypeOrmModule.forFeature([Cohort])],
imports: [TypeOrmModule.forFeature([Cohort]), AuditLogModule],
controllers: [CohortController],
providers: [CohortService],
exports: [CohortService],
+41 -2
View File
@@ -5,12 +5,14 @@ import { getTenantId } from '../../common/tenant/tenant.context';
import { Cohort } from './cohort.entity';
import { CreateCohortDto } from './dto/create-cohort.dto';
import { UpdateCohortDto } from './dto/update-cohort.dto';
import { AuditLogService } from '../audit-log/audit-log.service';
@Injectable()
export class CohortService {
constructor(
@InjectRepository(Cohort)
private readonly cohortRepository: Repository<Cohort>,
private readonly auditLogService: AuditLogService,
) {}
private mapDtoToEntity(dto: CreateCohortDto | UpdateCohortDto) {
@@ -96,19 +98,56 @@ export class CohortService {
async update(id: string, updateCohortDto: UpdateCohortDto): Promise<Cohort> {
const cohort = await this.findOne(id);
const beforeSnapshot = { ...cohort };
const mappedData = this.mapDtoToEntity(updateCohortDto);
Object.assign(cohort, mappedData);
return this.cohortRepository.save(cohort);
const after = await this.cohortRepository.save(cohort);
await this.auditLogService.log({
module: 'cohort',
action: 'UPDATE',
entityId: id,
entityLabel: cohort.name,
before: beforeSnapshot,
after: { ...after },
performedBy: getTenantId(),
});
return after;
}
async updateStatus(id: string, status: string): Promise<Cohort> {
const cohort = await this.findOne(id);
const beforeStatus = cohort.status;
cohort.status = status;
return this.cohortRepository.save(cohort);
const after = await this.cohortRepository.save(cohort);
await this.auditLogService.log({
module: 'cohort',
action: 'STATUS_CHANGE',
entityId: id,
entityLabel: cohort.name,
before: { status: beforeStatus },
after: { status },
performedBy: getTenantId(),
});
return after;
}
async remove(id: string): Promise<void> {
const cohort = await this.findOne(id);
const beforeSnapshot = { ...cohort };
await this.cohortRepository.remove(cohort);
await this.auditLogService.log({
module: 'cohort',
action: 'DELETE',
entityId: id,
entityLabel: beforeSnapshot.name,
before: beforeSnapshot,
after: undefined,
performedBy: getTenantId(),
});
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { IsString, IsNotEmpty, IsOptional, IsArray } from 'class-validator';
export class CreateCohortDto {
@IsString()
@IsNotEmpty()
name: string;
name!: string;
@IsString()
@IsOptional()
@@ -0,0 +1,45 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { DashboardService } from './dashboard.service';
import { PermissionsGuard } from '../auth/guards/permissions.guard';
import { RequirePermissions } from '../auth/decorators/permissions.decorator';
@ApiTags('Dashboard')
@Controller('dashboard')
@UseGuards(PermissionsGuard)
export class DashboardController {
constructor(private readonly dashboardService: DashboardService) {}
@Get('metrics')
@RequirePermissions('dashboard:view')
@ApiOperation({ summary: 'Get dashboard summary metrics' })
@ApiResponse({ status: 200, description: 'Return 5 aggregated metric cards for dashboard' })
getMetrics() {
return this.dashboardService.getMetrics();
}
@Get('exposure')
@RequirePermissions('dashboard:view')
@ApiOperation({ summary: 'Get refund and compensation exposure data' })
@ApiResponse({ status: 200, description: 'Return exposure category metrics' })
getExposure() {
return this.dashboardService.getExposure();
}
@Get('disruption-mix')
@RequirePermissions('dashboard:view')
@ApiOperation({ summary: 'Get disruption mix breakdown' })
@ApiResponse({ status: 200, description: 'Return disruption mix category breakdown' })
getDisruptionMix() {
return this.dashboardService.getDisruptionMix();
}
@Get('recent-incidents')
@RequirePermissions('dashboard:view')
@ApiOperation({ summary: 'Get recent recovery incidents for dashboard' })
@ApiResponse({ status: 200, description: 'Return recent incident rows' })
getRecentIncidents() {
return this.dashboardService.getRecentIncidents();
}
}
+18
View File
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DashboardController } from './dashboard.controller';
import { DashboardService } from './dashboard.service';
import { RecoveryIncident } from '../recovery-incident/entities/recovery-incident.entity';
import { Policy } from '../policy-engine/entities/policy.entity';
import { Cohort } from '../cohort/cohort.entity';
import { AuditLog } from '../audit-log/entities/audit-log.entity';
@Module({
imports: [
TypeOrmModule.forFeature([RecoveryIncident, Policy, Cohort, AuditLog]),
],
controllers: [DashboardController],
providers: [DashboardService],
exports: [DashboardService],
})
export class DashboardModule {}
+244
View File
@@ -0,0 +1,244 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RecoveryIncident } from '../recovery-incident/entities/recovery-incident.entity';
import { Policy } from '../policy-engine/entities/policy.entity';
import { PolicyStatus } from '../policy-engine/entities/policy.enums';
import { Cohort } from '../cohort/cohort.entity';
import { AuditLog } from '../audit-log/entities/audit-log.entity';
import { getTenantId } from '../../common/tenant/tenant.context';
export interface DashboardMetricResponse {
id: string;
title: string;
value: string;
trend: string;
isPositive: boolean;
sparklineData: number[];
}
export interface ExposureChartItemResponse {
category: string;
amount: number;
displayAmount: string;
color: string;
views?: string;
date?: string;
hasTooltip?: boolean;
}
export interface DisruptionMixItemResponse {
label: string;
percentage: number;
value: string;
color: string;
}
export interface RecentIncidentResponse {
id: string;
recoveryId: string;
passengerName: string;
pnr: string;
flightNumber: string;
category: string;
status: string;
value: string;
}
@Injectable()
export class DashboardService {
constructor(
@InjectRepository(RecoveryIncident)
private readonly incidentRepo: Repository<RecoveryIncident>,
@InjectRepository(Policy)
private readonly policyRepo: Repository<Policy>,
@InjectRepository(Cohort)
private readonly cohortRepo: Repository<Cohort>,
@InjectRepository(AuditLog)
private readonly auditLogRepo: Repository<AuditLog>,
) {}
private parseAmount(valStr?: string): number {
if (!valStr) return 0;
const num = parseFloat(valStr.replace(/[^0-9.]/g, ''));
return isNaN(num) ? 0 : num;
}
private formatCurrency(amount: number): string {
if (amount >= 1000000) {
return `$${(amount / 1000000).toFixed(1)}m`;
}
if (amount >= 1000) {
return `$${Math.round(amount / 1000)}k`;
}
return `$${amount.toLocaleString()}`;
}
async getMetrics(): Promise<DashboardMetricResponse[]> {
const tenantId = getTenantId();
const [incidents, activePoliciesCount, auditLogsCount] = await Promise.all([
this.incidentRepo.find({ where: { tenantId } }),
this.policyRepo.count({ where: { tenantId, status: PolicyStatus.ACTIVE } }),
this.auditLogRepo.count({ where: { tenantId } }),
]);
let totalExposure = 0;
let baggageClaimsCount = 0;
let ancillaryRefundsCount = 0;
incidents.forEach((inc) => {
totalExposure += this.parseAmount(inc.value);
const cat = (inc.category || '').toLowerCase();
if (cat.includes('baggage')) {
baggageClaimsCount++;
} else if (cat.includes('ancillary')) {
ancillaryRefundsCount++;
}
});
const totalSimulationsCount = incidents.length + auditLogsCount;
return [
{
id: 'total-simulations',
title: 'Total Simulations',
value: totalSimulationsCount.toLocaleString(),
trend: '+18%',
isPositive: true,
sparklineData: [20, 24, 22, 28, 25, 32, 30, 38],
},
{
id: 'refund-exposure',
title: 'Refund Exposure',
value: this.formatCurrency(totalExposure),
trend: '+12%',
isPositive: true,
sparklineData: [15, 18, 16, 22, 24, 21, 29, 34],
},
{
id: 'active-policies',
title: 'Active Policies',
value: activePoliciesCount.toLocaleString(),
trend: 'Stable',
isPositive: true,
sparklineData: [25, 26, 25, 27, 26, 28, 27, 29],
},
{
id: 'baggage-claims',
title: 'Baggage Claims',
value: baggageClaimsCount.toLocaleString(),
trend: '+5%',
isPositive: true,
sparklineData: [10, 14, 18, 15, 22, 20, 25, 30],
},
{
id: 'ancillary-refunds',
title: 'Ancillary Refunds',
value: ancillaryRefundsCount.toLocaleString(),
trend: '+22%',
isPositive: true,
sparklineData: [12, 16, 15, 24, 20, 28, 32, 40],
},
];
}
async getExposure(): Promise<ExposureChartItemResponse[]> {
const tenantId = getTenantId();
const incidents = await this.incidentRepo.find({ where: { tenantId } });
const categories: Record<string, { amount: number; color: string }> = {
'Flight Ops': { amount: 0, color: '#146C43' },
'Baggage': { amount: 0, color: '#23865D' },
'Ancillary': { amount: 0, color: '#5F9E7F' },
'Downgrade': { amount: 0, color: '#7BAA92' },
};
incidents.forEach((inc) => {
const val = this.parseAmount(inc.value);
const cat = inc.category || 'Flight Ops';
if (categories[cat]) {
categories[cat].amount += val;
} else {
categories['Flight Ops'].amount += val;
}
});
const nowStr = new Date().toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
});
return Object.entries(categories).map(([catName, item]) => ({
category: catName,
amount: item.amount,
displayAmount: this.formatCurrency(item.amount),
color: item.color,
views: `${incidents.filter((i) => (i.category || 'Flight Ops') === catName).length} cases`,
date: nowStr,
hasTooltip: catName === 'Baggage',
}));
}
async getDisruptionMix(): Promise<DisruptionMixItemResponse[]> {
const tenantId = getTenantId();
const incidents = await this.incidentRepo.find({ where: { tenantId } });
const categories: Record<string, { count: number; valueSum: number; color: string }> = {
'Flight Ops': { count: 0, valueSum: 0, color: '#146C43' },
'Baggage': { count: 0, valueSum: 0, color: '#23865D' },
'Ancillary': { count: 0, valueSum: 0, color: '#5F9E7F' },
'Downgrade': { count: 0, valueSum: 0, color: '#7BAA92' },
};
let totalSum = 0;
incidents.forEach((inc) => {
const val = this.parseAmount(inc.value);
const cat = inc.category || 'Flight Ops';
totalSum += val;
if (categories[cat]) {
categories[cat].count += 1;
categories[cat].valueSum += val;
} else {
categories['Flight Ops'].count += 1;
categories['Flight Ops'].valueSum += val;
}
});
const totalCount = incidents.length || 1;
return Object.entries(categories).map(([label, item]) => {
const percentage = totalSum > 0
? Math.round((item.valueSum / totalSum) * 100)
: Math.round((item.count / totalCount) * 100);
return {
label,
percentage,
value: this.formatCurrency(item.valueSum),
color: item.color,
};
});
}
async getRecentIncidents(): Promise<RecentIncidentResponse[]> {
const tenantId = getTenantId();
const incidents = await this.incidentRepo.find({
where: { tenantId },
order: { createdAt: 'DESC' },
take: 10,
});
return incidents.map((inc) => ({
id: inc.id,
recoveryId: inc.recoveryCode || inc.id.substring(0, 16),
passengerName: inc.passengerName || 'N/A',
pnr: inc.pnr || '',
flightNumber: inc.flightNumber || 'N/A',
category: inc.category || 'General',
status: inc.status || 'Active',
value: inc.value || '$0',
}));
}
}
+2
View File
@@ -1,12 +1,14 @@
import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { HealthService } from './health.service';
import { Public } from '../auth/decorators/public.decorator';
@ApiTags('health')
@Controller('health')
export class HealthController {
constructor(private readonly healthService: HealthService) {}
@Public()
@Get()
getHealth(): any {
return this.healthService.getHealth();
@@ -0,0 +1,79 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsObject, IsOptional, IsNumber, IsBoolean, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
export class ActionSubmissionValueDto {
@ApiProperty({ description: 'Field Definition ID', required: false })
@IsOptional()
@IsString()
field_definition_id?: string;
@ApiProperty({ description: 'Field Code', required: false })
@IsOptional()
@IsString()
field_code?: string;
@ApiProperty({ description: 'Value index for array inputs', required: false })
@IsOptional()
@IsNumber()
value_index?: number;
@ApiProperty({ description: 'Selected value ID for dropdowns and lookups', required: false })
@IsOptional()
@IsString()
selected_value_id?: string;
@ApiProperty({ description: 'Text value', required: false })
@IsOptional()
@IsString()
text_value?: string;
@ApiProperty({ description: 'Numeric value', required: false })
@IsOptional()
@IsNumber()
number_value?: number;
@ApiProperty({ description: 'Boolean value', required: false })
@IsOptional()
@IsBoolean()
boolean_value?: boolean;
@ApiProperty({ description: 'Date string (YYYY-MM-DD)', required: false })
@IsOptional()
@IsString()
date_value?: string;
@ApiProperty({ description: 'Time string (HH:MM)', required: false })
@IsOptional()
@IsString()
time_value?: string;
@ApiProperty({ description: 'Timestamp ISO string', required: false })
@IsOptional()
@IsString()
timestamp_value?: string;
}
export class ActionSubmissionDto {
@ApiProperty({ description: 'Category ID', example: 'cat_comp' })
@IsString()
@IsNotEmpty()
category_id!: string;
@ApiProperty({ description: 'Action Type ID', example: 'at_cash_comp' })
@IsString()
@IsNotEmpty()
action_type_id!: string;
@ApiProperty({ description: 'List of dynamic submission values', required: false, type: [ActionSubmissionValueDto] })
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => ActionSubmissionValueDto)
values?: ActionSubmissionValueDto[];
@ApiProperty({ description: 'Form submitted dynamic data dictionary (fallback)', required: false })
@IsOptional()
@IsObject()
data?: Record<string, any>;
}
@@ -0,0 +1,29 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsNumber } from 'class-validator';
export class CreateActionCategoryDto {
@ApiPropertyOptional({ example: 'passenger-compensation', description: 'Unique action category code' })
@IsString()
@IsNotEmpty()
code: string;
@ApiPropertyOptional({ example: 'Passenger Compensation', description: 'Display name for the action category' })
@IsString()
@IsNotEmpty()
name: string;
@ApiPropertyOptional({ example: 'Actions related to passenger compensation', description: 'Optional description' })
@IsString()
@IsOptional()
description?: string;
@ApiPropertyOptional({ example: 1, description: 'Order used for display sorting' })
@IsNumber()
@IsOptional()
displayOrder?: number;
@ApiPropertyOptional({ example: true, description: 'Whether the record is active' })
@IsBoolean()
@IsOptional()
isActive?: boolean;
}
@@ -0,0 +1,34 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID, IsNumber } from 'class-validator';
export class CreateActionTypeDto {
@ApiPropertyOptional({ example: '00000000-0000-0000-0000-000000000000', description: 'Parent action category id' })
@IsUUID()
@IsNotEmpty()
categoryId: string;
@ApiPropertyOptional({ example: 'voucher-refund', description: 'Unique action type code' })
@IsString()
@IsNotEmpty()
code: string;
@ApiPropertyOptional({ example: 'Voucher Refund', description: 'Display name for the action type' })
@IsString()
@IsNotEmpty()
name: string;
@ApiPropertyOptional({ example: 'Issue a compensation voucher refund', description: 'Optional description' })
@IsString()
@IsOptional()
description?: string;
@ApiPropertyOptional({ example: 1, description: 'Order used for display sorting' })
@IsNumber()
@IsOptional()
displayOrder?: number;
@ApiPropertyOptional({ example: true, description: 'Whether the record is active' })
@IsBoolean()
@IsOptional()
isActive?: boolean;
}
@@ -0,0 +1,86 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsString,
IsNotEmpty,
IsOptional,
IsBoolean,
IsInt,
IsObject,
} from 'class-validator';
export class CreateFieldDefinitionDto {
@ApiProperty({ description: 'Action type ID', example: 'at_cash_comp' })
@IsString()
@IsNotEmpty()
actionTypeId!: string;
@ApiProperty({ description: 'Unique field code within action type', example: 'payout_amount' })
@IsString()
@IsNotEmpty()
fieldCode!: string;
@ApiProperty({ description: 'Human readable field label', example: 'Payout Amount' })
@IsString()
@IsNotEmpty()
fieldName!: string;
@ApiProperty({ description: 'Control field type (textbox, currency, dropdown, etc.)', example: 'currency' })
@IsString()
@IsNotEmpty()
fieldType!: string;
@ApiPropertyOptional({ description: 'Master data code for lookup options (e.g. CURRENCIES)', example: 'CURRENCIES' })
@IsString()
@IsOptional()
lookupSource?: string;
@ApiPropertyOptional({ description: 'Required flag', example: true, default: false })
@IsBoolean()
@IsOptional()
isRequired?: boolean;
@ApiPropertyOptional({ description: 'Default value string', example: '600' })
@IsString()
@IsOptional()
defaultValue?: string;
@ApiPropertyOptional({ description: 'Placeholder text', example: 'Enter amount' })
@IsString()
@IsOptional()
placeholder?: string;
@ApiPropertyOptional({ description: 'Help text / instructions', example: 'Allowed compensation amount' })
@IsString()
@IsOptional()
helpText?: string;
@ApiPropertyOptional({ description: 'Grid column width (full, half, third, two_thirds)', example: 'half', default: 'full' })
@IsString()
@IsOptional()
width?: string;
@ApiPropertyOptional({ description: 'Form section header title', example: 'Financial Details' })
@IsString()
@IsOptional()
section?: string;
@ApiPropertyOptional({ description: 'Sorting display order', example: 1, default: 0 })
@IsInt()
@IsOptional()
displayOrder?: number;
@ApiPropertyOptional({ description: 'Active status flag', example: true, default: true })
@IsBoolean()
@IsOptional()
isActive?: boolean;
@ApiPropertyOptional({ description: 'Validation rules JSON ({ min, max, regex })' })
@IsObject()
@IsOptional()
validationJson?: Record<string, any>;
@ApiPropertyOptional({ description: 'Visibility condition JSON ({ field, operator, value })' })
@IsObject()
@IsOptional()
visibilityConditionJson?: Record<string, any>;
}
@@ -0,0 +1,29 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsNumber } from 'class-validator';
export class CreateOperatorDto {
@ApiPropertyOptional({ example: 'EQ', description: 'Unique operator code' })
@IsString()
@IsNotEmpty()
code: string;
@ApiPropertyOptional({ example: 'Equals', description: 'Display name of the operator' })
@IsString()
@IsNotEmpty()
name: string;
@ApiPropertyOptional({ example: '=', description: 'Operator symbol' })
@IsString()
@IsOptional()
symbol?: string;
@ApiPropertyOptional({ example: 1, description: 'Order used for display sorting' })
@IsNumber()
@IsOptional()
displayOrder?: number;
@ApiPropertyOptional({ example: true, description: 'Whether the operator is active' })
@IsBoolean()
@IsOptional()
isActive?: boolean;
}
@@ -0,0 +1,29 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsNumber } from 'class-validator';
export class CreateRuleCategoryDto {
@ApiPropertyOptional({ example: 'fare-type', description: 'Unique rule category code' })
@IsString()
@IsNotEmpty()
code: string;
@ApiPropertyOptional({ example: 'Fare Type', description: 'Display name for the rule category' })
@IsString()
@IsNotEmpty()
name: string;
@ApiPropertyOptional({ example: 'Fare-related rule categories', description: 'Optional description' })
@IsString()
@IsOptional()
description?: string;
@ApiPropertyOptional({ example: 1, description: 'Order used for display sorting' })
@IsNumber()
@IsOptional()
displayOrder?: number;
@ApiPropertyOptional({ example: true, description: 'Whether the record is active' })
@IsBoolean()
@IsOptional()
isActive?: boolean;
}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateActionCategoryDto } from './create-action-category.dto';
export class UpdateActionCategoryDto extends PartialType(CreateActionCategoryDto) {}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateActionTypeDto } from './create-action-type.dto';
export class UpdateActionTypeDto extends PartialType(CreateActionTypeDto) {}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateFieldDefinitionDto } from './create-field-definition.dto';
export class UpdateFieldDefinitionDto extends PartialType(CreateFieldDefinitionDto) {}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateOperatorDto } from './create-operator.dto';
export class UpdateOperatorDto extends PartialType(CreateOperatorDto) {}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateRuleCategoryDto } from './create-rule-category.dto';
export class UpdateRuleCategoryDto extends PartialType(CreateRuleCategoryDto) {}
@@ -0,0 +1,41 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
Index,
} from 'typeorm';
import { ActionType } from './action-type.entity';
@Entity({ name: 'tbl_action_categories', schema: 'masters_action_builder' })
@Index('UQ_action_category_code', ['code'], { unique: true })
export class ActionCategory {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
code!: string;
@Column()
name!: string;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ default: 0 })
displayOrder!: number;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
@OneToMany(() => ActionType, (actionType) => actionType.category)
actionTypes?: ActionType[];
}
@@ -0,0 +1,68 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { ActionSubmission } from './action-submission.entity';
import { FieldDefinition } from './field-definition.entity';
@Entity({ name: 'tbl_action_submission_values', schema: 'masters_action_builder' })
export class ActionSubmissionValue {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ type: 'uuid' })
submissionId!: string;
@ManyToOne(() => ActionSubmission, (submission) => submission.values, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'submissionId' })
submission?: ActionSubmission;
@Column({ type: 'uuid', nullable: true })
fieldDefinitionId?: string;
@ManyToOne(() => FieldDefinition, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'fieldDefinitionId' })
fieldDefinition?: FieldDefinition;
@Column({ type: 'varchar', nullable: true })
fieldCode?: string;
@Column({ type: 'int', default: 0 })
valueIndex!: number;
@Column({ type: 'varchar', nullable: true })
selectedValueId?: string;
@Column({ type: 'text', nullable: true })
textValue?: string;
@Column({ type: 'numeric', nullable: true })
numberValue?: number;
@Column({ type: 'boolean', nullable: true })
booleanValue?: boolean;
@Column({ type: 'date', nullable: true })
dateValue?: string;
@Column({ type: 'time', nullable: true })
timeValue?: string;
@Column({ type: 'timestamptz', nullable: true })
timestampValue?: Date;
@Column({ type: 'varchar', length: 10, nullable: true })
currencyCode?: string;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,42 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany,
JoinColumn,
} from 'typeorm';
import { ActionCategory } from './action-category.entity';
import { ActionType } from './action-type.entity';
import { ActionSubmissionValue } from './action-submission-value.entity';
@Entity({ name: 'tbl_action_submissions', schema: 'masters_action_builder' })
export class ActionSubmission {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ type: 'uuid' })
categoryId!: string;
@ManyToOne(() => ActionCategory, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'categoryId' })
category?: ActionCategory;
@Column({ type: 'uuid' })
actionTypeId!: string;
@ManyToOne(() => ActionType, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'actionTypeId' })
actionType?: ActionType;
@OneToMany(() => ActionSubmissionValue, (val) => val.submission, { cascade: true })
values!: ActionSubmissionValue[];
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,52 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
OneToMany,
} from 'typeorm';
import { ActionCategory } from './action-category.entity';
import { FieldDefinition } from './field-definition.entity';
@Entity({ name: 'tbl_action_types', schema: 'masters_action_builder' })
export class ActionType {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ type: 'uuid' })
categoryId!: string;
@ManyToOne(() => ActionCategory, (category) => category.actionTypes, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'categoryId' })
category!: ActionCategory;
@Column()
code!: string;
@Column()
name!: string;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ type: 'varchar', nullable: true })
icon?: string;
@Column({ default: 0 })
displayOrder!: number;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
@OneToMany(() => FieldDefinition, (field) => field.actionType)
fieldDefinitions?: FieldDefinition[];
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_airline_preferences', schema: 'masters_lookup' })
export class AirlinePreference {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_airline_responsibilities', schema: 'masters_lookup' })
export class AirlineResponsibility {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_airport_restrictions', schema: 'masters_lookup' })
export class AirportRestriction {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_amount_types', schema: 'masters_lookup' })
export class AmountType {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -5,25 +5,24 @@ import {
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
@Entity({ name: 'tbl_ancillary_purchases', schema: 'masters' })
export class AncillaryPurchase extends TenantOwnedEntity {
@Entity({ name: 'tbl_ancillary_purchases', schema: 'masters_lookup' })
export class AncillaryPurchase {
@PrimaryGeneratedColumn('uuid')
id: string;
id!: string;
@Column()
label: string;
label!: string;
@Column()
value: string;
value!: string;
@Column({ default: true })
isActive: boolean;
isActive!: boolean;
@CreateDateColumn()
createdAt: Date;
createdAt!: Date;
@UpdateDateColumn()
updatedAt: Date;
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_applicable_products', schema: 'masters_lookup' })
export class ApplicableProduct {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_approval_levels', schema: 'masters_lookup' })
export class ApprovalLevel {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_atc_restrictions', schema: 'masters_lookup' })
export class AtcRestriction {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_baggage_types', schema: 'masters_lookup' })
export class BaggageType {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_booking_channels', schema: 'masters_lookup' })
export class BookingChannel {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -5,25 +5,24 @@ import {
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
@Entity({ name: 'tbl_cabin_classes', schema: 'masters' })
export class CabinClass extends TenantOwnedEntity {
@Entity({ name: 'tbl_cabin_classes', schema: 'masters_lookup' })
export class CabinClass {
@PrimaryGeneratedColumn('uuid')
id: string;
id!: string;
@Column()
label: string;
label!: string;
@Column()
value: string;
value!: string;
@Column({ default: true })
isActive: boolean;
isActive!: boolean;
@CreateDateColumn()
createdAt: Date;
createdAt!: Date;
@UpdateDateColumn()
updatedAt: Date;
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_cabin_preferences', schema: 'masters_lookup' })
export class CabinPreference {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_cancellation_reasons', schema: 'masters_lookup' })
export class CancellationReason {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_carrier_types', schema: 'masters_lookup' })
export class CarrierType {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_compensation_bases', schema: 'masters_lookup' })
export class CompensationBasis {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_compensation_eligibilities', schema: 'masters_lookup' })
export class CompensationEligibility {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_compensation_rules', schema: 'masters_lookup' })
export class CompensationRule {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_compensation_types', schema: 'masters_lookup' })
export class CompensationType {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,42 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { ConditionGroup } from './condition-group.entity';
@Entity({ name: 'tbl_condition_fields', schema: 'masters_rule_engine' })
export class ConditionField {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ name: 'groupId', type: 'uuid' })
groupId!: string;
@ManyToOne(() => ConditionGroup, (group) => group.fields, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'groupId' })
group!: ConditionGroup;
@Column({ unique: true })
code!: string;
@Column()
name!: string;
@Column({ name: 'lookupTable', type: 'varchar', nullable: true })
lookupTable?: string;
@Column({ name: 'dataType', type: 'varchar', default: 'STRING' })
dataType!: string;
@Column({ name: 'operatorType', type: 'varchar', default: 'COMPARISON' })
operatorType!: string;
@Column({ name: 'displayOrder', type: 'int', default: 0 })
displayOrder!: number;
@Column({ name: 'isActive', default: true })
isActive!: boolean;
@CreateDateColumn({ name: 'createdAt' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updatedAt' })
updatedAt!: Date;
}
@@ -0,0 +1,33 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
import { ConditionField } from './condition-field.entity';
import { RuleCategoryGroup } from './rule-category-group.entity';
@Entity({ name: 'tbl_condition_groups', schema: 'masters_rule_engine' })
export class ConditionGroup {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ unique: true })
code!: string;
@Column()
name!: string;
@Column({ name: 'displayOrder', type: 'int', default: 0 })
displayOrder!: number;
@Column({ name: 'isActive', default: true })
isActive!: boolean;
@CreateDateColumn({ name: 'createdAt' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updatedAt' })
updatedAt!: Date;
@OneToMany(() => ConditionField, (field) => field.group)
fields!: ConditionField[];
@OneToMany(() => RuleCategoryGroup, (rcg) => rcg.conditionGroup)
categoryMappings!: RuleCategoryGroup[];
}
@@ -0,0 +1,31 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_currencies', schema: 'masters_lookup' })
export class Currency {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ nullable: true })
symbol?: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -5,25 +5,24 @@ import {
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
@Entity({ name: 'tbl_customer_values', schema: 'masters' })
export class CustomerValue extends TenantOwnedEntity {
@Entity({ name: 'tbl_customer_values', schema: 'masters_lookup' })
export class CustomerValue {
@PrimaryGeneratedColumn('uuid')
id: string;
id!: string;
@Column()
label: string;
label!: string;
@Column()
value: string;
value!: string;
@Column({ default: true })
isActive: boolean;
isActive!: boolean;
@CreateDateColumn()
createdAt: Date;
createdAt!: Date;
@UpdateDateColumn()
updatedAt: Date;
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_delay_durations', schema: 'masters_lookup' })
export class DelayDuration {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_delay_reasons', schema: 'masters_lookup' })
export class DelayReason {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}
@@ -0,0 +1,28 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_destination_types', schema: 'masters_lookup' })
export class DestinationType {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column()
label!: string;
@Column()
value!: string;
@Column({ default: true })
isActive!: boolean;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
}

Some files were not shown because too many files have changed in this diff Show More