From db6a512656e09d701a06f6a0127df102eba7cd59 Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Tue, 11 Feb 2025 16:21:39 -0800 Subject: [PATCH 01/87] Add application condition financial instrument entity - Entity - DTOs - Database Migration - Add instrument to application decision condition entity --- ...sion-condition-financial-instrument.dto.ts | 69 ++++++++++++++ ...n-condition-financial-instrument.entity.ts | 91 +++++++++++++++++++ .../application-decision-condition.entity.ts | 6 ++ ...lication_condition_financial_instrument.ts | 37 ++++++++ 4 files changed, 203 insertions(+) create mode 100644 services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts create mode 100644 services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1739313333099-add_application_condition_financial_instrument.ts diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts new file mode 100644 index 000000000..daf011e8d --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts @@ -0,0 +1,69 @@ +import { AutoMap } from 'automapper-classes'; +import { InstrumentType, HeldBy, InstrumentStatus } from './application-decision-condition-financial-instrument.entity'; +import { IsEnum, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; +import { OmitType } from '@nestjs/mapped-types'; + +export class ApplicationDecisionConditionFinancialInstrumentDto { + @AutoMap() + @IsUUID() + uuid: string; + + @AutoMap() + @IsString() + securityHolderPayee: string; + + @AutoMap() + @IsEnum(InstrumentType) + type: InstrumentType; + + @AutoMap() + @IsNumber() + issueDate: number; + + @AutoMap() + @IsNumber() + @IsOptional() + expiryDate?: number | null; + + @AutoMap() + @IsNumber() + amount: number; + + @AutoMap() + @IsString() + bank: string; + + @AutoMap() + @IsOptional() + instrumentNumber?: string | null; + + @AutoMap() + @IsEnum(HeldBy) + heldBy: HeldBy; + + @AutoMap() + @IsNumber() + receivedDate: number; + + @AutoMap() + @IsString() + @IsOptional() + notes?: string | null; + + @AutoMap() + @IsEnum(InstrumentStatus) + status: InstrumentStatus; + + @AutoMap() + @IsOptional() + statusDate?: number | null; + + @AutoMap() + @IsString() + explanation?: string | null; +} + +export class CreateUpdateApplicationDecisionConditionFinancialInstrumentDto extends OmitType( + ApplicationDecisionConditionFinancialInstrumentDto, + ['uuid'] as const, +) {} diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity.ts new file mode 100644 index 000000000..e3b014b86 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity.ts @@ -0,0 +1,91 @@ +import { Entity, Column, ManyToOne, OneToMany } from 'typeorm'; +import { Base } from '../../../../common/entities/base.entity'; +import { ApplicationDecisionCondition } from '../application-decision-condition.entity'; +import { AutoMap } from 'automapper-classes'; +import { ColumnNumericTransformer } from '../../../../utils/column-numeric-transform'; + +export enum InstrumentType { + BANK_DRAFT = 'Bank Draft', + CERTIFIED_CHEQUE = 'Certified Cheque', + EFT = 'EFT', + IRREVOCABLE_LETTER_OF_CREDIT = 'Irrevocable Letter of Credit', + OTHER = 'Other', + SAFEKEEPING_AGREEMENT = 'Safekeeping Agreement', +} + +export enum HeldBy { + ALC = 'ALC', + MINISTRY = 'Ministry', +} + +export enum InstrumentStatus { + RECEIVED = 'Received', + RELEASED = 'Released', + CASHED = 'Cashed', + REPLACED = 'Replaced', +} + +@Entity({ comment: 'Instrument for Financial Security Conditions' }) +export class ApplicationDecisionConditionFinancialInstrument extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap() + @Column({ type: 'varchar', nullable: false }) + securityHolderPayee: string; + + @AutoMap() + @Column({ type: 'enum', enum: InstrumentType, nullable: false }) + type: InstrumentType; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: false }) + issueDate: Date; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: true }) + expiryDate?: Date; + + @AutoMap() + @Column({ type: 'decimal', precision: 12, scale: 2, nullable: false, transformer: new ColumnNumericTransformer() }) + amount: number; + + @AutoMap() + @Column({ type: 'varchar', nullable: false }) + bank: string; + + @AutoMap() + @Column({ type: 'varchar', nullable: true }) + instrumentNumber: string | null; + + @AutoMap() + @Column({ type: 'enum', enum: HeldBy, nullable: false }) + heldBy: HeldBy; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: false }) + receivedDate: Date; + + @AutoMap() + @Column({ type: 'text', nullable: true }) + notes: string | null; + + @AutoMap() + @Column({ type: 'enum', enum: InstrumentStatus, default: InstrumentStatus.RECEIVED, nullable: false }) + status: InstrumentStatus; + + @AutoMap() + @Column({ type: 'date', nullable: true }) + statusDate?: Date | null; + + @AutoMap() + @Column({ type: 'text', nullable: true }) + explanation?: string | null; + + @ManyToOne(() => ApplicationDecisionCondition, (condition) => condition.financialInstruments, { onDelete: 'CASCADE' }) + condition: ApplicationDecisionCondition; +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts index 455e7aca1..0506716f1 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts @@ -8,6 +8,7 @@ import { ApplicationDecision } from '../application-decision.entity'; import { ApplicationDecisionConditionType } from './application-decision-condition-code.entity'; import { ApplicationDecisionConditionDate } from './application-decision-condition-date/application-decision-condition-date.entity'; import { ApplicationDecisionConditionCard } from './application-decision-condition-card/application-decision-condition-card.entity'; +import { ApplicationDecisionConditionFinancialInstrument } from './application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity'; @Entity({ comment: 'Fields present on the application decision conditions' }) export class ApplicationDecisionCondition extends Base { @@ -87,4 +88,9 @@ export class ApplicationDecisionCondition extends Base { nullable: true, }) conditionCard: ApplicationDecisionConditionCard | null; + + @OneToMany(() => ApplicationDecisionConditionFinancialInstrument, (instrument) => instrument.condition, { + cascade: true, + }) + financialInstruments?: ApplicationDecisionConditionFinancialInstrument[] | null; } diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1739313333099-add_application_condition_financial_instrument.ts b/services/apps/alcs/src/providers/typeorm/migrations/1739313333099-add_application_condition_financial_instrument.ts new file mode 100644 index 000000000..20e242298 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1739313333099-add_application_condition_financial_instrument.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddApplicationConditionFinancialInstrument1739313333099 implements MigrationInterface { + name = 'AddApplicationConditionFinancialInstrument1739313333099'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "alcs"."application_decision_condition_financial_instrument_type_enum" AS ENUM('Bank Draft', 'Certified Cheque', 'EFT', 'Irrevocable Letter of Credit', 'Other', 'Safekeeping Agreement')`, + ); + await queryRunner.query( + `CREATE TYPE "alcs"."application_decision_condition_financial_instrument_held_by_enum" AS ENUM('ALC', 'Ministry')`, + ); + await queryRunner.query( + `CREATE TYPE "alcs"."application_decision_condition_financial_instrument_status_enum" AS ENUM('Received', 'Released', 'Cashed', 'Replaced')`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."application_decision_condition_financial_instrument" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "security_holder_payee" character varying NOT NULL, "type" "alcs"."application_decision_condition_financial_instrument_type_enum" NOT NULL, "issue_date" TIMESTAMP WITH TIME ZONE NOT NULL, "expiry_date" TIMESTAMP WITH TIME ZONE, "amount" numeric(12,2) NOT NULL, "bank" character varying NOT NULL, "instrument_number" character varying, "held_by" "alcs"."application_decision_condition_financial_instrument_held_by_enum" NOT NULL, "received_date" TIMESTAMP WITH TIME ZONE NOT NULL, "notes" text, "status" "alcs"."application_decision_condition_financial_instrument_status_enum" NOT NULL DEFAULT 'Received', "status_date" date, "explanation" text, "condition_uuid" uuid, CONSTRAINT "PK_4474d61a96a50f86d8f1b6cce28" PRIMARY KEY ("uuid"))`, + ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."application_decision_condition_financial_instrument" IS 'Instrument for Financial Security Conditions'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_financial_instrument" ADD CONSTRAINT "FK_7ee6009fecca8304ed53608bdc5" FOREIGN KEY ("condition_uuid") REFERENCES "alcs"."application_decision_condition"("uuid") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_financial_instrument" DROP CONSTRAINT "FK_7ee6009fecca8304ed53608bdc5"`, + ); + await queryRunner.query(`COMMENT ON TABLE "alcs"."application_decision_condition_financial_instrument" IS NULL`); + await queryRunner.query(`DROP TABLE "alcs"."application_decision_condition_financial_instrument"`); + await queryRunner.query(`DROP TYPE "alcs"."application_decision_condition_financial_instrument_status_enum"`); + await queryRunner.query(`DROP TYPE "alcs"."application_decision_condition_financial_instrument_held_by_enum"`); + await queryRunner.query(`DROP TYPE "alcs"."application_decision_condition_financial_instrument_type_enum"`); + } +} From f4e3e3baff95dcc43154d8971f5b65309454c6e5 Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Wed, 12 Feb 2025 16:24:19 -0800 Subject: [PATCH 02/87] Change status date column to timestamp and make expiry date column optional --- ...lication-decision-condition-financial-instrument.entity.ts | 4 ++-- ...13333099-add_application_condition_financial_instrument.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity.ts index e3b014b86..bc5b37d04 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity.ts @@ -48,7 +48,7 @@ export class ApplicationDecisionConditionFinancialInstrument extends Base { @AutoMap() @Column({ type: 'timestamptz', nullable: true }) - expiryDate?: Date; + expiryDate?: Date | null; @AutoMap() @Column({ type: 'decimal', precision: 12, scale: 2, nullable: false, transformer: new ColumnNumericTransformer() }) @@ -79,7 +79,7 @@ export class ApplicationDecisionConditionFinancialInstrument extends Base { status: InstrumentStatus; @AutoMap() - @Column({ type: 'date', nullable: true }) + @Column({ type: 'timestamptz', nullable: true }) statusDate?: Date | null; @AutoMap() diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1739313333099-add_application_condition_financial_instrument.ts b/services/apps/alcs/src/providers/typeorm/migrations/1739313333099-add_application_condition_financial_instrument.ts index 20e242298..f972cb213 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1739313333099-add_application_condition_financial_instrument.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1739313333099-add_application_condition_financial_instrument.ts @@ -14,7 +14,7 @@ export class AddApplicationConditionFinancialInstrument1739313333099 implements `CREATE TYPE "alcs"."application_decision_condition_financial_instrument_status_enum" AS ENUM('Received', 'Released', 'Cashed', 'Replaced')`, ); await queryRunner.query( - `CREATE TABLE "alcs"."application_decision_condition_financial_instrument" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "security_holder_payee" character varying NOT NULL, "type" "alcs"."application_decision_condition_financial_instrument_type_enum" NOT NULL, "issue_date" TIMESTAMP WITH TIME ZONE NOT NULL, "expiry_date" TIMESTAMP WITH TIME ZONE, "amount" numeric(12,2) NOT NULL, "bank" character varying NOT NULL, "instrument_number" character varying, "held_by" "alcs"."application_decision_condition_financial_instrument_held_by_enum" NOT NULL, "received_date" TIMESTAMP WITH TIME ZONE NOT NULL, "notes" text, "status" "alcs"."application_decision_condition_financial_instrument_status_enum" NOT NULL DEFAULT 'Received', "status_date" date, "explanation" text, "condition_uuid" uuid, CONSTRAINT "PK_4474d61a96a50f86d8f1b6cce28" PRIMARY KEY ("uuid"))`, + `CREATE TABLE "alcs"."application_decision_condition_financial_instrument" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "security_holder_payee" character varying NOT NULL, "type" "alcs"."application_decision_condition_financial_instrument_type_enum" NOT NULL, "issue_date" TIMESTAMP WITH TIME ZONE NOT NULL, "expiry_date" TIMESTAMP WITH TIME ZONE, "amount" numeric(12,2) NOT NULL, "bank" character varying NOT NULL, "instrument_number" character varying, "held_by" "alcs"."application_decision_condition_financial_instrument_held_by_enum" NOT NULL, "received_date" TIMESTAMP WITH TIME ZONE NOT NULL, "notes" text, "status" "alcs"."application_decision_condition_financial_instrument_status_enum" NOT NULL DEFAULT 'Received', "status_date" TIMESTAMP WITH TIME ZONE, "explanation" text, "condition_uuid" uuid, CONSTRAINT "PK_4474d61a96a50f86d8f1b6cce28" PRIMARY KEY ("uuid"))`, ); await queryRunner.query( `COMMENT ON TABLE "alcs"."application_decision_condition_financial_instrument" IS 'Instrument for Financial Security Conditions'`, From 6a7e0f5d83729ca3171be931e409216fd57c38b7 Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Thu, 13 Feb 2025 09:52:08 -0800 Subject: [PATCH 03/87] Update instrument DTO fields validation - Add number validator for status date - Add optional validator for explanation --- .../application-decision-condition-financial-instrument.dto.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts index daf011e8d..9e6fa49cb 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts @@ -56,10 +56,12 @@ export class ApplicationDecisionConditionFinancialInstrumentDto { @AutoMap() @IsOptional() + @IsNumber() statusDate?: number | null; @AutoMap() @IsString() + @IsOptional() explanation?: string | null; } From 1618875f1bb99901dfdaa8ff4a160f19c9b743e4 Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Thu, 13 Feb 2025 09:56:10 -0800 Subject: [PATCH 04/87] Add application decision condition financial instrument services - Application decision condition financial instrument service - Add financial instrument APIs to decision condition controller - Add financial instrument to decision condition DTO - Add mappers - Add new mock entities for condition type and financial instrument --- ...ition-financial-instrument.service.spec.ts | 227 ++++++++++++++++++ ...-condition-financial-instrument.service.ts | 182 ++++++++++++++ ...tion-decision-condition.controller.spec.ts | 143 +++++++++++ ...plication-decision-condition.controller.ts | 80 +++++- .../application-decision-condition.dto.ts | 4 + .../application-decision-v2.module.ts | 4 + .../application-decision-v2.service.ts | 1 + ...lication-decision-v2.automapper.profile.ts | 54 +++++ services/apps/alcs/test/mocks/mockEntities.ts | 57 +++++ 9 files changed, 751 insertions(+), 1 deletion(-) create mode 100644 services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts new file mode 100644 index 000000000..4eea167c8 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts @@ -0,0 +1,227 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ApplicationDecisionConditionFinancialInstrumentService } from './application-decision-condition-financial-instrument.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { + ApplicationDecisionConditionFinancialInstrument, + HeldBy, + InstrumentStatus, + InstrumentType, +} from './application-decision-condition-financial-instrument.entity'; +import { ApplicationDecisionCondition } from '../application-decision-condition.entity'; +import { ApplicationDecisionConditionType } from '../application-decision-condition-code.entity'; +import { Repository } from 'typeorm'; +import { CreateUpdateApplicationDecisionConditionFinancialInstrumentDto } from './application-decision-condition-financial-instrument.dto'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { + ServiceInternalErrorException, + ServiceNotFoundException, + ServiceValidationException, +} from '../../../../../../../libs/common/src/exceptions/base.exception'; +import { + initApplicationDecisionConditionFinancialInstrumentMockEntity, + initApplicationDecisionConditionTypeMockEntity, +} from '../../../../../test/mocks/mockEntities'; + +describe('ApplicationDecisionConditionFinancialInstrumentService', () => { + let service: ApplicationDecisionConditionFinancialInstrumentService; + let mockRepository: DeepMocked>; + let mockConditionRepository: DeepMocked>; + let mockConditionTypeRepository: DeepMocked>; + let mockApplicationDecisionConditionType; + let mockApplicationDecisionConditionFinancialInstrument; + + beforeEach(async () => { + mockRepository = createMock(); + mockConditionRepository = createMock(); + mockConditionTypeRepository = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ApplicationDecisionConditionFinancialInstrumentService, + { + provide: getRepositoryToken(ApplicationDecisionConditionFinancialInstrument), + useValue: mockRepository, + }, + { + provide: getRepositoryToken(ApplicationDecisionCondition), + useValue: mockConditionRepository, + }, + { + provide: getRepositoryToken(ApplicationDecisionConditionType), + useValue: mockConditionTypeRepository, + }, + ], + }).compile(); + + service = module.get( + ApplicationDecisionConditionFinancialInstrumentService, + ); + + mockApplicationDecisionConditionType = initApplicationDecisionConditionTypeMockEntity('BOND'); + mockApplicationDecisionConditionFinancialInstrument = + initApplicationDecisionConditionFinancialInstrumentMockEntity(); + }); + + describe('getAll', () => { + it('should return all financial instruments for a condition', async () => { + const conditionUuid = 'condition-uuid'; + const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstruments = [mockApplicationDecisionConditionFinancialInstrument]; + + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.find.mockResolvedValue(financialInstruments); + + const result = await service.getAll(conditionUuid); + + expect(result).toEqual(financialInstruments); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ where: { code: 'BOND' } }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.find).toHaveBeenCalledWith({ where: { condition: { uuid: conditionUuid } } }); + }); + + it('should throw an error if condition type does not exist', async () => { + mockConditionTypeRepository.findOne.mockResolvedValue(null); + + await expect(service.getAll('condition-uuid')).rejects.toThrow(ServiceInternalErrorException); + }); + + it('should throw an error if condition is not found', async () => { + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(null); + + await expect(service.getAll('condition-uuid')).rejects.toThrow(ServiceNotFoundException); + }); + + it('should throw an error if condition is not of type Financial Security', async () => { + const conditionUuid = 'condition-uuid'; + const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'OTHER' }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + + await expect(service.getAll(conditionUuid)).rejects.toThrow(ServiceValidationException); + }); + }); + + describe('getByUuid', () => { + it('should return a financial instrument by uuid', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = new ApplicationDecisionConditionFinancialInstrument({ uuid }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(financialInstrument); + + const result = await service.getByUuid(conditionUuid, uuid); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ + where: { code: mockApplicationDecisionConditionType.code }, + }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { uuid, condition: { uuid: conditionUuid } } }); + }); + + it('should throw an error if financial instrument is not found', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.getByUuid(conditionUuid, uuid)).rejects.toThrow(ServiceNotFoundException); + }); + }); + + describe('create', () => { + it('should create a financial instrument', async () => { + const conditionUuid = 'condition-uuid'; + const dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + }; + const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = mockApplicationDecisionConditionFinancialInstrument; + + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.save.mockResolvedValue(financialInstrument); + + const result = await service.create(conditionUuid, dto); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ + where: { code: mockApplicationDecisionConditionType.code }, + }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.save).toHaveBeenCalledWith(expect.any(ApplicationDecisionConditionFinancialInstrument)); + }); + }); + + describe('update', () => { + it('should update a financial instrument', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + }; + const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = new ApplicationDecisionConditionFinancialInstrument({ uuid }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(financialInstrument); + mockRepository.save.mockResolvedValue(financialInstrument); + + const result = await service.update(conditionUuid, uuid, dto); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ + where: { code: mockApplicationDecisionConditionType.code }, + }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { uuid, condition: { uuid: conditionUuid } } }); + expect(mockRepository.save).toHaveBeenCalledWith(expect.any(ApplicationDecisionConditionFinancialInstrument)); + }); + }); + + describe('softRemove', () => { + it('should soft remove a financial instrument', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = new ApplicationDecisionConditionFinancialInstrument({ uuid }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(financialInstrument); + mockRepository.softRemove.mockResolvedValue(financialInstrument); + + const result = await service.softRemove(conditionUuid, uuid); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ where: { code: 'BOND' } }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { uuid, condition: { uuid: conditionUuid } } }); + expect(mockRepository.softRemove).toHaveBeenCalledWith(financialInstrument); + }); + }); +}); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts new file mode 100644 index 000000000..dedaa3250 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts @@ -0,0 +1,182 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + ApplicationDecisionConditionFinancialInstrument, + InstrumentStatus, +} from './application-decision-condition-financial-instrument.entity'; +import { Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + ServiceInternalErrorException, + ServiceNotFoundException, + ServiceValidationException, +} from '../../../../../../../libs/common/src/exceptions/base.exception'; +import { CreateUpdateApplicationDecisionConditionFinancialInstrumentDto } from './application-decision-condition-financial-instrument.dto'; +import { ApplicationDecisionCondition } from '../application-decision-condition.entity'; +import { ApplicationDecisionConditionType } from '../application-decision-condition-code.entity'; + +export enum ConditionType { + FINANCIAL_SECURITY = 'BOND', +} + +@Injectable() +export class ApplicationDecisionConditionFinancialInstrumentService { + constructor( + @InjectRepository(ApplicationDecisionConditionFinancialInstrument) + private readonly repository: Repository, + @InjectRepository(ApplicationDecisionCondition) + private readonly applicationDecisionConditionRepository: Repository, + @InjectRepository(ApplicationDecisionConditionType) + private readonly applicationDecisionConditionTypeRepository: Repository, + ) {} + + async throwErrorIfFinancialSecurityTypeNotExists(): Promise { + const exists = await this.applicationDecisionConditionTypeRepository.findOne({ + where: { code: ConditionType.FINANCIAL_SECURITY }, + }); + if (!exists) { + throw new ServiceInternalErrorException('Condition type Financial Security not found'); + } + } + + async getAll(conditionUuid: string): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.applicationDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + return this.repository.find({ where: { condition: { uuid: conditionUuid } } }); + } + + async getByUuid(conditionUuid: string, uuid: string): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.applicationDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + const financialInstrument = await this.repository.findOne({ where: { uuid, condition: { uuid: conditionUuid } } }); + + if (!financialInstrument) { + throw new ServiceNotFoundException(`Financial Instrument with uuid ${uuid} not found`); + } + + return financialInstrument; + } + + async create( + conditionUuid: string, + dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, + ): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.applicationDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + let instrument = new ApplicationDecisionConditionFinancialInstrument(); + instrument = this.mapDtoToEntity(dto, instrument); + instrument.condition = condition; + + return this.repository.save(instrument); + } + + async update( + conditionUuid: string, + uuid: string, + dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, + ): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.applicationDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + let instrument = await this.repository.findOne({ where: { uuid, condition: { uuid: conditionUuid } } }); + + if (!instrument) { + throw new ServiceNotFoundException(`Instrument with uuid ${uuid} not found`); + } + + instrument = this.mapDtoToEntity(dto, instrument); + + return this.repository.save(instrument); + } + + async softRemove(conditionUuid: string, uuid: string): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.applicationDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + const instrument = await this.repository.findOne({ where: { uuid, condition: { uuid: conditionUuid } } }); + + if (!instrument) { + throw new ServiceNotFoundException(`Instrument with uuid ${uuid} not found`); + } + + return await this.repository.softRemove(instrument); + } + + private mapDtoToEntity( + dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, + entity: ApplicationDecisionConditionFinancialInstrument, + ): ApplicationDecisionConditionFinancialInstrument { + entity.securityHolderPayee = dto.securityHolderPayee; + entity.type = dto.type; + entity.issueDate = new Date(dto.issueDate); + entity.expiryDate = dto.expiryDate ? new Date(dto.expiryDate) : null; + entity.amount = dto.amount; + entity.bank = dto.bank; + entity.instrumentNumber = dto.instrumentNumber ?? null; + entity.heldBy = dto.heldBy; + entity.receivedDate = new Date(dto.receivedDate); + entity.notes = dto.notes ?? null; + entity.status = dto.status; + if (dto.status !== InstrumentStatus.RECEIVED) { + if (!dto.statusDate || !dto.explanation) { + throw new ServiceValidationException('Status date and explanation are required when status is not RECEIVED'); + } + entity.statusDate = new Date(dto.statusDate); + entity.explanation = dto.explanation; + } else { + if (dto.statusDate || dto.explanation) { + throw new ServiceValidationException('Status date and explanation are not allowed when status is RECEIVED'); + } + entity.statusDate = null; + entity.explanation = null; + } + return entity; + } +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts index 522946112..195c1e144 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts @@ -9,13 +9,22 @@ import { ApplicationDecisionConditionController } from './application-decision-c import { UpdateApplicationDecisionConditionDto } from './application-decision-condition.dto'; import { ApplicationDecisionCondition } from './application-decision-condition.entity'; import { ApplicationDecisionConditionService } from './application-decision-condition.service'; +import { ApplicationDecisionConditionFinancialInstrumentService } from './application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service'; +import { + ApplicationDecisionConditionFinancialInstrument, + HeldBy, + InstrumentStatus, + InstrumentType, +} from './application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity'; describe('ApplicationDecisionConditionController', () => { let controller: ApplicationDecisionConditionController; let mockApplicationDecisionConditionService: DeepMocked; + let mockApplicationDecisionConditionFinancialInstrumentService: DeepMocked; beforeEach(async () => { mockApplicationDecisionConditionService = createMock(); + mockApplicationDecisionConditionFinancialInstrumentService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -30,6 +39,10 @@ describe('ApplicationDecisionConditionController', () => { provide: ApplicationDecisionConditionService, useValue: mockApplicationDecisionConditionService, }, + { + provide: ApplicationDecisionConditionFinancialInstrumentService, + useValue: mockApplicationDecisionConditionFinancialInstrumentService, + }, { provide: ClsService, useValue: {}, @@ -88,4 +101,134 @@ describe('ApplicationDecisionConditionController', () => { expect(result.approvalDependant).toEqual(updated.approvalDependant); }); }); + + describe('Financial Instruments', () => { + const conditionUuid = 'condition-uuid'; + const instrumentUuid = 'instrument-uuid'; + const financialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + notes: 'notes', + expiryDate: new Date().getTime(), + statusDate: new Date().getTime(), + explanation: 'explanation', + }; + + it('should get all financial instruments for a condition', async () => { + const financialInstruments = [ + new ApplicationDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }), + ]; + mockApplicationDecisionConditionFinancialInstrumentService.getAll.mockResolvedValue(financialInstruments); + + const result = await controller.getAllFinancialInstruments(conditionUuid); + + expect(mockApplicationDecisionConditionFinancialInstrumentService.getAll).toHaveBeenCalledWith(conditionUuid); + expect(result).toBeDefined(); + }); + + it('should get a financial instrument by uuid', async () => { + const financialInstrument = new ApplicationDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockApplicationDecisionConditionFinancialInstrumentService.getByUuid.mockResolvedValue(financialInstrument); + + const result = await controller.getFinancialInstrumentByUuid(conditionUuid, instrumentUuid); + + expect(mockApplicationDecisionConditionFinancialInstrumentService.getByUuid).toHaveBeenCalledWith( + conditionUuid, + instrumentUuid, + ); + expect(result).toBeDefined(); + }); + + it('should create a financial instrument', async () => { + const financialInstrument = new ApplicationDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockApplicationDecisionConditionFinancialInstrumentService.create.mockResolvedValue(financialInstrument); + + const result = await controller.createFinancialInstrument(conditionUuid, financialInstrumentDto); + + expect(mockApplicationDecisionConditionFinancialInstrumentService.create).toHaveBeenCalledWith( + conditionUuid, + financialInstrumentDto, + ); + expect(result).toBeDefined(); + }); + + it('should update a financial instrument', async () => { + const financialInstrument = new ApplicationDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockApplicationDecisionConditionFinancialInstrumentService.update.mockResolvedValue(financialInstrument); + + const result = await controller.updateFinancialInstrument(conditionUuid, instrumentUuid, financialInstrumentDto); + + expect(mockApplicationDecisionConditionFinancialInstrumentService.update).toHaveBeenCalledWith( + conditionUuid, + instrumentUuid, + financialInstrumentDto, + ); + expect(result).toBeDefined(); + }); + + it('should delete a financial instrument', async () => { + const financialInstrument = new ApplicationDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockApplicationDecisionConditionFinancialInstrumentService.softRemove.mockResolvedValue(financialInstrument); + + const result = await controller.deleteFinancialInstrument(conditionUuid, instrumentUuid); + + expect(mockApplicationDecisionConditionFinancialInstrumentService.softRemove).toHaveBeenCalledWith( + conditionUuid, + instrumentUuid, + ); + expect(result).toBeDefined(); + }); + }); }); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts index ae312f05e..78cfe2720 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts @@ -1,6 +1,6 @@ import { Mapper } from 'automapper-core'; import { InjectMapper } from 'automapper-nestjs'; -import { Body, Controller, Get, Param, Patch, Query, UseGuards } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; import { ApiOAuth2 } from '@nestjs/swagger'; import * as config from 'config'; import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; @@ -14,6 +14,12 @@ import { } from './application-decision-condition.dto'; import { ApplicationDecisionCondition } from './application-decision-condition.entity'; import { ApplicationDecisionConditionService } from './application-decision-condition.service'; +import { ApplicationDecisionConditionFinancialInstrumentService } from './application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service'; +import { + ApplicationDecisionConditionFinancialInstrumentDto, + CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, +} from './application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto'; +import { ApplicationDecisionConditionFinancialInstrument } from './application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('application-decision-condition') @@ -21,6 +27,7 @@ import { ApplicationDecisionConditionService } from './application-decision-cond export class ApplicationDecisionConditionController { constructor( private conditionService: ApplicationDecisionConditionService, + private conditionFinancialInstrumentService: ApplicationDecisionConditionFinancialInstrumentService, @InjectMapper() private mapper: Mapper, ) {} @@ -75,4 +82,75 @@ export class ApplicationDecisionConditionController { ApplicationDecisionConditionComponentDto, ); } + + @Get('/:uuid/financial-instruments') + @UserRoles(...ANY_AUTH_ROLE) + async getAllFinancialInstruments( + @Param('uuid') uuid: string, + ): Promise { + const financialInstruments = await this.conditionFinancialInstrumentService.getAll(uuid); + + return await this.mapper.mapArray( + financialInstruments, + ApplicationDecisionConditionFinancialInstrument, + ApplicationDecisionConditionFinancialInstrumentDto, + ); + } + + @Get('/:uuid/financial-instruments/:instrumentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async getFinancialInstrumentByUuid( + @Param('uuid') uuid: string, + @Param('instrumentUuid') instrumentUuid: string, + ): Promise { + const financialInstrument = await this.conditionFinancialInstrumentService.getByUuid(uuid, instrumentUuid); + return await this.mapper.map( + financialInstrument, + ApplicationDecisionConditionFinancialInstrument, + ApplicationDecisionConditionFinancialInstrumentDto, + ); + } + + @Post('/:uuid/financial-instruments') + @UserRoles(...ANY_AUTH_ROLE) + async createFinancialInstrument( + @Param('uuid') uuid: string, + @Body() dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, + ): Promise { + const financialInstrument = await this.conditionFinancialInstrumentService.create(uuid, dto); + return await this.mapper.map( + financialInstrument, + ApplicationDecisionConditionFinancialInstrument, + ApplicationDecisionConditionFinancialInstrumentDto, + ); + } + + @Patch('/:uuid/financial-instruments/:instrumentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async updateFinancialInstrument( + @Param('uuid') uuid: string, + @Param('instrumentUuid') instrumentUuid: string, + @Body() dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, + ): Promise { + const financialInstrument = await this.conditionFinancialInstrumentService.update(uuid, instrumentUuid, dto); + return await this.mapper.map( + financialInstrument, + ApplicationDecisionConditionFinancialInstrument, + ApplicationDecisionConditionFinancialInstrumentDto, + ); + } + + @Delete('/:uuid/financial-instruments/:instrumentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async deleteFinancialInstrument( + @Param('uuid') uuid: string, + @Param('instrumentUuid') instrumentUuid: string, + ): Promise { + const result = await this.conditionFinancialInstrumentService.softRemove(uuid, instrumentUuid); + return await this.mapper.map( + result, + ApplicationDecisionConditionFinancialInstrument, + ApplicationDecisionConditionFinancialInstrumentDto, + ); + } } diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts index 216586194..a58f7f275 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts @@ -10,6 +10,7 @@ import { ApplicationDecisionConditionHomeCardDto, } from './application-decision-condition-card/application-decision-condition-card.dto'; import { ApplicationDto } from '../../application/application.dto'; +import { ApplicationDecisionConditionFinancialInstrumentDto } from './application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto'; export class ApplicationDecisionConditionTypeDto extends BaseCodeDto { @IsBoolean() @@ -98,6 +99,9 @@ export class ApplicationDecisionConditionDto { conditionCard: ApplicationDecisionConditionCardUuidDto | null; status?: string | null; + + @AutoMap(() => ApplicationDecisionConditionFinancialInstrumentDto) + financialInstruments?: ApplicationDecisionConditionFinancialInstrumentDto[] | null; } export class ApplicationHomeDto { diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts index e06f83986..6dab2ef84 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts @@ -54,6 +54,8 @@ import { ApplicationDecisionConditionCardController } from '../application-decis import { ApplicationDecisionConditionCard } from '../application-decision-condition/application-decision-condition-card/application-decision-condition-card.entity'; import { ApplicationDecisionConditionCardService } from '../application-decision-condition/application-decision-condition-card/application-decision-condition-card.service'; import { User } from 'apps/alcs/src/user/user.entity'; +import { ApplicationDecisionConditionFinancialInstrument } from '../application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity'; +import { ApplicationDecisionConditionFinancialInstrumentService } from '../application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service'; @Module({ imports: [ @@ -84,6 +86,7 @@ import { User } from 'apps/alcs/src/user/user.entity'; ApplicationBoundaryAmendment, ApplicationDecisionConditionCard, User, + ApplicationDecisionConditionFinancialInstrument, ]), forwardRef(() => BoardModule), forwardRef(() => ApplicationModule), @@ -105,6 +108,7 @@ import { User } from 'apps/alcs/src/user/user.entity'; ApplicationConditionToComponentLotService, ApplicationBoundaryAmendmentService, ApplicationDecisionConditionCardService, + ApplicationDecisionConditionFinancialInstrumentService, ], controllers: [ ApplicationDecisionV2Controller, diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts index 48c1babb9..152f1ee41 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts @@ -126,6 +126,7 @@ export class ApplicationDecisionV2Service { }, dates: true, conditionCard: true, + financialInstruments: true, }, conditionCards: true, }, diff --git a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts index 00e7318f4..cae47575f 100644 --- a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts @@ -55,6 +55,11 @@ import { import { UserDto } from '../../user/user.dto'; import { User } from '../../user/user.entity'; import { Application } from '../../alcs/application/application.entity'; +import { + ApplicationDecisionConditionFinancialInstrument, + InstrumentStatus, +} from '../../alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity'; +import { ApplicationDecisionConditionFinancialInstrumentDto } from '../../alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto'; @Injectable() export class ApplicationDecisionProfile extends AutomapperProfile { @@ -283,6 +288,19 @@ export class ApplicationDecisionProfile extends AutomapperProfile { : null, ), ), + + forMember( + (dto) => dto.financialInstruments, + mapFrom((entity) => + entity.financialInstruments + ? this.mapper.mapArray( + entity.financialInstruments, + ApplicationDecisionConditionFinancialInstrument, + ApplicationDecisionConditionFinancialInstrumentDto, + ) + : [], + ), + ), ); createMap( @@ -495,6 +513,42 @@ export class ApplicationDecisionProfile extends AutomapperProfile { createMap(mapper, ApplicationDecision, ApplicationDecisionHomeDto); createMap(mapper, Application, ApplicationHomeDto); + + createMap( + mapper, + ApplicationDecisionConditionFinancialInstrument, + ApplicationDecisionConditionFinancialInstrumentDto, + forMember( + (dto) => dto.issueDate, + mapFrom((entity) => entity.issueDate.getTime()), + ), + forMember( + (dto) => dto.expiryDate, + mapFrom((entity) => (entity.expiryDate ? entity.expiryDate.getTime() : undefined)), + ), + forMember( + (dto) => dto.receivedDate, + mapFrom((entity) => entity.receivedDate.getTime()), + ), + forMember( + (dto) => dto.statusDate, + mapFrom((entity) => + entity.status !== InstrumentStatus.RECEIVED ? entity.statusDate?.getTime() || undefined : undefined, + ), + ), + forMember( + (dto) => dto.explanation, + mapFrom((entity) => entity.explanation || undefined), + ), + forMember( + (dto) => dto.notes, + mapFrom((entity) => entity.notes || undefined), + ), + forMember( + (dto) => dto.instrumentNumber, + mapFrom((entity) => entity.instrumentNumber || undefined), + ), + ); }; } } diff --git a/services/apps/alcs/test/mocks/mockEntities.ts b/services/apps/alcs/test/mocks/mockEntities.ts index 026063495..1947daa0e 100644 --- a/services/apps/alcs/test/mocks/mockEntities.ts +++ b/services/apps/alcs/test/mocks/mockEntities.ts @@ -27,6 +27,14 @@ import { Tag } from '../../src/alcs/tag/tag.entity'; import { NoticeOfIntent } from '../../src/alcs/notice-of-intent/notice-of-intent.entity'; import { NoticeOfIntentType } from '../../src/alcs/notice-of-intent/notice-of-intent-type/notice-of-intent-type.entity'; import { ApplicationDecisionConditionCard } from '../../src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.entity'; +import { ApplicationDecisionConditionType } from '../../src/alcs/application-decision/application-decision-condition/application-decision-condition-code.entity'; +import { ApplicationDecisionCondition } from '../../src/alcs/application-decision/application-decision-condition/application-decision-condition.entity'; +import { + ApplicationDecisionConditionFinancialInstrument, + HeldBy, + InstrumentStatus, + InstrumentType, +} from '../../src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity'; const initCardStatusMockEntity = (): CardStatus => { const cardStatus = new CardStatus(); @@ -411,6 +419,53 @@ const initMockApplicationDecisionConditionCard = ( return conditionCard; }; +const initApplicationDecisionConditionTypeMockEntity = (code?: string): ApplicationDecisionConditionType => { + const conditionType = new ApplicationDecisionConditionType(); + conditionType.code = code ? code : 'type_1'; + conditionType.description = 'condition desc 1'; + conditionType.label = 'condition_label'; + conditionType.isActive = true; + conditionType.isComponentToConditionChecked = true; + conditionType.isDescriptionChecked = true; + conditionType.isAdministrativeFeeAmountChecked = false; + conditionType.isAdministrativeFeeAmountRequired = null; + conditionType.administrativeFeeAmount = null; + conditionType.isDateChecked = false; + conditionType.isDateRequired = null; + conditionType.dateType = null; + conditionType.singleDateLabel = null; + conditionType.isSecurityAmountChecked = false; + conditionType.isSecurityAmountRequired = null; + conditionType.auditCreatedAt = new Date(1, 1, 1, 1, 1, 1, 1); + conditionType.auditUpdatedAt = new Date(1, 1, 1, 1, 1, 1, 1); + + return conditionType; +}; + +const initApplicationDecisionConditionFinancialInstrumentMockEntity = ( + payee?: string, + bank?: string, + instrumentNumber?: string, + condition?: ApplicationDecisionCondition, +): ApplicationDecisionConditionFinancialInstrument => { + const instrument = new ApplicationDecisionConditionFinancialInstrument(); + instrument.securityHolderPayee = 'fake-payee'; + instrument.type = InstrumentType.BANK_DRAFT; + instrument.issueDate = new Date(2022, 1, 1); + instrument.expiryDate = new Date(2023, 1, 1); + instrument.amount = 1000.0; + instrument.bank = 'fake-bank'; + instrument.instrumentNumber = '123456'; + instrument.heldBy = HeldBy.ALC; + instrument.receivedDate = new Date(2022, 1, 1); + instrument.notes = 'fake-notes'; + instrument.status = InstrumentStatus.RECEIVED; + instrument.statusDate = new Date(2022, 1, 1); + instrument.explanation = 'fake-explanation'; + instrument.condition = condition ?? new ApplicationDecisionCondition(); + return instrument; +}; + export { initCardStatusMockEntity, initApplicationMockEntity, @@ -436,4 +491,6 @@ export { initNoticeOfIntentMockEntity, initNoticeOfIntentWithTagsMockEntity, initMockApplicationDecisionConditionCard, + initApplicationDecisionConditionTypeMockEntity, + initApplicationDecisionConditionFinancialInstrumentMockEntity, }; From 8a390ef5bf69a9594a0dc46471bbdbd0abe7063f Mon Sep 17 00:00:00 2001 From: Tristan Slater <1631008+trslater@users.noreply.github.com> Date: Thu, 13 Feb 2025 15:27:32 -0800 Subject: [PATCH 05/87] Add 'No Data' to date chips --- .../decision-condition/decision-condition.component.ts | 2 +- .../decision-condition/decision-condition.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts index 1ec7ad882..ef715715c 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts @@ -208,7 +208,7 @@ export class DecisionConditionComponent implements OnInit, OnChanges { formatDate(timestamp: number | undefined): string { if (!timestamp) { - return ''; + return 'No Data'; } return moment(timestamp).format('YYYY-MMM-DD'); } diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts index d13cedb40..a984097da 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts @@ -203,7 +203,7 @@ export class DecisionConditionComponent implements OnInit, OnChanges { formatDate(timestamp: number | undefined): string { if (!timestamp) { - return ''; + return 'No Data'; } return moment(timestamp).format('YYYY-MMM-DD'); } From 53a3ed1ceade095ff7f72d7785d2ad437d717511 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Thu, 13 Feb 2025 16:29:09 -0800 Subject: [PATCH 06/87] ALCS-2433 Copy amendment uuid --- ...it-boundary-amendment-dialog.component.html | 18 +++++++++++++++++- ...it-boundary-amendment-dialog.component.scss | 15 +++++++++++++++ ...edit-boundary-amendment-dialog.component.ts | 12 ++++++++++++ .../apps/alcs/src/alcs/home/home.controller.ts | 3 --- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/alcs-frontend/src/app/features/application/boundary-amendment/edit-boundary-amendment-dialog/edit-boundary-amendment-dialog.component.html b/alcs-frontend/src/app/features/application/boundary-amendment/edit-boundary-amendment-dialog/edit-boundary-amendment-dialog.component.html index d707d81e6..e0941ba3c 100644 --- a/alcs-frontend/src/app/features/application/boundary-amendment/edit-boundary-amendment-dialog/edit-boundary-amendment-dialog.component.html +++ b/alcs-frontend/src/app/features/application/boundary-amendment/edit-boundary-amendment-dialog/edit-boundary-amendment-dialog.component.html @@ -1,8 +1,24 @@
-

{{ data ? 'Edit' : 'Create' }} Boundary Amendment

+

{{ data.existingAmendment ? 'Edit' : 'Create' }} Boundary Amendment

+
+ + UUID + + + +
Amendment Type* (null); type = new FormControl('', [Validators.required]); decisionComponents = new FormControl([], [Validators.required]); area = new FormControl(null, [Validators.required]); @@ -22,6 +24,7 @@ export class EditBoundaryAmendmentDialogComponent implements OnInit { selectableComponents: { label: string; value: string }[] = []; form: FormGroup = new FormGroup({ + uuid: this.uuid, type: this.type, decisionComponents: this.decisionComponents, area: this.area, @@ -33,6 +36,7 @@ export class EditBoundaryAmendmentDialogComponent implements OnInit { public matDialogRef: MatDialogRef, private applicationBoundaryAmendmentService: ApplicationBoundaryAmendmentService, private applicationDecisionV2Service: ApplicationDecisionV2Service, + private toastService: ToastService, @Inject(MAT_DIALOG_DATA) public data: { existingAmendment?: ApplicationBoundaryAmendmentDto; @@ -45,6 +49,7 @@ export class EditBoundaryAmendmentDialogComponent implements OnInit { const existingAmendment = this.data.existingAmendment; if (existingAmendment) { this.form.patchValue({ + uuid: existingAmendment.uuid, type: existingAmendment.type, area: existingAmendment.area, year: existingAmendment.year?.toString(), @@ -96,4 +101,11 @@ export class EditBoundaryAmendmentDialogComponent implements OnInit { } this.matDialogRef.close(true); } + + onCopy() { + if (this.uuid.value) { + navigator.clipboard.writeText(this.uuid.value); + this.toastService.showSuccessToast(`${this.uuid.value} copied to clipboard.`); + } + } } diff --git a/services/apps/alcs/src/alcs/home/home.controller.ts b/services/apps/alcs/src/alcs/home/home.controller.ts index cf8e14124..31051845e 100644 --- a/services/apps/alcs/src/alcs/home/home.controller.ts +++ b/services/apps/alcs/src/alcs/home/home.controller.ts @@ -349,9 +349,6 @@ export class HomeController { } private async mapNoticeOfIntentConditionsToDtos(noticeOfIntestConditions: NoticeOfIntentDecisionCondition[]) { - const noticeOfIntents = noticeOfIntestConditions.map((c) => c.decision.noticeOfIntent); - const uuids = noticeOfIntents.map((noi) => noi.uuid); - const timeMap = await this.noticeOfIntentService.getTimes(uuids); const holidays = await this.holidayService.fetchAllHolidays(); const result: HomepageSubtaskDTO[] = []; From 7e20c23ef7b857fa9b7086e5ce487233dade8166 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Fri, 14 Feb 2025 16:10:05 -0800 Subject: [PATCH 07/87] ALCS-1987 Handle Recon Type states --- ...edit-reconsideration-dialog.component.html | 14 ++++---- ...edit-reconsideration-dialog.component.scss | 4 +++ .../edit-reconsideration-dialog.component.ts | 36 ++++++++++++++++--- .../post-decision.component.html | 18 +++++----- ...reate-app-modification-dialog.component.ts | 3 +- ...eate-reconsideration-dialog.component.scss | 4 +++ ...create-reconsideration-dialog.component.ts | 30 +++++++++++++++- .../create/create-reconsideration-dialog.html | 7 ++-- 8 files changed, 91 insertions(+), 25 deletions(-) diff --git a/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.html b/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.html index 4fb9f7cdc..5928ed46a 100644 --- a/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.html +++ b/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.html @@ -38,7 +38,7 @@

Edit Reconsideration

- New Evidence* + New Evidence* Yes No @@ -46,7 +46,7 @@

Edit Reconsideration

- Incorrect or False Info* + Incorrect or False Info* Yes No @@ -54,7 +54,7 @@

Edit Reconsideration

- New Proposal* + New Proposal* Yes No @@ -66,8 +66,8 @@

Edit Reconsideration

-
- Review Outcome * +
+ Review Outcome * Edit Reconsideration
-
+
Does the reconsideration confirm, reverse, or vary the previous decision?**Does the reconsideration confirm, reverse, or vary the previous decision? (null); decisionOutcomeCodeControl = new FormControl(null); reviewDateControl = new FormControl(null); + isNewProposalControl = new FormControl(undefined, [Validators.required]); + isIncorrectFalseInfoControl = new FormControl(undefined, [Validators.required]); + isNewEvidenceControl = new FormControl(undefined, [Validators.required]); + + disable331Fields = false; form: FormGroup = new FormGroup({ submittedDate: new FormControl(undefined, [Validators.required]), @@ -35,9 +40,9 @@ export class EditReconsiderationDialogComponent implements OnInit { reviewDate: this.reviewDateControl, reconsidersDecisions: new FormControl([], [Validators.required]), description: new FormControl('', [Validators.required]), - isNewProposal: new FormControl(undefined, [Validators.required]), - isIncorrectFalseInfo: new FormControl(undefined, [Validators.required]), - isNewEvidence: new FormControl(undefined, [Validators.required]), + isNewProposal: this.isNewProposalControl, + isIncorrectFalseInfo: this.isIncorrectFalseInfoControl, + isNewEvidence: this.isNewEvidenceControl, }); decisions: { uuid: string; resolution: string }[] = []; @@ -70,6 +75,7 @@ export class EditReconsiderationDialogComponent implements OnInit { isIncorrectFalseInfo: parseBooleanToString(data.existingRecon.isIncorrectFalseInfo), isNewEvidence: parseBooleanToString(data.existingRecon.isNewEvidence), }); + this.handleReconType(data.existingRecon.type.code === RECONSIDERATION_TYPE.T_33); } ngOnInit(): void { @@ -93,7 +99,7 @@ export class EditReconsiderationDialogComponent implements OnInit { } = this.form.getRawValue(); const data: UpdateApplicationReconsiderationDto = { submittedDate: formatDateForApi(submittedDate!), - reviewOutcomeCode: reviewOutcomeCode, + reviewOutcomeCode: this.disable331Fields ? 'PRC' : reviewOutcomeCode, decisionOutcomeCode: decisionOutcomeCode, typeCode: type!, reviewDate: reviewDate ? formatDateForApi(reviewDate) : reviewDate, @@ -128,8 +134,10 @@ export class EditReconsiderationDialogComponent implements OnInit { async onTypeReconsiderationChange(reconsiderationType: string) { if (reconsiderationType === RECONSIDERATION_TYPE.T_33_1) { this.reviewOutcomeCodeControl.setValue(null); + this.handleReconType(false); } else { this.reviewOutcomeCodeControl.setValue('PEN'); + this.handleReconType(true); } } @@ -138,4 +146,24 @@ export class EditReconsiderationDialogComponent implements OnInit { this.decisionOutcomeCodeControl.setValue(null); this.reviewDateControl.setValue(null); } + + private handleReconType(enable: boolean) { + if (enable) { + this.isNewEvidenceControl.enable(); + this.isIncorrectFalseInfoControl.enable(); + this.isNewProposalControl.enable(); + this.reviewOutcomeCodeControl.enable(); + this.disable331Fields = false; + } else { + this.isNewEvidenceControl.disable(); + this.isNewEvidenceControl.setValue(null); + this.isIncorrectFalseInfoControl.disable(); + this.isIncorrectFalseInfoControl.setValue(null); + this.isNewProposalControl.disable(); + this.isNewProposalControl.setValue(null); + this.reviewOutcomeCodeControl.disable(); + this.reviewOutcomeCodeControl.setValue(null); + this.disable331Fields = true; + } + } } diff --git a/alcs-frontend/src/app/features/application/post-decision/post-decision.component.html b/alcs-frontend/src/app/features/application/post-decision/post-decision.component.html index d39ea7bc9..2b8bdd8e7 100644 --- a/alcs-frontend/src/app/features/application/post-decision/post-decision.component.html +++ b/alcs-frontend/src/app/features/application/post-decision/post-decision.component.html @@ -44,17 +44,17 @@
{{ reconsideration.reconsidersDecisionsNumbers.join(', ') }}
-
+
New Evidence
{{ reconsideration.isNewEvidence | booleanToString }}
-
+
Incorrect or False Info
{{ reconsideration.isIncorrectFalseInfo | booleanToString }}
-
+
New Proposal
{{ reconsideration.isNewProposal | booleanToString }}
@@ -91,7 +91,12 @@
-
+
+
Resulting Resolution
+ #{{ reconsideration.resultingDecision.resolutionNumber }}/{{ reconsideration.resultingDecision.resolutionYear }} +
+ +
Does the reconsideration confirm, reverse, or vary {{ reconsideration.reconsidersDecisionsNumbers.join(', ') }}? @@ -103,11 +108,6 @@
{{ reconsideration.decisionOutcome.label }}
- -
-
Resulting Resolution
- #{{ reconsideration.resultingDecision.resolutionNumber }}/{{ reconsideration.resultingDecision.resolutionYear }} -
diff --git a/alcs-frontend/src/app/features/board/dialogs/app-modification/create/create-app-modification-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/app-modification/create/create-app-modification-dialog.component.ts index 7453d6bbe..08ade938e 100644 --- a/alcs-frontend/src/app/features/board/dialogs/app-modification/create/create-app-modification-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/app-modification/create/create-app-modification-dialog.component.ts @@ -26,7 +26,7 @@ export class CreateAppModificationDialogComponent implements OnInit, OnDestroy { fileNumberControl = new FormControl({ value: '', disabled: true }, [Validators.required]); applicantControl = new FormControl({ value: '', disabled: true }, [Validators.required]); descriptionControl = new FormControl(null, [Validators.required]); - applicationTypeControl = new FormControl(null, [Validators.required]); + applicationTypeControl = new FormControl({ value: null, disabled: true }, [Validators.required]); regionControl = new FormControl({ value: null, disabled: true }, [Validators.required]); submittedDateControl = new FormControl(undefined, [Validators.required]); localGovernmentControl = new FormControl({ value: null, disabled: true }, [Validators.required]); @@ -68,6 +68,7 @@ export class CreateAppModificationDialogComponent implements OnInit, OnDestroy { if (!application.decisionDate) { this.isDecisionDateEmpty = true; } + this.applicationTypeControl.setValue(application.type.code); }); this.applicationService.$applicationTypes.pipe(takeUntil(this.$destroy)).subscribe((types) => { diff --git a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.scss b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.scss index 48315bfa7..873776a1a 100644 --- a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.scss +++ b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.scss @@ -62,3 +62,7 @@ mat-dialog-actions { margin-right: 16px; } } + +.field-disabled { + color: colors.$grey; +} diff --git a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.ts index d2808a827..cb8eb3465 100644 --- a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.ts @@ -6,6 +6,7 @@ import { Subject, takeUntil } from 'rxjs'; import { ApplicationTypeDto } from '../../../../../services/application/application-code.dto'; import { CreateApplicationReconsiderationDto, + RECONSIDERATION_TYPE, ReconsiderationTypeDto, } from '../../../../../services/application/application-reconsideration/application-reconsideration.dto'; import { ApplicationReconsiderationService } from '../../../../../services/application/application-reconsideration/application-reconsideration.service'; @@ -29,12 +30,13 @@ export class CreateReconsiderationDialogComponent implements OnInit, OnDestroy { reconTypes: ReconsiderationTypeDto[] = []; isLoading = false; isDecisionDateEmpty = false; + disable331Fields = false; decisions: { uuid: string; resolution: string }[] = []; fileNumberControl = new FormControl({ value: '', disabled: true }, [Validators.required]); applicantControl = new FormControl({ value: '', disabled: true }, [Validators.required]); - applicationTypeControl = new FormControl(null, [Validators.required]); + applicationTypeControl = new FormControl({ value: null, disabled: true }, [Validators.required]); boardControl = new FormControl(null, [Validators.required]); regionControl = new FormControl({ value: null, disabled: true }, [Validators.required]); submittedDateControl = new FormControl(undefined, [Validators.required]); @@ -87,6 +89,7 @@ export class CreateReconsiderationDialogComponent implements OnInit, OnDestroy { if (!application.decisionDate) { this.isDecisionDateEmpty = true; } + this.applicationTypeControl.setValue(application.type.code); }); this.applicationService.$applicationTypes.pipe(takeUntil(this.$destroy)).subscribe((types) => { @@ -182,4 +185,29 @@ export class CreateReconsiderationDialogComponent implements OnInit, OnDestroy { this.$destroy.next(); this.$destroy.complete(); } + + async onTypeReconsiderationChange() { + if (this.reconTypeControl.value === RECONSIDERATION_TYPE.T_33_1) { + this.handleReconType(false); + } else { + this.handleReconType(true); + } + } + + private handleReconType(enable: boolean) { + if (enable) { + this.isNewEvidenceControl.enable(); + this.isIncorrectFalseInfoControl.enable(); + this.isNewProposalControl.enable(); + this.disable331Fields = false; + } else { + this.isNewEvidenceControl.disable(); + this.isNewEvidenceControl.setValue(null); + this.isIncorrectFalseInfoControl.disable(); + this.isIncorrectFalseInfoControl.setValue(null); + this.isNewProposalControl.disable(); + this.isNewProposalControl.setValue(null); + this.disable331Fields = true; + } + } } diff --git a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.html b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.html index d49920ccc..57c9e410c 100644 --- a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.html +++ b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.html @@ -158,25 +158,26 @@

Create Reconsideration

bindValue="code" [clearable]="false" formControlName="reconType" + (change)="onTypeReconsiderationChange()" >
- New Evidence* + New Evidence* Yes No
- Incorrect or False Info* + Incorrect or False Info* Yes No
- New Proposal* + New Proposal* Yes No From 01c9b4253e04d4d4cf24dba363c8439e3b29bcda Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Tue, 18 Feb 2025 10:27:10 -0800 Subject: [PATCH 08/87] ALCS-1987 Always display 331 on linked request --- .../decision-v2/decision-input/decision-input-v2.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts index 0a833815b..922ca88bc 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -262,7 +262,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { (reconsideration) => (existingDecision && existingDecision.reconsiders?.uuid === reconsideration.uuid) || (reconsideration.reviewOutcome?.code === 'PRC' && !reconsideration.resultingDecision) || - (reconsideration.type.code === RECONSIDERATION_TYPE.T_33_1 && !reconsideration.resultingDecision), + (reconsideration.type.code === RECONSIDERATION_TYPE.T_33_1), ) .map((reconsideration, index) => ({ label: `Reconsideration Request #${reconsiderations.length - index} - ${reconsideration.reconsidersDecisions From 1780ee286f380c7c7f4d6259c16424fc19ae0f36 Mon Sep 17 00:00:00 2001 From: Tristan Slater <1631008+trslater@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:04:50 -0800 Subject: [PATCH 09/87] Add quick filters to apps --- .../conditions/conditions.component.html | 10 ++++- .../conditions/conditions.component.scss | 41 +++++++++++++++++++ .../conditions/conditions.component.ts | 27 ++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html index a5a1ef555..aeebebb83 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html @@ -34,7 +34,15 @@

View Conditions

-
+
+
Quick Filters:
+ + + {{ label.value }} + + +
+
= { + COMPLETE: 'Complete', + ONGOING: 'Ongoing', + PENDING: 'Pending', + PASTDUE: 'Past Due', + EXPIRED: 'Expired', + }; + $destroy = new Subject(); decisionUuid: string = ''; @@ -68,6 +77,8 @@ export class ConditionsComponent implements OnInit { reconLabel = RECON_TYPE_LABEL; modificationLabel = MODIFICATION_TYPE_LABEL; + conditionFilters: string[] = []; + constructor( private applicationDetailService: ApplicationDetailService, private decisionService: ApplicationDecisionV2Service, @@ -216,4 +227,20 @@ export class ConditionsComponent implements OnInit { } }); } + + onConditionFilterChange(change: MatChipListboxChange) { + if (document.activeElement instanceof HTMLElement) { + document.activeElement?.blur(); + } + + this.conditionFilters = change.value; + } + + filterConditions(conditions: ApplicationDecisionConditionWithStatus[]): ApplicationDecisionConditionWithStatus[] { + if (this.conditionFilters.length < 1) { + return conditions; + } + + return conditions.filter((condition) => this.conditionFilters.includes(condition.status)); + } } From d09e9ab97d0d2f71c4059767ecc2018fb083bed9 Mon Sep 17 00:00:00 2001 From: Tristan Slater <1631008+trslater@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:13:09 -0800 Subject: [PATCH 10/87] Add quick filters to NOI's --- .../conditions/conditions.component.html | 10 ++++- .../conditions/conditions.component.scss | 41 +++++++++++++++++++ .../conditions/conditions.component.ts | 27 ++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html index 660201cb0..656f87696 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html @@ -29,7 +29,15 @@

View Conditions

-
+
+
Quick Filters:
+ + + {{ label.value }} + + +
+
= { + COMPLETE: 'Complete', + ONGOING: 'Ongoing', + PENDING: 'Pending', + PASTDUE: 'Past Due', + EXPIRED: 'Expired', + }; + $destroy = new Subject(); decisionUuid: string = ''; @@ -64,6 +73,8 @@ export class ConditionsComponent implements OnInit { releasedDecisionLabel = RELEASED_DECISION_TYPE_LABEL; modificationLabel = MODIFICATION_TYPE_LABEL; + conditionFilters: string[] = []; + constructor( private noticeOfIntentDetailService: NoticeOfIntentDetailService, private decisionService: NoticeOfIntentDecisionV2Service, @@ -212,4 +223,20 @@ export class ConditionsComponent implements OnInit { } }); } + + onConditionFilterChange(change: MatChipListboxChange) { + if (document.activeElement instanceof HTMLElement) { + document.activeElement?.blur(); + } + + this.conditionFilters = change.value; + } + + filterConditions(conditions: DecisionConditionWithStatus[]): DecisionConditionWithStatus[] { + if (this.conditionFilters.length < 1) { + return conditions; + } + + return conditions.filter((condition) => this.conditionFilters.includes(condition.status)); + } } From 49233b11f473f79df6ef9691635d42133bca6f9c Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Tue, 18 Feb 2025 14:37:15 -0800 Subject: [PATCH 11/87] ALCS-2029 Added Alpha index --- .../decision-condition/decision-condition.component.html | 2 +- .../decision-condition/decision-condition.component.ts | 8 ++++++++ .../decision-conditions.component.html | 1 + .../decision-conditions.component.scss | 4 ++++ .../decision-conditions/decision-conditions.component.ts | 4 ++++ 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html index 312752a9b..8b7be6c59 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html @@ -1,5 +1,5 @@
-
{{ data.type?.label }}
+
{{ alphaIndex(index + 1) }}. {{ data.type?.label }}
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts index ef715715c..5a6dd919b 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts @@ -11,6 +11,9 @@ import { DecisionConditionDateDialogComponent, DueDate, } from './decision-condition-date-dialog/decision-condition-date-dialog.component'; +import { + countToString +} from '../../../../../../../shared/utils/count-to-string'; import moment, { Moment } from 'moment'; import { startWith } from 'rxjs'; @@ -32,6 +35,7 @@ export class DecisionConditionComponent implements OnInit, OnChanges { @Output() remove = new EventEmitter(); @Input() selectableComponents: SelectableComponent[] = []; + @Input() index: number = 0; uuid: string | undefined; @@ -235,4 +239,8 @@ export class DecisionConditionComponent implements OnInit, OnChanges { this.emitChanges(); }); } + + alphaIndex(index: number) { + return countToString(index); + } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html index 43da67356..cb67feab5 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html @@ -30,6 +30,7 @@

Conditions

(remove)="onRemoveCondition(index)" [selectableComponents]="selectableComponents" [showDateError]="showDateErrors" + [index]="index" >
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss index a55dff748..9031dc63f 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss @@ -5,6 +5,10 @@ section { margin: 24px 0 56px; } +.order-button { + margin-right: 20px !important; +} + .buttons { margin-top: 28px; } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts index 42afea130..cdf5441b1 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts @@ -214,4 +214,8 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy onValidate() { this.conditionComponents.forEach((component) => component.form.markAllAsTouched()); } + + openOrderDialog() { + alert('open dialog'); + } } From ba274677440a274d033fc456c3d85e0af3da4302 Mon Sep 17 00:00:00 2001 From: Tristan Slater <1631008+trslater@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:54:50 -0800 Subject: [PATCH 12/87] Reset router link from base instead of appending --- .../dialogs/application/application-dialog.component.ts | 8 +++++--- .../notice-of-intent/notice-of-intent-dialog.component.ts | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts index 382ff334a..f792e67b3 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts @@ -19,6 +19,8 @@ import { ApplicationSubmissionStatusPill } from '../../../../shared/application- import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { CardDialogComponent } from '../card-dialog/card-dialog.component'; +const ROUTER_LINK_BASE = 'application'; + @Component({ selector: 'app-detail-dialog', templateUrl: './application-dialog.component.html', @@ -31,7 +33,7 @@ export class ApplicationDialogComponent extends CardDialogComponent implements O application: ApplicationDto = this.data; status?: ApplicationSubmissionStatusPill; - routerLink = `application/`; + routerLink = ''; constructor( @Inject(MAT_DIALOG_DATA) public data: ApplicationDto, @@ -64,7 +66,7 @@ export class ApplicationDialogComponent extends CardDialogComponent implements O async populateApplicationSubmissionStatus(fileNumber: string) { let submissionStatus: ApplicationSubmissionToSubmissionStatusDto | null = null; - this.routerLink = this.routerLink + fileNumber; + this.routerLink = `${ROUTER_LINK_BASE}/${fileNumber}`; try { submissionStatus = await this.applicationSubmissionStatusService.fetchCurrentStatusByFileNumber( fileNumber, @@ -76,7 +78,7 @@ export class ApplicationDialogComponent extends CardDialogComponent implements O if (submissionStatus) { if (submissionStatus.statusTypeCode === SUBMISSION_STATUS.ALC_DECISION) { - this.routerLink = this.routerLink + '/decision' + this.routerLink = `${ROUTER_LINK_BASE}/${fileNumber}/decision`; } this.status = { backgroundColor: submissionStatus.status.alcsBackgroundColor, diff --git a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts index 1d164524e..d0b8dc644 100644 --- a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts @@ -21,6 +21,8 @@ import { RETROACTIVE_TYPE_LABEL } from '../../../../shared/application-type-pill import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { CardDialogComponent } from '../card-dialog/card-dialog.component'; +const ROUTER_LINK_BASE = 'notice-of-intent'; + @Component({ selector: 'app-notice-of-intent-dialog', templateUrl: './notice-of-intent-dialog.component.html', @@ -35,7 +37,7 @@ export class NoticeOfIntentDialogComponent extends CardDialogComponent implement noticeOfIntent: NoticeOfIntentDto = this.data; RETROACTIVE_TYPE = RETROACTIVE_TYPE_LABEL; - routerLink = `notice-of-intent/`; + routerLink = ''; constructor( @Inject(MAT_DIALOG_DATA) public data: NoticeOfIntentDto, @@ -78,7 +80,7 @@ export class NoticeOfIntentDialogComponent extends CardDialogComponent implement private async populateSubmissionStatus(fileNumber: string) { let submissionStatus: NoticeOfIntentSubmissionToSubmissionStatusDto | null = null; - this.routerLink = this.routerLink + fileNumber; + this.routerLink = `${ROUTER_LINK_BASE}/${fileNumber}`; try { submissionStatus = await this.noticeOfIntentSubmissionStatusService.fetchCurrentStatusByFileNumber( fileNumber, @@ -89,7 +91,7 @@ export class NoticeOfIntentDialogComponent extends CardDialogComponent implement } if (submissionStatus) { if (submissionStatus.statusTypeCode === NOI_SUBMISSION_STATUS.ALC_DECISION) { - this.routerLink = this.routerLink + '/decision' + this.routerLink = `${ROUTER_LINK_BASE}/${fileNumber}/decision`; } this.status = { backgroundColor: submissionStatus.status.alcsBackgroundColor, From b4510b7f040b12d5ab3334d4f1d0a7b0c3789f6d Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Wed, 19 Feb 2025 09:10:22 -0800 Subject: [PATCH 13/87] ALCS-1987 Custom ng-select adjustments --- alcs-frontend/src/styles/ngselect.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/alcs-frontend/src/styles/ngselect.scss b/alcs-frontend/src/styles/ngselect.scss index 1c0292cd5..2f0e83111 100644 --- a/alcs-frontend/src/styles/ngselect.scss +++ b/alcs-frontend/src/styles/ngselect.scss @@ -16,7 +16,7 @@ } .ng-select .ng-arrow { - color: colors.$primary-color !important; + color: colors.$primary-color; } .ng-select.ng-select-focused .ng-placeholder { @@ -53,6 +53,10 @@ height: calc(100% - 0.15em); } +.ng-select.ng-select-disabled>.ng-select-container.ng-appearance-outline:after { + border-color: colors.$grey-light !important; +} + .ng-select .ng-select-container .ng-value-container { border-top: 0.5em !important; padding: 1.4em 0 1em !important; From 9f1571392b8d382b2943fcb11f731f23e64284c3 Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Wed, 19 Feb 2025 11:09:27 -0800 Subject: [PATCH 14/87] Fix modification and reconsideration pills on condition cards Show modification and reconsideration pills on application and NOI condition cards when the file has modification or reconsideration request instead of the decision --- ...application-decision-condition-card.controller.spec.ts | 4 ++-- .../application-decision-condition-card.controller.ts | 8 ++------ .../application-decision-condition-card.service.ts | 6 ++---- ...e-of-intent-decision-condition-card.controller.spec.ts | 2 +- ...notice-of-intent-decision-condition-card.controller.ts | 4 +--- .../notice-of-intent-decision-condition-card.service.ts | 4 +--- 6 files changed, 9 insertions(+), 19 deletions(-) diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.spec.ts index ff8d46bcc..7c68070ea 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.spec.ts @@ -121,8 +121,8 @@ describe('ApplicationDecisionConditionCardController', () => { conditionCard.decision = { uuid: 'decision-uuid', application: { fileNumber: 'file-number' } } as any; mockService.getByBoardCard.mockResolvedValue(conditionCard); - mockReconsiderationService.getByApplicationDecisionUuid.mockResolvedValue([]); - mockModificationService.getByApplicationDecisionUuid.mockResolvedValue([]); + mockReconsiderationService.getByApplication.mockResolvedValue([]); + mockModificationService.getByApplication.mockResolvedValue([]); mockApplicationDecisionService.getDecisionOrder.mockResolvedValue(1); const result = await controller.getByCardUuid(uuid); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.ts index 0b728caea..422052b57 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.controller.ts @@ -68,12 +68,8 @@ export class ApplicationDecisionConditionCardController { ); dto.fileNumber = result.decision.application.fileNumber; - const appModifications = await this.applicationModificationService.getByApplicationDecisionUuid( - result.decision.uuid, - ); - const appReconsiderations = await this.applicationReconsiderationService.getByApplicationDecisionUuid( - result.decision.uuid, - ); + const appModifications = await this.applicationModificationService.getByApplication(dto.fileNumber); + const appReconsiderations = await this.applicationReconsiderationService.getByApplication(dto.fileNumber); dto.isModification = appModifications.length > 0; dto.isReconsideration = appReconsiderations.length > 0; diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.service.ts index 71975b13c..447bd5843 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-card/application-decision-condition-card.service.ts @@ -191,10 +191,8 @@ export class ApplicationDecisionConditionCardService { }); for (const dto of dtos) { - const appModifications = await this.applicationModificationService.getByApplicationDecisionUuid(dto.decisionUuid); - const appReconsiderations = await this.applicationReconsiderationService.getByApplicationDecisionUuid( - dto.decisionUuid, - ); + const appModifications = await this.applicationModificationService.getByApplication(dto.fileNumber); + const appReconsiderations = await this.applicationReconsiderationService.getByApplication(dto.fileNumber); dto.isModification = appModifications.length > 0; dto.isReconsideration = appReconsiderations.length > 0; diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.spec.ts index 131f2f970..3e03ffc36 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.spec.ts @@ -116,7 +116,7 @@ describe('NoticeOfIntentDecisionConditionCardController', () => { conditionCard.decision = { uuid: 'decision-uuid', noticeOfIntent: { fileNumber: 'file-number' } } as any; mockService.getByBoardCard.mockResolvedValue(conditionCard); - mockModificationService.getByNoticeOfIntentDecisionUuid.mockResolvedValue([]); + mockModificationService.getByFileNumber.mockResolvedValue([]); mockDecisionService.getDecisionOrder.mockResolvedValue(1); const result = await controller.getByCardUuid(uuid); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.ts index bb3ac2b14..1ca8fd662 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller.ts @@ -68,9 +68,7 @@ export class NoticeOfIntentDecisionConditionCardController { ); dto.fileNumber = result.decision.noticeOfIntent.fileNumber; - const appModifications = await this.noticeOfIntentModificationService.getByNoticeOfIntentDecisionUuid( - result.decision.uuid, - ); + const appModifications = await this.noticeOfIntentModificationService.getByFileNumber(dto.fileNumber); dto.isModification = appModifications.length > 0; diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.service.ts index 66db9db75..a8f4133c0 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.service.ts @@ -194,9 +194,7 @@ export class NoticeOfIntentDecisionConditionCardService { }); for (const dto of dtos) { - const appModifications = await this.noticeOfIntentModificationService.getByNoticeOfIntentDecisionUuid( - dto.decisionUuid, - ); + const appModifications = await this.noticeOfIntentModificationService.getByFileNumber(dto.fileNumber); dto.isModification = appModifications.length > 0; From e19f8e89be5c76b5beaf3b93a4bba601b120fa20 Mon Sep 17 00:00:00 2001 From: Dylan Rogowsky Date: Wed, 19 Feb 2025 14:46:51 -0700 Subject: [PATCH 15/87] ALCS-2514: Fix dockerfile warnings --- alcs-frontend/Dockerfile | 6 +++--- portal-frontend/Dockerfile | 4 ++-- services/Dockerfile | 2 +- services/Dockerfile.migrate | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/alcs-frontend/Dockerfile b/alcs-frontend/Dockerfile index a188b5806..0e4e9569d 100644 --- a/alcs-frontend/Dockerfile +++ b/alcs-frontend/Dockerfile @@ -13,7 +13,7 @@ RUN npm ci # Copy the source code to the /app directory COPY . . -ENV NODE_OPTIONS "--max-old-space-size=2048" +ENV NODE_OPTIONS="--max-old-space-size=2048" # Build the application RUN npm run build -- --output-path=dist --output-hashing=all @@ -47,10 +47,10 @@ COPY --from=build /app/dist /usr/share/nginx/html RUN chmod -R go+rwx /usr/share/nginx/html/assets # provide dynamic scp content-src -ENV ENABLED_CONNECT_SRC " 'self' http://localhost:* nrs.objectstore.gov.bc.ca" +ENV ENABLED_CONNECT_SRC=" 'self' http://localhost:* nrs.objectstore.gov.bc.ca" # set to true to enable maintenance mode -ENV MAINTENANCE_MODE "false" +ENV MAINTENANCE_MODE="false" # When the container starts, replace the settings.json with values from environment variables ENTRYPOINT [ "./init.sh" ] diff --git a/portal-frontend/Dockerfile b/portal-frontend/Dockerfile index 410a505cd..212720b0e 100644 --- a/portal-frontend/Dockerfile +++ b/portal-frontend/Dockerfile @@ -13,7 +13,7 @@ RUN npm ci # Copy the source code to the /app directory COPY . . -ENV NODE_OPTIONS "--max-old-space-size=2048" +ENV NODE_OPTIONS="--max-old-space-size=2048" # Build the application RUN npm run build -- --output-path=dist --output-hashing=all @@ -47,7 +47,7 @@ COPY --from=build /app/dist /usr/share/nginx/html RUN chmod -R go+rwx /usr/share/nginx/html/assets # provide dynamic scp content-src -ENV ENABLED_CONNECT_SRC " 'self' http://localhost:* nrs.objectstore.gov.bc.ca" +ENV ENABLED_CONNECT_SRC=" 'self' http://localhost:* nrs.objectstore.gov.bc.ca" # When the container starts, replace the settings.json with values from environment variables ENTRYPOINT [ "./init.sh" ] diff --git a/services/Dockerfile b/services/Dockerfile index d68aea7bf..303b1d7e8 100644 --- a/services/Dockerfile +++ b/services/Dockerfile @@ -52,7 +52,7 @@ WORKDIR /opt/app-root/ RUN chmod og+rwx /opt/app-root/ /var/run ARG environment=production -ENV NODE_ENV ${environment} +ENV NODE_ENV=${environment} COPY --from=build /opt/app-root/node_modules ./node_modules COPY --from=build /opt/app-root/dist/apps/${NEST_APP} ./dist diff --git a/services/Dockerfile.migrate b/services/Dockerfile.migrate index 5ebb2fbfe..e6de9126c 100644 --- a/services/Dockerfile.migrate +++ b/services/Dockerfile.migrate @@ -8,7 +8,7 @@ WORKDIR /opt/app-root/ # OpenShift fixes RUN chmod og+rwx /opt/app-root/ /var/run -ENV NPM_CONFIG_USERCONFIG /opt/app-root/.npmrc +ENV NPM_CONFIG_USERCONFIG=/opt/app-root/.npmrc RUN npm config set cache $/opt/app-root/.npm COPY package*.json ./ @@ -17,10 +17,10 @@ RUN npm ci ARG environment=production -ENV NODE_ENV ${environment} +ENV NODE_ENV=${environment} ARG NEST_APP=alcs -ENV NEST_APP ${NEST_APP} +ENV NEST_APP=${NEST_APP} COPY . . From 87805c85c90a8a492368e386a96aec49b2c988ff Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Wed, 19 Feb 2025 16:47:25 -0800 Subject: [PATCH 16/87] Add decision condition financial instrument common component and service - Add abstract service and common DTO - Add common component and dialog --- ...sion-condition-financial-instrument.dto.ts | 53 +++++++ ...-condition-financial-instrument.service.ts | 30 ++++ alcs-frontend/src/app/shared/constants.ts | 4 + ...financial-instrument-dialog.component.html | 141 ++++++++++++++++ ...financial-instrument-dialog.component.scss | 72 +++++++++ ...ancial-instrument-dialog.component.spec.ts | 23 +++ ...n-financial-instrument-dialog.component.ts | 150 ++++++++++++++++++ ...dition-financial-instrument.component.html | 9 ++ ...dition-financial-instrument.component.scss | 19 +++ ...ion-financial-instrument.component.spec.ts | 23 +++ ...ondition-financial-instrument.component.ts | 47 ++++++ alcs-frontend/src/app/shared/shared.module.ts | 7 + 12 files changed, 578 insertions(+) create mode 100644 alcs-frontend/src/app/services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto.ts create mode 100644 alcs-frontend/src/app/services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service.ts create mode 100644 alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.html create mode 100644 alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.scss create mode 100644 alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.spec.ts create mode 100644 alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.ts create mode 100644 alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html create mode 100644 alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss create mode 100644 alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.spec.ts create mode 100644 alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts diff --git a/alcs-frontend/src/app/services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto.ts b/alcs-frontend/src/app/services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto.ts new file mode 100644 index 000000000..e023bde6b --- /dev/null +++ b/alcs-frontend/src/app/services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto.ts @@ -0,0 +1,53 @@ +export enum InstrumentType { + BANK_DRAFT = 'Bank Draft', + CERTIFIED_CHEQUE = 'Certified Cheque', + EFT = 'EFT', + IRREVOCABLE_LETTER_OF_CREDIT = 'Irrevocable Letter of Credit', + OTHER = 'Other', + SAFEKEEPING_AGREEMENT = 'Safekeeping Agreement', +} + +export enum HeldBy { + ALC = 'ALC', + MINISTRY = 'Ministry', +} + +export enum InstrumentStatus { + RECEIVED = 'Received', + RELEASED = 'Released', + CASHED = 'Cashed', + REPLACED = 'Replaced', +} + +export interface DecisionConditionFinancialInstrumentDto { + uuid: string; + securityHolderPayee: string; + type: InstrumentType; + issueDate: number; + expiryDate?: number | null; + amount: number; + bank: string; + instrumentNumber?: string | null; + heldBy: HeldBy; + receivedDate: number; + notes?: string | null; + status: InstrumentStatus; + statusDate?: number | null; + explanation?: string | null; +} + +export interface CreateUpdateDecisionConditionFinancialInstrumentDto { + securityHolderPayee: string; + type: InstrumentType; + issueDate: number; + expiryDate?: number | null; + amount: number; + bank: string; + instrumentNumber?: string | null; + heldBy: HeldBy; + receivedDate: number; + notes?: string | null; + status: InstrumentStatus; + statusDate?: number | null; + explanation?: string | null; +} diff --git a/alcs-frontend/src/app/services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service.ts b/alcs-frontend/src/app/services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service.ts new file mode 100644 index 000000000..13938b951 --- /dev/null +++ b/alcs-frontend/src/app/services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service.ts @@ -0,0 +1,30 @@ +import { HttpClient } from '@angular/common/http'; +import { + CreateUpdateDecisionConditionFinancialInstrumentDto, + DecisionConditionFinancialInstrumentDto, +} from './decision-condition-financial-instrument.dto'; +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export abstract class DecisionConditionFinancialInstrumentService { + constructor(protected http: HttpClient) {} + + abstract getAll(conditionUuid: string): Promise; + + abstract get(conditionUuid: string, uuid: string): Promise; + + abstract create( + conditionUuid: string, + dto: CreateUpdateDecisionConditionFinancialInstrumentDto, + ): Promise; + + abstract update( + conditionUuid: string, + uuid: string, + dto: CreateUpdateDecisionConditionFinancialInstrumentDto, + ): Promise; + + abstract delete(conditionUuid: string, uuid: string): Promise; +} diff --git a/alcs-frontend/src/app/shared/constants.ts b/alcs-frontend/src/app/shared/constants.ts index 7e0b8ed4c..b168bc66b 100644 --- a/alcs-frontend/src/app/shared/constants.ts +++ b/alcs-frontend/src/app/shared/constants.ts @@ -1 +1,5 @@ export const FILE_NAME_TRUNCATE_LENGTH = 30; +export enum DialogAction { + ADD = 'add', + EDIT = 'edit', +} diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.html b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.html new file mode 100644 index 000000000..0e91fa2e9 --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.html @@ -0,0 +1,141 @@ +
+
+

{{ isEdit ? 'Edit Financial Instrument' : 'Add Financial Instrument' }}

+
+ +
+
+ + Security Holder/Payee + + +
+ +
+ + Type + + {{ + type.value + }} + + + + Issue Date + + + + +
+ +
+ + Amount + + + + Held By + + {{ + heldBy.value + }} + + +
+ +
+ + Bank + + + + Instrument Number + + +
+ +
+ + Received Date + + + + + + Expiry Date + + + + +
+ +
+ + Notes + + +
+ +
+
+ Select Instrument Status + + Received - instrument has been received or for EFTs, CSNR has confirmed + Released - conditions related to this security were met and security has been released + Cashed - the security will not be released + Replaced - this security was replaced by another security + +
+
+ + +
+ + Status Date + + + + +
+ +
+ + Explanation + + +
+
+
+ + + + + +
diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.scss b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.scss new file mode 100644 index 000000000..e8ba94471 --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.scss @@ -0,0 +1,72 @@ +@use '../../../../styles/colors'; + +.container { + display: flex; + flex-direction: column; + padding: 48px 36px 24px; + width: 100%; +} + +.header { + display: block; +} + +.content { + display: block; + flex-direction: column; +} + +.row { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 18px; + flex-grow: 1; + margin: 20px 0 0 0; + + & > * { + flex: 1; + } + + & > :only-child { + flex-grow: 1; + } +} + +.status-radio-group-container { + display: flex; + flex-direction: column !important; + flex: 1; +} + +.status-radio-group { + display: flex; + flex-direction: column; +} + +mat-radio-button { + margin-bottom: -8px !important; +} + +::ng-deep .mat-mdc-radio-button.mat-accent { + --mdc-radio-disabled-selected-icon-color: black; + --mdc-radio-disabled-unselected-icon-color: black; + --mdc-radio-unselected-hover-icon-color: #212121; + --mdc-radio-unselected-icon-color: rgba(0, 0, 0, 0.54); + --mdc-radio-unselected-pressed-icon-color: rgba(0, 0, 0, 0.54); + --mdc-radio-selected-focus-icon-color: #{colors.$primary-color}; + --mdc-radio-selected-hover-icon-color: #{colors.$primary-color}; + --mdc-radio-selected-icon-color: #{colors.$primary-color}; + --mdc-radio-selected-pressed-icon-color: #{colors.$primary-color}; + --mat-radio-ripple-color: black; + --mat-radio-checked-ripple-color: #{colors.$primary-color}; + --mat-radio-disabled-label-color: rgba(0, 0, 0, 0.38); +} + +.actions-container { + display: flex; + flex-direction: row; + justify-content: flex-end; + gap: 8px; + margin-top: 12px; +} diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.spec.ts b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.spec.ts new file mode 100644 index 000000000..109021608 --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DecisionConditionFinancialInstrumentDialogComponent } from './decision-condition-financial-instrument-dialog.component'; + +describe('DecisionConditionFinancialInstrumentDialogComponent', () => { + let component: DecisionConditionFinancialInstrumentDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DecisionConditionFinancialInstrumentDialogComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DecisionConditionFinancialInstrumentDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.ts b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.ts new file mode 100644 index 000000000..40150098d --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.ts @@ -0,0 +1,150 @@ +import { Component, Inject, Input, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { DialogAction } from '../../constants'; +import { + DecisionConditionFinancialInstrumentDto, + CreateUpdateDecisionConditionFinancialInstrumentDto, +} from '../../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; +import { FormControl, FormGroup, ValidatorFn, Validators, AbstractControl } from '@angular/forms'; +import { + HeldBy, + InstrumentStatus, + InstrumentType, +} from '../../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; +import { DecisionConditionFinancialInstrumentService } from '../../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; +import { ToastService } from '../../../services/toast/toast.service'; + +@Component({ + selector: 'app-decision-condition-financial-instrument-dialog', + templateUrl: './decision-condition-financial-instrument-dialog.component.html', + styleUrl: './decision-condition-financial-instrument-dialog.component.scss', +}) +export class DecisionConditionFinancialInstrumentDialogComponent implements OnInit { + @Input() service?: DecisionConditionFinancialInstrumentService; + isEdit: boolean = false; + form: FormGroup; + + securityHolderPayee = new FormControl('', Validators.required); + type = new FormControl(null, [Validators.required, this.enumValidator(InstrumentType)]); + issueDate = new FormControl(null, Validators.required); + expiryDate = new FormControl(null); + amount = new FormControl(null, Validators.required); + bank = new FormControl('', Validators.required); + instrumentNumber = new FormControl(null); + heldBy = new FormControl(null, [Validators.required, this.enumValidator(HeldBy)]); + receivedDate = new FormControl(null, Validators.required); + notes = new FormControl(null); + status = new FormControl(InstrumentStatus.RECEIVED, [ + Validators.required, + this.enumValidator(InstrumentStatus), + ]); + statusDate = new FormControl(null); + explanation = new FormControl(null); + + instrumentTypes = InstrumentType; + heldByOptions = HeldBy; + instrumentStatuses = InstrumentStatus; + + constructor( + @Inject(MAT_DIALOG_DATA) + public data: { + action: DialogAction; + conditionUuid: string; + instrument?: DecisionConditionFinancialInstrumentDto; + service: DecisionConditionFinancialInstrumentService; + }, + private dialogRef: MatDialogRef, + private toastService: ToastService, + ) { + this.form = new FormGroup({ + securityHolderPayee: this.securityHolderPayee, + type: this.type, + issueDate: this.issueDate, + expiryDate: this.expiryDate, + amount: this.amount, + bank: this.bank, + instrumentNumber: this.instrumentNumber, + heldBy: this.heldBy, + receivedDate: this.receivedDate, + notes: this.notes, + status: this.status, + statusDate: this.statusDate, + explanation: this.explanation, + }); + } + + ngOnInit(): void { + this.service = this.data.service; + this.isEdit = this.data.action === DialogAction.EDIT; + + this.form.get('status')?.valueChanges.subscribe((status) => { + if (status === InstrumentStatus.RECEIVED) { + this.form.get('statusDate')?.setValidators([]); + this.form.get('explanation')?.setValidators([]); + } else { + this.form.get('statusDate')?.setValidators([Validators.required]); + this.form.get('explanation')?.setValidators([Validators.required]); + } + + this.form.get('statusDate')?.updateValueAndValidity(); + this.form.get('explanation')?.updateValueAndValidity(); + }); + + if (this.isEdit && this.data.instrument) { + const instrument = this.data.instrument; + this.form.patchValue({ + ...instrument, + issueDate: instrument.issueDate ? new Date(instrument.issueDate) : null, + expiryDate: instrument.expiryDate ? new Date(instrument.expiryDate) : null, + receivedDate: instrument.receivedDate ? new Date(instrument.receivedDate) : null, + statusDate: instrument.statusDate ? new Date(instrument.statusDate) : null, + }); + } + } + + enumValidator(enumType: any): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } | null => { + if (!Object.values(enumType).includes(control.value)) { + return { enum: { value: control.value } }; + } + return null; + }; + } + + mapToDto() { + const formData = this.form.getRawValue(); + const dto: CreateUpdateDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: formData.securityHolderPayee, + type: formData.type, + issueDate: new Date(formData.issueDate).getTime(), + expiryDate: formData.expiryDate ? new Date(formData.expiryDate).getTime() : null, + amount: formData.amount, + bank: formData.bank, + instrumentNumber: formData.instrumentNumber, + heldBy: formData.heldBy, + receivedDate: new Date(formData.receivedDate).getTime(), + notes: formData.notes, + status: formData.status, + statusDate: formData.statusDate ? new Date(formData.statusDate).getTime() : null, + explanation: formData.explanation, + }; + return dto; + } + + async onSubmit() { + try { + const dto = this.mapToDto(); + if (this.isEdit && this.data.instrument?.uuid) { + await this.service!.update(this.data.conditionUuid, this.data.instrument.uuid, dto); + this.toastService.showSuccessToast('Financial Instrument updated successfully'); + } else { + await this.service!.create(this.data.conditionUuid, dto); + this.toastService.showSuccessToast('Financial Instrument created successfully'); + } + this.dialogRef.close({ action: this.isEdit ? DialogAction.EDIT : DialogAction.ADD, successful: true }); + } catch (error: any) { + console.error(error); + this.toastService.showErrorToast(error.message || 'An error occurred'); + } + } +} diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html new file mode 100644 index 000000000..d8cb29186 --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html @@ -0,0 +1,9 @@ +
+
+
Instrument
+ +
+
+
diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss new file mode 100644 index 000000000..a57ba1c65 --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss @@ -0,0 +1,19 @@ +.condition-instrument-container { + width: 100%; + display: flex; + flex-direction: column; + border-radius: 8px; + box-shadow: 0 2px 8px 1px #00000040; + padding: 16px 12px; +} + +.header-container { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: baseline; +} + +.header { + flex-grow: 1; +} diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.spec.ts b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.spec.ts new file mode 100644 index 000000000..3dbbc2fc4 --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DecisionConditionFinancialInstrumentComponent } from './decision-condition-financial-instrument.component'; + +describe('DecisionConditionFinancialInstrumentComponent', () => { + let component: DecisionConditionFinancialInstrumentComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DecisionConditionFinancialInstrumentComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DecisionConditionFinancialInstrumentComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts new file mode 100644 index 000000000..045f7fa18 --- /dev/null +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts @@ -0,0 +1,47 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { DecisionConditionFinancialInstrumentDto } from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; +import { DialogAction } from '../constants'; +import { DecisionConditionFinancialInstrumentDialogComponent } from './decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component'; +import { DecisionConditionFinancialInstrumentService } from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; + +@Component({ + selector: 'app-decision-condition-financial-instrument', + templateUrl: './decision-condition-financial-instrument.component.html', + styleUrl: './decision-condition-financial-instrument.component.scss', +}) +export class DecisionConditionFinancialInstrumentComponent implements OnInit { + @Input() conditionUuid!: string; + + constructor( + private dialog: MatDialog, + private financialInstrumentService: DecisionConditionFinancialInstrumentService, + ) {} + + ngOnInit(): void {} + + onAddInstrument(): void { + const dialogRef = this.dialog.open(DecisionConditionFinancialInstrumentDialogComponent, { + minWidth: '800px', + maxWidth: '1100px', + maxHeight: '80vh', + data: { + conditionUuid: this.conditionUuid, + action: DialogAction.ADD, + service: this.financialInstrumentService, + }, + }); + + dialogRef.afterClosed().subscribe((result) => {}); + } + + onEditInstrument(instrument: DecisionConditionFinancialInstrumentDto): void { + // Logic to edit an instrument + console.log(`Edit instrument with ID: ${instrument.uuid}`); + } + + onDeleteInstrument(instrument: DecisionConditionFinancialInstrumentDto): void { + // Logic to delete an instrument + console.log(`Delete instrument with ID: ${instrument.uuid}`); + } +} diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts index 28ea05cc7..6e2cd35cc 100644 --- a/alcs-frontend/src/app/shared/shared.module.ts +++ b/alcs-frontend/src/app/shared/shared.module.ts @@ -82,6 +82,8 @@ import { CommissionerTagsHeaderComponent } from './tags/commissioner-tags-header import { DocumentUploadDialogComponent } from './document-upload-dialog/document-upload-dialog.component'; import { FlagDialogComponent } from './flag-dialog/flag-dialog.component'; import { UnFlagDialogComponent } from './unflag-dialog/unflag-dialog.component'; +import { DecisionConditionFinancialInstrumentComponent } from './decision-condition-financial-instrument/decision-condition-financial-instrument.component'; +import { DecisionConditionFinancialInstrumentDialogComponent } from './decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component'; @NgModule({ declarations: [ @@ -129,6 +131,8 @@ import { UnFlagDialogComponent } from './unflag-dialog/unflag-dialog.component'; DocumentUploadDialogComponent, FlagDialogComponent, UnFlagDialogComponent, + DecisionConditionFinancialInstrumentComponent, + DecisionConditionFinancialInstrumentDialogComponent, ], imports: [ CommonModule, @@ -157,6 +161,7 @@ import { UnFlagDialogComponent } from './unflag-dialog/unflag-dialog.component'; MatChipsModule, MatAutocompleteModule, MatCheckboxModule, + MatRadioModule, ], exports: [ CommonModule, @@ -236,6 +241,8 @@ import { UnFlagDialogComponent } from './unflag-dialog/unflag-dialog.component'; DocumentUploadDialogComponent, FlagDialogComponent, UnFlagDialogComponent, + DecisionConditionFinancialInstrumentComponent, + DecisionConditionFinancialInstrumentDialogComponent, ], }) export class SharedModule { From 4371c325429013dcf8aab117ca76ff78031cc69f Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Wed, 19 Feb 2025 16:49:14 -0800 Subject: [PATCH 17/87] Add financial instrument component to applications --- .../application/application.component.ts | 10 +- .../condition/condition.component.html | 15 ++- .../condition/condition.component.scss | 4 + .../condition/condition.component.ts | 5 + ...sion-condition-financial-instrument.dto.ts | 8 ++ ...-condition-financial-instrument.service.ts | 98 +++++++++++++++++++ .../application-decision-v2.dto.ts | 4 + 7 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts create mode 100644 alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts diff --git a/alcs-frontend/src/app/features/application/application.component.ts b/alcs-frontend/src/app/features/application/application.component.ts index ac5691070..b77dee87c 100644 --- a/alcs-frontend/src/app/features/application/application.component.ts +++ b/alcs-frontend/src/app/features/application/application.component.ts @@ -33,6 +33,8 @@ import { FileTagService } from '../../services/common/file-tag.service'; import { ApplicationDecisionV2Service } from '../../services/application/decision/application-decision-v2/application-decision-v2.service'; import { ApplicationDecisionConditionCardDto } from '../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ApplicationDecisionConditionCardService } from '../../services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-card/application-decision-condition-card.service'; +import { DecisionConditionFinancialInstrumentService } from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; +import { ApplicationDecisionConditionFinancialInstrumentService } from '../../services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service'; export const unsubmittedRoutes = [ { @@ -175,7 +177,13 @@ export const appChildRoutes = [ selector: 'app-application', templateUrl: './application.component.html', styleUrls: ['./application.component.scss'], - providers: [{ provide: FileTagService, useClass: ApplicationTagService }], + providers: [ + { provide: FileTagService, useClass: ApplicationTagService }, + { + provide: DecisionConditionFinancialInstrumentService, + useClass: ApplicationDecisionConditionFinancialInstrumentService, + }, + ], }) export class ApplicationComponent implements OnInit, OnDestroy { destroy = new Subject(); diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html index eca365eed..fe908f02e 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html @@ -200,7 +200,14 @@

{{ condition.type.label }}

Description
{{ condition.type.label }} >
+ +
+ +
diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss index 31a82f2d7..c630ca0b4 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss @@ -96,3 +96,7 @@ ::ng-deep textarea:focus { outline: none; } + +.condition-instrument { + margin-top: 24px; +} diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts index 383810967..1e74f27fe 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts @@ -9,6 +9,7 @@ import { UpdateApplicationDecisionConditionDto, DateType, ApplicationDecisionConditionDateDto, + conditionType, } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { DECISION_CONDITION_COMPLETE_LABEL, @@ -72,6 +73,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { subdComponent?: ApplicationDecisionComponentDto; planNumbers: ApplicationDecisionConditionToComponentPlanNumberDto[] = []; + isFinancialSecurity: boolean = false; + displayColumns: string[] = ['index', 'due', 'completed', 'comment', 'action']; @ViewChild(MatSort) sort!: MatSort; @@ -114,6 +117,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { this.isRequireSurveyPlan = this.condition.type?.code === 'RSPL'; this.isThreeColumn = this.showAdmFeeField && this.showSecurityAmountField; + this.isFinancialSecurity = this.condition.type?.code === conditionType.FINANCIAL_SECURITY; + this.loadLots(); this.loadPlanNumber(); this.dataSource = new MatTableDataSource( diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts new file mode 100644 index 000000000..5ad6a66d1 --- /dev/null +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.dto.ts @@ -0,0 +1,8 @@ +import { + CreateUpdateDecisionConditionFinancialInstrumentDto, + DecisionConditionFinancialInstrumentDto, +} from '../../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; + +export interface ApplicationDecisionConditionFinancialInstrumentDto extends DecisionConditionFinancialInstrumentDto {} +export interface CreateUpdateApplicationDecisionConditionFinancialInstrumentDto + extends CreateUpdateDecisionConditionFinancialInstrumentDto {} diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts new file mode 100644 index 000000000..c7f042868 --- /dev/null +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts @@ -0,0 +1,98 @@ +import { Injectable } from '@angular/core'; +import { + ApplicationDecisionConditionFinancialInstrumentDto, + CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, +} from './application-decision-condition-financial-instrument.dto'; +import { DecisionConditionFinancialInstrumentService } from '../../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; +import { environment } from '../../../../../../../environments/environment'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class ApplicationDecisionConditionFinancialInstrumentService extends DecisionConditionFinancialInstrumentService { + private baseUrl = `${environment.apiUrl}/v2/application-decision-condition`; + private financialInstrumentUrl = 'financial-instruments'; + + constructor(http: HttpClient) { + super(http); + } + async getAll(conditionUuid: string): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}`; + + try { + return await firstValueFrom(this.http.get(url)); + } catch (e) { + this.handleError(e); + } + } + + get(conditionUuid: string, uuid: string): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; + + try { + return firstValueFrom(this.http.get(url)); + } catch (e) { + this.handleError(e); + } + } + + create( + conditionUuid: string, + dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, + ): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}`; + + try { + return firstValueFrom(this.http.post(url, dto)); + } catch (e) { + this.handleError(e); + } + } + + update( + conditionUuid: string, + uuid: string, + dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, + ): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; + + try { + return firstValueFrom(this.http.patch(url, dto)); + } catch (e) { + this.handleError(e); + } + } + + delete(conditionUuid: string, uuid: string): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; + + try { + return firstValueFrom(this.http.delete(url)); + } catch (e) { + this.handleError(e); + } + } + + private handleError(e: any): never { + console.error(e); + let message; + if (e instanceof HttpErrorResponse) { + if (e.status === 404) { + message = 'Condition/financial instrument not found'; + } else if (e.status === 400) { + message = 'Condition is not of type Financial Security'; + } else { + if (e.error.message === 'Condition type Financial Security not found') { + message = 'Condition type Financial Security not found'; + } else { + message = 'Failed to retrieve the financial instruments'; + } + } + } else { + message = 'Failed to perform the operation'; + } + throw new Error(message); + } +} diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts index 504b541f4..3189773c4 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts @@ -275,6 +275,10 @@ export enum DateType { MULTIPLE = 'Multiple', } +export enum conditionType { + FINANCIAL_SECURITY = 'BOND', +} + export interface ApplicationDecisionConditionTypeDto extends BaseCodeDto { isActive: boolean; isComponentToConditionChecked?: boolean | null; From 148b21730dd6a27781ea8bc59ede552e2fb3d1b2 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Thu, 20 Feb 2025 09:35:32 -0800 Subject: [PATCH 18/87] ALCS-2029 Created order dialog --- ...sion-condition-order-dialog.component.html | 47 ++++++++++++++++ ...sion-condition-order-dialog.component.scss | 53 ++++++++++++++++++ ...n-condition-order-dialog.component.spec.ts | 55 +++++++++++++++++++ ...cision-condition-order-dialog.component.ts | 35 ++++++++++++ .../decision-conditions.component.html | 27 +++++---- .../decision-conditions.component.scss | 5 ++ .../decision-conditions.component.ts | 22 +++++++- .../application/decision/decision.module.ts | 2 + 8 files changed, 233 insertions(+), 13 deletions(-) create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.html create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.scss create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.html new file mode 100644 index 000000000..040937ff6 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.html @@ -0,0 +1,47 @@ +
+
+

Re-order Conditions

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
# + {{ alphaIndex(i + 1) }} + Type + {{ row.type.label }} + Description + {{ row.description }} + + drag_indicator +
+
+
+
+ + +
+
+
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.scss new file mode 100644 index 000000000..2d6fa6292 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.scss @@ -0,0 +1,53 @@ +@use '../../../../../../../../styles/colors.scss' as *; + +.container { + display: flex; + flex-direction: column; + width: 100%; + padding: 12px 8px; + overflow-y: hidden; +} + +.section { + width: 100%; + padding: 16px; +} + +.button-row { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.column-select { + width: 10%; +} + +.column-index { + width: 10%; +} + +.column-type { + width: 30%; +} + +.column-description { + width: 50%; +} + +.conditions-table { + margin-top: 16px; + width: 100%; +} + +.table-container { + max-height: 300px; + overflow-y: auto; + padding: 2px; +} + +.disabled-row { + background-color: #f0f0f0; + opacity: 0.6; + cursor: default; +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts new file mode 100644 index 000000000..319806b79 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts @@ -0,0 +1,55 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MatTableModule } from '@angular/material/table'; +import { MatSortModule } from '@angular/material/sort'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ApplicationDecisionConditionCardService } from '../../../../../services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-card/application-decision-condition-card.service'; +import { BoardService } from '../../../../../services/board/board.service'; +import { ToastService } from '../../../../../services/toast/toast.service'; +import { ConditionCardDialogComponent } from './condition-card-dialog.component'; + +describe('ConditionCardDialogComponent', () => { + let component: ConditionCardDialogComponent; + let fixture: ComponentFixture; + let mockDecisionConditionCardService: DeepMocked; + let mockBoardService: DeepMocked; + let mockToastService: DeepMocked; + + beforeEach(async () => { + mockDecisionConditionCardService = createMock(); + mockBoardService = createMock(); + mockToastService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [ConditionCardDialogComponent], + imports: [ + MatDialogModule, + BrowserAnimationsModule, + MatTableModule, + MatSortModule, + HttpClientTestingModule, + RouterTestingModule, + ], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: { conditions: [], decision: 'decision-uuid' } }, + { provide: MatDialogRef, useValue: {} }, + { provide: ApplicationDecisionConditionCardService, useValue: mockDecisionConditionCardService }, + { provide: BoardService, useValue: mockBoardService }, + { provide: ToastService, useValue: mockToastService }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ConditionCardDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts new file mode 100644 index 000000000..de55a2052 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts @@ -0,0 +1,35 @@ +import { Component, Inject, OnInit, ViewChild } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { ApplicationDecisionConditionDto } from '../../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { countToString } from '../../../../../../../shared/utils/count-to-string'; + +@Component({ + selector: 'app-decision-condition-order-dialog', + templateUrl: './decision-condition-order-dialog.component.html', + styleUrl: './decision-condition-order-dialog.component.scss', +}) +export class DecisionConditionOrderDialogComponent implements OnInit { + displayedColumns = ['index', 'type', 'description', 'actions']; + + constructor( + @Inject(MAT_DIALOG_DATA) + public data: { conditions: { condition: ApplicationDecisionConditionDto; index: number }[]; decision: string }, + private dialogRef: MatDialogRef, + ) {} + + async ngOnInit() { + console.log(this.data); + } + + onCancel(): void { + this.dialogRef.close({ action: 'cancel' }); + } + + onSave(): void { + this.dialogRef.close({ action: 'cancel' }); + } + + alphaIndex(index: number) { + return countToString(index); + } +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html index cb67feab5..4397e652d 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html @@ -1,18 +1,6 @@

Conditions

-
- - -
@@ -33,4 +21,19 @@

Conditions

[index]="index" >
+ diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss index 9031dc63f..1ee8877aa 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss @@ -27,3 +27,8 @@ section { .remove-button { margin-top: 18px !important; } + +.cond-footer { + width: 100%; + text-align: right; +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts index cdf5441b1..9872ec7d2 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts @@ -21,6 +21,8 @@ import { import { ApplicationDecisionV2Service } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; import { ConfirmationDialogService } from '../../../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DecisionConditionComponent } from './decision-condition/decision-condition.component'; +import { DecisionConditionOrderDialogComponent } from './decision-condition-order-dialog/decision-condition-order-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; export type TempApplicationDecisionConditionDto = UpdateApplicationDecisionConditionDto & { tempUuid?: string }; export type SelectableComponent = { uuid?: string; tempId: string; decisionUuid: string; code: string; label: string }; @@ -56,6 +58,7 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy constructor( private decisionService: ApplicationDecisionV2Service, private confirmationDialogService: ConfirmationDialogService, + protected dialog: MatDialog, ) {} ngOnInit(): void { @@ -215,7 +218,24 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy this.conditionComponents.forEach((component) => component.form.markAllAsTouched()); } + emitChanges() { + console.log('emit changes'); + } + openOrderDialog() { - alert('open dialog'); + this.dialog + .open(DecisionConditionOrderDialogComponent, { + maxHeight: '80vh', + minHeight: '40vh', + minWidth: '80vh', + data: { + conditions: this.conditions, + }, + }) + .beforeClosed() + .subscribe(async () => { + alert('subscribe'); + this.emitChanges(); + }); } } diff --git a/alcs-frontend/src/app/features/application/decision/decision.module.ts b/alcs-frontend/src/app/features/application/decision/decision.module.ts index 4b3fd6922..f2168d0fb 100644 --- a/alcs-frontend/src/app/features/application/decision/decision.module.ts +++ b/alcs-frontend/src/app/features/application/decision/decision.module.ts @@ -35,6 +35,7 @@ import { RevertToDraftDialogComponent } from './decision-v2/revert-to-draft-dial import { DecisionComponent } from './decision.component'; import { DecisionConditionDateDialogComponent } from './decision-v2/decision-input/decision-conditions/decision-condition/decision-condition-date-dialog/decision-condition-date-dialog.component'; import { ConditionCardDialogComponent } from './conditions/condition-card-dialog/condition-card-dialog.component'; +import { DecisionConditionOrderDialogComponent } from './decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component'; export const decisionChildRoutes = [ { @@ -96,6 +97,7 @@ export const decisionChildRoutes = [ ConditionComponent, BasicComponent, ConditionCardDialogComponent, + DecisionConditionOrderDialogComponent, ], imports: [SharedModule, RouterModule.forChild(decisionChildRoutes), MatTabsModule, MatOptionModule, MatChipsModule], }) From c03d215aa7821b0ebd5293a889bd37fdd52fb3cc Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Thu, 20 Feb 2025 12:51:24 -0800 Subject: [PATCH 19/87] Add application condition financial instrument table and functionalities - Add edit and delete functionalities - Add instrument table - Add validation for instrument number field --- ...ition-financial-instrument.service.spec.ts | 335 ++++++++++++++++++ ...-condition-financial-instrument.service.ts | 26 +- ...ancial-instrument-dialog.component.spec.ts | 12 +- ...n-financial-instrument-dialog.component.ts | 14 +- ...dition-financial-instrument.component.html | 63 +++- ...dition-financial-instrument.component.scss | 44 +++ ...ion-financial-instrument.component.spec.ts | 18 +- ...ondition-financial-instrument.component.ts | 82 ++++- 8 files changed, 567 insertions(+), 27 deletions(-) create mode 100644 alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts new file mode 100644 index 000000000..f1dc449b1 --- /dev/null +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts @@ -0,0 +1,335 @@ +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { ApplicationDecisionConditionFinancialInstrumentService } from './application-decision-condition-financial-instrument.service'; +import { + ApplicationDecisionConditionFinancialInstrumentDto, + CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, +} from './application-decision-condition-financial-instrument.dto'; +import { + HeldBy, + InstrumentStatus, + InstrumentType, +} from '../../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; +import { environment } from '../../../../../../../environments/environment'; + +describe('ApplicationDecisionConditionFinancialInstrumentService', () => { + let service: ApplicationDecisionConditionFinancialInstrumentService; + let mockHttpClient: DeepMocked; + const conditionId = '1'; + const instrumentId = '1'; + let expectedUrl: string; + + beforeEach(() => { + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + }); + service = TestBed.inject(ApplicationDecisionConditionFinancialInstrumentService); + expectedUrl = `${environment.apiUrl}/v2/application-decision-condition/${conditionId}/financial-instruments`; + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make an http get for getAll', async () => { + const mockResponse: ApplicationDecisionConditionFinancialInstrumentDto[] = [ + { + uuid: '1', + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }, + ]; + + mockHttpClient.get.mockReturnValue(of(mockResponse)); + + const result = await service.getAll(conditionId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(expectedUrl); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if getAll fails', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.getAll(conditionId)).rejects.toThrow('Failed to retrieve the financial instruments'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(expectedUrl); + }); + + it('should make an http get for get', async () => { + const mockResponse: ApplicationDecisionConditionFinancialInstrumentDto = { + uuid: '1', + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + mockHttpClient.get.mockReturnValue(of(mockResponse)); + + const result = await service.get(conditionId, instrumentId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if get fails', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should make an http post for create', async () => { + const mockRequest: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + const mockResponse: ApplicationDecisionConditionFinancialInstrumentDto = { + uuid: '1', + ...mockRequest, + }; + + mockHttpClient.post.mockReturnValue(of(mockResponse)); + + const result = await service.create(conditionId, mockRequest); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post).toHaveBeenCalledWith(expectedUrl, mockRequest); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if create fails', async () => { + const mockRequest: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + mockHttpClient.post.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.create(conditionId, mockRequest)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post).toHaveBeenCalledWith(expectedUrl, mockRequest); + }); + + it('should make an http patch for update', async () => { + const mockRequest: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + const mockResponse: ApplicationDecisionConditionFinancialInstrumentDto = { + uuid: '1', + ...mockRequest, + }; + + mockHttpClient.patch.mockReturnValue(of(mockResponse)); + + const result = await service.update(conditionId, instrumentId, mockRequest); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`, mockRequest); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if update fails', async () => { + const mockRequest: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + mockHttpClient.patch.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.update(conditionId, instrumentId, mockRequest)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`, mockRequest); + }); + + it('should make an http delete for delete', async () => { + mockHttpClient.delete.mockReturnValue(of({})); + + await service.delete(conditionId, instrumentId); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should show an error if delete fails', async () => { + mockHttpClient.delete.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.delete(conditionId, instrumentId)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should remove statusDate and explanation when status is RECEIVED during update', async () => { + const mockRequest: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + statusDate: 1672531200000, + explanation: 'test', + instrumentNumber: '123', + }; + + const mockResponse: ApplicationDecisionConditionFinancialInstrumentDto = { + uuid: '1', + ...mockRequest, + }; + + mockHttpClient.patch.mockReturnValue(of(mockResponse)); + + await service.update(conditionId, instrumentId, mockRequest); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`, { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }); + }); + + it('should throw Condition/financial instrument not found', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => new HttpErrorResponse({ status: 404, error: { message: 'Condition/financial instrument not found' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Condition/financial instrument not found'), + ); + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should throw Condition is not of type Financial Security', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => new HttpErrorResponse({ status: 400, error: { message: 'Condition is not of type Financial Security' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Condition is not of type Financial Security'), + ); + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should throw Condition type Financial Security not found', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => new HttpErrorResponse({ status: 500, error: { message: 'Condition type Financial Security not found' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Condition type Financial Security not found'), + ); + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); +}); diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts index c7f042868..00730ae3d 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts @@ -7,6 +7,7 @@ import { DecisionConditionFinancialInstrumentService } from '../../../../../comm import { environment } from '../../../../../../../environments/environment'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { firstValueFrom } from 'rxjs'; +import { InstrumentStatus } from '../../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; @Injectable({ providedIn: 'root', @@ -28,48 +29,57 @@ export class ApplicationDecisionConditionFinancialInstrumentService extends Deci } } - get(conditionUuid: string, uuid: string): Promise { + async get(conditionUuid: string, uuid: string): Promise { const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; try { - return firstValueFrom(this.http.get(url)); + return await firstValueFrom(this.http.get(url)); } catch (e) { this.handleError(e); } } - create( + async create( conditionUuid: string, dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, ): Promise { const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}`; try { - return firstValueFrom(this.http.post(url, dto)); + return await firstValueFrom(this.http.post(url, dto)); } catch (e) { this.handleError(e); } } - update( + async update( conditionUuid: string, uuid: string, dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto, ): Promise { const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; + if (dto.status === InstrumentStatus.RECEIVED) { + if (dto.statusDate) { + delete dto.statusDate; + } + if (dto.explanation) { + delete dto.explanation; + } + } + try { - return firstValueFrom(this.http.patch(url, dto)); + return await firstValueFrom(this.http.patch(url, dto)); } catch (e) { this.handleError(e); } } - delete(conditionUuid: string, uuid: string): Promise { + async delete(conditionUuid: string, uuid: string): Promise { const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; try { - return firstValueFrom(this.http.delete(url)); + return await firstValueFrom(this.http.delete(url)); } catch (e) { this.handleError(e); } diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.spec.ts b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.spec.ts index 109021608..a3a2d5098 100644 --- a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.spec.ts +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { DecisionConditionFinancialInstrumentDialogComponent } from './decision-condition-financial-instrument-dialog.component'; @@ -8,10 +9,13 @@ describe('DecisionConditionFinancialInstrumentDialogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [DecisionConditionFinancialInstrumentDialogComponent] - }) - .compileComponents(); - + declarations: [DecisionConditionFinancialInstrumentDialogComponent], + providers: [ + { provide: MatDialogRef, useValue: {} }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(DecisionConditionFinancialInstrumentDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.ts b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.ts index 40150098d..1e227df17 100644 --- a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.ts +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, Input, OnInit } from '@angular/core'; +import { Component, Inject, Input, OnInit, ViewChild } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { DialogAction } from '../../constants'; import { @@ -30,7 +30,7 @@ export class DecisionConditionFinancialInstrumentDialogComponent implements OnIn expiryDate = new FormControl(null); amount = new FormControl(null, Validators.required); bank = new FormControl('', Validators.required); - instrumentNumber = new FormControl(null); + instrumentNumber = new FormControl(null, Validators.required); heldBy = new FormControl(null, [Validators.required, this.enumValidator(HeldBy)]); receivedDate = new FormControl(null, Validators.required); notes = new FormControl(null); @@ -90,6 +90,15 @@ export class DecisionConditionFinancialInstrumentDialogComponent implements OnIn this.form.get('explanation')?.updateValueAndValidity(); }); + this.form.get('type')?.valueChanges.subscribe((type) => { + if (type === InstrumentType.EFT) { + this.form.get('instrumentNumber')?.setValidators([]); + } else { + this.form.get('instrumentNumber')?.setValidators([Validators.required]); + } + this.form.get('instrumentNumber')?.updateValueAndValidity(); + }); + if (this.isEdit && this.data.instrument) { const instrument = this.data.instrument; this.form.patchValue({ @@ -145,6 +154,7 @@ export class DecisionConditionFinancialInstrumentDialogComponent implements OnIn } catch (error: any) { console.error(error); this.toastService.showErrorToast(error.message || 'An error occurred'); + this.dialogRef.close({ action: this.isEdit ? DialogAction.EDIT : DialogAction.ADD, successful: false }); } } } diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html index d8cb29186..00038bbf8 100644 --- a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html @@ -5,5 +5,66 @@
Instrument
+ Instrument
-
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Amount{{ element.amount | number }}Type{{ element.type }}Bank{{ element.bank }}Instrument # + + {{ element.instrumentNumber }} + Received Date + {{ element.receivedDate | date: 'yyyy-MMM-dd' }} + Status{{ getFormattedStatus(element) }}Action + + +
+
+
diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss index a57ba1c65..591f75be5 100644 --- a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss @@ -17,3 +17,47 @@ .header { flex-grow: 1; } + +.table-container { + overflow-x: auto; // Enable horizontal scrolling for smaller screens +} + +.instruments-table { + width: 100%; + border-spacing: 0; + + th.mat-header-cell, + td.mat-cell { + padding: 2px; + border-bottom: 1px solid #ddd; + text-align: left; + } + + .column-amount { + width: 10%; + } + + .column-type { + width: 15%; + } + + .column-bank { + width: 10%; + } + + .column-instrument-number { + width: 15%; + } + + .column-received-date { + width: 15%; + } + + .column-status { + width: 20%; + } + + .column-action { + width: 15%; + } +} diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.spec.ts b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.spec.ts index 3dbbc2fc4..0af13d36d 100644 --- a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.spec.ts +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.spec.ts @@ -1,17 +1,27 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DecisionConditionFinancialInstrumentComponent } from './decision-condition-financial-instrument.component'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { HttpClient } from '@angular/common/http'; +import { DecisionConditionFinancialInstrumentService } from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; describe('DecisionConditionFinancialInstrumentComponent', () => { let component: DecisionConditionFinancialInstrumentComponent; let fixture: ComponentFixture; + let mockHttpClient: DeepMocked; + let mockFinancialInstrumentService: DeepMocked; beforeEach(async () => { + mockHttpClient = createMock(); + mockFinancialInstrumentService = createMock(); await TestBed.configureTestingModule({ - declarations: [DecisionConditionFinancialInstrumentComponent] - }) - .compileComponents(); - + declarations: [DecisionConditionFinancialInstrumentComponent], + providers: [ + { provide: HttpClient, useValue: mockHttpClient }, + { provide: DecisionConditionFinancialInstrumentService, useValue: mockFinancialInstrumentService }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(DecisionConditionFinancialInstrumentComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts index 045f7fa18..fd4941d0c 100644 --- a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts @@ -1,9 +1,13 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; +import { MatSort, Sort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; import { DecisionConditionFinancialInstrumentDto } from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; +import { DecisionConditionFinancialInstrumentService } from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; import { DialogAction } from '../constants'; +import { ConfirmationDialogService } from '../confirmation-dialog/confirmation-dialog.service'; +import { ToastService } from '../../services/toast/toast.service'; import { DecisionConditionFinancialInstrumentDialogComponent } from './decision-condition-financial-instrument-dialog/decision-condition-financial-instrument-dialog.component'; -import { DecisionConditionFinancialInstrumentService } from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; @Component({ selector: 'app-decision-condition-financial-instrument', @@ -13,12 +17,28 @@ import { DecisionConditionFinancialInstrumentService } from '../../services/comm export class DecisionConditionFinancialInstrumentComponent implements OnInit { @Input() conditionUuid!: string; + displayColumns: string[] = ['Amount', 'Type', 'Bank', 'Instrument #', 'Received Date', 'Status', 'Action']; + @ViewChild(MatSort) sort!: MatSort; + dataSource: MatTableDataSource = new MatTableDataSource(); + + instruments: DecisionConditionFinancialInstrumentDto[] = []; + constructor( private dialog: MatDialog, private financialInstrumentService: DecisionConditionFinancialInstrumentService, + private confirmationDialogService: ConfirmationDialogService, + private toastService: ToastService, ) {} - ngOnInit(): void {} + ngOnInit(): void { + this.initData(); + } + + async initData() { + this.instruments = await this.financialInstrumentService.getAll(this.conditionUuid); + this.instruments.sort((a, b) => (a.receivedDate < b.receivedDate ? -1 : 1)); + this.dataSource.data = this.instruments; + } onAddInstrument(): void { const dialogRef = this.dialog.open(DecisionConditionFinancialInstrumentDialogComponent, { @@ -32,16 +52,62 @@ export class DecisionConditionFinancialInstrumentComponent implements OnInit { }, }); - dialogRef.afterClosed().subscribe((result) => {}); + dialogRef.afterClosed().subscribe((result: { action: DialogAction; successful: boolean }) => { + if (result.successful) { + this.initData(); + } + }); } onEditInstrument(instrument: DecisionConditionFinancialInstrumentDto): void { - // Logic to edit an instrument - console.log(`Edit instrument with ID: ${instrument.uuid}`); + const dialogRef = this.dialog.open(DecisionConditionFinancialInstrumentDialogComponent, { + minWidth: '800px', + maxWidth: '1100px', + maxHeight: '80vh', + data: { + conditionUuid: this.conditionUuid, + action: DialogAction.EDIT, + instrument: instrument, + service: this.financialInstrumentService, + }, + }); + + dialogRef.afterClosed().subscribe((result: { action: DialogAction; successful: boolean }) => { + if (result.successful) { + this.initData(); + } + }); } onDeleteInstrument(instrument: DecisionConditionFinancialInstrumentDto): void { - // Logic to delete an instrument - console.log(`Delete instrument with ID: ${instrument.uuid}`); + this.confirmationDialogService + .openDialog({ body: 'Are you sure you want to delete this instrument?' }) + .subscribe(async (confirmed) => { + if (confirmed) { + try { + await this.financialInstrumentService.delete(this.conditionUuid, instrument.uuid); + this.toastService.showSuccessToast('Instrument successfully deleted'); + this.initData(); + } catch (e) { + this.toastService.showErrorToast('Failed to delete the instrument'); + } + } + }); + } + + getFormattedStatus(instrument: DecisionConditionFinancialInstrumentDto): string { + if (instrument.status === 'Received') { + return instrument.status; + } + + if (instrument.receivedDate) { + return `${instrument.status} on ${new Date(instrument.receivedDate).toLocaleDateString('en-CA', { + year: 'numeric', + month: 'short', + day: '2-digit', + })}`; + } + + return instrument.status; } } From 07fb058744c2e21cc4005829cf61959287b45391 Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Thu, 20 Feb 2025 12:52:53 -0800 Subject: [PATCH 20/87] Add instrument number validation --- ...ication-decision-condition-financial-instrument.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts index dedaa3250..a1bf213ad 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { ApplicationDecisionConditionFinancialInstrument, InstrumentStatus, + InstrumentType, } from './application-decision-condition-financial-instrument.entity'; import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; @@ -159,6 +160,9 @@ export class ApplicationDecisionConditionFinancialInstrumentService { entity.expiryDate = dto.expiryDate ? new Date(dto.expiryDate) : null; entity.amount = dto.amount; entity.bank = dto.bank; + if (dto.type !== InstrumentType.EFT && !dto.instrumentNumber) { + throw new ServiceValidationException('Instrument number is required when type is not EFT'); + } entity.instrumentNumber = dto.instrumentNumber ?? null; entity.heldBy = dto.heldBy; entity.receivedDate = new Date(dto.receivedDate); From a71f3f78ff8769a272ca81539aa6fc6eb884cfe2 Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Thu, 20 Feb 2025 12:53:11 -0800 Subject: [PATCH 21/87] Change soft delete to hard delete --- ...sion-condition-financial-instrument.service.spec.ts | 10 +++++----- ...-decision-condition-financial-instrument.service.ts | 4 ++-- .../application-decision-condition.controller.spec.ts | 4 ++-- .../application-decision-condition.controller.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts index 4eea167c8..21f66672f 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts @@ -203,8 +203,8 @@ describe('ApplicationDecisionConditionFinancialInstrumentService', () => { }); }); - describe('softRemove', () => { - it('should soft remove a financial instrument', async () => { + describe('remove', () => { + it('should remove a financial instrument', async () => { const conditionUuid = 'condition-uuid'; const uuid = 'instrument-uuid'; const condition = new ApplicationDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); @@ -213,15 +213,15 @@ describe('ApplicationDecisionConditionFinancialInstrumentService', () => { mockConditionTypeRepository.findOne.mockResolvedValue(mockApplicationDecisionConditionType); mockConditionRepository.findOne.mockResolvedValue(condition); mockRepository.findOne.mockResolvedValue(financialInstrument); - mockRepository.softRemove.mockResolvedValue(financialInstrument); + mockRepository.remove.mockResolvedValue(financialInstrument); - const result = await service.softRemove(conditionUuid, uuid); + const result = await service.remove(conditionUuid, uuid); expect(result).toEqual(financialInstrument); expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ where: { code: 'BOND' } }); expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { uuid, condition: { uuid: conditionUuid } } }); - expect(mockRepository.softRemove).toHaveBeenCalledWith(financialInstrument); + expect(mockRepository.remove).toHaveBeenCalledWith(financialInstrument); }); }); }); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts index a1bf213ad..5e163ba2d 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.ts @@ -128,7 +128,7 @@ export class ApplicationDecisionConditionFinancialInstrumentService { return this.repository.save(instrument); } - async softRemove(conditionUuid: string, uuid: string): Promise { + async remove(conditionUuid: string, uuid: string): Promise { await this.throwErrorIfFinancialSecurityTypeNotExists(); const condition = await this.applicationDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); @@ -147,7 +147,7 @@ export class ApplicationDecisionConditionFinancialInstrumentService { throw new ServiceNotFoundException(`Instrument with uuid ${uuid} not found`); } - return await this.repository.softRemove(instrument); + return await this.repository.remove(instrument); } private mapDtoToEntity( diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts index 195c1e144..fbf1f7273 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts @@ -220,11 +220,11 @@ describe('ApplicationDecisionConditionController', () => { receivedDate: new Date(), status: InstrumentStatus.RECEIVED, }); - mockApplicationDecisionConditionFinancialInstrumentService.softRemove.mockResolvedValue(financialInstrument); + mockApplicationDecisionConditionFinancialInstrumentService.remove.mockResolvedValue(financialInstrument); const result = await controller.deleteFinancialInstrument(conditionUuid, instrumentUuid); - expect(mockApplicationDecisionConditionFinancialInstrumentService.softRemove).toHaveBeenCalledWith( + expect(mockApplicationDecisionConditionFinancialInstrumentService.remove).toHaveBeenCalledWith( conditionUuid, instrumentUuid, ); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts index 78cfe2720..85d427424 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts @@ -146,7 +146,7 @@ export class ApplicationDecisionConditionController { @Param('uuid') uuid: string, @Param('instrumentUuid') instrumentUuid: string, ): Promise { - const result = await this.conditionFinancialInstrumentService.softRemove(uuid, instrumentUuid); + const result = await this.conditionFinancialInstrumentService.remove(uuid, instrumentUuid); return await this.mapper.map( result, ApplicationDecisionConditionFinancialInstrument, From 7fe16f5c310c4c45003c937f81f8c33114493d56 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Thu, 20 Feb 2025 11:06:52 -0800 Subject: [PATCH 22/87] ALCS-2560 Change the search to APP/NOI File number --- .../alcs/src/alcs/home/home.controller.ts | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/services/apps/alcs/src/alcs/home/home.controller.ts b/services/apps/alcs/src/alcs/home/home.controller.ts index 31051845e..3a0087580 100644 --- a/services/apps/alcs/src/alcs/home/home.controller.ts +++ b/services/apps/alcs/src/alcs/home/home.controller.ts @@ -3,7 +3,7 @@ import { ApiOAuth2 } from '@nestjs/swagger'; import { Mapper } from 'automapper-core'; import { InjectMapper } from 'automapper-nestjs'; import * as config from 'config'; -import { In, Not, Repository } from 'typeorm'; +import { FindOptionsRelations, In, Not, Repository } from 'typeorm'; import { ANY_AUTH_ROLE } from '../../common/authorization/roles'; import { RolesGuard } from '../../common/authorization/roles-guard.service'; import { UserRoles } from '../../common/authorization/roles.decorator'; @@ -49,6 +49,40 @@ import { InjectRepository } from '@nestjs/typeorm'; const HIDDEN_CARD_STATUSES = [CARD_STATUS.CANCELLED, CARD_STATUS.DECISION_RELEASED]; +const DEFAULT_APP_RELATIONS: FindOptionsRelations = { + application: { + type: true, + region: true, + localGovernment: true, + decisionMeetings: true, + }, + card: { + board: true, + type: true, + status: true, + assignee: true, + }, + modifiesDecisions: true, + resultingDecision: true, + reviewOutcome: true, +}; + +const DEFAULT_NOI_RELATIONS: FindOptionsRelations = { + noticeOfIntent: { + region: true, + localGovernment: true, + }, + card: { + board: true, + type: true, + status: true, + assignee: true, + }, + modifiesDecisions: true, + resultingDecision: true, + reviewOutcome: true, +}; + @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('home') @UseGuards(RolesGuard) @@ -308,20 +342,17 @@ export class HomeController { if (!condition.conditionCard?.card) { continue; } + const appModifications = await this.modificationApplicationRepository.find({ - where: { - modifiesDecisions: { - uuid: condition.decision?.uuid, - }, - }, + where: { application: { fileNumber: condition.decision.application.fileNumber } }, + relations: DEFAULT_APP_RELATIONS, }); + const appReconsiderations = await this.reconsiderationApplicationRepository.find({ - where: { - reconsidersDecisions: { - uuid: condition.decision?.uuid, - }, - }, + where: { application: { fileNumber: condition.decision.application.fileNumber } }, + relations: DEFAULT_APP_RELATIONS, }); + for (const subtask of condition.conditionCard?.card?.subtasks) { result.push({ isCondition: true, @@ -368,11 +399,8 @@ export class HomeController { continue; } const noiModifications = await this.modificationNoticeOfIntentRepository.find({ - where: { - modifiesDecisions: { - uuid: condition.decision?.uuid, - }, - }, + where: { noticeOfIntent: { fileNumber: condition.decision.noticeOfIntent.fileNumber } }, + relations: DEFAULT_NOI_RELATIONS, }); for (const subtask of condition.conditionCard?.card?.subtasks) { result.push({ From c818f3e0ef4fbd3392e2ba0cbf23b175d52fc979 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Thu, 20 Feb 2025 12:19:22 -0800 Subject: [PATCH 23/87] ALCS-2560 Change MODI and RECON calls --- .../application-decision-condition.service.ts | 27 +++++++++++-------- .../alcs/src/alcs/home/home.controller.ts | 18 ------------- ...notice-of-intent-decision-condition.dto.ts | 4 +-- ...ce-of-intent-decision-condition.service.ts | 19 ++++++++----- 4 files changed, 30 insertions(+), 38 deletions(-) diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts index 4d06eabcb..d38b2a5ab 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { FindOptionsWhere, In, IsNull, Repository } from 'typeorm'; +import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Repository } from 'typeorm'; import { ServiceValidationException } from '../../../../../../libs/common/src/exceptions/base.exception'; import { ApplicationDecisionConditionToComponentLot } from '../application-condition-to-component-lot/application-decision-condition-to-component-lot.entity'; import { ApplicationDecisionConditionComponentPlanNumber } from '../application-decision-component-to-condition/application-decision-component-to-condition-plan-number.entity'; @@ -32,6 +32,15 @@ export class ApplicationDecisionConditionService { assignee: true, }; + private DEFAULT_APP_RELATIONS: FindOptionsRelations = { + application: { + type: true, + region: true, + localGovernment: true, + decisionMeetings: true, + }, + }; + constructor( @InjectRepository(ApplicationDecisionCondition) private repository: Repository, @@ -109,19 +118,15 @@ export class ApplicationDecisionConditionService { const condition = this.mapper.map(c, ApplicationDecisionCondition, ApplicationDecisionConditionHomeDto); const decision = this.mapper.map(c.decision, ApplicationDecision, ApplicationDecisionHomeDto); const application = this.mapper.map(c.decision.application, Application, ApplicationHomeDto); + const appModifications = await this.modificationRepository.find({ - where: { - modifiesDecisions: { - uuid: c.decision?.uuid, - }, - }, + where: { application: { fileNumber: condition?.decision?.application.fileNumber } }, + relations: this.DEFAULT_APP_RELATIONS, }); + const appReconsiderations = await this.reconsiderationRepository.find({ - where: { - reconsidersDecisions: { - uuid: c.decision?.uuid, - }, - }, + where: { application: { fileNumber: condition?.decision?.application.fileNumber } }, + relations: this.DEFAULT_APP_RELATIONS, }); return { diff --git a/services/apps/alcs/src/alcs/home/home.controller.ts b/services/apps/alcs/src/alcs/home/home.controller.ts index 3a0087580..18c40eaf3 100644 --- a/services/apps/alcs/src/alcs/home/home.controller.ts +++ b/services/apps/alcs/src/alcs/home/home.controller.ts @@ -56,15 +56,6 @@ const DEFAULT_APP_RELATIONS: FindOptionsRelations = { localGovernment: true, decisionMeetings: true, }, - card: { - board: true, - type: true, - status: true, - assignee: true, - }, - modifiesDecisions: true, - resultingDecision: true, - reviewOutcome: true, }; const DEFAULT_NOI_RELATIONS: FindOptionsRelations = { @@ -72,15 +63,6 @@ const DEFAULT_NOI_RELATIONS: FindOptionsRelations = region: true, localGovernment: true, }, - card: { - board: true, - type: true, - status: true, - assignee: true, - }, - modifiesDecisions: true, - resultingDecision: true, - reviewOutcome: true, }; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts index 30399bbcb..b19bf6734 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts @@ -116,7 +116,7 @@ export class NoticeOfIntentHomeDto { @AutoMap(() => NoticeOfIntentTypeDto) type: NoticeOfIntentTypeDto; - activeDays: number; + activeDays?: number; paused: boolean; pausedDays: number; } @@ -126,7 +126,7 @@ export class NoticeOfIntentDecisionHomeDto { uuid: string; @AutoMap() - application: NoticeOfIntentHomeDto; + noticeOfIntent: NoticeOfIntentHomeDto; } export class NoticeOfIntentDecisionConditionHomeDto { diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts index 80fdbe900..df5570920 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { FindOptionsWhere, In, IsNull, Repository } from 'typeorm'; +import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Repository } from 'typeorm'; import { ServiceValidationException } from '../../../../../../libs/common/src/exceptions/base.exception'; import { NoticeOfIntentDecisionComponent } from '../notice-of-intent-decision-component/notice-of-intent-decision-component.entity'; import { NoticeOfIntentDecisionConditionType } from './notice-of-intent-decision-condition-code.entity'; @@ -29,6 +29,14 @@ export class NoticeOfIntentDecisionConditionService { assignee: true, }; + private DEFAULT_NOI_RELATIONS: FindOptionsRelations = { + noticeOfIntent: { + type: true, + region: true, + localGovernment: true, + }, + }; + constructor( @InjectRepository(NoticeOfIntentDecisionCondition) private repository: Repository, @@ -132,11 +140,8 @@ export class NoticeOfIntentDecisionConditionService { const decision = this.mapper.map(c.decision, NoticeOfIntentDecision, NoticeOfIntentDecisionHomeDto); const noticeOfIntent = this.mapper.map(c.decision.noticeOfIntent, NoticeOfIntent, NoticeOfIntentHomeDto); const appModifications = await this.modificationRepository.find({ - where: { - modifiesDecisions: { - uuid: c.decision?.uuid, - }, - }, + where: { noticeOfIntent: { fileNumber: condition?.decision?.noticeOfIntent.fileNumber } }, + relations: this.DEFAULT_NOI_RELATIONS, }); return { @@ -147,7 +152,7 @@ export class NoticeOfIntentDecisionConditionService { noticeOfIntent: { ...noticeOfIntent, activeDays: undefined, - pausedDays: timeMap.get(noticeOfIntent.uuid)?.pausedDays ?? null, + pausedDays: timeMap.get(noticeOfIntent.uuid)!.pausedDays || 0, paused: timeMap.get(noticeOfIntent.uuid)?.pausedDays !== null, }, }, From 281949fa6b68ca6b4a223a4846e4aefae6105de6 Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Thu, 20 Feb 2025 14:09:04 -0800 Subject: [PATCH 24/87] Fix tests --- ...on-decision-condition-financial-instrument.service.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts index 21f66672f..e10397019 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.service.spec.ts @@ -143,7 +143,7 @@ describe('ApplicationDecisionConditionFinancialInstrumentService', () => { const conditionUuid = 'condition-uuid'; const dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { securityHolderPayee: 'holder', - type: InstrumentType.BANK_DRAFT, + type: InstrumentType.EFT, issueDate: new Date().getTime(), amount: 100, bank: 'bank', @@ -175,7 +175,7 @@ describe('ApplicationDecisionConditionFinancialInstrumentService', () => { const uuid = 'instrument-uuid'; const dto: CreateUpdateApplicationDecisionConditionFinancialInstrumentDto = { securityHolderPayee: 'holder', - type: InstrumentType.BANK_DRAFT, + type: InstrumentType.EFT, issueDate: new Date().getTime(), amount: 100, bank: 'bank', From d68775ab7e0f1acfb407d3f232576855ee53401c Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Thu, 20 Feb 2025 14:40:23 -0800 Subject: [PATCH 25/87] Allow paragraphs on staff comments field for applications and NOIs --- .../application/proposal/proposal.component.html | 1 + .../notice-of-intent/proposal/proposal.component.html | 1 + .../inline-textarea-edit.component.html | 11 ++++++++--- .../inline-textarea-edit.component.scss | 4 ++++ .../inline-textarea-edit.component.ts | 1 + 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/alcs-frontend/src/app/features/application/proposal/proposal.component.html b/alcs-frontend/src/app/features/application/proposal/proposal.component.html index 4d04777a9..dc252e1cc 100644 --- a/alcs-frontend/src/app/features/application/proposal/proposal.component.html +++ b/alcs-frontend/src/app/features/application/proposal/proposal.component.html @@ -58,6 +58,7 @@
Parcels
Staff Comments and Observations
diff --git a/alcs-frontend/src/app/features/notice-of-intent/proposal/proposal.component.html b/alcs-frontend/src/app/features/notice-of-intent/proposal/proposal.component.html index 04c06fd85..bc8e4a0bd 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/proposal/proposal.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/proposal/proposal.component.html @@ -49,6 +49,7 @@
Parcels
Staff Comments and Observations
diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.html b/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.html index b3031b168..7e3cea29e 100644 --- a/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.html +++ b/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.html @@ -1,8 +1,13 @@
- {{ placeholder}} - {{ value }} + {{ placeholder }} + +
{{ value }}
+
+ + {{ value }} +
edit @@ -11,7 +16,7 @@
From ccb91d3db7cbc8a0eeceb332c5b2c3e676509b0a Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Fri, 21 Feb 2025 12:34:48 -0800 Subject: [PATCH 29/87] Allow paragraph in proposal summary field This applies to applications and NOIs --- .../app/features/application/overview/overview.component.html | 1 + .../features/notice-of-intent/overview/overview.component.html | 1 + 2 files changed, 2 insertions(+) diff --git a/alcs-frontend/src/app/features/application/overview/overview.component.html b/alcs-frontend/src/app/features/application/overview/overview.component.html index 99ee613b8..e174490da 100644 --- a/alcs-frontend/src/app/features/application/overview/overview.component.html +++ b/alcs-frontend/src/app/features/application/overview/overview.component.html @@ -13,6 +13,7 @@

Overview

Proposal Summary
diff --git a/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.html b/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.html index 8e8960fa3..cfc537b6d 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.html @@ -13,6 +13,7 @@

Overview

Proposal Summary
From 1e9360b59c71313313d9fa79f876bf18c9252bfd Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Fri, 21 Feb 2025 14:53:22 -0800 Subject: [PATCH 30/87] Fix instrument table UI bugs - Inherit the parent background colour - Fix status date format - Fix horizontal scroll bar showing up for all sizes on Safari - Hide table columns when there is no data --- ...dition-financial-instrument.component.html | 2 +- ...dition-financial-instrument.component.scss | 7 +++++- ...ondition-financial-instrument.component.ts | 22 ++++++++++--------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html index 00038bbf8..19f27977e 100644 --- a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.html @@ -6,7 +6,7 @@
Instrument
-
+
diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss index 591f75be5..4794e44c1 100644 --- a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.scss @@ -19,12 +19,17 @@ } .table-container { - overflow-x: auto; // Enable horizontal scrolling for smaller screens + overflow-x: hidden; + + @media screen and (max-width: 1152px) { + overflow-x: scroll; + } } .instruments-table { width: 100%; border-spacing: 0; + background-color: inherit; th.mat-header-cell, td.mat-cell { diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts index fd4941d0c..ae4558e42 100644 --- a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts @@ -1,8 +1,11 @@ -import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; +import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatSort, Sort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; -import { DecisionConditionFinancialInstrumentDto } from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; +import { + DecisionConditionFinancialInstrumentDto, + InstrumentStatus, +} from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; import { DecisionConditionFinancialInstrumentService } from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; import { DialogAction } from '../constants'; import { ConfirmationDialogService } from '../confirmation-dialog/confirmation-dialog.service'; @@ -96,18 +99,17 @@ export class DecisionConditionFinancialInstrumentComponent implements OnInit { } getFormattedStatus(instrument: DecisionConditionFinancialInstrumentDto): string { - if (instrument.status === 'Received') { + if (instrument.status === InstrumentStatus.RECEIVED) { return instrument.status; } if (instrument.receivedDate) { - return `${instrument.status} on ${new Date(instrument.receivedDate).toLocaleDateString('en-CA', { - year: 'numeric', - month: 'short', - day: '2-digit', - })}`; + const date = new Date(instrument.receivedDate); + const year = date.getFullYear(); + const month = date.toLocaleDateString('en-CA', { month: 'short' }); + const day = String(date.getDate()).padStart(2, '0'); + return `${instrument.status} on ${year}-${month}-${day}`; } - - return instrument.status; + return ''; } } From 0af92bc1754a619038525a2d82f2a9a2981e3ce7 Mon Sep 17 00:00:00 2001 From: Tristan Slater <1631008+trslater@users.noreply.github.com> Date: Mon, 24 Feb 2025 08:51:40 -0800 Subject: [PATCH 31/87] Left-align label with rest of text --- .../application/decision/conditions/conditions.component.scss | 1 + .../decision/conditions/conditions.component.scss | 1 + 2 files changed, 2 insertions(+) diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss index 53e109565..a44b19b06 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss @@ -154,6 +154,7 @@ p { display: flex; align-items: center; gap: 10px; + margin-left: 16px; } .quick-filters-label { diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.scss index 53e109565..a44b19b06 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.scss +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.scss @@ -154,6 +154,7 @@ p { display: flex; align-items: center; gap: 10px; + margin-left: 16px; } .quick-filters-label { From 84c6892bf7658d38883db3d120fd3ee9941cb897 Mon Sep 17 00:00:00 2001 From: Tristan Slater <1631008+trslater@users.noreply.github.com> Date: Mon, 24 Feb 2025 08:59:20 -0800 Subject: [PATCH 32/87] Fix position and spacing --- .../conditions/conditions.component.html | 18 ++++++++++-------- .../conditions/conditions.component.scss | 1 + .../conditions/conditions.component.html | 18 ++++++++++-------- .../conditions/conditions.component.scss | 1 + 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html index aeebebb83..e00816d32 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html @@ -17,6 +17,16 @@

View Conditions

+ +
+
Quick Filters:
+ + + {{ label.value }} + + +
+
@@ -34,14 +44,6 @@

View Conditions

-
-
Quick Filters:
- - - {{ label.value }} - - -
View Conditions
+ +
+
Quick Filters:
+ + + {{ label.value }} + + +
+
@@ -29,14 +39,6 @@

View Conditions

-
-
Quick Filters:
- - - {{ label.value }} - - -
Date: Mon, 24 Feb 2025 09:11:55 -0800 Subject: [PATCH 33/87] Move router link bases to constants file --- .../dialogs/application/application-dialog.component.ts | 7 +++---- .../notice-of-intent/notice-of-intent-dialog.component.ts | 7 +++---- alcs-frontend/src/app/shared/constants.ts | 2 ++ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts index f792e67b3..5591fa55d 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts @@ -18,8 +18,7 @@ import { UserService } from '../../../../services/user/user.service'; import { ApplicationSubmissionStatusPill } from '../../../../shared/application-submission-status-type-pill/application-submission-status-type-pill.component'; import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { CardDialogComponent } from '../card-dialog/card-dialog.component'; - -const ROUTER_LINK_BASE = 'application'; +import { APPLICATION_ROUTER_LINK_BASE } from 'src/app/shared/constants'; @Component({ selector: 'app-detail-dialog', @@ -66,7 +65,7 @@ export class ApplicationDialogComponent extends CardDialogComponent implements O async populateApplicationSubmissionStatus(fileNumber: string) { let submissionStatus: ApplicationSubmissionToSubmissionStatusDto | null = null; - this.routerLink = `${ROUTER_LINK_BASE}/${fileNumber}`; + this.routerLink = `${APPLICATION_ROUTER_LINK_BASE}/${fileNumber}`; try { submissionStatus = await this.applicationSubmissionStatusService.fetchCurrentStatusByFileNumber( fileNumber, @@ -78,7 +77,7 @@ export class ApplicationDialogComponent extends CardDialogComponent implements O if (submissionStatus) { if (submissionStatus.statusTypeCode === SUBMISSION_STATUS.ALC_DECISION) { - this.routerLink = `${ROUTER_LINK_BASE}/${fileNumber}/decision`; + this.routerLink = `${APPLICATION_ROUTER_LINK_BASE}/${fileNumber}/decision`; } this.status = { backgroundColor: submissionStatus.status.alcsBackgroundColor, diff --git a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts index d0b8dc644..1eb58e5bd 100644 --- a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts @@ -20,8 +20,7 @@ import { ApplicationSubmissionStatusPill } from '../../../../shared/application- import { RETROACTIVE_TYPE_LABEL } from '../../../../shared/application-type-pill/application-type-pill.constants'; import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { CardDialogComponent } from '../card-dialog/card-dialog.component'; - -const ROUTER_LINK_BASE = 'notice-of-intent'; +import { NOI_ROUTER_LINK_BASE } from 'src/app/shared/constants'; @Component({ selector: 'app-notice-of-intent-dialog', @@ -80,7 +79,7 @@ export class NoticeOfIntentDialogComponent extends CardDialogComponent implement private async populateSubmissionStatus(fileNumber: string) { let submissionStatus: NoticeOfIntentSubmissionToSubmissionStatusDto | null = null; - this.routerLink = `${ROUTER_LINK_BASE}/${fileNumber}`; + this.routerLink = `${NOI_ROUTER_LINK_BASE}/${fileNumber}`; try { submissionStatus = await this.noticeOfIntentSubmissionStatusService.fetchCurrentStatusByFileNumber( fileNumber, @@ -91,7 +90,7 @@ export class NoticeOfIntentDialogComponent extends CardDialogComponent implement } if (submissionStatus) { if (submissionStatus.statusTypeCode === NOI_SUBMISSION_STATUS.ALC_DECISION) { - this.routerLink = `${ROUTER_LINK_BASE}/${fileNumber}/decision`; + this.routerLink = `${NOI_ROUTER_LINK_BASE}/${fileNumber}/decision`; } this.status = { backgroundColor: submissionStatus.status.alcsBackgroundColor, diff --git a/alcs-frontend/src/app/shared/constants.ts b/alcs-frontend/src/app/shared/constants.ts index 7e0b8ed4c..a0382d2c4 100644 --- a/alcs-frontend/src/app/shared/constants.ts +++ b/alcs-frontend/src/app/shared/constants.ts @@ -1 +1,3 @@ export const FILE_NAME_TRUNCATE_LENGTH = 30; +export const APPLICATION_ROUTER_LINK_BASE = 'application'; +export const NOI_ROUTER_LINK_BASE = 'notice-of-intent'; From a438b55be502157cf44cfc318575430c2e1d5935 Mon Sep 17 00:00:00 2001 From: Tristan Slater <1631008+trslater@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:25:39 -0800 Subject: [PATCH 34/87] Use relative imports --- .../board/dialogs/application/application-dialog.component.ts | 2 +- .../notice-of-intent/notice-of-intent-dialog.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts index 5591fa55d..63322c163 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts @@ -18,7 +18,7 @@ import { UserService } from '../../../../services/user/user.service'; import { ApplicationSubmissionStatusPill } from '../../../../shared/application-submission-status-type-pill/application-submission-status-type-pill.component'; import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { CardDialogComponent } from '../card-dialog/card-dialog.component'; -import { APPLICATION_ROUTER_LINK_BASE } from 'src/app/shared/constants'; +import { APPLICATION_ROUTER_LINK_BASE } from '../../../../shared/constants'; @Component({ selector: 'app-detail-dialog', diff --git a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts index 1eb58e5bd..364246e64 100644 --- a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts @@ -20,7 +20,7 @@ import { ApplicationSubmissionStatusPill } from '../../../../shared/application- import { RETROACTIVE_TYPE_LABEL } from '../../../../shared/application-type-pill/application-type-pill.constants'; import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { CardDialogComponent } from '../card-dialog/card-dialog.component'; -import { NOI_ROUTER_LINK_BASE } from 'src/app/shared/constants'; +import { NOI_ROUTER_LINK_BASE } from '../../../../shared/constants'; @Component({ selector: 'app-notice-of-intent-dialog', From 06459775c0e3dd8c32f6702e5c95f656e176ec36 Mon Sep 17 00:00:00 2001 From: Tristan Slater <1631008+trslater@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:12:48 -0800 Subject: [PATCH 35/87] Fix "COMPLETED" typo preventing correct filtering --- .../application/decision/conditions/conditions.component.ts | 6 +++--- .../decision/conditions/conditions.component.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts index 6fce7a8e6..f437daf76 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts @@ -40,7 +40,7 @@ export type ApplicationDecisionWithConditionComponentLabels = ApplicationDecisio }; export const CONDITION_STATUS = { - COMPLETE: 'COMPLETE', + COMPLETED: 'COMPLETED', ONGOING: 'ONGOING', PENDING: 'PENDING', PASTDUE: 'PASTDUE', @@ -54,7 +54,7 @@ export const CONDITION_STATUS = { }) export class ConditionsComponent implements OnInit { conditionLabelsByStatus: Record = { - COMPLETE: 'Complete', + COMPLETED: 'Complete', ONGOING: 'Ongoing', PENDING: 'Pending', PASTDUE: 'Past Due', @@ -155,7 +155,7 @@ export class ConditionsComponent implements OnInit { decision.conditions = conditions.sort((a, b) => { const order = [ CONDITION_STATUS.ONGOING, - CONDITION_STATUS.COMPLETE, + CONDITION_STATUS.COMPLETED, CONDITION_STATUS.PASTDUE, CONDITION_STATUS.EXPIRED, ]; diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts index 6e22cc628..a3b19d761 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts @@ -38,7 +38,7 @@ export type DecisionWithConditionComponentLabels = NoticeOfIntentDecisionWithLin }; export const CONDITION_STATUS = { - COMPLETE: 'complete', + COMPLETED: 'completed', ONGOING: 'ongoing', PENDING: 'pending', PASTDUE: 'pastdue', @@ -52,7 +52,7 @@ export const CONDITION_STATUS = { }) export class ConditionsComponent implements OnInit { conditionLabelsByStatus: Record = { - COMPLETE: 'Complete', + COMPLETED: 'Complete', ONGOING: 'Ongoing', PENDING: 'Pending', PASTDUE: 'Past Due', @@ -151,7 +151,7 @@ export class ConditionsComponent implements OnInit { decision.conditions = conditions.sort((a, b) => { const order = [ CONDITION_STATUS.ONGOING, - CONDITION_STATUS.COMPLETE, + CONDITION_STATUS.COMPLETED, CONDITION_STATUS.PASTDUE, CONDITION_STATUS.EXPIRED, ]; From 6c3525107a7cd681e3a3da3fb5ecf1c3cc7e83af Mon Sep 17 00:00:00 2001 From: Tristan Slater <1631008+trslater@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:19:15 -0800 Subject: [PATCH 36/87] Update status in list when changed in single --- .../decision/conditions/condition/condition.component.ts | 6 +++++- .../decision/conditions/conditions.component.html | 1 + .../application/decision/conditions/conditions.component.ts | 4 ++++ .../decision/conditions/condition/condition.component.ts | 6 +++++- .../decision/conditions/conditions.component.html | 1 + .../decision/conditions/conditions.component.ts | 4 ++++ 6 files changed, 20 insertions(+), 2 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts index 1e74f27fe..c61b3f0c0 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import moment from 'moment'; import { ApplicationDecisionComponentToConditionLotService } from '../../../../../services/application/decision/application-decision-v2/application-decision-component-to-condition-lot/application-decision-component-to-condition-lot.service'; import { ApplicationDecisionConditionService } from '../../../../../services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service'; @@ -49,6 +49,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { @Input() fileNumber!: string; @Input() index!: number; + @Output() statusChange: EventEmitter = new EventEmitter(); + DateType = DateType; dates: ApplicationDecisionConditionDateDto[] = []; @@ -308,6 +310,7 @@ export class ConditionComponent implements OnInit, AfterViewInit { } const conditionNewStatus = await this.decisionService.getStatus(this.condition.uuid); this.condition.status = conditionNewStatus.status; + this.statusChange.emit(this.condition.status); this.setPillLabel(this.condition.status); } @@ -323,6 +326,7 @@ export class ConditionComponent implements OnInit, AfterViewInit { ); const conditionNewStatus = await this.decisionService.getStatus(this.condition.uuid); this.condition.status = conditionNewStatus.status; + this.statusChange.emit(this.condition.status); this.setPillLabel(this.condition.status); } } diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html index e00816d32..0a51648f1 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html @@ -51,6 +51,7 @@

View Conditions

[isDraftDecision]="decision.isDraft" [fileNumber]="fileNumber" [index]="j + 1" + (statusChange)="onStatusChange(condition, $event)" >
diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts index f437daf76..7c2353cdc 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts @@ -243,4 +243,8 @@ export class ConditionsComponent implements OnInit { return conditions.filter((condition) => this.conditionFilters.includes(condition.status)); } + + onStatusChange(condition: ApplicationDecisionConditionWithStatus, newStatus: string) { + condition.status = newStatus; + } } diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts index 794688a02..66141e8ea 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import moment from 'moment'; import { NoticeOfIntentDecisionConditionService } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service'; import { @@ -40,6 +40,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { @Input() fileNumber!: string; @Input() index!: number; + @Output() statusChange: EventEmitter = new EventEmitter(); + DateType = DateType; dates: NoticeOfIntentDecisionConditionDateDto[] = []; @@ -219,6 +221,7 @@ export class ConditionComponent implements OnInit, AfterViewInit { const conditionNewStatus = await this.decisionService.getStatus(this.condition.uuid); this.condition.status = conditionNewStatus.status; + this.statusChange.emit(this.condition.status); this.setPillLabel(this.condition.status); } else { console.error('Date with specified UUID not found'); @@ -238,6 +241,7 @@ export class ConditionComponent implements OnInit, AfterViewInit { const conditionNewStatus = await this.decisionService.getStatus(this.condition.uuid); this.condition.status = conditionNewStatus.status; + this.statusChange.emit(this.condition.status); this.setPillLabel(this.condition.status); } } diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html index e9001a398..e79f115ef 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html @@ -46,6 +46,7 @@

View Conditions

[isDraftDecision]="decision.isDraft" [fileNumber]="fileNumber" [index]="j + 1" + (statusChange)="onStatusChange(condition, $event)" >
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts index a3b19d761..43dab3321 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts @@ -239,4 +239,8 @@ export class ConditionsComponent implements OnInit { return conditions.filter((condition) => this.conditionFilters.includes(condition.status)); } + + onStatusChange(condition: DecisionConditionWithStatus, newStatus: string) { + condition.status = newStatus; + } } From a16f77b4d91bf3591ddf61415c5363628c66bc46 Mon Sep 17 00:00:00 2001 From: Dylan Rogowsky Date: Mon, 24 Feb 2025 14:10:42 -0700 Subject: [PATCH 37/87] ALCS-2500: Backport workflow --- .github/workflows/backport-to-develop.yml | 110 ++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .github/workflows/backport-to-develop.yml diff --git a/.github/workflows/backport-to-develop.yml b/.github/workflows/backport-to-develop.yml new file mode 100644 index 000000000..94c72cd14 --- /dev/null +++ b/.github/workflows/backport-to-develop.yml @@ -0,0 +1,110 @@ +name: Backport to Develop +on: + pull_request: + types: + - closed + branches: + - main + +jobs: + backport: + # Only run if PR was merged (not just closed) and it wasn't from develop + if: | + github.event.pull_request.merged == true && + github.event.pull_request.head.ref != 'develop' + name: Backport to Develop + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + env: + BACKPORT_BRANCH: backport/pr-${{ github.event.pull_request.number }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: develop + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Get First Approver + id: get-approver + run: | + # Get reviews for the original PR + REVIEWS=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews") + + # Extract first APPROVED reviewer's login + FIRST_APPROVER=$(echo "$REVIEWS" | jq -r '.[] | select(.state=="APPROVED") | .user.login' | head -n 1) + + if [ ! -z "$FIRST_APPROVER" ]; then + echo "has_reviewer=true" >> $GITHUB_OUTPUT + echo "reviewer=$FIRST_APPROVER" >> $GITHUB_OUTPUT + else + echo "has_reviewer=false" >> $GITHUB_OUTPUT + fi + + - name: Create backport branch + run: | + # Create a new branch from develop + git checkout -b ${{ env.BACKPORT_BRANCH }} + + # Get the range of commits to cherry-pick + BASE_SHA=$(git merge-base ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}) + + # Cherry pick the range of commits + # Using -m 1 to handle merge commits, and --strategy=recursive --strategy-option=theirs to handle conflicts + if ! git cherry-pick -m 1 --strategy=recursive --strategy-option=theirs ${BASE_SHA}..${{ github.event.pull_request.merge_commit_sha }}; then + if [ -f .git/CHERRY_PICK_HEAD ]; then + # We're in a cherry-pick state + if git diff --cached --quiet && git diff --quiet; then + # No changes in working directory or index - safe to skip + git cherry-pick --skip + else + # There are uncommitted changes - could be conflicts + git cherry-pick --abort + exit 1 + fi + else + # Some other error occurred + exit 1 + fi + fi + + # Push the branch using the token for authentication + git push "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git" ${{ env.BACKPORT_BRANCH }} + + - name: Create Pull Request with Reviewer + if: steps.get-approver.outputs.has_reviewer == 'true' + uses: repo-sync/pull-request@v2 + with: + source_branch: ${{ env.BACKPORT_BRANCH }} + destination_branch: "develop" + github_token: ${{ secrets.GITHUB_TOKEN }} + pr_title: "Backport: ${{ github.event.pull_request.title }}" + pr_body: | + Automated backport of changes from main to develop + + Original PR: [#${{ github.event.pull_request.number }} - ${{ github.event.pull_request.title }}](${{ github.event.pull_request.html_url }}) + Original Author: @${{ github.event.pull_request.user.login }} + pr_label: "backport" + pr_reviewer: ${{ steps.get-approver.outputs.reviewer }} + + - name: Create Pull Request without Reviewer + if: steps.get-approver.outputs.has_reviewer != 'true' + uses: repo-sync/pull-request@v2 + with: + source_branch: ${{ env.BACKPORT_BRANCH }} + destination_branch: "develop" + github_token: ${{ secrets.GITHUB_TOKEN }} + pr_title: "Backport: ${{ github.event.pull_request.title }}" + pr_body: | + Automated backport of changes from main to develop + + Original PR: [#${{ github.event.pull_request.number }} - ${{ github.event.pull_request.title }}](${{ github.event.pull_request.html_url }}) + Original Author: @${{ github.event.pull_request.user.login }} + pr_label: "backport" \ No newline at end of file From c21017f82d60316a163fddf4755186f2c883200e Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Tue, 25 Feb 2025 16:34:03 -0800 Subject: [PATCH 38/87] Add NOI decision condition financial instrument backend components - NOI decision condition financial instrument entity, DTO, and service - Add NOI financial instrument to decision condition controller - Add DB migration - Add mapper - Add mock entities --- ...sion-condition-financial-instrument.dto.ts | 75 +++++ ...n-condition-financial-instrument.entity.ts | 93 ++++++ ...ition-financial-instrument.service.spec.ts | 289 ++++++++++++++++++ ...-condition-financial-instrument.service.ts | 186 +++++++++++ ...tent-decision-condition.controller.spec.ts | 137 ++++++++- ...of-intent-decision-condition.controller.ts | 80 ++++- ...notice-of-intent-decision-condition.dto.ts | 4 + ...ice-of-intent-decision-condition.entity.ts | 6 + .../notice-of-intent-decision-v2.service.ts | 1 + .../notice-of-intent-decision.module.ts | 4 + ...e-of-intent-decision.automapper.profile.ts | 54 ++++ ...f_intent_condition_financial_instrument.ts | 39 +++ services/apps/alcs/test/mocks/mockEntities.ts | 49 +++ 13 files changed, 1015 insertions(+), 2 deletions(-) create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1740529203706-add_notice_of_intent_condition_financial_instrument.ts diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto.ts new file mode 100644 index 000000000..1678e851f --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto.ts @@ -0,0 +1,75 @@ +import { AutoMap } from 'automapper-classes'; +import { + InstrumentType, + HeldBy, + InstrumentStatus, +} from './notice-of-intent-decision-condition-financial-instrument.entity'; +import { IsEnum, IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; +import { OmitType } from '@nestjs/mapped-types'; + +export class NoticeOfIntentDecisionConditionFinancialInstrumentDto { + @AutoMap() + @IsUUID() + uuid: string; + + @AutoMap() + @IsString() + securityHolderPayee: string; + + @AutoMap() + @IsEnum(InstrumentType) + type: InstrumentType; + + @AutoMap() + @IsNumber() + issueDate: number; + + @AutoMap() + @IsNumber() + @IsOptional() + expiryDate?: number | null; + + @AutoMap() + @IsNumber() + amount: number; + + @AutoMap() + @IsString() + bank: string; + + @AutoMap() + @IsOptional() + instrumentNumber?: string | null; + + @AutoMap() + @IsEnum(HeldBy) + heldBy: HeldBy; + + @AutoMap() + @IsNumber() + receivedDate: number; + + @AutoMap() + @IsString() + @IsOptional() + notes?: string | null; + + @AutoMap() + @IsEnum(InstrumentStatus) + status: InstrumentStatus; + + @AutoMap() + @IsOptional() + @IsNumber() + statusDate?: number | null; + + @AutoMap() + @IsString() + @IsOptional() + explanation?: string | null; +} + +export class CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto extends OmitType( + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + ['uuid'] as const, +) {} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity.ts new file mode 100644 index 000000000..817948066 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity.ts @@ -0,0 +1,93 @@ +import { Entity, Column, ManyToOne } from 'typeorm'; +import { Base } from '../../../../common/entities/base.entity'; +import { AutoMap } from 'automapper-classes'; +import { ColumnNumericTransformer } from '../../../../utils/column-numeric-transform'; +import { NoticeOfIntentDecisionCondition } from '../notice-of-intent-decision-condition.entity'; + +export enum InstrumentType { + BANK_DRAFT = 'Bank Draft', + CERTIFIED_CHEQUE = 'Certified Cheque', + EFT = 'EFT', + IRREVOCABLE_LETTER_OF_CREDIT = 'Irrevocable Letter of Credit', + OTHER = 'Other', + SAFEKEEPING_AGREEMENT = 'Safekeeping Agreement', +} + +export enum HeldBy { + ALC = 'ALC', + MINISTRY = 'Ministry', +} + +export enum InstrumentStatus { + RECEIVED = 'Received', + RELEASED = 'Released', + CASHED = 'Cashed', + REPLACED = 'Replaced', +} + +@Entity({ comment: 'Instrument for Financial Security Conditions' }) +export class NoticeOfIntentDecisionConditionFinancialInstrument extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap() + @Column({ type: 'varchar', nullable: false }) + securityHolderPayee: string; + + @AutoMap() + @Column({ type: 'enum', enum: InstrumentType, nullable: false }) + type: InstrumentType; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: false }) + issueDate: Date; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: true }) + expiryDate?: Date | null; + + @AutoMap() + @Column({ type: 'decimal', precision: 12, scale: 2, nullable: false, transformer: new ColumnNumericTransformer() }) + amount: number; + + @AutoMap() + @Column({ type: 'varchar', nullable: false }) + bank: string; + + @AutoMap() + @Column({ type: 'varchar', nullable: true }) + instrumentNumber: string | null; + + @AutoMap() + @Column({ type: 'enum', enum: HeldBy, nullable: false }) + heldBy: HeldBy; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: false }) + receivedDate: Date; + + @AutoMap() + @Column({ type: 'text', nullable: true }) + notes: string | null; + + @AutoMap() + @Column({ type: 'enum', enum: InstrumentStatus, default: InstrumentStatus.RECEIVED, nullable: false }) + status: InstrumentStatus; + + @AutoMap() + @Column({ type: 'timestamptz', nullable: true }) + statusDate?: Date | null; + + @AutoMap() + @Column({ type: 'text', nullable: true }) + explanation?: string | null; + + @ManyToOne(() => NoticeOfIntentDecisionCondition, (condition) => condition.financialInstruments, { + onDelete: 'CASCADE', + }) + condition: NoticeOfIntentDecisionCondition; +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.spec.ts new file mode 100644 index 000000000..2ca488f99 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.spec.ts @@ -0,0 +1,289 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentService } from './notice-of-intent-decision-condition-financial-instrument.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { + NoticeOfIntentDecisionConditionFinancialInstrument, + HeldBy, + InstrumentStatus, + InstrumentType, +} from './notice-of-intent-decision-condition-financial-instrument.entity'; +import { NoticeOfIntentDecisionCondition } from '../notice-of-intent-decision-condition.entity'; +import { NoticeOfIntentDecisionConditionType } from '../notice-of-intent-decision-condition-code.entity'; +import { Repository } from 'typeorm'; +import { CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto } from './notice-of-intent-decision-condition-financial-instrument.dto'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { + ServiceInternalErrorException, + ServiceNotFoundException, + ServiceValidationException, +} from '../../../../../../../libs/common/src/exceptions/base.exception'; +import { + initNoticeOfIntentDecisionConditionFinancialInstrumentMockEntity, + initNoticeOfIntentDecisionConditionTypeMockEntity, +} from '../../../../../test/mocks/mockEntities'; + +describe('NoticeOfIntentDecisionConditionFinancialInstrumentService', () => { + let service: NoticeOfIntentDecisionConditionFinancialInstrumentService; + let mockRepository: DeepMocked>; + let mockConditionRepository: DeepMocked>; + let mockConditionTypeRepository: DeepMocked>; + let mockNoticeOfIntentDecisionConditionType; + let mockNoticeOfIntentDecisionConditionFinancialInstrument; + + beforeEach(async () => { + mockRepository = createMock(); + mockConditionRepository = createMock(); + mockConditionTypeRepository = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NoticeOfIntentDecisionConditionFinancialInstrumentService, + { + provide: getRepositoryToken(NoticeOfIntentDecisionConditionFinancialInstrument), + useValue: mockRepository, + }, + { + provide: getRepositoryToken(NoticeOfIntentDecisionCondition), + useValue: mockConditionRepository, + }, + { + provide: getRepositoryToken(NoticeOfIntentDecisionConditionType), + useValue: mockConditionTypeRepository, + }, + ], + }).compile(); + + service = module.get( + NoticeOfIntentDecisionConditionFinancialInstrumentService, + ); + + mockNoticeOfIntentDecisionConditionType = initNoticeOfIntentDecisionConditionTypeMockEntity('BOND'); + mockNoticeOfIntentDecisionConditionFinancialInstrument = + initNoticeOfIntentDecisionConditionFinancialInstrumentMockEntity(); + }); + + describe('getAll', () => { + it('should return all financial instruments for a condition', async () => { + const conditionUuid = 'condition-uuid'; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstruments = [mockNoticeOfIntentDecisionConditionFinancialInstrument]; + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.find.mockResolvedValue(financialInstruments); + + const result = await service.getAll(conditionUuid); + + expect(result).toEqual(financialInstruments); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ where: { code: 'BOND' } }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.find).toHaveBeenCalledWith({ where: { condition: { uuid: conditionUuid } } }); + }); + + it('should throw an error if condition type does not exist', async () => { + mockConditionTypeRepository.findOne.mockResolvedValue(null); + + await expect(service.getAll('condition-uuid')).rejects.toThrow(ServiceInternalErrorException); + }); + + it('should throw an error if condition is not found', async () => { + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(null); + + await expect(service.getAll('condition-uuid')).rejects.toThrow(ServiceNotFoundException); + }); + + it('should throw an error if condition is not of type Financial Security', async () => { + const conditionUuid = 'condition-uuid'; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'OTHER' }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + + await expect(service.getAll(conditionUuid)).rejects.toThrow(ServiceValidationException); + }); + }); + + describe('getByUuid', () => { + it('should return a financial instrument by uuid', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = new NoticeOfIntentDecisionConditionFinancialInstrument({ uuid }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(financialInstrument); + + const result = await service.getByUuid(conditionUuid, uuid); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ + where: { code: mockNoticeOfIntentDecisionConditionType.code }, + }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { uuid, condition: { uuid: conditionUuid } } }); + }); + + it('should throw an error if financial instrument is not found', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.getByUuid(conditionUuid, uuid)).rejects.toThrow(ServiceNotFoundException); + }); + }); + + describe('create', () => { + it('should create a financial instrument', async () => { + const conditionUuid = 'condition-uuid'; + const dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.EFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + }; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = mockNoticeOfIntentDecisionConditionFinancialInstrument; + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.save.mockResolvedValue(financialInstrument); + + const result = await service.create(conditionUuid, dto); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ + where: { code: mockNoticeOfIntentDecisionConditionType.code }, + }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.save).toHaveBeenCalledWith(expect.any(NoticeOfIntentDecisionConditionFinancialInstrument)); + }); + }); + + describe('update', () => { + it('should update a financial instrument', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.EFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + }; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = new NoticeOfIntentDecisionConditionFinancialInstrument({ uuid }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(financialInstrument); + mockRepository.save.mockResolvedValue(financialInstrument); + + const result = await service.update(conditionUuid, uuid, dto); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ + where: { code: mockNoticeOfIntentDecisionConditionType.code }, + }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { uuid, condition: { uuid: conditionUuid } } }); + expect(mockRepository.save).toHaveBeenCalledWith(expect.any(NoticeOfIntentDecisionConditionFinancialInstrument)); + }); + }); + + describe('remove', () => { + it('should remove a financial instrument', async () => { + const conditionUuid = 'condition-uuid'; + const uuid = 'instrument-uuid'; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + const financialInstrument = new NoticeOfIntentDecisionConditionFinancialInstrument({ uuid }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + mockRepository.findOne.mockResolvedValue(financialInstrument); + mockRepository.remove.mockResolvedValue(financialInstrument); + + const result = await service.remove(conditionUuid, uuid); + + expect(result).toEqual(financialInstrument); + expect(mockConditionTypeRepository.findOne).toHaveBeenCalledWith({ where: { code: 'BOND' } }); + expect(mockConditionRepository.findOne).toHaveBeenCalledWith({ where: { uuid: conditionUuid } }); + expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { uuid, condition: { uuid: conditionUuid } } }); + expect(mockRepository.remove).toHaveBeenCalledWith(financialInstrument); + }); + }); + + it('should throw an error if instrument number is missing when type is not EFT', async () => { + const conditionUuid = 'condition-uuid'; + const dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + }; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + + await expect(service.create(conditionUuid, dto)).rejects.toThrow(ServiceValidationException); + }); + + it('should throw an error if status date or explanation is missing when status is not RECEIVED', async () => { + const conditionUuid = 'condition-uuid'; + const dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.EFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.CASHED, + }; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + + await expect(service.create(conditionUuid, dto)).rejects.toThrow(ServiceValidationException); + }); + + it('should throw an error if status date or explanation is provided when status is RECEIVED', async () => { + const conditionUuid = 'condition-uuid'; + const dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.EFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + explanation: 'test', + statusDate: new Date().getTime(), + }; + const condition = new NoticeOfIntentDecisionCondition({ uuid: conditionUuid, typeCode: 'BOND' }); + + mockConditionTypeRepository.findOne.mockResolvedValue(mockNoticeOfIntentDecisionConditionType); + mockConditionRepository.findOne.mockResolvedValue(condition); + + await expect(service.create(conditionUuid, dto)).rejects.toThrow(ServiceValidationException); + }); +}); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.ts new file mode 100644 index 000000000..f6cd69f3b --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.ts @@ -0,0 +1,186 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + NoticeOfIntentDecisionConditionFinancialInstrument, + InstrumentStatus, + InstrumentType, +} from './notice-of-intent-decision-condition-financial-instrument.entity'; +import { Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + ServiceInternalErrorException, + ServiceNotFoundException, + ServiceValidationException, +} from '../../../../../../../libs/common/src/exceptions/base.exception'; +import { CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto } from './notice-of-intent-decision-condition-financial-instrument.dto'; +import { NoticeOfIntentDecisionCondition } from '../notice-of-intent-decision-condition.entity'; +import { NoticeOfIntentDecisionConditionType } from '../notice-of-intent-decision-condition-code.entity'; + +export enum ConditionType { + FINANCIAL_SECURITY = 'BOND', +} + +@Injectable() +export class NoticeOfIntentDecisionConditionFinancialInstrumentService { + constructor( + @InjectRepository(NoticeOfIntentDecisionConditionFinancialInstrument) + private readonly repository: Repository, + @InjectRepository(NoticeOfIntentDecisionCondition) + private readonly noticeOfIntentDecisionConditionRepository: Repository, + @InjectRepository(NoticeOfIntentDecisionConditionType) + private readonly noticeOfIntentDecisionConditionTypeRepository: Repository, + ) {} + + async throwErrorIfFinancialSecurityTypeNotExists(): Promise { + const exists = await this.noticeOfIntentDecisionConditionTypeRepository.findOne({ + where: { code: ConditionType.FINANCIAL_SECURITY }, + }); + if (!exists) { + throw new ServiceInternalErrorException('Condition type Financial Security not found'); + } + } + + async getAll(conditionUuid: string): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.noticeOfIntentDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + return this.repository.find({ where: { condition: { uuid: conditionUuid } } }); + } + + async getByUuid(conditionUuid: string, uuid: string): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.noticeOfIntentDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + const financialInstrument = await this.repository.findOne({ where: { uuid, condition: { uuid: conditionUuid } } }); + + if (!financialInstrument) { + throw new ServiceNotFoundException(`Financial Instrument with uuid ${uuid} not found`); + } + + return financialInstrument; + } + + async create( + conditionUuid: string, + dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + ): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.noticeOfIntentDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + let instrument = new NoticeOfIntentDecisionConditionFinancialInstrument(); + instrument = this.mapDtoToEntity(dto, instrument); + instrument.condition = condition; + + return this.repository.save(instrument); + } + + async update( + conditionUuid: string, + uuid: string, + dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + ): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.noticeOfIntentDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + let instrument = await this.repository.findOne({ where: { uuid, condition: { uuid: conditionUuid } } }); + + if (!instrument) { + throw new ServiceNotFoundException(`Instrument with uuid ${uuid} not found`); + } + + instrument = this.mapDtoToEntity(dto, instrument); + + return this.repository.save(instrument); + } + + async remove(conditionUuid: string, uuid: string): Promise { + await this.throwErrorIfFinancialSecurityTypeNotExists(); + + const condition = await this.noticeOfIntentDecisionConditionRepository.findOne({ where: { uuid: conditionUuid } }); + + if (!condition) { + throw new ServiceNotFoundException(`Condition with uuid ${conditionUuid} not found`); + } + + if (condition.typeCode !== ConditionType.FINANCIAL_SECURITY) { + throw new ServiceValidationException(`Condition with uuid ${conditionUuid} is not of type Financial Security`); + } + + const instrument = await this.repository.findOne({ where: { uuid, condition: { uuid: conditionUuid } } }); + + if (!instrument) { + throw new ServiceNotFoundException(`Instrument with uuid ${uuid} not found`); + } + + return await this.repository.remove(instrument); + } + + private mapDtoToEntity( + dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + entity: NoticeOfIntentDecisionConditionFinancialInstrument, + ): NoticeOfIntentDecisionConditionFinancialInstrument { + entity.securityHolderPayee = dto.securityHolderPayee; + entity.type = dto.type; + entity.issueDate = new Date(dto.issueDate); + entity.expiryDate = dto.expiryDate ? new Date(dto.expiryDate) : null; + entity.amount = dto.amount; + entity.bank = dto.bank; + if (dto.type !== InstrumentType.EFT && !dto.instrumentNumber) { + throw new ServiceValidationException('Instrument number is required when type is not EFT'); + } + entity.instrumentNumber = dto.instrumentNumber ?? null; + entity.heldBy = dto.heldBy; + entity.receivedDate = new Date(dto.receivedDate); + entity.notes = dto.notes ?? null; + entity.status = dto.status; + if (dto.status !== InstrumentStatus.RECEIVED) { + if (!dto.statusDate || !dto.explanation) { + throw new ServiceValidationException('Status date and explanation are required when status is not RECEIVED'); + } + entity.statusDate = new Date(dto.statusDate); + entity.explanation = dto.explanation; + } else { + if (dto.statusDate || dto.explanation) { + throw new ServiceValidationException('Status date and explanation are not allowed when status is RECEIVED'); + } + entity.statusDate = null; + entity.explanation = null; + } + return entity; + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts index 7b5213ef3..0c489f342 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts @@ -1,5 +1,5 @@ import { classes } from 'automapper-classes'; -import { AutomapperModule } from 'automapper-nestjs'; +import { AutomapperModule, InjectMapper } from 'automapper-nestjs'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; @@ -9,13 +9,23 @@ import { NoticeOfIntentDecisionConditionController } from './notice-of-intent-de import { UpdateNoticeOfIntentDecisionConditionDto } from './notice-of-intent-decision-condition.dto'; import { NoticeOfIntentDecisionCondition } from './notice-of-intent-decision-condition.entity'; import { NoticeOfIntentDecisionConditionService } from './notice-of-intent-decision-condition.service'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentService } from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service'; +import { + NoticeOfIntentDecisionConditionFinancialInstrument, + HeldBy, + InstrumentStatus, + InstrumentType, +} from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity'; +import { CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto } from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto'; describe('NoticeOfIntentDecisionConditionController', () => { let controller: NoticeOfIntentDecisionConditionController; let mockNOIDecisionConditionService: DeepMocked; + let mockFinancialInstrumentService: DeepMocked; beforeEach(async () => { mockNOIDecisionConditionService = createMock(); + mockFinancialInstrumentService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -30,6 +40,10 @@ describe('NoticeOfIntentDecisionConditionController', () => { provide: NoticeOfIntentDecisionConditionService, useValue: mockNOIDecisionConditionService, }, + { + provide: NoticeOfIntentDecisionConditionFinancialInstrumentService, + useValue: mockFinancialInstrumentService, + }, { provide: ClsService, useValue: {}, @@ -88,4 +102,125 @@ describe('NoticeOfIntentDecisionConditionController', () => { expect(result.approvalDependant).toEqual(updated.approvalDependant); }); }); + + describe('Financial Instruments', () => { + const conditionUuid = 'condition-uuid'; + const instrumentUuid = 'instrument-uuid'; + const financialInstrumentDto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date().getTime(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date().getTime(), + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + notes: 'notes', + expiryDate: new Date().getTime(), + statusDate: new Date().getTime(), + explanation: 'explanation', + }; + + it('should get all financial instruments for a condition', async () => { + const financialInstruments = [ + new NoticeOfIntentDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }), + ]; + mockFinancialInstrumentService.getAll.mockResolvedValue(financialInstruments); + + const result = await controller.getAllFinancialInstruments(conditionUuid); + + expect(mockFinancialInstrumentService.getAll).toHaveBeenCalledWith(conditionUuid); + expect(result).toBeDefined(); + }); + + it('should get a financial instrument by uuid', async () => { + const financialInstrument = new NoticeOfIntentDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockFinancialInstrumentService.getByUuid.mockResolvedValue(financialInstrument); + + const result = await controller.getFinancialInstrumentByUuid(conditionUuid, instrumentUuid); + + expect(mockFinancialInstrumentService.getByUuid).toHaveBeenCalledWith(conditionUuid, instrumentUuid); + expect(result).toBeDefined(); + }); + + it('should create a financial instrument', async () => { + const financialInstrument = new NoticeOfIntentDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockFinancialInstrumentService.create.mockResolvedValue(financialInstrument); + + const result = await controller.createFinancialInstrument(conditionUuid, financialInstrumentDto); + + expect(mockFinancialInstrumentService.create).toHaveBeenCalledWith(conditionUuid, financialInstrumentDto); + expect(result).toBeDefined(); + }); + + it('should update a financial instrument', async () => { + const financialInstrument = new NoticeOfIntentDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockFinancialInstrumentService.update.mockResolvedValue(financialInstrument); + + const result = await controller.updateFinancialInstrument(conditionUuid, instrumentUuid, financialInstrumentDto); + + expect(mockFinancialInstrumentService.update).toHaveBeenCalledWith( + conditionUuid, + instrumentUuid, + financialInstrumentDto, + ); + expect(result).toBeDefined(); + }); + + it('should delete a financial instrument', async () => { + const financialInstrument = new NoticeOfIntentDecisionConditionFinancialInstrument({ + securityHolderPayee: 'holder', + type: InstrumentType.BANK_DRAFT, + issueDate: new Date(), + amount: 100, + bank: 'bank', + heldBy: HeldBy.ALC, + receivedDate: new Date(), + status: InstrumentStatus.RECEIVED, + }); + mockFinancialInstrumentService.remove.mockResolvedValue(financialInstrument); + + const result = await controller.deleteFinancialInstrument(conditionUuid, instrumentUuid); + + expect(mockFinancialInstrumentService.remove).toHaveBeenCalledWith(conditionUuid, instrumentUuid); + expect(result).toBeDefined(); + }); + }); }); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.ts index fa2b4be1c..6ba37bda6 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.ts @@ -1,6 +1,6 @@ import { Mapper } from 'automapper-core'; import { InjectMapper } from 'automapper-nestjs'; -import { Body, Controller, Get, Param, Patch, Query, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Param, Patch, Query, UseGuards, Post, Delete } from '@nestjs/common'; import { ApiOAuth2 } from '@nestjs/swagger'; import * as config from 'config'; import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; @@ -12,6 +12,12 @@ import { } from './notice-of-intent-decision-condition.dto'; import { NoticeOfIntentDecisionCondition } from './notice-of-intent-decision-condition.entity'; import { NoticeOfIntentDecisionConditionService } from './notice-of-intent-decision-condition.service'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentService } from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service'; +import { + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, +} from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto'; +import { NoticeOfIntentDecisionConditionFinancialInstrument } from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('notice-of-intent-decision-condition') @@ -19,6 +25,7 @@ import { NoticeOfIntentDecisionConditionService } from './notice-of-intent-decis export class NoticeOfIntentDecisionConditionController { constructor( private conditionService: NoticeOfIntentDecisionConditionService, + private conditionFinancialInstrumentService: NoticeOfIntentDecisionConditionFinancialInstrumentService, @InjectMapper() private mapper: Mapper, ) {} @@ -45,4 +52,75 @@ export class NoticeOfIntentDecisionConditionController { NoticeOfIntentDecisionConditionDto, ); } + + @Get('/:uuid/financial-instruments') + @UserRoles(...ANY_AUTH_ROLE) + async getAllFinancialInstruments( + @Param('uuid') uuid: string, + ): Promise { + const financialInstruments = await this.conditionFinancialInstrumentService.getAll(uuid); + + return await this.mapper.mapArray( + financialInstruments, + NoticeOfIntentDecisionConditionFinancialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + ); + } + + @Get('/:uuid/financial-instruments/:instrumentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async getFinancialInstrumentByUuid( + @Param('uuid') uuid: string, + @Param('instrumentUuid') instrumentUuid: string, + ): Promise { + const financialInstrument = await this.conditionFinancialInstrumentService.getByUuid(uuid, instrumentUuid); + return await this.mapper.map( + financialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + ); + } + + @Post('/:uuid/financial-instruments') + @UserRoles(...ANY_AUTH_ROLE) + async createFinancialInstrument( + @Param('uuid') uuid: string, + @Body() dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + ): Promise { + const financialInstrument = await this.conditionFinancialInstrumentService.create(uuid, dto); + return await this.mapper.map( + financialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + ); + } + + @Patch('/:uuid/financial-instruments/:instrumentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async updateFinancialInstrument( + @Param('uuid') uuid: string, + @Param('instrumentUuid') instrumentUuid: string, + @Body() dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + ): Promise { + const financialInstrument = await this.conditionFinancialInstrumentService.update(uuid, instrumentUuid, dto); + return await this.mapper.map( + financialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + ); + } + + @Delete('/:uuid/financial-instruments/:instrumentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async deleteFinancialInstrument( + @Param('uuid') uuid: string, + @Param('instrumentUuid') instrumentUuid: string, + ): Promise { + const result = await this.conditionFinancialInstrumentService.remove(uuid, instrumentUuid); + return await this.mapper.map( + result, + NoticeOfIntentDecisionConditionFinancialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + ); + } } diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts index b19bf6734..fb5a1d1d9 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts @@ -13,6 +13,7 @@ import { NoticeOfIntentDecisionConditionHomeCardDto, } from './notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.dto'; import { NoticeOfIntentTypeDto } from '../../notice-of-intent/notice-of-intent-type/notice-of-intent-type.dto'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentDto } from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto'; export class NoticeOfIntentDecisionConditionTypeDto extends BaseCodeDto { @IsBoolean() @@ -101,6 +102,9 @@ export class NoticeOfIntentDecisionConditionDto { conditionCard: NoticeOfIntentDecisionConditionCardUuidDto | null; status?: string | null; + + @AutoMap(() => NoticeOfIntentDecisionConditionFinancialInstrumentDto) + financialInstruments?: NoticeOfIntentDecisionConditionFinancialInstrumentDto[] | null; } export class NoticeOfIntentHomeDto { diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity.ts index 061941e0f..e3ed34386 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity.ts @@ -7,6 +7,7 @@ import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; import { NoticeOfIntentDecisionConditionType } from './notice-of-intent-decision-condition-code.entity'; import { NoticeOfIntentDecisionConditionDate } from './notice-of-intent-decision-condition-date/notice-of-intent-decision-condition-date.entity'; import { NoticeOfIntentDecisionConditionCard } from './notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.entity'; +import { NoticeOfIntentDecisionConditionFinancialInstrument } from './notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity'; @Entity({ comment: 'Decision Conditions for Notice of Intents', @@ -85,4 +86,9 @@ export class NoticeOfIntentDecisionCondition extends Base { nullable: true, }) conditionCard: NoticeOfIntentDecisionConditionCard | null; + + @OneToMany(() => NoticeOfIntentDecisionConditionFinancialInstrument, (instrument) => instrument.condition, { + cascade: true, + }) + financialInstruments?: NoticeOfIntentDecisionConditionFinancialInstrument[] | null; } diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts index fb11643fc..a155a3d88 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts @@ -99,6 +99,7 @@ export class NoticeOfIntentDecisionV2Service { components: true, dates: true, conditionCard: true, + financialInstruments: true, }, conditionCards: true, }, diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts index 8c0e79d1a..facc7a275 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts @@ -31,6 +31,8 @@ import { User } from '../../user/user.entity'; import { NoticeOfIntentDecisionConditionCard } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.entity'; import { NoticeOfIntentDecisionConditionCardService } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.service'; import { NoticeOfIntentDecisionConditionCardController } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.controller'; +import { NoticeOfIntentDecisionConditionFinancialInstrument } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentService } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service'; @Module({ imports: [ @@ -46,6 +48,7 @@ import { NoticeOfIntentDecisionConditionCardController } from './notice-of-inten NoticeOfIntentDecisionConditionType, NoticeOfIntentDecisionConditionDate, NoticeOfIntentDecisionConditionCard, + NoticeOfIntentDecisionConditionFinancialInstrument, User, ]), forwardRef(() => BoardModule), @@ -63,6 +66,7 @@ import { NoticeOfIntentDecisionConditionCardController } from './notice-of-inten NoticeOfIntentDecisionProfile, NoticeOfIntentModificationService, NoticeOfIntentDecisionConditionCardService, + NoticeOfIntentDecisionConditionFinancialInstrumentService, ], controllers: [ NoticeOfIntentDecisionV2Controller, diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts index 6136f4948..f00a4a6d5 100644 --- a/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts @@ -50,6 +50,11 @@ import { NoticeOfIntentDecisionConditionCardUuidDto, NoticeOfIntentDecisionConditionHomeCardDto, } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.dto'; +import { + InstrumentStatus, + NoticeOfIntentDecisionConditionFinancialInstrument, +} from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentDto } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto'; @Injectable() export class NoticeOfIntentDecisionProfile extends AutomapperProfile { @@ -215,6 +220,19 @@ export class NoticeOfIntentDecisionProfile extends AutomapperProfile { : null, ), ), + + forMember( + (dto) => dto.financialInstruments, + mapFrom((entity) => + entity.financialInstruments + ? this.mapper.mapArray( + entity.financialInstruments, + NoticeOfIntentDecisionConditionFinancialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + ) + : [], + ), + ), ); createMap( @@ -457,6 +475,42 @@ export class NoticeOfIntentDecisionProfile extends AutomapperProfile { mapFrom((ac) => ac.type), ), ); + + createMap( + mapper, + NoticeOfIntentDecisionConditionFinancialInstrument, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + forMember( + (dto) => dto.issueDate, + mapFrom((entity) => entity.issueDate.getTime()), + ), + forMember( + (dto) => dto.expiryDate, + mapFrom((entity) => (entity.expiryDate ? entity.expiryDate.getTime() : undefined)), + ), + forMember( + (dto) => dto.receivedDate, + mapFrom((entity) => entity.receivedDate.getTime()), + ), + forMember( + (dto) => dto.statusDate, + mapFrom((entity) => + entity.status !== InstrumentStatus.RECEIVED ? entity.statusDate?.getTime() || undefined : undefined, + ), + ), + forMember( + (dto) => dto.explanation, + mapFrom((entity) => entity.explanation || undefined), + ), + forMember( + (dto) => dto.notes, + mapFrom((entity) => entity.notes || undefined), + ), + forMember( + (dto) => dto.instrumentNumber, + mapFrom((entity) => entity.instrumentNumber || undefined), + ), + ); }; } } diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1740529203706-add_notice_of_intent_condition_financial_instrument.ts b/services/apps/alcs/src/providers/typeorm/migrations/1740529203706-add_notice_of_intent_condition_financial_instrument.ts new file mode 100644 index 000000000..1b660552f --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1740529203706-add_notice_of_intent_condition_financial_instrument.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddNoticeOfIntentConditionFinancialInstrument1740529203706 implements MigrationInterface { + name = 'AddNoticeOfIntentConditionFinancialInstrument1740529203706'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_type_enum" AS ENUM('Bank Draft', 'Certified Cheque', 'EFT', 'Irrevocable Letter of Credit', 'Other', 'Safekeeping Agreement')`, + ); + await queryRunner.query( + `CREATE TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_held_by_enum" AS ENUM('ALC', 'Ministry')`, + ); + await queryRunner.query( + `CREATE TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_status_enum" AS ENUM('Received', 'Released', 'Cashed', 'Replaced')`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_decision_condition_financial_instrument" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "security_holder_payee" character varying NOT NULL, "type" "alcs"."notice_of_intent_decision_condition_financial_instrument_type_enum" NOT NULL, "issue_date" TIMESTAMP WITH TIME ZONE NOT NULL, "expiry_date" TIMESTAMP WITH TIME ZONE, "amount" numeric(12,2) NOT NULL, "bank" character varying NOT NULL, "instrument_number" character varying, "held_by" "alcs"."notice_of_intent_decision_condition_financial_instrument_held_by_enum" NOT NULL, "received_date" TIMESTAMP WITH TIME ZONE NOT NULL, "notes" text, "status" "alcs"."notice_of_intent_decision_condition_financial_instrument_status_enum" NOT NULL DEFAULT 'Received', "status_date" TIMESTAMP WITH TIME ZONE, "explanation" text, "condition_uuid" uuid, CONSTRAINT "PK_cd31b04c238e6ccf3e6ac3e37f0" PRIMARY KEY ("uuid"))`, + ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notice_of_intent_decision_condition_financial_instrument" IS 'Instrument for Financial Security Conditions'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_condition_financial_instrument" ADD CONSTRAINT "FK_6dfce6b06252ca93fb88a21c471" FOREIGN KEY ("condition_uuid") REFERENCES "alcs"."notice_of_intent_decision_condition"("uuid") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_condition_financial_instrument" DROP CONSTRAINT "FK_6dfce6b06252ca93fb88a21c471"`, + ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notice_of_intent_decision_condition_financial_instrument" IS NULL`, + ); + await queryRunner.query(`DROP TABLE "alcs"."notice_of_intent_decision_condition_financial_instrument"`); + await queryRunner.query(`DROP TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_status_enum"`); + await queryRunner.query(`DROP TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_held_by_enum"`); + await queryRunner.query(`DROP TYPE "alcs"."notice_of_intent_decision_condition_financial_instrument_type_enum"`); + } +} diff --git a/services/apps/alcs/test/mocks/mockEntities.ts b/services/apps/alcs/test/mocks/mockEntities.ts index 1947daa0e..01992a923 100644 --- a/services/apps/alcs/test/mocks/mockEntities.ts +++ b/services/apps/alcs/test/mocks/mockEntities.ts @@ -35,6 +35,8 @@ import { InstrumentStatus, InstrumentType, } from '../../src/alcs/application-decision/application-decision-condition/application-decision-condition-financial-instrument/application-decision-condition-financial-instrument.entity'; +import { NoticeOfIntentDecisionConditionFinancialInstrument } from '../../src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.entity'; +import { NoticeOfIntentDecisionConditionType } from '../../src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-code.entity'; const initCardStatusMockEntity = (): CardStatus => { const cardStatus = new CardStatus(); @@ -466,6 +468,51 @@ const initApplicationDecisionConditionFinancialInstrumentMockEntity = ( return instrument; }; +const initNoticeOfIntentDecisionConditionTypeMockEntity = (code?: string): NoticeOfIntentDecisionConditionType => { + const conditionType = new NoticeOfIntentDecisionConditionType(); + conditionType.code = code ? code : 'type_1'; + conditionType.description = 'condition desc 1'; + conditionType.label = 'condition_label'; + conditionType.isActive = true; + conditionType.isComponentToConditionChecked = true; + conditionType.isDescriptionChecked = true; + conditionType.isAdministrativeFeeAmountChecked = false; + conditionType.isAdministrativeFeeAmountRequired = null; + conditionType.administrativeFeeAmount = null; + conditionType.isDateChecked = false; + conditionType.isDateRequired = null; + conditionType.dateType = null; + conditionType.singleDateLabel = null; + conditionType.isSecurityAmountChecked = false; + conditionType.isSecurityAmountRequired = null; + conditionType.auditCreatedAt = new Date(1, 1, 1, 1, 1, 1, 1); + conditionType.auditUpdatedAt = new Date(1, 1, 1, 1, 1, 1, 1); + + return conditionType; +}; + +const initNoticeOfIntentDecisionConditionFinancialInstrumentMockEntity = ( + payee?: string, + bank?: string, + instrumentNumber?: string, +): NoticeOfIntentDecisionConditionFinancialInstrument => { + const instrument = new NoticeOfIntentDecisionConditionFinancialInstrument(); + instrument.securityHolderPayee = 'fake-payee'; + instrument.type = InstrumentType.BANK_DRAFT; + instrument.issueDate = new Date(2022, 1, 1); + instrument.expiryDate = new Date(2023, 1, 1); + instrument.amount = 1000.0; + instrument.bank = 'fake-bank'; + instrument.instrumentNumber = '123456'; + instrument.heldBy = HeldBy.ALC; + instrument.receivedDate = new Date(2022, 1, 1); + instrument.notes = 'fake-notes'; + instrument.status = InstrumentStatus.RECEIVED; + instrument.statusDate = new Date(2022, 1, 1); + instrument.explanation = 'fake-explanation'; + return instrument; +}; + export { initCardStatusMockEntity, initApplicationMockEntity, @@ -493,4 +540,6 @@ export { initMockApplicationDecisionConditionCard, initApplicationDecisionConditionTypeMockEntity, initApplicationDecisionConditionFinancialInstrumentMockEntity, + initNoticeOfIntentDecisionConditionTypeMockEntity, + initNoticeOfIntentDecisionConditionFinancialInstrumentMockEntity, }; From d82ccf038e9c01cfe57f16e46cf93e253b8fbbf7 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Tue, 25 Feb 2025 16:54:02 -0800 Subject: [PATCH 39/87] ALCS-2029 Order conditions feature --- .../condition/condition.component.html | 4 +- .../condition/condition.component.ts | 7 +- .../conditions/conditions.component.ts | 18 +-- ...sion-condition-order-dialog.component.html | 31 ++-- ...sion-condition-order-dialog.component.scss | 137 +++++++++++++++++- ...cision-condition-order-dialog.component.ts | 95 +++++++++++- .../decision-condition.component.ts | 1 + .../decision-conditions.component.html | 2 +- .../decision-conditions.component.ts | 23 ++- .../decision-input-v2.component.ts | 2 +- .../decision-v2/decision-v2.component.ts | 2 +- .../application/decision/decision.module.ts | 3 +- .../application-decision-condition.service.ts | 8 + .../application-decision-v2.dto.ts | 2 + ...plication-decision-condition.controller.ts | 6 + .../application-decision-condition.dto.ts | 3 + .../application-decision-condition.entity.ts | 4 + .../application-decision-condition.service.ts | 18 +++ ...notice-of-intent-decision-condition.dto.ts | 3 + ...ice-of-intent-decision-condition.entity.ts | 4 + .../1740423770890-add_order_properties.ts | 16 ++ 21 files changed, 338 insertions(+), 51 deletions(-) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1740423770890-add_order_properties.ts diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html index fe908f02e..61bc9edfc 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html @@ -1,7 +1,5 @@
- - -

{{ condition.type.label }}

+

{{ alphaIndex(index) }}. {{ condition.type.label }}

diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts index 1e74f27fe..3eef45496 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts @@ -61,7 +61,6 @@ export class ConditionComponent implements OnInit, AfterViewInit { showAdmFeeField = false; showSecurityAmountField = false; singleDateFormated: string | undefined = undefined; - stringIndex: string = ''; isThreeColumn = true; @@ -88,7 +87,6 @@ export class ConditionComponent implements OnInit, AfterViewInit { ) {} async ngOnInit() { - this.stringIndex = countToString(this.index); if (this.condition) { this.dates = Array.isArray(this.condition.dates) ? this.condition.dates : []; if (this.condition.type?.dateType === DateType.SINGLE && this.dates.length <= 0) { @@ -193,6 +191,7 @@ export class ConditionComponent implements OnInit, AfterViewInit { if (condition) { const update = await this.conditionService.update(condition.uuid, { [field]: value, + order: condition.order, }); const labels = this.condition.componentLabelsStr; @@ -327,4 +326,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { } } } + + alphaIndex(index: number) { + return countToString(index); + } } diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts index 6fce7a8e6..242602809 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts @@ -152,23 +152,7 @@ export class ConditionsComponent implements OnInit { decision: ApplicationDecisionWithLinkedResolutionDto, conditions: ApplicationDecisionConditionWithStatus[], ) { - decision.conditions = conditions.sort((a, b) => { - const order = [ - CONDITION_STATUS.ONGOING, - CONDITION_STATUS.COMPLETE, - CONDITION_STATUS.PASTDUE, - CONDITION_STATUS.EXPIRED, - ]; - if (a.status === b.status) { - if (a.type && b.type) { - return a.type?.label.localeCompare(b.type.label); - } else { - return -1; - } - } else { - return order.indexOf(a.status) - order.indexOf(b.status); - } - }); + decision.conditions = conditions.sort((a, b) => a.order - b.order); } private async mapConditions( diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.html index 7dca24aa7..fbe7c09ef 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.html @@ -1,21 +1,32 @@ + + + +

Re-order Conditions

-
+ mat-table + > - - + @@ -43,9 +54,11 @@

Re-order Conditions

([]); + + @ViewChild('orderMenu') orderMenu!: TemplateRef; constructor( @Inject(MAT_DIALOG_DATA) - public data: { conditions: { condition: ApplicationDecisionConditionDto; index: number }[]; decision: string }, + public data: { conditions: ApplicationDecisionConditionDto[]; }, private dialogRef: MatDialogRef, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, ) {} - async ngOnInit() { - console.log(this.data); + ngOnInit(): void { + const orderIndexes = this.data.conditions.map((c) => c.order); + const isAllZero = orderIndexes.every((val, i, arr) => val === arr[0] && arr[0] === 0); + if (isAllZero) { + let index = 0; + this.data.conditions.forEach((c) => { + c.order = index; + index++; + }); + } + this.dataSource.data = this.data.conditions.sort((a,b) => a.order - b.order); + } + + async onRowDropped(event: CdkDragDrop) { + this.moveItem(event.previousIndex, event.currentIndex); + } + + async openMenu($event: MouseEvent, record: ApplicationDecisionConditionDto) { + this.overlayRef?.detach(); + $event.preventDefault(); + this.selectedRecord = record.uuid; + const positionStrategy = this.overlay + .position() + .flexibleConnectedTo({ x: $event.x, y: $event.y }) + .withPositions([ + { + originX: 'end', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top', + }, + ]); + + this.overlayRef = this.overlay.create({ + positionStrategy, + scrollStrategy: this.overlay.scrollStrategies.close(), + }); + + this.overlayRef.attach( + new TemplatePortal(this.orderMenu, this.viewContainerRef, { + $implicit: record, + }), + ); + } + + sendToBottom(record: ApplicationDecisionConditionDto) { + const currentIndex = this.data.conditions.findIndex((item) => item.uuid === record.uuid); + this.moveItem(currentIndex, this.data.conditions.length - 1); + this.overlayRef?.detach(); + this.selectedRecord = undefined; + } + + sendToTop(record: ApplicationDecisionConditionDto) { + const currentIndex = this.data.conditions.findIndex((item) => item.uuid === record.uuid); + this.moveItem(currentIndex, 0); + this.overlayRef?.detach(); + this.selectedRecord = undefined; + } + + clearMenu() { + this.overlayRef?.detach(); + this.selectedRecord = undefined; + } + + private moveItem(currentIndex: number, targetIndex: number) { + this.data.conditions[currentIndex].order = targetIndex; + this.data.conditions[targetIndex].order = currentIndex; + this.dataSource.data = this.data.conditions.sort((a,b) => a.order - b.order); } onCancel(): void { - this.dialogRef.close({ action: 'cancel' }); + this.dialogRef.close(); } onSave(): void { - this.dialogRef.close({ action: 'cancel' }); + const order = this.data.conditions.map((cond, index) => ({ + uuid: cond.uuid, + order: cond.order, + })); + this.dialogRef.close(order); } alphaIndex(index: number) { diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts index 5a6dd919b..dbe5184bd 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts @@ -118,6 +118,7 @@ export class DecisionConditionComponent implements OnInit, OnChanges { administrativeFee: this.administrativeFee.value !== null ? parseFloat(this.administrativeFee.value) : undefined, description: this.description.value ?? undefined, componentsToCondition: selectedOptions, + order: this.data.order, }; if (this.showSingleDateField) { diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html index 4397e652d..78005b144 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html @@ -23,7 +23,7 @@

Conditions

- -

Resolution

diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss index ffe555c27..6c8500113 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss @@ -58,7 +58,7 @@ hr { grid-template-rows: auto auto; row-gap: 10px; column-gap: 28px; - margin-bottom: 36px; + margin-bottom: 10px; .title { display: flex; From 3f8eb26df2611ad5eda175a9569553e23269d031 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Wed, 26 Feb 2025 10:21:15 -0800 Subject: [PATCH 45/87] ALCS-2470 Bold ALR Area Impacted --- .../decision-component/basic/basic.component.html | 7 ++++--- .../decision-component/basic/basic.component.html | 7 ++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.html index 0ba927a13..c44f5e061 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.html @@ -1,8 +1,9 @@
- ALR Area Impacted - (ha) - (m2) +
ALR Area Impacted + (ha) + (m2) +
-
ALR Area Impacted (ha)
+
+
ALR Area Impacted + (ha) + (m2) +
+
Date: Wed, 26 Feb 2025 11:13:35 -0800 Subject: [PATCH 46/87] Add NOI decision condition financial instrument frontend components - NOI decision condition financial instrument service and DTO - Add common decision condition financial instrument component to NOI --- .../condition/condition.component.html | 14 +- .../condition/condition.component.scss | 4 + .../condition/condition.component.ts | 5 + .../notice-of-intent.component.ts | 10 +- ...sion-condition-financial-instrument.dto.ts | 9 + ...ition-financial-instrument.service.spec.ts | 335 ++++++++++++++++++ ...-condition-financial-instrument.service.ts | 108 ++++++ .../notice-of-intent-decision.dto.ts | 4 + 8 files changed, 487 insertions(+), 2 deletions(-) create mode 100644 alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.spec.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.ts diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html index 36a0d45ae..f60dbe274 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html @@ -145,7 +145,13 @@

{{ condition.type.label }}

Description
{{ condition.type.label }} >
+ +
+ +
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.scss index 31a82f2d7..c630ca0b4 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.scss +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.scss @@ -96,3 +96,7 @@ ::ng-deep textarea:focus { outline: none; } + +.condition-instrument { + margin-top: 24px; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts index 66141e8ea..1b39ace15 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts @@ -2,6 +2,7 @@ import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output, ViewChil import moment from 'moment'; import { NoticeOfIntentDecisionConditionService } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service'; import { + ConditionType, NoticeOfIntentDecisionConditionDateDto, UpdateNoticeOfIntentDecisionConditionDto, } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision.dto'; @@ -64,6 +65,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { conditionStatus: string = ''; stringIndex: string = ''; + isFinancialSecurity: boolean = false; + displayColumns: string[] = ['index', 'due', 'completed', 'comment', 'action']; @ViewChild(MatSort) sort!: MatSort; @@ -106,6 +109,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { this.dataSource = new MatTableDataSource( this.addIndex(this.sortDates(this.dates)), ); + + this.isFinancialSecurity = this.condition.type?.code === ConditionType.FINANCIAL_SECURITY; } } diff --git a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts index 89d3ba4a5..2bd1e735b 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts @@ -21,6 +21,8 @@ import { FileTagService } from '../../services/common/file-tag.service'; import { NoticeOfIntentTagService } from '../../services/notice-of-intent/notice-of-intent-tag/notice-of-intent-tag.service'; import { NoticeOfIntentDecisionConditionCardDto } from '../../services/notice-of-intent/decision-v2/notice-of-intent-decision.dto'; import { NoticeOfIntentDecisionConditionCardService } from '../../services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-card/notice-of-intent-decision-condition-card.service'; +import { DecisionConditionFinancialInstrumentService } from '../../services/common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentService } from '../../services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service'; export const childRoutes = [ { @@ -96,7 +98,13 @@ const preSubmissionRoutes = [ selector: 'app-notice-of-intent', templateUrl: './notice-of-intent.component.html', styleUrls: ['./notice-of-intent.component.scss'], - providers: [{ provide: FileTagService, useClass: NoticeOfIntentTagService }], + providers: [ + { provide: FileTagService, useClass: NoticeOfIntentTagService }, + { + provide: DecisionConditionFinancialInstrumentService, + useClass: NoticeOfIntentDecisionConditionFinancialInstrumentService, + }, + ], }) export class NoticeOfIntentComponent implements OnInit, OnDestroy { destroy = new Subject(); diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto.ts new file mode 100644 index 000000000..bcbcecdfc --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.dto.ts @@ -0,0 +1,9 @@ +import { + CreateUpdateDecisionConditionFinancialInstrumentDto, + DecisionConditionFinancialInstrumentDto, +} from '../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; + +export interface NoticeOfIntentDecisionConditionFinancialInstrumentDto + extends DecisionConditionFinancialInstrumentDto {} +export interface CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto + extends CreateUpdateDecisionConditionFinancialInstrumentDto {} diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.spec.ts new file mode 100644 index 000000000..a746b4001 --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.spec.ts @@ -0,0 +1,335 @@ +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { NoticeOfIntentDecisionConditionFinancialInstrumentService } from './notice-of-intent-decision-condition-financial-instrument.service'; +import { environment } from '../../../../../../environments/environment'; +import { + CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + NoticeOfIntentDecisionConditionFinancialInstrumentDto, +} from './notice-of-intent-decision-condition-financial-instrument.dto'; +import { + HeldBy, + InstrumentStatus, + InstrumentType, +} from '../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; + +describe('NoticeOfIntentDecisionConditionFinancialInstrumentService', () => { + let service: NoticeOfIntentDecisionConditionFinancialInstrumentService; + let mockHttpClient: DeepMocked; + const conditionId = '1'; + const instrumentId = '1'; + let expectedUrl: string; + + beforeEach(() => { + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentDecisionConditionFinancialInstrumentService); + expectedUrl = `${environment.apiUrl}/notice-of-intent-decision-condition/${conditionId}/financial-instruments`; + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make an http get for getAll', async () => { + const mockResponse: NoticeOfIntentDecisionConditionFinancialInstrumentDto[] = [ + { + uuid: '1', + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }, + ]; + + mockHttpClient.get.mockReturnValue(of(mockResponse)); + + const result = await service.getAll(conditionId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(expectedUrl); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if getAll fails', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.getAll(conditionId)).rejects.toThrow('Failed to retrieve the financial instruments'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(expectedUrl); + }); + + it('should make an http get for get', async () => { + const mockResponse: NoticeOfIntentDecisionConditionFinancialInstrumentDto = { + uuid: '1', + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + mockHttpClient.get.mockReturnValue(of(mockResponse)); + + const result = await service.get(conditionId, instrumentId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if get fails', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should make an http post for create', async () => { + const mockRequest: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + const mockResponse: NoticeOfIntentDecisionConditionFinancialInstrumentDto = { + uuid: '1', + ...mockRequest, + }; + + mockHttpClient.post.mockReturnValue(of(mockResponse)); + + const result = await service.create(conditionId, mockRequest); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post).toHaveBeenCalledWith(expectedUrl, mockRequest); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if create fails', async () => { + const mockRequest: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + mockHttpClient.post.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.create(conditionId, mockRequest)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post).toHaveBeenCalledWith(expectedUrl, mockRequest); + }); + + it('should make an http patch for update', async () => { + const mockRequest: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + const mockResponse: NoticeOfIntentDecisionConditionFinancialInstrumentDto = { + uuid: '1', + ...mockRequest, + }; + + mockHttpClient.patch.mockReturnValue(of(mockResponse)); + + const result = await service.update(conditionId, instrumentId, mockRequest); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`, mockRequest); + expect(result).toEqual(mockResponse); + }); + + it('should show an error if update fails', async () => { + const mockRequest: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }; + + mockHttpClient.patch.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.update(conditionId, instrumentId, mockRequest)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`, mockRequest); + }); + + it('should make an http delete for delete', async () => { + mockHttpClient.delete.mockReturnValue(of({})); + + await service.delete(conditionId, instrumentId); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should show an error if delete fails', async () => { + mockHttpClient.delete.mockReturnValue( + throwError( + () => + new HttpErrorResponse({ status: 500, error: { message: 'Failed to retrieve the financial instruments' } }), + ), + ); + + await expect(service.delete(conditionId, instrumentId)).rejects.toThrow( + new Error('Failed to retrieve the financial instruments'), + ); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should remove statusDate and explanation when status is RECEIVED during update', async () => { + const mockRequest: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto = { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + statusDate: 1672531200000, + explanation: 'test', + instrumentNumber: '123', + }; + + const mockResponse: NoticeOfIntentDecisionConditionFinancialInstrumentDto = { + uuid: '1', + ...mockRequest, + }; + + mockHttpClient.patch.mockReturnValue(of(mockResponse)); + + await service.update(conditionId, instrumentId, mockRequest); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`, { + securityHolderPayee: 'Payee', + type: InstrumentType.BANK_DRAFT, + issueDate: 1627849200000, + amount: 1000, + bank: 'Bank', + heldBy: HeldBy.ALC, + receivedDate: 1627849200000, + status: InstrumentStatus.RECEIVED, + instrumentNumber: '123', + }); + }); + + it('should throw Condition/financial instrument not found', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => new HttpErrorResponse({ status: 404, error: { message: 'Condition/financial instrument not found' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Condition/financial instrument not found'), + ); + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should throw Condition is not of type Financial Security', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => new HttpErrorResponse({ status: 400, error: { message: 'Condition is not of type Financial Security' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Condition is not of type Financial Security'), + ); + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); + + it('should throw Condition type Financial Security not found', async () => { + mockHttpClient.get.mockReturnValue( + throwError( + () => new HttpErrorResponse({ status: 500, error: { message: 'Condition type Financial Security not found' } }), + ), + ); + + await expect(service.get(conditionId, instrumentId)).rejects.toThrow( + new Error('Condition type Financial Security not found'), + ); + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith(`${expectedUrl}/${instrumentId}`); + }); +}); diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.ts new file mode 100644 index 000000000..665d7e984 --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition-financial-instrument/notice-of-intent-decision-condition-financial-instrument.service.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@angular/core'; +import { + NoticeOfIntentDecisionConditionFinancialInstrumentDto, + CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, +} from './notice-of-intent-decision-condition-financial-instrument.dto'; +import { DecisionConditionFinancialInstrumentService } from '../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.service'; +import { environment } from '../../../../../../environments/environment'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; +import { InstrumentStatus } from '../../../../common/decision-condition-financial-instrument/decision-condition-financial-instrument.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentDecisionConditionFinancialInstrumentService extends DecisionConditionFinancialInstrumentService { + private baseUrl = `${environment.apiUrl}/notice-of-intent-decision-condition`; + private financialInstrumentUrl = 'financial-instruments'; + + constructor(http: HttpClient) { + super(http); + } + async getAll(conditionUuid: string): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}`; + + try { + return await firstValueFrom(this.http.get(url)); + } catch (e) { + this.handleError(e); + } + } + + async get(conditionUuid: string, uuid: string): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; + + try { + return await firstValueFrom(this.http.get(url)); + } catch (e) { + this.handleError(e); + } + } + + async create( + conditionUuid: string, + dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + ): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}`; + + try { + return await firstValueFrom(this.http.post(url, dto)); + } catch (e) { + this.handleError(e); + } + } + + async update( + conditionUuid: string, + uuid: string, + dto: CreateUpdateNoticeOfIntentDecisionConditionFinancialInstrumentDto, + ): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; + + if (dto.status === InstrumentStatus.RECEIVED) { + if (dto.statusDate) { + delete dto.statusDate; + } + if (dto.explanation) { + delete dto.explanation; + } + } + + try { + return await firstValueFrom(this.http.patch(url, dto)); + } catch (e) { + this.handleError(e); + } + } + + async delete(conditionUuid: string, uuid: string): Promise { + const url = `${this.baseUrl}/${conditionUuid}/${this.financialInstrumentUrl}/${uuid}`; + + try { + return await firstValueFrom(this.http.delete(url)); + } catch (e) { + this.handleError(e); + } + } + + private handleError(e: any): never { + console.error(e); + let message; + if (e instanceof HttpErrorResponse) { + if (e.status === 404) { + message = 'Condition/financial instrument not found'; + } else if (e.status === 400) { + message = 'Condition is not of type Financial Security'; + } else { + if (e.error.message === 'Condition type Financial Security not found') { + message = 'Condition type Financial Security not found'; + } else { + message = 'Failed to retrieve the financial instruments'; + } + } + } else { + message = 'Failed to perform the operation'; + } + throw new Error(message); + } +} diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision.dto.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision.dto.ts index 281bbf332..df551592e 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision.dto.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision.dto.ts @@ -4,6 +4,10 @@ import { DateLabel, DateType } from '../../application/decision/application-deci import { CardDto } from '../../card/card.dto'; import { NoticeOfIntentTypeDto } from '../notice-of-intent.dto'; +export enum ConditionType { + FINANCIAL_SECURITY = 'BOND', +} + export interface UpdateNoticeOfIntentDecisionDto { resolutionNumber?: number; resolutionYear?: number; From 39258f2cbd33546125710a3addcae891c3f87e81 Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Wed, 26 Feb 2025 11:14:48 -0800 Subject: [PATCH 47/87] Fix status column rendering received date instead of status date --- .../decision-condition-financial-instrument.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts index ae4558e42..08db4359c 100644 --- a/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts +++ b/alcs-frontend/src/app/shared/decision-condition-financial-instrument/decision-condition-financial-instrument.component.ts @@ -103,8 +103,8 @@ export class DecisionConditionFinancialInstrumentComponent implements OnInit { return instrument.status; } - if (instrument.receivedDate) { - const date = new Date(instrument.receivedDate); + if (instrument.statusDate) { + const date = new Date(instrument.statusDate); const year = date.getFullYear(); const month = date.toLocaleDateString('en-CA', { month: 'short' }); const day = String(date.getDate()).padStart(2, '0'); From 94a755af47129e6870a986e420b95b94b3976c15 Mon Sep 17 00:00:00 2001 From: Tristan Slater <1631008+trslater@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:21:06 -0800 Subject: [PATCH 48/87] Make upload dialog more robust and secure - Create interfaces for setting data to allow for better validation - Create a global options const that can be used for both add and edit to make sure they are consistent - Pass in parcel/owner services to avoid long modal opening times --- .../documents/documents.component.ts | 85 +++++++---------- .../inquiry/documents/documents.component.ts | 27 +++--- .../documents/documents.component.ts | 86 +++++++---------- .../documents/documents.component.ts | 36 +++++--- .../documents/documents.component.ts | 36 +++++--- .../document-upload-dialog.component.html | 10 +- .../document-upload-dialog.component.ts | 92 ++++++++++--------- .../document-upload-dialog.dto.ts | 22 ++++- .../document-upload-dialog.interface.ts | 40 +++++++- 9 files changed, 242 insertions(+), 192 deletions(-) diff --git a/alcs-frontend/src/app/features/application/documents/documents.component.ts b/alcs-frontend/src/app/features/application/documents/documents.component.ts index deb7d5e2f..6b6c278bd 100644 --- a/alcs-frontend/src/app/features/application/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/application/documents/documents.component.ts @@ -18,6 +18,25 @@ import { DocumentUploadDialogComponent, VisibilityGroup, } from '../../../shared/document-upload-dialog/document-upload-dialog.component'; +import { + DocumentUploadDialogData, + DocumentUploadDialogOptions, +} from '../../../shared/document-upload-dialog/document-upload-dialog.interface'; + +const DOCUMENT_UPLOAD_DIALOG_OPTIONS: DocumentUploadDialogOptions = { + allowedVisibilityFlags: ['A', 'C', 'G', 'P'], + allowsFileEdit: true, + documentTypeOverrides: { + [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: { + visibilityGroups: [VisibilityGroup.INTERNAL], + allowsFileEdit: false, + }, + [DOCUMENT_TYPE.CORPORATE_SUMMARY]: { + visibilityGroups: [VisibilityGroup.INTERNAL], + allowsFileEdit: false, + }, + }, +}; @Component({ selector: 'app-documents', @@ -63,37 +82,19 @@ export class DocumentsComponent implements OnInit { } async onUploadFile() { - const submission = await this.applicationSubmissionService.fetchSubmission(this.fileId); - const parcels = await this.applicationParcelService.fetchParcels(this.fileId); + const data: DocumentUploadDialogData = Object.assign(DOCUMENT_UPLOAD_DIALOG_OPTIONS, { + fileId: this.fileId, + documentService: this.applicationDocumentService, + parcelService: this.applicationParcelService, + submissionService: this.applicationSubmissionService, + }); this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - documentService: this.applicationDocumentService, - selectableParcels: parcels.map((parcel, index) => ({ ...parcel, index })), - selectableOwners: submission.owners - .filter((owner) => owner.type.code === 'ORGZ') - .map((owner) => ({ - label: owner.organizationName ?? owner.displayName, - uuid: owner.uuid, - })), - allowedVisibilityFlags: ['A', 'C', 'G', 'P'], - allowsFileEdit: true, - documentTypeOverrides: { - [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - [DOCUMENT_TYPE.CORPORATE_SUMMARY]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - }, - }, + data, }) .afterClosed() .subscribe((isDirty) => { @@ -126,37 +127,21 @@ export class DocumentsComponent implements OnInit { } async onEditFile(element: ApplicationDocumentDto) { - const submission = await this.applicationSubmissionService.fetchSubmission(this.fileId); - const parcels = await this.applicationParcelService.fetchParcels(this.fileId); + const data: DocumentUploadDialogData = Object.assign(DOCUMENT_UPLOAD_DIALOG_OPTIONS, { + allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, + fileId: this.fileId, + existingDocument: element, + documentService: this.applicationDocumentService, + parcelService: this.applicationParcelService, + submissionService: this.applicationSubmissionService, + }); this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - existingDocument: element, - documentService: this.applicationDocumentService, - selectableParcels: parcels.map((parcel, index) => ({ ...parcel, index })), - selectableOwners: submission.owners - .filter((owner) => owner.type.code === 'ORGZ') - .map((owner) => ({ - label: owner.organizationName ?? owner.displayName, - uuid: owner.uuid, - })), - allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, - documentTypeOverrides: { - [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - [DOCUMENT_TYPE.CORPORATE_SUMMARY]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - }, - }, + data, }) .afterClosed() .subscribe((isDirty: boolean) => { diff --git a/alcs-frontend/src/app/features/inquiry/documents/documents.component.ts b/alcs-frontend/src/app/features/inquiry/documents/documents.component.ts index 57a8dde9d..bf9f10799 100644 --- a/alcs-frontend/src/app/features/inquiry/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/inquiry/documents/documents.component.ts @@ -11,6 +11,7 @@ import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/c import { DOCUMENT_SYSTEM } from '../../../shared/document/document.dto'; import { FILE_NAME_TRUNCATE_LENGTH } from '../../../shared/constants'; import { DocumentUploadDialogComponent } from '../../../shared/document-upload-dialog/document-upload-dialog.component'; +import { DocumentUploadDialogData } from '../../../shared/document-upload-dialog/document-upload-dialog.interface'; @Component({ selector: 'app-documents', @@ -47,16 +48,18 @@ export class DocumentsComponent implements OnInit { } async onUploadFile() { + const data: DocumentUploadDialogData = { + allowsFileEdit: true, + fileId: this.fileId, + documentService: this.inquiryDocumentService, + }; + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - documentService: this.inquiryDocumentService, - allowsFileEdit: true, - }, + data, }) .afterClosed() .subscribe((isDirty) => { @@ -89,17 +92,19 @@ export class DocumentsComponent implements OnInit { } onEditFile(element: PlanningReviewDocumentDto) { + const data: DocumentUploadDialogData = { + allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, + fileId: this.fileId, + existingDocument: element, + documentService: this.inquiryDocumentService, + }; + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - existingDocument: element, - documentService: this.inquiryDocumentService, - allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, - }, + data, }) .afterClosed() .subscribe((isDirty: boolean) => { diff --git a/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts b/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts index 60fc85646..994df384a 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts @@ -23,6 +23,25 @@ import { } from '../../../shared/document-upload-dialog/document-upload-dialog.component'; import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service'; import { NoticeOfIntentParcelService } from '../../../services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { + DocumentUploadDialogData, + DocumentUploadDialogOptions, +} from '../../../shared/document-upload-dialog/document-upload-dialog.interface'; + +const DOCUMENT_UPLOAD_DIALOG_OPTIONS: DocumentUploadDialogOptions = { + allowedVisibilityFlags: ['A', 'C', 'G', 'P'], + allowsFileEdit: true, + documentTypeOverrides: { + [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: { + visibilityGroups: [VisibilityGroup.INTERNAL], + allowsFileEdit: false, + }, + [DOCUMENT_TYPE.CORPORATE_SUMMARY]: { + visibilityGroups: [VisibilityGroup.INTERNAL], + allowsFileEdit: false, + }, + }, +}; @Component({ selector: 'app-noi-documents', @@ -67,37 +86,19 @@ export class NoiDocumentsComponent implements OnInit { } async onUploadFile() { - const submission = await this.noiSubmissionService.fetchSubmission(this.fileId); - const parcels = await this.noiParcelService.fetchParcels(this.fileId); + const data: DocumentUploadDialogData = Object.assign(DOCUMENT_UPLOAD_DIALOG_OPTIONS, { + fileId: this.fileId, + documentService: this.noiDocumentService, + parcelService: this.noiParcelService, + submissionService: this.noiSubmissionService, + }); this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - documentService: this.noiDocumentService, - selectableParcels: parcels.map((parcel, index) => ({ ...parcel, index })), - selectableOwners: submission.owners - .filter((owner) => owner.type.code === 'ORGZ') - .map((owner) => ({ - label: owner.organizationName ?? owner.displayName, - uuid: owner.uuid, - })), - allowedVisibilityFlags: ['A', 'C', 'G', 'P'], - allowsFileEdit: true, - documentTypeOverrides: { - [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - [DOCUMENT_TYPE.CORPORATE_SUMMARY]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - }, - }, + data, }) .afterClosed() .subscribe((isDirty) => { @@ -116,38 +117,21 @@ export class NoiDocumentsComponent implements OnInit { } async onEditFile(element: NoticeOfIntentDocumentDto) { - const submission = await this.noiSubmissionService.fetchSubmission(this.fileId); - const parcels = await this.noiParcelService.fetchParcels(this.fileId); + const data: DocumentUploadDialogData = Object.assign(DOCUMENT_UPLOAD_DIALOG_OPTIONS, { + allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, + fileId: this.fileId, + existingDocument: element, + documentService: this.noiDocumentService, + parcelService: this.noiParcelService, + submissionService: this.noiSubmissionService, + }); this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - existingDocument: element, - documentService: this.noiDocumentService, - selectableParcels: parcels.map((parcel, index) => ({ ...parcel, index })), - selectableOwners: submission.owners - .filter((owner) => owner.type.code === 'ORGZ') - .map((owner) => ({ - label: owner.organizationName ?? owner.displayName, - uuid: owner.uuid, - })), - allowedVisibilityFlags: ['A', 'C', 'G', 'P'], - allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, - documentTypeOverrides: { - [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - [DOCUMENT_TYPE.CORPORATE_SUMMARY]: { - visibilityGroups: [VisibilityGroup.INTERNAL], - allowsFileEdit: false, - }, - }, - }, + data, }) .afterClosed() .subscribe((isDirty: boolean) => { diff --git a/alcs-frontend/src/app/features/notification/documents/documents.component.ts b/alcs-frontend/src/app/features/notification/documents/documents.component.ts index 304785905..b34c4d720 100644 --- a/alcs-frontend/src/app/features/notification/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/notification/documents/documents.component.ts @@ -12,6 +12,15 @@ import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/c import { DOCUMENT_SYSTEM } from '../../../shared/document/document.dto'; import { FILE_NAME_TRUNCATE_LENGTH } from '../../../shared/constants'; import { DocumentUploadDialogComponent } from '../../../shared/document-upload-dialog/document-upload-dialog.component'; +import { + DocumentUploadDialogData, + DocumentUploadDialogOptions, +} from '../../../shared/document-upload-dialog/document-upload-dialog.interface'; + +const DOCUMENT_UPLOAD_DIALOG_OPTIONS: DocumentUploadDialogOptions = { + allowedVisibilityFlags: ['A', 'G', 'P'], + allowsFileEdit: true, +}; @Component({ selector: 'app-notification-documents', @@ -48,17 +57,17 @@ export class NotificationDocumentsComponent implements OnInit { } async onUploadFile() { + const data: DocumentUploadDialogData = Object.assign(DOCUMENT_UPLOAD_DIALOG_OPTIONS, { + fileId: this.fileId, + documentService: this.notificationDocumentService, + }); + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - documentService: this.notificationDocumentService, - allowedVisibilityFlags: ['A', 'G', 'P'], - allowsFileEdit: true, - }, + data, }) .afterClosed() .subscribe((isDirty) => { @@ -91,18 +100,19 @@ export class NotificationDocumentsComponent implements OnInit { } onEditFile(element: NoticeOfIntentDocumentDto) { + const data: DocumentUploadDialogData = Object.assign(DOCUMENT_UPLOAD_DIALOG_OPTIONS, { + allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, + fileId: this.fileId, + existingDocument: element, + documentService: this.notificationDocumentService, + }); + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - existingDocument: element, - documentService: this.notificationDocumentService, - allowedVisibilityFlags: ['A', 'G', 'P'], - allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, - }, + data, }) .afterClosed() .subscribe((isDirty: boolean) => { diff --git a/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts b/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts index f666bed56..25e50b576 100644 --- a/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts @@ -10,6 +10,15 @@ import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/c import { DOCUMENT_SYSTEM } from '../../../shared/document/document.dto'; import { FILE_NAME_TRUNCATE_LENGTH } from '../../../shared/constants'; import { DocumentUploadDialogComponent } from '../../../shared/document-upload-dialog/document-upload-dialog.component'; +import { + DocumentUploadDialogData, + DocumentUploadDialogOptions, +} from '../../../shared/document-upload-dialog/document-upload-dialog.interface'; + +const DOCUMENT_UPLOAD_DIALOG_OPTIONS: DocumentUploadDialogOptions = { + allowedVisibilityFlags: ['C'], + allowsFileEdit: true, +}; @Component({ selector: 'app-documents', @@ -48,17 +57,17 @@ export class DocumentsComponent implements OnInit { } async onUploadFile() { + const data: DocumentUploadDialogData = Object.assign(DOCUMENT_UPLOAD_DIALOG_OPTIONS, { + fileId: this.fileId, + documentService: this.planningReviewDocumentService, + }); + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - documentService: this.planningReviewDocumentService, - allowedVisibilityFlags: ['C'], - allowsFileEdit: true, - }, + data, }) .afterClosed() .subscribe((isDirty) => { @@ -91,18 +100,19 @@ export class DocumentsComponent implements OnInit { } onEditFile(element: PlanningReviewDocumentDto) { + const data: DocumentUploadDialogData = Object.assign(DOCUMENT_UPLOAD_DIALOG_OPTIONS, { + allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, + fileId: this.fileId, + existingDocument: element, + documentService: this.planningReviewDocumentService, + }); + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', - data: { - fileId: this.fileId, - existingDocument: element, - documentService: this.planningReviewDocumentService, - allowsFileEdit: true, - allowedVisibilityFlags: ['C'], - }, + data, }) .afterClosed() .subscribe((isDirty: boolean) => { diff --git a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.html b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.html index 0931a4cc9..a4bcb11c6 100644 --- a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.html +++ b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.html @@ -86,23 +86,23 @@

{{ title }} Document

-
+
Associated Parcel - - #{{ parcel.index + 1 }} PID: + + #{{ i + 1 }} PID: {{ parcel.pid | mask: '000-000-000' }} No Data
-
+
Associated Organization - + {{ owner.label }} diff --git a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.ts b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.ts index 8dcd3d988..517d01bc9 100644 --- a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.ts +++ b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.ts @@ -3,29 +3,23 @@ import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from '@ang import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { ToastService } from '../../services/toast/toast.service'; -import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM, DOCUMENT_TYPE, DocumentTypeDto } from '../document/document.dto'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../document/document.dto'; import { FileHandle } from '../drag-drop-file/drag-drop-file.directive'; import { splitExtension } from '../utils/file'; -import { DecisionService, DocumentService } from './document-upload-dialog.interface'; import { CreateDocumentDto, - DocumentDto, SelectableOwnerDto, SelectableParcelDto, UpdateDocumentDto, } from './document-upload-dialog.dto'; import { Subject } from 'rxjs'; +import { DocumentUploadDialogData } from './document-upload-dialog.interface'; export enum VisibilityGroup { INTERNAL = 'Internal', PUBLIC = 'Public', } -export interface DocumentTypeConfig { - visibilityGroups: VisibilityGroup[]; - allowsFileEdit: boolean; -} - @Component({ selector: 'app-document-upload-dialog', templateUrl: './document-upload-dialog.component.html', @@ -74,20 +68,12 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { internalVisibilityLabel = ''; + selectableParcels: SelectableParcelDto[] = []; + selectableOwners: SelectableOwnerDto[] = []; + constructor( @Inject(MAT_DIALOG_DATA) - public data: { - fileId: string; - decisionUuid?: string; - existingDocument?: DocumentDto; - decisionService?: DecisionService; - documentService?: DocumentService; - selectableParcels?: SelectableParcelDto[]; - selectableOwners?: SelectableOwnerDto[]; - allowedVisibilityFlags?: ('A' | 'C' | 'G' | 'P')[]; - allowsFileEdit?: boolean; - documentTypeOverrides?: Record; - }, + public data: DocumentUploadDialogData, protected dialog: MatDialogRef, private toastService: ToastService, ) {} @@ -104,7 +90,7 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { this.allowsFileEdit = this.data.allowsFileEdit ?? this.allowsFileEdit; if (document.type && this.data.documentTypeOverrides && this.data.documentTypeOverrides[document.type.code]) { - this.allowsFileEdit = this.data.documentTypeOverrides[document.type.code].allowsFileEdit; + this.allowsFileEdit = !!this.data.documentTypeOverrides[document.type.code]?.allowsFileEdit; } if (document.type?.code === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { @@ -267,32 +253,54 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { } async prepareCertificateOfTitleUpload(uuid?: string) { - if (this.data.selectableParcels && this.data.selectableParcels.length > 0) { - this.parcelId.setValidators([Validators.required]); - this.parcelId.updateValueAndValidity(); - this.source.setValue(DOCUMENT_SOURCE.APPLICANT); + this.source.setValue(DOCUMENT_SOURCE.APPLICANT); + this.parcelId.setValidators([Validators.required]); + this.parcelId.updateValueAndValidity(); - const selectedParcel = this.data.selectableParcels.find((parcel) => parcel.certificateOfTitleUuid === uuid); - if (selectedParcel) { - this.parcelId.setValue(selectedParcel.uuid); - } else if (uuid) { - this.showSupersededWarning = true; - } + if (!this.data.parcelService) { + return; + } + + this.selectableParcels = await this.data.parcelService.fetchParcels(this.data.fileId); + + if (this.selectableParcels.length < 1) { + return; + } + + const selectedParcel = this.selectableParcels.find((parcel) => parcel.certificateOfTitleUuid === uuid); + if (selectedParcel) { + this.parcelId.setValue(selectedParcel.uuid); + } else if (uuid) { + this.showSupersededWarning = true; } } async prepareCorporateSummaryUpload(uuid?: string) { - if (this.data.selectableOwners && this.data.selectableOwners.length > 0) { - this.ownerId.setValidators([Validators.required]); - this.ownerId.updateValueAndValidity(); - this.source.setValue(DOCUMENT_SOURCE.APPLICANT); + this.source.setValue(DOCUMENT_SOURCE.APPLICANT); + this.ownerId.setValidators([Validators.required]); + this.ownerId.updateValueAndValidity(); - const selectedOwner = this.data.selectableOwners.find((owner) => owner.corporateSummaryUuid === uuid); - if (selectedOwner) { - this.ownerId.setValue(selectedOwner.uuid); - } else if (uuid) { - this.showSupersededWarning = true; - } + if (!this.data.submissionService) { + return; + } + + const submission = await this.data.submissionService.fetchSubmission(this.data.fileId); + this.selectableOwners = submission.owners + .filter((owner) => owner.type.code === 'ORGZ') + .map((owner) => ({ + ...owner, + label: owner.organizationName ?? owner.displayName, + })); + + if (this.selectableOwners.length < 1) { + return; + } + + const selectedOwner = this.selectableOwners.find((owner) => owner.corporateSummaryUuid === uuid); + if (selectedOwner) { + this.ownerId.setValue(selectedOwner.uuid); + } else if (uuid) { + this.showSupersededWarning = true; } } @@ -302,7 +310,7 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { } if (this.data.documentTypeOverrides && this.data.documentTypeOverrides[$event.code]) { - for (const visibilityGroup of this.data.documentTypeOverrides[$event.code].visibilityGroups) { + for (const visibilityGroup of this.data.documentTypeOverrides[$event.code]?.visibilityGroups ?? []) { if (visibilityGroup === VisibilityGroup.INTERNAL) { this.visibleToInternal.setValue(true); } diff --git a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.dto.ts b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.dto.ts index 73bb4085f..3f731540c 100644 --- a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.dto.ts +++ b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.dto.ts @@ -1,4 +1,5 @@ import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM, DOCUMENT_TYPE, DocumentTypeDto } from '../../shared/document/document.dto'; +import { BaseCodeDto } from '../dto/base.dto'; export interface UpdateDocumentDto { file?: File; @@ -32,13 +33,24 @@ export interface DocumentDto { export interface SelectableParcelDto { uuid: string; - pid: string; - certificateOfTitleUuid: string; - index: string; + pid?: string; + certificateOfTitleUuid?: string; +} + +export interface OwnerDto { + uuid: string; + displayName: string; + organizationName?: string | null; + corporateSummaryUuid?: string; + type: BaseCodeDto; } export interface SelectableOwnerDto { - label: string; uuid: string; - corporateSummaryUuid: string; + corporateSummaryUuid?: string; + label: string; +} + +export interface SubmissionOwnersDto { + owners: OwnerDto[]; } diff --git a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.interface.ts b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.interface.ts index 34e7f4dbc..70e249a65 100644 --- a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.interface.ts +++ b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.interface.ts @@ -1,5 +1,33 @@ -import { DocumentTypeDto } from '../document/document.dto'; -import { CreateDocumentDto, UpdateDocumentDto } from './document-upload-dialog.dto'; +import { DOCUMENT_TYPE, DocumentTypeDto } from '../document/document.dto'; +import { VisibilityGroup } from './document-upload-dialog.component'; +import { + CreateDocumentDto, + DocumentDto, + SelectableParcelDto, + SubmissionOwnersDto, + UpdateDocumentDto, +} from './document-upload-dialog.dto'; + +export interface DocumentTypeConfig { + visibilityGroups: VisibilityGroup[]; + allowsFileEdit: boolean; +} + +export interface DocumentUploadDialogOptions { + allowedVisibilityFlags?: ('A' | 'C' | 'G' | 'P')[]; + allowsFileEdit?: boolean; + documentTypeOverrides?: Partial>; +} + +export interface DocumentUploadDialogData extends DocumentUploadDialogOptions { + fileId: string; + decisionUuid?: string; + existingDocument?: DocumentDto; + decisionService?: DecisionService; + documentService?: DocumentService; + parcelService?: ParcelFetchingService; + submissionService?: SubmissionFetchingService; +} export interface DecisionService { uploadFile(decisionUuid: string, file: File): Promise; @@ -15,3 +43,11 @@ export interface DocumentService { fetchTypes(): Promise; delete(uuid: string): Promise; } + +export interface ParcelFetchingService { + fetchParcels(fileNumber: string): Promise; +} + +export interface SubmissionFetchingService { + fetchSubmission(fileNumber: string): Promise; +} From ca5d7c0deb530755ab85fa7183d5c90154dddaea Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Wed, 26 Feb 2025 16:14:23 -0800 Subject: [PATCH 49/87] ALCS-2200 Added UI fields --- .../app/features/search/search.component.html | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/alcs-frontend/src/app/features/search/search.component.html b/alcs-frontend/src/app/features/search/search.component.html index 935d1130a..b87cca91f 100644 --- a/alcs-frontend/src/app/features/search/search.component.html +++ b/alcs-frontend/src/app/features/search/search.component.html @@ -81,6 +81,34 @@

File Details

+
+
+ +
+ +
+ + +
{{ item.label }}
+
+
+
+
Date: Wed, 26 Feb 2025 16:31:48 -0800 Subject: [PATCH 50/87] ALCS-2029 QA1 --- ...ecision-condition-order-dialog.component.ts | 18 ++++++++++-------- .../decision-conditions.component.ts | 16 ++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts index 4d498229e..672f5e40f 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts @@ -17,6 +17,7 @@ export class DecisionConditionOrderDialogComponent implements OnInit { selectedRecord: string | undefined; overlayRef: OverlayRef | null = null; dataSource = new MatTableDataSource([]); + conditionsToOrder: ApplicationDecisionConditionDto[] = []; @ViewChild('orderMenu') orderMenu!: TemplateRef; @@ -38,7 +39,8 @@ export class DecisionConditionOrderDialogComponent implements OnInit { index++; }); } - this.dataSource.data = this.data.conditions.sort((a,b) => a.order - b.order); + this.conditionsToOrder = structuredClone(this.data.conditions.sort((a,b) => a.order - b.order)); + this.dataSource.data = this.conditionsToOrder; } async onRowDropped(event: CdkDragDrop) { @@ -74,14 +76,14 @@ export class DecisionConditionOrderDialogComponent implements OnInit { } sendToBottom(record: ApplicationDecisionConditionDto) { - const currentIndex = this.data.conditions.findIndex((item) => item.uuid === record.uuid); - this.moveItem(currentIndex, this.data.conditions.length - 1); + const currentIndex = this.conditionsToOrder.findIndex((item) => item.uuid === record.uuid); + this.moveItem(currentIndex, this.conditionsToOrder.length - 1); this.overlayRef?.detach(); this.selectedRecord = undefined; } sendToTop(record: ApplicationDecisionConditionDto) { - const currentIndex = this.data.conditions.findIndex((item) => item.uuid === record.uuid); + const currentIndex = this.conditionsToOrder.findIndex((item) => item.uuid === record.uuid); this.moveItem(currentIndex, 0); this.overlayRef?.detach(); this.selectedRecord = undefined; @@ -93,9 +95,9 @@ export class DecisionConditionOrderDialogComponent implements OnInit { } private moveItem(currentIndex: number, targetIndex: number) { - this.data.conditions[currentIndex].order = targetIndex; - this.data.conditions[targetIndex].order = currentIndex; - this.dataSource.data = this.data.conditions.sort((a,b) => a.order - b.order); + this.conditionsToOrder[currentIndex].order = targetIndex; + this.conditionsToOrder[targetIndex].order = currentIndex; + this.dataSource.data = this.conditionsToOrder.sort((a,b) => a.order - b.order); } onCancel(): void { @@ -107,7 +109,7 @@ export class DecisionConditionOrderDialogComponent implements OnInit { uuid: cond.uuid, order: cond.order, })); - this.dialogRef.close(order); + this.dialogRef.close({ payload: order, data: this.conditionsToOrder }); } alphaIndex(index: number) { diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts index ad2187ddb..7020a374f 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts @@ -102,16 +102,11 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy } onAddNewCondition(typeCode: string) { - this.mappedConditions.forEach((c) => { - if (c.order) { - c.order++; - } - }); const matchingType = this.activeTypes.find((type) => type.code === typeCode); - this.mappedConditions.unshift({ + this.mappedConditions.push({ type: matchingType, tempUuid: (Math.random() * 10000).toFixed(0), - order: 0, + order: this.mappedConditions.length, }); this.conditionsChange.emit({ conditions: this.mappedConditions, @@ -237,9 +232,10 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy }, }) .beforeClosed() - .subscribe(async (data) => { - if (data) { - this.conditionService.updateSort(data); + .subscribe(async (result) => { + if (result) { + this.conditionService.updateSort(result.payload); + this.mappedConditions = result.data; this.conditionsChange.emit({ conditions: this.mappedConditions, isValid: this.conditionComponents.reduce((isValid, component) => isValid && component.form.valid, true), From cd84d809952b2556dda619517fceba4d9f808be6 Mon Sep 17 00:00:00 2001 From: Tristan Slater <1631008+trslater@users.noreply.github.com> Date: Wed, 26 Feb 2025 16:00:55 -0800 Subject: [PATCH 51/87] Don't overwrite options object --- .../documents/documents.component.ts | 24 +++++++++++++++---- .../documents/documents.component.ts | 24 +++++++++++++++---- .../documents/documents.component.ts | 18 ++++++++++++++ .../documents/documents.component.ts | 18 ++++++++++++++ 4 files changed, 76 insertions(+), 8 deletions(-) diff --git a/alcs-frontend/src/app/features/application/documents/documents.component.ts b/alcs-frontend/src/app/features/application/documents/documents.component.ts index deb7d5e2f..3da938a5b 100644 --- a/alcs-frontend/src/app/features/application/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/application/documents/documents.component.ts @@ -63,8 +63,15 @@ export class DocumentsComponent implements OnInit { } async onUploadFile() { - const submission = await this.applicationSubmissionService.fetchSubmission(this.fileId); - const parcels = await this.applicationParcelService.fetchParcels(this.fileId); + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + fileId: this.fileId, + documentService: this.applicationDocumentService, + parcelService: this.applicationParcelService, + submissionService: this.applicationSubmissionService, + }, + }; this.dialog .open(DocumentUploadDialogComponent, { @@ -126,8 +133,17 @@ export class DocumentsComponent implements OnInit { } async onEditFile(element: ApplicationDocumentDto) { - const submission = await this.applicationSubmissionService.fetchSubmission(this.fileId); - const parcels = await this.applicationParcelService.fetchParcels(this.fileId); + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, + fileId: this.fileId, + existingDocument: element, + documentService: this.applicationDocumentService, + parcelService: this.applicationParcelService, + submissionService: this.applicationSubmissionService, + }, + }; this.dialog .open(DocumentUploadDialogComponent, { diff --git a/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts b/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts index 60fc85646..13520875b 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts @@ -67,8 +67,15 @@ export class NoiDocumentsComponent implements OnInit { } async onUploadFile() { - const submission = await this.noiSubmissionService.fetchSubmission(this.fileId); - const parcels = await this.noiParcelService.fetchParcels(this.fileId); + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + fileId: this.fileId, + documentService: this.noiDocumentService, + parcelService: this.noiParcelService, + submissionService: this.noiSubmissionService, + }, + }; this.dialog .open(DocumentUploadDialogComponent, { @@ -116,8 +123,17 @@ export class NoiDocumentsComponent implements OnInit { } async onEditFile(element: NoticeOfIntentDocumentDto) { - const submission = await this.noiSubmissionService.fetchSubmission(this.fileId); - const parcels = await this.noiParcelService.fetchParcels(this.fileId); + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, + fileId: this.fileId, + existingDocument: element, + documentService: this.noiDocumentService, + parcelService: this.noiParcelService, + submissionService: this.noiSubmissionService, + }, + }; this.dialog .open(DocumentUploadDialogComponent, { diff --git a/alcs-frontend/src/app/features/notification/documents/documents.component.ts b/alcs-frontend/src/app/features/notification/documents/documents.component.ts index 304785905..bdb9b3212 100644 --- a/alcs-frontend/src/app/features/notification/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/notification/documents/documents.component.ts @@ -48,6 +48,14 @@ export class NotificationDocumentsComponent implements OnInit { } async onUploadFile() { + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + fileId: this.fileId, + documentService: this.notificationDocumentService, + }, + }; + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', @@ -91,6 +99,16 @@ export class NotificationDocumentsComponent implements OnInit { } onEditFile(element: NoticeOfIntentDocumentDto) { + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, + fileId: this.fileId, + existingDocument: element, + documentService: this.notificationDocumentService, + }, + }; + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', diff --git a/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts b/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts index f666bed56..b910125d6 100644 --- a/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts @@ -48,6 +48,14 @@ export class DocumentsComponent implements OnInit { } async onUploadFile() { + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + fileId: this.fileId, + documentService: this.planningReviewDocumentService, + }, + }; + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', @@ -91,6 +99,16 @@ export class DocumentsComponent implements OnInit { } onEditFile(element: PlanningReviewDocumentDto) { + const data: DocumentUploadDialogData = { + ...DOCUMENT_UPLOAD_DIALOG_OPTIONS, + ...{ + allowsFileEdit: element.system === DOCUMENT_SYSTEM.ALCS, + fileId: this.fileId, + existingDocument: element, + documentService: this.planningReviewDocumentService, + }, + }; + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', From dbe9a0d762791bc9ef199e3b8c0df88875178c48 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Wed, 26 Feb 2025 16:45:47 -0800 Subject: [PATCH 52/87] ALCS-2029 Add structured clone to test --- .../decision-condition-order-dialog.component.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts index 3e851f6b8..417f5fd41 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts @@ -11,6 +11,7 @@ import { DecisionConditionOrderDialogComponent } from './decision-condition-orde describe('DecisionConditionOrderDialogComponent', () => { let component: DecisionConditionOrderDialogComponent; let fixture: ComponentFixture; + let structuredClone: Function; beforeEach(async () => { @@ -33,6 +34,9 @@ describe('DecisionConditionOrderDialogComponent', () => { fixture = TestBed.createComponent(DecisionConditionOrderDialogComponent); component = fixture.componentInstance; + structuredClone = jest.fn(val => { + return JSON.parse(JSON.stringify(val)); + }); fixture.detectChanges(); }); From 690111157fcf362d2d30fa1bf81536ee73d9d3ef Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Wed, 26 Feb 2025 16:56:16 -0800 Subject: [PATCH 53/87] ALCS-2029 Remove structredclone function --- .../decision-condition-order-dialog.component.spec.ts | 4 ---- .../decision-condition-order-dialog.component.ts | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts index 417f5fd41..3e851f6b8 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.spec.ts @@ -11,7 +11,6 @@ import { DecisionConditionOrderDialogComponent } from './decision-condition-orde describe('DecisionConditionOrderDialogComponent', () => { let component: DecisionConditionOrderDialogComponent; let fixture: ComponentFixture; - let structuredClone: Function; beforeEach(async () => { @@ -34,9 +33,6 @@ describe('DecisionConditionOrderDialogComponent', () => { fixture = TestBed.createComponent(DecisionConditionOrderDialogComponent); component = fixture.componentInstance; - structuredClone = jest.fn(val => { - return JSON.parse(JSON.stringify(val)); - }); fixture.detectChanges(); }); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts index 672f5e40f..f6107f9fe 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts @@ -39,7 +39,7 @@ export class DecisionConditionOrderDialogComponent implements OnInit { index++; }); } - this.conditionsToOrder = structuredClone(this.data.conditions.sort((a,b) => a.order - b.order)); + this.conditionsToOrder = this.data.conditions.sort((a,b) => a.order - b.order).map(a => {return {...a}}); this.dataSource.data = this.conditionsToOrder; } From 20d81f61b426a51f751948704b15ccd564c38770 Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Thu, 27 Feb 2025 13:24:45 -0800 Subject: [PATCH 54/87] Redirect to decision tab for modification and reconsideration cards Redirect to decision tab by clicking on "VIEW DETAIL" button for: - Application modification and reconsideration cards - NOI modification cards --- .../app-modification/app-modification-dialog.component.html | 2 +- .../noi-modification/noi-modification-dialog.component.html | 2 +- .../reconsiderations/reconsideration-dialog.component.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.html index c9fe6bef8..fa71f056f 100644 --- a/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.html @@ -20,7 +20,7 @@

color="accent" mat-flat-button [mat-dialog-close]="isDirty" - [routerLink]="['application', modification.application.fileNumber]" + [routerLink]="['application', modification.application.fileNumber, 'decision']" > View Detail diff --git a/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.html index 5767c928f..b5901cf82 100644 --- a/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.html @@ -23,7 +23,7 @@

color="accent" mat-flat-button [mat-dialog-close]="isDirty" - [routerLink]="['notice-of-intent', modification.noticeOfIntent.fileNumber]" + [routerLink]="['notice-of-intent', modification.noticeOfIntent.fileNumber, 'decision']" > View Detail diff --git a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.html index 01047723a..afa6bfb21 100644 --- a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.html @@ -20,7 +20,7 @@

color="accent" mat-flat-button [mat-dialog-close]="isDirty" - [routerLink]="['application', recon.application.fileNumber]" + [routerLink]="['application', recon.application.fileNumber, 'decision']" > View Detail From 8c04af6e71e40a36eec51f663269cc861a317f08 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Thu, 27 Feb 2025 14:59:58 -0800 Subject: [PATCH 55/87] ALCS-2200 Frontend implementation --- .../file-type-filter-drop-down.component.ts | 3 +- .../app/features/search/search.component.html | 13 ++- .../app/features/search/search.component.ts | 33 ++++++- .../application/application-code.dto.ts | 2 + .../application/application.service.ts | 6 ++ ...cision-outcome-data-source.service.spec.ts | 48 +++++++++++ .../decision-outcome-data-source.service.ts | 86 +++++++++++++++++++ .../src/app/services/search/search.dto.ts | 2 + .../application-code/application-code.dto.ts | 2 + .../alcs/src/alcs/code/code.controller.ts | 19 ++-- 10 files changed, 194 insertions(+), 20 deletions(-) create mode 100644 alcs-frontend/src/app/services/search/decision-outcome/decision-outcome-data-source.service.spec.ts create mode 100644 alcs-frontend/src/app/services/search/decision-outcome/decision-outcome-data-source.service.ts diff --git a/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.ts b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.ts index dd015bc2c..4a033e792 100644 --- a/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.ts +++ b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.ts @@ -9,6 +9,7 @@ import { TreeNode, } from '../../../services/search/file-type/file-type-data-source.service'; import { PortalStatusDataSourceService } from '../../../services/search/portal-status/portal-status-data-source.service'; +import { DecisionOutcomeDataSourceService } from '../../../services/search/decision-outcome/decision-outcome-data-source.service'; @Component({ selector: 'app-file-type-filter-drop-down[fileTypeData]', @@ -35,7 +36,7 @@ export class FileTypeFilterDropDownComponent implements OnInit { componentTypeControl = new FormControl(undefined); @Output() fileTypeChange = new EventEmitter(); - @Input() fileTypeData!: FileTypeDataSourceService | PortalStatusDataSourceService; + @Input() fileTypeData!: FileTypeDataSourceService | PortalStatusDataSourceService | DecisionOutcomeDataSourceService; @Input() label!: string; @Input() tooltip = ''; @Input() preExpanded: string[] = []; diff --git a/alcs-frontend/src/app/features/search/search.component.html b/alcs-frontend/src/app/features/search/search.component.html index b87cca91f..ea275bbf4 100644 --- a/alcs-frontend/src/app/features/search/search.component.html +++ b/alcs-frontend/src/app/features/search/search.component.html @@ -85,23 +85,22 @@

File Details

{{ item.label }}
diff --git a/alcs-frontend/src/app/features/search/search.component.ts b/alcs-frontend/src/app/features/search/search.component.ts index c620cda8b..e159c5714 100644 --- a/alcs-frontend/src/app/features/search/search.component.ts +++ b/alcs-frontend/src/app/features/search/search.component.ts @@ -17,6 +17,7 @@ import { NoticeOfIntentSubmissionStatusService } from '../../services/notice-of- import { NotificationSubmissionStatusService } from '../../services/notification/notification-submission-status/notification-submission-status.service'; import { NotificationSubmissionStatusDto } from '../../services/notification/notification.dto'; import { FileTypeDataSourceService } from '../../services/search/file-type/file-type-data-source.service'; +import { DecisionOutcomeDataSourceService } from '../../services/search/decision-outcome/decision-outcome-data-source.service'; import { PortalStatusDataSourceService } from '../../services/search/portal-status/portal-status-data-source.service'; import { AdvancedSearchResponseDto, @@ -39,6 +40,7 @@ import { TagDto } from '../../services/tag/tag.dto'; import { TagService } from '../../services/tag/tag.service'; import { MatAutocompleteSelectedEvent, MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { DecisionMakerDto } from '../../services/application/decision/application-decision-v2/application-decision-v2.dto'; export const defaultStatusBackgroundColour = '#ffffff'; export const defaultStatusColour = '#313132'; @@ -96,6 +98,8 @@ export class SearchComponent implements OnInit, OnDestroy { tagControl = new FormControl(); localGovernmentControl = new FormControl(undefined); portalStatusControl = new FormControl([]); + decisionOutcomeControl = new FormControl([]); + decisionMakerControl = new FormControl(undefined); componentTypeControl = new FormControl(undefined); pidControl = new FormControl(undefined); nameControl = new FormControl(undefined); @@ -109,6 +113,8 @@ export class SearchComponent implements OnInit, OnDestroy { resolutionYear: new FormControl(undefined), legacyId: new FormControl(undefined), portalStatus: this.portalStatusControl, + decisionOutcome: this.decisionOutcomeControl, + decisionMaker: this.decisionMakerControl, componentType: this.componentTypeControl, government: this.localGovernmentControl, region: new FormControl(undefined), @@ -127,6 +133,7 @@ export class SearchComponent implements OnInit, OnDestroy { allTags: TagDto[] = []; tagCategories: TagCategoryDto[] = []; applicationStatuses: ApplicationStatusDto[] = []; + decisionMakers: DecisionMakerDto[] = []; allStatuses: (ApplicationStatusDto | NoticeOfIntentStatusDto | NotificationSubmissionStatusDto)[] = []; formEmpty = true; @@ -151,6 +158,7 @@ export class SearchComponent implements OnInit, OnDestroy { private authService: AuthenticationService, public fileTypeService: FileTypeDataSourceService, public portalStatusDataService: PortalStatusDataSourceService, + public decisionOutcomeDataService: DecisionOutcomeDataSourceService, public tagCategoryService: TagCategoryService, public tagService: TagService, ) { @@ -164,10 +172,11 @@ export class SearchComponent implements OnInit, OnDestroy { this.applicationService.$applicationRegions .pipe(takeUntil(this.$destroy)) - .pipe(combineLatestWith(this.applicationService.$applicationStatuses, this.activatedRoute.queryParamMap)) - .subscribe(([regions, statuses, queryParamMap]) => { + .pipe(combineLatestWith(this.applicationService.$applicationStatuses, this.applicationService.$decisionMakers, this.activatedRoute.queryParamMap)) + .subscribe(([regions, statuses, decisionMakers, queryParamMap]) => { this.regions = regions; this.applicationStatuses = statuses; + this.decisionMakers = decisionMakers; this.populateAllStatuses(); const searchText = queryParamMap.get('searchText'); @@ -301,6 +310,8 @@ export class SearchComponent implements OnInit, OnDestroy { const resolutionNumberString = this.formatStringSearchParam(this.searchForm.controls.resolutionNumber.value); let fileTypes: string[]; let portalStatusCodes; + let decisionMakers; + let decisionOutcomes: string[]; if (this.searchForm.controls.componentType.value === null) { fileTypes = this.isCommissioner ? this.fileTypeService.getCommissionerListData() : []; @@ -308,6 +319,18 @@ export class SearchComponent implements OnInit, OnDestroy { fileTypes = this.searchForm.controls.componentType.value!; } + if (this.searchForm.controls.decisionOutcome.value === null) { + decisionOutcomes = []; + } else { + decisionOutcomes = this.searchForm.controls.decisionOutcome.value!; + } + + if (this.searchForm.controls.decisionOutcome.value === null) { + decisionMakers = []; + } else { + decisionMakers = this.searchForm.controls.decisionMaker.value!; + } + if (this.searchForm.controls.portalStatus.value?.length === 0) { portalStatusCodes = this.isCommissioner ? this.portalStatusDataService.getCommissionerListData() : []; } else { @@ -347,6 +370,8 @@ export class SearchComponent implements OnInit, OnDestroy { fileTypes: fileTypes, tagCategoryId: this.searchForm.controls.tagCategory.value ?? undefined, tagIds: this.tags.map((t) => t.uuid), + decisionOutcomes: decisionOutcomes, + decisionMaker: this.searchForm.controls.decisionMaker.value ?? undefined, }; } @@ -444,6 +469,10 @@ export class SearchComponent implements OnInit, OnDestroy { this.portalStatusControl.setValue(statusCodes); } + onDecisionOutcomeChange(decisionOutcomes: string[]) { + this.decisionOutcomeControl.setValue(decisionOutcomes); + } + onClick(): void { this.clicked = true; if (!this.firstClicked) { diff --git a/alcs-frontend/src/app/services/application/application-code.dto.ts b/alcs-frontend/src/app/services/application/application-code.dto.ts index 25d1e734a..94e2a338d 100644 --- a/alcs-frontend/src/app/services/application/application-code.dto.ts +++ b/alcs-frontend/src/app/services/application/application-code.dto.ts @@ -1,6 +1,7 @@ import { BaseCodeDto } from '../../shared/dto/base.dto'; import { ReconsiderationTypeDto } from './application-reconsideration/application-reconsideration.dto'; import { ApplicationStatusDto } from './application-submission-status/application-submission-status.dto'; +import { DecisionMakerDto } from './decision/application-decision-v2/application-decision.dto'; export interface CardStatusDto extends BaseCodeDto {} export interface ApplicationRegionDto extends BaseCodeDto {} @@ -18,4 +19,5 @@ export interface ApplicationMasterCodesDto { region: ApplicationRegionDto[]; reconsiderationType: ReconsiderationTypeDto[]; applicationStatusType: ApplicationStatusDto[]; + decisionMaker: DecisionMakerDto[]; } diff --git a/alcs-frontend/src/app/services/application/application.service.ts b/alcs-frontend/src/app/services/application/application.service.ts index 8b927b0ba..f0f0623da 100644 --- a/alcs-frontend/src/app/services/application/application.service.ts +++ b/alcs-frontend/src/app/services/application/application.service.ts @@ -11,6 +11,7 @@ import { } from './application-code.dto'; import { ApplicationStatusDto } from './application-submission-status/application-submission-status.dto'; import { ApplicationDto, CreateApplicationDto, UpdateApplicationDto } from './application.dto'; +import { DecisionMakerDto } from './decision/application-decision-v2/application-decision.dto'; @Injectable({ providedIn: 'root', @@ -20,11 +21,13 @@ export class ApplicationService { $applicationTypes = new BehaviorSubject([]); $applicationRegions = new BehaviorSubject([]); $applicationStatuses = new BehaviorSubject([]); + $decisionMakers = new BehaviorSubject([]); private baseUrl = `${environment.apiUrl}/application`; private statuses: CardStatusDto[] = []; private types: ApplicationTypeDto[] = []; private regions: ApplicationRegionDto[] = []; private applicationStatuses: ApplicationStatusDto[] = []; + private decisionMakers: DecisionMakerDto[] = []; private isInitialized = false; constructor( @@ -119,5 +122,8 @@ export class ApplicationService { this.applicationStatuses = codes.applicationStatusType; this.$applicationStatuses.next(this.applicationStatuses); + + this.decisionMakers = codes.decisionMaker; + this.$decisionMakers.next(this.decisionMakers); } } diff --git a/alcs-frontend/src/app/services/search/decision-outcome/decision-outcome-data-source.service.spec.ts b/alcs-frontend/src/app/services/search/decision-outcome/decision-outcome-data-source.service.spec.ts new file mode 100644 index 000000000..e0e26bab5 --- /dev/null +++ b/alcs-frontend/src/app/services/search/decision-outcome/decision-outcome-data-source.service.spec.ts @@ -0,0 +1,48 @@ +import { TestBed } from '@angular/core/testing'; +import { AuthenticationService, ICurrentUser } from '../../authentication/authentication.service'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { DecisionOutcomeDataSourceService, TreeNode } from './decision-outcome-data-source.service'; + +describe('DecisionOutcomeDataSourceService', () => { + let service: DecisionOutcomeDataSourceService; + let mockAuthenticationService: DeepMocked; + let currentUser: BehaviorSubject; + + beforeEach(() => { + mockAuthenticationService = createMock(); + currentUser = new BehaviorSubject(undefined); + TestBed.configureTestingModule({ + providers: [ + { + provide: AuthenticationService, + useValue: mockAuthenticationService, + }, + ], + }); + mockAuthenticationService.$currentUser = currentUser; + service = TestBed.inject(DecisionOutcomeDataSourceService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should load initial data', () => { + expect(service.data).toBeTruthy(); + expect(service.data.length).toBeGreaterThan(0); + }); + + it('should filter data', () => { + service.filter('Approved'); + expect(service.data.length).toBe(1); + + const node: TreeNode = service.data[0]; + expect(node.item.label).toEqual('Approved'); + }); + + it('should reset data when filtering with empty text', () => { + service.filter(''); + expect(service.data.length).toBeGreaterThan(0); + }); +}); diff --git a/alcs-frontend/src/app/services/search/decision-outcome/decision-outcome-data-source.service.ts b/alcs-frontend/src/app/services/search/decision-outcome/decision-outcome-data-source.service.ts new file mode 100644 index 000000000..195cfc0c5 --- /dev/null +++ b/alcs-frontend/src/app/services/search/decision-outcome/decision-outcome-data-source.service.ts @@ -0,0 +1,86 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { AuthenticationService, ROLES } from '../../authentication/authentication.service'; + +export interface TreeNodeItem { + label: string; + value: string | null; +} +/** + * Node for to-do item + */ +export interface TreeNode { + children?: TreeNode[]; + item: TreeNodeItem; +} + +/** Flat to-do item node with expandable and level information */ +export interface FlatTreeNode { + item: TreeNodeItem; + level: number; + expandable: boolean; +} + +const TREE_DATA: TreeNode[] = [ + { + item: { label: 'Approved', value: 'APPR' }, + }, + { + item: { label: 'Refused', value: 'REFU' }, + }, + { + item: { label: 'Ordered not to Proceed (NOI)', value: 'ONTP' }, + }, +]; + +@Injectable({ providedIn: 'root' }) +export class DecisionOutcomeDataSourceService { + dataChange = new BehaviorSubject([]); + treeData: TreeNode[] = []; + isCommissioner = false; + + get data(): TreeNode[] { + return this.dataChange.value; + } + + constructor() { + this.initialize(); + } + + initialize() { + this.treeData = TREE_DATA; + this.isCommissioner ? this.dataChange.next(TREE_DATA) : this.dataChange.next(TREE_DATA); + } + + public filter(filterText: string) { + let filteredTreeData; + if (filterText) { + // Filter the tree + const filter = (array: TreeNode[], text: string) => { + const getChildren = (result: any, object: any) => { + if (object.item.label.toLowerCase().includes(text.toLowerCase())) { + result.push(object); + return result; + } + if (Array.isArray(object.children)) { + const children = object.children.reduce(getChildren, []); + if (children.length) result.push({ ...object, children }); + } + return result; + }; + + return array.reduce(getChildren, []); + }; + + filteredTreeData = filter(this.treeData, filterText); + } else { + // Return the initial tree + filteredTreeData = this.treeData; + } + this.dataChange.next(filteredTreeData); + } + + public getCommissionerListData() { + return this.treeData; + } +} diff --git a/alcs-frontend/src/app/services/search/search.dto.ts b/alcs-frontend/src/app/services/search/search.dto.ts index a99e3fe84..8c0218544 100644 --- a/alcs-frontend/src/app/services/search/search.dto.ts +++ b/alcs-frontend/src/app/services/search/search.dto.ts @@ -80,6 +80,8 @@ export interface SearchRequestDto extends PagingRequestDto { dateDecidedFrom?: number; dateDecidedTo?: number; fileTypes: string[]; + decisionMaker?: string; + decisionOutcomes: string[]; tagIds?: string[]; tagCategoryId?: string; } diff --git a/services/apps/alcs/src/alcs/code/application-code/application-code.dto.ts b/services/apps/alcs/src/alcs/code/application-code/application-code.dto.ts index 8d9e01502..cec63d0c9 100644 --- a/services/apps/alcs/src/alcs/code/application-code/application-code.dto.ts +++ b/services/apps/alcs/src/alcs/code/application-code/application-code.dto.ts @@ -3,6 +3,7 @@ import { ReconsiderationTypeDto } from '../../application-decision/application-r import { CardStatusDto } from '../../card/card-status/card-status.dto'; import { ApplicationRegionDto } from './application-region/application-region.dto'; import { ApplicationTypeDto } from './application-type/application-type.dto'; +import { ApplicationDecisionMakerCodeDto } from '../../application-decision/application-decision-maker/decision-maker.dto'; export class MasterCodesDto { type: ApplicationTypeDto[]; @@ -10,4 +11,5 @@ export class MasterCodesDto { region: ApplicationRegionDto[]; reconsiderationType: ReconsiderationTypeDto[]; applicationStatusType: ApplicationStatusDto[]; + decisionMaker: ApplicationDecisionMakerCodeDto[]; } diff --git a/services/apps/alcs/src/alcs/code/code.controller.ts b/services/apps/alcs/src/alcs/code/code.controller.ts index 01b68675b..36b7f220e 100644 --- a/services/apps/alcs/src/alcs/code/code.controller.ts +++ b/services/apps/alcs/src/alcs/code/code.controller.ts @@ -18,6 +18,8 @@ import { ApplicationRegion } from './application-code/application-region/applica import { ApplicationTypeDto } from './application-code/application-type/application-type.dto'; import { ApplicationType } from './application-code/application-type/application-type.entity'; import { CodeService } from './code.service'; +import { ApplicationDecisionMakerCode } from '../application-decision/application-decision-maker/application-decision-maker.entity'; +import { ApplicationDecisionMakerCodeDto } from '../application-decision/application-decision-maker/decision-maker.dto'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('code') @@ -35,16 +37,8 @@ export class CodeController { return { status: this.mapper.mapArray(types.status, CardStatus, CardStatusDto), - type: this.mapper.mapArray( - types.type, - ApplicationType, - ApplicationTypeDto, - ), - region: this.mapper.mapArray( - types.region, - ApplicationRegion, - ApplicationRegionDto, - ), + type: this.mapper.mapArray(types.type, ApplicationType, ApplicationTypeDto), + region: this.mapper.mapArray(types.region, ApplicationRegion, ApplicationRegionDto), reconsiderationType: this.mapper.mapArray( types.reconsiderationTypes, ApplicationReconsiderationType, @@ -55,6 +49,11 @@ export class CodeController { ApplicationSubmissionStatusType, ApplicationStatusDto, ), + decisionMaker: this.mapper.mapArray( + types.decisionMakers, + ApplicationDecisionMakerCode, + ApplicationDecisionMakerCodeDto, + ), }; } } From 94d040d8d9a146fd50298edeb7364e243fd92d54 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Thu, 27 Feb 2025 15:10:48 -0800 Subject: [PATCH 56/87] ALCS-2200 Update Search DTO --- services/apps/alcs/src/alcs/search/search.dto.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/apps/alcs/src/alcs/search/search.dto.ts b/services/apps/alcs/src/alcs/search/search.dto.ts index 401dcf8ac..ae145f316 100644 --- a/services/apps/alcs/src/alcs/search/search.dto.ts +++ b/services/apps/alcs/src/alcs/search/search.dto.ts @@ -181,4 +181,11 @@ export class SearchRequestDto extends PagingRequestDto { @IsString() @IsOptional() tagCategoryId?: string; + + @IsArray() + decisionOutcomes: string[]; + + @IsString() + @IsOptional() + decisionMaker?: string; } From cdf8aa318238b51c13320cd956c14e9578c37780 Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Thu, 27 Feb 2025 15:53:10 -0800 Subject: [PATCH 57/87] Remove hover and interaction behaviour from tags in commissioner view --- .../commissioner-tags-header.component.html | 7 +------ .../commissioner-tags-header.component.scss | 8 -------- .../src/app/shared/tags/tag-chip/tag-chip.component.html | 6 +++--- .../src/app/shared/tags/tag-chip/tag-chip.component.scss | 2 +- .../src/app/shared/tags/tag-chip/tag-chip.component.ts | 5 +++++ 5 files changed, 10 insertions(+), 18 deletions(-) diff --git a/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.html b/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.html index 7502e487f..bec2a62f1 100644 --- a/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.html +++ b/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.html @@ -1,8 +1,3 @@ -
+
diff --git a/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.scss b/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.scss index 72d84b961..25dae12f3 100644 --- a/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.scss +++ b/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.scss @@ -5,14 +5,6 @@ border: 1px solid transparent; border-radius: 4px; padding: 5px; - - &.hovered { - border: 1px solid #aaaaaa; - } - - &.clicked { - border: 1px solid #929292; - } } .category { diff --git a/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.html b/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.html index 78a441da7..05691ae9c 100644 --- a/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.html +++ b/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.html @@ -1,6 +1,6 @@ -{{ tag.name }} + + {{ tag.name }} - + diff --git a/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.scss b/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.scss index 8caca14e5..58743e791 100644 --- a/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.scss +++ b/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.scss @@ -1,4 +1,4 @@ -mat-chip-row { +mat-chip { border-radius: 4px; border-color: #929292 !important; border: 1px solid; diff --git a/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.ts b/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.ts index 6b4ef6f68..d570762d6 100644 --- a/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.ts +++ b/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.ts @@ -14,4 +14,9 @@ export class TagChipComponent { onRemove() { this.removeClicked.emit(this.tag); } + + handleClick(event: MouseEvent) { + event.preventDefault(); + event.stopPropagation(); + } } From 85fff581fb88dc5751328b37f70699cd0a72fcdc Mon Sep 17 00:00:00 2001 From: Dylan Rogowsky Date: Thu, 27 Feb 2025 16:30:13 -0700 Subject: [PATCH 58/87] ALCS-2476: Reimplement concurrency groups --- .github/workflows/cd.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 4c1298075..4d70599e1 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -12,9 +12,15 @@ jobs: with: environment: test secrets: inherit + concurrency: + group: deploy-test + cancel-in-progress: true deploy-prod: needs: deploy-test uses: ./.github/workflows/deploy.yml with: environment: prod - secrets: inherit \ No newline at end of file + secrets: inherit + concurrency: + group: deploy-prod + cancel-in-progress: true \ No newline at end of file From 0562e058bff60b1bcc801eb9e17217422261f3b2 Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Thu, 27 Feb 2025 16:25:05 -0800 Subject: [PATCH 59/87] Add comma separator to security amount on view conditions --- .../decision/conditions/condition/condition.component.html | 2 +- .../decision/conditions/condition/condition.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html index 61bc9edfc..c7e722f30 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html @@ -19,7 +19,7 @@

{{ alphaIndex(index) }}. {{ condition.type.label }}
Security Amount
- {{ condition.securityAmount }} + {{ condition.securityAmount | number }}

diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html index f60dbe274..9506e3831 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html @@ -19,7 +19,7 @@

{{ condition.type.label }}

Security Amount
- {{ condition.securityAmount }} + {{ condition.securityAmount | number }}
From 4606f97297bfaa2e9fd3c3787d786aac7d0cc8d7 Mon Sep 17 00:00:00 2001 From: Dylan Rogowsky Date: Thu, 27 Feb 2025 16:31:14 -0700 Subject: [PATCH 60/87] ALCS-2578: Add Trivy image scan --- .github/workflows/_build-image.yml | 6 +++ .github/workflows/trivy-scan.yml | 78 ++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 .github/workflows/trivy-scan.yml diff --git a/.github/workflows/_build-image.yml b/.github/workflows/_build-image.yml index 00106984d..a9487103d 100644 --- a/.github/workflows/_build-image.yml +++ b/.github/workflows/_build-image.yml @@ -33,6 +33,12 @@ jobs: run: | DOCKER_IMAGE=ghcr.io/${{ steps.lowercase_repo_owner.outputs.lowercase }}/${{ inputs.image-name }} TAGS="${DOCKER_IMAGE}:${{ github.sha }},${DOCKER_IMAGE}:latest" + + # Add dev-latest tag for develop branch + if [ "${{ github.ref }}" = "refs/heads/develop" ]; then + TAGS="${TAGS},${DOCKER_IMAGE}:latest-dev" + fi + echo "tags=${TAGS}" >> $GITHUB_OUTPUT echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml new file mode 100644 index 000000000..4f69dbc99 --- /dev/null +++ b/.github/workflows/trivy-scan.yml @@ -0,0 +1,78 @@ +name: Weekly Trivy DEV Image Scans + +on: + schedule: + # Runs every week at 02:00 Sunday Morning. + - cron: '0 2 * * 0' + workflow_dispatch: + +permissions: + packages: read + security-events: write + +jobs: + image-scan-api: + name: Scan latest-dev API Image + runs-on: ubuntu-latest + steps: + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2 + with: + image-ref: 'ghcr.io/bcgov/alcs-api:latest-dev' + format: 'sarif' + output: 'trivy-results.sarif' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + limit-severities-for-sarif: true + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' + + image-scan-portal: + name: Scan latest-dev Portal Image + runs-on: ubuntu-latest + steps: + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2 + with: + image-ref: 'ghcr.io/bcgov/alcs-portal-frontend:latest-dev' + format: 'sarif' + output: 'trivy-results.sarif' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + limit-severities-for-sarif: true + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' + + image-scan-frontend: + name: Scan latest-dev Frontend Image + runs-on: ubuntu-latest + steps: + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + env: + TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2 + with: + image-ref: 'ghcr.io/bcgov/alcs-frontend:latest-dev' + format: 'sarif' + output: 'trivy-results.sarif' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + limit-severities-for-sarif: true + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' \ No newline at end of file From 2e7d4922de753e945d3ab377befcda33f89ad6d8 Mon Sep 17 00:00:00 2001 From: Dylan Rogowsky Date: Thu, 27 Feb 2025 16:32:33 -0700 Subject: [PATCH 61/87] ALCS-2533: Reconfigure OWASP ZAP scans --- .github/workflows/zap-scan.yml | 50 ++++++++++++++++++++++++++++++ .github/workflows/zap_api.yml | 13 -------- .github/workflows/zap_frontend.yml | 15 --------- .zap/portal.tsv | 3 ++ 4 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/zap-scan.yml delete mode 100644 .github/workflows/zap_api.yml delete mode 100644 .github/workflows/zap_frontend.yml create mode 100644 .zap/portal.tsv diff --git a/.github/workflows/zap-scan.yml b/.github/workflows/zap-scan.yml new file mode 100644 index 000000000..ec7dc092f --- /dev/null +++ b/.github/workflows/zap-scan.yml @@ -0,0 +1,50 @@ +name: Weekly OWASP ZAP Baseline Scan on DEV Site + +on: + schedule: + # Runs every week at 01:00 Sunday Morning. + - cron: '0 1 * * 0' + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + zap-scan-api: + name: OWASP ZAP API Scan + runs-on: ubuntu-latest + steps: + - name: API Scan + uses: zaproxy/action-api-scan@v0.9.0 + with: + target: 'https://alcs-dev-api.apps.silver.devops.gov.bc.ca/docs' + issue_title: OWASP ZAP API Scan Results + + zap-scan-frontend: + name: OWASP ZAP Frontend Scan + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Frontend Scan + uses: zaproxy/action-baseline@v0.14.0 + with: + target: "https://alcs-dev.apps.silver.devops.gov.bc.ca" + issue_title: OWASP ZAP Frontend Scan Results + rules_file_name: .zap/frontend.tsv + + zap-scan-portal: + name: OWASP ZAP Portal Scan + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Portal Scan + uses: zaproxy/action-baseline@v0.14.0 + with: + target: "https://alcs-dev-portal.apps.silver.devops.gov.bc.ca" + issue_title: OWASP ZAP Portal Scan Results + rules_file_name: .zap/portal.tsv \ No newline at end of file diff --git a/.github/workflows/zap_api.yml b/.github/workflows/zap_api.yml deleted file mode 100644 index ac7d0d792..000000000 --- a/.github/workflows/zap_api.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: OWASP Zap API Scan -on: workflow_dispatch - -jobs: - zap_scan: - runs-on: ubuntu-latest - name: OWASP Zap API Scan - steps: - - name: ZAP API Scan - uses: zaproxy/action-api-scan@v0.9.0 - with: - target: 'https://alcs-dev-api.apps.silver.devops.gov.bc.ca' - issue_title: OWASP Zap API Results \ No newline at end of file diff --git a/.github/workflows/zap_frontend.yml b/.github/workflows/zap_frontend.yml deleted file mode 100644 index 44b67bdc3..000000000 --- a/.github/workflows/zap_frontend.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: OWASP Zap Frontend Scan -on: workflow_dispatch - -jobs: - zap_scan: - runs-on: ubuntu-latest - name: OWASP Zap Frontend Scan - steps: - - uses: actions/checkout@v2 - - name: ZAP Scan - uses: zaproxy/action-baseline@v0.14.0 - with: - target: "https://alcs-dev.apps.silver.devops.gov.bc.ca" - issue_title: OWASP Zap Frontend Scan Results - rules_file_name: .zap/frontend.tsv diff --git a/.zap/portal.tsv b/.zap/portal.tsv new file mode 100644 index 000000000..98c3b974d --- /dev/null +++ b/.zap/portal.tsv @@ -0,0 +1,3 @@ +10055 IGNORE (CSP: style-src unsafe-inline) +10015 IGNORE (Incomplete or No Cache-control and Pragma HTTP Header Set) +10110 IGNORE (Dangerous JS Functions) \ No newline at end of file From c94791d7cf527ad2af001d71f3e1c41be566eff5 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Thu, 27 Feb 2025 16:42:27 -0800 Subject: [PATCH 62/87] ALCS-2029 Change move up algorithm --- .../decision-condition-order-dialog.component.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts index f6107f9fe..e9d7d381d 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts @@ -84,7 +84,13 @@ export class DecisionConditionOrderDialogComponent implements OnInit { sendToTop(record: ApplicationDecisionConditionDto) { const currentIndex = this.conditionsToOrder.findIndex((item) => item.uuid === record.uuid); - this.moveItem(currentIndex, 0); + this.conditionsToOrder.sort((a,b) => a.order - b.order).forEach((item) => { + if (item.order < currentIndex) { + item.order++; + } + }); + this.conditionsToOrder[currentIndex].order = 0; + this.dataSource.data = this.conditionsToOrder.sort((a,b) => a.order - b.order); this.overlayRef?.detach(); this.selectedRecord = undefined; } @@ -105,7 +111,7 @@ export class DecisionConditionOrderDialogComponent implements OnInit { } onSave(): void { - const order = this.data.conditions.map((cond, index) => ({ + const order = this.conditionsToOrder.map((cond, index) => ({ uuid: cond.uuid, order: cond.order, })); From ff574e8efce778f5b7d306395a118106d0e88b62 Mon Sep 17 00:00:00 2001 From: Dylan Rogowsky Date: Fri, 28 Feb 2025 08:55:45 -0700 Subject: [PATCH 63/87] ALCS-2533: Reconfigure OWASP ZAP scans artifact name --- .github/workflows/zap-scan.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/zap-scan.yml b/.github/workflows/zap-scan.yml index ec7dc092f..111a9ef65 100644 --- a/.github/workflows/zap-scan.yml +++ b/.github/workflows/zap-scan.yml @@ -20,6 +20,7 @@ jobs: with: target: 'https://alcs-dev-api.apps.silver.devops.gov.bc.ca/docs' issue_title: OWASP ZAP API Scan Results + artifact_name: zap-api-scan-report zap-scan-frontend: name: OWASP ZAP Frontend Scan @@ -34,6 +35,7 @@ jobs: target: "https://alcs-dev.apps.silver.devops.gov.bc.ca" issue_title: OWASP ZAP Frontend Scan Results rules_file_name: .zap/frontend.tsv + artifact_name: zap-frontend-scan-report zap-scan-portal: name: OWASP ZAP Portal Scan @@ -47,4 +49,5 @@ jobs: with: target: "https://alcs-dev-portal.apps.silver.devops.gov.bc.ca" issue_title: OWASP ZAP Portal Scan Results - rules_file_name: .zap/portal.tsv \ No newline at end of file + rules_file_name: .zap/portal.tsv + artifact_name: zap-portal-scan-report \ No newline at end of file From d0d739193fdd78a3ae0decec1835e76dcca82f6a Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Fri, 28 Feb 2025 08:52:14 -0800 Subject: [PATCH 64/87] ALCS-2029 Improve sort logic --- ...cision-condition-order-dialog.component.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts index e9d7d381d..ca5a38645 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts @@ -77,7 +77,13 @@ export class DecisionConditionOrderDialogComponent implements OnInit { sendToBottom(record: ApplicationDecisionConditionDto) { const currentIndex = this.conditionsToOrder.findIndex((item) => item.uuid === record.uuid); - this.moveItem(currentIndex, this.conditionsToOrder.length - 1); + this.conditionsToOrder.sort((a,b) => a.order - b.order).forEach((item) => { + if (item.order > currentIndex) { + item.order--; + } + }); + this.conditionsToOrder[currentIndex].order = this.conditionsToOrder.length; + this.dataSource.data = this.conditionsToOrder.sort((a,b) => a.order - b.order); this.overlayRef?.detach(); this.selectedRecord = undefined; } @@ -101,8 +107,19 @@ export class DecisionConditionOrderDialogComponent implements OnInit { } private moveItem(currentIndex: number, targetIndex: number) { + this.conditionsToOrder.sort((a,b) => a.order - b.order).forEach((item) => { + if (currentIndex > targetIndex) { + if (item.order < currentIndex && item.order >= targetIndex) { + item.order++; + } + } + else if (item.order > currentIndex) { + if (item.order <= targetIndex && item.order <= targetIndex) { + item.order--; + } + } + }); this.conditionsToOrder[currentIndex].order = targetIndex; - this.conditionsToOrder[targetIndex].order = currentIndex; this.dataSource.data = this.conditionsToOrder.sort((a,b) => a.order - b.order); } From 423b66b54ca988175f9325143ad60f53daaa2c2c Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Fri, 28 Feb 2025 09:05:34 -0800 Subject: [PATCH 65/87] ALCS-2029 Logic fix --- .../decision-condition-order-dialog.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts index ca5a38645..0182dacde 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts @@ -112,14 +112,14 @@ export class DecisionConditionOrderDialogComponent implements OnInit { if (item.order < currentIndex && item.order >= targetIndex) { item.order++; } - } - else if (item.order > currentIndex) { - if (item.order <= targetIndex && item.order <= targetIndex) { + } else if (item.order > currentIndex) { + if (item.order <= targetIndex) { item.order--; } } }); this.conditionsToOrder[currentIndex].order = targetIndex; + console.log(currentIndex, targetIndex, this.conditionsToOrder.sort((a,b) => a.order - b.order).map(x => `order: ${x.order} desc: ${x.description}`)); this.dataSource.data = this.conditionsToOrder.sort((a,b) => a.order - b.order); } From 40ae11fff43edf02c9fd49edeea6106d398dd3a4 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Fri, 28 Feb 2025 09:06:00 -0800 Subject: [PATCH 66/87] ALCS-2029 Remove log --- .../decision-condition-order-dialog.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts index 0182dacde..b2810a0c0 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition-order-dialog/decision-condition-order-dialog.component.ts @@ -119,7 +119,6 @@ export class DecisionConditionOrderDialogComponent implements OnInit { } }); this.conditionsToOrder[currentIndex].order = targetIndex; - console.log(currentIndex, targetIndex, this.conditionsToOrder.sort((a,b) => a.order - b.order).map(x => `order: ${x.order} desc: ${x.description}`)); this.dataSource.data = this.conditionsToOrder.sort((a,b) => a.order - b.order); } From bf2293db7cdefca23146b49c22ecbef0e0f4ec99 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Fri, 28 Feb 2025 13:42:11 -0800 Subject: [PATCH 67/87] ALCS-2200 Backend implementation --- .../application-advanced-search.service.ts | 10 +++++++ ...otice-of-intent-advanced-search.service.ts | 5 ++++ .../apps/alcs/src/portal/inbox/inbox.dto.ts | 8 ++++++ .../portal/public/search/public-search.dto.ts | 8 ++++++ .../search/application-search-filters.ts | 28 +++++++++++++++++++ .../search/notice-of-intent-search-filters.ts | 17 +++++++++++ 6 files changed, 76 insertions(+) diff --git a/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts b/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts index 80a08ebe8..d53f6a60a 100644 --- a/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts +++ b/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts @@ -138,6 +138,16 @@ export class ApplicationAdvancedSearchService { promises.push(promise); } + if (searchDto.decisionMaker) { + const promise = APP_SEARCH_FILTERS.addDecisionMakerResults(searchDto, this.applicationRepository); + promises.push(promise); + } + + if (searchDto.decisionOutcomes && searchDto.decisionOutcomes.length > 0) { + const promise = APP_SEARCH_FILTERS.addDecisionOutcomeResults(searchDto, this.applicationRepository); + promises.push(promise); + } + if (searchDto.governmentName) { const promise = APP_SEARCH_FILTERS.addGovernmentResults( searchDto, diff --git a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts index 1677d33c1..96711ff1c 100644 --- a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts +++ b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts @@ -139,6 +139,11 @@ export class NoticeOfIntentAdvancedSearchService { promises.push(promise); } + if (searchDto.decisionOutcomes && searchDto.decisionOutcomes.length > 0) { + const promise = NOI_SEARCH_FILTERS.addDecisionOutcomeResults(searchDto, this.noiRepository); + promises.push(promise); + } + if (searchDto.governmentName) { const promise = NOI_SEARCH_FILTERS.addGovernmentResults(searchDto, this.noiRepository, this.governmentRepository); promises.push(promise); diff --git a/services/apps/alcs/src/portal/inbox/inbox.dto.ts b/services/apps/alcs/src/portal/inbox/inbox.dto.ts index e681bb020..8f66ab0e3 100644 --- a/services/apps/alcs/src/portal/inbox/inbox.dto.ts +++ b/services/apps/alcs/src/portal/inbox/inbox.dto.ts @@ -92,6 +92,14 @@ export class InboxRequestDto { @IsString() @IsOptional() tagCategoryId?: string; + + @IsString() + @IsOptional() + decisionMaker?: string; + + @IsArray() + @IsOptional() + decisionOutcomes?: string[]; } // typeorm does not transform property names for the status diff --git a/services/apps/alcs/src/portal/public/search/public-search.dto.ts b/services/apps/alcs/src/portal/public/search/public-search.dto.ts index 230f6eb37..d10b857b7 100644 --- a/services/apps/alcs/src/portal/public/search/public-search.dto.ts +++ b/services/apps/alcs/src/portal/public/search/public-search.dto.ts @@ -112,6 +112,14 @@ export class SearchRequestDto extends PagingRequestDto { @IsArray() @IsOptional() tagCategoryId?: string; + + @IsArray() + @IsOptional() + decisionMaker?: string; + + @IsArray() + @IsOptional() + decisionOutcomes?: string[]; } // typeorm does not transform property names for the status diff --git a/services/apps/alcs/src/utils/search/application-search-filters.ts b/services/apps/alcs/src/utils/search/application-search-filters.ts index 2cdc167f2..66e60a790 100644 --- a/services/apps/alcs/src/utils/search/application-search-filters.ts +++ b/services/apps/alcs/src/utils/search/application-search-filters.ts @@ -66,6 +66,34 @@ export const APP_SEARCH_FILTERS = { }) .getMany(); }, + addDecisionMakerResults: (searchDto: SearchRequestDto | InboxRequestDto, appRepository: Repository) => { + return appRepository + .createQueryBuilder('app') + .select('app.fileNumber') + .leftJoin('application_decision', 'application_decision', 'application_decision.application_uuid = app.uuid') + .leftJoin( + 'application_decision_maker_code', + 'application_decision_maker_code', + 'application_decision_maker_code.code = application_decision.decision_maker_code', + ) + .where('application_decision_maker_code.code IN (:code)', { + code: searchDto.decisionMaker, + }) + .getMany(); + }, + addDecisionOutcomeResults: ( + searchDto: SearchRequestDto | InboxRequestDto, + appRepository: Repository, + ) => { + return appRepository + .createQueryBuilder('app') + .select('app.fileNumber') + .leftJoin('application_decision', 'application_decision', 'application_decision.application_uuid = app.uuid') + .where('application_decision.outcome_code IN (:...outcomeCodes)', { + outcomeCodes: searchDto.decisionOutcomes, + }) + .getMany(); + }, addNameResults: ( searchDto: SearchRequestDto | InboxRequestDto, applicationSubmissionRepository: Repository, diff --git a/services/apps/alcs/src/utils/search/notice-of-intent-search-filters.ts b/services/apps/alcs/src/utils/search/notice-of-intent-search-filters.ts index e951c81c5..ebaa0e7bd 100644 --- a/services/apps/alcs/src/utils/search/notice-of-intent-search-filters.ts +++ b/services/apps/alcs/src/utils/search/notice-of-intent-search-filters.ts @@ -60,6 +60,23 @@ export const NOI_SEARCH_FILTERS = { }) .getMany(); }, + addDecisionOutcomeResults: ( + searchDto: SearchRequestDto | InboxRequestDto, + appRepository: Repository, + ) => { + return appRepository + .createQueryBuilder('noi') + .select('noi.fileNumber') + .leftJoin( + 'notice_of_intent_decision', + 'notice_of_intent_decision', + 'notice_of_intent_decision.notice_of_intent_uuid = noi.uuid', + ) + .where('notice_of_intent_decision.outcome_code IN (:...outcomeCodes)', { + outcomeCodes: searchDto.decisionOutcomes, + }) + .getMany(); + }, addNameResults: ( searchDto: SearchRequestDto | InboxRequestDto, noiSubmissionRepository: Repository, From f5f5a6d7bd89e14b4aa3b58ee99b18575227e531 Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Fri, 28 Feb 2025 13:56:27 -0800 Subject: [PATCH 68/87] ALCS-2200 Fix tests --- portal-frontend/src/app/services/search/search.dto.ts | 2 +- services/apps/alcs/src/alcs/search/search.dto.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/portal-frontend/src/app/services/search/search.dto.ts b/portal-frontend/src/app/services/search/search.dto.ts index 97ef646f4..a78be0dd8 100644 --- a/portal-frontend/src/app/services/search/search.dto.ts +++ b/portal-frontend/src/app/services/search/search.dto.ts @@ -49,7 +49,7 @@ export interface SearchRequestDto extends PagingRequestDto { dateDecidedFrom?: number; dateDecidedTo?: number; fileTypes: string[]; - decisionOutcome?: string[]; + decisionOutcomes?: string[]; } export const displayedColumns = ['fileId', 'ownerName', 'type', 'portalStatus', 'lastUpdate', 'government']; \ No newline at end of file diff --git a/services/apps/alcs/src/alcs/search/search.dto.ts b/services/apps/alcs/src/alcs/search/search.dto.ts index ae145f316..b031cb739 100644 --- a/services/apps/alcs/src/alcs/search/search.dto.ts +++ b/services/apps/alcs/src/alcs/search/search.dto.ts @@ -183,7 +183,7 @@ export class SearchRequestDto extends PagingRequestDto { tagCategoryId?: string; @IsArray() - decisionOutcomes: string[]; + decisionOutcomes?: string[]; @IsString() @IsOptional() From e6476c05ee0dba3154491e01db82d50e76aea88c Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Fri, 28 Feb 2025 14:15:17 -0800 Subject: [PATCH 69/87] ALCS-2200 fix tests --- alcs-frontend/src/app/services/search/search.dto.ts | 2 +- .../src/app/features/public/search/public-search.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/alcs-frontend/src/app/services/search/search.dto.ts b/alcs-frontend/src/app/services/search/search.dto.ts index 8c0218544..1ef032c1c 100644 --- a/alcs-frontend/src/app/services/search/search.dto.ts +++ b/alcs-frontend/src/app/services/search/search.dto.ts @@ -81,7 +81,7 @@ export interface SearchRequestDto extends PagingRequestDto { dateDecidedTo?: number; fileTypes: string[]; decisionMaker?: string; - decisionOutcomes: string[]; + decisionOutcomes?: string[]; tagIds?: string[]; tagCategoryId?: string; } diff --git a/portal-frontend/src/app/features/public/search/public-search.component.ts b/portal-frontend/src/app/features/public/search/public-search.component.ts index e2a70dae2..e5a5bcc18 100644 --- a/portal-frontend/src/app/features/public/search/public-search.component.ts +++ b/portal-frontend/src/app/features/public/search/public-search.component.ts @@ -270,7 +270,7 @@ export class PublicSearchComponent implements OnInit, OnDestroy { fileNumber: this.formatStringSearchParam(searchControls.fileNumber.value), name: this.formatStringSearchParam(searchControls.name.value), civicAddress: this.formatStringSearchParam(searchControls.civicAddress.value), - decisionOutcome: searchControls.portalDecisionOutcome.value ?? undefined, + decisionOutcomes: searchControls.portalDecisionOutcome.value ?? undefined, pid: this.formatStringSearchParam(searchControls.pid.value), portalStatusCodes: searchControls.portalStatus.value ?? undefined, governmentName: this.formatStringSearchParam(searchControls.government.value), @@ -461,7 +461,7 @@ export class PublicSearchComponent implements OnInit, OnDestroy { civicAddress.setValue(storedSearch.civicAddress); government.setValue(storedSearch.governmentName); portalStatus.setValue(storedSearch.portalStatusCodes ?? null); - portalDecisionOutcome.setValue(storedSearch.decisionOutcome ?? null); + portalDecisionOutcome.setValue(storedSearch.decisionOutcomes ?? null); if (storedSearch.dateDecidedTo) { dateDecidedTo.setValue(new Date(storedSearch.dateDecidedTo)); From 3f906a9042b5bb6d6c181d809eae3999cf64af75 Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Mon, 3 Mar 2025 12:11:13 -0800 Subject: [PATCH 70/87] Emit conditions validations when calling onValidate function --- .../decision-conditions/decision-conditions.component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts index 7020a374f..8bf693b17 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts @@ -219,6 +219,10 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy onValidate() { this.conditionComponents.forEach((component) => component.form.markAllAsTouched()); + this.conditionsChange.emit({ + conditions: this.mappedConditions, + isValid: this.conditionComponents.reduce((isValid, component) => isValid && component.form.valid, true), + }); } openOrderDialog() { From 4dc6b9da841adc185c916d530e8372ccc86c8eaf Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Mon, 3 Mar 2025 12:19:11 -0800 Subject: [PATCH 71/87] Emit conditions validations when calling onValidate function --- .../decision-conditions/decision-conditions.component.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts index 978ed509d..dbbb3ce7d 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts @@ -213,5 +213,9 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy onValidate() { this.conditionComponents.forEach((component) => component.form.markAllAsTouched()); + this.conditionsChange.emit({ + conditions: this.mappedConditions, + isValid: this.conditionComponents.reduce((isValid, component) => isValid && component.form.valid, true), + }); } } From 7ecc79d7b8e7a0c790f1b3cf553db9882ad357d4 Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Mon, 3 Mar 2025 13:50:04 -0800 Subject: [PATCH 72/87] Add confirmation dialog for deleting dates in multiple date conditions The change applies for applications and NOIs --- .../condition/condition.component.ts | 38 +++++++++++------- .../condition/condition.component.ts | 40 +++++++++++-------- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts index e54af47a8..c3ac91482 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts @@ -28,6 +28,7 @@ import { countToString } from '../../../../../shared/utils/count-to-string'; import { ApplicationDecisionV2Service } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; import { MatTableDataSource } from '@angular/material/table'; import { MatSort } from '@angular/material/sort'; +import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; type Condition = ApplicationDecisionConditionWithStatus & { componentLabelsStr?: string; @@ -86,6 +87,7 @@ export class ConditionComponent implements OnInit, AfterViewInit { private conditionService: ApplicationDecisionConditionService, private conditionLotService: ApplicationDecisionComponentToConditionLotService, private decisionService: ApplicationDecisionV2Service, + private confirmationDialogService: ConfirmationDialogService, ) {} async ngOnInit() { @@ -314,21 +316,27 @@ export class ConditionComponent implements OnInit, AfterViewInit { } async onDeleteDate(dateUuid: string) { - const result = await this.conditionService.deleteDate(dateUuid); - if (result) { - const index = this.dates.findIndex((date) => date.uuid === dateUuid); - - if (index !== -1) { - this.dates.splice(index, 1); - this.dataSource = new MatTableDataSource( - this.addIndex(this.sortDates(this.dates)), - ); - const conditionNewStatus = await this.decisionService.getStatus(this.condition.uuid); - this.condition.status = conditionNewStatus.status; - this.statusChange.emit(this.condition.status); - this.setPillLabel(this.condition.status); - } - } + this.confirmationDialogService + .openDialog({ body: 'Are you sure you want to delete this date?' }) + .subscribe(async (confirmed) => { + if (confirmed) { + const result = await this.conditionService.deleteDate(dateUuid); + if (result) { + const index = this.dates.findIndex((date) => date.uuid === dateUuid); + + if (index !== -1) { + this.dates.splice(index, 1); + this.dataSource = new MatTableDataSource( + this.addIndex(this.sortDates(this.dates)), + ); + const conditionNewStatus = await this.decisionService.getStatus(this.condition.uuid); + this.condition.status = conditionNewStatus.status; + this.statusChange.emit(this.condition.status); + this.setPillLabel(this.condition.status); + } + } + } + }); } alphaIndex(index: number) { diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts index 1b39ace15..bf77fc11a 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts @@ -20,6 +20,7 @@ import { countToString } from '../../../../../shared/utils/count-to-string'; import { NoticeOfIntentDecisionV2Service } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; +import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; type Condition = DecisionConditionWithStatus & { componentLabelsStr?: string; @@ -76,6 +77,7 @@ export class ConditionComponent implements OnInit, AfterViewInit { constructor( private conditionService: NoticeOfIntentDecisionConditionService, private decisionService: NoticeOfIntentDecisionV2Service, + private confirmationDialogService: ConfirmationDialogService, ) {} async ngOnInit() { @@ -234,21 +236,27 @@ export class ConditionComponent implements OnInit, AfterViewInit { } async onDeleteDate(dateUuid: string) { - const result = await this.conditionService.deleteDate(dateUuid); - if (result) { - const index = this.dates.findIndex((date) => date.uuid === dateUuid); - - if (index !== -1) { - this.dates.splice(index, 1); - this.dataSource = new MatTableDataSource( - this.addIndex(this.sortDates(this.dates)), - ); - - const conditionNewStatus = await this.decisionService.getStatus(this.condition.uuid); - this.condition.status = conditionNewStatus.status; - this.statusChange.emit(this.condition.status); - this.setPillLabel(this.condition.status); - } - } + this.confirmationDialogService + .openDialog({ body: 'Are you sure you want to delete this date?' }) + .subscribe(async (confirmed) => { + if (confirmed) { + const result = await this.conditionService.deleteDate(dateUuid); + if (result) { + const index = this.dates.findIndex((date) => date.uuid === dateUuid); + + if (index !== -1) { + this.dates.splice(index, 1); + this.dataSource = new MatTableDataSource( + this.addIndex(this.sortDates(this.dates)), + ); + + const conditionNewStatus = await this.decisionService.getStatus(this.condition.uuid); + this.condition.status = conditionNewStatus.status; + this.statusChange.emit(this.condition.status); + this.setPillLabel(this.condition.status); + } + } + } + }); } } From 237e25b6865e0d094016a8a82abdb5b4833fb08e Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Mon, 3 Mar 2025 16:20:54 -0800 Subject: [PATCH 73/87] ALCS-2576 Implement alpha index on condition card --- .../condition-card-dialog.component.html | 4 ++-- .../condition-card-dialog.component.ts | 9 ++++++++- .../application-decision-condition-dialog.component.html | 2 +- .../application-decision-condition-dialog.component.ts | 7 +++++++ .../application-decision-condition.dto.ts | 4 ++++ .../application-decision-condition.service.ts | 1 + 6 files changed, 23 insertions(+), 4 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.html b/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.html index da9ca2c38..b8f975b47 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.html +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.html @@ -34,8 +34,8 @@

Create New Condition Card

-

- + + diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.ts index ef298bfee..8b010c9fc 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.ts @@ -6,12 +6,12 @@ import { ApplicationDecisionConditionDto, CreateApplicationDecisionConditionCardDto, } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; -import { ApplicationDecisionConditionDto as OriginalApplicationDecisionConditionDto } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ApplicationDecisionConditionCardService } from '../../../../../services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition-card/application-decision-condition-card.service'; import { BOARD_TYPE_CODES, BoardService } from '../../../../../services/board/board.service'; import { BoardDto, BoardStatusDto } from '../../../../../services/board/board.dto'; import { ToastService } from '../../../../../services/toast/toast.service'; import { CardType } from '../../../../../shared/card/card.component'; +import { countToString } from '../../../../../shared/utils/count-to-string'; @Component({ selector: 'app-condition-card-dialog', @@ -22,6 +22,7 @@ export class ConditionCardDialogComponent implements OnInit { displayColumns: string[] = ['select', 'index', 'type', 'description']; conditionBoard: BoardDto | undefined; selectedStatus = ''; + isOrderNull = false; @ViewChild(MatSort) sort!: MatSort; dataSource: MatTableDataSource<{ condition: ApplicationDecisionConditionDto; index: number; selected: boolean }> = @@ -37,6 +38,8 @@ export class ConditionCardDialogComponent implements OnInit { ) {} async ngOnInit() { + const orderIndexes = this.data.conditions.map((c) => c.condition.order); + this.isOrderNull = this.data.conditions.length > 1 && orderIndexes.every((val, i, arr) => val === arr[0] && arr[0] === 0); this.dataSource.data = this.data.conditions.map((item) => ({ condition: item.condition, selected: false, @@ -87,4 +90,8 @@ export class ConditionCardDialogComponent implements OnInit { this.dialogRef.close({ action: 'save', result: false }); } } + + alphaIndex(index: number) { + return countToString(index); + } } diff --git a/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.html index 6cf0ff45d..82a60ec98 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.html @@ -168,7 +168,7 @@

diff --git a/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.ts index 4da8884ad..66a09a9a9 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/application-decision-condition-dialog/application-decision-condition-dialog.component.ts @@ -44,6 +44,7 @@ export class ApplicationDecisionConditionDialogComponent extends CardDialogCompo applicationDecisionConditionCard: ApplicationDecisionConditionCardBoardDto = this.data.decisionConditionCard; isModification: boolean = false; isReconsideration: boolean = false; + isOrderNull = false; @ViewChild(MatSort) sort!: MatSort; dataSource: MatTableDataSource<{ condition: ApplicationDecisionConditionDto; index: number; selected: boolean }> = @@ -95,6 +96,8 @@ export class ApplicationDecisionConditionDialogComponent extends CardDialogCompo true, ); if (decision) { + const orderIndexes = decision.conditions.map((c) => c.order); + this.isOrderNull = decision.conditions.length > 1 && orderIndexes.every((val, i, arr) => val === arr[0] && arr[0] === 0); this.decision = decision; } } @@ -259,4 +262,8 @@ export class ApplicationDecisionConditionDialogComponent extends CardDialogCompo this.applicationDecisionConditionCard = applicationDecisionConditionCard!; this.populateData(); } + + alphaIndex(index: number) { + return countToString(index); + } } diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts index 76ef405a0..80c0889a0 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts @@ -191,6 +191,10 @@ export class UpdateApplicationDecisionConditionDto { @IsOptional() @IsUUID() conditionCardUuid?: string; + + @IsOptional() + @IsNumber() + order?: number; } export class UpdateApplicationDecisionConditionServiceDto { diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts index 1a1abbeb7..935641619 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts @@ -221,6 +221,7 @@ export class ApplicationDecisionConditionService { condition.description = updateDto.description ?? null; condition.securityAmount = updateDto.securityAmount ?? null; condition.approvalDependant = updateDto.approvalDependant ?? null; + condition.order = updateDto.order ?? 0; if (updateDto.dates) { condition.dates = updateDto.dates.map((dateDto) => { const dateEntity = new ApplicationDecisionConditionDate(); From 084a6d3f187e2b99982976ca49ff0ada7482522d Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Tue, 4 Mar 2025 08:54:46 -0800 Subject: [PATCH 74/87] ALCS-2576 Avoid order gaps when removing conditions --- .../decision-conditions/decision-conditions.component.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts index 8bf693b17..6aeadee27 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts @@ -129,6 +129,11 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy .subscribe((didConfirm) => { if (didConfirm) { this.mappedConditions.splice(index, 1); + this.mappedConditions.forEach((c) => { + if (c.order && c.order > index) { + c.order--; + } + }); this.onChanges(); } }); From e395656cb863aa8d668aeca77c843606165641aa Mon Sep 17 00:00:00 2001 From: Felipe Barreta Date: Tue, 4 Mar 2025 09:46:54 -0800 Subject: [PATCH 75/87] ALCS-2576 UI tweaks --- .../condition-card-dialog.component.html | 11 +++-------- .../condition-card-dialog.component.ts | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.html b/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.html index b8f975b47..d1c641be3 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.html +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.html @@ -33,14 +33,9 @@

Create New Condition Card

- -
- - - - - - + + + diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.ts index 8b010c9fc..193b9a784 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition-card-dialog/condition-card-dialog.component.ts @@ -19,7 +19,7 @@ import { countToString } from '../../../../../shared/utils/count-to-string'; styleUrl: './condition-card-dialog.component.scss', }) export class ConditionCardDialogComponent implements OnInit { - displayColumns: string[] = ['select', 'index', 'type', 'description']; + displayColumns: string[] = ['select', 'condition', 'description']; conditionBoard: BoardDto | undefined; selectedStatus = ''; isOrderNull = false; From 9c4740d790e7dac6b9c0442847bb643e5967585a Mon Sep 17 00:00:00 2001 From: Shayan Khorsandi Date: Tue, 4 Mar 2025 09:51:23 -0800 Subject: [PATCH 76/87] Remove interaction from tag chips in commissioner view --- .../commissioner-tags-header.component.html | 2 +- .../tags/tag-chip/tag-chip.component.html | 9 +++++- .../tags/tag-chip/tag-chip.component.scss | 28 +++++++++++++++++++ .../tags/tag-chip/tag-chip.component.ts | 1 + 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.html b/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.html index bec2a62f1..7e792df5a 100644 --- a/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.html +++ b/alcs-frontend/src/app/shared/tags/commissioner-tags-header/commissioner-tags-header.component.html @@ -1,3 +1,3 @@
- +
diff --git a/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.html b/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.html index 05691ae9c..15640b5a4 100644 --- a/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.html +++ b/alcs-frontend/src/app/shared/tags/tag-chip/tag-chip.component.html @@ -1,4 +1,11 @@ - + {{ tag.name }}
# +   {{ alphaIndex(i + 1) }}
#{{ element.index }}{{ !isOrderNull ? alphaIndex(element.condition.order + 1) : '' }} - {{ element.index }}. {{ element.condition.type.label }} + {{ !isOrderNull ? alphaIndex(element.condition.order + 1) + '.' : '' }} {{ element.condition.type.label }} {{ !isOrderNull ? alphaIndex(element.condition.order + 1) : '' }}Type{{ element.condition.type.label }}Condition{{ !isOrderNull ? alphaIndex(element.condition.order + 1) + '.' : '' }} {{ element.condition.type.label }}