From 3b62d562a243abb9fbfeaa7eb9d9b003e3200844 Mon Sep 17 00:00:00 2001 From: Peter Kovacs Date: Fri, 3 Oct 2025 14:56:58 +0200 Subject: [PATCH] FINERACT-2358: Advanced-accounting-rules-based-on-write-off-reason E2E tests --- .../ProductToGLAccountMappingRepository.java | 4 + .../test/data/codevalue/DefaultCodeValue.java | 2 +- .../data/loanproduct/DefaultLoanProduct.java | 1 + .../global/CodeGlobalInitializerStep.java | 12 ++- .../LoanProductGlobalInitializerStep.java | 59 ++++++++++- .../test/stepdef/loan/LoanStepDef.java | 22 +++++ .../fineract/test/support/TestContextKey.java | 1 + .../resources/features/LoanWriteOff.feature | 99 +++++++++++++++++++ .../data/AccountingBridgeDataDTO.java | 1 + .../accounting/journalentry/data/LoanDTO.java | 1 + .../service/AccountingProcessorHelper.java | 9 +- ...ccrualBasedAccountingProcessorForLoan.java | 16 ++- ...WritePlatformServiceJpaRepositoryImpl.java | 6 +- ...ateJournalEntriesForChargeOffLoanTest.java | 2 +- 14 files changed, 226 insertions(+), 9 deletions(-) diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java index 0ad7c3d15..885e59321 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java @@ -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); + } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/codevalue/DefaultCodeValue.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/codevalue/DefaultCodeValue.java index 907250345..59d77da1e 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/codevalue/DefaultCodeValue.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/codevalue/DefaultCodeValue.java @@ -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; diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java index a6ce2ab50..e7e8f4674 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java @@ -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 diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/CodeGlobalInitializerStep.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/CodeGlobalInitializerStep.java index f269f9df9..9a2c9f5b3 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/CodeGlobalInitializerStep.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/CodeGlobalInitializerStep.java @@ -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 rescheduleReasonNames = new ArrayList<>(); rescheduleReasonNames.add(CODE_VALUE_RESCHEDULE_REASON_TEST); createCodeValues(CODE_VALUE_RESCHEDULE_REASON_ID, rescheduleReasonNames); + + // add Write-off reasons + List 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 codeValueNames) { diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java index 319affea7..337acc99b 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java @@ -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 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 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, diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java index 841e9f4f6..8aed996dc 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java @@ -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 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 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); diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java index d7c1d9eaa..bb8f239fd 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java @@ -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"; } diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanWriteOff.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanWriteOff.feature index 72bb86f96..d651caa4e 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanWriteOff.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanWriteOff.feature @@ -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 | | \ No newline at end of file diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccountingBridgeDataDTO.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccountingBridgeDataDTO.java index 1dbb6c4e1..789696a42 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccountingBridgeDataDTO.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccountingBridgeDataDTO.java @@ -50,5 +50,6 @@ public class AccountingBridgeDataDTO { private boolean merchantBuyDownFee; private List buydownFeeClassificationCodeValue; private List capitalizedIncomeClassificationCodeValue; + private AdvancedMappingtDTO writeOffReasonCodeValue; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java index c415e2a7f..45a45f92d 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java @@ -50,4 +50,5 @@ public class LoanDTO { private boolean merchantBuyDownFee; private List buydownFeeAdvancedMappingData; private List capitalizedIncomeAdvancedMappingData; + private AdvancedMappingtDTO writeOffReasonAdvancedMappingData; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java index 2db1687b0..21e81186b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java @@ -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; diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java index b81b40aab..8216d59bd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java @@ -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); + } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java index c4f589c2f..dad040d9f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryWritePlatformServiceJpaRepositoryImpl.java @@ -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 getLoanTransactionClassificationId(final LoanTransaction loanTransaction) { diff --git a/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java b/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java index 05faf8f60..440eb8dd0 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java @@ -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