Skip to content

Commit 9dad940

Browse files
budaidevadamsaghy
authored andcommitted
FINERACT-2455: working capital loan approval/rejection
1 parent c26fc07 commit 9dad940

18 files changed

Lines changed: 1379 additions & 2 deletions

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,30 @@ public CommandWrapperBuilder deleteWorkingCapitalLoanApplication() {
594594
return this;
595595
}
596596

597+
public CommandWrapperBuilder approveWorkingCapitalLoanApplication(final Long loanId) {
598+
this.actionName = "APPROVE";
599+
this.entityName = "WORKINGCAPITALLOAN";
600+
this.entityId = loanId;
601+
this.href = "/workingcapitalloans/" + loanId;
602+
return this;
603+
}
604+
605+
public CommandWrapperBuilder rejectWorkingCapitalLoanApplication(final Long loanId) {
606+
this.actionName = "REJECT";
607+
this.entityName = "WORKINGCAPITALLOAN";
608+
this.entityId = loanId;
609+
this.href = "/workingcapitalloans/" + loanId;
610+
return this;
611+
}
612+
613+
public CommandWrapperBuilder undoWorkingCapitalLoanApplicationApproval(final Long loanId) {
614+
this.actionName = "APPROVALUNDO";
615+
this.entityName = "WORKINGCAPITALLOAN";
616+
this.entityId = loanId;
617+
this.href = "/workingcapitalloans/" + loanId;
618+
return this;
619+
}
620+
597621
public CommandWrapperBuilder createClientIdentifier(final Long clientId) {
598622
this.actionName = "CREATE";
599623
this.entityName = "CLIENTIDENTIFIER";

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,13 @@ private WorkingCapitalLoanConstants() {
4646
// Loan commands
4747
public static final String APPROVE_LOAN_COMMAND = "approve";
4848
public static final String DISBURSE_LOAN_COMMAND = "disburse";
49+
50+
// Approval / Rejection / Undo-approval parameters
51+
public static final String RESOURCE_NAME = WCL_RESOURCE_NAME;
52+
public static final String approvedOnDateParamName = "approvedOnDate";
53+
public static final String approvedLoanAmountParamName = "approvedLoanAmount";
54+
public static final String expectedDisbursementDateParamName = "expectedDisbursementDate";
55+
public static final String discountAmountParamName = "discountAmount";
56+
public static final String noteParamName = "note";
57+
public static final String rejectedOnDateParamName = "rejectedOnDate";
4958
}

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
import org.apache.fineract.infrastructure.core.api.jersey.Pagination;
4444
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
4545
import org.apache.fineract.infrastructure.core.domain.ExternalId;
46+
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
47+
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
4648
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
4749
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
4850
import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants;
@@ -190,6 +192,36 @@ public CommandProcessingResult deleteLoanApplication(
190192
return deleteLoanApplication(null, loanExternalId);
191193
}
192194

195+
@POST
196+
@Path("{loanId}")
197+
@Consumes({ MediaType.APPLICATION_JSON })
198+
@Produces({ MediaType.APPLICATION_JSON })
199+
@Operation(operationId = "stateTransitionWorkingCapitalLoanById", summary = "Approve/Reject/Undo-approve a Working Capital Loan", description = "Mandatory command query parameter: approve, reject, or undoapproval.")
200+
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.PostWorkingCapitalLoansLoanIdRequest.class)))
201+
@ApiResponses({
202+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.PostWorkingCapitalLoansLoanIdResponse.class))) })
203+
public CommandProcessingResult stateTransitionById(
204+
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
205+
@QueryParam("command") @Parameter(description = "command", required = true) final String commandParam,
206+
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
207+
return handleStateTransition(loanId, null, commandParam, apiRequestBodyAsJson);
208+
}
209+
210+
@POST
211+
@Path("external-id/{loanExternalId}")
212+
@Consumes({ MediaType.APPLICATION_JSON })
213+
@Produces({ MediaType.APPLICATION_JSON })
214+
@Operation(operationId = "stateTransitionWorkingCapitalLoanByExternalId", summary = "Approve/Reject/Undo-approve a Working Capital Loan by external id", description = "Mandatory command query parameter: approve, reject, or undoapproval.")
215+
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.PostWorkingCapitalLoansLoanIdRequest.class)))
216+
@ApiResponses({
217+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.PostWorkingCapitalLoansLoanIdResponse.class))) })
218+
public CommandProcessingResult stateTransitionByExternalId(
219+
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
220+
@QueryParam("command") @Parameter(description = "command", required = true) final String commandParam,
221+
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
222+
return handleStateTransition(null, loanExternalId, commandParam, apiRequestBodyAsJson);
223+
}
224+
193225
private CommandProcessingResult modifyLoanApplication(final Long loanId, final String loanExternalIdStr,
194226
final String apiRequestBodyAsJson) {
195227
final Long resolvedLoanId = loanId != null ? loanId
@@ -212,4 +244,29 @@ private CommandProcessingResult deleteLoanApplication(final Long loanId, final S
212244
.build();
213245
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
214246
}
247+
248+
private CommandProcessingResult handleStateTransition(final Long loanId, final String loanExternalIdStr, final String commandParam,
249+
final String apiRequestBodyAsJson) {
250+
final Long resolvedLoanId = loanId != null ? loanId
251+
: readPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalIdStr));
252+
if (resolvedLoanId == null) {
253+
throw new WorkingCapitalLoanNotFoundException(ExternalIdFactory.produce(loanExternalIdStr));
254+
}
255+
256+
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
257+
CommandWrapper commandRequest = null;
258+
if (CommandParameterUtil.is(commandParam, "approve")) {
259+
commandRequest = builder.approveWorkingCapitalLoanApplication(resolvedLoanId).build();
260+
} else if (CommandParameterUtil.is(commandParam, "reject")) {
261+
commandRequest = builder.rejectWorkingCapitalLoanApplication(resolvedLoanId).build();
262+
} else if (CommandParameterUtil.is(commandParam, "undoapproval")) {
263+
commandRequest = builder.undoWorkingCapitalLoanApplicationApproval(resolvedLoanId).build();
264+
}
265+
266+
if (commandRequest == null) {
267+
throw new UnrecognizedQueryParamException("command", commandParam);
268+
}
269+
270+
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
271+
}
215272
}

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,4 +397,45 @@ private DeleteWorkingCapitalLoansLoanIdResponse() {}
397397
@Schema(example = "1")
398398
public Long resourceId;
399399
}
400+
401+
@Schema(description = "PostWorkingCapitalLoansLoanIdResponse")
402+
public static final class PostWorkingCapitalLoansLoanIdResponse {
403+
404+
private PostWorkingCapitalLoansLoanIdResponse() {}
405+
406+
@Schema(example = "2")
407+
public Long officeId;
408+
@Schema(example = "6")
409+
public Long clientId;
410+
@Schema(example = "3")
411+
public Long loanId;
412+
@Schema(example = "3")
413+
public Long resourceId;
414+
@Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7")
415+
public String resourceExternalId;
416+
public Object changes;
417+
}
418+
419+
@Schema(description = "PostWorkingCapitalLoansLoanIdRequest")
420+
public static final class PostWorkingCapitalLoansLoanIdRequest {
421+
422+
private PostWorkingCapitalLoansLoanIdRequest() {}
423+
424+
@Schema(example = "15 January 2024", description = "Date of approval")
425+
public String approvedOnDate;
426+
@Schema(example = "10000.00", description = "Approved principal amount (optional, defaults to proposed principal)")
427+
public BigDecimal approvedLoanAmount;
428+
@Schema(example = "1 February 2024", description = "Expected disbursement date")
429+
public String expectedDisbursementDate;
430+
@Schema(example = "0.0", description = "Discount amount (cannot exceed creation-time discount)")
431+
public BigDecimal discountAmount;
432+
@Schema(example = "15 January 2024", description = "Date of rejection")
433+
public String rejectedOnDate;
434+
@Schema(example = "Approval/Rejection note")
435+
public String note;
436+
@Schema(example = "en_GB")
437+
public String locale;
438+
@Schema(example = "dd MMMM yyyy")
439+
public String dateFormat;
440+
}
400441
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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.domain;
20+
21+
public enum WorkingCapitalLoanEvent {
22+
23+
LOAN_APPROVED, //
24+
LOAN_APPROVAL_UNDO, //
25+
LOAN_REJECTED //
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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.domain;
20+
21+
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
22+
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
23+
import org.springframework.stereotype.Component;
24+
25+
@Component
26+
public class WorkingCapitalLoanLifecycleStateMachine {
27+
28+
public void transition(final WorkingCapitalLoanEvent event, final WorkingCapitalLoan loan) {
29+
LoanStatus newStatus = getNextStatus(event, loan);
30+
if (newStatus != null) {
31+
loan.setLoanStatus(newStatus);
32+
} else {
33+
throw new PlatformApiDataValidationException("validation.msg.wc.loan.transition.not.allowed",
34+
"Transition " + event + " is not allowed from status " + loan.getLoanStatus(), "loanStatus");
35+
}
36+
}
37+
38+
private LoanStatus getNextStatus(final WorkingCapitalLoanEvent event, final WorkingCapitalLoan loan) {
39+
LoanStatus from = loan.getLoanStatus();
40+
if (from == null) {
41+
return null;
42+
}
43+
44+
return switch (event) {
45+
case LOAN_APPROVED -> from.isSubmittedAndPendingApproval() ? LoanStatus.APPROVED : null;
46+
case LOAN_APPROVAL_UNDO -> from.isApproved() ? LoanStatus.SUBMITTED_AND_PENDING_APPROVAL : null;
47+
case LOAN_REJECTED -> from.isSubmittedAndPendingApproval() ? LoanStatus.REJECTED : null;
48+
};
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.handler;
20+
21+
import lombok.RequiredArgsConstructor;
22+
import org.apache.fineract.commands.annotation.CommandType;
23+
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
24+
import org.apache.fineract.infrastructure.core.api.JsonCommand;
25+
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
26+
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanWritePlatformService;
27+
import org.springframework.stereotype.Service;
28+
import org.springframework.transaction.annotation.Transactional;
29+
30+
@Service
31+
@RequiredArgsConstructor
32+
@CommandType(entity = "WORKINGCAPITALLOAN", action = "APPROVE")
33+
public class ApproveWorkingCapitalLoanCommandHandler implements NewCommandSourceHandler {
34+
35+
private final WorkingCapitalLoanWritePlatformService writePlatformService;
36+
37+
@Transactional
38+
@Override
39+
public CommandProcessingResult processCommand(final JsonCommand command) {
40+
return this.writePlatformService.approveApplication(command.entityId(), command);
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.handler;
20+
21+
import lombok.RequiredArgsConstructor;
22+
import org.apache.fineract.commands.annotation.CommandType;
23+
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
24+
import org.apache.fineract.infrastructure.core.api.JsonCommand;
25+
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
26+
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanWritePlatformService;
27+
import org.springframework.stereotype.Service;
28+
import org.springframework.transaction.annotation.Transactional;
29+
30+
@Service
31+
@RequiredArgsConstructor
32+
@CommandType(entity = "WORKINGCAPITALLOAN", action = "REJECT")
33+
public class RejectWorkingCapitalLoanCommandHandler implements NewCommandSourceHandler {
34+
35+
private final WorkingCapitalLoanWritePlatformService writePlatformService;
36+
37+
@Transactional
38+
@Override
39+
public CommandProcessingResult processCommand(final JsonCommand command) {
40+
return this.writePlatformService.rejectApplication(command.entityId(), command);
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.handler;
20+
21+
import lombok.RequiredArgsConstructor;
22+
import org.apache.fineract.commands.annotation.CommandType;
23+
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
24+
import org.apache.fineract.infrastructure.core.api.JsonCommand;
25+
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
26+
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanWritePlatformService;
27+
import org.springframework.stereotype.Service;
28+
import org.springframework.transaction.annotation.Transactional;
29+
30+
@Service
31+
@RequiredArgsConstructor
32+
@CommandType(entity = "WORKINGCAPITALLOAN", action = "APPROVALUNDO")
33+
public class UndoApproveWorkingCapitalLoanCommandHandler implements NewCommandSourceHandler {
34+
35+
private final WorkingCapitalLoanWritePlatformService writePlatformService;
36+
37+
@Transactional
38+
@Override
39+
public CommandProcessingResult processCommand(final JsonCommand command) {
40+
return this.writePlatformService.undoApplicationApproval(command.entityId(), command);
41+
}
42+
}

0 commit comments

Comments
 (0)