This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 9dad940236 FINERACT-2455: working capital loan approval/rejection
9dad940236 is described below

commit 9dad940236eb9c3f5f180bff9b3fff224620077e
Author: Attila Budai <[email protected]>
AuthorDate: Thu Mar 5 15:32:09 2026 +0100

    FINERACT-2455: working capital loan approval/rejection
---
 .../commands/service/CommandWrapperBuilder.java    |  24 ++
 .../WorkingCapitalLoanConstants.java               |   9 +
 .../api/WorkingCapitalLoanApiResource.java         |  57 +++
 .../api/WorkingCapitalLoanApiResourceSwagger.java  |  41 ++
 .../domain/WorkingCapitalLoanEvent.java            |  26 ++
 .../WorkingCapitalLoanLifecycleStateMachine.java   |  50 +++
 .../ApproveWorkingCapitalLoanCommandHandler.java   |  42 ++
 .../RejectWorkingCapitalLoanCommandHandler.java    |  42 ++
 ...ndoApproveWorkingCapitalLoanCommandHandler.java |  42 ++
 .../WorkingCapitalLoanDataValidator.java           | 183 ++++++++
 .../service/WorkingCapitalLoanAssemblerImpl.java   |   4 +-
 .../WorkingCapitalLoanWritePlatformService.java    |  31 ++
 ...WorkingCapitalLoanWritePlatformServiceImpl.java | 190 ++++++++
 .../workingcapitalloan/module-changelog-master.xml |   1 +
 .../parts/0010_loan_account_permissions.xml        |  71 +++
 .../WorkingCapitalLoanApprovalRejectionTest.java   | 479 +++++++++++++++++++++
 .../WorkingCapitalLoanApplicationHelper.java       |  47 ++
 .../WorkingCapitalLoanApplicationTestBuilder.java  |  42 ++
 18 files changed, 1379 insertions(+), 2 deletions(-)

diff --git 
a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
 
b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
index a25e595b0c..b7cd3cd6f4 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
@@ -594,6 +594,30 @@ public class CommandWrapperBuilder {
         return this;
     }
 
+    public CommandWrapperBuilder approveWorkingCapitalLoanApplication(final 
Long loanId) {
+        this.actionName = "APPROVE";
+        this.entityName = "WORKINGCAPITALLOAN";
+        this.entityId = loanId;
+        this.href = "/workingcapitalloans/" + loanId;
+        return this;
+    }
+
+    public CommandWrapperBuilder rejectWorkingCapitalLoanApplication(final 
Long loanId) {
+        this.actionName = "REJECT";
+        this.entityName = "WORKINGCAPITALLOAN";
+        this.entityId = loanId;
+        this.href = "/workingcapitalloans/" + loanId;
+        return this;
+    }
+
+    public CommandWrapperBuilder 
undoWorkingCapitalLoanApplicationApproval(final Long loanId) {
+        this.actionName = "APPROVALUNDO";
+        this.entityName = "WORKINGCAPITALLOAN";
+        this.entityId = loanId;
+        this.href = "/workingcapitalloans/" + loanId;
+        return this;
+    }
+
     public CommandWrapperBuilder createClientIdentifier(final Long clientId) {
         this.actionName = "CREATE";
         this.entityName = "CLIENTIDENTIFIER";
diff --git 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java
 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java
index 9bc368b2cc..2b96ea695d 100644
--- 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java
+++ 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java
@@ -46,4 +46,13 @@ public final class WorkingCapitalLoanConstants {
     // Loan commands
     public static final String APPROVE_LOAN_COMMAND = "approve";
     public static final String DISBURSE_LOAN_COMMAND = "disburse";
+
+    // Approval / Rejection / Undo-approval parameters
+    public static final String RESOURCE_NAME = WCL_RESOURCE_NAME;
+    public static final String approvedOnDateParamName = "approvedOnDate";
+    public static final String approvedLoanAmountParamName = 
"approvedLoanAmount";
+    public static final String expectedDisbursementDateParamName = 
"expectedDisbursementDate";
+    public static final String discountAmountParamName = "discountAmount";
+    public static final String noteParamName = "note";
+    public static final String rejectedOnDateParamName = "rejectedOnDate";
 }
diff --git 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResource.java
 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResource.java
index 1b18646567..68cb75af5e 100644
--- 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResource.java
+++ 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResource.java
@@ -43,6 +43,8 @@ import 
org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformS
 import org.apache.fineract.infrastructure.core.api.jersey.Pagination;
 import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
 import org.apache.fineract.infrastructure.core.domain.ExternalId;
+import 
org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
+import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
 import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
 import 
org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
 import 
org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants;
@@ -190,6 +192,36 @@ public class WorkingCapitalLoanApiResource {
         return deleteLoanApplication(null, loanExternalId);
     }
 
+    @POST
+    @Path("{loanId}")
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Operation(operationId = "stateTransitionWorkingCapitalLoanById", summary 
= "Approve/Reject/Undo-approve a Working Capital Loan", description = 
"Mandatory command query parameter: approve, reject, or undoapproval.")
+    @RequestBody(required = true, content = @Content(schema = 
@Schema(implementation = 
WorkingCapitalLoanApiResourceSwagger.PostWorkingCapitalLoansLoanIdRequest.class)))
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "OK", content = 
@Content(schema = @Schema(implementation = 
WorkingCapitalLoanApiResourceSwagger.PostWorkingCapitalLoansLoanIdResponse.class)))
 })
+    public CommandProcessingResult stateTransitionById(
+            @PathParam("loanId") @Parameter(description = "loanId", required = 
true) final Long loanId,
+            @QueryParam("command") @Parameter(description = "command", 
required = true) final String commandParam,
+            @Parameter(hidden = true) final String apiRequestBodyAsJson) {
+        return handleStateTransition(loanId, null, commandParam, 
apiRequestBodyAsJson);
+    }
+
+    @POST
+    @Path("external-id/{loanExternalId}")
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Operation(operationId = "stateTransitionWorkingCapitalLoanByExternalId", 
summary = "Approve/Reject/Undo-approve a Working Capital Loan by external id", 
description = "Mandatory command query parameter: approve, reject, or 
undoapproval.")
+    @RequestBody(required = true, content = @Content(schema = 
@Schema(implementation = 
WorkingCapitalLoanApiResourceSwagger.PostWorkingCapitalLoansLoanIdRequest.class)))
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "OK", content = 
@Content(schema = @Schema(implementation = 
WorkingCapitalLoanApiResourceSwagger.PostWorkingCapitalLoansLoanIdResponse.class)))
 })
+    public CommandProcessingResult stateTransitionByExternalId(
+            @PathParam("loanExternalId") @Parameter(description = 
"loanExternalId", required = true) final String loanExternalId,
+            @QueryParam("command") @Parameter(description = "command", 
required = true) final String commandParam,
+            @Parameter(hidden = true) final String apiRequestBodyAsJson) {
+        return handleStateTransition(null, loanExternalId, commandParam, 
apiRequestBodyAsJson);
+    }
+
     private CommandProcessingResult modifyLoanApplication(final Long loanId, 
final String loanExternalIdStr,
             final String apiRequestBodyAsJson) {
         final Long resolvedLoanId = loanId != null ? loanId
@@ -212,4 +244,29 @@ public class WorkingCapitalLoanApiResource {
                 .build();
         return 
this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
     }
+
+    private CommandProcessingResult handleStateTransition(final Long loanId, 
final String loanExternalIdStr, final String commandParam,
+            final String apiRequestBodyAsJson) {
+        final Long resolvedLoanId = loanId != null ? loanId
+                : 
readPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalIdStr));
+        if (resolvedLoanId == null) {
+            throw new 
WorkingCapitalLoanNotFoundException(ExternalIdFactory.produce(loanExternalIdStr));
+        }
+
+        final CommandWrapperBuilder builder = new 
CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
+        CommandWrapper commandRequest = null;
+        if (CommandParameterUtil.is(commandParam, "approve")) {
+            commandRequest = 
builder.approveWorkingCapitalLoanApplication(resolvedLoanId).build();
+        } else if (CommandParameterUtil.is(commandParam, "reject")) {
+            commandRequest = 
builder.rejectWorkingCapitalLoanApplication(resolvedLoanId).build();
+        } else if (CommandParameterUtil.is(commandParam, "undoapproval")) {
+            commandRequest = 
builder.undoWorkingCapitalLoanApplicationApproval(resolvedLoanId).build();
+        }
+
+        if (commandRequest == null) {
+            throw new UnrecognizedQueryParamException("command", commandParam);
+        }
+
+        return 
this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
+    }
 }
diff --git 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java
 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java
index 881a9881f1..18b987b278 100644
--- 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java
+++ 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java
@@ -397,4 +397,45 @@ public final class WorkingCapitalLoanApiResourceSwagger {
         @Schema(example = "1")
         public Long resourceId;
     }
+
+    @Schema(description = "PostWorkingCapitalLoansLoanIdResponse")
+    public static final class PostWorkingCapitalLoansLoanIdResponse {
+
+        private PostWorkingCapitalLoansLoanIdResponse() {}
+
+        @Schema(example = "2")
+        public Long officeId;
+        @Schema(example = "6")
+        public Long clientId;
+        @Schema(example = "3")
+        public Long loanId;
+        @Schema(example = "3")
+        public Long resourceId;
+        @Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7")
+        public String resourceExternalId;
+        public Object changes;
+    }
+
+    @Schema(description = "PostWorkingCapitalLoansLoanIdRequest")
+    public static final class PostWorkingCapitalLoansLoanIdRequest {
+
+        private PostWorkingCapitalLoansLoanIdRequest() {}
+
+        @Schema(example = "15 January 2024", description = "Date of approval")
+        public String approvedOnDate;
+        @Schema(example = "10000.00", description = "Approved principal amount 
(optional, defaults to proposed principal)")
+        public BigDecimal approvedLoanAmount;
+        @Schema(example = "1 February 2024", description = "Expected 
disbursement date")
+        public String expectedDisbursementDate;
+        @Schema(example = "0.0", description = "Discount amount (cannot exceed 
creation-time discount)")
+        public BigDecimal discountAmount;
+        @Schema(example = "15 January 2024", description = "Date of rejection")
+        public String rejectedOnDate;
+        @Schema(example = "Approval/Rejection note")
+        public String note;
+        @Schema(example = "en_GB")
+        public String locale;
+        @Schema(example = "dd MMMM yyyy")
+        public String dateFormat;
+    }
 }
diff --git 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanEvent.java
 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanEvent.java
new file mode 100644
index 0000000000..58c398ab15
--- /dev/null
+++ 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanEvent.java
@@ -0,0 +1,26 @@
+/**
+ * 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.domain;
+
+public enum WorkingCapitalLoanEvent {
+
+    LOAN_APPROVED, //
+    LOAN_APPROVAL_UNDO, //
+    LOAN_REJECTED //
+}
diff --git 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanLifecycleStateMachine.java
 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanLifecycleStateMachine.java
new file mode 100644
index 0000000000..0d7bae6488
--- /dev/null
+++ 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanLifecycleStateMachine.java
@@ -0,0 +1,50 @@
+/**
+ * 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.domain;
+
+import 
org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
+import org.springframework.stereotype.Component;
+
+@Component
+public class WorkingCapitalLoanLifecycleStateMachine {
+
+    public void transition(final WorkingCapitalLoanEvent event, final 
WorkingCapitalLoan loan) {
+        LoanStatus newStatus = getNextStatus(event, loan);
+        if (newStatus != null) {
+            loan.setLoanStatus(newStatus);
+        } else {
+            throw new 
PlatformApiDataValidationException("validation.msg.wc.loan.transition.not.allowed",
+                    "Transition " + event + " is not allowed from status " + 
loan.getLoanStatus(), "loanStatus");
+        }
+    }
+
+    private LoanStatus getNextStatus(final WorkingCapitalLoanEvent event, 
final WorkingCapitalLoan loan) {
+        LoanStatus from = loan.getLoanStatus();
+        if (from == null) {
+            return null;
+        }
+
+        return switch (event) {
+            case LOAN_APPROVED -> from.isSubmittedAndPendingApproval() ? 
LoanStatus.APPROVED : null;
+            case LOAN_APPROVAL_UNDO -> from.isApproved() ? 
LoanStatus.SUBMITTED_AND_PENDING_APPROVAL : null;
+            case LOAN_REJECTED -> from.isSubmittedAndPendingApproval() ? 
LoanStatus.REJECTED : null;
+        };
+    }
+}
diff --git 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/ApproveWorkingCapitalLoanCommandHandler.java
 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/ApproveWorkingCapitalLoanCommandHandler.java
new file mode 100644
index 0000000000..ca08d91218
--- /dev/null
+++ 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/ApproveWorkingCapitalLoanCommandHandler.java
@@ -0,0 +1,42 @@
+/**
+ * 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.handler;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import 
org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanWritePlatformService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@CommandType(entity = "WORKINGCAPITALLOAN", action = "APPROVE")
+public class ApproveWorkingCapitalLoanCommandHandler implements 
NewCommandSourceHandler {
+
+    private final WorkingCapitalLoanWritePlatformService writePlatformService;
+
+    @Transactional
+    @Override
+    public CommandProcessingResult processCommand(final JsonCommand command) {
+        return 
this.writePlatformService.approveApplication(command.entityId(), command);
+    }
+}
diff --git 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/RejectWorkingCapitalLoanCommandHandler.java
 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/RejectWorkingCapitalLoanCommandHandler.java
new file mode 100644
index 0000000000..4573053f9c
--- /dev/null
+++ 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/RejectWorkingCapitalLoanCommandHandler.java
@@ -0,0 +1,42 @@
+/**
+ * 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.handler;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import 
org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanWritePlatformService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@CommandType(entity = "WORKINGCAPITALLOAN", action = "REJECT")
+public class RejectWorkingCapitalLoanCommandHandler implements 
NewCommandSourceHandler {
+
+    private final WorkingCapitalLoanWritePlatformService writePlatformService;
+
+    @Transactional
+    @Override
+    public CommandProcessingResult processCommand(final JsonCommand command) {
+        return this.writePlatformService.rejectApplication(command.entityId(), 
command);
+    }
+}
diff --git 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/UndoApproveWorkingCapitalLoanCommandHandler.java
 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/UndoApproveWorkingCapitalLoanCommandHandler.java
new file mode 100644
index 0000000000..d1bd9d0162
--- /dev/null
+++ 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/UndoApproveWorkingCapitalLoanCommandHandler.java
@@ -0,0 +1,42 @@
+/**
+ * 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.handler;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import 
org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanWritePlatformService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@CommandType(entity = "WORKINGCAPITALLOAN", action = "APPROVALUNDO")
+public class UndoApproveWorkingCapitalLoanCommandHandler implements 
NewCommandSourceHandler {
+
+    private final WorkingCapitalLoanWritePlatformService writePlatformService;
+
+    @Transactional
+    @Override
+    public CommandProcessingResult processCommand(final JsonCommand command) {
+        return 
this.writePlatformService.undoApplicationApproval(command.entityId(), command);
+    }
+}
diff --git 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java
 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java
new file mode 100644
index 0000000000..ea926f23e5
--- /dev/null
+++ 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java
@@ -0,0 +1,183 @@
+/**
+ * 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.serialization;
+
+import com.google.gson.JsonElement;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.infrastructure.core.data.ApiParameterError;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
+import 
org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import 
org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants;
+import 
org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class WorkingCapitalLoanDataValidator {
+
+    private final FromJsonHelper fromApiJsonHelper;
+
+    // Per requirement: only principal, discount, approved date, expected 
disbursement date, and notes
+    private static final Set<String> APPROVAL_SUPPORTED_PARAMETERS = new 
HashSet<>(
+            Arrays.asList("locale", "dateFormat", 
WorkingCapitalLoanConstants.approvedOnDateParamName,
+                    WorkingCapitalLoanConstants.approvedLoanAmountParamName, 
WorkingCapitalLoanConstants.expectedDisbursementDateParamName,
+                    WorkingCapitalLoanConstants.discountAmountParamName, 
WorkingCapitalLoanConstants.noteParamName));
+
+    private static final Set<String> REJECTION_SUPPORTED_PARAMETERS = new 
HashSet<>(Arrays.asList("locale", "dateFormat",
+            WorkingCapitalLoanConstants.rejectedOnDateParamName, 
WorkingCapitalLoanConstants.noteParamName));
+
+    private static final Set<String> UNDO_APPROVAL_SUPPORTED_PARAMETERS = new 
HashSet<>(
+            Arrays.asList("locale", "dateFormat", 
WorkingCapitalLoanConstants.noteParamName));
+
+    public void validateApproval(final String json, final WorkingCapitalLoan 
loan) {
+        if (StringUtils.isBlank(json)) {
+            throw new InvalidJsonException();
+        }
+
+        final Type typeOfMap = new TypeToken<Map<String, Object>>() 
{}.getType();
+        this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, 
APPROVAL_SUPPORTED_PARAMETERS);
+
+        final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
+        final DataValidatorBuilder baseDataValidator = new 
DataValidatorBuilder(dataValidationErrors)
+                .resource(WorkingCapitalLoanConstants.RESOURCE_NAME);
+
+        final JsonElement element = this.fromApiJsonHelper.parse(json);
+
+        // approvedOnDate is mandatory
+        final LocalDate approvedOnDate = 
this.fromApiJsonHelper.extractLocalDateNamed(WorkingCapitalLoanConstants.approvedOnDateParamName,
+                element);
+        
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.approvedOnDateParamName).value(approvedOnDate).notNull();
+
+        if (approvedOnDate != null) {
+            if (DateUtils.isDateInTheFuture(approvedOnDate)) {
+                
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.approvedOnDateParamName)
+                        .failWithCode("cannot.be.a.future.date");
+            }
+
+            if (loan.getSubmittedOnDate() != null && 
DateUtils.isBefore(approvedOnDate, loan.getSubmittedOnDate())) {
+                
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.approvedOnDateParamName)
+                        .failWithCode("cannot.be.before.submittal.date");
+            }
+        }
+
+        // approvedLoanAmount must be positive and <= proposedPrincipal
+        if 
(this.fromApiJsonHelper.parameterExists(WorkingCapitalLoanConstants.approvedLoanAmountParamName,
 element)) {
+            final BigDecimal approvedLoanAmount = this.fromApiJsonHelper
+                    
.extractBigDecimalNamed(WorkingCapitalLoanConstants.approvedLoanAmountParamName,
 element, new HashSet<>());
+            
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.approvedLoanAmountParamName).value(approvedLoanAmount)
+                    .ignoreIfNull().positiveAmount();
+
+            if (approvedLoanAmount != null && loan.getProposedPrincipal() != 
null
+                    && 
approvedLoanAmount.compareTo(loan.getProposedPrincipal()) > 0) {
+                
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.approvedLoanAmountParamName)
+                        
.failWithCode("amount.cannot.exceed.proposed.principal");
+            }
+        }
+
+        // expectedDisbursementDate is mandatory
+        final LocalDate expectedDisbursementDate = this.fromApiJsonHelper
+                
.extractLocalDateNamed(WorkingCapitalLoanConstants.expectedDisbursementDateParamName,
 element);
+        
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.expectedDisbursementDateParamName).value(expectedDisbursementDate)
+                .notNull();
+        if (expectedDisbursementDate != null && approvedOnDate != null && 
DateUtils.isBefore(expectedDisbursementDate, approvedOnDate)) {
+            
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.expectedDisbursementDateParamName)
+                    .failWithCode("cannot.be.before.approval.date");
+        }
+
+        // discountAmount must be >= 0 and <= current (creation-time) discount
+        if 
(this.fromApiJsonHelper.parameterExists(WorkingCapitalLoanConstants.discountAmountParamName,
 element)) {
+            final BigDecimal discountAmount = this.fromApiJsonHelper
+                    
.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName, 
element, new HashSet<>());
+            
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName).value(discountAmount).ignoreIfNull()
+                    .zeroOrPositiveAmount();
+
+            final BigDecimal currentDiscount = 
loan.getLoanProductRelatedDetails() != null
+                    ? loan.getLoanProductRelatedDetails().getDiscount()
+                    : null;
+            if (discountAmount != null && currentDiscount != null && 
discountAmount.compareTo(currentDiscount) > 0) {
+                
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName)
+                        .failWithCode("amount.cannot.exceed.created.discount");
+            }
+        }
+
+        throwExceptionIfValidationWarningsExist(dataValidationErrors);
+    }
+
+    public void validateRejection(final String json, final WorkingCapitalLoan 
loan) {
+        if (StringUtils.isBlank(json)) {
+            throw new InvalidJsonException();
+        }
+
+        final Type typeOfMap = new TypeToken<Map<String, Object>>() 
{}.getType();
+        this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, 
REJECTION_SUPPORTED_PARAMETERS);
+
+        final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
+        final DataValidatorBuilder baseDataValidator = new 
DataValidatorBuilder(dataValidationErrors)
+                .resource(WorkingCapitalLoanConstants.RESOURCE_NAME);
+
+        final JsonElement element = this.fromApiJsonHelper.parse(json);
+
+        final LocalDate rejectedOnDate = 
this.fromApiJsonHelper.extractLocalDateNamed(WorkingCapitalLoanConstants.rejectedOnDateParamName,
+                element);
+        
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.rejectedOnDateParamName).value(rejectedOnDate).notNull();
+
+        if (rejectedOnDate != null) {
+            if (DateUtils.isDateInTheFuture(rejectedOnDate)) {
+                
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.rejectedOnDateParamName)
+                        .failWithCode("cannot.be.a.future.date");
+            }
+
+            if (loan.getSubmittedOnDate() != null && 
DateUtils.isBefore(rejectedOnDate, loan.getSubmittedOnDate())) {
+                
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.rejectedOnDateParamName)
+                        .failWithCode("cannot.be.before.submittal.date");
+            }
+        }
+
+        throwExceptionIfValidationWarningsExist(dataValidationErrors);
+    }
+
+    public void validateUndoApproval(final String json) {
+        if (StringUtils.isBlank(json)) {
+            return;
+        }
+
+        final Type typeOfMap = new TypeToken<Map<String, Object>>() 
{}.getType();
+        this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, 
UNDO_APPROVAL_SUPPORTED_PARAMETERS);
+    }
+
+    private void throwExceptionIfValidationWarningsExist(final 
List<ApiParameterError> dataValidationErrors) {
+        if (!dataValidationErrors.isEmpty()) {
+            throw new PlatformApiDataValidationException(dataValidationErrors);
+        }
+    }
+}
diff --git 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAssemblerImpl.java
 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAssemblerImpl.java
index 10ada48d13..8a5f96c78f 100644
--- 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAssemblerImpl.java
+++ 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAssemblerImpl.java
@@ -129,7 +129,7 @@ public class WorkingCapitalLoanAssemblerImpl implements 
WorkingCapitalLoanAssemb
             loan.getDisbursementDetails().add(detail);
         }
         loan.setProposedPrincipal(principal);
-        loan.setApprovedPrincipal(principal);
+        loan.setApprovedPrincipal(BigDecimal.ZERO);
         final WorkingCapitalLoanBalance balance = 
WorkingCapitalLoanBalance.createFor(loan);
         balance.setPrincipalOutstanding(principal != null ? principal : 
BigDecimal.ZERO);
         balance.setTotalPayment(totalPayment != null ? totalPayment : 
BigDecimal.ZERO);
@@ -249,7 +249,7 @@ public class WorkingCapitalLoanAssemblerImpl implements 
WorkingCapitalLoanAssemb
             final BigDecimal principal = fromApiJsonHelper
                     
.extractBigDecimalWithLocaleNamed(WorkingCapitalLoanConstants.principalAmountParamName,
 element);
             loan.setProposedPrincipal(principal);
-            loan.setApprovedPrincipal(principal);
+            loan.setApprovedPrincipal(BigDecimal.ZERO);
             ensureBalance(loan).setPrincipalOutstanding(principal != null ? 
principal : BigDecimal.ZERO);
             changes.put(WorkingCapitalLoanConstants.principalAmountParamName, 
principal);
         }
diff --git 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java
 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java
new file mode 100644
index 0000000000..5b22ea9a73
--- /dev/null
+++ 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java
@@ -0,0 +1,31 @@
+/**
+ * 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.service;
+
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+
+public interface WorkingCapitalLoanWritePlatformService {
+
+    CommandProcessingResult approveApplication(Long loanId, JsonCommand 
command);
+
+    CommandProcessingResult undoApplicationApproval(Long loanId, JsonCommand 
command);
+
+    CommandProcessingResult rejectApplication(Long loanId, JsonCommand 
command);
+}
diff --git 
a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java
 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java
new file mode 100644
index 0000000000..a93cd5cac5
--- /dev/null
+++ 
b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java
@@ -0,0 +1,190 @@
+/**
+ * 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.service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import 
org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import 
org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import 
org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants;
+import 
org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan;
+import 
org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanEvent;
+import 
org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanLifecycleStateMachine;
+import 
org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanNote;
+import 
org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException;
+import 
org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanNoteRepository;
+import 
org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository;
+import 
org.apache.fineract.portfolio.workingcapitalloan.serialization.WorkingCapitalLoanDataValidator;
+import 
org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoanProduct;
+import 
org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoanProductRelatedDetail;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class WorkingCapitalLoanWritePlatformServiceImpl implements 
WorkingCapitalLoanWritePlatformService {
+
+    private final PlatformSecurityContext context;
+    private final WorkingCapitalLoanRepository loanRepository;
+    private final WorkingCapitalLoanDataValidator validator;
+    private final WorkingCapitalLoanLifecycleStateMachine stateMachine;
+    private final FromJsonHelper fromApiJsonHelper;
+    private final WorkingCapitalLoanNoteRepository noteRepository;
+
+    @Override
+    public CommandProcessingResult approveApplication(final Long loanId, final 
JsonCommand command) {
+        final WorkingCapitalLoan loan = this.loanRepository.findById(loanId)
+                .orElseThrow(() -> new 
WorkingCapitalLoanNotFoundException(loanId));
+
+        this.validator.validateApproval(command.json(), loan);
+
+        final AppUser currentUser = this.context.authenticatedUser();
+
+        this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_APPROVED, 
loan);
+
+        // Approved date
+        final LocalDate approvedOnDate = 
command.localDateValueOfParameterNamed(WorkingCapitalLoanConstants.approvedOnDateParamName);
+        loan.setApprovedOnDate(approvedOnDate);
+        loan.setApprovedBy(currentUser);
+
+        // Principal amount (optional, defaults to proposed)
+        if 
(command.parameterExists(WorkingCapitalLoanConstants.approvedLoanAmountParamName))
 {
+            final BigDecimal approvedAmount = this.fromApiJsonHelper
+                    
.extractBigDecimalNamed(WorkingCapitalLoanConstants.approvedLoanAmountParamName,
 command.parsedJson(), new HashSet<>());
+            if (approvedAmount != null) {
+                loan.setApprovedPrincipal(approvedAmount);
+            }
+        }
+        if (loan.getApprovedPrincipal() == null) {
+            loan.setApprovedPrincipal(loan.getProposedPrincipal());
+        }
+
+        // Expected disbursement date (mandatory, validated)
+        final LocalDate expectedDisbursementDate = command
+                
.localDateValueOfParameterNamed(WorkingCapitalLoanConstants.expectedDisbursementDateParamName);
+        if (expectedDisbursementDate != null && 
!loan.getDisbursementDetails().isEmpty()) {
+            
loan.getDisbursementDetails().getFirst().setExpectedDisbursementDate(expectedDisbursementDate);
+        }
+
+        // Discount amount (optional, can only be reduced per requirement)
+        if 
(command.parameterExists(WorkingCapitalLoanConstants.discountAmountParamName)) {
+            final BigDecimal discount = 
this.fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName,
+                    command.parsedJson(), new HashSet<>());
+            if (discount != null) {
+                loan.getLoanProductRelatedDetails().setDiscount(discount);
+            }
+        }
+
+        this.loanRepository.saveAndFlush(loan);
+
+        
createNote(command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName),
 loan);
+
+        final Map<String, Object> changes = new LinkedHashMap<>();
+        changes.put(WorkingCapitalLoanConstants.approvedOnDateParamName, 
approvedOnDate);
+        changes.put("status", loan.getLoanStatus());
+
+        log.debug("Working capital loan {} approved by user {}", loanId, 
currentUser.getId());
+
+        return new 
CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loanId)
+                
.withEntityExternalId(loan.getExternalId()).withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId())
+                .withLoanId(loanId).with(changes).build();
+    }
+
+    @Override
+    public CommandProcessingResult undoApplicationApproval(final Long loanId, 
final JsonCommand command) {
+        final WorkingCapitalLoan loan = this.loanRepository.findById(loanId)
+                .orElseThrow(() -> new 
WorkingCapitalLoanNotFoundException(loanId));
+
+        this.validator.validateUndoApproval(command.json());
+
+        
this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_APPROVAL_UNDO, loan);
+
+        loan.setApprovedOnDate(null);
+        loan.setApprovedBy(null);
+        loan.setApprovedPrincipal(BigDecimal.ZERO);
+
+        // Reset discount to product default.
+        // Note: if discount was customized at submission time, it resets to 
product default,
+        // not the submission-time value, because we don't store a 
pre-approval snapshot.
+        // The loan is back in SUBMITTED state and can be modified.
+        final WorkingCapitalLoanProduct product = loan.getLoanProduct();
+        final WorkingCapitalLoanProductRelatedDetail productDetail = 
product.getRelatedDetail();
+        
loan.getLoanProductRelatedDetails().setDiscount(productDetail.getDiscount());
+
+        this.loanRepository.saveAndFlush(loan);
+
+        
createNote(command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName),
 loan);
+
+        final Map<String, Object> changes = new LinkedHashMap<>();
+        changes.put("status", loan.getLoanStatus());
+
+        log.debug("Working capital loan {} approval undone", loanId);
+
+        return new 
CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loanId)
+                
.withEntityExternalId(loan.getExternalId()).withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId())
+                .withLoanId(loanId).with(changes).build();
+    }
+
+    @Override
+    public CommandProcessingResult rejectApplication(final Long loanId, final 
JsonCommand command) {
+        final WorkingCapitalLoan loan = this.loanRepository.findById(loanId)
+                .orElseThrow(() -> new 
WorkingCapitalLoanNotFoundException(loanId));
+
+        this.validator.validateRejection(command.json(), loan);
+
+        final AppUser currentUser = this.context.authenticatedUser();
+
+        this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_REJECTED, 
loan);
+
+        final LocalDate rejectedOnDate = 
command.localDateValueOfParameterNamed(WorkingCapitalLoanConstants.rejectedOnDateParamName);
+        loan.setRejectedOnDate(rejectedOnDate);
+        loan.setRejectedBy(currentUser);
+
+        this.loanRepository.saveAndFlush(loan);
+
+        
createNote(command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName),
 loan);
+
+        final Map<String, Object> changes = new LinkedHashMap<>();
+        changes.put(WorkingCapitalLoanConstants.rejectedOnDateParamName, 
rejectedOnDate);
+        changes.put("status", loan.getLoanStatus());
+
+        log.debug("Working capital loan {} rejected by user {}", loanId, 
currentUser.getId());
+
+        return new 
CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loanId)
+                
.withEntityExternalId(loan.getExternalId()).withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId())
+                .withLoanId(loanId).with(changes).build();
+    }
+
+    private void createNote(final String noteText, final WorkingCapitalLoan 
loan) {
+        if (StringUtils.isNotBlank(noteText)) {
+            final WorkingCapitalLoanNote note = 
WorkingCapitalLoanNote.create(loan, noteText);
+            this.noteRepository.save(note);
+        }
+    }
+}
diff --git 
a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml
 
b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml
index d49e648c1d..0946a57d43 100644
--- 
a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml
+++ 
b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml
@@ -31,4 +31,5 @@
   <include relativeToChangelogFile="true" 
file="parts/0007_drop_flat_percentage_amount.xml"/>
   <include relativeToChangelogFile="true" 
file="parts/0008_delinquency_for_working_capital_loans.xml"/>
   <include relativeToChangelogFile="true" 
file="parts/0009_wc_loan_amortization_model.xml"/>
+  <include relativeToChangelogFile="true" 
file="parts/0010_loan_account_permissions.xml"/>
 </databaseChangeLog>
diff --git 
a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0010_loan_account_permissions.xml
 
b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0010_loan_account_permissions.xml
new file mode 100644
index 0000000000..be470293f1
--- /dev/null
+++ 
b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0010_loan_account_permissions.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog";
+                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+                   
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog 
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd";>
+
+    <!-- Permissions for Working Capital Loan Account approval/rejection 
operations -->
+    <changeSet author="fineract" id="wcl-0007-1">
+        <preConditions onFail="MARK_RAN">
+            <sqlCheck expectedResult="0">
+                SELECT COUNT(*) FROM m_permission WHERE code = 
'APPROVE_WORKINGCAPITALLOAN';
+            </sqlCheck>
+        </preConditions>
+        <insert tableName="m_permission">
+            <column name="grouping" value="portfolio"/>
+            <column name="code" value="APPROVE_WORKINGCAPITALLOAN"/>
+            <column name="entity_name" value="WORKINGCAPITALLOAN"/>
+            <column name="action_name" value="APPROVE"/>
+            <column name="can_maker_checker" valueBoolean="false"/>
+        </insert>
+    </changeSet>
+
+    <changeSet author="fineract" id="wcl-0007-2">
+        <preConditions onFail="MARK_RAN">
+            <sqlCheck expectedResult="0">
+                SELECT COUNT(*) FROM m_permission WHERE code = 
'REJECT_WORKINGCAPITALLOAN';
+            </sqlCheck>
+        </preConditions>
+        <insert tableName="m_permission">
+            <column name="grouping" value="portfolio"/>
+            <column name="code" value="REJECT_WORKINGCAPITALLOAN"/>
+            <column name="entity_name" value="WORKINGCAPITALLOAN"/>
+            <column name="action_name" value="REJECT"/>
+            <column name="can_maker_checker" valueBoolean="false"/>
+        </insert>
+    </changeSet>
+
+    <changeSet author="fineract" id="wcl-0007-3">
+        <preConditions onFail="MARK_RAN">
+            <sqlCheck expectedResult="0">
+                SELECT COUNT(*) FROM m_permission WHERE code = 
'APPROVALUNDO_WORKINGCAPITALLOAN';
+            </sqlCheck>
+        </preConditions>
+        <insert tableName="m_permission">
+            <column name="grouping" value="portfolio"/>
+            <column name="code" value="APPROVALUNDO_WORKINGCAPITALLOAN"/>
+            <column name="entity_name" value="WORKINGCAPITALLOAN"/>
+            <column name="action_name" value="APPROVALUNDO"/>
+            <column name="can_maker_checker" valueBoolean="false"/>
+        </insert>
+    </changeSet>
+</databaseChangeLog>
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanApprovalRejectionTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanApprovalRejectionTest.java
new file mode 100644
index 0000000000..0159105fcb
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanApprovalRejectionTest.java
@@ -0,0 +1,479 @@
+/**
+ * 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.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.UUID;
+import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.funds.FundsResourceHandler;
+import 
org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper;
+import 
org.apache.fineract.integrationtests.common.workingcapitalloan.WorkingCapitalLoanApplicationHelper;
+import 
org.apache.fineract.integrationtests.common.workingcapitalloan.WorkingCapitalLoanApplicationTestBuilder;
+import 
org.apache.fineract.integrationtests.common.workingcapitalloanproduct.WorkingCapitalLoanProductHelper;
+import 
org.apache.fineract.integrationtests.common.workingcapitalloanproduct.WorkingCapitalLoanProductTestBuilder;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class WorkingCapitalLoanApprovalRejectionTest {
+
+    private static RequestSpecification requestSpec;
+    private static ResponseSpecification responseSpec;
+    private static Long delinquencyBucketId;
+    private static Long fundId;
+
+    private final WorkingCapitalLoanApplicationHelper applicationHelper = new 
WorkingCapitalLoanApplicationHelper();
+    private final WorkingCapitalLoanProductHelper productHelper = new 
WorkingCapitalLoanProductHelper();
+
+    @BeforeAll
+    static void init() {
+        Utils.initializeRESTAssured();
+        requestSpec = new 
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        requestSpec.header("Authorization", "Basic " + 
Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        requestSpec.header("Fineract-Platform-TenantId", "default");
+        responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        delinquencyBucketId = DelinquencyBucketsHelper.createDefaultBucket();
+        fundId = (long) FundsResourceHandler.createFund(requestSpec, 
responseSpec);
+    }
+
+    // ===== AC: User should be able to approve the created loan account (via 
API) =====
+
+    @Test
+    public void testApproveWorkingCapitalLoan() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final Long loanId = submitLoan(clientId, productId);
+
+        final LocalDate approvedOnDate = getSubmittedOnDate(loanId);
+        applicationHelper.approveById(loanId, 
WorkingCapitalLoanApplicationTestBuilder.buildApproveJson(approvedOnDate));
+
+        final JsonObject data = retrieveLoan(loanId);
+        assertEquals("loanStatusType.approved", 
data.getAsJsonObject("status").get("code").getAsString());
+        assertDateEquals(approvedOnDate, data.get("approvedOnDate"));
+        // approvedPrincipal should default to proposedPrincipal
+        assertNotNull(data.get("approvedPrincipal"));
+    }
+
+    // ===== AC: Fields modifiable during approval: Principal, Discount, Date, 
ExpDisbDate =====
+
+    @Test
+    public void testApproveWithPrincipalAndDiscountOverride() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+
+        // Submit with discount = 100
+        final Long loanId = applicationHelper.submit(new 
WorkingCapitalLoanApplicationTestBuilder() //
+                .withClientId(clientId) //
+                .withProductId(productId) //
+                .withPrincipal(BigDecimal.valueOf(5000)) //
+                .withPeriodPaymentRate(BigDecimal.ONE) //
+                .withTotalPayment(BigDecimal.valueOf(5500)) //
+                .withDiscount(BigDecimal.valueOf(100)) //
+                .buildSubmitJson());
+
+        final LocalDate approvedOnDate = getSubmittedOnDate(loanId);
+        final BigDecimal approvedAmount = BigDecimal.valueOf(3000);
+        final BigDecimal discountAmount = BigDecimal.valueOf(50); // reduced 
from 100 to 50
+
+        applicationHelper.approveById(loanId,
+                
WorkingCapitalLoanApplicationTestBuilder.buildApproveJson(approvedOnDate, 
approvedAmount, discountAmount));
+
+        final JsonObject data = retrieveLoan(loanId);
+        assertEquals("loanStatusType.approved", 
data.getAsJsonObject("status").get("code").getAsString());
+        assertEqualBigDecimal(approvedAmount, data.get("approvedPrincipal"));
+        assertEqualBigDecimal(discountAmount, data.get("discount"));
+    }
+
+    @Test
+    public void testRejectWorkingCapitalLoan() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final Long loanId = submitLoan(clientId, productId);
+
+        final LocalDate rejectedOnDate = getSubmittedOnDate(loanId);
+        applicationHelper.rejectById(loanId, 
WorkingCapitalLoanApplicationTestBuilder.buildRejectJson(rejectedOnDate));
+
+        final JsonObject data = retrieveLoan(loanId);
+        assertEquals("loanStatusType.rejected", 
data.getAsJsonObject("status").get("code").getAsString());
+        assertDateEquals(rejectedOnDate, data.get("rejectedOnDate"));
+    }
+
+    // ===== AC: User should be able to undo the approval; moves back to 
created state =====
+
+    @Test
+    public void testUndoApproval() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final Long loanId = submitLoan(clientId, productId);
+
+        applicationHelper.approveById(loanId, 
WorkingCapitalLoanApplicationTestBuilder.buildApproveJson(getSubmittedOnDate(loanId)));
+
+        applicationHelper.undoApprovalById(loanId, 
WorkingCapitalLoanApplicationTestBuilder.buildUndoApproveJson());
+
+        final JsonObject data = retrieveLoan(loanId);
+        assertEquals("loanStatusType.submitted.and.pending.approval", 
data.getAsJsonObject("status").get("code").getAsString());
+
+        applicationHelper.deleteById(loanId);
+        productHelper.deleteWorkingCapitalLoanProductById(productId);
+    }
+
+    @Test
+    public void testUndoApprovalResetsToCreatedState() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+
+        // Submit with discount = 100
+        final Long loanId = applicationHelper.submit(new 
WorkingCapitalLoanApplicationTestBuilder() //
+                .withClientId(clientId) //
+                .withProductId(productId) //
+                .withPrincipal(BigDecimal.valueOf(5000)) //
+                .withPeriodPaymentRate(BigDecimal.ONE) //
+                .withTotalPayment(BigDecimal.valueOf(5500)) //
+                .withDiscount(BigDecimal.valueOf(100)) //
+                .buildSubmitJson());
+
+        // Approve with reduced principal and discount
+        applicationHelper.approveById(loanId, 
WorkingCapitalLoanApplicationTestBuilder.buildApproveJson(getSubmittedOnDate(loanId),
+                BigDecimal.valueOf(3000), BigDecimal.valueOf(50)));
+
+        final JsonObject approvedData = retrieveLoan(loanId);
+        assertEqualBigDecimal(BigDecimal.valueOf(3000), 
approvedData.get("approvedPrincipal"));
+        assertEqualBigDecimal(BigDecimal.valueOf(50), 
approvedData.get("discount"));
+
+        // Undo approval
+        applicationHelper.undoApprovalById(loanId, 
WorkingCapitalLoanApplicationTestBuilder.buildUndoApproveJson());
+
+        final JsonObject undoData = retrieveLoan(loanId);
+        assertEquals("loanStatusType.submitted.and.pending.approval", 
undoData.getAsJsonObject("status").get("code").getAsString());
+        // approvedPrincipal should reset to 0 after undo (loan is back in 
submitted state, not yet approved)
+        assertEqualBigDecimal(BigDecimal.ZERO, 
undoData.get("approvedPrincipal"));
+
+        applicationHelper.deleteById(loanId);
+        productHelper.deleteWorkingCapitalLoanProductById(productId);
+    }
+
+    // ========== State transition validation tests ==========
+
+    @Test
+    public void testApproveAlreadyApprovedLoanFails() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final Long loanId = submitLoan(clientId, productId);
+
+        final LocalDate submittedOnDate = getSubmittedOnDate(loanId);
+        applicationHelper.approveById(loanId, 
WorkingCapitalLoanApplicationTestBuilder.buildApproveJson(submittedOnDate));
+
+        CallFailedRuntimeException ex = 
applicationHelper.runApproveExpectingFailure(loanId,
+                
WorkingCapitalLoanApplicationTestBuilder.buildApproveJson(submittedOnDate));
+        assertNotNull(ex);
+    }
+
+    @Test
+    public void testRejectApprovedLoanFails() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final Long loanId = submitLoan(clientId, productId);
+
+        final LocalDate submittedOnDate = getSubmittedOnDate(loanId);
+        applicationHelper.approveById(loanId, 
WorkingCapitalLoanApplicationTestBuilder.buildApproveJson(submittedOnDate));
+
+        CallFailedRuntimeException ex = 
applicationHelper.runRejectExpectingFailure(loanId,
+                
WorkingCapitalLoanApplicationTestBuilder.buildRejectJson(submittedOnDate));
+        assertNotNull(ex);
+    }
+
+    @Test
+    public void testUndoNonApprovedLoanFails() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final Long loanId = submitLoan(clientId, productId);
+
+        CallFailedRuntimeException ex = 
applicationHelper.runUndoApprovalExpectingFailure(loanId,
+                
WorkingCapitalLoanApplicationTestBuilder.buildUndoApproveJson());
+        assertNotNull(ex);
+
+        applicationHelper.deleteById(loanId);
+        productHelper.deleteWorkingCapitalLoanProductById(productId);
+    }
+
+    // ========== Input validation tests ==========
+
+    @Test
+    public void testApproveWithoutApprovedOnDateFails() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final Long loanId = submitLoan(clientId, productId);
+
+        CallFailedRuntimeException ex = 
applicationHelper.runApproveExpectingFailure(loanId,
+                
WorkingCapitalLoanApplicationTestBuilder.buildApproveJson(null));
+        assertNotNull(ex);
+
+        applicationHelper.deleteById(loanId);
+        productHelper.deleteWorkingCapitalLoanProductById(productId);
+    }
+
+    @Test
+    public void testApproveWithFutureDateFails() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final Long loanId = submitLoan(clientId, productId);
+
+        CallFailedRuntimeException ex = 
applicationHelper.runApproveExpectingFailure(loanId,
+                
WorkingCapitalLoanApplicationTestBuilder.buildApproveJson(getSubmittedOnDate(loanId).plusDays(10)));
+        assertNotNull(ex);
+
+        applicationHelper.deleteById(loanId);
+        productHelper.deleteWorkingCapitalLoanProductById(productId);
+    }
+
+    @Test
+    public void testApproveWithDateBeforeSubmittedOnDateFails() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final LocalDate submittedOnDate = 
LocalDate.now(ZoneId.systemDefault());
+
+        final Long loanId = applicationHelper.submit(new 
WorkingCapitalLoanApplicationTestBuilder() //
+                .withClientId(clientId) //
+                .withProductId(productId) //
+                .withPrincipal(BigDecimal.valueOf(5000)) //
+                .withPeriodPaymentRate(BigDecimal.ONE) //
+                .withTotalPayment(BigDecimal.valueOf(5500)) //
+                .withSubmittedOnDate(submittedOnDate) //
+                .buildSubmitJson());
+
+        CallFailedRuntimeException ex = 
applicationHelper.runApproveExpectingFailure(loanId,
+                
WorkingCapitalLoanApplicationTestBuilder.buildApproveJson(submittedOnDate.minusDays(1)));
+        assertNotNull(ex);
+
+        applicationHelper.deleteById(loanId);
+        productHelper.deleteWorkingCapitalLoanProductById(productId);
+    }
+
+    @Test
+    public void testRejectWithoutRejectedOnDateFails() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final Long loanId = submitLoan(clientId, productId);
+
+        CallFailedRuntimeException ex = 
applicationHelper.runRejectExpectingFailure(loanId,
+                
WorkingCapitalLoanApplicationTestBuilder.buildRejectJson(null));
+        assertNotNull(ex);
+
+        applicationHelper.deleteById(loanId);
+        productHelper.deleteWorkingCapitalLoanProductById(productId);
+    }
+
+    @Test
+    public void testApproveWithNegativeAmountFails() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final Long loanId = submitLoan(clientId, productId);
+
+        CallFailedRuntimeException ex = 
applicationHelper.runApproveExpectingFailure(loanId,
+                
WorkingCapitalLoanApplicationTestBuilder.buildApproveJson(getSubmittedOnDate(loanId),
 BigDecimal.valueOf(-100), null));
+        assertNotNull(ex);
+
+        applicationHelper.deleteById(loanId);
+        productHelper.deleteWorkingCapitalLoanProductById(productId);
+    }
+
+    @Test
+    public void testApproveWithAmountExceedingProposedPrincipalFails() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final Long loanId = submitLoan(clientId, productId); // proposed 
principal = 5000
+
+        CallFailedRuntimeException ex = 
applicationHelper.runApproveExpectingFailure(loanId,
+                
WorkingCapitalLoanApplicationTestBuilder.buildApproveJson(getSubmittedOnDate(loanId),
 BigDecimal.valueOf(6000), null));
+        assertNotNull(ex);
+
+        applicationHelper.deleteById(loanId);
+        productHelper.deleteWorkingCapitalLoanProductById(productId);
+    }
+
+    @Test
+    public void testApproveWithoutExpectedDisbursementDateFails() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final Long loanId = submitLoan(clientId, productId);
+
+        // Build approve JSON without expectedDisbursementDate
+        final String json = 
"{\"locale\":\"en\",\"dateFormat\":\"yyyy-MM-dd\",\"approvedOnDate\":\"" + 
getSubmittedOnDate(loanId) + "\"}";
+        CallFailedRuntimeException ex = 
applicationHelper.runApproveExpectingFailure(loanId, json);
+        assertNotNull(ex);
+
+        applicationHelper.deleteById(loanId);
+        productHelper.deleteWorkingCapitalLoanProductById(productId);
+    }
+
+    @Test
+    public void testApproveWithDiscountExceedingCreatedValueFails() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+
+        // Submit with discount = 100
+        final Long loanId = applicationHelper.submit(new 
WorkingCapitalLoanApplicationTestBuilder() //
+                .withClientId(clientId) //
+                .withProductId(productId) //
+                .withPrincipal(BigDecimal.valueOf(5000)) //
+                .withPeriodPaymentRate(BigDecimal.ONE) //
+                .withTotalPayment(BigDecimal.valueOf(5500)) //
+                .withDiscount(BigDecimal.valueOf(100)) //
+                .buildSubmitJson());
+
+        // Approve with discount = 200 (exceeds creation-time 100) → should 
fail
+        CallFailedRuntimeException ex = 
applicationHelper.runApproveExpectingFailure(loanId,
+                
WorkingCapitalLoanApplicationTestBuilder.buildApproveJson(getSubmittedOnDate(loanId),
 null, BigDecimal.valueOf(200)));
+        assertNotNull(ex);
+
+        applicationHelper.deleteById(loanId);
+        productHelper.deleteWorkingCapitalLoanProductById(productId);
+    }
+
+    // ========== External-ID endpoint tests ==========
+
+    @Test
+    public void testApproveAndUndoByExternalId() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final String externalId = UUID.randomUUID().toString();
+
+        final Long loanId = applicationHelper.submit(new 
WorkingCapitalLoanApplicationTestBuilder() //
+                .withClientId(clientId) //
+                .withProductId(productId) //
+                .withPrincipal(BigDecimal.valueOf(5000)) //
+                .withPeriodPaymentRate(BigDecimal.ONE) //
+                .withTotalPayment(BigDecimal.valueOf(5500)) //
+                .withExternalId(externalId) //
+                .buildSubmitJson());
+
+        final LocalDate approvedOnDate = getSubmittedOnDate(loanId);
+        applicationHelper.approveByExternalId(externalId, 
WorkingCapitalLoanApplicationTestBuilder.buildApproveJson(approvedOnDate));
+
+        JsonObject data = retrieveLoan(loanId);
+        assertEquals("loanStatusType.approved", 
data.getAsJsonObject("status").get("code").getAsString());
+
+        applicationHelper.undoApprovalByExternalId(externalId, 
WorkingCapitalLoanApplicationTestBuilder.buildUndoApproveJson());
+
+        data = retrieveLoan(loanId);
+        assertEquals("loanStatusType.submitted.and.pending.approval", 
data.getAsJsonObject("status").get("code").getAsString());
+    }
+
+    @Test
+    public void testRejectByExternalId() {
+        final Long productId = createProduct();
+        final Long clientId = createClient();
+        final String externalId = UUID.randomUUID().toString();
+
+        final Long loanId = applicationHelper.submit(new 
WorkingCapitalLoanApplicationTestBuilder() //
+                .withClientId(clientId) //
+                .withProductId(productId) //
+                .withPrincipal(BigDecimal.valueOf(5000)) //
+                .withPeriodPaymentRate(BigDecimal.ONE) //
+                .withTotalPayment(BigDecimal.valueOf(5500)) //
+                .withExternalId(externalId) //
+                .buildSubmitJson());
+
+        final LocalDate rejectedOnDate = getSubmittedOnDate(loanId);
+        applicationHelper.rejectByExternalId(externalId, 
WorkingCapitalLoanApplicationTestBuilder.buildRejectJson(rejectedOnDate));
+
+        final JsonObject data = retrieveLoan(loanId);
+        assertEquals("loanStatusType.rejected", 
data.getAsJsonObject("status").get("code").getAsString());
+    }
+
+    // ========== Helper methods ==========
+
+    private Long submitLoan(final Long clientId, final Long productId) {
+        return applicationHelper.submit(new 
WorkingCapitalLoanApplicationTestBuilder() //
+                .withClientId(clientId) //
+                .withProductId(productId) //
+                .withPrincipal(BigDecimal.valueOf(5000)) //
+                .withPeriodPaymentRate(BigDecimal.ONE) //
+                .withTotalPayment(BigDecimal.valueOf(5500)) //
+                .buildSubmitJson());
+    }
+
+    private JsonObject retrieveLoan(final Long loanId) {
+        final String response = applicationHelper.retrieveById(loanId);
+        assertNotNull(response);
+        return new Gson().fromJson(response, JsonObject.class);
+    }
+
+    /**
+     * Retrieves the submittedOnDate from the server for the given loan. This 
avoids timezone mismatches between the
+     * test JVM and the server (which uses the tenant timezone).
+     */
+    private LocalDate getSubmittedOnDate(final Long loanId) {
+        final JsonObject data = retrieveLoan(loanId);
+        return extractDate(data.get("submittedOnDate"));
+    }
+
+    private static LocalDate extractDate(final com.google.gson.JsonElement 
element) {
+        assertNotNull(element, "Expected date element");
+        if (element.isJsonArray()) {
+            final com.google.gson.JsonArray arr = element.getAsJsonArray();
+            return LocalDate.of(arr.get(0).getAsInt(), arr.get(1).getAsInt(), 
arr.get(2).getAsInt());
+        }
+        return LocalDate.parse(element.getAsString());
+    }
+
+    private Long createProduct() {
+        final String uniqueName = "WCL Product " + 
UUID.randomUUID().toString().substring(0, 8);
+        final String uniqueShortName = 
UUID.randomUUID().toString().replace("-", "").substring(0, 4);
+        return productHelper
+                .createWorkingCapitalLoanProduct(
+                        new 
WorkingCapitalLoanProductTestBuilder().withName(uniqueName).withShortName(uniqueShortName).build())
+                .getResourceId();
+    }
+
+    private Long createClient() {
+        return 
ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+    }
+
+    private static void assertEqualBigDecimal(final BigDecimal expected, final 
com.google.gson.JsonElement actual) {
+        assertNotNull(actual, "Expected value for field");
+        assertEquals(0, 
expected.compareTo(actual.getAsJsonPrimitive().getAsBigDecimal()),
+                "Expected " + expected + " but got " + actual.getAsString());
+    }
+
+    private static void assertDateEquals(final LocalDate expected, final 
com.google.gson.JsonElement actual) {
+        assertNotNull(actual, "Expected date value");
+        if (actual.isJsonArray()) {
+            final com.google.gson.JsonArray arr = actual.getAsJsonArray();
+            assertEquals(expected.getYear(), arr.get(0).getAsInt());
+            assertEquals(expected.getMonthValue(), arr.get(1).getAsInt());
+            assertEquals(expected.getDayOfMonth(), arr.get(2).getAsInt());
+        } else {
+            assertEquals(expected.toString(), actual.getAsString());
+        }
+    }
+}
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WorkingCapitalLoanApplicationHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WorkingCapitalLoanApplicationHelper.java
index ad733527e6..de4853ce7d 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WorkingCapitalLoanApplicationHelper.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WorkingCapitalLoanApplicationHelper.java
@@ -26,6 +26,7 @@ import 
org.apache.fineract.client.feign.services.WorkingCapitalLoansApi;
 import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
 import org.apache.fineract.client.feign.util.FeignCalls;
 import org.apache.fineract.client.models.GetWorkingCapitalLoansLoanIdResponse;
+import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdRequest;
 import org.apache.fineract.client.models.PostWorkingCapitalLoansRequest;
 import org.apache.fineract.client.models.PostWorkingCapitalLoansResponse;
 import org.apache.fineract.client.models.PutWorkingCapitalLoansLoanIdRequest;
@@ -90,6 +91,52 @@ public class WorkingCapitalLoanApplicationHelper {
         return toJson(response);
     }
 
+    public Long approveById(final Long loanId, final String jsonBody) {
+        PostWorkingCapitalLoansLoanIdRequest request = fromJson(jsonBody, 
PostWorkingCapitalLoansLoanIdRequest.class);
+        return FeignCalls.ok(() -> 
api().stateTransitionWorkingCapitalLoanById(loanId, "approve", 
request)).getResourceId();
+    }
+
+    public Long rejectById(final Long loanId, final String jsonBody) {
+        PostWorkingCapitalLoansLoanIdRequest request = fromJson(jsonBody, 
PostWorkingCapitalLoansLoanIdRequest.class);
+        return FeignCalls.ok(() -> 
api().stateTransitionWorkingCapitalLoanById(loanId, "reject", 
request)).getResourceId();
+    }
+
+    public Long undoApprovalById(final Long loanId, final String jsonBody) {
+        PostWorkingCapitalLoansLoanIdRequest request = fromJson(jsonBody, 
PostWorkingCapitalLoansLoanIdRequest.class);
+        return FeignCalls.ok(() -> 
api().stateTransitionWorkingCapitalLoanById(loanId, "undoapproval", 
request)).getResourceId();
+    }
+
+    public Long approveByExternalId(final String externalId, final String 
jsonBody) {
+        PostWorkingCapitalLoansLoanIdRequest request = fromJson(jsonBody, 
PostWorkingCapitalLoansLoanIdRequest.class);
+        return FeignCalls.ok(() -> 
api().stateTransitionWorkingCapitalLoanByExternalId(externalId, "approve", 
request)).getResourceId();
+    }
+
+    public Long rejectByExternalId(final String externalId, final String 
jsonBody) {
+        PostWorkingCapitalLoansLoanIdRequest request = fromJson(jsonBody, 
PostWorkingCapitalLoansLoanIdRequest.class);
+        return FeignCalls.ok(() -> 
api().stateTransitionWorkingCapitalLoanByExternalId(externalId, "reject", 
request)).getResourceId();
+    }
+
+    public Long undoApprovalByExternalId(final String externalId, final String 
jsonBody) {
+        PostWorkingCapitalLoansLoanIdRequest request = fromJson(jsonBody, 
PostWorkingCapitalLoansLoanIdRequest.class);
+        return FeignCalls.ok(() -> 
api().stateTransitionWorkingCapitalLoanByExternalId(externalId, "undoapproval", 
request))
+                .getResourceId();
+    }
+
+    public CallFailedRuntimeException runApproveExpectingFailure(final Long 
loanId, final String jsonBody) {
+        PostWorkingCapitalLoansLoanIdRequest request = fromJson(jsonBody, 
PostWorkingCapitalLoansLoanIdRequest.class);
+        return FeignCalls.fail(() -> 
api().stateTransitionWorkingCapitalLoanById(loanId, "approve", request));
+    }
+
+    public CallFailedRuntimeException runRejectExpectingFailure(final Long 
loanId, final String jsonBody) {
+        PostWorkingCapitalLoansLoanIdRequest request = fromJson(jsonBody, 
PostWorkingCapitalLoansLoanIdRequest.class);
+        return FeignCalls.fail(() -> 
api().stateTransitionWorkingCapitalLoanById(loanId, "reject", request));
+    }
+
+    public CallFailedRuntimeException runUndoApprovalExpectingFailure(final 
Long loanId, final String jsonBody) {
+        PostWorkingCapitalLoansLoanIdRequest request = fromJson(jsonBody, 
PostWorkingCapitalLoansLoanIdRequest.class);
+        return FeignCalls.fail(() -> 
api().stateTransitionWorkingCapitalLoanById(loanId, "undoapproval", request));
+    }
+
     /**
      * For validation tests: run submit expecting failure.
      */
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WorkingCapitalLoanApplicationTestBuilder.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WorkingCapitalLoanApplicationTestBuilder.java
index a2c88472b0..a55cc23046 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WorkingCapitalLoanApplicationTestBuilder.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WorkingCapitalLoanApplicationTestBuilder.java
@@ -242,6 +242,48 @@ public class WorkingCapitalLoanApplicationTestBuilder {
         return json;
     }
 
+    public static String buildApproveJson(final LocalDate approvedOnDate, 
final BigDecimal approvedLoanAmount,
+            final BigDecimal discountAmount) {
+        final JsonObject json = new JsonObject();
+        json.addProperty("locale", DEFAULT_LOCALE);
+        json.addProperty("dateFormat", DEFAULT_DATE_FORMAT);
+        if (approvedOnDate != null) {
+            json.addProperty("approvedOnDate", 
approvedOnDate.format(DateTimeFormatter.ISO_LOCAL_DATE));
+        }
+        // expectedDisbursementDate is mandatory — default to 7 days after 
approval
+        final LocalDate disbursementDate = approvedOnDate != null ? 
approvedOnDate.plusDays(7)
+                : LocalDate.now(ZoneId.systemDefault()).plusDays(7);
+        json.addProperty("expectedDisbursementDate", 
disbursementDate.format(DateTimeFormatter.ISO_LOCAL_DATE));
+        if (approvedLoanAmount != null) {
+            json.addProperty("approvedLoanAmount", approvedLoanAmount);
+        }
+        if (discountAmount != null) {
+            json.addProperty("discountAmount", discountAmount);
+        }
+        return json.toString();
+    }
+
+    public static String buildApproveJson(final LocalDate approvedOnDate) {
+        return buildApproveJson(approvedOnDate, null, null);
+    }
+
+    public static String buildRejectJson(final LocalDate rejectedOnDate) {
+        final JsonObject json = new JsonObject();
+        json.addProperty("locale", DEFAULT_LOCALE);
+        json.addProperty("dateFormat", DEFAULT_DATE_FORMAT);
+        if (rejectedOnDate != null) {
+            json.addProperty("rejectedOnDate", 
rejectedOnDate.format(DateTimeFormatter.ISO_LOCAL_DATE));
+        }
+        return json.toString();
+    }
+
+    public static String buildUndoApproveJson() {
+        final JsonObject json = new JsonObject();
+        json.addProperty("locale", DEFAULT_LOCALE);
+        json.addProperty("dateFormat", DEFAULT_DATE_FORMAT);
+        return json.toString();
+    }
+
     private JsonArray buildPaymentAllocationJson() {
         final JsonArray paymentAllocation = new JsonArray();
         final JsonObject rule = new JsonObject();

Reply via email to