diff --git a/database/schema.sql b/database/schema.sql index 284e9b0..b8e1463 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -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); + + diff --git a/database/seed-master-data.sql b/database/seed-master-data.sql index be72838..846a95e 100644 --- a/database/seed-master-data.sql +++ b/database/seed-master-data.sql @@ -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); + diff --git a/src/modules/master-data/dto/action-submission.dto.ts b/src/modules/master-data/dto/action-submission.dto.ts index 26cc7f3..d06a027 100644 --- a/src/modules/master-data/dto/action-submission.dto.ts +++ b/src/modules/master-data/dto/action-submission.dto.ts @@ -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; + data?: Record; } diff --git a/src/modules/master-data/entities/action-submission-value.entity.ts b/src/modules/master-data/entities/action-submission-value.entity.ts new file mode 100644 index 0000000..59088c9 --- /dev/null +++ b/src/modules/master-data/entities/action-submission-value.entity.ts @@ -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; +} + diff --git a/src/modules/master-data/entities/action-submission.entity.ts b/src/modules/master-data/entities/action-submission.entity.ts index 0839f83..39ae20c 100644 --- a/src/modules/master-data/entities/action-submission.entity.ts +++ b/src/modules/master-data/entities/action-submission.entity.ts @@ -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; + @ManyToOne(() => ActionType, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'actionTypeId' }) + actionType?: ActionType; + + @OneToMany(() => ActionSubmissionValue, (val) => val.submission, { cascade: true }) + values!: ActionSubmissionValue[]; @CreateDateColumn() createdAt!: Date; diff --git a/src/modules/master-data/entities/amount-type.entity.ts b/src/modules/master-data/entities/amount-type.entity.ts new file mode 100644 index 0000000..b108a53 --- /dev/null +++ b/src/modules/master-data/entities/amount-type.entity.ts @@ -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; +} diff --git a/src/modules/master-data/master-data.controller.ts b/src/modules/master-data/master-data.controller.ts index d9f54a9..18be71b 100644 --- a/src/modules/master-data/master-data.controller.ts +++ b/src/modules/master-data/master-data.controller.ts @@ -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') diff --git a/src/modules/master-data/master-data.module.ts b/src/modules/master-data/master-data.module.ts index e4049a5..c9eaa20 100644 --- a/src/modules/master-data/master-data.module.ts +++ b/src/modules/master-data/master-data.module.ts @@ -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], diff --git a/src/modules/master-data/master-data.service.ts b/src/modules/master-data/master-data.service.ts index 6b74a67..91d81e3 100644 --- a/src/modules/master-data/master-data.service.ts +++ b/src/modules/master-data/master-data.service.ts @@ -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, @InjectRepository(FieldDefinition) private fieldDefinitionRepo: Repository, @InjectRepository(ActionSubmission) private actionSubmissionRepo: Repository, + @InjectRepository(ActionSubmissionValue) private actionSubmissionValueRepo: Repository, @InjectRepository(BookingChannel) private bookingChannelRepo: Repository, @InjectRepository(FlightType) private flightTypeRepo: Repository, @@ -97,7 +100,8 @@ export class MasterDataService implements OnModuleInit { @InjectRepository(RefundBasis) private refundBasisRepo: Repository, @InjectRepository(Currency) private currencyRepo: Repository, @InjectRepository(RefundMethod) private refundMethodRepo: Repository, - ) {} + @InjectRepository(AmountType) private amountTypeRepo: Repository, + ) { } private getRepositoryByCategory(category: string): Repository { 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 { - 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[] = []; + + 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 { diff --git a/src/modules/policy-engine/dto/create-policy.dto.ts b/src/modules/policy-engine/dto/create-policy.dto.ts index 4f5bdd3..bc68b5c 100644 --- a/src/modules/policy-engine/dto/create-policy.dto.ts +++ b/src/modules/policy-engine/dto/create-policy.dto.ts @@ -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 { diff --git a/src/modules/policy-engine/entities/policy-action-value.entity.ts b/src/modules/policy-engine/entities/policy-action-value.entity.ts new file mode 100644 index 0000000..21f4a9a --- /dev/null +++ b/src/modules/policy-engine/entities/policy-action-value.entity.ts @@ -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; +} diff --git a/src/modules/policy-engine/entities/policy-action.entity.ts b/src/modules/policy-engine/entities/policy-action.entity.ts index cf53ab8..89781e2 100644 --- a/src/modules/policy-engine/entities/policy-action.entity.ts +++ b/src/modules/policy-engine/entities/policy-action.entity.ts @@ -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[]; } + diff --git a/src/modules/policy-engine/entities/policy-rule.entity.ts b/src/modules/policy-engine/entities/policy-rule.entity.ts index 5acac77..b66db04 100644 --- a/src/modules/policy-engine/entities/policy-rule.entity.ts +++ b/src/modules/policy-engine/entities/policy-rule.entity.ts @@ -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; diff --git a/src/modules/policy-engine/policy-engine.module.ts b/src/modules/policy-engine/policy-engine.module.ts index 9fce6e3..e097376 100644 --- a/src/modules/policy-engine/policy-engine.module.ts +++ b/src/modules/policy-engine/policy-engine.module.ts @@ -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 { } + diff --git a/src/modules/policy-engine/policy-engine.service.ts b/src/modules/policy-engine/policy-engine.service.ts index 317014c..848355b 100644 --- a/src/modules/policy-engine/policy-engine.service.ts +++ b/src/modules/policy-engine/policy-engine.service.ts @@ -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,