Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SU-459 #1387

Merged
merged 2 commits into from
Dec 13, 2024
Merged

SU-459 #1387

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4494,13 +4494,18 @@ public LoanRepaymentScheduleInstallmentData validateSpecialWriteOffConcepts(fina
}

public LoanTransaction writeOff(final LoanRepaymentScheduleInstallmentData loanRepaymentScheduleInstallmentData,
final LocalDate writtenOffOnLocalDate, final ExternalId externalId) {
final LocalDate writtenOffOnLocalDate, final ExternalId externalId, boolean isCreditNote) {
final MonetaryCurrency currency = loanCurrency();
final Money principalPortion = Money.of(currency, loanRepaymentScheduleInstallmentData.getPrincipalPortion());
final Money interestPortion = Money.of(currency, loanRepaymentScheduleInstallmentData.getInterestPortion());
final Money feeChargesPortion = Money.of(currency, loanRepaymentScheduleInstallmentData.getFeeChargesPortion());
final Money penaltychargesPortion = Money.of(currency, loanRepaymentScheduleInstallmentData.getPenaltyChargesPortion());
final LoanTransaction loanTransaction = LoanTransaction.writeoff(this, getOffice(), writtenOffOnLocalDate, externalId);
LoanTransaction loanTransaction;
if (isCreditNote) {
loanTransaction = LoanTransaction.creditNote(this, getOffice(), writtenOffOnLocalDate, externalId);
} else {
loanTransaction = LoanTransaction.writeoff(this, getOffice(), writtenOffOnLocalDate, externalId);
}
loanTransaction.setSpecialWriteOff(true);
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
loanTransaction.updateLoan(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,16 @@ public static LoanTransaction writeoff(final Loan loan, final Office office, fin
return new LoanTransaction(loan, office, LoanTransactionType.WRITEOFF, null, writeOffDate, externalId);
}

public static LoanTransaction creditNote(final Loan loan, final Office office, final LocalDate writeOffDate,
final ExternalId externalId, final BigDecimal amount) {
return new LoanTransaction(loan, office, LoanTransactionType.CREDIT_NOTE, amount, writeOffDate, externalId);
}

public static LoanTransaction creditNote(final Loan loan, final Office office, final LocalDate writeOffDate,
final ExternalId externalId) {
return new LoanTransaction(loan, office, LoanTransactionType.CREDIT_NOTE, null, writeOffDate, externalId);
}

public static LoanTransaction chargeOff(final Loan loan, final LocalDate chargeOffDate, final ExternalId externalId) {
BigDecimal principalPortion = loan.getLoanSummary().getTotalPrincipalOutstanding().compareTo(BigDecimal.ZERO) != 0
? loan.getLoanSummary().getTotalPrincipalOutstanding()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ public enum LoanTransactionType {
CHARGEBACK(25, "loanTransactionType.chargeback"), //
CHARGE_ADJUSTMENT(26, "loanTransactionType.chargeAdjustment"), //
CHARGE_OFF(27, "loanTransactionType.chargeOff"), //
DOWN_PAYMENT(28, "loanTransactionType.downPayment");
DOWN_PAYMENT(28, "loanTransactionType.downPayment"), //
CREDIT_NOTE(29, "loanTransactionType.creditNote");

private final Integer value;
private final String code;
Expand Down Expand Up @@ -104,6 +105,7 @@ public static LoanTransactionType fromInt(final Integer transactionType) {
case 26 -> LoanTransactionType.CHARGE_ADJUSTMENT;
case 27 -> LoanTransactionType.CHARGE_OFF;
case 28 -> LoanTransactionType.DOWN_PAYMENT;
case 29 -> LoanTransactionType.CREDIT_NOTE;
default -> LoanTransactionType.INVALID;
};
}
Expand Down Expand Up @@ -195,4 +197,8 @@ public boolean isChargeOff() {
public boolean isDownPayment() {
return this.equals(LoanTransactionType.DOWN_PAYMENT);
}

public boolean isCreditNote() {
return this.equals(LoanTransactionType.CREDIT_NOTE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbur
public Money processLatestTransaction(final LoanTransaction loanTransaction, final TransactionCtx ctx) {
Money transactionAmountUnprocessed = Money.zero(ctx.getCurrency());
switch (loanTransaction.getTypeOf()) {
case WRITEOFF ->
case WRITEOFF, CREDIT_NOTE ->
handleWriteOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(), ctx.getOverpaymentHolder());
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges());
case CHARGEBACK -> handleChargeback(loanTransaction, ctx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ public Money processLatestTransaction(LoanTransaction loanTransaction, Transacti
Money unprocessed = Money.zero(ctx.getCurrency());
switch (loanTransaction.getTypeOf()) {
case DISBURSEMENT -> handleDisbursement(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
case WRITEOFF ->
case WRITEOFF, CREDIT_NOTE ->
handleWriteOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(), ctx.getOverpaymentHolder());
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges());
case CHARGEBACK -> handleChargeback(loanTransaction, ctx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ public static LoanTransactionEnumData transactionType(final LoanTransactionType
LoanTransactionType.CHARGE_OFF.getCode(), "Charge-off");
case DOWN_PAYMENT -> new LoanTransactionEnumData(LoanTransactionType.DOWN_PAYMENT.getValue().longValue(),
LoanTransactionType.DOWN_PAYMENT.getCode(), "Down Payment");
case CREDIT_NOTE -> new LoanTransactionEnumData(LoanTransactionType.CREDIT_NOTE.getValue().longValue(),
LoanTransactionType.CREDIT_NOTE.getCode(), "Credit Note");
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ public class SpecialWriteOffPayload {
private BigDecimal totalWriteOffAmount;
private String dateFormat;
private String locale;
private boolean isCreditNote;
}
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ public void validateSpecialWriteOff(final String json) {
throw new InvalidJsonException();
}
final Set<String> disbursementParameters = new HashSet<>(Arrays.asList("principalPortion", "interestPortion", "locale",
"dateFormat", "charges", "writeoffReasonId", "loanId", "totalWriteOffAmount", "isImportedTransaction"));
"dateFormat", "charges", "writeoffReasonId", "loanId", "totalWriteOffAmount", "isImportedTransaction", "isCreditNote"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
Expand All @@ -24,17 +25,14 @@
import org.apache.fineract.infrastructure.documentmanagement.domain.DocumentRepository;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCreditNoteBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
import org.apache.fineract.portfolio.loanaccount.data.SpecialWriteOffPayload;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditNote;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditNoteRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.*;
import org.apache.fineract.portfolio.loanaccount.exception.LoanCreditNoteAmountCannotBeZeroException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanCreditNoteDateCannotBeFutureException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
Expand Down Expand Up @@ -159,7 +157,8 @@ private String generateSpecialWriteOffPayload(Loan loan, LoanCreditNote creditNo

SpecialWriteOffPayload specialWriteOffPayload = SpecialWriteOffPayload.builder().loanId(loan.getId())
.principalPortion(creditNote.getCapital()).interestPortion(creditNote.getCurrentInterest())
.totalWriteOffAmount(creditNote.getTotalAmount()).dateFormat(CustomDateUtils.SPANISH_DATE_FORMAT).locale("es").build();
.totalWriteOffAmount(creditNote.getTotalAmount()).dateFormat(CustomDateUtils.SPANISH_DATE_FORMAT).locale("es")
.isCreditNote(true).build();

if (!charges.isEmpty()) {
specialWriteOffPayload.setCharges(charges);
Expand Down Expand Up @@ -200,8 +199,12 @@ private List<Map<String, Object>> generateChargesForSpecialWriteOff(Loan loan, L
LoanCharge insuranceCharge = loanCharges.stream().filter(loanCharge -> loanCharge.getCharge().isVoluntaryInsurance())
.findFirst().orElse(null);
if (insuranceCharge != null) {
BigDecimal vatAmountToBePaid = BigDecimal.ZERO;
vatAmountToBePaid = calculateVatAmount(loanCharges, insuranceCharge, insuranceCharge.amountOutstanding(),
loan.getCurrency(), charges);
BigDecimal chargeAmount = creditNote.getInsurance().subtract(vatAmountToBePaid);
charges.add(Map.of("chargeId", Objects.requireNonNull(insuranceCharge.getCharge().getId()), "writeOffAmount",
creditNote.getInsurance()));
chargeAmount));
} else {
creditNote.setInsurance(BigDecimal.ZERO);
}
Expand All @@ -216,8 +219,12 @@ private List<Map<String, Object>> generateChargesForSpecialWriteOff(Loan loan, L
log.info(" is charge present {}", insuranceCharge);
// check if insurance is present
if (insuranceCharge != null) {
BigDecimal vatAmountToBePaid = BigDecimal.ZERO;
vatAmountToBePaid = calculateVatAmount(loanCharges, insuranceCharge, insuranceCharge.amountOutstanding(),
loan.getCurrency(), charges);
BigDecimal chargeAmount = creditNote.getMandatoryInsurance().subtract(vatAmountToBePaid);
charges.add(Map.of("chargeId", Objects.requireNonNull(insuranceCharge.getCharge().getId()), "writeOffAmount",
creditNote.getMandatoryInsurance()));
chargeAmount));
} else {
creditNote.setMandatoryInsurance(BigDecimal.ZERO);
}
Expand All @@ -240,8 +247,11 @@ private List<Map<String, Object>> generateChargesForSpecialWriteOff(Loan loan, L
LoanCharge avalCharge = loanCharges.stream().filter(loanCharge -> loanCharge.getCharge().isAvalCharge()).findFirst()
.orElse(null);
if (avalCharge != null) {
charges.add(Map.of("chargeId", Objects.requireNonNull(avalCharge.getCharge().getId()), "writeOffAmount",
creditNote.getAval()));
BigDecimal vatAmountToBePaid = BigDecimal.ZERO;
vatAmountToBePaid = calculateVatAmount(loanCharges, avalCharge, avalCharge.amountOutstanding(), loan.getCurrency(),
charges);
BigDecimal chargeAmount = creditNote.getAval().subtract(vatAmountToBePaid);
charges.add(Map.of("chargeId", Objects.requireNonNull(avalCharge.getCharge().getId()), "writeOffAmount", chargeAmount));
} else {
creditNote.setAval(BigDecimal.ZERO);
}
Expand All @@ -253,4 +263,50 @@ private List<Map<String, Object>> generateChargesForSpecialWriteOff(Loan loan, L
creditNote.calculateTotalAmount();
return charges;
}

private BigDecimal percentageOf(BigDecimal amount, BigDecimal percentage, final RoundingMode roundingMode, MonetaryCurrency currency) {
final BigDecimal newAmount = amount.multiply(percentage).divide(BigDecimal.valueOf(100), roundingMode);
return Money.of(currency, newAmount).getAmount();
}

private BigDecimal calculateVatAmount(Collection<LoanCharge> loanCharges, LoanCharge parentCharge, BigDecimal amount,
MonetaryCurrency currency, List<Map<String, Object>> charges) {
BigDecimal vatAmount = BigDecimal.ZERO;
BigDecimal parentChargeAmount = BigDecimal.ZERO;
for (LoanInstallmentCharge installmentCharge : parentCharge.installmentCharges()) {
if (installmentCharge.isPaid()) {
continue;
} else {
parentChargeAmount = installmentCharge.getAmountOutstanding();
break;
}
}
for (LoanCharge vatCharge : loanCharges) {
if (Objects.equals(parentCharge.getCharge().getId(), vatCharge.getCharge().getParentChargeId())) {
BigDecimal outstandingVatAmount = BigDecimal.ZERO;
for (LoanInstallmentCharge installmentCharge : vatCharge.installmentCharges()) {
if (installmentCharge.isPaid()) {
continue;
} else {
outstandingVatAmount = installmentCharge.getAmountOutstanding();
break;
}
}
if (outstandingVatAmount.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal calculatedAmount = percentageOf(parentChargeAmount, vatCharge.amountOrPercentage(), RoundingMode.HALF_UP,
currency);
if (calculatedAmount.compareTo(outstandingVatAmount) < 0) {
outstandingVatAmount = calculatedAmount;
}
}
vatAmount = outstandingVatAmount;
if (vatAmount.compareTo(BigDecimal.ZERO) > 0) {
charges.add(Map.of("chargeId", Objects.requireNonNull(vatCharge.getCharge().getId()), "writeOffAmount", vatAmount));
}
break;
}
}

return vatAmount;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2039,9 +2039,16 @@ public CommandProcessingResult specialWriteOff(final Long loanId, final JsonComm
final boolean isAccountTransfer = false;
final HolidayDetailDTO holidayDetailDto = null;
final boolean isHolidayValidationDone = false;
writeOffTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.WRITEOFF, loan, transactionDate,
totalWriteOffAmount, paymentDetail, noteText, externalId, isRecoveryRepayment, chargeRefundChargeType,
isAccountTransfer, holidayDetailDto, isHolidayValidationDone);
final boolean isCreditNote = command.booleanPrimitiveValueOfParameterNamed("isCreditNote");
LoanTransactionType transactionType = null;
if (isCreditNote) {
transactionType = LoanTransactionType.CREDIT_NOTE;
} else {
transactionType = LoanTransactionType.WRITEOFF;
}
writeOffTransaction = this.loanAccountDomainService.makeRepayment(transactionType, loan, transactionDate, totalWriteOffAmount,
paymentDetail, noteText, externalId, isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto,
isHolidayValidationDone);
} else {
final MonetaryCurrency currency = loan.getCurrency();
final LoanRepaymentScheduleInstallment specialWriteOffInstallment = loan.fetchLoanSpecialWriteOffDetail(transactionDate);
Expand Down Expand Up @@ -2195,7 +2202,8 @@ public CommandProcessingResult specialWriteOff(final Long loanId, final JsonComm
}
saveAndFlushLoanWithIntegrityChecks(loan);
}
writeOffTransaction = loan.writeOff(loanRepaymentScheduleInstallmentData, transactionDate, externalId);
final boolean isCreditNote = command.booleanPrimitiveValueOfParameterNamed("isCreditNote");
writeOffTransaction = loan.writeOff(loanRepaymentScheduleInstallmentData, transactionDate, externalId, isCreditNote);
currentScheduleInstallment.updateInterestCharged(interestToBeChargedAndWrittenOff.getAmount());
loan.updateLoanSummaryDerivedFields();
loan.getRepaymentScheduleInstallments().forEach(rp -> rp.checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency));
Expand Down
Loading