FINERACT-1694: Fix missing payment details data for events

This commit is contained in:
Adam Saghy
2023-02-07 18:07:34 +01:00
parent 3f2feceea1
commit 51203b173f
14 changed files with 102 additions and 63 deletions
@@ -261,6 +261,20 @@ final class LoanChargesApiResourceSwagger {
public Double amount;
@Schema(example = "786444UUUYYH7")
public String externalId;
@Schema(example = "An optional note")
public String note;
@Schema(example = "3")
public Long paymentTypeId;
@Schema(example = "acc123")
public String accountNumber;
@Schema(example = "che123")
public String checkNumber;
@Schema(example = "rou123")
public String routingCode;
@Schema(example = "rec123")
public String receiptNumber;
@Schema(example = "ban123")
public String bankNumber;
}
@Schema(description = "PostLoansLoanIdChargesChargeIdResponse")
@@ -83,19 +83,6 @@ public class LoanTransactionEnumData {
this.chargeAdjustment = Long.valueOf(26).equals(this.id);
}
/**
* Returns true if the transaction involves either a payment (disbursement) or a receipt (repayments, repayments
* during disbursement etc)
*
* @return
*/
public boolean isPaymentOrReceipt() {
if (isDisbursement() || isRepaymentType() || isRepaymentAtDisbursement() || isRecoveryRepayment()) {
return true;
}
return false;
}
public boolean isRepaymentType() {
if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()
|| isChargeAdjustment()) {
@@ -6272,8 +6272,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
public void handleChargebackTransaction(final LoanTransaction chargebackTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
chargebackTransaction.updateLoan(this);
if (!chargebackTransaction.isChargeback()) {
final String errorMessage = "A transaction of type chargeback was expected but not received.";
throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.chargeback.transaction", errorMessage);
@@ -97,5 +97,5 @@ public interface LoanAccountDomainService {
void recalculateAccruals(Loan loan, boolean isInterestCalcualtionHappened);
LoanTransaction creditBalanceRefund(Loan loan, LocalDate transactionDate, BigDecimal transactionAmount, String noteText,
ExternalId externalId);
ExternalId externalId, PaymentDetail paymentDetail);
}
@@ -632,19 +632,19 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
@Override
public LoanTransaction creditBalanceRefund(final Loan loan, final LocalDate transactionDate, final BigDecimal transactionAmount,
final String noteText, final ExternalId externalId) {
final String noteText, final ExternalId externalId, PaymentDetail paymentDetail) {
businessEventNotifierService.notifyPreBusinessEvent(new LoanCreditBalanceRefundPreBusinessEvent(loan));
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
final Money refundAmount = Money.of(loan.getCurrency(), transactionAmount);
final LoanTransaction newCreditBalanceRefundTransaction = LoanTransaction.creditBalanceRefund(loan, loan.getOffice(), refundAmount,
transactionDate, externalId);
LoanTransaction newCreditBalanceRefundTransaction = LoanTransaction.creditBalanceRefund(loan, loan.getOffice(), refundAmount,
transactionDate, externalId, paymentDetail);
loan.creditBalanceRefund(newCreditBalanceRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds,
existingReversedTransactionIds);
this.loanTransactionRepository.saveAndFlush(newCreditBalanceRefundTransaction);
newCreditBalanceRefundTransaction = this.loanTransactionRepository.saveAndFlush(newCreditBalanceRefundTransaction);
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanTransactionNote(loan, newCreditBalanceRefundTransaction, noteText);
@@ -159,9 +159,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
return new LoanTransaction(null, office, LoanTransactionType.REPAYMENT, paymentDetail, amount.getAmount(), paymentDate, externalId);
}
public static LoanTransaction chargeback(final Office office, final Money amount, final PaymentDetail paymentDetail,
public static LoanTransaction chargeback(final Loan loan, final Money amount, final PaymentDetail paymentDetail,
final LocalDate paymentDate, final ExternalId externalId) {
LoanTransaction loanTransaction = new LoanTransaction(null, office, LoanTransactionType.CHARGEBACK, paymentDetail,
LoanTransaction loanTransaction = new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CHARGEBACK, paymentDetail,
amount.getAmount(), paymentDate, externalId);
loanTransaction.principalPortion = amount.getAmount();
return loanTransaction;
@@ -175,8 +175,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
}
public static LoanTransaction chargeAdjustment(final Loan loan, final BigDecimal amount, final LocalDate transactionDate,
final ExternalId externalId) {
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CHARGE_ADJUSTMENT, amount, transactionDate, externalId);
final ExternalId externalId, PaymentDetail paymentDetail) {
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CHARGE_ADJUSTMENT, paymentDetail, amount, transactionDate,
externalId);
}
public void setLoanTransactionToRepaymentScheduleMappings(final Integer installmentId, final BigDecimal chargePerInstallment) {
@@ -290,9 +291,9 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
}
public static LoanTransaction creditBalanceRefund(final Loan loan, final Office office, final Money amount, final LocalDate paymentDate,
final ExternalId externalId) {
final ExternalId externalId, PaymentDetail paymentDetail) {
return new LoanTransaction(loan, office, LoanTransactionType.CREDIT_BALANCE_REFUND.getValue(), paymentDate, amount.getAmount(),
null, null, null, null, amount.getAmount(), false, null, externalId);
null, null, null, null, amount.getAmount(), false, paymentDetail, externalId);
}
public static LoanTransaction refundForActiveLoan(final Office office, final Money amount, final PaymentDetail paymentDetail,
@@ -184,13 +184,14 @@ public final class LoanChargeApiJsonValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateLoanAdjustmentRequest(final Long loanId, final Long loanChargeId, final String json) {
public void validateLoanChargeAdjustmentRequest(final Long loanId, final Long loanChargeId, final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
Set<String> transactionParameters = new HashSet<>(List.of("amount", "externalId", "locale"));
Set<String> transactionParameters = new HashSet<>(List.of("amount", "externalId", "locale", "paymentTypeId", "accountNumber",
"checkNumber", "routingCode", "receiptNumber", "bankNumber", "note"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
@@ -208,6 +209,9 @@ public final class LoanChargeApiJsonValidator {
baseDataValidator.reset().parameter("loanId").value(loanId).notNull().positiveAmount();
baseDataValidator.reset().parameter("loanChargeId").value(loanChargeId).notNull().positiveAmount();
final String note = this.fromApiJsonHelper.extractStringNamed("note", element);
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@@ -123,6 +123,10 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJson
import org.apache.fineract.portfolio.loanproduct.data.LoanOverdueDTO;
import org.apache.fineract.portfolio.loanproduct.exception.InvalidCurrencyException;
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -154,6 +158,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
private final AccountTransferDetailRepository accountTransferDetailRepository;
private final LoanChargeAssembler loanChargeAssembler;
private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService;
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
private final NoteRepository noteRepository;
private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
return e.getFromDate().isBefore(loanCharge.getDueDate()) && !loanCharge.getDueDate().isAfter(e.getDueDate());
@@ -640,7 +646,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
@Transactional
@Override
public CommandProcessingResult adjustmentForLoanCharge(Long loanId, Long loanChargeId, JsonCommand command) {
this.loanChargeApiJsonValidator.validateLoanAdjustmentRequest(loanId, loanChargeId, command.json());
this.loanChargeApiJsonValidator.validateLoanChargeAdjustmentRequest(loanId, loanChargeId, command.json());
final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId);
final LocalDate transactionDate = DateUtils.getBusinessLocalDate();
@@ -648,12 +654,22 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
final ExternalId externalId = externalIdFactory.createFromCommand(command, "externalId");
final String locale = command.locale();
Map<String, Object> changes = new HashMap<>();
changes.put("externalId", externalId);
changes.put("amount", transactionAmount);
changes.put("transactionDate", transactionDate);
changes.put("locale", locale);
loanChargeAdjustmentEntranceValidation(loanCharge, transactionAmount);
final Loan loan = loanAssembler.assembleFrom(loanId);
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
LoanTransaction loanTransaction = applyChargeAdjustment(loan, loanCharge, transactionAmount, transactionDate, externalId);
PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createPaymentDetail(command, changes);
if (paymentDetail != null) {
paymentDetail = this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
}
LoanTransaction loanTransaction = applyChargeAdjustment(loan, loanCharge, transactionAmount, transactionDate, externalId,
paymentDetail);
// Update loan transaction on repayment.
if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) {
@@ -671,13 +687,17 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
}
this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements);
}
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanNote(loan, noteText);
changes.put("note", noteText);
this.noteRepository.save(note);
}
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargeAdjustmentPostBusinessEvent(loanTransaction));
Map<String, Object> changes = new HashMap<>();
changes.put("externalId", externalId);
changes.put("amount", transactionAmount);
changes.put("transactionDate", transactionDate);
changes.put("locale", locale);
return commandProcessingResultBuilder.withCommandId(command.commandId()) //
.withLoanId(loanId) //
.withEntityId(loanChargeId) //
@@ -753,13 +773,13 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
}
private LoanTransaction applyChargeAdjustment(final Loan loan, final LoanCharge loanCharge, final BigDecimal transactionAmount,
final LocalDate transactionDate, final ExternalId txnExternalId) {
final LocalDate transactionDate, final ExternalId txnExternalId, PaymentDetail paymentDetail) {
businessEventNotifierService.notifyPreBusinessEvent(new LoanChargeAdjustmentPreBusinessEvent(loan));
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
LoanTransaction loanChargeAdjustmentTransaction = LoanTransaction.chargeAdjustment(loan, transactionAmount, transactionDate,
txnExternalId);
txnExternalId, paymentDetail);
LoanTransactionRelation loanTransactionRelation = LoanTransactionRelation.linkToCharge(loanChargeAdjustmentTransaction, loanCharge,
LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT);
loanChargeAdjustmentTransaction.getLoanTransactionRelations().add(loanTransactionRelation);
@@ -1386,20 +1386,19 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
PaymentDetailData paymentDetailData = null;
if (transactionType.isPaymentOrReceipt()) {
final Long paymentTypeId = JdbcSupport.getLong(rs, "paymentType");
if (paymentTypeId != null) {
final String typeName = rs.getString("paymentTypeName");
final PaymentTypeData paymentType = PaymentTypeData.instance(paymentTypeId, typeName);
final String accountNumber = rs.getString("accountNumber");
final String checkNumber = rs.getString("checkNumber");
final String routingCode = rs.getString("routingCode");
final String receiptNumber = rs.getString("receiptNumber");
final String bankNumber = rs.getString("bankNumber");
paymentDetailData = new PaymentDetailData(id, paymentType, accountNumber, checkNumber, routingCode, receiptNumber,
bankNumber);
}
final Long paymentTypeId = JdbcSupport.getLong(rs, "paymentType");
if (paymentTypeId != null) {
final String typeName = rs.getString("paymentTypeName");
final PaymentTypeData paymentType = PaymentTypeData.instance(paymentTypeId, typeName);
final String accountNumber = rs.getString("accountNumber");
final String checkNumber = rs.getString("checkNumber");
final String routingCode = rs.getString("routingCode");
final String receiptNumber = rs.getString("receiptNumber");
final String bankNumber = rs.getString("bankNumber");
paymentDetailData = new PaymentDetailData(id, paymentType, accountNumber, checkNumber, routingCode, receiptNumber,
bankNumber);
}
final LocalDate date = JdbcSupport.getLocalDate(rs, "date");
final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate");
final BigDecimal totalAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "total");
@@ -1256,20 +1256,21 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
changes.put("paymentTypeId", command.longValueOfParameterNamed(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.getOffice(), transactionAmountAsMoney, paymentDetail,
transactionDate, txnExternalId);
PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createPaymentDetail(command, changes);
if (paymentDetail != null) {
paymentDetail = this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
}
LoanTransaction newTransaction = LoanTransaction.chargeback(loan, transactionAmountAsMoney, paymentDetail, transactionDate,
txnExternalId);
validateLoanTransactionAmountChargeBack(loanTransaction, newTransaction);
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
// Store the Loan Transaction Relation
LoanTransactionRelation loanTransactionRelation = LoanTransactionRelation.linkToTransaction(loanTransaction, newTransaction,
LoanTransactionRelationTypeEnum.CHARGEBACK);
this.loanTransactionRelationRepository.save(loanTransactionRelation);
this.loanTransactionRepository.save(newTransaction);
newTransaction = this.loanTransactionRepository.saveAndFlush(newTransaction);
loan.handleChargebackTransaction(newTransaction, defaultLoanLifecycleStateMachine);
@@ -2439,9 +2440,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (!externalId.isEmpty()) {
changes.put(LoanApiConstants.externalIdParameterName, externalId);
}
changes.put("paymentTypeId", command.longValueOfParameterNamed(LoanApiConstants.PAYMENT_TYPE_PARAMNAME));
PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createPaymentDetail(command, changes);
if (paymentDetail != null) {
paymentDetail = this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
}
final LoanTransaction loanTransaction = this.loanAccountDomainService.creditBalanceRefund(loan, transactionDate, transactionAmount,
noteText, externalId);
noteText, externalId, paymentDetail);
return new CommandProcessingResultBuilder() //
.withEntityId(loanTransaction.getId()) //
@@ -82,6 +82,7 @@ public final class PaymentDetail extends AbstractPersistableCustom {
if (StringUtils.isNotBlank(bankNumber)) {
changes.put(PaymentDetailConstants.bankNumberParamName, bankNumber);
}
changes.put("paymentTypeId", paymentType.getId());
final PaymentDetail paymentDetail = new PaymentDetail(paymentType, accountNumber, checkNumber, routingCode, receiptNumber,
bankNumber);
return paymentDetail;
@@ -163,7 +163,7 @@ public class LoanDelinquencyDomainServiceTest {
final Money zeroMoney = Money.zero(currency);
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, 1, fromDate, dueDate, principal,
zeroAmount, zeroAmount, zeroAmount, false, new HashSet<>(), zeroAmount);
LoanTransaction loanTransaction = LoanTransaction.chargeback(null, Money.of(currency, principal), paymentDetail, transactionDate,
LoanTransaction loanTransaction = LoanTransaction.chargeback(loan, Money.of(currency, principal), paymentDetail, transactionDate,
null);
installment.getLoanTransactionToRepaymentScheduleMappings().add(LoanTransactionToRepaymentScheduleMapping
.createFrom(loanTransaction, installment, zeroMoney, zeroMoney, zeroMoney, zeroMoney));
@@ -6169,7 +6169,8 @@ public class ClientLoanIntegrationTest {
String externalId = UUID.randomUUID().toString();
PostLoansLoanIdChargesChargeIdResponse chargeAdjustmentResponse = this.loanTransactionHelper.chargeAdjustment((long) loanID,
(long) penalty1LoanChargeId, new PostLoansLoanIdChargesChargeIdRequest().amount(10.0).externalId(externalId));
(long) penalty1LoanChargeId,
new PostLoansLoanIdChargesChargeIdRequest().amount(10.0).externalId(externalId).paymentTypeId(1L));
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID);
assertEquals(2, loanSchedule.size());
@@ -6196,6 +6197,7 @@ public class ClientLoanIntegrationTest {
assertEquals(chargeAdjustmentResponse.getSubResourceId(), transactionRelation.getFromLoanTransaction());
assertEquals((long) penalty1LoanChargeId, transactionRelation.getToLoanCharge());
assertEquals("CHARGE_ADJUSTMENT", transactionRelation.getRelationType());
assertEquals(1L, chargeAdjustmentTransaction.getPaymentDetailData().getPaymentType().getId());
PostLoansLoanIdTransactionsResponse repaymentResult = loanTransactionHelper.makeLoanRepayment((long) loanID,
new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("06 September 2022").locale("en")
@@ -7177,7 +7179,7 @@ public class ClientLoanIntegrationTest {
assertTrue(loanDetails.getStatus().getOverpaid());
this.loanTransactionHelper.makeCreditBalanceRefund((long) loanID, new PostLoansLoanIdTransactionsRequest()
.transactionAmount(200.0).transactionDate("10 October 2022").dateFormat("dd MMMM yyyy").locale("en"));
.transactionAmount(200.0).transactionDate("10 October 2022").dateFormat("dd MMMM yyyy").locale("en").paymentTypeId(1L));
loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanID);
assertTrue(loanDetails.getStatus().getClosedObligationsMet());
@@ -7198,7 +7200,7 @@ public class ClientLoanIntegrationTest {
assertEquals(200.0, loanDetails.getTransactions().get(3).getOverpaymentPortion());
assertEquals(LocalDate.of(2022, 10, 10), loanDetails.getTransactions().get(3).getDate());
assertEquals(0.0, loanDetails.getTransactions().get(3).getOutstandingLoanBalance());
assertEquals(1L, loanDetails.getTransactions().get(3).getPaymentDetailData().getPaymentType().getId());
GetJournalEntriesTransactionIdResponse journalEntriesForTransaction = journalEntryHelper
.getJournalEntries("L" + loanDetails.getTransactions().get(3).getId());
List<JournalEntryTransactionItem> journalItems = journalEntriesForTransaction.getPageItems();
@@ -32,6 +32,7 @@ import java.util.HashMap;
import java.util.UUID;
import org.apache.fineract.client.models.GetDelinquencyBucketsResponse;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
@@ -91,9 +92,14 @@ public class LoanAccountCreditRefundPayoutWithChargebackTest {
.transactionAmount(1000.0));
// Chargeback full repayment amount to add to principal balance
loanTransactionHelper.chargebackLoanTransaction(loanExternalIdStr, repaymentTransaction_1.getResourceId(),
PostLoansLoanIdTransactionsResponse chargebackResponse = loanTransactionHelper.chargebackLoanTransaction(loanExternalIdStr,
repaymentTransaction_1.getResourceId(),
new PostLoansLoanIdTransactionsTransactionIdRequest().locale("en").transactionAmount(1000.0).paymentTypeId(1L));
GetLoansLoanIdTransactionsTransactionIdResponse chargebackTransactionResponse = loanTransactionHelper
.getLoanTransactionDetails(chargebackResponse.getLoanId(), chargebackResponse.getResourceId());
assertEquals(1L, chargebackTransactionResponse.getPaymentDetailData().getPaymentType().getId());
// Verify Goodwill Credit can be created with full amount
final PostLoansLoanIdTransactionsResponse goodwillCredit_1 = loanTransactionHelper.makeGoodwillCredit((long) loanId,
new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("8 September 2022").locale("en")