feat: implement master data entities and initial database schema for the policy engine module

This commit is contained in:
Syed Waseem
2026-08-03 11:45:26 +05:30
parent 1fe403cb11
commit 9f3b3ad4fd
15 changed files with 521 additions and 33 deletions
+97 -1
View File
@@ -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,7 +436,23 @@ 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()
);
@@ -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);
+13 -2
View File
@@ -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],
+64 -5
View File
@@ -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,10 +142,18 @@ 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,
priority: ruleDto.priority,