Files
cbsnexora/src/app/loan-origination/loan-fineract-bridge.ts
T
niteshandClaude Sonnet 5 c18219fce5
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
feat(loan-origination): persist the sanctioning pipeline to the backend
Replaces the in-memory mock (LOAN_APPLICATIONS array + seeded RNG, reset on
every refresh) with real Fineract-backed persistence: a new
LoanOriginationService for CRUD plus one API call per stage transition, a
loan-fineract-bridge that creates a real loan account (create + approve +
disburse) at disbursement for Existing Customers, and file uploads through
Fineract's generic document store keyed by a stable checklist slot so
"On file"/View controls survive a reload.

Also fixes several gaps found while verifying full-cycle persistence:
- CIC charge-account selection never saved (missing persist() call)
- Co-borrower relationship/monthly income lived in local-only component state
  instead of on the application, so they reset on refresh
- Field Investigation "View Full Details" showed local-session photos only;
  stored photos are now downloaded and shown as real thumbnails
- "Assign Field Officer" was free text; now a dropdown of real CBS loan
  officers pulled from /staff
- site photos and cheque/signature images now open in the same zoomable,
  pannable lightgallery viewer used elsewhere in the app

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 17:05:05 +05:30

102 lines
3.9 KiB
TypeScript

/**
* 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 { firstValueFrom } from 'rxjs';
import { Dates } from '../core/utils/dates';
import { LoansService } from '../loans/loans.service';
import { ProductsService } from '../products/products.service';
import { SettingsService } from '../settings/settings.service';
import { LoanApplication, ORIGINATION_PRODUCTS } from './loan-origination-data';
export interface RealLoanCreationResult {
loanId: number;
loanAccountNo: string;
}
const cleanName = (name: string): string => name.trim().toLowerCase().replace(/\s+/g, ' ');
/** Creates, approves and disburses a real Fineract loan account for an Existing-Customer
* application at the moment an officer disburses it -- see loan-fineract-bridge plan for why this
* only fires at final disbursement rather than at wizard-submit or Committee sanction. Every
* mandatory Fineract field this mock pipeline never collected (loan purpose, interest type,
* amortization type, interest calculation period, repayment strategy) is pulled from the real
* loan product's own template rather than guessed, so it's always valid for whatever that
* Fineract instance actually has configured. */
export async function createRealLoanAccount(
app: LoanApplication,
loansService: LoansService,
productsService: ProductsService,
dateUtils: Dates,
settings: SettingsService
): Promise<RealLoanCreationResult> {
const mockProduct = ORIGINATION_PRODUCTS.find((p) => p.code === app.productCode);
const wantedName = cleanName(mockProduct?.name || app.productName);
const products: any[] = (await firstValueFrom(productsService.getLoanProducts())) || [];
const matchedProduct = products.find((p) => cleanName(p.name) === wantedName);
if (!matchedProduct) {
throw new Error(
`No Fineract loan product named "${mockProduct?.name || app.productName}" was found -- ask an admin to configure one before disbursing.`
);
}
const template: any = await firstValueFrom(productsService.getLoanProduct(String(matchedProduct.id), true));
const repaymentEvery: number = template.repaymentEvery || 1;
const locale = settings.language.code;
const dateFormat = settings.dateFormat;
const today = dateUtils.formatDate(new Date(), dateFormat);
const payload = {
clientId: app.clientId,
productId: matchedProduct.id,
loanType: 'individual',
principal: app.requestedAmount,
loanTermFrequency: app.tenureMonths,
loanTermFrequencyType: 2, // Months
numberOfRepayments: Math.max(1, Math.round(app.tenureMonths / repaymentEvery)),
repaymentEvery,
repaymentFrequencyType: template.repaymentFrequencyType?.id,
interestRatePerPeriod: app.interestRate,
interestType: template.interestType?.id,
interestCalculationPeriodType: template.interestCalculationPeriodType?.id,
amortizationType: template.amortizationType?.id,
transactionProcessingStrategyCode: template.transactionProcessingStrategyCode,
submittedOnDate: today,
expectedDisbursementDate: today,
dateFormat,
locale
};
const created: any = await firstValueFrom(loansService.createLoansAccount(payload));
const loanId: number = created.loanId ?? created.resourceId;
await firstValueFrom(
loansService.loanActionButtons(loanId, 'approve', {
approvedOnDate: today,
dateFormat,
locale
})
);
await firstValueFrom(
loansService.loanActionButtons(loanId, 'disburse', {
actualDisbursementDate: today,
transactionAmount: app.requestedAmount,
dateFormat,
locale
})
);
const account: any = await firstValueFrom(loansService.getLoanAccountResource(String(loanId), ''));
return {
loanId,
loanAccountNo: account?.accountNo || String(loanId)
};
}