feat: implement master data entities and initial database schema for the policy engine module
This commit is contained in:
+97
-1
@@ -20,6 +20,7 @@ CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
CREATE SCHEMA IF NOT EXISTS tenant;
|
||||
CREATE SCHEMA IF NOT EXISTS masters;
|
||||
CREATE SCHEMA IF NOT EXISTS cohort;
|
||||
CREATE SCHEMA IF NOT EXISTS policy_engine;
|
||||
|
||||
-- ─── Enum Types ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -435,11 +436,27 @@ CREATE TABLE IF NOT EXISTS masters.tbl_action_submissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"categoryId" UUID NOT NULL REFERENCES masters.tbl_action_categories(id) ON DELETE CASCADE,
|
||||
"actionTypeId" UUID NOT NULL REFERENCES masters.tbl_action_types(id) ON DELETE CASCADE,
|
||||
data JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS masters.tbl_action_submission_values (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"submissionId" UUID NOT NULL REFERENCES masters.tbl_action_submissions(id) ON DELETE CASCADE,
|
||||
"fieldDefinitionId" UUID REFERENCES masters.tbl_field_definitions(id) ON DELETE CASCADE,
|
||||
"fieldCode" VARCHAR,
|
||||
"valueIndex" INT NOT NULL DEFAULT 0,
|
||||
"selectedValueId" VARCHAR,
|
||||
"textValue" TEXT,
|
||||
"numberValue" NUMERIC,
|
||||
"booleanValue" BOOLEAN,
|
||||
"dateValue" DATE,
|
||||
"timeValue" TIME,
|
||||
"timestampValue" TIMESTAMP WITH TIME ZONE,
|
||||
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS masters.tbl_refund_bases (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
label VARCHAR NOT NULL,
|
||||
@@ -468,6 +485,85 @@ CREATE TABLE IF NOT EXISTS masters.tbl_refund_methods (
|
||||
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS masters.tbl_amount_types (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
label VARCHAR NOT NULL,
|
||||
value VARCHAR NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ─── Policy Engine ──────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS policy_engine.policies (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"tenantId" UUID NOT NULL REFERENCES tenant.tbl_tenants(id) ON DELETE CASCADE,
|
||||
policy_name VARCHAR(200) NOT NULL,
|
||||
jurisdiction_id UUID REFERENCES masters.tbl_jurisdictions(id) ON DELETE SET NULL,
|
||||
description TEXT,
|
||||
status VARCHAR NOT NULL DEFAULT 'draft',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
audience_type VARCHAR NOT NULL DEFAULT 'ALL',
|
||||
created_by UUID,
|
||||
updated_by UUID,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS policy_engine.policy_target_audiences (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
policy_id UUID NOT NULL REFERENCES policy_engine.policies(id) ON DELETE CASCADE,
|
||||
target_type VARCHAR NOT NULL,
|
||||
target_id UUID
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS policy_engine.policy_rules (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
policy_id UUID NOT NULL REFERENCES policy_engine.policies(id) ON DELETE CASCADE,
|
||||
rule_category_id UUID REFERENCES masters.tbl_rule_categories(id) ON DELETE SET NULL,
|
||||
priority INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS policy_engine.rule_conditions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
rule_id UUID NOT NULL REFERENCES policy_engine.policy_rules(id) ON DELETE CASCADE,
|
||||
field_id UUID NOT NULL,
|
||||
operator_id UUID NOT NULL,
|
||||
value_text TEXT,
|
||||
logical_operator VARCHAR NOT NULL DEFAULT 'AND',
|
||||
sequence INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS policy_engine.policy_actions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
rule_id UUID NOT NULL REFERENCES policy_engine.policy_rules(id) ON DELETE CASCADE,
|
||||
action_type_id UUID NOT NULL REFERENCES masters.tbl_action_types(id) ON DELETE CASCADE,
|
||||
sequence INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS policy_engine.policy_action_values (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
action_id UUID NOT NULL REFERENCES policy_engine.policy_actions(id) ON DELETE CASCADE,
|
||||
field_definition_id UUID REFERENCES masters.tbl_field_definitions(id) ON DELETE SET NULL,
|
||||
field_code VARCHAR NOT NULL,
|
||||
value_index INT NOT NULL DEFAULT 0,
|
||||
selected_value_id VARCHAR,
|
||||
text_value TEXT,
|
||||
number_value NUMERIC,
|
||||
boolean_value BOOLEAN,
|
||||
date_value DATE,
|
||||
time_value TIME,
|
||||
timestamp_value TIMESTAMP WITH TIME ZONE,
|
||||
currency_code_id UUID,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_action_values_action
|
||||
ON policy_engine.policy_action_values(action_id);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -260,7 +260,8 @@ SELECT code, name, "tableName", description, "displayOrder", true FROM (VALUES
|
||||
('region', 'Region', 'tbl_regions', 'Region master', 30),
|
||||
('revenue-segment', 'Revenue Segment', 'tbl_revenue_segments', 'Revenue Segment master', 31),
|
||||
('jurisdiction', 'Jurisdiction', 'tbl_jurisdictions', 'Jurisdiction master', 32),
|
||||
('operator', 'Operator', 'tbl_operators', 'Operator master', 33)
|
||||
('operator', 'Operator', 'tbl_operators', 'Operator master', 33),
|
||||
('amount-type', 'Amount Type', 'tbl_amount_types', 'Amount Type master', 34)
|
||||
) AS t(code, name, "tableName", description, "displayOrder")
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters.tbl_rules_categories WHERE masters.tbl_rules_categories.code = t.code);
|
||||
|
||||
@@ -300,7 +301,8 @@ FROM (VALUES
|
||||
('region', 'tbl_regions'),
|
||||
('revenue-segment', 'tbl_revenue_segments'),
|
||||
('jurisdiction', 'tbl_jurisdictions'),
|
||||
('operator', 'tbl_operators')
|
||||
('operator', 'tbl_operators'),
|
||||
('amount-type', 'tbl_amount_types')
|
||||
) AS v(code, "tableName")
|
||||
WHERE c.code = v.code AND (c."tableName" IS NULL OR c."tableName" != v."tableName");
|
||||
|
||||
@@ -620,5 +622,14 @@ SELECT label, value, true FROM (VALUES
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters.tbl_refund_methods WHERE masters.tbl_refund_methods.value = t.value);
|
||||
|
||||
-- 37. Amount Types
|
||||
INSERT INTO masters.tbl_amount_types (label, value, "isActive")
|
||||
SELECT label, value, true FROM (VALUES
|
||||
('Fixed', 'fixed'),
|
||||
('Percentage', 'percentage'),
|
||||
('Formula', 'formula')
|
||||
) AS t(label, value)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM masters.tbl_amount_types WHERE masters.tbl_amount_types.value = t.value);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,58 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsObject } from 'class-validator';
|
||||
import { IsString, IsNotEmpty, IsObject, IsOptional, IsNumber, IsBoolean, IsArray, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class ActionSubmissionValueDto {
|
||||
@ApiProperty({ description: 'Field Definition ID', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
field_definition_id?: string;
|
||||
|
||||
@ApiProperty({ description: 'Field Code', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
field_code?: string;
|
||||
|
||||
@ApiProperty({ description: 'Value index for array inputs', required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
value_index?: number;
|
||||
|
||||
@ApiProperty({ description: 'Selected value ID for dropdowns and lookups', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
selected_value_id?: string;
|
||||
|
||||
@ApiProperty({ description: 'Text value', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
text_value?: string;
|
||||
|
||||
@ApiProperty({ description: 'Numeric value', required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
number_value?: number;
|
||||
|
||||
@ApiProperty({ description: 'Boolean value', required: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
boolean_value?: boolean;
|
||||
|
||||
@ApiProperty({ description: 'Date string (YYYY-MM-DD)', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
date_value?: string;
|
||||
|
||||
@ApiProperty({ description: 'Time string (HH:MM)', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
time_value?: string;
|
||||
|
||||
@ApiProperty({ description: 'Timestamp ISO string', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
timestamp_value?: string;
|
||||
}
|
||||
|
||||
export class ActionSubmissionDto {
|
||||
@ApiProperty({ description: 'Category ID', example: 'cat_comp' })
|
||||
@@ -12,8 +65,15 @@ export class ActionSubmissionDto {
|
||||
@IsNotEmpty()
|
||||
action_type_id!: string;
|
||||
|
||||
@ApiProperty({ description: 'Form submitted dynamic data dictionary' })
|
||||
@ApiProperty({ description: 'List of dynamic submission values', required: false, type: [ActionSubmissionValueDto] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ActionSubmissionValueDto)
|
||||
values?: ActionSubmissionValueDto[];
|
||||
|
||||
@ApiProperty({ description: 'Form submitted dynamic data dictionary (fallback)', required: false })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@IsNotEmpty()
|
||||
data!: Record<string, any>;
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { ActionSubmission } from './action-submission.entity';
|
||||
import { FieldDefinition } from './field-definition.entity';
|
||||
|
||||
@Entity({ name: 'tbl_action_submission_values', schema: 'masters' })
|
||||
export class ActionSubmissionValue {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
submissionId!: string;
|
||||
|
||||
@ManyToOne(() => ActionSubmission, (submission) => submission.values, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'submissionId' })
|
||||
submission?: ActionSubmission;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
fieldDefinitionId?: string;
|
||||
|
||||
@ManyToOne(() => FieldDefinition, { onDelete: 'CASCADE', nullable: true })
|
||||
@JoinColumn({ name: 'fieldDefinitionId' })
|
||||
fieldDefinition?: FieldDefinition;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
fieldCode?: string;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
valueIndex!: number;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
selectedValueId?: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
textValue?: string;
|
||||
|
||||
@Column({ type: 'numeric', nullable: true })
|
||||
numberValue?: number;
|
||||
|
||||
@Column({ type: 'boolean', nullable: true })
|
||||
booleanValue?: boolean;
|
||||
|
||||
@Column({ type: 'date', nullable: true })
|
||||
dateValue?: string;
|
||||
|
||||
@Column({ type: 'time', nullable: true })
|
||||
timeValue?: string;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
timestampValue?: Date;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: true })
|
||||
currencyCode?: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@ import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { ActionCategory } from './action-category.entity';
|
||||
import { ActionType } from './action-type.entity';
|
||||
import { ActionSubmissionValue } from './action-submission-value.entity';
|
||||
|
||||
@Entity({ name: 'tbl_action_submissions', schema: 'masters' })
|
||||
export class ActionSubmission {
|
||||
@@ -14,11 +20,19 @@ export class ActionSubmission {
|
||||
@Column({ type: 'uuid' })
|
||||
categoryId!: string;
|
||||
|
||||
@ManyToOne(() => ActionCategory, { onDelete: 'CASCADE', nullable: true })
|
||||
@JoinColumn({ name: 'categoryId' })
|
||||
category?: ActionCategory;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
actionTypeId!: string;
|
||||
|
||||
@Column({ type: 'jsonb' })
|
||||
data!: Record<string, any>;
|
||||
@ManyToOne(() => ActionType, { onDelete: 'CASCADE', nullable: true })
|
||||
@JoinColumn({ name: 'actionTypeId' })
|
||||
actionType?: ActionType;
|
||||
|
||||
@OneToMany(() => ActionSubmissionValue, (val) => val.submission, { cascade: true })
|
||||
values!: ActionSubmissionValue[];
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tbl_amount_types', schema: 'masters' })
|
||||
export class AmountType {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
label!: string;
|
||||
|
||||
@Column()
|
||||
value!: string;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -330,6 +330,12 @@ export class MasterDataController {
|
||||
return this.masterDataService.findAll('REFUND_METHOD');
|
||||
}
|
||||
|
||||
@Get('amount-types')
|
||||
@ApiOperation({ summary: 'Get all amount types' })
|
||||
async findAllAmountTypes() {
|
||||
return this.masterDataService.findAll('AMOUNT_TYPE');
|
||||
}
|
||||
|
||||
// --- Generic Master Data Endpoints ---
|
||||
|
||||
@Post(':category')
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ActionCategory } from './entities/action-category.entity';
|
||||
import { ActionType } from './entities/action-type.entity';
|
||||
import { FieldDefinition } from './entities/field-definition.entity';
|
||||
import { ActionSubmission } from './entities/action-submission.entity';
|
||||
import { ActionSubmissionValue } from './entities/action-submission-value.entity';
|
||||
|
||||
import { BookingChannel } from './entities/booking-channel.entity';
|
||||
import { FlightType } from './entities/flight-type.entity';
|
||||
@@ -42,6 +43,7 @@ import { TechnicalFaultCategory } from './entities/technical-fault-category.enti
|
||||
import { RefundBasis } from './entities/refund-basis.entity';
|
||||
import { Currency } from './entities/currency.entity';
|
||||
import { RefundMethod } from './entities/refund-method.entity';
|
||||
import { AmountType } from './entities/amount-type.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -61,6 +63,7 @@ import { RefundMethod } from './entities/refund-method.entity';
|
||||
ActionType,
|
||||
FieldDefinition,
|
||||
ActionSubmission,
|
||||
ActionSubmissionValue,
|
||||
BookingChannel,
|
||||
FlightType,
|
||||
JourneyType,
|
||||
@@ -84,6 +87,7 @@ import { RefundMethod } from './entities/refund-method.entity';
|
||||
RefundBasis,
|
||||
Currency,
|
||||
RefundMethod,
|
||||
AmountType,
|
||||
]),
|
||||
],
|
||||
controllers: [MasterDataController],
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ActionCategory } from './entities/action-category.entity';
|
||||
import { ActionType } from './entities/action-type.entity';
|
||||
import { FieldDefinition } from './entities/field-definition.entity';
|
||||
import { ActionSubmission } from './entities/action-submission.entity';
|
||||
import { ActionSubmissionValue } from './entities/action-submission-value.entity';
|
||||
|
||||
import { BookingChannel } from './entities/booking-channel.entity';
|
||||
import { FlightType } from './entities/flight-type.entity';
|
||||
@@ -43,6 +44,7 @@ import { TechnicalFaultCategory } from './entities/technical-fault-category.enti
|
||||
import { RefundBasis } from './entities/refund-basis.entity';
|
||||
import { Currency } from './entities/currency.entity';
|
||||
import { RefundMethod } from './entities/refund-method.entity';
|
||||
import { AmountType } from './entities/amount-type.entity';
|
||||
|
||||
import { CreateActionCategoryDto } from './dto/create-action-category.dto';
|
||||
import { UpdateActionCategoryDto } from './dto/update-action-category.dto';
|
||||
@@ -73,6 +75,7 @@ export class MasterDataService implements OnModuleInit {
|
||||
@InjectRepository(ActionType) private actionTypeRepo: Repository<ActionType>,
|
||||
@InjectRepository(FieldDefinition) private fieldDefinitionRepo: Repository<FieldDefinition>,
|
||||
@InjectRepository(ActionSubmission) private actionSubmissionRepo: Repository<ActionSubmission>,
|
||||
@InjectRepository(ActionSubmissionValue) private actionSubmissionValueRepo: Repository<ActionSubmissionValue>,
|
||||
|
||||
@InjectRepository(BookingChannel) private bookingChannelRepo: Repository<BookingChannel>,
|
||||
@InjectRepository(FlightType) private flightTypeRepo: Repository<FlightType>,
|
||||
@@ -97,7 +100,8 @@ export class MasterDataService implements OnModuleInit {
|
||||
@InjectRepository(RefundBasis) private refundBasisRepo: Repository<RefundBasis>,
|
||||
@InjectRepository(Currency) private currencyRepo: Repository<Currency>,
|
||||
@InjectRepository(RefundMethod) private refundMethodRepo: Repository<RefundMethod>,
|
||||
) {}
|
||||
@InjectRepository(AmountType) private amountTypeRepo: Repository<AmountType>,
|
||||
) { }
|
||||
|
||||
private getRepositoryByCategory(category: string): Repository<any> {
|
||||
const key = category?.toUpperCase().replace(/-/g, '_');
|
||||
@@ -243,6 +247,11 @@ export class MasterDataService implements OnModuleInit {
|
||||
case 'TBL_REFUND_METHODS':
|
||||
return this.refundMethodRepo;
|
||||
|
||||
case 'AMOUNT_TYPE':
|
||||
case 'AMOUNT_TYPES':
|
||||
case 'TBL_AMOUNT_TYPES':
|
||||
return this.amountTypeRepo;
|
||||
|
||||
default:
|
||||
throw new BadRequestException(`Invalid category: ${category}`);
|
||||
}
|
||||
@@ -319,12 +328,18 @@ export class MasterDataService implements OnModuleInit {
|
||||
}
|
||||
|
||||
async findRuleCategoryValuesByCode(code: string): Promise<any[]> {
|
||||
const category = await this.ruleCategoryRepo.findOne({
|
||||
let category = await this.ruleCategoryRepo.findOne({
|
||||
where: { code },
|
||||
});
|
||||
|
||||
if (!category && code && code.includes('-')) {
|
||||
category = await this.ruleCategoryRepo.findOne({
|
||||
where: { id: code },
|
||||
});
|
||||
}
|
||||
|
||||
if (!category) {
|
||||
throw new BadRequestException(`Rule category with code ${code} not found`);
|
||||
throw new BadRequestException(`Rule category with code or id ${code} not found`);
|
||||
}
|
||||
|
||||
const targetKey = category.tableName || category.code;
|
||||
@@ -514,10 +529,54 @@ export class MasterDataService implements OnModuleInit {
|
||||
const submission = this.actionSubmissionRepo.create({
|
||||
categoryId: submissionDto.category_id,
|
||||
actionTypeId: submissionDto.action_type_id,
|
||||
data: submissionDto.data,
|
||||
});
|
||||
|
||||
return this.actionSubmissionRepo.save(submission);
|
||||
const savedSubmission = await this.actionSubmissionRepo.save(submission);
|
||||
|
||||
const valuesToInsert: Partial<ActionSubmissionValue>[] = [];
|
||||
|
||||
if (submissionDto.values && submissionDto.values.length > 0) {
|
||||
for (const val of submissionDto.values) {
|
||||
valuesToInsert.push({
|
||||
submissionId: savedSubmission.id,
|
||||
fieldDefinitionId: val.field_definition_id || undefined,
|
||||
fieldCode: val.field_code || undefined,
|
||||
valueIndex: val.value_index ?? 0,
|
||||
selectedValueId: val.selected_value_id || undefined,
|
||||
textValue: val.text_value !== undefined ? String(val.text_value) : undefined,
|
||||
numberValue: val.number_value,
|
||||
booleanValue: val.boolean_value,
|
||||
dateValue: val.date_value,
|
||||
timeValue: val.time_value,
|
||||
timestampValue: val.time_value ? new Date(val.time_value) : undefined,
|
||||
});
|
||||
}
|
||||
} else if (submissionDto.data) {
|
||||
Object.entries(submissionDto.data).forEach(([key, val]) => {
|
||||
valuesToInsert.push({
|
||||
submissionId: savedSubmission.id,
|
||||
fieldCode: key,
|
||||
valueIndex: 0,
|
||||
textValue: typeof val === 'object' ? JSON.stringify(val) : String(val),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (valuesToInsert.length > 0) {
|
||||
const valueEntities = this.actionSubmissionValueRepo.create(valuesToInsert);
|
||||
await this.actionSubmissionValueRepo.save(valueEntities);
|
||||
}
|
||||
|
||||
const result = await this.actionSubmissionRepo.findOne({
|
||||
where: { id: savedSubmission.id },
|
||||
relations: {
|
||||
values: { fieldDefinition: true },
|
||||
category: true,
|
||||
actionType: true,
|
||||
},
|
||||
});
|
||||
|
||||
return result || savedSubmission;
|
||||
}
|
||||
|
||||
async validateAndSubmitAction(submissionDto: ActionSubmissionDto): Promise<ActionSubmission> {
|
||||
|
||||
@@ -5,6 +5,9 @@ import {
|
||||
IsUUID,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
@@ -41,21 +44,66 @@ export class CreateRuleConditionDto {
|
||||
sequence: number;
|
||||
}
|
||||
|
||||
export class CreatePolicyActionValueDto {
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
fieldDefinitionId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
fieldCode: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
valueIndex?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
selectedValueId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
textValue?: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
numberValue?: number;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
booleanValue?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
dateValue?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
timeValue?: string;
|
||||
|
||||
@Type(() => Date)
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
timestampValue?: Date;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
currencyCodeId?: string;
|
||||
}
|
||||
|
||||
export class CreatePolicyActionDto {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
actionTypeId: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
parameterId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
valueText?: string;
|
||||
|
||||
@IsInt()
|
||||
sequence: number;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreatePolicyActionValueDto)
|
||||
values?: CreatePolicyActionValueDto[];
|
||||
}
|
||||
|
||||
export class CreatePolicyRuleDto {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { PolicyAction } from './policy-action.entity';
|
||||
import { FieldDefinition } from '../../master-data/entities/field-definition.entity';
|
||||
|
||||
@Entity({ name: 'policy_action_values', schema: 'policy_engine' })
|
||||
export class PolicyActionValue {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ name: 'action_id', type: 'uuid' })
|
||||
actionId!: string;
|
||||
|
||||
@ManyToOne(() => PolicyAction, (action) => action.values, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'action_id' })
|
||||
action?: PolicyAction;
|
||||
|
||||
@Column({ name: 'field_definition_id', type: 'uuid', nullable: true })
|
||||
fieldDefinitionId?: string;
|
||||
|
||||
@ManyToOne(() => FieldDefinition, { onDelete: 'SET NULL', nullable: true })
|
||||
@JoinColumn({ name: 'field_definition_id' })
|
||||
fieldDefinition?: FieldDefinition;
|
||||
|
||||
@Column({ name: 'field_code', type: 'varchar' })
|
||||
fieldCode!: string;
|
||||
|
||||
@Column({ name: 'value_index', type: 'int', default: 0 })
|
||||
valueIndex!: number;
|
||||
|
||||
@Column({ name: 'selected_value_id', type: 'varchar', nullable: true })
|
||||
selectedValueId?: string;
|
||||
|
||||
@Column({ name: 'text_value', type: 'text', nullable: true })
|
||||
textValue?: string;
|
||||
|
||||
@Column({ name: 'number_value', type: 'numeric', nullable: true })
|
||||
numberValue?: number;
|
||||
|
||||
@Column({ name: 'boolean_value', type: 'boolean', nullable: true })
|
||||
booleanValue?: boolean;
|
||||
|
||||
@Column({ name: 'date_value', type: 'date', nullable: true })
|
||||
dateValue?: string;
|
||||
|
||||
@Column({ name: 'time_value', type: 'time', nullable: true })
|
||||
timeValue?: string;
|
||||
|
||||
@Column({ name: 'timestamp_value', type: 'timestamptz', nullable: true })
|
||||
timestampValue?: Date;
|
||||
|
||||
@Column({ name: 'currency_code_id', type: 'uuid', nullable: true })
|
||||
currencyCodeId?: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
|
||||
import { PolicyRule } from './policy-rule.entity';
|
||||
import { PolicyActionValue } from './policy-action-value.entity';
|
||||
import { ActionType } from '../../master-data/entities/action-type.entity';
|
||||
|
||||
@Entity({ name: 'policy_actions', schema: 'policy_engine' })
|
||||
export class PolicyAction {
|
||||
@@ -12,11 +14,9 @@ export class PolicyAction {
|
||||
@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;
|
||||
@ManyToOne(() => ActionType, { nullable: true, eager: false, createForeignKeyConstraints: false })
|
||||
@JoinColumn({ name: 'action_type_id' })
|
||||
actionType?: ActionType;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
sequence!: number;
|
||||
@@ -24,4 +24,8 @@ export class PolicyAction {
|
||||
@ManyToOne(() => PolicyRule, (rule) => rule.actions, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'rule_id' })
|
||||
rule!: PolicyRule;
|
||||
|
||||
@OneToMany(() => PolicyActionValue, (value) => value.action, { cascade: true, onDelete: 'CASCADE' })
|
||||
values!: PolicyActionValue[];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColum
|
||||
import { Policy } from './policy.entity';
|
||||
import { RuleCondition } from './rule-condition.entity';
|
||||
import { PolicyAction } from './policy-action.entity';
|
||||
import { RuleCategory } from '../../master-data/entities/rule-category.entity';
|
||||
|
||||
@Entity({ name: 'policy_rules', schema: 'policy_engine' })
|
||||
export class PolicyRule {
|
||||
@@ -14,6 +15,10 @@ export class PolicyRule {
|
||||
@Column({ name: 'rule_category_id', type: 'uuid', nullable: true })
|
||||
ruleCategoryId!: string;
|
||||
|
||||
@ManyToOne(() => RuleCategory, { nullable: true, eager: false, createForeignKeyConstraints: false })
|
||||
@JoinColumn({ name: 'rule_category_id' })
|
||||
ruleCategory?: RuleCategory;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
priority!: number;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ 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 { PolicyActionValue } from './entities/policy-action-value.entity';
|
||||
import { MasterDataModule } from '../master-data/master-data.module';
|
||||
|
||||
@Module({
|
||||
@@ -17,6 +18,7 @@ import { MasterDataModule } from '../master-data/master-data.module';
|
||||
PolicyRule,
|
||||
RuleCondition,
|
||||
PolicyAction,
|
||||
PolicyActionValue,
|
||||
]),
|
||||
MasterDataModule,
|
||||
],
|
||||
@@ -25,3 +27,4 @@ import { MasterDataModule } from '../master-data/master-data.module';
|
||||
exports: [PolicyEngineService],
|
||||
})
|
||||
export class PolicyEngineModule { }
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ 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 { PolicyActionValue } from './entities/policy-action-value.entity';
|
||||
import { CreatePolicyDto } from './dto/create-policy.dto';
|
||||
import { UpdatePolicyDto } from './dto/update-policy.dto';
|
||||
import { PolicyStatus, AudienceType } from './entities/policy.enums';
|
||||
@@ -26,8 +27,14 @@ export class PolicyEngineService {
|
||||
jurisdiction: true,
|
||||
targetAudiences: true,
|
||||
rules: {
|
||||
ruleCategory: true,
|
||||
conditions: true,
|
||||
actions: true,
|
||||
actions: {
|
||||
actionType: true,
|
||||
values: {
|
||||
fieldDefinition: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -104,7 +111,7 @@ export class PolicyEngineService {
|
||||
// 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)
|
||||
// 2. Find and delete existing rules (cascades to conditions and actions/values)
|
||||
const existingRules = await transactionalEntityManager.find(PolicyRule, { where: { policyId: id } });
|
||||
if (existingRules.length > 0) {
|
||||
await transactionalEntityManager.remove(PolicyRule, existingRules);
|
||||
@@ -135,9 +142,17 @@ export class PolicyEngineService {
|
||||
transactionalEntityManager.create(RuleCondition, cond),
|
||||
) || [];
|
||||
|
||||
const actions = ruleDto.actions?.map((act) =>
|
||||
transactionalEntityManager.create(PolicyAction, act),
|
||||
) || [];
|
||||
const actions = ruleDto.actions?.map((act) => {
|
||||
const values = act.values?.map((val) =>
|
||||
transactionalEntityManager.create(PolicyActionValue, val),
|
||||
) || [];
|
||||
|
||||
return transactionalEntityManager.create(PolicyAction, {
|
||||
actionTypeId: act.actionTypeId,
|
||||
sequence: act.sequence,
|
||||
values,
|
||||
});
|
||||
}) || [];
|
||||
|
||||
return transactionalEntityManager.create(PolicyRule, {
|
||||
ruleCategoryId: ruleDto.ruleCategoryId,
|
||||
|
||||
Reference in New Issue
Block a user