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>
102 lines
3.9 KiB
TypeScript
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)
|
|
};
|
|
}
|