From c2ad767371cd50aa0b9ed523730b82871ff953ea Mon Sep 17 00:00:00 2001 From: 1cbyc Date: Fri, 16 Oct 2026 06:46:56 +0000 Subject: [PATCH 1/2] fix(products): show repayment start date options --- .../loan-product-form.component.test.ts | 24 +++++++++++++++++-- .../products/loan-product-form.component.ts | 9 +++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/app/features/products/loan-product-form.component.test.ts b/src/app/features/products/loan-product-form.component.test.ts index eb2df224b..f87cb19f9 100644 --- a/src/app/features/products/loan-product-form.component.test.ts +++ b/src/app/features/products/loan-product-form.component.test.ts @@ -113,7 +113,16 @@ const TEMPLATE = { }, ], repaymentStartDateTypeOptions: [ - { id: 1, code: 'repaymentStartDateType.disbursement', description: 'Disbursement date' }, + { + id: 1, + code: 'repaymentStartDateType.disbursementDate', + value: 'Disbursement Date', + }, + { + id: 2, + code: 'repaymentStartDateType.submittedOnDate', + value: 'Submitted On Date', + }, ], chargeOffBehaviourOptions: [ { code: 'REGULAR', value: 'Regular' }, @@ -492,7 +501,18 @@ describe('LoanProductFormComponent', () => { expect(component.capitalizedIncomeTypeOptions()).toHaveLength(2); expect(component.capitalizedIncomeStrategyOptions()[0].value).toBe(EQUAL_AMORTIZATION_LABEL); expect(component.chargeOffBehaviourOptions()).toHaveLength(2); - expect(component.repaymentStartDateTypeOptions()).toHaveLength(1); + expect(component.repaymentStartDateTypeOptions()).toHaveLength(2); + }); + + it('renders repayment start date labels from the template response', () => { + const select = fixture.nativeElement.querySelector( + '[data-testid="loan-product-repayment-start-date-type"]', + ); + const labels = Array.from(select.querySelectorAll('ion-select-option')).map( + (option) => option.textContent.trim(), + ); + + expect(labels).toEqual(['Disbursement Date', 'Submitted On Date']); }); }); diff --git a/src/app/features/products/loan-product-form.component.ts b/src/app/features/products/loan-product-form.component.ts index 10c996ecf..81533b144 100644 --- a/src/app/features/products/loan-product-form.component.ts +++ b/src/app/features/products/loan-product-form.component.ts @@ -87,6 +87,11 @@ import { TooltipDirective } from '../../shared/directives/tooltip.directive'; */ const DAILY_INTEREST_CALCULATION_PERIOD = 0; +/** The live template response uses `value`; the generated contract currently omits it. */ +type RepaymentStartDateTypeOption = GetLoanProductsRepaymentStartDateType & { + value?: string; +}; + @Component({ selector: 'app-loan-product-form', standalone: true, @@ -1216,7 +1221,7 @@ const DAILY_INTEREST_CALCULATION_PERIOD = 0; > @for (option of repaymentStartDateTypeOptions(); track option.id) { {{ - option.description + option.value ?? option.description }} } @@ -1431,7 +1436,7 @@ export class LoanProductFormComponent implements OnInit { readonly preClosureStrategyOptions = signal< GetLoanProductsPreClosureInterestCalculationStrategy[] >([]); - readonly repaymentStartDateTypeOptions = signal([]); + readonly repaymentStartDateTypeOptions = signal([]); readonly chargeOffBehaviourOptions = signal([]); /** From d3dedf1a7f00825e35f36e600f41e83a869735f3 Mon Sep 17 00:00:00 2001 From: 1cbyc Date: Fri, 16 Oct 2026 09:27:15 +0000 Subject: [PATCH 2/2] fix(products): label template enum options consistently --- .../loan-product-form.component.test.ts | 45 ++++++++++++------- .../products/loan-product-form.component.ts | 44 +++++++++++------- 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/src/app/features/products/loan-product-form.component.test.ts b/src/app/features/products/loan-product-form.component.test.ts index f87cb19f9..98fd2325b 100644 --- a/src/app/features/products/loan-product-form.component.test.ts +++ b/src/app/features/products/loan-product-form.component.test.ts @@ -87,29 +87,29 @@ const TEMPLATE = { buyDownFeeCalculationTypeOptions: [{ code: 'FLAT', value: 'Flat' }], buyDownFeeStrategyOptions: [{ code: 'EQUAL_AMORTIZATION', value: EQUAL_AMORTIZATION_LABEL }], interestRecalculationCompoundingTypeOptions: [ - { id: 0, code: 'interestRecalculationCompoundingMethod.none', description: 'None' }, - { id: 1, code: 'interestRecalculationCompoundingMethod.fee', description: 'Fee' }, + { id: 0, code: 'interestRecalculationCompoundingMethod.none', value: 'None' }, + { id: 1, code: 'interestRecalculationCompoundingMethod.fee', value: 'Fee' }, ], interestRecalculationFrequencyTypeOptions: [ { id: 1, code: 'interestRecalculationFrequencyType.same.as.repayment.period', - description: 'Same as repayment period', + value: 'Same as repayment period', }, - { id: 2, code: 'interestRecalculationFrequencyType.daily', description: 'Daily' }, + { id: 2, code: 'interestRecalculationFrequencyType.daily', value: 'Daily' }, ], rescheduleStrategyTypeOptions: [ { id: 1, code: 'loanRescheduleStrategyMethod.reduce.emi.amount', - description: 'Reduce EMI amount', + value: 'Reduce EMI amount', }, ], preClosureInterestCalculationStrategyOptions: [ { id: 1, - code: 'preClosureInterestCalculationStrategy.tillPreClosureDate', - description: 'Till pre-close date', + code: 'loanPreClosureInterestCalculationStrategy.tillPreClosureDate', + value: 'Till pre-close date', }, ], repaymentStartDateTypeOptions: [ @@ -504,15 +504,30 @@ describe('LoanProductFormComponent', () => { expect(component.repaymentStartDateTypeOptions()).toHaveLength(2); }); - it('renders repayment start date labels from the template response', () => { - const select = fixture.nativeElement.querySelector( - '[data-testid="loan-product-repayment-start-date-type"]', - ); - const labels = Array.from(select.querySelectorAll('ion-select-option')).map( - (option) => option.textContent.trim(), - ); + it('renders every template enum label from the live response shape', () => { + component.onInterestRecalculationChange(true); + component.onCompoundingMethodChange(1); + fixture.detectChanges(); - expect(labels).toEqual(['Disbursement Date', 'Submitted On Date']); + const labels = (testId: string) => + Array.from( + fixture.nativeElement + .querySelector(`[data-testid="${CSS.escape(testId)}"]`) + .querySelectorAll('ion-select-option'), + ).map((option) => option.textContent.trim()); + + expect(labels('loan-product-compounding-method')).toEqual(['None', 'Fee']); + expect(labels('loan-product-reschedule-strategy')).toEqual(['Reduce EMI amount']); + expect(labels('loan-product-rest-frequency')).toEqual(['Same as repayment period', 'Daily']); + expect(labels('loan-product-compounding-frequency')).toEqual([ + 'Same as repayment period', + 'Daily', + ]); + expect(labels('loan-product-pre-closure-strategy')).toEqual(['Till pre-close date']); + expect(labels('loan-product-repayment-start-date-type')).toEqual([ + 'Disbursement Date', + 'Submitted On Date', + ]); }); }); diff --git a/src/app/features/products/loan-product-form.component.ts b/src/app/features/products/loan-product-form.component.ts index 81533b144..1e9231d15 100644 --- a/src/app/features/products/loan-product-form.component.ts +++ b/src/app/features/products/loan-product-form.component.ts @@ -87,10 +87,12 @@ import { TooltipDirective } from '../../shared/directives/tooltip.directive'; */ const DAILY_INTEREST_CALCULATION_PERIOD = 0; -/** The live template response uses `value`; the generated contract currently omits it. */ -type RepaymentStartDateTypeOption = GetLoanProductsRepaymentStartDateType & { - value?: string; -}; +/** + * Fineract serializes enum options as `{ id, code, value }` (`BaseEnumOptionData`). The generated + * loan-product models declare `description` instead, which the server does not send; see #653. + * Drop this compatibility type once the models are regenerated from the corrected contract. + */ +type TemplateEnumOption = T & { value?: string }; @Component({ selector: 'app-loan-product-form', @@ -997,7 +999,7 @@ type RepaymentStartDateTypeOption = GetLoanProductsRepaymentStartDateType & { > @for (option of compoundingTypeOptions(); track option.id) { {{ - option.description + enumLabel(option) }} } @@ -1024,7 +1026,7 @@ type RepaymentStartDateTypeOption = GetLoanProductsRepaymentStartDateType & { > @for (option of rescheduleStrategyOptions(); track option.id) { {{ - option.description + enumLabel(option) }} } @@ -1051,7 +1053,7 @@ type RepaymentStartDateTypeOption = GetLoanProductsRepaymentStartDateType & { > @for (option of recalculationFrequencyOptions(); track option.id) { {{ - option.description + enumLabel(option) }} } @@ -1103,7 +1105,7 @@ type RepaymentStartDateTypeOption = GetLoanProductsRepaymentStartDateType & { > @for (option of recalculationFrequencyOptions(); track option.id) { {{ - option.description + enumLabel(option) }} } @@ -1157,7 +1159,7 @@ type RepaymentStartDateTypeOption = GetLoanProductsRepaymentStartDateType & { > @for (option of preClosureStrategyOptions(); track option.id) { {{ - option.description + enumLabel(option) }} } @@ -1221,7 +1223,7 @@ type RepaymentStartDateTypeOption = GetLoanProductsRepaymentStartDateType & { > @for (option of repaymentStartDateTypeOptions(); track option.id) { {{ - option.value ?? option.description + enumLabel(option) }} } @@ -1426,17 +1428,21 @@ export class LoanProductFormComponent implements OnInit { readonly compoundingMethod = signal(undefined); readonly restFrequencyType = signal(undefined); readonly compoundingFrequencyType = signal(undefined); - readonly compoundingTypeOptions = signal( - [], - ); + readonly compoundingTypeOptions = signal< + TemplateEnumOption[] + >([]); readonly recalculationFrequencyOptions = signal< - GetLoanProductsInterestRecalculationCompoundingFrequencyType[] + TemplateEnumOption[] + >([]); + readonly rescheduleStrategyOptions = signal< + TemplateEnumOption[] >([]); - readonly rescheduleStrategyOptions = signal([]); readonly preClosureStrategyOptions = signal< - GetLoanProductsPreClosureInterestCalculationStrategy[] + TemplateEnumOption[] + >([]); + readonly repaymentStartDateTypeOptions = signal< + TemplateEnumOption[] >([]); - readonly repaymentStartDateTypeOptions = signal([]); readonly chargeOffBehaviourOptions = signal([]); /** @@ -1502,6 +1508,10 @@ export class LoanProductFormComponent implements OnInit { this.product.update((product) => ({ ...product, [field]: value })); } + protected enumLabel(option: { value?: string; description?: string; code?: string }): string { + return option.value ?? option.description ?? option.code ?? ''; + } + /** Applies multiple related product fields in one signal notification. */ private patchProductFields(patch: Partial): void { this.product.update((product) => ({ ...product, ...patch }));