Skip to content
Open
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 @@ -122,6 +122,14 @@
"null",
"bigdecimal"
]
},
{
"default": null,
"name": "amortizedIncomePortion",
"type": [
"null",
"bigdecimal"
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@ public class ProjectedAmortizationSchedulePaymentData {
private final BigDecimal actualAmortizationAmount;
private final BigDecimal incomeModification;
private final BigDecimal deferredBalance;
private final BigDecimal feesAmount;

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -84,4 +89,5 @@ public class WorkingCapitalLoanData implements Serializable {
private StringEnumOptionData delinquencyStartType;

private WorkingCapitalLoanCollectionData collectionData;
private WorkingCapitalLoanSummaryData summary;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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<WorkingCapitalLoanData> toDataList(List<WorkingCapitalLoan> loans);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<WorkingCapitalLoanTransaction> 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<WorkingCapitalLoanTransaction> 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<WorkingCapitalLoanTransaction> transactions,
final Function<WorkingCapitalLoanTransactionAllocation, BigDecimal> 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);
}
}
Loading
Loading