diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.html b/src/app/clients/client-stepper/client-general-step/client-general-step.component.html index 09391e0b97..1a5f30468d 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.html +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.html @@ -171,8 +171,8 @@ {{ 'labels.inputs.Incorporation Validity Till Date' | translate }} diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.spec.ts b/src/app/clients/client-stepper/client-general-step/client-general-step.component.spec.ts new file mode 100644 index 0000000000..246b9aaa04 --- /dev/null +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.spec.ts @@ -0,0 +1,114 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { provideNativeDateAdapter } from '@angular/material/core'; +import { MatDatepickerInput } from '@angular/material/datepicker'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { CdkStepper } from '@angular/cdk/stepper'; +import { FaIconLibrary } from '@fortawesome/angular-fontawesome'; +import * as solidIcons from '@fortawesome/free-solid-svg-icons'; +import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; +import { describe, it, expect, jest, beforeEach } from '@jest/globals'; + +import { ClientGeneralStepComponent } from './client-general-step.component'; +import { ClientsService } from 'app/clients/clients.service'; +import { LegalFormId } from 'app/clients/models/legal-form.enum'; +import { Dates } from 'app/core/utils/dates'; +import { SettingsService } from 'app/settings/settings.service'; + +describe('ClientGeneralStepComponent WEB-1103 incorporation validity date', () => { + let fixture: ComponentFixture; + let component: ClientGeneralStepComponent; + + const businessDate = new Date(2026, 0, 15); + const maxFutureDate = new Date(2100, 0, 1); + + const clientTemplate: any = { + officeOptions: [{ id: 1, name: 'Head Office' }], + staffOptions: [], + clientLegalFormOptions: [ + { id: LegalFormId.PERSON, value: 'PERSON' }, + { id: LegalFormId.ENTITY, value: 'ENTITY' } + ], + clientTypeOptions: [], + clientClassificationOptions: [], + clientNonPersonMainBusinessLineOptions: [], + clientNonPersonConstitutionOptions: [], + genderOptions: [], + savingProductOptions: [] + }; + + /** The `Incorporation Validity Till Date` datepicker, as bound in the template. */ + function incorpValidityDatePicker(): MatDatepickerInput { + return fixture.debugElement + .query(By.css('input[formControlName="incorpValidityTillDate"]')) + .injector.get>(MatDatepickerInput); + } + + function incorpValidityControl() { + return component.createClientForm.get('clientNonPersonDetails.incorpValidityTillDate'); + } + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + ClientGeneralStepComponent, + TranslateModule.forRoot() + ], + providers: [ + provideNativeDateAdapter(), + provideNoopAnimations(), + { provide: CdkStepper, useValue: { next: jest.fn(), previous: jest.fn() } }, + { provide: Dates, useValue: { formatDate: jest.fn(() => '15 January 2026') } }, + { + provide: SettingsService, + useValue: { businessDate, maxFutureDate, dateFormat: 'dd MMMM yyyy', language: { code: 'en-US' } } + }, + { provide: ClientsService, useValue: { getClientWithOfficeTemplate: jest.fn(() => of(clientTemplate)) } } + ] + }).compileComponents(); + + const faIconLibrary = TestBed.inject(FaIconLibrary); + faIconLibrary.addIcons( + ...Object.keys(solidIcons) + .filter((key) => key !== 'fas' && key !== 'prefix' && key.startsWith('fa')) + .map((icon) => (solidIcons as any)[icon]) + ); + + fixture = TestBed.createComponent(ClientGeneralStepComponent); + component = fixture.componentInstance; + component.clientTemplate = clientTemplate; + fixture.detectChanges(); + + component.createClientForm.get('legalFormId').patchValue(LegalFormId.ENTITY); + fixture.detectChanges(); + }); + + it('allows future incorporation validity dates', () => { + expect(incorpValidityDatePicker().max).toEqual(maxFutureDate); + }); + + it('falls back to the generic minimum when no incorporation date is set', () => { + expect(incorpValidityDatePicker().min).toEqual(component.minDate); + }); + + it('rejects validity dates before the incorporation date', () => { + const incorporationDate = new Date(2026, 5, 10); + component.createClientForm.get('dateOfBirth').patchValue(incorporationDate); + fixture.detectChanges(); + + expect(incorpValidityDatePicker().min).toEqual(incorporationDate); + + incorpValidityControl().patchValue(new Date(2026, 4, 10)); + + expect(incorpValidityControl().hasError('matDatepickerMin')).toBe(true); + }); +}); diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts index 78ff13d3f0..7302b76d55 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts @@ -70,6 +70,8 @@ export class ClientGeneralStepComponent implements OnInit { minDate = new Date(2000, 0, 1); /** Maximum date allowed. */ maxDate = new Date(); + /** Maximum date allowed for fields that accept future dates. */ + maxFutureDate = this.settingsService.maxFutureDate; /** Client Template */ @Input() clientTemplate: any; diff --git a/src/app/clients/edit-client/edit-client.component.html b/src/app/clients/edit-client/edit-client.component.html index 22dbbfda35..272fe3b15c 100644 --- a/src/app/clients/edit-client/edit-client.component.html +++ b/src/app/clients/edit-client/edit-client.component.html @@ -238,8 +238,8 @@

{{ 'labels.heading.' + productionGeneralHeading | [placeholder]="'labels.inputs.Incorporation Validity Till Date' | translate" [title]="'labels.inputs.Incorporation Validity Till Date' | translate" matInput - [min]="minDate" - [max]="maxDate" + [min]="editClientForm.value.dateOfBirth || minDate" + [max]="maxFutureDate" [matDatepicker]="incorpValidityTillDateDatePicker" formControlName="incorpValidityTillDate" /> diff --git a/src/app/clients/edit-client/edit-client.component.spec.ts b/src/app/clients/edit-client/edit-client.component.spec.ts index 0e6f23c27c..d8ad2108d5 100644 --- a/src/app/clients/edit-client/edit-client.component.spec.ts +++ b/src/app/clients/edit-client/edit-client.component.spec.ts @@ -9,6 +9,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { provideNativeDateAdapter } from '@angular/material/core'; +import { MatDatepickerInput } from '@angular/material/datepicker'; +import { By } from '@angular/platform-browser'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { TranslateModule } from '@ngx-translate/core'; import { of, throwError } from 'rxjs'; @@ -29,6 +31,8 @@ describe('EditClientComponent WEB-1161 production edit flow', () => { let router: jest.Mocked; let originalProductionMode: boolean; + const maxFutureDate = new Date(2100, 0, 1); + const clientDataAndTemplate: any = { id: 1, officeId: 1, @@ -284,6 +288,7 @@ describe('EditClientComponent WEB-1161 production edit flow', () => { provide: SettingsService, useValue: { businessDate: new Date(2024, 0, 15), + maxFutureDate, language: { code: 'en' }, dateFormat: 'dd MMMM yyyy' } @@ -854,4 +859,55 @@ describe('EditClientComponent WEB-1161 production edit flow', () => { expect.objectContaining({ is_pep: true, pep_details: 'Public office' }) ); }); + + describe('WEB-1103 incorporation validity till date', () => { + /** The `Incorporation Validity Till Date` datepicker, as bound in the edit template. */ + function incorpValidityDatePicker(): MatDatepickerInput { + return fixture.debugElement + .query(By.css('input[formControlName="incorpValidityTillDate"]')) + .injector.get>(MatDatepickerInput); + } + + function configureEntityClient(dateOfBirth: any) { + configureTestingModule(true, { + legalForm: { id: 2 }, + fullname: 'Acme Holdings LLC', + dateOfBirth, + clientNonPersonDetails: { + constitution: { id: 1 }, + mainBusinessLine: { id: 2 }, + incorpNumber: 'INC-1' + }, + clientNonPersonConstitutionOptions: [{ id: 1, name: 'LLC' }], + clientNonPersonMainBusinessLineOptions: [{ id: 2, name: 'Services' }] + }); + } + + it('allows future incorporation validity dates', () => { + configureEntityClient(null); + + expect(incorpValidityDatePicker().max).toEqual(maxFutureDate); + }); + + it('falls back to the generic minimum when no incorporation date is set', () => { + configureEntityClient(null); + + expect(incorpValidityDatePicker().min).toEqual(component.minDate); + }); + + it('rejects validity dates before the incorporation date', () => { + const incorporationDate = new Date(2026, 5, 10); + configureEntityClient(null); + + component.editClientForm.get('dateOfBirth').patchValue(incorporationDate); + fixture.detectChanges(); + + expect(incorpValidityDatePicker().min).toEqual(incorporationDate); + + const validityControl = component.editClientForm.get('clientNonPersonDetails.incorpValidityTillDate'); + validityControl.patchValue(new Date(2026, 4, 10)); + + expect(validityControl.hasError('matDatepickerMin')).toBe(true); + }); + }); }); diff --git a/src/app/clients/edit-client/edit-client.component.ts b/src/app/clients/edit-client/edit-client.component.ts index 09d701c778..bc8dab5ea6 100644 --- a/src/app/clients/edit-client/edit-client.component.ts +++ b/src/app/clients/edit-client/edit-client.component.ts @@ -93,6 +93,8 @@ export class EditClientComponent implements OnInit { minDate = new Date(2000, 0, 1); /** Maximum date allowed. */ maxDate = new Date(); + /** Maximum date allowed for fields that accept future dates. */ + maxFutureDate = this.settingsService.maxFutureDate; /** Client Data and Template */ clientDataAndTemplate: any;