FINERACT-1724

Added batch support for the Get Charge by Id call.
Added externalId field to PostLoansLoanIdChargesRequest.
Added submittedOnDate column to m_loan_charge table.
Updated logic to return new submittedOnDate field in GetLoansLoanIdChargesChargeIdResponse.
This commit is contained in:
Arnold Galovics
2022-10-14 17:57:29 +02:00
committed by Arnold Gálovics
parent b33e3ac892
commit 388612b1ae
14 changed files with 410 additions and 13 deletions
@@ -91,6 +91,8 @@ public class CommandStrategyProvider {
this.commandStrategies.put(CommandContext.resource("loans\\/\\d+\\/charges").method("POST").build(), "createChargeCommandStrategy");
this.commandStrategies.put(CommandContext.resource("loans\\/\\d+\\/charges").method("GET").build(),
"collectChargesCommandStrategy");
this.commandStrategies.put(CommandContext.resource("loans\\/\\d+\\/charges\\/\\d+").method("GET").build(),
"getChargeByIdCommandStrategy");
this.commandStrategies.put(CommandContext.resource("loans\\/\\d+\\/transactions\\?command=repayment").method("POST").build(),
"createTransactionLoanCommandStrategy");
this.commandStrategies.put(
@@ -0,0 +1,87 @@
/**
* 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.batch.command.internal;
import com.google.common.base.Splitter;
import java.util.List;
import java.util.Map;
import javax.ws.rs.core.UriInfo;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.batch.command.CommandStrategy;
import org.apache.fineract.batch.command.CommandStrategyUtils;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.infrastructure.core.api.MutableUriInfo;
import org.apache.fineract.portfolio.loanaccount.api.LoanChargesApiResource;
import org.apache.http.HttpStatus;
import org.springframework.stereotype.Component;
/**
* Implements {@link CommandStrategy} to retrieve a charge by id. It passes the contents of the body from the
* BatchRequest to {@link LoanChargesApiResource} and gets back the response. This class will also catch any errors
* raised by {@link LoanChargesApiResource} and map those errors to appropriate status codes in BatchResponse.
*
* @see CommandStrategy
* @see BatchRequest
* @see BatchResponse
*/
@Component
@RequiredArgsConstructor
public class GetChargeByIdCommandStrategy implements CommandStrategy {
private final LoanChargesApiResource loanChargesApiResource;
@Override
public BatchResponse execute(final BatchRequest request, UriInfo uriInfo) {
final MutableUriInfo parameterizedUriInfo = new MutableUriInfo(uriInfo);
final BatchResponse response = new BatchResponse();
final String responseBody;
response.setRequestId(request.getRequestId());
response.setHeaders(request.getHeaders());
final String relativeUrl = request.getRelativeUrl();
// Get the loan and charge ids for use in loanChargesApiResource
final List<String> pathParameters = Splitter.on('/').splitToList(relativeUrl);
final Long loanId = Long.parseLong(pathParameters.get(1));
Long chargeId;
if (relativeUrl.indexOf('?') > 0) {
chargeId = Long.parseLong(StringUtils.substringBeforeLast(pathParameters.get(3), "?"));
} else {
chargeId = Long.parseLong(pathParameters.get(3));
}
Map<String, String> queryParameters;
if (relativeUrl.indexOf('?') > 0) {
queryParameters = CommandStrategyUtils.getQueryParameters(relativeUrl);
// Add the query parameters sent in the relative URL to UriInfo
CommandStrategyUtils.addQueryParametersToUriInfo(parameterizedUriInfo, queryParameters);
}
responseBody = loanChargesApiResource.retrieveLoanCharge(loanId, chargeId, parameterizedUriInfo);
response.setStatusCode(HttpStatus.SC_OK);
response.setBody(responseBody);
return response;
}
}
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.loanaccount.api;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import java.util.Set;
/**
@@ -102,6 +103,8 @@ final class LoanChargesApiResourceSwagger {
public Double amountOrPercentage;
@Schema(example = "false")
public Boolean penalty;
@Schema(example = "27 March 2013")
public LocalDate submittedOnDate;
}
@Schema(description = "GetLoansLoanIdChargesTemplateResponse")
@@ -179,6 +182,8 @@ final class LoanChargesApiResourceSwagger {
public String dateFormat;
@Schema(example = "29 April 2013")
public String dueDate;
@Schema(example = "786444UUUYYH7")
public String externalId;
}
@Schema(description = " PostLoansLoanIdChargesResponse")
@@ -39,6 +39,8 @@ public class LoanChargeData {
private final String name;
private final EnumOptionData chargeTimeType;
private final LocalDate submittedOnDate;
private final LocalDate dueDate;
private final EnumOptionData chargeCalculationType;
@@ -103,10 +105,10 @@ public class LoanChargeData {
public LoanChargeData(final Long id, final Long chargeId, final String name, final CurrencyData currency, final BigDecimal amount,
final BigDecimal amountPaid, final BigDecimal amountWaived, final BigDecimal amountWrittenOff,
final BigDecimal amountOutstanding, final EnumOptionData chargeTimeType, final LocalDate dueDate,
final EnumOptionData chargeCalculationType, final BigDecimal percentage, final BigDecimal amountPercentageAppliedTo,
final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid, final boolean waived, final Long loanId,
final BigDecimal minCap, final BigDecimal maxCap, final BigDecimal amountOrPercentage,
final BigDecimal amountOutstanding, final EnumOptionData chargeTimeType, final LocalDate submittedOnDate,
final LocalDate dueDate, final EnumOptionData chargeCalculationType, final BigDecimal percentage,
final BigDecimal amountPercentageAppliedTo, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid,
final boolean waived, final Long loanId, final BigDecimal minCap, final BigDecimal maxCap, final BigDecimal amountOrPercentage,
Collection<LoanInstallmentChargeData> installmentChargeData, final String externalId) {
this.id = id;
this.chargeId = chargeId;
@@ -118,6 +120,7 @@ public class LoanChargeData {
this.amountWrittenOff = amountWrittenOff;
this.amountOutstanding = amountOutstanding;
this.chargeTimeType = chargeTimeType;
this.submittedOnDate = submittedOnDate;
this.dueDate = dueDate;
this.chargeCalculationType = chargeCalculationType;
this.percentage = percentage;
@@ -162,6 +165,7 @@ public class LoanChargeData {
this.amountWrittenOff = BigDecimal.ZERO;
this.amountOutstanding = amount;
this.chargeTimeType = chargeTimeType;
this.submittedOnDate = null;
this.dueDate = null;
this.chargeCalculationType = chargeCalculationType;
this.percentage = percentage;
@@ -203,6 +207,7 @@ public class LoanChargeData {
this.amountWrittenOff = null;
this.amountOutstanding = amountOutstanding;
this.chargeTimeType = chargeTimeType;
this.submittedOnDate = null;
this.dueDate = dueAsOfDate;
this.chargeCalculationType = null;
this.percentage = null;
@@ -234,6 +239,7 @@ public class LoanChargeData {
this.amountWrittenOff = null;
this.amountOutstanding = null;
this.chargeTimeType = chargeTimeType;
this.submittedOnDate = null;
this.dueDate = dueAsOfDate;
this.chargeCalculationType = null;
this.percentage = null;
@@ -264,6 +270,7 @@ public class LoanChargeData {
this.amountWrittenOff = null;
this.amountOutstanding = null;
this.chargeTimeType = chargeData.chargeTimeType;
this.submittedOnDate = chargeData.submittedOnDate;
this.dueDate = chargeData.dueDate;
this.chargeCalculationType = null;
this.percentage = null;
@@ -294,6 +301,7 @@ public class LoanChargeData {
this.amountWrittenOff = chargeData.amountWrittenOff;
this.amountOutstanding = chargeData.amountOutstanding;
this.chargeTimeType = chargeData.chargeTimeType;
this.submittedOnDate = chargeData.submittedOnDate;
this.dueDate = chargeData.dueDate;
this.chargeCalculationType = chargeData.chargeCalculationType;
this.percentage = chargeData.percentage;
@@ -326,6 +334,7 @@ public class LoanChargeData {
this.amountWrittenOff = null;
this.amountOutstanding = null;
this.chargeTimeType = chargeTimeType;
this.submittedOnDate = null;
this.dueDate = dueAsOfDate;
this.chargeCalculationType = chargeCalculationType;
this.percentage = null;
@@ -356,6 +365,7 @@ public class LoanChargeData {
this.amountWrittenOff = null;
this.amountOutstanding = null;
this.chargeTimeType = null;
this.submittedOnDate = null;
this.dueDate = dueAsOfDate;
this.chargeCalculationType = null;
this.percentage = null;
@@ -42,6 +42,7 @@ import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
@@ -68,6 +69,9 @@ public class LoanCharge extends AbstractPersistableCustom {
@Column(name = "charge_time_enum", nullable = false)
private Integer chargeTime;
@Column(name = "submitted_on_date", nullable = true)
private LocalDate submittedOnDate;
@Column(name = "due_for_collection_as_of_date")
private LocalDate dueDate;
@@ -229,6 +233,7 @@ public class LoanCharge extends AbstractPersistableCustom {
final ChargePaymentMode chargePaymentMode, final Integer numberOfRepayments, final BigDecimal loanCharge) {
this.loan = loan;
this.charge = chargeDefinition;
this.submittedOnDate = DateUtils.getBusinessLocalDate();
this.penaltyCharge = chargeDefinition.isPenalty();
this.minCap = chargeDefinition.getMinCap();
this.maxCap = chargeDefinition.getMaxCap();
@@ -597,6 +602,10 @@ public class LoanCharge extends AbstractPersistableCustom {
return this.dueDate;
}
public LocalDate getSubmittedOnDate() {
return submittedOnDate;
}
private boolean determineIfFullyPaid() {
if (this.amount == null) {
return true;
@@ -59,8 +59,8 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform
private static final class LoanChargeMapper implements RowMapper<LoanChargeData> {
public String schema() {
return "lc.id as id, lc.external_id as externalId, c.id as chargeId, c.name as name, " + "lc.amount as amountDue, "
+ "lc.amount_paid_derived as amountPaid, " + "lc.amount_waived_derived as amountWaived, "
return "lc.id as id, lc.external_id as externalId, c.id as chargeId, c.name as name, lc.submitted_on_date as submittedOnDate, "
+ "lc.amount as amountDue, " + "lc.amount_paid_derived as amountPaid, " + "lc.amount_waived_derived as amountWaived, "
+ "lc.amount_writtenoff_derived as amountWrittenOff, " + "lc.amount_outstanding_derived as amountOutstanding, "
+ "lc.calculation_percentage as percentageOf, lc.calculation_on_amount as amountPercentageAppliedTo, "
+ "lc.charge_time_enum as chargeTime, " + "lc.is_penalty as penalty, "
@@ -116,6 +116,7 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform
final BigDecimal maxCap = rs.getBigDecimal("maxCap");
final BigDecimal amountOrPercentage = rs.getBigDecimal("amountOrPercentage");
final LocalDate disbursementDate = JdbcSupport.getLocalDate(rs, "disbursementDate");
final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate");
if (disbursementDate != null) {
dueAsOfDate = disbursementDate;
@@ -123,8 +124,8 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform
final String externalId = rs.getString("externalId");
return new LoanChargeData(id, chargeId, name, currency, amount, amountPaid, amountWaived, amountWrittenOff, amountOutstanding,
chargeTimeType, dueAsOfDate, chargeCalculationType, percentageOf, amountPercentageAppliedTo, penalty, paymentMode, paid,
waived, null, minCap, maxCap, amountOrPercentage, null, externalId);
chargeTimeType, submittedOnDate, dueAsOfDate, chargeCalculationType, percentageOf, amountPercentageAppliedTo, penalty,
paymentMode, paid, waived, null, minCap, maxCap, amountOrPercentage, null, externalId);
}
}
@@ -526,7 +526,8 @@ public final class LoanProductDataValidator {
baseDataValidator.reset().parameter(LoanProductConstants.fixedPrincipalPercentagePerInstallmentParamName)
.value(fixedPrincipalPercentagePerInstallment).notLessThanMin(BigDecimal.ONE).notGreaterThanMax(BigDecimal.valueOf(100));
if (!amortizationType.equals(AmortizationMethod.EQUAL_PRINCIPAL.getValue()) && fixedPrincipalPercentagePerInstallment != null) {
if (amortizationType != null && !amortizationType.equals(AmortizationMethod.EQUAL_PRINCIPAL.getValue())
&& fixedPrincipalPercentagePerInstallment != null) {
baseDataValidator.reset().parameter(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName).failWithCode(
"not.supported.principal.fixing.not.allowed.with.equal.installments",
"Principal fixing cannot be done with equal installment amortization");
@@ -74,4 +74,5 @@
<include file="parts/0052_loan_transaction_chargeback.xml" relativeToChangelogFile="true"/>
<include file="parts/0053_add_external_events_purge_job.xml" relativeToChangelogFile="true"/>
<include file="parts/0054_additional_fields_loan_repayment_schedule.xml" relativeToChangelogFile="true"/>
<include file="parts/0055_add_submitted_on_date_to_loan_charge.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_loan_charge">
<column name="submitted_on_date" type="date">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -36,6 +36,7 @@ import org.apache.fineract.batch.command.internal.CreateClientCommandStrategy;
import org.apache.fineract.batch.command.internal.CreateLoanRescheduleRequestCommandStrategy;
import org.apache.fineract.batch.command.internal.CreateTransactionLoanCommandStrategy;
import org.apache.fineract.batch.command.internal.DisburseLoanCommandStrategy;
import org.apache.fineract.batch.command.internal.GetChargeByIdCommandStrategy;
import org.apache.fineract.batch.command.internal.GetDatatableEntryByAppTableIdCommandStrategy;
import org.apache.fineract.batch.command.internal.GetLoanByIdCommandStrategy;
import org.apache.fineract.batch.command.internal.GetTransactionByIdCommandStrategy;
@@ -69,6 +70,8 @@ public class CommandStrategyProviderTest {
Arguments.of("loans/123/charges", HttpMethod.POST, "createChargeCommandStrategy", mock(CreateChargeCommandStrategy.class)),
Arguments.of("loans/123/charges", HttpMethod.GET, "collectChargesCommandStrategy",
mock(CollectChargesCommandStrategy.class)),
Arguments.of("loans/123/charges/123", HttpMethod.GET, "getChargeByIdCommandStrategy",
mock(GetChargeByIdCommandStrategy.class)),
Arguments.of("loans/123/transactions?command=repayment", HttpMethod.POST, "createTransactionLoanCommandStrategy",
mock(CreateTransactionLoanCommandStrategy.class)),
Arguments.of("loans/123/transactions?command=creditBalanceRefund", HttpMethod.POST, "createTransactionLoanCommandStrategy",
@@ -130,7 +133,8 @@ public class CommandStrategyProviderTest {
private static Stream<Arguments> provideCommandStrategyResourceDetailsForErrors() {
return Stream.of(Arguments.of("loans/123?command=reject", HttpMethod.POST),
Arguments.of("loans/glimAccount/746?command=approve", HttpMethod.POST), Arguments.of("loans/123", HttpMethod.PUT),
Arguments.of("datatables/test_dt_table", HttpMethod.GET), Arguments.of("datatables", HttpMethod.GET));
Arguments.of("datatables/test_dt_table", HttpMethod.GET), Arguments.of("datatables", HttpMethod.GET),
Arguments.of("loans//charges/123", HttpMethod.GET), Arguments.of("loans/123/charges/", HttpMethod.GET));
}
@@ -0,0 +1,149 @@
/**
* 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.batch.command.internal;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import java.util.stream.Stream;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.infrastructure.core.api.MutableUriInfo;
import org.apache.fineract.portfolio.loanaccount.api.LoanChargesApiResource;
import org.apache.http.HttpStatus;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* The {@link GetChargeByIdCommandStrategy} test class.
*/
public class GetChargeByIdCommandStrategyTest {
/**
* Query parameter provider.
*
* @return the test data stream
*/
private static Stream<Arguments> provideQueryParameters() {
return Stream.of(Arguments.of(null, 0), Arguments.of("fields=name,amountOrPercentage", 1));
}
/**
* Test {@link GetChargeByIdCommandStrategy#execute} happy path scenario.
*/
@ParameterizedTest
@MethodSource("provideQueryParameters")
public void testExecuteSuccessScenario(final String queryParameter, final int numberOfQueryParams) {
final TestContext testContext = new TestContext();
final Long loanId = Long.valueOf(RandomStringUtils.randomNumeric(4));
final Long chargeId = Long.valueOf(RandomStringUtils.randomNumeric(4));
final BatchRequest request = getBatchRequest(loanId, chargeId, queryParameter);
final String responseBody = "someResponseBody";
given(testContext.loanChargesApiResource.retrieveLoanCharge(eq(loanId), eq(chargeId), any(UriInfo.class))).willReturn(responseBody);
final BatchResponse response = testContext.subjectToTest.execute(request, testContext.uriInfo);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
assertThat(response.getRequestId()).isEqualTo(request.getRequestId());
assertThat(response.getHeaders()).isEqualTo(request.getHeaders());
assertThat(response.getBody()).isEqualTo(responseBody);
verify(testContext.loanChargesApiResource).retrieveLoanCharge(eq(loanId), eq(chargeId), testContext.uriInfoCaptor.capture());
MutableUriInfo mutableUriInfo = testContext.uriInfoCaptor.getValue();
assertThat(mutableUriInfo.getAdditionalQueryParameters()).hasSize(numberOfQueryParams);
}
/**
* Creates and returns a request with the given loan id and charge id.
*
* @param loanId
* the loan id
* @param chargeId
* the charge id
* @param queryParameter
* the query parameter
* @return BatchRequest
*/
private BatchRequest getBatchRequest(final Long loanId, final Long chargeId, final String queryParameter) {
final BatchRequest br = new BatchRequest();
String relativeUrl = "loans/" + loanId + "/charges/" + chargeId;
if (queryParameter != null) {
relativeUrl = relativeUrl + "?" + queryParameter;
}
br.setRequestId(Long.valueOf(RandomStringUtils.randomNumeric(5)));
br.setRelativeUrl(relativeUrl);
br.setMethod(HttpMethod.GET);
br.setReference(Long.valueOf(RandomStringUtils.randomNumeric(5)));
br.setBody("{}");
return br;
}
/**
* Private test context class used since testng runs in parallel to avoid state between tests
*/
private static class TestContext {
/**
* The subject to test
*/
private final GetChargeByIdCommandStrategy subjectToTest;
/**
* The mock uri info
*/
@Mock
private UriInfo uriInfo;
/**
* The mock {@link LoanChargesApiResource} object.
*/
@Mock
private LoanChargesApiResource loanChargesApiResource;
/**
* The uri info captor
*/
@Captor
private ArgumentCaptor<MutableUriInfo> uriInfoCaptor;
/**
* Test Context constructor
*/
TestContext() {
MockitoAnnotations.openMocks(this);
subjectToTest = new GetChargeByIdCommandStrategy(loanChargesApiResource);
}
}
}