Skip to content

Commit fed79a3

Browse files
FINERACT-2455: WC - Transaction Type - Repayment
1 parent 4a8bdc5 commit fed79a3

30 files changed

Lines changed: 1515 additions & 20 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: 52 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,47 @@ 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+
public CommandProcessingResult executeLoanTransactionById(
188+
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
189+
@QueryParam("command") @Parameter(description = "command", required = true) final String commandParam,
190+
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
191+
return executeTransaction(loanId, null, commandParam, apiRequestBodyAsJson);
192+
}
193+
194+
@POST
195+
@Path("external-id/{loanExternalId}/transactions")
196+
@Consumes({ MediaType.APPLICATION_JSON })
197+
@Produces({ MediaType.APPLICATION_JSON })
198+
@Operation(operationId = "executeWorkingCapitalLoanTransactionByExternalId", summary = "Execute Working Capital Loan transaction by external id", description = "Supported command query parameter: repayment")
199+
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanTransactionsApiResourceSwagger.PostWorkingCapitalLoanTransactionsRequest.class)))
200+
public CommandProcessingResult executeLoanTransactionByExternalId(
201+
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
202+
@QueryParam("command") @Parameter(description = "command", required = true) final String commandParam,
203+
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
204+
return executeTransaction(null, loanExternalId, commandParam, apiRequestBodyAsJson);
205+
}
206+
207+
private CommandProcessingResult executeTransaction(final Long loanId, final String loanExternalIdStr, final String commandParam,
208+
final String apiRequestBodyAsJson) {
209+
final Long resolvedLoanId = loanId != null ? loanId
210+
: loanReadPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalIdStr));
211+
if (resolvedLoanId == null) {
212+
throw new WorkingCapitalLoanNotFoundException(ExternalIdFactory.produce(loanExternalIdStr));
213+
}
214+
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
215+
final CommandWrapper commandRequest;
216+
if (CommandParameterUtil.is(commandParam, "repayment")) {
217+
commandRequest = builder.repaymentWorkingCapitalLoanTransaction(resolvedLoanId).build();
218+
} else {
219+
throw new UnrecognizedQueryParamException("command", commandParam);
220+
}
221+
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
222+
}
171223
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,27 @@ 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+
}
128151
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,37 @@ public ProjectedAmortizationScheduleModel regenerate(final BigDecimal newDiscoun
184184
return newModel;
185185
}
186186

187+
public void recalculateNetAmortizationAndDeferredBalanceFrom(final LocalDate repaymentDate) {
188+
if (repaymentDate == null || payments == null || payments.isEmpty()) {
189+
return;
190+
}
191+
final ProjectedPayment lastRepayment = payments.stream().filter(p -> p.paymentNo() > 0).filter(p -> repaymentDate.equals(p.date()))
192+
.reduce((a, b) -> b).orElse(null);
193+
194+
if (lastRepayment == null) {
195+
return;
196+
}
197+
198+
int fromIndex = payments.indexOf(lastRepayment);
199+
200+
BigDecimal runningNetAmortization = amountOrZero(payments.get(fromIndex).netAmortizationAmount());
201+
BigDecimal runningDeferredBalance = amountOrZero(payments.get(fromIndex).deferredBalance());
202+
203+
final List<ProjectedPayment> adjusted = new ArrayList<>(payments.subList(0, fromIndex + 1));
204+
for (int i = fromIndex + 1; i < payments.size(); i++) {
205+
final ProjectedPayment current = payments.get(i);
206+
final BigDecimal actualTotalAmortization = amountOrZero(current.actualAmortizationAmount());
207+
runningNetAmortization = runningNetAmortization.subtract(actualTotalAmortization, mc);
208+
runningDeferredBalance = runningDeferredBalance.subtract(actualTotalAmortization, mc);
209+
210+
adjusted.add(new ProjectedPayment(current.paymentNo(), current.date(), current.count(), current.paymentsLeft(),
211+
current.expectedPaymentAmount(), current.forecastPaymentAmount(), current.discountFactor(), current.npvValue(),
212+
current.balance(), current.expectedAmortizationAmount(), money(runningNetAmortization), current.actualPaymentAmount(),
213+
current.actualAmortizationAmount(), current.incomeModification(), money(runningDeferredBalance)));
214+
}
215+
this.payments = List.copyOf(adjusted);
216+
}
217+
187218
private void rebuildPayments() {
188219
final Map<LocalDate, BigDecimal> paymentsByDate = aggregatePaymentsByDate();
189220
final List<BigDecimal> paymentList = buildPaymentList(paymentsByDate);
@@ -275,6 +306,10 @@ private List<ProjectedPayment> buildPayments(final List<BigDecimal> payments, fi
275306
return result;
276307
}
277308

309+
private static BigDecimal amountOrZero(final Money value) {
310+
return value != null && value.getAmount() != null ? value.getAmount() : BigDecimal.ZERO;
311+
}
312+
278313
private ProjectedPayment createDisbursementPayment(final int appliedCount) {
279314
final Money negDisbursement = netDisbursementAmount.negated(mc);
280315
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)