FINERACT-2356: Fix incorrect accounting during write-off in case the loan was already charged-off

This commit is contained in:
mariiaKraievska
2025-09-01 13:43:43 +02:00
committed by Adam Saghy
parent 567e1c9800
commit b9564f806b
3 changed files with 304 additions and 89 deletions
@@ -42,6 +42,7 @@ public enum TransactionType {
BUY_DOWN_FEE_ADJUSTMENT("buyDownFeeAdjustment"), //
BUY_DOWN_FEE_AMORTIZATION("buyDownFeeAmortization"), //
INTEREST_REFUND("interestRefund"), //
WRITE_OFF("writeOff"), //
;
public final String value;
@@ -60,8 +60,64 @@
| Close (as written-off) | 650.0 | 650.0 | 0.0 | 0.0 | 0.0 | 0.0 |
Then Admin fails to undo "1"th transaction made on "29 January 2023"
@TestRailId:C4006
Scenario: Verify accounting during Write-off when loan was already charged-off
When Admin sets the business date to "1 January 2023"
And Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP1_INTEREST_FLAT | 1 January 2023 | 1000 | 12 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | PENALTIES_FEES_INTEREST_PRINCIPAL_ORDER |
And Admin successfully approves the loan on "1 January 2023" with "1000" amount and expected disbursement date on "1 January 2023"
And Admin successfully disburse the loan on "1 January 2023" with "1000" EUR transaction amount
And Admin adds an NSF fee because of payment bounce with "1 January 2023" transaction date
When Admin sets the business date to "22 February 2023"
And Admin adds a 10 % Processing charge to the loan with "en" locale on date: "22 February 2023"
And Admin does charge-off the loan on "22 February 2023"
Then Loan marked as charged-off on "22 February 2023"
Then Loan status will be "ACTIVE"
Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2023" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | 1000.0 | |
| LIABILITY | 145023 | Suspense/Clearing account | | 1000.0 |
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "22 February 2023" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 1000.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 143.0 |
| EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0 | |
| INCOME | 404001 | Interest Income Charge Off | 30.0 | |
| INCOME | 404008 | Fee Charge Off | 113.0 | |
When Admin sets the business date to "1 March 2023"
And Admin does write-off the loan on "1 March 2023"
Then Loan status will be "CLOSED_WRITTEN_OFF"
Then Loan Transactions tab has a "WRITE_OFF" transaction with date "01 March 2023" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| EXPENSE | 744007 | Credit Loss/Bad Debt | | 1000.0 |
| INCOME | 404001 | Interest Income Charge Off | | 30.0 |
| INCOME | 404008 | Fee Charge Off | | 113.0 |
| EXPENSE | e4 | Written off | 1143.0 | |
@TestRailId:C4007
Scenario: Verify accounting during Write-off when loan was not charged-off before
When Admin sets the business date to "1 January 2023"
And Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP1_INTEREST_FLAT | 1 January 2023 | 1000 | 12 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | PENALTIES_FEES_INTEREST_PRINCIPAL_ORDER |
And Admin successfully approves the loan on "1 January 2023" with "1000" amount and expected disbursement date on "1 January 2023"
And Admin successfully disburse the loan on "1 January 2023" with "1000" EUR transaction amount
And Admin adds an NSF fee because of payment bounce with "1 January 2023" transaction date
When Admin sets the business date to "22 February 2023"
And Admin adds a 10 % Processing charge to the loan with "en" locale on date: "22 February 2023"
Then Loan status will be "ACTIVE"
Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2023" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | 1000.0 | |
| LIABILITY | 145023 | Suspense/Clearing account | | 1000.0 |
When Admin sets the business date to "1 March 2023"
And Admin does write-off the loan on "1 March 2023"
Then Loan status will be "CLOSED_WRITTEN_OFF"
Then Loan Transactions tab has a "WRITE_OFF" transaction with date "01 March 2023" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 1000.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 143.0 |
| EXPENSE | e4 | Written off | 1143.0 | |
@@ -79,8 +79,7 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
*/
else if ((transactionType.isRepaymentType() && !transactionType.isChargeAdjustment())
|| transactionType.isRepaymentAtDisbursement() || transactionType.isChargePayment()) {
createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, false,
transactionType.isRepaymentAtDisbursement());
createJournalEntriesForRepayments(loanDTO, loanTransactionDTO, office, transactionType.isRepaymentAtDisbursement());
}
// Logic for handling recovery payments
@@ -100,7 +99,7 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
// Handle Write Offs
else if ((transactionType.isWriteOff() || transactionType.isWaiveInterest() || transactionType.isWaiveCharges())) {
createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, true, false);
createJournalEntriesForWriteOffs(loanDTO, loanTransactionDTO, office);
}
// Logic for Refunds of Active Loans
@@ -1168,35 +1167,32 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
*
* <b>Penalty Repayment</b>: Debits "Fund Source" and Credits "Receivable Penalties" <br/>
* <br/>
* Handles write offs using the following posting rules <br/>
* <br/>
* <b>Principal Write off</b>: Debits "Losses Written Off" and Credits "Loan Portfolio"<br/>
*
* <b>Interest Write off</b>:Debits "Losses Written off" and Credits "Receivable Interest" <br/>
*
* <b>Fee Write off</b>:Debits "Losses Written off" and Credits "Receivable Fees" <br/>
*
* <b>Penalty Write off</b>: Debits "Losses Written off" and Credits "Receivable Penalties" <br/>
* <br/>
* <br/>
*
* @param loanTransactionDTO
* @param loanDTO
* @param office
*/
private void createJournalEntriesForRepaymentsAndWriteOffs(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
final Office office, final boolean writeOff, final boolean isIncomeFromFee) {
private void createJournalEntriesForRepayments(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office,
final boolean isIncomeFromFee) {
final boolean isMarkedChargeOff = loanDTO.isMarkedAsChargeOff();
if (isMarkedChargeOff) {
createJournalEntriesForChargeOffLoanRepaymentAndWriteOffs(loanDTO, loanTransactionDTO, office, writeOff, isIncomeFromFee);
createJournalEntriesForRepaymentWhenLoanIsChargedOff(loanDTO, loanTransactionDTO, office, isIncomeFromFee);
} else {
createJournalEntriesForLoansRepaymentAndWriteOffs(loanDTO, loanTransactionDTO, office, writeOff, isIncomeFromFee);
createJournalEntriesForLoanRepayments(loanDTO, loanTransactionDTO, office, isIncomeFromFee);
}
}
private void createJournalEntriesForChargeOffLoanRepaymentAndWriteOffs(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO,
Office office, boolean writeOff, boolean isIncomeFromFee) {
private void createJournalEntriesForWriteOffs(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office) {
final boolean isMarkedChargeOff = loanDTO.isMarkedAsChargeOff();
if (isMarkedChargeOff) {
createJournalEntriesForWriteOffsWhenLoanIsChargedOff(loanDTO, loanTransactionDTO, office);
} else {
createJournalEntriesForLoanWriteOffs(loanDTO, loanTransactionDTO, office);
}
}
private void createJournalEntriesForRepaymentWhenLoanIsChargedOff(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
final Office office, final boolean isIncomeFromFee) {
// loan properties
final Long loanProductId = loanDTO.getLoanProductId();
final Long loanId = loanDTO.getLoanId();
@@ -1212,7 +1208,7 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
BigDecimal totalDebitAmount = new BigDecimal(0);
@@ -1240,17 +1236,14 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(),
glAccountBalanceHolder);
}
} else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) {
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId,
AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), AccrualAccountsForLoan.GOODWILL_CREDIT.getValue(),
glAccountBalanceHolder);
} else if (loanTransactionDTO.getTransactionType().isRepayment()) {
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId,
AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(),
glAccountBalanceHolder);
} else {
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
AccrualAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder);
@@ -1314,7 +1307,7 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
this.helper.createCreditJournalEntryForLoanCharges(office, currencyCode,
AccrualAccountsForLoan.INCOME_FROM_FEES.getValue(), loanProductId, loanId, transactionId, transactionDate,
feesAmount, loanTransactionDTO.getFeePayments());
GLAccount debitAccount = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
final GLAccount debitAccount = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.FUND_SOURCE.getValue(), paymentTypeId);
glAccountBalanceHolder.addToDebit(debitAccount, feesAmount);
@@ -1322,7 +1315,6 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(),
AccrualAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder);
}
}
}
@@ -1389,30 +1381,25 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
// create credit entries
for (Map.Entry<Long, BigDecimal> creditEntry : glAccountBalanceHolder.getCreditBalances().entrySet()) {
if (MathUtil.isGreaterThanZero(creditEntry.getValue())) {
GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey());
final GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey());
this.helper.createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
creditEntry.getValue(), glAccount);
}
}
if (MathUtil.isGreaterThanZero(totalDebitAmount)) {
if (writeOff) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, AccrualAccountsForLoan.LOSSES_WRITTEN_OFF.getValue(),
if (loanTransactionDTO.isLoanToLoanTransfer()) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.ASSET_TRANSFER.getValue(), loanProductId,
paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
} else if (loanTransactionDTO.isAccountTransfer()) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
} else {
if (loanTransactionDTO.isLoanToLoanTransfer()) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.ASSET_TRANSFER.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
} else if (loanTransactionDTO.isAccountTransfer()) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
} else {
// create debit entries
for (Map.Entry<Long, BigDecimal> debitEntry : glAccountBalanceHolder.getDebitBalances().entrySet()) {
GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(debitEntry.getKey());
this.helper.createDebitJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
debitEntry.getValue(), glAccount);
}
// create debit entries
for (Map.Entry<Long, BigDecimal> debitEntry : glAccountBalanceHolder.getDebitBalances().entrySet()) {
final GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(debitEntry.getKey());
this.helper.createDebitJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
debitEntry.getValue(), glAccount);
}
}
}
@@ -1423,7 +1410,8 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
***/
if (MathUtil.isGreaterThanZero(totalDebitAmount)) {
if (loanTransactionDTO.getTransactionType().isChargeRefund()) {
Integer incomeAccount = this.helper.getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType());
final Integer incomeAccount = this.helper
.getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType());
this.helper.createJournalEntriesForLoan(office, currencyCode, incomeAccount, AccrualAccountsForLoan.FUND_SOURCE.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
}
@@ -1431,8 +1419,87 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
}
private void createJournalEntriesForLoansRepaymentAndWriteOffs(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
final Office office, final boolean writeOff, final boolean isIncomeFromFee) {
private void createJournalEntriesForWriteOffsWhenLoanIsChargedOff(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
final Office office) {
// loan properties
final Long loanProductId = loanDTO.getLoanProductId();
final Long loanId = loanDTO.getLoanId();
final String currencyCode = loanDTO.getCurrencyCode();
final boolean isMarkedFraud = loanDTO.isMarkedAsFraud();
// transaction properties
final String transactionId = loanTransactionDTO.getTransactionId();
final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
final BigDecimal interestAmount = loanTransactionDTO.getInterest();
final BigDecimal feesAmount = loanTransactionDTO.getFees();
final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
BigDecimal totalDebitAmount = new BigDecimal(0);
// principal payment
if (MathUtil.isGreaterThanZero(principalAmount)) {
totalDebitAmount = totalDebitAmount.add(principalAmount);
if (isMarkedFraud) {
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId,
AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(),
glAccountBalanceHolder);
} else {
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(),
AccrualAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder);
}
}
// interest payment
if (MathUtil.isGreaterThanZero(interestAmount)) {
totalDebitAmount = totalDebitAmount.add(interestAmount);
populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId,
AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(),
glAccountBalanceHolder);
}
// handle fees payment
if (MathUtil.isGreaterThanZero(feesAmount)) {
totalDebitAmount = totalDebitAmount.add(feesAmount);
populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(),
AccrualAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder);
}
// handle penalties
if (MathUtil.isGreaterThanZero(penaltiesAmount)) {
totalDebitAmount = totalDebitAmount.add(penaltiesAmount);
populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId,
AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(),
glAccountBalanceHolder);
}
// overpayment
if (MathUtil.isGreaterThanZero(overPaymentAmount)) {
totalDebitAmount = totalDebitAmount.add(overPaymentAmount);
populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, AccrualAccountsForLoan.OVERPAYMENT.getValue(),
AccrualAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder);
}
// create credit entries
for (Map.Entry<Long, BigDecimal> creditEntry : glAccountBalanceHolder.getCreditBalances().entrySet()) {
if (MathUtil.isGreaterThanZero(creditEntry.getValue())) {
final GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey());
this.helper.createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
creditEntry.getValue(), glAccount);
}
}
if (MathUtil.isGreaterThanZero(totalDebitAmount)) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, AccrualAccountsForLoan.LOSSES_WRITTEN_OFF.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
}
}
private void createJournalEntriesForLoanRepayments(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
final Office office, final boolean isIncomeFromFee) {
// loan properties
final Long loanProductId = loanDTO.getLoanProductId();
final Long loanId = loanDTO.getLoanId();
@@ -1450,13 +1517,13 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
BigDecimal totalDebitAmount = new BigDecimal(0);
Map<GLAccount, BigDecimal> accountMap = new LinkedHashMap<>();
Map<Integer, BigDecimal> debitAccountMapForGoodwillCredit = new LinkedHashMap<>();
final Map<GLAccount, BigDecimal> accountMap = new LinkedHashMap<>();
final Map<Integer, BigDecimal> debitAccountMapForGoodwillCredit = new LinkedHashMap<>();
// handle principal payment or writeOff
// handle principal payment
if (MathUtil.isGreaterThanZero(principalAmount)) {
totalDebitAmount = totalDebitAmount.add(principalAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId);
accountMap.put(account, principalAmount);
if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) {
@@ -1465,13 +1532,13 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
}
}
// handle interest payment of writeOff
// handle interest payment
if (MathUtil.isGreaterThanZero(interestAmount)) {
totalDebitAmount = totalDebitAmount.add(interestAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(interestAmount);
final BigDecimal amount = accountMap.get(account).add(interestAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, interestAmount);
@@ -1483,17 +1550,17 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
}
}
// handle fees payment of writeOff
// handle fees payment
if (MathUtil.isGreaterThanZero(feesAmount)) {
totalDebitAmount = totalDebitAmount.add(feesAmount);
if (isIncomeFromFee) {
this.helper.createCreditJournalEntryForLoanCharges(office, currencyCode, AccrualAccountsForLoan.INCOME_FROM_FEES.getValue(),
loanProductId, loanId, transactionId, transactionDate, feesAmount, loanTransactionDTO.getFeePayments());
} else {
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(feesAmount);
final BigDecimal amount = accountMap.get(account).add(feesAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, feesAmount);
@@ -1505,23 +1572,23 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
}
}
// handle penalties payment of writeOff
// handle penalties payment
if (MathUtil.isGreaterThanZero(penaltiesAmount)) {
totalDebitAmount = totalDebitAmount.add(penaltiesAmount);
if (isIncomeFromFee) {
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.INCOME_FROM_PENALTIES.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(penaltiesAmount);
final BigDecimal amount = accountMap.get(account).add(penaltiesAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, penaltiesAmount);
}
} else {
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(penaltiesAmount);
final BigDecimal amount = accountMap.get(account).add(penaltiesAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, penaltiesAmount);
@@ -1537,10 +1604,10 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
if (MathUtil.isGreaterThanZero(overPaymentAmount)) {
totalDebitAmount = totalDebitAmount.add(overPaymentAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, AccrualAccountsForLoan.OVERPAYMENT.getValue(),
paymentTypeId);
final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.OVERPAYMENT.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(overPaymentAmount);
final BigDecimal amount = accountMap.get(account).add(overPaymentAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, overPaymentAmount);
@@ -1559,31 +1626,26 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
}
/**
* Single DEBIT transaction for write-offs or Repayments
* Single DEBIT transaction for Repayments
***/
if (MathUtil.isGreaterThanZero(totalDebitAmount)) {
if (writeOff) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, AccrualAccountsForLoan.LOSSES_WRITTEN_OFF.getValue(),
if (loanTransactionDTO.isLoanToLoanTransfer()) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.ASSET_TRANSFER.getValue(), loanProductId,
paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
} else if (loanTransactionDTO.isAccountTransfer()) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
} else {
if (loanTransactionDTO.isLoanToLoanTransfer()) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.ASSET_TRANSFER.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
} else if (loanTransactionDTO.isAccountTransfer()) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
} else {
if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) {
// create debit entries
for (Map.Entry<Integer, BigDecimal> debitEntry : debitAccountMapForGoodwillCredit.entrySet()) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, debitEntry.getKey().intValue(), loanProductId,
paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue());
}
} else {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, AccrualAccountsForLoan.FUND_SOURCE.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) {
// create debit entries
for (Map.Entry<Integer, BigDecimal> debitEntry : debitAccountMapForGoodwillCredit.entrySet()) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, debitEntry.getKey().intValue(), loanProductId,
paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue());
}
} else {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, AccrualAccountsForLoan.FUND_SOURCE.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
}
}
}
@@ -1593,12 +1655,108 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
* repayment above
***/
if (MathUtil.isGreaterThanZero(totalDebitAmount) && loanTransactionDTO.getTransactionType().isChargeRefund()) {
Integer incomeAccount = this.helper.getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType());
final Integer incomeAccount = this.helper.getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType());
this.helper.createJournalEntriesForLoan(office, currencyCode, incomeAccount, AccrualAccountsForLoan.FUND_SOURCE.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
}
}
private void createJournalEntriesForLoanWriteOffs(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
final Office office) {
// loan properties
final Long loanProductId = loanDTO.getLoanProductId();
final Long loanId = loanDTO.getLoanId();
final String currencyCode = loanDTO.getCurrencyCode();
// transaction properties
final String transactionId = loanTransactionDTO.getTransactionId();
final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
final BigDecimal interestAmount = loanTransactionDTO.getInterest();
final BigDecimal feesAmount = loanTransactionDTO.getFees();
final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
BigDecimal totalDebitAmount = new BigDecimal(0);
final Map<GLAccount, BigDecimal> accountMap = new LinkedHashMap<>();
// handle principal payment of writeOff
if (MathUtil.isGreaterThanZero(principalAmount)) {
totalDebitAmount = totalDebitAmount.add(principalAmount);
final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId);
accountMap.put(account, principalAmount);
}
// handle interest payment of writeOff
if (MathUtil.isGreaterThanZero(interestAmount)) {
totalDebitAmount = totalDebitAmount.add(interestAmount);
final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
final BigDecimal amount = accountMap.get(account).add(interestAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, interestAmount);
}
}
// handle fees payment of writeOff
if (MathUtil.isGreaterThanZero(feesAmount)) {
totalDebitAmount = totalDebitAmount.add(feesAmount);
final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
final BigDecimal amount = accountMap.get(account).add(feesAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, feesAmount);
}
}
// handle penalties payment of writeOff
if (MathUtil.isGreaterThanZero(penaltiesAmount)) {
totalDebitAmount = totalDebitAmount.add(penaltiesAmount);
final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
final BigDecimal amount = accountMap.get(account).add(penaltiesAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, penaltiesAmount);
}
}
if (MathUtil.isGreaterThanZero(overPaymentAmount)) {
totalDebitAmount = totalDebitAmount.add(overPaymentAmount);
final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.OVERPAYMENT.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
final BigDecimal amount = accountMap.get(account).add(overPaymentAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, overPaymentAmount);
}
}
for (Map.Entry<GLAccount, BigDecimal> entry : accountMap.entrySet()) {
if (MathUtil.isGreaterThanZero(entry.getValue())) {
this.helper.createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate, entry.getValue(),
entry.getKey());
}
}
/**
* Single DEBIT transaction for write-offs
***/
if (MathUtil.isGreaterThanZero(totalDebitAmount)) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, AccrualAccountsForLoan.LOSSES_WRITTEN_OFF.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
}
}
private void populateDebitAccountEntry(Long loanProductId, BigDecimal transactionPartAmount, Integer debitAccountType,
Map<Integer, BigDecimal> accountMapForDebit, Long paymentTypeId) {
Integer accountDebit = returnExistingDebitAccountInMapMatchingGLAccount(loanProductId, paymentTypeId, debitAccountType,