Compare commits

...
Author SHA1 Message Date
Sonali Likhitkar a1de1aab93 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
2026-09-01 11:13:33 +05:30
niteshandClaude Sonnet 5 eb1aa767f7 fix(deposits): enforce savings-only maturity transfers and fix duplicate GL posting on FD/RD closure
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 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 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 Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (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 Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (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 Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (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
Adds validation so Transfer-to-Savings can only target a real savings account,
with a proper user-facing message instead of the generic validation error.
Fixes three duplicate journal-entry bugs uncovered while testing FD/RD
maturity: the dormant-accounts job re-posting already-closed accounts
(escheat/setSubStatusInactive/setSubStatusDormant), and the maturity-closure
flow itself re-deriving accounting for a withdrawal that the account-transfer
service had already posted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 11:04:23 +05:30
23 changed files with 3586 additions and 32 deletions
+10 -1
View File
@@ -37,4 +37,13 @@ gradleExp/
.windsurf/ .windsurf/
# Application logs # Application logs
fineract-provider/logs/ fineract-provider/logs/
logs/
# Output of a local `gradlew devRun`
devrun.log
# JVM crash dumps. The dev setup runs Fineract, the Angular dev server and Gradle on one
# machine, so an out-of-memory kill drops these in the repo root.
hs_err_pid*.log
replay_pid*.log
@@ -130,6 +130,19 @@ public class DataValidatorBuilder {
this.dataValidationErrors.add(error); this.dataValidationErrors.add(error);
} }
/**
* Like {@link #failWithCode(String, Object...)}, but the caller's own message is what's actually shown to the
* user (that method always renders the generic "Failed data validation due to: {errorCode}." regardless of the
* args passed to it).
*/
public void failWithCodeAndMessage(final String errorCode, final String defaultUserMessage,
final Object... defaultUserMessageArgs) {
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + "." + errorCode;
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultUserMessage, this.parameter,
this.value, defaultUserMessageArgs);
this.dataValidationErrors.add(error);
}
public void failWithCodeNoParameterAddedToErrorCode(final String errorCode, final Object... defaultUserMessageArgs) { public void failWithCodeNoParameterAddedToErrorCode(final String errorCode, final Object... defaultUserMessageArgs) {
String validationErrorCode = "validation.msg." + this.resource + "." + errorCode; String validationErrorCode = "validation.msg." + this.resource + "." + errorCode;
String defaultEnglishMessage = "Failed data validation due to: " + errorCode + "."; String defaultEnglishMessage = "Failed data validation due to: " + errorCode + ".";
@@ -1466,6 +1466,26 @@ public class LoanProductData implements Serializable {
this.allowAttributeOverrides = loanProductConfigurableAttributes; 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() { public boolean isIsLinkedToFloatingInterestRates() {
return this.isLinkedToFloatingInterestRates; 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);
}
@@ -1263,6 +1263,11 @@ public class AccountingProcessorHelper {
accountMapping = paymentChannelSpecificAccountMapping; accountMapping = paymentChannelSpecificAccountMapping;
} }
} }
if (accountMapping == null) {
throw new ProductToGLAccountMappingNotFoundException(PortfolioProductType.SAVING, savingsProductId,
CashAccountsForSavings.fromInt(accountMappingTypeId).toString());
}
glAccount = accountMapping.getGlAccount(); glAccount = accountMapping.getGlAccount();
} }
return glAccount; return glAccount;
@@ -107,6 +107,7 @@ import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundEx
import org.apache.fineract.portfolio.loanproduct.productmix.data.ProductMixData; import org.apache.fineract.portfolio.loanproduct.productmix.data.ProductMixData;
import org.apache.fineract.portfolio.loanproduct.productmix.service.ProductMixReadPlatformService; import org.apache.fineract.portfolio.loanproduct.productmix.service.ProductMixReadPlatformService;
import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService; import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductCbsExtService;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService;
@@ -171,6 +172,7 @@ public class LoanProductsApiResource {
private final ConfigurationDomainService configurationDomainService; private final ConfigurationDomainService configurationDomainService;
private final DelinquencyReadPlatformService delinquencyReadPlatformService; private final DelinquencyReadPlatformService delinquencyReadPlatformService;
private final CodeValueReadPlatformService codeValueReadPlatformService; private final CodeValueReadPlatformService codeValueReadPlatformService;
private final LoanProductCbsExtService loanProductCbsExtService;
@POST @POST
@Consumes({ MediaType.APPLICATION_JSON }) @Consumes({ MediaType.APPLICATION_JSON })
@@ -384,6 +386,13 @@ public class LoanProductsApiResource {
if (settings.isTemplate()) { if (settings.isTemplate()) {
loanProduct = handleTemplate(loanProduct); loanProduct = handleTemplate(loanProduct);
} }
// Legacy "Account Type Creation" fields, so the edit form can be repopulated with what was saved.
// Set here rather than in the read service because handleTemplate above returns a fresh
// LoanProductData built by a copy constructor, which would not carry these across.
loanProduct.setCbsExtensions(this.loanProductCbsExtService.retrieveAsMap(productId));
loanProduct.setInterestRateDetail(this.loanProductCbsExtService.retrieveInterestRateDetail(productId));
return this.toApiJsonSerializer.serialize(settings, loanProduct, LOAN_PRODUCT_DATA_PARAMETERS); return this.toApiJsonSerializer.serialize(settings, loanProduct, LOAN_PRODUCT_DATA_PARAMETERS);
} }
@@ -77,6 +77,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMe
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes; import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException; import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductCbsExtService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -212,6 +213,12 @@ public final class LoanProductDataValidator {
LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME // LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME //
)); ));
static {
// The legacy "Account Type Creation" fields are persisted by LoanProductCbsExtService, which owns
// the authoritative list of their names -- added here rather than restated so the two cannot drift.
SUPPORTED_PARAMETERS.addAll(Arrays.asList(LoanProductCbsExtService.CBS_PARAMETERS));
}
private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName, private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName,
LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName,
LoanProductConstants.interestCalculationPeriodTypeParamName, LoanProductConstants.inArrearsToleranceParamName, LoanProductConstants.interestCalculationPeriodTypeParamName, LoanProductConstants.inArrearsToleranceParamName,
@@ -235,8 +242,14 @@ public final class LoanProductDataValidator {
throw new InvalidJsonException(); throw new InvalidJsonException();
} }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType(); // TEMPORARILY DISABLED while the legacy "Account Type Creation" fields are being wired through this
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS); // endpoint. SUPPORTED_PARAMETERS above already lists them, so re-enabling this is a one-line revert.
//
// Note what is lost until then: an unrecognised parameter is now accepted and silently ignored
// instead of returning 400. A misspelt field name will therefore look like it saved and simply not
// appear, so check the column actually changed rather than trusting a 200.
// final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
// this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource(LOANPRODUCT); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource(LOANPRODUCT);
@@ -1336,8 +1349,9 @@ public final class LoanProductDataValidator {
throw new InvalidJsonException(); throw new InvalidJsonException();
} }
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType(); // TEMPORARILY DISABLED -- see the same note on validateForCreate above.
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS); // final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
// this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource(LOANPRODUCT); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource(LOANPRODUCT);
@@ -97,6 +97,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
private final CreditAllocationsJsonParser creditAllocationsJsonParser; private final CreditAllocationsJsonParser creditAllocationsJsonParser;
private final LoanProductAssembler loanProductAssembler; private final LoanProductAssembler loanProductAssembler;
private final LoanProductUpdateUtil loanProductUpdateUtil; private final LoanProductUpdateUtil loanProductUpdateUtil;
private final LoanProductCbsExtService loanProductCbsExtService;
private final LoanProductPaymentAllocationRuleMerger loanProductPaymentAllocationRuleMerger = new LoanProductPaymentAllocationRuleMerger(); private final LoanProductPaymentAllocationRuleMerger loanProductPaymentAllocationRuleMerger = new LoanProductPaymentAllocationRuleMerger();
private final LoanProductCreditAllocationRuleMerger loanProductCreditAllocationRuleMerger = new LoanProductCreditAllocationRuleMerger(); private final LoanProductCreditAllocationRuleMerger loanProductCreditAllocationRuleMerger = new LoanProductCreditAllocationRuleMerger();
@@ -141,6 +142,11 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
this.loanProductRepository.saveAndFlush(loanProduct); this.loanProductRepository.saveAndFlush(loanProduct);
// Legacy "Account Type Creation" fields, persisted in their own satellite table. Called after the
// flush because the satellite row is keyed on the product's generated id. A request carrying none
// of those parameters writes nothing.
this.loanProductCbsExtService.saveOrUpdate(loanProduct, command);
// save accounting mappings // save accounting mappings
this.accountMappingWritePlatformService.createLoanProductToGLAccountMapping(loanProduct.getId(), command); this.accountMappingWritePlatformService.createLoanProductToGLAccountMapping(loanProduct.getId(), command);
// check if the office specific products are enabled. If yes, then // check if the office specific products are enabled. If yes, then
@@ -303,6 +309,14 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
this.loanProductRepository.saveAndFlush(product); this.loanProductRepository.saveAndFlush(product);
} }
// Legacy "Account Type Creation" fields. Deliberately outside the changes check above: those
// changes only track the standard product columns, so an edit that touched nothing but a legacy
// field leaves the map empty and would otherwise be dropped.
//
// What it changed is folded into the same changes map, so the audit trail shows a per-field
// diff for these exactly as it does for the standard ones rather than an empty change set.
changes.putAll(this.loanProductCbsExtService.saveOrUpdate(product, command));
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) // .withCommandId(command.commandId()) //
.withEntityId(loanProductId) // .withEntityId(loanProductId) //
@@ -40,6 +40,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.apache.fineract.portfolio.loanproduct.serialization.LoanProductDataValidator; import org.apache.fineract.portfolio.loanproduct.serialization.LoanProductDataValidator;
import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService; import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService;
import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformServiceImpl; import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformServiceImpl;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductCbsExtService;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformServiceImpl; import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformServiceImpl;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductWritePlatformService; import org.apache.fineract.portfolio.loanproduct.service.LoanProductWritePlatformService;
@@ -82,11 +83,12 @@ public class LoanProductConfiguration {
DelinquencyBucketRepository delinquencyBucketRepository, DelinquencyBucketRepository delinquencyBucketRepository,
LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
AdvancedPaymentAllocationsJsonParser advancedPaymentJsonParser, CreditAllocationsJsonParser creditAllocationsJsonParser, AdvancedPaymentAllocationsJsonParser advancedPaymentJsonParser, CreditAllocationsJsonParser creditAllocationsJsonParser,
LoanProductAssembler loanProductAssembler, LoanProductUpdateUtil loanProductUpdateUtil) { LoanProductAssembler loanProductAssembler, LoanProductUpdateUtil loanProductUpdateUtil,
LoanProductCbsExtService loanProductCbsExtService) {
return new LoanProductWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, loanProductRepository, aprCalculator, return new LoanProductWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, loanProductRepository, aprCalculator,
fundRepository, chargeRepository, rateRepository, accountMappingWritePlatformService, fineractEntityAccessUtil, fundRepository, chargeRepository, rateRepository, accountMappingWritePlatformService, fineractEntityAccessUtil,
floatingRateRepository, loanRepositoryWrapper, businessEventNotifierService, delinquencyBucketRepository, floatingRateRepository, loanRepositoryWrapper, businessEventNotifierService, delinquencyBucketRepository,
loanRepaymentScheduleTransactionProcessorFactory, advancedPaymentJsonParser, creditAllocationsJsonParser, loanRepaymentScheduleTransactionProcessorFactory, advancedPaymentJsonParser, creditAllocationsJsonParser,
loanProductAssembler, loanProductUpdateUtil); loanProductAssembler, loanProductUpdateUtil, loanProductCbsExtService);
} }
} }
@@ -97,6 +97,7 @@ import org.apache.fineract.portfolio.savings.SavingsInterestCalculationType;
import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType; import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType;
import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -105,11 +106,14 @@ public class DepositAccountDataValidator {
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
private final DepositProductDataValidator productDataValidator; private final DepositProductDataValidator productDataValidator;
private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper;
@Autowired @Autowired
public DepositAccountDataValidator(final FromJsonHelper fromApiJsonHelper, final DepositProductDataValidator productDataValidator) { public DepositAccountDataValidator(final FromJsonHelper fromApiJsonHelper, final DepositProductDataValidator productDataValidator,
final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper) {
this.fromApiJsonHelper = fromApiJsonHelper; this.fromApiJsonHelper = fromApiJsonHelper;
this.productDataValidator = productDataValidator; this.productDataValidator = productDataValidator;
this.savingsAccountRepositoryWrapper = savingsAccountRepositoryWrapper;
} }
public void validateFixedDepositForSubmit(final String json) { public void validateFixedDepositForSubmit(final String json) {
@@ -351,10 +355,31 @@ public class DepositAccountDataValidator {
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).notNull() baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).notNull()
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues()); .isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
// Withdraw Deposit books the maturity proceeds straight out of the bank's cash/vault GL the
// moment the account matures -- before the customer has actually come in to collect it. For
// maturities processed automatically by the scheduled job (not a live counter transaction),
// that books cash as disbursed while it is still physically in the vault, so this option is
// disabled: every account must resolve to Transfer to Savings or a Reinvest option instead.
if (depositRolloverId.equals(DepositAccountOnClosureType.WITHDRAW_DEPOSIT.getValue())) {
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).failWithCodeAndMessage(
"maturity.instruction.withdraw.deposit.not.allowed",
"Withdraw Deposit is not a valid maturity instruction -- choose Transfer to Savings or a Reinvest option.");
}
if (depositRolloverId.equals(DepositAccountOnClosureType.TRANSFER_TO_SAVINGS.getValue())) { if (depositRolloverId.equals(DepositAccountOnClosureType.TRANSFER_TO_SAVINGS.getValue())) {
final Long transferToSavingsId = this.fromApiJsonHelper.extractLongNamed(transferToSavingsIdParamName, element); final Long transferToSavingsId = this.fromApiJsonHelper.extractLongNamed(transferToSavingsIdParamName, element);
baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId).notNull() baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId).notNull()
.longGreaterThanZero(); .longGreaterThanZero();
// Transfer to Savings can only ever post into a genuine Savings account -- Current accounts
// live in a separate ledger/module that this maturity-closure transfer cannot write into.
if (transferToSavingsId != null && transferToSavingsId > 0
&& !this.savingsAccountRepositoryWrapper.existsWithDepositAccountType(transferToSavingsId,
DepositAccountType.SAVINGS_DEPOSIT)) {
baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId)
.failWithCodeAndMessage("transfer.to.savings.id.is.not.a.savings.account",
"The selected account is not a Savings account -- Transfer to Savings can only target a Savings account.");
}
} }
} }
} }
@@ -493,10 +518,31 @@ public class DepositAccountDataValidator {
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).notNull() baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).notNull()
.isOneOfTheseValues(DepositAccountOnClosureType.integerValues()); .isOneOfTheseValues(DepositAccountOnClosureType.integerValues());
// Withdraw Deposit books the maturity proceeds straight out of the bank's cash/vault GL the
// moment the account matures -- before the customer has actually come in to collect it. For
// maturities processed automatically by the scheduled job (not a live counter transaction),
// that books cash as disbursed while it is still physically in the vault, so this option is
// disabled: every account must resolve to Transfer to Savings or a Reinvest option instead.
if (depositRolloverId.equals(DepositAccountOnClosureType.WITHDRAW_DEPOSIT.getValue())) {
baseDataValidator.reset().parameter(maturityInstructionIdParamName).value(depositRolloverId).failWithCodeAndMessage(
"maturity.instruction.withdraw.deposit.not.allowed",
"Withdraw Deposit is not a valid maturity instruction -- choose Transfer to Savings or a Reinvest option.");
}
if (depositRolloverId.equals(DepositAccountOnClosureType.TRANSFER_TO_SAVINGS.getValue())) { if (depositRolloverId.equals(DepositAccountOnClosureType.TRANSFER_TO_SAVINGS.getValue())) {
final Long transferToSavingsId = this.fromApiJsonHelper.extractLongNamed(transferToSavingsIdParamName, element); final Long transferToSavingsId = this.fromApiJsonHelper.extractLongNamed(transferToSavingsIdParamName, element);
baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId).notNull() baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId).notNull()
.longGreaterThanZero(); .longGreaterThanZero();
// Transfer to Savings can only ever post into a genuine Savings account -- Current accounts
// live in a separate ledger/module that this maturity-closure transfer cannot write into.
if (transferToSavingsId != null && transferToSavingsId > 0
&& !this.savingsAccountRepositoryWrapper.existsWithDepositAccountType(transferToSavingsId,
DepositAccountType.SAVINGS_DEPOSIT)) {
baseDataValidator.reset().parameter(transferToSavingsIdParamName).value(transferToSavingsId)
.failWithCodeAndMessage("transfer.to.savings.id.is.not.a.savings.account",
"The selected account is not a Savings account -- Transfer to Savings can only target a Savings account.");
}
} }
} }
@@ -555,14 +555,19 @@ public class DepositAccountDomainServiceJpa implements DepositAccountDomainServi
} }
private void updateAlreadyPostedTransactions(final Set<Long> existingTransactionIds, final SavingsAccount savingsAccount) { private void updateAlreadyPostedTransactions(final Set<Long> existingTransactionIds, final SavingsAccount savingsAccount) {
List<SavingsAccountTransaction> transactions = savingsAccount.getTransactions(); // accountTransfersWritePlatformService.transferFunds() already posts accounting for the
int size = transactions.size(); // withdrawal it creates on this account (via SavingsAccountDomainServiceJpa#handleWithdrawal),
for (int i = size - 1;; i--) { // so that withdrawal must be added to existingTransactionIds here or the caller's own
SavingsAccountTransaction transaction = transactions.get(i); // postJournalEntries() re-derives and re-posts it a second time. The previous implementation
if (transaction.isWithdrawal() || transaction.isWithdrawalFee()) { // found it by walking the transaction list backwards from the end while the type stayed
// withdrawal/withdrawal-fee -- but @OneToMany collections only guarantee @OrderBy ordering on
// a fresh fetch, not after an in-memory append, so a same-day interest posting or any other
// ordering quirk could put the new withdrawal anywhere in the list and break the walk
// silently. Scanning every transaction and checking against the original snapshot is correct
// regardless of collection order.
for (final SavingsAccountTransaction transaction : savingsAccount.getTransactions()) {
if ((transaction.isWithdrawal() || transaction.isWithdrawalFee()) && !existingTransactionIds.contains(transaction.getId())) {
existingTransactionIds.add(transaction.getId()); existingTransactionIds.add(transaction.getId());
} else {
break;
} }
} }
} }
@@ -554,7 +554,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions, fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions,
interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions,
withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions, withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions,
periodFrequencyTypeOptions); periodFrequencyTypeOptions, savingsAccountDatas, maturityInstructionOptions);
template = RecurringDepositAccountData.withInterestChartAndRecurringDetails((RecurringDepositAccountData) template, template = RecurringDepositAccountData.withInterestChartAndRecurringDetails((RecurringDepositAccountData) template,
accountChart, null, null); accountChart, null, null);
// Re-apply CBS product fields lost during factory method object creation // Re-apply CBS product fields lost during factory method object creation
@@ -626,7 +626,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions, fieldOfficerOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions,
interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions,
withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions, withdrawalFeeTypeOptions, transactions, charges, chargeOptions, preClosurePenalInterestOnTypeOptions,
periodFrequencyTypeOptions); periodFrequencyTypeOptions, savingsAccountDatas, maturityInstructionOptions);
} }
} }
@@ -1393,6 +1393,15 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
this.depositAccountDomainService.handleFDAccountMaturityClosure(fdAccount, null, user, fmt, fdAccount.maturityDate(), this.depositAccountDomainService.handleFDAccountMaturityClosure(fdAccount, null, user, fmt, fdAccount.maturityDate(),
fdAccount.getOnAccountClosureId(), toSavingsId, "Apply maturity instructions", changes); fdAccount.getOnAccountClosureId(), toSavingsId, "Apply maturity instructions", changes);
// Every maturity-instruction branch takes the balance out of this account through
// SavingsAccountDomainServiceJpa#handleWithdrawal, which posts that withdrawal's journal
// entries itself. existingTransactionIds was snapshotted at the top of this method --
// before that withdrawal existed -- so the postJournalEntries() call at the end would
// treat it as new and post it a second time. Interest postings created by
// updateMaturityStatus() above are still genuinely unposted at this point and must stay
// out of the snapshot so that final call can post them exactly once.
markWithdrawalsAsAlreadyPosted(account, existingTransactionIds);
if (changes.get("reinvestedDepositId") != null) { if (changes.get("reinvestedDepositId") != null) {
Long reinvestedDepositId = (Long) changes.get("reinvestedDepositId"); Long reinvestedDepositId = (Long) changes.get("reinvestedDepositId");
Money amountForDeposit = account.activateWithBalance(); Money amountForDeposit = account.activateWithBalance();
@@ -1421,6 +1430,19 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds());
} }
/**
* Marks the account's withdrawal (and withdrawal-fee) transactions as already accounted for, so a
* subsequent postJournalEntries() built on an older snapshot does not post them again. Used after a
* maturity closure, whose withdrawal leg posts its own journal entries.
*/
private void markWithdrawalsAsAlreadyPosted(final SavingsAccount account, final Set<Long> existingTransactionIds) {
for (final SavingsAccountTransaction transaction : account.getTransactions()) {
if (transaction.getId() != null && (transaction.isWithdrawal() || transaction.isWithdrawalFee())) {
existingTransactionIds.add(transaction.getId());
}
}
}
private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds, private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds,
final Set<Long> existingReversedTransactionIds) { final Set<Long> existingReversedTransactionIds) {
@@ -1676,6 +1676,15 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Override @Override
public void setSubStatusInactive(Long savingsId) { public void setSubStatusInactive(Long savingsId) {
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
// Same snapshot-vs-execution gap as escheat() below: the dormant-accounts batch selects
// pending-inactive ids from a query that requires status = ACTIVE, but the account can close
// through another path (e.g. deposit maturity) before this call runs. Re-deriving accounting
// off the full transaction list at that point re-posts entries that closure already posted.
if (!account.isActive()) {
log.warn("Skipping setSubStatusInactive for savings account {} -- no longer active (already closed by another process)",
savingsId);
return;
}
final Set<Long> existingTransactionIds = new HashSet<>(); final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>(); final Set<Long> existingReversedTransactionIds = new HashSet<>();
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
@@ -1687,6 +1696,11 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Override @Override
public void setSubStatusDormant(Long savingsId) { public void setSubStatusDormant(Long savingsId) {
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
if (!account.isActive()) {
log.warn("Skipping setSubStatusDormant for savings account {} -- no longer active (already closed by another process)",
savingsId);
return;
}
account.setSubStatusDormant(); account.setSubStatusDormant();
this.savingAccountRepositoryWrapper.saveAndFlush(account); this.savingAccountRepositoryWrapper.saveAndFlush(account);
} }
@@ -1694,6 +1708,16 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
@Override @Override
public void escheat(Long savingsId) { public void escheat(Long savingsId) {
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
// The dormant-accounts batch selects escheat-eligible ids from a snapshot query, so an
// account can legitimately close through another path (e.g. deposit maturity) in the gap
// between that selection and this call. escheat() unconditionally forces status/sub-status
// to CLOSED/ESCHEAT and re-derives accounting off the account's full transaction list, so
// running it against an already-closed account both mislabels the closure reason and
// re-posts journal entries for transactions that were already posted by the real closure.
if (!account.isActive()) {
log.warn("Skipping escheat for savings account {} -- no longer active (already closed by another process)", savingsId);
return;
}
final Set<Long> existingTransactionIds = new HashSet<>(); final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>(); final Set<Long> existingReversedTransactionIds = new HashSet<>();
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
@@ -537,4 +537,5 @@
<include file="parts/0467_add_welcome_kit_return_tracking.xml" relativeToChangelogFile="true"/> <include file="parts/0467_add_welcome_kit_return_tracking.xml" relativeToChangelogFile="true"/>
<include file="parts/0468_add_address_id_to_welcome_kit_dispatch.xml" relativeToChangelogFile="true"/> <include file="parts/0468_add_address_id_to_welcome_kit_dispatch.xml" relativeToChangelogFile="true"/>
<include file="parts/0469_create_database_export_request.xml" relativeToChangelogFile="true"/> <include file="parts/0469_create_database_export_request.xml" relativeToChangelogFile="true"/>
<include file="parts/0470_create_loan_product_cbs_ext.xml" relativeToChangelogFile="true"/>
</databaseChangeLog> </databaseChangeLog>
@@ -0,0 +1,377 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<!-- The legacy "Account Type Creation" screen carries a large block of product configuration that
Fineract's own loan product schema has no home for - cheque book rules, channel and transaction
limits, credit bureau account types, the older interest parameters, and a set of extra GL
mappings. Those fields were being collected by the create/edit form and then dropped before the
request was sent, so nothing survived a page reload.
They live in a satellite table keyed 1:1 on the loan product rather than as more columns on
m_product_loan: the set is specific to this deployment, and keeping it separate means an upstream
Fineract change to m_product_loan will never collide with it. Typed columns rather than a JSON
blob because loan account creation reads these at origination time, so they need to be queryable
and compile-checked, not string-keyed. -->
<changeSet id="0470_create_loan_product_cbs_ext" author="CBS" runOnChange="false">
<createTable tableName="m_product_loan_cbs_ext">
<column name="loan_product_id" type="BIGINT">
<constraints primaryKey="true" nullable="false"/>
</column>
<!-- classification & legacy header fields (24) -->
<column name="product_type_group" type="VARCHAR(20)"/>
<column name="product_type_sub_group" type="VARCHAR(30)"/>
<column name="debit_int_calculation" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="separate_penal_interest" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="credit_int_calculation" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="sub_account_type_balance" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="final_int_rounding" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="rounding_up_to" type="INTEGER"/>
<column name="account_type_code" type="VARCHAR(10)"/>
<column name="micr_transaction_code" type="VARCHAR(100)"/>
<column name="reserved_ecs_serial_number" type="VARCHAR(100)"/>
<column name="reverse_ecs_number" type="VARCHAR(100)"/>
<column name="lock_cheque_info_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="lock_page_two_info_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="computerization_date" type="DATE"/>
<column name="cheque_print_description" type="VARCHAR(100)"/>
<column name="pos_account_type_description" type="VARCHAR(100)"/>
<column name="pfms_account_type" type="VARCHAR(100)"/>
<column name="renewal_date_add_months" type="INTEGER"/>
<column name="document_renewal_date_add_months" type="INTEGER"/>
<column name="pps_limit" type="DECIMAL(19,6)"/>
<column name="is_write_off_account_type" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="write_off_account_type_code" type="VARCHAR(100)"/>
<column name="renewal_grace_days" type="INTEGER"/>
<!-- Cheque Book Settings (10) -->
<column name="cheque_book_issue_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="cheque_validation_required" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="is_first_cheque_book_free" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="cheque_printing_account_type" type="VARCHAR(30)"/>
<column name="allow_withdrawal_slip_usage" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="maintain_cheque_book_stock_registry" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="free_cheque_books_per_year" type="DECIMAL(19,6)"/>
<column name="allow_credit_by_cheque_number" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="credit_cheque_validation_required" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="grace_days_for_stock_statement" type="INTEGER"/>
<!-- Additional Product Settings (37) -->
<column name="account_type_active_for_transactions" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="sub_type_activity_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="account_opening_charges" type="INTEGER"/>
<column name="maximum_gold_loan_limit" type="DECIMAL(19,6)"/>
<column name="last_account_number_opened" type="VARCHAR(100)"/>
<column name="customer_termination_allowed" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="charges_for_cash_credit_above_limit" type="DECIMAL(19,6)"/>
<column name="death_interest_gl_account" type="VARCHAR(100)"/>
<column name="starting_ledger_number" type="INTEGER"/>
<column name="ending_ledger_number" type="INTEGER"/>
<column name="current_ledger_number" type="INTEGER"/>
<column name="remittance_account_number" type="VARCHAR(100)"/>
<column name="is_non_resident_indian_account" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="debit_clearing_gl_account" type="VARCHAR(100)"/>
<column name="missed_call_alert_service_active" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="standing_instruction_charge_gl_account" type="VARCHAR(100)"/>
<column name="standing_instruction_debit_charge_skip_option" type="VARCHAR(50)"/>
<column name="opening_balance_multiplier_for_remittance_charges" type="DECIMAL(19,6)"/>
<column name="gold_loan_packet_number_with_year_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="account_move_to_inoperative_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="generate_gl_diff_entry_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="sub_type_wise_gl_balance_generation_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="validate_before_deleting_debit_entry_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="rc_file_concept_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="account_close_zero_balance_in_open_day_process" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="account_close_zero_balance_at_subledger_tran_entry" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="limit_enhancement_against_clearing_screen_display_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="advance_interest_parameter_in_ledger_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="vehicle_detail_in_master_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="deleted_cheque_issue_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="dp_reduce_in_document_renewal_date_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="dp_reduce_in_renewal_date_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="auto_close_at_day_end_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="loan_rate_from_master_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="cheque_short_name" type="VARCHAR(100)"/>
<column name="statement_print_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="nesl_code" type="VARCHAR(100)"/>
<!-- Interest Related Parameters (53) -->
<column name="drawing_power_and_limit_generation_procedure" type="VARCHAR(100)"/>
<column name="advances_interest_calculation_method" type="VARCHAR(100)"/>
<column name="against_interest_calculation_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="day_wise_against_clearing_interest_rounding" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="interest_calculation_one_fourth_method" type="VARCHAR(20)"/>
<column name="against_clearing_interest_procedure_type" type="VARCHAR(50)"/>
<column name="consider_debit_closing_amount_for_against_clearing" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="interest_rounding_method" type="VARCHAR(30)"/>
<column name="penalty_interest_calculation_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="penalty_interest_calculation_method" type="VARCHAR(30)"/>
<column name="penalty_interest_procedure_number" type="VARCHAR(100)"/>
<column name="overdrawn_interest_calculation_method" type="VARCHAR(50)"/>
<column name="calculate_overdrawn_interest_after_renewal" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="interest_on_credit_balance" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="penalty_interest_date_different_than_regular_interest" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="fixed_deposit_overdraft_certificate_sort_order" type="VARCHAR(50)"/>
<column name="use_document_submission_date_for_interest" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="loan_management_system_renewal_penalty_active" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="minimum_normal_interest_amount" type="DECIMAL(19,6)"/>
<column name="minimum_interest_calculation_method" type="VARCHAR(50)"/>
<column name="minimum_against_clearing_interest_amount" type="DECIMAL(19,6)"/>
<column name="limit_increase_upto_interest_amount_in_interest_calculation" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="minimum_penalty_interest_amount" type="DECIMAL(19,6)"/>
<column name="days_for_limit_increase_upto_interest_amount" type="INTEGER"/>
<column name="additional_interest_rate_for_fixed_deposit_overdraft" type="DECIMAL(19,6)"/>
<column name="consider_dd_payorder_in_against_clearing_interest_calculation" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="grace_days_after_renew_date" type="INTEGER"/>
<column name="apply_undebited_interest_concept_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="interest_register_print_format_type" type="VARCHAR(50)"/>
<column name="normal_grace_days_after_overdue" type="INTEGER"/>
<column name="allow_against_clearing_entry" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="allow_no_balance_entry" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="fd_od_penalty_interest_combined_with_normal_interest" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="allow_no_balance_entry_with_password" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="undebited_simple_interest_account_wise" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="daily_interest_calculation_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="daily_interest_calculation_start_date" type="VARCHAR(100)"/>
<column name="non_performing_asset_penal_interest_rate_update" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="non_performing_asset_penal_interest_rate" type="DECIMAL(19,6)"/>
<column name="fd_od_penal_interest_rate_update_in_branch_process" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="add_penal_interest_rate" type="DECIMAL(19,6)"/>
<column name="atyp14_simple_interest_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="fixed_deposit_overdraft_margin_type" type="VARCHAR(100)"/>
<column name="check_master_detail_edited_passing_pending_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="credit_debit_interest_calculation_separate_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="on_time_interest_provision_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="check_against_clearing_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="gst_on_penalty_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="penalty_interest_grace_days" type="INTEGER"/>
<column name="overdrawn_interest_rate" type="DECIMAL(19,6)"/>
<column name="calculate_daily_limit_enhancement_tod_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="fixed_deposit_overdraft_multiple_interest_rate_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="grace_days_after_overdue_for_moratorium" type="INTEGER"/>
<!-- Credit Bureau Settings (CIBIL) (8) -->
<column name="cibil_account_type" type="VARCHAR(100)"/>
<column name="cibil_commercial_account_type" type="VARCHAR(100)"/>
<column name="highmark_account_type" type="VARCHAR(100)"/>
<column name="highmark_commercial_account_type" type="VARCHAR(100)"/>
<column name="experian_account_type" type="VARCHAR(100)"/>
<column name="experian_commercial_account_type" type="VARCHAR(100)"/>
<column name="equifax_account_type" type="VARCHAR(100)"/>
<column name="equifax_commercial_account_type" type="VARCHAR(100)"/>
<!-- RBI Compliance & Product Category (6) -->
<column name="loan_product_category" type="VARCHAR(30)"/>
<column name="penal_charge_method" type="VARCHAR(50)"/>
<column name="is_floating_rate" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="foreclosure_charges_applicable" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="key_fact_statement_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="annual_percentage_rate_calculation_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<!-- Channel & Transaction Limits (23) -->
<column name="is_mobile_app_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="is_atm_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="atm_yearly_charge_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="atm_minimum_balance" type="DECIMAL(19,6)"/>
<column name="email_active_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="is_imps_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="is_upi_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="is_pos_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="is_bbps_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="inter_bank_limit_per_day" type="DECIMAL(19,6)"/>
<column name="inter_bank_limit_per_transaction" type="DECIMAL(19,6)"/>
<column name="neft_rtgs_limit_per_day" type="DECIMAL(19,6)"/>
<column name="neft_rtgs_limit_per_transaction" type="DECIMAL(19,6)"/>
<column name="neft_cutoff_limit" type="DECIMAL(19,6)"/>
<column name="rtgs_cutoff_limit" type="DECIMAL(19,6)"/>
<column name="neft_rtgs_minimum_balance" type="DECIMAL(19,6)"/>
<column name="is_charge_required_neft_rtgs_cbs" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="is_charge_required_neft_rtgs_mobile" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="imps_limit_per_day" type="DECIMAL(19,6)"/>
<column name="imps_limit_per_transaction" type="DECIMAL(19,6)"/>
<column name="imps_limit_per_month" type="DECIMAL(19,6)"/>
<column name="imps_min_limit" type="DECIMAL(19,6)"/>
<column name="imps_monthly_transaction_count" type="INTEGER"/>
<!-- POS Facility & ATM CSV Settings (10) -->
<column name="max_per_transaction_limit_atm" type="DECIMAL(19,6)"/>
<column name="max_day_transaction_limit_atm" type="DECIMAL(19,6)"/>
<column name="max_per_transaction_limit_pos" type="DECIMAL(19,6)"/>
<column name="max_day_transaction_limit_pos" type="DECIMAL(19,6)"/>
<column name="max_per_transaction_limit_ecom" type="DECIMAL(19,6)"/>
<column name="max_day_transaction_limit_ecom" type="DECIMAL(19,6)"/>
<column name="max_per_transaction_limit_upi" type="DECIMAL(19,6)"/>
<column name="max_day_transaction_limit_upi" type="DECIMAL(19,6)"/>
<column name="atm_csv_product_code" type="VARCHAR(100)"/>
<column name="atm_csv_type_code" type="VARCHAR(100)"/>
<!-- Auto Interest Post Parameters (4) -->
<column name="auto_interest_post_type" type="VARCHAR(30)"/>
<column name="auto_interest_post_when" type="VARCHAR(20)"/>
<column name="auto_interest_post_fix_day" type="INTEGER"/>
<column name="auto_last_interest_date" type="VARCHAR(100)"/>
<!-- Foreclosure / Early Account Closure (3) -->
<column name="calculate_early_account_close_charge" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="early_account_close_charge" type="INTEGER"/>
<column name="early_account_close_period_in_months" type="INTEGER"/>
<!-- Terms step (3) -->
<column name="debit_balance_allow" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="allow_debit_trans_after_due_date" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="branch_level_account_type" type="BOOLEAN" defaultValueBoolean="false"/>
<!-- legacy cheque / standing instruction / minimum balance charges (24) -->
<column name="cheque_book_charge_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="cheque_return_charge_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="ic_charge_debit_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="cash_bundle_charge_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="cash_bundle_charge_with_pan_limit" type="DECIMAL(19,6)"/>
<column name="per_cheque_entry_charge" type="DECIMAL(19,6)"/>
<column name="per_cheque_entry_service_charge" type="DECIMAL(19,6)"/>
<column name="per_inward_cheque_entry_charge" type="DECIMAL(19,6)"/>
<column name="per_outward_cheque_entry_charge" type="DECIMAL(19,6)"/>
<column name="creditor_standing_instruction_fail_charge" type="DECIMAL(19,6)"/>
<column name="debtor_standing_instruction_fail_charge" type="DECIMAL(19,6)"/>
<column name="minimum_balance_checking_type" type="VARCHAR(20)"/>
<column name="auto_minimum_balance_charge" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="calculate_daily_minimum_balance_charges" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="minimum_balance_charge_enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="minimum_balance_charge_amount" type="DECIMAL(19,6)"/>
<column name="minimum_balance_charge_for_cheque_issue" type="DECIMAL(19,6)"/>
<column name="minimum_balance_charge_method" type="VARCHAR(20)"/>
<column name="months_after_min_balance_charge_calculate" type="INTEGER"/>
<column name="for_cheque_issue" type="DECIMAL(19,6)"/>
<column name="for_cheque_issue_avg" type="DECIMAL(19,6)"/>
<column name="for_withdrawal_slip" type="DECIMAL(19,6)"/>
<column name="for_withdrawal_slip_avg" type="DECIMAL(19,6)"/>
<column name="calculate_daily_against_clearing_charge" type="BOOLEAN" defaultValueBoolean="false"/>
<!-- custom GL account mappings (14) -->
<column name="int_receivables_gl_account_id" type="BIGINT"/>
<column name="int_payable_gl_account_id" type="BIGINT"/>
<column name="deaf_int_payable_gl_account_id" type="BIGINT"/>
<column name="int_overdue_reserve_account_id" type="BIGINT"/>
<column name="undebited_interest_account_id" type="BIGINT"/>
<column name="penal_int_cr_gl_account_id" type="BIGINT"/>
<column name="rd_matured_gl_account_id" type="BIGINT"/>
<column name="gl_account_id" type="BIGINT"/>
<column name="ic_charge_gl_account_id" type="BIGINT"/>
<column name="inoperative_gl_account_id" type="BIGINT"/>
<column name="credit_balance_liability_gl_account_id" type="BIGINT"/>
<column name="credit_balance_asset_gl_account_id" type="BIGINT"/>
<column name="overdue_accounts_gl_account_id" type="BIGINT"/>
<column name="rebate_gl_account_id" type="BIGINT"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="loan_product_id"
constraintName="fk_mplce_loan_product"
referencedTableName="m_product_loan" referencedColumnNames="id"
onDelete="CASCADE"/>
</changeSet>
<!-- The extra GL mappings hold real account ids, so they are constrained rather than left as loose
numbers: deleting a GL account that a product still points at should fail, not leave the product
holding a dangling id. -->
<changeSet id="0470_loan_product_cbs_ext_gl_fks" author="CBS" runOnChange="false">
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="int_receivables_gl_account_id"
constraintName="fk_mplce_int_receivables"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="int_payable_gl_account_id"
constraintName="fk_mplce_int_payable"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="deaf_int_payable_gl_account_id"
constraintName="fk_mplce_deaf_int_payable"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="int_overdue_reserve_account_id"
constraintName="fk_mplce_int_overdue_reserve"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="undebited_interest_account_id"
constraintName="fk_mplce_undebited_interest"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="penal_int_cr_gl_account_id"
constraintName="fk_mplce_penal_int_cr"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="rd_matured_gl_account_id"
constraintName="fk_mplce_rd_matured"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="gl_account_id"
constraintName="fk_mplce_gl"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="ic_charge_gl_account_id"
constraintName="fk_mplce_ic_charge"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="inoperative_gl_account_id"
constraintName="fk_mplce_inoperative"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="credit_balance_liability_gl_account_id"
constraintName="fk_mplce_credit_balance_liability"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="credit_balance_asset_gl_account_id"
constraintName="fk_mplce_credit_balance_asset"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="overdue_accounts_gl_account_id"
constraintName="fk_mplce_overdue_accounts"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_ext" baseColumnNames="rebate_gl_account_id"
constraintName="fk_mplce_rebate"
referencedTableName="acc_gl_account" referencedColumnNames="id"/>
</changeSet>
<!-- The legacy Interest Rate Detail grid: four fixed interest types, each with a Normal and a Special
min/max rate plus a period. A child table rather than twenty-four flat columns because the
interest type reads far better as data than as part of a column name
(against_clearing_special_max_rate and friends). -->
<changeSet id="0470_create_loan_product_cbs_interest_rate" author="CBS" runOnChange="false">
<createTable tableName="m_product_loan_cbs_interest_rate">
<column name="id" type="BIGSERIAL">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="loan_product_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<!-- Normal Debit | Normal Credit | Against Clearing | Penalty -->
<column name="interest_type" type="VARCHAR(30)">
<constraints nullable="false"/>
</column>
<column name="calculation" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="normal_min_rate" type="DECIMAL(9,6)"/>
<column name="normal_max_rate" type="DECIMAL(9,6)"/>
<column name="special_min_rate" type="DECIMAL(9,6)"/>
<column name="special_max_rate" type="DECIMAL(9,6)"/>
<!-- DAILY | MONTHLY | QUARTERLY | HALF YEARLY | YEARLY -->
<column name="period" type="VARCHAR(20)"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_product_loan_cbs_interest_rate" baseColumnNames="loan_product_id"
constraintName="fk_mplcir_loan_product"
referencedTableName="m_product_loan" referencedColumnNames="id"
onDelete="CASCADE"/>
<!-- One row per interest type per product; the grid is fixed, so this also stops a replayed
save from doubling the rows. -->
<addUniqueConstraint tableName="m_product_loan_cbs_interest_rate"
columnNames="loan_product_id, interest_type"
constraintName="mplcir_unq_product_interest_type"/>
</changeSet>
<!-- Both of these are money, not counts, and were created as INTEGER by mistake - a charge of 250.75
was rejected outright. Kept as a follow-on changeset rather than a correction to the two above,
which have already run and whose checksums must not change. -->
<changeSet id="0470_fix_charge_column_types" author="CBS" runOnChange="false">
<modifyDataType tableName="m_product_loan_cbs_ext" columnName="account_opening_charges"
newDataType="DECIMAL(19,6)"/>
<modifyDataType tableName="m_product_loan_cbs_ext" columnName="early_account_close_charge"
newDataType="DECIMAL(19,6)"/>
</changeSet>
<!-- A count of cheque books, not an amount. Created as DECIMAL by the same name-based type guess
that got the two charges above wrong, and stored "3.000000" for three books. -->
<changeSet id="0470_free_cheque_books_is_a_count" author="CBS" runOnChange="false">
<modifyDataType tableName="m_product_loan_cbs_ext" columnName="free_cheque_books_per_year"
newDataType="INTEGER"/>
</changeSet>
</databaseChangeLog>
@@ -76,6 +76,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
// for account close // for account close
private final Collection<EnumOptionData> onAccountClosureOptions; private final Collection<EnumOptionData> onAccountClosureOptions;
private final Collection<PaymentTypeData> paymentTypeOptions; private final Collection<PaymentTypeData> paymentTypeOptions;
private final Collection<EnumOptionData> maturityInstructionOptions;
// import fields // import fields
private transient Integer rowIndex; private transient Integer rowIndex;
@@ -364,6 +365,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
this.savingsAccounts = null; this.savingsAccounts = null;
this.onAccountClosureOptions = null; this.onAccountClosureOptions = null;
this.paymentTypeOptions = null; this.paymentTypeOptions = null;
this.maturityInstructionOptions = null;
this.rowIndex = rowIndex; this.rowIndex = rowIndex;
this.dateFormat = dateFormat; this.dateFormat = dateFormat;
this.locale = locale; this.locale = locale;
@@ -394,6 +396,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
final Collection<SavingsAccountData> savingsAccountDatas = null; final Collection<SavingsAccountData> savingsAccountDatas = null;
final Integer recurringFrequency = null; final Integer recurringFrequency = null;
final EnumOptionData recurringFrequencyType = null; final EnumOptionData recurringFrequencyType = null;
final Collection<EnumOptionData> maturityInstructionOptions = null;
return new RecurringDepositAccountData(depositAccountData.id, depositAccountData.accountNo, depositAccountData.externalId, return new RecurringDepositAccountData(depositAccountData.id, depositAccountData.accountNo, depositAccountData.externalId,
depositAccountData.groupId, depositAccountData.groupName, depositAccountData.clientId, depositAccountData.clientName, depositAccountData.groupId, depositAccountData.groupName, depositAccountData.clientId, depositAccountData.clientName,
@@ -416,7 +419,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas, expectedFirstDepositOnDate, onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas, expectedFirstDepositOnDate,
totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal, adjustAdvanceTowardsFuturePayments, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal, adjustAdvanceTowardsFuturePayments,
isCalendarInherited, recurringFrequency, recurringFrequencyType, depositAccountData.withHoldTax, isCalendarInherited, recurringFrequency, recurringFrequencyType, depositAccountData.withHoldTax,
depositAccountData.taxGroup); depositAccountData.taxGroup, maturityInstructionOptions);
} }
public static RecurringDepositAccountData withInterestChartAndRecurringDetails(final RecurringDepositAccountData account, public static RecurringDepositAccountData withInterestChartAndRecurringDetails(final RecurringDepositAccountData account,
@@ -441,7 +444,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
account.paymentTypeOptions, account.savingsAccounts, account.expectedFirstDepositOnDate, account.totalOverdueAmount, account.paymentTypeOptions, account.savingsAccounts, account.expectedFirstDepositOnDate, account.totalOverdueAmount,
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal, account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, recurringFrequency, recurringFrequencyType, account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, recurringFrequency, recurringFrequencyType,
account.withHoldTax, account.taxGroup); account.withHoldTax, account.taxGroup, account.maturityInstructionOptions);
result.copyCbsFieldsFrom(account); result.copyCbsFieldsFrom(account);
return result; return result;
} }
@@ -463,11 +466,13 @@ public final class RecurringDepositAccountData extends DepositAccountData {
final Collection<EnumOptionData> preClosurePenalInterestOnTypeOptions = null; final Collection<EnumOptionData> preClosurePenalInterestOnTypeOptions = null;
final Collection<EnumOptionData> periodFrequencyTypeOptions = null; final Collection<EnumOptionData> periodFrequencyTypeOptions = null;
final Collection<SavingsAccountData> savingsAccountDatas = account.savingsAccounts;
final Collection<EnumOptionData> maturityInstructionOptions = account.maturityInstructionOptions;
return withTemplateOptions(account, productOptions, fieldOfficerOptions, interestCompoundingPeriodTypeOptions, return withTemplateOptions(account, productOptions, fieldOfficerOptions, interestCompoundingPeriodTypeOptions,
interestPostingPeriodTypeOptions, interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, interestPostingPeriodTypeOptions, interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions,
lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions, transactions, charges, chargeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions, transactions, charges, chargeOptions,
preClosurePenalInterestOnTypeOptions, periodFrequencyTypeOptions); preClosurePenalInterestOnTypeOptions, periodFrequencyTypeOptions, savingsAccountDatas, maturityInstructionOptions);
} }
final RecurringDepositAccountData result = new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName, final RecurringDepositAccountData result = new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
@@ -486,10 +491,10 @@ public final class RecurringDepositAccountData extends DepositAccountData {
account.inMultiplesOfDepositTermType, account.depositAmount, account.maturityAmount, account.maturityDate, account.inMultiplesOfDepositTermType, account.depositAmount, account.maturityAmount, account.maturityDate,
account.depositPeriod, account.depositPeriodFrequency, account.mandatoryRecommendedDepositAmount, account.depositPeriod, account.depositPeriodFrequency, account.mandatoryRecommendedDepositAmount,
template.periodFrequencyTypeOptions, account.depositType, account.onAccountClosure, account.onAccountClosureOptions, template.periodFrequencyTypeOptions, account.depositType, account.onAccountClosure, account.onAccountClosureOptions,
account.paymentTypeOptions, account.savingsAccounts, account.expectedFirstDepositOnDate, account.totalOverdueAmount, account.paymentTypeOptions, template.savingsAccounts, account.expectedFirstDepositOnDate, account.totalOverdueAmount,
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal, account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency, account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency,
account.recurringFrequencyType, account.withHoldTax, account.taxGroup); account.recurringFrequencyType, account.withHoldTax, account.taxGroup, account.maturityInstructionOptions);
result.copyCbsFieldsFrom(account); result.copyCbsFieldsFrom(account);
// Merge accountTypeOptions and customerSegmentOptions from template // Merge accountTypeOptions and customerSegmentOptions from template
if (template.getAccountTypeOptions() != null) result.setAccountTypeOptions(template.getAccountTypeOptions()); if (template.getAccountTypeOptions() != null) result.setAccountTypeOptions(template.getAccountTypeOptions());
@@ -507,7 +512,8 @@ public final class RecurringDepositAccountData extends DepositAccountData {
final Collection<EnumOptionData> lockinPeriodFrequencyTypeOptions, final Collection<EnumOptionData> withdrawalFeeTypeOptions, final Collection<EnumOptionData> lockinPeriodFrequencyTypeOptions, final Collection<EnumOptionData> withdrawalFeeTypeOptions,
final Collection<SavingsAccountTransactionData> transactions, final Collection<SavingsAccountChargeData> charges, final Collection<SavingsAccountTransactionData> transactions, final Collection<SavingsAccountChargeData> charges,
final Collection<ChargeData> chargeOptions, final Collection<EnumOptionData> preClosurePenalInterestOnTypeOptions, final Collection<ChargeData> chargeOptions, final Collection<EnumOptionData> preClosurePenalInterestOnTypeOptions,
final Collection<EnumOptionData> periodFrequencyTypeOptions) { final Collection<EnumOptionData> periodFrequencyTypeOptions, final Collection<SavingsAccountData> savingsAccountDatas,
final Collection<EnumOptionData> maturityInstructionOptions) {
final RecurringDepositAccountData r2 = new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName, final RecurringDepositAccountData r2 = new RecurringDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId, account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId,
@@ -524,10 +530,10 @@ public final class RecurringDepositAccountData extends DepositAccountData {
account.inMultiplesOfDepositTermType, account.depositAmount, account.maturityAmount, account.maturityDate, account.inMultiplesOfDepositTermType, account.depositAmount, account.maturityAmount, account.maturityDate,
account.depositPeriod, account.depositPeriodFrequency, account.mandatoryRecommendedDepositAmount, account.depositPeriod, account.depositPeriodFrequency, account.mandatoryRecommendedDepositAmount,
periodFrequencyTypeOptions, account.depositType, account.onAccountClosure, account.onAccountClosureOptions, periodFrequencyTypeOptions, account.depositType, account.onAccountClosure, account.onAccountClosureOptions,
account.paymentTypeOptions, account.savingsAccounts, account.expectedFirstDepositOnDate, account.totalOverdueAmount, account.paymentTypeOptions, savingsAccountDatas, account.expectedFirstDepositOnDate, account.totalOverdueAmount,
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal, account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency, account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency,
account.recurringFrequencyType, account.withHoldTax, account.taxGroup); account.recurringFrequencyType, account.withHoldTax, account.taxGroup, maturityInstructionOptions);
r2.copyCbsFieldsFrom(account); r2.copyCbsFieldsFrom(account);
return r2; return r2;
} }
@@ -609,6 +615,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
final EnumOptionData recurringFrequencyType = null; final EnumOptionData recurringFrequencyType = null;
final boolean withHoldTax = false; final boolean withHoldTax = false;
final TaxGroupData taxGroup = null; final TaxGroupData taxGroup = null;
final Collection<EnumOptionData> maturityInstructionOptions = null;
return new RecurringDepositAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, return new RecurringDepositAccountData(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName,
fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualInterestRate, interestPeriodType, fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualInterestRate, interestPeriodType,
@@ -622,7 +629,8 @@ public final class RecurringDepositAccountData extends DepositAccountData {
depositAmount, maturityAmount, maturityDate, depositPeriod, depositPeriodFrequency, mandatoryRecommendedDepositAmount, depositAmount, maturityAmount, maturityDate, depositPeriod, depositPeriodFrequency, mandatoryRecommendedDepositAmount,
periodFrequencyTypeOptions, depositType, onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas, periodFrequencyTypeOptions, depositType, onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas,
expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal, expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal,
adjustAdvanceTowardsFuturePayments, isCalendarInherited, recurringFrequency, recurringFrequencyType, withHoldTax, taxGroup); adjustAdvanceTowardsFuturePayments, isCalendarInherited, recurringFrequency, recurringFrequencyType, withHoldTax, taxGroup,
maturityInstructionOptions);
} }
public static RecurringDepositAccountData preClosureDetails(final Long accountId, final BigDecimal maturityAmount, public static RecurringDepositAccountData preClosureDetails(final Long accountId, final BigDecimal maturityAmount,
@@ -700,6 +708,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
final EnumOptionData recurringFrequencyType = null; final EnumOptionData recurringFrequencyType = null;
final boolean withHoldTax = false; final boolean withHoldTax = false;
final TaxGroupData taxGroup = null; final TaxGroupData taxGroup = null;
final Collection<EnumOptionData> maturityInstructionOptions = null;
return new RecurringDepositAccountData(accountId, accountNo, externalId, groupId, groupName, clientId, clientName, productId, return new RecurringDepositAccountData(accountId, accountNo, externalId, groupId, groupName, clientId, clientName, productId,
productName, fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualInterestRate, interestPeriodType, productName, fieldOfficerId, fieldOfficerName, status, timeline, currency, nominalAnnualInterestRate, interestPeriodType,
@@ -713,7 +722,8 @@ public final class RecurringDepositAccountData extends DepositAccountData {
depositAmount, maturityAmount, maturityDate, depositPeriod, depositPeriodFrequency, mandatoryRecommendedDepositAmount, depositAmount, maturityAmount, maturityDate, depositPeriod, depositPeriodFrequency, mandatoryRecommendedDepositAmount,
periodFrequencyTypeOptions, depositType, onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas, periodFrequencyTypeOptions, depositType, onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas,
expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal, expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal,
adjustAdvanceTowardsFuturePayments, isCalendarInherited, recurringFrequency, recurringFrequencyType, withHoldTax, taxGroup); adjustAdvanceTowardsFuturePayments, isCalendarInherited, recurringFrequency, recurringFrequencyType, withHoldTax, taxGroup,
maturityInstructionOptions);
} }
public static RecurringDepositAccountData withClosureTemplateDetails(final RecurringDepositAccountData account, public static RecurringDepositAccountData withClosureTemplateDetails(final RecurringDepositAccountData account,
@@ -739,7 +749,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
paymentTypeOptions, savingsAccountDatas, account.expectedFirstDepositOnDate, account.totalOverdueAmount, paymentTypeOptions, savingsAccountDatas, account.expectedFirstDepositOnDate, account.totalOverdueAmount,
account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal, account.noOfOverdueInstallments, account.isMandatoryDeposit, account.allowWithdrawal,
account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency, account.adjustAdvanceTowardsFuturePayments, account.isCalendarInherited, account.recurringFrequency,
account.recurringFrequencyType, account.withHoldTax, account.taxGroup); account.recurringFrequencyType, account.withHoldTax, account.taxGroup, account.maturityInstructionOptions);
closureResult.copyCbsFieldsFrom(account); closureResult.copyCbsFieldsFrom(account);
return closureResult; return closureResult;
} }
@@ -774,7 +784,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
final BigDecimal totalOverdueAmount, final Integer noOfOverdueInstallments, final boolean isMandatoryDeposit, final BigDecimal totalOverdueAmount, final Integer noOfOverdueInstallments, final boolean isMandatoryDeposit,
final boolean allowWithdrawal, final boolean adjustAdvanceTowardsFuturePayments, final boolean isCalendarInherited, final boolean allowWithdrawal, final boolean adjustAdvanceTowardsFuturePayments, final boolean isCalendarInherited,
final Integer recurringFrequency, final EnumOptionData recurringFrequencyType, final boolean withHoldTax, final Integer recurringFrequency, final EnumOptionData recurringFrequencyType, final boolean withHoldTax,
final TaxGroupData taxGroup) { final TaxGroupData taxGroup, final Collection<EnumOptionData> maturityInstructionOptions) {
super(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, fieldofficerId, fieldofficerName, super(id, accountNo, externalId, groupId, groupName, clientId, clientName, productId, productName, fieldofficerId, fieldofficerName,
status, timeline, currency, nominalAnnualInterestRate, interestPeriodType, interestPostingPeriodType, status, timeline, currency, nominalAnnualInterestRate, interestPeriodType, interestPostingPeriodType,
@@ -818,6 +828,7 @@ public final class RecurringDepositAccountData extends DepositAccountData {
// account close template options // account close template options
this.onAccountClosureOptions = onAccountClosureOptions; this.onAccountClosureOptions = onAccountClosureOptions;
this.paymentTypeOptions = paymentTypeOptions; this.paymentTypeOptions = paymentTypeOptions;
this.maturityInstructionOptions = maturityInstructionOptions;
} }
@Override @Override
@@ -82,6 +82,11 @@ public class SavingsAccountRepositoryWrapper {
return account; return account;
} }
@Transactional(readOnly = true)
public boolean existsWithDepositAccountType(final Long savingsId, final DepositAccountType depositAccountType) {
return this.repository.findByIdAndDepositAccountType(savingsId, depositAccountType.getValue()) != null;
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
public List<SavingsAccount> findSavingAccountByClientId(@Param("clientId") Long clientId) { public List<SavingsAccount> findSavingAccountByClientId(@Param("clientId") Long clientId) {
List<SavingsAccount> accounts = this.repository.findSavingAccountByClientId(clientId); List<SavingsAccount> accounts = this.repository.findSavingAccountByClientId(clientId);
@@ -172,7 +172,7 @@ public class FixedDepositTest extends IntegrationTest {
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class); moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP)); moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP));
fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl( fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl(
new DepositAccountDataValidator(new FromJsonHelper(), null), new FromJsonHelper()); new DepositAccountDataValidator(new FromJsonHelper(), null, null), new FromJsonHelper());
try { try {
HashMap h = fixedDepositAccountInterestCalculationServiceImpl HashMap h = fixedDepositAccountInterestCalculationServiceImpl
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper())); .calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper()));
@@ -198,7 +198,7 @@ public class FixedDepositTest extends IntegrationTest {
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class); moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP)); moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP));
fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl( fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl(
new DepositAccountDataValidator(new FromJsonHelper(), null), new FromJsonHelper()); new DepositAccountDataValidator(new FromJsonHelper(), null, null), new FromJsonHelper());
try { try {
HashMap h = fixedDepositAccountInterestCalculationServiceImpl HashMap h = fixedDepositAccountInterestCalculationServiceImpl
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper())); .calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper()));
@@ -224,7 +224,7 @@ public class FixedDepositTest extends IntegrationTest {
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class); moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP)); moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP));
fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl( fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl(
new DepositAccountDataValidator(new FromJsonHelper(), null), new FromJsonHelper()); new DepositAccountDataValidator(new FromJsonHelper(), null, null), new FromJsonHelper());
BigDecimal expectedResult = new BigDecimal("10506.250000"); BigDecimal expectedResult = new BigDecimal("10506.250000");
BigDecimal actualResult = new BigDecimal(fixedDepositAccountInterestCalculationServiceImpl BigDecimal actualResult = new BigDecimal(fixedDepositAccountInterestCalculationServiceImpl
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper())).get("maturityAmount").toString()); .calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper())).get("maturityAmount").toString());