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/launch.json
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
|
||||||
|
# Local agent customization
|
||||||
|
.agent.md
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
.env
|
|
||||||
.env.test
|
|
||||||
.env.production
|
|
||||||
.env.local
|
.env.local
|
||||||
|
.env
|
||||||
|
|||||||
@@ -1,30 +1,124 @@
|
|||||||
# Aero Resolve - Backend
|
# AeroResolve Backend
|
||||||
|
test12345
|
||||||
|
NestJS REST API for AeroResolve.
|
||||||
|
|
||||||
This is the backend REST API powered by NestJS.
|
## Current Modules
|
||||||
|
|
||||||
## 🚀 Getting Started
|
| Module | Status | Description |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| `health` | Implemented | Health check |
|
||||||
|
| `database` | Implemented | PostgreSQL / TypeORM setup |
|
||||||
|
| `master-data` | Implemented | CRUD and seed data for lookup categories |
|
||||||
|
| `cohort` | Implemented | Cohort CRUD, pagination, status update |
|
||||||
|
|
||||||
### Prerequisites
|
## Prerequisites
|
||||||
- Node.js (v18+)
|
|
||||||
|
|
||||||
### Installation
|
- Node.js 18+
|
||||||
1. Navigate to the backend directory (if you aren't already here):
|
- npm
|
||||||
```bash
|
- PostgreSQL
|
||||||
cd backend
|
|
||||||
```
|
## Install
|
||||||
2. Install the dependencies using npm:
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development
|
|
||||||
Start the local development server in watch mode:
|
|
||||||
```bash
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Files
|
||||||
|
|
||||||
|
The app loads environment files based on `NODE_ENV`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.env.local
|
||||||
|
.env.dev
|
||||||
|
.env.test
|
||||||
|
.env.uat
|
||||||
|
```
|
||||||
|
|
||||||
|
Required values:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PORT=3001
|
||||||
|
CORS_ORIGIN=http://localhost:5174
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=<postgres-user>
|
||||||
|
DB_PASSWORD=<postgres-password>
|
||||||
|
DB_NAME=<database-name>
|
||||||
|
DB_SYNCHRONIZE=true
|
||||||
|
DB_SSL=false
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Local
|
||||||
|
npm run local
|
||||||
|
npm run start:local
|
||||||
|
|
||||||
|
# Dev
|
||||||
npm run dev
|
npm run dev
|
||||||
|
npm run start:dev
|
||||||
|
|
||||||
|
# UAT
|
||||||
|
npm run uat
|
||||||
|
npm run start:uat
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build for Production
|
## API
|
||||||
To compile the NestJS application:
|
|
||||||
```bash
|
Base URL:
|
||||||
npm run build
|
|
||||||
|
```text
|
||||||
|
http://localhost:3001/api
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Swagger:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:3001/docs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
| Method | Path |
|
||||||
|
|--------|------|
|
||||||
|
| `GET` | `/api/health` |
|
||||||
|
| `GET` | `/api/master-data/:category` |
|
||||||
|
| `POST` | `/api/master-data/:category` |
|
||||||
|
| `GET` | `/api/master-data/:category/:id` |
|
||||||
|
| `PUT` | `/api/master-data/:category/:id` |
|
||||||
|
| `DELETE` | `/api/master-data/:category/:id` |
|
||||||
|
| `GET` | `/api/cohorts?page=1&limit=10` |
|
||||||
|
| `POST` | `/api/cohorts` |
|
||||||
|
| `GET` | `/api/cohorts/:id` |
|
||||||
|
| `PUT` | `/api/cohorts/:id` |
|
||||||
|
| `PATCH` | `/api/cohorts/:id/status` |
|
||||||
|
| `DELETE` | `/api/cohorts/:id` |
|
||||||
|
|
||||||
|
## Seed Master Data
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run seed:local
|
||||||
|
npm run seed:dev
|
||||||
|
npm run seed:test
|
||||||
|
npm run seed:uat
|
||||||
|
```
|
||||||
|
|
||||||
|
Seeded categories:
|
||||||
|
|
||||||
|
- `MEMBERSHIP_TIER`
|
||||||
|
- `CUSTOMER_VALUE`
|
||||||
|
- `REGION`
|
||||||
|
- `TRIP_PURPOSE`
|
||||||
|
- `CABIN_CLASS`
|
||||||
|
- `PASSENGER_TYPE`
|
||||||
|
- `ANCILLARY_PURCHASE`
|
||||||
|
- `REVENUE_SEGMENT`
|
||||||
|
|
||||||
|
## Planned Next
|
||||||
|
|
||||||
|
- Tenant foundation
|
||||||
|
- Auth / RBAC
|
||||||
|
- Audit logging
|
||||||
|
- Policy / regulation engine backend
|
||||||
|
- Compensation engine
|
||||||
|
|||||||
@@ -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,
|
"private": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"dev": "cross-env NODE_ENV=dev nest start",
|
||||||
"dev": "nest start --watch",
|
"uat": "cross-env NODE_ENV=uat nest start",
|
||||||
|
"local": "cross-env NODE_ENV=local nest start",
|
||||||
|
"seed:dev": "cross-env NODE_ENV=dev ts-node -r tsconfig-paths/register scripts/seed-master-data.ts",
|
||||||
|
"seed:local": "cross-env NODE_ENV=local ts-node -r tsconfig-paths/register scripts/seed-master-data.ts",
|
||||||
|
"seed:uat": "cross-env NODE_ENV=uat ts-node -r tsconfig-paths/register scripts/seed-master-data.ts",
|
||||||
|
"seed:test": "cross-env NODE_ENV=test ts-node -r tsconfig-paths/register scripts/seed-master-data.ts",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "cross-env NODE_ENV=dev nest start --watch",
|
||||||
|
"start:uat": "cross-env NODE_ENV=uat nest start --watch",
|
||||||
|
"start:local": "cross-env NODE_ENV=local nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"test": "jest",
|
"test": "cross-env NODE_ENV=test nest start",
|
||||||
"test:watch": "jest --watch",
|
"test:cov": "cross-env NODE_ENV=test jest --coverage",
|
||||||
"test:cov": "jest --coverage",
|
"test:debug": "cross-env NODE_ENV=test node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:e2e": "cross-env NODE_ENV=test jest --config ./test/jest-e2e.json"
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
@@ -26,10 +32,13 @@
|
|||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/swagger": "^11.2.0",
|
"@nestjs/swagger": "^11.2.0",
|
||||||
|
"@nestjs/typeorm": "^11.0.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
|
"pg": "^8.22.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1",
|
||||||
|
"typeorm": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
@@ -41,6 +50,7 @@
|
|||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^24.0.0",
|
||||||
"@types/supertest": "^7.0.0",
|
"@types/supertest": "^7.0.0",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-prettier": "^5.2.2",
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
|||||||
@@ -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 { ConfigModule } from '@nestjs/config';
|
||||||
|
import { DatabaseModule } from './database/database.module';
|
||||||
import { HealthModule } from './modules/health/health.module';
|
import { HealthModule } from './modules/health/health.module';
|
||||||
import { DisruptionModule } from './modules/disruption/disruption.module';
|
import { MasterDataModule } from './modules/master-data/master-data.module';
|
||||||
|
import { CohortModule } from './modules/cohort/cohort.module';
|
||||||
|
import { TenantModule } from './modules/tenant/tenant.module';
|
||||||
|
import { TenantMiddleware } from './common/tenant/tenant.middleware';
|
||||||
|
import { PolicyEngineModule } from './modules/policy-engine/policy-engine.module';
|
||||||
|
|
||||||
|
const env = process.env.NODE_ENV;
|
||||||
|
const envFilePath = env ? [`.env.${env}`, '.env.local', '.env'] : ['.env.local', '.env'];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
envFilePath,
|
||||||
|
}),
|
||||||
|
DatabaseModule,
|
||||||
HealthModule,
|
HealthModule,
|
||||||
DisruptionModule,
|
TenantModule,
|
||||||
|
MasterDataModule,
|
||||||
|
CohortModule,
|
||||||
|
PolicyEngineModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule implements NestModule {
|
||||||
|
configure(consumer: MiddlewareConsumer) {
|
||||||
|
consumer.apply(TenantMiddleware).forRoutes('*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Column, Index, JoinColumn, ManyToOne } from 'typeorm';
|
||||||
|
import { Tenant } from '../../modules/tenant/tenant.entity';
|
||||||
|
|
||||||
|
export abstract class TenantOwnedEntity {
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
@Index()
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Tenant, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'tenantId' })
|
||||||
|
tenant: Tenant;
|
||||||
|
}
|
||||||
@@ -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 { NestFactory } from '@nestjs/core';
|
||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule, {
|
||||||
|
logger: ['log', 'warn', 'error'],
|
||||||
|
});
|
||||||
|
const configService = app.get(ConfigService);
|
||||||
|
const dbHost = configService.get('DB_HOST');
|
||||||
|
const dbPort = configService.get('DB_PORT') ?? '5432';
|
||||||
|
const corsOrigin = configService.get<string>('CORS_ORIGIN') ?? 'http://localhost:5174';
|
||||||
|
const corsOrigins = corsOrigin.split(',').map(origin => origin.trim()).filter(Boolean);
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: ['http://localhost:5174'],
|
origin: corsOrigins.length > 1 ? corsOrigins : corsOrigins[0],
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-Tenant-Id'],
|
||||||
});
|
});
|
||||||
|
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
@@ -18,6 +29,10 @@ async function bootstrap() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.setGlobalPrefix('api');
|
||||||
|
|
||||||
|
const logger = new Logger('Bootstrap');
|
||||||
|
|
||||||
const swaggerConfig = new DocumentBuilder()
|
const swaggerConfig = new DocumentBuilder()
|
||||||
.setTitle('AeroResolve API')
|
.setTitle('AeroResolve API')
|
||||||
.setDescription('Passenger recovery and compensation intelligence platform')
|
.setDescription('Passenger recovery and compensation intelligence platform')
|
||||||
@@ -28,7 +43,17 @@ async function bootstrap() {
|
|||||||
SwaggerModule.setup('docs', app, document);
|
SwaggerModule.setup('docs', app, document);
|
||||||
|
|
||||||
const port = process.env.PORT ?? 3001;
|
const port = process.env.PORT ?? 3001;
|
||||||
|
const environment = process.env.NODE_ENV ?? 'dev';
|
||||||
|
|
||||||
|
logger.log(`🌍 Environment: ${environment}`);
|
||||||
|
|
||||||
await app.listen(port);
|
await app.listen(port);
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
logger.log('🚀 AeroResolve API started successfully');
|
||||||
|
logger.log(`🌐 Server : http://localhost:${port}`);
|
||||||
|
logger.log(`📚 Swagger : http://localhost:${port}/docs`);
|
||||||
|
logger.log(`🗄️ Database : PostgreSQL (${dbHost}:${dbPort})`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -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",
|
"target": "ES2023",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"baseUrl": "./",
|
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user