charges add module add in the saving account and current account
This commit is contained in:
+3
-2
@@ -24,6 +24,7 @@ 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;
|
||||
@@ -97,7 +98,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 +158,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");
|
||||
|
||||
+152
-2
@@ -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;
|
||||
}
|
||||
|
||||
+267
-1
@@ -44,6 +44,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;
|
||||
@@ -61,6 +69,10 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
||||
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
|
||||
@@ -114,6 +126,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));
|
||||
}
|
||||
@@ -141,7 +173,8 @@ public class CurrentAccountWritePlatformServiceImpl implements CurrentAccountWri
|
||||
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());
|
||||
@@ -221,8 +254,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();
|
||||
}
|
||||
|
||||
@@ -396,4 +494,172 @@ 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;
|
||||
}
|
||||
|
||||
final String transactionRef = "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(), transactionRef);
|
||||
postEntry(account, controlAccount, JournalEntryType.CREDIT, txn.getAmount(), txn.getTransactionDate(), transactionRef);
|
||||
log.info("[GL] Deposit GL posted for account id={}, amount={}", account.getId(), txn.getAmount());
|
||||
}
|
||||
}
|
||||
|
||||
// Charges: DR SAVINGS_CONTROL (liability reduced), CR INCOME_FROM_FEES
|
||||
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;
|
||||
postEntry(account, controlAccount, JournalEntryType.DEBIT, txn.getAmount(), txn.getTransactionDate(), transactionRef);
|
||||
postEntry(account, feeIncomeAccount, JournalEntryType.CREDIT, txn.getAmount(), txn.getTransactionDate(), transactionRef);
|
||||
log.info("[GL] Charge GL posted for account id={}, charge={}, amount={}", account.getId(), entry.charge().getCharge().getName(), txn.getAmount());
|
||||
}
|
||||
}
|
||||
} 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 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));
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -98,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;
|
||||
@@ -423,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,
|
||||
@@ -431,7 +432,7 @@ public class SavingsConfiguration {
|
||||
chargeRepository, savingsAccountChargeRepository, holidayRepository, workingDaysRepository, configurationDomainService,
|
||||
depositAccountOnHoldTransactionRepository, entityDatatableChecksWritePlatformService, appuserRepository,
|
||||
standingInstructionRepository, businessEventNotifierService, gsimRepository, savingsAccountInterestPostingService,
|
||||
errorHandler);
|
||||
errorHandler, facilityChargeAutoApplyService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
Reference in New Issue
Block a user