From 40b6eb50f6b17b3f5bd00512bfc883384c83843d Mon Sep 17 00:00:00 2001 From: Jose Alberto Hernandez Date: Sun, 18 Sep 2022 18:33:41 -0500 Subject: [PATCH] Chargeback Loan Transaction --- .../avro/payment/v1/PaymentTypeDataV1.avsc | 16 ++ .../service/CommandWrapperBuilder.java | 9 ++ ...oanChargebackTransactionBusinessEvent.java | 40 +++++ .../service/InteropServiceImpl.java | 4 +- .../NotificationDomainServiceImpl.java | 17 +++ .../portfolio/charge/domain/Charge.java | 2 +- .../loanaccount/api/LoanApiConstants.java | 5 + .../api/LoanTransactionsApiResource.java | 11 +- .../loanaccount/data/LoanTransactionData.java | 6 + .../data/LoanTransactionRelationData.java | 39 +++++ .../portfolio/loanaccount/domain/Loan.java | 18 ++- .../loanaccount/domain/LoanEvent.java | 3 +- .../loanaccount/domain/LoanTransaction.java | 24 ++- .../domain/LoanTransactionRelation.java | 65 +++++++++ .../LoanTransactionRelationRepository.java | 31 ++++ .../LoanTransactionRelationTypeEnum.java | 54 +++++++ .../domain/LoanTransactionType.java | 5 + ...LoanRepaymentChargebackCommandHandler.java | 43 ++++++ .../mapper/LoanTransactionRelationMapper.java | 39 +++++ .../LoanEventApiJsonValidator.java | 44 +++++- .../service/LoanReadPlatformService.java | 4 + .../service/LoanReadPlatformServiceImpl.java | 100 ++++++------- .../service/LoanWritePlatformService.java | 2 + ...WritePlatformServiceJpaRepositoryImpl.java | 104 ++++++++++++- .../paymentdetail/domain/PaymentDetail.java | 2 +- .../api/PaymentTypeApiResource.java | 31 ++-- .../api/PaymentTypeApiResourceConstants.java | 2 + .../api/PaymentTypeApiResourceSwagger.java | 16 ++ .../paymenttype/data/PaymentTypeData.java | 14 +- .../data/PaymentTypeDataValidator.java | 34 ++++- .../paymenttype/domain/PaymentType.java | 36 ++--- .../domain/PaymentTypeRepository.java | 5 + .../domain/PaymentTypeRepositoryWrapper.java | 13 +- .../paymenttype/mapper/PaymentTypeMapper.java | 40 +++++ .../PaymentTypeReadPlatformService.java | 2 + .../PaymentTypeReadPlatformServiceImpl.java | 60 +++----- .../service/PaymentTypeWriteServiceImpl.java | 19 +-- .../savings/domain/SavingsAccount.java | 4 +- ...SavingsAccountReadPlatformServiceImpl.java | 3 +- .../db/changelog/tenant/changelog-tenant.xml | 1 + .../0052_loan_transaction_chargeback.xml | 134 +++++++++++++++++ .../LoanTransactionChargebackTest.java | 138 ++++++++++++++++++ .../common/PaymentTypeHelper.java | 13 +- .../common/loans/LoanTransactionHelper.java | 9 ++ 44 files changed, 1080 insertions(+), 181 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanChargebackTransactionBusinessEvent.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionRelationData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanRepaymentChargebackCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionRelationMapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/mapper/PaymentTypeMapper.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0052_loan_transaction_chargeback.xml create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java diff --git a/fineract-avro-schemas/src/main/avro/payment/v1/PaymentTypeDataV1.avsc b/fineract-avro-schemas/src/main/avro/payment/v1/PaymentTypeDataV1.avsc index a1a02919c..0c96a5b7e 100644 --- a/fineract-avro-schemas/src/main/avro/payment/v1/PaymentTypeDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/payment/v1/PaymentTypeDataV1.avsc @@ -42,6 +42,22 @@ "null", "long" ] + }, + { + "default": null, + "name": "codeName", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "isSystemDefined", + "type": [ + "null", + "boolean" + ] } ] } diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 952da0cec..28d523242 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -905,6 +905,15 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder chargebackTransaction(final Long loanId, final Long transactionId) { + this.actionName = "CHARGEBACK"; + this.entityName = "LOAN"; + this.entityId = transactionId; + this.loanId = loanId; + this.href = "/loans/" + loanId + "/transactions/" + transactionId; + return this; + } + public CommandWrapperBuilder loanForeclosure(final Long loanId) { this.actionName = "FORECLOSURE"; this.entityName = "LOAN"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanChargebackTransactionBusinessEvent.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanChargebackTransactionBusinessEvent.java new file mode 100644 index 000000000..6cc63c795 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanChargebackTransactionBusinessEvent.java @@ -0,0 +1,40 @@ +/** + * 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.loan; + +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; + +public class LoanChargebackTransactionBusinessEvent extends LoanTransactionBusinessEvent { + + public static final String LOAN_CHARGEBACK_TRANSACTION_BUSINESS_EVENT = "LoanChargebackTransactionBusinessEvent"; + public static final String LOAN_CHARGEBACK_TRANSACTION_PERMISSION = "LOAN_CHARGEBACK"; + public static final String LOAN_CHARGEBACK_TRANSACTION_OBJECT_TYPE = "loanTransaction"; + public static final String LOAN_CHARGEBACK_TRANSACTION_EVENT_TYPE = "loanTransactionChargeback"; + public static final String LOAN_CHARGEBACK_TRANSACTION_NOTIFICATION = "Loan Transaction has been chargeback"; + + public LoanChargebackTransactionBusinessEvent(LoanTransaction transactionToChargeback) { + super(transactionToChargeback); + } + + @Override + public String getType() { + return LOAN_CHARGEBACK_TRANSACTION_BUSINESS_EVENT; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java index 81691513c..c045c09aa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java @@ -332,7 +332,7 @@ public class InteropServiceImpl implements InteropService { InteropTransactionRequestData request = dataValidator.validateAndParseCreateRequest(command); // TODO: error handling - SavingsAccount savingsAccount = validateAndGetSavingAccount(request); + validateAndGetSavingAccount(request); return InteropTransactionRequestResponseData.build(command.commandId(), request.getTransactionCode(), InteropActionState.ACCEPTED, request.getExpiration(), request.getExtensionList(), request.getRequestCode()); @@ -617,7 +617,7 @@ public class InteropServiceImpl implements InteropService { PaymentType findPaymentType() { List paymentTypes = paymentTypeRepository.findAll(); for (PaymentType paymentType : paymentTypes) { - if (!paymentType.isCashPayment()) { + if (!paymentType.getIsCashPayment()) { return paymentType; } // TODO: for now first not cash is retured: diff --git a/fineract-provider/src/main/java/org/apache/fineract/notification/service/NotificationDomainServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/notification/service/NotificationDomainServiceImpl.java index e68eaeb30..9e607f73e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/notification/service/NotificationDomainServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/notification/service/NotificationDomainServiceImpl.java @@ -34,6 +34,7 @@ import org.apache.fineract.infrastructure.event.business.domain.deposit.Recurrin import org.apache.fineract.infrastructure.event.business.domain.group.CentersCreateBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.group.GroupsCreateBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanApprovedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.LoanChargebackTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCloseAsRescheduleBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCloseBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCreatedBusinessEvent; @@ -53,6 +54,7 @@ import org.apache.fineract.notification.data.NotificationData; import org.apache.fineract.notification.eventandlistener.NotificationEventPublisher; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.domain.FixedDepositAccount; @@ -92,6 +94,8 @@ public class NotificationDomainServiceImpl implements NotificationDomainService businessEventNotifierService.addPostBusinessEventListener(LoanCreatedBusinessEvent.class, new LoanCreatedListener()); businessEventNotifierService.addPostBusinessEventListener(LoanApprovedBusinessEvent.class, new LoanApprovedListener()); businessEventNotifierService.addPostBusinessEventListener(LoanCloseBusinessEvent.class, new LoanClosedListener()); + businessEventNotifierService.addPostBusinessEventListener(LoanChargebackTransactionBusinessEvent.class, + new LoanChargebackTransactionListener()); businessEventNotifierService.addPostBusinessEventListener(LoanCloseAsRescheduleBusinessEvent.class, new LoanCloseAsRescheduledListener()); businessEventNotifierService.addPostBusinessEventListener(LoanTransactionMakeRepaymentPostBusinessEvent.class, @@ -247,6 +251,19 @@ public class NotificationDomainServiceImpl implements NotificationDomainService } } + private class LoanChargebackTransactionListener implements BusinessEventListener { + + @Override + public void onBusinessEvent(LoanChargebackTransactionBusinessEvent event) { + LoanTransaction loanTransaction = event.get(); + buildNotification(LoanChargebackTransactionBusinessEvent.LOAN_CHARGEBACK_TRANSACTION_PERMISSION, + LoanChargebackTransactionBusinessEvent.LOAN_CHARGEBACK_TRANSACTION_OBJECT_TYPE, loanTransaction.getId(), + LoanChargebackTransactionBusinessEvent.LOAN_CHARGEBACK_TRANSACTION_NOTIFICATION, + LoanChargebackTransactionBusinessEvent.LOAN_CHARGEBACK_TRANSACTION_EVENT_TYPE, context.authenticatedUser().getId(), + loanTransaction.getLoan().getOfficeId()); + } + } + private class LoanMakeRepaymentListener implements BusinessEventListener { @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java index 8d2bf69d8..45270e9d1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java @@ -670,7 +670,7 @@ public class Charge extends AbstractPersistableCustom { PaymentTypeData paymentTypeData = null; if (this.paymentType != null) { - paymentTypeData = PaymentTypeData.instance(paymentType.getId(), paymentType.getPaymentName()); + paymentTypeData = PaymentTypeData.instance(paymentType.getId(), paymentType.getName()); } final CurrencyData currency = new CurrencyData(this.currencyCode, null, 0, 0, null, null); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java index 47eb7d89e..5c3281ee3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java @@ -150,4 +150,9 @@ public interface LoanApiConstants { String DISALLOW_EXPECTED_DISBURSEMENTS = "disallowExpectedDisbursements"; + String TRANSACTION_AMOUNT_PARAMNAME = "transactionAmount"; + String PAYMENT_TYPE_PARAMNAME = "paymentTypeId"; + + // Commands + String CHARGEBACK_TRANSACTION_COMMAND = "chargeback"; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java index 62bc82943..a15b976a2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java @@ -275,10 +275,17 @@ public class LoanTransactionsApiResource { @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.PostLoansLoanIdTransactionsTransactionIdResponse.class))) }) public String adjustLoanTransaction(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, @PathParam("transactionId") @Parameter(description = "transactionId") final Long transactionId, - @Parameter(hidden = true) final String apiRequestBodyAsJson) { + @Parameter(hidden = true) final String apiRequestBodyAsJson, + @QueryParam("command") @Parameter(description = "command") final String commandParam) { final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson); - final CommandWrapper commandRequest = builder.adjustTransaction(loanId, transactionId).build(); + + CommandWrapper commandRequest = null; + if (CommandParameterUtil.is(commandParam, LoanApiConstants.CHARGEBACK_TRANSACTION_COMMAND)) { + commandRequest = builder.chargebackTransaction(loanId, transactionId).build(); + } else { // Default to adjust the Loan Transaction + commandRequest = builder.adjustTransaction(loanId, transactionId).build(); + } final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java index 1d1006855..e32065d36 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java @@ -91,6 +91,8 @@ public class LoanTransactionData { private String reversalExternalId; private LocalDate reversedOnDate; + private List transactionRelations; + public static LoanTransactionData importInstance(BigDecimal repaymentAmount, LocalDate lastRepaymentDate, Long repaymentTypeId, Integer rowIndex, String locale, String dateFormat) { return new LoanTransactionData(repaymentAmount, lastRepaymentDate, repaymentTypeId, rowIndex, locale, dateFormat); @@ -348,4 +350,8 @@ public class LoanTransactionData { public void setLoanChargePaidByList(Collection loanChargePaidByList) { this.loanChargePaidByList = loanChargePaidByList; } + + public void setLoanTransactionRelations(List transactionRelations) { + this.transactionRelations = transactionRelations; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionRelationData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionRelationData.java new file mode 100644 index 000000000..aa848ed72 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionRelationData.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.portfolio.loanaccount.data; + +import java.io.Serializable; +import java.math.BigDecimal; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; + +@Getter +@Setter +@AllArgsConstructor +public class LoanTransactionRelationData implements Serializable { + + private Long fromLoanTransaction; + private Long toLoanTransaction; + private LoanTransactionRelationTypeEnum relationType; + private BigDecimal amount; + private String paymentType; + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index f3d72a97d..d70c6ab28 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -1338,7 +1338,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final Money principal = this.loanRepaymentScheduleDetail.getPrincipal(); this.summary.updateSummary(loanCurrency(), principal, getRepaymentScheduleInstallments(), this.loanSummaryWrapper, isDisbursed(), this.charges); - updateLoanOutstandingBalaces(); + updateLoanOutstandingBalances(); } } @@ -2850,7 +2850,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { chargesPayment.updateComponentsAndTotal(zero, zero, disbursentMoney, zero); chargesPayment.updateLoan(this); addLoanTransaction(chargesPayment); - updateLoanOutstandingBalaces(); + updateLoanOutstandingBalances(); } if (getApprovedOnDate() != null && disbursedOn.isBefore(getApprovedOnDate())) { @@ -2897,7 +2897,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { chargesPayment.updateComponents(zero, zero, charge.getAmount(getCurrency()), zero); chargesPayment.updateLoan(this); addLoanTransaction(chargesPayment); - updateLoanOutstandingBalaces(); + updateLoanOutstandingBalances(); charge.markAsFullyPaid(); return chargesPayment; } @@ -3426,7 +3426,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { addLoanTransaction(finalAccrual); } } - updateLoanOutstandingBalaces(); + updateLoanOutstandingBalances(); } private void determineCumulativeIncomeFromInstallments(HashMap cumulativeIncomeFromInstallments) { @@ -3898,6 +3898,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return changedTransactionDetail; } + public void handleChargebackTransaction(LoanTransaction chargebackTransaction, + final LoanLifecycleStateMachine loanLifecycleStateMachine) { + chargebackTransaction.updateLoan(this); + // TODO To be updated in the repayment schedule + } + /** * Behaviour added to comply with capability of previous mifos product to support easier transition to fineract * platform. @@ -5422,7 +5428,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { addLoanTransaction(accrual); } } - updateLoanOutstandingBalaces(); + updateLoanOutstandingBalances(); } private void determineFeeDetails(LocalDate fromDate, LocalDate toDate, HashMap feeDetails) { @@ -5665,7 +5671,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return interestRecalculatedOn; } - private void updateLoanOutstandingBalaces() { + private void updateLoanOutstandingBalances() { Money outstanding = Money.zero(getCurrency()); List loanTransactions = retreiveListOfTransactionsExcludeAccruals(); for (LoanTransaction loanTransaction : loanTransactions) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java index 9d7f1dfba..e0c11e21f 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java @@ -48,5 +48,6 @@ public enum LoanEvent { LOAN_INITIATE_TRANSFER, // LOAN_REJECT_TRANSFER, // LOAN_WITHDRAW_TRANSFER, // - LOAN_CREDIT_BALANCE_REFUND; + LOAN_CREDIT_BALANCE_REFUND, // + LOAN_CHARGEBACK; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index a4d5d3e00..22313cc2b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -130,6 +130,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, mappedBy = "loanTransaction") private Set loanTransactionToRepaymentScheduleMappings = new HashSet<>(); + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "fromTransaction") + private Set loanTransactionRelations = new HashSet<>(); + protected LoanTransaction() {} public static LoanTransaction incomePosting(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount, @@ -155,6 +158,14 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { return new LoanTransaction(null, office, LoanTransactionType.REPAYMENT, paymentDetail, amount.getAmount(), paymentDate, externalId); } + public static LoanTransaction chargeback(final Loan loan, final Money amount, final PaymentDetail paymentDetail, + final LocalDate paymentDate, final String externalId) { + LoanTransaction loanTransaction = new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CHARGEBACK, paymentDetail, + amount.getAmount(), paymentDate, externalId); + loanTransaction.principalPortion = amount.getAmount(); + return loanTransaction; + } + public static LoanTransaction repaymentType(final LoanTransactionType repaymentType, final Office office, final Money amount, final PaymentDetail paymentDetail, final LocalDate paymentDate, final String externalId, final String chargeRefundChargeType) { return new LoanTransaction(null, office, repaymentType, paymentDetail, amount.getAmount(), paymentDate, externalId, @@ -170,7 +181,6 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { break; } } - } public static LoanTransaction recoveryRepayment(final Office office, final Money amount, final PaymentDetail paymentDetail, @@ -589,6 +599,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { return getTypeOf().isChargePayment() && isNotReversed(); } + public boolean isChargeback() { + return getTypeOf().isChargeback() && isNotReversed(); + } + public boolean isPenaltyPayment() { boolean isPenalty = false; if (isChargePayment()) { @@ -849,6 +863,14 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { return submittedOnDate; } + public Set getLoanTransactionRelations() { + return loanTransactionRelations; + } + + public BigDecimal getAmount() { + return amount; + } + // TODO missing hashCode(), equals(Object obj), but probably OK as long as // this is never stored in a Collection. } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java new file mode 100644 index 000000000..07cc3a57c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java @@ -0,0 +1,65 @@ +/** + * 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.loanaccount.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.JoinColumn; +import javax.persistence.Table; +import javax.persistence.Version; +import javax.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; + +@Getter +@Setter +@Entity +@Table(name = "m_loan_transaction_relation") +public class LoanTransactionRelation extends AbstractAuditableWithUTCDateTimeCustom { + + @JoinColumn(name = "from_loan_transaction_id", nullable = false) + private LoanTransaction fromTransaction; + + @JoinColumn(name = "to_loan_transaction_id", nullable = false) + private LoanTransaction toTransaction; + + @Enumerated(EnumType.ORDINAL) + @Column(name = "relation_type_enum", nullable = false) + private LoanTransactionRelationTypeEnum relationType; + + @Version + private Long version; + + protected LoanTransactionRelation() {} + + protected LoanTransactionRelation(@NotNull LoanTransaction fromTransaction, @NotNull LoanTransaction toTransaction, + LoanTransactionRelationTypeEnum relationType) { + this.fromTransaction = fromTransaction; + this.toTransaction = toTransaction; + this.relationType = relationType; + } + + public static LoanTransactionRelation instance(@NotNull LoanTransaction fromTransaction, @NotNull LoanTransaction toTransaction, + LoanTransactionRelationTypeEnum relation) { + return new LoanTransactionRelation(fromTransaction, toTransaction, relation); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationRepository.java new file mode 100644 index 000000000..6eeec08ea --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationRepository.java @@ -0,0 +1,31 @@ +/** + * 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.loanaccount.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface LoanTransactionRelationRepository + extends JpaRepository, JpaSpecificationExecutor { + + List findByFromTransactionAndRelationType(LoanTransaction loanTransaction, + LoanTransactionRelationTypeEnum relationType); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java new file mode 100644 index 000000000..d07987798 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelationTypeEnum.java @@ -0,0 +1,54 @@ +/** + * 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.loanaccount.domain; + +public enum LoanTransactionRelationTypeEnum { + + INVALID(0, "loanTransactionType.invalid"), // + CHARGEBACK(1, "loanTransactionRelationType.chargeback"); + + private final Integer value; + private final String code; + + LoanTransactionRelationTypeEnum(final Integer value, final String code) { + this.value = value; + this.code = code; + } + + public Integer getValue() { + return this.value; + } + + public String getCode() { + return this.code; + } + + public static LoanTransactionRelationTypeEnum fromInt(final Integer transactionType) { + + if (transactionType == null) { + return LoanTransactionRelationTypeEnum.INVALID; + } + + return switch (transactionType) { + case 1 -> LoanTransactionRelationTypeEnum.CHARGEBACK; + default -> LoanTransactionRelationTypeEnum.INVALID; + }; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java index 1515d656e..7fee78b88 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java @@ -173,4 +173,9 @@ public enum LoanTransactionType { public boolean isIncomePosting() { return this.value.equals(LoanTransactionType.INCOME_POSTING.getValue()); } + + public boolean isChargeback() { + return this.value.equals(LoanTransactionType.CHARGEBACK.getValue()); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanRepaymentChargebackCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanRepaymentChargebackCommandHandler.java new file mode 100644 index 000000000..58991fac3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanRepaymentChargebackCommandHandler.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.loanaccount.handler; + +import lombok.RequiredArgsConstructor; +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.loanaccount.service.LoanWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "LOAN", action = "CHARGEBACK") +public class LoanRepaymentChargebackCommandHandler implements NewCommandSourceHandler { + + private final LoanWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.chargebackLoanTransaction(command.getLoanId(), command.entityId(), command); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionRelationMapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionRelationMapper.java new file mode 100644 index 000000000..37f524e50 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionRelationMapper.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.portfolio.loanaccount.mapper; + +import java.util.List; +import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionRelationData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface LoanTransactionRelationMapper { + + @Mapping(target = "fromLoanTransaction", source = "source.fromTransaction.id") + @Mapping(target = "toLoanTransaction", source = "source.toTransaction.id") + @Mapping(target = "relationType", source = "source.relationType") + @Mapping(target = "amount", source = "source.toTransaction.amount") + @Mapping(target = "paymentType", source = "source.toTransaction.paymentDetail.paymentType.name") + LoanTransactionRelationData map(LoanTransactionRelation source); + + List map(List sources); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java index 1e0ac6c90..1da8a069f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java @@ -212,7 +212,37 @@ public final class LoanEventApiJsonValidator { baseDataValidator.reset().parameter(LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME).ignoreIfNull().value(reversalExternalId) .notExceedingLengthOf(100); - validatePaymentDetails(baseDataValidator, element); + validatePaymentDetails(baseDataValidator, element, false); + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateChargebackTransaction(final String json) { + + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Set transactionParameters = new HashSet<>(Arrays.asList(LoanApiConstants.TRANSACTION_AMOUNT_PARAMNAME, + LoanApiConstants.localeParameterName, LoanApiConstants.externalIdParameterName, LoanApiConstants.noteParameterName, + LoanApiConstants.PAYMENT_TYPE_PARAMNAME)); + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final BigDecimal transactionAmount = this.fromApiJsonHelper + .extractBigDecimalWithLocaleNamed(LoanApiConstants.TRANSACTION_AMOUNT_PARAMNAME, element); + baseDataValidator.reset().parameter(LoanApiConstants.TRANSACTION_AMOUNT_PARAMNAME).value(transactionAmount).notNull() + .zeroOrPositiveAmount(); + + final String note = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.noteParameterName, element); + baseDataValidator.reset().parameter(LoanApiConstants.noteParameterName).value(note).notExceedingLengthOf(1000); + + validatePaymentDetails(baseDataValidator, element, true); throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -262,9 +292,19 @@ public final class LoanEventApiJsonValidator { } private void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element) { + final boolean paymentDetailRequired = false; // Default value for backward compatibility + validatePaymentDetails(baseDataValidator, element, paymentDetailRequired); + } + + private void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element, + final boolean paymentDetailRequired) { // Validate all string payment detail fields for max length final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("paymentTypeId", element); - baseDataValidator.reset().parameter("paymentTypeId").value(paymentTypeId).ignoreIfNull().integerGreaterThanZero(); + if (paymentDetailRequired) { + baseDataValidator.reset().parameter("paymentTypeId").value(paymentTypeId).notNull().integerGreaterThanZero(); + } else { + baseDataValidator.reset().parameter("paymentTypeId").value(paymentTypeId).ignoreIfNull().integerGreaterThanZero(); + } final Set paymentDetailParameters = new HashSet<>( Arrays.asList("accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber")); for (final String paymentDetailParameterName : paymentDetailParameters) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java index f52f20163..b47ff7547 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java @@ -36,6 +36,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData; import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; +import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionRelationData; import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData; import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; import org.apache.fineract.portfolio.loanaccount.domain.Loan; @@ -157,4 +158,7 @@ public interface LoanReadPlatformService { List getRepaymentDataResponse(Long loanId); CollectionData retrieveLoanCollectionData(Long loanId); + + List retrieveLoanTransactionRelationsByLoanTransactionId(Long loanTransactionId); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 9c0d3266e..7005b8adb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; +import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.infrastructure.codes.data.CodeValueData; @@ -97,6 +98,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData; +import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionRelationData; import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData; import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; @@ -108,12 +110,17 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; +import org.apache.fineract.portfolio.loanaccount.mapper.LoanTransactionRelationMapper; import org.apache.fineract.portfolio.loanproduct.data.LoanProductData; import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData; import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; @@ -125,7 +132,6 @@ import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService; import org.apache.fineract.useradministration.domain.AppUser; import org.jetbrains.annotations.NotNull; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -136,6 +142,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; +@AllArgsConstructor @Service @Transactional(readOnly = true) public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { @@ -154,7 +161,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { private final CalendarReadPlatformService calendarReadPlatformService; private final StaffReadPlatformService staffReadPlatformService; private final PaginationHelper paginationHelper; - private final LoanMapper loanMapper; private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; private final PaymentTypeReadPlatformService paymentTypeReadPlatformService; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; @@ -165,48 +171,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { private final ColumnValidator columnValidator; private final DatabaseSpecificSQLGenerator sqlGenerator; private final DelinquencyReadPlatformService delinquencyReadPlatformService; - - @Autowired - public LoanReadPlatformServiceImpl(final PlatformSecurityContext context, - final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository, - final LoanProductReadPlatformService loanProductReadPlatformService, final ClientReadPlatformService clientReadPlatformService, - final GroupReadPlatformService groupReadPlatformService, final LoanDropdownReadPlatformService loanDropdownReadPlatformService, - final FundReadPlatformService fundReadPlatformService, final ChargeReadPlatformService chargeReadPlatformService, - final CodeValueReadPlatformService codeValueReadPlatformService, final JdbcTemplate jdbcTemplate, - final NamedParameterJdbcTemplate namedParameterJdbcTemplate, final CalendarReadPlatformService calendarReadPlatformService, - final StaffReadPlatformService staffReadPlatformService, final PaymentTypeReadPlatformService paymentTypeReadPlatformService, - final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, - final FloatingRatesReadPlatformService floatingRatesReadPlatformService, final LoanUtilService loanUtilService, - final ConfigurationDomainService configurationDomainService, - final DelinquencyReadPlatformService delinquencyReadPlatformService, - final AccountDetailsReadPlatformService accountDetailsReadPlatformService, final LoanRepositoryWrapper loanRepositoryWrapper, - final ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { - this.context = context; - this.loanRepositoryWrapper = loanRepositoryWrapper; - this.applicationCurrencyRepository = applicationCurrencyRepository; - this.loanProductReadPlatformService = loanProductReadPlatformService; - this.clientReadPlatformService = clientReadPlatformService; - this.groupReadPlatformService = groupReadPlatformService; - this.loanDropdownReadPlatformService = loanDropdownReadPlatformService; - this.fundReadPlatformService = fundReadPlatformService; - this.chargeReadPlatformService = chargeReadPlatformService; - this.codeValueReadPlatformService = codeValueReadPlatformService; - this.calendarReadPlatformService = calendarReadPlatformService; - this.staffReadPlatformService = staffReadPlatformService; - this.jdbcTemplate = jdbcTemplate; - this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; - this.paymentTypeReadPlatformService = paymentTypeReadPlatformService; - this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory; - this.floatingRatesReadPlatformService = floatingRatesReadPlatformService; - this.loanUtilService = loanUtilService; - this.configurationDomainService = configurationDomainService; - this.accountDetailsReadPlatformService = accountDetailsReadPlatformService; - this.columnValidator = columnValidator; - this.loanMapper = new LoanMapper(sqlGenerator, delinquencyReadPlatformService); - this.sqlGenerator = sqlGenerator; - this.paginationHelper = paginationHelper; - this.delinquencyReadPlatformService = delinquencyReadPlatformService; - } + private final LoanTransactionRepository loanTransactionRepository; + private final LoanTransactionRelationRepository loanTransactionRelationRepository; + private final LoanTransactionRelationMapper loanTransactionRelationMapper; @Override public LoanAccountData retrieveOne(final Long loanId) { @@ -296,9 +263,16 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { /*** * TODO Vishwas: Remove references to "Contra" from the codebase ***/ - final String sql = "select " + rm.loanPaymentsSchema() - + " where tr.loan_id = ? and tr.transaction_type_enum not in (0, 3) and (tr.is_reversed=false or tr.manually_adjusted_or_reversed = true) order by tr.transaction_date ASC,id "; - return this.jdbcTemplate.query(sql, rm, loanId); // NOSONAR + final String sql = "select " + rm.loanPaymentsSchema() + " where tr.loan_id = ? and tr.transaction_type_enum not in (0, 3) " + + " and (tr.is_reversed=false or tr.manually_adjusted_or_reversed = true) " + " order by tr.transaction_date ASC,id "; + Collection loanTransactionData = this.jdbcTemplate.query(sql, rm, loanId); // NOSONAR + for (LoanTransactionData loanTransaction : loanTransactionData) { + if (loanTransaction.getType().isRepaymentType()) { + loanTransaction + .setLoanTransactionRelations(this.retrieveLoanTransactionRelationsByLoanTransactionId(loanTransaction.getId())); + } + } + return loanTransactionData; } catch (final EmptyResultDataAccessException e) { return null; } @@ -310,10 +284,11 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final AppUser currentUser = this.context.authenticatedUser(); final String hierarchy = currentUser.getOffice().getHierarchy(); final String hierarchySearchString = hierarchy + "%"; + final LoanMapper loanMapper = new LoanMapper(sqlGenerator, delinquencyReadPlatformService); final StringBuilder sqlBuilder = new StringBuilder(200); sqlBuilder.append("select " + sqlGenerator.calcFoundRows() + " "); - sqlBuilder.append(this.loanMapper.loanSchema()); + sqlBuilder.append(loanMapper.loanSchema()); // TODO - for time being this will data scope list of loans returned to // only loans that have a client associated. @@ -377,7 +352,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { } final Object[] objectArray = extraCriterias.toArray(); final Object[] finalObjectArray = Arrays.copyOf(objectArray, arrayPos); - return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), finalObjectArray, this.loanMapper); + return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), finalObjectArray, loanMapper); } @Override @@ -588,7 +563,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { try { final LoanTransactionsMapper rm = new LoanTransactionsMapper(sqlGenerator); final String sql = "select " + rm.loanPaymentsSchema() + " where l.id = ? and tr.id = ? "; - return this.jdbcTemplate.queryForObject(sql, rm, loanId, transactionId); // NOSONAR + LoanTransactionData loanTransactionData = this.jdbcTemplate.queryForObject(sql, rm, loanId, transactionId); // NOSONAR + loanTransactionData.setLoanTransactionRelations(this.retrieveLoanTransactionRelationsByLoanTransactionId(transactionId)); + return loanTransactionData; } catch (final EmptyResultDataAccessException e) { throw new LoanTransactionNotFoundException(transactionId, e); } @@ -1310,10 +1287,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { public String loanPaymentsSchema() { return " tr.id as id, tr.transaction_type_enum as transactionType, tr.transaction_date as " + sqlGenerator.escape("date") - + ", tr.amount as total, " + " tr.principal_portion_derived as principal, tr.interest_portion_derived as interest, " - + " tr.fee_charges_portion_derived as fees, tr.penalty_charges_portion_derived as penalties, " + + ", tr.amount as total, tr.principal_portion_derived as principal, tr.interest_portion_derived as interest, " + + " tr.fee_charges_portion_derived as fees, tr.penalty_charges_portion_derived as penalties, " + " tr.overpayment_portion_derived as overpayment, tr.outstanding_loan_balance_derived as outstandingLoanBalance, " - + " tr.unrecognized_income_portion as unrecognizedIncome," + " tr.submitted_on_date as submittedOnDate, " + + " tr.unrecognized_income_portion as unrecognizedIncome, tr.submitted_on_date as submittedOnDate, " + " tr.manually_adjusted_or_reversed as manuallyReversed, tr.reversal_external_id as reversalExternalId, tr.reversed_on_date as reversedOnDate, " + " pd.payment_type_id as paymentType,pd.account_number as accountNumber,pd.check_number as checkNumber, " + " pd.receipt_number as receiptNumber, pd.bank_number as bankNumber,pd.routing_code as routingCode, l.net_disbursal_amount as netDisbursalAmount," @@ -1323,10 +1300,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { + " pt.value as paymentTypeName, tr.external_id as externalId, tr.office_id as officeId, office.name as officeName, " + " fromtran.id as fromTransferId, fromtran.is_reversed as fromTransferReversed," + " fromtran.transaction_date as fromTransferDate, fromtran.amount as fromTransferAmount," - + " fromtran.description as fromTransferDescription," - + " totran.id as toTransferId, totran.is_reversed as toTransferReversed," + + " fromtran.description as fromTransferDescription, " + + " totran.id as toTransferId, totran.is_reversed as toTransferReversed, " + " totran.transaction_date as toTransferDate, totran.amount as toTransferAmount," - + " totran.description as toTransferDescription " + " from m_loan l join m_loan_transaction tr on tr.loan_id = l.id" + + " totran.description as toTransferDescription from m_loan l join m_loan_transaction tr on tr.loan_id = l.id " + " join m_currency rc on rc." + sqlGenerator.escape("code") + " = l.currency_code " + " left JOIN m_payment_detail pd ON tr.payment_detail_id = pd.id" + " left join m_payment_type pt on pd.payment_type_id = pt.id" + " left join m_office office on office.id=tr.office_id" @@ -1405,6 +1382,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { transfer = AccountTransferData.transferBasicDetails(toTransferId, currencyData, toTransferAmount, toTransferDate, toTransferDescription, toTransferReversed); } + return new LoanTransactionData(id, officeId, officeName, transactionType, paymentDetailData, currencyData, date, totalAmount, netDisbursalAmount, principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion, overPaymentPortion, unrecognizedIncomePortion, externalId, transfer, null, outstandingLoanBalance, submittedOnDate, manuallyReversed, @@ -2066,7 +2044,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { public String schema() { return " tr.id as id, tr.transaction_type_enum as transactionType, tr.transaction_date as " + sqlGenerator.escape("date") - + ", tr.amount as total, " + " tr.principal_portion_derived as principal, tr.interest_portion_derived as interest, " + + ", tr.amount as total, tr.principal_portion_derived as principal, tr.interest_portion_derived as interest, " + " tr.fee_charges_portion_derived as fees, tr.penalty_charges_portion_derived as penalties, " + " tr.overpayment_portion_derived as overpayment, tr.outstanding_loan_balance_derived as outstandingLoanBalance, " + " tr.unrecognized_income_portion as unrecognizedIncome " + " from m_loan_transaction tr "; @@ -2492,4 +2470,12 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { delinquentAmount, lastPaymentDate, lastPaymentAmount); } } + + @Override + public List retrieveLoanTransactionRelationsByLoanTransactionId(Long loanTransactionId) { + final LoanTransaction loanTransaction = this.loanTransactionRepository.getReferenceById(loanTransactionId); + List loanTransactionRelations = this.loanTransactionRelationRepository + .findByFromTransactionAndRelationType(loanTransaction, LoanTransactionRelationTypeEnum.CHARGEBACK); + return loanTransactionRelationMapper.map(loanTransactionRelations); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java index d8e8a29c1..fb419edd7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformService.java @@ -50,6 +50,8 @@ public interface LoanWritePlatformService { CommandProcessingResult adjustLoanTransaction(Long loanId, Long transactionId, JsonCommand command); + CommandProcessingResult chargebackLoanTransaction(Long loanId, Long transactionId, JsonCommand command); + CommandProcessingResult waiveInterestOnLoan(Long loanId, JsonCommand command); CommandProcessingResult writeOff(Long loanId, JsonCommand command); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 7115b2d90..7d010b191 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -59,6 +59,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAcceptT import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanApplyOverdueChargeBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.loan.LoanChargebackTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCloseAsRescheduleBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCloseBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanDisbursalBusinessEvent; @@ -180,6 +181,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheDisbursementCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException; @@ -237,6 +241,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final LoanAccountDomainService loanAccountDomainService; private final NoteRepository noteRepository; private final LoanTransactionRepository loanTransactionRepository; + private final LoanTransactionRelationRepository loanTransactionRelationRepository; private final LoanAssembler loanAssembler; private final ChargeRepositoryWrapper chargeRepository; private final LoanChargeRepository loanChargeRepository; @@ -392,7 +397,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Map changes = new LinkedHashMap<>(); final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); - if (paymentDetail != null && paymentDetail.getPaymentType() != null && paymentDetail.getPaymentType().isCashPayment()) { + if (paymentDetail != null && paymentDetail.getPaymentType() != null && paymentDetail.getPaymentType().getIsCashPayment()) { BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); this.cashierTransactionDataValidator.validateOnLoanDisbursal(currentUser, loan.getCurrencyCode(), transactionAmount); } @@ -1184,6 +1189,103 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .with(changes).build(); } + @Transactional + @Override + public CommandProcessingResult chargebackLoanTransaction(final Long loanId, final Long transactionId, final JsonCommand command) { + this.loanEventApiJsonValidator.validateChargebackTransaction(command.json()); + + Loan loan = this.loanAssembler.assembleFrom(loanId); + if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.LOAN)) { + throw new PlatformServiceUnavailableException("error.msg.loan.transfer.transaction.update.not.allowed", + "Loan transaction:" + transactionId + " chargeback not allowed as it involves in account transfer", transactionId); + } + if (loan.isClosedWrittenOff()) { + throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed", + "Loan transaction:" + transactionId + " chargeback not allowed as loan status is written off", transactionId); + } + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed", + "Loan transaction:" + transactionId + " chargeback not allowed as loan product is interest recalculation enabled", + transactionId); + } + + final List existingTransactionIds = new ArrayList<>(); + final List existingReversedTransactionIds = new ArrayList<>(); + + checkClientOrGroupActive(loan); + + LoanTransaction loanTransaction = this.loanTransactionRepository.findById(transactionId) + .orElseThrow(() -> new LoanTransactionNotFoundException(transactionId)); + + if (!loanTransaction.isRepayment()) { + throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed", + "Loan transaction:" + transactionId + " chargeback not allowed as loan transaction is not repayment", transactionId); + } + + businessEventNotifierService.notifyPreBusinessEvent(new LoanChargebackTransactionBusinessEvent(loanTransaction)); + + final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.TRANSACTION_AMOUNT_PARAMNAME); + final String txnExternalId = command.stringValueOfParameterNamedAllowingNull(LoanApiConstants.externalIdParameterName); + + final Map changes = new LinkedHashMap<>(); + changes.put("transactionAmount", command.stringValueOfParameterNamed(LoanApiConstants.TRANSACTION_AMOUNT_PARAMNAME)); + changes.put("locale", command.locale()); + changes.put("dateFormat", command.dateFormat()); + changes.put("paymentTypeId", command.stringValueOfParameterNamed(LoanApiConstants.PAYMENT_TYPE_PARAMNAME)); + + final Money transactionAmountAsMoney = Money.of(loan.getCurrency(), transactionAmount); + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createPaymentDetail(command, changes); + LoanTransaction newTransaction = LoanTransaction.chargeback(loan, transactionAmountAsMoney, paymentDetail, transactionDate, + txnExternalId); + + validateLoanTransactionAmountChargeBack(loanTransaction, newTransaction); + + this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail); + + loan.handleChargebackTransaction(newTransaction, defaultLoanLifecycleStateMachine()); + + loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); + + // Store the Loan Transaction Relation + LoanTransactionRelation loanTransactionRelation = LoanTransactionRelation.instance(loanTransaction, newTransaction, + LoanTransactionRelationTypeEnum.CHARGEBACK); + this.loanTransactionRelationRepository.save(loanTransactionRelation); + + this.loanTransactionRepository.saveAndFlush(newTransaction); + + final String noteText = command.stringValueOfParameterNamed(LoanApiConstants.noteParamName); + if (StringUtils.isNotBlank(noteText)) { + changes.put("note", noteText); + Note note = Note.loanTransactionNote(loan, newTransaction, noteText); + this.noteRepository.save(note); + } + + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); + + businessEventNotifierService.notifyPostBusinessEvent(new LoanChargebackTransactionBusinessEvent(loanTransaction)); + + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(transactionId) + .withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()).withGroupId(loan.getGroupId()).withLoanId(loanId) + .with(changes).build(); + } + + private void validateLoanTransactionAmountChargeBack(LoanTransaction loanTransaction, LoanTransaction chargebackTransaction) { + BigDecimal actualAmount = BigDecimal.ZERO; + for (LoanTransactionRelation loanTransactionRelation : loanTransaction.getLoanTransactionRelations()) { + if (loanTransactionRelation.getRelationType().equals(LoanTransactionRelationTypeEnum.CHARGEBACK)) { + actualAmount = actualAmount.add(loanTransactionRelation.getToTransaction().getPrincipalPortion()); + } + } + actualAmount = actualAmount.add(chargebackTransaction.getPrincipalPortion()); + if (actualAmount.compareTo(loanTransaction.getPrincipalPortion()) > 0) { + throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed", + "Loan transaction:" + loanTransaction.getId() + " chargeback not allowed as loan transaction amount is not enough", + loanTransaction.getId()); + } + } + @Transactional @Override public CommandProcessingResult waiveInterestOnLoan(final Long loanId, final JsonCommand command) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymentdetail/domain/PaymentDetail.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymentdetail/domain/PaymentDetail.java index 8c0cbaecf..b737c7631 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymentdetail/domain/PaymentDetail.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymentdetail/domain/PaymentDetail.java @@ -103,7 +103,7 @@ public final class PaymentDetail extends AbstractPersistableCustom { } public PaymentDetailData toData() { - final PaymentTypeData paymentTypeData = this.paymentType.toData(); + final PaymentTypeData paymentTypeData = null; // this.paymentType.toData(); final PaymentDetailData paymentDetailData = new PaymentDetailData(getId(), paymentTypeData, this.accountNumber, this.checkNumber, this.routingCode, this.receiptNumber, this.bankNumber); return paymentDetailData; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResource.java index f967457cf..aeb359e77 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResource.java @@ -36,9 +36,11 @@ import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriInfo; +import lombok.AllArgsConstructor; import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; @@ -50,9 +52,9 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepositoryWrapper; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +@AllArgsConstructor @Path("/paymenttypes") @Component @@ -64,34 +66,23 @@ public class PaymentTypeApiResource { private final PaymentTypeReadPlatformService readPlatformService; private final PortfolioCommandSourceWritePlatformService commandWritePlatformService; private final ApiRequestParameterHelper apiRequestParameterHelper; - // private final String resourceNameForPermissions = "PAYMENT_TYPE"; private final PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper; - // private final Set RESPONSE_DATA_PARAMETERS = new - // HashSet<>(Arrays.asList("id", "value", "description", "isCashPayment")); - - @Autowired - public PaymentTypeApiResource(PlatformSecurityContext securityContext, DefaultToApiJsonSerializer jsonSerializer, - PaymentTypeReadPlatformService readPlatformService, PaymentTypeRepositoryWrapper paymentTypeRepositoryWrapper, - ApiRequestParameterHelper apiRequestParameterHelper, PortfolioCommandSourceWritePlatformService commandWritePlatformService) { - - this.securityContext = securityContext; - this.jsonSerializer = jsonSerializer; - this.readPlatformService = readPlatformService; - this.paymentTypeRepositoryWrapper = paymentTypeRepositoryWrapper; - this.apiRequestParameterHelper = apiRequestParameterHelper; - this.commandWritePlatformService = commandWritePlatformService; - } - @GET @Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON }) @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Retrieve all Payment Types", description = "Retrieve list of payment types") @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = PaymentTypeApiResourceSwagger.GetPaymentTypesResponse.class)))) }) - public String getAllPaymentTypes(@Context final UriInfo uriInfo) { + public String getAllPaymentTypes(@Context final UriInfo uriInfo, + @QueryParam("onlyWithCode") @Parameter(description = "onlyWithCode") final boolean onlyWithCode) { this.securityContext.authenticatedUser().validateHasReadPermission(PaymentTypeApiResourceConstants.resourceNameForPermissions); - final Collection paymentTypes = this.readPlatformService.retrieveAllPaymentTypes(); + Collection paymentTypes = null; + if (onlyWithCode) { + paymentTypes = this.readPlatformService.retrieveAllPaymentTypesWithCode(); + } else { + paymentTypes = this.readPlatformService.retrieveAllPaymentTypes(); + } final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); return this.jsonSerializer.serialize(settings, paymentTypes, PaymentTypeApiResourceConstants.RESPONSE_DATA_PARAMETERS); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResourceConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResourceConstants.java index 8d8440983..be7b4f00f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResourceConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResourceConstants.java @@ -37,6 +37,8 @@ public final class PaymentTypeApiResourceConstants { public static final String DESCRIPTION = "description"; public static final String ISCASHPAYMENT = "isCashPayment"; public static final String POSITION = "position"; + public static final String CODE_NAME = "code_name"; + public static final String IS_SYSTEM_DEFINED = "system_defined"; static final Set RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(ID, NAME, DESCRIPTION, ISCASHPAYMENT)); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResourceSwagger.java index ead8bd51d..8813ec286 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/api/PaymentTypeApiResourceSwagger.java @@ -42,6 +42,10 @@ final class PaymentTypeApiResourceSwagger { public Boolean isCashPayment; @Schema(example = "0") public Integer position; + @Schema(example = "REPAYMENT_REFUND") + public String codeName; + @Schema(example = "false") + public Boolean isSystemDefined; } @Schema(description = "GetPaymentTypesPaymentTypeIdResponse") @@ -59,6 +63,10 @@ final class PaymentTypeApiResourceSwagger { public Boolean isCashPayment; @Schema(example = "1") public Integer position; + @Schema(example = "REPAYMENT_REFUND") + public String codeName; + @Schema(example = "false") + public Boolean isSystemDefined; } @Schema(description = "PostPaymentTypesRequest") @@ -74,6 +82,10 @@ final class PaymentTypeApiResourceSwagger { public Boolean isCashPayment; @Schema(example = "1") public Integer position; + @Schema(example = "REPAYMENT_REFUND") + public String codeName; + @Schema(example = "false") + public Boolean isSystemDefined; } @Schema(description = "PostPaymentTypesResponse") @@ -98,6 +110,10 @@ final class PaymentTypeApiResourceSwagger { public Boolean isCashPayment; @Schema(example = "3") public Integer position; + @Schema(example = "REPAYMENT_REFUND") + public String codeName; + @Schema(example = "false") + public Boolean isSystemDefined; } @Schema(description = "PutPaymentTypesPaymentTypeIdResponse") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/data/PaymentTypeData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/data/PaymentTypeData.java index ee18a19ff..b626469bb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/data/PaymentTypeData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/data/PaymentTypeData.java @@ -22,8 +22,12 @@ import java.io.Serializable; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +@ToString @Getter +@Setter @EqualsAndHashCode @AllArgsConstructor public class PaymentTypeData implements Serializable { @@ -33,16 +37,22 @@ public class PaymentTypeData implements Serializable { private String description; private Boolean isCashPayment; private Long position; + private String codeName; + private Boolean isSystemDefined; public static PaymentTypeData instance(final Long id, final String name, final String description, final Boolean isCashPayment, final Long position) { - return new PaymentTypeData(id, name, description, isCashPayment, position); + String codeName = null; + Boolean isSystemDefined = false; + return new PaymentTypeData(id, name, description, isCashPayment, position, codeName, isSystemDefined); } public static PaymentTypeData instance(final Long id, final String name) { String description = null; Boolean isCashPayment = null; Long position = null; - return new PaymentTypeData(id, name, description, isCashPayment, position); + String codeName = null; + Boolean isSystemDefined = false; + return new PaymentTypeData(id, name, description, isCashPayment, position, codeName, isSystemDefined); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/data/PaymentTypeDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/data/PaymentTypeDataValidator.java index b735af4a2..23a570242 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/data/PaymentTypeDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/data/PaymentTypeDataValidator.java @@ -43,11 +43,13 @@ public class PaymentTypeDataValidator { private final FromJsonHelper fromApiJsonHelper; private static final Set CREATE_PAYMENT_TYPE_REQUEST_DATA_PARAMETERS = new HashSet<>( Arrays.asList(PaymentTypeApiResourceConstants.NAME, PaymentTypeApiResourceConstants.DESCRIPTION, - PaymentTypeApiResourceConstants.ISCASHPAYMENT, PaymentTypeApiResourceConstants.POSITION)); + PaymentTypeApiResourceConstants.ISCASHPAYMENT, PaymentTypeApiResourceConstants.POSITION, + PaymentTypeApiResourceConstants.CODE_NAME, PaymentTypeApiResourceConstants.IS_SYSTEM_DEFINED)); private static final Set UPDATE_PAYMENT_TYPE_REQUEST_DATA_PARAMETERS = new HashSet<>( Arrays.asList(PaymentTypeApiResourceConstants.NAME, PaymentTypeApiResourceConstants.DESCRIPTION, - PaymentTypeApiResourceConstants.ISCASHPAYMENT, PaymentTypeApiResourceConstants.POSITION)); + PaymentTypeApiResourceConstants.ISCASHPAYMENT, PaymentTypeApiResourceConstants.POSITION, + PaymentTypeApiResourceConstants.CODE_NAME, PaymentTypeApiResourceConstants.IS_SYSTEM_DEFINED)); @Autowired public PaymentTypeDataValidator(final FromJsonHelper fromApiJsonHelper) { @@ -93,6 +95,19 @@ public class PaymentTypeDataValidator { .integerZeroOrGreater(); } + if (this.fromApiJsonHelper.parameterExists(PaymentTypeApiResourceConstants.CODE_NAME, element)) { + final String codeName = this.fromApiJsonHelper.extractStringNamed(PaymentTypeApiResourceConstants.CODE_NAME, element); + baseDataValidator.reset().parameter(PaymentTypeApiResourceConstants.CODE_NAME).value(codeName).ignoreIfNull() + .notExceedingLengthOf(100); + } + + if (this.fromApiJsonHelper.parameterExists(PaymentTypeApiResourceConstants.IS_SYSTEM_DEFINED, element)) { + final Boolean isSystemDefined = this.fromApiJsonHelper.extractBooleanNamed(PaymentTypeApiResourceConstants.IS_SYSTEM_DEFINED, + element); + baseDataValidator.reset().parameter(PaymentTypeApiResourceConstants.IS_SYSTEM_DEFINED).value(isSystemDefined) + .validateForBooleanValue(); + } + throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -142,7 +157,20 @@ public class PaymentTypeDataValidator { .integerZeroOrGreater(); } - throwExceptionIfValidationWarningsExist(dataValidationErrors); + if (this.fromApiJsonHelper.parameterExists(PaymentTypeApiResourceConstants.CODE_NAME, element)) { + final String codeName = this.fromApiJsonHelper.extractStringNamed(PaymentTypeApiResourceConstants.CODE_NAME, element); + baseDataValidator.reset().parameter(PaymentTypeApiResourceConstants.CODE_NAME).value(codeName).ignoreIfNull() + .notExceedingLengthOf(100); + } + if (this.fromApiJsonHelper.parameterExists(PaymentTypeApiResourceConstants.IS_SYSTEM_DEFINED, element)) { + final Boolean isSystemDefined = this.fromApiJsonHelper.extractBooleanNamed(PaymentTypeApiResourceConstants.IS_SYSTEM_DEFINED, + element); + baseDataValidator.reset().parameter(PaymentTypeApiResourceConstants.IS_SYSTEM_DEFINED).value(isSystemDefined) + .validateForBooleanValue(); + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/domain/PaymentType.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/domain/PaymentType.java index 1207eba64..ab1bd383b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/domain/PaymentType.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/domain/PaymentType.java @@ -23,14 +23,19 @@ import java.util.Map; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.portfolio.paymenttype.api.PaymentTypeApiResourceConstants; -import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; +@Getter +@Setter @Entity @Table(name = "m_payment_type") +@AllArgsConstructor public class PaymentType extends AbstractPersistableCustom { @Column(name = "value") @@ -45,19 +50,14 @@ public class PaymentType extends AbstractPersistableCustom { @Column(name = "order_position") private Long position; + @Column(name = "code_name") + private String codeName; + + @Column(name = "is_system_defined") + private Boolean isSystemDefined; + protected PaymentType() {} - public PaymentType(final String name, final String description, final Boolean isCashPayment, final Long position) { - this.name = name; - this.description = description; - this.isCashPayment = isCashPayment; - this.position = position; - } - - public static PaymentType create(String name, String description, Boolean isCashPayment, Long position) { - return new PaymentType(name, description, isCashPayment, position); - } - public Map update(final JsonCommand command) { final Map actualChanges = new LinkedHashMap<>(3); @@ -89,16 +89,4 @@ public class PaymentType extends AbstractPersistableCustom { return actualChanges; } - public PaymentTypeData toData() { - return PaymentTypeData.instance(getId(), this.name, this.description, this.isCashPayment, this.position); - } - - public Boolean isCashPayment() { - return isCashPayment; - } - - public String getPaymentName() { - return name; - } - } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/domain/PaymentTypeRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/domain/PaymentTypeRepository.java index d4119bdf2..eaa8ed285 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/domain/PaymentTypeRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/domain/PaymentTypeRepository.java @@ -18,9 +18,14 @@ */ package org.apache.fineract.portfolio.paymenttype.domain; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface PaymentTypeRepository extends JpaRepository, JpaSpecificationExecutor { + List findAllByOrderByPositionAsc(); + + List findAllByCodeNameIsNotNullOrderByPositionAsc(); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/domain/PaymentTypeRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/domain/PaymentTypeRepositoryWrapper.java index 6b03bdb71..8bcf81d02 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/domain/PaymentTypeRepositoryWrapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/domain/PaymentTypeRepositoryWrapper.java @@ -18,18 +18,23 @@ */ package org.apache.fineract.portfolio.paymenttype.domain; +import java.util.List; +import lombok.RequiredArgsConstructor; import org.apache.fineract.portfolio.paymenttype.exception.PaymentTypeNotFoundException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class PaymentTypeRepositoryWrapper { private final PaymentTypeRepository repository; - @Autowired - public PaymentTypeRepositoryWrapper(final PaymentTypeRepository repository) { - this.repository = repository; + public List findAll() { + return this.repository.findAllByOrderByPositionAsc(); + } + + public List findAllWithCodeName() { + return this.repository.findAllByCodeNameIsNotNullOrderByPositionAsc(); } public PaymentType findOneWithNotFoundDetection(final Long id) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/mapper/PaymentTypeMapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/mapper/PaymentTypeMapper.java new file mode 100644 index 000000000..af48e1336 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/mapper/PaymentTypeMapper.java @@ -0,0 +1,40 @@ +/** + * 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.paymenttype.mapper; + +import java.util.List; +import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; +import org.apache.fineract.portfolio.paymenttype.domain.PaymentType; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface PaymentTypeMapper { + + @Mapping(target = "id", source = "source.id") + @Mapping(target = "name", source = "source.name") + @Mapping(target = "description", source = "source.description") + @Mapping(target = "isCashPayment", source = "source.isCashPayment") + @Mapping(target = "codeName", source = "source.codeName") + @Mapping(target = "isSystemDefined", source = "source.isSystemDefined") + PaymentTypeData map(PaymentType source); + + List map(List sources); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeReadPlatformService.java index b3aead6d3..072f5a903 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeReadPlatformService.java @@ -25,6 +25,8 @@ public interface PaymentTypeReadPlatformService { Collection retrieveAllPaymentTypes(); + Collection retrieveAllPaymentTypesWithCode(); + PaymentTypeData retrieveOne(Long paymentTypeId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeReadPlatformServiceImpl.java index 65fd525b2..a7edbd5d4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeReadPlatformServiceImpl.java @@ -18,28 +18,24 @@ */ package org.apache.fineract.portfolio.paymenttype.service; -import java.sql.ResultSet; -import java.sql.SQLException; import java.util.Collection; +import java.util.List; +import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; -import org.springframework.beans.factory.annotation.Autowired; +import org.apache.fineract.portfolio.paymenttype.domain.PaymentType; +import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepositoryWrapper; +import org.apache.fineract.portfolio.paymenttype.mapper.PaymentTypeMapper; import org.springframework.cache.annotation.Cacheable; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class PaymentTypeReadPlatformServiceImpl implements PaymentTypeReadPlatformService { - private final JdbcTemplate jdbcTemplate; private final PlatformSecurityContext context; - - @Autowired - public PaymentTypeReadPlatformServiceImpl(final PlatformSecurityContext context, final JdbcTemplate jdbcTemplate) { - this.context = context; - this.jdbcTemplate = jdbcTemplate; - } + private final PaymentTypeMapper paymentTypeMapper; + private final PaymentTypeRepositoryWrapper paymentTypeRepository; @Override @Cacheable(value = "payment_types", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat('payment_types')") @@ -47,10 +43,18 @@ public class PaymentTypeReadPlatformServiceImpl implements PaymentTypeReadPlatfo // TODO Auto-generated method stub this.context.authenticatedUser(); - final PaymentTypeMapper ptm = new PaymentTypeMapper(); - final String sql = "select " + ptm.schema() + "order by position"; + List paymentType = this.paymentTypeRepository.findAll(); + return this.paymentTypeMapper.map(paymentType); + } - return this.jdbcTemplate.query(sql, ptm); // NOSONAR + @Override + @Cacheable(value = "paymentTypesWithCode", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat('payment_types')") + public Collection retrieveAllPaymentTypesWithCode() { + // TODO Auto-generated method stub + this.context.authenticatedUser(); + + List paymentType = this.paymentTypeRepository.findAllWithCodeName(); + return this.paymentTypeMapper.map(paymentType); } @Override @@ -58,30 +62,8 @@ public class PaymentTypeReadPlatformServiceImpl implements PaymentTypeReadPlatfo // TODO Auto-generated method stub this.context.authenticatedUser(); - final PaymentTypeMapper ptm = new PaymentTypeMapper(); - final String sql = "select " + ptm.schema() + "where pt.id = ?"; - - return this.jdbcTemplate.queryForObject(sql, ptm, new Object[] { paymentTypeId }); // NOSONAR - } - - private static final class PaymentTypeMapper implements RowMapper { - - public String schema() { - return " pt.id as id, pt.value as name, pt.description as description,pt.is_cash_payment as isCashPayment,pt.order_position as position from m_payment_type pt "; - } - - @Override - public PaymentTypeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { - - final Long id = rs.getLong("id"); - final String name = rs.getString("name"); - final String description = rs.getString("description"); - final boolean isCashPayment = rs.getBoolean("isCashPayment"); - final Long position = rs.getLong("position"); - - return PaymentTypeData.instance(id, name, description, isCashPayment, position); - } - + final PaymentType paymentType = this.paymentTypeRepository.findOneWithNotFoundDetection(paymentTypeId); + return this.paymentTypeMapper.map(paymentType); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeWriteServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeWriteServiceImpl.java index 0fcf4f26c..aa5a893c6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeWriteServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/paymenttype/service/PaymentTypeWriteServiceImpl.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.paymenttype.service; import java.util.Map; +import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; @@ -28,28 +29,19 @@ import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeDataValidator; import org.apache.fineract.portfolio.paymenttype.domain.PaymentType; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepository; import org.apache.fineract.portfolio.paymenttype.domain.PaymentTypeRepositoryWrapper; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.orm.jpa.JpaSystemException; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class PaymentTypeWriteServiceImpl implements PaymentTypeWriteService { private final PaymentTypeRepository repository; private final PaymentTypeRepositoryWrapper repositoryWrapper; private final PaymentTypeDataValidator fromApiJsonDeserializer; - @Autowired - public PaymentTypeWriteServiceImpl(PaymentTypeRepository repository, PaymentTypeRepositoryWrapper repositoryWrapper, - PaymentTypeDataValidator fromApiJsonDeserializer) { - this.repository = repository; - this.repositoryWrapper = repositoryWrapper; - this.fromApiJsonDeserializer = fromApiJsonDeserializer; - - } - @Override @CacheEvict(value = "payment_types", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat('payment_types')") public CommandProcessingResult createPaymentType(JsonCommand command) { @@ -58,8 +50,13 @@ public class PaymentTypeWriteServiceImpl implements PaymentTypeWriteService { String description = command.stringValueOfParameterNamed(PaymentTypeApiResourceConstants.DESCRIPTION); Boolean isCashPayment = command.booleanObjectValueOfParameterNamed(PaymentTypeApiResourceConstants.ISCASHPAYMENT); Long position = command.longValueOfParameterNamed(PaymentTypeApiResourceConstants.POSITION); + String codeName = command.stringValueOfParameterNamed(PaymentTypeApiResourceConstants.CODE_NAME); + Boolean isSystemDefined = command.booleanObjectValueOfParameterNamed(PaymentTypeApiResourceConstants.IS_SYSTEM_DEFINED); + if (isSystemDefined == null) { + isSystemDefined = false; + } - PaymentType newPaymentType = PaymentType.create(name, description, isCashPayment, position); + PaymentType newPaymentType = new PaymentType(name, description, isCashPayment, position, codeName, isSystemDefined); this.repository.saveAndFlush(newPaymentType); return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(newPaymentType.getId()).build(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index 459fac396..ae10f099a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -1322,11 +1322,11 @@ public class SavingsAccount extends AbstractPersistableCustom { if (charge.isEnablePaymentType() && charge.isEnableFreeWithdrawal()) { // discount transaction to // specific paymentType - if (paymentDetail.getPaymentType().getPaymentName().equals(charge.getCharge().getPaymentType().getPaymentName())) { + if (paymentDetail.getPaymentType().getName().equals(charge.getCharge().getPaymentType().getName())) { resetFreeChargeDaysCount(charge, transactionAmount, transactionDate, user, refNo); } } else if (charge.isEnablePaymentType()) { // normal charge-transaction to specific paymentType - if (paymentDetail.getPaymentType().getPaymentName().equals(charge.getCharge().getPaymentType().getPaymentName())) { + if (paymentDetail.getPaymentType().getName().equals(charge.getCharge().getPaymentType().getName())) { charge.updateWithdralFeeAmount(transactionAmount); this.payCharge(charge, charge.getAmountOutstanding(this.getCurrency()), transactionDate, user, backdatedTxnsAllowedTill, refNo); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java index a6fd7cb16..7a8d19d9d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountReadPlatformServiceImpl.java @@ -616,7 +616,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead final Long paymentTypeId = JdbcSupport.getLong(rs, "paymentType"); if (paymentTypeId != null) { final String typeName = rs.getString("paymentTypeName"); - final PaymentTypeData paymentTypeData = new PaymentTypeData(paymentTypeId, typeName, null, false, null); + final PaymentTypeData paymentTypeData = new PaymentTypeData(paymentTypeId, typeName, null, false, null, null, + false); paymentDetailData = new PaymentDetailData(id, paymentTypeData, null, null, null, null, null); } } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 6ec9cf739..ba4e3bfc4 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -71,4 +71,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0052_loan_transaction_chargeback.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0052_loan_transaction_chargeback.xml new file mode 100644 index 000000000..638f126fe --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0052_loan_transaction_chargeback.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java new file mode 100644 index 000000000..cd07d376d --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java @@ -0,0 +1,138 @@ +/** + * 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.integrationtests; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.Gson; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.GetPaymentTypesResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.CommonConstants; +import org.apache.fineract.integrationtests.common.PaymentTypeHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@Slf4j +public class LoanTransactionChargebackTest { + + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + } + + @Test + public void applyLoanTransactionChargeback() { + final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + + // Client and Loan account creation + final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012"); + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper, null); + assertNotNull(getLoanProductsProductResponse); + log.info("Loan Product Bucket Name: {}", getLoanProductsProductResponse.getDelinquencyBucket().getName()); + + final LocalDate todaysDate = Utils.getLocalDateOfTenant(); + // Older date to have more than one overdue installment + final LocalDate transactionDate = todaysDate.minusDays(45); + String operationDate = Utils.dateFormatter.format(transactionDate); + + final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(), + getLoanProductsProductResponse.getId().toString(), operationDate, "1000"); + + GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + assertNotNull(getLoansLoanIdResponse); + + loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse); + + operationDate = Utils.dateFormatter.format(todaysDate); + Float amount = Float.valueOf("1100.0"); + PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, + loanId); + assertNotNull(loanIdTransactionsResponse); + final Integer transactionId = loanIdTransactionsResponse.getResourceId(); + + List paymentTypeList = PaymentTypeHelper.getSystemPaymentType(requestSpec, responseSpec); + assertTrue(!paymentTypeList.isEmpty()); + + amount = Float.valueOf("800.0"); + final String payload = createChargebackPayload(amount.toString(), paymentTypeList.get(0).getId()); + PostLoansLoanIdTransactionsTransactionIdRequest postLoansTransactionCommandRequest = loanTransactionHelper + .applyLoanTransactionCommand(loanId, transactionId, "chargeback", payload); + assertNotNull(postLoansTransactionCommandRequest); + + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + assertNotNull(getLoansLoanIdResponse); + } + + private String createChargebackPayload(final String transactionAmount, final Integer paymentTypeId) { + final HashMap map = new HashMap<>(); + map.put("transactionAmount", transactionAmount); + map.put("paymentTypeId", paymentTypeId); + map.put("locale", CommonConstants.LOCALE); + final String chargebackPayload = new Gson().toJson(map); + log.info("{}", chargebackPayload); + return chargebackPayload; + } + + private GetLoanProductsProductIdResponse createLoanProduct(final LoanTransactionHelper loanTransactionHelper, + final Integer delinquencyBucketId) { + final HashMap loanProductMap = new LoanProductTestBuilder().build(null, delinquencyBucketId); + final Integer loanProductId = loanTransactionHelper.getLoanProductId(Utils.convertToJson(loanProductMap)); + return loanTransactionHelper.getLoanProduct(loanProductId); + } + + private Integer createLoanAccount(final LoanTransactionHelper loanTransactionHelper, final String clientId, final String loanProductId, + final String operationDate, final String principalAmount) { + final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principalAmount).withLoanTermFrequency("12") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("12").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths() // + .withInterestRatePerPeriod("2") // + .withExpectedDisbursementDate(operationDate) // + .withInterestTypeAsDecliningBalance() // + .withSubmittedOnDate(operationDate) // + .build(clientId, loanProductId, null); + final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON); + loanTransactionHelper.approveLoan(operationDate, principalAmount, loanId, null); + loanTransactionHelper.disburseLoanWithNetDisbursalAmount(operationDate, loanId, principalAmount); + return loanId; + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/PaymentTypeHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/PaymentTypeHelper.java index 9a54fa5e2..0f2c396dc 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/PaymentTypeHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/PaymentTypeHelper.java @@ -24,7 +24,10 @@ import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; +import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.HashMap; +import org.apache.fineract.client.models.GetPaymentTypesResponse; @SuppressWarnings({ "rawtypes", "unchecked" }) public final class PaymentTypeHelper { @@ -33,8 +36,16 @@ public final class PaymentTypeHelper { } - private static final String CREATE_PAYMENTTYPE_URL = "/fineract-provider/api/v1/paymenttypes?" + Utils.TENANT_IDENTIFIER; private static final String PAYMENTTYPE_URL = "/fineract-provider/api/v1/paymenttypes"; + private static final String CREATE_PAYMENTTYPE_URL = PAYMENTTYPE_URL + "?" + Utils.TENANT_IDENTIFIER; + + public static ArrayList getSystemPaymentType(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec) { + String response = Utils.performServerGet(requestSpec, responseSpec, + PAYMENTTYPE_URL + "?onlyWithCode=true&" + Utils.TENANT_IDENTIFIER); + Type paymentTypeList = new TypeToken>() {}.getType(); + return new Gson().fromJson(response, paymentTypeList); + } public static Integer createPaymentType(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final String name, final String description, final Boolean isCashPayment, final Integer position) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 96c51a0f5..aeb79b8d9 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -47,6 +47,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.PostLoansLoanIdResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; import org.apache.fineract.client.models.PutLoansLoanIdResponse; import org.apache.fineract.client.util.JSON; import org.apache.fineract.integrationtests.common.CommonConstants; @@ -239,6 +240,14 @@ public class LoanTransactionHelper { return Utils.performServerGet(requestSpec, responseSpec, GET_REPAYMENTS_URL, "loanRepaymentScheduleInstallments"); } + public PostLoansLoanIdTransactionsTransactionIdRequest applyLoanTransactionCommand(final Integer loanId, final Integer transactionId, + final String command, final String payload) { + final String LOAN_TRANSACTION_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/" + transactionId + "?command=" + + command + "&" + Utils.TENANT_IDENTIFIER; + final String response = Utils.performServerPost(requestSpec, responseSpec, LOAN_TRANSACTION_URL, payload, null); + return GSON.fromJson(response, PostLoansLoanIdTransactionsTransactionIdRequest.class); + } + public HashMap approveLoan(final String approvalDate, final Integer loanID) { String loanApprovalCommand = createLoanOperationURL(APPROVE_LOAN_COMMAND, loanID); String loanApprovalRequest = getApproveLoanAsJSON(approvalDate);