FINERACT-2358: Advanced-accounting-rules-based-on-write-off-reason E2E tests

This commit is contained in:
Peter Kovacs
2025-10-15 09:52:16 +02:00
committed by Arnold Gálovics
parent c518d8622a
commit 3b62d562a2
14 changed files with 226 additions and 9 deletions
@@ -106,4 +106,8 @@ public interface ProductToGLAccountMappingRepository
ProductToGLAccountMapping findCapitalizedIncomeClassificationMapping(@Param("productId") Long productId,
@Param("productType") Integer productType, @Param("classificationId") Long classificationId);
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.writeOffReason.id = :writeOffReasonId AND mapping.productId = :productId AND mapping.productType = :productType")
ProductToGLAccountMapping findWriteOffReasonMapping(@Param("productId") Long productId, @Param("productType") Integer productType,
@Param("writeOffReasonId") Long writeOffReasonId);
}
@@ -23,7 +23,7 @@ public enum DefaultCodeValue implements CodeValue {
// Charge-off reason
FRAUD("Fraud"), //
DELINQUENT("Delinquent"), //
OTHER("Other"); //
OTHER("Other"), BAD_DEBT("Bad Debt"), FORGIVEN("Forgiven"), TEST("Test"); //
private final String customName;
@@ -164,6 +164,7 @@ public enum DefaultLoanProduct implements LoanProduct {
LP2_ADV_PYMNT_INTEREST_DECL_BAL_SARP_EMI_360_30_NO_INT_RECALC_MULTIDISB_PARTIAL_PERIOD, //
LP2_ADV_PYMNT_INTEREST_DECL_BAL_SARP_EMI_360_30_NO_INT_RECALC_MULTIDISB_NO_PARTIAL_PERIOD, //
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_MIN_INT_3_MAX_INT_20, //
LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_WRITE_OFF_REASON_MAP, //
;
@Override
@@ -107,9 +107,12 @@ public class CodeGlobalInitializerStep implements FineractGlobalInitializerStep
public static final String CODE_VALUE_FAMILY_MARITAL_STATUS_WIDOWED = "Widowed";
public static final Long CODE_VALUE_CONSTITUTION_ID = 24L;
public static final String CODE_VALUE_CONSTITUTION_TEST = "Test";
public static final Long CODE_VALUE_RESCHEDULE_REASON_ID = 23L;
public static final String CODE_VALUE_RESCHEDULE_REASON_TEST = "Test";
public static final Long CODE_VALUE_WRITE_OFF_REASON_ID = 26L;
public static final String CODE_VALUE_WRITE_OFF_REASON_TEST_1 = "Bad Debt";
public static final String CODE_VALUE_WRITE_OFF_REASON_TEST_2 = "Forgiven";
public static final String CODE_VALUE_WRITE_OFF_REASON_TEST_3 = "Test";
private final CodesApi codesApi;
private final CodeValuesApi codeValuesApi;
@@ -246,6 +249,13 @@ public class CodeGlobalInitializerStep implements FineractGlobalInitializerStep
List<String> rescheduleReasonNames = new ArrayList<>();
rescheduleReasonNames.add(CODE_VALUE_RESCHEDULE_REASON_TEST);
createCodeValues(CODE_VALUE_RESCHEDULE_REASON_ID, rescheduleReasonNames);
// add Write-off reasons
List<String> writeOffReasonNames = new ArrayList<>();
writeOffReasonNames.add(CODE_VALUE_WRITE_OFF_REASON_TEST_1);
writeOffReasonNames.add(CODE_VALUE_WRITE_OFF_REASON_TEST_2);
writeOffReasonNames.add(CODE_VALUE_WRITE_OFF_REASON_TEST_3);
createCodeValues(CODE_VALUE_WRITE_OFF_REASON_ID, writeOffReasonNames);
}
public void createCodeValues(Long codeId, List<String> codeValueNames) {
@@ -43,6 +43,7 @@ import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostClassificationToIncomeAccountMappings;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostWriteOffReasonToExpenseAccountMappings;
import org.apache.fineract.client.services.LoanProductsApi;
import org.apache.fineract.test.data.AdvancePaymentsAdjustmentType;
import org.apache.fineract.test.data.ChargeProductType;
@@ -55,8 +56,12 @@ import org.apache.fineract.test.data.OverAppliedCalculationType;
import org.apache.fineract.test.data.PreClosureInterestCalculationRule;
import org.apache.fineract.test.data.RecalculationRestFrequencyType;
import org.apache.fineract.test.data.TransactionProcessingStrategyCode;
import org.apache.fineract.test.data.codevalue.CodeValue;
import org.apache.fineract.test.data.codevalue.CodeValueResolver;
import org.apache.fineract.test.data.codevalue.DefaultCodeValue;
import org.apache.fineract.test.data.loanproduct.DefaultLoanProduct;
import org.apache.fineract.test.factory.LoanProductsRequestFactory;
import org.apache.fineract.test.helper.CodeHelper;
import org.apache.fineract.test.helper.Utils;
import org.apache.fineract.test.support.TestContext;
import org.apache.fineract.test.support.TestContextKey;
@@ -69,6 +74,8 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
private final LoanProductsApi loanProductsApi;
private final LoanProductsRequestFactory loanProductsRequestFactory;
private final CodeHelper codeHelper;
private final CodeValueResolver codeValueResolver;
@Override
public void initialize() throws Exception {
@@ -1319,7 +1326,7 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
.recalculationRestFrequencyType(1)//
.recalculationRestFrequencyInterval(1)//
.repaymentEvery(1)//
.interestRatePerPeriod((double) 7.0)//
.interestRatePerPeriod(7D)//
.interestRateFrequencyType(INTEREST_RATE_FREQUENCY_TYPE_MONTH)//
.enableDownPayment(false)//
.interestRecalculationCompoundingMethod(0)//
@@ -3818,6 +3825,56 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PMNT_ALLOCATION_CAPITALIZED_INCOME_ADJ_CUSTOM_ALLOC_CLASSIFICATION_INCOME_MAP,
responseLoanProductsRequestLP2ProgressiveAdvPaymAllocCapitaizedIncomeClassificationIncomeMap);
// LP2 with progressive loan schedule + horizontal + interest EMI + 360/30
// + interest recalculation, buy down fees enabled
// + Write off reason expense map
final String name142 = DefaultLoanProduct.LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_WRITE_OFF_REASON_MAP.getName();
final Long writeOffReasonCodeId = codeHelper.retrieveCodeByName("WriteOffReasons").getId();
final CodeValue writeOffReasonCodeValueBadDebt = DefaultCodeValue.valueOf("BAD_DEBT");
final CodeValue writeOffReasonCodeValueForgiven = DefaultCodeValue.valueOf("FORGIVEN");
final CodeValue writeOffReasonCodeValueTest = DefaultCodeValue.valueOf("TEST");
long writeOffReasonIdBadDebt = codeValueResolver.resolve(writeOffReasonCodeId, writeOffReasonCodeValueBadDebt);
long writeOffReasonIdForgiven = codeValueResolver.resolve(writeOffReasonCodeId, writeOffReasonCodeValueForgiven);
long writeOffReasonIdTest = codeValueResolver.resolve(writeOffReasonCodeId, writeOffReasonCodeValueTest);
List<PostWriteOffReasonToExpenseAccountMappings> writeOffReasonToExpenseAccountMappings = new ArrayList<>();
PostWriteOffReasonToExpenseAccountMappings writeOffReasonToExpenseAccountMappingsBadDebt = new PostWriteOffReasonToExpenseAccountMappings();
writeOffReasonToExpenseAccountMappingsBadDebt.setWriteOffReasonCodeValueId(String.valueOf(writeOffReasonIdBadDebt));
writeOffReasonToExpenseAccountMappingsBadDebt.setExpenseAccountId("12"); // Credit Loss/Bad Debt
PostWriteOffReasonToExpenseAccountMappings writeOffReasonToExpenseAccountMappingsForgiven = new PostWriteOffReasonToExpenseAccountMappings();
writeOffReasonToExpenseAccountMappingsForgiven.setWriteOffReasonCodeValueId(String.valueOf(writeOffReasonIdForgiven));
writeOffReasonToExpenseAccountMappingsForgiven.setExpenseAccountId("23"); // Buy Down Expense
PostWriteOffReasonToExpenseAccountMappings writeOffReasonToExpenseAccountMappingsTest = new PostWriteOffReasonToExpenseAccountMappings();
writeOffReasonToExpenseAccountMappingsTest.setWriteOffReasonCodeValueId(String.valueOf(writeOffReasonIdTest));
writeOffReasonToExpenseAccountMappingsTest.setExpenseAccountId("16"); // Written off
writeOffReasonToExpenseAccountMappings.add(writeOffReasonToExpenseAccountMappingsBadDebt);
writeOffReasonToExpenseAccountMappings.add(writeOffReasonToExpenseAccountMappingsForgiven);
writeOffReasonToExpenseAccountMappings.add(writeOffReasonToExpenseAccountMappingsTest);
final PostLoanProductsRequest loanProductsRequestLP2ProgressiveAdvPaymentWriteOffReasonMap = loanProductsRequestFactory
.defaultLoanProductsRequestLP2BuyDownFees()//
.name(name142)//
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION.getValue())//
.loanScheduleType("PROGRESSIVE") //
.isInterestRecalculationEnabled(true)//
.preClosureInterestCalculationStrategy(1)//
.rescheduleStrategyMethod(4)//
.interestRecalculationCompoundingMethod(0)//
.recalculationRestFrequencyType(2)//
.recalculationRestFrequencyInterval(1)//
.paymentAllocation(List.of(//
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")))//
.writeOffReasonsToExpenseMappings(writeOffReasonToExpenseAccountMappings);//
final Response<PostLoanProductsResponse> responseLoanProductsRequestLP2ProgressiveAdvPaymentWriteOffReasonMap = loanProductsApi
.createLoanProduct(loanProductsRequestLP2ProgressiveAdvPaymentWriteOffReasonMap).execute();
TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_WRITE_OFF_REASON_MAP,
responseLoanProductsRequestLP2ProgressiveAdvPaymentWriteOffReasonMap);
}
public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
@@ -3286,6 +3286,28 @@ public class LoanStepDef extends AbstractStepDef {
ErrorHelper.checkSuccessfulApiCall(writeOffResponse);
}
@And("Admin does write-off the loan on {string} with write off reason: {string}")
public void writeOffLoan(String transactionDate, String writeOffReason) throws IOException {
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
long loanId = loanResponse.body().getLoanId();
final Long writeOffReasonCodeId = codeHelper.retrieveCodeByName("WriteOffReasons").getId();
final CodeValue writeOffReasonCodeValueBadDebt = DefaultCodeValue.valueOf(writeOffReason);
long writeOffReasonId = codeValueResolver.resolve(writeOffReasonCodeId, writeOffReasonCodeValueBadDebt);
PostLoansLoanIdTransactionsRequest writeOffRequest = new PostLoansLoanIdTransactionsRequest()//
.transactionDate(transactionDate)//
.writeoffReasonId(writeOffReasonId)//
.dateFormat(DATE_FORMAT)//
.locale(DEFAULT_LOCALE)//
.note("Write Off");//
Response<PostLoansLoanIdTransactionsResponse> writeOffResponse = loanTransactionsApi
.executeLoanTransaction(loanId, writeOffRequest, "writeoff").execute();
testContext().set(TestContextKey.LOAN_WRITE_OFF_RESPONSE, writeOffResponse);
ErrorHelper.checkSuccessfulApiCall(writeOffResponse);
}
@Then("Admin fails to undo {string}th transaction made on {string}")
public void undoTransaction(String nthTransaction, String transactionDate) throws IOException {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
@@ -269,4 +269,5 @@ public abstract class TestContextKey {
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DECL_BAL_SARP_EMI_360_30_NO_INT_RECALC_MULTIDISB_NO_PARTIAL_PERIOD = "loanProductCreateResponseLP2AdvPmtIntDeclSarpEmi3630NoIntRecalcMutiDisbNoPartial";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES = "loanProductCreateResponseLP2ProgressiveAdvancedPaymentInterestRecalculationMultidisbursalApprovedOverAppliedAmountExpectedTransches";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_MIN_INT_3_MAX_INT_20 = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyTillPreCloseMinInt3MaxInt20";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_WRITE_OFF_REASON_MAP = "loanProductCreateResponseLP2ProgressiveAdvancedPaymentWriteOffReasonMap";
}
@@ -361,3 +361,102 @@
| Type | Account code | Account name | Debit | Credit |
| EXPENSE | 744037 | Credit Loss/Bad Debt-Fraud | | 750.0 |
| EXPENSE | e4 | Written off | 750.0 | |
@TestRailId:C4111
Scenario: Verify GL entries for Write Off reason mapping - UC1: Write off, LP with write off reason mapping, NO write off reason to expense account
When Admin sets the business date to "01 January 2025"
And Admin creates a client with random data
And 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 |
| LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_WRITE_OFF_REASON_MAP | 01 January 2025 | 1000 | 12 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2025" with "1000" amount and expected disbursement date on "01 January 2025"
And Admin successfully disburse the loan on "01 January 2025" with "1000" EUR transaction amount
And Admin sets the business date to "02 January 2025"
And Admin runs inline COB job for Loan
And Admin does write-off the loan on "02 January 2025"
Then Loan status will be "CLOSED_WRITTEN_OFF"
And Loan Repayment schedule has 4 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2025 | 02 January 2025 | 753.72 | 246.28 | 10.0 | 0.0 | 0.0 | 256.28 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2 | 28 | 01 March 2025 | 02 January 2025 | 504.98 | 248.74 | 7.54 | 0.0 | 0.0 | 256.28 | 0.0 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2025 | 02 January 2025 | 253.75 | 251.23 | 5.05 | 0.0 | 0.0 | 256.28 | 0.0 | 0.0 | 0.0 | 0.0 |
| 4 | 30 | 01 May 2025 | 02 January 2025 | 0.0 | 253.75 | 2.54 | 0.0 | 0.0 | 256.29 | 0.0 | 0.0 | 0.0 | 0.0 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000 | 25.13 | 0 | 0 | 1025.13 | 0 | 0 | 0 | 0.0 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2025 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 02 January 2025 | Close (as written-off) | 1025.13 | 1000.0 | 25.13 | 0.0 | 0.0 | 0.0 | false | false |
And Loan Transactions tab has a "WRITE_OFF" transaction with date "02 January 2025" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 1000.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 25.13 |
| EXPENSE | e4 | Written off | 1025.13 | |
@TestRailId:C4112
Scenario: Verify GL entries for Write Off reason mapping - UC2: Write off, LP with write off reason mapping, with write off reason: Bad Debt
When Admin sets the business date to "01 January 2025"
And Admin creates a client with random data
And 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 |
| LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_WRITE_OFF_REASON_MAP | 01 January 2025 | 1000 | 12 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2025" with "1000" amount and expected disbursement date on "01 January 2025"
And Admin successfully disburse the loan on "01 January 2025" with "1000" EUR transaction amount
And Admin sets the business date to "02 January 2025"
And Admin runs inline COB job for Loan
And Admin does write-off the loan on "02 January 2025" with write off reason: "BAD_DEBT"
Then Loan status will be "CLOSED_WRITTEN_OFF"
And Loan Repayment schedule has 4 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2025 | 02 January 2025 | 753.72 | 246.28 | 10.0 | 0.0 | 0.0 | 256.28 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2 | 28 | 01 March 2025 | 02 January 2025 | 504.98 | 248.74 | 7.54 | 0.0 | 0.0 | 256.28 | 0.0 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2025 | 02 January 2025 | 253.75 | 251.23 | 5.05 | 0.0 | 0.0 | 256.28 | 0.0 | 0.0 | 0.0 | 0.0 |
| 4 | 30 | 01 May 2025 | 02 January 2025 | 0.0 | 253.75 | 2.54 | 0.0 | 0.0 | 256.29 | 0.0 | 0.0 | 0.0 | 0.0 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000 | 25.13 | 0 | 0 | 1025.13 | 0 | 0 | 0 | 0.0 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2025 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 02 January 2025 | Close (as written-off) | 1025.13 | 1000.0 | 25.13 | 0.0 | 0.0 | 0.0 | false | false |
And Loan Transactions tab has a "WRITE_OFF" transaction with date "02 January 2025" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 1000.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 25.13 |
| EXPENSE | 744007 | Credit Loss/Bad Debt | 1025.13 | |
@TestRailId:C4113
Scenario: Verify GL entries for Write Off reason mapping - UC3: Write off, LP without write off reason mapping, with write off reason: Bad Debt
When Admin sets the business date to "01 January 2025"
And Admin creates a client with random data
And 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 |
| LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_BUYDOWN_FEES_CLASSIFICATION_INCOME_MAP | 01 January 2025 | 1000 | 12 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2025" with "1000" amount and expected disbursement date on "01 January 2025"
And Admin successfully disburse the loan on "01 January 2025" with "1000" EUR transaction amount
And Admin sets the business date to "02 January 2025"
And Admin runs inline COB job for Loan
And Admin does write-off the loan on "02 January 2025"
Then Loan status will be "CLOSED_WRITTEN_OFF"
And Loan Repayment schedule has 4 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2025 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 1 | 31 | 01 February 2025 | 02 January 2025 | 753.72 | 246.28 | 10.0 | 0.0 | 0.0 | 256.28 | 0.0 | 0.0 | 0.0 | 0.0 |
| 2 | 28 | 01 March 2025 | 02 January 2025 | 504.98 | 248.74 | 7.54 | 0.0 | 0.0 | 256.28 | 0.0 | 0.0 | 0.0 | 0.0 |
| 3 | 31 | 01 April 2025 | 02 January 2025 | 253.75 | 251.23 | 5.05 | 0.0 | 0.0 | 256.28 | 0.0 | 0.0 | 0.0 | 0.0 |
| 4 | 30 | 01 May 2025 | 02 January 2025 | 0.0 | 253.75 | 2.54 | 0.0 | 0.0 | 256.29 | 0.0 | 0.0 | 0.0 | 0.0 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000 | 25.13 | 0 | 0 | 1025.13 | 0 | 0 | 0 | 0.0 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2025 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
| 02 January 2025 | Close (as written-off) | 1025.13 | 1000.0 | 25.13 | 0.0 | 0.0 | 0.0 | false | false |
And Loan Transactions tab has a "WRITE_OFF" transaction with date "02 January 2025" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | | 1000.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 25.13 |
| EXPENSE | e4 | Written off | 1025.13 | |
@@ -50,5 +50,6 @@ public class AccountingBridgeDataDTO {
private boolean merchantBuyDownFee;
private List<AdvancedMappingtDTO> buydownFeeClassificationCodeValue;
private List<AdvancedMappingtDTO> capitalizedIncomeClassificationCodeValue;
private AdvancedMappingtDTO writeOffReasonCodeValue;
}
@@ -50,4 +50,5 @@ public class LoanDTO {
private boolean merchantBuyDownFee;
private List<AdvancedMappingtDTO> buydownFeeAdvancedMappingData;
private List<AdvancedMappingtDTO> capitalizedIncomeAdvancedMappingData;
private AdvancedMappingtDTO writeOffReasonAdvancedMappingData;
}
@@ -174,7 +174,7 @@ public class AccountingProcessorHelper {
periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud,
chargeOffReasonCodeValue, isLoanMarkedAsWrittenOff, merchantBuyDownFee,
accountingBridgeData.getBuydownFeeClassificationCodeValue(),
accountingBridgeData.getCapitalizedIncomeClassificationCodeValue());
accountingBridgeData.getCapitalizedIncomeClassificationCodeValue(), accountingBridgeData.getWriteOffReasonCodeValue());
}
public ProductToGLAccountMapping getChargeOffMappingByCodeValue(Long loanProductId, PortfolioProductType productType,
@@ -182,6 +182,11 @@ public class AccountingProcessorHelper {
return accountMappingRepository.findChargeOffReasonMapping(loanProductId, productType.getValue(), chargeOffReasonId);
}
public ProductToGLAccountMapping getWriteOffMappingByCodeValue(Long loanProductId, PortfolioProductType productType,
Long writeOffReasonId) {
return accountMappingRepository.findWriteOffReasonMapping(loanProductId, productType.getValue(), writeOffReasonId);
}
public ProductToGLAccountMapping getClassificationMappingByCodeValue(Long loanProductId, PortfolioProductType productType,
final Long classificationId, final String classificationType) {
if (LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue().equals(classificationType)) {
@@ -937,7 +942,7 @@ public class AccountingProcessorHelper {
persistJournalEntry(journalEntry);
}
private void createDebitJournalEntryForLoan(final Office office, final String currencyCode, final GLAccount account, final Long loanId,
public void createDebitJournalEntryForLoan(final Office office, final String currencyCode, final GLAccount account, final Long loanId,
final String transactionId, final LocalDate transactionDate, final BigDecimal amount) {
final boolean manualEntry = false;
Long loanTransactionId = null;
@@ -1913,8 +1913,20 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
* 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);
final AdvancedMappingtDTO writeAdvancedMappingtDTO = loanDTO.getWriteOffReasonAdvancedMappingData();
final ProductToGLAccountMapping mapping = (writeAdvancedMappingtDTO != null
&& writeAdvancedMappingtDTO.getReferenceValueId() != null)
? helper.getWriteOffMappingByCodeValue(loanProductId, PortfolioProductType.LOAN,
writeAdvancedMappingtDTO.getReferenceValueId())
: null;
if (mapping == null) {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, AccrualAccountsForLoan.LOSSES_WRITTEN_OFF.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount);
} else {
this.helper.createDebitJournalEntryForLoan(office, currencyCode, mapping.getGlAccount(), loanId, transactionId,
transactionDate, totalDebitAmount);
}
}
}
@@ -872,13 +872,17 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
} else if (loanTransaction.isCapitalizedIncomeAmortization()) {
capitalizedIncomeAdvancedMappingData = getLoanTransactionClassificationId(loanTransaction);
}
AdvancedMappingtDTO writeOffReasonAdvancedMappingData = null;
if (loan.isClosedWrittenOff() && loan.getWriteOffReason() != null) {
writeOffReasonAdvancedMappingData = new AdvancedMappingtDTO(loan.getWriteOffReason().getId(), BigDecimal.ZERO);
}
return new AccountingBridgeDataDTO(loan.getId(), loan.productId(), loan.getOfficeId(), currencyCode,
loan.getSummary().getTotalInterestCharged(), loan.isCashBasedAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(),
isAccountTransfer, wasChargedOffAtTransactionTime, loan.isFraud(), loan.fetchChargeOffReasonId(), loan.isClosedWrittenOff(),
transactions, loan.getLoanProductRelatedDetail().isMerchantBuyDownFee(), buydownFeeAdvancedMappingData,
capitalizedIncomeAdvancedMappingData);
capitalizedIncomeAdvancedMappingData, writeOffReasonAdvancedMappingData);
}
private List<AdvancedMappingtDTO> getLoanTransactionClassificationId(final LoanTransaction loanTransaction) {
@@ -73,7 +73,7 @@ class CreateJournalEntriesForChargeOffLoanTest {
Collections.emptyList(), false, "", null, null, null, null);
loanDTO = new LoanDTO(1L, 1L, 1L, "USD", false, true, true, List.of(loanTransactionDTO), false, false, chargeOffReasonId, false,
false, null, null);
false, null, null, null);
}
@Test