Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbc2737ada | ||
|
|
f457b9b8c7 | ||
|
|
23ee4f1063 | ||
|
|
bc38216ca3 |
+2
@@ -123,6 +123,7 @@ public final class DepositsApiConstants {
|
|||||||
public static final String isSeniorCitizenParamName = "isSeniorCitizen";
|
public static final String isSeniorCitizenParamName = "isSeniorCitizen";
|
||||||
public static final String seniorCitizenInterestRateParamName = "seniorCitizenInterestRate";
|
public static final String seniorCitizenInterestRateParamName = "seniorCitizenInterestRate";
|
||||||
public static final String tenureInMonthsParamName = "tenureInMonths";
|
public static final String tenureInMonthsParamName = "tenureInMonths";
|
||||||
|
public static final String tenureInDaysParamName = "tenureInDays";
|
||||||
public static final String interestCompoundingPeriodInMonthsParamName = "interestCompoundingPeriodInMonths";
|
public static final String interestCompoundingPeriodInMonthsParamName = "interestCompoundingPeriodInMonths";
|
||||||
|
|
||||||
// Preclosure parameters
|
// Preclosure parameters
|
||||||
@@ -511,6 +512,7 @@ public final class DepositsApiConstants {
|
|||||||
fixedDepositInterestCalculationData.add(principalAmountParamName);
|
fixedDepositInterestCalculationData.add(principalAmountParamName);
|
||||||
fixedDepositInterestCalculationData.add(annualInterestRateParamName);
|
fixedDepositInterestCalculationData.add(annualInterestRateParamName);
|
||||||
fixedDepositInterestCalculationData.add(tenureInMonthsParamName);
|
fixedDepositInterestCalculationData.add(tenureInMonthsParamName);
|
||||||
|
fixedDepositInterestCalculationData.add(tenureInDaysParamName);
|
||||||
fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName);
|
fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName);
|
||||||
fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName);
|
fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName);
|
||||||
// Exact calendar-based calculation params
|
// Exact calendar-based calculation params
|
||||||
|
|||||||
+97
-6
@@ -418,13 +418,20 @@ public class CashierDepositRequestApiResource {
|
|||||||
txnId = executeGlJournalEntry(glAccountId, contraGlId, glOfficeId, currencyCode,
|
txnId = executeGlJournalEntry(glAccountId, contraGlId, glOfficeId, currencyCode,
|
||||||
amount, ptId, txnDateStr, description, true);
|
amount, ptId, txnDateStr, description, true);
|
||||||
} else {
|
} else {
|
||||||
// Determine real account type by checking which table owns this ID.
|
// Determine real account type by checking which table/column owns this ID.
|
||||||
// Do NOT rely solely on the stored account_type field — old records may have 'SAVINGS'
|
// Do NOT rely solely on the stored account_type field — old records may have 'SAVINGS'
|
||||||
// even for current accounts (submitted before the frontend fix).
|
// even for current accounts (submitted before the frontend fix), and FD/RD accounts share
|
||||||
final boolean isCurrentAccount = isCurrentAccountById(savingsAccountId);
|
// the very same m_savings_account table as a plain savings account, distinguished only by
|
||||||
txnId = isCurrentAccount
|
// deposit_type_enum -- so this is looked up fresh rather than trusted from the request.
|
||||||
? executeCurrentAccountDeposit(savingsAccountId, amount, ptId, txnDateStr, description)
|
final String depositCategory = resolveDepositAccountCategory(savingsAccountId);
|
||||||
: executeSavingsDeposit(savingsAccountId, amount, ptId, txnDateStr, description);
|
final boolean isCurrentAccount = "CURRENT".equals(depositCategory);
|
||||||
|
txnId = switch (depositCategory) {
|
||||||
|
case "CURRENT" -> executeCurrentAccountDeposit(savingsAccountId, amount, ptId, txnDateStr, description);
|
||||||
|
case "FIXED_DEPOSIT" -> executeFixedDepositDeposit(savingsAccountId, amount, ptId, txnDateStr, description);
|
||||||
|
case "RECURRING_DEPOSIT" ->
|
||||||
|
executeRecurringDepositDeposit(savingsAccountId, amount, ptId, txnDateStr, description);
|
||||||
|
default -> executeSavingsDeposit(savingsAccountId, amount, ptId, txnDateStr, description);
|
||||||
|
};
|
||||||
if (!isCurrentAccount && txnId != null) {
|
if (!isCurrentAccount && txnId != null) {
|
||||||
// approve() is one @Transactional block, so an uncaught exception here would roll back
|
// approve() is one @Transactional block, so an uncaught exception here would roll back
|
||||||
// the deposit that was just posted along with it -- every Cheque/DD approval that ever
|
// the deposit that was just posted along with it -- every Cheque/DD approval that ever
|
||||||
@@ -718,6 +725,64 @@ public class CashierDepositRequestApiResource {
|
|||||||
return result.getResourceId() != null ? result.getResourceId().toString() : null;
|
return result.getResourceId() != null ? result.getResourceId().toString() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String executeFixedDepositDeposit(
|
||||||
|
final Long accountId, final BigDecimal amount,
|
||||||
|
final Object paymentTypeId, final String txnDateStr, final String note) {
|
||||||
|
|
||||||
|
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||||
|
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||||
|
|
||||||
|
final JsonObject payload = new JsonObject();
|
||||||
|
payload.addProperty("locale", "en");
|
||||||
|
payload.addProperty("dateFormat", "dd MMMM yyyy");
|
||||||
|
payload.addProperty("transactionDate", date);
|
||||||
|
payload.addProperty("transactionAmount", amount);
|
||||||
|
if (paymentTypeId != null) {
|
||||||
|
payload.addProperty("paymentTypeId", ((Number) paymentTypeId).longValue());
|
||||||
|
}
|
||||||
|
if (note != null && !note.isBlank()) {
|
||||||
|
payload.addProperty("note", note);
|
||||||
|
}
|
||||||
|
|
||||||
|
final CommandWrapper commandRequest = new CommandWrapperBuilder()
|
||||||
|
.fixedDepositAccountDeposit(accountId)
|
||||||
|
.withJson(payload.toString())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final CommandProcessingResult result = voucherService
|
||||||
|
.withOwnVoucher(() -> commandsSourceWritePlatformService.logCommandSource(commandRequest));
|
||||||
|
return result.getResourceId() != null ? result.getResourceId().toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String executeRecurringDepositDeposit(
|
||||||
|
final Long accountId, final BigDecimal amount,
|
||||||
|
final Object paymentTypeId, final String txnDateStr, final String note) {
|
||||||
|
|
||||||
|
final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
|
||||||
|
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
|
||||||
|
|
||||||
|
final JsonObject payload = new JsonObject();
|
||||||
|
payload.addProperty("locale", "en");
|
||||||
|
payload.addProperty("dateFormat", "dd MMMM yyyy");
|
||||||
|
payload.addProperty("transactionDate", date);
|
||||||
|
payload.addProperty("transactionAmount", amount);
|
||||||
|
if (paymentTypeId != null) {
|
||||||
|
payload.addProperty("paymentTypeId", ((Number) paymentTypeId).longValue());
|
||||||
|
}
|
||||||
|
if (note != null && !note.isBlank()) {
|
||||||
|
payload.addProperty("note", note);
|
||||||
|
}
|
||||||
|
|
||||||
|
final CommandWrapper commandRequest = new CommandWrapperBuilder()
|
||||||
|
.recurringAccountDeposit(accountId)
|
||||||
|
.withJson(payload.toString())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final CommandProcessingResult result = voucherService
|
||||||
|
.withOwnVoucher(() -> commandsSourceWritePlatformService.logCommandSource(commandRequest));
|
||||||
|
return result.getResourceId() != null ? result.getResourceId().toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Cheque/DD deposit is credited to the ledger immediately (same as cash) but is not really the
|
* A Cheque/DD deposit is credited to the ledger immediately (same as cash) but is not really the
|
||||||
* branch's own money yet -- it still has to clear. Real-world CBS practice is to hold that amount
|
* branch's own money yet -- it still has to clear. Real-world CBS practice is to hold that amount
|
||||||
@@ -809,6 +874,32 @@ public class CashierDepositRequestApiResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "CURRENT", "FIXED_DEPOSIT", "RECURRING_DEPOSIT" or "SAVINGS" — checked against the database
|
||||||
|
* rather than the request's own account_type string. FD and RD accounts live in the same
|
||||||
|
* m_savings_account table as a plain savings account (deposit_type_enum 200/300 vs 100), so a
|
||||||
|
* plain "not current → savings" check would post an FD/RD deposit through the generic savings
|
||||||
|
* deposit command, which does not accept a term-deposit account id.
|
||||||
|
*/
|
||||||
|
private String resolveDepositAccountCategory(final Long accountId) {
|
||||||
|
if (accountId == null) {
|
||||||
|
return "SAVINGS";
|
||||||
|
}
|
||||||
|
if (isCurrentAccountById(accountId)) {
|
||||||
|
return "CURRENT";
|
||||||
|
}
|
||||||
|
final List<Integer> depositTypes = jdbcTemplate.queryForList(
|
||||||
|
"SELECT deposit_type_enum FROM m_savings_account WHERE id = ?", Integer.class, accountId);
|
||||||
|
final Integer depositType = depositTypes.isEmpty() ? null : depositTypes.get(0);
|
||||||
|
if (Integer.valueOf(200).equals(depositType)) {
|
||||||
|
return "FIXED_DEPOSIT";
|
||||||
|
}
|
||||||
|
if (Integer.valueOf(300).equals(depositType)) {
|
||||||
|
return "RECURRING_DEPOSIT";
|
||||||
|
}
|
||||||
|
return "SAVINGS";
|
||||||
|
}
|
||||||
|
|
||||||
// Parses a Fineract transaction id string to Long.
|
// Parses a Fineract transaction id string to Long.
|
||||||
// Savings/current txn ids are decimal; GL journal entry ids are hex strings.
|
// Savings/current txn ids are decimal; GL journal entry ids are hex strings.
|
||||||
private Long parseTxnIdToLong(final String txnId) {
|
private Long parseTxnIdToLong(final String txnId) {
|
||||||
|
|||||||
+4
@@ -284,6 +284,7 @@ public class FixedDepositAccountsApiResource {
|
|||||||
@QueryParam("principalAmount") @Parameter(description = "Deposit principal amount") final BigDecimal principalAmount,
|
@QueryParam("principalAmount") @Parameter(description = "Deposit principal amount") final BigDecimal principalAmount,
|
||||||
@QueryParam("annualInterestRate") @Parameter(description = "Annual interest rate (%)") final BigDecimal annualInterestRate,
|
@QueryParam("annualInterestRate") @Parameter(description = "Annual interest rate (%)") final BigDecimal annualInterestRate,
|
||||||
@QueryParam("tenureInMonths") @Parameter(description = "Tenure in months") final Long tenureInMonths,
|
@QueryParam("tenureInMonths") @Parameter(description = "Tenure in months") final Long tenureInMonths,
|
||||||
|
@QueryParam("tenureInDays") @Parameter(description = "Tenure in days (alternative to tenureInMonths, for sub-month FD Single tenures)") final Long tenureInDays,
|
||||||
@QueryParam("interestCompoundingPeriodInMonths") @Parameter(description = "Compounding period in months (1,3,6,12)") final Long interestCompoundingPeriodInMonths,
|
@QueryParam("interestCompoundingPeriodInMonths") @Parameter(description = "Compounding period in months (1,3,6,12)") final Long interestCompoundingPeriodInMonths,
|
||||||
@QueryParam("interestPostingPeriodInMonths") @Parameter(description = "Posting period in months (1,3,6,12)") final Long interestPostingPeriodInMonths,
|
@QueryParam("interestPostingPeriodInMonths") @Parameter(description = "Posting period in months (1,3,6,12)") final Long interestPostingPeriodInMonths,
|
||||||
@QueryParam("submittedOnDate") @Parameter(description = "Submission date (yyyy-MM-dd) for exact calendar calculation") final String submittedOnDate,
|
@QueryParam("submittedOnDate") @Parameter(description = "Submission date (yyyy-MM-dd) for exact calendar calculation") final String submittedOnDate,
|
||||||
@@ -291,6 +292,9 @@ public class FixedDepositAccountsApiResource {
|
|||||||
HashMap request = new HashMap<>();
|
HashMap request = new HashMap<>();
|
||||||
request.put("annualInterestRate", annualInterestRate);
|
request.put("annualInterestRate", annualInterestRate);
|
||||||
request.put("tenureInMonths", tenureInMonths);
|
request.put("tenureInMonths", tenureInMonths);
|
||||||
|
if (tenureInDays != null) {
|
||||||
|
request.put("tenureInDays", tenureInDays);
|
||||||
|
}
|
||||||
request.put("interestCompoundingPeriodInMonths", interestCompoundingPeriodInMonths);
|
request.put("interestCompoundingPeriodInMonths", interestCompoundingPeriodInMonths);
|
||||||
request.put("interestPostingPeriodInMonths", interestPostingPeriodInMonths);
|
request.put("interestPostingPeriodInMonths", interestPostingPeriodInMonths);
|
||||||
request.put("principalAmount", principalAmount);
|
request.put("principalAmount", principalAmount);
|
||||||
|
|||||||
+10
-1
@@ -43,6 +43,7 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClos
|
|||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.principalAmountParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.principalAmountParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.recurringFrequencyParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.recurringFrequencyParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.recurringFrequencyTypeParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.recurringFrequencyTypeParamName;
|
||||||
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInDaysParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferInterestToSavingsParamName;
|
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.DepositsApiConstants.transferToSavingsIdParamName;
|
||||||
@@ -681,8 +682,16 @@ public class DepositAccountDataValidator {
|
|||||||
baseDataValidator.reset().parameter(annualInterestRateParamName).value(annualInterestRate).notNull()
|
baseDataValidator.reset().parameter(annualInterestRateParamName).value(annualInterestRate).notNull()
|
||||||
.notLessThanMin(BigDecimal.valueOf(0));
|
.notLessThanMin(BigDecimal.valueOf(0));
|
||||||
|
|
||||||
|
// FD Single accounts under 30 days are entered in whole days rather than months (a whole-month
|
||||||
|
// tenure would round down to 0 months); tenureInDays is an alternative to tenureInMonths, not
|
||||||
|
// an addition, so only one of the two needs to be a positive value.
|
||||||
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
|
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
|
||||||
baseDataValidator.reset().parameter(tenureInMonthsParamName).value(tenureInMonths).notNull().longGreaterThanZero();
|
Long tenureInDays = this.fromApiJsonHelper.extractLongNamed(tenureInDaysParamName, element);
|
||||||
|
if (tenureInDays != null && tenureInDays > 0) {
|
||||||
|
baseDataValidator.reset().parameter(tenureInMonthsParamName).value(tenureInMonths).ignoreIfNull().longGreaterThanZero();
|
||||||
|
} else {
|
||||||
|
baseDataValidator.reset().parameter(tenureInMonthsParamName).value(tenureInMonths).notNull().longGreaterThanZero();
|
||||||
|
}
|
||||||
|
|
||||||
Long interestPostingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestPostingPeriodInMonthsParamName, element);
|
Long interestPostingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestPostingPeriodInMonthsParamName, element);
|
||||||
baseDataValidator.reset().parameter(interestPostingPeriodInMonthsParamName).value(interestPostingPeriodInMonths).notNull()
|
baseDataValidator.reset().parameter(interestPostingPeriodInMonthsParamName).value(interestPostingPeriodInMonths).notNull()
|
||||||
|
|||||||
+25
-12
@@ -414,20 +414,33 @@ public class SavingsAccountAssembler {
|
|||||||
// dependency on the shareaccounts domain class) rather than trusted blindly, the same way an
|
// dependency on the shareaccounts domain class) rather than trusted blindly, the same way an
|
||||||
// account number typed for a linked savings/loan account elsewhere in this form is verified
|
// account number typed for a linked savings/loan account elsewhere in this form is verified
|
||||||
// before being accepted.
|
// before being accepted.
|
||||||
|
//
|
||||||
|
// For a term deposit (FD/RD), the product screen labels this same flag "Deposit Account Link" --
|
||||||
|
// it links an existing Savings/Current account (linkedAccountId, set below), not a co-op share
|
||||||
|
// account, so it's validated against that field instead of shareAccountId.
|
||||||
if (Boolean.TRUE.equals(account.getShareLinkingEnabled())) {
|
if (Boolean.TRUE.equals(account.getShareLinkingEnabled())) {
|
||||||
final Long shareAccountId = this.fromApiJsonHelper.extractLongNamed(shareAccountIdParamName, element);
|
final boolean isTermDeposit = account.depositAccountType().isFixedDeposit() || account.depositAccountType().isRecurringDeposit();
|
||||||
if (shareAccountId == null) {
|
if (isTermDeposit) {
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.share.account.required",
|
final Long linkedAccountIdForCheck = this.fromApiJsonHelper.extractLongNamed(linkedAccountIdParamName, element);
|
||||||
"A share account must be chosen when Share Linking is enabled.");
|
if (linkedAccountIdForCheck == null) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.linked.account.required",
|
||||||
|
"A Savings/Current account must be linked when Deposit Account Link is enabled.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final Long shareAccountId = this.fromApiJsonHelper.extractLongNamed(shareAccountIdParamName, element);
|
||||||
|
if (shareAccountId == null) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.share.account.required",
|
||||||
|
"A share account must be chosen when Share Linking is enabled.");
|
||||||
|
}
|
||||||
|
final Integer matches = this.jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_share_account WHERE id = ? AND client_id = ? AND status_enum = 300", Integer.class,
|
||||||
|
shareAccountId, account.clientId());
|
||||||
|
if (matches == null || matches == 0) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.share.account.not.active.for.client",
|
||||||
|
"Share account " + shareAccountId + " is not an active share account for this client.");
|
||||||
|
}
|
||||||
|
account.setShareAccountId(shareAccountId);
|
||||||
}
|
}
|
||||||
final Integer matches = this.jdbcTemplate.queryForObject(
|
|
||||||
"SELECT COUNT(*) FROM m_share_account WHERE id = ? AND client_id = ? AND status_enum = 300", Integer.class,
|
|
||||||
shareAccountId, account.clientId());
|
|
||||||
if (matches == null || matches == 0) {
|
|
||||||
throw new GeneralPlatformDomainRuleException("error.msg.savingsaccount.share.account.not.active.for.client",
|
|
||||||
"Share account " + shareAccountId + " is not an active share account for this client.");
|
|
||||||
}
|
|
||||||
account.setShareAccountId(shareAccountId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Senior citizen rate — runs after validateDomainRules so it is the final persisted value
|
// Senior citizen rate — runs after validateDomainRules so it is the final persisted value
|
||||||
|
|||||||
+20
-5
@@ -1512,6 +1512,23 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
if (depositAccountType.isFixedDeposit()) {
|
if (depositAccountType.isFixedDeposit()) {
|
||||||
((FixedDepositAccount) account).updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
((FixedDepositAccount) account).updateMaturityStatus(isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth);
|
||||||
FixedDepositAccount fdAccount = ((FixedDepositAccount) account);
|
FixedDepositAccount fdAccount = ((FixedDepositAccount) account);
|
||||||
|
|
||||||
|
// Post the interest transaction(s) updateMaturityStatus() just created BEFORE any maturity
|
||||||
|
// closure below runs. The closure's own withdrawal (handleWithdrawal -> postJournalEntries)
|
||||||
|
// snapshots "existing" transactions the moment IT starts, which by then already includes
|
||||||
|
// this interest posting -- so its own posting treats the interest as already accounted for
|
||||||
|
// and skips it, while this method's own final postJournalEntries() call (previously relying
|
||||||
|
// on the interest staying out of existingTransactionIds so it would post there instead)
|
||||||
|
// never actually saw it get posted either: for an FD whose entire term is a single posting
|
||||||
|
// period -- interest posts and the account matures in this same run -- that left the
|
||||||
|
// interest transaction with a real ledger balance but no matching GL entry at all, forever.
|
||||||
|
// Posting it now, while it is still the only new thing on the account, gives it exactly one
|
||||||
|
// correct journal entry, and folding it into existingTransactionIds below (the same way
|
||||||
|
// markWithdrawalsAsAlreadyPosted() already does for the withdrawal) keeps the closure branch
|
||||||
|
// and the final call from ever trying to post it again.
|
||||||
|
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
||||||
|
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds);
|
||||||
|
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||||
// handle maturity instructions
|
// handle maturity instructions
|
||||||
|
|
||||||
if (fdAccount.isMatured() && (fdAccount.isReinvestOnClosure() || fdAccount.isTransferToSavingsOnClosure())) {
|
if (fdAccount.isMatured() && (fdAccount.isReinvestOnClosure() || fdAccount.isTransferToSavingsOnClosure())) {
|
||||||
@@ -1524,11 +1541,9 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
|
|
||||||
// Every maturity-instruction branch takes the balance out of this account through
|
// Every maturity-instruction branch takes the balance out of this account through
|
||||||
// SavingsAccountDomainServiceJpa#handleWithdrawal, which posts that withdrawal's journal
|
// SavingsAccountDomainServiceJpa#handleWithdrawal, which posts that withdrawal's journal
|
||||||
// entries itself. existingTransactionIds was snapshotted at the top of this method --
|
// entries itself. existingTransactionIds was refreshed just above (after the interest was
|
||||||
// before that withdrawal existed -- so the postJournalEntries() call at the end would
|
// posted) -- before that withdrawal existed -- so the postJournalEntries() call at the end
|
||||||
// treat it as new and post it a second time. Interest postings created by
|
// would treat it as new and post it a second time without this.
|
||||||
// updateMaturityStatus() above are still genuinely unposted at this point and must stay
|
|
||||||
// out of the snapshot so that final call can post them exactly once.
|
|
||||||
markWithdrawalsAsAlreadyPosted(account, existingTransactionIds);
|
markWithdrawalsAsAlreadyPosted(account, existingTransactionIds);
|
||||||
|
|
||||||
if (changes.get("reinvestedDepositId") != null) {
|
if (changes.get("reinvestedDepositId") != null) {
|
||||||
|
|||||||
+4
-1
@@ -22,6 +22,7 @@ package org.apache.fineract.portfolio.savings.service;
|
|||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.annualInterestRateParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.annualInterestRateParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.interestCompoundingPeriodInMonthsParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.interestCompoundingPeriodInMonthsParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.principalAmountParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.principalAmountParamName;
|
||||||
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInDaysParamName;
|
||||||
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
|
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
@@ -52,6 +53,7 @@ public class FixedDepositAccountInterestCalculationServiceImpl implements FixedD
|
|||||||
BigDecimal principalAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalAmountParamName, element);
|
BigDecimal principalAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalAmountParamName, element);
|
||||||
BigDecimal annualInterestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(annualInterestRateParamName, element);
|
BigDecimal annualInterestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(annualInterestRateParamName, element);
|
||||||
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
|
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
|
||||||
|
Long tenureInDays = this.fromApiJsonHelper.extractLongNamed(tenureInDaysParamName, element);
|
||||||
Long interestCompoundingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestCompoundingPeriodInMonthsParamName, element);
|
Long interestCompoundingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestCompoundingPeriodInMonthsParamName, element);
|
||||||
|
|
||||||
// Optional params for exact calendar-based calculation
|
// Optional params for exact calendar-based calculation
|
||||||
@@ -73,7 +75,8 @@ public class FixedDepositAccountInterestCalculationServiceImpl implements FixedD
|
|||||||
BigDecimal maturityAmount;
|
BigDecimal maturityAmount;
|
||||||
if (submittedOnDate != null && daysInYear != null && interestPostingPeriodInMonths != null) {
|
if (submittedOnDate != null && daysInYear != null && interestPostingPeriodInMonths != null) {
|
||||||
// Exact calendar-based calculation — same algorithm as account creation
|
// Exact calendar-based calculation — same algorithm as account creation
|
||||||
final LocalDate maturityDate = submittedOnDate.plusMonths(tenureInMonths);
|
final LocalDate maturityDate = (tenureInDays != null && tenureInDays > 0) ? submittedOnDate.plusDays(tenureInDays)
|
||||||
|
: submittedOnDate.plusMonths(tenureInMonths);
|
||||||
maturityAmount = this.calculateExact(principalAmount, annualInterestRate, submittedOnDate, maturityDate,
|
maturityAmount = this.calculateExact(principalAmount, annualInterestRate, submittedOnDate, maturityDate,
|
||||||
interestCompoundingPeriodInMonths, interestPostingPeriodInMonths, daysInYear);
|
interestCompoundingPeriodInMonths, interestPostingPeriodInMonths, daysInYear);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user