Merge remote-tracking branch 'self-hosted/feature/account-charges' into feature/Backend-DSC-MFA

# Conflicts:
#	fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
This commit is contained in:
Aditya
2026-05-20 13:26:58 +05:30
112 changed files with 207214 additions and 46034 deletions
@@ -0,0 +1,173 @@
-- ============================================================
-- Facility Charge Auto-Seed Migration
-- Step 1: Create mapping table
-- Step 2: Insert charges into m_charge
-- Step 3: Insert mapping with charge IDs
-- charge_applies_to_enum: 2=SAVINGS, 5=CURRENT_ACCOUNT
-- charge_time_enum: 3=SAVINGS_ACTIVATION, 6=ANNUAL_FEE, 7=MONTHLY_FEE
-- charge_calculation_enum: 1=FLAT
-- ============================================================
-- Step 1: Mapping table
CREATE TABLE IF NOT EXISTS m_facility_charge_mapping (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
facility_type VARCHAR(60) NOT NULL UNIQUE,
charge_id BIGINT,
charge_name VARCHAR(200) NOT NULL,
default_amount DECIMAL(19,6) NOT NULL DEFAULT 0.000000,
charge_time_type INT NOT NULL DEFAULT 3,
charge_applies_to INT NOT NULL DEFAULT 2,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
FOREIGN KEY (charge_id) REFERENCES m_charge(id)
);
-- Step 2: Insert charges into m_charge (only if not already exists)
-- income_or_liability_account_id = first available INCOME DETAIL GL account
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'ATM Facility Charge', 'INR', 100.00, 2, 3, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'ATM Facility Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'ATM Withdrawal Charge', 'INR', 20.00, 2, 3, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'ATM Withdrawal Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'UPI Facility Charge', 'INR', 100.00, 2, 3, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'UPI Facility Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'POS Transaction Charge', 'INR', 100.00, 2, 3, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'POS Transaction Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'Cheque Book Charge', 'INR', 300.00, 2, 3, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Cheque Book Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'IMPS Facility Charge', 'INR', 5.00, 2, 3, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'IMPS Facility Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'SMS Alert Charge', 'INR', 15.00, 2, 7, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'SMS Alert Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'BBPS Facility Charge', 'INR', 2.00, 2, 3, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'BBPS Facility Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'Insurance Cover Charge', 'INR', 500.00, 2, 6, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Insurance Cover Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'BEPG Facility Charge', 'INR', 5.00, 2, 3, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'BEPG Facility Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'NEFT Facility Charge', 'INR', 5.00, 2, 3, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'NEFT Facility Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'RTGS Facility Charge', 'INR', 25.00, 2, 3, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'RTGS Facility Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'Inter-Bank Cap Charge', 'INR', 0.00, 2, 3, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Inter-Bank Cap Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'Withdrawal Facility Charge', 'INR', 5.00, 2, 3, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Withdrawal Facility Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'Deposit KYC Charge', 'INR', 5.00, 2, 3, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Deposit KYC Charge' AND is_deleted = 0);
INSERT IGNORE INTO m_charge
(name, currency_code, amount, charge_applies_to_enum, charge_time_enum,
charge_calculation_enum, is_penalty, is_active, is_deleted,
is_free_withdrawal, is_payment_type, income_or_liability_account_id)
SELECT 'Virtual Debit Card Charge', 'INR', 199.00, 2, 3, 1, 0, 1, 0, 0, 0,
(SELECT id FROM acc_gl_account WHERE classification_enum = 4 AND usage_enum = 1 AND disabled = 0 ORDER BY id LIMIT 1)
WHERE NOT EXISTS (SELECT 1 FROM m_charge WHERE name = 'Virtual Debit Card Charge' AND is_deleted = 0);
-- Step 3: Insert mapping linking facility_type → charge_id
INSERT IGNORE INTO m_facility_charge_mapping
(facility_type, charge_name, default_amount, charge_time_type, charge_applies_to, charge_id)
VALUES
('ATM_FACILITY', 'ATM Facility Charge', 100.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'ATM Facility Charge' AND is_deleted = 0 LIMIT 1)),
('ATM_WITHDRAWAL', 'ATM Withdrawal Charge', 20.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'ATM Withdrawal Charge' AND is_deleted = 0 LIMIT 1)),
('UPI_FACILITY', 'UPI Facility Charge', 100.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'UPI Facility Charge' AND is_deleted = 0 LIMIT 1)),
('POS_TRANSACTIONS', 'POS Transaction Charge', 100.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'POS Transaction Charge' AND is_deleted = 0 LIMIT 1)),
('CHEQUE_BOOK', 'Cheque Book Charge', 300.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Cheque Book Charge' AND is_deleted = 0 LIMIT 1)),
('IMPS_FACILITY', 'IMPS Facility Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'IMPS Facility Charge' AND is_deleted = 0 LIMIT 1)),
('SMS_ALERTS', 'SMS Alert Charge', 15.00, 7, 2, (SELECT id FROM m_charge WHERE name = 'SMS Alert Charge' AND is_deleted = 0 LIMIT 1)),
('BBPS_FACILITY', 'BBPS Facility Charge', 2.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'BBPS Facility Charge' AND is_deleted = 0 LIMIT 1)),
('INSURANCE_COVER', 'Insurance Cover Charge', 500.00, 6, 2, (SELECT id FROM m_charge WHERE name = 'Insurance Cover Charge' AND is_deleted = 0 LIMIT 1)),
('BEPG_FACILITY', 'BEPG Facility Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'BEPG Facility Charge' AND is_deleted = 0 LIMIT 1)),
('NEFT_FACILITY', 'NEFT Facility Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'NEFT Facility Charge' AND is_deleted = 0 LIMIT 1)),
('RTGS_FACILITY', 'RTGS Facility Charge', 25.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'RTGS Facility Charge' AND is_deleted = 0 LIMIT 1)),
('INTER_BANK_TOTAL_CAP','Inter-Bank Cap Charge', 0.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Inter-Bank Cap Charge' AND is_deleted = 0 LIMIT 1)),
('WITHDRAWAL_FACILITY', 'Withdrawal Facility Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Withdrawal Facility Charge' AND is_deleted = 0 LIMIT 1)),
('DEPOSIT_FACILITY_KYC','Deposit KYC Charge', 5.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Deposit KYC Charge' AND is_deleted = 0 LIMIT 1)),
('VIRTUAL_DEBIT_CARD', 'Virtual Debit Card Charge', 199.00, 3, 2, (SELECT id FROM m_charge WHERE name = 'Virtual Debit Card Charge' AND is_deleted = 0 LIMIT 1));
+79
View File
@@ -0,0 +1,79 @@
-- Migration: Add facility charge columns to product facility config tables
-- Each facility (ATM, UPI, IMPS, etc.) links to an existing m_charge entry
-- When account is activated, enabled facilities auto-apply their linked charge
-- Current Product Facility Config
ALTER TABLE m_current_product_facility_config
ADD COLUMN IF NOT EXISTS atm_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS atm_withdrawal_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS upi_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS pos_transactions_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS cheque_book_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS imps_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS sms_alerts_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS bbps_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS insurance_cover_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS bepg_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS neft_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS rtgs_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS inter_bank_total_cap_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS withdrawal_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS deposit_facility_kyc_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS virtual_debit_card_charge_id BIGINT NULL;
-- Foreign key constraints for current product
ALTER TABLE m_current_product_facility_config
ADD CONSTRAINT fk_cpfc_atm_charge FOREIGN KEY (atm_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_atm_wd_charge FOREIGN KEY (atm_withdrawal_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_upi_charge FOREIGN KEY (upi_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_pos_charge FOREIGN KEY (pos_transactions_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_chq_charge FOREIGN KEY (cheque_book_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_imps_charge FOREIGN KEY (imps_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_sms_charge FOREIGN KEY (sms_alerts_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_bbps_charge FOREIGN KEY (bbps_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_ins_charge FOREIGN KEY (insurance_cover_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_bepg_charge FOREIGN KEY (bepg_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_neft_charge FOREIGN KEY (neft_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_rtgs_charge FOREIGN KEY (rtgs_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_ibtc_charge FOREIGN KEY (inter_bank_total_cap_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_wd_charge FOREIGN KEY (withdrawal_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_dep_charge FOREIGN KEY (deposit_facility_kyc_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_cpfc_vdc_charge FOREIGN KEY (virtual_debit_card_charge_id) REFERENCES m_charge(id);
-- Savings Product Facility Config
ALTER TABLE m_savings_product_facility_config
ADD COLUMN IF NOT EXISTS atm_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS atm_withdrawal_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS upi_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS pos_transactions_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS cheque_book_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS imps_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS sms_alerts_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS bbps_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS insurance_cover_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS bepg_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS neft_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS rtgs_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS inter_bank_total_cap_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS withdrawal_facility_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS deposit_facility_kyc_charge_id BIGINT NULL,
ADD COLUMN IF NOT EXISTS virtual_debit_card_charge_id BIGINT NULL;
-- Foreign key constraints for savings product
ALTER TABLE m_savings_product_facility_config
ADD CONSTRAINT fk_spfc_atm_charge FOREIGN KEY (atm_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_atm_wd_charge FOREIGN KEY (atm_withdrawal_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_upi_charge FOREIGN KEY (upi_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_pos_charge FOREIGN KEY (pos_transactions_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_chq_charge FOREIGN KEY (cheque_book_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_imps_charge FOREIGN KEY (imps_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_sms_charge FOREIGN KEY (sms_alerts_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_bbps_charge FOREIGN KEY (bbps_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_ins_charge FOREIGN KEY (insurance_cover_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_bepg_charge FOREIGN KEY (bepg_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_neft_charge FOREIGN KEY (neft_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_rtgs_charge FOREIGN KEY (rtgs_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_ibtc_charge FOREIGN KEY (inter_bank_total_cap_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_wd_charge FOREIGN KEY (withdrawal_facility_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_dep_charge FOREIGN KEY (deposit_facility_kyc_charge_id) REFERENCES m_charge(id),
ADD CONSTRAINT fk_spfc_vdc_charge FOREIGN KEY (virtual_debit_card_charge_id) REFERENCES m_charge(id);
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.accounting.journalentry;
import java.time.OffsetDateTime;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
@@ -45,7 +46,7 @@ public interface JournalEntryMapper {
@Mapping(target = "amount", source = "amount")
@Mapping(target = "entityType", source = "entityType", qualifiedByName = "entityType")
@Mapping(target = "entityId", source = "entityId")
@Mapping(target = "submittedOnDate", source = "submittedOnDate")
@Mapping(target = "submittedOnDate", expression = "java(journalEntry.getCreatedDate().orElse(null))")
@Mapping(target = "transactionId", source = "transactionId")
@Mapping(target = "currency", source = "currencyCode")
@Mapping(target = "manualEntry", source = "manualEntry")
@@ -0,0 +1,41 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.accounting.journalentry.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DayWiseSummaryData {
private LocalDate date;
private BigDecimal openingBalance;
private BigDecimal totalDebit;
private BigDecimal totalCredit;
private BigDecimal netMovement;
private BigDecimal closingBalance;
private int entryCount;
}
@@ -0,0 +1,72 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.accounting.journalentry.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.infrastructure.core.service.Page;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GLAccountLedgerReportData {
/** GL Account details (name, code, type, description, usage) */
private GLAccountData glAccount;
/** Balance of the account before the fromDate (0 if no fromDate or no glAccountId) */
private BigDecimal openingBalance;
/** Balance of the account as of toDate = openingBalance + netMovement */
private BigDecimal closingBalance;
/** Sum of all DEBIT entries in the requested period */
private BigDecimal totalDebit;
/** Sum of all CREDIT entries in the requested period */
private BigDecimal totalCredit;
/**
* Net movement: for ASSET/EXPENSE (debit-normal) = totalDebit - totalCredit;
* for LIABILITY/INCOME/EQUITY (credit-normal) = totalCredit - totalDebit
*/
private BigDecimal netMovement;
/** true for ASSET and EXPENSE accounts (debit-normal balance convention) */
private boolean debitNormal;
private LocalDate fromDate;
private LocalDate toDate;
/** Day-wise breakdown of balances for the period */
private List<DayWiseSummaryData> dayWiseSummary;
/** Total number of journal entries matching the filter (for the KPI card) */
private long totalEntries;
/** Paginated journal entries for the period */
private Page<JournalEntryData> entries;
}
@@ -20,6 +20,7 @@ package org.apache.fineract.accounting.journalentry.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.List;
import lombok.Data;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
@@ -60,7 +61,7 @@ public class JournalEntryData {
@SuppressWarnings("unused")
private Long createdByUserId;
@SuppressWarnings("unused")
private LocalDate createdDate;
private OffsetDateTime createdDate;
@SuppressWarnings("unused")
private String createdByUserName;
@SuppressWarnings("unused")
@@ -79,7 +80,7 @@ public class JournalEntryData {
@SuppressWarnings("unused")
private TransactionDetailData transactionDetails;
@SuppressWarnings("unused")
private LocalDate submittedOnDate;
private OffsetDateTime submittedOnDate;
// import fields
private transient Integer rowIndex;
@@ -189,7 +190,7 @@ public class JournalEntryData {
public JournalEntryData(final Long id, final Long officeId, final String officeName, final String glAccountName, final Long glAccountId,
final String glAccountCode, final EnumOptionData glAccountClassification, final LocalDate transactionDate,
final EnumOptionData entryType, final BigDecimal amount, final String transactionId, final Boolean manualEntry,
final EnumOptionData entityType, final Long entityId, final Long createdByUserId, final LocalDate submittedOnDate,
final EnumOptionData entityType, final Long entityId, final Long createdByUserId, final OffsetDateTime submittedOnDate,
final String createdByUserName, final String comments, final Boolean reversed, final String referenceNumber,
final BigDecimal officeRunningBalance, final BigDecimal organizationRunningBalance, final Boolean runningBalanceComputed,
final TransactionDetailData transactionDetailData, final CurrencyData currency, final String externalAssetOwner) {
@@ -251,7 +252,7 @@ public class JournalEntryData {
final EnumOptionData entityType = null;
final Long entityId = null;
final Long createdByUserId = null;
final LocalDate submittedOnDate = null;
final OffsetDateTime submittedOnDate = null;
final String createdByUserName = null;
final String comments = null;
final Boolean reversed = null;
@@ -19,6 +19,7 @@
package org.apache.fineract.accounting.journalentry.service;
import java.time.LocalDate;
import org.apache.fineract.accounting.journalentry.data.GLAccountLedgerReportData;
import org.apache.fineract.accounting.journalentry.data.JournalEntryAssociationParametersData;
import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
import org.apache.fineract.accounting.journalentry.data.OfficeOpeningBalancesData;
@@ -36,4 +37,11 @@ public interface JournalEntryReadPlatformService {
OfficeOpeningBalancesData retrieveOfficeOpeningBalances(Long officeId, String currencyCode);
Page<JournalEntryData> retrieveJournalEntriesByEntityId(String transactionId, Long entityId, Integer entityType);
/**
* CBS GL Ledger Report: returns opening balance, day-wise summary, and paginated entries for a GL account.
* Opening balance is computed as the net balance of all entries before fromDate.
*/
GLAccountLedgerReportData retrieveGLAccountLedger(Long glAccountId, Long officeId, Boolean onlyManualEntries,
LocalDate fromDate, LocalDate toDate, String transactionId, SearchParameters searchParameters);
}
@@ -41,22 +41,36 @@ public interface ClientNomineeRepository extends JpaRepository<ClientNominee, Lo
@Query("SELECT n FROM ClientNominee n WHERE n.client.id = :clientId AND n.accountNo = :accountNo AND n.accountType = :accountType AND n.isDeleted = false ORDER BY n.id")
List<ClientNominee> findByClientIdAndAccountNoAndAccountType(@Param("clientId") Long clientId, @Param("accountNo") String accountNo, @Param("accountType") String accountType);
@Query("SELECT COUNT(n) FROM ClientNominee n WHERE n.accountNo = :accountNo AND n.accountType = :accountType AND n.isDeleted = false")
@Query("SELECT COUNT(n) FROM ClientNominee n WHERE n.accountNo = :accountNo AND n.accountType = :accountType")
long countByAccountNoAndAccountType(@Param("accountNo") String accountNo, @Param("accountType") String accountType);
/**
* Counts non-deleted nominee rows that link {@code nomineeClientId} as a nominee under
* {@code clientId} for the given {@code accountType}. Backs the rule that the same person
* may not be linked as a nominee more than once per (account owner, account type).
* {@code excludeNomineeId} is set on update so a row doesn't conflict with itself.
*/
@Query("SELECT COUNT(n) FROM ClientNominee n "
+ "WHERE n.client.id = :clientId "
+ "AND n.nomineeClient.id = :nomineeClientId "
+ "AND n.accountType = :accountType "
+ "AND n.isDeleted = false")
long countActiveByClientNomineeAndAccountType(@Param("clientId") Long clientId,
@Param("nomineeClientId") Long nomineeClientId,
@Param("accountType") String accountType);
@Query("SELECT COUNT(n) FROM ClientNominee n "
+ "WHERE n.client.id = :clientId "
+ "AND n.nomineeClient.id = :nomineeClientId "
+ "AND n.accountNo = :accountNo "
+ "AND n.accountType = :accountType "
+ "AND n.isDeleted = false")
long countActiveByClientNomineeAndAccountNoAndType(@Param("clientId") Long clientId,
@Param("nomineeClientId") Long nomineeClientId,
@Param("accountNo") String accountNo,
@Param("accountType") String accountType);
@Query("SELECT COUNT(n) FROM ClientNominee n "
+ "WHERE n.client.id = :clientId "
+ "AND n.nomineeClient.id = :nomineeClientId "
+ "AND n.accountType = :accountType "
+ "AND n.isDeleted = false "
+ "AND (:excludeNomineeId IS NULL OR n.id <> :excludeNomineeId)")
long countActiveByClientNomineeAndAccountType(@Param("clientId") Long clientId,
+ "AND n.id <> :excludeNomineeId")
long countActiveByClientNomineeAndAccountTypeExcluding(@Param("clientId") Long clientId,
@Param("nomineeClientId") Long nomineeClientId,
@Param("accountType") String accountType,
@Param("excludeNomineeId") Long excludeNomineeId);
@@ -274,6 +274,46 @@ public final class DepositsApiConstants {
fixedDepositRequestData.addAll(DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS);
fixedDepositRequestData.addAll(PRECLOSURE_REQUEST_DATA_PARAMETERS);
fixedDepositRequestData.addAll(DEPOSIT_TERM_REQUEST_DATA_PARAMETERS);
fixedDepositRequestData.add(SavingsApiConstants.minBalanceForInterestCalculationParamName);
fixedDepositRequestData.add("accountType");
fixedDepositRequestData.add("customerSegment");
fixedDepositRequestData.add("productCode");
fixedDepositRequestData.add("fdType");
fixedDepositRequestData.add(SavingsApiConstants.glAccountIdParamName);
fixedDepositRequestData.add("effectiveDate");
fixedDepositRequestData.add("effectiveTo");
fixedDepositRequestData.add("endDate");
fixedDepositRequestData.add("introducerRequired");
fixedDepositRequestData.add("shareLinkingEnabled");
fixedDepositRequestData.add("nominationFacility");
fixedDepositRequestData.add("dormancyAlert");
fixedDepositRequestData.add(SavingsApiConstants.isDormancyTrackingActiveParamName);
fixedDepositRequestData.add(SavingsApiConstants.daysToInactiveParamName);
fixedDepositRequestData.add(SavingsApiConstants.daysToDormancyParamName);
fixedDepositRequestData.add(SavingsApiConstants.daysToEscheatParamName);
fixedDepositRequestData.add("interestCalculateSelector");
fixedDepositRequestData.add("averagePeriod");
fixedDepositRequestData.add("autoPost");
fixedDepositRequestData.add("frequencyFixDay");
fixedDepositRequestData.add("lastInterestDate");
fixedDepositRequestData.add("enableLockinPeriod");
fixedDepositRequestData.add("balanceFrequency");
fixedDepositRequestData.add("accountClosedZeroBalanceInDayClosing");
fixedDepositRequestData.add("autoDebitFacility");
fixedDepositRequestData.add("loanFacility");
fixedDepositRequestData.add("prematureWithdrawalOption");
fixedDepositRequestData.add("tenureFlexibility");
fixedDepositRequestData.add("lienMarked");
fixedDepositRequestData.add("withdrawalFeeForTransfers");
fixedDepositRequestData.add("maturityInstructions");
fixedDepositRequestData.add("creditMaturityProceeds");
fixedDepositRequestData.add("penaltyGracePeriodMissedInstallments");
fixedDepositRequestData.add("panelRequestApplicable");
fixedDepositRequestData.add("noPenaltyNomineeHeirClosure");
fixedDepositRequestData.add("deceasedSettlement");
fixedDepositRequestData.add("allowDebitTxnAfterDueDate");
fixedDepositRequestData.add("escheatLiabilityId");
fixedDepositRequestData.add("fdSlabs");
return fixedDepositRequestData;
}
@@ -282,6 +322,54 @@ public final class DepositsApiConstants {
fixedDepositRequestData.addAll(DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS);
fixedDepositRequestData.addAll(PRECLOSURE_RESPONSE_DATA_PARAMETERS);
fixedDepositRequestData.addAll(DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS);
fixedDepositRequestData.add(SavingsApiConstants.minBalanceForInterestCalculationParamName);
fixedDepositRequestData.add("interestCalculationTypeOption");
fixedDepositRequestData.add("periodFrequencyOptions");
fixedDepositRequestData.add("yesNoOptions");
fixedDepositRequestData.add("accountTypeOptions");
fixedDepositRequestData.add("customerSegmentOptions");
fixedDepositRequestData.add("accountType");
fixedDepositRequestData.add("customerSegment");
fixedDepositRequestData.add("productCode");
fixedDepositRequestData.add("fdType");
fixedDepositRequestData.add(SavingsApiConstants.glAccountIdParamName);
fixedDepositRequestData.add("glAccountName");
fixedDepositRequestData.add("effectiveDate");
fixedDepositRequestData.add("effectiveTo");
fixedDepositRequestData.add("endDate");
fixedDepositRequestData.add("introducerRequired");
fixedDepositRequestData.add("shareLinkingEnabled");
fixedDepositRequestData.add("nominationFacility");
fixedDepositRequestData.add("dormancyAlert");
fixedDepositRequestData.add(SavingsApiConstants.isDormancyTrackingActiveParamName);
fixedDepositRequestData.add(SavingsApiConstants.daysToInactiveParamName);
fixedDepositRequestData.add(SavingsApiConstants.daysToDormancyParamName);
fixedDepositRequestData.add(SavingsApiConstants.daysToEscheatParamName);
fixedDepositRequestData.add("interestCalculateSelector");
fixedDepositRequestData.add("averagePeriod");
fixedDepositRequestData.add("autoPost");
fixedDepositRequestData.add("frequencyFixDay");
fixedDepositRequestData.add("lastInterestDate");
fixedDepositRequestData.add("enableLockinPeriod");
fixedDepositRequestData.add("balanceFrequency");
fixedDepositRequestData.add("accountClosedZeroBalanceInDayClosing");
fixedDepositRequestData.add("autoDebitFacility");
fixedDepositRequestData.add("loanFacility");
fixedDepositRequestData.add("prematureWithdrawalOption");
fixedDepositRequestData.add("tenureFlexibility");
fixedDepositRequestData.add("lienMarked");
fixedDepositRequestData.add("withdrawalFeeForTransfers");
fixedDepositRequestData.add("maturityInstructions");
fixedDepositRequestData.add("creditMaturityProceeds");
fixedDepositRequestData.add("penaltyGracePeriodMissedInstallments");
fixedDepositRequestData.add("panelRequestApplicable");
fixedDepositRequestData.add("noPenaltyNomineeHeirClosure");
fixedDepositRequestData.add("deceasedSettlement");
fixedDepositRequestData.add("allowDebitTxnAfterDueDate");
fixedDepositRequestData.add("accountingMappings");
fixedDepositRequestData.add("paymentChannelToFundSourceMappings");
fixedDepositRequestData.add("feeToIncomeAccountMappings");
fixedDepositRequestData.add("penaltyToIncomeAccountMappings");
return fixedDepositRequestData;
}
@@ -332,6 +420,7 @@ public final class DepositsApiConstants {
recurringDepositRequestData.add("deceasedSettlement");
recurringDepositRequestData.add("allowDebitTxnAfterDueDate");
recurringDepositRequestData.add("accountClosedZeroBalanceInDayClosing");
recurringDepositRequestData.add("escheatLiabilityId");
// Custom CBS fields — Interest rate slabs (sent inline but ignored server-side; saved separately)
recurringDepositRequestData.add("rdSlabs");
return recurringDepositRequestData;
@@ -387,6 +476,11 @@ public final class DepositsApiConstants {
recurringDepositRequestData.add("noPenaltyNomineeHeirClosure");
recurringDepositRequestData.add("deceasedSettlement");
recurringDepositRequestData.add("allowDebitTxnAfterDueDate");
recurringDepositRequestData.add("escheatLiabilityId");
recurringDepositRequestData.add("accountingMappings");
recurringDepositRequestData.add("paymentChannelToFundSourceMappings");
recurringDepositRequestData.add("feeToIncomeAccountMappings");
recurringDepositRequestData.add("penaltyToIncomeAccountMappings");
return recurringDepositRequestData;
}
@@ -428,6 +522,28 @@ public final class DepositsApiConstants {
fixedDepositRequestData.addAll(DEPOSIT_TERM_REQUEST_DATA_PARAMETERS);
fixedDepositRequestData.add(linkedAccountParamName);
fixedDepositRequestData.add(transferInterestToSavingsParamName);
fixedDepositRequestData.add("accountType");
fixedDepositRequestData.add("customerSegment");
fixedDepositRequestData.add(SavingsApiConstants.accountStatusParamName);
fixedDepositRequestData.add(SavingsApiConstants.modeOfOperationParamName);
fixedDepositRequestData.add(SavingsApiConstants.specialCategoryParamName);
fixedDepositRequestData.add(SavingsApiConstants.weakerSectionParamName);
fixedDepositRequestData.add(SavingsApiConstants.guardianNameParamName);
fixedDepositRequestData.add(SavingsApiConstants.guardianCifNumberParamName);
fixedDepositRequestData.add(SavingsApiConstants.guardianRelationshipParamName);
fixedDepositRequestData.add(SavingsApiConstants.guardianClientIdParamName);
fixedDepositRequestData.add(SavingsApiConstants.minorDateOfBirthParamName);
fixedDepositRequestData.add(SavingsApiConstants.introducerIdParamName);
fixedDepositRequestData.add(SavingsApiConstants.introducerNameParamName);
fixedDepositRequestData.add(SavingsApiConstants.introducerCifNumberParamName);
fixedDepositRequestData.add(SavingsApiConstants.nomineeParamName);
fixedDepositRequestData.add("nominees");
fixedDepositRequestData.add(SavingsApiConstants.linkedAccountIdParamName);
fixedDepositRequestData.add(SavingsApiConstants.linkedAccountTypeParamName);
fixedDepositRequestData.add(SavingsApiConstants.linkedAccountNoParamName);
fixedDepositRequestData.add(SavingsApiConstants.jointHoldersParamName);
fixedDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName);
fixedDepositRequestData.add(SavingsApiConstants.nominationFacilityParamName);
return fixedDepositRequestData;
}
@@ -438,6 +554,31 @@ public final class DepositsApiConstants {
fixedDepositResponseData.addAll(DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS);
fixedDepositResponseData.add(linkedAccountParamName);
fixedDepositResponseData.add(transferInterestToSavingsParamName);
fixedDepositResponseData.add("accountType");
fixedDepositResponseData.add("customerSegment");
fixedDepositResponseData.add("accountTypeOptions");
fixedDepositResponseData.add("customerSegmentOptions");
fixedDepositResponseData.add("productCode");
fixedDepositResponseData.add("previewAccountNo");
fixedDepositResponseData.add(SavingsApiConstants.accountStatusParamName);
fixedDepositResponseData.add(SavingsApiConstants.modeOfOperationParamName);
fixedDepositResponseData.add(SavingsApiConstants.specialCategoryParamName);
fixedDepositResponseData.add(SavingsApiConstants.weakerSectionParamName);
fixedDepositResponseData.add(SavingsApiConstants.guardianNameParamName);
fixedDepositResponseData.add(SavingsApiConstants.guardianCifNumberParamName);
fixedDepositResponseData.add(SavingsApiConstants.guardianRelationshipParamName);
fixedDepositResponseData.add(SavingsApiConstants.guardianClientIdParamName);
fixedDepositResponseData.add(SavingsApiConstants.minorDateOfBirthParamName);
fixedDepositResponseData.add(SavingsApiConstants.introducerIdParamName);
fixedDepositResponseData.add(SavingsApiConstants.introducerNameParamName);
fixedDepositResponseData.add(SavingsApiConstants.introducerCifNumberParamName);
fixedDepositResponseData.add(SavingsApiConstants.nomineeParamName);
fixedDepositResponseData.add(SavingsApiConstants.linkedAccountIdParamName);
fixedDepositResponseData.add(SavingsApiConstants.linkedAccountTypeParamName);
fixedDepositResponseData.add(SavingsApiConstants.linkedAccountNoParamName);
fixedDepositResponseData.add(SavingsApiConstants.jointHoldersParamName);
fixedDepositResponseData.add(SavingsApiConstants.nominationFacilityParamName);
fixedDepositResponseData.add(SavingsApiConstants.autoDebitFacilityParamName);
return fixedDepositResponseData;
}
@@ -447,6 +588,30 @@ public final class DepositsApiConstants {
recurringDepositRequestData.addAll(PRECLOSURE_REQUEST_DATA_PARAMETERS);
recurringDepositRequestData.addAll(DEPOSIT_TERM_REQUEST_DATA_PARAMETERS);
recurringDepositRequestData.addAll(RECURRING_DETAILS_REQUEST_DATA_PARAMETERS);
// CBS Account fields
recurringDepositRequestData.add("accountType");
recurringDepositRequestData.add("customerSegment");
recurringDepositRequestData.add(SavingsApiConstants.accountStatusParamName);
recurringDepositRequestData.add(SavingsApiConstants.modeOfOperationParamName);
recurringDepositRequestData.add(SavingsApiConstants.specialCategoryParamName);
recurringDepositRequestData.add(SavingsApiConstants.weakerSectionParamName);
recurringDepositRequestData.add(SavingsApiConstants.guardianNameParamName);
recurringDepositRequestData.add(SavingsApiConstants.guardianCifNumberParamName);
recurringDepositRequestData.add(SavingsApiConstants.guardianRelationshipParamName);
recurringDepositRequestData.add(SavingsApiConstants.guardianClientIdParamName);
recurringDepositRequestData.add(SavingsApiConstants.minorDateOfBirthParamName);
recurringDepositRequestData.add(SavingsApiConstants.introducerIdParamName);
recurringDepositRequestData.add(SavingsApiConstants.introducerNameParamName);
recurringDepositRequestData.add(SavingsApiConstants.introducerCifNumberParamName);
recurringDepositRequestData.add(SavingsApiConstants.nomineeParamName);
recurringDepositRequestData.add("nominees");
recurringDepositRequestData.add(SavingsApiConstants.linkedAccountIdParamName);
recurringDepositRequestData.add(SavingsApiConstants.linkedAccountTypeParamName);
recurringDepositRequestData.add(SavingsApiConstants.linkedAccountNoParamName);
recurringDepositRequestData.add(SavingsApiConstants.jointHoldersParamName);
recurringDepositRequestData.add(SavingsApiConstants.balanceSwapOptionParamName);
recurringDepositRequestData.add(SavingsApiConstants.msmeParamName);
recurringDepositRequestData.add(SavingsApiConstants.autoDebitFacilityParamName);
return recurringDepositRequestData;
}
@@ -494,6 +659,10 @@ public final class DepositsApiConstants {
recurringDepositResponseData.add("noPenaltyNomineeHeirClosure");
recurringDepositResponseData.add("deceasedSettlement");
recurringDepositResponseData.add("allowDebitTxnAfterDueDate");
recurringDepositResponseData.add("accountingMappings");
recurringDepositResponseData.add("paymentChannelToFundSourceMappings");
recurringDepositResponseData.add("feeToIncomeAccountMappings");
recurringDepositResponseData.add("penaltyToIncomeAccountMappings");
return recurringDepositResponseData;
}
@@ -32,6 +32,8 @@ public class SavingsApiConstants {
public static final String CURRENT_PRODUCT_CUSTOMER_SEGMENT = "CurrentProductCustomerSegment";
public static final String RD_PRODUCT_ACCOUNT_TYPE = "RecurringDepositProductAccountType";
public static final String RD_PRODUCT_CUSTOMER_SEGMENT = "RecurringDepositProductCustomerSegment";
public static final String FD_PRODUCT_ACCOUNT_TYPE = "FixedDepositProductAccountType";
public static final String FD_PRODUCT_CUSTOMER_SEGMENT = "FixedDepositProductCustomerSegment";
public static final String PERIOD_FREQUENCY = "PeriodFrequency";
public static final String INTEREST_CALCULATION_TYPE_OPTION = "InterestCalculationTypeOption";
@@ -41,7 +41,10 @@ import org.apache.fineract.organisation.staff.domain.Staff;
import org.apache.fineract.portfolio.client.domain.Client;
@Entity
@Table(name = "m_current_account")
@Table(name = "m_current_account", uniqueConstraints = {
@jakarta.persistence.UniqueConstraint(columnNames = { "account_no" }, name = "ca_account_no_UNIQUE"),
@jakarta.persistence.UniqueConstraint(columnNames = { "external_id" }, name = "ca_unq_external_id")
})
@Getter
@Setter
public class CurrentAccount extends AbstractAuditableCustom {
@@ -291,6 +294,17 @@ public class CurrentAccount extends AbstractAuditableCustom {
@Column(name = "avg_balance_calc_period", length = 10)
private String avgBalanceCalcPeriod;
// Extra fields copied from product (not in m_savings_account dormancy set)
@Column(name = "account_closed_zero_balance_in_day_closing")
private Boolean accountClosedZeroBalanceInDayClosing = false;
@Column(name = "valid_for_period", length = 10)
private String validForPeriod;
@Column(name = "od_account_closed_zero_balance_in_day_closing")
private boolean odAccountClosedZeroBalanceInDayClosing = false;
// Relationships
@OneToMany(mappedBy = "currentAccount", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<CurrentAccountTransaction> transactions = new ArrayList<>();
@@ -383,4 +397,13 @@ public class CurrentAccount extends AbstractAuditableCustom {
this.statusEnum = 600;
this.closedOnDate = closedOnDate;
}
public void setMinRequiredOpeningBalance(java.math.BigDecimal val) { this.minRequiredOpeningBalance = val; }
public void setMinRequiredBalance(java.math.BigDecimal val) { this.minRequiredBalance = val; }
public void setEnforceMinRequiredBalance(boolean val) { this.enforceMinRequiredBalance = val; }
public void setLockinPeriodFrequency(Integer val) { this.lockinPeriodFrequency = val; }
public void setLockinPeriodFrequencyEnum(Integer val) { this.lockinPeriodFrequencyEnum = val; }
public void setNominalAnnualInterestRateOverdraft(java.math.BigDecimal val) { this.nominalAnnualInterestRateOverdraft = val; }
public void setWithdrawalFeeForTransfer(boolean val) { this.withdrawalFeeForTransfer = val; }
public void setMinOverdraftForInterestCalculation(java.math.BigDecimal val) { this.minOverdraftForInterestCalculation = val; }
}
@@ -106,4 +106,22 @@ public class CurrentAccountCharge extends AbstractPersistableCustom<Long> {
private LocalDate chargeResetDate;
protected CurrentAccountCharge() {}
public static CurrentAccountCharge createFromFacility(final CurrentAccount account, final Charge charge) {
final CurrentAccountCharge c = new CurrentAccountCharge();
c.currentAccount = account;
c.charge = charge;
c.chargeTimeEnum = charge.getChargeTimeType();
c.chargeCalculationEnum = charge.getChargeCalculation();
c.amount = charge.getAmount();
c.amountOutstandingDerived = charge.getAmount();
c.penalty = charge.isPenalty();
c.active = true;
if (charge.getFeeOnMonthDay() != null) {
c.feeOnMonth = charge.getFeeOnMonthDay().getMonthValue();
c.feeOnDay = charge.getFeeOnMonthDay().getDayOfMonth();
}
c.feeInterval = charge.getFeeInterval();
return c;
}
}
@@ -92,7 +92,7 @@ public class CurrentAccountFacilityConfig extends AbstractPersistableCustom<Long
@Column(name = "mobile_banking", nullable = false)
private boolean mobileBanking = false;
protected CurrentAccountFacilityConfig() {}
public CurrentAccountFacilityConfig() {}
public static CurrentAccountFacilityConfig createFor(CurrentAccount account) {
CurrentAccountFacilityConfig cfg = new CurrentAccountFacilityConfig();
@@ -60,5 +60,8 @@ public class CurrentAccountJointHolder extends AbstractPersistableCustom<Long> {
@Column(name = "is_primary", nullable = false)
private boolean primary = false;
@Column(name = "is_mandatory", nullable = false)
private boolean mandatory = false;
public CurrentAccountJointHolder() {}
}
@@ -37,4 +37,7 @@ public interface CurrentAccountRepository extends JpaRepository<CurrentAccount,
@Query("SELECT ca FROM CurrentAccount ca WHERE ca.client.id = :clientId AND ca.statusEnum != 600")
List<CurrentAccount> findActiveByClientId(@Param("clientId") Long clientId);
@Query("SELECT COUNT(ca) FROM CurrentAccount ca WHERE ca.client.id = :clientId AND ca.productId = :productId AND ca.statusEnum != 600")
long countActiveByClientAndProduct(@Param("clientId") Long clientId, @Param("productId") Long productId);
}
@@ -122,6 +122,18 @@ public class CurrentAccountTransaction extends AbstractPersistableCustom<Long> {
return txn;
}
public static CurrentAccountTransaction chargeDeduction(CurrentAccount account, Office office,
BigDecimal amount, LocalDate transactionDate) {
CurrentAccountTransaction txn = new CurrentAccountTransaction();
txn.currentAccount = account;
txn.office = office;
txn.transactionTypeEnum = 7; // PAY_CHARGE
txn.amount = amount;
txn.transactionDate = transactionDate;
txn.submittedOnDate = transactionDate;
return txn;
}
public static CurrentAccountTransaction withdrawal(CurrentAccount account, Office office,
BigDecimal amount, LocalDate transactionDate, String refNo) {
CurrentAccountTransaction txn = new CurrentAccountTransaction();
@@ -226,7 +226,7 @@ public class CurrentProduct extends AbstractPersistableCustom<Long> {
@Column(name = "customer_segment", length = 50)
private String customerSegment;
@Column(name = "product_code", length = 20, unique = true)
@Column(name = "product_code", length = 20)
private String productCode;
@Column(name = "gl_account_id")
@@ -447,6 +447,10 @@ public class CurrentProduct extends AbstractPersistableCustom<Long> {
return this.withHoldTax;
}
public boolean isLienAllowed() { return this.lienAllowed; }
public BigDecimal getMaxAllowedLienLimit() { return this.maxAllowedLienLimit; }
public BigDecimal getMinBalanceForInterestCalculation() { return this.minBalanceForInterestCalculation; }
public TaxGroup getTaxGroup() {
return this.taxGroup;
}
@@ -867,6 +871,14 @@ public class CurrentProduct extends AbstractPersistableCustom<Long> {
public void setLastInterestDate(LocalDate lastInterestDate) { this.lastInterestDate = lastInterestDate; }
public boolean isEnableLockinPeriod() { return enableLockinPeriod; }
public void setEnableLockinPeriod(boolean enableLockinPeriod) { this.enableLockinPeriod = enableLockinPeriod; }
public BigDecimal getMinRequiredOpeningBalance() { return minRequiredOpeningBalance; }
public BigDecimal getMinRequiredBalance() { return minRequiredBalance; }
public boolean isEnforceMinRequiredBalance() { return enforceMinRequiredBalance; }
public Integer getLockinPeriodFrequency() { return lockinPeriodFrequency; }
public Integer getLockinPeriodFrequencyType() { return lockinPeriodFrequencyType; }
public BigDecimal getNominalAnnualInterestRateOverdraft() { return nominalAnnualInterestRateOverdraft; }
public boolean isWithdrawalFeeApplicableForTransfer() { return withdrawalFeeApplicableForTransfer; }
public BigDecimal getMinOverdraftForInterestCalculation() { return minOverdraftForInterestCalculation; }
public String getBalanceFrequency() { return balanceFrequency; }
public void setBalanceFrequency(String balanceFrequency) { this.balanceFrequency = balanceFrequency; }
public Boolean getAccountClosedZeroBalanceInDayClosing() { return accountClosedZeroBalanceInDayClosing; }
@@ -240,7 +240,8 @@ public class CurrentProductAssembler {
product.setCustomerSegment(command.stringValueOfParameterNamed(customerSegmentParamName));
}
if (command.parameterExists(productCodeParamName)) {
product.setProductCode(command.stringValueOfParameterNamed(productCodeParamName));
final String productCode = command.stringValueOfParameterNamed(productCodeParamName);
product.setProductCode((productCode != null && !productCode.isBlank()) ? productCode : null);
}
if (command.parameterExists(glAccountIdParamName)) {
product.setGlAccountId(command.longValueOfParameterNamed(glAccountIdParamName));
+1
View File
@@ -123,6 +123,7 @@ dependencies {
'com.sun.activation:jakarta.activation',
'com.sun.mail:jakarta.mail',
'org.springframework.boot:spring-boot-starter-mail',
'org.springdoc:springdoc-openapi-starter-webmvc-ui',
'org.mapstruct:mapstruct',
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -44,6 +44,7 @@ import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.journalentry.command.JournalEntryCommand;
import org.apache.fineract.accounting.journalentry.data.GLAccountLedgerReportData;
import org.apache.fineract.accounting.journalentry.data.JournalEntryAssociationParametersData;
import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
import org.apache.fineract.accounting.journalentry.data.OfficeOpeningBalancesData;
@@ -268,6 +269,57 @@ public class JournalEntriesApiResource {
return this.apiJsonSerializerService.serialize(settings, officeOpeningBalancesData);
}
/**
* CBS GL Account Ledger Report — returns opening balance, day-wise summary,
* period totals, and paginated entries for a given GL account and date range.
*
* GET /journalentries/ledger?glAccountId=5&fromDate=01 Jan 2025&toDate=31 Jan 2025&dateFormat=dd MMM yyyy&locale=en
*/
@GET
@Path("ledger")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "GL Account Ledger Report",
description = "Returns opening balance, day-wise running balance summary, period totals, and paginated journal entries for a GL account.")
public String retrieveGLAccountLedger(@Context final UriInfo uriInfo,
@QueryParam("glAccountId") @Parameter(description = "glAccountId") final Long glAccountId,
@QueryParam("officeId") @Parameter(description = "officeId") final Long officeId,
@QueryParam("manualEntriesOnly") @Parameter(description = "manualEntriesOnly") final Boolean onlyManualEntries,
@QueryParam("fromDate") @Parameter(description = "fromDate") final DateParam fromDateParam,
@QueryParam("toDate") @Parameter(description = "toDate") final DateParam toDateParam,
@QueryParam("transactionId") @Parameter(description = "transactionId") final String transactionId,
@QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
@QueryParam("locale") @Parameter(description = "locale") final String locale,
@QueryParam("dateFormat") @Parameter(description = "dateFormat") final String rawDateFormat) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSION);
final DateFormat dateFormat = StringUtils.isBlank(rawDateFormat) ? null : new DateFormat(rawDateFormat);
LocalDate fromDate = null;
if (fromDateParam != null) {
fromDate = fromDateParam.getDate("fromDate", dateFormat, locale);
}
LocalDate toDate = null;
if (toDateParam != null) {
toDate = toDateParam.getDate("toDate", dateFormat, locale);
}
sqlValidator.validate(orderBy);
sqlValidator.validate(sortOrder);
final SearchParameters searchParameters = SearchParameters.builder()
.limit(limit).officeId(officeId).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build();
final GLAccountLedgerReportData ledgerData = this.journalEntryReadPlatformService
.retrieveGLAccountLedger(glAccountId, officeId, onlyManualEntries, fromDate, toDate, transactionId, searchParameters);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.apiJsonSerializerService.serialize(settings, ledgerData);
}
private boolean is(final String commandParam, final String commandValue) {
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
}
@@ -24,9 +24,11 @@ import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
@@ -36,6 +38,8 @@ import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialA
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.glaccount.service.GLAccountReadPlatformService;
import org.apache.fineract.accounting.journalentry.data.DayWiseSummaryData;
import org.apache.fineract.accounting.journalentry.data.GLAccountLedgerReportData;
import org.apache.fineract.accounting.journalentry.data.JournalEntryAssociationParametersData;
import org.apache.fineract.accounting.journalentry.data.JournalEntryData;
import org.apache.fineract.accounting.journalentry.data.OfficeOpeningBalancesData;
@@ -97,7 +101,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
.append(" journalEntry.type_enum as entryType,journalEntry.amount as amount, journalEntry.transaction_id as transactionId,")
.append(" journalEntry.entity_type_enum as entityType, journalEntry.entity_id as entityId, creatingUser.id as createdByUserId, ")
.append(" creatingUser.username as createdByUserName, journalEntry.description as comments, ")
.append(" journalEntry.submitted_on_date as submittedOnDate, journalEntry.reversed as reversed, ")
.append(" journalEntry.created_on_utc as submittedOnDate, journalEntry.reversed as reversed, ")
.append(" journalEntry.currency_code as currencyCode, curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ")
.append(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ")
.append(" eao.external_id as externalAssetOwner ");
@@ -157,7 +161,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
final Long entityId = JdbcSupport.getLong(rs, "entityId");
final Long createdByUserId = rs.getLong("createdByUserId");
final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate");
final OffsetDateTime submittedOnDate = rs.getObject("submittedOnDate", OffsetDateTime.class);
final String createdByUserName = rs.getString("createdByUserName");
final String comments = rs.getString("comments");
final Boolean reversed = rs.getBoolean("reversed");
@@ -212,16 +216,20 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
TransactionTypeEnumData transactionTypeEnumData = null;
if (PortfolioAccountType.fromInt(entityTypeId).isLoanAccount()) {
final LoanTransactionEnumData loanTransactionType = LoanEnumerations
.transactionType(JdbcSupport.getInteger(rs, "loanTransactionType"));
transactionTypeEnumData = new TransactionTypeEnumData(loanTransactionType.getId(), loanTransactionType.getCode(),
loanTransactionType.getValue());
} else if (PortfolioAccountType.fromInt(entityTypeId).isSavingsAccount()) {
final SavingsAccountTransactionEnumData savingsTransactionType = SavingsEnumerations
.transactionType(JdbcSupport.getInteger(rs, "savingsTransactionType"));
transactionTypeEnumData = new TransactionTypeEnumData(savingsTransactionType.getId(), savingsTransactionType.getCode(),
savingsTransactionType.getValue());
if (entityTypeId != null && PortfolioAccountType.fromInt(entityTypeId).isLoanAccount()) {
final Integer loanTxnTypeId = JdbcSupport.getInteger(rs, "loanTransactionType");
if (loanTxnTypeId != null) {
final LoanTransactionEnumData loanTransactionType = LoanEnumerations.transactionType(loanTxnTypeId);
transactionTypeEnumData = new TransactionTypeEnumData(loanTransactionType.getId(), loanTransactionType.getCode(),
loanTransactionType.getValue());
}
} else if (entityTypeId != null && PortfolioAccountType.fromInt(entityTypeId).isSavingsAccount()) {
final Integer savingsTxnTypeId = JdbcSupport.getInteger(rs, "savingsTransactionType");
if (savingsTxnTypeId != null) {
final SavingsAccountTransactionEnumData savingsTransactionType = SavingsEnumerations.transactionType(savingsTxnTypeId);
transactionTypeEnumData = new TransactionTypeEnumData(savingsTransactionType.getId(), savingsTransactionType.getCode(),
savingsTransactionType.getValue());
}
}
transactionDetailData = new TransactionDetailData(transaction, paymentDetailData, noteData, transactionTypeEnumData);
@@ -538,4 +546,212 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
throw new JournalEntriesNotFoundException(entityId, e);
}
}
@Override
public GLAccountLedgerReportData retrieveGLAccountLedger(final Long glAccountId, final Long officeId,
final Boolean onlyManualEntries, final LocalDate fromDate, final LocalDate toDate,
final String transactionId, final SearchParameters searchParameters) {
// ── 1. Fetch GL Account details ──────────────────────────────────────
GLAccountData glAccountData = null;
boolean isDebitNormal = true; // ASSET / EXPENSE = debit-normal
if (glAccountId != null && glAccountId > 0) {
glAccountData = this.glAccountReadPlatformService.retrieveGLAccountById(glAccountId,
new JournalEntryAssociationParametersData());
if (glAccountData != null && glAccountData.getType() != null) {
final int typeId = glAccountData.getType().getId().intValue();
// 1=ASSET, 2=LIABILITY, 3=EQUITY, 4=INCOME, 5=EXPENSE
isDebitNormal = (typeId == 1 || typeId == 5);
}
}
// ── 2. Compute opening balance (all entries before fromDate) ─────────
BigDecimal openingBalance = BigDecimal.ZERO;
if (glAccountId != null && glAccountId > 0 && fromDate != null) {
final List<Object> obParams = new ArrayList<>();
final StringBuilder obSql = new StringBuilder(
"SELECT COALESCE(SUM(CASE WHEN je.type_enum = 2 THEN je.amount ELSE 0 END), 0) AS ob_debit, "
+ "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS ob_credit "
+ "FROM acc_gl_journal_entry je "
+ "WHERE je.account_id = ? AND je.entry_date < ?");
obParams.add(glAccountId);
obParams.add(fromDate);
if (officeId != null && officeId > 0) {
obSql.append(" AND je.office_id = ?");
obParams.add(officeId);
}
final BigDecimal[] ob = this.jdbcTemplate.queryForObject(obSql.toString(),
(rs, rowNum) -> new BigDecimal[]{
Objects.requireNonNullElse(rs.getBigDecimal("ob_debit"), BigDecimal.ZERO),
Objects.requireNonNullElse(rs.getBigDecimal("ob_credit"), BigDecimal.ZERO)
}, obParams.toArray());
if (ob != null) {
openingBalance = isDebitNormal ? ob[0].subtract(ob[1]) : ob[1].subtract(ob[0]);
}
}
// ── 3. Day-wise summary for the period ───────────────────────────────
final List<DayWiseSummaryData> dayWiseSummary = new ArrayList<>();
{
final List<Object> dwParams = new ArrayList<>();
final StringBuilder dwSql = new StringBuilder(
"SELECT je.entry_date AS dw_date, "
+ "COALESCE(SUM(CASE WHEN je.type_enum = 2 THEN je.amount ELSE 0 END), 0) AS dw_debit, "
+ "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS dw_credit, "
+ "COUNT(*) AS dw_count "
+ "FROM acc_gl_journal_entry je "
+ "WHERE 1=1");
if (glAccountId != null && glAccountId > 0) {
dwSql.append(" AND je.account_id = ?");
dwParams.add(glAccountId);
}
if (officeId != null && officeId > 0) {
dwSql.append(" AND je.office_id = ?");
dwParams.add(officeId);
}
if (fromDate != null) {
dwSql.append(" AND je.entry_date >= ?");
dwParams.add(fromDate);
}
if (toDate != null) {
dwSql.append(" AND je.entry_date <= ?");
dwParams.add(toDate);
}
if (onlyManualEntries != null && onlyManualEntries) {
dwSql.append(" AND je.manual_entry = true");
}
dwSql.append(" GROUP BY je.entry_date ORDER BY je.entry_date ASC");
final boolean debitNormalFinal = isDebitNormal;
final List<Map<String, Object>> rows = this.jdbcTemplate.queryForList(dwSql.toString(), dwParams.toArray());
BigDecimal running = openingBalance;
for (final Map<String, Object> row : rows) {
// Handle both java.sql.Date (older drivers) and java.time.LocalDate (newer drivers)
final Object rawDate = row.get("dw_date");
LocalDate date;
if (rawDate instanceof LocalDate) {
date = (LocalDate) rawDate;
} else if (rawDate instanceof java.sql.Date) {
date = ((java.sql.Date) rawDate).toLocalDate();
} else {
date = LocalDate.parse(String.valueOf(rawDate).substring(0, 10));
}
final BigDecimal dr = Objects.requireNonNullElse((BigDecimal) row.get("dw_debit"), BigDecimal.ZERO);
final BigDecimal cr = Objects.requireNonNullElse((BigDecimal) row.get("dw_credit"), BigDecimal.ZERO);
final int count = ((Number) row.get("dw_count")).intValue();
final BigDecimal net = debitNormalFinal ? dr.subtract(cr) : cr.subtract(dr);
final BigDecimal closing = running.add(net);
dayWiseSummary.add(DayWiseSummaryData.builder()
.date(date)
.openingBalance(running)
.totalDebit(dr)
.totalCredit(cr)
.netMovement(net)
.closingBalance(closing)
.entryCount(count)
.build());
running = closing;
}
}
// ── 4. Paginated journal entries — only fetch if limit > 0 ───────────
final JournalEntryAssociationParametersData assocParams = new JournalEntryAssociationParametersData(true, true);
final int reqLimit = searchParameters != null && searchParameters.getLimit() != null
? searchParameters.getLimit() : 0;
final Page<JournalEntryData> entries;
if (reqLimit > 0) {
entries = retrieveAll(searchParameters, glAccountId, onlyManualEntries,
fromDate, toDate, null, null, transactionId, null, assocParams);
} else {
// Summary-only request — return empty page, no SQL needed
entries = new Page<>(new ArrayList<>(), 0);
}
// ── 5. Compute period totals ─────────────────────────────────────────
BigDecimal totalDebit = BigDecimal.ZERO;
BigDecimal totalCredit = BigDecimal.ZERO;
{
final List<Object> totParams = new ArrayList<>();
final StringBuilder totSql = new StringBuilder(
"SELECT COALESCE(SUM(CASE WHEN je.type_enum = 2 THEN je.amount ELSE 0 END), 0) AS totalDebit, "
+ "COALESCE(SUM(CASE WHEN je.type_enum = 1 THEN je.amount ELSE 0 END), 0) AS totalCredit "
+ "FROM acc_gl_journal_entry je WHERE 1=1");
if (glAccountId != null && glAccountId > 0) {
totSql.append(" AND je.account_id = ?");
totParams.add(glAccountId);
}
if (officeId != null && officeId > 0) {
totSql.append(" AND je.office_id = ?");
totParams.add(officeId);
}
if (fromDate != null) {
totSql.append(" AND je.entry_date >= ?");
totParams.add(fromDate);
}
if (toDate != null) {
totSql.append(" AND je.entry_date <= ?");
totParams.add(toDate);
}
if (onlyManualEntries != null && onlyManualEntries) {
totSql.append(" AND je.manual_entry = true");
}
final BigDecimal[] totResult = this.jdbcTemplate.queryForObject(totSql.toString(),
(rs, rowNum) -> new BigDecimal[]{
Objects.requireNonNullElse(rs.getBigDecimal("totalDebit"), BigDecimal.ZERO),
Objects.requireNonNullElse(rs.getBigDecimal("totalCredit"), BigDecimal.ZERO)
}, totParams.toArray());
if (totResult != null) {
totalDebit = totResult[0];
totalCredit = totResult[1];
}
}
final BigDecimal netMovement = isDebitNormal ? totalDebit.subtract(totalCredit) : totalCredit.subtract(totalDebit);
final BigDecimal closingBalance = openingBalance.add(netMovement);
// ── 6. Total entries count ────────────────────────────────────────────
long totalEntries = 0L;
{
final List<Object> cntParams = new ArrayList<>();
final StringBuilder cntSql = new StringBuilder(
"SELECT COUNT(*) FROM acc_gl_journal_entry je WHERE 1=1");
if (glAccountId != null && glAccountId > 0) {
cntSql.append(" AND je.account_id = ?");
cntParams.add(glAccountId);
}
if (officeId != null && officeId > 0) {
cntSql.append(" AND je.office_id = ?");
cntParams.add(officeId);
}
if (fromDate != null) {
cntSql.append(" AND je.entry_date >= ?");
cntParams.add(fromDate);
}
if (toDate != null) {
cntSql.append(" AND je.entry_date <= ?");
cntParams.add(toDate);
}
if (onlyManualEntries != null && onlyManualEntries) {
cntSql.append(" AND je.manual_entry = true");
}
final Long cnt = this.jdbcTemplate.queryForObject(cntSql.toString(), Long.class, cntParams.toArray());
if (cnt != null) { totalEntries = cnt; }
}
return GLAccountLedgerReportData.builder()
.glAccount(glAccountData)
.openingBalance(openingBalance)
.closingBalance(closingBalance)
.totalDebit(totalDebit)
.totalCredit(totalCredit)
.netMovement(netMovement)
.debitNormal(isDebitNormal)
.fromDate(fromDate)
.toDate(toDate)
.dayWiseSummary(dayWiseSummary)
.totalEntries(totalEntries)
.entries(entries)
.build();
}
}
@@ -189,7 +189,16 @@ public class AccountAssociationsReadPlatformServiceImpl implements AccountAssoci
@Override
public PortfolioAccountData retriveSavingsAccount(final Long savingsId) {
String accountNo = jdbcTemplate.queryForObject("select account_no from m_savings_account where id = ?", String.class, savingsId);
String accountNo = null;
try {
accountNo = jdbcTemplate.queryForObject("select account_no from m_savings_account where id = ?", String.class, savingsId);
} catch (EmptyResultDataAccessException e) {
try {
accountNo = jdbcTemplate.queryForObject("select account_no from m_current_account where id = ?", String.class, savingsId);
} catch (EmptyResultDataAccessException ex) {
return null;
}
}
return PortfolioAccountData.lookup(savingsId, accountNo);
}
}
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.client.service;
import com.google.gson.JsonElement;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
@@ -28,4 +29,13 @@ public interface ClientNomineeWritePlatformService {
CommandProcessingResult updateNominee(Long clientId, Long nomineeId, JsonCommand command);
CommandProcessingResult deleteNominee(Long clientId, Long nomineeId);
/**
* Saves nominee from a JSON element already extracted from the account creation payload.
* Called internally during account creation (SB/CB/FD/RD) so nominee is persisted
* in the same transaction without a separate API call.
*/
void saveNomineeIfPresent(Long clientId, String accountNo, String accountType, JsonElement nomineeJson);
void saveNomineesIfPresent(Long clientId, String accountNo, String accountType, JsonElement nomineesJson);
}
@@ -18,10 +18,13 @@
*/
package org.apache.fineract.portfolio.client.service;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.math.BigDecimal;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
@@ -43,6 +46,7 @@ import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Slf4j
public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWritePlatformService {
private static final String RELATIONSHIP_CODE = "RELATIONSHIP";
@@ -283,8 +287,11 @@ public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWrite
if (clientId == null || nomineeClientId == null || accountType == null || accountType.isBlank()) {
return;
}
final long existing = nomineeRepository.countActiveByClientNomineeAndAccountType(
clientId, nomineeClientId, accountType, excludeNomineeId);
final long existing = (excludeNomineeId != null)
? nomineeRepository.countActiveByClientNomineeAndAccountTypeExcluding(
clientId, nomineeClientId, accountType, excludeNomineeId)
: nomineeRepository.countActiveByClientNomineeAndAccountType(
clientId, nomineeClientId, accountType);
if (existing > 0) {
throw new PlatformDataIntegrityException("error.msg.nominee.already.linked.for.account.type",
"This person is already a nominee for one of the client's "
@@ -330,4 +337,95 @@ public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWrite
"percentageShare", total);
}
}
@Override
public void saveNomineeIfPresent(final Long clientId, final String accountNo, final String accountType, final JsonElement nomineeJson) {
if (nomineeJson == null || nomineeJson.isJsonNull() || !nomineeJson.isJsonObject()) return;
final JsonObject n = nomineeJson.getAsJsonObject();
if (!n.has("nomineeClientId") || n.get("nomineeClientId").isJsonNull()) return;
try {
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
final Long nomineeClientId = n.get("nomineeClientId").getAsLong();
if (nomineeClientId.equals(clientId)) return; // self-nomination guard
// Skip if nominee already exists for this exact account
if (nomineeRepository.countActiveByClientNomineeAndAccountNoAndType(clientId, nomineeClientId, accountNo, accountType) > 0) {
log.info("Nominee already exists for clientId={}, nomineeClientId={}, accountNo={}, accountType={} — skipping duplicate insert",
clientId, nomineeClientId, accountNo, accountType);
return;
}
final Client nomineeClient = clientRepository.findOneWithNotFoundDetection(nomineeClientId);
final BigDecimal share = n.has("percentageShare") && !n.get("percentageShare").isJsonNull()
? n.get("percentageShare").getAsBigDecimal() : BigDecimal.ZERO;
validatePercentageShareForAccount(accountNo, accountType, null, share);
final ClientNominee nominee = new ClientNominee();
nominee.setClient(client);
nominee.setNomineeClient(nomineeClient);
nominee.setPercentageShare(share);
nominee.setIsActive(true);
nominee.setAccountNo(accountNo);
nominee.setAccountType(accountType);
nominee.setNomineeIdentifier(generateNomineeIdentifier(accountNo, accountType));
if (n.has("relationshipId") && !n.get("relationshipId").isJsonNull()) {
final Long relationshipId = n.get("relationshipId").getAsLong();
final CodeValue relationship = codeValueRepository.findOneByCodeNameAndIdWithNotFoundDetection(
RELATIONSHIP_CODE, relationshipId);
nominee.setRelationship(relationship);
}
final boolean isMinor = n.has("nomineeIsMinor") && !n.get("nomineeIsMinor").isJsonNull()
&& n.get("nomineeIsMinor").getAsBoolean();
nominee.setNomineeIsMinor(isMinor);
if (isMinor && n.has("guardianClientId") && !n.get("guardianClientId").isJsonNull()) {
final Long guardianClientId = n.get("guardianClientId").getAsLong();
final Client guardian = clientRepository.findOneWithNotFoundDetection(guardianClientId);
nominee.setGuardianClient(guardian);
}
if (n.has("remarks") && !n.get("remarks").isJsonNull()) {
nominee.setRemarks(n.get("remarks").getAsString());
}
nomineeRepository.saveAndFlush(nominee);
} catch (Exception e) {
log.warn("Failed to save nominee during account creation for clientId={}, accountNo={}: {}", clientId, accountNo, e.getMessage());
}
}
@Override
public void saveNomineesIfPresent(final Long clientId, final String accountNo, final String accountType, final JsonElement nomineesJson) {
if (nomineesJson == null || nomineesJson.isJsonNull()) return;
if (nomineesJson.isJsonArray()) {
final com.google.gson.JsonArray arr = nomineesJson.getAsJsonArray();
BigDecimal totalShare = BigDecimal.ZERO;
for (com.google.gson.JsonElement el : arr) {
if (!el.isJsonObject()) continue;
final com.google.gson.JsonObject n = el.getAsJsonObject();
if (!n.has("nomineeClientId") || n.get("nomineeClientId").isJsonNull()) continue;
final BigDecimal share = n.has("percentageShare") && !n.get("percentageShare").isJsonNull()
? n.get("percentageShare").getAsBigDecimal() : BigDecimal.ZERO;
totalShare = totalShare.add(share);
}
if (totalShare.compareTo(new BigDecimal("100")) > 0) {
throw new PlatformDataIntegrityException(
"error.msg.nominee.total.share.exceeds.100",
"Total nominee percentage share cannot exceed 100%. Provided total: " + totalShare + "%.",
"percentageShare", totalShare);
}
for (com.google.gson.JsonElement el : arr) {
if (el.isJsonObject()) saveNomineeIfPresent(clientId, accountNo, accountType, el);
}
} else if (nomineesJson.isJsonObject()) {
// fallback: single nominee object
saveNomineeIfPresent(clientId, accountNo, accountType, nomineesJson);
}
}
}
@@ -102,21 +102,6 @@ public class CurrentProductsApiResource {
private final CodeValueReadPlatformService codeValueReadPlatformService;
private final JdbcTemplate jdbcTemplate;
private String generateNextProductCode(final String tableName, final String prefix) {
try {
String maxCode = jdbcTemplate.queryForObject(
"SELECT MAX(product_code) FROM " + tableName + " WHERE product_code LIKE ?",
String.class, prefix + "%");
if (maxCode == null || maxCode.isBlank()) {
return prefix + "001";
}
String numStr = maxCode.substring(prefix.length());
int num = Integer.parseInt(numStr);
return prefix + String.format("%03d", num + 1);
} catch (Exception e) {
return prefix + "001";
}
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@@ -306,7 +291,7 @@ public class CurrentProductsApiResource {
interestCalculationTypeOption, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions,
periodFrequencyOptions, paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, chargeOptions,
penaltyOptions, taxGroupOptions, yesNoOptions, accountTypeOptions, customerSegmentOptions, accountMappingForPayment);
savingsProductToReturn.setProductCode(generateNextProductCode("m_current_product", "CA-IND-"));
// product code removed
}
return savingsProductToReturn;
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.api;
import java.math.BigDecimal;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class FdInterestRateSaveRequest {
private String fdType;
private String versionLabel;
private String remarks;
private List<SlabRequest> slabs;
@Getter
@Setter
@NoArgsConstructor
public static class SlabRequest {
private Integer srNo;
private String description;
private Integer periodTypeEnum;
private Integer fromPeriod;
private Integer toPeriod;
private BigDecimal annualInterestRate;
private BigDecimal seniorCitizenRate;
}
}
@@ -54,9 +54,13 @@ import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import java.util.Locale;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService;
@@ -110,6 +114,8 @@ public class FixedDepositProductsApiResource {
private final DropdownReadPlatformService dropdownReadPlatformService;
private final PaymentTypeReadService paymentTypeReadPlatformService;
private final TaxReadPlatformService taxReadPlatformService;
private final CodeValueReadPlatformService codeValueReadPlatformService;
private final org.springframework.jdbc.core.JdbcTemplate jdbcTemplate;
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@@ -315,6 +321,16 @@ public class FixedDepositProductsApiResource {
// interest rate chart template
final InterestRateChartData chartTemplate = this.interestRateChartReadPlatformService.template();
// CBS custom options
final Collection<EnumOptionData> interestCalculationTypeOption = retrieveInterestCalculationTypeOption();
final Collection<EnumOptionData> periodFrequencyOptions = retrievePeriodFrequencyOptions();
final List<CodeValueData> yesNoOptions = new ArrayList<>(
this.codeValueReadPlatformService.retrieveCodeValuesByCode(SavingsApiConstants.YES_NO));
final List<CodeValueData> accountTypeOptions = new ArrayList<>(
this.codeValueReadPlatformService.retrieveCodeValuesByCode(SavingsApiConstants.FD_PRODUCT_ACCOUNT_TYPE));
final List<CodeValueData> customerSegmentOptions = new ArrayList<>(
this.codeValueReadPlatformService.retrieveCodeValuesByCode(SavingsApiConstants.FD_PRODUCT_CUSTOMER_SEGMENT));
FixedDepositProductData fixedDepositProductToReturn = null;
if (savingsProduct != null) {
fixedDepositProductToReturn = FixedDepositProductData.withTemplate(savingsProduct, currencyOptions,
@@ -329,11 +345,34 @@ public class FixedDepositProductsApiResource {
interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions,
paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, chargeOptions, penaltyOptions, chartTemplate,
preClosurePenalInterestOnTypeOptions, periodFrequencyTypeOptions, taxGroupOptions);
// product code removed
}
fixedDepositProductToReturn.setInterestCalculationTypeOption(interestCalculationTypeOption);
fixedDepositProductToReturn.setPeriodFrequencyOptions(periodFrequencyOptions);
fixedDepositProductToReturn.setYesNoOptions(yesNoOptions);
fixedDepositProductToReturn.setAccountTypeOptions(accountTypeOptions);
fixedDepositProductToReturn.setCustomerSegmentOptions(customerSegmentOptions);
return fixedDepositProductToReturn;
}
private Collection<EnumOptionData> retrievePeriodFrequencyOptions() {
return this.codeValueReadPlatformService.retrieveCodeValuesByCode(SavingsApiConstants.PERIOD_FREQUENCY).stream()
.map(codeValue -> new EnumOptionData(codeValue.getId(),
"periodFrequencyType." + codeValue.getName().toLowerCase(Locale.ROOT).replace(' ', '_'), codeValue.getName()))
.toList();
}
private Collection<EnumOptionData> retrieveInterestCalculationTypeOption() {
return this.codeValueReadPlatformService.retrieveCodeValuesByCode(SavingsApiConstants.INTEREST_CALCULATION_TYPE_OPTION).stream()
.map(codeValue -> new EnumOptionData(codeValue.getId(),
"interestCalculationTypeOption." + codeValue.getName().toLowerCase(Locale.ROOT).replace(' ', '_'),
codeValue.getName()))
.toList();
}
@DELETE
@Path("{productId}")
@Consumes({ MediaType.APPLICATION_JSON })
@@ -342,6 +342,7 @@ final class FixedDepositProductsApiResourceSwagger {
public GetFixedDepositProductsGlAccount interestOnSavingsAccount;
public GetFixedDepositProductsGlAccount savingsControlAccount;
public GetFixedDepositProductsGlAccount transfersInSuspenseAccount;
public GetFixedDepositProductsGlAccount escheatLiabilityAccount;
public GetFixedDepositProductsGlAccount interestPayableAccount;
}
@@ -0,0 +1,87 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.api;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializer;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/fixeddepositproducts/baseinterestrates")
@Component
@RequiredArgsConstructor
public class FixedDepositProductsBaseInterestRatesApiResource {
private final PlatformSecurityContext context;
private final FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService;
private final Gson gson = new GsonBuilder()
.registerTypeAdapter(LocalDate.class,
(JsonSerializer<LocalDate>) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.toString()))
.create();
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Get all active FD base interest rate charts", description = "Returns active CBS FD base interest-rate versions with slabs. Optionally filter by fdType (SINGLE, DOUBLE, SPECIAL).")
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
public String retrieveAllActiveInterestRates(@QueryParam("fdType") final String fdType) {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME);
final List<FdInterestRateEffectiveData> data = fdType != null && !fdType.isBlank()
? this.fdBaseInterestRateReadPlatformService.retrieveActiveInterestRatesByType(fdType)
: this.fdBaseInterestRateReadPlatformService.retrieveAllActiveInterestRates();
return gson.toJson(data);
}
@GET
@Path("history")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Get all FD base interest rate charts", description = "Returns the full FD base chart history.")
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
public String retrieveHistory() {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME);
final List<FdInterestRateEffectiveData> data = this.fdBaseInterestRateReadPlatformService.retrieveAllInterestRates();
return gson.toJson(data);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save new FD base interest rate version", description = "Creates a new FD CBS base chart version for the given fdType and deactivates the previous active version.")
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
public String saveInterestRates(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME);
final FdInterestRateSaveRequest request = gson.fromJson(apiRequestBodyAsJson, FdInterestRateSaveRequest.class);
final Long createdById = this.context.authenticatedUser().getId();
final FdInterestRateEffectiveData saved = this.fdBaseInterestRateReadPlatformService.saveInterestRates(request, createdById);
return gson.toJson(saved);
}
}
@@ -45,6 +45,7 @@ import jakarta.ws.rs.core.UriInfo;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import lombok.RequiredArgsConstructor;
@@ -77,6 +78,7 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformService;
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService;
import org.apache.fineract.portfolio.savings.service.RecurringDepositAccountInterestCalculationService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
@@ -99,6 +101,7 @@ public class RecurringDepositAccountsApiResource {
private final DepositAccountPreMatureCalculationPlatformService accountPreMatureCalculationPlatformService;
private final BulkImportWorkbookService bulkImportWorkbookService;
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
private final RecurringDepositAccountInterestCalculationService recurringDepositAccountInterestCalculationService;
private final SqlValidator sqlValidator;
@GET
@@ -369,6 +372,18 @@ public class RecurringDepositAccountsApiResource {
return this.toApiJsonSerializer.serialize(result);
}
@POST
@Path("calculate-rd-interest")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String calculateRecurringDepositInterest(@Context final UriInfo uriInfo, final String apiRequestBodyAsJson) {
JsonElement jsonElement = fromJsonHelper.parse(apiRequestBodyAsJson);
HashMap<String, Object> result = recurringDepositAccountInterestCalculationService
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, jsonElement, fromJsonHelper));
return toApiJsonSerializer.serializeResult(result);
}
private boolean is(final String commandParam, final String commandValue) {
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
}
@@ -333,7 +333,7 @@ public class RecurringDepositProductsApiResource {
interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions,
paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, chargeOptions, penaltyOptions, chartTemplate,
preClosurePenalInterestOnTypeOptions, periodFrequencyTypeOptions, taxGroupOptions);
recurringDepositProductToReturn.setProductCode(generateNextProductCode("m_savings_product", "RD-IND-"));
// product code removed
}
recurringDepositProductToReturn.setInterestCalculationTypeOption(interestCalculationTypeOption);
@@ -360,21 +360,6 @@ public class RecurringDepositProductsApiResource {
.toList();
}
private String generateNextProductCode(final String tableName, final String prefix) {
try {
String maxCode = jdbcTemplate.queryForObject(
"SELECT MAX(product_code) FROM " + tableName + " WHERE product_code LIKE ? AND deposit_type_enum = 300",
String.class, prefix + "%");
if (maxCode == null || maxCode.isBlank()) {
return prefix + "001";
}
String numStr = maxCode.substring(prefix.length());
int num = Integer.parseInt(numStr);
return prefix + String.format("%03d", num + 1);
} catch (Exception e) {
return prefix + "001";
}
}
// ── RD Interest Rate Chart endpoints ────────────────────────────────────
@@ -360,6 +360,7 @@ final class RecurringDepositProductsApiResourceSwagger {
public GetRecurringDepositProductsGlAccount interestOnSavingsAccount;
public GetRecurringDepositProductsGlAccount savingsControlAccount;
public GetRecurringDepositProductsGlAccount transfersInSuspenseAccount;
public GetRecurringDepositProductsGlAccount escheatLiabilityAccount;
public GetRecurringDepositProductsGlAccount feeReceivableAccount;
public GetRecurringDepositProductsGlAccount penaltyReceivableAccount;
public GetRecurringDepositProductsGlAccount interestPayableAccount;
@@ -106,21 +106,6 @@ public class SavingsProductsApiResource {
private final ConfigurationDomainService configurationDomainService;
private final JdbcTemplate jdbcTemplate;
private String generateNextProductCode(final String tableName, final String prefix) {
try {
String maxCode = jdbcTemplate.queryForObject(
"SELECT MAX(product_code) FROM " + tableName + " WHERE product_code LIKE ?",
String.class, prefix + "%");
if (maxCode == null || maxCode.isBlank()) {
return prefix + "001";
}
String numStr = maxCode.substring(prefix.length());
int num = Integer.parseInt(numStr);
return prefix + String.format("%03d", num + 1);
} catch (Exception e) {
return prefix + "001";
}
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@@ -315,7 +300,7 @@ public class SavingsProductsApiResource {
periodFrequencyOptions, paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, chargeOptions,
penaltyOptions, taxGroupOptions, yesNoOptions, accountTypeOptions, customerSegmentOptions,
accountMappingForPayment);
savingsProductToReturn.setProductCode(generateNextProductCode("m_savings_product", "SB-IND-"));
// product code generation removed
}
return savingsProductToReturn;
@@ -285,6 +285,7 @@ final class SavingsProductsApiResourceSwagger {
public GetSavingsProductsGlAccount writeOffAccount;
public GetSavingsProductsGlAccount savingsControlAccount;
public GetSavingsProductsGlAccount transfersInSuspenseAccount;
public GetSavingsProductsGlAccount escheatLiabilityAccount;
public GetSavingsProductsGlAccount interestPayableAccount;
}
@@ -47,6 +47,7 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureI
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferInterestToSavingsParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferToSavingsIdParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountNoParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.customerSegmentParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.amountParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargeIdParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargesParamName;
@@ -260,6 +261,9 @@ public class DepositAccountDataValidator {
baseDataValidator.reset().parameter(externalIdParamName).value(externalId).notExceedingLengthOf(100);
}
final String customerSegment = this.fromApiJsonHelper.extractStringNamed(customerSegmentParamName, element);
baseDataValidator.reset().parameter(customerSegmentParamName).value(customerSegment).notBlank();
if (this.fromApiJsonHelper.parameterExists(nominalAnnualInterestRateParamName, element)) {
final BigDecimal interestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(nominalAnnualInterestRateParamName,
element);
@@ -567,7 +571,7 @@ public class DepositAccountDataValidator {
baseDataValidator.reset().parameter(depositAmountParamName).value(depositAmount).notNull().positiveAmount();
}
if (depositType.isFixedDeposit() || fromApiJsonHelper.parameterExists(depositPeriodParamName, element)) {
if (depositType.isFixedDeposit() || depositType.isRecurringDeposit() || fromApiJsonHelper.parameterExists(depositPeriodParamName, element)) {
final Integer depositPeriod = fromApiJsonHelper.extractIntegerSansLocaleNamed(depositPeriodParamName, element);
baseDataValidator.reset().parameter(depositPeriodParamName).value(depositPeriod).notNull().integerGreaterThanZero();
}
@@ -642,8 +646,8 @@ public class DepositAccountDataValidator {
final BigDecimal mandatoryRecommendedDepositAmount = fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(mandatoryRecommendedDepositAmountParamName, element);
baseDataValidator.reset().parameter(mandatoryRecommendedDepositAmountParamName).value(mandatoryRecommendedDepositAmount).notNull()
.positiveAmount();
baseDataValidator.reset().parameter(mandatoryRecommendedDepositAmountParamName).value(mandatoryRecommendedDepositAmount)
.notNull().positiveAmount();
if (this.fromApiJsonHelper.parameterExists(isMandatoryDepositParamName, element)) {
final Boolean isMandatoryDeposit = this.fromApiJsonHelper.extractBooleanNamed(isMandatoryDepositParamName, element);
@@ -632,8 +632,12 @@ public class DepositProductDataValidator {
}
private void validateDepositAmountForCreate(JsonElement element, DataValidatorBuilder baseDataValidator) {
final BigDecimal depositAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(depositAmountParamName, element);
baseDataValidator.reset().parameter(depositAmountParamName).value(depositAmount).notNull().positiveAmount();
final BigDecimal depositAmount = this.fromApiJsonHelper.parameterExists(depositAmountParamName, element)
? this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(depositAmountParamName, element)
: null;
if (depositAmount != null) {
baseDataValidator.reset().parameter(depositAmountParamName).value(depositAmount).positiveAmount();
}
BigDecimal depositMinAmount = null;
if (this.fromApiJsonHelper.parameterExists(depositMinAmountParamName, element)) {
@@ -667,7 +671,7 @@ public class DepositProductDataValidator {
if (this.fromApiJsonHelper.parameterExists(depositAmountParamName, element)) {
depositAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(depositAmountParamName, element);
baseDataValidator.reset().parameter(depositAmountParamName).value(depositAmount).notNull().positiveAmount();
baseDataValidator.reset().parameter(depositAmountParamName).value(depositAmount).positiveAmount();
}
BigDecimal depositMinAmount = null;
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.data;
import java.time.LocalDate;
import java.util.List;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class FdInterestRateEffectiveData {
private Long id;
private String fdType;
private String versionLabel;
private LocalDate effectiveFromDate;
private LocalDate effectiveToDate;
private Boolean isActive;
private String remarks;
private List<FdInterestRateSlabData> slabs;
}
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.data;
import java.math.BigDecimal;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class FdInterestRateSlabData {
private Long id;
private Integer srNo;
private String description;
private Integer periodTypeEnum;
private String periodTypeLabel;
private Integer fromPeriod;
private Integer toPeriod;
private BigDecimal annualInterestRate;
private BigDecimal seniorCitizenRate;
}
@@ -120,6 +120,7 @@ public class DepositAccountAssembler {
private final PaymentDetailAssembler paymentDetailAssembler;
private final ExternalIdFactory externalIdFactory;
private final SavingsAccountAssembler savingsAccountAssembler;
@Autowired
public DepositAccountAssembler(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper,
@@ -130,7 +131,8 @@ public class DepositAccountAssembler {
final DepositProductAssembler depositProductAssembler,
final RecurringDepositProductRepository recurringDepositProductRepository,
final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final PlatformSecurityContext context,
final PaymentDetailAssembler paymentDetailAssembler, ExternalIdFactory externalIdFactory) {
final PaymentDetailAssembler paymentDetailAssembler, ExternalIdFactory externalIdFactory,
final SavingsAccountAssembler savingsAccountAssembler) {
this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper;
this.clientRepository = clientRepository;
@@ -146,6 +148,7 @@ public class DepositAccountAssembler {
this.context = context;
this.paymentDetailAssembler = paymentDetailAssembler;
this.externalIdFactory = externalIdFactory;
this.savingsAccountAssembler = savingsAccountAssembler;
}
/**
@@ -358,6 +361,9 @@ public class DepositAccountAssembler {
if (account != null) {
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper);
account.validateNewApplicationState(depositAccountType.resourceName());
// Apply CBS-specific fields (accountStatus, customerSegment, autoDebitFacility,
// linkedAccount, nominee, jointHolders, guardian, etc.)
this.savingsAccountAssembler.applyCbsFields(command, element, account);
}
return account;
@@ -0,0 +1,75 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "m_fd_base_interest_rate_effective")
@Getter
@NoArgsConstructor
public class FdBaseInterestRateEffective {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "fd_type", length = 20)
private String fdType;
@Column(name = "version_label")
private String versionLabel;
@Column(name = "effective_from_date")
private LocalDate effectiveFromDate;
@Column(name = "effective_to_date")
private LocalDate effectiveToDate;
@Column(name = "is_active")
private Boolean isActive;
@Column(name = "remarks")
private String remarks;
@Column(name = "created_by_id")
private Long createdById;
@Column(name = "created_date")
private LocalDate createdDate;
public static FdBaseInterestRateEffective create(String fdType, String versionLabel, LocalDate effectiveFromDate, String remarks,
Long createdById) {
FdBaseInterestRateEffective e = new FdBaseInterestRateEffective();
e.fdType = fdType;
e.versionLabel = versionLabel;
e.effectiveFromDate = effectiveFromDate;
e.isActive = true;
e.remarks = remarks;
e.createdById = createdById;
e.createdDate = LocalDate.now();
return e;
}
public void deactivate(LocalDate toDate) {
this.isActive = false;
this.effectiveToDate = toDate;
}
}
@@ -0,0 +1,26 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface FdBaseInterestRateEffectiveRepository extends JpaRepository<FdBaseInterestRateEffective, Long> {
List<FdBaseInterestRateEffective> findByFdTypeAndIsActiveTrueOrderByEffectiveFromDateDesc(String fdType);
List<FdBaseInterestRateEffective> findByFdTypeOrderByEffectiveFromDateDesc(String fdType);
List<FdBaseInterestRateEffective> findByIsActiveTrueOrderByEffectiveFromDateDesc();
List<FdBaseInterestRateEffective> findByOrderByEffectiveFromDateDesc();
}
@@ -0,0 +1,76 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "m_fd_base_interest_rate_slab_history")
@Getter
@NoArgsConstructor
public class FdBaseInterestRateSlabHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "effective_id")
private Long effectiveId;
@Column(name = "sr_no")
private Integer srNo;
@Column(name = "description")
private String description;
@Column(name = "period_type_enum")
private Integer periodTypeEnum;
@Column(name = "from_period")
private Integer fromPeriod;
@Column(name = "to_period")
private Integer toPeriod;
@Column(name = "annual_interest_rate")
private BigDecimal annualInterestRate;
@Column(name = "senior_citizen_rate")
private BigDecimal seniorCitizenRate;
@Column(name = "created_date")
private LocalDate createdDate;
public static FdBaseInterestRateSlabHistory create(Long effectiveId, Integer srNo, String description, Integer periodTypeEnum,
Integer fromPeriod, Integer toPeriod, BigDecimal annualInterestRate, BigDecimal seniorCitizenRate) {
FdBaseInterestRateSlabHistory s = new FdBaseInterestRateSlabHistory();
s.effectiveId = effectiveId;
s.srNo = srNo;
s.description = description;
s.periodTypeEnum = periodTypeEnum != null ? periodTypeEnum : 1;
s.fromPeriod = fromPeriod;
s.toPeriod = toPeriod;
s.annualInterestRate = annualInterestRate;
s.seniorCitizenRate = seniorCitizenRate;
s.createdDate = LocalDate.now();
return s;
}
}
@@ -0,0 +1,20 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface FdBaseInterestRateSlabHistoryRepository extends JpaRepository<FdBaseInterestRateSlabHistory, Long> {
List<FdBaseInterestRateSlabHistory> findByEffectiveIdOrderBySrNo(Long effectiveId);
}
@@ -230,12 +230,14 @@ public class RecurringDepositAccount extends SavingsAccount {
}
final BigDecimal depositAmount = accountTermAndPreClosure.depositAmount();
BigDecimal applicableInterestRate = this.chart.getApplicableInterestRate(depositAmount, depositStartDate(), depositCloseDate,
this.client);
BigDecimal applicableInterestRate = BigDecimal.ZERO;
if (this.chart != null) {
applicableInterestRate = this.chart.getApplicableInterestRate(depositAmount, depositStartDate(), depositCloseDate, this.client);
if (applyPreMaturePenalty) {
applicableInterestRate = applicableInterestRate.subtract(penalInterest);
applicableInterestRate = applicableInterestRate.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : applicableInterestRate;
if (applyPreMaturePenalty) {
applicableInterestRate = applicableInterestRate.subtract(penalInterest);
applicableInterestRate = applicableInterestRate.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : applicableInterestRate;
}
}
this.nominalAnnualInterestRate = applicableInterestRate;
@@ -1070,10 +1072,8 @@ public class RecurringDepositAccount extends SavingsAccount {
.failWithCodeNoParameterAddedToErrorCode("no.applicable.interest.rate.is.found.based.on.amount.and.deposit.period");
}
} else if (this.nominalAnnualInterestRate == null || this.nominalAnnualInterestRate.compareTo(BigDecimal.ZERO) == 0) {
baseDataValidator.reset().parameter(DepositsApiConstants.nominalAnnualInterestRateParamName).value(nominalAnnualInterestRate)
.failWithCodeNoParameterAddedToErrorCode("interest.chart.or.nominal.interest.rate.required");
}
// CBS: chart is optional — CBS base interest rate chart is used instead
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
@@ -40,6 +40,8 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.minorDat
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nomineeParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.specialCategoryParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.weakerSectionParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.nominationFacilityParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.autoDebitFacilityParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.allowOverdraftParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.clientIdParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.enforceMinRequiredBalanceParamName;
@@ -367,7 +369,7 @@ public class SavingsAccountAssembler {
private static final Gson GSON = new Gson();
private void applyCbsFields(final JsonCommand command, final JsonElement element, final SavingsAccount account) {
public void applyCbsFields(final JsonCommand command, final JsonElement element, final SavingsAccount account) {
// Product classification
final String cbsAccountType = this.fromApiJsonHelper.extractStringNamed(accountTypeParamName, element);
if (cbsAccountType != null) account.setCbsAccountType(cbsAccountType);
@@ -384,6 +386,14 @@ public class SavingsAccountAssembler {
account.setSpecialCategory(this.fromApiJsonHelper.extractStringNamed(specialCategoryParamName, element));
account.setWeakerSection(this.fromApiJsonHelper.extractStringNamed(weakerSectionParamName, element));
if (command.parameterExists(nominationFacilityParamName)) {
account.setNominationFacility(this.fromApiJsonHelper.extractBooleanNamed(nominationFacilityParamName, element));
}
if (command.parameterExists(autoDebitFacilityParamName)) {
account.setAutoDebitFacility(this.fromApiJsonHelper.extractBooleanNamed(autoDebitFacilityParamName, element));
}
account.setGuardianName(this.fromApiJsonHelper.extractStringNamed(guardianNameParamName, element));
account.setGuardianCifNumber(this.fromApiJsonHelper.extractStringNamed(guardianCifNumberParamName, element));
account.setGuardianRelationship(this.fromApiJsonHelper.extractStringNamed(guardianRelationshipParamName, element));
@@ -458,8 +468,9 @@ public class SavingsAccountAssembler {
role = isPrimary ? "Primary Account Holder" : "Co-Holder";
}
final boolean isMandatory = h.has("isMandatory") && h.get("isMandatory").getAsBoolean();
holders.add(new SavingsAccountJointHolder(account, clientId, cifNumber, displayName,
sharePercentage, signingAuthority, role, isPrimary));
sharePercentage, signingAuthority, role, isPrimary, isMandatory));
}
account.getJointHolders().clear();
account.getJointHolders().addAll(holders);
@@ -63,7 +63,154 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead
if (rows.isEmpty()) {
throw new CurrentAccountNotFoundException(accountId);
}
return buildStructuredResponse(rows.get(0));
final Map<String, Object> response = buildStructuredResponse(rows.get(0));
response.put("charges", retrieveCharges(accountId));
response.put("transactions", retrieveTransactions(accountId));
return response;
}
private List<Map<String, Object>> retrieveTransactions(Long accountId) {
final String sql =
"SELECT t.id, t.transaction_type_enum, t.transaction_date, t.amount, "
+ "t.running_balance_derived, t.is_reversed, t.is_reversal, t.ref_no, t.remarks, "
+ "t.submitted_on_date "
+ "FROM m_current_account_transaction t "
+ "WHERE t.current_account_id = ? "
+ "ORDER BY t.transaction_date ASC, t.id ASC";
return jdbcTemplate.query(sql, (rs, rowNum) -> {
final Map<String, Object> t = new LinkedHashMap<>();
t.put("id", rs.getLong("id"));
final int txnTypeEnum = rs.getInt("transaction_type_enum");
t.put("transactionType", buildTransactionType(txnTypeEnum));
final java.time.LocalDate txnDate = rs.getObject("transaction_date", java.time.LocalDate.class);
if (txnDate != null) {
t.put("date", new int[]{txnDate.getYear(), txnDate.getMonthValue(), txnDate.getDayOfMonth()});
} else {
t.put("date", null);
}
t.put("amount", rs.getBigDecimal("amount"));
t.put("runningBalance", rs.getBigDecimal("running_balance_derived"));
t.put("reversed", rs.getBoolean("is_reversed"));
t.put("isReversal", rs.getBoolean("is_reversal"));
t.put("refNo", rs.getString("ref_no"));
t.put("remarks", rs.getString("remarks"));
final java.time.LocalDate submittedOn = rs.getObject("submitted_on_date", java.time.LocalDate.class);
if (submittedOn != null) {
t.put("submittedOnDate", new int[]{submittedOn.getYear(), submittedOn.getMonthValue(), submittedOn.getDayOfMonth()});
} else {
t.put("submittedOnDate", null);
}
return t;
}, accountId);
}
private Map<String, Object> buildTransactionType(int typeEnum) {
final Map<String, Object> tt = new LinkedHashMap<>();
tt.put("id", typeEnum);
tt.put("deposit", typeEnum == 1);
tt.put("withdrawal", typeEnum == 2);
tt.put("interestPosting", typeEnum == 3);
tt.put("feeDeduction", typeEnum == 4 || typeEnum == 7);
tt.put("overdraftInterest", false);
tt.put("withholdTax", false);
tt.put("initiateTransfer", false);
tt.put("approveTransfer", false);
tt.put("withdrawTransfer", false);
tt.put("rejectTransfer", false);
tt.put("writtenoff", false);
tt.put("overdraftFee", false);
tt.put("escheat", false);
tt.put("amountHold", false);
tt.put("amountRelease", false);
tt.put("accrual", false);
switch (typeEnum) {
case 1 -> { tt.put("code", "savingsTransactionType.deposit"); tt.put("value", "Deposit"); }
case 2 -> { tt.put("code", "savingsTransactionType.withdrawal"); tt.put("value", "Withdrawal"); }
case 3 -> { tt.put("code", "savingsTransactionType.interestPosting"); tt.put("value", "Interest Posting"); }
case 4 -> { tt.put("code", "savingsTransactionType.feeDeduction"); tt.put("value", "Fee Deduction"); }
case 7 -> { tt.put("code", "savingsTransactionType.payCharge"); tt.put("value", "Charge Deduction"); }
default -> { tt.put("code", "savingsTransactionType.deposit"); tt.put("value", "Deposit"); }
}
return tt;
}
private List<Map<String, Object>> retrieveCharges(Long accountId) {
final String sql =
"SELECT cac.id, cac.charge_id, ch.name AS charge_name, "
+ "cac.charge_time_enum, cac.charge_due_date, cac.fee_on_month, cac.fee_on_day, "
+ "cac.fee_interval, cac.charge_calculation_enum, "
+ "cac.amount, cac.amount_paid_derived, cac.amount_waived_derived, "
+ "cac.amount_writtenoff_derived, cac.amount_outstanding_derived, "
+ "cac.is_paid_derived, cac.waived, cac.is_penalty, cac.is_active "
+ "FROM m_current_account_charge cac "
+ "JOIN m_charge ch ON ch.id = cac.charge_id "
+ "WHERE cac.current_account_id = ? AND cac.is_active = true "
+ "ORDER BY cac.id";
return jdbcTemplate.query(sql, (rs, rowNum) -> {
final Map<String, Object> c = new LinkedHashMap<>();
c.put("id", rs.getLong("id"));
c.put("chargeId", rs.getLong("charge_id"));
c.put("name", rs.getString("charge_name"));
final int chargeTimeEnum = rs.getInt("charge_time_enum");
c.put("chargeTimeType", buildEnumOption(chargeTimeEnum, chargeTimeValue(chargeTimeEnum)));
c.put("chargeDueDate", rs.getDate("charge_due_date") != null ? rs.getDate("charge_due_date").toLocalDate().toString() : null);
c.put("feeOnMonthDay", buildFeeOnMonthDay(rs.getObject("fee_on_month"), rs.getObject("fee_on_day")));
c.put("feeInterval", rs.getObject("fee_interval"));
final int chargeCalcEnum = rs.getInt("charge_calculation_enum");
c.put("chargeCalculationType", buildEnumOption(chargeCalcEnum, chargeCalcValue(chargeCalcEnum)));
c.put("amount", rs.getBigDecimal("amount"));
c.put("amountPaid", rs.getBigDecimal("amount_paid_derived"));
c.put("amountWaived", rs.getBigDecimal("amount_waived_derived"));
c.put("amountWrittenOff", rs.getBigDecimal("amount_writtenoff_derived"));
c.put("amountOutstanding", rs.getBigDecimal("amount_outstanding_derived"));
c.put("paid", rs.getBoolean("is_paid_derived"));
c.put("waived", rs.getBoolean("waived"));
c.put("penalty", rs.getBoolean("is_penalty"));
c.put("isActive", rs.getBoolean("is_active"));
return c;
}, accountId);
}
private Map<String, Object> buildFeeOnMonthDay(Object month, Object day) {
if (month == null || day == null) return null;
final Map<String, Object> m = new LinkedHashMap<>();
m.put("month", month);
m.put("day", day);
return m;
}
private String chargeTimeValue(int id) {
return switch (id) {
case 1 -> "Disbursement";
case 2 -> "Specified due date";
case 3 -> "Savings Activation";
case 4 -> "Withdrawal Fee";
case 5 -> "Annual Fee";
case 6 -> "Annual Fee";
case 7 -> "Monthly Fee";
case 8 -> "Overdue Fee";
default -> "Specified due date";
};
}
private String chargeCalcValue(int id) {
return switch (id) {
case 1 -> "Flat";
case 2 -> "% Amount";
case 3 -> "% Loan Amount + Interest";
case 4 -> "% Interest";
default -> "Flat";
};
}
@Override
@@ -71,7 +218,10 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead
List<Map<String, Object>> rows = jdbcTemplate.query(BASE_SQL, new FlatRowMapper());
List<Map<String, Object>> result = new ArrayList<>();
for (Map<String, Object> row : rows) {
result.add(buildStructuredResponse(row));
final Map<String, Object> response = buildStructuredResponse(row);
final Long id = ((Number) row.get("id")).longValue();
response.put("charges", retrieveCharges(id));
result.add(response);
}
return result;
}
@@ -88,10 +238,12 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead
out.put("groupId", row.get("group_id"));
out.put("productId", row.get("product_id"));
out.put("productName", row.get("product_name"));
out.put("currentAccountProductName", row.get("product_name"));
out.put("shortProductName", row.get("product_short_name"));
out.put("officeId", row.get("office_id"));
out.put("officeName", row.get("office_name"));
out.put("staffId", row.get("staff_id"));
out.put("fieldOfficerId", row.get("staff_id"));
out.put("fieldOfficerName", row.get("staff_name"));
// status
@@ -234,6 +386,12 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead
};
s.put("code", "SavingsAccountSubStatusEnum." + value.replace(" ", ""));
s.put("value", value);
s.put("none", subStatusEnum == 0);
s.put("inactive", subStatusEnum == 100);
s.put("dormant", subStatusEnum == 200);
s.put("block", subStatusEnum == 300);
s.put("blockDebit", subStatusEnum == 400);
s.put("blockCredit", subStatusEnum == 500);
return s;
}
@@ -29,11 +29,13 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.service.ClientNomineeWritePlatformService;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountJointHolder;
@@ -43,6 +45,14 @@ import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountWriteP
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountNumberGenerator;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository;
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings;
import org.apache.fineract.accounting.common.AccountingRuleType;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
import org.apache.fineract.portfolio.PortfolioProductType;
import org.apache.fineract.portfolio.savings.exception.SavingsAccountNotFoundException;
import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException;
import org.springframework.stereotype.Service;
@@ -59,6 +69,11 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
private final ClientRepositoryWrapper clientRepository;
private final OfficeRepositoryWrapper officeRepository;
private final CurrentAccountNumberGenerator accountNumberGenerator;
private final ClientNomineeWritePlatformService clientNomineeWritePlatformService;
private final org.apache.fineract.portfolio.savings.domain.FacilityChargeMappingRepository facilityChargeMappingRepository;
private final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository currentAccountTransactionRepository;
private final JournalEntryRepository journalEntryRepository;
private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository;
@Override
@Transactional
@@ -74,6 +89,13 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
final CurrentProduct product = currentProductRepository.findById(productId)
.orElseThrow(() -> new SavingsProductNotFoundException(productId));
if (currentAccountRepository.countActiveByClientAndProduct(clientId, productId) > 0) {
throw new PlatformDataIntegrityException(
"error.msg.savings.account.duplicate.product.for.client",
"Client already has an active Current Account with the same product.",
"productId", productId);
}
final Office office = officeId != null
? officeRepository.findOneWithNotFoundDetection(officeId)
: client.getOffice();
@@ -112,6 +134,26 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
product.getInterestCalculateSelector(), product.getAveragePeriod(),
product.getAvgBalanceCalcPeriod());
// Copy product-level fields that are not in createNew()
account.setMinRequiredOpeningBalance(product.getMinRequiredOpeningBalance());
account.setMinRequiredBalance(product.getMinRequiredBalance());
account.setEnforceMinRequiredBalance(product.isEnforceMinRequiredBalance());
account.setLockinPeriodFrequency(product.getLockinPeriodFrequency());
account.setLockinPeriodFrequencyEnum(product.getLockinPeriodFrequencyType());
account.setNominalAnnualInterestRateOverdraft(product.getNominalAnnualInterestRateOverdraft());
account.setWithdrawalFeeForTransfer(product.isWithdrawalFeeApplicableForTransfer());
account.setMinOverdraftForInterestCalculation(product.getMinOverdraftForInterestCalculation());
account.setFrequencyFixDay(product.getFrequencyFixDay());
account.setEnableLockinPeriod(product.isEnableLockinPeriod());
account.setBalanceFrequency(product.getBalanceFrequency());
account.setAccountClosedZeroBalanceInDayClosing(product.getAccountClosedZeroBalanceInDayClosing());
account.setValidForPeriod(product.getValidForPeriod());
account.setOdAccountClosedZeroBalanceInDayClosing(product.isOdAccountClosedZeroBalanceInDayClosing());
account.setLienAllowed(product.isLienAllowed());
account.setMaxAllowedLienLimit(product.getMaxAllowedLienLimit());
account.setMinBalanceForInterestCalculation(product.getMinBalanceForInterestCalculation());
account.setWithholdTax(product.withHoldTax());
if (json.has("establishmentDate") && !json.get("establishmentDate").isJsonNull()) {
account.setEstablishmentDate(parseDate(json.get("establishmentDate").getAsString(), dateFormat, locale));
}
@@ -130,13 +172,17 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
if (json.has("introducerName") && !json.get("introducerName").isJsonNull()) {
account.setIntroducerName(json.get("introducerName").getAsString());
}
if (json.has("introducerCifNumber") && !json.get("introducerCifNumber").isJsonNull()) {
account.setIntroducerCifNumber(json.get("introducerCifNumber").getAsString());
}
// UI may send "introducerClientId" or "introducerId"
Long introducerClientId = getLong(json, "introducerClientId");
if (introducerClientId == null) introducerClientId = getLong(json, "introducerId");
if (introducerClientId != null) account.setIntroducerClientId(introducerClientId);
if (json.has("externalId") && !json.get("externalId").isJsonNull()) {
account.setExternalId(json.get("externalId").getAsString());
final String extId = json.get("externalId").getAsString().trim();
account.setExternalId(extId.isEmpty() ? null : extId);
}
if (json.has("accountStatus") && !json.get("accountStatus").isJsonNull()) {
account.setAccountStatus(json.get("accountStatus").getAsString());
@@ -147,12 +193,24 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
}
// Save first to get ID
currentAccountRepository.saveAndFlush(account);
try {
currentAccountRepository.saveAndFlush(account);
} catch (RuntimeException e) {
throwIfDuplicateExternalId(e, account.getExternalId());
throw e;
}
// Generate account number: 00 + GL_CODE(6) + OFFICE_ID(2) + ACCOUNT_ID(5)
final String accountNo = accountNumberGenerator.generate(account);
account.updateAccountNo(accountNo);
// Save nominees if provided in payload (same transaction)
if (json.has("nominees") && !json.get("nominees").isJsonNull()) {
clientNomineeWritePlatformService.saveNomineesIfPresent(clientId, accountNo, "CB", json.get("nominees"));
} else if (json.has("nominee") && !json.get("nominee").isJsonNull()) {
clientNomineeWritePlatformService.saveNomineeIfPresent(clientId, accountNo, "CB", json.get("nominee"));
}
// Facility config — UI sends "accountFacilityConfig", internal uses "facilityConfig"
JsonObject fcJson = json.getAsJsonObject("accountFacilityConfig");
if (fcJson == null) fcJson = json.getAsJsonObject("facilityConfig");
@@ -170,7 +228,12 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
assemblePartners(account, json.getAsJsonArray("partners"));
}
currentAccountRepository.saveAndFlush(account);
try {
currentAccountRepository.saveAndFlush(account);
} catch (RuntimeException e) {
throwIfDuplicateExternalId(e, account.getExternalId());
throw e;
}
return new CommandProcessingResultBuilder()
.withEntityId(account.getId())
@@ -211,8 +274,73 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
final JsonObject json = command.parsedJson().getAsJsonObject();
LocalDate activatedOnDate = parseDate(getString(json, "activatedOnDate"), getString(json, "dateFormat"), getString(json, "locale"));
if (activatedOnDate == null) activatedOnDate = LocalDate.now();
// Validate minimum opening balance before activation
java.math.BigDecimal openingBalanceValue = java.math.BigDecimal.ZERO;
if (json.has("openingBalance") && !json.get("openingBalance").isJsonNull()) {
try { openingBalanceValue = json.get("openingBalance").getAsBigDecimal(); } catch (Exception ignored) {}
}
final java.math.BigDecimal minRequired = account.getMinRequiredOpeningBalance();
if (minRequired != null && minRequired.compareTo(java.math.BigDecimal.ZERO) > 0
&& openingBalanceValue.compareTo(minRequired) < 0) {
throw new org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException(
java.util.List.of(org.apache.fineract.infrastructure.core.data.ApiParameterError.parameterError(
"openingBalance",
"Opening balance (" + openingBalanceValue + ") is less than minimum required opening balance (" + minRequired + ")",
"validation.msg.currentaccount.openingBalance.less.than.min.required",
openingBalanceValue, minRequired)));
}
account.activate(activatedOnDate);
applyFacilityChargesOnActivation(account);
java.math.BigDecimal currentBalance = java.math.BigDecimal.ZERO;
final java.util.List<org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction> depositTxns = new java.util.ArrayList<>();
final java.util.List<ChargeGlEntry> chargeEntries = new java.util.ArrayList<>();
// Opening balance deposit transaction
if (openingBalanceValue.compareTo(java.math.BigDecimal.ZERO) > 0) {
try {
final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction deposit =
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction
.deposit(account, account.getOffice(), openingBalanceValue, activatedOnDate, "OPENING-BALANCE");
deposit.setRunningBalanceDerived(openingBalanceValue);
account.setTotalDepositsDerived(openingBalanceValue);
currentAccountTransactionRepository.saveAndFlush(deposit);
currentBalance = openingBalanceValue;
depositTxns.add(deposit);
log.info("Opening balance deposit {} created for account id={}", openingBalanceValue, accountId);
} catch (Exception e) {
log.warn("Could not process openingBalance for account id={}: {}", accountId, e.getMessage());
}
}
// Auto-deduct activation charges (chargeTimeEnum=3) from opening balance
for (final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge charge : account.getCharges()) {
if (charge.getChargeTimeEnum() != null && charge.getChargeTimeEnum() == 3
&& charge.getAmount() != null
&& charge.getAmount().compareTo(java.math.BigDecimal.ZERO) > 0
&& charge.getCharge() != null) {
charge.setAmountPaidDerived(charge.getAmount());
charge.setAmountOutstandingDerived(java.math.BigDecimal.ZERO);
charge.setPaid(true);
currentBalance = currentBalance.subtract(charge.getAmount());
final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction chargeTxn =
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction
.chargeDeduction(account, account.getOffice(), charge.getAmount(), activatedOnDate);
chargeTxn.setRunningBalanceDerived(currentBalance);
currentAccountTransactionRepository.saveAndFlush(chargeTxn);
chargeEntries.add(new ChargeGlEntry(chargeTxn, charge));
log.info("Auto-deducted activation charge '{}' {} from account id={}, running balance={}",
charge.getCharge().getName(), charge.getAmount(), accountId, currentBalance);
}
}
account.setAccountBalanceDerived(currentBalance);
currentAccountRepository.saveAndFlush(account);
// Post GL journal entries — deposits in batch, each charge individually
postGlEntries(account, depositTxns, chargeEntries);
return new CommandProcessingResultBuilder().withEntityId(accountId).build();
}
@@ -324,6 +452,11 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
if (jo.has("displayName") && !jo.get("displayName").isJsonNull()) holder.setDisplayName(jo.get("displayName").getAsString());
if (jo.has("signingAuthority") && !jo.get("signingAuthority").isJsonNull()) holder.setSigningAuthority(jo.get("signingAuthority").getAsString());
if (jo.has("role") && !jo.get("role").isJsonNull()) holder.setRole(jo.get("role").getAsString());
if (jo.has("sharePercentage") && !jo.get("sharePercentage").isJsonNull()) {
try { holder.setSharePercentage(jo.get("sharePercentage").getAsBigDecimal()); } catch (Exception ignored) {}
}
if (jo.has("isPrimary") && !jo.get("isPrimary").isJsonNull()) holder.setPrimary(jo.get("isPrimary").getAsBoolean());
if (jo.has("isMandatory") && !jo.get("isMandatory").isJsonNull()) holder.setMandatory(jo.get("isMandatory").getAsBoolean());
account.getJointHolders().add(holder);
}
}
@@ -337,6 +470,10 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
if (cid != null) partner.setClientId(cid);
if (jo.has("clientCif") && !jo.get("clientCif").isJsonNull()) partner.setClientCif(jo.get("clientCif").getAsString());
if (jo.has("clientName") && !jo.get("clientName").isJsonNull()) partner.setClientName(jo.get("clientName").getAsString());
if (jo.has("sharePercentage") && !jo.get("sharePercentage").isJsonNull()) {
try { partner.setSharePercentage(jo.get("sharePercentage").getAsBigDecimal()); } catch (Exception ignored) {}
}
if (jo.has("isMandatory") && !jo.get("isMandatory").isJsonNull()) partner.setMandatory(jo.get("isMandatory").getAsBoolean());
account.getPartners().add(partner);
}
}
@@ -377,4 +514,189 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
}
}
}
private void applyFacilityChargesOnActivation(final CurrentAccount account) {
// Use account-level facility config — reflects what was selected at account opening time
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig config =
account.getFacilityConfig();
log.info("[FacilityCharge] Activating account id={}, facilityConfig={}",
account.getId(), config != null ? "FOUND" : "NULL");
if (config == null) {
// Fallback: use product-level facility config
log.info("[FacilityCharge] No account-level config, falling back to product id={}", account.getProductId());
config = currentProductRepository.findById(account.getProductId())
.map(p -> {
final var pc = p.getFacilityConfig();
if (pc == null) {
log.info("[FacilityCharge] Product has no facility config either");
return null;
}
// Map product config to account config fields inline
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig ac =
new org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig();
ac.setAtmFacility(Boolean.TRUE.equals(pc.getAtmFacility()));
ac.setAtmWithdrawalFacility(Boolean.TRUE.equals(pc.getAtmWithdrawalFacility()));
ac.setUpiFacility(Boolean.TRUE.equals(pc.getUpiFacility()));
ac.setPosTransactionsFacility(Boolean.TRUE.equals(pc.getPosTransactionsFacility()));
ac.setChequeBook(Boolean.TRUE.equals(pc.getChequeBook()));
ac.setImpsFacility(Boolean.TRUE.equals(pc.getImpsFacility()));
ac.setSmsAlerts(Boolean.TRUE.equals(pc.getSmsAlerts()));
ac.setBbpsFacility(Boolean.TRUE.equals(pc.getBbpsFacility()));
ac.setInsuranceCover(Boolean.TRUE.equals(pc.getInsuranceCover()));
ac.setBepgFacility(Boolean.TRUE.equals(pc.getBepgFacility()));
ac.setNeftFacility(Boolean.TRUE.equals(pc.getNeftFacility()));
ac.setRtgsFacility(Boolean.TRUE.equals(pc.getRtgsFacility()));
ac.setInterBankTotalCapFacility(Boolean.TRUE.equals(pc.getInterBankTotalCapFacility()));
ac.setWithdrawalFacility(Boolean.TRUE.equals(pc.getWithdrawalFacility()));
ac.setDepositFacilityKyc(Boolean.TRUE.equals(pc.getDepositFacilityKyc()));
ac.setVirtualDebitCard(Boolean.TRUE.equals(pc.getVirtualDebitCard()));
return ac;
}).orElse(null);
}
if (config == null) {
log.warn("[FacilityCharge] No facility config found for account id={} — no charges applied", account.getId());
return;
}
final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig fc = config;
log.info("[FacilityCharge] Config found — ATM={} UPI={} NEFT={} RTGS={} CHEQUE={} SMS={} IMPs={} POS={} BBPS={} INS={} BEPG={} IBCAP={} WD={} KYC={} VCARD={} ATM_WD={}",
fc.isAtmFacility(), fc.isUpiFacility(), fc.isNeftFacility(), fc.isRtgsFacility(),
fc.isChequeBook(), fc.isSmsAlerts(), fc.isImpsFacility(), fc.isPosTransactionsFacility(),
fc.isBbpsFacility(), fc.isInsuranceCover(), fc.isBepgFacility(), fc.isInterBankTotalCapFacility(),
fc.isWithdrawalFacility(), fc.isDepositFacilityKyc(), fc.isVirtualDebitCard(), fc.isAtmWithdrawalFacility());
applyMapped(account, fc.isAtmFacility(), "ATM_FACILITY");
applyMapped(account, fc.isAtmWithdrawalFacility(), "ATM_WITHDRAWAL");
applyMapped(account, fc.isUpiFacility(), "UPI_FACILITY");
applyMapped(account, fc.isPosTransactionsFacility(), "POS_TRANSACTIONS");
applyMapped(account, fc.isChequeBook(), "CHEQUE_BOOK");
applyMapped(account, fc.isImpsFacility(), "IMPS_FACILITY");
applyMapped(account, fc.isSmsAlerts(), "SMS_ALERTS");
applyMapped(account, fc.isBbpsFacility(), "BBPS_FACILITY");
applyMapped(account, fc.isInsuranceCover(), "INSURANCE_COVER");
applyMapped(account, fc.isBepgFacility(), "BEPG_FACILITY");
applyMapped(account, fc.isNeftFacility(), "NEFT_FACILITY");
applyMapped(account, fc.isRtgsFacility(), "RTGS_FACILITY");
applyMapped(account, fc.isInterBankTotalCapFacility(),"INTER_BANK_TOTAL_CAP");
applyMapped(account, fc.isWithdrawalFacility(), "WITHDRAWAL_FACILITY");
applyMapped(account, fc.isDepositFacilityKyc(), "DEPOSIT_FACILITY_KYC");
applyMapped(account, fc.isVirtualDebitCard(), "VIRTUAL_DEBIT_CARD");
}
// Wrapper to carry a charge transaction together with its charge entity for GL posting
private record ChargeGlEntry(
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction txn,
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge charge) {}
// entity type for savings/current accounts in acc_gl_journal_entry
private static final int ENTITY_TYPE_SAVINGS = 2;
private void postGlEntries(final CurrentAccount account,
final java.util.List<org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction> depositTxns,
final java.util.List<ChargeGlEntry> chargeEntries) {
try {
final CurrentProduct product = currentProductRepository.findById(account.getProductId()).orElse(null);
if (product == null) return;
final Integer accountingRule = product.getAccountingType();
if (accountingRule == null || accountingRule.equals(AccountingRuleType.NONE.getValue())) {
log.debug("[GL] Product {} accounting=NONE — skipping", account.getProductId());
return;
}
final GLAccount cashAccount = findGlAccount(account.getProductId(), CashAccountsForSavings.SAVINGS_REFERENCE.getValue());
final GLAccount controlAccount = findGlAccount(account.getProductId(), CashAccountsForSavings.SAVINGS_CONTROL.getValue());
final GLAccount feeIncomeAccount = findGlAccount(account.getProductId(), CashAccountsForSavings.INCOME_FROM_FEES.getValue());
if (cashAccount == null || controlAccount == null) {
log.warn("[GL] Missing GL account mappings for product id={} — skipping GL posting", account.getProductId());
return;
}
// Deposit uses CA{accountId} as transaction ref (one per activation)
final String depositRef = "CA" + account.getId();
// Deposits: DR SAVINGS_REFERENCE (cash in), CR SAVINGS_CONTROL (liability)
if (depositTxns != null) {
for (final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction txn : depositTxns) {
if (txn.getId() == null || txn.getAmount() == null) continue;
postEntry(account, cashAccount, JournalEntryType.DEBIT, txn.getAmount(), txn.getTransactionDate(), depositRef);
postEntry(account, controlAccount, JournalEntryType.CREDIT, txn.getAmount(), txn.getTransactionDate(), depositRef);
log.info("[GL] Deposit GL posted for account id={}, amount={}", account.getId(), txn.getAmount());
}
}
// Charges: each charge gets its own transaction ref = "CAC" + txn.getId()
// so each charge appears as a separate transaction in journal entries
if (chargeEntries != null && feeIncomeAccount != null) {
for (final ChargeGlEntry entry : chargeEntries) {
final org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction txn = entry.txn();
if (txn.getId() == null || txn.getAmount() == null) continue;
final String chargeRef = "CAC" + txn.getId();
postEntry(account, controlAccount, JournalEntryType.DEBIT, txn.getAmount(), txn.getTransactionDate(), chargeRef);
postEntry(account, feeIncomeAccount, JournalEntryType.CREDIT, txn.getAmount(), txn.getTransactionDate(), chargeRef);
log.info("[GL] Charge GL posted for account id={}, charge={}, amount={}, ref={}", account.getId(), entry.charge().getCharge().getName(), txn.getAmount(), chargeRef);
}
}
} catch (Exception e) {
log.error("[GL] Failed GL posting for current account id={}: {}", account.getId(), e.getMessage(), e);
}
}
private GLAccount findGlAccount(final Long productId, final int financialAccountType) {
final org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping mapping =
productToGLAccountMappingRepository.findByProductIdAndProductTypeAndFinancialAccountTypeAndPaymentTypeId(
productId, PortfolioProductType.SAVING.getValue(), financialAccountType, null);
return mapping != null ? mapping.getGlAccount() : null;
}
private void postEntry(final CurrentAccount account, final GLAccount glAccount,
final JournalEntryType entryType, final java.math.BigDecimal amount,
final LocalDate date, final String transactionRef) {
final JournalEntry entry = JournalEntry.createNew(
account.getOffice(), null, glAccount,
account.getCurrencyCode(), transactionRef,
false, date, entryType, amount,
"Current Account activation",
ENTITY_TYPE_SAVINGS, account.getId(),
null, null, null, null, null);
journalEntryRepository.save(entry);
}
private void throwIfDuplicateExternalId(final RuntimeException e, final String externalId) {
Throwable cause = e;
while (cause != null) {
final String msg = cause.getMessage();
if (msg != null && (msg.contains("ca_external_id_UNIQUE") || msg.contains("ca_unq_external_id"))) {
throw new PlatformDataIntegrityException(
"error.msg.savings.account.duplicate.externalid",
"Current Account with this External ID already exists.",
"externalId", externalId);
}
cause = cause.getCause();
}
}
private void applyMapped(final CurrentAccount account, final boolean enabled, final String facilityType) {
if (!enabled) {
log.debug("[FacilityCharge] {} disabled — skip", facilityType);
return;
}
final var mappingOpt = facilityChargeMappingRepository.findByFacilityType(facilityType);
if (mappingOpt.isEmpty()) {
log.warn("[FacilityCharge] {} enabled but NO mapping found in m_facility_charge_mapping", facilityType);
return;
}
final org.apache.fineract.portfolio.charge.domain.Charge charge = mappingOpt.get().getCharge();
if (charge == null) {
log.warn("[FacilityCharge] {} mapping exists but charge_id is NULL", facilityType);
return;
}
log.info("[FacilityCharge] Applying charge '{}' (id={}) for facility {}", charge.getName(), charge.getId(), facilityType);
account.getCharges().add(
org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountCharge
.createFromFacility(account, charge));
}
}
@@ -143,7 +143,7 @@ public class DepositAccountInterestRateChartReadPlatformServiceImpl implements D
return preparedStatement;
}, this.chartExtractor);
if (chartDatas == null || chartDatas.isEmpty()) {
throw new DepositAccountInterestRateChartNotFoundException(accountId);
return null;
}
return chartDatas.iterator().next();
@@ -275,10 +275,15 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
} else if (depositAccountType.isRecurringDeposit()) {
CalendarData calendar = this.calendarReadPlatformService.retrieveCollctionCalendarByEntity(accountId,
CalendarEntityType.SAVINGS.getValue());
final Integer frequency = calendar.getInterval() == -1 ? 1 : calendar.getInterval();
final CalendarFrequencyType calendarFrequencyType = CalendarFrequencyType.fromInt(calendar.getFrequency().getId().intValue());
final PeriodFrequencyType periodFrequencyType = CalendarFrequencyType.from(calendarFrequencyType);
final EnumOptionData frequencyType = CommonEnumerations.termFrequencyType(periodFrequencyType, "recurring.deposit.frequency.");
Integer frequency = 1;
EnumOptionData frequencyType = null;
if (calendar != null) {
frequency = calendar.getInterval() == -1 ? 1 : calendar.getInterval();
final CalendarFrequencyType calendarFrequencyType = CalendarFrequencyType
.fromInt(calendar.getFrequency().getId().intValue());
final PeriodFrequencyType periodFrequencyType = CalendarFrequencyType.from(calendarFrequencyType);
frequencyType = CommonEnumerations.termFrequencyType(periodFrequencyType, "recurring.deposit.frequency.");
}
depositAccount = RecurringDepositAccountData.withInterestChartAndRecurringDetails((RecurringDepositAccountData) depositAccount,
chart, frequency, frequencyType);
}
@@ -403,6 +408,24 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
periodFrequencyTypeOptions, savingsAccountDatas, maturityInstructionOptions);
template = FixedDepositAccountData.withInterestChart((FixedDepositAccountData) template, accountChart);
try {
final Collection<CodeValueData> accountTypeOpts = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(SavingsApiConstants.FD_PRODUCT_ACCOUNT_TYPE);
final Collection<CodeValueData> customerSegmentOpts = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(SavingsApiConstants.FD_PRODUCT_CUSTOMER_SEGMENT);
((FixedDepositAccountData) template).setAccountTypeOptions(accountTypeOpts);
((FixedDepositAccountData) template).setCustomerSegmentOptions(customerSegmentOpts);
} catch (final Exception ex) {
log.warn("Failed to load FD account type/customer segment options: {}", ex.getMessage());
}
try {
final String preview = accountNumberGenerator.previewAccountNumber(productId, officeId);
template.setPreviewAccountNo(preview);
} catch (final Exception ex) {
log.warn("Failed to generate preview account number for FD productId={}: {}", productId, ex.getMessage());
}
} else if (depositAccountType.isRecurringDeposit()) {
// Save CBS fields from mapper result before factory methods create new objects
final RecurringDepositAccountData rdTemplateWithCbs = (RecurringDepositAccountData) template;
@@ -691,6 +714,16 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
selectFieldsSqlBuilder.append("sa.total_penalty_charge_derived as totalPenaltyCharge, ");
selectFieldsSqlBuilder.append("sa.total_withhold_tax_derived as totalWithholdTax,");
selectFieldsSqlBuilder.append("sa.deposit_type_enum as depositTypeId, ");
selectFieldsSqlBuilder.append("sa.cbs_account_type as accountType, sa.customer_segment as customerSegment, ");
selectFieldsSqlBuilder.append("sa.account_status as accountStatus, sa.mode_of_operation as modeOfOperation, ");
selectFieldsSqlBuilder.append("sa.special_category as specialCategory, sa.weaker_section as weakerSection, ");
selectFieldsSqlBuilder.append("sa.guardian_name as guardianName, sa.guardian_cif_number as guardianCifNumber, ");
selectFieldsSqlBuilder.append("sa.guardian_relationship as guardianRelationship, sa.guardian_client_id as guardianClientId, ");
selectFieldsSqlBuilder.append("sa.minor_date_of_birth as minorDateOfBirth, ");
selectFieldsSqlBuilder.append("sa.introducer_client_id as introducerClientId, sa.introducer_name as introducerName, ");
selectFieldsSqlBuilder.append("sa.introducer_cif_number as introducerCifNumber, ");
selectFieldsSqlBuilder.append("sa.linked_account_id as linkedAccountId, sa.linked_account_type as linkedAccountType, ");
selectFieldsSqlBuilder.append("sa.linked_account_no as linkedAccountNo, ");
selectFieldsSqlBuilder.append("sa.min_balance_for_interest_calculation as minBalanceForInterestCalculation, ");
selectFieldsSqlBuilder.append("sa.withhold_tax as withHoldTax,");
selectFieldsSqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName ");
@@ -701,6 +734,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
selectTablesSqlBuilder.append("from m_savings_account sa ");
selectTablesSqlBuilder.append("left join m_deposit_account_term_and_preclosure datp on sa.id = datp.savings_account_id ");
selectTablesSqlBuilder.append("join m_savings_product sp ON sa.product_id = sp.id ");
selectTablesSqlBuilder.append("left join m_code_value icv on icv.id = sp.interest_calculate_selector ");
selectTablesSqlBuilder.append("join m_currency curr on curr.code = sa.currency_code ");
selectTablesSqlBuilder.append("left join m_client c ON c.id = sa.client_id ");
selectTablesSqlBuilder.append("left join m_group g ON g.id = sa.group_id ");
@@ -709,7 +743,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
selectTablesSqlBuilder.append("left join m_appuser rbu on rbu.id = sa.rejectedon_userid ");
selectTablesSqlBuilder.append("left join m_appuser wbu on wbu.id = sa.withdrawnon_userid ");
selectTablesSqlBuilder.append("left join m_appuser abu on abu.id = sa.approvedon_userid ");
selectTablesSqlBuilder.append("left join m_appuser avbu on rbu.id = sa.activatedon_userid ");
selectTablesSqlBuilder.append("left join m_appuser avbu on avbu.id = sa.activatedon_userid ");
selectTablesSqlBuilder.append("left join m_appuser cbu on cbu.id = sa.closedon_userid ");
selectTablesSqlBuilder.append("left join m_tax_group tg on tg.id = sa.tax_group_id ");
@@ -899,7 +933,28 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
sqlBuilder.append("datp.deposit_period_frequency_enum as depositPeriodFrequencyTypeId, ");
sqlBuilder.append("datp.on_account_closure_enum as onAccountClosureId, ");
sqlBuilder.append("datp.transfer_interest_to_linked_account as transferInterestToSavings, ");
sqlBuilder.append("datp.transfer_to_savings_account_id as transferToSavingsId ");
sqlBuilder.append("datp.transfer_to_savings_account_id as transferToSavingsId, ");
sqlBuilder.append("sp.fd_type as fdType, sp.product_code as productCode, sp.effective_date as effectiveDate, ");
sqlBuilder.append("sp.effective_to as effectiveTo, sp.end_date as endDate, sp.introducer_required as introducerRequired, ");
sqlBuilder.append("sp.share_linking_enabled as shareLinkingEnabled, ");
sqlBuilder.append("COALESCE(sa.nomination_facility, sp.nomination_facility) as nominationFacility, ");
sqlBuilder.append("sp.dormancy_alert as dormancyAlert, sp.is_dormancy_tracking_active as isDormancyTrackingActive, ");
sqlBuilder.append("sp.days_to_inactive as daysToInactive, sp.days_to_dormancy as daysToDormancy, ");
sqlBuilder.append("sp.days_to_escheat as daysToEscheat, sp.interest_calculate_selector as interestCalculateSelector, ");
sqlBuilder.append("icv.code_value as interestCalculationTypeName, sp.average_period as averagePeriod, ");
sqlBuilder.append("sp.auto_post as autoPost, sp.frequency_fix_day as frequencyFixDay, ");
sqlBuilder.append("sp.last_interest_date as lastInterestDate, sp.enable_lockin_period as enableLockinPeriod, ");
sqlBuilder.append("sp.balance_frequency as balanceFrequency, ");
sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, ");
sqlBuilder.append("sp.balance_swap_option as balanceSwapOption, sp.msme as msme, ");
sqlBuilder.append("COALESCE(sa.auto_debit_facility, sp.auto_debit_facility) as autoDebitFacility, ");
sqlBuilder.append("sp.loan_facility as loanFacility, sp.premature_withdrawal_option as prematureWithdrawalOption, ");
sqlBuilder.append("sp.tenure_flexibility as tenureFlexibility, sp.lien_marked as lienMarked, ");
sqlBuilder.append("sp.maturity_instructions as maturityInstructions, sp.credit_maturity_proceeds as creditMaturityProceeds, ");
sqlBuilder.append("sp.penalty_grace_period_missed_installments as penaltyGracePeriodMissedInstallments, ");
sqlBuilder.append("sp.panel_request_applicable as panelRequestApplicable, ");
sqlBuilder.append("sp.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, ");
sqlBuilder.append("sp.deceased_settlement as deceasedSettlement, sp.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate ");
sqlBuilder.append(super.selectTablesSql());
@@ -949,10 +1004,26 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
final Long transferToSavingsId = JdbcSupport.getLong(rs, TRANSFER_TO_SAVINGS_ID);
return FixedDepositAccountData.instance(depositAccountData, preClosurePenalApplicable, preClosurePenalInterest,
final FixedDepositAccountData result = FixedDepositAccountData.instance(depositAccountData, preClosurePenalApplicable, preClosurePenalInterest,
preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType,
inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, maturityAmount, maturityDate, depositPeriod,
depositPeriodFrequencyType, onAccountClosureType, transferInterestToSavings, transferToSavingsId);
result.setCbsProductFields(rs.getString("accountType"), rs.getString("customerSegment"), rs.getString("fdType"), rs.getString("productCode"),
JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"),
JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"),
rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), rs.getBoolean("isDormancyTrackingActive"),
JdbcSupport.getLong(rs, "daysToInactive"), JdbcSupport.getLong(rs, "daysToDormancy"),
JdbcSupport.getLong(rs, "daysToEscheat"), JdbcSupport.getInteger(rs, "interestCalculateSelector"),
rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"),
JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"),
rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"),
rs.getBoolean("accountClosedZeroBalanceInDayClosing"), rs.getBoolean("balanceSwapOption"), rs.getBoolean("msme"),
rs.getBoolean("autoDebitFacility"), rs.getBoolean("loanFacility"), rs.getBoolean("prematureWithdrawalOption"),
rs.getBoolean("tenureFlexibility"), rs.getBoolean("lienMarked"), rs.getBoolean("maturityInstructions"),
rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"),
rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"),
rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate"));
return result;
}
}
@@ -1009,7 +1080,28 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
sqlBuilder.append("datp.maturity_date as maturityDate, ");
sqlBuilder.append("datp.deposit_period as depositPeriod, ");
sqlBuilder.append("datp.deposit_period_frequency_enum as depositPeriodFrequencyTypeId, ");
sqlBuilder.append("datp.on_account_closure_enum as onAccountClosureId ");
sqlBuilder.append("datp.on_account_closure_enum as onAccountClosureId, ");
sqlBuilder.append("sp.product_code as productCode, sp.effective_date as effectiveDate, ");
sqlBuilder.append("sp.effective_to as effectiveTo, sp.end_date as endDate, sp.introducer_required as introducerRequired, ");
sqlBuilder.append("sp.share_linking_enabled as shareLinkingEnabled, ");
sqlBuilder.append("COALESCE(sa.nomination_facility, sp.nomination_facility) as nominationFacility, ");
sqlBuilder.append("sp.dormancy_alert as dormancyAlert, sp.is_dormancy_tracking_active as isDormancyTrackingActive, ");
sqlBuilder.append("sp.days_to_inactive as daysToInactive, sp.days_to_dormancy as daysToDormancy, ");
sqlBuilder.append("sp.days_to_escheat as daysToEscheat, sp.interest_calculate_selector as interestCalculateSelector, ");
sqlBuilder.append("icv.code_value as interestCalculationTypeName, sp.average_period as averagePeriod, ");
sqlBuilder.append("sp.auto_post as autoPost, sp.frequency_fix_day as frequencyFixDay, ");
sqlBuilder.append("sp.last_interest_date as lastInterestDate, sp.enable_lockin_period as enableLockinPeriod, ");
sqlBuilder.append("sp.balance_frequency as balanceFrequency, ");
sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, ");
sqlBuilder.append("sp.balance_swap_option as balanceSwapOption, sp.msme as msme, ");
sqlBuilder.append("COALESCE(sa.auto_debit_facility, sp.auto_debit_facility) as autoDebitFacility, ");
sqlBuilder.append("sp.loan_facility as loanFacility, sp.premature_withdrawal_option as prematureWithdrawalOption, ");
sqlBuilder.append("sp.tenure_flexibility as tenureFlexibility, sp.lien_marked as lienMarked, ");
sqlBuilder.append("sp.maturity_instructions as maturityInstructions, sp.credit_maturity_proceeds as creditMaturityProceeds, ");
sqlBuilder.append("sp.penalty_grace_period_missed_installments as penaltyGracePeriodMissedInstallments, ");
sqlBuilder.append("sp.panel_request_applicable as panelRequestApplicable, ");
sqlBuilder.append("sp.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, ");
sqlBuilder.append("sp.deceased_settlement as deceasedSettlement, sp.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate ");
sqlBuilder.append(this.selectTablesSql());
sqlBuilder.append("left join m_deposit_account_recurring_detail dard on sa.id = dard.savings_account_id ");
@@ -1066,12 +1158,30 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
: SavingsEnumerations.depositAccountOnClosureType(onAccountClosureId);
final LocalDate expectedFirstDepositOnDate = JdbcSupport.getLocalDate(rs, EXPECTED_FIRST_DEPOSIT_ON_DATE);
return RecurringDepositAccountData.instance(depositAccountData, preClosurePenalApplicable, preClosurePenalInterest,
preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType,
inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, maturityAmount, maturityDate, depositPeriod,
depositPeriodFrequencyType, mandatoryRecommendedDepositAmount, onAccountClosureType, expectedFirstDepositOnDate,
totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit, allowWithdrawal, adjustAdvanceTowardsFuturePayments,
isCalendarInherited);
final RecurringDepositAccountData result = RecurringDepositAccountData.instance(depositAccountData,
preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm,
minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount,
maturityAmount, maturityDate, depositPeriod, depositPeriodFrequencyType, mandatoryRecommendedDepositAmount,
onAccountClosureType, expectedFirstDepositOnDate, totalOverdueAmount, noOfOverdueInstallments, isMandatoryDeposit,
allowWithdrawal, adjustAdvanceTowardsFuturePayments, isCalendarInherited);
result.setCbsProductFields(rs.getString("accountType"), rs.getString("customerSegment"), rs.getString("productCode"),
JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"),
JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"),
rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), rs.getBoolean("isDormancyTrackingActive"),
JdbcSupport.getInteger(rs, "daysToInactive"), JdbcSupport.getInteger(rs, "daysToDormancy"),
JdbcSupport.getInteger(rs, "daysToEscheat"), JdbcSupport.getInteger(rs, "interestCalculateSelector"),
rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"),
JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"),
rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"),
rs.getBoolean("accountClosedZeroBalanceInDayClosing"), rs.getBoolean("balanceSwapOption"), rs.getBoolean("msme"),
rs.getBoolean("autoDebitFacility"), rs.getBoolean("loanFacility"), rs.getBoolean("prematureWithdrawalOption"),
rs.getBoolean("tenureFlexibility"), rs.getBoolean("lienMarked"), rs.getBoolean("maturityInstructions"),
rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"),
rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"),
rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate"));
return result;
}
}
@@ -1418,6 +1528,31 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
sqlBuilder.append(", dptp.pre_closure_penal_applicable as preClosurePenalApplicable, ");
sqlBuilder.append("dptp.pre_closure_penal_interest as preClosurePenalInterest, ");
sqlBuilder.append("dptp.pre_closure_penal_interest_on_enum as preClosurePenalInterestOnId, ");
sqlBuilder.append("sa.account_type as accountType, sa.customer_segment as customerSegment, sa.fd_type as fdType, ");
sqlBuilder.append("sa.product_code as productCode, ");
sqlBuilder.append("sa.effective_date as effectiveDate, sa.effective_to as effectiveTo, sa.end_date as endDate, ");
sqlBuilder.append("sa.introducer_required as introducerRequired, sa.share_linking_enabled as shareLinkingEnabled, ");
sqlBuilder.append("sa.nomination_facility as nominationFacility, sa.dormancy_alert as dormancyAlert, ");
sqlBuilder.append("sa.is_dormancy_tracking_active as isDormancyTrackingActive, ");
sqlBuilder.append("sa.days_to_inactive as daysToInactive, sa.days_to_dormancy as daysToDormancy, ");
sqlBuilder.append("sa.days_to_escheat as daysToEscheat, ");
sqlBuilder.append("sa.interest_calculate_selector as interestCalculateSelector, ");
sqlBuilder.append("icv.code_value as interestCalculationTypeName, ");
sqlBuilder.append("sa.average_period as averagePeriod, ");
sqlBuilder.append("sa.auto_post as autoPost, sa.frequency_fix_day as frequencyFixDay, ");
sqlBuilder.append("sa.last_interest_date as lastInterestDate, sa.enable_lockin_period as enableLockinPeriod, ");
sqlBuilder.append("sa.balance_frequency as balanceFrequency, ");
sqlBuilder.append("sa.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, ");
sqlBuilder.append("sa.balance_swap_option as balanceSwapOption, sa.msme as msme, ");
sqlBuilder.append("sa.auto_debit_facility as autoDebitFacility, sa.loan_facility as loanFacility, ");
sqlBuilder.append("sa.premature_withdrawal_option as prematureWithdrawalOption, sa.tenure_flexibility as tenureFlexibility, ");
sqlBuilder.append("sa.lien_marked as lienMarked, sa.maturity_instructions as maturityInstructions, ");
sqlBuilder.append("sa.credit_maturity_proceeds as creditMaturityProceeds, ");
sqlBuilder.append("sa.penalty_grace_period_missed_installments as penaltyGracePeriodMissedInstallments, ");
sqlBuilder.append("sa.panel_request_applicable as panelRequestApplicable, ");
sqlBuilder.append("sa.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, ");
sqlBuilder.append("sa.deceased_settlement as deceasedSettlement, ");
sqlBuilder.append("sa.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate, ");
sqlBuilder.append("dptp.min_deposit_term as minDepositTerm, ");
sqlBuilder.append("dptp.max_deposit_term as maxDepositTerm, ");
sqlBuilder.append("dptp.min_deposit_term_type_enum as minDepositTermTypeId, ");
@@ -1426,6 +1561,7 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
sqlBuilder.append("dptp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId ");
sqlBuilder.append(super.selectTablesSql());
sqlBuilder.append("left join m_code_value icv on icv.id = sa.interest_calculate_selector ");
this.schemaSql = sqlBuilder.toString();
}
@@ -1467,10 +1603,26 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
final EnumOptionData onAccountClosureType = null;
final Boolean transferInterestToSavings = false;
return FixedDepositAccountData.instance(depositAccountData, preClosurePenalApplicable, preClosurePenalInterest,
preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType,
inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, maturityAmount, maturityDate, depositPeriod,
depositPeriodFrequencyType, onAccountClosureType, transferInterestToSavings, null);
final FixedDepositAccountData result = FixedDepositAccountData.instance(depositAccountData, preClosurePenalApplicable,
preClosurePenalInterest, preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType,
maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, depositAmount, maturityAmount,
maturityDate, depositPeriod, depositPeriodFrequencyType, onAccountClosureType, transferInterestToSavings, null);
result.setCbsProductFields(rs.getString("accountType"), rs.getString("customerSegment"), rs.getString("fdType"), rs.getString("productCode"),
JdbcSupport.getLocalDate(rs, "effectiveDate"), JdbcSupport.getLocalDate(rs, "effectiveTo"),
JdbcSupport.getLocalDate(rs, "endDate"), rs.getBoolean("introducerRequired"), rs.getBoolean("shareLinkingEnabled"),
rs.getBoolean("nominationFacility"), rs.getBoolean("dormancyAlert"), rs.getBoolean("isDormancyTrackingActive"),
JdbcSupport.getLong(rs, "daysToInactive"), JdbcSupport.getLong(rs, "daysToDormancy"),
JdbcSupport.getLong(rs, "daysToEscheat"), JdbcSupport.getInteger(rs, "interestCalculateSelector"),
rs.getString("interestCalculationTypeName"), rs.getString("averagePeriod"), rs.getBoolean("autoPost"),
JdbcSupport.getInteger(rs, "frequencyFixDay"), JdbcSupport.getLocalDate(rs, "lastInterestDate"),
rs.getBoolean("enableLockinPeriod"), rs.getString("balanceFrequency"),
rs.getBoolean("accountClosedZeroBalanceInDayClosing"), rs.getBoolean("balanceSwapOption"), rs.getBoolean("msme"),
rs.getBoolean("autoDebitFacility"), rs.getBoolean("loanFacility"), rs.getBoolean("prematureWithdrawalOption"),
rs.getBoolean("tenureFlexibility"), rs.getBoolean("lienMarked"), rs.getBoolean("maturityInstructions"),
rs.getBoolean("creditMaturityProceeds"), rs.getBoolean("penaltyGracePeriodMissedInstallments"),
rs.getBoolean("panelRequestApplicable"), rs.getBoolean("noPenaltyNomineeHeirClosure"),
rs.getBoolean("deceasedSettlement"), rs.getBoolean("allowDebitTxnAfterDueDate"));
return result;
}
}
@@ -197,8 +197,6 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, false, postReversals);
}
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
final boolean isPreMatureClosure = false;
@@ -287,7 +285,6 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck);
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
}
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
final MathContext mc = MathContext.DECIMAL64;
@@ -48,6 +48,7 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.event.business.domain.deposit.FixedDepositAccountCreateBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.deposit.RecurringDepositAccountCreateBusinessEvent;
@@ -69,6 +70,7 @@ import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
import org.apache.fineract.portfolio.client.service.ClientNomineeWritePlatformService;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.domain.GroupRepository;
@@ -89,6 +91,7 @@ import org.apache.fineract.portfolio.savings.domain.RecurringDepositAccountRepos
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountCharge;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountChargeAssembler;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.domain.SavingsProduct;
import org.apache.fineract.portfolio.savings.domain.SavingsProductRepository;
@@ -121,6 +124,8 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
private final ConfigurationDomainService configurationDomainService;
private final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository;
private final BusinessEventNotifierService businessEventNotifierService;
private final SavingsAccountAssembler savingsAccountAssembler;
private final ClientNomineeWritePlatformService clientNomineeWritePlatformService;
/*
* Guaranteed to throw an exception no matter what the data integrity issue is.
@@ -130,16 +135,16 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
String msg = "Unknown data integrity issue with savings account.";
String param = null;
Object[] msgArgs;
Throwable checkEx = realCause == null ? dve : realCause;
if (checkEx.getMessage().contains("sa_account_no_UNIQUE")) {
final String causeMsg = getAllCauseMessages(realCause, dve);
if (causeMsg.contains("sa_account_no_UNIQUE")) {
final String accountNo = command.stringValueOfParameterNamed("accountNo");
msgCode += ".duplicate.accountNo";
msg = "Savings account with accountNo " + accountNo + " already exists";
param = "accountNo";
msgArgs = new Object[] { accountNo, dve };
} else if (checkEx.getMessage().contains("sa_external_id_UNIQUE")) {
} else if (causeMsg.contains("sa_external_id_UNIQUE") || causeMsg.contains("m_savings_account_external_id_key")) {
final String externalId = command.stringValueOfParameterNamed("externalId");
msgCode += ".duplicate.externalId";
msgCode = "error.msg.savingsaccount.duplicate.externalId";
msg = "Savings account with externalId " + externalId + " already exists";
param = "externalId";
msgArgs = new Object[] { externalId, dve };
@@ -151,6 +156,18 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
throw ErrorHandler.getMappable(dve, msgCode, msg, param, msgArgs);
}
private String getAllCauseMessages(final Throwable realCause, final Exception dve) {
final StringBuilder sb = new StringBuilder();
Throwable t = realCause != null ? realCause : dve;
while (t != null) {
if (t.getMessage() != null) {
sb.append(t.getMessage()).append(' ');
}
t = t.getCause();
}
return sb.toString();
}
@Transactional
@Override
public CommandProcessingResult submitFDApplication(final JsonCommand command) {
@@ -165,11 +182,25 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(command, submittedBy,
DepositAccountType.FIXED_DEPOSIT);
if (account.getClient() != null && account.productId() != null) {
long existingCount = this.savingAccountRepository.countActiveAccountsByClientAndProduct(
account.getClient().getId(), account.productId());
if (existingCount > 0) {
throw new PlatformDataIntegrityException(
"error.msg.savings.account.duplicate.product.for.client",
"Client already has an active Fixed Deposit account with the same product.",
"productId", account.productId());
}
}
final MathContext mc = MathContext.DECIMAL64;
final boolean isPreMatureClosure = false;
account.updateMaturityDateAndAmountBeforeAccountActivation(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth);
this.savingsAccountAssembler.applyCbsFields(command, command.parsedJson(), account);
this.fixedDepositAccountRepository.saveAndFlush(account);
if (account.isAccountNumberRequiresAutoGeneration()) {
@@ -191,6 +222,16 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
this.accountAssociationsRepository.save(accountAssociations);
}
// Save nominees if provided in payload (same transaction)
final com.google.gson.JsonObject fdRoot = command.parsedJson().getAsJsonObject();
if (fdRoot.has("nominees") && !fdRoot.get("nominees").isJsonNull()) {
clientNomineeWritePlatformService.saveNomineesIfPresent(
account.clientId(), account.getAccountNumber(), "FD", fdRoot.get("nominees"));
} else if (fdRoot.has("nominee") && !fdRoot.get("nominee").isJsonNull()) {
clientNomineeWritePlatformService.saveNomineeIfPresent(
account.clientId(), account.getAccountNumber(), "FD", fdRoot.get("nominee"));
}
final Long savingsId = account.getId();
businessEventNotifierService.notifyPostBusinessEvent(new FixedDepositAccountCreateBusinessEvent(account));
@@ -226,6 +267,19 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(command,
submittedBy, DepositAccountType.RECURRING_DEPOSIT);
if (account.getClient() != null && account.productId() != null) {
long existingCount = this.savingAccountRepository.countActiveAccountsByClientAndProduct(
account.getClient().getId(), account.productId());
if (existingCount > 0) {
throw new PlatformDataIntegrityException(
"error.msg.savings.account.duplicate.product.for.client",
"Client already has an active Recurring Deposit account with the same product.",
"productId", account.productId());
}
}
this.savingsAccountAssembler.applyCbsFields(command, command.parsedJson(), account);
this.recurringDepositAccountRepository.save(account);
if (account.isAccountNumberRequiresAutoGeneration()) {
@@ -251,6 +305,17 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
financialYearBeginningMonth);
account.validateApplicableInterestRate();
savingAccountRepository.save(account);
// Save nominees if provided in payload (same transaction)
final com.google.gson.JsonObject rdRoot = command.parsedJson().getAsJsonObject();
if (rdRoot.has("nominees") && !rdRoot.get("nominees").isJsonNull()) {
clientNomineeWritePlatformService.saveNomineesIfPresent(
account.clientId(), account.getAccountNumber(), "RD", rdRoot.get("nominees"));
} else if (rdRoot.has("nominee") && !rdRoot.get("nominee").isJsonNull()) {
clientNomineeWritePlatformService.saveNomineeIfPresent(
account.clientId(), account.getAccountNumber(), "RD", rdRoot.get("nominee"));
}
businessEventNotifierService.notifyPostBusinessEvent(new RecurringDepositAccountCreateBusinessEvent(account));
return new CommandProcessingResultBuilder() //
@@ -346,6 +411,7 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
DepositAccountType.FIXED_DEPOSIT);
checkClientOrGroupActive(account);
account.modifyApplication(command, changes);
this.savingsAccountAssembler.applyCbsFields(command, command.parsedJson(), account);
account.validateNewApplicationState(DepositAccountType.FIXED_DEPOSIT.resourceName());
if (!changes.isEmpty()) {
@@ -436,6 +502,7 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
DepositAccountType.RECURRING_DEPOSIT);
checkClientOrGroupActive(account);
account.modifyApplication(command, changes);
this.savingsAccountAssembler.applyCbsFields(command, command.parsedJson(), account);
account.validateNewApplicationState(DepositAccountType.RECURRING_DEPOSIT.resourceName());
if (!changes.isEmpty()) {
@@ -230,11 +230,37 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
sqlBuilder.append("dptp.in_multiples_of_deposit_term as inMultiplesOfDepositTerm, ");
sqlBuilder.append("dptp.in_multiples_of_deposit_term_type_enum as inMultiplesOfDepositTermTypeId, ");
sqlBuilder.append("dptp.min_deposit_amount as minDepositAmount, dptp.deposit_amount as depositAmount, ");
sqlBuilder.append("dptp.max_deposit_amount as maxDepositAmount ");
sqlBuilder.append("dptp.max_deposit_amount as maxDepositAmount, ");
sqlBuilder.append("sp.account_type as accountType, sp.fd_type as fdType, sp.product_code as productCode, ");
sqlBuilder.append("sp.customer_segment as customerSegment, ");
sqlBuilder.append("sp.gl_account_id as glAccountId, gla.name as glAccountName, ");
sqlBuilder.append("sp.effective_date as effectiveDate, sp.effective_to as effectiveTo, sp.end_date as endDate, ");
sqlBuilder.append("sp.introducer_required as introducerRequired, sp.share_linking_enabled as shareLinkingEnabled, ");
sqlBuilder.append("sp.nomination_facility as nominationFacility, sp.dormancy_alert as dormancyAlert, ");
sqlBuilder.append("sp.is_dormancy_tracking_active as isDormancyTrackingActive, ");
sqlBuilder.append("sp.days_to_inactive as daysToInactive, sp.days_to_dormancy as daysToDormancy, ");
sqlBuilder.append("sp.days_to_escheat as daysToEscheat, ");
sqlBuilder.append("sp.interest_calculate_selector as interestCalculateSelector, ");
sqlBuilder.append("sp.average_period as averagePeriod, ");
sqlBuilder.append("sp.auto_post as autoPost, sp.frequency_fix_day as frequencyFixDay, ");
sqlBuilder.append("sp.last_interest_date as lastInterestDate, sp.enable_lockin_period as enableLockinPeriod, ");
sqlBuilder.append("sp.balance_frequency as balanceFrequency, ");
sqlBuilder.append("sp.account_closed_zero_balance_in_day_closing as accountClosedZeroBalanceInDayClosing, ");
sqlBuilder.append("sp.auto_debit_facility as autoDebitFacility, sp.loan_facility as loanFacility, ");
sqlBuilder.append("sp.premature_withdrawal_option as prematureWithdrawalOption, sp.tenure_flexibility as tenureFlexibility, ");
sqlBuilder.append("sp.lien_marked as lienMarked, sp.maturity_instructions as maturityInstructions, ");
sqlBuilder.append("sp.credit_maturity_proceeds as creditMaturityProceeds, ");
sqlBuilder.append("sp.penalty_grace_period_missed_installments as penaltyGracePeriodMissedInstallments, ");
sqlBuilder.append("sp.panel_request_applicable as panelRequestApplicable, ");
sqlBuilder.append("sp.no_penalty_nominee_heir_closure as noPenaltyNomineeHeirClosure, ");
sqlBuilder.append("sp.deceased_settlement as deceasedSettlement, ");
sqlBuilder.append("sp.allow_debit_txn_after_due_date as allowDebitTxnAfterDueDate, ");
sqlBuilder.append("sp.withdrawal_fee_for_transfer as withdrawalFeeForTransfers ");
sqlBuilder.append("from m_savings_product sp ");
sqlBuilder.append("left join m_deposit_product_term_and_preclosure dptp on sp.id=dptp.savings_product_id ");
sqlBuilder.append("join m_currency curr on curr.code = sp.currency_code ");
sqlBuilder.append(" left join m_tax_group tg on tg.id = sp.tax_group_id ");
sqlBuilder.append("left join m_tax_group tg on tg.id = sp.tax_group_id ");
sqlBuilder.append("left join acc_gl_account gla on gla.id = sp.gl_account_id ");
this.schemaSql = sqlBuilder.toString();
}
@@ -271,9 +297,55 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead
final BigDecimal depositAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "depositAmount");
final BigDecimal maxDepositAmount = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "maxDepositAmount");
return FixedDepositProductData.instance(depositProductData, preClosurePenalApplicable, preClosurePenalInterest,
preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType,
inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, minDepositAmount, depositAmount, maxDepositAmount);
final FixedDepositProductData result = FixedDepositProductData.instance(depositProductData, preClosurePenalApplicable,
preClosurePenalInterest, preClosurePenalInterestOnType, minDepositTerm, maxDepositTerm, minDepositTermType,
maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, minDepositAmount, depositAmount,
maxDepositAmount);
// CBS Custom Fields
result.setAccountType(rs.getString("accountType"));
result.setFdType(rs.getString("fdType"));
result.setProductCode(rs.getString("productCode"));
result.setCustomerSegment(rs.getString("customerSegment"));
result.setGlAccountId(JdbcSupport.getLong(rs, "glAccountId"));
result.setGlAccountName(rs.getString("glAccountName"));
final java.time.LocalDate effectiveDate = JdbcSupport.getLocalDate(rs, "effectiveDate");
result.setEffectiveDate(effectiveDate != null ? effectiveDate.toString() : null);
final java.time.LocalDate effectiveTo = JdbcSupport.getLocalDate(rs, "effectiveTo");
result.setEffectiveTo(effectiveTo != null ? effectiveTo.toString() : null);
final java.time.LocalDate endDate = JdbcSupport.getLocalDate(rs, "endDate");
result.setEndDate(endDate != null ? endDate.toString() : null);
result.setIntroducerRequired(rs.getBoolean("introducerRequired"));
result.setShareLinkingEnabled(rs.getBoolean("shareLinkingEnabled"));
result.setNominationFacility(rs.getBoolean("nominationFacility"));
result.setDormancyAlert(rs.getBoolean("dormancyAlert"));
result.setIsDormancyTrackingActive(rs.getBoolean("isDormancyTrackingActive"));
result.setDaysToInactive(JdbcSupport.getLong(rs, "daysToInactive"));
result.setDaysToDormancy(JdbcSupport.getLong(rs, "daysToDormancy"));
result.setDaysToEscheat(JdbcSupport.getLong(rs, "daysToEscheat"));
result.setInterestCalculateSelector(JdbcSupport.getInteger(rs, "interestCalculateSelector"));
result.setAveragePeriod(rs.getString("averagePeriod"));
result.setAutoPost(rs.getBoolean("autoPost"));
result.setFrequencyFixDay(JdbcSupport.getInteger(rs, "frequencyFixDay"));
final java.time.LocalDate lastInterestDate = JdbcSupport.getLocalDate(rs, "lastInterestDate");
result.setLastInterestDate(lastInterestDate != null ? lastInterestDate.toString() : null);
result.setEnableLockinPeriod(rs.getBoolean("enableLockinPeriod"));
result.setBalanceFrequency(rs.getString("balanceFrequency"));
result.setAccountClosedZeroBalanceInDayClosing(rs.getBoolean("accountClosedZeroBalanceInDayClosing"));
result.setAutoDebitFacility(rs.getBoolean("autoDebitFacility"));
result.setLoanFacility(rs.getBoolean("loanFacility"));
result.setPrematureWithdrawalOption(rs.getBoolean("prematureWithdrawalOption"));
result.setTenureFlexibility(rs.getBoolean("tenureFlexibility"));
result.setLienMarked(rs.getBoolean("lienMarked"));
result.setMaturityInstructions(rs.getBoolean("maturityInstructions"));
result.setCreditMaturityProceeds(rs.getBoolean("creditMaturityProceeds"));
result.setPenaltyGracePeriodMissedInstallments(rs.getBoolean("penaltyGracePeriodMissedInstallments"));
result.setPanelRequestApplicable(rs.getBoolean("panelRequestApplicable"));
result.setNoPenaltyNomineeHeirClosure(rs.getBoolean("noPenaltyNomineeHeirClosure"));
result.setDeceasedSettlement(rs.getBoolean("deceasedSettlement"));
result.setAllowDebitTxnAfterDueDate(rs.getBoolean("allowDebitTxnAfterDueDate"));
result.setWithdrawalFeeForTransfers(rs.getBoolean("withdrawalFeeForTransfers"));
return result;
}
}
@@ -0,0 +1,27 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.service;
import java.util.List;
import org.apache.fineract.portfolio.savings.api.FdInterestRateSaveRequest;
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
public interface FdBaseInterestRateReadPlatformService {
List<FdInterestRateEffectiveData> retrieveAllInterestRates();
List<FdInterestRateEffectiveData> retrieveAllActiveInterestRates();
List<FdInterestRateEffectiveData> retrieveActiveInterestRatesByType(String fdType);
FdInterestRateEffectiveData saveInterestRates(FdInterestRateSaveRequest request, Long createdById);
}
@@ -0,0 +1,128 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
package org.apache.fineract.portfolio.savings.service;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.savings.api.FdInterestRateSaveRequest;
import org.apache.fineract.portfolio.savings.data.FdInterestRateEffectiveData;
import org.apache.fineract.portfolio.savings.data.FdInterestRateSlabData;
import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateEffective;
import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateEffectiveRepository;
import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateSlabHistory;
import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateSlabHistoryRepository;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class FdBaseInterestRateReadPlatformServiceImpl implements FdBaseInterestRateReadPlatformService {
private final FdBaseInterestRateEffectiveRepository effectiveRepository;
private final FdBaseInterestRateSlabHistoryRepository slabRepository;
@Override
public List<FdInterestRateEffectiveData> retrieveAllInterestRates() {
return effectiveRepository.findByOrderByEffectiveFromDateDesc().stream().map(this::toData).collect(Collectors.toList());
}
@Override
public List<FdInterestRateEffectiveData> retrieveAllActiveInterestRates() {
return effectiveRepository.findByIsActiveTrueOrderByEffectiveFromDateDesc().stream().map(this::toData)
.collect(Collectors.toList());
}
@Override
public List<FdInterestRateEffectiveData> retrieveActiveInterestRatesByType(String fdType) {
return effectiveRepository.findByFdTypeAndIsActiveTrueOrderByEffectiveFromDateDesc(fdType).stream().map(this::toData)
.collect(Collectors.toList());
}
@Override
@Transactional
public FdInterestRateEffectiveData saveInterestRates(FdInterestRateSaveRequest request, Long createdById) {
final String fdType = request.getFdType();
final LocalDate today = LocalDate.now();
final List<FdBaseInterestRateEffective> activeVersions = effectiveRepository
.findByFdTypeAndIsActiveTrueOrderByEffectiveFromDateDesc(fdType);
for (FdBaseInterestRateEffective version : activeVersions) {
version.deactivate(today);
}
if (!activeVersions.isEmpty()) {
effectiveRepository.saveAll(activeVersions);
}
final String label = request.getVersionLabel() != null && !request.getVersionLabel().isBlank()
? request.getVersionLabel()
: "FD " + fdType + " Rate Version " + LocalDate.now();
FdBaseInterestRateEffective newVersion = FdBaseInterestRateEffective.create(fdType, label, today, request.getRemarks(), createdById);
newVersion = effectiveRepository.saveAndFlush(newVersion);
final Long versionId = newVersion.getId();
int srCounter = 1;
if (request.getSlabs() != null) {
for (FdInterestRateSaveRequest.SlabRequest slabRequest : request.getSlabs()) {
final int sr = slabRequest.getSrNo() != null ? slabRequest.getSrNo() : srCounter++;
final FdBaseInterestRateSlabHistory slab = FdBaseInterestRateSlabHistory.create(versionId, sr,
slabRequest.getDescription(), slabRequest.getPeriodTypeEnum(), slabRequest.getFromPeriod(),
slabRequest.getToPeriod(), slabRequest.getAnnualInterestRate(), slabRequest.getSeniorCitizenRate());
slabRepository.save(slab);
}
}
return toData(newVersion);
}
private FdInterestRateEffectiveData toData(FdBaseInterestRateEffective e) {
List<FdInterestRateSlabData> slabs = slabRepository.findByEffectiveIdOrderBySrNo(e.getId()).stream()
.map(s -> FdInterestRateSlabData.builder()
.id(s.getId())
.srNo(s.getSrNo())
.description(s.getDescription())
.periodTypeEnum(s.getPeriodTypeEnum())
.periodTypeLabel(periodLabel(s.getPeriodTypeEnum()))
.fromPeriod(s.getFromPeriod())
.toPeriod(s.getToPeriod())
.annualInterestRate(s.getAnnualInterestRate())
.seniorCitizenRate(s.getSeniorCitizenRate())
.build())
.collect(Collectors.toList());
return FdInterestRateEffectiveData.builder()
.id(e.getId())
.fdType(e.getFdType())
.versionLabel(e.getVersionLabel())
.effectiveFromDate(e.getEffectiveFromDate())
.effectiveToDate(e.getEffectiveToDate())
.isActive(e.getIsActive())
.remarks(e.getRemarks())
.slabs(slabs)
.build();
}
private String periodLabel(Integer code) {
if (code == null) {
return "Days";
}
return switch (code) {
case 0 -> "Days";
case 1 -> "Weeks";
case 2 -> "Months";
case 3 -> "Years";
default -> "Days";
};
}
}
@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.service;
import java.util.HashMap;
import org.apache.fineract.infrastructure.core.api.JsonQuery;
public interface RecurringDepositAccountInterestCalculationService {
HashMap<String, Object> calculateInterest(JsonQuery query);
}
@@ -0,0 +1,138 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.service;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.annualInterestRateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositAmountParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.interestCompoundingPeriodInMonthsParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
import com.google.gson.JsonElement;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.HashMap;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonQuery;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class RecurringDepositAccountInterestCalculationServiceImpl implements RecurringDepositAccountInterestCalculationService {
private final FromJsonHelper fromApiJsonHelper;
@Override
public HashMap<String, Object> calculateInterest(JsonQuery query) {
JsonElement element = query.parsedJson();
BigDecimal depositAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(depositAmountParamName, element);
BigDecimal annualInterestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(annualInterestRateParamName, element);
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
Long interestCompoundingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestCompoundingPeriodInMonthsParamName,
element);
LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed("submittedOnDate", element);
if (submittedOnDate == null) {
submittedOnDate = LocalDate.now();
}
BigDecimal maturityAmount = this.calculateInterestInternal(depositAmount, annualInterestRate, tenureInMonths,
interestCompoundingPeriodInMonths, submittedOnDate);
BigDecimal totalPrincipal = depositAmount.multiply(BigDecimal.valueOf(tenureInMonths));
BigDecimal totalInterest = maturityAmount.subtract(totalPrincipal);
String warning = "This is an approximate calculated amount - it may vary slightly when the account is created";
HashMap<String, Object> result = new HashMap<>();
result.put("maturityAmount", maturityAmount);
result.put("totalPrincipal", totalPrincipal);
result.put("totalInterest", totalInterest);
result.put("daysInYear", 365); // Standard for this product as per image
result.put("warning", warning);
return result;
}
public BigDecimal calculateInterestInternal(BigDecimal installmentAmount, BigDecimal annualInterestRate, Long tenureInMonths,
Long interestCompoundingPeriodInMonths, LocalDate submittedOnDate) {
MathContext mc = MoneyHelper.getMathContext();
BigDecimal maturityAmount = BigDecimal.ZERO;
int totalMonths = Math.toIntExact(tenureInMonths);
if (interestCompoundingPeriodInMonths == 0) {
// Standard Banking Approach for Daily Compounding RD using Actual Calendar Days
BigDecimal currentBalance = BigDecimal.ZERO;
double dailyRate = annualInterestRate.doubleValue() / 36500.0;
LocalDate date = submittedOnDate;
for (int m = 1; m <= totalMonths; m++) {
// Add monthly installment at the start of the month
currentBalance = currentBalance.add(installmentAmount);
// Get actual days in the current month
int daysInMonth = date.lengthOfMonth();
// Calculate interest for the month using daily compounding
double multiplier = Math.pow(1.0 + dailyRate, daysInMonth);
currentBalance = currentBalance.multiply(BigDecimal.valueOf(multiplier), mc);
// Move to next month
date = date.plusMonths(1);
}
maturityAmount = currentBalance;
} else {
// Periodic Compounding (Monthly, Quarterly, etc.)
BigDecimal monthlyRate = annualInterestRate.divide(BigDecimal.valueOf(1200), mc);
BigDecimal i = monthlyRate;
int n = totalMonths;
// Standard RD formula for monthly compounding:
// M = P * ((1+i)^n - 1) / i * (1+i)
// For other periods, we can approximate or use a loop.
// Using loop for consistency with various compounding periods.
BigDecimal compoundingFrequencyMonths = BigDecimal.valueOf(interestCompoundingPeriodInMonths);
BigDecimal ratePerCompounding = annualInterestRate.divide(BigDecimal.valueOf(100), mc)
.multiply(compoundingFrequencyMonths).divide(BigDecimal.valueOf(12), mc);
for (int m = 1; m <= totalMonths; m++) {
BigDecimal monthsRemaining = BigDecimal.valueOf(totalMonths - m + 1);
BigDecimal numberOfCompoundings = monthsRemaining.divide(compoundingFrequencyMonths, mc);
BigDecimal installmentMaturity = installmentAmount.multiply(
BigDecimal.ONE.add(ratePerCompounding).pow(numberOfCompoundings.intValue(), mc), mc);
// Add remaining partial compounding period interest (simple interest for the rest)
BigDecimal remainingMonths = monthsRemaining.remainder(compoundingFrequencyMonths);
if (remainingMonths.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal partialInterest = installmentMaturity.multiply(annualInterestRate).multiply(remainingMonths)
.divide(BigDecimal.valueOf(1200), mc);
installmentMaturity = installmentMaturity.add(partialInterest);
}
maturityAmount = maturityAmount.add(installmentMaturity);
}
}
return maturityAmount.setScale(2, RoundingMode.HALF_UP);
}
}
@@ -798,6 +798,10 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
sqlBuilder.append("sa.linked_account_id as linkedAccountId, ");
sqlBuilder.append("sa.linked_account_type as linkedAccountType, ");
sqlBuilder.append("sa.linked_account_no as linkedAccountNo, ");
sqlBuilder.append("sp.balance_swap_option as balanceSwapOption, ");
sqlBuilder.append("sp.msme as msme, ");
sqlBuilder.append("COALESCE(sa.nomination_facility, sp.nomination_facility) as nominationFacility, ");
sqlBuilder.append("COALESCE(sa.auto_debit_facility, sp.auto_debit_facility) as autoDebitFacility, ");
// Facility config fields
sqlBuilder.append("safc.atm_facility as atmFacility, ");
sqlBuilder.append("safc.atm_withdrawal_facility as atmWithdrawalFacility, ");
@@ -1049,6 +1053,10 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
accountData.setLinkedAccountId(JdbcSupport.getLong(rs, "linkedAccountId"));
accountData.setLinkedAccountType(rs.getString("linkedAccountType"));
accountData.setLinkedAccountNo(rs.getString("linkedAccountNo"));
accountData.setBalanceSwapOption(rs.getObject("balanceSwapOption") != null ? rs.getBoolean("balanceSwapOption") : null);
accountData.setMsme(rs.getObject("msme") != null ? rs.getBoolean("msme") : null);
accountData.setNominationFacility(rs.getObject("nominationFacility") != null ? rs.getBoolean("nominationFacility") : null);
accountData.setAutoDebitFacility(rs.getObject("autoDebitFacility") != null ? rs.getBoolean("autoDebitFacility") : null);
// Nominee as parsed map
final String nomineeDataStr = rs.getString("nomineeData");
@@ -96,6 +96,7 @@ import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.apache.fineract.portfolio.savings.service.FacilityChargeAutoApplyService;
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
import org.apache.fineract.portfolio.savings.SavingsApiConstants;
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
@@ -165,6 +166,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
private final GSIMRepositoy gsimRepository;
private final SavingsAccountInterestPostingService savingsAccountInterestPostingService;
private final ErrorHandler errorHandler;
private final FacilityChargeAutoApplyService facilityChargeAutoApplyService;
@Transactional
@Override
@@ -199,6 +201,24 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false);
// Validate minimum opening balance
BigDecimal openingBalanceForValidation = BigDecimal.ZERO;
if (command.parameterExists("openingBalance")) {
final BigDecimal ob = command.bigDecimalValueOfParameterNamed("openingBalance");
if (ob != null) openingBalanceForValidation = ob;
}
final BigDecimal minRequiredOpening = account.getMinRequiredOpeningBalance();
if (minRequiredOpening != null && minRequiredOpening.compareTo(BigDecimal.ZERO) > 0
&& openingBalanceForValidation.compareTo(minRequiredOpening) < 0) {
final List<ApiParameterError> errors = new ArrayList<>();
errors.add(ApiParameterError.parameterError(
"openingBalance",
"Opening balance (" + openingBalanceForValidation + ") is less than minimum required opening balance (" + minRequiredOpening + ")",
"validation.msg.savingsaccount.openingBalance.less.than.min.required",
openingBalanceForValidation, minRequiredOpening));
throw new PlatformApiDataValidationException(errors);
}
checkClientOrGroupActive(account);
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
@@ -214,6 +234,13 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
processPostActiveActions(account, fmt, existingTransactionIds, existingReversedTransactionIds);
// Auto-apply facility charges based on product facility config
facilityChargeAutoApplyService.applyFacilityChargesOnActivation(account);
this.savingAccountRepositoryWrapper.saveAndFlush(account);
// Collect any newly added facility activation charges (after flush so amountOutstanding is set)
// payCharge() internally adds transactions to account.transactions via handleChargeTransactions
// Do NOT add to existingTransactionIds — postJournalEntries must include them for GL posting
facilityChargeAutoApplyService.collectActivationCharges(account);
this.savingAccountRepositoryWrapper.saveAndFlush(account);
}
@@ -1391,8 +1418,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
public SavingsAccountData fallbackPostInterest(SavingsAccountData savingsAccountData, boolean postInterestAs, LocalDate transactionDate,
boolean backdatedTxnsAllowedTill, Throwable t) {
// NOTE: allow caller to catch the exceptions
// NOTE: wrap throwable only if really necessary
throw errorHandler.getMappable(t, null, null, "savings.postinterest");
throw org.apache.fineract.infrastructure.core.exception.ErrorHandler.getMappable(t, null, null, "savings.postinterest");
}
@Transactional
@@ -47,6 +47,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.dataqueries.data.EntityTables;
import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum;
import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService;
@@ -62,6 +63,7 @@ import org.apache.fineract.portfolio.account.service.AccountNumberGenerator;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
import org.apache.fineract.portfolio.client.service.ClientNomineeWritePlatformService;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.domain.GroupRepository;
import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper;
@@ -113,6 +115,7 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
private final GSIMRepositoy gsimRepository;
private final GroupRepositoryWrapper groupRepositoryWrapper;
private final GroupSavingsIndividualMonitoringWritePlatformService gsimWritePlatformService;
private final ClientNomineeWritePlatformService clientNomineeWritePlatformService;
@Transactional
@Override
@@ -143,6 +146,18 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
final AppUser submittedBy = this.context.authenticatedUser();
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(command, submittedBy);
if (account.getClient() != null && account.productId() != null) {
long existingCount = this.savingAccountRepository.countActiveAccountsByClientAndProduct(
account.getClient().getId(), account.productId());
if (existingCount > 0) {
throw new PlatformDataIntegrityException(
"error.msg.savings.account.duplicate.product.for.client",
"Client already has an active account with the same product.",
"productId", account.productId());
}
}
this.savingAccountRepository.save(account);
String accountNumber = "";
GroupSavingsIndividualMonitoring gsimAccount = null;
@@ -231,6 +246,16 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
}
}
// end of gsim
// Save nominees if provided in payload (same transaction)
final com.google.gson.JsonObject root = command.parsedJson().getAsJsonObject();
if (root.has("nominees") && !root.get("nominees").isJsonNull()) {
clientNomineeWritePlatformService.saveNomineesIfPresent(
account.clientId(), account.getAccountNumber(), "SB", root.get("nominees"));
} else if (root.has("nominee") && !root.get("nominee").isJsonNull()) {
clientNomineeWritePlatformService.saveNomineeIfPresent(
account.clientId(), account.getAccountNumber(), "SB", root.get("nominee"));
}
final Long savingsId = account.getId();
if (command.parameterExists(SavingsApiConstants.datatables)) {
this.entityDatatableChecksWritePlatformService.saveDatatables(StatusEnum.CREATE.getValue(), EntityTables.SAVINGS.getName(),
@@ -692,16 +717,16 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
String msg = "Unknown data integrity issue with savings account.";
String param = null;
Object[] msgArgs;
Throwable checkEx = realCause == null ? dve : realCause;
if (checkEx.getMessage().contains("sa_account_no_UNIQUE")) {
final String causeMsg = getRootCauseMessage(realCause, dve);
if (causeMsg.contains("sa_account_no_UNIQUE")) {
final String accountNo = command.stringValueOfParameterNamed("accountNo");
msgCode += ".duplicate.accountNo";
msg = "Savings account with accountNo " + accountNo + " already exists";
param = "accountNo";
msgArgs = new Object[] { accountNo, dve };
} else if (checkEx.getMessage().contains("sa_externalid_UNIQUE")) {
} else if (causeMsg.contains("sa_external_id_UNIQUE") || causeMsg.contains("m_savings_account_external_id_key")) {
final String externalId = command.stringValueOfParameterNamed("externalId");
msgCode += ".duplicate.externalId";
msgCode = "error.msg.savingsaccount.duplicate.externalId";
msg = "Savings account with externalId " + externalId + " already exists";
param = "externalId";
msgArgs = new Object[] { externalId, dve };
@@ -712,4 +737,16 @@ public class SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
log.error("Error occured.", dve);
throw ErrorHandler.getMappable(dve, msgCode, msg, param, msgArgs);
}
private String getRootCauseMessage(final Throwable realCause, final Exception dve) {
final StringBuilder sb = new StringBuilder();
Throwable t = realCause != null ? realCause : dve;
while (t != null) {
if (t.getMessage() != null) {
sb.append(t.getMessage()).append(' ');
}
t = t.getCause();
}
return sb.toString();
}
}
@@ -55,6 +55,7 @@ import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.apache.fineract.portfolio.charge.service.ChargeDropdownReadPlatformService;
import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.service.ClientNomineeWritePlatformService;
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService;
import org.apache.fineract.portfolio.group.domain.GroupRepository;
@@ -97,6 +98,7 @@ import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfig
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimit;
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityLimitRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsProductRepository;
import org.apache.fineract.portfolio.savings.service.FacilityChargeAutoApplyService;
import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformService;
import org.apache.fineract.portfolio.savings.service.DepositAccountInterestRateChartReadPlatformServiceImpl;
import org.apache.fineract.portfolio.savings.service.DepositAccountOnHoldTransactionReadPlatformService;
@@ -146,8 +148,12 @@ import org.apache.fineract.portfolio.savings.service.SavingsProductWritePlatform
import org.apache.fineract.portfolio.savings.service.SavingsProductWritePlatformServiceJpaRepositoryImpl;
import org.apache.fineract.portfolio.savings.domain.RdBaseInterestRateEffectiveRepository;
import org.apache.fineract.portfolio.savings.domain.RdBaseInterestRateSlabHistoryRepository;
import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateEffectiveRepository;
import org.apache.fineract.portfolio.savings.domain.FdBaseInterestRateSlabHistoryRepository;
import org.apache.fineract.portfolio.savings.service.RdBaseInterestRateReadPlatformService;
import org.apache.fineract.portfolio.savings.service.RdBaseInterestRateReadPlatformServiceImpl;
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformService;
import org.apache.fineract.portfolio.savings.service.FdBaseInterestRateReadPlatformServiceImpl;
import org.apache.fineract.portfolio.savings.service.SavingsSchedularInterestPoster;
import org.apache.fineract.portfolio.savings.service.SavingsSchedularInterestPosterTask;
import org.apache.fineract.portfolio.savings.service.search.SavingsAccountTransactionSearchService;
@@ -171,6 +177,14 @@ public class SavingsConfiguration {
return new RdBaseInterestRateReadPlatformServiceImpl(effectiveRepository, slabRepository);
}
@Bean
@ConditionalOnMissingBean(FdBaseInterestRateReadPlatformService.class)
public FdBaseInterestRateReadPlatformService fdBaseInterestRateReadPlatformService(
FdBaseInterestRateEffectiveRepository effectiveRepository,
FdBaseInterestRateSlabHistoryRepository slabRepository) {
return new FdBaseInterestRateReadPlatformServiceImpl(effectiveRepository, slabRepository);
}
@Bean
@ConditionalOnMissingBean(SavingsAccountTransactionSearchService.class)
public SavingsAccountTransactionSearchService savingsAccountTransactionSearchService(PlatformSecurityContext context,
@@ -277,13 +291,14 @@ public class SavingsConfiguration {
SavingsAccountChargeAssembler savingsAccountChargeAssembler, AccountAssociationsRepository accountAssociationsRepository,
FromJsonHelper fromJsonHelper, CalendarInstanceRepository calendarInstanceRepository,
ConfigurationDomainService configurationDomainService, AccountNumberFormatRepositoryWrapper accountNumberFormatRepository,
BusinessEventNotifierService businessEventNotifierService) {
BusinessEventNotifierService businessEventNotifierService, SavingsAccountAssembler savingsAccountAssembler,
ClientNomineeWritePlatformService clientNomineeWritePlatformService) {
return new DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepository,
fixedDepositAccountRepository, recurringDepositAccountRepository, depositAccountAssembler, depositAccountDataValidator,
accountNumberGenerator, clientRepository, groupRepository, savingsProductRepository, noteRepository, staffRepository,
savingsAccountApplicationTransitionApiJsonValidator, savingsAccountChargeAssembler, accountAssociationsRepository,
fromJsonHelper, calendarInstanceRepository, configurationDomainService, accountNumberFormatRepository,
businessEventNotifierService);
businessEventNotifierService, savingsAccountAssembler, clientNomineeWritePlatformService);
}
@Bean
@@ -409,7 +424,7 @@ public class SavingsConfiguration {
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, AppUserRepositoryWrapper appuserRepository,
StandingInstructionRepository standingInstructionRepository, BusinessEventNotifierService businessEventNotifierService,
GSIMRepositoy gsimRepository, SavingsAccountInterestPostingService savingsAccountInterestPostingService,
ErrorHandler errorHandler) {
ErrorHandler errorHandler, FacilityChargeAutoApplyService facilityChargeAutoApplyService) {
return new SavingsAccountWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, savingAccountRepositoryWrapper,
staffRepository, savingsAccountTransactionRepository, savingAccountAssembler, savingsAccountTransactionDataValidator,
savingsAccountChargeDataValidator, paymentDetailWritePlatformService, journalEntryWritePlatformService,
@@ -417,7 +432,7 @@ public class SavingsConfiguration {
chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService,
depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository,
standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService,
errorHandler);
errorHandler, facilityChargeAutoApplyService);
}
@Bean
@@ -432,13 +447,14 @@ public class SavingsConfiguration {
SavingsAccountDomainService savingsAccountDomainService, SavingsAccountWritePlatformService savingsAccountWritePlatformService,
AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, BusinessEventNotifierService businessEventNotifierService,
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GSIMRepositoy gsimRepository,
GroupRepositoryWrapper groupRepositoryWrapper, GroupSavingsIndividualMonitoringWritePlatformService gsimWritePlatformService) {
GroupRepositoryWrapper groupRepositoryWrapper, GroupSavingsIndividualMonitoringWritePlatformService gsimWritePlatformService,
ClientNomineeWritePlatformService clientNomineeWritePlatformService) {
return new SavingsApplicationProcessWritePlatformServiceJpaRepositoryImpl(context, savingAccountRepository, savingAccountAssembler,
savingsAccountDataValidator, accountNumberGenerator, clientRepository, groupRepository, savingsProductRepository,
noteRepository, staffRepository, savingsAccountApplicationTransitionApiJsonValidator, savingsAccountChargeAssembler,
commandProcessingService, savingsAccountDomainService, savingsAccountWritePlatformService, accountNumberFormatRepository,
businessEventNotifierService, entityDatatableChecksWritePlatformService, gsimRepository, groupRepositoryWrapper,
gsimWritePlatformService);
gsimWritePlatformService, clientNomineeWritePlatformService);
}
@Bean
@@ -35,6 +35,7 @@ import org.apache.fineract.useradministration.dsc.data.DscLoginChallengeStartRes
import org.apache.fineract.useradministration.dsc.data.DscLoginStatusResponse;
import org.apache.fineract.useradministration.dsc.data.DscLoginVerifyRequest;
import org.apache.fineract.useradministration.dsc.service.DscLoginChallengeService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
@@ -54,6 +55,7 @@ import org.springframework.stereotype.Component;
*/
@Path("/v1/login/dsc")
@Component
@ConditionalOnProperty("fineract.security.2fa.enabled")
@Tag(name = "Login DSC", description = "DSC-based two-factor authentication")
@RequiredArgsConstructor
public class LoginDscApiResource {
@@ -407,7 +407,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:root}
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863}
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
@@ -289,6 +289,11 @@
<include file="parts/0273_add_gl_account_id_to_current_product.xml" relativeToChangelogFile="true"/>
<include file="parts/0276_create_current_account_tables.xml" relativeToChangelogFile="true"/>
<include file="parts/0277_fix_current_account_audit_columns.xml" relativeToChangelogFile="true"/>
<include file="parts/0278_add_is_mandatory_to_joint_holder.xml" relativeToChangelogFile="true"/>
<include file="parts/0279_add_fd_type_and_base_interest_rate_tables.xml" relativeToChangelogFile="true"/>
<include file="parts/0280_add_fixed_deposit_product_codes.xml" relativeToChangelogFile="true"/>
<include file="parts/0284_remove_product_code_unique_constraints.xml" relativeToChangelogFile="true"/>
<include file="parts/0281_update_fd_base_interest_rate_seed_data.xml" relativeToChangelogFile="true"/>
<include file="parts/0278_add_soft_delete_to_family_members.xml" relativeToChangelogFile="true"/>
<include file="parts/0279_add_soft_delete_columns_client_tables.xml" relativeToChangelogFile="true"/>
<include file="parts/0280_add_deletion_audit_and_version_columns.xml" relativeToChangelogFile="true"/>
@@ -308,5 +313,7 @@
<include file="parts/0294_add_tfa_token_to_dsc_challenge.xml" relativeToChangelogFile="true"/>
<include file="parts/0295_unique_email_mobile_on_m_appuser.xml" relativeToChangelogFile="true"/>
<include file="parts/0296_unique_nominee_per_account_type.xml" relativeToChangelogFile="true"/>
<include file="parts/0297_seed_indian_states.xml" relativeToChangelogFile="true"/>
<include file="parts/0297_fix_unique_nominee_per_account.xml" relativeToChangelogFile="true"/>
<include file="parts/0298_unique_external_id_on_current_account.xml" relativeToChangelogFile="true"/>
<include file="parts/0299_seed_indian_states.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,35 @@
<?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.3.xsd">
<changeSet id="0278_add_is_mandatory_to_joint_holder" author="system">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="m_current_account_joint_holder" columnName="is_mandatory"/>
</not>
</preConditions>
<addColumn tableName="m_current_account_joint_holder">
<column name="is_mandatory" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0278b_add_is_mandatory_to_savings_joint_holder" author="system">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_savings_account_joint_holders"/>
<not>
<columnExists tableName="m_savings_account_joint_holders" columnName="is_mandatory"/>
</not>
</and>
</preConditions>
<addColumn tableName="m_savings_account_joint_holders">
<column name="is_mandatory" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,300 @@
<?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">
<!-- 1. Add fd_type column to m_savings_product -->
<changeSet id="0279a_add_fd_type_to_savings_product" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_savings_product" columnName="fd_type"/></not>
</preConditions>
<addColumn tableName="m_savings_product">
<column name="fd_type" type="VARCHAR(20)" defaultValue="SINGLE">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
<!-- 2. Create FD base interest rate effective (version) table -->
<changeSet id="0279b_create_fd_base_interest_rate_effective" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_fd_base_interest_rate_effective"/></not>
</preConditions>
<createTable tableName="m_fd_base_interest_rate_effective">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="fd_type" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="version_label" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="effective_from_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="effective_to_date" type="DATE"/>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="remarks" type="VARCHAR(500)"/>
<column name="created_by_id" type="BIGINT"/>
<column name="created_date" type="DATE"/>
</createTable>
</changeSet>
<!-- 3. Create FD base interest rate slab history table -->
<changeSet id="0279c_create_fd_base_interest_rate_slab_history" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_fd_base_interest_rate_slab_history"/></not>
</preConditions>
<createTable tableName="m_fd_base_interest_rate_slab_history">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="effective_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="sr_no" type="INT">
<constraints nullable="false"/>
</column>
<column name="description" type="VARCHAR(200)"/>
<!-- period_type_enum: 0=DAYS, 1=WEEKS, 2=MONTHS, 3=YEARS -->
<column name="period_type_enum" type="INT" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="from_period" type="INT">
<constraints nullable="false"/>
</column>
<column name="to_period" type="INT"/>
<column name="annual_interest_rate" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<!-- senior_citizen_rate: additional rate for senior citizens -->
<column name="senior_citizen_rate" type="DECIMAL(19,6)" defaultValueNumeric="0"/>
<column name="created_date" type="DATE"/>
</createTable>
<addForeignKeyConstraint
baseTableName="m_fd_base_interest_rate_slab_history"
baseColumnNames="effective_id"
referencedTableName="m_fd_base_interest_rate_effective"
referencedColumnNames="id"
constraintName="fk_fd_base_rate_slab_effective"/>
</changeSet>
<!-- 4. Seed initial FD base interest rate data -->
<changeSet id="0279d_seed_fd_base_interest_rate_single" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SINGLE'</sqlCheck>
</preConditions>
<!-- FD SINGLE: &lt; 1 year, period in DAYS -->
<insert tableName="m_fd_base_interest_rate_effective">
<column name="fd_type" value="SINGLE"/>
<column name="version_label" value="FD-SINGLE-V1"/>
<column name="effective_from_date" value="2025-01-01"/>
<column name="is_active" valueBoolean="true"/>
<column name="remarks" value="Initial FD Single (Short Term) rates"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
</changeSet>
<changeSet id="0279e_seed_fd_single_slabs" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_fd_base_interest_rate_slab_history sh JOIN m_fd_base_interest_rate_effective e ON sh.effective_id = e.id WHERE e.fd_type = 'SINGLE'</sqlCheck>
</preConditions>
<!-- 1-29 days -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SINGLE' AND version_label='FD-SINGLE-V1')"/>
<column name="sr_no" valueNumeric="1"/>
<column name="description" value="1 to 29 Days"/>
<column name="period_type_enum" valueNumeric="0"/>
<column name="from_period" valueNumeric="1"/>
<column name="to_period" valueNumeric="29"/>
<column name="annual_interest_rate" valueNumeric="4.50"/>
<column name="senior_citizen_rate" valueNumeric="5.00"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 30-89 days -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SINGLE' AND version_label='FD-SINGLE-V1')"/>
<column name="sr_no" valueNumeric="2"/>
<column name="description" value="30 to 89 Days"/>
<column name="period_type_enum" valueNumeric="0"/>
<column name="from_period" valueNumeric="30"/>
<column name="to_period" valueNumeric="89"/>
<column name="annual_interest_rate" valueNumeric="5.00"/>
<column name="senior_citizen_rate" valueNumeric="5.50"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 90-179 days -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SINGLE' AND version_label='FD-SINGLE-V1')"/>
<column name="sr_no" valueNumeric="3"/>
<column name="description" value="90 to 179 Days"/>
<column name="period_type_enum" valueNumeric="0"/>
<column name="from_period" valueNumeric="90"/>
<column name="to_period" valueNumeric="179"/>
<column name="annual_interest_rate" valueNumeric="5.50"/>
<column name="senior_citizen_rate" valueNumeric="6.00"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 180+ days -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SINGLE' AND version_label='FD-SINGLE-V1')"/>
<column name="sr_no" valueNumeric="4"/>
<column name="description" value="180+ Days"/>
<column name="period_type_enum" valueNumeric="0"/>
<column name="from_period" valueNumeric="180"/>
<column name="annual_interest_rate" valueNumeric="6.00"/>
<column name="senior_citizen_rate" valueNumeric="6.50"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
</changeSet>
<changeSet id="0279f_seed_fd_base_interest_rate_double" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_fd_base_interest_rate_effective WHERE fd_type = 'DOUBLE'</sqlCheck>
</preConditions>
<!-- FD DOUBLE: >= 1 year, period in YEARS -->
<insert tableName="m_fd_base_interest_rate_effective">
<column name="fd_type" value="DOUBLE"/>
<column name="version_label" value="FD-DOUBLE-V1"/>
<column name="effective_from_date" value="2025-01-01"/>
<column name="is_active" valueBoolean="true"/>
<column name="remarks" value="Initial FD Double (Long Term) rates"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
</changeSet>
<changeSet id="0279g_seed_fd_double_slabs" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_fd_base_interest_rate_slab_history sh JOIN m_fd_base_interest_rate_effective e ON sh.effective_id = e.id WHERE e.fd_type = 'DOUBLE'</sqlCheck>
</preConditions>
<!-- 1-2 years -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='DOUBLE' AND version_label='FD-DOUBLE-V1')"/>
<column name="sr_no" valueNumeric="1"/>
<column name="description" value="1 to 2 Years"/>
<column name="period_type_enum" valueNumeric="3"/>
<column name="from_period" valueNumeric="1"/>
<column name="to_period" valueNumeric="2"/>
<column name="annual_interest_rate" valueNumeric="6.50"/>
<column name="senior_citizen_rate" valueNumeric="7.00"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 3-5 years -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='DOUBLE' AND version_label='FD-DOUBLE-V1')"/>
<column name="sr_no" valueNumeric="2"/>
<column name="description" value="3 to 5 Years"/>
<column name="period_type_enum" valueNumeric="3"/>
<column name="from_period" valueNumeric="3"/>
<column name="to_period" valueNumeric="5"/>
<column name="annual_interest_rate" valueNumeric="6.75"/>
<column name="senior_citizen_rate" valueNumeric="7.25"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 6-10 years -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='DOUBLE' AND version_label='FD-DOUBLE-V1')"/>
<column name="sr_no" valueNumeric="3"/>
<column name="description" value="6 to 10 Years"/>
<column name="period_type_enum" valueNumeric="3"/>
<column name="from_period" valueNumeric="6"/>
<column name="to_period" valueNumeric="10"/>
<column name="annual_interest_rate" valueNumeric="7.00"/>
<column name="senior_citizen_rate" valueNumeric="7.50"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 11+ years -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='DOUBLE' AND version_label='FD-DOUBLE-V1')"/>
<column name="sr_no" valueNumeric="4"/>
<column name="description" value="11+ Years"/>
<column name="period_type_enum" valueNumeric="3"/>
<column name="from_period" valueNumeric="11"/>
<column name="annual_interest_rate" valueNumeric="7.25"/>
<column name="senior_citizen_rate" valueNumeric="7.75"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
</changeSet>
<changeSet id="0279h_seed_fd_base_interest_rate_special" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SPECIAL'</sqlCheck>
</preConditions>
<!-- FD SPECIAL: monthly interest payout, period in MONTHS -->
<insert tableName="m_fd_base_interest_rate_effective">
<column name="fd_type" value="SPECIAL"/>
<column name="version_label" value="FD-SPECIAL-V1"/>
<column name="effective_from_date" value="2025-01-01"/>
<column name="is_active" valueBoolean="true"/>
<column name="remarks" value="Initial FD Special (Monthly Payout) rates"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
</changeSet>
<changeSet id="0279i_seed_fd_special_slabs" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_fd_base_interest_rate_slab_history sh JOIN m_fd_base_interest_rate_effective e ON sh.effective_id = e.id WHERE e.fd_type = 'SPECIAL'</sqlCheck>
</preConditions>
<!-- 1-12 months -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SPECIAL' AND version_label='FD-SPECIAL-V1')"/>
<column name="sr_no" valueNumeric="1"/>
<column name="description" value="1 to 12 Months"/>
<column name="period_type_enum" valueNumeric="2"/>
<column name="from_period" valueNumeric="1"/>
<column name="to_period" valueNumeric="12"/>
<column name="annual_interest_rate" valueNumeric="6.00"/>
<column name="senior_citizen_rate" valueNumeric="6.50"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 13-24 months -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SPECIAL' AND version_label='FD-SPECIAL-V1')"/>
<column name="sr_no" valueNumeric="2"/>
<column name="description" value="13 to 24 Months"/>
<column name="period_type_enum" valueNumeric="2"/>
<column name="from_period" valueNumeric="13"/>
<column name="to_period" valueNumeric="24"/>
<column name="annual_interest_rate" valueNumeric="6.50"/>
<column name="senior_citizen_rate" valueNumeric="7.00"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 25-60 months -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SPECIAL' AND version_label='FD-SPECIAL-V1')"/>
<column name="sr_no" valueNumeric="3"/>
<column name="description" value="25 to 60 Months"/>
<column name="period_type_enum" valueNumeric="2"/>
<column name="from_period" valueNumeric="25"/>
<column name="to_period" valueNumeric="60"/>
<column name="annual_interest_rate" valueNumeric="7.00"/>
<column name="senior_citizen_rate" valueNumeric="7.50"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
<!-- 61+ months -->
<insert tableName="m_fd_base_interest_rate_slab_history">
<column name="effective_id" valueComputed="(SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type='SPECIAL' AND version_label='FD-SPECIAL-V1')"/>
<column name="sr_no" valueNumeric="4"/>
<column name="description" value="61+ Months"/>
<column name="period_type_enum" valueNumeric="2"/>
<column name="from_period" valueNumeric="61"/>
<column name="annual_interest_rate" valueNumeric="7.25"/>
<column name="senior_citizen_rate" valueNumeric="7.75"/>
<column name="created_date" valueDate="2025-01-01"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -34,7 +34,10 @@
<!-- 1. Tables that already have is_active — add is_deleted only -->
<changeSet id="0279a_is_deleted_client_address" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_address" columnName="is_deleted"/></not>
<and>
<tableExists tableName="m_client_address"/>
<not><columnExists tableName="m_client_address" columnName="is_deleted"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_address">
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
@@ -45,7 +48,10 @@
<changeSet id="0279b_is_deleted_client_nominee" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_nominee" columnName="is_deleted"/></not>
<and>
<tableExists tableName="m_client_nominee"/>
<not><columnExists tableName="m_client_nominee" columnName="is_deleted"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_nominee">
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
@@ -56,7 +62,10 @@
<changeSet id="0279c_is_deleted_client_charge" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_charge" columnName="is_deleted"/></not>
<and>
<tableExists tableName="m_client_charge"/>
<not><columnExists tableName="m_client_charge" columnName="is_deleted"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_charge">
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
@@ -68,7 +77,10 @@
<!-- 2. Tables that have neither — add both -->
<changeSet id="0279d_flags_client_non_person" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_non_person" columnName="is_active"/></not>
<and>
<tableExists tableName="m_client_non_person"/>
<not><columnExists tableName="m_client_non_person" columnName="is_active"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_non_person">
<column name="is_active" type="boolean" defaultValueBoolean="true">
@@ -82,7 +94,10 @@
<changeSet id="0279e_flags_client_identifier" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_identifier" columnName="is_active"/></not>
<and>
<tableExists tableName="m_client_identifier"/>
<not><columnExists tableName="m_client_identifier" columnName="is_active"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_identifier">
<column name="is_active" type="boolean" defaultValueBoolean="true">
@@ -96,7 +111,10 @@
<changeSet id="0279f_flags_client_kyc" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_kyc" columnName="is_active"/></not>
<and>
<tableExists tableName="m_client_kyc"/>
<not><columnExists tableName="m_client_kyc" columnName="is_active"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_kyc">
<column name="is_active" type="boolean" defaultValueBoolean="true">
@@ -110,7 +128,10 @@
<changeSet id="0279g_flags_address" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_address" columnName="is_active"/></not>
<and>
<tableExists tableName="m_address"/>
<not><columnExists tableName="m_address" columnName="is_active"/></not>
</and>
</preConditions>
<addColumn tableName="m_address">
<column name="is_active" type="boolean" defaultValueBoolean="true">
@@ -124,7 +145,10 @@
<changeSet id="0279h_flags_client_priority_sector" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_priority_sector" columnName="is_active"/></not>
<and>
<tableExists tableName="m_client_priority_sector"/>
<not><columnExists tableName="m_client_priority_sector" columnName="is_active"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_priority_sector">
<column name="is_active" type="boolean" defaultValueBoolean="true">
@@ -138,7 +162,10 @@
<changeSet id="0279i_flags_client_non_priority_sector" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_non_priority_sector" columnName="is_active"/></not>
<and>
<tableExists tableName="m_client_non_priority_sector"/>
<not><columnExists tableName="m_client_non_priority_sector" columnName="is_active"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_non_priority_sector">
<column name="is_active" type="boolean" defaultValueBoolean="true">
@@ -152,7 +179,10 @@
<changeSet id="0279j_flags_client_msme" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_msme" columnName="is_active"/></not>
<and>
<tableExists tableName="m_client_msme"/>
<not><columnExists tableName="m_client_msme" columnName="is_active"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_msme">
<column name="is_active" type="boolean" defaultValueBoolean="true">
@@ -166,7 +196,10 @@
<changeSet id="0279k_flags_client_mudra_borrower" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_mudra_borrower" columnName="is_active"/></not>
<and>
<tableExists tableName="m_client_mudra_borrower"/>
<not><columnExists tableName="m_client_mudra_borrower" columnName="is_active"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_mudra_borrower">
<column name="is_active" type="boolean" defaultValueBoolean="true">
@@ -180,7 +213,10 @@
<changeSet id="0279l_flags_client_startup_india" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_startup_india" columnName="is_active"/></not>
<and>
<tableExists tableName="m_client_startup_india"/>
<not><columnExists tableName="m_client_startup_india" columnName="is_active"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_startup_india">
<column name="is_active" type="boolean" defaultValueBoolean="true">
@@ -194,7 +230,10 @@
<changeSet id="0279m_flags_client_eou" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_eou" columnName="is_active"/></not>
<and>
<tableExists tableName="m_client_eou"/>
<not><columnExists tableName="m_client_eou" columnName="is_active"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_eou">
<column name="is_active" type="boolean" defaultValueBoolean="true">
@@ -208,7 +247,10 @@
<changeSet id="0279n_flags_client_charge_paid_by" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_charge_paid_by" columnName="is_active"/></not>
<and>
<tableExists tableName="m_client_charge_paid_by"/>
<not><columnExists tableName="m_client_charge_paid_by" columnName="is_active"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_charge_paid_by">
<column name="is_active" type="boolean" defaultValueBoolean="true">
@@ -222,7 +264,10 @@
<changeSet id="0279o_flags_client_collateral_management" author="fineract">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_collateral_management" columnName="is_active"/></not>
<and>
<tableExists tableName="m_client_collateral_management"/>
<not><columnExists tableName="m_client_collateral_management" columnName="is_active"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_collateral_management">
<column name="is_active" type="boolean" defaultValueBoolean="true">
@@ -28,7 +28,12 @@
-->
<changeSet id="0280a_deletion_audit_family_members" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_family_members" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_family_members"/>
<not><columnExists tableName="m_family_members" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_family_members">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -37,7 +42,12 @@
</changeSet>
<changeSet id="0280b_deletion_audit_client_address" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_address" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_address"/>
<not><columnExists tableName="m_client_address" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_address">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -46,7 +56,12 @@
</changeSet>
<changeSet id="0280c_deletion_audit_client_nominee" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_nominee" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_nominee"/>
<not><columnExists tableName="m_client_nominee" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_nominee">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -55,7 +70,12 @@
</changeSet>
<changeSet id="0280d_deletion_audit_client_charge" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_charge" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_charge"/>
<not><columnExists tableName="m_client_charge" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_charge">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -64,7 +84,12 @@
</changeSet>
<changeSet id="0280e_deletion_audit_client_non_person" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_non_person" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_non_person"/>
<not><columnExists tableName="m_client_non_person" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_non_person">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -73,7 +98,12 @@
</changeSet>
<changeSet id="0280f_deletion_audit_client_identifier" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_identifier" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_identifier"/>
<not><columnExists tableName="m_client_identifier" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_identifier">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -82,7 +112,12 @@
</changeSet>
<changeSet id="0280g_deletion_audit_client_kyc" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_kyc" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_kyc"/>
<not><columnExists tableName="m_client_kyc" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_kyc">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -91,7 +126,12 @@
</changeSet>
<changeSet id="0280h_deletion_audit_address" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_address" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_address"/>
<not><columnExists tableName="m_address" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_address">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -100,7 +140,12 @@
</changeSet>
<changeSet id="0280i_deletion_audit_client_priority_sector" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_priority_sector" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_priority_sector"/>
<not><columnExists tableName="m_client_priority_sector" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_priority_sector">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -109,7 +154,12 @@
</changeSet>
<changeSet id="0280j_deletion_audit_client_non_priority_sector" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_non_priority_sector" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_non_priority_sector"/>
<not><columnExists tableName="m_client_non_priority_sector" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_non_priority_sector">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -118,7 +168,12 @@
</changeSet>
<changeSet id="0280k_deletion_audit_client_msme" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_msme" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_msme"/>
<not><columnExists tableName="m_client_msme" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_msme">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -127,7 +182,12 @@
</changeSet>
<changeSet id="0280l_deletion_audit_client_mudra_borrower" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_mudra_borrower" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_mudra_borrower"/>
<not><columnExists tableName="m_client_mudra_borrower" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_mudra_borrower">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -136,7 +196,12 @@
</changeSet>
<changeSet id="0280m_deletion_audit_client_startup_india" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_startup_india" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_startup_india"/>
<not><columnExists tableName="m_client_startup_india" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_startup_india">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -145,7 +210,12 @@
</changeSet>
<changeSet id="0280n_deletion_audit_client_eou" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_eou" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_eou"/>
<not><columnExists tableName="m_client_eou" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_eou">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -154,7 +224,12 @@
</changeSet>
<changeSet id="0280o_deletion_audit_client_charge_paid_by" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_charge_paid_by" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_charge_paid_by"/>
<not><columnExists tableName="m_client_charge_paid_by" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_charge_paid_by">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -163,7 +238,12 @@
</changeSet>
<changeSet id="0280p_deletion_audit_client_collateral_management" author="fineract">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client_collateral_management" columnName="deleted_at"/></not></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_collateral_management"/>
<not><columnExists tableName="m_client_collateral_management" columnName="deleted_at"/></not>
</and>
</preConditions>
<addColumn tableName="m_client_collateral_management">
<column name="deleted_by" type="bigint"/>
<column name="deleted_at" type="datetime"/>
@@ -0,0 +1,53 @@
<?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">
<!-- FixedDepositProductAccountType code and values -->
<changeSet id="0280a_add_fd_product_account_type_code" author="system">
<validCheckSum>ANY</validCheckSum>
<sql>
INSERT INTO m_code (code_name, is_system_defined)
SELECT 'FixedDepositProductAccountType', false
WHERE NOT EXISTS (SELECT 1 FROM m_code WHERE code_name = 'FixedDepositProductAccountType');
</sql>
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
SELECT c.id, v.code_value, v.order_position, true, false
FROM m_code c,
(VALUES
('Fd Account', 1)
) AS v(code_value, order_position)
WHERE c.code_name = 'FixedDepositProductAccountType'
AND NOT EXISTS (
SELECT 1 FROM m_code_value cv
WHERE cv.code_id = c.id AND cv.code_value = v.code_value
);
</sql>
</changeSet>
<!-- FixedDepositProductCustomerSegment code and values -->
<changeSet id="0280b_add_fd_product_customer_segment_code" author="system">
<validCheckSum>ANY</validCheckSum>
<sql>
INSERT INTO m_code (code_name, is_system_defined)
SELECT 'FixedDepositProductCustomerSegment', false
WHERE NOT EXISTS (SELECT 1 FROM m_code WHERE code_name = 'FixedDepositProductCustomerSegment');
</sql>
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
SELECT c.id, v.code_value, v.order_position, true, false
FROM m_code c,
(VALUES
('Individual', 1),
('Non-Individual', 2)
) AS v(code_value, order_position)
WHERE c.code_name = 'FixedDepositProductCustomerSegment'
AND NOT EXISTS (
SELECT 1 FROM m_code_value cv
WHERE cv.code_id = c.id AND cv.code_value = v.code_value
);
</sql>
</changeSet>
</databaseChangeLog>
@@ -38,63 +38,108 @@
<!-- =========================== Group A: rename =========================== -->
<changeSet id="0281a_rename_audit_client_nominee" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_nominee" columnName="last_modified_by"/></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_nominee"/>
<columnExists tableName="m_client_nominee" columnName="last_modified_by"/>
</and>
</preConditions>
<renameColumn tableName="m_client_nominee" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_nominee" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_nominee" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281b_rename_audit_client_identifier" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_identifier" columnName="last_modified_by"/></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_identifier"/>
<columnExists tableName="m_client_identifier" columnName="last_modified_by"/>
</and>
</preConditions>
<renameColumn tableName="m_client_identifier" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_identifier" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_identifier" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281c_rename_audit_client_kyc" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_kyc" columnName="last_modified_by"/></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_kyc"/>
<columnExists tableName="m_client_kyc" columnName="last_modified_by"/>
</and>
</preConditions>
<renameColumn tableName="m_client_kyc" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_kyc" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_kyc" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281d_rename_audit_client_priority_sector" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_priority_sector" columnName="last_modified_by"/></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_priority_sector"/>
<columnExists tableName="m_client_priority_sector" columnName="last_modified_by"/>
</and>
</preConditions>
<renameColumn tableName="m_client_priority_sector" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_priority_sector" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_priority_sector" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281e_rename_audit_client_non_priority_sector" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_non_priority_sector" columnName="last_modified_by"/></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_non_priority_sector"/>
<columnExists tableName="m_client_non_priority_sector" columnName="last_modified_by"/>
</and>
</preConditions>
<renameColumn tableName="m_client_non_priority_sector" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_non_priority_sector" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_non_priority_sector" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281f_rename_audit_client_msme" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_msme" columnName="last_modified_by"/></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_msme"/>
<columnExists tableName="m_client_msme" columnName="last_modified_by"/>
</and>
</preConditions>
<renameColumn tableName="m_client_msme" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_msme" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_msme" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281g_rename_audit_client_mudra_borrower" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_mudra_borrower" columnName="last_modified_by"/></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_mudra_borrower"/>
<columnExists tableName="m_client_mudra_borrower" columnName="last_modified_by"/>
</and>
</preConditions>
<renameColumn tableName="m_client_mudra_borrower" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_mudra_borrower" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_mudra_borrower" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281h_rename_audit_client_startup_india" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_startup_india" columnName="last_modified_by"/></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_startup_india"/>
<columnExists tableName="m_client_startup_india" columnName="last_modified_by"/>
</and>
</preConditions>
<renameColumn tableName="m_client_startup_india" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_startup_india" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_startup_india" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
</changeSet>
<changeSet id="0281i_rename_audit_client_eou" author="fineract">
<preConditions onFail="MARK_RAN"><columnExists tableName="m_client_eou" columnName="last_modified_by"/></preConditions>
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_client_eou"/>
<columnExists tableName="m_client_eou" columnName="last_modified_by"/>
</and>
</preConditions>
<renameColumn tableName="m_client_eou" oldColumnName="last_modified_by" newColumnName="modified_by" columnDataType="bigint"/>
<renameColumn tableName="m_client_eou" oldColumnName="created_on_utc" newColumnName="created_at" columnDataType="datetime"/>
<renameColumn tableName="m_client_eou" oldColumnName="last_modified_on_utc" newColumnName="modified_on" columnDataType="datetime"/>
@@ -0,0 +1,105 @@
<?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">
<changeSet id="0281_update_fd_base_interest_rate_seed_data" author="fineract">
<validCheckSum>ANY</validCheckSum>
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_fd_base_interest_rate_effective"/>
</preConditions>
<sql>
UPDATE m_fd_base_interest_rate_slab_history
SET description = '1 to 29 Days',
from_period = 1,
to_period = 29
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SINGLE' AND version_label = 'FD-SINGLE-V1')
AND sr_no = 1;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '30 to 89 Days',
from_period = 30,
to_period = 89
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SINGLE' AND version_label = 'FD-SINGLE-V1')
AND sr_no = 2;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '90 to 179 Days',
from_period = 90,
to_period = 179
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SINGLE' AND version_label = 'FD-SINGLE-V1')
AND sr_no = 3;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '180+ Days',
from_period = 180,
to_period = NULL
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SINGLE' AND version_label = 'FD-SINGLE-V1')
AND sr_no = 4;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '1 to 2 Years',
from_period = 1,
to_period = 2
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'DOUBLE' AND version_label = 'FD-DOUBLE-V1')
AND sr_no = 1;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '3 to 5 Years',
from_period = 3,
to_period = 5
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'DOUBLE' AND version_label = 'FD-DOUBLE-V1')
AND sr_no = 2;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '6 to 10 Years',
from_period = 6,
to_period = 10
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'DOUBLE' AND version_label = 'FD-DOUBLE-V1')
AND sr_no = 3;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '11+ Years',
from_period = 11,
to_period = NULL
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'DOUBLE' AND version_label = 'FD-DOUBLE-V1')
AND sr_no = 4;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '1 to 12 Months',
from_period = 1,
to_period = 12
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SPECIAL' AND version_label = 'FD-SPECIAL-V1')
AND sr_no = 1;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '13 to 24 Months',
from_period = 13,
to_period = 24
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SPECIAL' AND version_label = 'FD-SPECIAL-V1')
AND sr_no = 2;
UPDATE m_fd_base_interest_rate_slab_history
SET description = '25 to 60 Months',
from_period = 25,
to_period = 60
WHERE effective_id = (SELECT id FROM m_fd_base_interest_rate_effective WHERE fd_type = 'SPECIAL' AND version_label = 'FD-SPECIAL-V1')
AND sr_no = 3;
INSERT INTO m_fd_base_interest_rate_slab_history
(effective_id, sr_no, description, period_type_enum, from_period, to_period, annual_interest_rate, senior_citizen_rate, created_date)
SELECT e.id, 4, '61+ Months', 2, 61, NULL, 7.250000, 7.750000, CURRENT_DATE
FROM m_fd_base_interest_rate_effective e
WHERE e.fd_type = 'SPECIAL' AND e.version_label = 'FD-SPECIAL-V1'
AND NOT EXISTS (
SELECT 1
FROM m_fd_base_interest_rate_slab_history sh
WHERE sh.effective_id = e.id
AND sh.sr_no = 4
);
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,25 @@
<?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">
<changeSet id="0284_remove_product_code_unique_constraints" author="system">
<!-- For Savings/RD/FD products -->
<dropUniqueConstraint tableName="m_savings_product" constraintName="m_savings_product_product_code_key"/>
<!-- For Current products -->
<dropUniqueConstraint tableName="m_current_product" constraintName="cp_unq_product_code"/>
<!-- Set existing empty or blank product_codes to NULL to avoid future issues -->
<update tableName="m_savings_product">
<column name="product_code" valueComputed="NULL"/>
<where>product_code = '' OR product_code IS NULL</where>
</update>
<update tableName="m_current_product">
<column name="product_code" valueComputed="NULL"/>
<where>product_code = '' OR product_code IS NULL</where>
</update>
</changeSet>
</databaseChangeLog>
@@ -1,39 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<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">
<!--
Enforce uniqueness on m_appuser.email and m_appuser.mobile_no.
Username is already unique (constraint name 'username_org' from the upstream schema).
Email is NOT NULL in the entity; mobile_no IS nullable. Both PostgreSQL and MySQL allow
multiple NULLs in a UNIQUE constraint, so a plain UNIQUE works for mobile_no — multiple
users with no mobile remain valid.
Note: if your existing data already contains duplicate emails or mobiles, this migration
WILL fail. Dedupe first:
SELECT LOWER(email), COUNT(*) FROM m_appuser GROUP BY LOWER(email) HAVING COUNT(*) > 1;
SELECT mobile_no, COUNT(*) FROM m_appuser WHERE mobile_no IS NOT NULL GROUP BY mobile_no HAVING COUNT(*) > 1;
-->
<changeSet id="0295_add_unique_email_to_m_appuser" author="fineract">
<preConditions onFail="MARK_RAN">
<not>
<indexExists tableName="m_appuser" indexName="uq_m_appuser_email"/>
</not>
<sqlCheck expectedResult="0">
SELECT COUNT(*)
FROM (
SELECT LOWER(email)
FROM m_appuser
WHERE email IS NOT NULL
GROUP BY LOWER(email)
HAVING COUNT(*) &gt; 1
) t
</sqlCheck>
</preConditions>
<addUniqueConstraint tableName="m_appuser" columnNames="email"
constraintName="uq_m_appuser_email"/>
@@ -44,8 +29,17 @@
<not>
<indexExists tableName="m_appuser" indexName="uq_m_appuser_mobile_no"/>
</not>
<sqlCheck expectedResult="0">
SELECT COUNT(*)
FROM (
SELECT mobile_no
FROM m_appuser
WHERE mobile_no IS NOT NULL
GROUP BY mobile_no
HAVING COUNT(*) &gt; 1
) t
</sqlCheck>
</preConditions>
<!-- Drop the plain non-unique index added in 0283 first — the unique constraint replaces it. -->
<dropIndex tableName="m_appuser" indexName="idx_m_appuser_mobile_no"/>
<addUniqueConstraint tableName="m_appuser" columnNames="mobile_no"
constraintName="uq_m_appuser_mobile_no"/>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
-->
<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">
<!--
Fix: previous constraint uq_client_nominee_per_account_type was on
(client_id, nominee_client_id, account_type) — this incorrectly blocked the
same nominee from being added to multiple accounts of the same type
(e.g. two SB accounts). The correct uniqueness is per individual account:
(client_id, nominee_client_id, account_no, account_type).
-->
<changeSet id="0297_fix_unique_nominee_per_account" author="fineract" dbms="postgresql">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_client_nominee"/>
</preConditions>
<sql>
DROP INDEX IF EXISTS uq_client_nominee_per_account_type;
CREATE UNIQUE INDEX IF NOT EXISTS uq_client_nominee_per_account
ON m_client_nominee (client_id, nominee_client_id, account_no, account_type)
WHERE is_deleted = false;
</sql>
<rollback>
<sql>
DROP INDEX IF EXISTS uq_client_nominee_per_account;
CREATE UNIQUE INDEX IF NOT EXISTS uq_client_nominee_per_account_type
ON m_client_nominee (client_id, nominee_client_id, account_type)
WHERE is_deleted = false;
</sql>
</rollback>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,18 @@
<?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">
<changeSet id="0298_unique_external_id_on_current_account" author="CBS">
<preConditions onFail="MARK_RAN">
<tableExists tableName="m_current_account"/>
<not><indexExists tableName="m_current_account" indexName="ca_external_id_UNIQUE"/></not>
<not><indexExists tableName="m_current_account" indexName="ca_unq_external_id"/></not>
</preConditions>
<addUniqueConstraint
tableName="m_current_account"
columnNames="external_id"
constraintName="ca_external_id_UNIQUE"/>
</changeSet>
</databaseChangeLog>
@@ -45,6 +45,7 @@ import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccount;
import org.apache.fineract.portfolio.shareproducts.domain.ShareProduct;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
public class AccountNumberGeneratorTest {
@@ -54,6 +55,7 @@ public class AccountNumberGeneratorTest {
private SavingsAccountRepository savingsRepo;
private GLAccountRepository glAccountRepo;
private SavingsProductRepository savingsProductRepo;
private JdbcTemplate jdbcTemplate;
private AccountNumberGenerator generator;
@@ -65,8 +67,9 @@ public class AccountNumberGeneratorTest {
savingsRepo = mock(SavingsAccountRepository.class);
glAccountRepo = mock(GLAccountRepository.class);
savingsProductRepo = mock(SavingsProductRepository.class);
jdbcTemplate = mock(JdbcTemplate.class);
generator = new AccountNumberGenerator(configService, clientRepo, loanRepo, savingsRepo, glAccountRepo, savingsProductRepo);
generator = new AccountNumberGenerator(configService, clientRepo, loanRepo, savingsRepo, glAccountRepo, savingsProductRepo, jdbcTemplate);
GlobalConfigurationPropertyData accountLengthConfig = mock(GlobalConfigurationPropertyData.class);
when(accountLengthConfig.getValue()).thenReturn(Long.valueOf("9"));
@@ -46,6 +46,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
@@ -265,6 +266,11 @@ public class InterestRateChart extends AbstractPersistableCustom<Long> {
chartSlab.update(chartSlabsCommand, chartSlabsChanges, baseDataValidator, locale);
}
} else {
final InterestRateChartSlab matchingChartSlab = this.findChartSlabByRanges(chartSlabsCommand, locale);
if (matchingChartSlab != null) {
matchingChartSlab.update(chartSlabsCommand, chartSlabsChanges, baseDataValidator, locale);
continue;
}
/**
* TODO: AA: Move this code to InterestRateChartSlabAssembler
@@ -321,6 +327,24 @@ public class InterestRateChart extends AbstractPersistableCustom<Long> {
return chartSlabs.remove(chartSlab);
}
private InterestRateChartSlab findChartSlabByRanges(final JsonCommand chartSlabsCommand, final Locale locale) {
final Integer fromPeriod = chartSlabsCommand.integerValueOfParameterNamed(fromPeriodParamName, locale);
final Integer toPeriod = chartSlabsCommand.integerValueOfParameterNamed(toPeriodParamName, locale);
final BigDecimal amountRangeFrom = chartSlabsCommand.bigDecimalValueOfParameterNamed(amountRangeFromParamName, locale);
final BigDecimal amountRangeTo = chartSlabsCommand.bigDecimalValueOfParameterNamed(amountRangeToParamName, locale);
for (InterestRateChartSlab chartSlab : setOfChartSlabs()) {
final InterestRateChartSlabFields slabFields = chartSlab.slabFields();
if (Objects.equals(slabFields.fromPeriod(), fromPeriod) && Objects.equals(slabFields.toPeriod(), toPeriod)
&& slabFields.isBigDecimalSame(slabFields.getAmountRangeFrom(), amountRangeFrom)
&& slabFields.isBigDecimalSame(slabFields.getAmountRangeTo(), amountRangeTo)) {
return chartSlab;
}
}
return null;
}
public LocalDate getFromDate() {
return this.chartFields.getFromDate();
}
@@ -24,6 +24,7 @@ import java.util.Collection;
import lombok.Getter;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.staff.data.StaffData;
@@ -73,6 +74,48 @@ public final class FixedDepositAccountData extends DepositAccountData {
private Collection<PaymentTypeData> paymentTypeOptions;
private final Collection<EnumOptionData> maturityInstructionOptions;
// CBS product/account fields used by the FD UI
private String accountType;
private String customerSegment;
private String fdType;
private Collection<CodeValueData> accountTypeOptions;
private Collection<CodeValueData> customerSegmentOptions;
private String productCode;
private LocalDate effectiveDate;
private LocalDate effectiveTo;
private LocalDate endDate;
private Boolean introducerRequired;
private Boolean shareLinkingEnabled;
private Boolean nominationFacility;
private Boolean dormancyAlert;
private Boolean isDormancyTrackingActive;
private Long daysToInactive;
private Long daysToDormancy;
private Long daysToEscheat;
private Integer interestCalculateSelector;
private String interestCalculationTypeName;
private String averagePeriod;
private Boolean autoPost;
private Integer frequencyFixDay;
private LocalDate lastInterestDate;
private Boolean enableLockinPeriod;
private String balanceFrequency;
private Boolean accountClosedZeroBalanceInDayClosing;
private Boolean balanceSwapOption;
private Boolean msme;
private Boolean autoDebitFacility;
private Boolean loanFacility;
private Boolean prematureWithdrawalOption;
private Boolean tenureFlexibility;
private Boolean lienMarked;
private Boolean maturityInstructions;
private Boolean creditMaturityProceeds;
private Boolean penaltyGracePeriodMissedInstallments;
private Boolean panelRequestApplicable;
private Boolean noPenaltyNomineeHeirClosure;
private Boolean deceasedSettlement;
private Boolean allowDebitTxnAfterDueDate;
// import fields
private transient Integer rowIndex;
private String dateFormat;
@@ -92,6 +135,123 @@ public final class FixedDepositAccountData extends DepositAccountData {
locale, dateFormat);
}
public void setAccountType(String accountType) {
this.accountType = accountType;
}
public void setCustomerSegment(String customerSegment) {
this.customerSegment = customerSegment;
}
public void setAccountTypeOptions(Collection<CodeValueData> accountTypeOptions) {
this.accountTypeOptions = accountTypeOptions;
}
public void setCustomerSegmentOptions(Collection<CodeValueData> customerSegmentOptions) {
this.customerSegmentOptions = customerSegmentOptions;
}
public void setFdType(String fdType) { this.fdType = fdType; }
public String getFdType() { return fdType; }
public void setCbsProductFields(final String accountType, final String customerSegment, final String fdType, final String productCode,
final LocalDate effectiveDate, final LocalDate effectiveTo, final LocalDate endDate, final Boolean introducerRequired,
final Boolean shareLinkingEnabled, final Boolean nominationFacility, final Boolean dormancyAlert,
final Boolean isDormancyTrackingActive, final Long daysToInactive, final Long daysToDormancy, final Long daysToEscheat,
final Integer interestCalculateSelector, final String interestCalculationTypeName, final String averagePeriod,
final Boolean autoPost, final Integer frequencyFixDay, final LocalDate lastInterestDate, final Boolean enableLockinPeriod,
final String balanceFrequency, final Boolean accountClosedZeroBalanceInDayClosing, final Boolean balanceSwapOption,
final Boolean msme, final Boolean autoDebitFacility, final Boolean loanFacility, final Boolean prematureWithdrawalOption,
final Boolean tenureFlexibility, final Boolean lienMarked, final Boolean maturityInstructions,
final Boolean creditMaturityProceeds, final Boolean penaltyGracePeriodMissedInstallments,
final Boolean panelRequestApplicable, final Boolean noPenaltyNomineeHeirClosure, final Boolean deceasedSettlement,
final Boolean allowDebitTxnAfterDueDate) {
this.accountType = accountType;
this.customerSegment = customerSegment;
this.fdType = fdType;
this.productCode = productCode;
this.effectiveDate = effectiveDate;
this.effectiveTo = effectiveTo;
this.endDate = endDate;
this.introducerRequired = introducerRequired;
this.shareLinkingEnabled = shareLinkingEnabled;
this.nominationFacility = nominationFacility;
this.dormancyAlert = dormancyAlert;
this.isDormancyTrackingActive = isDormancyTrackingActive;
this.daysToInactive = daysToInactive;
this.daysToDormancy = daysToDormancy;
this.daysToEscheat = daysToEscheat;
this.interestCalculateSelector = interestCalculateSelector;
this.interestCalculationTypeName = interestCalculationTypeName;
this.averagePeriod = averagePeriod;
this.autoPost = autoPost;
this.frequencyFixDay = frequencyFixDay;
this.lastInterestDate = lastInterestDate;
this.enableLockinPeriod = enableLockinPeriod;
this.balanceFrequency = balanceFrequency;
this.accountClosedZeroBalanceInDayClosing = accountClosedZeroBalanceInDayClosing;
this.balanceSwapOption = balanceSwapOption;
this.msme = msme;
this.autoDebitFacility = autoDebitFacility;
this.loanFacility = loanFacility;
this.prematureWithdrawalOption = prematureWithdrawalOption;
this.tenureFlexibility = tenureFlexibility;
this.lienMarked = lienMarked;
this.maturityInstructions = maturityInstructions;
this.creditMaturityProceeds = creditMaturityProceeds;
this.penaltyGracePeriodMissedInstallments = penaltyGracePeriodMissedInstallments;
this.panelRequestApplicable = panelRequestApplicable;
this.noPenaltyNomineeHeirClosure = noPenaltyNomineeHeirClosure;
this.deceasedSettlement = deceasedSettlement;
this.allowDebitTxnAfterDueDate = allowDebitTxnAfterDueDate;
}
public void copyCbsFieldsFrom(final FixedDepositAccountData src) {
if (src == null) {
return;
}
this.accountType = src.accountType;
this.customerSegment = src.customerSegment;
this.fdType = src.fdType;
this.accountTypeOptions = src.accountTypeOptions;
this.customerSegmentOptions = src.customerSegmentOptions;
this.productCode = src.productCode;
this.effectiveDate = src.effectiveDate;
this.effectiveTo = src.effectiveTo;
this.endDate = src.endDate;
this.introducerRequired = src.introducerRequired;
this.shareLinkingEnabled = src.shareLinkingEnabled;
this.nominationFacility = src.nominationFacility;
this.dormancyAlert = src.dormancyAlert;
this.isDormancyTrackingActive = src.isDormancyTrackingActive;
this.daysToInactive = src.daysToInactive;
this.daysToDormancy = src.daysToDormancy;
this.daysToEscheat = src.daysToEscheat;
this.interestCalculateSelector = src.interestCalculateSelector;
this.interestCalculationTypeName = src.interestCalculationTypeName;
this.averagePeriod = src.averagePeriod;
this.autoPost = src.autoPost;
this.frequencyFixDay = src.frequencyFixDay;
this.lastInterestDate = src.lastInterestDate;
this.enableLockinPeriod = src.enableLockinPeriod;
this.balanceFrequency = src.balanceFrequency;
this.accountClosedZeroBalanceInDayClosing = src.accountClosedZeroBalanceInDayClosing;
this.balanceSwapOption = src.balanceSwapOption;
this.msme = src.msme;
this.autoDebitFacility = src.autoDebitFacility;
this.loanFacility = src.loanFacility;
this.prematureWithdrawalOption = src.prematureWithdrawalOption;
this.tenureFlexibility = src.tenureFlexibility;
this.lienMarked = src.lienMarked;
this.maturityInstructions = src.maturityInstructions;
this.creditMaturityProceeds = src.creditMaturityProceeds;
this.penaltyGracePeriodMissedInstallments = src.penaltyGracePeriodMissedInstallments;
this.panelRequestApplicable = src.panelRequestApplicable;
this.noPenaltyNomineeHeirClosure = src.noPenaltyNomineeHeirClosure;
this.deceasedSettlement = src.deceasedSettlement;
this.allowDebitTxnAfterDueDate = src.allowDebitTxnAfterDueDate;
}
private FixedDepositAccountData(Long clientId, Long productId, Long fieldofficerId, LocalDate submittedOnDate,
EnumOptionData interestCompoundingPeriodType, EnumOptionData interestPostingPeriodType, EnumOptionData interestCalculationType,
EnumOptionData interestCalculationDaysInYearType, Integer lockinPeriodFrequency, EnumOptionData lockinPeriodFrequencyType,
@@ -150,7 +310,7 @@ public final class FixedDepositAccountData extends DepositAccountData {
final Collection<PaymentTypeData> paymentTypeOptions = null;
final Collection<SavingsAccountData> savingsAccountDatas = null;
return new FixedDepositAccountData(depositAccountData.id, depositAccountData.accountNo, depositAccountData.externalId,
final FixedDepositAccountData result = new FixedDepositAccountData(depositAccountData.id, depositAccountData.accountNo, depositAccountData.externalId,
depositAccountData.groupId, depositAccountData.groupName, depositAccountData.clientId, depositAccountData.clientName,
depositAccountData.depositProductId, depositAccountData.depositProductName, depositAccountData.fieldOfficerId,
depositAccountData.fieldOfficerName, depositAccountData.status, depositAccountData.timeline, depositAccountData.currency,
@@ -170,11 +330,15 @@ public final class FixedDepositAccountData extends DepositAccountData {
depositPeriod, depositPeriodFrequency, periodFrequencyTypeOptions, depositType, onAccountClosure, onAccountClosureOptions,
paymentTypeOptions, savingsAccountDatas, linkedAccount, transferInterestToSavings, depositAccountData.withHoldTax,
depositAccountData.taxGroup, maturityInstructionOptions, transferToSavingsId, transferToSavingsAccount);
if (depositAccountData instanceof FixedDepositAccountData) {
result.copyCbsFieldsFrom((FixedDepositAccountData) depositAccountData);
}
return result;
}
public static FixedDepositAccountData withInterestChart(final FixedDepositAccountData account,
final DepositAccountInterestRateChartData accountChart) {
return new FixedDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
final FixedDepositAccountData result = new FixedDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId,
account.fieldOfficerName, account.status, account.timeline, account.currency, account.nominalAnnualInterestRate,
account.interestCompoundingPeriodType, account.interestPostingPeriodType, account.interestCalculationType,
@@ -192,6 +356,8 @@ public final class FixedDepositAccountData extends DepositAccountData {
account.onAccountClosure, account.onAccountClosureOptions, account.paymentTypeOptions, account.savingsAccounts,
account.linkedAccount, account.transferInterestToSavings, account.withHoldTax, account.taxGroup,
account.maturityInstructionOptions, account.transferToSavingsId, account.transferToSavingsAccount);
result.copyCbsFieldsFrom(account);
return result;
}
public static FixedDepositAccountData associationsAndTemplate(final FixedDepositAccountData account, FixedDepositAccountData template,
@@ -202,7 +368,7 @@ public final class FixedDepositAccountData extends DepositAccountData {
template = account;
}
return new FixedDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
final FixedDepositAccountData result = new FixedDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId,
account.fieldOfficerName, account.status, account.timeline, account.currency, account.nominalAnnualInterestRate,
account.interestCompoundingPeriodType, account.interestPostingPeriodType, account.interestCalculationType,
@@ -220,6 +386,8 @@ public final class FixedDepositAccountData extends DepositAccountData {
account.onAccountClosure, account.onAccountClosureOptions, account.paymentTypeOptions, template.savingsAccounts,
linkedAccount, account.transferInterestToSavings, account.withHoldTax, account.taxGroup, account.maturityInstructionOptions,
account.transferToSavingsId, transferToSavingsAccount);
result.copyCbsFieldsFrom(account);
return result;
}
public static FixedDepositAccountData withTemplateOptions(final FixedDepositAccountData account,
@@ -234,7 +402,7 @@ public final class FixedDepositAccountData extends DepositAccountData {
final Collection<EnumOptionData> periodFrequencyTypeOptions, final Collection<SavingsAccountData> savingsAccounts,
final Collection<EnumOptionData> maturityInstructionOptions) {
return new FixedDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
final FixedDepositAccountData result = new FixedDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId,
account.fieldOfficerName, account.status, account.timeline, account.currency, account.nominalAnnualInterestRate,
account.interestCompoundingPeriodType, account.interestPostingPeriodType, account.interestCalculationType,
@@ -251,6 +419,8 @@ public final class FixedDepositAccountData extends DepositAccountData {
account.onAccountClosure, account.onAccountClosureOptions, account.paymentTypeOptions, savingsAccounts,
account.linkedAccount, account.transferInterestToSavings, account.withHoldTax, account.taxGroup, maturityInstructionOptions,
account.transferToSavingsId, account.transferToSavingsAccount);
result.copyCbsFieldsFrom(account);
return result;
}
public static FixedDepositAccountData withClientTemplate(final Long clientId, final String clientName, final Long groupId,
@@ -427,7 +597,7 @@ public final class FixedDepositAccountData extends DepositAccountData {
final Collection<EnumOptionData> onAccountClosureOptions, final Collection<PaymentTypeData> paymentTypeOptions,
final Collection<SavingsAccountData> savingsAccountDatas) {
return new FixedDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
final FixedDepositAccountData result = new FixedDepositAccountData(account.id, account.accountNo, account.externalId, account.groupId, account.groupName,
account.clientId, account.clientName, account.depositProductId, account.depositProductName, account.fieldOfficerId,
account.fieldOfficerName, account.status, account.timeline, account.currency, account.nominalAnnualInterestRate,
account.interestCompoundingPeriodType, account.interestPostingPeriodType, account.interestCalculationType,
@@ -445,7 +615,8 @@ public final class FixedDepositAccountData extends DepositAccountData {
account.onAccountClosure, onAccountClosureOptions, paymentTypeOptions, savingsAccountDatas, account.linkedAccount,
account.transferInterestToSavings, account.withHoldTax, account.taxGroup, account.maturityInstructionOptions,
account.transferToSavingsId, account.transferToSavingsAccount);
result.copyCbsFieldsFrom(account);
return result;
}
private FixedDepositAccountData(final Long id, final String accountNo, final String externalId, final Long groupId,
@@ -112,9 +112,9 @@ public final class FixedDepositProductData extends DepositProductData {
}
public static FixedDepositProductData withCharges(final FixedDepositProductData existingProduct, final Collection<ChargeData> charges) {
return new FixedDepositProductData(existingProduct.id, existingProduct.name, existingProduct.shortName, existingProduct.description,
existingProduct.currency, existingProduct.nominalAnnualInterestRate, existingProduct.interestCompoundingPeriodType,
existingProduct.interestPostingPeriodType, existingProduct.interestCalculationType,
final FixedDepositProductData result = new FixedDepositProductData(existingProduct.id, existingProduct.name, existingProduct.shortName,
existingProduct.description, existingProduct.currency, existingProduct.nominalAnnualInterestRate,
existingProduct.interestCompoundingPeriodType, existingProduct.interestPostingPeriodType, existingProduct.interestCalculationType,
existingProduct.interestCalculationDaysInYearType, existingProduct.lockinPeriodFrequency,
existingProduct.lockinPeriodFrequencyType, existingProduct.minBalanceForInterestCalculation, existingProduct.accountingRule,
existingProduct.accountingMappings, existingProduct.paymentChannelToFundSourceMappings, existingProduct.currencyOptions,
@@ -131,6 +131,8 @@ public final class FixedDepositProductData extends DepositProductData {
existingProduct.minDepositAmount, existingProduct.depositAmount, existingProduct.maxDepositAmount,
existingProduct.periodFrequencyTypeOptions, existingProduct.withHoldTax, existingProduct.taxGroup,
existingProduct.taxGroupOptions);
result.copyCbsFieldsFrom(existingProduct);
return result;
}
/**
@@ -152,9 +154,9 @@ public final class FixedDepositProductData extends DepositProductData {
final Collection<EnumOptionData> preClosurePenalInterestOnTypeOptions,
final Collection<EnumOptionData> periodFrequencyTypeOptions, final Collection<TaxGroupData> taxGroupOptions) {
return new FixedDepositProductData(existingProduct.id, existingProduct.name, existingProduct.shortName, existingProduct.description,
existingProduct.currency, existingProduct.nominalAnnualInterestRate, existingProduct.interestCompoundingPeriodType,
existingProduct.interestPostingPeriodType, existingProduct.interestCalculationType,
final FixedDepositProductData result = new FixedDepositProductData(existingProduct.id, existingProduct.name, existingProduct.shortName,
existingProduct.description, existingProduct.currency, existingProduct.nominalAnnualInterestRate,
existingProduct.interestCompoundingPeriodType, existingProduct.interestPostingPeriodType, existingProduct.interestCalculationType,
existingProduct.interestCalculationDaysInYearType, existingProduct.lockinPeriodFrequency,
existingProduct.lockinPeriodFrequencyType, existingProduct.minBalanceForInterestCalculation, existingProduct.accountingRule,
existingProduct.accountingMappings, existingProduct.paymentChannelToFundSourceMappings, currencyOptions,
@@ -169,6 +171,8 @@ public final class FixedDepositProductData extends DepositProductData {
existingProduct.inMultiplesOfDepositTermType, existingProduct.minDepositAmount, existingProduct.depositAmount,
existingProduct.maxDepositAmount, periodFrequencyTypeOptions, existingProduct.withHoldTax, existingProduct.taxGroup,
taxGroupOptions);
result.copyCbsFieldsFrom(existingProduct);
return result;
}
public static FixedDepositProductData withAccountingDetails(final FixedDepositProductData existingProduct,
@@ -189,9 +193,9 @@ public final class FixedDepositProductData extends DepositProductData {
final Collection<ChargeData> chargeOptions = null;
final Collection<ChargeData> penaltyOptions = null;
return new FixedDepositProductData(existingProduct.id, existingProduct.name, existingProduct.shortName, existingProduct.description,
existingProduct.currency, existingProduct.nominalAnnualInterestRate, existingProduct.interestCompoundingPeriodType,
existingProduct.interestPostingPeriodType, existingProduct.interestCalculationType,
final FixedDepositProductData result = new FixedDepositProductData(existingProduct.id, existingProduct.name,
existingProduct.shortName, existingProduct.description, existingProduct.currency, existingProduct.nominalAnnualInterestRate,
existingProduct.interestCompoundingPeriodType, existingProduct.interestPostingPeriodType, existingProduct.interestCalculationType,
existingProduct.interestCalculationDaysInYearType, existingProduct.lockinPeriodFrequency,
existingProduct.lockinPeriodFrequencyType, existingProduct.minBalanceForInterestCalculation, existingProduct.accountingRule,
accountingMappings, paymentChannelToFundSourceMappings, currencyOptions, interestCompoundingPeriodTypeOptions,
@@ -206,6 +210,8 @@ public final class FixedDepositProductData extends DepositProductData {
existingProduct.minDepositAmount, existingProduct.depositAmount, existingProduct.maxDepositAmount,
existingProduct.periodFrequencyTypeOptions, existingProduct.withHoldTax, existingProduct.taxGroup,
existingProduct.taxGroupOptions);
result.copyCbsFieldsFrom(existingProduct);
return result;
}
public static FixedDepositProductData instance(final DepositProductData depositProductData, final boolean preClosurePenalApplicable,
@@ -243,7 +249,7 @@ public final class FixedDepositProductData extends DepositProductData {
depositProductData.interestCompoundingPeriodType, depositProductData.interestPostingPeriodType,
depositProductData.interestCalculationType, depositProductData.interestCalculationDaysInYearType,
depositProductData.lockinPeriodFrequency, depositProductData.lockinPeriodFrequencyType,
depositProductData.minBalanceForInterestCalculation, depositProductData.accountingRule, accountingMappings,
depositProductData.getMinBalanceForInterestCalculation(), depositProductData.accountingRule, accountingMappings,
paymentChannelToFundSourceMappings, currencyOptions, interestCompoundingPeriodTypeOptions, interestPostingPeriodTypeOptions,
interestCalculationTypeOptions, interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions,
withdrawalFeeTypeOptions, paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, charges, chargeOptions,
@@ -321,9 +327,9 @@ public final class FixedDepositProductData extends DepositProductData {
public static FixedDepositProductData withInterestChart(final FixedDepositProductData existingProduct,
final Collection<InterestRateChartData> interestRateCharts) {
return new FixedDepositProductData(existingProduct.id, existingProduct.name, existingProduct.shortName, existingProduct.description,
existingProduct.currency, existingProduct.nominalAnnualInterestRate, existingProduct.interestCompoundingPeriodType,
existingProduct.interestPostingPeriodType, existingProduct.interestCalculationType,
final FixedDepositProductData result = new FixedDepositProductData(existingProduct.id, existingProduct.name,
existingProduct.shortName, existingProduct.description, existingProduct.currency, existingProduct.nominalAnnualInterestRate,
existingProduct.interestCompoundingPeriodType, existingProduct.interestPostingPeriodType, existingProduct.interestCalculationType,
existingProduct.interestCalculationDaysInYearType, existingProduct.lockinPeriodFrequency,
existingProduct.lockinPeriodFrequencyType, existingProduct.minBalanceForInterestCalculation, existingProduct.accountingRule,
existingProduct.accountingMappings, existingProduct.paymentChannelToFundSourceMappings, existingProduct.currencyOptions,
@@ -340,7 +346,8 @@ public final class FixedDepositProductData extends DepositProductData {
existingProduct.minDepositAmount, existingProduct.depositAmount, existingProduct.maxDepositAmount,
existingProduct.periodFrequencyTypeOptions, existingProduct.withHoldTax, existingProduct.taxGroup,
existingProduct.taxGroupOptions);
result.copyCbsFieldsFrom(existingProduct);
return result;
}
private FixedDepositProductData(final Long id, final String name, final String shortName, final String description,
@@ -445,4 +452,160 @@ public final class FixedDepositProductData extends DepositProductData {
return preClosurePenalApplicable;
}
// CBS Custom Fields
private Collection<org.apache.fineract.infrastructure.core.data.EnumOptionData> interestCalculationTypeOption;
private Collection<org.apache.fineract.infrastructure.core.data.EnumOptionData> periodFrequencyOptions;
private java.util.List<org.apache.fineract.infrastructure.codes.data.CodeValueData> yesNoOptions;
private java.util.List<org.apache.fineract.infrastructure.codes.data.CodeValueData> accountTypeOptions;
private java.util.List<org.apache.fineract.infrastructure.codes.data.CodeValueData> customerSegmentOptions;
private String productCode;
private String accountType;
private String fdType;
private Long glAccountId;
private String glAccountName;
private String customerSegment;
private Boolean introducerRequired;
private Boolean shareLinkingEnabled;
private Boolean nominationFacility;
private Boolean dormancyAlert;
private Boolean isDormancyTrackingActive;
private Long daysToInactive;
private Long daysToDormancy;
private Long daysToEscheat;
private String effectiveDate;
private String effectiveTo;
private String endDate;
private Integer interestCalculateSelector;
private String averagePeriod;
private Boolean autoPost;
private Integer frequencyFixDay;
private String lastInterestDate;
private Boolean enableLockinPeriod;
private Boolean autoDebitFacility;
private Boolean loanFacility;
private Boolean prematureWithdrawalOption;
private Boolean tenureFlexibility;
private Boolean lienMarked;
private Boolean maturityInstructions;
private Boolean creditMaturityProceeds;
private Boolean penaltyGracePeriodMissedInstallments;
private Boolean panelRequestApplicable;
private Boolean noPenaltyNomineeHeirClosure;
private Boolean deceasedSettlement;
private Boolean allowDebitTxnAfterDueDate;
private Boolean accountClosedZeroBalanceInDayClosing;
private String balanceFrequency;
// Template option setters
public void setInterestCalculationTypeOption(Collection<org.apache.fineract.infrastructure.core.data.EnumOptionData> v) { this.interestCalculationTypeOption = v; }
public void setPeriodFrequencyOptions(Collection<org.apache.fineract.infrastructure.core.data.EnumOptionData> v) { this.periodFrequencyOptions = v; }
public void setYesNoOptions(java.util.List<org.apache.fineract.infrastructure.codes.data.CodeValueData> v) { this.yesNoOptions = v; }
public void setAccountTypeOptions(java.util.List<org.apache.fineract.infrastructure.codes.data.CodeValueData> v) { this.accountTypeOptions = v; }
public void setCustomerSegmentOptions(java.util.List<org.apache.fineract.infrastructure.codes.data.CodeValueData> v) { this.customerSegmentOptions = v; }
// CBS field setters
public void setProductCode(String v) { this.productCode = v; }
public void setAccountType(String v) { this.accountType = v; }
public void setFdType(String v) { this.fdType = v; }
public void setGlAccountId(Long v) { this.glAccountId = v; }
public void setGlAccountName(String v) { this.glAccountName = v; }
public void setCustomerSegment(String v) { this.customerSegment = v; }
public void setIntroducerRequired(Boolean v) { this.introducerRequired = v; }
public void setShareLinkingEnabled(Boolean v) { this.shareLinkingEnabled = v; }
public void setNominationFacility(Boolean v) { this.nominationFacility = v; }
public void setDormancyAlert(Boolean v) { this.dormancyAlert = v; }
public void setIsDormancyTrackingActive(Boolean v) { this.isDormancyTrackingActive = v; }
public void setDaysToInactive(Long v) { this.daysToInactive = v; }
public void setDaysToDormancy(Long v) { this.daysToDormancy = v; }
public void setDaysToEscheat(Long v) { this.daysToEscheat = v; }
public void setEffectiveDate(String v) { this.effectiveDate = v; }
public void setEffectiveTo(String v) { this.effectiveTo = v; }
public void setEndDate(String v) { this.endDate = v; }
public void setInterestCalculateSelector(Integer v) { this.interestCalculateSelector = v; }
public void setAveragePeriod(String v) { this.averagePeriod = v; }
public void setAutoPost(Boolean v) { this.autoPost = v; }
public void setFrequencyFixDay(Integer v) { this.frequencyFixDay = v; }
public void setLastInterestDate(String v) { this.lastInterestDate = v; }
public void setEnableLockinPeriod(Boolean v) { this.enableLockinPeriod = v; }
public void setAutoDebitFacility(Boolean v) { this.autoDebitFacility = v; }
public void setLoanFacility(Boolean v) { this.loanFacility = v; }
public void setPrematureWithdrawalOption(Boolean v) { this.prematureWithdrawalOption = v; }
public void setTenureFlexibility(Boolean v) { this.tenureFlexibility = v; }
public void setLienMarked(Boolean v) { this.lienMarked = v; }
public void setMaturityInstructions(Boolean v) { this.maturityInstructions = v; }
public void setCreditMaturityProceeds(Boolean v) { this.creditMaturityProceeds = v; }
public void setPenaltyGracePeriodMissedInstallments(Boolean v) { this.penaltyGracePeriodMissedInstallments = v; }
public void setPanelRequestApplicable(Boolean v) { this.panelRequestApplicable = v; }
public void setNoPenaltyNomineeHeirClosure(Boolean v) { this.noPenaltyNomineeHeirClosure = v; }
public void setDeceasedSettlement(Boolean v) { this.deceasedSettlement = v; }
public void setAllowDebitTxnAfterDueDate(Boolean v) { this.allowDebitTxnAfterDueDate = v; }
public void setAccountClosedZeroBalanceInDayClosing(Boolean v) { this.accountClosedZeroBalanceInDayClosing = v; }
public void setBalanceFrequency(String v) { this.balanceFrequency = v; }
// CBS field getters
public String getProductCode() { return productCode; }
public String getAccountType() { return accountType; }
public String getFdType() { return fdType; }
public Long getGlAccountId() { return glAccountId; }
public String getGlAccountName() { return glAccountName; }
public String getCustomerSegment() { return customerSegment; }
public Boolean getIntroducerRequired() { return introducerRequired; }
public Boolean getShareLinkingEnabled() { return shareLinkingEnabled; }
public Boolean getNominationFacility() { return nominationFacility; }
public Boolean getDormancyAlert() { return dormancyAlert; }
public Boolean getIsDormancyTrackingActive() { return isDormancyTrackingActive; }
public Long getDaysToInactive() { return daysToInactive; }
public Long getDaysToDormancy() { return daysToDormancy; }
public Long getDaysToEscheat() { return daysToEscheat; }
public void copyCbsFieldsFrom(FixedDepositProductData src) {
if (src == null) return;
this.accountType = src.accountType;
this.fdType = src.fdType;
this.customerSegmentOptions = src.customerSegmentOptions;
this.productCode = src.productCode;
this.glAccountId = src.glAccountId;
this.glAccountName = src.glAccountName;
this.interestCalculationTypeOption = src.interestCalculationTypeOption;
this.periodFrequencyOptions = src.periodFrequencyOptions;
this.yesNoOptions = src.yesNoOptions;
this.accountTypeOptions = src.accountTypeOptions;
this.customerSegment = src.customerSegment;
this.introducerRequired = src.introducerRequired;
this.shareLinkingEnabled = src.shareLinkingEnabled;
this.nominationFacility = src.nominationFacility;
this.dormancyAlert = src.dormancyAlert;
this.isDormancyTrackingActive = src.isDormancyTrackingActive;
this.daysToInactive = src.daysToInactive;
this.daysToDormancy = src.daysToDormancy;
this.daysToEscheat = src.daysToEscheat;
this.effectiveDate = src.effectiveDate;
this.effectiveTo = src.effectiveTo;
this.endDate = src.endDate;
this.interestCalculateSelector = src.interestCalculateSelector;
this.averagePeriod = src.averagePeriod;
this.autoPost = src.autoPost;
this.frequencyFixDay = src.frequencyFixDay;
this.lastInterestDate = src.lastInterestDate;
this.enableLockinPeriod = src.enableLockinPeriod;
this.autoDebitFacility = src.autoDebitFacility;
this.loanFacility = src.loanFacility;
this.prematureWithdrawalOption = src.prematureWithdrawalOption;
this.tenureFlexibility = src.tenureFlexibility;
this.lienMarked = src.lienMarked;
this.maturityInstructions = src.maturityInstructions;
this.creditMaturityProceeds = src.creditMaturityProceeds;
this.penaltyGracePeriodMissedInstallments = src.penaltyGracePeriodMissedInstallments;
this.panelRequestApplicable = src.panelRequestApplicable;
this.noPenaltyNomineeHeirClosure = src.noPenaltyNomineeHeirClosure;
this.deceasedSettlement = src.deceasedSettlement;
this.allowDebitTxnAfterDueDate = src.allowDebitTxnAfterDueDate;
this.accountClosedZeroBalanceInDayClosing = src.accountClosedZeroBalanceInDayClosing;
this.balanceFrequency = src.balanceFrequency;
this.withdrawalFeeForTransfers = src.withdrawalFeeForTransfers;
}
private Boolean withdrawalFeeForTransfers;
public void setWithdrawalFeeForTransfers(Boolean v) { this.withdrawalFeeForTransfers = v; }
public Boolean getWithdrawalFeeForTransfers() { return withdrawalFeeForTransfers; }
}
@@ -46,8 +46,9 @@ public class SavingsAccountConstant extends SavingsApiConstants {
guardianNameParamName, guardianCifNumberParamName, guardianRelationshipParamName,
guardianClientIdParamName, minorDateOfBirthParamName,
introducerIdParamName, introducerNameParamName, introducerCifNumberParamName,
nomineeParamName, linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName,
accountFacilityConfigParamName, jointHoldersParamName));
nomineeParamName, "nominees", linkedAccountIdParamName, linkedAccountTypeParamName, linkedAccountNoParamName,
accountFacilityConfigParamName, jointHoldersParamName,
balanceSwapOptionParamName, msmeParamName, autoDebitFacilityParamName));
/**
* These parameters will match the class level parameters of {@link SavingsAccountData}. Where possible, we try to
@@ -64,7 +65,7 @@ public class SavingsAccountConstant extends SavingsApiConstants {
Arrays.asList(idParamName, accountNoParamName));
protected static final Set<String> SAVINGS_ACCOUNT_ACTIVATION_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(localeParamName, dateFormatParamName, activatedOnDateParamName));
Arrays.asList(localeParamName, dateFormatParamName, activatedOnDateParamName, "openingBalance"));
protected static final Set<String> SAVINGS_ACCOUNT_CLOSE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(localeParamName, dateFormatParamName, closedOnDateParamName, noteParamName, paymentTypeIdParamName,
@@ -187,6 +187,65 @@ public class DepositProductAssembler {
// update product reference
productTermAndPreClosure.updateProductReference(fixedDepositProduct);
// CBS Custom Fields
fixedDepositProduct.setAccountType(command.stringValueOfParameterNamed("accountType"));
fixedDepositProduct.setCustomerSegment(command.stringValueOfParameterNamed("customerSegment"));
final String fdProductCode = command.stringValueOfParameterNamed("productCode");
fixedDepositProduct.setProductCode((fdProductCode != null && !fdProductCode.isBlank()) ? fdProductCode : null);
final String fdType = command.stringValueOfParameterNamed("fdType");
fixedDepositProduct.setFdType((fdType != null && !fdType.isBlank()) ? fdType : null);
if (command.parameterExists("glAccountId")) {
fixedDepositProduct.setGlAccountId(command.longValueOfParameterNamed("glAccountId"));
}
fixedDepositProduct.setIntroducerRequired(command.booleanPrimitiveValueOfParameterNamed("introducerRequired"));
fixedDepositProduct.setShareLinkingEnabled(command.booleanPrimitiveValueOfParameterNamed("shareLinkingEnabled"));
fixedDepositProduct.setEffectiveDate(parseCbsDate(command.stringValueOfParameterNamed("effectiveDate")));
fixedDepositProduct.setEffectiveTo(parseCbsDate(command.stringValueOfParameterNamed("effectiveTo")));
fixedDepositProduct.setEndDate(parseCbsDate(command.stringValueOfParameterNamed("endDate")));
fixedDepositProduct.setNominationFacility(command.booleanPrimitiveValueOfParameterNamed("nominationFacility"));
final boolean dormancyAlertValue = command.booleanPrimitiveValueOfParameterNamed("dormancyAlert");
fixedDepositProduct.setDormancyAlert(dormancyAlertValue);
// isDormancyTrackingActive is driven by dormancyAlert for FD products (same as RD)
fixedDepositProduct.setIsDormancyTrackingActive(dormancyAlertValue);
if (command.parameterExists("daysToInactive")) {
fixedDepositProduct.setDaysToInactive(command.longValueOfParameterNamed("daysToInactive"));
}
if (command.parameterExists("daysToDormancy")) {
fixedDepositProduct.setDaysToDormancy(command.longValueOfParameterNamed("daysToDormancy"));
}
if (command.parameterExists("daysToEscheat")) {
fixedDepositProduct.setDaysToEscheat(command.longValueOfParameterNamed("daysToEscheat"));
}
fixedDepositProduct.setInterestCalculateSelector(command.integerValueOfParameterNamed("interestCalculateSelector"));
fixedDepositProduct.setAveragePeriod(command.stringValueOfParameterNamed("averagePeriod"));
fixedDepositProduct.setAutoPost(command.booleanPrimitiveValueOfParameterNamed("autoPost"));
fixedDepositProduct.setFrequencyFixDay(command.integerValueOfParameterNamed("frequencyFixDay"));
fixedDepositProduct.setLastInterestDate(parseCbsDate(command.stringValueOfParameterNamed("lastInterestDate")));
fixedDepositProduct.setEnableLockinPeriod(command.booleanPrimitiveValueOfParameterNamed("enableLockinPeriod"));
fixedDepositProduct.setAutoDebitFacility(command.booleanPrimitiveValueOfParameterNamed("autoDebitFacility"));
fixedDepositProduct.setLoanFacility(command.booleanPrimitiveValueOfParameterNamed("loanFacility"));
fixedDepositProduct.setPrematureWithdrawalOption(command.booleanPrimitiveValueOfParameterNamed("prematureWithdrawalOption"));
fixedDepositProduct.setTenureFlexibility(command.booleanPrimitiveValueOfParameterNamed("tenureFlexibility"));
fixedDepositProduct.setLienMarked(command.booleanPrimitiveValueOfParameterNamed("lienMarked"));
fixedDepositProduct.setMaturityInstructions(command.booleanPrimitiveValueOfParameterNamed("maturityInstructions"));
fixedDepositProduct.setCreditMaturityProceeds(command.booleanPrimitiveValueOfParameterNamed("creditMaturityProceeds"));
fixedDepositProduct.setPenaltyGracePeriodMissedInstallments(
command.booleanPrimitiveValueOfParameterNamed("penaltyGracePeriodMissedInstallments"));
fixedDepositProduct.setPanelRequestApplicable(command.booleanPrimitiveValueOfParameterNamed("panelRequestApplicable"));
fixedDepositProduct.setNoPenaltyNomineeHeirClosure(
command.booleanPrimitiveValueOfParameterNamed("noPenaltyNomineeHeirClosure"));
fixedDepositProduct.setDeceasedSettlement(command.booleanPrimitiveValueOfParameterNamed("deceasedSettlement"));
fixedDepositProduct.setAllowDebitTxnAfterDueDate(command.booleanPrimitiveValueOfParameterNamed("allowDebitTxnAfterDueDate"));
if (command.parameterExists("accountClosedZeroBalanceInDayClosing")) {
fixedDepositProduct.setAccountClosedZeroBalanceInDayClosing(
command.booleanPrimitiveValueOfParameterNamed("accountClosedZeroBalanceInDayClosing"));
}
fixedDepositProduct.setBalanceFrequency(command.stringValueOfParameterNamed("balanceFrequency"));
if (command.parameterExists("withdrawalFeeForTransfers")) {
fixedDepositProduct.setWithdrawalFeeApplicableForTransfer(command.booleanPrimitiveValueOfParameterNamed("withdrawalFeeForTransfers"));
}
fixedDepositProduct.validateDomainRules();
return fixedDepositProduct;
@@ -284,7 +343,8 @@ public class DepositProductAssembler {
// ── CBS Custom Fields (from 0251 migration) ─────────────────────────
recurringDepositProduct.setAccountType(command.stringValueOfParameterNamed("accountType"));
recurringDepositProduct.setCustomerSegment(command.stringValueOfParameterNamed("customerSegment"));
recurringDepositProduct.setProductCode(command.stringValueOfParameterNamed("productCode"));
final String productCode = command.stringValueOfParameterNamed("productCode");
recurringDepositProduct.setProductCode((productCode != null && !productCode.isBlank()) ? productCode : null);
if (command.parameterExists("glAccountId")) {
recurringDepositProduct.setGlAccountId(command.longValueOfParameterNamed("glAccountId"));
}
@@ -337,6 +397,10 @@ public class DepositProductAssembler {
recurringDepositProduct.setDeceasedSettlement(command.booleanPrimitiveValueOfParameterNamed("deceasedSettlement"));
recurringDepositProduct.setAllowDebitTxnAfterDueDate(command.booleanPrimitiveValueOfParameterNamed("allowDebitTxnAfterDueDate"));
if (command.parameterExists("withdrawalFeeForTransfers")) {
recurringDepositProduct.setWithdrawalFeeApplicableForTransfer(command.booleanPrimitiveValueOfParameterNamed("withdrawalFeeForTransfers"));
}
recurringDepositProduct.validateDomainRules();
return recurringDepositProduct;
@@ -0,0 +1,66 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.portfolio.charge.domain.Charge;
@Entity
@Table(name = "m_facility_charge_mapping")
@Getter
@Setter
public class FacilityChargeMapping {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "facility_type", nullable = false, unique = true, length = 60)
private String facilityType;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "charge_id")
private Charge charge;
@Column(name = "charge_name", nullable = false, length = 200)
private String chargeName;
@Column(name = "default_amount", nullable = false, precision = 19, scale = 6)
private BigDecimal defaultAmount;
@Column(name = "charge_time_type", nullable = false)
private Integer chargeTimeType;
@Column(name = "charge_applies_to", nullable = false)
private Integer chargeAppliesTo;
@Column(name = "is_active", nullable = false)
private Boolean isActive = true;
}
@@ -0,0 +1,30 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.savings.domain;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface FacilityChargeMappingRepository extends JpaRepository<FacilityChargeMapping, Long> {
Optional<FacilityChargeMapping> findByFacilityType(String facilityType);
List<FacilityChargeMapping> findAllByIsActiveTrue();
}
@@ -19,6 +19,8 @@
package org.apache.fineract.portfolio.savings.domain;
import static org.apache.fineract.portfolio.interestratechart.InterestRateChartApiConstants.deleteParamName;
import static org.apache.fineract.portfolio.interestratechart.InterestRateChartApiConstants.endDateParamName;
import static org.apache.fineract.portfolio.interestratechart.InterestRateChartApiConstants.fromDateParamName;
import static org.apache.fineract.portfolio.interestratechart.InterestRateChartApiConstants.idParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maxDepositTermParamName;
@@ -26,6 +28,7 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maxDepo
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
@@ -34,6 +37,7 @@ import jakarta.persistence.JoinTable;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Transient;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
@@ -43,6 +47,7 @@ import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.apache.fineract.accounting.common.AccountingRuleType;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
@@ -76,6 +81,17 @@ public class FixedDepositProduct extends SavingsProduct {
@Transient
protected InterestRateChartAssembler chartAssembler;
@Column(name = "fd_type", length = 50)
private String fdType;
public String getFdType() {
return fdType;
}
public void setFdType(String fdType) {
this.fdType = fdType;
}
protected FixedDepositProduct() {
}
@@ -161,6 +177,12 @@ public class FixedDepositProduct extends SavingsProduct {
actualChanges.putAll(super.update(command));
if (command.isChangeInStringParameterNamed("fdType", this.fdType)) {
final String newValue = command.stringValueOfParameterNamed("fdType");
actualChanges.put("fdType", newValue);
this.fdType = (newValue != null && !newValue.isBlank()) ? newValue : null;
}
if (this.productTermAndPreClosure != null) {
actualChanges.putAll(this.productTermAndPreClosure.update(command, baseDataValidator));
}
@@ -196,10 +218,16 @@ public class FixedDepositProduct extends SavingsProduct {
chart.update(chartCommand, chartsChanges, baseDataValidator, this.setOfCharts(), this.currency().getCode());
}
} else {
// assemble chart
final InterestRateChart newChart = this.chartAssembler.assembleFrom(chartElement, this.currency().getCode(),
baseDataValidator);
this.addChart(newChart);
final InterestRateChart matchingChart = this.findChartByDates(chartCommand);
if (matchingChart != null) {
matchingChart.update(chartCommand, chartsChanges, baseDataValidator, this.setOfCharts(),
this.currency().getCode());
} else {
// assemble chart
final InterestRateChart newChart = this.chartAssembler.assembleFrom(chartElement, this.currency().getCode(),
baseDataValidator);
this.addChart(newChart);
}
}
}
}
@@ -241,6 +269,23 @@ public class FixedDepositProduct extends SavingsProduct {
return charts.remove(chart);
}
private InterestRateChart findChartByDates(final JsonCommand chartCommand) {
if (!chartCommand.parameterExists(fromDateParamName) || !chartCommand.parameterExists(endDateParamName)) {
return this.charts != null && this.charts.size() == 1 ? this.charts.iterator().next() : null;
}
final LocalDate fromDate = chartCommand.localDateValueOfParameterNamed(fromDateParamName);
final LocalDate endDate = chartCommand.localDateValueOfParameterNamed(endDateParamName);
for (InterestRateChart chart : setOfCharts()) {
if (Objects.equals(chart.getFromDate(), fromDate) && Objects.equals(chart.getEndDate(), endDate)) {
return chart;
}
}
return null;
}
public void setHelpers(final InterestRateChartAssembler chartAssembler) {
this.chartAssembler = chartAssembler;
}
@@ -3487,6 +3487,11 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions);
}
public void recalculateBalancesAfterFacilityCharges(final LocalDate transactionDate) {
recalculateDailyBalances(Money.zero(this.currency), transactionDate, false, false);
this.summary.updateSummary(this.currency, this.savingsAccountTransactionSummaryWrapper, this.transactions);
}
public void setSubStatusDormant() {
this.sub_status = SavingsAccountSubStatusEnum.DORMANT.getValue();
}
@@ -3957,6 +3962,13 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
@Column(name = "linked_account_no", length = 50)
private String linkedAccountNo;
@Column(name = "nomination_facility", nullable = false, columnDefinition = "boolean default false")
private Boolean nominationFacility = false;
@Column(name = "auto_debit_facility", nullable = false, columnDefinition = "boolean default false")
private Boolean autoDebitFacility = false;
@jakarta.persistence.OneToOne(cascade = jakarta.persistence.CascadeType.ALL, mappedBy = "savingsAccount", orphanRemoval = true, fetch = FetchType.LAZY)
private SavingsAccountFacilityConfig facilityConfig;
@@ -4022,4 +4034,10 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
public List<SavingsAccountJointHolder> getJointHolders() { return jointHolders; }
public void setJointHolders(List<SavingsAccountJointHolder> v) { this.jointHolders = v; }
public Boolean getNominationFacility() { return nominationFacility; }
public void setNominationFacility(Boolean v) { this.nominationFacility = v; }
public Boolean getAutoDebitFacility() { return autoDebitFacility; }
public void setAutoDebitFacility(Boolean v) { this.autoDebitFacility = v; }
}
@@ -56,10 +56,13 @@ public class SavingsAccountJointHolder extends AbstractPersistableCustom<Long> {
@Column(name = "is_primary")
private boolean primary = false;
@Column(name = "is_mandatory")
private boolean mandatory = false;
protected SavingsAccountJointHolder() {}
public SavingsAccountJointHolder(SavingsAccount account, Long clientId, String cifNumber,
String displayName, BigDecimal sharePercentage, String signingAuthority, String role, boolean primary) {
String displayName, BigDecimal sharePercentage, String signingAuthority, String role, boolean primary, boolean mandatory) {
this.savingsAccount = account;
this.clientId = clientId;
this.cifNumber = cifNumber;
@@ -68,6 +71,7 @@ public class SavingsAccountJointHolder extends AbstractPersistableCustom<Long> {
this.signingAuthority = signingAuthority;
this.role = role;
this.primary = primary;
this.mandatory = mandatory;
}
public SavingsAccount getSavingsAccount() { return savingsAccount; }
@@ -78,4 +82,5 @@ public class SavingsAccountJointHolder extends AbstractPersistableCustom<Long> {
public String getSigningAuthority() { return signingAuthority; }
public String getRole() { return role; }
public boolean isPrimary() { return primary; }
public boolean isMandatory() { return mandatory; }
}
@@ -56,6 +56,9 @@ public interface SavingsAccountRepository extends JpaRepository<SavingsAccount,
@Query("select case when (count (saving) > 0) then 'true' else 'false' end from SavingsAccount saving where saving.client.id = :clientId and saving.status in (100,200,300,303,304)")
boolean doNonClosedSavingAccountsExistForClient(@Param("clientId") Long clientId);
@Query("SELECT COUNT(sa) FROM SavingsAccount sa WHERE sa.client.id = :clientId AND sa.product.id = :productId AND sa.status IN (100, 200, 300, 303, 304)")
long countActiveAccountsByClientAndProduct(@Param("clientId") Long clientId, @Param("productId") Long productId);
@Query("select sa from SavingsAccount sa where sa.client.id is null and sa.group.id = :groupId")
List<SavingsAccount> findByGroupId(@Param("groupId") Long groupId);
@@ -181,4 +181,8 @@ public class SavingsAccountRepositoryWrapper {
public List<Long> findLoanIdsByStatusId(Integer status) {
return null;
}
public long countActiveAccountsByClientAndProduct(final Long clientId, final Long productId) {
return this.repository.countActiveAccountsByClientAndProduct(clientId, productId);
}
}
@@ -246,7 +246,7 @@ public class SavingsProduct extends AbstractPersistableCustom<Long> {
@Column(name = "customer_segment", length = 50)
private String customerSegment;
@Column(name = "product_code", length = 20, unique = true)
@Column(name = "product_code", length = 20)
private String productCode;
@Column(name = "gl_account_id")
@@ -1034,6 +1034,10 @@ public class SavingsProduct extends AbstractPersistableCustom<Long> {
return this.withdrawalFeeApplicableForTransfer;
}
public void setWithdrawalFeeApplicableForTransfer(boolean withdrawalFeeApplicableForTransfer) {
this.withdrawalFeeApplicableForTransfer = withdrawalFeeApplicableForTransfer;
}
public boolean isAllowOverdraft() {
return this.allowOverdraft;
}

Some files were not shown because too many files have changed in this diff Show More