Skip to content

Commit 7f2d0c2

Browse files
FINERACT-2455: WC - Transaction Type - Accrual and accrual handling
1 parent 347694a commit 7f2d0c2

11 files changed

Lines changed: 537 additions & 0 deletions

File tree

fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAccrual.feature

Lines changed: 299 additions & 0 deletions
Large diffs are not rendered by default.

fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalTransactionAllocation.feature

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ Feature: Working Capital Transaction Allocation
224224
Then Working Capital Loan has transactions:
225225
| transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed |
226226
| 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false |
227+
| 10 January 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false |
227228
| 20 January 2026 | Repayment | 100.0 | 0.0 | 100.0 | 0.0 | false |
228229
And Working Capital Loan has charges with the following data:
229230
| Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode |
@@ -236,6 +237,7 @@ Feature: Working Capital Transaction Allocation
236237
Then Working Capital Loan has transactions:
237238
| transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed |
238239
| 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false |
240+
| 10 January 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false |
239241
| 10 January 2026 | Repayment | 100.0 | 0.0 | 100.0 | 0.0 | false |
240242
| 20 January 2026 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | false |
241243
And Working Capital Loan has charges with the following data:
@@ -263,6 +265,7 @@ Feature: Working Capital Transaction Allocation
263265
Then Working Capital Loan has transactions:
264266
| transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed |
265267
| 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false |
268+
| 10 January 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false |
266269
| 11 January 2026 | Charge Adjustment | 70.0 | 0.0 | 70.0 | 0.0 | false |
267270
And Working Capital Loan has charges with the following data:
268271
| Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode |

fineract-e2e-tests-runner/src/test/resources/features/WorkingCapital_COB.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Feature: Working Capital COB Job
1616
| WC_BREACH_SCHEDULE | 4 |
1717
| WC_NEAR_BREACH_EVALUATION | 5 |
1818
| WC_DISCOUNT_FEE_AMORTIZATION | 6 |
19+
| WC_CHARGE_ACCRUAL | 7 |
1920
Then Admin verifies scheduler job "WC_COB" has display name "Working Capital Loan COB"
2021
Then Admin verifies scheduler job "WC_COB" has active status "false"
2122

fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualWithDeferredRevenueAmortizationAccountingProcessorForWorkingCapitalLoan.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public void postJournalEntries(final WorkingCapitalLoan loan, final WorkingCapit
8787
case LoanTransactionType.CREDIT_BALANCE_REFUND -> postCreditBalanceRefundJournalEntries(loan, txn);
8888
case LoanTransactionType.CHARGE_ADJUSTMENT ->
8989
postChargeAdjustmentJournalEntries(loan, txn, feesPortion, penaltiesPortion, isChargedOff);
90+
case LoanTransactionType.ACCRUAL -> postChargeAccrualJournalEntries(loan, txn, feesPortion, penaltiesPortion);
9091
default -> {
9192
throw new NotImplementedException(
9293
"Post Journal Entries is not implemented yet for " + txn.getTypeOf().getCode() + " for Working Capital Loan");
@@ -111,6 +112,19 @@ private void postChargeAdjustmentJournalEntries(final WorkingCapitalLoan loan, f
111112
accountPostHelper.postCreditJournalEntry(CashAccountsForLoan.PENALTIES_RECEIVABLE, penaltiesPortion);
112113
}
113114

115+
private void postChargeAccrualJournalEntries(final WorkingCapitalLoan loan, final WorkingCapitalLoanTransaction txn,
116+
final BigDecimal feesPortion, final BigDecimal penaltiesPortion) {
117+
final JournalEntryPostingHelper accountPostHelper = new JournalEntryPostingHelper(loan, txn);
118+
119+
// Debit receivable when the charge becomes accrued.
120+
accountPostHelper.postDebitJournalEntry(CashAccountsForLoan.FEES_RECEIVABLE, feesPortion);
121+
accountPostHelper.postDebitJournalEntry(CashAccountsForLoan.PENALTIES_RECEIVABLE, penaltiesPortion);
122+
123+
// Credit the corresponding income account.
124+
accountPostHelper.postCreditJournalEntry(CashAccountsForLoan.INCOME_FROM_FEES, feesPortion);
125+
accountPostHelper.postCreditJournalEntry(CashAccountsForLoan.INCOME_FROM_PENALTIES, penaltiesPortion);
126+
}
127+
114128
private void postGoodwillCreditJournalEntries(WorkingCapitalLoan loan, WorkingCapitalLoanTransaction txn, BigDecimal principalPortion,
115129
BigDecimal feesPortion, BigDecimal penaltiesPortion, BigDecimal overpaymentPortion) {
116130
BigDecimal overpaymentPlusPrincipal = principalPortion.add(overpaymentPortion);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.cob.workingcapitalloan.businessstep;
20+
21+
import lombok.RequiredArgsConstructor;
22+
import org.apache.fineract.infrastructure.core.service.DateUtils;
23+
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan;
24+
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanChargeAccrualService;
25+
import org.springframework.stereotype.Component;
26+
27+
@Component
28+
@RequiredArgsConstructor
29+
public class ChargeAccrualBusinessStep extends WorkingCapitalLoanCOBBusinessStep {
30+
31+
private final WorkingCapitalLoanChargeAccrualService chargeAccrualService;
32+
33+
@Override
34+
public WorkingCapitalLoan execute(final WorkingCapitalLoan input) {
35+
chargeAccrualService.processDueDateAccruals(input, DateUtils.getBusinessLocalDate());
36+
return input;
37+
}
38+
39+
@Override
40+
public String getEnumStyledName() {
41+
return "WC_CHARGE_ACCRUAL";
42+
}
43+
44+
@Override
45+
public String getHumanReadableName() {
46+
return "WC Charge Accrual";
47+
}
48+
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransaction.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,13 @@ public static WorkingCapitalLoanTransaction chargeAdjustment(final WorkingCapita
194194
return transaction;
195195
}
196196

197+
public static WorkingCapitalLoanTransaction accrual(final WorkingCapitalLoan loan, final ExternalId externalId, final BigDecimal amount,
198+
final LocalDate transactionDate) {
199+
final WorkingCapitalLoanTransaction transaction = new WorkingCapitalLoanTransaction();
200+
transaction.initialize(loan, LoanTransactionType.ACCRUAL, transactionDate, amount, null, null, externalId);
201+
return transaction;
202+
}
203+
197204
private void initialize(final WorkingCapitalLoan loan, final LoanTransactionType transactionType, final LocalDate transactionDate,
198205
final BigDecimal amount, final PaymentDetail paymentDetail, final CodeValue classification, final ExternalId externalId) {
199206
this.wcLoan = loan;

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionAllocation.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,14 @@ public static WorkingCapitalLoanTransactionAllocation forChargeAdjustment(final
109109
allocation.penaltyChargesPortion = isPenalty ? MathUtil.nullToZero(amount) : BigDecimal.ZERO;
110110
return allocation;
111111
}
112+
113+
public static WorkingCapitalLoanTransactionAllocation forChargeAccrual(final WorkingCapitalLoanTransaction transaction,
114+
final BigDecimal amount, final boolean isPenalty) {
115+
final WorkingCapitalLoanTransactionAllocation allocation = new WorkingCapitalLoanTransactionAllocation();
116+
allocation.wcLoanTransaction = transaction;
117+
allocation.principalPortion = BigDecimal.ZERO;
118+
allocation.feeChargesPortion = isPenalty ? BigDecimal.ZERO : MathUtil.nullToZero(amount);
119+
allocation.penaltyChargesPortion = isPenalty ? MathUtil.nullToZero(amount) : BigDecimal.ZERO;
120+
return allocation;
121+
}
112122
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.workingcapitalloan.service;
20+
21+
import java.time.LocalDate;
22+
import java.util.List;
23+
import lombok.RequiredArgsConstructor;
24+
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
25+
import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper;
26+
import org.apache.fineract.infrastructure.core.domain.ExternalId;
27+
import org.apache.fineract.infrastructure.core.service.MathUtil;
28+
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
29+
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
30+
import org.apache.fineract.portfolio.workingcapitalloan.accounting.WorkingCapitalLoanAccountingProcessor;
31+
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan;
32+
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanCharge;
33+
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction;
34+
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionAllocation;
35+
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionRelation;
36+
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionRelationRepository;
37+
import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanChargeRepository;
38+
import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanTransactionAllocationRepository;
39+
import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanTransactionRepository;
40+
import org.springframework.stereotype.Service;
41+
42+
@Service
43+
@RequiredArgsConstructor
44+
public class WorkingCapitalLoanChargeAccrualService {
45+
46+
private static final String SUBMITTED_DATE = "submitted-date";
47+
private static final String DUE_DATE = "due-date";
48+
private static final String DEFAULT_ACCRUAL_DATE_CONFIG = DUE_DATE;
49+
50+
private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository;
51+
private final WorkingCapitalLoanChargeRepository chargeRepository;
52+
private final WorkingCapitalLoanTransactionRepository transactionRepository;
53+
private final WorkingCapitalLoanTransactionAllocationRepository allocationRepository;
54+
private final WorkingCapitalLoanTransactionRelationRepository relationRepository;
55+
private final WorkingCapitalLoanAccountingProcessor accountingProcessor;
56+
57+
public void processOnChargeAdded(final WorkingCapitalLoan loan, final WorkingCapitalLoanCharge charge) {
58+
if (isAccrualPostingDisabled(loan)) {
59+
return;
60+
}
61+
if (!SUBMITTED_DATE.equalsIgnoreCase(retrieveChargeAccrualDateConfig())) {
62+
return;
63+
}
64+
createChargeAccrualIfMissing(loan, charge, charge.getSubmittedOnDate());
65+
}
66+
67+
public void processDueDateAccruals(final WorkingCapitalLoan loan, final LocalDate businessDate) {
68+
if (isAccrualPostingDisabled(loan)) {
69+
return;
70+
}
71+
if (!DUE_DATE.equalsIgnoreCase(retrieveChargeAccrualDateConfig())) {
72+
return;
73+
}
74+
final List<WorkingCapitalLoanCharge> activeCharges = chargeRepository.findByLoanIdAndActiveTrueOrderByDueDateAscIdAsc(loan.getId());
75+
activeCharges.stream().filter(charge -> charge.getDueDate() != null && !charge.getDueDate().isAfter(businessDate))
76+
.forEach(charge -> createChargeAccrualIfMissing(loan, charge, charge.getDueDate()));
77+
}
78+
79+
private boolean isAccrualPostingDisabled(final WorkingCapitalLoan loan) {
80+
return !loan.getLoanProduct().getAccountingRule().isAccrualWithDeferredRevenueAmortization();
81+
}
82+
83+
private void createChargeAccrualIfMissing(final WorkingCapitalLoan loan, final WorkingCapitalLoanCharge charge,
84+
final LocalDate accrualDate) {
85+
if (accrualDate == null || isAlreadyAccrued(charge) || !MathUtil.isGreaterThanZero(charge.getAmountOutstanding())) {
86+
return;
87+
}
88+
final WorkingCapitalLoanTransaction accrualTransaction = WorkingCapitalLoanTransaction.accrual(loan, ExternalId.empty(),
89+
charge.getAmount(), accrualDate);
90+
final WorkingCapitalLoanTransactionRelation relation = WorkingCapitalLoanTransactionRelation.linkToCharge(accrualTransaction,
91+
charge, LoanTransactionRelationTypeEnum.RELATED);
92+
accrualTransaction.getLoanTransactionRelations().add(relation);
93+
94+
transactionRepository.saveAndFlush(accrualTransaction);
95+
96+
final WorkingCapitalLoanTransactionAllocation allocation = WorkingCapitalLoanTransactionAllocation
97+
.forChargeAccrual(accrualTransaction, charge.getAmount(), charge.isPenaltyCharge());
98+
allocationRepository.saveAndFlush(allocation);
99+
accountingProcessor.postJournalEntries(loan, accrualTransaction, allocation, false);
100+
}
101+
102+
private boolean isAlreadyAccrued(final WorkingCapitalLoanCharge charge) {
103+
return !relationRepository
104+
.findAllByToChargeAndFromTransactionReversedAndFromTransactionTransactionType(charge, false, LoanTransactionType.ACCRUAL)
105+
.isEmpty();
106+
}
107+
108+
private String retrieveChargeAccrualDateConfig() {
109+
final String configuredValue = globalConfigurationRepository
110+
.findOneByNameWithNotFoundDetection(GlobalConfigurationConstants.CHARGE_ACCRUAL_DATE).getStringValue();
111+
return configuredValue == null || configuredValue.isBlank() ? DEFAULT_ACCRUAL_DATE_CONFIG : configuredValue;
112+
}
113+
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformServiceImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ public class WorkingCapitalLoanChargeWritePlatformServiceImpl implements Working
8888
private final WorkingCapitalLoanAccountingProcessor accountingProcessor;
8989
private final WorkingCapitalLoanChargePaymentHandler chargePaymentHandler;
9090
private final WorkingCapitalLoanBalanceUpdater balanceUpdater;
91+
private final WorkingCapitalLoanChargeAccrualService chargeAccrualService;
9192

9293
@Override
9394
public CommandProcessingResult createLoanCharge(Long loanId, JsonCommand command) {
@@ -100,6 +101,7 @@ public CommandProcessingResult createLoanCharge(Long loanId, JsonCommand command
100101
loanCharge = loanChargeRepository.saveAndFlush(loanCharge);
101102

102103
addChargeToBalance(loan, loanCharge);
104+
chargeAccrualService.processOnChargeAdded(loan, loanCharge);
103105

104106
return new CommandProcessingResultBuilder() //
105107
.withCommandId(command.commandId()) //

fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,5 @@
7272
<include relativeToChangelogFile="true" file="parts/0048_rename_wc_accounting_type_cash_based_to_acc_def_rev_am.xml"/>
7373
<include relativeToChangelogFile="true" file="parts/0049_wc_loan_breach_reset.xml"/>
7474
<include relativeToChangelogFile="true" file="parts/0050_wc_loan_breach_disable.xml"/>
75+
<include relativeToChangelogFile="true" file="parts/0051_wc_loan_charge_accrual_business_step.xml"/>
7576
</databaseChangeLog>

0 commit comments

Comments
 (0)