diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java index 8d9e0fda760..33cdcb25971 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/api/ClientApiConstants.java @@ -50,6 +50,8 @@ public class ClientApiConstants { public static final String CLIENT_CHARGE_COMMAND_WAIVE_CHARGE = "waive"; public static final String CLIENT_CHARGE_COMMAND_PAY_CHARGE = "paycharge"; public static final String CLIENT_CHARGE_COMMAND_INACTIVATE_CHARGE = "inactivate"; + public static final String isActiveParamName = "isActive"; + public static final String inactivationDateParamName = "inactivationDate"; public static final String CLIENT_TRANSACTION_COMMAND_UNDO = "undo"; public static final String CLIENT_CLOSURE_REASON = "ClientClosureReason"; @@ -199,10 +201,11 @@ public class ClientApiConstants { staffOptionsParamName, dateOfBirthParamName, genderParamName, clientTypeParamName, clientClassificationParamName, legalFormParamName, clientNonPersonDetailsParamName, isStaffParamName, legalFormParamName)); - protected static final Set CLIENT_CHARGES_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(chargeIdParamName, - clientIdParamName, chargeNameParamName, penaltyParamName, chargeTimeTypeParamName, dueAsOfDateParamName, - chargeCalculationTypeParamName, currencyParamName, amountWaivedParamName, amountWrittenOffParamName, amountOutstandingParamName, - amountOrPercentageParamName, amountParamName, amountPaidParamName, chargeOptionsParamName, transactionsParamName)); + protected static final Set CLIENT_CHARGES_RESPONSE_DATA_PARAMETERS = new HashSet<>( + Arrays.asList(chargeIdParamName, clientIdParamName, chargeNameParamName, penaltyParamName, chargeTimeTypeParamName, + dueAsOfDateParamName, chargeCalculationTypeParamName, currencyParamName, amountWaivedParamName, + amountWrittenOffParamName, amountOutstandingParamName, amountOrPercentageParamName, amountParamName, + amountPaidParamName, chargeOptionsParamName, transactionsParamName, isActiveParamName, inactivationDateParamName)); protected static final Set CLIENT_TRANSACTION_RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(idParamName, transactionAmountParamName, paymentDetailDataParamName, reversedParamName, dateParamName, officeIdParamName, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientChargesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientChargesApiResource.java index 5d1b8e75de3..ba8640bf80d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientChargesApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientChargesApiResource.java @@ -182,7 +182,7 @@ public String applyClientCharge(@PathParam("clientId") @Parameter(description = @Path("{chargeId}") @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Pay a Client Charge | Waive a Client Charge", operationId = "payOrWaiveClientCharge", description = "Pay a Client Charge:\n\n" + @Operation(summary = "Pay a Client Charge | Waive a Client Charge | Inactivate a Client Charge", operationId = "payOrWaiveClientCharge", description = "Pay a Client Charge:\n\n" + "Mandatory Fields:" + "transactionDate and amount " + "" + "\"Pay either a part of or the entire due amount for a charge.(command=paycharge)\n" + "\n" + "Waive a Client Charge:\n" + "\n" + "\n" + "This API provides the facility of waiving off the remaining amount on a client charge (command=waive)\n\n" diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientCharge.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientCharge.java index 237e094fcbe..4017084b38a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientCharge.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientCharge.java @@ -140,7 +140,10 @@ public void undoPayment(final Money transactionAmount) { this.amountPaid = amountPaid.getAmount(); this.amountOutstanding = calculateOutstanding(); this.paid = false; - this.status = true; + // Only restore active status if charge was never explicitly inactivated + if (this.inactivationDate == null) { + this.status = true; + } } public Money waive() { @@ -159,7 +162,15 @@ public void undoWaiver(final Money transactionAmount) { this.amountWaived = amountWaived.getAmount(); this.amountOutstanding = calculateOutstanding(); this.waived = false; - this.status = true; + // Only restore active status if charge was never explicitly inactivated + if (this.inactivationDate == null) { + this.status = true; + } + } + + public void inactivate(final LocalDate inactivationOnDate) { + this.status = false; + this.inactivationDate = inactivationOnDate; } private void populateDerivedFields(final BigDecimal amount) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/handler/InactivateClientChargeCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/handler/InactivateClientChargeCommandHandler.java new file mode 100644 index 00000000000..9f13a956c57 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/handler/InactivateClientChargeCommandHandler.java @@ -0,0 +1,48 @@ +/** + * 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.client.handler; + +import org.apache.fineract.commands.annotation.CommandType; +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.client.api.ClientApiConstants; +import org.apache.fineract.portfolio.client.service.ClientChargeWritePlatformService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = ClientApiConstants.CLIENT_CHARGES_RESOURCE_NAME, action = ClientApiConstants.CLIENT_CHARGE_ACTION_INACTIVATE) +public class InactivateClientChargeCommandHandler implements NewCommandSourceHandler { + + private final ClientChargeWritePlatformService writePlatformService; + + @Autowired + public InactivateClientChargeCommandHandler(final ClientChargeWritePlatformService writePlatformService) { + this.writePlatformService = writePlatformService; + } + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.inactivateCharge(command.getClientId(), command.entityId()); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientChargeWritePlatformServiceImpl.java index fe388beb54e..403081f4a9f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientChargeWritePlatformServiceImpl.java @@ -353,10 +353,50 @@ public CommandProcessingResult updateCharge(@SuppressWarnings("unused") Long cli } @Override - @SuppressWarnings("unused") public CommandProcessingResult inactivateCharge(Long clientId, Long clientChargeId) { - // functionality not yet supported - return null; + try { + final Client client = this.clientRepository.getActiveClientInUserScope(clientId); + final ClientCharge clientCharge = this.clientChargeRepository.findOneWithNotFoundDetection(clientChargeId); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(ClientApiConstants.CLIENT_CHARGES_RESOURCE_NAME); + + if (clientCharge.isNotActive()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("charge.is.already.inactive"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (clientCharge.isWaived()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("charge.is.already.waived"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (clientCharge.isPaid()) { + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("charge.is.already.paid"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final LocalDate inactivationOnDate = DateUtils.getBusinessLocalDate(); + clientCharge.inactivate(inactivationOnDate); + this.clientChargeRepository.saveAndFlush(clientCharge); + + return new CommandProcessingResultBuilder() // + .withEntityId(clientCharge.getId()) // + .withOfficeId(client.getOffice().getId()) // + .withClientId(client.getId()) // + .build(); + } catch (final JpaSystemException | DataIntegrityViolationException dve) { + final Throwable throwable = dve.getMostSpecificCause(); + handleDataIntegrityIssues(clientId, clientChargeId, throwable, dve); + return CommandProcessingResult.empty(); + } } /** diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTransactionWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTransactionWritePlatformServiceJpaRepositoryImpl.java index 97841807ea3..4f42f5f60ad 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTransactionWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientTransactionWritePlatformServiceJpaRepositoryImpl.java @@ -18,13 +18,19 @@ */ package org.apache.fineract.portfolio.client.service; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.organisation.monetary.domain.OrganisationCurrencyRepositoryWrapper; +import org.apache.fineract.portfolio.client.api.ClientApiConstants; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.ClientCharge; import org.apache.fineract.portfolio.client.domain.ClientChargePaidBy; @@ -67,6 +73,16 @@ public CommandProcessingResult undo(Long clientId, Long transactionId) { final ClientCharge clientCharge = clientChargePaidBy.getClientCharge(); clientCharge.setCurrency( organisationCurrencyRepository.findOneWithNotFoundDetection(clientCharge.getCharge().getCurrencyCode())); + + // Cannot undo a transaction on an explicitly inactivated charge + if (clientCharge.isNotActive()) { + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(ClientApiConstants.CLIENT_CHARGES_RESOURCE_NAME); + baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("transaction.undo.not.allowed.charge.is.inactive"); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + if (clientTransaction.isPayChargeTransaction()) { clientCharge.undoPayment(clientTransaction.getAmount()); } else if (clientTransaction.isWaiveChargeTransaction()) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientChargesTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientChargesTest.java index d92577abf0b..d36e7b25749 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientChargesTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientChargesTest.java @@ -149,6 +149,100 @@ public void clientChargeTest() { } + @Test + public void clientChargeInactivateTest() { + + // Create a charge definition applicable to clients + final Integer chargeId = ChargesHelper.createCharges(this.requestSpec, this.responseSpec, + ChargesHelper.getChargeSpecifiedDueDateJSON()); + Assertions.assertNotNull(chargeId); + + // Create a client with activation date + final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 October 2011"); + Assertions.assertNotNull(clientId); + + // Associate the charge with the client + final Integer clientChargeId = ClientHelper.addChargesForClient(this.requestSpec, this.responseSpec, clientId, + ClientHelper.getSpecifiedDueDateChargesClientAsJSON(chargeId.toString(), "29 October 2011")); + Assertions.assertNotNull(clientChargeId); + + // Inactivate the charge - this is the core operation under test + final Integer inactivatedChargeId = ClientHelper.inactivateChargesForClients(this.requestSpec, this.responseSpec, clientId, + clientChargeId); + Assertions.assertNotNull(inactivatedChargeId); + Assertions.assertEquals(clientChargeId, inactivatedChargeId); + + // Retrieve and assert the charge is now inactive + // The JSON field name matches the Java field name in ClientChargeData: isActive + final Object isActive = ClientHelper.getClientChargeField(requestSpec, responseSpec, clientId.toString(), clientChargeId.toString(), + "isActive"); + Assertions.assertEquals(Boolean.FALSE, isActive); + + // Attempting to inactivate an already inactive charge must fail with 400 + final ResponseSpecification responseSpecFailure = new ResponseSpecBuilder().expectStatusCode(400).build(); + final Integer duplicateInactivate = ClientHelper.inactivateChargesForClients(this.requestSpec, responseSpecFailure, clientId, + clientChargeId); + Assertions.assertNull(duplicateInactivate); + } + + @Test + public void clientChargeInactivateBlocksUndoTest() { + + // Create charge and client + final Integer chargeId = ChargesHelper.createCharges(this.requestSpec, this.responseSpec, + ChargesHelper.getChargeSpecifiedDueDateJSON()); + Assertions.assertNotNull(chargeId); + final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 October 2011"); + Assertions.assertNotNull(clientId); + final Integer clientChargeId = ClientHelper.addChargesForClient(this.requestSpec, this.responseSpec, clientId, + ClientHelper.getSpecifiedDueDateChargesClientAsJSON(chargeId.toString(), "29 October 2011")); + Assertions.assertNotNull(clientChargeId); + + // Pay the charge partially + final String transactionId = ClientHelper.payChargesForClients(this.requestSpec, this.responseSpec, clientId, clientChargeId, + ClientHelper.getPayChargeJSON("25 August 2015", "10")); + Assertions.assertNotNull(transactionId); + + // Inactivate the charge + final Integer inactivatedId = ClientHelper.inactivateChargesForClients(this.requestSpec, this.responseSpec, clientId, + clientChargeId); + Assertions.assertNotNull(inactivatedId); + + // Attempt to undo the payment - must fail with 400 because charge is inactive + final ResponseSpecification responseSpecFailure = new ResponseSpecBuilder().expectStatusCode(400).build(); + final Integer undoResult = ClientHelper.revertClientChargeTransaction(this.requestSpec, responseSpecFailure, clientId.toString(), + transactionId); + Assertions.assertNull(undoResult); + } + + @Test + public void clientChargeInactivateFieldsFilterTest() { + + // Create charge and client + final Integer chargeId = ChargesHelper.createCharges(this.requestSpec, this.responseSpec, + ChargesHelper.getChargeSpecifiedDueDateJSON()); + Assertions.assertNotNull(chargeId); + final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 October 2011"); + Assertions.assertNotNull(clientId); + final Integer clientChargeId = ClientHelper.addChargesForClient(this.requestSpec, this.responseSpec, clientId, + ClientHelper.getSpecifiedDueDateChargesClientAsJSON(chargeId.toString(), "29 October 2011")); + Assertions.assertNotNull(clientChargeId); + + // Inactivate the charge + ClientHelper.inactivateChargesForClients(this.requestSpec, this.responseSpec, clientId, clientChargeId); + + // Assert isActive is returned correctly in full response + final Object isActive = ClientHelper.getClientChargeField(requestSpec, responseSpec, clientId.toString(), clientChargeId.toString(), + "isActive"); + Assertions.assertEquals(Boolean.FALSE, isActive); + + // Assert isActive is returned when using explicit ?fields= filter + // This validates CLIENT_CHARGES_RESPONSE_DATA_PARAMETERS includes isActive + final Object isActiveFiltered = ClientHelper.getClientChargeFieldWithFilter(requestSpec, responseSpec, clientId.toString(), + clientChargeId.toString(), "isActive", "isActive"); + Assertions.assertEquals(Boolean.FALSE, isActiveFiltered); + } + /** * It checks whether the client charge transaction is reversed or not. * diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java index 408b17b78a1..74a8a7ce8bc 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ClientHelper.java @@ -1086,6 +1086,19 @@ public static String payChargesForClientsTransactionExternalId(final RequestSpec return response.get("subResourceExternalId") != null ? response.get("subResourceExternalId").toString() : null; } + // TODO: Rewrite to use fineract-client instead! + // Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long, + // org.apache.fineract.client.models.PostLoansLoanIdRequest) + @Deprecated(forRemoval = true) + public static Integer inactivateChargesForClients(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final Integer clientId, final Integer clientChargeId) { + log.info("--------------------------------- INACTIVATE CHARGE FOR CLIENT --------------------------------"); + final String CHARGES_URL = "/fineract-provider/api/v1/clients/" + clientId + "/charges/" + clientChargeId + "?command=inactivate&" + + Utils.TENANT_IDENTIFIER; + final HashMap response = Utils.performServerPost(requestSpec, responseSpec, CHARGES_URL, "", ""); + return response != null ? (Integer) response.get("resourceId") : null; + } + // TODO: Rewrite to use fineract-client instead! // Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long, // org.apache.fineract.client.models.PostLoansLoanIdRequest) @@ -1131,6 +1144,22 @@ public static Object getClientCharge(final RequestSpecification requestSpec, fin // Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long, // org.apache.fineract.client.models.PostLoansLoanIdRequest) @Deprecated(forRemoval = true) + public static Object getClientChargeField(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final String clientId, final String clientChargeId, final String field) { + log.info("---------------------------------GET CLIENT CHARGE FIELD---------------------------------------------"); + final String CHARGES_URL = "/fineract-provider/api/v1/clients/" + clientId + "/charges/" + clientChargeId + "?" + + Utils.TENANT_IDENTIFIER; + return Utils.performServerGet(requestSpec, responseSpec, CHARGES_URL, field); + } + + public static Object getClientChargeFieldWithFilter(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final String clientId, final String clientChargeId, final String fields, final String returnField) { + log.info("---------------------------------GET CLIENT CHARGE WITH FIELDS FILTER---------------------------------------------"); + final String CHARGES_URL = "/fineract-provider/api/v1/clients/" + clientId + "/charges/" + clientChargeId + "?fields=" + fields + + "&" + Utils.TENANT_IDENTIFIER; + return Utils.performServerGet(requestSpec, responseSpec, CHARGES_URL, returnField); + } + public static Boolean getClientTransactions(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final String clientId, final String transactionId) { log.info("---------------------------------GET CLIENT CHARGE TRANSACTIONS---------------------------------------------");