diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanTransactionDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanTransactionDataV1.avsc index 2332ef0f6bf..94f9149fe07 100644 --- a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanTransactionDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanTransactionDataV1.avsc @@ -122,6 +122,14 @@ "null", "bigdecimal" ] + }, + { + "default": null, + "name": "amortizedIncomePortion", + "type": [ + "null", + "bigdecimal" + ] } ] } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java index 25a2e916d55..1a4d36e7d81 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java @@ -89,6 +89,7 @@ public class LoanAccountData { private boolean isLoanProductLinkedToFloatingRate; private Long fundId; private String fundName; + private String officeName; private Long loanPurposeId; private String loanPurposeName; private Long loanOfficerId; diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java index b2530a50167..b87a6ef399f 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java @@ -197,6 +197,14 @@ private GetWorkingCapitalLoansLoanIdResponse() {} public BigDecimal discountProposed; @Schema(example = "0.0", description = "Approved discount set during loan approval") public BigDecimal discountApproved; + @Schema(example = "90", description = "Loan term in days (originalPaymentNumber from amortization schedule); null if schedule not yet generated") + public Integer totalDays; + @Schema(example = "116.67", description = "Daily expected payment amount from the amortization schedule; null if schedule not yet generated") + public BigDecimal periodPaymentAmount; + @Schema(example = "0.000435", description = "Periodic (daily) effective interest rate computed via RATE(); null if schedule not yet generated") + public BigDecimal dailyEir; + @Schema(example = "0.1691", description = "Annualized EIR: (1 + dailyEir)^365 − 1; null if schedule not yet generated") + public BigDecimal calculatedAnnualEir; @Schema(description = "Working capital breach)") public WorkingCapitalLoanProductApiResourceSwagger.GetWorkingCapitalLoanProductsResponse.GetWorkingCapitalLoanBreach breach; public WorkingCapitalLoanProductApiResourceSwagger.GetWorkingCapitalLoanNearBreach nearBreach; diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResourceSwagger.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResourceSwagger.java index 779233678f6..87f3352c7ef 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResourceSwagger.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResourceSwagger.java @@ -81,6 +81,8 @@ private GetWorkingCapitalLoanTransactionIdResponse() {} public BigDecimal feeChargesPortion; @Schema(example = "0.00", description = "Penalty charges portion from allocation") public BigDecimal penaltyChargesPortion; + @Schema(example = "500.00", description = "Amortized income portion (discount fee for transactions with an income relation, zero otherwise)") + public BigDecimal amortizedIncomePortion; } @Schema(description = "Loan transaction type enum data (same as basic loan)") diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationSchedulePaymentData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationSchedulePaymentData.java index 10f5a849b4d..3093a950027 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationSchedulePaymentData.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationSchedulePaymentData.java @@ -40,5 +40,6 @@ public class ProjectedAmortizationSchedulePaymentData { private final BigDecimal actualAmortizationAmount; private final BigDecimal incomeModification; private final BigDecimal deferredBalance; + private final BigDecimal feesAmount; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanData.java index 76965d13beb..d0bcfd42035 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanData.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanData.java @@ -54,6 +54,7 @@ public class WorkingCapitalLoanData implements Serializable { private ExternalId externalId; private ClientData client; private Long officeId; + private String officeName; private Long fundId; private String fundName; private WorkingCapitalLoanProductData product; @@ -71,6 +72,10 @@ public class WorkingCapitalLoanData implements Serializable { private BigDecimal discount; private BigDecimal discountProposed; private BigDecimal discountApproved; + private Integer totalDays; + private BigDecimal periodPaymentAmount; + private BigDecimal dailyEir; + private BigDecimal calculatedAnnualEir; private DelinquencyBucketData delinquencyBucket; private WorkingCapitalBreachData breach; private WorkingCapitalNearBreachData nearBreach; @@ -84,4 +89,5 @@ public class WorkingCapitalLoanData implements Serializable { private StringEnumOptionData delinquencyStartType; private WorkingCapitalLoanCollectionData collectionData; + private WorkingCapitalLoanSummaryData summary; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanSummaryData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanSummaryData.java new file mode 100644 index 00000000000..82d99962895 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanSummaryData.java @@ -0,0 +1,68 @@ +/** + * 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.workingcapitalloan.data; + +import java.io.Serializable; +import java.math.BigDecimal; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.organisation.monetary.data.CurrencyData; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class WorkingCapitalLoanSummaryData implements Serializable { + + private CurrencyData currency; + + // Principal + private BigDecimal principalDisbursed; + private BigDecimal principalPaid; + private BigDecimal principalOutstanding; + private BigDecimal principalOverdue; + + // Discount fee (cargo financiero del WC loan) + private BigDecimal discountCharged; + private BigDecimal discountPaid; + private BigDecimal discountOutstanding; + private BigDecimal discountOverdue; + + // Income recognition + private BigDecimal realizedIncome; + private BigDecimal unrealizedIncome; + private BigDecimal overpaymentAmount; + + // Aggregates + private BigDecimal totalExpectedRepayment; + private BigDecimal totalRepayment; + private BigDecimal totalOutstanding; + private BigDecimal totalOverdue; + private BigDecimal totalRecovered; + + // Transaction summaries + private BigDecimal totalDisbursement; + private BigDecimal totalRepaymentTransaction; + private BigDecimal totalRepaymentTransactionReversed; + private BigDecimal totalDiscountFee; +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanTransactionData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanTransactionData.java index 8e2d84fc325..004d6409618 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanTransactionData.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanTransactionData.java @@ -28,6 +28,7 @@ import lombok.Setter; import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData; import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; @@ -40,6 +41,7 @@ public class WorkingCapitalLoanTransactionData implements Serializable { private Long id; private Long wcLoanId; + private CurrencyData currency; private LoanTransactionEnumData type; private LocalDate transactionDate; private LocalDate submittedOnDate; @@ -55,4 +57,6 @@ public class WorkingCapitalLoanTransactionData implements Serializable { private BigDecimal principalPortion; private BigDecimal feeChargesPortion; private BigDecimal penaltyChargesPortion; + // Income recognized in this transaction (e.g. discount fee on disbursement). + private BigDecimal amortizedIncomePortion; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/ProjectedAmortizationScheduleMapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/ProjectedAmortizationScheduleMapper.java index 19cb8794ef9..36a7c7b1459 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/ProjectedAmortizationScheduleMapper.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/ProjectedAmortizationScheduleMapper.java @@ -65,6 +65,7 @@ private ProjectedAmortizationSchedulePaymentData toPaymentData(final ProjectedPa .actualAmortizationAmount(roundMoney(payment.actualAmortizationAmount())) // .incomeModification(roundMoney(payment.incomeModification())) // .deferredBalance(roundMoney(payment.deferredBalance())) // + .feesAmount(BigDecimal.ZERO) // .build(); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanMapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanMapper.java index 8a5f2e45f4f..2daeeb6fe60 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanMapper.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanMapper.java @@ -49,12 +49,13 @@ @Mapper(config = MapstructMapperConfig.class, uses = { DelinquencyBucketMapper.class, WorkingCapitalLoanProductMapper.class, WorkingCapitalLoanBalanceMapper.class, WorkingCapitalLoanDisbursementDetailMapper.class, WorkingCapitalLoanTransactionMapper.class, - WorkingCapitalBreachMapper.class, WorkingCapitalNearBreachMapper.class }) + WorkingCapitalBreachMapper.class, WorkingCapitalNearBreachMapper.class, WorkingCapitalLoanSummaryDataMapper.class }) public interface WorkingCapitalLoanMapper { @Mapping(target = "accountNo", source = "accountNumber") @Mapping(target = "client", source = "client", qualifiedByName = "clientToData") @Mapping(target = "officeId", source = "client.office.id") + @Mapping(target = "officeName", source = "client.office.name") @Mapping(target = "fundId", source = "fund.id") @Mapping(target = "fundName", source = "fund.name") @Mapping(target = "product", source = "loanProduct") @@ -77,6 +78,11 @@ public interface WorkingCapitalLoanMapper { @Mapping(target = "delinquencyGraceDays", source = "loanProductRelatedDetails.delinquencyGraceDays") @Mapping(target = "delinquencyStartType", source = "loanProductRelatedDetails", qualifiedByName = "delinquencyStartTypeData") @Mapping(target = "collectionData", ignore = true) + @Mapping(target = "totalDays", ignore = true) + @Mapping(target = "periodPaymentAmount", ignore = true) + @Mapping(target = "dailyEir", ignore = true) + @Mapping(target = "calculatedAnnualEir", ignore = true) + @Mapping(target = "summary", source = ".", qualifiedByName = "toSummaryData") WorkingCapitalLoanData toData(WorkingCapitalLoan loan); List toDataList(List loans); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanSummaryDataMapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanSummaryDataMapper.java new file mode 100644 index 00000000000..d979243d135 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanSummaryDataMapper.java @@ -0,0 +1,159 @@ +/** + * 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.workingcapitalloan.mapper; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanSummaryData; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionAllocation; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +@Mapper(config = MapstructMapperConfig.class) +public interface WorkingCapitalLoanSummaryDataMapper { + + @Named("toSummaryData") + @Mapping(target = "currency", source = ".", qualifiedByName = "toCurrency") + // Principal + @Mapping(target = "principalDisbursed", source = ".", qualifiedByName = "toPrincipalDisbursed") + @Mapping(target = "principalPaid", source = ".", qualifiedByName = "toPrincipalPaid") + @Mapping(target = "principalOutstanding", source = ".", qualifiedByName = "toPrincipalOutstanding") + @Mapping(target = "principalOverdue", expression = "java(java.math.BigDecimal.ZERO)") + // Discount fee + @Mapping(target = "discountCharged", source = ".", qualifiedByName = "toDiscountCharged") + @Mapping(target = "discountPaid", source = ".", qualifiedByName = "toDiscountPaid") + @Mapping(target = "discountOutstanding", source = ".", qualifiedByName = "toDiscountOutstanding") + @Mapping(target = "discountOverdue", expression = "java(java.math.BigDecimal.ZERO)") + // Income recognition + @Mapping(target = "realizedIncome", source = ".", qualifiedByName = "toRealizedIncome") + @Mapping(target = "unrealizedIncome", source = ".", qualifiedByName = "toUnrealizedIncome") + @Mapping(target = "overpaymentAmount", source = ".", qualifiedByName = "toOverpaymentAmount") + // Aggregates + @Mapping(target = "totalExpectedRepayment", source = ".", qualifiedByName = "toTotalExpectedRepayment") + @Mapping(target = "totalRepayment", source = ".", qualifiedByName = "toTotalRepayment") + @Mapping(target = "totalOutstanding", source = ".", qualifiedByName = "toTotalOutstanding") + @Mapping(target = "totalOverdue", expression = "java(java.math.BigDecimal.ZERO)") + @Mapping(target = "totalRecovered", expression = "java(java.math.BigDecimal.ZERO)") + // Transaction summaries + @Mapping(target = "totalDisbursement", source = ".", qualifiedByName = "toPrincipalDisbursed") + @Mapping(target = "totalRepaymentTransaction", source = ".", qualifiedByName = "toTotalRepaymentTransaction") + @Mapping(target = "totalRepaymentTransactionReversed", source = ".", qualifiedByName = "toTotalRepaymentTransactionReversed") + @Mapping(target = "totalDiscountFee", source = ".", qualifiedByName = "toDiscountCharged") + WorkingCapitalLoanSummaryData toData(WorkingCapitalLoan loan); + + @Named("toCurrency") + default CurrencyData toCurrency(final WorkingCapitalLoan loan) { + return loan.getLoanProduct().getCurrency().toData(); + } + + @Named("toPrincipalDisbursed") + default BigDecimal toPrincipalDisbursed(final WorkingCapitalLoan loan) { + return sumActive(loan.getTransactions(), LoanTransactionType.DISBURSEMENT); + } + + @Named("toPrincipalPaid") + default BigDecimal toPrincipalPaid(final WorkingCapitalLoan loan) { + return loan.getBalance() != null ? MathUtil.nullToZero(loan.getBalance().getTotalPaidPrincipal()) : BigDecimal.ZERO; + } + + @Named("toPrincipalOutstanding") + default BigDecimal toPrincipalOutstanding(final WorkingCapitalLoan loan) { + return loan.getBalance() != null ? MathUtil.nullToZero(loan.getBalance().getPrincipalOutstanding()) : BigDecimal.ZERO; + } + + @Named("toDiscountCharged") + default BigDecimal toDiscountCharged(final WorkingCapitalLoan loan) { + return sumActive(loan.getTransactions(), LoanTransactionType.DISCOUNT_FEE); + } + + @Named("toDiscountPaid") + default BigDecimal toDiscountPaid(final WorkingCapitalLoan loan) { + return sumActiveAllocationField(loan.getTransactions(), WorkingCapitalLoanTransactionAllocation::getFeeChargesPortion); + } + + @Named("toDiscountOutstanding") + default BigDecimal toDiscountOutstanding(final WorkingCapitalLoan loan) { + return toDiscountCharged(loan).subtract(toDiscountPaid(loan)); + } + + @Named("toRealizedIncome") + default BigDecimal toRealizedIncome(final WorkingCapitalLoan loan) { + return loan.getBalance() != null ? MathUtil.nullToZero(loan.getBalance().getRealizedIncome()) : BigDecimal.ZERO; + } + + @Named("toUnrealizedIncome") + default BigDecimal toUnrealizedIncome(final WorkingCapitalLoan loan) { + return loan.getBalance() != null ? MathUtil.nullToZero(loan.getBalance().getUnrealizedIncome()) : BigDecimal.ZERO; + } + + @Named("toOverpaymentAmount") + default BigDecimal toOverpaymentAmount(final WorkingCapitalLoan loan) { + return loan.getBalance() != null ? MathUtil.nullToZero(loan.getBalance().getOverpaymentAmount()) : BigDecimal.ZERO; + } + + @Named("toTotalExpectedRepayment") + default BigDecimal toTotalExpectedRepayment(final WorkingCapitalLoan loan) { + return toPrincipalDisbursed(loan).add(toDiscountCharged(loan)); + } + + @Named("toTotalRepayment") + default BigDecimal toTotalRepayment(final WorkingCapitalLoan loan) { + return loan.getBalance() != null ? MathUtil.nullToZero(loan.getBalance().getTotalPayment()) : BigDecimal.ZERO; + } + + @Named("toTotalOutstanding") + default BigDecimal toTotalOutstanding(final WorkingCapitalLoan loan) { + return toPrincipalOutstanding(loan).add(toDiscountOutstanding(loan)); + } + + @Named("toTotalRepaymentTransaction") + default BigDecimal toTotalRepaymentTransaction(final WorkingCapitalLoan loan) { + return sumActive(loan.getTransactions(), LoanTransactionType.REPAYMENT); + } + + @Named("toTotalRepaymentTransactionReversed") + default BigDecimal toTotalRepaymentTransactionReversed(final WorkingCapitalLoan loan) { + return sumReversed(loan.getTransactions(), LoanTransactionType.REPAYMENT); + } + + private BigDecimal sumActive(final List transactions, final LoanTransactionType type) { + return transactions.stream().filter(t -> t.getTypeOf() == type && !t.isReversed()) + .map(WorkingCapitalLoanTransaction::getTransactionAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private BigDecimal sumReversed(final List transactions, final LoanTransactionType type) { + return transactions.stream().filter(t -> t.getTypeOf() == type && t.isReversed()) + .map(WorkingCapitalLoanTransaction::getTransactionAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private BigDecimal sumActiveAllocationField(final List transactions, + final Function extractor) { + return transactions.stream().filter(t -> !t.isReversed() && t.getAllocation() != null).map(t -> extractor.apply(t.getAllocation())) + .filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanTransactionMapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanTransactionMapper.java index 305867e8d94..74dfbc87237 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanTransactionMapper.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanTransactionMapper.java @@ -18,15 +18,18 @@ */ package org.apache.fineract.portfolio.workingcapitalloan.mapper; +import java.math.BigDecimal; import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanTransactionData; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -43,6 +46,8 @@ public interface WorkingCapitalLoanTransactionMapper { @Mapping(target = "principalPortion", source = "allocation.principalPortion") @Mapping(target = "feeChargesPortion", source = "allocation.feeChargesPortion") @Mapping(target = "penaltyChargesPortion", source = "allocation.penaltyChargesPortion") + @Mapping(target = "amortizedIncomePortion", source = ".", qualifiedByName = "resolveAmortizedIncome") + @Mapping(target = "currency", source = "wcLoan", qualifiedByName = "currencyData") WorkingCapitalLoanTransactionData toData(WorkingCapitalLoanTransaction transaction); @Named("loanTransactionTypeToEnumData") @@ -64,4 +69,17 @@ default PaymentDetailData paymentDetailToData(final PaymentDetail paymentDetail) default CodeValueData codeValueToData(final CodeValue codeValue) { return codeValue == null ? null : CodeValueData.instance(codeValue.getId(), codeValue.getLabel()); } + + @Named("resolveAmortizedIncome") + default BigDecimal resolveAmortizedIncome(final WorkingCapitalLoanTransaction transaction) { + if (transaction.getLoanTransactionRelations() != null && !transaction.getLoanTransactionRelations().isEmpty()) { + return transaction.getTransactionAmount(); + } + return BigDecimal.ZERO; + } + + @Named("currencyData") + default CurrencyData currencyData(final WorkingCapitalLoan wcLoan) { + return wcLoan.getLoanProduct().getCurrency().toData(); + } } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationReadPlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationReadPlatformServiceImpl.java index 55a231c1bfd..102acd3a2e9 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationReadPlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationReadPlatformServiceImpl.java @@ -19,6 +19,8 @@ package org.apache.fineract.portfolio.workingcapitalloan.service; import jakarta.persistence.criteria.Predicate; +import java.math.BigDecimal; +import java.math.MathContext; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -31,6 +33,8 @@ import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.accountdetails.data.WorkingCapitalLoanAccountSummaryData; import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; @@ -73,6 +77,7 @@ public class WorkingCapitalLoanApplicationReadPlatformServiceImpl implements Wor private final WorkingCapitalBreachReadPlatformService breachReadPlatformService; private final WorkingCapitalLoanDelinquencyReadPlatformService workingCapitalLoanDelinquencyReadPlatformService; private final WorkingCapitalNearBreachReadPlatformService nearBreachReadPlatformService; + private final ProjectedAmortizationScheduleRepositoryWrapper scheduleRepositoryWrapper; @Override public WorkingCapitalLoanTemplateData retrieveTemplate(final Long productId, final Long clientId) { @@ -162,6 +167,7 @@ public WorkingCapitalLoanData retrieveOne(final Long loanId) { WorkingCapitalLoanCollectionData collectionData = workingCapitalLoanDelinquencyReadPlatformService.getCollectionData(loanId, ThreadLocalContextUtil.getBusinessDate()); data.setCollectionData(collectionData); + enrichWithRateAndTerm(loan, data); return data; } @@ -175,9 +181,24 @@ public WorkingCapitalLoanData retrieveOne(final ExternalId externalId) { WorkingCapitalLoanCollectionData collectionData = workingCapitalLoanDelinquencyReadPlatformService.getCollectionData(loan.getId(), ThreadLocalContextUtil.getBusinessDate()); data.setCollectionData(collectionData); + enrichWithRateAndTerm(loanWithDetails, data); return data; } + private void enrichWithRateAndTerm(final WorkingCapitalLoan loan, final WorkingCapitalLoanData data) { + final MathContext mc = MoneyHelper.getMathContext(); + final CurrencyData currency = WorkingCapitalLoanCurrencyResolver.resolveCurrency(loan); + scheduleRepositoryWrapper.readModel(loan.getId(), mc, currency).ifPresent(model -> { + final BigDecimal dailyEir = model.effectiveInterestRate(); + data.setTotalDays(model.effectiveTotalTerm()); + data.setPeriodPaymentAmount(model.expectedPaymentAmount() != null ? model.expectedPaymentAmount().getAmount() : null); + data.setDailyEir(dailyEir); + if (dailyEir != null) { + data.setCalculatedAnnualEir(BigDecimal.ONE.add(dailyEir, mc).pow(365, mc).subtract(BigDecimal.ONE, mc)); + } + }); + } + @Override public Long getResolvedLoanId(final ExternalId externalId) { return this.repository.findByExternalId(externalId).map(WorkingCapitalLoan::getId).orElse(null); diff --git a/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanTransactionMapperTest.java b/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanTransactionMapperTest.java index e8286c21421..a12fa80c901 100644 --- a/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanTransactionMapperTest.java +++ b/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanTransactionMapperTest.java @@ -25,11 +25,16 @@ import java.math.BigDecimal; import java.time.LocalDate; +import java.util.Set; import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanTransactionData; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionAllocation; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionRelation; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoanProduct; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mapstruct.factory.Mappers; @@ -47,6 +52,17 @@ class WorkingCapitalLoanTransactionMapperTest { @Mock private WorkingCapitalLoanTransactionAllocation allocation; + @Mock + private WorkingCapitalLoanTransactionRelation relation; + + @Mock + private WorkingCapitalLoan wcLoan; + + @Mock + private WorkingCapitalLoanProduct wcProduct; + + private final MonetaryCurrency currency = new MonetaryCurrency("USD", 2, null); + @Test void toData_mapsAllFieldsIncludingAllocationPortions() { final LocalDate txnDate = LocalDate.of(2024, 2, 1); @@ -65,11 +81,16 @@ void toData_mapsAllFieldsIncludingAllocationPortions() { when(allocation.getFeeChargesPortion()).thenReturn(null); when(allocation.getPenaltyChargesPortion()).thenReturn(null); + when(transaction.getWcLoan()).thenReturn(wcLoan); + when(wcLoan.getLoanProduct()).thenReturn(wcProduct); + when(wcProduct.getCurrency()).thenReturn(currency); + final WorkingCapitalLoanTransactionData data = mapper.toData(transaction); assertNotNull(data); assertEquals(1L, data.getId()); assertNotNull(data.getType()); + assertNotNull(data.getCurrency()); assertEquals(LoanTransactionType.DISBURSEMENT.getValue().longValue(), data.getType().getId()); assertEquals(LoanTransactionType.DISBURSEMENT.getCode(), data.getType().getCode()); assertEquals(txnDate, data.getTransactionDate()); @@ -79,6 +100,7 @@ void toData_mapsAllFieldsIncludingAllocationPortions() { assertNull(data.getFeeChargesPortion()); assertNull(data.getPenaltyChargesPortion()); assertEquals(false, data.getReversed()); + assertEquals(BigDecimal.ZERO, data.getAmortizedIncomePortion()); } @Test @@ -94,13 +116,45 @@ void toData_whenAllocationNull_setsPortionsToNull() { when(transaction.getReversedOnDate()).thenReturn(null); when(transaction.getAllocation()).thenReturn(null); + when(transaction.getWcLoan()).thenReturn(wcLoan); + when(wcLoan.getLoanProduct()).thenReturn(wcProduct); + when(wcProduct.getCurrency()).thenReturn(currency); + final WorkingCapitalLoanTransactionData data = mapper.toData(transaction); assertNotNull(data); assertNotNull(data.getType()); + assertNotNull(data.getCurrency()); assertEquals(LoanTransactionType.DISBURSEMENT.getCode(), data.getType().getCode()); assertNull(data.getPrincipalPortion()); assertNull(data.getFeeChargesPortion()); assertNull(data.getPenaltyChargesPortion()); + assertEquals(BigDecimal.ZERO, data.getAmortizedIncomePortion()); + } + + @Test + void toData_whenTransactionHasRelations_setsAmortizedIncomeToTransactionAmount() { + final BigDecimal discountAmount = BigDecimal.valueOf(500); + when(transaction.getId()).thenReturn(3L); + when(transaction.getTransactionType()).thenReturn(LoanTransactionType.DISCOUNT_FEE); + when(transaction.getTransactionDate()).thenReturn(LocalDate.of(2024, 2, 1)); + when(transaction.getSubmittedOnDate()).thenReturn(LocalDate.of(2024, 2, 1)); + when(transaction.getTransactionAmount()).thenReturn(discountAmount); + when(transaction.getExternalId()).thenReturn(new ExternalId("discount-ext-1")); + when(transaction.isReversed()).thenReturn(false); + when(transaction.getReversalExternalId()).thenReturn(null); + when(transaction.getReversedOnDate()).thenReturn(null); + when(transaction.getAllocation()).thenReturn(null); + when(transaction.getLoanTransactionRelations()).thenReturn(Set.of(relation)); + + when(transaction.getWcLoan()).thenReturn(wcLoan); + when(wcLoan.getLoanProduct()).thenReturn(wcProduct); + when(wcProduct.getCurrency()).thenReturn(currency); + + final WorkingCapitalLoanTransactionData data = mapper.toData(transaction); + + assertNotNull(data); + assertNotNull(data.getCurrency()); + assertEquals(discountAmount, data.getAmortizedIncomePortion()); } }