Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cd42c914f | ||
|
|
9cf49bc05e | ||
|
|
1d4b160660 | ||
|
|
2cdd921012 | ||
|
|
dfd2ff8127 | ||
|
|
de7527e1bd | ||
|
|
f3acfd2181 | ||
|
|
4fd1058dfe | ||
|
|
c5b360bf56 | ||
|
|
4ba1316289 | ||
|
|
fd196f559b | ||
|
|
13fa6c2d46 | ||
|
|
7473456317 | ||
|
|
49b5fcb487 | ||
|
|
3137cb8038 | ||
|
|
6e7ff937dc | ||
|
|
e99f292e24 | ||
|
|
18454d5197 | ||
|
|
4441fa2cce | ||
|
|
c3936ca692 | ||
|
|
1d0c41bf8c | ||
|
|
28837230fa | ||
|
|
1e3ec441dc | ||
|
|
06598ea462 | ||
|
|
44afa45f7a | ||
|
|
9e64243759 | ||
|
|
8021937518 | ||
|
|
25302cc222 | ||
|
|
46e4a0ed81 | ||
|
|
82ea313f34 | ||
|
|
025f114828 |
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
PORT=3001
|
||||
DATABASE_URL=postgresql://username:password@localhost:5432/dbname
|
||||
REDIS_URL=redis://localhost:6379
|
||||
KAFKA_BROKERS=localhost:9092
|
||||
@@ -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
|
||||
@@ -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
@@ -34,8 +34,10 @@ lerna-debug.log*
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
|
||||
# Local agent customization
|
||||
.agent.md
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.test
|
||||
.env.production
|
||||
.env.local
|
||||
.env
|
||||
|
||||
@@ -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
|
||||
- Node.js (v18+)
|
||||
## Prerequisites
|
||||
|
||||
### Installation
|
||||
1. Navigate to the backend directory (if you aren't already here):
|
||||
```bash
|
||||
cd backend
|
||||
```
|
||||
2. Install the dependencies using npm:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
- Node.js 18+
|
||||
- npm
|
||||
- PostgreSQL
|
||||
|
||||
## Install
|
||||
|
||||
### Development
|
||||
Start the local development server in watch mode:
|
||||
```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 start:dev
|
||||
|
||||
# UAT
|
||||
npm run uat
|
||||
npm run start:uat
|
||||
```
|
||||
|
||||
### Build for Production
|
||||
To compile the NestJS application:
|
||||
```bash
|
||||
npm run build
|
||||
## API
|
||||
|
||||
Base URL:
|
||||
|
||||
```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
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
Generated
+548
-116
File diff suppressed because it is too large
Load Diff
+19
-9
@@ -6,19 +6,25 @@
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"dev": "nest start --watch",
|
||||
"dev": "cross-env NODE_ENV=dev nest start",
|
||||
"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\"",
|
||||
"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:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
"test": "cross-env NODE_ENV=test nest start",
|
||||
"test:cov": "cross-env NODE_ENV=test 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:e2e": "cross-env NODE_ENV=test jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
@@ -26,10 +32,13 @@
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.2.0",
|
||||
"@nestjs/typeorm": "^11.0.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"pg": "^8.22.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
@@ -41,6 +50,7 @@
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/supertest": "^7.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
|
||||
@@ -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
@@ -1,13 +1,32 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { DatabaseModule } from './database/database.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({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath,
|
||||
}),
|
||||
DatabaseModule,
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -1,14 +1,25 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { AppModule } from './app.module';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
|
||||
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({
|
||||
origin: ['http://localhost:5174'],
|
||||
origin: corsOrigins.length > 1 ? corsOrigins : corsOrigins[0],
|
||||
credentials: true,
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Tenant-Id'],
|
||||
});
|
||||
|
||||
app.useGlobalPipes(
|
||||
@@ -18,6 +29,10 @@ async function bootstrap() {
|
||||
}),
|
||||
);
|
||||
|
||||
app.setGlobalPrefix('api');
|
||||
|
||||
const logger = new Logger('Bootstrap');
|
||||
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle('AeroResolve API')
|
||||
.setDescription('Passenger recovery and compensation intelligence platform')
|
||||
@@ -28,7 +43,17 @@ async function bootstrap() {
|
||||
SwaggerModule.setup('docs', app, document);
|
||||
|
||||
const port = process.env.PORT ?? 3001;
|
||||
const environment = process.env.NODE_ENV ?? 'dev';
|
||||
|
||||
logger.log(`🌍 Environment: ${environment}`);
|
||||
|
||||
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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
"target": "ES2023",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
|
||||
Reference in New Issue
Block a user