Chargeback Loan Transaction

This commit is contained in:
Jose Alberto Hernandez
2022-09-30 21:22:23 +02:00
committed by Aleksandar Vidakovic
parent 5bd804ada8
commit 40b6eb50f6
44 changed files with 1080 additions and 181 deletions
@@ -42,6 +42,22 @@
"null",
"long"
]
},
{
"default": null,
"name": "codeName",
"type": [
"null",
"string"
]
},
{
"default": null,
"name": "isSystemDefined",
"type": [
"null",
"boolean"
]
}
]
}
@@ -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";
@@ -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;
}
}
@@ -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<PaymentType> paymentTypes = paymentTypeRepository.findAll();
for (PaymentType paymentType : paymentTypes) {
if (!paymentType.isCashPayment()) {
if (!paymentType.getIsCashPayment()) {
return paymentType;
}
// TODO: for now first not cash is retured:
@@ -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<LoanChargebackTransactionBusinessEvent> {
@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<LoanTransactionMakeRepaymentPostBusinessEvent> {
@Override
@@ -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);
@@ -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";
}
@@ -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);
@@ -91,6 +91,8 @@ public class LoanTransactionData {
private String reversalExternalId;
private LocalDate reversedOnDate;
private List<LoanTransactionRelationData> 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<LoanChargePaidByData> loanChargePaidByList) {
this.loanChargePaidByList = loanChargePaidByList;
}
public void setLoanTransactionRelations(List<LoanTransactionRelationData> transactionRelations) {
this.transactionRelations = transactionRelations;
}
}
@@ -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;
}
@@ -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<String, BigDecimal> 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<String, Object> feeDetails) {
@@ -5665,7 +5671,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return interestRecalculatedOn;
}
private void updateLoanOutstandingBalaces() {
private void updateLoanOutstandingBalances() {
Money outstanding = Money.zero(getCurrency());
List<LoanTransaction> loanTransactions = retreiveListOfTransactionsExcludeAccruals();
for (LoanTransaction loanTransaction : loanTransactions) {
@@ -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;
}
@@ -130,6 +130,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, mappedBy = "loanTransaction")
private Set<LoanTransactionToRepaymentScheduleMapping> loanTransactionToRepaymentScheduleMappings = new HashSet<>();
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "fromTransaction")
private Set<LoanTransactionRelation> 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<LoanTransactionRelation> 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.
}
@@ -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);
}
}
@@ -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<LoanTransactionRelation, Long>, JpaSpecificationExecutor<LoanTransactionRelation> {
List<LoanTransactionRelation> findByFromTransactionAndRelationType(LoanTransaction loanTransaction,
LoanTransactionRelationTypeEnum relationType);
}
@@ -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;
};
}
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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<LoanTransactionRelationData> map(List<LoanTransactionRelation> sources);
}
@@ -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<String> transactionParameters = new HashSet<>(Arrays.asList(LoanApiConstants.TRANSACTION_AMOUNT_PARAMNAME,
LoanApiConstants.localeParameterName, LoanApiConstants.externalIdParameterName, LoanApiConstants.noteParameterName,
LoanApiConstants.PAYMENT_TYPE_PARAMNAME));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters);
final List<ApiParameterError> 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<String> paymentDetailParameters = new HashSet<>(
Arrays.asList("accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber"));
for (final String paymentDetailParameterName : paymentDetailParameters) {
@@ -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<LoanRepaymentScheduleInstallmentData> getRepaymentDataResponse(Long loanId);
CollectionData retrieveLoanCollectionData(Long loanId);
List<LoanTransactionRelationData> retrieveLoanTransactionRelationsByLoanTransactionId(Long loanTransactionId);
}
@@ -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> 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<LoanTransactionRelationData> retrieveLoanTransactionRelationsByLoanTransactionId(Long loanTransactionId) {
final LoanTransaction loanTransaction = this.loanTransactionRepository.getReferenceById(loanTransactionId);
List<LoanTransactionRelation> loanTransactionRelations = this.loanTransactionRelationRepository
.findByFromTransactionAndRelationType(loanTransaction, LoanTransactionRelationTypeEnum.CHARGEBACK);
return loanTransactionRelationMapper.map(loanTransactionRelations);
}
}
@@ -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);
@@ -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<String, Object> 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<Long> existingTransactionIds = new ArrayList<>();
final List<Long> 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<String, Object> 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) {
@@ -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;
@@ -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<String> RESPONSE_DATA_PARAMETERS = new
// HashSet<>(Arrays.asList("id", "value", "description", "isCashPayment"));
@Autowired
public PaymentTypeApiResource(PlatformSecurityContext securityContext, DefaultToApiJsonSerializer<PaymentTypeData> 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<PaymentTypeData> paymentTypes = this.readPlatformService.retrieveAllPaymentTypes();
Collection<PaymentTypeData> 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);
}
@@ -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<String> RESPONSE_DATA_PARAMETERS = new HashSet<>(Arrays.asList(ID, NAME, DESCRIPTION, ISCASHPAYMENT));
}
@@ -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")
@@ -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);
}
}
@@ -43,11 +43,13 @@ public class PaymentTypeDataValidator {
private final FromJsonHelper fromApiJsonHelper;
private static final Set<String> 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<String> 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);
}
}
@@ -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<String, Object> update(final JsonCommand command) {
final Map<String, Object> 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;
}
}
@@ -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<PaymentType, Long>, JpaSpecificationExecutor<PaymentType> {
List<PaymentType> findAllByOrderByPositionAsc();
List<PaymentType> findAllByCodeNameIsNotNullOrderByPositionAsc();
}
@@ -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<PaymentType> findAll() {
return this.repository.findAllByOrderByPositionAsc();
}
public List<PaymentType> findAllWithCodeName() {
return this.repository.findAllByCodeNameIsNotNullOrderByPositionAsc();
}
public PaymentType findOneWithNotFoundDetection(final Long id) {
@@ -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<PaymentTypeData> map(List<PaymentType> sources);
}
@@ -25,6 +25,8 @@ public interface PaymentTypeReadPlatformService {
Collection<PaymentTypeData> retrieveAllPaymentTypes();
Collection<PaymentTypeData> retrieveAllPaymentTypesWithCode();
PaymentTypeData retrieveOne(Long paymentTypeId);
}
@@ -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> 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<PaymentTypeData> retrieveAllPaymentTypesWithCode() {
// TODO Auto-generated method stub
this.context.authenticatedUser();
List<PaymentType> 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<PaymentTypeData> {
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);
}
}
@@ -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();
}
@@ -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);
@@ -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);
}
}
@@ -71,4 +71,5 @@
<include file="parts/0049_add_send_asynchronous_events_job.xml" relativeToChangelogFile="true"/>
<include file="parts/0050_add_reverse_flag_disbursement_details.xml" relativeToChangelogFile="true"/>
<include file="parts/0051_external_event_table_category_info.xml" relativeToChangelogFile="true"/>
<include file="parts/0052_loan_transaction_chargeback.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet author="fineract" id="1" context="mysql">
<createTable tableName="m_loan_transaction_relation">
<column autoIncrement="true" name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="from_loan_transaction_id" type="BIGINT">
<constraints nullable="false" />
</column>
<column name="to_loan_transaction_id" type="BIGINT">
<constraints nullable="false" />
</column>
<column name="relation_type_enum" type="INT">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT">
<constraints nullable="false" />
</column>
<column name="created_on_utc" type="DATETIME">
<constraints nullable="false" />
</column>
<column name="version" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="last_modified_by" type="BIGINT">
<constraints nullable="false" />
</column>
<column name="last_modified_on_utc" type="DATETIME">
<constraints nullable="false" />
</column>
</createTable>
</changeSet>
<changeSet author="fineract" id="1" context="postgresql">
<createTable tableName="m_loan_transaction_relation">
<column autoIncrement="true" name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="from_loan_transaction_id" type="BIGINT">
<constraints nullable="false" />
</column>
<column name="to_loan_transaction_id" type="BIGINT">
<constraints nullable="false" />
</column>
<column name="relation_type_enum" type="INT">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT">
<constraints nullable="false" />
</column>
<column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE">
<constraints nullable="false" />
</column>
<column name="version" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="last_modified_by" type="BIGINT">
<constraints nullable="false" />
</column>
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE">
<constraints nullable="false" />
</column>
</createTable>
</changeSet>
<changeSet author="fineract" id="2">
<addForeignKeyConstraint baseColumnNames="from_loan_transaction_id" baseTableName="m_loan_transaction_relation"
constraintName="FK_m_loan_transaction_from_relation" deferrable="false"
initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
referencedColumnNames="id" referencedTableName="m_loan_transaction" validate="true"/>
<addForeignKeyConstraint baseColumnNames="to_loan_transaction_id" baseTableName="m_loan_transaction_relation"
constraintName="FK_m_loan_transaction_to_relation" deferrable="false"
initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
referencedColumnNames="id" referencedTableName="m_loan_transaction" validate="true"/>
</changeSet>
<changeSet author="fineract" id="3">
<insert tableName="m_permission">
<column name="grouping" value="transaction_loan"/>
<column name="code" value="CHARGEBACK_LOAN"/>
<column name="entity_name" value="LOAN"/>
<column name="action_name" value="CHARGEBACK"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet author="fineract" id="4">
<addColumn tableName="m_payment_type">
<column name="code_name" type="VARCHAR(100)"/>
</addColumn>
<addColumn tableName="m_payment_type">
<column name="is_system_defined" type="boolean" defaultValueComputed="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="5">
<insert tableName="m_payment_type">
<column name="value" value="Repayment Adjustment Chargeback"/>
<column name="description" value="Repayment Adjustment Chargeback"/>
<column name="is_cash_payment" valueBoolean="false"/>
<column name="order_position" valueNumeric="1"/>
<column name="code_name" value="REPAYMENT_ADJUSTMENT_CHARGEBACK"/>
<column name="is_system_defined" valueNumeric="true"/>
</insert>
<insert tableName="m_payment_type">
<column name="value" value="Repayment Adjustment Refund"/>
<column name="description" value="Repayment Adjustment Refund"/>
<column name="is_cash_payment" valueBoolean="false"/>
<column name="order_position" valueNumeric="1"/>
<column name="code_name" value="REPAYMENT_ADJUSTMENT_REFUND"/>
<column name="is_system_defined" valueBoolean="true"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -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<GetPaymentTypesResponse> 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<String, Object> 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<String, Object> 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;
}
}
@@ -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<GetPaymentTypesResponse> getSystemPaymentType(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec) {
String response = Utils.performServerGet(requestSpec, responseSpec,
PAYMENTTYPE_URL + "?onlyWithCode=true&" + Utils.TENANT_IDENTIFIER);
Type paymentTypeList = new TypeToken<ArrayList<GetPaymentTypesResponse>>() {}.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) {
@@ -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);