Merge pull request 'waseem' (#8) from waseem into development

Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_backend/pulls/8
This commit is contained in:
Syed Waseem khadri Rafai
2026-07-21 05:54:34 +00:00
30 changed files with 1582 additions and 11 deletions
+41
View File
@@ -146,6 +146,47 @@ CREATE TABLE IF NOT EXISTS masters.tbl_revenue_segments (
);
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 (
+7 -7
View File
@@ -2394,14 +2394,14 @@
}
},
"node_modules/@nestjs/platform-express": {
"version": "11.1.27",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.27.tgz",
"integrity": "sha512-0ZFhz6H6EdGh4xQVbUNwjoAwBuz73P7FvUAl67h9CTdMqQlJDaQYJApBv8pKfVZ1fGjMCbl0m9DcC6pXaZPWSQ==",
"version": "11.1.28",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.28.tgz",
"integrity": "sha512-hU+9Sz4m+onHrR5AmelI59QKmY/Re546bPnygnpqqeQdHDiJpBgjWbL4t6Jr73CBpS60cpyng7WzjgphNB9iwA==",
"license": "MIT",
"dependencies": {
"cors": "2.8.6",
"express": "5.2.1",
"multer": "2.1.1",
"multer": "2.2.0",
"path-to-regexp": "8.4.2",
"tslib": "2.8.1"
},
@@ -7873,9 +7873,9 @@
"license": "MIT"
},
"node_modules/multer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
"integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz",
"integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
+2
View File
@@ -6,6 +6,7 @@ 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'];
@@ -21,6 +22,7 @@ const envFilePath = env ? [`.env.${env}`, '.env.local', '.env'] : ['.env.local',
TenantModule,
MasterDataModule,
CohortModule,
PolicyEngineModule,
],
})
export class AppModule implements NestModule {
+2 -2
View File
@@ -17,9 +17,9 @@ export class TenantMiddleware implements NestMiddleware {
return next();
}
const tenantHeader = req.headers['x-tenant-id'];
let tenantHeader = req.headers['x-tenant-id'];
if (!tenantHeader || Array.isArray(tenantHeader)) {
throw new BadRequestException('Missing X-Tenant-Id header');
tenantHeader = 'demo-airline';
}
try {
+1
View File
@@ -33,6 +33,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
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) {
@@ -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 { 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_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,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,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;
}
@@ -1,12 +1,249 @@
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(
@@ -11,6 +11,10 @@ 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([
@@ -22,10 +26,14 @@ import { RevenueSegment } from './entities/revenue-segment.entity';
PassengerType,
AncillaryPurchase,
RevenueSegment,
Jurisdiction,
RuleCategory,
RuleCategoryValue,
Operator,
]),
],
controllers: [MasterDataController],
providers: [MasterDataService],
exports: [MasterDataService],
exports: [MasterDataService, TypeOrmModule],
})
export class MasterDataModule {}
@@ -11,6 +11,10 @@ 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 {
@@ -25,6 +29,10 @@ export class MasterDataService {
@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 {
@@ -41,6 +49,10 @@ export class MasterDataService {
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}`);
}
@@ -85,6 +97,114 @@ export class MasterDataService {
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}...`);
@@ -184,6 +304,339 @@ export class MasterDataService {
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);
}
}
-1
View File
@@ -13,7 +13,6 @@
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,