Compare commits
65
Commits
younus
...
development
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca446e42b8 | ||
|
|
4fcbc4b925 | ||
|
|
7cb8a7ea84 | ||
|
|
07845f2af5 | ||
|
|
bf3f987dce | ||
|
|
cf458536fb | ||
|
|
2dc4faa410 | ||
|
|
5d83099957 | ||
|
|
61d5edb7a2 | ||
|
|
b8510d9902 | ||
|
|
6c1a80587e | ||
|
|
5780b5cd47 | ||
|
|
1d96290618 | ||
|
|
b83ec24268 | ||
|
|
0a06a3856f | ||
|
|
46770d4b2b | ||
|
|
e7a9ee911e | ||
|
|
4ee9e36e2b | ||
|
|
5779501812 | ||
|
|
fd54fc6a0e | ||
|
|
8d11cf85ea | ||
|
|
9d386580c8 | ||
|
|
67756b88bd | ||
|
|
5e4e37c948 | ||
|
|
dfce163ca0 | ||
|
|
e9f5fd2e7b | ||
|
|
1f8c29d850 | ||
|
|
689be81442 | ||
|
|
ad90a5b082 | ||
|
|
7f529d8ae4 | ||
|
|
5f95a5c341 | ||
|
|
06a1ee518e | ||
|
|
e920c9e3b5 | ||
|
|
dbfce10983 | ||
|
|
a7733ad28f | ||
|
|
98df6f18b4 | ||
|
|
345d04407a | ||
|
|
e3edd7a6c4 | ||
|
|
ee7796e6d3 | ||
|
|
0d8a6228f4 | ||
|
|
03990c71fe | ||
|
|
7413167f79 | ||
|
|
2c0cad7093 | ||
|
|
86f435500a | ||
|
|
a8c8b11ba5 | ||
|
|
9f3b3ad4fd | ||
|
|
7afd58c178 | ||
|
|
1fe403cb11 | ||
|
|
e209541475 | ||
|
|
046d36d069 | ||
|
|
393963a982 | ||
|
|
5cd42c914f | ||
|
|
c90c96b8e5 | ||
|
|
9cf49bc05e | ||
|
|
894660b865 | ||
|
|
1d4b160660 | ||
|
|
a5770d1e0f | ||
|
|
2cdd921012 | ||
|
|
bef7aa98c1 | ||
|
|
dfd2ff8127 | ||
|
|
e8e6da31be | ||
|
|
de7527e1bd | ||
|
|
f3acfd2181 | ||
|
|
4fd1058dfe | ||
|
|
fd196f559b |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
Generated
+282
-10
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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...');
|
||||
|
||||
|
||||
@@ -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('*');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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') ||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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',
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user