Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5b360bf56 | ||
|
|
4ba1316289 | ||
|
|
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
|
||||
|
||||
This is the backend REST API powered by NestJS.
|
||||
NestJS REST API for AeroResolve.
|
||||
|
||||
## 🚀 Getting Started
|
||||
## Current Modules
|
||||
|
||||
### Prerequisites
|
||||
- Node.js (v18+)
|
||||
| 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 |
|
||||
|
||||
### 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
|
||||
```
|
||||
## Prerequisites
|
||||
|
||||
- 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,209 @@
|
||||
-- =============================================================================
|
||||
-- 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");
|
||||
|
||||
-- ─── 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
+541
-109
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);
|
||||
});
|
||||
+22
-5
@@ -1,13 +1,30 @@
|
||||
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';
|
||||
|
||||
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,
|
||||
],
|
||||
})
|
||||
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();
|
||||
}
|
||||
|
||||
const tenantHeader = req.headers['x-tenant-id'];
|
||||
if (!tenantHeader || Array.isArray(tenantHeader)) {
|
||||
throw new BadRequestException('Missing X-Tenant-Id header');
|
||||
}
|
||||
|
||||
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,47 @@
|
||||
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";`);
|
||||
|
||||
// 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,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateMasterDataDto } from './create-master-data.dto';
|
||||
|
||||
export class UpdateMasterDataDto extends PartialType(CreateMasterDataDto) {}
|
||||
@@ -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_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,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,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,52 @@
|
||||
import { Controller, Get, Post, Put, Delete, Param, Body } from '@nestjs/common';
|
||||
import { MasterDataService } from './master-data.service';
|
||||
import { CreateMasterDataDto } from './dto/create-master-data.dto';
|
||||
import { UpdateMasterDataDto } from './dto/update-master-data.dto';
|
||||
|
||||
@Controller('master-data')
|
||||
export class MasterDataController {
|
||||
constructor(private readonly masterDataService: MasterDataService) {}
|
||||
|
||||
// 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,31 @@
|
||||
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';
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
MembershipTier,
|
||||
CustomerValue,
|
||||
Region,
|
||||
TripPurpose,
|
||||
CabinClass,
|
||||
PassengerType,
|
||||
AncillaryPurchase,
|
||||
RevenueSegment,
|
||||
]),
|
||||
],
|
||||
controllers: [MasterDataController],
|
||||
providers: [MasterDataService],
|
||||
exports: [MasterDataService],
|
||||
})
|
||||
export class MasterDataModule {}
|
||||
@@ -0,0 +1,189 @@
|
||||
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';
|
||||
|
||||
@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>,
|
||||
) {}
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
this.logger.log(`Master data seeded for tenant ${resolvedTenantId}`);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user