31 Commits
Author SHA1 Message Date
maskantech 5cd42c914f Update README.md 2026-07-21 17:48:55 +00:00
maskantech 9cf49bc05e Update README.md 2026-07-21 17:44:52 +00:00
maskantech 1d4b160660 Update README.md 2026-07-21 17:34:07 +00:00
maskantech 2cdd921012 Update README.md 2026-07-21 17:25:55 +00:00
maskantech dfd2ff8127 Update README.md 2026-07-21 11:25:05 +00:00
Syed Waseem khadri Rafai de7527e1bd Merge pull request 'waseem' (#8) from waseem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/8
2026-07-21 05:54:34 +00:00
Syed Waseem f3acfd2181 feat(master-data): add jurisdiction and rule category entities with CRUD operations
- Introduced Jurisdiction and RuleCategory entities along with their respective values.
- Implemented service methods for managing rule categories and values.
- Updated MasterDataService to include seeding logic for jurisdictions and rule categories.
- Added DTOs for creating and updating rule categories and values.

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

- Created Policy, PolicyRule, PolicyAction, PolicyTargetAudience, and RuleCondition entities.
- Implemented create, update, and delete operations for policies and their associated rules and actions.
- Added enums for PolicyStatus and AudienceType to manage policy states and target audiences.
- Developed DTOs for creating and updating policies with nested structures for rules and audiences.
2026-07-21 11:22:54 +05:30
Syed Waseem 4fd1058dfe Merge branch 'development' of https://gitea.maskantech.in/gitea_admin/aeroresolve_backend into waseem 2026-07-18 11:13:00 +05:30
Syed Waseem khadri Rafai c5b360bf56 Merge pull request 'azeem' (#7) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/7
Reviewed-by: Syed Waseem khadri Rafai <waseem.khadri@maskatech.com>
2026-07-15 07:45:11 +00:00
azeeee05 4ba1316289 feat: initialize database module with multi-schema support and define master, tenant, and cohort entities 2026-07-15 13:06:03 +05:30
waseem khadri fd196f559b feat: initialize policy-engine module and integrate into AppModule 2026-07-15 09:04:42 +05:30
mytechpark 13fa6c2d46 v1 code db 2026-07-14 10:43:46 +05:30
Syed Waseem khadri Rafai 7473456317 Merge pull request 'azeem' (#6) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/6
Reviewed-by: Syed Waseem khadri Rafai <waseem.khadri@maskatech.com>
2026-07-10 05:39:02 +00:00
azeeee05 49b5fcb487 feat: implement Cohort entity and DTO with environment variable exclusion in gitignore 2026-07-10 10:54:02 +05:30
azeeee05 3137cb8038 feat: implement cohort module with entity schema and create DTO along with environment file management 2026-07-10 10:53:49 +05:30
azeeee05 6e7ff937dc feat: implement cohort module with entity, DTO, and CRUD service operations 2026-07-10 10:42:43 +05:30
Syed Waseem khadri Rafai e99f292e24 Merge pull request 'refactor: reorganize environment configurations by replacing global .env with environment-specific files and updating npm scripts accordingly' (#5) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/5
2026-07-09 09:29:11 +00:00
azeeee05 18454d5197 refactor: reorganize environment configurations by replacing global .env with environment-specific files and updating npm scripts accordingly 2026-07-09 14:39:05 +05:30
Syed Waseem khadri Rafai 4441fa2cce Merge pull request 'feat: implement MasterDataService for CRUD operations and database seeding, and update test script command' (#4) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/4
Reviewed-by: Syed Waseem khadri Rafai <waseem.khadri@maskatech.com>
2026-07-09 05:45:30 +00:00
azeeee05 c3936ca692 feat: implement MasterDataService for CRUD operations and database seeding, and update test script command 2026-07-09 11:14:12 +05:30
Syed Waseem khadri Rafai 1d0c41bf8c Merge pull request 'azeem' (#3) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/3
2026-07-08 10:54:22 +00:00
azeeee05 28837230fa feat: implement CRUD operations and pagination for cohort module 2026-07-08 12:49:25 +05:30
azeeee05 1e3ec441dc feat: implement master data service with seeding and introduce cohort module for filtering logic 2026-07-08 11:53:05 +05:30
azeeee05 06598ea462 feat: initialize master data entities for reference lookup tables 2026-07-08 11:22:06 +05:30
azeeee05 44afa45f7a feat: implement master data module with CRUD services and entity support for common lookup tables 2026-07-08 11:11:58 +05:30
azeeee05 9e64243759 Merge branch 'development' of https://gitea.maskantech.in/gitea_admin/aeroresolve_backend into azeem 2026-07-07 17:24:07 +05:30
azeeee05 8021937518 feat: implement master data and cohort management modules with CRUD operations and database seeding 2026-07-07 17:23:45 +05:30
Syed Waseem khadri Rafai 25302cc222 Merge pull request 'feat: update environment configuration and add support for multiple environments' (#2) from waseem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/2
2026-07-07 11:28:43 +00:00
waseem khadri 46e4a0ed81 feat: update environment configuration and add support for multiple environments 2026-07-07 16:57:36 +05:30
Syed Waseem khadri Rafai 82ea313f34 Merge pull request 'feat: integrate PostgreSQL with TypeORM and update app configuration' (#1) from waseem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/1
2026-07-07 10:04:33 +00:00
waseem khadri 025f114828 feat: integrate PostgreSQL with TypeORM and update app configuration
- Added @nestjs/typeorm and pg dependencies for PostgreSQL support.
- Configured TypeORM in app.module.ts to connect to PostgreSQL using environment variables.
- Enhanced main.ts to log database connection details during bootstrap.
- Removed unused disruption module, controller, service, and demo data.
2026-07-07 15:32:03 +05:30
66 changed files with 3754 additions and 330 deletions
+10
View File
@@ -0,0 +1,10 @@
DB_HOST=106.51.105.22
DB_PORT=5432
DB_NAME=ar_dev
DB_USER=ar_user
DB_PASSWORD="V$:Q1Hc6Qh]@#Fr"
DB_SYNCHRONIZE=true
DB_SSL=false
PORT=9501
CORS_ORIGIN=https://ardev.maskantech.in
NODE_ENV=dev
-4
View File
@@ -1,4 +0,0 @@
PORT=3001
DATABASE_URL=postgresql://username:password@localhost:5432/dbname
REDIS_URL=redis://localhost:6379
KAFKA_BROKERS=localhost:9092
+10
View File
@@ -0,0 +1,10 @@
DB_HOST=106.51.105.22
DB_PORT=5432
DB_NAME=ar_test
DB_USER=ar_user
DB_PASSWORD="V$:Q1Hc6Qh]@#Fr"
DB_SYNCHRONIZE=true
DB_SSL=false
PORT=9502
CORS_ORIGIN=https://artest.maskantech.in
NODE_ENV=test
+10
View File
@@ -0,0 +1,10 @@
DB_HOST=106.51.105.22
DB_PORT=5432
DB_NAME=ar_uat
DB_USER=ar_user
DB_PASSWORD="V$:Q1Hc6Qh]@#Fr"
DB_SYNCHRONIZE=true
DB_SSL=false
PORT=9501
CORS_ORIGIN=https://aruat.maskantech.in
NODE_ENV=uat
+5 -3
View File
@@ -34,8 +34,10 @@ lerna-debug.log*
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
# Local agent customization
.agent.md
# Environment variables # Environment variables
.env
.env.test
.env.production
.env.local .env.local
.env
+114 -20
View File
@@ -1,30 +1,124 @@
# Aero Resolve - Backend # AeroResolve Backend
test12345
NestJS REST API for AeroResolve.
This is the backend REST API powered by NestJS. ## Current Modules
## 🚀 Getting Started | 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 |
### Prerequisites ## Prerequisites
- Node.js (v18+)
### Installation - Node.js 18+
1. Navigate to the backend directory (if you aren't already here): - npm
```bash - PostgreSQL
cd backend
``` ## Install
2. Install the dependencies using npm:
```bash
npm install
```
### Development
Start the local development server in watch mode:
```bash ```bash
npm install
```
## Environment Files
The app loads environment files based on `NODE_ENV`:
```text
.env.local
.env.dev
.env.test
.env.uat
```
Required values:
```text
PORT=3001
CORS_ORIGIN=http://localhost:5174
DB_HOST=localhost
DB_PORT=5432
DB_USER=<postgres-user>
DB_PASSWORD=<postgres-password>
DB_NAME=<database-name>
DB_SYNCHRONIZE=true
DB_SSL=false
```
## Run
```bash
# Local
npm run local
npm run start:local
# Dev
npm run dev npm run dev
npm run start:dev
# UAT
npm run uat
npm run start:uat
``` ```
### Build for Production ## API
To compile the NestJS application:
```bash Base URL:
npm run build
```text
http://localhost:3001/api
``` ```
Swagger:
```text
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` |
| `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` |
## Seed Master Data
```bash
npm run seed:local
npm run seed:dev
npm run seed:test
npm run seed:uat
```
Seeded categories:
- `MEMBERSHIP_TIER`
- `CUSTOMER_VALUE`
- `REGION`
- `TRIP_PURPOSE`
- `CABIN_CLASS`
- `PASSENGER_TYPE`
- `ANCILLARY_PURCHASE`
- `REVENUE_SEGMENT`
## Planned Next
- Tenant foundation
- Auth / RBAC
- Audit logging
- Policy / regulation engine backend
- Compensation engine
+250
View File
@@ -0,0 +1,250 @@
-- =============================================================================
-- AeroResolve Database Schema
-- 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 cohort;
-- ─── Enum Types ─────────────────────────────────────────────────────────────
DO $$ BEGIN
CREATE TYPE cohort.cohort_high_value_passenger_enum AS ENUM (
'Any',
'Yes(VIP/Strategic)',
'No'
);
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
DO $$ BEGIN
CREATE TYPE cohort.cohort_flight_type_enum AS ENUM (
'Domestic Only',
'International Only',
'Both (All)'
);
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
-- ─── Tenants ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS tenant.tbl_tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug VARCHAR NOT NULL,
name VARCHAR NOT NULL,
tier VARCHAR NOT NULL DEFAULT 'standard',
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT uq_tbl_tenants_slug UNIQUE (slug)
);
-- ─── Master Data ────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS masters.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 (
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 (
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 (
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 (
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 (
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 (
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 (
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.tbl_rules_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenantId" UUID NOT NULL REFERENCES tenant.tbl_tenants(id) ON DELETE CASCADE,
code VARCHAR(100) UNIQUE NOT NULL,
name VARCHAR(150) 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 INDEX IF NOT EXISTS idx_tbl_rules_categories_tenant ON masters.tbl_rules_categories("tenantId");
CREATE TABLE IF NOT EXISTS masters.tbl_rule_categories_values (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenantId" UUID NOT NULL REFERENCES tenant.tbl_tenants(id) ON DELETE CASCADE,
"categoryId" UUID NOT NULL REFERENCES masters.tbl_rules_categories(id) ON DELETE CASCADE,
code VARCHAR(100),
value VARCHAR(255) NOT NULL,
"displayOrder" INT DEFAULT 0,
"isActive" BOOLEAN DEFAULT TRUE,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_tbl_rule_categories_values_tenant ON masters.tbl_rule_categories_values("tenantId");
CREATE INDEX IF NOT EXISTS idx_tbl_rule_categories_values_category ON masters.tbl_rule_categories_values("categoryId");
CREATE TABLE IF NOT EXISTS masters.tbl_operators (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenantId" UUID NOT NULL REFERENCES tenant.tbl_tenants(id) ON DELETE CASCADE,
code VARCHAR(50) UNIQUE NOT NULL,
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()
);
CREATE INDEX IF NOT EXISTS idx_tbl_operators_tenant ON masters.tbl_operators("tenantId");
-- ─── Cohorts ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS cohort.tbl_cohorts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"tenantId" UUID NOT NULL REFERENCES tenant.tbl_tenants(id) ON DELETE CASCADE,
"scopeType" VARCHAR,
name VARCHAR NOT NULL,
description VARCHAR,
status VARCHAR NOT NULL DEFAULT 'Draft',
"highValuePassenger" cohort.cohort_high_value_passenger_enum NOT NULL DEFAULT 'Any',
"flightType" cohort.cohort_flight_type_enum,
"originAirport" TEXT,
"destinationAirport" TEXT,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_tbl_cohorts_tenant ON cohort.tbl_cohorts("tenantId");
-- ─── Cohort Join Tables (Many-to-Many) ──────────────────────────────────────
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,
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,
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,
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,
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,
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,
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,
PRIMARY KEY ("cohortId", "tripPurposeId")
);
+548 -116
View File
File diff suppressed because it is too large Load Diff
+19 -9
View File
@@ -6,19 +6,25 @@
"private": true, "private": true,
"license": "UNLICENSED", "license": "UNLICENSED",
"scripts": { "scripts": {
"build": "nest build", "dev": "cross-env NODE_ENV=dev nest start",
"dev": "nest start --watch", "uat": "cross-env NODE_ENV=uat nest start",
"local": "cross-env NODE_ENV=local nest start",
"seed:dev": "cross-env NODE_ENV=dev ts-node -r tsconfig-paths/register scripts/seed-master-data.ts",
"seed:local": "cross-env NODE_ENV=local ts-node -r tsconfig-paths/register scripts/seed-master-data.ts",
"seed:uat": "cross-env NODE_ENV=uat ts-node -r tsconfig-paths/register scripts/seed-master-data.ts",
"seed:test": "cross-env NODE_ENV=test ts-node -r tsconfig-paths/register scripts/seed-master-data.ts",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start", "start": "nest start",
"start:dev": "nest start --watch", "start:dev": "cross-env NODE_ENV=dev nest start --watch",
"start:uat": "cross-env NODE_ENV=uat nest start --watch",
"start:local": "cross-env NODE_ENV=local nest start --watch",
"start:debug": "nest start --debug --watch", "start:debug": "nest start --debug --watch",
"start:prod": "node dist/main", "start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest", "test": "cross-env NODE_ENV=test nest start",
"test:watch": "jest --watch", "test:cov": "cross-env NODE_ENV=test jest --coverage",
"test:cov": "jest --coverage", "test:debug": "cross-env NODE_ENV=test node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "cross-env NODE_ENV=test jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
@@ -26,10 +32,13 @@
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.0", "@nestjs/swagger": "^11.2.0",
"@nestjs/typeorm": "^11.0.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"pg": "^8.22.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1" "rxjs": "^7.8.1",
"typeorm": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",
@@ -41,6 +50,7 @@
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^24.0.0", "@types/node": "^24.0.0",
"@types/supertest": "^7.0.0", "@types/supertest": "^7.0.0",
"cross-env": "^10.1.0",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2", "eslint-plugin-prettier": "^5.2.2",
+33
View File
@@ -0,0 +1,33 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from '../src/app.module';
import { MasterDataService } from '../src/modules/master-data/master-data.service';
import { TenantService } from '../src/modules/tenant/tenant.service';
import { Logger } from '@nestjs/common';
async function bootstrap() {
const logger = new Logger('SeedMasterData');
logger.log('Starting seed script (tenants + master data)...');
const app = await NestFactory.createApplicationContext(AppModule);
const tenantService = app.get(TenantService);
const masterDataService = app.get(MasterDataService);
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('Seeding complete. Exiting...');
await app.close();
process.exit(0);
}
bootstrap().catch((err) => {
console.error('Failed to run seed script:', err);
process.exit(1);
});
+24 -5
View File
@@ -1,13 +1,32 @@
import { Module } from '@nestjs/common'; import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module';
import { HealthModule } from './modules/health/health.module'; import { HealthModule } from './modules/health/health.module';
import { DisruptionModule } from './modules/disruption/disruption.module'; 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 { PolicyEngineModule } from './modules/policy-engine/policy-engine.module';
const env = process.env.NODE_ENV;
const envFilePath = env ? [`.env.${env}`, '.env.local', '.env'] : ['.env.local', '.env'];
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ isGlobal: true }), ConfigModule.forRoot({
isGlobal: true,
envFilePath,
}),
DatabaseModule,
HealthModule, HealthModule,
DisruptionModule, TenantModule,
MasterDataModule,
CohortModule,
PolicyEngineModule,
], ],
}) })
export class AppModule {} export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(TenantMiddleware).forRoutes('*');
}
}
@@ -0,0 +1,12 @@
import { Column, Index, JoinColumn, ManyToOne } from 'typeorm';
import { Tenant } from '../../modules/tenant/tenant.entity';
export abstract class TenantOwnedEntity {
@Column({ type: 'uuid' })
@Index()
tenantId: string;
@ManyToOne(() => Tenant, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'tenantId' })
tenant: Tenant;
}
+27
View File
@@ -0,0 +1,27 @@
import { AsyncLocalStorage } from 'async_hooks';
export interface TenantContextStore {
tenantId: string;
tenantSlug: string;
}
export const tenantContext = new AsyncLocalStorage<TenantContextStore>();
export function getTenantId(): string {
const tenantId = tenantContext.getStore()?.tenantId;
if (!tenantId) {
throw new Error('Tenant context is not set');
}
return tenantId;
}
export function getTenantIdOrNull(): string | null {
return tenantContext.getStore()?.tenantId ?? null;
}
export function runWithTenantContext<T>(
store: TenantContextStore,
fn: () => T,
): T {
return tenantContext.run(store, fn);
}
+50
View File
@@ -0,0 +1,50 @@
import {
BadRequestException,
Injectable,
NestMiddleware,
NotFoundException,
} from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
import { TenantService } from '../../modules/tenant/tenant.service';
import { tenantContext } from './tenant.context';
@Injectable()
export class TenantMiddleware implements NestMiddleware {
constructor(private readonly tenantService: TenantService) {}
async use(req: Request, res: Response, next: NextFunction) {
if (this.isPublicRoute(req.path)) {
return next();
}
let tenantHeader = req.headers['x-tenant-id'];
if (!tenantHeader || Array.isArray(tenantHeader)) {
tenantHeader = 'demo-airline';
}
try {
const tenant = await this.tenantService.resolveTenant(String(tenantHeader));
return tenantContext.run(
{ tenantId: tenant.id, tenantSlug: tenant.slug },
() => next(),
);
} catch (error) {
if (error instanceof NotFoundException) {
throw error;
}
throw new BadRequestException('Invalid X-Tenant-Id header');
}
}
private isPublicRoute(path: string): boolean {
return (
path === '/api/health' ||
path === '/health' ||
path.startsWith('/api/tenants') ||
path.startsWith('/tenants') ||
path.startsWith('/docs') ||
path.startsWith('/api-json')
);
}
}
+48
View File
@@ -0,0 +1,48 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get<string>('DB_HOST'),
port: Number(configService.get<string>('DB_PORT') ?? 5432),
username: configService.get<string>('DB_USER'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_NAME'),
synchronize: configService.get<string>('DB_SYNCHRONIZE') === 'true',
autoLoadEntities: true,
uuidExtension: 'pgcrypto',
ssl: configService.get<string>('DB_SSL') === 'true' ? { rejectUnauthorized: false } : false,
logging: false,
}),
dataSourceFactory: async (options) => {
if (!options) {
throw new Error('Invalid options passed');
}
const { DataSource } = await import('typeorm');
// Initialize without syncing first
const dataSource = new DataSource({ ...options, synchronize: false });
await dataSource.initialize();
// Create schemas if they don't exist
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 "cohort";`);
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "policy_engine";`);
// Run synchronization manually if it was enabled
if (options.synchronize) {
await dataSource.synchronize();
}
return dataSource;
},
}),
],
})
export class DatabaseModule {}
+27 -2
View File
@@ -1,14 +1,25 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { Logger } from '@nestjs/common';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule, {
logger: ['log', 'warn', 'error'],
});
const configService = app.get(ConfigService);
const dbHost = configService.get('DB_HOST');
const dbPort = configService.get('DB_PORT') ?? '5432';
const corsOrigin = configService.get<string>('CORS_ORIGIN') ?? 'http://localhost:5174';
const corsOrigins = corsOrigin.split(',').map(origin => origin.trim()).filter(Boolean);
app.enableCors({ app.enableCors({
origin: ['http://localhost:5174'], origin: corsOrigins.length > 1 ? corsOrigins : corsOrigins[0],
credentials: true, credentials: true,
allowedHeaders: ['Content-Type', 'Authorization', 'X-Tenant-Id'],
}); });
app.useGlobalPipes( app.useGlobalPipes(
@@ -18,6 +29,10 @@ async function bootstrap() {
}), }),
); );
app.setGlobalPrefix('api');
const logger = new Logger('Bootstrap');
const swaggerConfig = new DocumentBuilder() const swaggerConfig = new DocumentBuilder()
.setTitle('AeroResolve API') .setTitle('AeroResolve API')
.setDescription('Passenger recovery and compensation intelligence platform') .setDescription('Passenger recovery and compensation intelligence platform')
@@ -28,7 +43,17 @@ async function bootstrap() {
SwaggerModule.setup('docs', app, document); SwaggerModule.setup('docs', app, document);
const port = process.env.PORT ?? 3001; const port = process.env.PORT ?? 3001;
const environment = process.env.NODE_ENV ?? 'dev';
logger.log(`🌍 Environment: ${environment}`);
await app.listen(port); await app.listen(port);
if (process.env.NODE_ENV !== 'production') {
logger.log('🚀 AeroResolve API started successfully');
logger.log(`🌐 Server : http://localhost:${port}`);
logger.log(`📚 Swagger : http://localhost:${port}/docs`);
logger.log(`🗄️ Database : PostgreSQL (${dbHost}:${dbPort})`);
}
} }
bootstrap(); bootstrap();
+42
View File
@@ -0,0 +1,42 @@
import { Controller, Get, Post, Body, Param, Put, Delete, Patch, Query } from '@nestjs/common';
import { CohortService } from './cohort.service';
import { CreateCohortDto } from './dto/create-cohort.dto';
import { UpdateCohortDto } from './dto/update-cohort.dto';
@Controller('cohorts')
export class CohortController {
constructor(private readonly cohortService: CohortService) {}
@Post()
async create(@Body() createCohortDto: CreateCohortDto) {
return this.cohortService.create(createCohortDto);
}
@Get()
async findAll(
@Query('page') page: string = '1',
@Query('limit') limit: string = '10',
) {
return this.cohortService.findAll(Number(page), Number(limit));
}
@Get(':id')
async findOne(@Param('id') id: string) {
return this.cohortService.findOne(id);
}
@Put(':id')
async update(@Param('id') id: string, @Body() updateCohortDto: UpdateCohortDto) {
return this.cohortService.update(id, updateCohortDto);
}
@Patch(':id/status')
async updateStatus(@Param('id') id: string, @Body('status') status: string) {
return this.cohortService.updateStatus(id, status);
}
@Delete(':id')
async remove(@Param('id') id: string) {
return this.cohortService.remove(id);
}
}
+111
View File
@@ -0,0 +1,111 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToMany, JoinTable } from 'typeorm';
import { TenantOwnedEntity } from '../../common/entities/tenant-owned.entity';
import { CabinClass } from '../master-data/entities/cabin-class.entity';
import { PassengerType } from '../master-data/entities/passenger-type.entity';
import { AncillaryPurchase } from '../master-data/entities/ancillary-purchase.entity';
import { MembershipTier } from '../master-data/entities/membership-tier.entity';
import { RevenueSegment } from '../master-data/entities/revenue-segment.entity';
import { Region } from '../master-data/entities/region.entity';
import { TripPurpose } from '../master-data/entities/trip-purpose.entity';
@Entity({ name: 'tbl_cohorts', schema: 'cohort' })
export class Cohort extends TenantOwnedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: true })
scopeType: string;
@Column()
name: string;
@Column({ nullable: true })
description: string;
@Column({ default: 'Draft' })
status: string; // Draft, Active, Inactive
// Passenger Attributes
@ManyToMany(() => CabinClass)
@JoinTable({
name: 'tbl_cohort_cabin_classes',
joinColumn: { name: 'cohortId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'cabinClassId', referencedColumnName: 'id' }
})
cabinClasses: CabinClass[];
@ManyToMany(() => PassengerType)
@JoinTable({
name: 'tbl_cohort_passenger_types',
joinColumn: { name: 'cohortId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'passengerTypeId', referencedColumnName: 'id' }
})
passengerTypes: PassengerType[];
@ManyToMany(() => AncillaryPurchase)
@JoinTable({
name: 'tbl_cohort_ancillary_purchases',
joinColumn: { name: 'cohortId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'ancillaryPurchaseId', referencedColumnName: 'id' }
})
ancillaryPurchases: AncillaryPurchase[];
// Customer Value
@ManyToMany(() => MembershipTier)
@JoinTable({
name: 'tbl_cohort_loyalty_tiers',
joinColumn: { name: 'cohortId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'membershipTierId', referencedColumnName: 'id' }
})
loyaltyTiers: MembershipTier[];
@ManyToMany(() => RevenueSegment)
@JoinTable({
name: 'tbl_cohort_revenue_segments',
joinColumn: { name: 'cohortId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'revenueSegmentId', referencedColumnName: 'id' }
})
revenueSegments: RevenueSegment[];
@Column({
type: 'enum',
enum: ['Any', 'Yes(VIP/Strategic)', 'No'],
default: 'Any'
})
highValuePassenger: string;
@Column({
type: 'enum',
enum: ['Domestic Only', 'International Only', 'Both (All)'],
nullable: true
})
flightType: string;
@ManyToMany(() => Region)
@JoinTable({
name: 'tbl_cohort_regions',
joinColumn: { name: 'cohortId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'regionId', referencedColumnName: 'id' }
})
regions: Region[];
@ManyToMany(() => TripPurpose)
@JoinTable({
name: 'tbl_cohort_trip_purposes',
joinColumn: { name: 'cohortId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'tripPurposeId', referencedColumnName: 'id' }
})
tripPurposes: TripPurpose[];
@Column('simple-array', { nullable: true })
originAirport: string[];
@Column('simple-array', { nullable: true })
destinationAirport: string[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
+13
View File
@@ -0,0 +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';
@Module({
imports: [TypeOrmModule.forFeature([Cohort])],
controllers: [CohortController],
providers: [CohortService],
exports: [CohortService],
})
export class CohortModule {}
+114
View File
@@ -0,0 +1,114 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DeepPartial } from 'typeorm';
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';
@Injectable()
export class CohortService {
constructor(
@InjectRepository(Cohort)
private readonly cohortRepository: Repository<Cohort>,
) {}
private mapDtoToEntity(dto: CreateCohortDto | UpdateCohortDto) {
const {
cabinClassIds,
passengerTypeIds,
ancillaryPurchaseIds,
loyaltyTierIds,
revenueSegmentIds,
regionIds,
tripPurposeIds,
originAirports,
destinationAirports,
...rest
} = dto;
const mapping: any = { ...rest };
if (cabinClassIds) mapping.cabinClasses = cabinClassIds.map(id => ({ id }));
if (passengerTypeIds) mapping.passengerTypes = passengerTypeIds.map(id => ({ id }));
if (ancillaryPurchaseIds) mapping.ancillaryPurchases = ancillaryPurchaseIds.map(id => ({ id }));
if (loyaltyTierIds) mapping.loyaltyTiers = loyaltyTierIds.map(id => ({ id }));
if (revenueSegmentIds) mapping.revenueSegments = revenueSegmentIds.map(id => ({ id }));
if (regionIds) mapping.regions = regionIds.map(id => ({ id }));
if (tripPurposeIds) mapping.tripPurposes = tripPurposeIds.map(id => ({ id }));
if (originAirports !== undefined) mapping.originAirport = originAirports;
if (destinationAirports !== undefined) mapping.destinationAirport = destinationAirports;
return mapping as DeepPartial<Cohort>;
}
private getRelations() {
return {
cabinClasses: true,
passengerTypes: true,
ancillaryPurchases: true,
loyaltyTiers: true,
revenueSegments: true,
regions: true,
tripPurposes: true
};
}
async create(createCohortDto: CreateCohortDto): Promise<Cohort> {
const mappedData = this.mapDtoToEntity(createCohortDto);
const cohort = this.cohortRepository.create({
...mappedData,
tenantId: getTenantId(),
});
return this.cohortRepository.save(cohort);
}
async findAll(page: number = 1, limit: number = 10) {
const skip = (page - 1) * limit;
const tenantId = getTenantId();
const [data, total] = await this.cohortRepository.findAndCount({
where: { tenantId },
skip,
take: limit,
order: { createdAt: 'DESC' },
relations: this.getRelations(),
});
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
async findOne(id: string): Promise<Cohort> {
const cohort = await this.cohortRepository.findOne({
where: { id, tenantId: getTenantId() },
relations: this.getRelations(),
});
if (!cohort) {
throw new NotFoundException(`Cohort with ID ${id} not found`);
}
return cohort;
}
async update(id: string, updateCohortDto: UpdateCohortDto): Promise<Cohort> {
const cohort = await this.findOne(id);
const mappedData = this.mapDtoToEntity(updateCohortDto);
Object.assign(cohort, mappedData);
return this.cohortRepository.save(cohort);
}
async updateStatus(id: string, status: string): Promise<Cohort> {
const cohort = await this.findOne(id);
cohort.status = status;
return this.cohortRepository.save(cohort);
}
async remove(id: string): Promise<void> {
const cohort = await this.findOne(id);
await this.cohortRepository.remove(cohort);
}
}
@@ -0,0 +1,72 @@
import { IsString, IsNotEmpty, IsOptional, IsArray } from 'class-validator';
export class CreateCohortDto {
@IsString()
@IsNotEmpty()
name: string;
@IsString()
@IsOptional()
scopeType?: string;
@IsString()
@IsOptional()
description?: string;
@IsString()
@IsOptional()
status?: string;
@IsArray()
@IsString({ each: true })
@IsOptional()
cabinClassIds?: string[];
@IsArray()
@IsString({ each: true })
@IsOptional()
passengerTypeIds?: string[];
@IsArray()
@IsString({ each: true })
@IsOptional()
ancillaryPurchaseIds?: string[];
@IsArray()
@IsString({ each: true })
@IsOptional()
loyaltyTierIds?: string[];
@IsArray()
@IsString({ each: true })
@IsOptional()
revenueSegmentIds?: string[];
@IsString()
@IsOptional()
highValuePassenger?: string;
@IsString()
@IsOptional()
flightType?: string;
@IsArray()
@IsString({ each: true })
@IsOptional()
regionIds?: string[];
@IsArray()
@IsString({ each: true })
@IsOptional()
tripPurposeIds?: string[];
@IsArray()
@IsString({ each: true })
@IsOptional()
originAirports?: string[];
@IsArray()
@IsString({ each: true })
@IsOptional()
destinationAirports?: string[];
}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateCohortDto } from './create-cohort.dto';
export class UpdateCohortDto extends PartialType(CreateCohortDto) {}
@@ -1,72 +0,0 @@
const DEMO_DISRUPTION_ID = 'disruption-ek542';
const baseDisruption: any = {
id: DEMO_DISRUPTION_ID,
tenantId: 'tenant-demo',
flightNumber: 'EK542',
route: { origin: 'DXB', destination: 'CDG' },
scheduledDeparture: '2026-07-02T08:00:00.000Z',
estimatedDeparture: '2026-07-02T12:00:00.000Z',
delayMinutes: 240,
type: 'DELAY',
status: 'AWAITING_APPROVAL',
reason: 'Technical Issue',
aircraftType: 'A380-800',
airlineResponsible: true,
createdAt: '2026-07-02T06:15:00.000Z',
updatedAt: '2026-07-02T06:20:00.000Z',
};
const impactSummary: any = {
totalPassengers: 150,
compensationEligible: 92,
connectionsAtRisk: 25,
estimatedExposure: 36800,
currency: 'EUR',
jurisdiction: 'EU261',
};
const passengerSegments: any[] = [
{ label: 'Premium / Business', count: 18, percentage: 12 },
{ label: 'Frequent Flyers', count: 35, percentage: 23 },
{ label: 'Families', count: 20, percentage: 13 },
{ label: 'Connecting Pax', count: 25, percentage: 17 },
{ label: 'Economy', count: 52, percentage: 35 },
];
const recoveryPlan: any = {
id: 'recovery-ek542',
disruptionId: DEMO_DISRUPTION_ID,
status: 'PENDING_APPROVAL',
actions: [
{ type: 'HOTEL', description: 'Hotel allocation', quantity: 70, estimatedCost: 14000 },
{ type: 'MEAL_VOUCHER', description: 'Meal vouchers', quantity: 150, estimatedCost: 4500 },
{ type: 'REBOOKING', description: 'Next available rebooking', quantity: 25, estimatedCost: 0 },
{ type: 'LOUNGE', description: 'Lounge access', quantity: 18, estimatedCost: 1800 },
{ type: 'GROUND_TRANSPORT', description: 'Ground transport', quantity: 20, estimatedCost: 2200 },
],
estimatedRecoveryCost: 22500,
estimatedExposure: 36800,
potentialSavings: 14300,
currency: 'EUR',
};
export const demoDisruptionDetail: any = {
disruption: baseDisruption,
impact: impactSummary,
segments: passengerSegments,
recoveryPlan,
};
export function cloneDetail(
overrides: Partial<any> = {},
): any {
return {
disruption: { ...demoDisruptionDetail.disruption, ...overrides.disruption },
impact: { ...demoDisruptionDetail.impact, ...overrides.impact },
segments: overrides.segments ?? demoDisruptionDetail.segments,
recoveryPlan: overrides.recoveryPlan
? { ...demoDisruptionDetail.recoveryPlan!, ...overrides.recoveryPlan }
: demoDisruptionDetail.recoveryPlan,
};
}
@@ -1,33 +0,0 @@
import { Controller, Get, Param, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { DisruptionService } from './disruption.service';
@ApiTags('disruptions')
@Controller('disruptions')
export class DisruptionController {
constructor(private readonly disruptionService: DisruptionService) {}
@Get()
@ApiOperation({ summary: 'List active disruptions' })
findAll(): any[] {
return this.disruptionService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get disruption detail with impact and recovery plan' })
findOne(@Param('id') id: string): any {
return this.disruptionService.findOne(id);
}
@Post(':id/approve')
@ApiOperation({ summary: 'Approve recovery plan' })
approve(@Param('id') id: string): any {
return this.disruptionService.approve(id);
}
@Post(':id/reject')
@ApiOperation({ summary: 'Reject recovery plan' })
reject(@Param('id') id: string): any {
return this.disruptionService.reject(id);
}
}
@@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { DisruptionController } from './disruption.controller';
import { DisruptionService } from './disruption.service';
@Module({
controllers: [DisruptionController],
providers: [DisruptionService],
exports: [DisruptionService],
})
export class DisruptionModule {}
@@ -1,55 +0,0 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { cloneDetail, demoDisruptionDetail } from './data/demo-disruption';
@Injectable()
export class DisruptionService {
private readonly store = new Map<string, any>([
[demoDisruptionDetail.disruption.id, cloneDetail()],
]);
findAll(): any[] {
return Array.from(this.store.values()).map((item) => item.disruption);
}
findOne(id: string): any {
const detail = this.store.get(id);
if (!detail) {
throw new NotFoundException(`Disruption ${id} not found`);
}
return cloneDetail(detail);
}
approve(id: string): any {
const current = this.findOne(id);
const updated = cloneDetail({
disruption: {
...current.disruption,
status: 'COMPLETED',
updatedAt: new Date().toISOString(),
},
recoveryPlan: {
...current.recoveryPlan!,
status: 'COMPLETED',
},
});
this.store.set(id, updated);
return cloneDetail(updated);
}
reject(id: string): any {
const current = this.findOne(id);
const updated = cloneDetail({
disruption: {
...current.disruption,
status: 'REJECTED',
updatedAt: new Date().toISOString(),
},
recoveryPlan: {
...current.recoveryPlan!,
status: 'REJECTED',
},
});
this.store.set(id, updated);
return cloneDetail(updated);
}
}
@@ -0,0 +1,15 @@
import { IsString, IsNotEmpty, IsBoolean, IsOptional, IsNumber } from 'class-validator';
export class CreateMasterDataDto {
@IsString()
@IsNotEmpty()
label: string;
@IsString()
@IsNotEmpty()
value: string;
@IsBoolean()
@IsOptional()
isActive?: boolean;
}
@@ -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, IsUUID, IsNumber } from 'class-validator';
export class CreateRuleCategoryValueDto {
@ApiPropertyOptional({ example: '00000000-0000-0000-0000-000000000000', description: 'Parent rule category id' })
@IsUUID()
@IsNotEmpty()
categoryId: string;
@ApiPropertyOptional({ example: 'economy', description: 'Optional code for the value' })
@IsString()
@IsOptional()
code?: string;
@ApiPropertyOptional({ example: 'Economy', description: 'Display value' })
@IsString()
@IsNotEmpty()
value: 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,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 { CreateMasterDataDto } from './create-master-data.dto';
export class UpdateMasterDataDto extends PartialType(CreateMasterDataDto) {}
@@ -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 { CreateRuleCategoryValueDto } from './create-rule-category-value.dto';
export class UpdateRuleCategoryValueDto extends PartialType(CreateRuleCategoryValueDto) {}
@@ -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,29 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
@Entity({ name: 'tbl_ancillary_purchases', schema: 'masters' })
export class AncillaryPurchase extends TenantOwnedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
label: string;
@Column()
value: string;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
@@ -0,0 +1,29 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
@Entity({ name: 'tbl_cabin_classes', schema: 'masters' })
export class CabinClass extends TenantOwnedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
label: string;
@Column()
value: string;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
@@ -0,0 +1,29 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
@Entity({ name: 'tbl_customer_values', schema: 'masters' })
export class CustomerValue extends TenantOwnedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
label: string;
@Column()
value: string;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
@@ -0,0 +1,29 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
@Entity({ name: 'tbl_jurisdictions', schema: 'masters' })
export class Jurisdiction extends TenantOwnedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
label: string;
@Column()
value: string;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
@@ -0,0 +1,29 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
@Entity({ name: 'tbl_membership_tiers', schema: 'masters' })
export class MembershipTier extends TenantOwnedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
label: string;
@Column()
value: string;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
@@ -0,0 +1,35 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
@Entity({ name: 'tbl_operators', schema: 'masters' })
export class Operator extends TenantOwnedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
code: string;
@Column()
name: string;
@Column({ nullable: true })
symbol?: string;
@Column({ default: true })
isActive: boolean;
@Column({ default: 0 })
displayOrder: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
@@ -0,0 +1,29 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
@Entity({ name: 'tbl_passenger_types', schema: 'masters' })
export class PassengerType extends TenantOwnedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
label: string;
@Column()
value: string;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
@@ -0,0 +1,29 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
@Entity({ name: 'tbl_regions', schema: 'masters' })
export class Region extends TenantOwnedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
label: string;
@Column()
value: string;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
@@ -0,0 +1,29 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
@Entity({ name: 'tbl_revenue_segments', schema: 'masters' })
export class RevenueSegment extends TenantOwnedEntity {
@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 { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
import { RuleCategory } from './rule-category.entity';
@Entity({ name: 'tbl_rule_categories_values', schema: 'masters' })
export class RuleCategoryValue extends TenantOwnedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
categoryId: string;
@ManyToOne(() => RuleCategory, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'categoryId' })
category: RuleCategory;
@Column({ nullable: true })
code?: string;
@Column()
value: string;
@Column({ default: 0 })
displayOrder: number;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
@@ -0,0 +1,35 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
@Entity({ name: 'tbl_rules_categories', schema: 'masters' })
export class RuleCategory extends TenantOwnedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
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;
}
@@ -0,0 +1,29 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
@Entity({ name: 'tbl_trip_purposes', schema: 'masters' })
export class TripPurpose extends TenantOwnedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
label: string;
@Column()
value: string;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
@@ -0,0 +1,289 @@
import { Controller, Get, Post, Put, Delete, Param, Body } from '@nestjs/common';
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { MasterDataService } from './master-data.service';
import { CreateMasterDataDto } from './dto/create-master-data.dto';
import { UpdateMasterDataDto } from './dto/update-master-data.dto';
import { CreateRuleCategoryDto } from './dto/create-rule-category.dto';
import { UpdateRuleCategoryDto } from './dto/update-rule-category.dto';
import { CreateRuleCategoryValueDto } from './dto/create-rule-category-value.dto';
import { UpdateRuleCategoryValueDto } from './dto/update-rule-category-value.dto';
import { CreateOperatorDto } from './dto/create-operator.dto';
import { UpdateOperatorDto } from './dto/update-operator.dto';
@ApiTags('master-data')
@Controller('master-data')
export class MasterDataController {
constructor(private readonly masterDataService: MasterDataService) {}
@Get('rule-categories')
@ApiOperation({ summary: 'Get all rule categories' })
@ApiResponse({ status: 200, description: 'List of rule categories' })
async findAllRuleCategories() {
return this.masterDataService.findAllRuleCategories();
}
@Get('rule-categories/:id')
@ApiOperation({ summary: 'Get one rule category' })
@ApiParam({ name: 'id', type: String, description: 'Rule category id' })
@ApiResponse({ status: 200, description: 'Rule category found' })
async findOneRuleCategory(@Param('id') id: string) {
return this.masterDataService.findOneRuleCategory(id);
}
@Post('rule-categories')
@ApiOperation({ summary: 'Create a rule category' })
@ApiBody({
type: CreateRuleCategoryDto,
description: 'Rule category payload',
examples: {
default: {
value: {
code: 'fare-type',
name: 'Fare Type',
description: 'Fare-related rule categories',
displayOrder: 1,
isActive: true,
},
},
},
})
@ApiResponse({ status: 201, description: 'Rule category created' })
async createRuleCategory(@Body() createDto: CreateRuleCategoryDto) {
return this.masterDataService.createRuleCategory(createDto);
}
@Put('rule-categories/:id')
@ApiOperation({ summary: 'Update a rule category' })
@ApiParam({ name: 'id', type: String, description: 'Rule category id' })
@ApiBody({
type: UpdateRuleCategoryDto,
description: 'Rule category update payload',
examples: {
default: {
value: {
name: 'Fare Type',
description: 'Updated fare rule category',
displayOrder: 2,
isActive: true,
},
},
},
})
@ApiResponse({ status: 200, description: 'Rule category updated' })
async updateRuleCategory(
@Param('id') id: string,
@Body() updateDto: UpdateRuleCategoryDto,
) {
return this.masterDataService.updateRuleCategory(id, updateDto);
}
@Delete('rule-categories/:id')
@ApiOperation({ summary: 'Delete a rule category' })
@ApiParam({ name: 'id', type: String, description: 'Rule category id' })
@ApiResponse({ status: 200, description: 'Rule category deleted' })
async removeRuleCategory(@Param('id') id: string) {
return this.masterDataService.removeRuleCategory(id);
}
@Get('rule-category-values')
@ApiOperation({ summary: 'Get all rule category values' })
@ApiResponse({ status: 200, description: 'List of rule category values' })
async findAllRuleCategoryValues() {
return this.masterDataService.findAllRuleCategoryValues();
}
@Get('rule-category-values/:code')
@ApiOperation({ summary: 'Get rule category values by category code' })
@ApiParam({ name: 'code', type: String, description: 'Rule category code' })
@ApiResponse({ status: 200, description: 'Rule category values for the requested code' })
async findRuleCategoryValuesByCode(@Param('code') code: string) {
return this.masterDataService.findRuleCategoryValuesByCode(code);
}
@Get('rule-category-values/:id')
@ApiOperation({ summary: 'Get one rule category value' })
@ApiParam({ name: 'id', type: String, description: 'Rule category value id' })
@ApiResponse({ status: 200, description: 'Rule category value found' })
async findOneRuleCategoryValue(@Param('id') id: string) {
return this.masterDataService.findOneRuleCategoryValue(id);
}
@Post('rule-category-values')
@ApiOperation({ summary: 'Create a rule category value' })
@ApiBody({
type: CreateRuleCategoryValueDto,
description: 'Rule category value payload',
examples: {
default: {
value: {
categoryId: '00000000-0000-0000-0000-000000000000',
code: 'economy',
value: 'Economy',
displayOrder: 1,
isActive: true,
},
},
},
})
@ApiResponse({ status: 201, description: 'Rule category value created' })
async createRuleCategoryValue(@Body() createDto: CreateRuleCategoryValueDto) {
return this.masterDataService.createRuleCategoryValue(createDto);
}
@Put('rule-category-values/:id')
@ApiOperation({ summary: 'Update a rule category value' })
@ApiParam({ name: 'id', type: String, description: 'Rule category value id' })
@ApiBody({
type: UpdateRuleCategoryValueDto,
description: 'Rule category value update payload',
examples: {
default: {
value: {
value: 'Premium Economy',
displayOrder: 2,
isActive: true,
},
},
},
})
@ApiResponse({ status: 200, description: 'Rule category value updated' })
async updateRuleCategoryValue(
@Param('id') id: string,
@Body() updateDto: UpdateRuleCategoryValueDto,
) {
return this.masterDataService.updateRuleCategoryValue(id, updateDto);
}
@Delete('rule-category-values/:id')
@ApiOperation({ summary: 'Delete a rule category value' })
@ApiParam({ name: 'id', type: String, description: 'Rule category value id' })
@ApiResponse({ status: 200, description: 'Rule category value deleted' })
async removeRuleCategoryValue(@Param('id') id: string) {
return this.masterDataService.removeRuleCategoryValue(id);
}
@Get('operators')
@ApiOperation({ summary: 'Get all operators' })
@ApiResponse({ status: 200, description: 'List of operators' })
async findAllOperators() {
return this.masterDataService.findAllOperators();
}
@Get('operators/:id')
@ApiOperation({ summary: 'Get one operator' })
@ApiParam({ name: 'id', type: String, description: 'Operator id' })
@ApiResponse({ status: 200, description: 'Operator found' })
async findOneOperator(@Param('id') id: string) {
return this.masterDataService.findOneOperator(id);
}
@Post('operators')
@ApiOperation({ summary: 'Create an operator' })
@ApiBody({
type: CreateOperatorDto,
description: 'Operator payload',
examples: {
default: {
value: {
code: 'EQ',
name: 'Equals',
symbol: '=',
displayOrder: 1,
isActive: true,
},
},
},
})
@ApiResponse({ status: 201, description: 'Operator created' })
async createOperator(@Body() createDto: CreateOperatorDto) {
return this.masterDataService.createOperator(createDto);
}
@Put('operators/:id')
@ApiOperation({ summary: 'Update an operator' })
@ApiParam({ name: 'id', type: String, description: 'Operator id' })
@ApiBody({
type: UpdateOperatorDto,
description: 'Operator update payload',
examples: {
default: {
value: {
name: 'Equals',
symbol: '=',
displayOrder: 1,
isActive: true,
},
},
},
})
@ApiResponse({ status: 200, description: 'Operator updated' })
async updateOperator(@Param('id') id: string, @Body() updateDto: UpdateOperatorDto) {
return this.masterDataService.updateOperator(id, updateDto);
}
@Delete('operators/:id')
@ApiOperation({ summary: 'Delete an operator' })
@ApiParam({ name: 'id', type: String, description: 'Operator id' })
@ApiResponse({ status: 200, description: 'Operator deleted' })
async removeOperator(@Param('id') id: string) {
return this.masterDataService.removeOperator(id);
}
@Get('jurisdictions')
@ApiOperation({ summary: 'Get all jurisdictions' })
@ApiResponse({ status: 200, description: 'List of jurisdictions' })
async findAllJurisdictions() {
return this.masterDataService.findAll('JURISDICTION');
}
@Get('jurisdictions/:id')
@ApiOperation({ summary: 'Get one jurisdiction' })
@ApiParam({ name: 'id', type: String, description: 'Jurisdiction id' })
@ApiResponse({ status: 200, description: 'Jurisdiction found' })
async findOneJurisdiction(@Param('id') id: string) {
return this.masterDataService.findOne('JURISDICTION', id);
}
// Example: POST /master-data/REGION
@Post(':category')
async create(
@Param('category') category: string,
@Body() createDto: CreateMasterDataDto,
) {
return this.masterDataService.create(category, createDto);
}
// Example: GET /master-data/REGION
@Get(':category')
async findAll(@Param('category') category: string) {
return this.masterDataService.findAll(category);
}
// Example: GET /master-data/REGION/uuid
@Get(':category/:id')
async findOne(
@Param('category') category: string,
@Param('id') id: string,
) {
return this.masterDataService.findOne(category, id);
}
// Example: PUT /master-data/REGION/uuid
@Put(':category/:id')
async update(
@Param('category') category: string,
@Param('id') id: string,
@Body() updateDto: UpdateMasterDataDto,
) {
return this.masterDataService.update(category, id, updateDto);
}
// Example: DELETE /master-data/REGION/uuid
@Delete(':category/:id')
async remove(
@Param('category') category: string,
@Param('id') id: string,
) {
return this.masterDataService.remove(category, id);
}
}
@@ -0,0 +1,39 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MasterDataController } from './master-data.controller';
import { MasterDataService } from './master-data.service';
import { MembershipTier } from './entities/membership-tier.entity';
import { CustomerValue } from './entities/customer-value.entity';
import { Region } from './entities/region.entity';
import { TripPurpose } from './entities/trip-purpose.entity';
import { CabinClass } from './entities/cabin-class.entity';
import { PassengerType } from './entities/passenger-type.entity';
import { AncillaryPurchase } from './entities/ancillary-purchase.entity';
import { RevenueSegment } from './entities/revenue-segment.entity';
import { Jurisdiction } from './entities/jurisdiction.entity';
import { RuleCategory } from './entities/rule-category.entity';
import { RuleCategoryValue } from './entities/rule-category-value.entity';
import { Operator } from './entities/operator.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
MembershipTier,
CustomerValue,
Region,
TripPurpose,
CabinClass,
PassengerType,
AncillaryPurchase,
RevenueSegment,
Jurisdiction,
RuleCategory,
RuleCategoryValue,
Operator,
]),
],
controllers: [MasterDataController],
providers: [MasterDataService],
exports: [MasterDataService, TypeOrmModule],
})
export class MasterDataModule {}
@@ -0,0 +1,642 @@
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { getTenantId } from '../../common/tenant/tenant.context';
import { MembershipTier } from './entities/membership-tier.entity';
import { CustomerValue } from './entities/customer-value.entity';
import { Region } from './entities/region.entity';
import { TripPurpose } from './entities/trip-purpose.entity';
import { CabinClass } from './entities/cabin-class.entity';
import { PassengerType } from './entities/passenger-type.entity';
import { AncillaryPurchase } from './entities/ancillary-purchase.entity';
import { RevenueSegment } from './entities/revenue-segment.entity';
import { Jurisdiction } from './entities/jurisdiction.entity';
import { RuleCategory } from './entities/rule-category.entity';
import { RuleCategoryValue } from './entities/rule-category-value.entity';
import { Operator } from './entities/operator.entity';
@Injectable()
export class MasterDataService {
private readonly logger = new Logger(MasterDataService.name);
constructor(
@InjectRepository(MembershipTier) private membershipTierRepo: Repository<MembershipTier>,
@InjectRepository(CustomerValue) private customerValueRepo: Repository<CustomerValue>,
@InjectRepository(Region) private regionRepo: Repository<Region>,
@InjectRepository(TripPurpose) private tripPurposeRepo: Repository<TripPurpose>,
@InjectRepository(CabinClass) private cabinClassRepo: Repository<CabinClass>,
@InjectRepository(PassengerType) private passengerTypeRepo: Repository<PassengerType>,
@InjectRepository(AncillaryPurchase) private ancillaryPurchaseRepo: Repository<AncillaryPurchase>,
@InjectRepository(RevenueSegment) private revenueSegmentRepo: Repository<RevenueSegment>,
@InjectRepository(Jurisdiction) private jurisdictionRepo: Repository<Jurisdiction>,
@InjectRepository(RuleCategory) private ruleCategoryRepo: Repository<RuleCategory>,
@InjectRepository(RuleCategoryValue) private ruleCategoryValueRepo: Repository<RuleCategoryValue>,
@InjectRepository(Operator) private operatorRepo: Repository<Operator>,
) {}
private resolveTenantId(tenantId?: string): string {
return tenantId ?? getTenantId();
}
private getRepositoryByCategory(category: string): Repository<any> {
switch (category?.toUpperCase()) {
case 'MEMBERSHIP_TIER': return this.membershipTierRepo;
case 'CUSTOMER_VALUE': return this.customerValueRepo;
case 'REGION': return this.regionRepo;
case 'TRIP_PURPOSE': return this.tripPurposeRepo;
case 'CABIN_CLASS': return this.cabinClassRepo;
case 'PASSENGER_TYPE': return this.passengerTypeRepo;
case 'ANCILLARY_PURCHASE': return this.ancillaryPurchaseRepo;
case 'REVENUE_SEGMENT': return this.revenueSegmentRepo;
case 'JURISDICTION': return this.jurisdictionRepo;
case 'RULE_CATEGORY': return this.ruleCategoryRepo;
case 'RULE_CATEGORY_VALUE': return this.ruleCategoryValueRepo;
case 'OPERATORS': return this.operatorRepo;
default:
throw new BadRequestException(`Invalid category: ${category}`);
}
}
async findAll(category: string): Promise<any[]> {
if (!category) {
throw new BadRequestException('Category is required');
}
const repo = this.getRepositoryByCategory(category);
return repo.find({
where: { isActive: true, tenantId: getTenantId() },
});
}
async findOne(category: string, id: string): Promise<any> {
const repo = this.getRepositoryByCategory(category);
const item = await repo.findOne({ where: { id, tenantId: getTenantId() } });
if (!item) {
throw new BadRequestException(`${category} with id ${id} not found`);
}
return item;
}
async create(category: string, data: any): Promise<any> {
const repo = this.getRepositoryByCategory(category);
const newItem = repo.create({ ...data, tenantId: getTenantId() });
return repo.save(newItem);
}
async update(category: string, id: string, data: any): Promise<any> {
const repo = this.getRepositoryByCategory(category);
const item = await this.findOne(category, id);
Object.assign(item, data);
return repo.save(item);
}
async remove(category: string, id: string): Promise<void> {
const repo = this.getRepositoryByCategory(category);
const item = await this.findOne(category, id);
await repo.remove(item);
}
async findAllRuleCategories(): Promise<RuleCategory[]> {
return this.ruleCategoryRepo.find({
where: { isActive: true, tenantId: getTenantId() },
order: { displayOrder: 'ASC', name: 'ASC' },
});
}
async findOneRuleCategory(id: string): Promise<RuleCategory> {
const item = await this.ruleCategoryRepo.findOne({ where: { id, tenantId: getTenantId() } });
if (!item) {
throw new BadRequestException(`Rule category with id ${id} not found`);
}
return item;
}
async createRuleCategory(data: Partial<RuleCategory>): Promise<RuleCategory> {
const newItem = this.ruleCategoryRepo.create({ ...data, tenantId: getTenantId() });
return this.ruleCategoryRepo.save(newItem);
}
async updateRuleCategory(id: string, data: Partial<RuleCategory>): Promise<RuleCategory> {
const item = await this.findOneRuleCategory(id);
Object.assign(item, data);
return this.ruleCategoryRepo.save(item);
}
async removeRuleCategory(id: string): Promise<void> {
const item = await this.findOneRuleCategory(id);
await this.ruleCategoryRepo.remove(item);
}
async findAllRuleCategoryValues(): Promise<RuleCategoryValue[]> {
return this.ruleCategoryValueRepo.find({
where: { isActive: true, tenantId: getTenantId() },
order: { displayOrder: 'ASC', value: 'ASC' },
});
}
async findRuleCategoryValuesByCode(code: string): Promise<RuleCategoryValue[]> {
const category = await this.ruleCategoryRepo.findOne({
where: { code, tenantId: getTenantId() },
});
if (!category) {
throw new BadRequestException(`Rule category with code ${code} not found`);
}
return this.ruleCategoryValueRepo.find({
where: { categoryId: category.id, isActive: true, tenantId: getTenantId() },
order: { displayOrder: 'ASC', value: 'ASC' },
});
}
async findOneRuleCategoryValue(id: string): Promise<RuleCategoryValue> {
const item = await this.ruleCategoryValueRepo.findOne({ where: { id, tenantId: getTenantId() } });
if (!item) {
throw new BadRequestException(`Rule category value with id ${id} not found`);
}
return item;
}
async createRuleCategoryValue(data: Partial<RuleCategoryValue>): Promise<RuleCategoryValue> {
const newItem = this.ruleCategoryValueRepo.create({ ...data, tenantId: getTenantId() });
return this.ruleCategoryValueRepo.save(newItem);
}
async updateRuleCategoryValue(id: string, data: Partial<RuleCategoryValue>): Promise<RuleCategoryValue> {
const item = await this.findOneRuleCategoryValue(id);
Object.assign(item, data);
return this.ruleCategoryValueRepo.save(item);
}
async removeRuleCategoryValue(id: string): Promise<void> {
const item = await this.findOneRuleCategoryValue(id);
await this.ruleCategoryValueRepo.remove(item);
}
async findAllOperators(): Promise<Operator[]> {
return this.operatorRepo.find({
where: { isActive: true, tenantId: getTenantId() },
order: { displayOrder: 'ASC', name: 'ASC' },
});
}
async findOneOperator(id: string): Promise<Operator> {
const item = await this.operatorRepo.findOne({ where: { id, tenantId: getTenantId() } });
if (!item) {
throw new BadRequestException(`Operator with id ${id} not found`);
}
return item;
}
async createOperator(data: Partial<Operator>): Promise<Operator> {
const newItem = this.operatorRepo.create({ ...data, tenantId: getTenantId() });
return this.operatorRepo.save(newItem);
}
async updateOperator(id: string, data: Partial<Operator>): Promise<Operator> {
const item = await this.findOneOperator(id);
Object.assign(item, data);
return this.operatorRepo.save(item);
}
async removeOperator(id: string): Promise<void> {
const item = await this.findOneOperator(id);
await this.operatorRepo.remove(item);
}
public async seedData(tenantId?: string) {
const resolvedTenantId = this.resolveTenantId(tenantId);
this.logger.log(`Checking if master data needs seeding for tenant ${resolvedTenantId}...`);
if (await this.membershipTierRepo.count({ where: { tenantId: resolvedTenantId } }) === 0) {
const data = [
{ label: 'Platinum', value: 'platinum', tenantId: resolvedTenantId },
{ label: 'Gold', value: 'gold', tenantId: resolvedTenantId },
{ label: 'Silver', value: 'silver', tenantId: resolvedTenantId },
{ label: 'Bronze', value: 'bronze', tenantId: resolvedTenantId },
{ label: 'Basic', value: 'basic', tenantId: resolvedTenantId },
{ label: 'Non-Member', value: 'non-member', tenantId: resolvedTenantId },
];
await this.membershipTierRepo.save(this.membershipTierRepo.create(data));
}
if (await this.customerValueRepo.count({ where: { tenantId: resolvedTenantId } }) === 0) {
const data = [
{ label: 'High Value', value: 'high', tenantId: resolvedTenantId },
{ label: 'Medium Value', value: 'medium', tenantId: resolvedTenantId },
{ label: 'Low Value', value: 'low', tenantId: resolvedTenantId },
];
await this.customerValueRepo.save(this.customerValueRepo.create(data));
}
if (await this.regionRepo.count({ where: { tenantId: resolvedTenantId } }) === 0) {
const data = [
{ label: 'Global', value: 'global', tenantId: resolvedTenantId },
{ label: 'Americas', value: 'americas', tenantId: resolvedTenantId },
{ label: 'Europe', value: 'europe', tenantId: resolvedTenantId },
{ label: 'Middle East', value: 'middle-east', tenantId: resolvedTenantId },
{ label: 'Asia Pacific', value: 'asia-pacific', tenantId: resolvedTenantId },
{ label: 'Africa', value: 'africa', tenantId: resolvedTenantId },
];
await this.regionRepo.save(this.regionRepo.create(data));
}
if (await this.tripPurposeRepo.count({ where: { tenantId: resolvedTenantId } }) === 0) {
const data = [
{ label: 'Business', value: 'business', tenantId: resolvedTenantId },
{ label: 'Leisure', value: 'leisure', tenantId: resolvedTenantId },
{ label: 'Corporate', value: 'corporate', tenantId: resolvedTenantId },
{ label: 'Government', value: 'government', tenantId: resolvedTenantId },
];
await this.tripPurposeRepo.save(this.tripPurposeRepo.create(data));
}
if (await this.cabinClassRepo.count({ where: { tenantId: resolvedTenantId } }) === 0) {
const data = [
{ label: 'First Class', value: 'first-class', tenantId: resolvedTenantId },
{ label: 'Business Class', value: 'business-class', tenantId: resolvedTenantId },
{ label: 'Premium Economy', value: 'premium-economy', tenantId: resolvedTenantId },
{ label: 'Economy', value: 'economy', tenantId: resolvedTenantId },
];
await this.cabinClassRepo.save(this.cabinClassRepo.create(data));
}
if (await this.passengerTypeRepo.count({ where: { tenantId: resolvedTenantId } }) === 0) {
const data = [
{ label: 'Adult', value: 'adult', tenantId: resolvedTenantId },
{ label: 'Child', value: 'child', tenantId: resolvedTenantId },
{ label: 'Infant', value: 'infant', tenantId: resolvedTenantId },
{ label: 'Senior Citizen', value: 'senior', tenantId: resolvedTenantId },
];
await this.passengerTypeRepo.save(this.passengerTypeRepo.create(data));
}
if (await this.ancillaryPurchaseRepo.count({ where: { tenantId: resolvedTenantId } }) === 0) {
const data = [
{ label: 'Preferred Seat', value: 'preferred-seat', tenantId: resolvedTenantId },
{ label: 'Extra Legroom', value: 'extra-legroom', tenantId: resolvedTenantId },
{ label: 'Wi-Fi', value: 'wi-fi', tenantId: resolvedTenantId },
{ label: 'Lounge Access', value: 'lounge-access', tenantId: resolvedTenantId },
{ label: 'Priority Boarding', value: 'priority-boarding', tenantId: resolvedTenantId },
{ label: 'Fast Track', value: 'fast-track', tenantId: resolvedTenantId },
{ label: 'Paid Meal', value: 'paid-meal', tenantId: resolvedTenantId },
{ label: 'Special Meal', value: 'special-meal', tenantId: resolvedTenantId },
{ label: 'Extra Baggage', value: 'extra-baggage', tenantId: resolvedTenantId },
{ label: 'Upgrade Purchase', value: 'upgrade-purchase', tenantId: resolvedTenantId },
{ label: 'Airport Transfer', value: 'airport-transfer', tenantId: resolvedTenantId },
{ label: 'Chauffeur Service', value: 'chauffeur-service', tenantId: resolvedTenantId },
{ label: 'Sports Equipment', value: 'sports-equipment', tenantId: resolvedTenantId },
{ label: 'Musical Instrument', value: 'musical-instrument', tenantId: resolvedTenantId },
{ label: 'In-flight Entertainment', value: 'in-flight-entertainment', tenantId: resolvedTenantId },
{ label: 'Power Outlet', value: 'power-outlet', tenantId: resolvedTenantId },
{ label: 'Carbon Offset', value: 'carbon-offset', tenantId: resolvedTenantId },
];
await this.ancillaryPurchaseRepo.save(this.ancillaryPurchaseRepo.create(data));
}
if (await this.revenueSegmentRepo.count({ where: { tenantId: resolvedTenantId } }) === 0) {
const data = [
{ label: 'High Value', value: 'high', tenantId: resolvedTenantId },
{ label: 'Medium Value', value: 'medium', tenantId: resolvedTenantId },
{ label: 'Low Value', value: 'low', tenantId: resolvedTenantId },
];
await this.revenueSegmentRepo.save(this.revenueSegmentRepo.create(data));
}
if (await this.jurisdictionRepo.count({ where: { tenantId: resolvedTenantId } }) === 0) {
const data = [
{ label: 'United Arab Emirates', value: 'uae', tenantId: resolvedTenantId },
{ label: 'European Union', value: 'eu', tenantId: resolvedTenantId },
{ label: 'United States', value: 'us', tenantId: resolvedTenantId },
{ label: 'United Kingdom', value: 'uk', tenantId: resolvedTenantId },
{ label: 'India', value: 'india', tenantId: resolvedTenantId },
{ label: 'Asia Pacific', value: 'apac', tenantId: resolvedTenantId },
{ label: 'Global', value: 'global', tenantId: resolvedTenantId },
];
await this.jurisdictionRepo.save(this.jurisdictionRepo.create(data));
}
if (
(await this.ruleCategoryRepo.count({
where: { tenantId: resolvedTenantId },
})) === 0
) {
const categories = [
{ code: 'passenger-type', name: 'Passenger Type', displayOrder: 1 },
{ code: 'cabin-class', name: 'Cabin Class', displayOrder: 2 },
{ code: 'booking-channel', name: 'Booking Channel', displayOrder: 3 },
{ code: 'loyalty-tier', name: 'Loyalty Tier', displayOrder: 4 },
{ code: 'flight-type', name: 'Flight Type', displayOrder: 5 },
{ code: 'journey-type', name: 'Journey Type', displayOrder: 6 },
{ code: 'fare-flexibility', name: 'Fare Flexibility', displayOrder: 7 },
{ code: 'trip-purpose', name: 'Trip Purpose', displayOrder: 8 },
{ code: 'carrier-type', name: 'Carrier Type', displayOrder: 9 },
{ code: 'special-assistance-type', name: 'Special Assistance Type', displayOrder: 10 },
{ code: 'delay-reason', name: 'Delay Reason', displayOrder: 11 },
{ code: 'delay-duration', name: 'Delay Duration', displayOrder: 12 },
{ code: 'extraordinary-circumstances', name: 'Extraordinary Circumstances', displayOrder: 13 },
{ code: 'cancellation-reason', name: 'Cancellation Reason', displayOrder: 14 },
{ code: 'diversion-reason', name: 'Diversion Reason', displayOrder: 15 },
{ code: 'missed-connection-reason', name: 'Missed Connection Reason', displayOrder: 16 },
{ code: 'compensation-eligibility', name: 'Compensation Eligibility', displayOrder: 17 },
{ code: 'compensation-type', name: 'Compensation Type', displayOrder: 18 },
{ code: 'refund-type', name: 'Refund Type', displayOrder: 19 },
{ code: 'flight-disruption-type', name: 'Flight Disruption Type', displayOrder: 20 },
{ code: 'airline-responsibility', name: 'Airline Responsibility', displayOrder: 21 },
{ code: 'weather-condition', name: 'Weather Condition', displayOrder: 22 },
{ code: 'atc-restriction', name: 'ATC Restriction', displayOrder: 23 },
{ code: 'technical-fault-category', name: 'Technical Fault Category', displayOrder: 24 },
].map((item) => ({
...item,
description: `${item.name} master`,
tenantId: resolvedTenantId,
}));
await this.ruleCategoryRepo.save(this.ruleCategoryRepo.create(categories));
}
if (
(await this.ruleCategoryValueRepo.count({
where: { tenantId: resolvedTenantId },
})) === 0
) {
const categories = await this.ruleCategoryRepo.find({
where: { tenantId: resolvedTenantId },
});
const categoryMap = new Map(
categories.map((c) => [c.code, c.id]),
);
const masterData = {
'passenger-type': [
'Adult',
'Child',
'Infant',
],
'cabin-class': [
'Economy',
'Premium Economy',
'Business',
'First',
],
'booking-channel': [
'Airline Website',
'Airline Mobile App',
'Airport Ticket Counter',
'Call Center',
'Corporate Booking Tool',
'Global Distribution System',
'Online Travel Agency',
'Travel Agent',
],
'loyalty-tier': [
'Basic',
'Silver',
'Gold',
'Platinum',
'Diamond',
'Elite',
'Lifetime',
],
'flight-type': [
'Domestic',
'International',
],
'journey-type': [
'One Way',
'Round Trip',
'Multi City',
],
'fare-flexibility': [
'Non Refundable',
'Partially Refundable',
'Refundable',
'Exchangeable',
'Non Changeable',
],
'trip-purpose': [
'Business',
'Leisure',
'Medical',
'Education',
'Government',
'Military',
'Religious',
'Transit',
],
'carrier-type': [
'Operating Carrier',
'Marketing Carrier',
'Partner Carrier',
'Regional Carrier',
'Low Cost Carrier',
'Full Service Carrier',
'Charter Carrier',
],
'special-assistance-type': [
'Wheelchair Assistance',
'Wheelchair Ramp',
'Wheelchair Steps',
'Wheelchair Cabin',
'Blind Passenger',
'Deaf Passenger',
'Medical Assistance',
'Oxygen Required',
'Stretcher',
'Unaccompanied Minor',
'Service Animal',
'Pregnant Passenger',
'Elderly Passenger',
'Other',
],
'delay-reason': [
'Air Traffic Control Restriction',
'Aircraft Rotation',
'Airport Congestion',
'Crew Availability',
'Customs Delay',
'Fueling Delay',
'Late Arrival of Aircraft',
'Operational Decision',
'Passenger Handling',
'Runway Closure',
'Security',
'Severe Weather',
'Technical Fault',
'Other',
],
'delay-duration': [
'Less than 1 Hour',
'1-2 Hours',
'2-3 Hours',
'3-4 Hours',
'More than 4 Hours',
],
'extraordinary-circumstances': [
'Air Traffic Management Decision',
'Airport Closure',
'Bird Strike',
'Civil Unrest',
'Medical Emergency',
'Political Instability',
'Security Threat',
'Severe Weather',
'Strike (External)',
'War',
'Other',
],
'cancellation-reason': [
'Air Traffic Control Restriction',
'Airport Closure',
'Commercial Decision',
'Crew Availability',
'Operational Decision',
'Overbooking',
'Security',
'Severe Weather',
'Strike',
'Technical Fault',
],
'diversion-reason': [
'Airport Closure',
'Destination Weather',
'Fuel Emergency',
'Medical Emergency',
'Runway Obstruction',
'Security Threat',
'Technical Fault',
],
'missed-connection-reason': [
'Customs Delay',
'Flight Delay',
'Immigration Delay',
'Passenger Delay',
'Security Screening Delay',
],
'compensation-eligibility': [
'Eligible',
'Not Eligible',
'Requires Manual Review',
],
'compensation-type': [
'Cash',
'Cheque',
'Flight Voucher',
'Loyalty Miles',
'Meal Voucher',
'Hotel Accommodation',
'Ground Transport',
],
'refund-type': [
'Full Refund',
'Partial Refund',
'Future Travel Credit',
'Travel Voucher',
'Tax Refund Only',
'Telephone Reimbursement',
],
'flight-disruption-type': [
'Cancellation',
'Delay',
'Denied Boarding',
'Diversion',
'Missed Connection',
],
'airline-responsibility': [
'Airline Responsible',
'Airport Responsible',
'ATC Responsible',
'Passenger Responsible',
'Shared Responsibility',
'Third Party Responsible',
'Snow',
],
'weather-condition': [
'Fog',
'Heavy Rain',
'Hurricane',
'Ice',
'Lightning',
'Sandstorm',
'Thunderstorm',
],
'atc-restriction': [
'Airspace Closure',
'Flow Control',
'Ground Stop',
'Slot Restriction',
'Traffic Congestion',
'Navigation System',
],
'technical-fault-category': [
'Aircraft Damage',
'Avionics',
'Cabin Systems',
'Engine',
'Hydraulic System',
'Landing Gear',
'Volcanic Ash',
'Wind Shear',
],
};
const values = Object.entries(masterData).flatMap(([categoryCode, items]) =>
items.map((value, index) => ({
categoryId: categoryMap.get(categoryCode),
code: value.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
value,
tenantId: resolvedTenantId,
displayOrder: index + 1,
})),
);
await this.ruleCategoryValueRepo.save(
this.ruleCategoryValueRepo.create(values),
);
}
if ((await this.operatorRepo.count({ where: { tenantId: resolvedTenantId } })) === 0) {
const operators = [
{ code: 'EQ', name: 'Equals', symbol: '=', displayOrder: 1 },
{ code: 'NE', name: 'Not Equals', symbol: '!=', displayOrder: 2 },
{ code: 'GT', name: 'Greater Than', symbol: '>', displayOrder: 3 },
{ code: 'GTE', name: 'Greater Than or Equal', symbol: '>=', displayOrder: 4 },
{ code: 'LT', name: 'Less Than', symbol: '<', displayOrder: 5 },
{ code: 'LTE', name: 'Less Than or Equal', symbol: '<=', displayOrder: 6 },
{ code: 'BETWEEN', name: 'Between', symbol: 'BETWEEN', displayOrder: 7 },
{ code: 'CONTAINS', name: 'Contains', symbol: 'CONTAINS', displayOrder: 8 },
{ code: 'IN', name: 'In', symbol: 'IN', displayOrder: 9 },
{ code: 'IS_EMPTY', name: 'Is Empty', symbol: 'IS EMPTY', displayOrder: 10 },
].map((item) => ({ ...item, tenantId: resolvedTenantId }));
await this.operatorRepo.save(this.operatorRepo.create(operators));
}
this.logger.log(`Master data seeded for tenant ${resolvedTenantId}`);
}
}
@@ -0,0 +1,124 @@
import {
IsString,
IsNotEmpty,
IsOptional,
IsUUID,
IsEnum,
IsInt,
IsArray,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
import { PolicyStatus, AudienceType, LogicalOperator } from '../entities/policy.enums';
export class CreatePolicyTargetAudienceDto {
@IsEnum(AudienceType)
targetType: AudienceType;
@IsUUID()
@IsOptional()
targetId?: string;
}
export class CreateRuleConditionDto {
@IsUUID()
@IsNotEmpty()
fieldId: string;
@IsUUID()
@IsNotEmpty()
operatorId: string;
@IsString()
@IsOptional()
valueText?: string;
@IsEnum(LogicalOperator)
@IsOptional()
logicalOperator?: LogicalOperator;
@IsInt()
sequence: number;
}
export class CreatePolicyActionDto {
@IsUUID()
@IsNotEmpty()
actionTypeId: string;
@IsUUID()
@IsOptional()
parameterId?: string;
@IsString()
@IsOptional()
valueText?: string;
@IsInt()
sequence: number;
}
export class CreatePolicyRuleDto {
@IsUUID()
@IsOptional()
ruleCategoryId?: string;
@IsInt()
priority: number;
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateRuleConditionDto)
conditions: CreateRuleConditionDto[];
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreatePolicyActionDto)
actions: CreatePolicyActionDto[];
}
export class CreatePolicyDto {
@IsString()
@IsNotEmpty()
policyName: string;
@IsUUID()
@IsOptional()
jurisdictionId?: string;
@IsString()
@IsOptional()
description?: string;
@IsEnum(PolicyStatus)
@IsOptional()
status?: PolicyStatus;
@IsInt()
@IsOptional()
version?: number;
@IsEnum(AudienceType)
@IsOptional()
audienceType?: AudienceType;
@IsUUID()
@IsOptional()
createdBy?: string;
@IsUUID()
@IsOptional()
updatedBy?: string;
@IsArray()
@IsOptional()
@ValidateNested({ each: true })
@Type(() => CreatePolicyTargetAudienceDto)
targetAudiences?: CreatePolicyTargetAudienceDto[];
@IsArray()
@IsOptional()
@ValidateNested({ each: true })
@Type(() => CreatePolicyRuleDto)
rules?: CreatePolicyRuleDto[];
}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreatePolicyDto } from './create-policy.dto';
export class UpdatePolicyDto extends PartialType(CreatePolicyDto) {}
@@ -0,0 +1,27 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { PolicyRule } from './policy-rule.entity';
@Entity({ name: 'policy_actions', schema: 'policy_engine' })
export class PolicyAction {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'rule_id', type: 'uuid' })
ruleId: string;
@Column({ name: 'action_type_id', type: 'uuid' })
actionTypeId: string;
@Column({ name: 'parameter_id', type: 'uuid', nullable: true })
parameterId: string;
@Column({ name: 'value_text', type: 'text', nullable: true })
valueText: string;
@Column({ type: 'int', default: 0 })
sequence: number;
@ManyToOne(() => PolicyRule, (rule) => rule.actions, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'rule_id' })
rule: PolicyRule;
}
@@ -0,0 +1,29 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
import { Policy } from './policy.entity';
import { RuleCondition } from './rule-condition.entity';
import { PolicyAction } from './policy-action.entity';
@Entity({ name: 'policy_rules', schema: 'policy_engine' })
export class PolicyRule {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'policy_id', type: 'uuid' })
policyId: string;
@Column({ name: 'rule_category_id', type: 'uuid', nullable: true })
ruleCategoryId: string;
@Column({ type: 'int', default: 0 })
priority: number;
@ManyToOne(() => Policy, (policy) => policy.rules, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'policy_id' })
policy: Policy;
@OneToMany(() => RuleCondition, (condition) => condition.rule, { cascade: true, onDelete: 'CASCADE' })
conditions: RuleCondition[];
@OneToMany(() => PolicyAction, (action) => action.rule, { cascade: true, onDelete: 'CASCADE' })
actions: PolicyAction[];
}
@@ -0,0 +1,26 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { Policy } from './policy.entity';
import { AudienceType } from './policy.enums';
@Entity({ name: 'policy_target_audience', schema: 'policy_engine' })
export class PolicyTargetAudience {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'policy_id', type: 'uuid' })
policyId: string;
@Column({
name: 'target_type',
type: 'enum',
enum: AudienceType,
})
targetType: AudienceType;
@Column({ name: 'target_id', type: 'uuid', nullable: true })
targetId: string;
@ManyToOne(() => Policy, (policy) => policy.targetAudiences, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'policy_id' })
policy: Policy;
}
@@ -0,0 +1,62 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany, ManyToOne, JoinColumn } from 'typeorm';
import { TenantOwnedEntity } from '../../../common/entities/tenant-owned.entity';
import { PolicyStatus, AudienceType } from './policy.enums';
import { PolicyTargetAudience } from './policy-target-audience.entity';
import { PolicyRule } from './policy-rule.entity';
import { Jurisdiction } from '../../master-data/entities/jurisdiction.entity';
@Entity({ name: 'policies', schema: 'policy_engine' })
export class Policy extends TenantOwnedEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'policy_name', type: 'varchar', length: 200 })
policyName: string;
@Column({ name: 'jurisdiction_id', type: 'uuid', nullable: true })
jurisdictionId: string;
@Column({ type: 'text', nullable: true })
description: string;
@Column({
type: 'enum',
enum: PolicyStatus,
default: PolicyStatus.DRAFT,
})
status: PolicyStatus;
@Column({ type: 'int', default: 1 })
version: number;
@Column({
name: 'audience_type',
type: 'enum',
enum: AudienceType,
default: AudienceType.ALL,
})
audienceType: AudienceType;
@Column({ name: 'created_by', type: 'uuid', nullable: true })
createdBy: string;
@Column({ name: 'updated_by', type: 'uuid', nullable: true })
updatedBy: string;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
// Relations
@OneToMany(() => PolicyTargetAudience, (target) => target.policy, { cascade: true, onDelete: 'CASCADE' })
targetAudiences: PolicyTargetAudience[];
@OneToMany(() => PolicyRule, (rule) => rule.policy, { cascade: true, onDelete: 'CASCADE' })
rules: PolicyRule[];
@ManyToOne(() => Jurisdiction, { nullable: true, eager: false, createForeignKeyConstraints: false })
@JoinColumn({ name: 'jurisdiction_id' })
jurisdiction: Jurisdiction;
}
@@ -0,0 +1,16 @@
export enum PolicyStatus {
DRAFT = 'draft',
ACTIVE = 'active',
INACTIVE = 'inactive',
ARCHIVED = 'archived',
}
export enum AudienceType {
ALL = 'ALL',
COHORT = 'COHORT',
}
export enum LogicalOperator {
AND = 'AND',
OR = 'OR',
}
@@ -0,0 +1,36 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { PolicyRule } from './policy-rule.entity';
import { LogicalOperator } from './policy.enums';
@Entity({ name: 'policy_rule_conditions', schema: 'policy_engine' })
export class RuleCondition {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'rule_id', type: 'uuid' })
ruleId: string;
@Column({ name: 'field_id', type: 'uuid' })
fieldId: string;
@Column({ name: 'operator_id', type: 'uuid' })
operatorId: string;
@Column({ name: 'value_text', type: 'text', nullable: true })
valueText: string;
@Column({
name: 'logical_operator',
type: 'enum',
enum: LogicalOperator,
nullable: true,
})
logicalOperator: LogicalOperator;
@Column({ type: 'int', default: 0 })
sequence: number;
@ManyToOne(() => PolicyRule, (rule) => rule.conditions, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'rule_id' })
rule: PolicyRule;
}
@@ -0,0 +1,66 @@
import {
Controller,
Get,
Post,
Put,
Delete,
Patch,
Body,
Param,
Query,
} from '@nestjs/common';
import { PolicyEngineService } from './policy-engine.service';
import { CreatePolicyDto } from './dto/create-policy.dto';
import { UpdatePolicyDto } from './dto/update-policy.dto';
import { PolicyStatus, AudienceType } from './entities/policy.enums';
@Controller('policy-engine')
export class PolicyEngineController {
constructor(private readonly policyEngineService: PolicyEngineService) { }
@Post()
async create(@Body() createPolicyDto: CreatePolicyDto) {
return this.policyEngineService.create(createPolicyDto);
}
@Get()
async findAll(
@Query('page') page: string = '1',
@Query('limit') limit: string = '10',
@Query('status') status?: PolicyStatus,
@Query('audienceType') audienceType?: AudienceType,
) {
return this.policyEngineService.findAll(
Number(page),
Number(limit),
status,
audienceType,
);
}
@Get(':id')
async findOne(@Param('id') id: string) {
return this.policyEngineService.findOne(id);
}
@Put(':id')
async update(
@Param('id') id: string,
@Body() updatePolicyDto: UpdatePolicyDto,
) {
return this.policyEngineService.update(id, updatePolicyDto);
}
@Patch(':id/status')
async updateStatus(
@Param('id') id: string,
@Body('status') status: PolicyStatus,
) {
return this.policyEngineService.updateStatus(id, status);
}
@Delete(':id')
async remove(@Param('id') id: string) {
return this.policyEngineService.remove(id);
}
}
@@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PolicyEngineService } from './policy-engine.service';
import { PolicyEngineController } from './policy-engine.controller';
import { Policy } from './entities/policy.entity';
import { PolicyTargetAudience } from './entities/policy-target-audience.entity';
import { PolicyRule } from './entities/policy-rule.entity';
import { RuleCondition } from './entities/rule-condition.entity';
import { PolicyAction } from './entities/policy-action.entity';
import { MasterDataModule } from '../master-data/master-data.module';
@Module({
imports: [
TypeOrmModule.forFeature([
Policy,
PolicyTargetAudience,
PolicyRule,
RuleCondition,
PolicyAction,
]),
MasterDataModule,
],
controllers: [PolicyEngineController],
providers: [PolicyEngineService],
exports: [PolicyEngineService],
})
export class PolicyEngineModule { }
@@ -0,0 +1,173 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { getTenantId } from '../../common/tenant/tenant.context';
import { Policy } from './entities/policy.entity';
import { PolicyTargetAudience } from './entities/policy-target-audience.entity';
import { PolicyRule } from './entities/policy-rule.entity';
import { RuleCondition } from './entities/rule-condition.entity';
import { PolicyAction } from './entities/policy-action.entity';
import { CreatePolicyDto } from './dto/create-policy.dto';
import { UpdatePolicyDto } from './dto/update-policy.dto';
import { PolicyStatus, AudienceType } from './entities/policy.enums';
import { Jurisdiction } from '../master-data/entities/jurisdiction.entity';
@Injectable()
export class PolicyEngineService {
constructor(
@InjectRepository(Policy)
private readonly policyRepository: Repository<Policy>,
@InjectRepository(Jurisdiction)
private readonly jurisdictionRepository: Repository<Jurisdiction>,
) {}
private getRelations() {
return {
jurisdiction: true,
targetAudiences: true,
rules: {
conditions: true,
actions: true,
},
};
}
async create(createPolicyDto: CreatePolicyDto): Promise<Policy> {
const tenantId = getTenantId();
// Create the policy entity and cascade nested associations using TypeORM
const policy = this.policyRepository.create({
...createPolicyDto,
tenantId,
});
const saved = await this.policyRepository.save(policy);
return this.findOne(saved.id);
}
async findAll(
page: number = 1,
limit: number = 10,
status?: PolicyStatus,
audienceType?: AudienceType,
) {
const skip = (page - 1) * limit;
const tenantId = getTenantId();
const whereClause: any = { tenantId };
if (status) {
whereClause.status = status;
}
if (audienceType) {
whereClause.audienceType = audienceType;
}
const [data, total] = await this.policyRepository.findAndCount({
where: whereClause,
skip,
take: limit,
order: { createdAt: 'DESC' },
relations: this.getRelations(),
});
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
async findOne(id: string): Promise<Policy> {
const tenantId = getTenantId();
const policy = await this.policyRepository.findOne({
where: { id, tenantId },
relations: this.getRelations(),
});
if (!policy) {
throw new NotFoundException(`Policy with ID ${id} not found`);
}
return policy;
}
async update(id: string, updatePolicyDto: UpdatePolicyDto): Promise<Policy> {
// Ensure policy exists and belongs to the tenant
await this.findOne(id);
const { targetAudiences, rules, ...policyData } = updatePolicyDto;
// Execute database operations in a transaction for clean replacement of nested objects
return this.policyRepository.manager.transaction(async (transactionalEntityManager) => {
// 1. Delete all existing target audiences associated with this policy
await transactionalEntityManager.delete(PolicyTargetAudience, { policyId: id });
// 2. Find and delete existing rules (cascades to conditions and actions)
const existingRules = await transactionalEntityManager.find(PolicyRule, { where: { policyId: id } });
if (existingRules.length > 0) {
await transactionalEntityManager.remove(PolicyRule, existingRules);
}
// 3. Fetch original policy again in transaction to ensure we have a fresh copy
const policy = await transactionalEntityManager.findOneOrFail(Policy, {
where: { id, tenantId: getTenantId() },
});
// 4. Update policy basic columns
Object.assign(policy, policyData);
// 5. Build new Target Audience entities if provided
if (targetAudiences) {
policy.targetAudiences = targetAudiences.map((target) =>
transactionalEntityManager.create(PolicyTargetAudience, {
...target,
policyId: id,
}),
);
}
// 6. Build new Policy Rule entities with Conditions and Actions if provided
if (rules) {
policy.rules = rules.map((ruleDto) => {
const conditions = ruleDto.conditions?.map((cond) =>
transactionalEntityManager.create(RuleCondition, cond),
) || [];
const actions = ruleDto.actions?.map((act) =>
transactionalEntityManager.create(PolicyAction, act),
) || [];
return transactionalEntityManager.create(PolicyRule, {
ruleCategoryId: ruleDto.ruleCategoryId,
priority: ruleDto.priority,
policyId: id,
conditions,
actions,
});
});
}
// 7. Save updated policy and cascades
const savedPolicy = await transactionalEntityManager.save(Policy, policy);
// Reload updated policy with all relations and return
return transactionalEntityManager.findOneOrFail(Policy, {
where: { id: savedPolicy.id },
relations: this.getRelations(),
});
});
}
async updateStatus(id: string, status: PolicyStatus): Promise<Policy> {
const policy = await this.findOne(id);
policy.status = status;
return this.policyRepository.save(policy);
}
async remove(id: string): Promise<void> {
const policy = await this.findOne(id);
await this.policyRepository.remove(policy);
}
}
@@ -0,0 +1,15 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateTenantDto {
@IsString()
@IsNotEmpty()
slug: string;
@IsString()
@IsNotEmpty()
name: string;
@IsString()
@IsOptional()
tier?: string;
}
+20
View File
@@ -0,0 +1,20 @@
import { Body, Controller, Get, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { TenantService } from './tenant.service';
import { CreateTenantDto } from './dto/create-tenant.dto';
@ApiTags('tenants')
@Controller('tenants')
export class TenantController {
constructor(private readonly tenantService: TenantService) {}
@Get()
findAll() {
return this.tenantService.findAll();
}
@Post()
create(@Body() createTenantDto: CreateTenantDto) {
return this.tenantService.create(createTenantDto);
}
}
+31
View File
@@ -0,0 +1,31 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({ name: 'tbl_tenants', schema: 'tenant' })
export class Tenant {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
slug: string;
@Column()
name: string;
@Column({ default: 'standard' })
tier: string;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
+14
View File
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Tenant } from './tenant.entity';
import { TenantService } from './tenant.service';
import { TenantController } from './tenant.controller';
import { TenantMiddleware } from '../../common/tenant/tenant.middleware';
@Module({
imports: [TypeOrmModule.forFeature([Tenant])],
controllers: [TenantController],
providers: [TenantService, TenantMiddleware],
exports: [TenantService, TenantMiddleware],
})
export class TenantModule {}
+80
View File
@@ -0,0 +1,80 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Tenant } from './tenant.entity';
import { CreateTenantDto } from './dto/create-tenant.dto';
const UUID_REGEX =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@Injectable()
export class TenantService {
constructor(
@InjectRepository(Tenant)
private readonly tenantRepository: Repository<Tenant>,
) {}
async findAll(): Promise<Tenant[]> {
return this.tenantRepository.find({
where: { isActive: true },
order: { name: 'ASC' },
});
}
async findById(id: string): Promise<Tenant> {
const tenant = await this.tenantRepository.findOne({ where: { id, isActive: true } });
if (!tenant) {
throw new NotFoundException(`Tenant ${id} not found`);
}
return tenant;
}
async findBySlug(slug: string): Promise<Tenant> {
const tenant = await this.tenantRepository.findOne({
where: { slug: slug.toLowerCase(), isActive: true },
});
if (!tenant) {
throw new NotFoundException(`Tenant ${slug} not found`);
}
return tenant;
}
async resolveTenant(identifier: string): Promise<Tenant> {
if (UUID_REGEX.test(identifier)) {
return this.findById(identifier);
}
return this.findBySlug(identifier);
}
async create(createTenantDto: CreateTenantDto): Promise<Tenant> {
const tenant = this.tenantRepository.create({
...createTenantDto,
slug: createTenantDto.slug.toLowerCase(),
});
return this.tenantRepository.save(tenant);
}
async seedDemoTenants(): Promise<Tenant[]> {
const demos = [
{ slug: 'demo-airline', name: 'Demo Airline', tier: 'standard' },
{ slug: 'etihad', name: 'Etihad Airways', tier: 'enterprise' },
];
const tenants: Tenant[] = [];
for (const demo of demos) {
const existing = await this.tenantRepository.findOne({
where: { slug: demo.slug },
});
if (existing) {
tenants.push(existing);
continue;
}
tenants.push(await this.create(demo));
}
return tenants;
}
}
-1
View File
@@ -13,7 +13,6 @@
"target": "ES2023", "target": "ES2023",
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
"baseUrl": "./",
"incremental": true, "incremental": true,
"skipLibCheck": true, "skipLibCheck": true,
"strictNullChecks": true, "strictNullChecks": true,