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 @@ -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 totalNoPayments;
@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 @@ -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 totalNoPayments;
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,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.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;

// Discount fee
private BigDecimal discountCharged;
private BigDecimal discountPaid;
private BigDecimal discountOutstanding;

// Income recognition
private BigDecimal realizedIncome;
private BigDecimal unrealizedIncome;

// Overpayment
private BigDecimal overpaymentAmount;
Comment thread
adamsaghy marked this conversation as resolved.

// Aggregates
private BigDecimal totalExpectedRepayment;
private BigDecimal totalRepayment;
private BigDecimal totalOutstanding;

// Transaction summaries
private BigDecimal totalDisbursement;
private BigDecimal totalRepaymentTransaction;
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 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 = "totalNoPayments", 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,145 @@
/**
* 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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont like this approach... source should point to the appropriate field and mapper method which checks whether null or zero to be called on it. 1 method can be reused across all fields.


@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")
// Discount fee
@Mapping(target = "discountCharged", source = ".", qualifiedByName = "toDiscountCharged")
@Mapping(target = "discountPaid", source = ".", qualifiedByName = "toDiscountPaid")
@Mapping(target = "discountOutstanding", source = ".", qualifiedByName = "toDiscountOutstanding")
// Income recognition
@Mapping(target = "realizedIncome", source = ".", qualifiedByName = "toRealizedIncome")
@Mapping(target = "unrealizedIncome", source = ".", qualifiedByName = "toUnrealizedIncome")
// Overpayment
@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")
// Transaction summaries
@Mapping(target = "totalDisbursement", source = ".", qualifiedByName = "toPrincipalDisbursed")
@Mapping(target = "totalRepaymentTransaction", source = ".", qualifiedByName = "toTotalRepaymentTransaction")
@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);
}

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 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
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;
Expand All @@ -43,6 +45,7 @@ public interface WorkingCapitalLoanTransactionMapper {
@Mapping(target = "principalPortion", source = "allocation.principalPortion")
@Mapping(target = "feeChargesPortion", source = "allocation.feeChargesPortion")
@Mapping(target = "penaltyChargesPortion", source = "allocation.penaltyChargesPortion")
@Mapping(target = "currency", source = "wcLoan", qualifiedByName = "currencyData")
WorkingCapitalLoanTransactionData toData(WorkingCapitalLoanTransaction transaction);

@Named("loanTransactionTypeToEnumData")
Expand All @@ -64,4 +67,9 @@ default PaymentDetailData paymentDetailToData(final PaymentDetail paymentDetail)
default CodeValueData codeValueToData(final CodeValue codeValue) {
return codeValue == null ? null : CodeValueData.instance(codeValue.getId(), codeValue.getLabel());
}

@Named("currencyData")
default CurrencyData currencyData(final WorkingCapitalLoan wcLoan) {
return wcLoan.getLoanProduct().getCurrency().toData();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}

Expand All @@ -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.setTotalNoPayments(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);
Expand Down
Loading
Loading