implementation of loan product creation
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s

This commit is contained in:
Sonali Likhitkar
2026-09-01 11:13:33 +05:30
parent eb1aa767f7
commit a1de1aab93
13 changed files with 3430 additions and 7 deletions
@@ -1466,6 +1466,26 @@ public class LoanProductData implements Serializable {
this.allowAttributeOverrides = loanProductConfigurableAttributes;
}
/**
* The legacy "Account Type Creation" fields, flat and keyed by their form field name.
*
* Set after construction rather than through a constructor parameter: the main constructor already
* takes over a hundred arguments, and threading two more through it would touch every call site for
* no benefit. Follows the same pattern as setLoanProductConfigurableAttributes above.
*/
private Map<String, Object> cbsExtensions;
/** The legacy Interest Rate Detail grid: one entry per interest type, in form order. */
private List<Map<String, Object>> interestRateDetail;
public void setCbsExtensions(final Map<String, Object> cbsExtensions) {
this.cbsExtensions = cbsExtensions;
}
public void setInterestRateDetail(final List<Map<String, Object>> interestRateDetail) {
this.interestRateDetail = interestRateDetail;
}
public boolean isIsLinkedToFloatingInterestRates() {
return this.isLinkedToFloatingInterestRates;
}
@@ -0,0 +1,740 @@
/**
* 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/.
*/
package org.apache.fineract.portfolio.loanproduct.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* The legacy "Account Type Creation" configuration for a loan product.
*
* Fineract's loan product schema has no home for these fields, and they are specific to this
* deployment, so they hang off {@link LoanProduct} 1:1 instead of widening m_product_loan - an
* upstream change to that table can then never collide with them.
*
* Loan account creation reads this at origination time, which is why these are typed columns rather
* than a JSON blob: callers get a compile-checked getter and reports get a plain column.
*/
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "m_product_loan_cbs_ext")
public class LoanProductCbsExt {
@Id
@Column(name = "loan_product_id")
private Long loanProductId;
/**
* Shared primary key: this and {@link #loanProductId} are the same column, so the association is
* mapped read-only and {@code loanProductId} is the one that writes it. EclipseLink rejects an
* entity with two writable mappings for one field.
*/
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "loan_product_id", insertable = false, updatable = false)
private LoanProduct loanProduct;
// ---- classification & legacy header fields (24) ---------------------
@Column(name = "product_type_group")
private String productTypeGroup;
@Column(name = "product_type_sub_group")
private String productTypeSubGroup;
@Column(name = "debit_int_calculation")
private Boolean debitIntCalculation;
@Column(name = "separate_penal_interest")
private Boolean separatePenalInterest;
@Column(name = "credit_int_calculation")
private Boolean creditIntCalculation;
@Column(name = "sub_account_type_balance")
private Boolean subAccountTypeBalance;
@Column(name = "final_int_rounding")
private Boolean finalIntRounding;
@Column(name = "rounding_up_to")
private Integer roundingUpTo;
@Column(name = "account_type_code")
private String accountTypeCode;
@Column(name = "micr_transaction_code")
private String micrTransactionCode;
@Column(name = "reserved_ecs_serial_number")
private String reservedEcsSerialNumber;
@Column(name = "reverse_ecs_number")
private String reverseEcsNumber;
@Column(name = "lock_cheque_info_enabled")
private Boolean lockChequeInfoEnabled;
@Column(name = "lock_page_two_info_enabled")
private Boolean lockPageTwoInfoEnabled;
@Column(name = "computerization_date")
private LocalDate computerizationDate;
@Column(name = "cheque_print_description")
private String chequePrintDescription;
@Column(name = "pos_account_type_description")
private String posAccountTypeDescription;
@Column(name = "pfms_account_type")
private String pfmsAccountType;
@Column(name = "renewal_date_add_months")
private Integer renewalDateAddMonths;
@Column(name = "document_renewal_date_add_months")
private Integer documentRenewalDateAddMonths;
@Column(name = "pps_limit")
private BigDecimal ppsLimit;
@Column(name = "is_write_off_account_type")
private Boolean isWriteOffAccountType;
@Column(name = "write_off_account_type_code")
private String writeOffAccountTypeCode;
@Column(name = "renewal_grace_days")
private Integer renewalGraceDays;
// ---- Cheque Book Settings (10) --------------------------------------
@Column(name = "cheque_book_issue_enabled")
private Boolean chequeBookIssueEnabled;
@Column(name = "cheque_validation_required")
private Boolean chequeValidationRequired;
@Column(name = "is_first_cheque_book_free")
private Boolean isFirstChequeBookFree;
@Column(name = "cheque_printing_account_type")
private String chequePrintingAccountType;
@Column(name = "allow_withdrawal_slip_usage")
private Boolean allowWithdrawalSlipUsage;
@Column(name = "maintain_cheque_book_stock_registry")
private Boolean maintainChequeBookStockRegistry;
@Column(name = "free_cheque_books_per_year")
private Integer freeChequeBooksPerYear;
@Column(name = "allow_credit_by_cheque_number")
private Boolean allowCreditByChequeNumber;
@Column(name = "credit_cheque_validation_required")
private Boolean creditChequeValidationRequired;
@Column(name = "grace_days_for_stock_statement")
private Integer graceDaysForStockStatement;
// ---- Additional Product Settings (37) -------------------------------
@Column(name = "account_type_active_for_transactions")
private Boolean accountTypeActiveForTransactions;
@Column(name = "sub_type_activity_enabled")
private Boolean subTypeActivityEnabled;
@Column(name = "account_opening_charges")
private BigDecimal accountOpeningCharges;
@Column(name = "maximum_gold_loan_limit")
private BigDecimal maximumGoldLoanLimit;
@Column(name = "last_account_number_opened")
private String lastAccountNumberOpened;
@Column(name = "customer_termination_allowed")
private Boolean customerTerminationAllowed;
@Column(name = "charges_for_cash_credit_above_limit")
private BigDecimal chargesForCashCreditAboveLimit;
@Column(name = "death_interest_gl_account")
private String deathInterestGLAccount;
@Column(name = "starting_ledger_number")
private Integer startingLedgerNumber;
@Column(name = "ending_ledger_number")
private Integer endingLedgerNumber;
@Column(name = "current_ledger_number")
private Integer currentLedgerNumber;
@Column(name = "remittance_account_number")
private String remittanceAccountNumber;
@Column(name = "is_non_resident_indian_account")
private Boolean isNonResidentIndianAccount;
@Column(name = "debit_clearing_gl_account")
private String debitClearingGLAccount;
@Column(name = "missed_call_alert_service_active")
private Boolean missedCallAlertServiceActive;
@Column(name = "standing_instruction_charge_gl_account")
private String standingInstructionChargeGLAccount;
@Column(name = "standing_instruction_debit_charge_skip_option")
private String standingInstructionDebitChargeSkipOption;
@Column(name = "opening_balance_multiplier_for_remittance_charges")
private BigDecimal openingBalanceMultiplierForRemittanceCharges;
@Column(name = "gold_loan_packet_number_with_year_enabled")
private Boolean goldLoanPacketNumberWithYearEnabled;
@Column(name = "account_move_to_inoperative_enabled")
private Boolean accountMoveToInoperativeEnabled;
@Column(name = "generate_gl_diff_entry_enabled")
private Boolean generateGlDiffEntryEnabled;
@Column(name = "sub_type_wise_gl_balance_generation_enabled")
private Boolean subTypeWiseGlBalanceGenerationEnabled;
@Column(name = "validate_before_deleting_debit_entry_enabled")
private Boolean validateBeforeDeletingDebitEntryEnabled;
@Column(name = "rc_file_concept_enabled")
private Boolean rcFileConceptEnabled;
@Column(name = "account_close_zero_balance_in_open_day_process")
private Boolean accountCloseZeroBalanceInOpenDayProcess;
@Column(name = "account_close_zero_balance_at_subledger_tran_entry")
private Boolean accountCloseZeroBalanceAtSubledgerTranEntry;
@Column(name = "limit_enhancement_against_clearing_screen_display_enabled")
private Boolean limitEnhancementAgainstClearingScreenDisplayEnabled;
@Column(name = "advance_interest_parameter_in_ledger_enabled")
private Boolean advanceInterestParameterInLedgerEnabled;
@Column(name = "vehicle_detail_in_master_enabled")
private Boolean vehicleDetailInMasterEnabled;
@Column(name = "deleted_cheque_issue_enabled")
private Boolean deletedChequeIssueEnabled;
@Column(name = "dp_reduce_in_document_renewal_date_enabled")
private Boolean dpReduceInDocumentRenewalDateEnabled;
@Column(name = "dp_reduce_in_renewal_date_enabled")
private Boolean dpReduceInRenewalDateEnabled;
@Column(name = "auto_close_at_day_end_enabled")
private Boolean autoCloseAtDayEndEnabled;
@Column(name = "loan_rate_from_master_enabled")
private Boolean loanRateFromMasterEnabled;
@Column(name = "cheque_short_name")
private String chequeShortName;
@Column(name = "statement_print_enabled")
private Boolean statementPrintEnabled;
@Column(name = "nesl_code")
private String neslCode;
// ---- Interest Related Parameters (53) -------------------------------
@Column(name = "drawing_power_and_limit_generation_procedure")
private String drawingPowerAndLimitGenerationProcedure;
@Column(name = "advances_interest_calculation_method")
private String advancesInterestCalculationMethod;
@Column(name = "against_interest_calculation_enabled")
private Boolean againstInterestCalculationEnabled;
@Column(name = "day_wise_against_clearing_interest_rounding")
private Boolean dayWiseAgainstClearingInterestRounding;
@Column(name = "interest_calculation_one_fourth_method")
private String interestCalculationOneFourthMethod;
@Column(name = "against_clearing_interest_procedure_type")
private String againstClearingInterestProcedureType;
@Column(name = "consider_debit_closing_amount_for_against_clearing")
private Boolean considerDebitClosingAmountForAgainstClearing;
@Column(name = "interest_rounding_method")
private String interestRoundingMethod;
@Column(name = "penalty_interest_calculation_enabled")
private Boolean penaltyInterestCalculationEnabled;
@Column(name = "penalty_interest_calculation_method")
private String penaltyInterestCalculationMethod;
@Column(name = "penalty_interest_procedure_number")
private String penaltyInterestProcedureNumber;
@Column(name = "overdrawn_interest_calculation_method")
private String overdrawnInterestCalculationMethod;
@Column(name = "calculate_overdrawn_interest_after_renewal")
private Boolean calculateOverdrawnInterestAfterRenewal;
@Column(name = "interest_on_credit_balance")
private Boolean interestOnCreditBalance;
@Column(name = "penalty_interest_date_different_than_regular_interest")
private Boolean penaltyInterestDateDifferentThanRegularInterest;
@Column(name = "fixed_deposit_overdraft_certificate_sort_order")
private String fixedDepositOverdraftCertificateSortOrder;
@Column(name = "use_document_submission_date_for_interest")
private Boolean useDocumentSubmissionDateForInterest;
@Column(name = "loan_management_system_renewal_penalty_active")
private Boolean loanManagementSystemRenewalPenaltyActive;
@Column(name = "minimum_normal_interest_amount")
private BigDecimal minimumNormalInterestAmount;
@Column(name = "minimum_interest_calculation_method")
private String minimumInterestCalculationMethod;
@Column(name = "minimum_against_clearing_interest_amount")
private BigDecimal minimumAgainstClearingInterestAmount;
@Column(name = "limit_increase_upto_interest_amount_in_interest_calculation")
private Boolean limitIncreaseUptoInterestAmountInInterestCalculation;
@Column(name = "minimum_penalty_interest_amount")
private BigDecimal minimumPenaltyInterestAmount;
@Column(name = "days_for_limit_increase_upto_interest_amount")
private Integer daysForLimitIncreaseUptoInterestAmount;
@Column(name = "additional_interest_rate_for_fixed_deposit_overdraft")
private BigDecimal additionalInterestRateForFixedDepositOverdraft;
@Column(name = "consider_dd_payorder_in_against_clearing_interest_calculation")
private Boolean considerDdPayorderInAgainstClearingInterestCalculation;
@Column(name = "grace_days_after_renew_date")
private Integer graceDaysAfterRenewDate;
@Column(name = "apply_undebited_interest_concept_enabled")
private Boolean applyUndebitedInterestConceptEnabled;
@Column(name = "interest_register_print_format_type")
private String interestRegisterPrintFormatType;
@Column(name = "normal_grace_days_after_overdue")
private Integer normalGraceDaysAfterOverdue;
@Column(name = "allow_against_clearing_entry")
private Boolean allowAgainstClearingEntry;
@Column(name = "allow_no_balance_entry")
private Boolean allowNoBalanceEntry;
@Column(name = "fd_od_penalty_interest_combined_with_normal_interest")
private Boolean fixedDepositOverdraftPenaltyInterestCombinedWithNormalInterest;
@Column(name = "allow_no_balance_entry_with_password")
private Boolean allowNoBalanceEntryWithPassword;
@Column(name = "undebited_simple_interest_account_wise")
private Boolean undebitedSimpleInterestAccountWise;
@Column(name = "daily_interest_calculation_enabled")
private Boolean dailyInterestCalculationEnabled;
@Column(name = "daily_interest_calculation_start_date")
private String dailyInterestCalculationStartDate;
@Column(name = "non_performing_asset_penal_interest_rate_update")
private Boolean nonPerformingAssetPenalInterestRateUpdate;
@Column(name = "non_performing_asset_penal_interest_rate")
private BigDecimal nonPerformingAssetPenalInterestRate;
@Column(name = "fd_od_penal_interest_rate_update_in_branch_process")
private Boolean fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess;
@Column(name = "add_penal_interest_rate")
private BigDecimal addPenalInterestRate;
@Column(name = "atyp14_simple_interest_enabled")
private Boolean atyp14SimpleInterestEnabled;
@Column(name = "fixed_deposit_overdraft_margin_type")
private String fixedDepositOverdraftMarginType;
@Column(name = "check_master_detail_edited_passing_pending_enabled")
private Boolean checkMasterDetailEditedPassingPendingEnabled;
@Column(name = "credit_debit_interest_calculation_separate_enabled")
private Boolean creditDebitInterestCalculationSeparateEnabled;
@Column(name = "on_time_interest_provision_enabled")
private Boolean onTimeInterestProvisionEnabled;
@Column(name = "check_against_clearing_enabled")
private Boolean checkAgainstClearingEnabled;
@Column(name = "gst_on_penalty_enabled")
private Boolean gstOnPenaltyEnabled;
@Column(name = "penalty_interest_grace_days")
private Integer penaltyInterestGraceDays;
@Column(name = "overdrawn_interest_rate")
private BigDecimal overdrawnInterestRate;
@Column(name = "calculate_daily_limit_enhancement_tod_enabled")
private Boolean calculateDailyLimitEnhancementTodEnabled;
@Column(name = "fixed_deposit_overdraft_multiple_interest_rate_enabled")
private Boolean fixedDepositOverdraftMultipleInterestRateEnabled;
@Column(name = "grace_days_after_overdue_for_moratorium")
private Integer graceDaysAfterOverdueForMoratorium;
// ---- Credit Bureau Settings (CIBIL) (8) ----------------------------
@Column(name = "cibil_account_type")
private String cibilAccountType;
@Column(name = "cibil_commercial_account_type")
private String cibilCommercialAccountType;
@Column(name = "highmark_account_type")
private String highmarkAccountType;
@Column(name = "highmark_commercial_account_type")
private String highmarkCommercialAccountType;
@Column(name = "experian_account_type")
private String experianAccountType;
@Column(name = "experian_commercial_account_type")
private String experianCommercialAccountType;
@Column(name = "equifax_account_type")
private String equifaxAccountType;
@Column(name = "equifax_commercial_account_type")
private String equifaxCommercialAccountType;
// ---- RBI Compliance & Product Category (6) -------------------------
@Column(name = "loan_product_category")
private String loanProductCategory;
@Column(name = "penal_charge_method")
private String penalChargeMethod;
@Column(name = "is_floating_rate")
private Boolean isFloatingRate;
@Column(name = "foreclosure_charges_applicable")
private Boolean foreclosureChargesApplicable;
@Column(name = "key_fact_statement_enabled")
private Boolean keyFactStatementEnabled;
@Column(name = "annual_percentage_rate_calculation_enabled")
private Boolean annualPercentageRateCalculationEnabled;
// ---- Channel & Transaction Limits (23) ------------------------------
@Column(name = "is_mobile_app_enabled")
private Boolean isMobileAppEnabled;
@Column(name = "is_atm_enabled")
private Boolean isAtmEnabled;
@Column(name = "atm_yearly_charge_enabled")
private Boolean atmYearlyChargeEnabled;
@Column(name = "atm_minimum_balance")
private BigDecimal atmMinimumBalance;
@Column(name = "email_active_enabled")
private Boolean emailActiveEnabled;
@Column(name = "is_imps_enabled")
private Boolean isImpsEnabled;
@Column(name = "is_upi_enabled")
private Boolean isUpiEnabled;
@Column(name = "is_pos_enabled")
private Boolean isPosEnabled;
@Column(name = "is_bbps_enabled")
private Boolean isBbpsEnabled;
@Column(name = "inter_bank_limit_per_day")
private BigDecimal interBankLimitPerDay;
@Column(name = "inter_bank_limit_per_transaction")
private BigDecimal interBankLimitPerTransaction;
@Column(name = "neft_rtgs_limit_per_day")
private BigDecimal neftRtgsLimitPerDay;
@Column(name = "neft_rtgs_limit_per_transaction")
private BigDecimal neftRtgsLimitPerTransaction;
@Column(name = "neft_cutoff_limit")
private BigDecimal neftCutoffLimit;
@Column(name = "rtgs_cutoff_limit")
private BigDecimal rtgsCutoffLimit;
@Column(name = "neft_rtgs_minimum_balance")
private BigDecimal neftRtgsMinimumBalance;
@Column(name = "is_charge_required_neft_rtgs_cbs")
private Boolean isChargeRequiredNeftRtgsCbs;
@Column(name = "is_charge_required_neft_rtgs_mobile")
private Boolean isChargeRequiredNeftRtgsMobile;
@Column(name = "imps_limit_per_day")
private BigDecimal impsLimitPerDay;
@Column(name = "imps_limit_per_transaction")
private BigDecimal impsLimitPerTransaction;
@Column(name = "imps_limit_per_month")
private BigDecimal impsLimitPerMonth;
@Column(name = "imps_min_limit")
private BigDecimal impsMinLimit;
@Column(name = "imps_monthly_transaction_count")
private Integer impsMonthlyTransactionCount;
// ---- POS Facility & ATM CSV Settings (10) ---------------------------
@Column(name = "max_per_transaction_limit_atm")
private BigDecimal maxPerTransactionLimitAtm;
@Column(name = "max_day_transaction_limit_atm")
private BigDecimal maxDayTransactionLimitAtm;
@Column(name = "max_per_transaction_limit_pos")
private BigDecimal maxPerTransactionLimitPos;
@Column(name = "max_day_transaction_limit_pos")
private BigDecimal maxDayTransactionLimitPos;
@Column(name = "max_per_transaction_limit_ecom")
private BigDecimal maxPerTransactionLimitEcom;
@Column(name = "max_day_transaction_limit_ecom")
private BigDecimal maxDayTransactionLimitEcom;
@Column(name = "max_per_transaction_limit_upi")
private BigDecimal maxPerTransactionLimitUpi;
@Column(name = "max_day_transaction_limit_upi")
private BigDecimal maxDayTransactionLimitUpi;
@Column(name = "atm_csv_product_code")
private String atmCsvProductCode;
@Column(name = "atm_csv_type_code")
private String atmCsvTypeCode;
// ---- Auto Interest Post Parameters (4) -----------------------------
@Column(name = "auto_interest_post_type")
private String autoInterestPostType;
@Column(name = "auto_interest_post_when")
private String autoInterestPostWhen;
@Column(name = "auto_interest_post_fix_day")
private Integer autoInterestPostFixDay;
@Column(name = "auto_last_interest_date")
private String autoLastInterestDate;
// ---- Foreclosure / Early Account Closure (3) -----------------------
@Column(name = "calculate_early_account_close_charge")
private Boolean calculateEarlyAccountCloseCharge;
@Column(name = "early_account_close_charge")
private BigDecimal earlyAccountCloseCharge;
@Column(name = "early_account_close_period_in_months")
private Integer earlyAccountClosePeriodInMonths;
// ---- Terms step (3) ------------------------------------------------
@Column(name = "debit_balance_allow")
private Boolean debitBalanceAllow;
@Column(name = "allow_debit_trans_after_due_date")
private Boolean allowDebitTransAfterDueDate;
@Column(name = "branch_level_account_type")
private Boolean branchLevelAccountType;
// ---- legacy cheque / standing instruction / minimum balance charges (24)
@Column(name = "cheque_book_charge_enabled")
private Boolean chequeBookChargeEnabled;
@Column(name = "cheque_return_charge_enabled")
private Boolean chequeReturnChargeEnabled;
@Column(name = "ic_charge_debit_enabled")
private Boolean icChargeDebitEnabled;
@Column(name = "cash_bundle_charge_enabled")
private Boolean cashBundleChargeEnabled;
@Column(name = "cash_bundle_charge_with_pan_limit")
private BigDecimal cashBundleChargeWithPanLimit;
@Column(name = "per_cheque_entry_charge")
private BigDecimal perChequeEntryCharge;
@Column(name = "per_cheque_entry_service_charge")
private BigDecimal perChequeEntryServiceCharge;
@Column(name = "per_inward_cheque_entry_charge")
private BigDecimal perInwardChequeEntryCharge;
@Column(name = "per_outward_cheque_entry_charge")
private BigDecimal perOutwardChequeEntryCharge;
@Column(name = "creditor_standing_instruction_fail_charge")
private BigDecimal creditorStandingInstructionFailCharge;
@Column(name = "debtor_standing_instruction_fail_charge")
private BigDecimal debtorStandingInstructionFailCharge;
@Column(name = "minimum_balance_checking_type")
private String minimumBalanceCheckingType;
@Column(name = "auto_minimum_balance_charge")
private Boolean autoMinimumBalanceCharge;
@Column(name = "calculate_daily_minimum_balance_charges")
private Boolean calculateDailyMinimumBalanceCharges;
@Column(name = "minimum_balance_charge_enabled")
private Boolean minimumBalanceChargeEnabled;
@Column(name = "minimum_balance_charge_amount")
private BigDecimal minimumBalanceChargeAmount;
@Column(name = "minimum_balance_charge_for_cheque_issue")
private BigDecimal minimumBalanceChargeForChequeIssue;
@Column(name = "minimum_balance_charge_method")
private String minimumBalanceChargeMethod;
@Column(name = "months_after_min_balance_charge_calculate")
private Integer monthsAfterMinBalanceChargeCalculate;
@Column(name = "for_cheque_issue")
private BigDecimal forChequeIssue;
@Column(name = "for_cheque_issue_avg")
private BigDecimal forChequeIssueAvg;
@Column(name = "for_withdrawal_slip")
private BigDecimal forWithdrawalSlip;
@Column(name = "for_withdrawal_slip_avg")
private BigDecimal forWithdrawalSlipAvg;
@Column(name = "calculate_daily_against_clearing_charge")
private Boolean calculateDailyAgainstClearingCharge;
// ---- custom GL account mappings (14) --------------------------------
@Column(name = "int_receivables_gl_account_id")
private Long intReceivablesGlAccountId;
@Column(name = "int_payable_gl_account_id")
private Long intPayableGlAccountId;
@Column(name = "deaf_int_payable_gl_account_id")
private Long deafIntPayableGlAccountId;
@Column(name = "int_overdue_reserve_account_id")
private Long intOverdueReserveAccountId;
@Column(name = "undebited_interest_account_id")
private Long undebitedInterestAccountId;
@Column(name = "penal_int_cr_gl_account_id")
private Long penalIntCrGlAccountId;
@Column(name = "rd_matured_gl_account_id")
private Long rdMaturedGlAccountId;
@Column(name = "gl_account_id")
private Long glAccountId;
@Column(name = "ic_charge_gl_account_id")
private Long icChargeGlAccountId;
@Column(name = "inoperative_gl_account_id")
private Long inoperativeGlAccountId;
@Column(name = "credit_balance_liability_gl_account_id")
private Long creditBalanceLiabilityGlAccountId;
@Column(name = "credit_balance_asset_gl_account_id")
private Long creditBalanceAssetGlAccountId;
@Column(name = "overdue_accounts_gl_account_id")
private Long overdueAccountsGlAccountId;
@Column(name = "rebate_gl_account_id")
private Long rebateGlAccountId;
public LoanProductCbsExt(final LoanProduct loanProduct) {
this.loanProduct = loanProduct;
this.loanProductId = loanProduct.getId();
}
}
@@ -0,0 +1,20 @@
/**
* 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/.
*/
package org.apache.fineract.portfolio.loanproduct.domain;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface LoanProductCbsExtRepository extends JpaRepository<LoanProductCbsExt, Long> {
/**
* A product created before this table existed - or one saved without any legacy field filled in - has
* no row here, so callers must handle the empty case rather than assuming one exists.
*/
Optional<LoanProductCbsExt> findByLoanProductId(Long loanProductId);
}
@@ -0,0 +1,82 @@
/**
* 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/.
*/
package org.apache.fineract.portfolio.loanproduct.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
/**
* One row of the legacy "Interest Rate Detail" grid on the loan product form.
*
* The grid is a fixed four rows - Normal Debit, Normal Credit, Against Clearing and Penalty - each
* carrying a Normal and a Special min/max rate plus the period they apply over. It is a child table
* rather than twenty-four columns on {@link LoanProductCbsExt} because the interest type reads far
* better as data than as part of a column name: against_clearing_special_max_rate and its siblings
* are almost unreadable, and any new interest type would mean another migration.
*
* The rows are fixed today, so {@link #INTEREST_TYPES} is the full set and the table carries a unique
* constraint on (product, interest type) - a replayed save updates in place instead of doubling the
* grid.
*/
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "m_product_loan_cbs_interest_rate", uniqueConstraints = {
@UniqueConstraint(columnNames = { "loan_product_id", "interest_type" }, name = "mplcir_unq_product_interest_type") })
public class LoanProductCbsInterestRate extends AbstractPersistableCustom<Long> {
/** The grid's rows, in the order the form shows them. */
public static final String[] INTEREST_TYPES = { "Normal Debit", "Normal Credit", "Against Clearing", "Penalty" };
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "loan_product_id", nullable = false)
private LoanProduct loanProduct;
@Column(name = "interest_type", nullable = false, length = 30)
private String interestType;
@Column(name = "calculation")
private Boolean calculation;
@Column(name = "normal_min_rate", precision = 9, scale = 6)
private BigDecimal normalMinRate;
@Column(name = "normal_max_rate", precision = 9, scale = 6)
private BigDecimal normalMaxRate;
@Column(name = "special_min_rate", precision = 9, scale = 6)
private BigDecimal specialMinRate;
@Column(name = "special_max_rate", precision = 9, scale = 6)
private BigDecimal specialMaxRate;
@Column(name = "period", length = 20)
private String period;
public LoanProductCbsInterestRate(final LoanProduct loanProduct, final String interestType) {
this.loanProduct = loanProduct;
this.interestType = interestType;
}
/** True when the officer left this row untouched, so it need not be persisted. */
public boolean isEmpty() {
return !Boolean.TRUE.equals(this.calculation) && this.normalMinRate == null && this.normalMaxRate == null
&& this.specialMinRate == null && this.specialMaxRate == null && (this.period == null || this.period.isBlank());
}
}
@@ -0,0 +1,18 @@
/**
* 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/.
*/
package org.apache.fineract.portfolio.loanproduct.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface LoanProductCbsInterestRateRepository extends JpaRepository<LoanProductCbsInterestRate, Long> {
List<LoanProductCbsInterestRate> findByLoanProductIdOrderById(Long loanProductId);
LoanProductCbsInterestRate findByLoanProductIdAndInterestType(Long loanProductId, String interestType);
}