From 12c5e7210653886f5518e325e67ab5a34f8d6303 Mon Sep 17 00:00:00 2001 From: Parth Sharma Date: Sun, 13 Sep 2026 15:58:03 +0530 Subject: [PATCH] fix(WEB-1103): allow future incorporation validity till dates Incorporation Validity Till Date is the expiry of an entity's incorporation documents, so it is future-dated by definition. Both the create stepper and the edit form capped it at [max]="maxDate", which ngOnInit sets to settingsService.businessDate - every date after today was greyed out in the picker. Point the cap at settingsService.maxFutureDate instead of dropping it. That is what every other field in the app that legitimately accepts a future date already uses (loan disbursement and approval, teller and cashier dates, charge due dates, collection sheet), so the field keeps a bound and matches the rest of the app rather than becoming unbounded. Tighten the floor while here. minDate is a flat 1 Jan 2000, which let a validity date be set before the incorporation date it derives from. Fineract stores an entity's incorporation date in the dateOfBirth field - the create and edit templates both relabel that control to "Incorporation Date" for legal form Entity - so bind [min] to it and fall back to minDate until it is filled. This mirrors the Activated On field, which already reads [min]="editClientForm.value.submittedOnDate". The specs render the entity branch of the create stepper and of the edit form, reading min and max off the MatDatepickerInput, so they fail if the business-date cap comes back or the floor stops tracking the incorporation date. Not included: the field has no mat-error, so a validity date that falls before the incorporation date shows Material's invalid styling with no message. Adding one needs a new label across all 13 locales; worth a separate change. Continues #3795. Co-authored-by: Farah Nahle --- .../client-general-step.component.html | 4 +- .../client-general-step.component.spec.ts | 114 ++++++++++++++++++ .../client-general-step.component.ts | 2 + .../edit-client/edit-client.component.html | 4 +- .../edit-client/edit-client.component.spec.ts | 56 +++++++++ .../edit-client/edit-client.component.ts | 2 + 6 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 src/app/clients/client-stepper/client-general-step/client-general-step.component.spec.ts 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;