diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index f4a0dc6e19d..d96301cbf7d 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -1147,6 +1147,15 @@ public CommandWrapperBuilder createWorkingCapitalLoanCharge(final Long loanId) { return this; } + public CommandWrapperBuilder adjustmentForWorkingCapitalLoanCharge(final Long loanId, final Long loanChargeId) { + this.actionName = ACTION_ADJUSTMENT; + this.entityName = ENTITY_WORKINGCAPITALLOANCHARGE; + this.entityId = loanChargeId; + this.loanId = loanId; + this.href = "/working-capital-loans/" + loanId + "/charges/" + loanChargeId; + return this; + } + public CommandWrapperBuilder updateLoanCharge(final Long loanId, final Long loanChargeId) { this.actionName = ACTION_UPDATE; this.entityName = ENTITY_LOANCHARGE; diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalChargeStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalChargeStepDef.java index f5dd380380c..52a5af0344f 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalChargeStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalChargeStepDef.java @@ -28,6 +28,7 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -39,10 +40,15 @@ import org.apache.fineract.client.models.EnumOptionData; import org.apache.fineract.client.models.GetBalance; import org.apache.fineract.client.models.GetChargesResponse; +import org.apache.fineract.client.models.GetWorkingCapitalLoanTransactionIdResponse; +import org.apache.fineract.client.models.GetWorkingCapitalLoanTransactionsResponse; import org.apache.fineract.client.models.GetWorkingCapitalLoansLoanIdResponse; import org.apache.fineract.client.models.PostChargesResponse; import org.apache.fineract.client.models.PostLoansLoanIdChargesRequest; import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse; +import org.apache.fineract.client.models.PostWorkingCapitalLoanTransactionsRequest; +import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdChargesChargeIdRequest; +import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdChargesChargeIdResponse; import org.apache.fineract.client.models.PostWorkingCapitalLoansResponse; import org.apache.fineract.client.models.WorkingCapitalLoanChargeData; import org.apache.fineract.test.data.ChargeCalculationType; @@ -409,6 +415,101 @@ public void createWclChargeWithInvalidParamsFails(String chargeTimeTypeName, Str chargeTimeTypeName, chargeCalcTypeName, exception.getStatus(), expectedErrorMessage); } + @When("Admin makes a charge adjustment for the last added charge with {double} amount on working capital loan") + public void makeWcChargeAdjustment(final Double amount) { + final Long loanId = getLoanId(); + final Long loanChargeId = getLastAddedLoanChargeId(); + final PostWorkingCapitalLoansLoanIdChargesChargeIdRequest request = new PostWorkingCapitalLoansLoanIdChargesChargeIdRequest() + .amount(amount).locale("en"); + final PostWorkingCapitalLoansLoanIdChargesChargeIdResponse response = ok( + () -> fineractClient.workingCapitalLoanCharges().adjustLoanCharge(loanId, loanChargeId, request, "adjustment")); + Assertions.assertNotNull(response); + testContext().set(TestContextKey.WORKING_CAPITAL_CHARGE_ADJUSTMENT_RESPONSE, response); + log.debug("WC charge adjustment response: {}", response); + } + + @When("Admin makes a charge adjustment for the last added fee charge with {double} amount on working capital loan") + public void makeWcFeeChargeAdjustment(final Double amount) { + final Long loanId = getLoanId(); + final Long loanChargeId = getLastAddedFeeChargeId(loanId); + final PostWorkingCapitalLoansLoanIdChargesChargeIdRequest request = new PostWorkingCapitalLoansLoanIdChargesChargeIdRequest() + .amount(amount).locale("en"); + final PostWorkingCapitalLoansLoanIdChargesChargeIdResponse response = ok( + () -> fineractClient.workingCapitalLoanCharges().adjustLoanCharge(loanId, loanChargeId, request, "adjustment")); + Assertions.assertNotNull(response); + testContext().set(TestContextKey.WORKING_CAPITAL_CHARGE_ADJUSTMENT_RESPONSE, response); + log.debug("WC fee charge adjustment response: {}", response); + } + + @When("Admin makes a charge adjustment for the last added penalty charge with {double} amount on working capital loan") + public void makeWcPenaltyChargeAdjustment(final Double amount) { + final Long loanId = getLoanId(); + final Long loanChargeId = getLastAddedPenaltyChargeId(loanId); + final PostWorkingCapitalLoansLoanIdChargesChargeIdRequest request = new PostWorkingCapitalLoansLoanIdChargesChargeIdRequest() + .amount(amount).locale("en"); + final PostWorkingCapitalLoansLoanIdChargesChargeIdResponse response = ok( + () -> fineractClient.workingCapitalLoanCharges().adjustLoanCharge(loanId, loanChargeId, request, "adjustment")); + Assertions.assertNotNull(response); + testContext().set(TestContextKey.WORKING_CAPITAL_CHARGE_ADJUSTMENT_RESPONSE, response); + log.debug("WC penalty charge adjustment response: {}", response); + } + + @When("Admin makes a charge adjustment for the last added charge with {double} amount and transaction date {string} on working capital loan") + public void makeWcChargeAdjustmentWithDate(final Double amount, final String transactionDate) { + final Long loanId = getLoanId(); + final Long loanChargeId = getLastAddedLoanChargeId(); + final LocalDate parsedDate = LocalDate.parse(transactionDate, FORMATTER); + final PostWorkingCapitalLoansLoanIdChargesChargeIdRequest request = new PostWorkingCapitalLoansLoanIdChargesChargeIdRequest() + .amount(amount).transactionDate(parsedDate.format(FORMATTER_API)).dateFormat(DATE_FORMAT_API).locale("en"); + final PostWorkingCapitalLoansLoanIdChargesChargeIdResponse response = ok( + () -> fineractClient.workingCapitalLoanCharges().adjustLoanCharge(loanId, loanChargeId, request, "adjustment")); + Assertions.assertNotNull(response); + testContext().set(TestContextKey.WORKING_CAPITAL_CHARGE_ADJUSTMENT_RESPONSE, response); + log.debug("WC charge adjustment with date response: {}", response); + } + + @Then("Making a charge adjustment with {double} amount on working capital loan results an error with the following data:") + public void makeWcChargeAdjustmentFails(final Double amount, final DataTable table) { + final Long loanId = getLoanId(); + final Long loanChargeId = getLastAddedLoanChargeId(); + final PostWorkingCapitalLoansLoanIdChargesChargeIdRequest request = new PostWorkingCapitalLoansLoanIdChargesChargeIdRequest() + .amount(amount).locale("en"); + final Map expectedData = table.asMaps().get(0); + final int expectedHttpCode = Integer.parseInt(expectedData.get("httpCode")); + final String expectedErrorMessage = expectedData.get("errorMessage").trim(); + final CallFailedRuntimeException exception = fail( + () -> fineractClient.workingCapitalLoanCharges().adjustLoanCharge(loanId, loanChargeId, request, "adjustment")); + assertHttpStatus(exception, expectedHttpCode); + assertErrorMessage(exception, expectedErrorMessage); + log.info("Verified WC charge adjustment failed with status {} and message: {}", exception.getStatus(), expectedErrorMessage); + } + + @When("Admin reverts the last charge adjustment on working capital loan") + public void revertLastWcChargeAdjustment() { + final Long loanId = getLoanId(); + final GetWorkingCapitalLoanTransactionIdResponse adjustmentTxn = getLastChargeAdjustmentTransaction(loanId, false); + final PostWorkingCapitalLoanTransactionsRequest request = new PostWorkingCapitalLoanTransactionsRequest(); + ok(() -> fineractClient.workingCapitalLoanTransactions().executeWorkingCapitalLoanTransactionCommandById(loanId, + adjustmentTxn.getId(), "undo", request)); + log.debug("Reverted WC charge adjustment transaction id={} on loan {}", adjustmentTxn.getId(), loanId); + } + + @Then("Reverting an already reversed charge adjustment on working capital loan results an error with the following data:") + public void revertAlreadyRevertedWcChargeAdjustmentFails(final DataTable table) { + final Long loanId = getLoanId(); + final GetWorkingCapitalLoanTransactionIdResponse adjustmentTxn = getLastChargeAdjustmentTransaction(loanId, null); + final PostWorkingCapitalLoanTransactionsRequest request = new PostWorkingCapitalLoanTransactionsRequest(); + final Map expectedData = table.asMaps().get(0); + final int expectedHttpCode = Integer.parseInt(expectedData.get("httpCode")); + final String expectedErrorMessage = expectedData.get("errorMessage").trim(); + final CallFailedRuntimeException exception = fail(() -> fineractClient.workingCapitalLoanTransactions() + .executeWorkingCapitalLoanTransactionCommandById(loanId, adjustmentTxn.getId(), "undo", request)); + assertHttpStatus(exception, expectedHttpCode); + assertErrorMessage(exception, expectedErrorMessage); + log.info("Verified reverting already reversed WC charge adjustment failed with status {} and message: {}", expectedHttpCode, + expectedErrorMessage); + } + @Then("Trying to add working capital loan charge by loan id and charge id with amount {double} and due date {string} results an error with the following data:") public void tryAddWorkingCapitalLoanChargeWithError(Double amount, String dueDate, DataTable table) { Long loanId = getLoanId(); @@ -431,6 +532,43 @@ public void tryAddWorkingCapitalLoanChargeWithError(Double amount, String dueDat expectedErrorMessage); } + // Charge Adjustment Helpers + private Long getLastAddedLoanChargeId() { + final PostLoansLoanIdChargesResponse response = testContext().get(TestContextKey.ADD_DUE_DATE_CHARGE_WORKING_CAPITAL_RESPONSE); + Assertions.assertNotNull(response, "No charge has been added to the working capital loan yet"); + return response.getResourceId(); + } + + private Long getLastAddedFeeChargeId(final Long loanId) { + return getLastAddedChargeIdByPenaltyFlag(loanId, false); + } + + private Long getLastAddedPenaltyChargeId(final Long loanId) { + return getLastAddedChargeIdByPenaltyFlag(loanId, true); + } + + private Long getLastAddedChargeIdByPenaltyFlag(final Long loanId, final boolean isPenalty) { + final List charges = ok( + () -> fineractClient.workingCapitalLoanCharges().retrieveAllWorkingCapitalLoanChargesByLoanId(loanId)); + Assertions.assertNotNull(charges, "No charges found on loan " + loanId); + final String chargeType = isPenalty ? "penalty" : "fee"; + return charges.stream().filter(c -> isPenalty == Boolean.TRUE.equals(c.getPenalty())) + .max(Comparator.comparing(WorkingCapitalLoanChargeData::getId)).map(WorkingCapitalLoanChargeData::getId) + .orElseThrow(() -> new IllegalStateException("No active " + chargeType + " charge found on loan " + loanId)); + } + + private GetWorkingCapitalLoanTransactionIdResponse getLastChargeAdjustmentTransaction(final Long loanId, + final Boolean excludeReversed) { + final GetWorkingCapitalLoanTransactionsResponse body = ok( + () -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanTransactionsById(loanId)); + Assertions.assertNotNull(body.getContent(), "No WC loan transactions found"); + return body.getContent().stream() + .filter(t -> t.getType() != null && "loanTransactionType.chargeAdjustment".equals(t.getType().getCode())) + .filter(t -> excludeReversed == null || !Boolean.TRUE.equals(t.getReversed())) + .max(Comparator.comparing(GetWorkingCapitalLoanTransactionIdResponse::getId)) + .orElseThrow(() -> new IllegalStateException("No charge adjustment transaction found on loan " + loanId)); + } + // Charge API Helpers private void createChargeAndStore(final ChargeRequest request) { final PostChargesResponse response = ok(() -> fineractClient.charges().createCharge(request)); diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java index 73d35035d07..8a29ed15b8b 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java @@ -163,7 +163,8 @@ public void createClientAndDisburseWorkingCapitalLoanWithData(final DataTable ta // Disburse loan using existing helper method final PostWorkingCapitalLoansLoanIdRequest disburseRequest = workingCapitalLoanRequestFactory .defaultWorkingCapitalLoanDisburseRequest()// - .actualDisbursementDate(submittedOnDate); + .actualDisbursementDate(submittedOnDate)// + .transactionAmount(new BigDecimal(principalAmount)); executeStateTransition("disburse", disburseRequest, TestContextKey.LOAN_DISBURSE_RESPONSE, false); // Verify loan is ACTIVE 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 66823316ba8..fbacecc9135 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 @@ -372,6 +372,7 @@ public abstract class TestContextKey { public static final String WORKING_CAPITAL_CHARGE_ID = "workingCapitalChargeId"; public static final String WORKING_CAPITAL_LOAN_CHARGE_IDS = "workingCapitalLoanChargeIds"; public static final String WORKING_CAPITAL_CHARGE_TEMPLATE = "workingCapitalChargeTemplate"; + public static final String WORKING_CAPITAL_CHARGE_ADJUSTMENT_RESPONSE = "workingCapitalChargeAdjustmentResponse"; public static final String WORKING_CAPITAL_LOAN_DISBURSE_DISCOUNT_EXTERNAL_ID_USER_GENERATED = "workingCapitalLoanDisburseDiscountExternalIdUserGenerated"; public static final String WORKING_CAPITAL_LOAN_DISCOUNT_FEE_EXTERNAL_ID_USER_GENERATED = "workingCapitalLoanDiscountFeeExternalIdUserGenerated"; public static final String WORKING_CAPITAL_LOAN_DISCOUNT_FEE_RESPONSE = "workingCapitalLoanDiscountFeeResponse"; diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAdjustment.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAdjustment.feature new file mode 100644 index 00000000000..96e8f06f847 --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAdjustment.feature @@ -0,0 +1,327 @@ +@WorkingCapital +@WorkingCapitalLoanChargeAdjustmentFeature +Feature: WorkingCapitalLoanChargeAdjustmentFeature + + @TestRailId:C85221 + Scenario: Verify Working Capital fee charge adjustment - UC1: full fee charge adjustment is processed successfully + Given Admin sets the business date to "01 January 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "10 January 2026" due date and 100.0 transaction amount + Then Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | + When Admin makes a charge adjustment for the last added charge with 100.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Charge Adjustment | 100.0 | 0.0 | 100.0 | 0.0 | false | + And Working Capital Loan has charges with the following data: + | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | + | Working Capital Loan Fee | 10 January 2026 | 100.0 | EUR | false | Specified due date | Flat | Regular | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | + + @TestRailId:C85222 + Scenario: Verify Working Capital penalty charge adjustment - UC2: full penalty charge adjustment is processed successfully + Given Admin sets the business date to "01 January 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_PENALTY" specified due date charge to working capital loan with "10 January 2026" due date and 50.0 transaction amount + Then Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 0.0 | 0.0 | 0.0 | 50.0 | 50.0 | 0.0 | + When Admin makes a charge adjustment for the last added charge with 50.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Charge Adjustment | 50.0 | 0.0 | 0.0 | 50.0 | false | + And Working Capital Loan has charges with the following data: + | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | + | Working Capital Loan Penalty | 10 January 2026 | 50.0 | EUR | true | Specified due date | Flat | Regular | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 0.0 | 0.0 | 0.0 | 50.0 | 0.0 | 50.0 | + + @TestRailId:C85223 + Scenario: Verify Working Capital charge adjustment - UC3: partial fee charge adjustment leaves correct outstanding + Given Admin sets the business date to "01 January 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "10 January 2026" due date and 100.0 transaction amount + When Admin makes a charge adjustment for the last added charge with 40.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Charge Adjustment | 40.0 | 0.0 | 40.0 | 0.0 | false | + And Working Capital Loan has charges with the following data: + | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | + | Working Capital Loan Fee | 10 January 2026 | 100.0 | EUR | false | Specified due date | Flat | Regular | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 60.0 | 40.0 | 0.0 | 0.0 | 0.0 | + + @TestRailId:C85224 + Scenario: Verify Working Capital charge adjustment - UC4: partial penalty charge adjustment leaves correct outstanding + Given Admin sets the business date to "01 January 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_PENALTY" specified due date charge to working capital loan with "10 January 2026" due date and 60.0 transaction amount + When Admin makes a charge adjustment for the last added charge with 25.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Charge Adjustment | 25.0 | 0.0 | 0.0 | 25.0 | false | + And Working Capital Loan has charges with the following data: + | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | + | Working Capital Loan Penalty | 10 January 2026 | 60.0 | EUR | true | Specified due date | Flat | Regular | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 0.0 | 0.0 | 0.0 | 60.0 | 35.0 | 25.0 | + + @TestRailId:C85225 + Scenario: Verify Working Capital charge adjustment - UC5: two partial adjustments sum to full charge amount + Given Admin sets the business date to "01 January 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "10 January 2026" due date and 100.0 transaction amount + When Admin makes a charge adjustment for the last added charge with 60.0 amount on working capital loan + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 40.0 | 60.0 | 0.0 | 0.0 | 0.0 | + When Admin makes a charge adjustment for the last added charge with 40.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Charge Adjustment | 60.0 | 0.0 | 60.0 | 0.0 | false | + | 10 January 2026 | Charge Adjustment | 40.0 | 0.0 | 40.0 | 0.0 | false | + And Working Capital Loan has charges with the following data: + | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | + | Working Capital Loan Fee | 10 January 2026 | 100.0 | EUR | false | Specified due date | Flat | Regular | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | + + @TestRailId:C85226 + Scenario: Verify Working Capital charge adjustment - UC6: two partial adjustments on different days sum to full charge amount + Given Admin sets the business date to "01 January 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "10 January 2026" due date and 100.0 transaction amount + When Admin sets the business date to "11 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin makes a charge adjustment for the last added charge with 60.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 11 January 2026 | Charge Adjustment | 60.0 | 0.0 | 60.0 | 0.0 | false | + And Working Capital Loan has charges with the following data: + | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | + | Working Capital Loan Fee | 10 January 2026 | 100.0 | EUR | false | Specified due date | Flat | Regular | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 40.0 | 60.0 | 0.0 | 0.0 | 0.0 | + When Admin sets the business date to "12 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin makes a charge adjustment for the last added charge with 40.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 11 January 2026 | Charge Adjustment | 60.0 | 0.0 | 60.0 | 0.0 | false | + | 12 January 2026 | Charge Adjustment | 40.0 | 0.0 | 40.0 | 0.0 | false | + And Working Capital Loan has charges with the following data: + | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | + | Working Capital Loan Fee | 10 January 2026 | 100.0 | EUR | false | Specified due date | Flat | Regular | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | + + @TestRailId:C85227 + Scenario: Verify Working Capital charge adjustment - UC7: fee and penalty charges adjusted independently + Given Admin sets the business date to "01 January 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "10 January 2026" due date and 80.0 transaction amount + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_PENALTY" specified due date charge to working capital loan with "10 January 2026" due date and 30.0 transaction amount + When Admin makes a charge adjustment for the last added fee charge with 80.0 amount on working capital loan + And Admin makes a charge adjustment for the last added penalty charge with 30.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Charge Adjustment | 80.0 | 0.0 | 80.0 | 0.0 | false | + | 10 January 2026 | Charge Adjustment | 30.0 | 0.0 | 0.0 | 30.0 | false | + And Working Capital Loan has charges with the following data: + | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | + | Working Capital Loan Fee | 10 January 2026 | 80.0 | EUR | false | Specified due date | Flat | Regular | + | Working Capital Loan Penalty | 10 January 2026 | 30.0 | EUR | true | Specified due date | Flat | Regular | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 80.0 | 0.0 | 80.0 | 30.0 | 0.0 | 30.0 | + + @TestRailId:C85228 + Scenario: Verify Working Capital charge adjustment - UC8: fee and penalty charges adjusted independently on different days + Given Admin sets the business date to "01 January 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "10 January 2026" due date and 80.0 transaction amount + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_PENALTY" specified due date charge to working capital loan with "10 January 2026" due date and 30.0 transaction amount + When Admin sets the business date to "11 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin makes a charge adjustment for the last added fee charge with 80.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 11 January 2026 | Charge Adjustment | 80.0 | 0.0 | 80.0 | 0.0 | false | + And Working Capital Loan has charges with the following data: + | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | + | Working Capital Loan Fee | 10 January 2026 | 80.0 | EUR | false | Specified due date | Flat | Regular | + | Working Capital Loan Penalty | 10 January 2026 | 30.0 | EUR | true | Specified due date | Flat | Regular | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 80.0 | 0.0 | 80.0 | 30.0 | 30.0 | 0.0 | + When Admin sets the business date to "12 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin makes a charge adjustment for the last added penalty charge with 30.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 11 January 2026 | Charge Adjustment | 80.0 | 0.0 | 80.0 | 0.0 | false | + | 12 January 2026 | Charge Adjustment | 30.0 | 0.0 | 0.0 | 30.0 | false | + And Working Capital Loan has charges with the following data: + | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | + | Working Capital Loan Fee | 10 January 2026 | 80.0 | EUR | false | Specified due date | Flat | Regular | + | Working Capital Loan Penalty | 10 January 2026 | 30.0 | EUR | true | Specified due date | Flat | Regular | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 80.0 | 0.0 | 80.0 | 30.0 | 0.0 | 30.0 | + + @TestRailId:C85229 + Scenario: Verify Working Capital charge adjustment - UC9: adjustment amount exceeding charge amount results an error (Negative) + Given Admin sets the business date to "01 January 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "10 January 2026" due date and 100.0 transaction amount + Then Making a charge adjustment with 101.0 amount on working capital loan results an error with the following data: + | httpCode | errorMessage | + | 403 | Transaction amount cannot be higher than the charge amount | + + @TestRailId:C85230 + Scenario: Verify Working Capital charge adjustment - UC10: second adjustment exceeding remaining available amount results an error (Negative) + Given Admin sets the business date to "01 January 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "10 January 2026" due date and 100.0 transaction amount + When Admin makes a charge adjustment for the last added charge with 90.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Charge Adjustment | 90.0 | 0.0 | 90.0 | 0.0 | false | + And Working Capital Loan has charges with the following data: + | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | + | Working Capital Loan Fee | 10 January 2026 | 100.0 | EUR | false | Specified due date | Flat | Regular | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 10.0 | 90.0 | 0.0 | 0.0 | 0.0 | + Then Making a charge adjustment with 11.0 amount on working capital loan results an error with the following data: + | httpCode | errorMessage | + | 403 | Transaction amount cannot be higher than the available charge amount for adjustment | + + @TestRailId:C85231 + Scenario: Verify Working Capital charge adjustment - UC11: reversal of charge adjustment restores balances + Given Admin sets the business date to "01 January 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "10 January 2026" due date and 100.0 transaction amount + When Admin makes a charge adjustment for the last added charge with 100.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Charge Adjustment | 100.0 | 0.0 | 100.0 | 0.0 | false | + And Working Capital Loan has charges with the following data: + | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | + | Working Capital Loan Fee | 10 January 2026 | 100.0 | EUR | false | Specified due date | Flat | Regular | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | + When Admin reverts the last charge adjustment on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Charge Adjustment | 100.0 | 0.0 | 100.0 | 0.0 | true | + And Working Capital Loan has charges with the following data: + | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | + | Working Capital Loan Fee | 10 January 2026 | 100.0 | EUR | false | Specified due date | Flat | Regular | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | + + @TestRailId:C85232 + Scenario: Verify Working Capital charge adjustment - UC12: reversal of already reversed adjustment results an error (Negative) + Given Admin sets the business date to "01 January 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "10 January 2026" due date and 100.0 transaction amount + When Admin makes a charge adjustment for the last added charge with 100.0 amount on working capital loan + And Admin reverts the last charge adjustment on working capital loan + Then Reverting an already reversed charge adjustment on working capital loan results an error with the following data: + | httpCode | errorMessage | + | 400 | Charge adjustment transaction is already reversed | + + @TestRailId:C85233 + Scenario: Verify Working Capital charge adjustment - UC13: charge adjustment with explicit transaction date + Given Admin sets the business date to "01 January 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "10 January 2026" due date and 100.0 transaction amount + When Admin sets the business date to "15 January 2026" + And Admin makes a charge adjustment for the last added charge with 100.0 amount and transaction date "10 January 2026" on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Charge Adjustment | 100.0 | 0.0 | 100.0 | 0.0 | false | + And Working Capital Loan has charges with the following data: + | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | + | Working Capital Loan Fee | 10 January 2026 | 100.0 | EUR | false | Specified due date | Flat | Regular | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForWorkingCapitalLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForWorkingCapitalLoan.java index 2a69ceea159..2c02b717206 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForWorkingCapitalLoan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForWorkingCapitalLoan.java @@ -84,6 +84,8 @@ public void postJournalEntries(final WorkingCapitalLoan loan, final WorkingCapit } } case LoanTransactionType.CREDIT_BALANCE_REFUND -> postCreditBalanceRefundJournalEntries(loan, txn); + case LoanTransactionType.CHARGE_ADJUSTMENT -> + postChargeAdjustmentJournalEntries(loan, txn, feesPortion, penaltiesPortion, isChargedOff); default -> { throw new NotImplementedException( "Post Journal Entries is not implemented yet for " + txn.getTypeOf().getCode() + " for Working Capital Loan"); @@ -91,6 +93,23 @@ public void postJournalEntries(final WorkingCapitalLoan loan, final WorkingCapit } } + private void postChargeAdjustmentJournalEntries(final WorkingCapitalLoan loan, final WorkingCapitalLoanTransaction txn, + final BigDecimal feesPortion, final BigDecimal penaltiesPortion, final boolean isChargedOff) { + final JournalEntryPostingHelper accountPostHelper = new JournalEntryPostingHelper(loan, txn); + final CashAccountsForLoan feeIncomeAccountType = isChargedOff ? CashAccountsForLoan.INCOME_FROM_RECOVERY + : CashAccountsForLoan.INCOME_FROM_FEES; + final CashAccountsForLoan penaltyIncomeAccountType = isChargedOff ? CashAccountsForLoan.INCOME_FROM_RECOVERY + : CashAccountsForLoan.INCOME_FROM_PENALTIES; + + // Debit income (reverse income recognized at accrual time) + accountPostHelper.postDebitJournalEntry(feeIncomeAccountType, feesPortion); + accountPostHelper.postDebitJournalEntry(penaltyIncomeAccountType, penaltiesPortion); + + // Credit receivable (reduce the outstanding charge) + accountPostHelper.postCreditJournalEntry(CashAccountsForLoan.FEES_RECEIVABLE, feesPortion); + accountPostHelper.postCreditJournalEntry(CashAccountsForLoan.PENALTIES_RECEIVABLE, penaltiesPortion); + } + private void postGoodwillCreditJournalEntries(WorkingCapitalLoan loan, WorkingCapitalLoanTransaction txn, BigDecimal principalPortion, BigDecimal feesPortion, BigDecimal penaltiesPortion, BigDecimal overpaymentPortion) { BigDecimal overpaymentPlusPrincipal = principalPortion.add(overpaymentPortion); diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java index 568a1e254cd..78673e840f1 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java @@ -116,8 +116,8 @@ public void givenAllConfigurationWhenValidatedThenValidationSuccessful() throws "SavingsAccountsStayedLockedBusinessEvent", "SavingsAccountForceWithdrawalBusinessEvent", "WorkingCapitalLoanDisbursalTransactionBusinessEvent", "WorkingCapitalLoanUndoDisbursalTransactionBusinessEvent", "WorkingCapitalLoanRepaymentTransactionBusinessEvent", "WorkingCapitalLoanDiscountFeeTransactionBusinessEvent", - "WorkingCapitalLoanDiscountFeeAdjustmentTransactionBusinessEvent", - "WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent"); + "WorkingCapitalLoanDiscountFeeAdjustmentTransactionBusinessEvent", "WorkingCapitalLoanChargeAdjustmentPreBusinessEvent", + "WorkingCapitalLoanChargeAdjustmentPostBusinessEvent", "WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent"); List tenants = Arrays .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/transaction/WorkingCapitalLoanChargeAdjustmentPostBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/transaction/WorkingCapitalLoanChargeAdjustmentPostBusinessEvent.java new file mode 100644 index 00000000000..92cbb5e355c --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/transaction/WorkingCapitalLoanChargeAdjustmentPostBusinessEvent.java @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction; + +public class WorkingCapitalLoanChargeAdjustmentPostBusinessEvent extends WorkingCapitalLoanTransactionBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanChargeAdjustmentPostBusinessEvent"; + + public WorkingCapitalLoanChargeAdjustmentPostBusinessEvent(final WorkingCapitalLoanTransaction value) { + super(value); + } + + public WorkingCapitalLoanChargeAdjustmentPostBusinessEvent(final WorkingCapitalLoanTransaction value, final Long aggregateRootId) { + super(value, aggregateRootId); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/transaction/WorkingCapitalLoanChargeAdjustmentPreBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/transaction/WorkingCapitalLoanChargeAdjustmentPreBusinessEvent.java new file mode 100644 index 00000000000..8bbb8155446 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/transaction/WorkingCapitalLoanChargeAdjustmentPreBusinessEvent.java @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction; + +public class WorkingCapitalLoanChargeAdjustmentPreBusinessEvent extends WorkingCapitalLoanTransactionBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanChargeAdjustmentPreBusinessEvent"; + + public WorkingCapitalLoanChargeAdjustmentPreBusinessEvent(final WorkingCapitalLoanTransaction value) { + super(value); + } + + public WorkingCapitalLoanChargeAdjustmentPreBusinessEvent(final WorkingCapitalLoanTransaction value, final Long aggregateRootId) { + super(value, aggregateRootId); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanChargesApiResource.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanChargesApiResource.java index 59c6725fdd5..10b620d715c 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanChargesApiResource.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanChargesApiResource.java @@ -27,11 +27,13 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.UriInfo; @@ -42,6 +44,8 @@ import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException; +import org.apache.fineract.infrastructure.core.service.CommandParameterUtil; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.charge.data.ChargeData; @@ -50,6 +54,7 @@ import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanChargeData; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanChargeRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; +import org.apache.fineract.portfolio.workingcapitalloan.serialization.WorkingCapitalLoanChargeConstants; import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanChargeReadPlatformService; import org.springframework.stereotype.Component; @@ -115,6 +120,61 @@ public CommandProcessingResult executeLoanCharge( return handleExecuteLoanCharge(null, loanExternalId, "create", apiRequestBodyAsJson); } + @POST + @Path("{loanId}/charges/{loanChargeId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Adjust a Working Capital Loan Charge", description = "Adjusts a working capital loan charge by creating a CHARGE_ADJUSTMENT transaction. Pass command=adjustment.") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanChargesApiResourceSwagger.PostWorkingCapitalLoansLoanIdChargesChargeIdResponse.class))) + public CommandProcessingResult adjustLoanCharge(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + @PathParam("loanChargeId") @Parameter(description = "loanChargeId") final Long loanChargeId, + @QueryParam("command") @DefaultValue("") @Parameter(description = "command") final String commandParam, + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanChargesApiResourceSwagger.PostWorkingCapitalLoansLoanIdChargesChargeIdRequest.class))) final String apiRequestBodyAsJson) { + return handleExecuteLoanChargeWithChargeId(loanId, null, loanChargeId, null, commandParam, apiRequestBodyAsJson); + } + + @POST + @Path("{loanId}/charges/external-id/{loanChargeExternalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Adjust a Working Capital Loan Charge by Charge External Id", description = "Adjusts a working capital loan charge by creating a CHARGE_ADJUSTMENT transaction. Pass command=adjustment.") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanChargesApiResourceSwagger.PostWorkingCapitalLoansLoanIdChargesChargeIdResponse.class))) + public CommandProcessingResult adjustLoanChargeByChargeExternalId( + @PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + @PathParam("loanChargeExternalId") @Parameter(description = "loanChargeExternalId") final String loanChargeExternalId, + @QueryParam("command") @DefaultValue("") @Parameter(description = "command") final String commandParam, + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanChargesApiResourceSwagger.PostWorkingCapitalLoansLoanIdChargesChargeIdRequest.class))) final String apiRequestBodyAsJson) { + return handleExecuteLoanChargeWithChargeId(loanId, null, null, loanChargeExternalId, commandParam, apiRequestBodyAsJson); + } + + @POST + @Path("external-id/{loanExternalId}/charges/{loanChargeId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Adjust a Working Capital Loan Charge by Loan External Id", description = "Adjusts a working capital loan charge by creating a CHARGE_ADJUSTMENT transaction. Pass command=adjustment.") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanChargesApiResourceSwagger.PostWorkingCapitalLoansLoanIdChargesChargeIdResponse.class))) + public CommandProcessingResult adjustLoanChargeByLoanExternalId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, + @PathParam("loanChargeId") @Parameter(description = "loanChargeId") final Long loanChargeId, + @QueryParam("command") @DefaultValue("") @Parameter(description = "command") final String commandParam, + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanChargesApiResourceSwagger.PostWorkingCapitalLoansLoanIdChargesChargeIdRequest.class))) final String apiRequestBodyAsJson) { + return handleExecuteLoanChargeWithChargeId(null, loanExternalId, loanChargeId, null, commandParam, apiRequestBodyAsJson); + } + + @POST + @Path("external-id/{loanExternalId}/charges/external-id/{loanChargeExternalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Adjust a Working Capital Loan Charge by Loan and Charge External Ids", description = "Adjusts a working capital loan charge by creating a CHARGE_ADJUSTMENT transaction. Pass command=adjustment.") + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanChargesApiResourceSwagger.PostWorkingCapitalLoansLoanIdChargesChargeIdResponse.class))) + public CommandProcessingResult adjustLoanChargeByLoanAndChargeExternalId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, + @PathParam("loanChargeExternalId") @Parameter(description = "loanChargeExternalId") final String loanChargeExternalId, + @QueryParam("command") @DefaultValue("") @Parameter(description = "command") final String commandParam, + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanChargesApiResourceSwagger.PostWorkingCapitalLoansLoanIdChargesChargeIdRequest.class))) final String apiRequestBodyAsJson) { + return handleExecuteLoanChargeWithChargeId(null, loanExternalId, null, loanChargeExternalId, commandParam, apiRequestBodyAsJson); + } + @GET @Path("{loanId}/charges") @Produces({ MediaType.APPLICATION_JSON }) @@ -238,4 +298,23 @@ private CommandProcessingResult handleExecuteLoanCharge(final Long loanId, final .withJson(apiRequestBodyAsJson).build(); return commandsSourceWritePlatformService.logCommandSource(commandRequest); } + + private CommandProcessingResult handleExecuteLoanChargeWithChargeId(final Long loanId, final String loanExternalIdStr, + final Long loanChargeId, final String loanChargeExternalIdStr, final String commandParam, final String apiRequestBodyAsJson) { + + final ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr); + final ExternalId loanChargeExternalId = ExternalIdFactory.produce(loanChargeExternalIdStr); + + final Long resolvedLoanId = loanId == null ? workingCapitalLoanRepository.findIdByExternalId(loanExternalId) : loanId; + final Long resolvedLoanChargeId = loanChargeId == null ? loanChargeRepository.findIdByExternalId(loanChargeExternalId) + : loanChargeId; + + if (!CommandParameterUtil.is(commandParam, WorkingCapitalLoanChargeConstants.ADJUSTMENT_LOAN_CHARGE_COMMAND)) { + throw new UnrecognizedQueryParamException("command", commandParam); + } + + final CommandWrapper commandRequest = new CommandWrapperBuilder() + .adjustmentForWorkingCapitalLoanCharge(resolvedLoanId, resolvedLoanChargeId).withJson(apiRequestBodyAsJson).build(); + return commandsSourceWritePlatformService.logCommandSource(commandRequest); + } } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanChargesApiResourceSwagger.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanChargesApiResourceSwagger.java index 72c563d13cc..cced78a0d36 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanChargesApiResourceSwagger.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanChargesApiResourceSwagger.java @@ -61,4 +61,44 @@ private PostLoansLoanIdChargesResponse() {} public String resourceExternalId; } + @Schema(description = "PostWorkingCapitalLoansLoanIdChargesChargeIdRequest") + public static final class PostWorkingCapitalLoansLoanIdChargesChargeIdRequest { + + private PostWorkingCapitalLoansLoanIdChargesChargeIdRequest() {} + + @Schema(example = "100.00") + public Double amount; + @Schema(example = "en") + public String locale; + @Schema(example = "dd MMMM yyyy") + public String dateFormat; + @Schema(example = "29 April 2013") + public String transactionDate; + @Schema(example = "786444UUUYYH7") + public String externalId; + @Schema(example = "some note") + public String note; + } + + @Schema(description = "PostWorkingCapitalLoansLoanIdChargesChargeIdResponse") + public static final class PostWorkingCapitalLoansLoanIdChargesChargeIdResponse { + + private PostWorkingCapitalLoansLoanIdChargesChargeIdResponse() {} + + @Schema(example = "1") + public Long officeId; + @Schema(example = "1") + public Long clientId; + @Schema(example = "1") + public Long loanId; + @Schema(example = "31") + public Long resourceId; + @Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7") + public String resourceExternalId; + @Schema(example = "12") + public Long subEntityId; + @Schema(example = "a1b2c3d4-0000-0000-0000-000000000000") + public String subEntityExternalId; + } + } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransaction.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransaction.java index 90ef9583efb..f1a38a51f14 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransaction.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransaction.java @@ -187,6 +187,13 @@ public static WorkingCapitalLoanTransaction discountFeeAdjustment(final WorkingC return transaction; } + public static WorkingCapitalLoanTransaction chargeAdjustment(final WorkingCapitalLoan loan, final ExternalId externalId, + final BigDecimal amount, final LocalDate transactionDate, final PaymentDetail paymentDetail) { + final WorkingCapitalLoanTransaction transaction = new WorkingCapitalLoanTransaction(); + transaction.initialize(loan, LoanTransactionType.CHARGE_ADJUSTMENT, transactionDate, amount, paymentDetail, null, externalId); + return transaction; + } + private void initialize(final WorkingCapitalLoan loan, final LoanTransactionType transactionType, final LocalDate transactionDate, final BigDecimal amount, final PaymentDetail paymentDetail, final CodeValue classification, final ExternalId externalId) { this.wcLoan = loan; diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionAllocation.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionAllocation.java index 686c4e69641..a83d78e6a07 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionAllocation.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionAllocation.java @@ -89,4 +89,14 @@ public static WorkingCapitalLoanTransactionAllocation forDiscountFeeAdjustment(f allocation.penaltyChargesPortion = BigDecimal.ZERO; return allocation; } + + public static WorkingCapitalLoanTransactionAllocation forChargeAdjustment(final WorkingCapitalLoanTransaction transaction, + final BigDecimal amount, final boolean isPenalty) { + final WorkingCapitalLoanTransactionAllocation allocation = new WorkingCapitalLoanTransactionAllocation(); + allocation.wcLoanTransaction = transaction; + allocation.principalPortion = BigDecimal.ZERO; + allocation.feeChargesPortion = isPenalty ? BigDecimal.ZERO : MathUtil.nullToZero(amount); + allocation.penaltyChargesPortion = isPenalty ? MathUtil.nullToZero(amount) : BigDecimal.ZERO; + return allocation; + } } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelation.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelation.java index 4f38bb8dc01..b9b40ce7f21 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelation.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelation.java @@ -21,6 +21,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; @@ -45,6 +46,11 @@ public class WorkingCapitalLoanTransactionRelation extends AbstractAuditableWith @JoinColumn(name = "to_loan_transaction_id") private WorkingCapitalLoanTransaction toTransaction; + @Setter + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "to_loan_charge_id") + private WorkingCapitalLoanCharge toCharge; + @Column(name = "relation_type_enum", nullable = false) @Convert(converter = LoanTransactionRelationTypeEnumConverter.class) private LoanTransactionRelationTypeEnum relationType; @@ -58,4 +64,12 @@ public WorkingCapitalLoanTransactionRelation(@NotNull WorkingCapitalLoanTransact this.relationType = relationType; } + public static WorkingCapitalLoanTransactionRelation linkToCharge(@NotNull WorkingCapitalLoanTransaction fromTransaction, + @NotNull WorkingCapitalLoanCharge charge, LoanTransactionRelationTypeEnum relationType) { + final WorkingCapitalLoanTransactionRelation relation = new WorkingCapitalLoanTransactionRelation(fromTransaction, null, + relationType); + relation.toCharge = charge; + return relation; + } + } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelationRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelationRepository.java index 6afa71ff1d0..32929b76b36 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelationRepository.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelationRepository.java @@ -35,4 +35,7 @@ Optional findByToTransactionAndFromTransa List findAllByToTransactionAndFromTransactionReversedAndFromTransactionTransactionType( WorkingCapitalLoanTransaction relatedDisbursementTransaction, boolean reversed, LoanTransactionType transactionType); + + List findAllByToChargeAndFromTransactionReversedAndFromTransactionTransactionType( + WorkingCapitalLoanCharge toCharge, boolean reversed, LoanTransactionType transactionType); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/exception/WorkingCapitalLoanChargeAdjustmentException.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/exception/WorkingCapitalLoanChargeAdjustmentException.java new file mode 100644 index 00000000000..ebc060058d1 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/exception/WorkingCapitalLoanChargeAdjustmentException.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class WorkingCapitalLoanChargeAdjustmentException extends AbstractPlatformDomainRuleException { + + public WorkingCapitalLoanChargeAdjustmentException(final String errorCode, final String defaultUserMessage, + final Object... defaultUserMessageArgs) { + super(errorCode, defaultUserMessage, defaultUserMessageArgs); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/exception/WorkingCapitalLoanChargeNotFoundException.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/exception/WorkingCapitalLoanChargeNotFoundException.java new file mode 100644 index 00000000000..c47d112610e --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/exception/WorkingCapitalLoanChargeNotFoundException.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class WorkingCapitalLoanChargeNotFoundException extends AbstractPlatformResourceNotFoundException { + + public WorkingCapitalLoanChargeNotFoundException(final Long id) { + super("error.msg.wc.loan.charge.id.invalid", "Working Capital Loan Charge with identifier " + id + " does not exist", id); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/WorkingCapitalLoanChargeAdjustmentCommandHandler.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/WorkingCapitalLoanChargeAdjustmentCommandHandler.java new file mode 100644 index 00000000000..42e8b1f63e5 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/WorkingCapitalLoanChargeAdjustmentCommandHandler.java @@ -0,0 +1,43 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.domain.CommandWrapperConstants; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanChargeWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = CommandWrapperConstants.ENTITY_WORKINGCAPITALLOANCHARGE, action = CommandWrapperConstants.ACTION_ADJUSTMENT) +public class WorkingCapitalLoanChargeAdjustmentCommandHandler implements NewCommandSourceHandler { + + private final WorkingCapitalLoanChargeWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return writePlatformService.adjustmentForLoanCharge(command.getLoanId(), command.entityId(), command); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanChargeConstants.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanChargeConstants.java index acd96cb8102..68bb4bbef79 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanChargeConstants.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanChargeConstants.java @@ -29,4 +29,18 @@ private WorkingCapitalLoanChargeConstants() {} public static final String localeParamName = "locale"; public static final String dateFormatParamName = "dateFormat"; public static final String externalIdParamName = "externalId"; + + // Adjustment parameters + public static final String transactionDateParamName = "transactionDate"; + public static final String noteParamName = "note"; + public static final String paymentDetailsParamName = "paymentDetails"; + public static final String paymentTypeIdParamName = "paymentTypeId"; + public static final String accountNumberParamName = "accountNumber"; + public static final String checkNumberParamName = "checkNumber"; + public static final String routingCodeParamName = "routingCode"; + public static final String receiptNumberParamName = "receiptNumber"; + public static final String bankNumberParamName = "bankNumber"; + + // Adjustment command + public static final String ADJUSTMENT_LOAN_CHARGE_COMMAND = "adjustment"; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanChargeDataValidator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanChargeDataValidator.java index d0732cb0f01..9cb0acfc989 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanChargeDataValidator.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanChargeDataValidator.java @@ -44,6 +44,42 @@ public class WorkingCapitalLoanChargeDataValidator { private final FromJsonHelper fromJsonHelper; + public void validateChargeAdjustmentRequest(final String json) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Set allowedParameters = new HashSet<>(Arrays.asList(WorkingCapitalLoanChargeConstants.amountParamName, + WorkingCapitalLoanChargeConstants.transactionDateParamName, WorkingCapitalLoanChargeConstants.externalIdParamName, + WorkingCapitalLoanChargeConstants.localeParamName, WorkingCapitalLoanChargeConstants.dateFormatParamName, + WorkingCapitalLoanChargeConstants.noteParamName, WorkingCapitalLoanChargeConstants.paymentDetailsParamName, + WorkingCapitalLoanChargeConstants.paymentTypeIdParamName, WorkingCapitalLoanChargeConstants.accountNumberParamName, + WorkingCapitalLoanChargeConstants.checkNumberParamName, WorkingCapitalLoanChargeConstants.routingCodeParamName, + WorkingCapitalLoanChargeConstants.receiptNumberParamName, WorkingCapitalLoanChargeConstants.bankNumberParamName)); + + final Type typeOfMap = new TypeToken>() {}.getType(); + fromJsonHelper.checkForUnsupportedParameters(typeOfMap, json, allowedParameters); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource("workingCapitalLoanChargeAdjustment"); + + final JsonElement element = this.fromJsonHelper.parse(json); + + final BigDecimal amount = this.fromJsonHelper.extractBigDecimalWithLocaleNamed(WorkingCapitalLoanChargeConstants.amountParamName, + element); + baseDataValidator.reset().parameter(WorkingCapitalLoanChargeConstants.amountParamName).value(amount).notNull().positiveAmount(); + + if (this.fromJsonHelper.parameterExists(WorkingCapitalLoanChargeConstants.transactionDateParamName, element)) { + final LocalDate transactionDate = this.fromJsonHelper + .extractLocalDateNamed(WorkingCapitalLoanChargeConstants.transactionDateParamName, element); + baseDataValidator.reset().parameter(WorkingCapitalLoanChargeConstants.transactionDateParamName).value(transactionDate) + .notBlank(); + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + public void validateCreateLoanCharge(final String json) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformService.java index 55cca6ab6f6..a2cdfc360bf 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformService.java @@ -25,4 +25,6 @@ public interface WorkingCapitalLoanChargeWritePlatformService { CommandProcessingResult createLoanCharge(Long loanId, JsonCommand command); + + CommandProcessingResult adjustmentForLoanCharge(Long loanId, Long wcLoanChargeId, JsonCommand command); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformServiceImpl.java index 8bd1053d9c4..0c329479ebd 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformServiceImpl.java @@ -19,30 +19,56 @@ package org.apache.fineract.portfolio.workingcapitalloan.service; +import com.google.gson.JsonElement; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.LinkedHashMap; +import java.util.Map; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanChargeAdjustmentPostBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanChargeAdjustmentPreBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; +import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; +import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants; +import org.apache.fineract.portfolio.workingcapitalloan.accounting.WorkingCapitalLoanAccountingProcessor; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBalance; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanCharge; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanNote; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionAllocation; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionRelation; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionRelationRepository; +import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanChargeAdjustmentException; +import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanChargeNotFoundException; import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBalanceRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanChargeRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanNoteRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanTransactionAllocationRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanTransactionRepository; import org.apache.fineract.portfolio.workingcapitalloan.serialization.WorkingCapitalLoanChargeConstants; import org.apache.fineract.portfolio.workingcapitalloan.serialization.WorkingCapitalLoanChargeDataValidator; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -54,6 +80,13 @@ public class WorkingCapitalLoanChargeWritePlatformServiceImpl implements Working private final WorkingCapitalLoanChargeRepository loanChargeRepository; private final ExternalIdFactory externalIdFactory; private final WorkingCapitalLoanBalanceRepository balanceRepository; + private final WorkingCapitalLoanTransactionRepository transactionRepository; + private final WorkingCapitalLoanTransactionAllocationRepository allocationRepository; + private final WorkingCapitalLoanTransactionRelationRepository relationRepository; + private final PaymentDetailWritePlatformService paymentDetailService; + private final WorkingCapitalLoanNoteRepository noteRepository; + private final BusinessEventNotifierService businessEventNotifierService; + private final WorkingCapitalLoanAccountingProcessor accountingProcessor; @Override public CommandProcessingResult createLoanCharge(Long loanId, JsonCommand command) { @@ -77,6 +110,156 @@ public CommandProcessingResult createLoanCharge(Long loanId, JsonCommand command .build(); } + @Transactional + @Override + public CommandProcessingResult adjustmentForLoanCharge(final Long loanId, final Long wcLoanChargeId, final JsonCommand command) { + loanChargeDataValidator.validateChargeAdjustmentRequest(command.json()); + + final WorkingCapitalLoan loan = workingCapitalLoanRepository.findById(loanId) + .orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loanId)); + final WorkingCapitalLoanCharge wcCharge = loanChargeRepository.findById(wcLoanChargeId) + .orElseThrow(() -> new WorkingCapitalLoanChargeNotFoundException(wcLoanChargeId)); + + if (wcCharge.getLoan() == null || !loanId.equals(wcCharge.getLoan().getId())) { + throw new WorkingCapitalLoanChargeAdjustmentException("wc.loan.charge.adjustment.charge.not.belongs.to.loan", + "Working capital loan charge " + wcLoanChargeId + " does not belong to loan " + loanId); + } + + final BigDecimal amount = command.bigDecimalValueOfParameterNamed(WorkingCapitalLoanChargeConstants.amountParamName); + final LocalDate transactionDate = resolveTransactionDate(command); + final ExternalId externalId = externalIdFactory.createFromCommand(command, WorkingCapitalLoanChargeConstants.externalIdParamName); + + chargeAdjustmentEntranceValidation(loan, wcCharge, amount); + + final Map changes = new LinkedHashMap<>(); + changes.put(WorkingCapitalLoanChargeConstants.amountParamName, amount); + changes.put(WorkingCapitalLoanChargeConstants.transactionDateParamName, transactionDate); + changes.put(WorkingCapitalLoanChargeConstants.externalIdParamName, externalId); + + final PaymentDetail paymentDetail = createAndPersistPaymentDetailFromCommand(command, changes); + + final WorkingCapitalLoanTransaction adjustmentTx = WorkingCapitalLoanTransaction.chargeAdjustment(loan, externalId, amount, + transactionDate, paymentDetail); + + businessEventNotifierService + .notifyPreBusinessEvent(new WorkingCapitalLoanChargeAdjustmentPreBusinessEvent(adjustmentTx, loan.getId())); + + final WorkingCapitalLoanTransactionRelation relation = WorkingCapitalLoanTransactionRelation.linkToCharge(adjustmentTx, wcCharge, + LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT); + adjustmentTx.getLoanTransactionRelations().add(relation); + transactionRepository.saveAndFlush(adjustmentTx); + + final WorkingCapitalLoanTransactionAllocation allocation = WorkingCapitalLoanTransactionAllocation.forChargeAdjustment(adjustmentTx, + amount, wcCharge.isPenaltyCharge()); + allocationRepository.saveAndFlush(allocation); + + applyChargeAmountPaid(wcCharge, amount); + applyBalanceAdjustment(loan, wcCharge, amount); + + if (loan.getLoanProduct().getAccountingRule().isCashBased()) { + accountingProcessor.postJournalEntries(loan, adjustmentTx, allocation, false); + } + + final String noteText = command.stringValueOfParameterNamed(WorkingCapitalLoanChargeConstants.noteParamName); + if (StringUtils.isNotBlank(noteText)) { + noteRepository.save(WorkingCapitalLoanNote.create(loan, noteText)); + changes.put(WorkingCapitalLoanChargeConstants.noteParamName, noteText); + } + + businessEventNotifierService + .notifyPostBusinessEvent(new WorkingCapitalLoanChargeAdjustmentPostBusinessEvent(adjustmentTx, loan.getId())); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(wcLoanChargeId) // + .withEntityExternalId(wcCharge.getExternalId()) // + .withSubEntityId(adjustmentTx.getId()) // + .withSubEntityExternalId(adjustmentTx.getExternalId()) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withLoanId(loanId) // + .with(changes) // + .build(); + } + + private LocalDate resolveTransactionDate(final JsonCommand command) { + final LocalDate requested = command.localDateValueOfParameterNamed(WorkingCapitalLoanChargeConstants.transactionDateParamName); + return requested != null ? requested : ThreadLocalContextUtil.getBusinessDate(); + } + + private void chargeAdjustmentEntranceValidation(final WorkingCapitalLoan loan, final WorkingCapitalLoanCharge wcCharge, + final BigDecimal amount) { + if (loan.getLoanStatus() != LoanStatus.ACTIVE && loan.getLoanStatus() != LoanStatus.CLOSED_OBLIGATIONS_MET + && loan.getLoanStatus() != LoanStatus.OVERPAID) { + throw new WorkingCapitalLoanChargeAdjustmentException("wc.loan.charge.adjustment.invalid.status", + "Adjustment is not supported for the status of " + loan.getLoanStatus()); + } + + if (!wcCharge.isActive()) { + throw new WorkingCapitalLoanChargeAdjustmentException("wc.loan.charge.adjustment.inactive.charge", + "Adjustment is not supported for inactive charges"); + } + + if (amount.compareTo(wcCharge.getAmount()) > 0) { + throw new WorkingCapitalLoanChargeAdjustmentException("wc.loan.charge.adjustment.invalid.amount", + "Transaction amount cannot be higher than the charge amount: " + wcCharge.getAmount()); + } + + final BigDecimal available = calculateAvailableAmountForChargeAdjustment(wcCharge); + if (amount.compareTo(available) > 0) { + throw new WorkingCapitalLoanChargeAdjustmentException("wc.loan.charge.adjustment.invalid.amount", + "Transaction amount cannot be higher than the available charge amount for adjustment: " + available); + } + + checkClientActive(loan); + } + + private BigDecimal calculateAvailableAmountForChargeAdjustment(final WorkingCapitalLoanCharge wcCharge) { + final BigDecimal previouslyAdjusted = relationRepository + .findAllByToChargeAndFromTransactionReversedAndFromTransactionTransactionType(wcCharge, false, + LoanTransactionType.CHARGE_ADJUSTMENT) + .stream().map(rel -> rel.getFromTransaction().getTransactionAmount()).reduce(BigDecimal.ZERO, BigDecimal::add); + return wcCharge.getAmount().subtract(previouslyAdjusted); + } + + private void checkClientActive(final WorkingCapitalLoan loan) { + if (loan.getClient() != null && loan.getClient().isNotActive()) { + throw new ClientNotActiveException(loan.getClient().getId()); + } + } + + private void applyChargeAmountPaid(final WorkingCapitalLoanCharge wcCharge, final BigDecimal amount) { + final BigDecimal newPaid = MathUtil.nullToZero(wcCharge.getAmountPaid()).add(amount); + wcCharge.setAmountPaid(newPaid); + if (newPaid.compareTo(MathUtil.nullToZero(wcCharge.getAmount())) >= 0) { + wcCharge.setPaid(true); + } + loanChargeRepository.saveAndFlush(wcCharge); + } + + private void applyBalanceAdjustment(final WorkingCapitalLoan loan, final WorkingCapitalLoanCharge wcCharge, final BigDecimal amount) { + final WorkingCapitalLoanBalance balance = balanceRepository.findByWcLoan_Id(loan.getId()) + .orElseGet(() -> WorkingCapitalLoanBalance.createFor(loan)); + if (wcCharge.isPenaltyCharge()) { + balance.setPenaltyPaid(MathUtil.nullToZero(balance.getPenaltyPaid()).add(amount)); + } else { + balance.setFeePaid(MathUtil.nullToZero(balance.getFeePaid()).add(amount)); + } + balanceRepository.saveAndFlush(balance); + } + + private PaymentDetail createAndPersistPaymentDetailFromCommand(final JsonCommand command, final Map changes) { + final JsonElement paymentDetailsElement = command.jsonElement(WorkingCapitalLoanConstants.paymentDetailsParamName); + if (paymentDetailsElement != null && paymentDetailsElement.isJsonNull()) { + return null; + } + if (paymentDetailsElement != null && paymentDetailsElement.isJsonObject()) { + final JsonCommand paymentDetailsCommand = JsonCommand.fromExistingCommand(command, paymentDetailsElement); + return paymentDetailService.createPaymentDetail(paymentDetailsCommand, changes); + } + return paymentDetailService.createPaymentDetail(command, changes); + } + private WorkingCapitalLoanCharge assemblyChargeFromCommand(WorkingCapitalLoan loan, JsonCommand command) { final BigDecimal amount = command.bigDecimalValueOfParameterNamed("amount"); final LocalDate dueDate = command.dateValueOfParameterNamed("dueDate"); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java index d450bf9261b..e3326fff87e 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java @@ -606,12 +606,60 @@ public CommandProcessingResult undoTransaction(final Long loanId, final Long tra "Working capital loan transaction not found", WorkingCapitalLoanConstants.transactionIdParamName)); return switch (transaction.getTypeOf()) { case DISCOUNT_FEE_ADJUSTMENT -> undoDiscountFeeAdjustment(loan, transaction, command); + case CHARGE_ADJUSTMENT -> undoChargeAdjustment(loan, transaction, command); default -> throw new PlatformApiDataValidationException("validation.msg.wc.loan.transaction.undo.not.supported", "Undo is not supported for transaction type " + transaction.getTypeOf(), WorkingCapitalLoanConstants.transactionTypeParamName); }; } + private CommandProcessingResult undoChargeAdjustment(final WorkingCapitalLoan loan, + final WorkingCapitalLoanTransaction adjustmentTransaction, final JsonCommand command) { + if (adjustmentTransaction.isReversed()) { + throw new PlatformApiDataValidationException("validation.msg.wc.loan.charge.adjustment.already.reversed", + "Charge adjustment transaction is already reversed", WorkingCapitalLoanConstants.transactionIdParamName); + } + + final WorkingCapitalLoanTransactionRelation chargeRelation = adjustmentTransaction.getLoanTransactionRelations().stream() + .filter(r -> r.getToCharge() != null && r.getRelationType() == LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT) + .findFirst() + .orElseThrow(() -> new PlatformApiDataValidationException("validation.msg.wc.loan.charge.adjustment.relation.missing", + "Charge adjustment transaction is missing the link to the charge", + WorkingCapitalLoanConstants.transactionIdParamName)); + + final org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanCharge wcCharge = chargeRelation.getToCharge(); + final BigDecimal amount = adjustmentTransaction.getTransactionAmount(); + + reverseTransaction(adjustmentTransaction); + accountingProcessor.postReversalJournalEntries(loan, adjustmentTransaction); + + final BigDecimal newPaid = MathUtil.subtract(MathUtil.nullToZero(wcCharge.getAmountPaid()), amount).max(BigDecimal.ZERO); + wcCharge.setAmountPaid(newPaid); + if (newPaid.compareTo(MathUtil.nullToZero(wcCharge.getAmount())) < 0) { + wcCharge.setPaid(false); + } + + final WorkingCapitalLoanBalance balance = balanceRepository.findByWcLoan_Id(loan.getId()) + .orElseGet(() -> WorkingCapitalLoanBalance.createFor(loan)); + if (wcCharge.isPenaltyCharge()) { + balance.setPenaltyPaid(MathUtil.subtract(MathUtil.nullToZero(balance.getPenaltyPaid()), amount).max(BigDecimal.ZERO)); + } else { + balance.setFeePaid(MathUtil.subtract(MathUtil.nullToZero(balance.getFeePaid()), amount).max(BigDecimal.ZERO)); + } + balanceRepository.saveAndFlush(balance); + + final String noteText = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName); + createNote(noteText, loan); + + final Map changes = new LinkedHashMap<>(); + if (StringUtils.isNotBlank(noteText)) { + changes.put(WorkingCapitalLoanConstants.noteParamName, noteText); + } + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(adjustmentTransaction.getId()) + .withEntityExternalId(adjustmentTransaction.getExternalId()).withOfficeId(loan.getOfficeId()) + .withClientId(loan.getClientId()).withLoanId(loan.getId()).with(changes).build(); + } + private CommandProcessingResult undoDiscountFeeAdjustment(final WorkingCapitalLoan loan, final WorkingCapitalLoanTransaction adjustmentTransaction, final JsonCommand command) { validator.validateUndoDiscountAdjustmentTransaction(loan, adjustmentTransaction); diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml index 25190c291cb..6fcc1ddd652 100644 --- a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml @@ -65,4 +65,5 @@ + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0044_wc_loan_charge_adjustment.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0044_wc_loan_charge_adjustment.xml new file mode 100644 index 00000000000..1b5f76f8b34 --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0044_wc_loan_charge_adjustment.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) + FROM m_external_event_configuration + WHERE type = 'WorkingCapitalLoanChargeAdjustmentPreBusinessEvent' + + + + + + + + + + SELECT COUNT(*) + FROM m_external_event_configuration + WHERE type = 'WorkingCapitalLoanChargeAdjustmentPostBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_permission + WHERE code = 'ADJUSTMENT_WORKINGCAPITALLOANCHARGE' + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission + WHERE code = 'ADJUSTMENT_WORKINGCAPITALLOANCHARGE_CHECKER' + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java index d6750fbfe26..a97e9fc8184 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java @@ -705,6 +705,16 @@ public static ArrayList> getDefaultExternalEventConfiguratio workingCapitalLoanDiscountFeeAdjustmentTransactionBusinessEvent.put("enabled", false); defaults.add(workingCapitalLoanDiscountFeeAdjustmentTransactionBusinessEvent); + Map workingCapitalLoanChargeAdjustmentPreBusinessEvent = new HashMap<>(); + workingCapitalLoanChargeAdjustmentPreBusinessEvent.put("type", "WorkingCapitalLoanChargeAdjustmentPreBusinessEvent"); + workingCapitalLoanChargeAdjustmentPreBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanChargeAdjustmentPreBusinessEvent); + + Map workingCapitalLoanChargeAdjustmentPostBusinessEvent = new HashMap<>(); + workingCapitalLoanChargeAdjustmentPostBusinessEvent.put("type", "WorkingCapitalLoanChargeAdjustmentPostBusinessEvent"); + workingCapitalLoanChargeAdjustmentPostBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanChargeAdjustmentPostBusinessEvent); + return defaults; }