Skip to content

Commit 51ecea1

Browse files
FINERACT-2455: WC - Transaction Type - Repayment
1 parent 7eb7b78 commit 51ecea1

30 files changed

Lines changed: 1545 additions & 26 deletions

File tree

fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,14 @@ public CommandWrapperBuilder updateDiscountWorkingCapitalLoanApplication(final L
872872
return this;
873873
}
874874

875+
public CommandWrapperBuilder repaymentWorkingCapitalLoanTransaction(final Long loanId) {
876+
this.actionName = ACTION_REPAYMENT;
877+
this.entityName = ENTITY_WORKINGCAPITALLOAN;
878+
this.entityId = loanId;
879+
this.href = "/working-capital-loans/" + loanId + "/transactions?command=repayment";
880+
return this;
881+
}
882+
875883
public CommandWrapperBuilder createClientIdentifier(final Long clientId) {
876884
this.actionName = ACTION_CREATE;
877885
this.entityName = ENTITY_CLIENTIDENTIFIER;

fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ public void givenAllConfigurationWhenValidatedThenValidationSuccessful() throws
114114
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
115115
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "LoanApprovedAmountChangedBusinessEvent",
116116
"SavingsAccountsStayedLockedBusinessEvent", "SavingsAccountForceWithdrawalBusinessEvent",
117-
"WorkingCapitalLoanDisbursalTransactionBusinessEvent", "WorkingCapitalLoanUndoDisbursalTransactionBusinessEvent");
117+
"WorkingCapitalLoanDisbursalTransactionBusinessEvent", "WorkingCapitalLoanUndoDisbursalTransactionBusinessEvent",
118+
"WorkingCapitalLoanRepaymentTransactionBusinessEvent");
118119

119120
List<FineractPlatformTenant> tenants = Arrays
120121
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -211,7 +212,8 @@ public void givenMissingEventConfigurationWhenValidatedThenThrowException() thro
211212
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
212213
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "LoanApprovedAmountChangedBusinessEvent",
213214
"SavingsAccountsStayedLockedBusinessEvent", "SavingsAccountForceWithdrawalBusinessEvent",
214-
"WorkingCapitalLoanDisbursalTransactionBusinessEvent", "WorkingCapitalLoanUndoDisbursalTransactionBusinessEvent");
215+
"WorkingCapitalLoanDisbursalTransactionBusinessEvent", "WorkingCapitalLoanUndoDisbursalTransactionBusinessEvent",
216+
"WorkingCapitalLoanRepaymentTransactionBusinessEvent");
215217

216218
List<FineractPlatformTenant> tenants = Arrays
217219
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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.infrastructure.event.business.domain.workingcapitalloan.transaction;
20+
21+
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction;
22+
23+
public class WorkingCapitalLoanRepaymentTransactionBusinessEvent extends WorkingCapitalLoanTransactionBusinessEvent {
24+
25+
private static final String TYPE = "WorkingCapitalLoanRepaymentTransactionBusinessEvent";
26+
27+
public WorkingCapitalLoanRepaymentTransactionBusinessEvent(final WorkingCapitalLoanTransaction value) {
28+
super(value);
29+
}
30+
31+
public WorkingCapitalLoanRepaymentTransactionBusinessEvent(final WorkingCapitalLoanTransaction value, final Long aggregateRootId) {
32+
super(value, aggregateRootId);
33+
}
34+
35+
@Override
36+
public String getType() {
37+
return TYPE;
38+
}
39+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ private WorkingCapitalLoanConstants() {
4646
// Loan commands
4747
public static final String APPROVE_LOAN_COMMAND = "approve";
4848
public static final String DISBURSE_LOAN_COMMAND = "disburse";
49+
public static final String REPAYMENT_LOAN_COMMAND = "repayment";
4950

5051
// Approval / Rejection / Undo-approval parameters
5152
public static final String RESOURCE_NAME = WCL_RESOURCE_NAME;
@@ -62,11 +63,13 @@ private WorkingCapitalLoanConstants() {
6263
public static final String classificationIdParamName = "classificationId";
6364

6465
public static final String DISBURSEMENT_CLASSIFICATION_CODE_NAME = "working_capital_loan_disbursement_classification";
66+
public static final String REPAYMENT_CLASSIFICATION_CODE_NAME = "working_capital_loan_repayment_classification";
6567
public static final String paymentDetailsParamName = "paymentDetails";
6668
public static final String paymentTypeIdParamName = "paymentTypeId";
6769
public static final String accountNumberParamName = "accountNumber";
6870
public static final String checkNumberParamName = "checkNumber";
6971
public static final String routingCodeParamName = "routingCode";
7072
public static final String receiptNumberParamName = "receiptNumber";
7173
public static final String bankNumberParamName = "bankNumber";
74+
public static final String transactionDateParamName = "transactionDate";
7275
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResource.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@
2222
import io.swagger.v3.oas.annotations.Parameter;
2323
import io.swagger.v3.oas.annotations.media.Content;
2424
import io.swagger.v3.oas.annotations.media.Schema;
25+
import io.swagger.v3.oas.annotations.parameters.RequestBody;
2526
import io.swagger.v3.oas.annotations.responses.ApiResponse;
2627
import io.swagger.v3.oas.annotations.responses.ApiResponses;
2728
import io.swagger.v3.oas.annotations.tags.Tag;
29+
import jakarta.ws.rs.Consumes;
2830
import jakarta.ws.rs.GET;
31+
import jakarta.ws.rs.POST;
2932
import jakarta.ws.rs.Path;
3033
import jakarta.ws.rs.PathParam;
3134
import jakarta.ws.rs.Produces;
@@ -34,8 +37,13 @@
3437
import jakarta.ws.rs.core.MediaType;
3538
import jakarta.ws.rs.core.UriInfo;
3639
import lombok.RequiredArgsConstructor;
40+
import org.apache.fineract.commands.domain.CommandWrapper;
41+
import org.apache.fineract.commands.service.CommandWrapperBuilder;
42+
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
3743
import org.apache.fineract.infrastructure.core.api.jersey.Pagination;
44+
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
3845
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
46+
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
3947
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
4048
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
4149
import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants;
@@ -59,6 +67,7 @@ public class WorkingCapitalLoanTransactionsApiResource {
5967
private final PlatformSecurityContext context;
6068
private final WorkingCapitalLoanApplicationReadPlatformService loanReadPlatformService;
6169
private final WorkingCapitalLoanTransactionReadPlatformService transactionReadPlatformService;
70+
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
6271

6372
@GET
6473
@Path("{loanId}/transactions")
@@ -168,4 +177,51 @@ private WorkingCapitalLoanCommandTemplateData handleLoanTransactionTemplate(fina
168177

169178
return loanTransactionTemplateData;
170179
}
180+
181+
@POST
182+
@Path("{loanId}/transactions")
183+
@Consumes({ MediaType.APPLICATION_JSON })
184+
@Produces({ MediaType.APPLICATION_JSON })
185+
@Operation(operationId = "executeWorkingCapitalLoanTransactionById", summary = "Execute Working Capital Loan transaction", description = "Supported command query parameter: repayment")
186+
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanTransactionsApiResourceSwagger.PostWorkingCapitalLoanTransactionsRequest.class)))
187+
@ApiResponses({
188+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanTransactionsApiResourceSwagger.PostWorkingCapitalLoanTransactionsResponse.class))) })
189+
public CommandProcessingResult executeLoanTransactionById(
190+
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
191+
@QueryParam("command") @Parameter(description = "command", required = true) final String commandParam,
192+
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
193+
return executeTransaction(loanId, null, commandParam, apiRequestBodyAsJson);
194+
}
195+
196+
@POST
197+
@Path("external-id/{loanExternalId}/transactions")
198+
@Consumes({ MediaType.APPLICATION_JSON })
199+
@Produces({ MediaType.APPLICATION_JSON })
200+
@Operation(operationId = "executeWorkingCapitalLoanTransactionByExternalId", summary = "Execute Working Capital Loan transaction by external id", description = "Supported command query parameter: repayment")
201+
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanTransactionsApiResourceSwagger.PostWorkingCapitalLoanTransactionsRequest.class)))
202+
@ApiResponses({
203+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanTransactionsApiResourceSwagger.PostWorkingCapitalLoanTransactionsResponse.class))) })
204+
public CommandProcessingResult executeLoanTransactionByExternalId(
205+
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
206+
@QueryParam("command") @Parameter(description = "command", required = true) final String commandParam,
207+
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
208+
return executeTransaction(null, loanExternalId, commandParam, apiRequestBodyAsJson);
209+
}
210+
211+
private CommandProcessingResult executeTransaction(final Long loanId, final String loanExternalIdStr, final String commandParam,
212+
final String apiRequestBodyAsJson) {
213+
final Long resolvedLoanId = loanId != null ? loanId
214+
: loanReadPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalIdStr));
215+
if (resolvedLoanId == null) {
216+
throw new WorkingCapitalLoanNotFoundException(ExternalIdFactory.produce(loanExternalIdStr));
217+
}
218+
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
219+
final CommandWrapper commandRequest;
220+
if (CommandParameterUtil.is(commandParam, "repayment")) {
221+
commandRequest = builder.repaymentWorkingCapitalLoanTransaction(resolvedLoanId).build();
222+
} else {
223+
throw new UnrecognizedQueryParamException("command", commandParam);
224+
}
225+
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
226+
}
171227
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResourceSwagger.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,44 @@ private CodeValueData() {}
125125
@Schema(example = "Some classification")
126126
public String name;
127127
}
128+
129+
@Schema(description = "Request for transaction command: repayment")
130+
public static final class PostWorkingCapitalLoanTransactionsRequest {
131+
132+
private PostWorkingCapitalLoanTransactionsRequest() {}
133+
134+
@Schema(example = "en_GB")
135+
public String locale;
136+
@Schema(example = "dd MMMM yyyy")
137+
public String dateFormat;
138+
@Schema(example = "28 June 2024", description = "Repayment transaction date")
139+
public String transactionDate;
140+
@Schema(example = "100.0", description = "Repayment amount")
141+
public BigDecimal transactionAmount;
142+
@Schema(example = "12", description = "Optional code value id for repayment classification")
143+
public Long classificationId;
144+
@Schema(example = "Repayment note")
145+
public String note;
146+
@Schema(example = "repayment-ext-001")
147+
public String externalId;
148+
@Schema(description = "Payment details")
149+
public WorkingCapitalLoanTransactionPaymentDetailData paymentDetails;
150+
}
151+
152+
@Schema(description = "Response for repayment transaction command")
153+
public static final class PostWorkingCapitalLoanTransactionsResponse {
154+
155+
private PostWorkingCapitalLoanTransactionsResponse() {}
156+
157+
@Schema(example = "1")
158+
public Long officeId;
159+
@Schema(example = "2")
160+
public Long clientId;
161+
@Schema(example = "3")
162+
public Long loanId;
163+
@Schema(example = "4")
164+
public Long resourceId;
165+
@Schema(example = "repayment-ext-001")
166+
public String resourceExternalId;
167+
}
128168
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleModel.java

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,15 +162,24 @@ public static ProjectedAmortizationScheduleModel generate(final BigDecimal origi
162162
expectedDisbursementDate, Money.of(currency, expectedPayment, mc), term, eir, mc, currency);
163163
}
164164

165+
public LocalDate normalizePaymentDateForSchedule(final LocalDate paymentDate) {
166+
Objects.requireNonNull(paymentDate, "paymentDate");
167+
if (paymentDate.equals(expectedDisbursementDate)) {
168+
return expectedDisbursementDate.plusDays(1);
169+
}
170+
return paymentDate;
171+
}
172+
165173
public void applyPayment(final LocalDate paymentDate, final BigDecimal amount) {
166174
Objects.requireNonNull(paymentDate, "paymentDate");
167175
Objects.requireNonNull(amount, "amount");
168-
final int index = resolvePaymentIndex(paymentDate);
176+
final LocalDate scheduleDate = normalizePaymentDateForSchedule(paymentDate);
177+
final int index = resolvePaymentIndex(scheduleDate);
169178
if (index < 0 || index >= loanTerm) {
170179
throw new IllegalArgumentException("paymentDate " + paymentDate + " is outside the valid range ["
171180
+ expectedDisbursementDate.plusDays(1) + " .. " + expectedDisbursementDate.plusDays(loanTerm) + "]");
172181
}
173-
appliedPayments.add(new AppliedPayment(paymentDate, amount));
182+
appliedPayments.add(new AppliedPayment(scheduleDate, amount));
174183
rebuildPayments();
175184
}
176185

@@ -184,6 +193,37 @@ public ProjectedAmortizationScheduleModel regenerate(final BigDecimal newDiscoun
184193
return newModel;
185194
}
186195

196+
public void recalculateNetAmortizationAndDeferredBalanceFrom(final LocalDate repaymentDate) {
197+
if (repaymentDate == null || payments == null || payments.isEmpty()) {
198+
return;
199+
}
200+
final ProjectedPayment lastRepayment = payments.stream().filter(p -> p.paymentNo() > 0).filter(p -> repaymentDate.equals(p.date()))
201+
.reduce((a, b) -> b).orElse(null);
202+
203+
if (lastRepayment == null) {
204+
return;
205+
}
206+
207+
int fromIndex = payments.indexOf(lastRepayment);
208+
209+
BigDecimal runningNetAmortization = amountOrZero(payments.get(fromIndex).netAmortizationAmount());
210+
BigDecimal runningDeferredBalance = amountOrZero(payments.get(fromIndex).deferredBalance());
211+
212+
final List<ProjectedPayment> adjusted = new ArrayList<>(payments.subList(0, fromIndex + 1));
213+
for (int i = fromIndex + 1; i < payments.size(); i++) {
214+
final ProjectedPayment current = payments.get(i);
215+
final BigDecimal actualTotalAmortization = amountOrZero(current.actualAmortizationAmount());
216+
runningNetAmortization = runningNetAmortization.subtract(actualTotalAmortization, mc);
217+
runningDeferredBalance = runningDeferredBalance.subtract(actualTotalAmortization, mc);
218+
219+
adjusted.add(new ProjectedPayment(current.paymentNo(), current.date(), current.count(), current.paymentsLeft(),
220+
current.expectedPaymentAmount(), current.forecastPaymentAmount(), current.discountFactor(), current.npvValue(),
221+
current.balance(), current.expectedAmortizationAmount(), money(runningNetAmortization), current.actualPaymentAmount(),
222+
current.actualAmortizationAmount(), current.incomeModification(), money(runningDeferredBalance)));
223+
}
224+
this.payments = List.copyOf(adjusted);
225+
}
226+
187227
private void rebuildPayments() {
188228
final Map<LocalDate, BigDecimal> paymentsByDate = aggregatePaymentsByDate();
189229
final List<BigDecimal> paymentList = buildPaymentList(paymentsByDate);
@@ -275,6 +315,10 @@ private List<ProjectedPayment> buildPayments(final List<BigDecimal> payments, fi
275315
return result;
276316
}
277317

318+
private static BigDecimal amountOrZero(final Money value) {
319+
return value != null && value.getAmount() != null ? value.getAmount() : BigDecimal.ZERO;
320+
}
321+
278322
private ProjectedPayment createDisbursementPayment(final int appliedCount) {
279323
final Money negDisbursement = netDisbursementAmount.negated(mc);
280324
final long count = (long) loanTerm + appliedCount;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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.data;
20+
21+
import java.math.BigDecimal;
22+
23+
public record RepaymentAmortizationData(BigDecimal currentDayAmortizedAmount, BigDecimal totalAmortizedAmount) {
24+
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,22 @@ public static WorkingCapitalLoanTransaction disbursement(final WorkingCapitalLoa
118118
txn.reversedOnDate = null;
119119
return txn;
120120
}
121+
122+
public static WorkingCapitalLoanTransaction repayment(final WorkingCapitalLoan loan, final BigDecimal amount,
123+
final PaymentDetail paymentDetail, final LocalDate transactionDate, final CodeValue classification,
124+
final ExternalId externalId) {
125+
final WorkingCapitalLoanTransaction txn = new WorkingCapitalLoanTransaction();
126+
txn.wcLoan = loan;
127+
txn.transactionType = LoanTransactionType.REPAYMENT;
128+
txn.transactionDate = transactionDate;
129+
txn.submittedOnDate = transactionDate;
130+
txn.transactionAmount = amount;
131+
txn.paymentDetail = paymentDetail;
132+
txn.classification = classification;
133+
txn.externalId = externalId != null ? externalId : ExternalId.empty();
134+
txn.reversed = false;
135+
txn.reversalExternalId = null;
136+
txn.reversedOnDate = null;
137+
return txn;
138+
}
121139
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public class WorkingCapitalLoanTransactionAllocation extends AbstractAuditableWi
5959

6060
protected WorkingCapitalLoanTransactionAllocation() {}
6161

62-
public static WorkingCapitalLoanTransactionAllocation forDisbursement(final WorkingCapitalLoanTransaction transaction,
62+
public static WorkingCapitalLoanTransactionAllocation forPrincipalAllocation(final WorkingCapitalLoanTransaction transaction,
6363
final BigDecimal principalAmount) {
6464
final WorkingCapitalLoanTransactionAllocation allocation = new WorkingCapitalLoanTransactionAllocation();
6565
allocation.wcLoanTransaction = transaction;

0 commit comments

Comments
 (0)