feat(master-data): add jurisdiction and rule category entities with CRUD operations
- Introduced Jurisdiction and RuleCategory entities along with their respective values. - Implemented service methods for managing rule categories and values. - Updated MasterDataService to include seeding logic for jurisdictions and rule categories. - Added DTOs for creating and updating rule categories and values. feat(policy-engine): enhance policy management with new entities and operations - Created Policy, PolicyRule, PolicyAction, PolicyTargetAudience, and RuleCondition entities. - Implemented create, update, and delete operations for policies and their associated rules and actions. - Added enums for PolicyStatus and AudienceType to manage policy states and target audiences. - Developed DTOs for creating and updating policies with nested structures for rules and audiences.
This commit is contained in:
@@ -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 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 ────────────────────────────────────────────────────────────────
|
-- ─── Cohorts ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS cohort.tbl_cohorts (
|
CREATE TABLE IF NOT EXISTS cohort.tbl_cohorts (
|
||||||
|
|||||||
Generated
+7
-7
@@ -2394,14 +2394,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nestjs/platform-express": {
|
"node_modules/@nestjs/platform-express": {
|
||||||
"version": "11.1.27",
|
"version": "11.1.28",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.27.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.28.tgz",
|
||||||
"integrity": "sha512-0ZFhz6H6EdGh4xQVbUNwjoAwBuz73P7FvUAl67h9CTdMqQlJDaQYJApBv8pKfVZ1fGjMCbl0m9DcC6pXaZPWSQ==",
|
"integrity": "sha512-hU+9Sz4m+onHrR5AmelI59QKmY/Re546bPnygnpqqeQdHDiJpBgjWbL4t6Jr73CBpS60cpyng7WzjgphNB9iwA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "2.8.6",
|
"cors": "2.8.6",
|
||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
"multer": "2.1.1",
|
"multer": "2.2.0",
|
||||||
"path-to-regexp": "8.4.2",
|
"path-to-regexp": "8.4.2",
|
||||||
"tslib": "2.8.1"
|
"tslib": "2.8.1"
|
||||||
},
|
},
|
||||||
@@ -7873,9 +7873,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/multer": {
|
"node_modules/multer": {
|
||||||
"version": "2.1.1",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz",
|
||||||
"integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
|
"integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"append-field": "^1.0.0",
|
"append-field": "^1.0.0",
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ export class TenantMiddleware implements NestMiddleware {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const tenantHeader = req.headers['x-tenant-id'];
|
let tenantHeader = req.headers['x-tenant-id'];
|
||||||
if (!tenantHeader || Array.isArray(tenantHeader)) {
|
if (!tenantHeader || Array.isArray(tenantHeader)) {
|
||||||
throw new BadRequestException('Missing X-Tenant-Id header');
|
tenantHeader = 'demo-airline';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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 "tenant";`);
|
||||||
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "masters";`);
|
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 "cohort";`);
|
||||||
|
await dataSource.query(`CREATE SCHEMA IF NOT EXISTS "policy_engine";`);
|
||||||
|
|
||||||
// Run synchronization manually if it was enabled
|
// Run synchronization manually if it was enabled
|
||||||
if (options.synchronize) {
|
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 { 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 { MasterDataService } from './master-data.service';
|
||||||
import { CreateMasterDataDto } from './dto/create-master-data.dto';
|
import { CreateMasterDataDto } from './dto/create-master-data.dto';
|
||||||
import { UpdateMasterDataDto } from './dto/update-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')
|
@Controller('master-data')
|
||||||
export class MasterDataController {
|
export class MasterDataController {
|
||||||
constructor(private readonly masterDataService: MasterDataService) {}
|
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
|
// Example: POST /master-data/REGION
|
||||||
@Post(':category')
|
@Post(':category')
|
||||||
async create(
|
async create(
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import { CabinClass } from './entities/cabin-class.entity';
|
|||||||
import { PassengerType } from './entities/passenger-type.entity';
|
import { PassengerType } from './entities/passenger-type.entity';
|
||||||
import { AncillaryPurchase } from './entities/ancillary-purchase.entity';
|
import { AncillaryPurchase } from './entities/ancillary-purchase.entity';
|
||||||
import { RevenueSegment } from './entities/revenue-segment.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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([
|
TypeOrmModule.forFeature([
|
||||||
@@ -22,10 +26,14 @@ import { RevenueSegment } from './entities/revenue-segment.entity';
|
|||||||
PassengerType,
|
PassengerType,
|
||||||
AncillaryPurchase,
|
AncillaryPurchase,
|
||||||
RevenueSegment,
|
RevenueSegment,
|
||||||
|
Jurisdiction,
|
||||||
|
RuleCategory,
|
||||||
|
RuleCategoryValue,
|
||||||
|
Operator,
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
controllers: [MasterDataController],
|
controllers: [MasterDataController],
|
||||||
providers: [MasterDataService],
|
providers: [MasterDataService],
|
||||||
exports: [MasterDataService],
|
exports: [MasterDataService, TypeOrmModule],
|
||||||
})
|
})
|
||||||
export class MasterDataModule {}
|
export class MasterDataModule {}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import { CabinClass } from './entities/cabin-class.entity';
|
|||||||
import { PassengerType } from './entities/passenger-type.entity';
|
import { PassengerType } from './entities/passenger-type.entity';
|
||||||
import { AncillaryPurchase } from './entities/ancillary-purchase.entity';
|
import { AncillaryPurchase } from './entities/ancillary-purchase.entity';
|
||||||
import { RevenueSegment } from './entities/revenue-segment.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()
|
@Injectable()
|
||||||
export class MasterDataService {
|
export class MasterDataService {
|
||||||
@@ -25,6 +29,10 @@ export class MasterDataService {
|
|||||||
@InjectRepository(PassengerType) private passengerTypeRepo: Repository<PassengerType>,
|
@InjectRepository(PassengerType) private passengerTypeRepo: Repository<PassengerType>,
|
||||||
@InjectRepository(AncillaryPurchase) private ancillaryPurchaseRepo: Repository<AncillaryPurchase>,
|
@InjectRepository(AncillaryPurchase) private ancillaryPurchaseRepo: Repository<AncillaryPurchase>,
|
||||||
@InjectRepository(RevenueSegment) private revenueSegmentRepo: Repository<RevenueSegment>,
|
@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 {
|
private resolveTenantId(tenantId?: string): string {
|
||||||
@@ -41,6 +49,10 @@ export class MasterDataService {
|
|||||||
case 'PASSENGER_TYPE': return this.passengerTypeRepo;
|
case 'PASSENGER_TYPE': return this.passengerTypeRepo;
|
||||||
case 'ANCILLARY_PURCHASE': return this.ancillaryPurchaseRepo;
|
case 'ANCILLARY_PURCHASE': return this.ancillaryPurchaseRepo;
|
||||||
case 'REVENUE_SEGMENT': return this.revenueSegmentRepo;
|
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:
|
default:
|
||||||
throw new BadRequestException(`Invalid category: ${category}`);
|
throw new BadRequestException(`Invalid category: ${category}`);
|
||||||
}
|
}
|
||||||
@@ -85,6 +97,114 @@ export class MasterDataService {
|
|||||||
await repo.remove(item);
|
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) {
|
public async seedData(tenantId?: string) {
|
||||||
const resolvedTenantId = this.resolveTenantId(tenantId);
|
const resolvedTenantId = this.resolveTenantId(tenantId);
|
||||||
this.logger.log(`Checking if master data needs seeding for tenant ${resolvedTenantId}...`);
|
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));
|
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}`);
|
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;
|
||||||
|
}
|
||||||
@@ -1,7 +1,66 @@
|
|||||||
import { Controller } from '@nestjs/common';
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Delete,
|
||||||
|
Patch,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { PolicyEngineService } from './policy-engine.service';
|
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')
|
@Controller('policy-engine')
|
||||||
export class PolicyEngineController {
|
export class PolicyEngineController {
|
||||||
constructor(private readonly policyEngineService: PolicyEngineService) {}
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { PolicyEngineService } from './policy-engine.service';
|
import { PolicyEngineService } from './policy-engine.service';
|
||||||
import { PolicyEngineController } from './policy-engine.controller';
|
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({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
Policy,
|
||||||
|
PolicyTargetAudience,
|
||||||
|
PolicyRule,
|
||||||
|
RuleCondition,
|
||||||
|
PolicyAction,
|
||||||
|
]),
|
||||||
|
MasterDataModule,
|
||||||
|
],
|
||||||
controllers: [PolicyEngineController],
|
controllers: [PolicyEngineController],
|
||||||
providers: [PolicyEngineService],
|
providers: [PolicyEngineService],
|
||||||
exports: [PolicyEngineService],
|
exports: [PolicyEngineService],
|
||||||
|
|||||||
@@ -1,4 +1,173 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
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()
|
@Injectable()
|
||||||
export class PolicyEngineService {}
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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