feat(chequebook): add cheque book module with Maker-Checker workflow

Adds cheque series, stock lot/transfer, cheque book issuance,
stop-payment/lost/cancel-leaf actions, and pending-request approval
APIs with Maker-Checker command handlers and liquibase changelogs.
This commit is contained in:
Jaanu423
2026-07-15 11:13:06 +05:30
parent 9d09d9ebd4
commit 04d65d857f
85 changed files with 6146 additions and 3 deletions
@@ -71,6 +71,158 @@ public class CommandWrapperBuilder {
this.sanitizeJsonKeys);
}
public CommandWrapperBuilder createChequeSeries() {
this.actionName = "CREATE";
this.entityName = "CHEQUESERIES";
this.entityId = null;
this.href = "/chequeseries";
return this;
}
public CommandWrapperBuilder updateChequeSeries(final Long seriesId) {
this.actionName = "UPDATE";
this.entityName = "CHEQUESERIES";
this.entityId = seriesId;
this.href = "/chequeseries/" + seriesId;
return this;
}
public CommandWrapperBuilder deleteChequeSeries(final Long seriesId) {
this.actionName = "DELETE";
this.entityName = "CHEQUESERIES";
this.entityId = seriesId;
this.href = "/chequeseries/" + seriesId;
return this;
}
public CommandWrapperBuilder createChequeStockLot() {
this.actionName = "CREATE";
this.entityName = "CHEQUESTOCKLOT";
this.entityId = null;
this.href = "/chequestocklots";
return this;
}
public CommandWrapperBuilder reportLostChequeStockLot(final Long lotId) {
this.actionName = "REPORTLOST";
this.entityName = "CHEQUESTOCKLOT";
this.entityId = lotId;
this.href = "/chequestocklots/" + lotId;
return this;
}
public CommandWrapperBuilder createChequeStockTransfer() {
this.actionName = "CREATE";
this.entityName = "CHEQUESTOCKTRANSFER";
this.entityId = null;
this.href = "/chequestocktransfers";
return this;
}
public CommandWrapperBuilder receiveChequeStockTransfer(final Long transferId) {
this.actionName = "RECEIVE";
this.entityName = "CHEQUESTOCKTRANSFER";
this.entityId = transferId;
this.href = "/chequestocktransfers/" + transferId;
return this;
}
public CommandWrapperBuilder rejectChequeStockTransfer(final Long transferId) {
this.actionName = "REJECT";
this.entityName = "CHEQUESTOCKTRANSFER";
this.entityId = transferId;
this.href = "/chequestocktransfers/" + transferId;
return this;
}
public CommandWrapperBuilder createChequeBook() {
this.actionName = "CREATE";
this.entityName = "CHEQUEBOOK";
this.entityId = null;
this.href = "/chequebooks";
return this;
}
public CommandWrapperBuilder issueChequeBook(final Long chequeBookId) {
this.actionName = "ISSUE";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder rejectChequeBook(final Long chequeBookId) {
this.actionName = "REJECT";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder withdrawChequeBook(final Long chequeBookId) {
this.actionName = "WITHDRAW";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder stopPaymentChequeBook(final Long chequeBookId) {
this.actionName = "STOPPAYMENT";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder revokeStopPaymentChequeBook(final Long chequeBookId) {
this.actionName = "REVOKESTOPPAYMENT";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder reportLostChequeBook(final Long chequeBookId) {
this.actionName = "REPORTLOST";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder cancelLeafChequeBook(final Long chequeBookId) {
this.actionName = "CANCELLEAF";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder markUsedChequeBook(final Long chequeBookId) {
this.actionName = "MARKUSED";
this.entityName = "CHEQUEBOOK";
this.entityId = chequeBookId;
this.href = "/chequebooks/" + chequeBookId;
return this;
}
public CommandWrapperBuilder approveChequeBookPendingRequest(final Long pendingRequestId) {
this.actionName = "APPROVE";
this.entityName = "CHEQUEBOOKPENDINGREQUEST";
this.entityId = pendingRequestId;
this.href = "/chequebook/pending-requests/" + pendingRequestId;
return this;
}
public CommandWrapperBuilder rejectChequeBookPendingRequest(final Long pendingRequestId) {
this.actionName = "REJECT";
this.entityName = "CHEQUEBOOKPENDINGREQUEST";
this.entityId = pendingRequestId;
this.href = "/chequebook/pending-requests/" + pendingRequestId;
return this;
}
public CommandWrapperBuilder updateCreditBureau() {
this.actionName = "UPDATE";
this.entityName = "ORGANISATIONCREDITBUREAU";
@@ -0,0 +1,47 @@
/**
* 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.currentaccount.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* <p>
* Wrapper for {@link CurrentAccountRepository} that is responsible for checking if {@link CurrentAccount} is
* returned when using <code>findOne</code> repository method and throwing an appropriate not found exception.
* </p>
*
* <p>
* This is to avoid need for checking and throwing in multiple areas of code base where
* {@link CurrentAccountRepository} is required.
* </p>
*/
@Service
@RequiredArgsConstructor
public class CurrentAccountRepositoryWrapper {
private final CurrentAccountRepository repository;
@Transactional(readOnly = true)
public CurrentAccount findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new CurrentAccountNotFoundException(id));
}
}
@@ -0,0 +1,35 @@
/**
* 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.chequebook.api;
public final class ChequeBookApiConstants {
private ChequeBookApiConstants() {}
public static final String ENTITY_SERIES = "CHEQUESERIES";
public static final String ENTITY_STOCK_LOT = "CHEQUESTOCKLOT";
public static final String ENTITY_STOCK_TRANSFER = "CHEQUESTOCKTRANSFER";
public static final String ENTITY_BOOK = "CHEQUEBOOK";
public static final String ENTITY_PENDING_REQUEST = "CHEQUEBOOKPENDINGREQUEST";
public static final String RESOURCE_SERIES = "chequeseries";
public static final String RESOURCE_STOCK_LOT = "chequestocklot";
public static final String RESOURCE_STOCK_TRANSFER = "chequestocktransfer";
public static final String RESOURCE_BOOK = "chequebook";
}
@@ -0,0 +1,113 @@
/**
* 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.chequebook.api;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
public enum ChequeBookApiJsonInputParams {
// series create/update
SERIES_CODE("seriesCode"), //
NAME("name"), //
LEAVES_PER_BOOK("leavesPerBook"), //
APPLICABLE_ACCOUNT_TYPE("applicableAccountType"), //
CTS_COMPLIANT("ctsCompliant"), //
REORDER_LEVEL("reorderLevel"), //
// stock lot receive
SERIES_ID("seriesId"), //
OFFICE_ID("officeId"), //
RANGE_START("rangeStart"), //
RANGE_END("rangeEnd"), //
BATCH_REF("batchRef"), //
// stock lot report-lost
FROM_SERIAL_NO("fromSerialNo"), //
REASON("reason"), //
// stock transfer dispatch
FROM_OFFICE_ID("fromOfficeId"), //
TO_OFFICE_ID("toOfficeId"), //
FROM_LOT_ID("fromLotId"), //
NO_OF_LEAVES("noOfLeaves"), //
// book request
ACCOUNT_TYPE("accountType"), //
ACCOUNT_ID("accountId"), //
NO_OF_BOOKS("noOfBooks"), //
// book issue
COLLECTION_ACKNOWLEDGED("collectionAcknowledged"), //
// book reject
REJECTION_REASON("rejectionReason"), //
// book leaf actions (stop-payment / revoke / report-lost / cancel-leaf)
FROM_LEAF_SEQ("fromLeafSeq"), //
TO_LEAF_SEQ("toLeafSeq"), //
// common — the web client always sends these on command payloads
LOCALE("locale"), //
DATE_FORMAT("dateFormat"); //
private final String value;
ChequeBookApiJsonInputParams(final String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
public static Set<String> seriesCreateParams() {
return toSet(SERIES_CODE, NAME, LEAVES_PER_BOOK, APPLICABLE_ACCOUNT_TYPE, CTS_COMPLIANT, REORDER_LEVEL, LOCALE, DATE_FORMAT);
}
public static Set<String> seriesUpdateParams() {
return toSet(NAME, LEAVES_PER_BOOK, APPLICABLE_ACCOUNT_TYPE, CTS_COMPLIANT, REORDER_LEVEL, LOCALE, DATE_FORMAT);
}
public static Set<String> stockLotReceiveParams() {
return toSet(SERIES_ID, OFFICE_ID, RANGE_START, RANGE_END, BATCH_REF, LOCALE, DATE_FORMAT);
}
public static Set<String> stockLotReportLostParams() {
return toSet(FROM_SERIAL_NO, REASON, LOCALE, DATE_FORMAT);
}
public static Set<String> stockTransferDispatchParams() {
return toSet(SERIES_ID, FROM_OFFICE_ID, TO_OFFICE_ID, FROM_LOT_ID, NO_OF_LEAVES, LOCALE, DATE_FORMAT);
}
public static Set<String> bookRequestParams() {
return toSet(ACCOUNT_TYPE, ACCOUNT_ID, OFFICE_ID, SERIES_ID, NO_OF_BOOKS, LOCALE, DATE_FORMAT);
}
public static Set<String> bookIssueParams() {
return toSet(COLLECTION_ACKNOWLEDGED, LOCALE, DATE_FORMAT);
}
public static Set<String> bookRejectParams() {
return toSet(REJECTION_REASON, LOCALE, DATE_FORMAT);
}
public static Set<String> bookLeafActionParams() {
return toSet(FROM_LEAF_SEQ, TO_LEAF_SEQ, REASON, LOCALE, DATE_FORMAT);
}
private static Set<String> toSet(final ChequeBookApiJsonInputParams... params) {
return Arrays.stream(params).map(ChequeBookApiJsonInputParams::getValue).collect(Collectors.toCollection(HashSet::new));
}
}
@@ -0,0 +1,130 @@
/**
* 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.chequebook.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.chequebook.data.ChequeBookData;
import org.apache.fineract.portfolio.chequebook.data.ChequeLeafData;
import org.apache.fineract.portfolio.chequebook.service.ChequeBookReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/chequebooks")
@Component
@Tag(name = "Cheque Books", description = "Requisition, issuance, stop-payment, hotlisting and cancellation of cheque books issued "
+ "against a Savings or Current account.")
@RequiredArgsConstructor
public class ChequeBookApiResource {
private final PlatformSecurityContext context;
private final ChequeBookReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List cheque books")
public Page<ChequeBookData> retrieveAll(@QueryParam("accountType") final String accountType,
@QueryParam("accountId") final Long accountId, @QueryParam("officeId") final Long officeId,
@QueryParam("status") final String status, @QueryParam("offset") @DefaultValue("0") final Integer offset,
@QueryParam("limit") @DefaultValue("20") final Integer limit) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_BOOK);
final int safeLimit = Math.min(limit, 200);
return this.readPlatformService.retrieveAllBooks(accountType, accountId, officeId, status, offset, safeLimit);
}
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a cheque book")
public ChequeBookData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_BOOK);
return this.readPlatformService.retrieveBook(id);
}
@GET
@Path("{id}/leaves")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List the individual leaves of a cheque book")
public List<ChequeLeafData> retrieveLeaves(@PathParam("id") @Parameter(description = "id") final Long id) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_BOOK);
return this.readPlatformService.retrieveLeaves(id);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Request a cheque book against an account")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createChequeBook().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@POST
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Run a command on a cheque book", description = "Supported commands: issue, reject, withdraw, "
+ "stoppayment, revokestoppayment, reportlost, cancelleaf, markused.")
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
@QueryParam("command") @Parameter(description = "issue | reject | withdraw | stoppayment | revokestoppayment | "
+ "reportlost | cancelleaf | markused") final String commandParam,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
final CommandWrapper request;
if ("issue".equalsIgnoreCase(commandParam)) {
request = builder.issueChequeBook(id).build();
} else if ("reject".equalsIgnoreCase(commandParam)) {
request = builder.rejectChequeBook(id).build();
} else if ("withdraw".equalsIgnoreCase(commandParam)) {
request = builder.withdrawChequeBook(id).build();
} else if ("stoppayment".equalsIgnoreCase(commandParam)) {
request = builder.stopPaymentChequeBook(id).build();
} else if ("revokestoppayment".equalsIgnoreCase(commandParam)) {
request = builder.revokeStopPaymentChequeBook(id).build();
} else if ("reportlost".equalsIgnoreCase(commandParam)) {
request = builder.reportLostChequeBook(id).build();
} else if ("cancelleaf".equalsIgnoreCase(commandParam)) {
request = builder.cancelLeafChequeBook(id).build();
} else if ("markused".equalsIgnoreCase(commandParam)) {
request = builder.markUsedChequeBook(id).build();
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,102 @@
/**
* 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.chequebook.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.chequebook.data.ChequeBookPendingRequestData;
import org.apache.fineract.portfolio.chequebook.service.ChequeBookReadPlatformService;
import org.springframework.stereotype.Component;
/**
* Bespoke maker-checker queue for the three cheque-book creation actions (series master, stock receipt,
* book requisition) — submitted via the normal {@code /chequeseries}, {@code /chequestocklots},
* {@code /chequebooks} POST endpoints, which now hold the request here as PENDING instead of creating the
* record immediately. A DIFFERENT user than the maker must approve or reject it from here.
*/
@Path("/v1/chequebook/pending-requests")
@Component
@Tag(name = "Cheque Book Pending Requests", description = "Maker-checker queue for cheque series, stock receipt, and book requisition "
+ "submissions — approving replays the original request and creates the record; the maker can never approve their own request.")
@RequiredArgsConstructor
public class ChequeBookPendingRequestApiResource {
private final PlatformSecurityContext context;
private final ChequeBookReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List cheque book pending requests")
public Page<ChequeBookPendingRequestData> retrieveAll(@QueryParam("status") final String status,
@QueryParam("fromDate") final String fromDate, @QueryParam("toDate") final String toDate,
@QueryParam("offset") @DefaultValue("0") final Integer offset, @QueryParam("limit") @DefaultValue("20") final Integer limit) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_PENDING_REQUEST);
final int safeLimit = Math.min(limit, 200);
return this.readPlatformService.retrievePendingRequests(status, fromDate, toDate, offset, safeLimit);
}
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a cheque book pending request")
public ChequeBookPendingRequestData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_PENDING_REQUEST);
return this.readPlatformService.retrievePendingRequest(id);
}
@POST
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Approve or reject a cheque book pending request", description = "Supported commands: approve, reject. "
+ "The user submitting the original request can never approve/reject it themselves.")
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
@QueryParam("command") @Parameter(description = "approve | reject") final String commandParam,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
final CommandWrapper request;
if ("approve".equalsIgnoreCase(commandParam)) {
request = builder.approveChequeBookPendingRequest(id).build();
} else if ("reject".equalsIgnoreCase(commandParam)) {
request = builder.rejectChequeBookPendingRequest(id).build();
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,56 @@
/**
* 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.chequebook.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.chequebook.data.ChequeLeafData;
import org.apache.fineract.portfolio.chequebook.service.ChequeBookReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/chequeleaves")
@Component
@Tag(name = "Cheque Leaves", description = "The teller/counter quick-lookup: given a bare cheque-leaf serial number, is it "
+ "active, stopped, lost, or was it ever issued at all — one code path covers every case. A serial number is only "
+ "unique within its own series, so more than one series can match.")
@RequiredArgsConstructor
public class ChequeLeafApiResource {
private final PlatformSecurityContext context;
private final ChequeBookReadPlatformService readPlatformService;
@GET
@Path("lookup")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Look up a cheque leaf by its serial number", description = "Returns every leaf across every series "
+ "that carries this serial number — normally one, but more than one series can legitimately share a number.")
public List<ChequeLeafData> lookup(@QueryParam("serialNo") @Parameter(description = "serialNo") final Long serialNo) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_BOOK);
return this.readPlatformService.retrieveLeafBySerialNo(serialNo);
}
}
@@ -0,0 +1,103 @@
/**
* 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.chequebook.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.chequebook.data.ChequeSeriesData;
import org.apache.fineract.portfolio.chequebook.service.ChequeBookReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/chequeseries")
@Component
@Tag(name = "Cheque Book Series", description = "Cheque-leaf stationery master — leaves-per-book, CTS-2010 compliance, low-stock reorder threshold.")
@RequiredArgsConstructor
public class ChequeSeriesApiResource {
private final PlatformSecurityContext context;
private final ChequeBookReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List cheque series")
public Page<ChequeSeriesData> retrieveAll(@QueryParam("status") final String status,
@QueryParam("offset") @DefaultValue("0") final Integer offset, @QueryParam("limit") @DefaultValue("20") final Integer limit) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_SERIES);
final int safeLimit = Math.min(limit, 500);
return this.readPlatformService.retrieveAllSeries(status, offset, safeLimit);
}
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a cheque series")
public ChequeSeriesData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_SERIES);
return this.readPlatformService.retrieveSeries(id);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create a cheque series")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createChequeSeries().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@PUT
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update a cheque series")
public CommandProcessingResult update(@PathParam("id") @Parameter(description = "id") final Long id,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().updateChequeSeries(id).withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@DELETE
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Deactivate a cheque series")
public CommandProcessingResult delete(@PathParam("id") @Parameter(description = "id") final Long id) {
final CommandWrapper request = new CommandWrapperBuilder().deleteChequeSeries(id).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,102 @@
/**
* 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.chequebook.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.chequebook.data.ChequeStockLotData;
import org.apache.fineract.portfolio.chequebook.service.ChequeBookReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/chequestocklots")
@Component
@Tag(name = "Cheque Stock Lots", description = "Unified stock ledger — a contiguous leaf-serial range currently sitting at one office, "
+ "with a drawdown cursor shared by both issuance-to-customer and transfer-to-office.")
@RequiredArgsConstructor
public class ChequeStockLotApiResource {
private final PlatformSecurityContext context;
private final ChequeBookReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List cheque stock lots")
public Page<ChequeStockLotData> retrieveAll(@QueryParam("officeId") final Long officeId,
@QueryParam("seriesId") final Long seriesId, @QueryParam("status") final String status,
@QueryParam("offset") @DefaultValue("0") final Integer offset, @QueryParam("limit") @DefaultValue("20") final Integer limit) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_STOCK_LOT);
final int safeLimit = Math.min(limit, 500);
return this.readPlatformService.retrieveStockLots(officeId, seriesId, status, offset, safeLimit);
}
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a cheque stock lot")
public ChequeStockLotData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_STOCK_LOT);
return this.readPlatformService.retrieveStockLot(id);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Receive a bulk cheque stock lot (from press/vendor)")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createChequeStockLot().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@POST
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Run a command on a cheque stock lot", description = "Supported commands: reportlost.")
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
@QueryParam("command") @Parameter(description = "reportlost") final String commandParam,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
final CommandWrapper request;
if ("reportlost".equalsIgnoreCase(commandParam)) {
request = builder.reportLostChequeStockLot(id).build();
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,103 @@
/**
* 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.chequebook.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.chequebook.data.ChequeStockTransferData;
import org.apache.fineract.portfolio.chequebook.service.ChequeBookReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/chequestocktransfers")
@Component
@Tag(name = "Cheque Stock Transfers", description = "Office-to-office cheque stock movement: dispatch draws a sub-range down from the "
+ "source lot, the destination office then acknowledges receipt (or rejects).")
@RequiredArgsConstructor
public class ChequeStockTransferApiResource {
private final PlatformSecurityContext context;
private final ChequeBookReadPlatformService readPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List cheque stock transfers")
public Page<ChequeStockTransferData> retrieveAll(@QueryParam("status") final String status,
@QueryParam("offset") @DefaultValue("0") final Integer offset, @QueryParam("limit") @DefaultValue("20") final Integer limit) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_STOCK_TRANSFER);
final int safeLimit = Math.min(limit, 500);
return this.readPlatformService.retrieveStockTransfers(status, offset, safeLimit);
}
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a cheque stock transfer")
public ChequeStockTransferData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) {
this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_STOCK_TRANSFER);
return this.readPlatformService.retrieveStockTransfer(id);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Dispatch a cheque stock transfer to another office")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper request = new CommandWrapperBuilder().createChequeStockTransfer().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
@POST
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Run a command on a cheque stock transfer", description = "Supported commands: receive, reject.")
public CommandProcessingResult handleCommand(@PathParam("id") @Parameter(description = "id") final Long id,
@QueryParam("command") @Parameter(description = "receive | reject") final String commandParam,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
final CommandWrapper request;
if ("receive".equalsIgnoreCase(commandParam)) {
request = builder.receiveChequeStockTransfer(id).build();
} else if ("reject".equalsIgnoreCase(commandParam)) {
request = builder.rejectChequeStockTransfer(id).build();
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
return this.commandsSourceWritePlatformService.logCommandSource(request);
}
}
@@ -0,0 +1,55 @@
/**
* 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.chequebook.data;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class ChequeBookData {
private final Long id;
private final String accountType;
private final Long accountId;
private final String accountNo;
private final String clientName;
private final Long officeId;
private final String officeName;
private final Long seriesId;
private final String seriesCode;
private final LocalDate requestedDate;
private final Long requestedBy;
private final Integer noOfBooks;
private final Integer noOfLeavesRequested;
private final String status;
private final LocalDate issueDate;
private final Long issuedBy;
private final Long lotId;
private final Long rangeStart;
private final Long rangeEnd;
private final Integer totalLeaves;
private final String rejectionReason;
private final boolean collectionAcknowledged;
private final LocalDate collectedDate;
private final Long collectedBy;
}
@@ -0,0 +1,46 @@
/**
* 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.chequebook.data;
import java.time.OffsetDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class ChequeBookPendingRequestData {
private final Long id;
private final String requestType;
private final String payloadJson;
private final String status;
private final Long makerId;
private final String makerName;
private final String makerUsername;
private final OffsetDateTime submittedOn;
private final Long checkerId;
private final String checkerName;
private final String checkerUsername;
private final OffsetDateTime checkedOn;
private final String rejectionReason;
private final Long createdResourceId;
private final String activityLog;
}
@@ -0,0 +1,60 @@
/**
* 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.chequebook.data;
import java.time.OffsetDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
/**
* A single leaf. {@code chequeBookId}/{@code accountNo}/{@code clientName} are null when the row was
* generated directly against a stock lot (stock reported lost/stolen before ever being issued) — the
* quick-lookup-by-serial-number screen relies on this being the single source of truth for every case.
*/
@Getter
@Builder
@AllArgsConstructor
public class ChequeLeafData {
private final Long id;
private final Long chequeBookId;
private final Long lotId;
private final String seriesCode;
private final Long leafSerialNo;
private final Integer leafSeqNo;
private final String status;
private final String stopReason;
private final Long stoppedBy;
private final OffsetDateTime stoppedOn;
private final String lostReason;
private final Long lostReportedBy;
private final OffsetDateTime lostReportedOn;
private final String cancelledReason;
private final Long cancelledBy;
private final OffsetDateTime cancelledOn;
private final String usedReason;
private final Long usedBy;
private final OffsetDateTime usedOn;
private final String accountType;
private final Long accountId;
private final String accountNo;
private final String clientName;
private final String officeName;
}
@@ -0,0 +1,38 @@
/**
* 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.chequebook.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class ChequeSeriesData {
private final Long id;
private final String seriesCode;
private final String name;
private final Integer leavesPerBook;
private final String applicableAccountType;
private final boolean ctsCompliant;
private final Integer reorderLevel;
private final boolean active;
}
@@ -0,0 +1,46 @@
/**
* 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.chequebook.data;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class ChequeStockLotData {
private final Long id;
private final Long seriesId;
private final String seriesCode;
private final Integer leavesPerBook;
private final Long officeId;
private final String officeName;
private final Long rangeStart;
private final Long rangeEnd;
private final Long nextSerialNo;
private final Long remainingLeaves;
private final Long remainingBooks;
private final String source;
private final Long sourceRefId;
private final String batchRef;
private final String status;
private final boolean lowStock;
}
@@ -0,0 +1,48 @@
/**
* 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.chequebook.data;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class ChequeStockTransferData {
private final Long id;
private final Long seriesId;
private final String seriesCode;
private final Long fromOfficeId;
private final String fromOfficeName;
private final Long toOfficeId;
private final String toOfficeName;
private final Long fromLotId;
private final Long toLotId;
private final Long rangeStart;
private final Long rangeEnd;
private final Integer totalLeaves;
private final LocalDate dispatchDate;
private final String status;
private final LocalDate receivedDate;
private final Long dispatchedBy;
private final Long receivedBy;
}
@@ -0,0 +1,40 @@
/**
* 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.chequebook.domain;
import org.apache.commons.lang3.StringUtils;
/** Which account model a cheque book is issued against — the two are not interchangeable in this codebase. */
public enum ChequeAccountType {
SAVINGS, //
CURRENT; //
public static ChequeAccountType fromString(final String value) {
if (StringUtils.isBlank(value)) {
return null;
}
for (final ChequeAccountType type : values()) {
if (type.name().equalsIgnoreCase(value.trim())) {
return type;
}
}
return null;
}
}
@@ -0,0 +1,184 @@
/**
* 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.chequebook.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.office.domain.Office;
/**
* A cheque book requested and (once stock is drawn) issued against an account. {@code accountId} is a
* plain column (no JPA relation) since the account may be either a {@code SavingsAccount} or a
* {@code CurrentAccount} — two account models in this codebase that are not interchangeable — mirroring
* the same "plain column to avoid cross-module entity dependency" precedent used by
* {@code CurrentAccount.productId}.
*/
@Entity
@Table(name = "m_cheque_book")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ChequeBook extends AbstractPersistableCustom<Long> {
@Column(name = "account_type", nullable = false, length = 10)
private String accountType;
@Column(name = "account_id", nullable = false)
private Long accountId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "office_id", nullable = false)
private Office office;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "series_id")
private ChequeSeries series;
@Column(name = "requested_date", nullable = false)
private LocalDate requestedDate;
@Column(name = "requested_by")
private Long requestedBy;
/** How many whole books the customer requested; leaves handed over = noOfBooks x series.leavesPerBook. */
@Column(name = "no_of_books", nullable = false)
private Integer noOfBooks;
/** Legacy free-form leaf count; superseded by {@link #noOfBooks}. Kept nullable for historical rows. */
@Column(name = "no_of_leaves_requested")
private Integer noOfLeavesRequested;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "issue_date")
private LocalDate issueDate;
@Column(name = "issued_by")
private Long issuedBy;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "lot_id")
private ChequeStockLot lot;
@Column(name = "range_start")
private Long rangeStart;
@Column(name = "range_end")
private Long rangeEnd;
@Column(name = "total_leaves")
private Integer totalLeaves;
@Column(name = "rejection_reason", length = 500)
private String rejectionReason;
@Column(name = "collection_acknowledged", nullable = false)
private boolean collectionAcknowledged;
@Column(name = "collected_date")
private LocalDate collectedDate;
@Column(name = "collected_by")
private Long collectedBy;
@Column(name = "created_on")
private OffsetDateTime createdOn;
@Version
@Column(name = "version")
private Integer version;
/**
* The customer/branch picks the denomination (series) at requisition time — matching real cheque-book
* requisition slips, which specify the book type up front rather than leaving it to the issuing teller
* to guess. {@code series} must already be applicable to {@code accountType} (checked by the caller).
*/
public static ChequeBook requestNew(final ChequeAccountType accountType, final Long accountId, final Office office,
final ChequeSeries series, final Integer noOfBooks, final Long requestedBy) {
final ChequeBook book = new ChequeBook();
book.accountType = accountType.name();
book.accountId = accountId;
book.office = office;
book.series = series;
book.noOfBooks = noOfBooks;
book.requestedDate = DateUtils.getBusinessLocalDate();
book.requestedBy = requestedBy;
book.status = ChequeBookStatus.REQUESTED.name();
book.collectionAcknowledged = false;
book.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return book;
}
private void assertRequested() {
if (!ChequeBookStatus.REQUESTED.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.not.requested",
"Cheque book " + this.getId() + " is not in REQUESTED status.", this.getId());
}
}
/** The series was already fixed at request time; issuing only draws stock and materializes the leaves. */
public void issue(final ChequeStockLot lot, final Long rangeStart, final Long rangeEnd, final Integer totalLeaves,
final Long issuedBy) {
assertRequested();
this.lot = lot;
this.rangeStart = rangeStart;
this.rangeEnd = rangeEnd;
this.totalLeaves = totalLeaves;
this.issueDate = DateUtils.getBusinessLocalDate();
this.issuedBy = issuedBy;
this.status = ChequeBookStatus.ISSUED.name();
}
public void reject(final String reason) {
assertRequested();
this.status = ChequeBookStatus.REJECTED.name();
this.rejectionReason = reason;
}
public void withdraw() {
if (!ChequeBookStatus.ISSUED.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.not.issued",
"Cheque book " + this.getId() + " is not in ISSUED status.", this.getId());
}
this.status = ChequeBookStatus.WITHDRAWN.name();
}
public void acknowledgeCollection(final Long collectedBy) {
this.collectionAcknowledged = true;
this.collectedBy = collectedBy;
this.collectedDate = DateUtils.getBusinessLocalDate();
}
public boolean isIssued() {
return ChequeBookStatus.ISSUED.name().equals(this.status);
}
}
@@ -0,0 +1,130 @@
/**
* 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.chequebook.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.useradministration.domain.AppUser;
/**
* Bespoke maker-checker for cheque-book creation actions (series master, stock receipt, book requisition) —
* modelled on {@code m_clerk_disbursement_request} rather than Fineract's native command-approval engine,
* because that engine's hold is bypassed by any role carrying ALL_FUNCTIONS (Super user, Clerk in this
* tenant), which made it a no-op for this CBS's actual admin accounts. A request holds the raw submission
* JSON; approval replays it through the same creation logic used elsewhere in this module. The maker can
* never approve/reject their own request — enforced in the write service, not just by role.
*/
@Entity
@Table(name = "m_chequebook_pending_request")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ChequeBookPendingRequest extends AbstractPersistableCustom<Long> {
@Column(name = "request_type", nullable = false, length = 30)
private String requestType;
@Column(name = "payload_json", nullable = false)
private String payloadJson;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "maker_id", nullable = false)
private Long makerId;
@Column(name = "maker_name", length = 150)
private String makerName;
@Column(name = "maker_username", length = 100)
private String makerUsername;
@Column(name = "submitted_on", nullable = false)
private OffsetDateTime submittedOn;
@Column(name = "checker_id")
private Long checkerId;
@Column(name = "checker_name", length = 150)
private String checkerName;
@Column(name = "checker_username", length = 100)
private String checkerUsername;
@Column(name = "checked_on")
private OffsetDateTime checkedOn;
@Column(name = "rejection_reason", length = 500)
private String rejectionReason;
@Column(name = "created_resource_id")
private Long createdResourceId;
@Column(name = "activity_log")
private String activityLog;
public static ChequeBookPendingRequest submit(final ChequeBookPendingRequestType requestType, final String payloadJson,
final AppUser maker, final String activityLog) {
final ChequeBookPendingRequest request = new ChequeBookPendingRequest();
request.requestType = requestType.name();
request.payloadJson = payloadJson;
request.status = ChequeBookPendingRequestStatus.PENDING.name();
request.makerId = maker.getId();
request.makerName = maker.getDisplayName();
request.makerUsername = maker.getUsername();
request.submittedOn = DateUtils.getOffsetDateTimeOfTenant();
request.activityLog = activityLog;
return request;
}
public boolean isPending() {
return ChequeBookPendingRequestStatus.PENDING.name().equals(this.status);
}
/** The maker can never be their own checker — a distinct human must approve/reject a request. */
public boolean isMadeBy(final Long userId) {
return this.makerId != null && this.makerId.equals(userId);
}
public void approve(final AppUser checker, final Long createdResourceId, final String activityLog) {
this.status = ChequeBookPendingRequestStatus.APPROVED.name();
this.checkerId = checker.getId();
this.checkerName = checker.getDisplayName();
this.checkerUsername = checker.getUsername();
this.checkedOn = DateUtils.getOffsetDateTimeOfTenant();
this.createdResourceId = createdResourceId;
this.activityLog = activityLog;
}
public void reject(final AppUser checker, final String reason, final String activityLog) {
this.status = ChequeBookPendingRequestStatus.REJECTED.name();
this.checkerId = checker.getId();
this.checkerName = checker.getDisplayName();
this.checkerUsername = checker.getUsername();
this.checkedOn = DateUtils.getOffsetDateTimeOfTenant();
this.rejectionReason = reason;
this.activityLog = activityLog;
}
}
@@ -0,0 +1,25 @@
/**
* 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.chequebook.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ChequeBookPendingRequestRepository extends JpaRepository<ChequeBookPendingRequest, Long> {
}
@@ -0,0 +1,38 @@
/**
* 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.chequebook.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.chequebook.exception.ChequeBookPendingRequestNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChequeBookPendingRequestRepositoryWrapper {
private final ChequeBookPendingRequestRepository repository;
public ChequeBookPendingRequest findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new ChequeBookPendingRequestNotFoundException(id));
}
public ChequeBookPendingRequest saveAndFlush(final ChequeBookPendingRequest request) {
return this.repository.saveAndFlush(request);
}
}
@@ -0,0 +1,27 @@
/**
* 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.chequebook.domain;
/** Lifecycle of a {@link ChequeBookPendingRequest}. Terminal once APPROVED or REJECTED. */
public enum ChequeBookPendingRequestStatus {
PENDING, //
APPROVED, //
REJECTED; //
}
@@ -0,0 +1,28 @@
/**
* 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.chequebook.domain;
/** Which cheque-book action a {@link ChequeBookPendingRequest} holds. */
public enum ChequeBookPendingRequestType {
CREATE_SERIES, //
RECEIVE_STOCK, //
REQUEST_BOOK, //
DELETE_SERIES; //
}
@@ -0,0 +1,25 @@
/**
* 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.chequebook.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ChequeBookRepository extends JpaRepository<ChequeBook, Long> {
}
@@ -0,0 +1,38 @@
/**
* 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.chequebook.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.chequebook.exception.ChequeBookNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChequeBookRepositoryWrapper {
private final ChequeBookRepository repository;
public ChequeBook findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new ChequeBookNotFoundException(id));
}
public ChequeBook saveAndFlush(final ChequeBook book) {
return this.repository.saveAndFlush(book);
}
}
@@ -0,0 +1,27 @@
/**
* 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.chequebook.domain;
public enum ChequeBookStatus {
REQUESTED, //
ISSUED, //
REJECTED, //
WITHDRAWN; //
}
@@ -0,0 +1,194 @@
/**
* 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.chequebook.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/**
* One physical cheque leaf. {@code chequeBook} is nullable: a leaf reported lost/stolen while still
* unissued stock (see {@link ChequeStockLot#reportRemainingLost(long)}) has no book yet, but still needs a
* row so the serial-number quick-lookup screen has exactly one code path for every case — issued-active,
* issued-stopped, issued-lost, and unissued-but-stolen.
*
* <p>
* Legal transitions: (created)-&gt;ACTIVE (issued) or (created, no book)-&gt;LOST (unissued theft report);
* ACTIVE&lt;-&gt;STOP_PAYMENT (the one round-trip); ACTIVE-&gt;CANCELLED / LOST / WITHDRAWN / USED (all terminal).
* A recovered "lost" cheque conventionally stays hotlisted rather than silently reactivating, since the
* customer has typically already been issued a replacement book. USED is manually recorded by a branch,
* never automatic — see {@link #markUsed(String, Long)}.
*/
@Entity
@Table(name = "m_cheque_leaf")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ChequeLeaf extends AbstractPersistableCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "cheque_book_id")
private ChequeBook chequeBook;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "lot_id", nullable = false)
private ChequeStockLot lot;
@Column(name = "leaf_serial_no", nullable = false)
private Long leafSerialNo;
@Column(name = "leaf_seq_no")
private Integer leafSeqNo;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "stop_reason", length = 500)
private String stopReason;
@Column(name = "stopped_by")
private Long stoppedBy;
@Column(name = "stopped_on")
private OffsetDateTime stoppedOn;
@Column(name = "lost_reason", length = 500)
private String lostReason;
@Column(name = "lost_reported_by")
private Long lostReportedBy;
@Column(name = "lost_reported_on")
private OffsetDateTime lostReportedOn;
@Column(name = "cancelled_reason", length = 500)
private String cancelledReason;
@Column(name = "cancelled_by")
private Long cancelledBy;
@Column(name = "cancelled_on")
private OffsetDateTime cancelledOn;
/** Manually recorded once the branch sees this leaf clear on the bank/clearing statement — never automatic. */
@Column(name = "used_reason", length = 500)
private String usedReason;
@Column(name = "used_by")
private Long usedBy;
@Column(name = "used_on")
private OffsetDateTime usedOn;
@Column(name = "created_on")
private OffsetDateTime createdOn;
public static ChequeLeaf issueForBook(final ChequeBook chequeBook, final ChequeStockLot lot, final Long leafSerialNo,
final Integer leafSeqNo) {
final ChequeLeaf leaf = new ChequeLeaf();
leaf.chequeBook = chequeBook;
leaf.lot = lot;
leaf.leafSerialNo = leafSerialNo;
leaf.leafSeqNo = leafSeqNo;
leaf.status = ChequeLeafStatus.ACTIVE.name();
leaf.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return leaf;
}
public static ChequeLeaf lostUnissued(final ChequeStockLot lot, final Long leafSerialNo, final String reason,
final Long reportedBy) {
final ChequeLeaf leaf = new ChequeLeaf();
leaf.lot = lot;
leaf.leafSerialNo = leafSerialNo;
leaf.status = ChequeLeafStatus.LOST.name();
leaf.lostReason = reason;
leaf.lostReportedBy = reportedBy;
leaf.lostReportedOn = DateUtils.getOffsetDateTimeOfTenant();
leaf.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return leaf;
}
private void assertActive() {
if (!ChequeLeafStatus.ACTIVE.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequeleaf.not.active",
"Cheque leaf " + this.leafSerialNo + " is not ACTIVE.", this.leafSerialNo);
}
}
public void stopPayment(final String reason, final Long stoppedBy) {
assertActive();
this.status = ChequeLeafStatus.STOP_PAYMENT.name();
this.stopReason = reason;
this.stoppedBy = stoppedBy;
this.stoppedOn = DateUtils.getOffsetDateTimeOfTenant();
}
public void revokeStopPayment() {
if (!ChequeLeafStatus.STOP_PAYMENT.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequeleaf.not.stopped",
"Cheque leaf " + this.leafSerialNo + " is not under stop-payment.", this.leafSerialNo);
}
this.status = ChequeLeafStatus.ACTIVE.name();
this.stopReason = null;
this.stoppedBy = null;
this.stoppedOn = null;
}
public void reportLost(final String reason, final Long reportedBy) {
assertActive();
this.status = ChequeLeafStatus.LOST.name();
this.lostReason = reason;
this.lostReportedBy = reportedBy;
this.lostReportedOn = DateUtils.getOffsetDateTimeOfTenant();
}
public void cancel(final String reason, final Long cancelledBy) {
assertActive();
this.status = ChequeLeafStatus.CANCELLED.name();
this.cancelledReason = reason;
this.cancelledBy = cancelledBy;
this.cancelledOn = DateUtils.getOffsetDateTimeOfTenant();
}
public void withdraw() {
assertActive();
this.status = ChequeLeafStatus.WITHDRAWN.name();
}
/**
* Manually recorded once a branch sees this leaf clear on the bank/clearing statement. Never set
* automatically — this CBS has no clearing/presentment integration to detect it on its own.
*/
public void markUsed(final String reason, final Long markedBy) {
assertActive();
this.status = ChequeLeafStatus.USED.name();
this.usedReason = reason;
this.usedBy = markedBy;
this.usedOn = DateUtils.getOffsetDateTimeOfTenant();
}
}
@@ -0,0 +1,33 @@
/**
* 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.chequebook.domain;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ChequeLeafRepository extends JpaRepository<ChequeLeaf, Long> {
Optional<ChequeLeaf> findByLeafSerialNo(Long leafSerialNo);
List<ChequeLeaf> findByChequeBookIdOrderByLeafSeqNoAsc(Long chequeBookId);
List<ChequeLeaf> findByChequeBookIdAndLeafSeqNoBetweenOrderByLeafSeqNoAsc(Long chequeBookId, Integer fromSeqNo,
Integer toSeqNo);
}
@@ -0,0 +1,38 @@
/**
* 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.chequebook.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.chequebook.exception.ChequeLeafNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChequeLeafRepositoryWrapper {
private final ChequeLeafRepository repository;
public ChequeLeaf findByLeafSerialNoWithNotFoundDetection(final Long leafSerialNo) {
return this.repository.findByLeafSerialNo(leafSerialNo).orElseThrow(() -> new ChequeLeafNotFoundException(leafSerialNo));
}
public ChequeLeaf saveAndFlush(final ChequeLeaf leaf) {
return this.repository.saveAndFlush(leaf);
}
}
@@ -0,0 +1,38 @@
/**
* 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.chequebook.domain;
/**
* Legal transitions: (created)-&gt;ACTIVE (issued) or (created, no book)-&gt;LOST (unissued stock reported
* stolen); ACTIVE&lt;-&gt;STOP_PAYMENT (the one round-trip, via stop-payment/revoke); ACTIVE-&gt;CANCELLED
* (spoiled leaf voided); ACTIVE-&gt;LOST (reported lost/stolen post-issuance); ACTIVE-&gt;WITHDRAWN (account
* closed); ACTIVE-&gt;USED (manually recorded once the branch sees the leaf clear on the bank/clearing
* statement — this CBS has no clearing/presentment integration, so USED can never be set automatically).
* LOST, CANCELLED, WITHDRAWN and USED are all terminal — a recovered "lost" cheque conventionally stays
* hotlisted since the customer has typically already been issued a replacement book.
*/
public enum ChequeLeafStatus {
ACTIVE, //
STOP_PAYMENT, //
CANCELLED, //
LOST, //
WITHDRAWN, //
USED; //
}
@@ -0,0 +1,111 @@
/**
* 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.chequebook.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
/** Cheque-leaf stationery master — leaves-per-book, CTS-2010 compliance, and a low-stock reorder threshold. */
@Entity
@Table(name = "m_cheque_series")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ChequeSeries extends AbstractPersistableCustom<Long> {
@Column(name = "series_code", nullable = false, length = 20)
private String seriesCode;
@Column(name = "name", nullable = false, length = 100)
private String name;
@Column(name = "leaves_per_book", nullable = false)
private Integer leavesPerBook;
/** SAVINGS / CURRENT — which account scheme this stationery may be issued to. */
@Column(name = "applicable_account_type", nullable = false, length = 10)
private String applicableAccountType;
@Column(name = "cts_compliant", nullable = false)
private boolean ctsCompliant;
@Column(name = "reorder_level")
private Integer reorderLevel;
@Column(name = "is_active", nullable = false)
private boolean active;
@Column(name = "created_by")
private Long createdBy;
@Column(name = "created_on")
private OffsetDateTime createdOn;
public static ChequeSeries createNew(final String seriesCode, final String name, final Integer leavesPerBook,
final ChequeSeriesApplicability applicableAccountType, final boolean ctsCompliant, final Integer reorderLevel,
final Long createdBy) {
final ChequeSeries series = new ChequeSeries();
series.seriesCode = seriesCode;
series.name = name;
series.leavesPerBook = leavesPerBook;
series.applicableAccountType = applicableAccountType.name();
series.ctsCompliant = ctsCompliant;
series.reorderLevel = reorderLevel;
series.active = true;
series.createdBy = createdBy;
series.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return series;
}
public void updateDetails(final String name, final Integer leavesPerBook, final ChequeSeriesApplicability applicableAccountType,
final Boolean ctsCompliant, final Integer reorderLevel) {
if (name != null) {
this.name = name;
}
if (leavesPerBook != null) {
this.leavesPerBook = leavesPerBook;
}
if (applicableAccountType != null) {
this.applicableAccountType = applicableAccountType.name();
}
if (ctsCompliant != null) {
this.ctsCompliant = ctsCompliant;
}
if (reorderLevel != null) {
this.reorderLevel = reorderLevel;
}
}
public void deactivate() {
this.active = false;
}
/** Whether this series' stationery may be issued to the given account type. */
public boolean appliesTo(final ChequeAccountType accountType) {
final ChequeSeriesApplicability applicability = ChequeSeriesApplicability.fromString(this.applicableAccountType);
// a legacy row with an unrecognised/blank value (e.g. a pre-existing "BOTH") is treated permissively
return applicability == null || applicability.appliesTo(accountType);
}
}
@@ -0,0 +1,53 @@
/**
* 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.chequebook.domain;
import org.apache.commons.lang3.StringUtils;
/**
* Which account scheme a cheque series' stationery is meant for. Real banks stock physically different
* cheque books for Savings (SB) and Current (CA) accounts (different leaf count, design, MICR transaction
* code), so every series is tagged SAVINGS-only or CURRENT-only — never both — and issuance is validated
* against it.
*/
public enum ChequeSeriesApplicability {
SAVINGS, //
CURRENT; //
public static ChequeSeriesApplicability fromString(final String value) {
if (StringUtils.isBlank(value)) {
return null;
}
for (final ChequeSeriesApplicability applicability : values()) {
if (applicability.name().equalsIgnoreCase(value.trim())) {
return applicability;
}
}
return null;
}
/** Whether a series with this applicability may be issued to the given account type. */
public boolean appliesTo(final ChequeAccountType accountType) {
if (accountType == null) {
return false;
}
return this.name().equals(accountType.name());
}
}
@@ -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.chequebook.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ChequeSeriesRepository extends JpaRepository<ChequeSeries, Long> {
boolean existsBySeriesCode(String seriesCode);
}
@@ -0,0 +1,38 @@
/**
* 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.chequebook.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.chequebook.exception.ChequeSeriesNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChequeSeriesRepositoryWrapper {
private final ChequeSeriesRepository repository;
public ChequeSeries findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new ChequeSeriesNotFoundException(id));
}
public ChequeSeries saveAndFlush(final ChequeSeries series) {
return this.repository.saveAndFlush(series);
}
}
@@ -0,0 +1,192 @@
/**
* 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.chequebook.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.office.domain.Office;
/**
* A contiguous range of cheque-leaf serials currently sitting at one office, with a drawdown cursor
* ({@link #nextSerialNo}) that advances every time a sub-range is issued to a customer or transferred to
* another office — both are structurally the same "take N off the front of the remaining range" operation.
* This single concept covers both an HO bulk receipt from the press and a branch's received-transfer-in
* stock; a transfer-in is materially the same thing as a receipt, just sourced from another office.
*/
@Entity
@Table(name = "m_cheque_stock_lot")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ChequeStockLot extends AbstractPersistableCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "series_id", nullable = false)
private ChequeSeries series;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "office_id", nullable = false)
private Office office;
@Column(name = "range_start", nullable = false)
private Long rangeStart;
@Column(name = "range_end", nullable = false)
private Long rangeEnd;
@Column(name = "next_serial_no", nullable = false)
private Long nextSerialNo;
@Column(name = "source", nullable = false, length = 20)
private String source;
@Column(name = "source_ref_id")
private Long sourceRefId;
@Column(name = "batch_ref", length = 100)
private String batchRef;
@Column(name = "status", nullable = false, length = 25)
private String status;
@Column(name = "created_by")
private Long createdBy;
@Column(name = "created_on")
private OffsetDateTime createdOn;
@Column(name = "received_by")
private Long receivedBy;
@Column(name = "received_on")
private OffsetDateTime receivedOn;
@Version
@Column(name = "version")
private Integer version;
public static ChequeStockLot createReceipt(final ChequeSeries series, final Office office, final Long rangeStart,
final Long rangeEnd, final String batchRef, final Long createdBy) {
final ChequeStockLot lot = new ChequeStockLot();
lot.series = series;
lot.office = office;
lot.rangeStart = rangeStart;
lot.rangeEnd = rangeEnd;
lot.nextSerialNo = rangeStart;
lot.source = ChequeStockLotSource.RECEIPT.name();
lot.batchRef = batchRef;
lot.status = ChequeStockLotStatus.AVAILABLE.name();
lot.createdBy = createdBy;
lot.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return lot;
}
public static ChequeStockLot createTransferIn(final ChequeSeries series, final Office office, final Long rangeStart,
final Long rangeEnd, final Long transferId) {
final ChequeStockLot lot = new ChequeStockLot();
lot.series = series;
lot.office = office;
lot.rangeStart = rangeStart;
lot.rangeEnd = rangeEnd;
lot.nextSerialNo = rangeStart;
lot.source = ChequeStockLotSource.TRANSFER_IN.name();
lot.sourceRefId = transferId;
lot.status = ChequeStockLotStatus.DISPATCHED.name();
lot.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return lot;
}
/** Leaves still available to draw down, regardless of status. */
public long remainingLeaves() {
return this.rangeEnd - this.nextSerialNo + 1;
}
/**
* Takes the next {@code qty} leaf serials off the front of this lot's remaining range, advancing the
* cursor and marking the lot EXHAUSTED if nothing remains. Used identically for issuing a book to a
* customer and for dispatching a sub-range to another office.
*/
public long[] drawDown(final int qty) {
if (!ChequeStockLotStatus.AVAILABLE.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.not.available",
"Cheque stock lot " + this.getId() + " is not available for drawdown.", this.getId());
}
if (remainingLeaves() < qty) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.insufficient.stock",
"Cheque stock lot " + this.getId() + " does not have enough remaining leaves.", this.getId());
}
final long start = this.nextSerialNo;
final long end = start + qty - 1;
this.nextSerialNo = end + 1;
if (this.nextSerialNo > this.rangeEnd) {
this.status = ChequeStockLotStatus.EXHAUSTED.name();
}
return new long[] { start, end };
}
/** Transfer-in receive-ack: the destination lot becomes usable at its new custodian office. */
public void markAvailable(final Long receivedBy) {
if (!ChequeStockLotStatus.DISPATCHED.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.not.dispatched",
"Cheque stock lot " + this.getId() + " is not awaiting receipt.", this.getId());
}
this.status = ChequeStockLotStatus.AVAILABLE.name();
this.receivedBy = receivedBy;
this.receivedOn = DateUtils.getOffsetDateTimeOfTenant();
}
/** Transfer rejected in transit: the destination lot never becomes usable. */
public void cancel() {
this.status = ChequeStockLotStatus.CANCELLED.name();
}
/**
* Reports the remaining undrawn stock from {@code fromSerialNo} onward as lost/stolen. Only a suffix of
* the remaining range can be reported (an arbitrary internal sub-range would imply a custody failure
* severe enough that manual reconciliation, not an automatic system action, is the honest response).
* Returns the pre-mutation range end so the caller can generate LOST leaf rows for
* [fromSerialNo, returned value].
*/
public long reportRemainingLost(final long fromSerialNo) {
if (fromSerialNo < this.nextSerialNo || fromSerialNo > this.rangeEnd) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.invalid.report.range",
"The reported range must be a suffix of the lot's remaining undrawn stock.", this.getId());
}
final long originalRangeEnd = this.rangeEnd;
if (fromSerialNo == this.nextSerialNo) {
this.nextSerialNo = this.rangeEnd + 1;
this.status = ChequeStockLotStatus.CANCELLED.name();
} else {
this.rangeEnd = fromSerialNo - 1;
this.status = ChequeStockLotStatus.PARTIALLY_COMPROMISED.name();
}
return originalRangeEnd;
}
}
@@ -0,0 +1,39 @@
/**
* 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.chequebook.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface ChequeStockLotRepository extends JpaRepository<ChequeStockLot, Long> {
List<ChequeStockLot> findBySeriesIdAndOfficeIdAndStatusOrderByIdAsc(Long seriesId, Long officeId, String status);
/**
* Anti-duplication control: a newly received range must not overlap any existing lot's range within the SAME
* series. Each series is its own stationery batch from the press (different leaf count/paper stock) and is
* numbered independently — two different series legitimately reusing the same numeric range is normal in
* real-world cheque stock, matching how the press assigns a fresh serial block per print order.
*/
@Query("SELECT COUNT(l) FROM ChequeStockLot l WHERE l.series.id = :seriesId AND l.rangeStart <= :rangeEnd AND l.rangeEnd >= :rangeStart")
long countOverlappingInSeries(@Param("seriesId") long seriesId, @Param("rangeStart") long rangeStart,
@Param("rangeEnd") long rangeEnd);
}
@@ -0,0 +1,38 @@
/**
* 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.chequebook.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.chequebook.exception.ChequeStockLotNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChequeStockLotRepositoryWrapper {
private final ChequeStockLotRepository repository;
public ChequeStockLot findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new ChequeStockLotNotFoundException(id));
}
public ChequeStockLot saveAndFlush(final ChequeStockLot lot) {
return this.repository.saveAndFlush(lot);
}
}
@@ -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.chequebook.domain;
/** How a {@link ChequeStockLot} came to exist at its current custodian office. */
public enum ChequeStockLotSource {
RECEIPT, //
TRANSFER_IN; //
}
@@ -0,0 +1,34 @@
/**
* 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.chequebook.domain;
/**
* Lifecycle of a {@link ChequeStockLot}. DISPATCHED = in transit to another office, awaiting receive-ack.
* AVAILABLE = usable at this office for issuance or further transfer-out. EXHAUSTED = fully drawn down.
* PARTIALLY_COMPROMISED = a suffix of the remaining range was reported lost/stolen, but a front portion is
* still issuable. CANCELLED = fully voided (rejected transfer, or entirely reported lost/stolen).
*/
public enum ChequeStockLotStatus {
DISPATCHED, //
AVAILABLE, //
EXHAUSTED, //
PARTIALLY_COMPROMISED, //
CANCELLED; //
}
@@ -0,0 +1,133 @@
/**
* 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.chequebook.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.office.domain.Office;
/**
* Audit/workflow record of a stock movement between offices. The actual range drawdown at the source and
* the destination lot creation happen as part of dispatch (see write-service); this row tracks the
* dispatch → receive-ack (or reject) lifecycle.
*/
@Entity
@Table(name = "m_cheque_stock_transfer")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ChequeStockTransfer extends AbstractPersistableCustom<Long> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "series_id", nullable = false)
private ChequeSeries series;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "from_office_id", nullable = false)
private Office fromOffice;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "to_office_id", nullable = false)
private Office toOffice;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "from_lot_id", nullable = false)
private ChequeStockLot fromLot;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "to_lot_id", nullable = false)
private ChequeStockLot toLot;
@Column(name = "range_start", nullable = false)
private Long rangeStart;
@Column(name = "range_end", nullable = false)
private Long rangeEnd;
@Column(name = "total_leaves", nullable = false)
private Integer totalLeaves;
@Column(name = "dispatch_date", nullable = false)
private LocalDate dispatchDate;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "received_date")
private LocalDate receivedDate;
@Column(name = "dispatched_by")
private Long dispatchedBy;
@Column(name = "received_by")
private Long receivedBy;
@Column(name = "created_on")
private OffsetDateTime createdOn;
public static ChequeStockTransfer createNew(final ChequeSeries series, final Office fromOffice, final Office toOffice,
final ChequeStockLot fromLot, final ChequeStockLot toLot, final Long rangeStart, final Long rangeEnd,
final Integer totalLeaves, final LocalDate dispatchDate, final Long dispatchedBy) {
final ChequeStockTransfer transfer = new ChequeStockTransfer();
transfer.series = series;
transfer.fromOffice = fromOffice;
transfer.toOffice = toOffice;
transfer.fromLot = fromLot;
transfer.toLot = toLot;
transfer.rangeStart = rangeStart;
transfer.rangeEnd = rangeEnd;
transfer.totalLeaves = totalLeaves;
transfer.dispatchDate = dispatchDate;
transfer.status = ChequeStockTransferStatus.DISPATCHED.name();
transfer.dispatchedBy = dispatchedBy;
transfer.createdOn = DateUtils.getOffsetDateTimeOfTenant();
return transfer;
}
public void markReceived(final Long receivedBy, final LocalDate receivedDate) {
if (!ChequeStockTransferStatus.DISPATCHED.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocktransfer.not.dispatched",
"Transfer " + this.getId() + " is not awaiting receipt.", this.getId());
}
this.status = ChequeStockTransferStatus.RECEIVED.name();
this.receivedBy = receivedBy;
this.receivedDate = receivedDate;
}
public void reject(final Long rejectedBy, final LocalDate rejectedDate) {
if (!ChequeStockTransferStatus.DISPATCHED.name().equals(this.status)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocktransfer.not.dispatched",
"Transfer " + this.getId() + " is not awaiting receipt.", this.getId());
}
this.status = ChequeStockTransferStatus.REJECTED.name();
this.receivedBy = rejectedBy;
this.receivedDate = rejectedDate;
}
}
@@ -0,0 +1,25 @@
/**
* 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.chequebook.domain;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ChequeStockTransferRepository extends JpaRepository<ChequeStockTransfer, Long> {
}
@@ -0,0 +1,38 @@
/**
* 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.chequebook.domain;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.chequebook.exception.ChequeStockTransferNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChequeStockTransferRepositoryWrapper {
private final ChequeStockTransferRepository repository;
public ChequeStockTransfer findOneWithNotFoundDetection(final Long id) {
return this.repository.findById(id).orElseThrow(() -> new ChequeStockTransferNotFoundException(id));
}
public ChequeStockTransfer saveAndFlush(final ChequeStockTransfer transfer) {
return this.repository.saveAndFlush(transfer);
}
}
@@ -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.chequebook.domain;
public enum ChequeStockTransferStatus {
DISPATCHED, //
RECEIVED, //
REJECTED; //
}
@@ -0,0 +1,28 @@
/**
* 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.chequebook.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ChequeBookNotFoundException extends AbstractPlatformResourceNotFoundException {
public ChequeBookNotFoundException(final Long id) {
super("error.msg.chequebook.id.invalid", "Cheque book with identifier " + id + " does not exist", id);
}
}
@@ -0,0 +1,29 @@
/**
* 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.chequebook.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ChequeBookPendingRequestNotFoundException extends AbstractPlatformResourceNotFoundException {
public ChequeBookPendingRequestNotFoundException(final Long id) {
super("error.msg.chequebookpendingrequest.id.invalid", "Cheque book pending request with identifier " + id + " does not exist",
id);
}
}
@@ -0,0 +1,28 @@
/**
* 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.chequebook.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ChequeLeafNotFoundException extends AbstractPlatformResourceNotFoundException {
public ChequeLeafNotFoundException(final Long leafSerialNo) {
super("error.msg.chequeleaf.serialno.invalid", "No cheque leaf found with serial number " + leafSerialNo, leafSerialNo);
}
}
@@ -0,0 +1,28 @@
/**
* 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.chequebook.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ChequeSeriesNotFoundException extends AbstractPlatformResourceNotFoundException {
public ChequeSeriesNotFoundException(final Long id) {
super("error.msg.chequeseries.id.invalid", "Cheque series with identifier " + id + " does not exist", id);
}
}
@@ -0,0 +1,28 @@
/**
* 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.chequebook.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ChequeStockLotNotFoundException extends AbstractPlatformResourceNotFoundException {
public ChequeStockLotNotFoundException(final Long id) {
super("error.msg.chequestocklot.id.invalid", "Cheque stock lot with identifier " + id + " does not exist", id);
}
}
@@ -0,0 +1,28 @@
/**
* 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.chequebook.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
public class ChequeStockTransferNotFoundException extends AbstractPlatformResourceNotFoundException {
public ChequeStockTransferNotFoundException(final Long id) {
super("error.msg.chequestocktransfer.id.invalid", "Cheque stock transfer with identifier " + id + " does not exist", id);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOKPENDINGREQUEST", action = "APPROVE")
@RequiredArgsConstructor
public class ApproveChequeBookPendingRequestCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.approvePendingRequest(command.entityId());
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "CANCELLEAF")
@RequiredArgsConstructor
public class CancelLeafChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.cancelLeaf(command.entityId(), command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "CREATE")
@RequiredArgsConstructor
public class CreateChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.requestChequeBook(command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESERIES", action = "CREATE")
@RequiredArgsConstructor
public class CreateChequeSeriesCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.createSeries(command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESTOCKLOT", action = "CREATE")
@RequiredArgsConstructor
public class CreateChequeStockLotCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.receiveStock(command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESTOCKTRANSFER", action = "CREATE")
@RequiredArgsConstructor
public class CreateChequeStockTransferCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.dispatchStockTransfer(command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESERIES", action = "DELETE")
@RequiredArgsConstructor
public class DeleteChequeSeriesCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.deleteSeries(command.entityId());
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "ISSUE")
@RequiredArgsConstructor
public class IssueChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.issueChequeBook(command.entityId(), command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "MARKUSED")
@RequiredArgsConstructor
public class MarkUsedChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.markLeafUsed(command.entityId(), command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESTOCKTRANSFER", action = "RECEIVE")
@RequiredArgsConstructor
public class ReceiveChequeStockTransferCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.receiveStockTransfer(command.entityId(), command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "REJECT")
@RequiredArgsConstructor
public class RejectChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.rejectChequeBook(command.entityId(), command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOKPENDINGREQUEST", action = "REJECT")
@RequiredArgsConstructor
public class RejectChequeBookPendingRequestCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.rejectPendingRequest(command.entityId(), command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESTOCKTRANSFER", action = "REJECT")
@RequiredArgsConstructor
public class RejectChequeStockTransferCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.rejectStockTransfer(command.entityId(), command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "REPORTLOST")
@RequiredArgsConstructor
public class ReportLostChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.reportChequeBookLost(command.entityId(), command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESTOCKLOT", action = "REPORTLOST")
@RequiredArgsConstructor
public class ReportLostChequeStockLotCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.reportStockLotLost(command.entityId(), command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "REVOKESTOPPAYMENT")
@RequiredArgsConstructor
public class RevokeStopPaymentChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.revokeStopPayment(command.entityId(), command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "STOPPAYMENT")
@RequiredArgsConstructor
public class StopPaymentChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.stopPayment(command.entityId(), command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUESERIES", action = "UPDATE")
@RequiredArgsConstructor
public class UpdateChequeSeriesCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.updateSeries(command.entityId(), command);
}
}
@@ -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.chequebook.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.chequebook.service.ChequeBookWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CHEQUEBOOK", action = "WITHDRAW")
@RequiredArgsConstructor
public class WithdrawChequeBookCommandHandler implements NewCommandSourceHandler {
private final ChequeBookWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.withdrawChequeBook(command.entityId(), command);
}
}
@@ -0,0 +1,267 @@
/**
* 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.chequebook.serialization;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
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.portfolio.chequebook.api.ChequeBookApiConstants;
import org.apache.fineract.portfolio.chequebook.api.ChequeBookApiJsonInputParams;
import org.apache.fineract.portfolio.chequebook.domain.ChequeAccountType;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public final class ChequeBookCommandFromApiJsonDeserializer {
private final FromJsonHelper fromApiJsonHelper;
public void validateForCreateSeries(final String json) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.seriesCreateParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_SERIES);
final String seriesCode = this.fromApiJsonHelper.extractStringNamed(ChequeBookApiJsonInputParams.SERIES_CODE.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.SERIES_CODE.getValue()).value(seriesCode).notBlank().notExceedingLengthOf(20);
final String name = this.fromApiJsonHelper.extractStringNamed(ChequeBookApiJsonInputParams.NAME.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.NAME.getValue()).value(name).notBlank().notExceedingLengthOf(100);
final Integer leavesPerBook = this.fromApiJsonHelper
.extractIntegerSansLocaleNamed(ChequeBookApiJsonInputParams.LEAVES_PER_BOOK.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.LEAVES_PER_BOOK.getValue()).value(leavesPerBook).notNull()
.integerGreaterThanZero();
final Integer reorderLevel = this.fromApiJsonHelper
.extractIntegerSansLocaleNamed(ChequeBookApiJsonInputParams.REORDER_LEVEL.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.REORDER_LEVEL.getValue()).value(reorderLevel).ignoreIfNull()
.integerGreaterThanZero();
validateApplicableAccountType(element, v, true);
throwIfErrors(errors);
}
public void validateForUpdateSeries(final String json) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.seriesUpdateParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_SERIES);
final Integer leavesPerBook = this.fromApiJsonHelper
.extractIntegerSansLocaleNamed(ChequeBookApiJsonInputParams.LEAVES_PER_BOOK.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.LEAVES_PER_BOOK.getValue()).value(leavesPerBook).ignoreIfNull()
.integerGreaterThanZero();
final Integer reorderLevel = this.fromApiJsonHelper
.extractIntegerSansLocaleNamed(ChequeBookApiJsonInputParams.REORDER_LEVEL.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.REORDER_LEVEL.getValue()).value(reorderLevel).ignoreIfNull()
.integerGreaterThanZero();
validateApplicableAccountType(element, v, false);
throwIfErrors(errors);
}
private void validateApplicableAccountType(final JsonElement element, final DataValidatorBuilder v, final boolean required) {
final String applicableAccountType = this.fromApiJsonHelper
.extractStringNamed(ChequeBookApiJsonInputParams.APPLICABLE_ACCOUNT_TYPE.getValue(), element);
// NB: isOneOfTheseStringValues lowercases the input before comparing, so the expected set must be lowercase
final DataValidatorBuilder builder = v.reset().parameter(ChequeBookApiJsonInputParams.APPLICABLE_ACCOUNT_TYPE.getValue())
.value(applicableAccountType);
if (required) {
builder.notBlank();
} else {
builder.ignoreIfNull();
}
builder.isOneOfTheseStringValues("savings", "current");
}
public void validateForStockLotReceive(final String json) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.stockLotReceiveParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_STOCK_LOT);
final Long seriesId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.SERIES_ID.getValue()).value(seriesId).notNull().longGreaterThanZero();
final Long officeId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.OFFICE_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.OFFICE_ID.getValue()).value(officeId).notNull().longGreaterThanZero();
final Long rangeStart = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.RANGE_START.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.RANGE_START.getValue()).value(rangeStart).notNull().longGreaterThanZero();
final Long rangeEnd = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.RANGE_END.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.RANGE_END.getValue()).value(rangeEnd).notNull().longGreaterThanZero();
if (rangeStart != null && rangeEnd != null && rangeEnd <= rangeStart) {
v.reset().parameter(ChequeBookApiJsonInputParams.RANGE_END.getValue()).value(rangeEnd)
.failWithCode("must.be.greater.than.range.start", "rangeEnd must be greater than rangeStart");
}
throwIfErrors(errors);
}
public void validateForStockLotReportLost(final String json) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.stockLotReportLostParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_STOCK_LOT);
final Long fromSerialNo = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.FROM_SERIAL_NO.getValue(),
element);
v.reset().parameter(ChequeBookApiJsonInputParams.FROM_SERIAL_NO.getValue()).value(fromSerialNo).notNull().longGreaterThanZero();
final String reason = this.fromApiJsonHelper.extractStringNamed(ChequeBookApiJsonInputParams.REASON.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.REASON.getValue()).value(reason).notBlank().notExceedingLengthOf(500);
throwIfErrors(errors);
}
public void validateForStockTransferDispatch(final String json) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.stockTransferDispatchParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_STOCK_TRANSFER);
final Long seriesId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.SERIES_ID.getValue()).value(seriesId).notNull().longGreaterThanZero();
final Long fromOfficeId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.FROM_OFFICE_ID.getValue(),
element);
v.reset().parameter(ChequeBookApiJsonInputParams.FROM_OFFICE_ID.getValue()).value(fromOfficeId).notNull().longGreaterThanZero();
final Long toOfficeId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.TO_OFFICE_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.TO_OFFICE_ID.getValue()).value(toOfficeId).notNull().longGreaterThanZero();
if (fromOfficeId != null && fromOfficeId.equals(toOfficeId)) {
v.reset().parameter(ChequeBookApiJsonInputParams.TO_OFFICE_ID.getValue()).value(toOfficeId)
.failWithCode("must.differ.from.from.office", "toOfficeId must differ from fromOfficeId");
}
final Long fromLotId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.FROM_LOT_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.FROM_LOT_ID.getValue()).value(fromLotId).notNull().longGreaterThanZero();
final Integer noOfLeaves = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(
ChequeBookApiJsonInputParams.NO_OF_LEAVES.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.NO_OF_LEAVES.getValue()).value(noOfLeaves).notNull().integerGreaterThanZero();
throwIfErrors(errors);
}
public void validateForBookRequest(final String json) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.bookRequestParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_BOOK);
final String accountType = this.fromApiJsonHelper.extractStringNamed(ChequeBookApiJsonInputParams.ACCOUNT_TYPE.getValue(),
element);
v.reset().parameter(ChequeBookApiJsonInputParams.ACCOUNT_TYPE.getValue()).value(accountType).notBlank();
if (StringUtils.isNotBlank(accountType) && ChequeAccountType.fromString(accountType) == null) {
v.reset().parameter(ChequeBookApiJsonInputParams.ACCOUNT_TYPE.getValue()).value(accountType)
.failWithCode("not.one.of.expected.enumerations", "SAVINGS, CURRENT");
}
final Long accountId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.ACCOUNT_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.ACCOUNT_ID.getValue()).value(accountId).notNull().longGreaterThanZero();
final Long officeId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.OFFICE_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.OFFICE_ID.getValue()).value(officeId).notNull().longGreaterThanZero();
final Integer noOfBooks = this.fromApiJsonHelper
.extractIntegerSansLocaleNamed(ChequeBookApiJsonInputParams.NO_OF_BOOKS.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.NO_OF_BOOKS.getValue()).value(noOfBooks).notNull().integerGreaterThanZero();
// the denomination is picked at requisition time, matching a real cheque-book request slip
final Long seriesId = this.fromApiJsonHelper.extractLongNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.SERIES_ID.getValue()).value(seriesId).notNull().longGreaterThanZero();
throwIfErrors(errors);
}
/** Issue no longer takes a series — it was already fixed at request time; only optional collection-ack. */
public void validateForBookIssue(final String json) {
parse(json, ChequeBookApiJsonInputParams.bookIssueParams());
}
public void validateForBookReject(final String json) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.bookRejectParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_BOOK);
final String reason = this.fromApiJsonHelper.extractStringNamed(ChequeBookApiJsonInputParams.REJECTION_REASON.getValue(),
element);
v.reset().parameter(ChequeBookApiJsonInputParams.REJECTION_REASON.getValue()).value(reason).notBlank().notExceedingLengthOf(500);
throwIfErrors(errors);
}
/** Shared by stop-payment / revoke-stop-payment / report-lost / cancel-leaf on a book. */
public void validateForBookLeafAction(final String json, final boolean requireReason) {
final JsonElement element = parse(json, ChequeBookApiJsonInputParams.bookLeafActionParams());
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ChequeBookApiConstants.RESOURCE_BOOK);
final Integer fromLeafSeq = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(
ChequeBookApiJsonInputParams.FROM_LEAF_SEQ.getValue(), element);
v.reset().parameter(ChequeBookApiJsonInputParams.FROM_LEAF_SEQ.getValue()).value(fromLeafSeq).ignoreIfNull()
.integerGreaterThanZero();
final Integer toLeafSeq = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(ChequeBookApiJsonInputParams.TO_LEAF_SEQ.getValue(),
element);
v.reset().parameter(ChequeBookApiJsonInputParams.TO_LEAF_SEQ.getValue()).value(toLeafSeq).ignoreIfNull()
.integerGreaterThanZero();
if (fromLeafSeq != null && toLeafSeq != null && toLeafSeq < fromLeafSeq) {
v.reset().parameter(ChequeBookApiJsonInputParams.TO_LEAF_SEQ.getValue()).value(toLeafSeq)
.failWithCode("must.be.greater.than.or.equal.to.from.leaf.seq", "toLeafSeq must not be less than fromLeafSeq");
}
final String reason = this.fromApiJsonHelper.extractStringNamed(ChequeBookApiJsonInputParams.REASON.getValue(), element);
final DataValidatorBuilder reasonValidator = v.reset().parameter(ChequeBookApiJsonInputParams.REASON.getValue()).value(reason);
if (requireReason) {
reasonValidator.notBlank().notExceedingLengthOf(500);
} else {
reasonValidator.ignoreIfNull().notExceedingLengthOf(500);
}
throwIfErrors(errors);
}
private JsonElement parse(final String json, final Set<String> supportedParams) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParams);
return this.fromApiJsonHelper.parse(json);
}
private void throwIfErrors(final List<ApiParameterError> errors) {
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
}
@@ -0,0 +1,63 @@
/**
* 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.chequebook.service;
import java.util.List;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.portfolio.chequebook.data.ChequeBookData;
import org.apache.fineract.portfolio.chequebook.data.ChequeBookPendingRequestData;
import org.apache.fineract.portfolio.chequebook.data.ChequeLeafData;
import org.apache.fineract.portfolio.chequebook.data.ChequeSeriesData;
import org.apache.fineract.portfolio.chequebook.data.ChequeStockLotData;
import org.apache.fineract.portfolio.chequebook.data.ChequeStockTransferData;
public interface ChequeBookReadPlatformService {
/** {@code status}: "ACTIVE" / "INACTIVE" — maps to the series' is_active flag. */
Page<ChequeSeriesData> retrieveAllSeries(String status, Integer offset, Integer limit);
ChequeSeriesData retrieveSeries(Long id);
Page<ChequeStockLotData> retrieveStockLots(Long officeId, Long seriesId, String status, Integer offset, Integer limit);
ChequeStockLotData retrieveStockLot(Long id);
Page<ChequeStockTransferData> retrieveStockTransfers(String status, Integer offset, Integer limit);
ChequeStockTransferData retrieveStockTransfer(Long id);
Page<ChequeBookData> retrieveAllBooks(String accountType, Long accountId, Long officeId, String status, Integer offset,
Integer limit);
ChequeBookData retrieveBook(Long id);
List<ChequeLeafData> retrieveLeaves(Long chequeBookId);
/**
* Quick-lookup — the single teller/counter operation that answers "is this cheque number safe to honour".
* A bare serial number is only unique within its own series, so more than one series can legitimately match.
*/
List<ChequeLeafData> retrieveLeafBySerialNo(Long leafSerialNo);
/** Bespoke maker-checker queue for series/stock/book creation requests — see {@code m_chequebook_pending_request}. */
Page<ChequeBookPendingRequestData> retrievePendingRequests(String status, String fromDate, String toDate, Integer offset,
Integer limit);
ChequeBookPendingRequestData retrievePendingRequest(Long id);
}
@@ -0,0 +1,498 @@
/**
* 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.chequebook.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.portfolio.chequebook.data.ChequeBookData;
import org.apache.fineract.portfolio.chequebook.data.ChequeBookPendingRequestData;
import org.apache.fineract.portfolio.chequebook.data.ChequeLeafData;
import org.apache.fineract.portfolio.chequebook.data.ChequeSeriesData;
import org.apache.fineract.portfolio.chequebook.data.ChequeStockLotData;
import org.apache.fineract.portfolio.chequebook.data.ChequeStockTransferData;
import org.apache.fineract.portfolio.chequebook.domain.ChequeAccountType;
import org.apache.fineract.portfolio.chequebook.exception.ChequeBookNotFoundException;
import org.apache.fineract.portfolio.chequebook.exception.ChequeBookPendingRequestNotFoundException;
import org.apache.fineract.portfolio.chequebook.exception.ChequeLeafNotFoundException;
import org.apache.fineract.portfolio.chequebook.exception.ChequeSeriesNotFoundException;
import org.apache.fineract.portfolio.chequebook.exception.ChequeStockLotNotFoundException;
import org.apache.fineract.portfolio.chequebook.exception.ChequeStockTransferNotFoundException;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ChequeBookReadPlatformServiceImpl implements ChequeBookReadPlatformService {
private final JdbcTemplate jdbcTemplate;
private final SavingsAccountRepository savingsAccountRepository;
private final CurrentAccountRepository currentAccountRepository;
private final SeriesMapper seriesMapper = new SeriesMapper();
private final StockLotMapper stockLotMapper = new StockLotMapper();
private final StockTransferMapper stockTransferMapper = new StockTransferMapper();
private final BookMapper bookMapper = new BookMapper();
private final LeafMapper leafMapper = new LeafMapper();
private final PendingRequestMapper pendingRequestMapper = new PendingRequestMapper();
@Override
public Page<ChequeSeriesData> retrieveAllSeries(final String status, final Integer offset, final Integer limit) {
final StringBuilder where = new StringBuilder(" where 1=1");
final List<Object> args = new ArrayList<>();
if ("ACTIVE".equalsIgnoreCase(status)) {
where.append(" and is_active = true");
} else if ("INACTIVE".equalsIgnoreCase(status)) {
where.append(" and is_active = false");
}
final String countSql = "select count(*) from (select " + this.seriesMapper.schema() + where + ") x";
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
final StringBuilder pageSql = new StringBuilder("select ").append(this.seriesMapper.schema()).append(where)
.append(" order by id desc limit ? offset ?");
final List<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(limit);
pageArgs.add(offset);
final List<ChequeSeriesData> series = this.jdbcTemplate.query(pageSql.toString(), this.seriesMapper, pageArgs.toArray());
return new Page<>(series, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
}
@Override
public ChequeSeriesData retrieveSeries(final Long id) {
try {
final String sql = "select " + this.seriesMapper.schema() + " where id = ?";
return this.jdbcTemplate.queryForObject(sql, this.seriesMapper, id);
} catch (final EmptyResultDataAccessException e) {
throw new ChequeSeriesNotFoundException(id);
}
}
@Override
public Page<ChequeStockLotData> retrieveStockLots(final Long officeId, final Long seriesId, final String status,
final Integer offset, final Integer limit) {
final StringBuilder where = new StringBuilder(" where 1=1");
final List<Object> args = new ArrayList<>();
if (officeId != null) {
where.append(" and l.office_id = ?");
args.add(officeId);
}
if (seriesId != null) {
where.append(" and l.series_id = ?");
args.add(seriesId);
}
if (StringUtils.isNotBlank(status)) {
where.append(" and l.status = ?");
args.add(status.trim().toUpperCase());
}
final String countSql = "select count(*) from (select " + this.stockLotMapper.schema() + where + ") x";
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
final StringBuilder pageSql = new StringBuilder("select ").append(this.stockLotMapper.schema()).append(where)
.append(" order by l.id desc limit ? offset ?");
final List<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(limit);
pageArgs.add(offset);
final List<ChequeStockLotData> lots = this.jdbcTemplate.query(pageSql.toString(), this.stockLotMapper, pageArgs.toArray());
return new Page<>(lots, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
}
@Override
public ChequeStockLotData retrieveStockLot(final Long id) {
try {
final String sql = "select " + this.stockLotMapper.schema() + " where l.id = ?";
return this.jdbcTemplate.queryForObject(sql, this.stockLotMapper, id);
} catch (final EmptyResultDataAccessException e) {
throw new ChequeStockLotNotFoundException(id);
}
}
@Override
public Page<ChequeStockTransferData> retrieveStockTransfers(final String status, final Integer offset, final Integer limit) {
final StringBuilder where = new StringBuilder(" where 1=1");
final List<Object> args = new ArrayList<>();
if (StringUtils.isNotBlank(status)) {
where.append(" and t.status = ?");
args.add(status.trim().toUpperCase());
}
final String countSql = "select count(*) from (select " + this.stockTransferMapper.schema() + where + ") x";
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
final StringBuilder pageSql = new StringBuilder("select ").append(this.stockTransferMapper.schema()).append(where)
.append(" order by t.id desc limit ? offset ?");
final List<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(limit);
pageArgs.add(offset);
final List<ChequeStockTransferData> transfers = this.jdbcTemplate.query(pageSql.toString(), this.stockTransferMapper,
pageArgs.toArray());
return new Page<>(transfers, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
}
@Override
public ChequeStockTransferData retrieveStockTransfer(final Long id) {
try {
final String sql = "select " + this.stockTransferMapper.schema() + " where t.id = ?";
return this.jdbcTemplate.queryForObject(sql, this.stockTransferMapper, id);
} catch (final EmptyResultDataAccessException e) {
throw new ChequeStockTransferNotFoundException(id);
}
}
@Override
public Page<ChequeBookData> retrieveAllBooks(final String accountType, final Long accountId, final Long officeId,
final String status, final Integer offset, final Integer limit) {
final StringBuilder where = new StringBuilder(" where 1=1");
final List<Object> args = new ArrayList<>();
if (StringUtils.isNotBlank(accountType)) {
where.append(" and b.account_type = ?");
args.add(accountType.trim().toUpperCase());
}
if (accountId != null) {
where.append(" and b.account_id = ?");
args.add(accountId);
}
if (officeId != null) {
where.append(" and b.office_id = ?");
args.add(officeId);
}
if (StringUtils.isNotBlank(status)) {
where.append(" and b.status = ?");
args.add(status.trim().toUpperCase());
}
// wrap the mapper's own select (joins to office/series) so the count always matches the page query 1:1
final String countSql = "select count(*) from (select " + this.bookMapper.schema() + where + ") x";
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
final StringBuilder pageSql = new StringBuilder("select ").append(this.bookMapper.schema()).append(where)
.append(" order by b.id desc limit ? offset ?");
final List<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(limit);
pageArgs.add(offset);
final List<ChequeBookData> books = this.jdbcTemplate.query(pageSql.toString(), this.bookMapper, pageArgs.toArray());
final List<ChequeBookData> withAccountInfo = books.stream().map(this::withAccountInfo).toList();
return new Page<>(withAccountInfo, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
}
@Override
public ChequeBookData retrieveBook(final Long id) {
try {
final String sql = "select " + this.bookMapper.schema() + " where b.id = ?";
final ChequeBookData book = this.jdbcTemplate.queryForObject(sql, this.bookMapper, id);
return withAccountInfo(book);
} catch (final EmptyResultDataAccessException e) {
throw new ChequeBookNotFoundException(id);
}
}
@Override
public List<ChequeLeafData> retrieveLeaves(final Long chequeBookId) {
final String sql = "select " + this.leafMapper.schema() + " where f.cheque_book_id = ? order by f.leaf_seq_no asc";
final List<ChequeLeafData> leaves = this.jdbcTemplate.query(sql, this.leafMapper, chequeBookId);
return leaves.stream().map(this::withAccountInfo).toList();
}
@Override
public List<ChequeLeafData> retrieveLeafBySerialNo(final Long leafSerialNo) {
// a bare serial number is only unique WITHIN a series (see ChequeStockLotRepository.countOverlappingInSeries) —
// two different series can legitimately share a number, so this can return more than one match
final String sql = "select " + this.leafMapper.schema() + " where f.leaf_serial_no = ? order by s.series_code asc";
final List<ChequeLeafData> leaves = this.jdbcTemplate.query(sql, this.leafMapper, leafSerialNo);
if (leaves.isEmpty()) {
throw new ChequeLeafNotFoundException(leafSerialNo);
}
return leaves.stream().map(this::withAccountInfo).toList();
}
@Override
public Page<ChequeBookPendingRequestData> retrievePendingRequests(final String status, final String fromDate, final String toDate,
final Integer offset, final Integer limit) {
final StringBuilder where = new StringBuilder(" where 1=1");
final List<Object> args = new ArrayList<>();
if (StringUtils.isNotBlank(status)) {
where.append(" and status = ?");
args.add(status.trim().toUpperCase());
}
if (StringUtils.isNotBlank(fromDate)) {
where.append(" and submitted_on::date >= ?::date");
args.add(fromDate);
}
if (StringUtils.isNotBlank(toDate)) {
where.append(" and submitted_on::date <= ?::date");
args.add(toDate);
}
final String countSql = "select count(*) from m_chequebook_pending_request" + where;
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
final StringBuilder pageSql = new StringBuilder("select ").append(this.pendingRequestMapper.schema()).append(where)
.append(" order by id desc limit ? offset ?");
final List<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(limit);
pageArgs.add(offset);
final List<ChequeBookPendingRequestData> requests = this.jdbcTemplate.query(pageSql.toString(), this.pendingRequestMapper,
pageArgs.toArray());
return new Page<>(requests, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
}
@Override
public ChequeBookPendingRequestData retrievePendingRequest(final Long id) {
try {
final String sql = "select " + this.pendingRequestMapper.schema() + " where id = ?";
return this.jdbcTemplate.queryForObject(sql, this.pendingRequestMapper, id);
} catch (final EmptyResultDataAccessException e) {
throw new ChequeBookPendingRequestNotFoundException(id);
}
}
/** Best-effort account/client display-name resolution — a missing account never fails the whole read. */
private ChequeBookData withAccountInfo(final ChequeBookData book) {
final AccountDisplay display = resolveAccountDisplay(book.getAccountType(), book.getAccountId());
return ChequeBookData.builder().id(book.getId()).accountType(book.getAccountType()).accountId(book.getAccountId())
.accountNo(display.accountNo).clientName(display.clientName).officeId(book.getOfficeId())
.officeName(book.getOfficeName()).seriesId(book.getSeriesId()).seriesCode(book.getSeriesCode())
.requestedDate(book.getRequestedDate()).requestedBy(book.getRequestedBy()).noOfBooks(book.getNoOfBooks())
.noOfLeavesRequested(book.getNoOfLeavesRequested()).status(book.getStatus()).issueDate(book.getIssueDate())
.issuedBy(book.getIssuedBy()).lotId(book.getLotId()).rangeStart(book.getRangeStart()).rangeEnd(book.getRangeEnd())
.totalLeaves(book.getTotalLeaves()).rejectionReason(book.getRejectionReason())
.collectionAcknowledged(book.isCollectionAcknowledged()).collectedDate(book.getCollectedDate())
.collectedBy(book.getCollectedBy()).build();
}
private ChequeLeafData withAccountInfo(final ChequeLeafData leaf) {
final AccountDisplay display = resolveAccountDisplay(leaf.getAccountType(), leaf.getAccountId());
return ChequeLeafData.builder().id(leaf.getId()).chequeBookId(leaf.getChequeBookId()).lotId(leaf.getLotId())
.seriesCode(leaf.getSeriesCode()).leafSerialNo(leaf.getLeafSerialNo()).leafSeqNo(leaf.getLeafSeqNo()).status(leaf.getStatus())
.stopReason(leaf.getStopReason()).stoppedBy(leaf.getStoppedBy()).stoppedOn(leaf.getStoppedOn())
.lostReason(leaf.getLostReason()).lostReportedBy(leaf.getLostReportedBy()).lostReportedOn(leaf.getLostReportedOn())
.cancelledReason(leaf.getCancelledReason()).cancelledBy(leaf.getCancelledBy()).cancelledOn(leaf.getCancelledOn())
.usedReason(leaf.getUsedReason()).usedBy(leaf.getUsedBy()).usedOn(leaf.getUsedOn())
.accountType(leaf.getAccountType()).accountId(leaf.getAccountId()).accountNo(display.accountNo)
.clientName(display.clientName).officeName(leaf.getOfficeName()).build();
}
private AccountDisplay resolveAccountDisplay(final String accountType, final Long accountId) {
if (accountType == null || accountId == null) {
return new AccountDisplay(null, null);
}
final ChequeAccountType type = ChequeAccountType.fromString(accountType);
if (type == ChequeAccountType.SAVINGS) {
final SavingsAccount account = this.savingsAccountRepository.findById(accountId).orElse(null);
if (account == null) {
return new AccountDisplay(null, null);
}
final String clientName = account.getClient() == null ? null : account.getClient().getDisplayName();
return new AccountDisplay(account.getAccountNumber(), clientName);
}
if (type == ChequeAccountType.CURRENT) {
final CurrentAccount account = this.currentAccountRepository.findById(accountId).orElse(null);
if (account == null) {
return new AccountDisplay(null, null);
}
final String clientName = account.getClient() == null ? null : account.getClient().getDisplayName();
return new AccountDisplay(account.getAccountNo(), clientName);
}
return new AccountDisplay(null, null);
}
private static final class AccountDisplay {
private final String accountNo;
private final String clientName;
private AccountDisplay(final String accountNo, final String clientName) {
this.accountNo = accountNo;
this.clientName = clientName;
}
}
private static final class SeriesMapper implements RowMapper<ChequeSeriesData> {
String schema() {
return " id, series_code, name, leaves_per_book, applicable_account_type, cts_compliant, reorder_level, is_active "
+ "from m_cheque_series";
}
@Override
public ChequeSeriesData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
return ChequeSeriesData.builder().id(rs.getLong("id")).seriesCode(rs.getString("series_code")).name(rs.getString("name"))
.leavesPerBook((Integer) rs.getObject("leaves_per_book"))
.applicableAccountType(rs.getString("applicable_account_type")).ctsCompliant(rs.getBoolean("cts_compliant"))
.reorderLevel((Integer) rs.getObject("reorder_level")).active(rs.getBoolean("is_active")).build();
}
}
private static final class StockLotMapper implements RowMapper<ChequeStockLotData> {
String schema() {
return " l.id as id, l.series_id as series_id, s.series_code as series_code, s.leaves_per_book as leaves_per_book, "
+ "l.office_id as office_id, o.name as office_name, l.range_start as range_start, l.range_end as range_end, "
+ "l.next_serial_no as next_serial_no, l.source as source, l.source_ref_id as source_ref_id, "
+ "l.batch_ref as batch_ref, l.status as status, s.reorder_level as reorder_level "
+ "from m_cheque_stock_lot l join m_cheque_series s on s.id = l.series_id join m_office o on o.id = l.office_id";
}
@Override
public ChequeStockLotData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
final long rangeEnd = rs.getLong("range_end");
final long nextSerialNo = rs.getLong("next_serial_no");
final long remaining = rangeEnd - nextSerialNo + 1;
final Integer leavesPerBook = (Integer) rs.getObject("leaves_per_book");
// full issuable books of this denomination — a trailing partial book cannot be issued
final Long remainingBooks = leavesPerBook != null && leavesPerBook > 0 ? remaining / leavesPerBook : null;
final Integer reorderLevel = (Integer) rs.getObject("reorder_level");
final boolean lowStock = reorderLevel != null && remaining <= reorderLevel;
return ChequeStockLotData.builder().id(rs.getLong("id")).seriesId(rs.getLong("series_id"))
.seriesCode(rs.getString("series_code")).leavesPerBook(leavesPerBook).officeId(rs.getLong("office_id"))
.officeName(rs.getString("office_name")).rangeStart(rs.getLong("range_start")).rangeEnd(rangeEnd)
.nextSerialNo(nextSerialNo).remainingLeaves(remaining).remainingBooks(remainingBooks)
.source(rs.getString("source")).sourceRefId((Long) rs.getObject("source_ref_id"))
.batchRef(rs.getString("batch_ref")).status(rs.getString("status")).lowStock(lowStock).build();
}
}
private static final class StockTransferMapper implements RowMapper<ChequeStockTransferData> {
String schema() {
return " t.id as id, t.series_id as series_id, s.series_code as series_code, t.from_office_id as from_office_id, "
+ "fo.name as from_office_name, t.to_office_id as to_office_id, tofc.name as to_office_name, "
+ "t.from_lot_id as from_lot_id, t.to_lot_id as to_lot_id, t.range_start as range_start, "
+ "t.range_end as range_end, t.total_leaves as total_leaves, t.dispatch_date as dispatch_date, "
+ "t.status as status, t.received_date as received_date, t.dispatched_by as dispatched_by, "
+ "t.received_by as received_by "
+ "from m_cheque_stock_transfer t join m_cheque_series s on s.id = t.series_id "
+ "join m_office fo on fo.id = t.from_office_id join m_office tofc on tofc.id = t.to_office_id";
}
@Override
public ChequeStockTransferData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
return ChequeStockTransferData.builder().id(rs.getLong("id")).seriesId(rs.getLong("series_id"))
.seriesCode(rs.getString("series_code")).fromOfficeId(rs.getLong("from_office_id"))
.fromOfficeName(rs.getString("from_office_name")).toOfficeId(rs.getLong("to_office_id"))
.toOfficeName(rs.getString("to_office_name")).fromLotId(rs.getLong("from_lot_id"))
.toLotId(rs.getLong("to_lot_id")).rangeStart(rs.getLong("range_start")).rangeEnd(rs.getLong("range_end"))
.totalLeaves((Integer) rs.getObject("total_leaves")).dispatchDate(rs.getObject("dispatch_date", LocalDate.class))
.status(rs.getString("status")).receivedDate(rs.getObject("received_date", LocalDate.class))
.dispatchedBy((Long) rs.getObject("dispatched_by")).receivedBy((Long) rs.getObject("received_by")).build();
}
}
private static final class BookMapper implements RowMapper<ChequeBookData> {
String schema() {
return " b.id as id, b.account_type as account_type, b.account_id as account_id, b.office_id as office_id, "
+ "o.name as office_name, b.series_id as series_id, s.series_code as series_code, "
+ "b.requested_date as requested_date, b.requested_by as requested_by, b.no_of_books as no_of_books, "
+ "b.no_of_leaves_requested as no_of_leaves_requested, b.status as status, b.issue_date as issue_date, "
+ "b.issued_by as issued_by, b.lot_id as lot_id, b.range_start as range_start, b.range_end as range_end, "
+ "b.total_leaves as total_leaves, b.rejection_reason as rejection_reason, "
+ "b.collection_acknowledged as collection_acknowledged, b.collected_date as collected_date, "
+ "b.collected_by as collected_by "
+ "from m_cheque_book b join m_office o on o.id = b.office_id left join m_cheque_series s on s.id = b.series_id";
}
@Override
public ChequeBookData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
return ChequeBookData.builder().id(rs.getLong("id")).accountType(rs.getString("account_type"))
.accountId(rs.getLong("account_id")).accountNo(null).clientName(null).officeId(rs.getLong("office_id"))
.officeName(rs.getString("office_name")).seriesId((Long) rs.getObject("series_id"))
.seriesCode(rs.getString("series_code")).requestedDate(rs.getObject("requested_date", LocalDate.class))
.requestedBy((Long) rs.getObject("requested_by")).noOfBooks((Integer) rs.getObject("no_of_books"))
.noOfLeavesRequested((Integer) rs.getObject("no_of_leaves_requested"))
.status(rs.getString("status")).issueDate(rs.getObject("issue_date", LocalDate.class))
.issuedBy((Long) rs.getObject("issued_by")).lotId((Long) rs.getObject("lot_id"))
.rangeStart((Long) rs.getObject("range_start")).rangeEnd((Long) rs.getObject("range_end"))
.totalLeaves((Integer) rs.getObject("total_leaves")).rejectionReason(rs.getString("rejection_reason"))
.collectionAcknowledged(rs.getBoolean("collection_acknowledged"))
.collectedDate(rs.getObject("collected_date", LocalDate.class)).collectedBy((Long) rs.getObject("collected_by"))
.build();
}
}
private static final class LeafMapper implements RowMapper<ChequeLeafData> {
String schema() {
return " f.id as id, f.cheque_book_id as cheque_book_id, f.lot_id as lot_id, s.series_code as series_code, "
+ "f.leaf_serial_no as leaf_serial_no, "
+ "f.leaf_seq_no as leaf_seq_no, f.status as status, f.stop_reason as stop_reason, f.stopped_by as stopped_by, "
+ "f.stopped_on as stopped_on, f.lost_reason as lost_reason, f.lost_reported_by as lost_reported_by, "
+ "f.lost_reported_on as lost_reported_on, f.cancelled_reason as cancelled_reason, "
+ "f.cancelled_by as cancelled_by, f.cancelled_on as cancelled_on, "
+ "f.used_reason as used_reason, f.used_by as used_by, f.used_on as used_on, "
+ "b.account_type as account_type, b.account_id as account_id, bo.name as book_office_name, "
+ "lo.name as lot_office_name "
+ "from m_cheque_leaf f left join m_cheque_book b on b.id = f.cheque_book_id left join m_office bo on bo.id = b.office_id "
+ "join m_cheque_stock_lot l on l.id = f.lot_id join m_office lo on lo.id = l.office_id "
+ "join m_cheque_series s on s.id = l.series_id";
}
@Override
public ChequeLeafData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
final String bookOfficeName = rs.getString("book_office_name");
final String officeName = bookOfficeName != null ? bookOfficeName : rs.getString("lot_office_name");
return ChequeLeafData.builder().id(rs.getLong("id")).chequeBookId((Long) rs.getObject("cheque_book_id"))
.lotId(rs.getLong("lot_id")).seriesCode(rs.getString("series_code")).leafSerialNo(rs.getLong("leaf_serial_no"))
.leafSeqNo((Integer) rs.getObject("leaf_seq_no")).status(rs.getString("status"))
.stopReason(rs.getString("stop_reason")).stoppedBy((Long) rs.getObject("stopped_by"))
.stoppedOn(rs.getObject("stopped_on", OffsetDateTime.class)).lostReason(rs.getString("lost_reason"))
.lostReportedBy((Long) rs.getObject("lost_reported_by"))
.lostReportedOn(rs.getObject("lost_reported_on", OffsetDateTime.class))
.cancelledReason(rs.getString("cancelled_reason")).cancelledBy((Long) rs.getObject("cancelled_by"))
.cancelledOn(rs.getObject("cancelled_on", OffsetDateTime.class)).usedReason(rs.getString("used_reason"))
.usedBy((Long) rs.getObject("used_by")).usedOn(rs.getObject("used_on", OffsetDateTime.class))
.accountType(rs.getString("account_type")).accountId((Long) rs.getObject("account_id")).accountNo(null)
.clientName(null).officeName(officeName).build();
}
}
private static final class PendingRequestMapper implements RowMapper<ChequeBookPendingRequestData> {
String schema() {
return " id, request_type as request_type, payload_json as payload_json, status as status, maker_id as maker_id, "
+ "maker_name as maker_name, maker_username as maker_username, submitted_on as submitted_on, "
+ "checker_id as checker_id, checker_name as checker_name, checker_username as checker_username, "
+ "checked_on as checked_on, rejection_reason as rejection_reason, created_resource_id as created_resource_id, "
+ "activity_log as activity_log from m_chequebook_pending_request";
}
@Override
public ChequeBookPendingRequestData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
return ChequeBookPendingRequestData.builder().id(rs.getLong("id")).requestType(rs.getString("request_type"))
.payloadJson(rs.getString("payload_json")).status(rs.getString("status")).makerId(rs.getLong("maker_id"))
.makerName(rs.getString("maker_name")).makerUsername(rs.getString("maker_username"))
.submittedOn(rs.getObject("submitted_on", OffsetDateTime.class)).checkerId((Long) rs.getObject("checker_id"))
.checkerName(rs.getString("checker_name")).checkerUsername(rs.getString("checker_username"))
.checkedOn(rs.getObject("checked_on", OffsetDateTime.class)).rejectionReason(rs.getString("rejection_reason"))
.createdResourceId((Long) rs.getObject("created_resource_id")).activityLog(rs.getString("activity_log")).build();
}
}
}
@@ -0,0 +1,78 @@
/**
* 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.chequebook.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface ChequeBookWritePlatformService {
CommandProcessingResult createSeries(JsonCommand command);
CommandProcessingResult updateSeries(Long id, JsonCommand command);
CommandProcessingResult deleteSeries(Long id);
/** HO/branch bulk receipt from the press — creates an AVAILABLE stock lot. */
CommandProcessingResult receiveStock(JsonCommand command);
/** Reports the remaining undrawn suffix of a stock lot's range as lost/stolen before ever being issued. */
CommandProcessingResult reportStockLotLost(Long lotId, JsonCommand command);
/** Dispatches a sub-range from an available lot to another office. */
CommandProcessingResult dispatchStockTransfer(JsonCommand command);
CommandProcessingResult receiveStockTransfer(Long transferId, JsonCommand command);
CommandProcessingResult rejectStockTransfer(Long transferId, JsonCommand command);
/** Customer/branch requisition — no stock drawn yet. */
CommandProcessingResult requestChequeBook(JsonCommand command);
/** Draws a range from an available lot at the book's office and materializes the leaf rows. */
CommandProcessingResult issueChequeBook(Long chequeBookId, JsonCommand command);
CommandProcessingResult rejectChequeBook(Long chequeBookId, JsonCommand command);
/** Account closed — withdraws the book's remaining ACTIVE leaves. */
CommandProcessingResult withdrawChequeBook(Long chequeBookId, JsonCommand command);
CommandProcessingResult stopPayment(Long chequeBookId, JsonCommand command);
CommandProcessingResult revokeStopPayment(Long chequeBookId, JsonCommand command);
CommandProcessingResult reportChequeBookLost(Long chequeBookId, JsonCommand command);
CommandProcessingResult cancelLeaf(Long chequeBookId, JsonCommand command);
/**
* Manually records that leaves cleared, once a branch sees them on the bank/clearing statement — never
* automatic, since this CBS has no clearing/presentment integration to detect it on its own.
*/
CommandProcessingResult markLeafUsed(Long chequeBookId, JsonCommand command);
/**
* Checker approves a pending series/stock/book request — replays the maker's original submission and
* actually creates the record. Fails if the checker is the same user who submitted it.
*/
CommandProcessingResult approvePendingRequest(Long pendingRequestId);
/** Checker rejects a pending series/stock/book request. Fails if the checker is the original maker. */
CommandProcessingResult rejectPendingRequest(Long pendingRequestId, JsonCommand command);
}
@@ -0,0 +1,767 @@
/**
* 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.chequebook.service;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.portfolio.chequebook.api.ChequeBookApiJsonInputParams;
import org.apache.fineract.portfolio.chequebook.domain.ChequeAccountType;
import org.apache.fineract.portfolio.chequebook.domain.ChequeBook;
import org.apache.fineract.portfolio.chequebook.domain.ChequeBookPendingRequest;
import org.apache.fineract.portfolio.chequebook.domain.ChequeBookPendingRequestRepositoryWrapper;
import org.apache.fineract.portfolio.chequebook.domain.ChequeBookPendingRequestType;
import org.apache.fineract.portfolio.chequebook.domain.ChequeBookRepositoryWrapper;
import org.apache.fineract.portfolio.chequebook.domain.ChequeLeaf;
import org.apache.fineract.portfolio.chequebook.domain.ChequeLeafRepository;
import org.apache.fineract.portfolio.chequebook.domain.ChequeLeafStatus;
import org.apache.fineract.portfolio.chequebook.domain.ChequeSeries;
import org.apache.fineract.portfolio.chequebook.domain.ChequeSeriesApplicability;
import org.apache.fineract.portfolio.chequebook.domain.ChequeSeriesRepository;
import org.apache.fineract.portfolio.chequebook.domain.ChequeSeriesRepositoryWrapper;
import org.apache.fineract.portfolio.chequebook.domain.ChequeStockLot;
import org.apache.fineract.portfolio.chequebook.domain.ChequeStockLotRepository;
import org.apache.fineract.portfolio.chequebook.domain.ChequeStockLotRepositoryWrapper;
import org.apache.fineract.portfolio.chequebook.domain.ChequeStockLotStatus;
import org.apache.fineract.portfolio.chequebook.domain.ChequeStockTransfer;
import org.apache.fineract.portfolio.chequebook.domain.ChequeStockTransferRepositoryWrapper;
import org.apache.fineract.portfolio.chequebook.serialization.ChequeBookCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountFacilityConfig;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepositoryWrapper;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityConfig;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductFacilityConfigRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountFacilityConfig;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountSubStatusEnum;
import org.apache.fineract.portfolio.savings.domain.SavingsProductFacilityConfig;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Slf4j
public class ChequeBookWritePlatformServiceImpl implements ChequeBookWritePlatformService {
private final PlatformSecurityContext context;
private final ChequeBookCommandFromApiJsonDeserializer validator;
private final ChequeSeriesRepository chequeSeriesRepository;
private final ChequeSeriesRepositoryWrapper chequeSeriesRepositoryWrapper;
private final ChequeStockLotRepository chequeStockLotRepository;
private final ChequeStockLotRepositoryWrapper chequeStockLotRepositoryWrapper;
private final ChequeStockTransferRepositoryWrapper chequeStockTransferRepositoryWrapper;
private final ChequeBookRepositoryWrapper chequeBookRepositoryWrapper;
private final ChequeLeafRepository chequeLeafRepository;
private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper;
private final CurrentAccountRepositoryWrapper currentAccountRepositoryWrapper;
private final CurrentProductFacilityConfigRepository currentProductFacilityConfigRepository;
private final ChequeBookPendingRequestRepositoryWrapper chequeBookPendingRequestRepositoryWrapper;
private final ChequeSeriesHardDeleteAttempter chequeSeriesHardDeleteAttempter;
private final FromJsonHelper fromApiJsonHelper;
private final Gson gson = new GsonBuilder().serializeNulls().create();
// ============================= Series =============================
@Transactional
@Override
public CommandProcessingResult createSeries(final JsonCommand command) {
this.validator.validateForCreateSeries(command.json());
final String seriesCode = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_CODE.getValue());
if (this.chequeSeriesRepository.existsBySeriesCode(seriesCode)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequeseries.duplicate.code",
"A cheque series with code " + seriesCode + " already exists.", seriesCode);
}
final Long pendingId = submitPendingRequest(ChequeBookPendingRequestType.CREATE_SERIES, command);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(pendingId).build();
}
/**
* Runs only once a checker approves the pending request — replays the maker's original submission.
* {@code makerId} is the original submitter, not whichever checker triggers this replay.
*/
private Long finalizeCreateSeries(final JsonCommand command, final Long makerId) {
final String seriesCode = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_CODE.getValue());
if (this.chequeSeriesRepository.existsBySeriesCode(seriesCode)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequeseries.duplicate.code",
"A cheque series with code " + seriesCode + " already exists.", seriesCode);
}
final String name = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.NAME.getValue());
final Integer leavesPerBook = command
.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.LEAVES_PER_BOOK.getValue());
final ChequeSeriesApplicability applicableAccountType = ChequeSeriesApplicability
.fromString(command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.APPLICABLE_ACCOUNT_TYPE.getValue()));
final Boolean ctsCompliant = command.booleanObjectValueOfParameterNamed(ChequeBookApiJsonInputParams.CTS_COMPLIANT.getValue());
final Integer reorderLevel = command
.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.REORDER_LEVEL.getValue());
final ChequeSeries series = ChequeSeries.createNew(seriesCode, name, leavesPerBook, applicableAccountType,
ctsCompliant == null || ctsCompliant, reorderLevel, makerId);
final ChequeSeries saved = this.chequeSeriesRepositoryWrapper.saveAndFlush(series);
return saved.getId();
}
@Transactional
@Override
public CommandProcessingResult updateSeries(final Long id, final JsonCommand command) {
this.validator.validateForUpdateSeries(command.json());
final ChequeSeries series = this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(id);
final String name = command.stringValueOfParameterNamedAllowingNull(ChequeBookApiJsonInputParams.NAME.getValue());
final Integer leavesPerBook = command
.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.LEAVES_PER_BOOK.getValue());
final ChequeSeriesApplicability applicableAccountType = ChequeSeriesApplicability
.fromString(command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.APPLICABLE_ACCOUNT_TYPE.getValue()));
final Boolean ctsCompliant = command.booleanObjectValueOfParameterNamed(ChequeBookApiJsonInputParams.CTS_COMPLIANT.getValue());
final Integer reorderLevel = command
.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.REORDER_LEVEL.getValue());
series.updateDetails(name, leavesPerBook, applicableAccountType, ctsCompliant, reorderLevel);
this.chequeSeriesRepositoryWrapper.saveAndFlush(series);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(id).build();
}
@Transactional
@Override
public CommandProcessingResult deleteSeries(final Long id) {
this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(id); // fail fast for the maker if it doesn't exist
final JsonObject payload = new JsonObject();
payload.addProperty(ChequeBookApiJsonInputParams.SERIES_ID.getValue(), id);
final Long pendingId = insertPendingRequest(ChequeBookPendingRequestType.DELETE_SERIES, this.gson.toJson(payload));
return new CommandProcessingResultBuilder().withEntityId(pendingId).build();
}
/**
* Runs only once a checker approves the pending request. Permanently removes the series if the
* database's own foreign-key constraints allow it (nothing ever referenced it); otherwise falls back to
* the same deactivate used previously, preserving every stock lot/book/leaf already tied to this series.
*/
private Long finalizeDeleteSeries(final Long seriesId) {
final boolean hardDeleted = this.chequeSeriesHardDeleteAttempter.attemptDelete(seriesId);
if (!hardDeleted) {
final ChequeSeries series = this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(seriesId);
series.deactivate();
this.chequeSeriesRepositoryWrapper.saveAndFlush(series);
}
return seriesId;
}
// ============================= Stock =============================
@Transactional
@Override
public CommandProcessingResult receiveStock(final JsonCommand command) {
this.validator.validateForStockLotReceive(command.json());
final Long seriesId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue());
final Long rangeStart = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.RANGE_START.getValue());
final Long rangeEnd = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.RANGE_END.getValue());
// fail fast for the maker; re-checked authoritatively at approval time in case stock changed meanwhile
if (this.chequeStockLotRepository.countOverlappingInSeries(seriesId, rangeStart, rangeEnd) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.range.overlap",
"The given leaf-serial range overlaps with an existing stock lot for this series.", rangeStart);
}
final Long pendingId = submitPendingRequest(ChequeBookPendingRequestType.RECEIVE_STOCK, command);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(pendingId).build();
}
/**
* Runs only once a checker approves the pending request — replays the maker's original submission.
* {@code makerId} is the original submitter, not whichever checker triggers this replay.
*/
private Long finalizeReceiveStock(final JsonCommand command, final Long makerId) {
final Long seriesId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue());
final Long officeId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.OFFICE_ID.getValue());
final Long rangeStart = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.RANGE_START.getValue());
final Long rangeEnd = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.RANGE_END.getValue());
final String batchRef = command.stringValueOfParameterNamedAllowingNull(ChequeBookApiJsonInputParams.BATCH_REF.getValue());
if (this.chequeStockLotRepository.countOverlappingInSeries(seriesId, rangeStart, rangeEnd) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.range.overlap",
"The given leaf-serial range overlaps with an existing stock lot for this series.", rangeStart);
}
final ChequeSeries series = this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(seriesId);
final Office office = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
final ChequeStockLot lot = ChequeStockLot.createReceipt(series, office, rangeStart, rangeEnd, batchRef, makerId);
final ChequeStockLot saved = this.chequeStockLotRepositoryWrapper.saveAndFlush(lot);
log.info("Received cheque stock lot {} ({} - {}) for series {} at office {}", saved.getId(), rangeStart, rangeEnd, seriesId,
officeId);
return saved.getId();
}
@Transactional
@Override
public CommandProcessingResult reportStockLotLost(final Long lotId, final JsonCommand command) {
this.validator.validateForStockLotReportLost(command.json());
final ChequeStockLot lot = this.chequeStockLotRepositoryWrapper.findOneWithNotFoundDetection(lotId);
final Long fromSerialNo = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.FROM_SERIAL_NO.getValue());
final String reason = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.REASON.getValue());
final Long reporterId = this.context.authenticatedUser().getId();
final long originalRangeEnd = lot.reportRemainingLost(fromSerialNo);
this.chequeStockLotRepositoryWrapper.saveAndFlush(lot);
final List<ChequeLeaf> lostLeaves = new ArrayList<>();
for (long serial = fromSerialNo; serial <= originalRangeEnd; serial++) {
lostLeaves.add(ChequeLeaf.lostUnissued(lot, serial, reason, reporterId));
}
this.chequeLeafRepository.saveAll(lostLeaves);
log.info("Reported {} unissued cheque leaves lost/stolen from stock lot {}", lostLeaves.size(), lotId);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(lotId).build();
}
@Transactional
@Override
public CommandProcessingResult dispatchStockTransfer(final JsonCommand command) {
this.validator.validateForStockTransferDispatch(command.json());
final Long seriesId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue());
final Long fromOfficeId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.FROM_OFFICE_ID.getValue());
final Long toOfficeId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.TO_OFFICE_ID.getValue());
final Long fromLotId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.FROM_LOT_ID.getValue());
final Integer noOfLeaves = command.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.NO_OF_LEAVES.getValue());
final ChequeSeries series = this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(seriesId);
final Office fromOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(fromOfficeId);
final Office toOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(toOfficeId);
final ChequeStockLot fromLot = this.chequeStockLotRepositoryWrapper.findOneWithNotFoundDetection(fromLotId);
if (!fromLot.getOffice().getId().equals(fromOfficeId)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.office.mismatch",
"Stock lot " + fromLotId + " is not custodied at the given fromOfficeId.", fromLotId);
}
if (!fromLot.getSeries().getId().equals(seriesId)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequestocklot.series.mismatch",
"Stock lot " + fromLotId + " does not belong to the given series.", fromLotId);
}
final long[] range = fromLot.drawDown(noOfLeaves);
this.chequeStockLotRepositoryWrapper.saveAndFlush(fromLot);
final ChequeStockLot toLot = ChequeStockLot.createTransferIn(series, toOffice, range[0], range[1], null);
final ChequeStockLot savedToLot = this.chequeStockLotRepositoryWrapper.saveAndFlush(toLot);
final Long dispatchedBy = this.context.authenticatedUser().getId();
final ChequeStockTransfer transfer = ChequeStockTransfer.createNew(series, fromOffice, toOffice, fromLot, savedToLot, range[0],
range[1], noOfLeaves, DateUtils.getBusinessLocalDate(), dispatchedBy);
final ChequeStockTransfer saved = this.chequeStockTransferRepositoryWrapper.saveAndFlush(transfer);
log.info("Dispatched cheque stock transfer {} ({} leaves, {} - {}) from office {} to office {}", saved.getId(), noOfLeaves,
range[0], range[1], fromOfficeId, toOfficeId);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(saved.getId()).build();
}
@Transactional
@Override
public CommandProcessingResult receiveStockTransfer(final Long transferId, final JsonCommand command) {
final ChequeStockTransfer transfer = this.chequeStockTransferRepositoryWrapper.findOneWithNotFoundDetection(transferId);
final Long receivedBy = this.context.authenticatedUser().getId();
transfer.markReceived(receivedBy, DateUtils.getBusinessLocalDate());
this.chequeStockTransferRepositoryWrapper.saveAndFlush(transfer);
transfer.getToLot().markAvailable(receivedBy);
this.chequeStockLotRepositoryWrapper.saveAndFlush(transfer.getToLot());
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(transferId).build();
}
@Transactional
@Override
public CommandProcessingResult rejectStockTransfer(final Long transferId, final JsonCommand command) {
final ChequeStockTransfer transfer = this.chequeStockTransferRepositoryWrapper.findOneWithNotFoundDetection(transferId);
final Long rejectedBy = this.context.authenticatedUser().getId();
transfer.reject(rejectedBy, DateUtils.getBusinessLocalDate());
this.chequeStockTransferRepositoryWrapper.saveAndFlush(transfer);
transfer.getToLot().cancel();
this.chequeStockLotRepositoryWrapper.saveAndFlush(transfer.getToLot());
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(transferId).build();
}
// ============================= Cheque books =============================
@Transactional
@Override
public CommandProcessingResult requestChequeBook(final JsonCommand command) {
this.validator.validateForBookRequest(command.json());
final ChequeAccountType accountType = ChequeAccountType
.fromString(command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.ACCOUNT_TYPE.getValue()));
final Long accountId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.ACCOUNT_ID.getValue());
final Long seriesId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue());
// fail fast for the maker; re-checked authoritatively at approval time in case anything changed meanwhile
assertEligible(accountType, accountId);
final ChequeSeries series = this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(seriesId);
if (!series.appliesTo(accountType)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.series.account.type.mismatch",
"Cheque series " + seriesId + " is not applicable to a " + accountType + " account.", seriesId);
}
final Long pendingId = submitPendingRequest(ChequeBookPendingRequestType.REQUEST_BOOK, command);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(pendingId).build();
}
/**
* Runs only once a checker approves the pending request — replays the maker's original submission.
* {@code makerId} is the original submitter, not whichever checker triggers this replay.
*
* Requesting and issuing happen together here — the checker's approval IS the authorization to hand
* out the book, so there is no separate "Issue" click left for anyone to do afterward. The book is
* created and immediately drawn down from available stock in the same step.
*/
private Long finalizeRequestChequeBook(final JsonCommand command, final Long makerId) {
final ChequeAccountType accountType = ChequeAccountType
.fromString(command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.ACCOUNT_TYPE.getValue()));
final Long accountId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.ACCOUNT_ID.getValue());
final Long officeId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.OFFICE_ID.getValue());
final Integer noOfBooks = command.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.NO_OF_BOOKS.getValue());
final Long seriesId = command.longValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue());
assertEligible(accountType, accountId);
final Office office = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
final ChequeSeries series = this.chequeSeriesRepositoryWrapper.findOneWithNotFoundDetection(seriesId);
// the denomination requested must be meant for this account's scheme (SB vs CA)
if (!series.appliesTo(accountType)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.series.account.type.mismatch",
"Cheque series " + seriesId + " is not applicable to a " + accountType + " account.", seriesId);
}
final ChequeBook book = ChequeBook.requestNew(accountType, accountId, office, series, noOfBooks, makerId);
final ChequeBook saved = this.chequeBookRepositoryWrapper.saveAndFlush(book);
// leaves actually handed to the customer = requested books x this series' fixed denomination
final int totalLeaves = noOfBooks * series.getLeavesPerBook();
final List<ChequeStockLot> candidateLots = this.chequeStockLotRepository
.findBySeriesIdAndOfficeIdAndStatusOrderByIdAsc(seriesId, officeId, ChequeStockLotStatus.AVAILABLE.name());
ChequeStockLot chosenLot = null;
for (final ChequeStockLot candidate : candidateLots) {
if (candidate.remainingLeaves() >= totalLeaves) {
chosenLot = candidate;
break;
}
}
if (chosenLot == null) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.insufficient.stock",
"No cheque stock lot at this office has enough remaining leaves (" + totalLeaves + " needed) for series "
+ seriesId + ".",
seriesId);
}
final long[] range = chosenLot.drawDown(totalLeaves);
this.chequeStockLotRepositoryWrapper.saveAndFlush(chosenLot);
// the checker approving right now is the one authorizing the physical issue, not the original maker
final Long checkerId = this.context.authenticatedUser().getId();
saved.issue(chosenLot, range[0], range[1], totalLeaves, checkerId);
this.chequeBookRepositoryWrapper.saveAndFlush(saved);
final List<ChequeLeaf> leaves = new ArrayList<>();
int seqNo = 1;
for (long serial = range[0]; serial <= range[1]; serial++) {
leaves.add(ChequeLeaf.issueForBook(saved, chosenLot, serial, seqNo++));
}
this.chequeLeafRepository.saveAll(leaves);
log.info("Cheque book {} requested and issued together on approval ({} leaves, {} - {}) for {} account {}", saved.getId(),
leaves.size(), range[0], range[1], accountType, accountId);
return saved.getId();
}
@Transactional
@Override
public CommandProcessingResult issueChequeBook(final Long chequeBookId, final JsonCommand command) {
this.validator.validateForBookIssue(command.json());
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
// the denomination was already fixed at request time — matches a real requisition slip
final ChequeSeries series = book.getSeries();
if (series == null) {
// a request created before series-selection-at-request-time was introduced has no series on file
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.series.not.set",
"Cheque book " + chequeBookId + " has no series recorded against it. Reject this request and submit "
+ "a new one, which will capture the series at requisition time.",
chequeBookId);
}
final Long seriesId = series.getId();
// re-validate eligibility at issue time — time may have passed since the request was recorded
final ChequeAccountType accountType = ChequeAccountType.fromString(book.getAccountType());
assertEligible(accountType, book.getAccountId());
// leaves actually handed to the customer = requested books x this series' fixed denomination
final int totalLeaves = book.getNoOfBooks() * series.getLeavesPerBook();
final List<ChequeStockLot> candidateLots = this.chequeStockLotRepository.findBySeriesIdAndOfficeIdAndStatusOrderByIdAsc(seriesId,
book.getOffice().getId(), ChequeStockLotStatus.AVAILABLE.name());
ChequeStockLot chosenLot = null;
for (final ChequeStockLot candidate : candidateLots) {
if (candidate.remainingLeaves() >= totalLeaves) {
chosenLot = candidate;
break;
}
}
if (chosenLot == null) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.insufficient.stock",
"No cheque stock lot at this office has enough remaining leaves (" + totalLeaves + " needed) for series "
+ seriesId + ".",
seriesId);
}
final long[] range = chosenLot.drawDown(totalLeaves);
this.chequeStockLotRepositoryWrapper.saveAndFlush(chosenLot);
final Long issuedBy = this.context.authenticatedUser().getId();
book.issue(chosenLot, range[0], range[1], totalLeaves, issuedBy);
final Boolean collectionAcknowledged = command
.booleanObjectValueOfParameterNamed(ChequeBookApiJsonInputParams.COLLECTION_ACKNOWLEDGED.getValue());
if (Boolean.TRUE.equals(collectionAcknowledged)) {
book.acknowledgeCollection(issuedBy);
}
this.chequeBookRepositoryWrapper.saveAndFlush(book);
final List<ChequeLeaf> leaves = new ArrayList<>();
int seqNo = 1;
for (long serial = range[0]; serial <= range[1]; serial++) {
leaves.add(ChequeLeaf.issueForBook(book, chosenLot, serial, seqNo++));
}
this.chequeLeafRepository.saveAll(leaves);
log.info("Issued cheque book {} ({} leaves, {} - {}) against {} account {}", chequeBookId, leaves.size(), range[0], range[1],
book.getAccountType(), book.getAccountId());
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult rejectChequeBook(final Long chequeBookId, final JsonCommand command) {
this.validator.validateForBookReject(command.json());
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
final String reason = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.REJECTION_REASON.getValue());
book.reject(reason);
this.chequeBookRepositoryWrapper.saveAndFlush(book);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult withdrawChequeBook(final Long chequeBookId, final JsonCommand command) {
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
book.withdraw();
this.chequeBookRepositoryWrapper.saveAndFlush(book);
final List<ChequeLeaf> leaves = this.chequeLeafRepository.findByChequeBookIdOrderByLeafSeqNoAsc(chequeBookId);
final List<ChequeLeaf> toWithdraw = new ArrayList<>();
for (final ChequeLeaf leaf : leaves) {
if (ChequeLeafStatus.ACTIVE.name().equals(leaf.getStatus())) {
leaf.withdraw();
toWithdraw.add(leaf);
}
}
this.chequeLeafRepository.saveAll(toWithdraw);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult stopPayment(final Long chequeBookId, final JsonCommand command) {
this.validator.validateForBookLeafAction(command.json(), true);
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
final String reason = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.REASON.getValue());
final Long actorId = this.context.authenticatedUser().getId();
final List<ChequeLeaf> leaves = resolveLeavesInRange(book, command);
for (final ChequeLeaf leaf : leaves) {
leaf.stopPayment(reason, actorId);
}
this.chequeLeafRepository.saveAll(leaves);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult revokeStopPayment(final Long chequeBookId, final JsonCommand command) {
this.validator.validateForBookLeafAction(command.json(), false);
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
final List<ChequeLeaf> leaves = resolveLeavesInRange(book, command);
for (final ChequeLeaf leaf : leaves) {
leaf.revokeStopPayment();
}
this.chequeLeafRepository.saveAll(leaves);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult reportChequeBookLost(final Long chequeBookId, final JsonCommand command) {
this.validator.validateForBookLeafAction(command.json(), true);
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
final String reason = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.REASON.getValue());
final Long reporterId = this.context.authenticatedUser().getId();
final List<ChequeLeaf> leaves = resolveLeavesInRange(book, command);
for (final ChequeLeaf leaf : leaves) {
leaf.reportLost(reason, reporterId);
}
this.chequeLeafRepository.saveAll(leaves);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult cancelLeaf(final Long chequeBookId, final JsonCommand command) {
this.validator.validateForBookLeafAction(command.json(), true);
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
final String reason = command.stringValueOfParameterNamed(ChequeBookApiJsonInputParams.REASON.getValue());
final Long actorId = this.context.authenticatedUser().getId();
final List<ChequeLeaf> leaves = resolveLeavesInRange(book, command);
for (final ChequeLeaf leaf : leaves) {
leaf.cancel(reason, actorId);
}
this.chequeLeafRepository.saveAll(leaves);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
@Transactional
@Override
public CommandProcessingResult markLeafUsed(final Long chequeBookId, final JsonCommand command) {
// informational record-keeping, not a fraud control — a note is welcome but not required
this.validator.validateForBookLeafAction(command.json(), false);
final ChequeBook book = this.chequeBookRepositoryWrapper.findOneWithNotFoundDetection(chequeBookId);
final String reason = command.stringValueOfParameterNamedAllowingNull(ChequeBookApiJsonInputParams.REASON.getValue());
final Long actorId = this.context.authenticatedUser().getId();
final List<ChequeLeaf> leaves = resolveLeavesInRange(book, command);
for (final ChequeLeaf leaf : leaves) {
leaf.markUsed(reason, actorId);
}
this.chequeLeafRepository.saveAll(leaves);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(chequeBookId).build();
}
// ============================= Pending requests (bespoke maker-checker) =============================
//
// Fineract's native command-approval engine holds nothing for a user whose role carries ALL_FUNCTIONS
// (Super user, Clerk in this tenant) - it bypasses maker-checker unconditionally for them, which made it
// a no-op for this CBS's actual admin accounts. These three actions instead go through this bespoke
// table (mirrors m_clerk_disbursement_request): submit stores the raw request JSON as PENDING; approval
// replays that JSON through the same finalize* method that used to run immediately.
private Long submitPendingRequest(final ChequeBookPendingRequestType type, final JsonCommand command) {
return insertPendingRequest(type, command.json());
}
private Long insertPendingRequest(final ChequeBookPendingRequestType type, final String payloadJson) {
final AppUser maker = this.context.authenticatedUser();
final String activityLog = appendActivityLog(null, "SUBMITTED", maker, null);
final ChequeBookPendingRequest request = ChequeBookPendingRequest.submit(type, payloadJson, maker, activityLog);
final ChequeBookPendingRequest saved = this.chequeBookPendingRequestRepositoryWrapper.saveAndFlush(request);
log.info("Cheque book pending request {} ({}) submitted by {}", saved.getId(), type, maker.getUsername());
return saved.getId();
}
@Transactional
@Override
public CommandProcessingResult approvePendingRequest(final Long pendingRequestId) {
final ChequeBookPendingRequest request = this.chequeBookPendingRequestRepositoryWrapper
.findOneWithNotFoundDetection(pendingRequestId);
if (!request.isPending()) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebookpendingrequest.not.pending",
"Cheque book pending request " + pendingRequestId + " has already been actioned.", pendingRequestId);
}
final AppUser checker = this.context.authenticatedUser();
if (request.isMadeBy(checker.getId())) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebookpendingrequest.self.approval",
"You cannot approve a cheque book request you submitted yourself — a different user must approve it.",
pendingRequestId);
}
final JsonCommand replay = reconstructCommand(request.getPayloadJson());
final ChequeBookPendingRequestType type = ChequeBookPendingRequestType.valueOf(request.getRequestType());
final Long createdResourceId = switch (type) {
case CREATE_SERIES -> finalizeCreateSeries(replay, request.getMakerId());
case RECEIVE_STOCK -> finalizeReceiveStock(replay, request.getMakerId());
case REQUEST_BOOK -> finalizeRequestChequeBook(replay, request.getMakerId());
case DELETE_SERIES ->
finalizeDeleteSeries(replay.longValueOfParameterNamed(ChequeBookApiJsonInputParams.SERIES_ID.getValue()));
};
final String activityLog = appendActivityLog(request.getActivityLog(), "APPROVED", checker, null);
request.approve(checker, createdResourceId, activityLog);
this.chequeBookPendingRequestRepositoryWrapper.saveAndFlush(request);
log.info("Cheque book pending request {} approved by {}, created resource {}", pendingRequestId, checker.getUsername(),
createdResourceId);
return new CommandProcessingResultBuilder().withEntityId(createdResourceId).build();
}
@Transactional
@Override
public CommandProcessingResult rejectPendingRequest(final Long pendingRequestId, final JsonCommand command) {
final ChequeBookPendingRequest request = this.chequeBookPendingRequestRepositoryWrapper
.findOneWithNotFoundDetection(pendingRequestId);
if (!request.isPending()) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebookpendingrequest.not.pending",
"Cheque book pending request " + pendingRequestId + " has already been actioned.", pendingRequestId);
}
final AppUser checker = this.context.authenticatedUser();
if (request.isMadeBy(checker.getId())) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebookpendingrequest.self.approval",
"You cannot reject a cheque book request you submitted yourself — a different user must action it.",
pendingRequestId);
}
final String reason = command.stringValueOfParameterNamedAllowingNull(ChequeBookApiJsonInputParams.REASON.getValue());
if (reason == null || reason.isBlank()) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebookpendingrequest.reject.reason.required",
"A reason is required to reject a cheque book pending request.", pendingRequestId);
}
final String activityLog = appendActivityLog(request.getActivityLog(), "REJECTED", checker, reason);
request.reject(checker, reason, activityLog);
this.chequeBookPendingRequestRepositoryWrapper.saveAndFlush(request);
log.info("Cheque book pending request {} rejected by {}", pendingRequestId, checker.getUsername());
return new CommandProcessingResultBuilder().withEntityId(pendingRequestId).build();
}
/** Reconstructs a usable {@link JsonCommand} from a pending request's stored raw JSON, for replay at approval. */
private JsonCommand reconstructCommand(final String json) {
// entityName, resourceId, subresourceId, groupId, clientId, loanId, savingsId, transactionId, url,
// productId, creditBureauId, organisationCreditBureauId, jobName, loanExternalId - none apply here
return JsonCommand.from(json, this.fromApiJsonHelper.parse(json), this.fromApiJsonHelper, null, null, null, null, null, null, null,
null, null, null, null, null, null, null);
}
private String appendActivityLog(final String existingJson, final String action, final AppUser user, final String note) {
JsonArray log = new JsonArray();
if (existingJson != null && !existingJson.isBlank()) {
try {
log = this.fromApiJsonHelper.parse(existingJson).getAsJsonArray();
} catch (final Exception e) {
log = new JsonArray();
}
}
final JsonObject entry = new JsonObject();
entry.addProperty("timestamp", Instant.now().toString());
entry.addProperty("action", action);
entry.addProperty("user", user.getUsername());
if (note != null && !note.isBlank()) {
entry.addProperty("note", note);
}
log.add(entry);
return this.gson.toJson(log);
}
// ============================= Helpers =============================
private List<ChequeLeaf> resolveLeavesInRange(final ChequeBook book, final JsonCommand command) {
if (!book.isIssued()) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.not.issued",
"Cheque book " + book.getId() + " has not been issued.", book.getId());
}
final Integer fromSeq = command.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.FROM_LEAF_SEQ.getValue());
final Integer toSeq = command.integerValueSansLocaleOfParameterNamed(ChequeBookApiJsonInputParams.TO_LEAF_SEQ.getValue());
final List<ChequeLeaf> leaves;
if (fromSeq == null && toSeq == null) {
leaves = this.chequeLeafRepository.findByChequeBookIdOrderByLeafSeqNoAsc(book.getId());
} else {
final int from = fromSeq != null ? fromSeq : 1;
final int to = toSeq != null ? toSeq : book.getTotalLeaves();
leaves = this.chequeLeafRepository.findByChequeBookIdAndLeafSeqNoBetweenOrderByLeafSeqNoAsc(book.getId(), from, to);
}
if (leaves.isEmpty()) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.no.leaves.in.range",
"No leaves found in the given range for cheque book " + book.getId() + ".", book.getId());
}
return leaves;
}
/**
* Resolves the account (Savings or Current — not interchangeable in this codebase), checks it is active
* and not dormant/blocked, and checks the {@code chequeBook} facility flag (account-level config,
* falling back to product-level config when the account-level config is absent).
*/
private void assertEligible(final ChequeAccountType accountType, final Long accountId) {
if (accountType == ChequeAccountType.SAVINGS) {
assertSavingsEligible(accountId);
} else if (accountType == ChequeAccountType.CURRENT) {
assertCurrentEligible(accountId);
} else {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.account.type.invalid", "Unknown account type.",
accountId);
}
}
private void assertSavingsEligible(final Long accountId) {
final SavingsAccount account = this.savingsAccountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
if (!account.isActive()) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.account.not.active",
"Savings account " + accountId + " is not active.", accountId);
}
final SavingsAccountSubStatusEnum subStatus = SavingsAccountSubStatusEnum.fromInt(account.getSubStatus());
if (subStatus != null && (subStatus.isSubStatusDormant() || subStatus.isSubStatusAccountBlocked()
|| subStatus.isSubStatusDebitBlocked())) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.account.blocked",
"Savings account " + accountId + " is dormant or blocked.", accountId);
}
Boolean chequeBookFlag = null;
final SavingsAccountFacilityConfig accountConfig = account.getFacilityConfig();
if (accountConfig != null) {
chequeBookFlag = accountConfig.getChequeBook();
} else if (account.savingsProduct() != null) {
final SavingsProductFacilityConfig productConfig = account.savingsProduct().getFacilityConfig();
if (productConfig != null) {
chequeBookFlag = productConfig.getChequeBook();
}
}
if (!Boolean.TRUE.equals(chequeBookFlag)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.facility.not.enabled",
"The cheque book facility is not enabled for savings account " + accountId + ".", accountId);
}
}
private void assertCurrentEligible(final Long accountId) {
final CurrentAccount account = this.currentAccountRepositoryWrapper.findOneWithNotFoundDetection(accountId);
if (!account.isActive()) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.account.not.active",
"Current account " + accountId + " is not active.", accountId);
}
final Integer subStatusEnum = account.getSubStatusEnum();
// 200=Dormant, 300=Block
if (subStatusEnum != null && (subStatusEnum == 200 || subStatusEnum == 300)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.account.blocked",
"Current account " + accountId + " is dormant or blocked.", accountId);
}
Boolean chequeBookFlag = null;
final CurrentAccountFacilityConfig accountConfig = account.getFacilityConfig();
if (accountConfig != null) {
chequeBookFlag = accountConfig.isChequeBook();
} else {
final CurrentProductFacilityConfig productConfig = this.currentProductFacilityConfigRepository
.findByCurrentProductId(account.getProductId());
if (productConfig != null) {
chequeBookFlag = productConfig.getChequeBook();
}
}
if (!Boolean.TRUE.equals(chequeBookFlag)) {
throw new GeneralPlatformDomainRuleException("error.msg.chequebook.facility.not.enabled",
"The cheque book facility is not enabled for current account " + accountId + ".", accountId);
}
}
}
@@ -0,0 +1,59 @@
/**
* 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.chequebook.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.portfolio.chequebook.domain.ChequeSeriesRepository;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* Whether a cheque series can be permanently removed is decided ATOMICALLY by the database's own foreign
* key constraints (stock lots/transfers/books referencing it), not by a separate "count references first"
* check — a count-then-act approach has a race: something could start referencing the series in the gap
* between the check and the delete (e.g. a concurrently-approved "receive stock" request). Attempting the
* real delete and catching a constraint violation makes the check and the action the same atomic step.
*
* This attempt runs in its own transaction (REQUIRES_NEW) so a rejection here never poisons the caller's
* transaction — Postgres marks a transaction aborted after a constraint violation, so the caller's
* subsequent deactivate-fallback would otherwise never be allowed to run.
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class ChequeSeriesHardDeleteAttempter {
private final ChequeSeriesRepository chequeSeriesRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean attemptDelete(final Long seriesId) {
try {
this.chequeSeriesRepository.deleteById(seriesId);
this.chequeSeriesRepository.flush();
return true;
} catch (final DataIntegrityViolationException e) {
log.info("Cheque series {} still has stock lots/transfers/books referencing it — deactivating instead of deleting",
seriesId);
return false;
}
}
}
@@ -46,7 +46,7 @@ fineract.security.oauth2.client.registrations.frontend-client.require-authorizat
fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres}
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:Keshav@898966}
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:313863}
fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
@@ -407,7 +407,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:Keshav@898966}
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863}
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
@@ -689,4 +689,4 @@ fineract.email.from-name=${FINERACT_EMAIL_FROM_NAME:CBSnexora}
fineract.email.support-address=${FINERACT_EMAIL_SUPPORT_ADDRESS:support@cbsnexora.com}
# Frontend base URL — used for email links (voting, etc.)
cbs.frontend.base-url=${CBS_FRONTEND_BASE_URL:http://localhost:4200}
cbs.frontend.base-url=${CBS_FRONTEND_BASE_URL:https://cbs.godemo.in}
@@ -399,4 +399,13 @@
<include file="parts/0340_add_terminate_true_to_savings_and_current_account.xml" relativeToChangelogFile="true"/>
<include file="parts/0368_catchup_savings_account_columns.xml" relativeToChangelogFile="true"/>
<include file="parts/0369_catchup_saving_product_temp_table.xml" relativeToChangelogFile="true"/>
<include file="parts/0379_chequebook_module.xml" relativeToChangelogFile="true"/>
<include file="parts/0380_cheque_series_account_type.xml" relativeToChangelogFile="true"/>
<include file="parts/0381_chequebook_no_of_books.xml" relativeToChangelogFile="true"/>
<include file="parts/0382_chequeleaf_used_status.xml" relativeToChangelogFile="true"/>
<include file="parts/0383_cheque_series_no_both.xml" relativeToChangelogFile="true"/>
<include file="parts/0384_chequebook_maker_checker.xml" relativeToChangelogFile="true"/>
<include file="parts/0385_chequebook_pending_requests.xml" relativeToChangelogFile="true"/>
<include file="parts/0386_enable_global_maker_checker.xml" relativeToChangelogFile="true"/>
<include file="parts/0387_chequeleaf_serial_unique_per_lot.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,287 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.1.xsd">
<!--
Cheque Book Inventory module: stationery master, a unified stock ledger (HO receipt and branch
transfer-in are the same "range of leaf serials sitting at an office, with a drawdown cursor"
concept), issuance to an account (Savings or Current — both exist in this codebase and are not
interchangeable, so a discriminator column is used instead of a JPA relation), and per-leaf
tracking (materialized at issuance, OR directly against a stock lot when unissued stock is
reported lost/stolen — see m_cheque_leaf.cheque_book_id being nullable).
Scope is deliberately limited to inventory + stop-payment/hotlisting. It does not model RBI's
Positive Pay System, "used/cleared" leaf status, or automatic cheque-validity staleness — all three
require clearing/presentment integration this CBS does not have; see the design plan for why
faking any of them would be actively wrong rather than merely incomplete.
Every changeSet is guarded so this migration is a safe no-op where already applied.
-->
<changeSet id="0379-cheque-series" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cheque_series"/></not>
</preConditions>
<createTable tableName="m_cheque_series">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="series_code" type="VARCHAR(20)">
<constraints nullable="false" unique="true" uniqueConstraintName="uk_cheque_series_code"/>
</column>
<column name="name" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="leaves_per_book" type="INTEGER">
<constraints nullable="false"/>
</column>
<column name="cts_compliant" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="reorder_level" type="INTEGER"/>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
</changeSet>
<changeSet id="0379-cheque-stock-lot" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cheque_stock_lot"/></not>
</preConditions>
<createTable tableName="m_cheque_stock_lot">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="series_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_stock_lot_series" references="m_cheque_series(id)"/>
</column>
<column name="office_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_stock_lot_office" references="m_office(id)"/>
</column>
<column name="range_start" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="range_end" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="next_serial_no" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="source" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="source_ref_id" type="BIGINT"/>
<column name="batch_ref" type="VARCHAR(100)"/>
<column name="status" type="VARCHAR(25)" defaultValue="AVAILABLE">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="received_by" type="BIGINT"/>
<column name="received_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="version" type="INTEGER" defaultValueNumeric="0"/>
</createTable>
</changeSet>
<changeSet id="0379-cheque-stock-transfer" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cheque_stock_transfer"/></not>
</preConditions>
<createTable tableName="m_cheque_stock_transfer">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="series_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_stock_transfer_series" references="m_cheque_series(id)"/>
</column>
<column name="from_office_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_stock_transfer_from_office" references="m_office(id)"/>
</column>
<column name="to_office_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_stock_transfer_to_office" references="m_office(id)"/>
</column>
<column name="from_lot_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_stock_transfer_from_lot" references="m_cheque_stock_lot(id)"/>
</column>
<column name="to_lot_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_stock_transfer_to_lot" references="m_cheque_stock_lot(id)"/>
</column>
<column name="range_start" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="range_end" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="total_leaves" type="INTEGER">
<constraints nullable="false"/>
</column>
<column name="dispatch_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="status" type="VARCHAR(20)" defaultValue="DISPATCHED">
<constraints nullable="false"/>
</column>
<column name="received_date" type="DATE"/>
<column name="dispatched_by" type="BIGINT"/>
<column name="received_by" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
</changeSet>
<changeSet id="0379-cheque-book" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cheque_book"/></not>
</preConditions>
<createTable tableName="m_cheque_book">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="account_type" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="account_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="office_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_book_office" references="m_office(id)"/>
</column>
<column name="series_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_cheque_book_series" references="m_cheque_series(id)"/>
</column>
<column name="requested_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="requested_by" type="BIGINT"/>
<column name="no_of_leaves_requested" type="INTEGER">
<constraints nullable="false"/>
</column>
<column name="status" type="VARCHAR(20)" defaultValue="REQUESTED">
<constraints nullable="false"/>
</column>
<column name="issue_date" type="DATE"/>
<column name="issued_by" type="BIGINT"/>
<column name="lot_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_cheque_book_lot" references="m_cheque_stock_lot(id)"/>
</column>
<column name="range_start" type="BIGINT"/>
<column name="range_end" type="BIGINT"/>
<column name="total_leaves" type="INTEGER"/>
<column name="rejection_reason" type="VARCHAR(500)"/>
<column name="collection_acknowledged" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="collected_date" type="DATE"/>
<column name="collected_by" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="version" type="INTEGER" defaultValueNumeric="0"/>
</createTable>
</changeSet>
<changeSet id="0379-cheque-leaf" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cheque_leaf"/></not>
</preConditions>
<createTable tableName="m_cheque_leaf">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="cheque_book_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_cheque_leaf_book" references="m_cheque_book(id)"/>
</column>
<column name="lot_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_cheque_leaf_lot" references="m_cheque_stock_lot(id)"/>
</column>
<column name="leaf_serial_no" type="BIGINT">
<constraints nullable="false" unique="true" uniqueConstraintName="uk_cheque_leaf_serial_no"/>
</column>
<column name="leaf_seq_no" type="INTEGER"/>
<column name="status" type="VARCHAR(20)" defaultValue="ACTIVE">
<constraints nullable="false"/>
</column>
<column name="stop_reason" type="VARCHAR(500)"/>
<column name="stopped_by" type="BIGINT"/>
<column name="stopped_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="lost_reason" type="VARCHAR(500)"/>
<column name="lost_reported_by" type="BIGINT"/>
<column name="lost_reported_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="cancelled_reason" type="VARCHAR(500)"/>
<column name="cancelled_by" type="BIGINT"/>
<column name="cancelled_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
</changeSet>
<changeSet id="0379-permissions" author="CBS" runOnChange="false">
<sql>
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_CHEQUESERIES', 'CHEQUESERIES', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CHEQUESERIES');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_CHEQUESERIES', 'CHEQUESERIES', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CHEQUESERIES');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'UPDATE_CHEQUESERIES', 'CHEQUESERIES', 'UPDATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'UPDATE_CHEQUESERIES');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'DELETE_CHEQUESERIES', 'CHEQUESERIES', 'DELETE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'DELETE_CHEQUESERIES');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_CHEQUESTOCKLOT', 'CHEQUESTOCKLOT', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CHEQUESTOCKLOT');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_CHEQUESTOCKLOT', 'CHEQUESTOCKLOT', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CHEQUESTOCKLOT');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'REPORTLOST_CHEQUESTOCKLOT', 'CHEQUESTOCKLOT', 'REPORTLOST', true
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REPORTLOST_CHEQUESTOCKLOT');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_CHEQUESTOCKTRANSFER', 'CHEQUESTOCKTRANSFER', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CHEQUESTOCKTRANSFER');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'RECEIVE_CHEQUESTOCKTRANSFER', 'CHEQUESTOCKTRANSFER', 'RECEIVE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'RECEIVE_CHEQUESTOCKTRANSFER');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'REJECT_CHEQUESTOCKTRANSFER', 'CHEQUESTOCKTRANSFER', 'REJECT', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REJECT_CHEQUESTOCKTRANSFER');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_CHEQUEBOOK', 'CHEQUEBOOK', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CREATE_CHEQUEBOOK', 'CHEQUEBOOK', 'CREATE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'ISSUE_CHEQUEBOOK', 'CHEQUEBOOK', 'ISSUE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'ISSUE_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'REJECT_CHEQUEBOOK', 'CHEQUEBOOK', 'REJECT', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REJECT_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'WITHDRAW_CHEQUEBOOK', 'CHEQUEBOOK', 'WITHDRAW', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'WITHDRAW_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'CANCELLEAF_CHEQUEBOOK', 'CHEQUEBOOK', 'CANCELLEAF', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CANCELLEAF_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'STOPPAYMENT_CHEQUEBOOK', 'CHEQUEBOOK', 'STOPPAYMENT', true
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'STOPPAYMENT_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'REVOKESTOPPAYMENT_CHEQUEBOOK', 'CHEQUEBOOK', 'REVOKESTOPPAYMENT', true
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REVOKESTOPPAYMENT_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'REPORTLOST_CHEQUEBOOK', 'CHEQUEBOOK', 'REPORTLOST', true
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REPORTLOST_CHEQUEBOOK');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'heading_menu', 'MENU_CHEQUEBOOK', 'MENU', 'CHEQUEBOOK', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'MENU_CHEQUEBOOK');
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.1.xsd">
<!--
Segregate cheque stationery by account scheme (Savings vs Current). Real banks stock physically
different cheque books for SB and CA accounts, so each series is tagged SAVINGS / CURRENT / BOTH.
Existing rows default to BOTH (backward compatible: any series stays issuable to any account type
until an operator narrows it). Issuance is validated against this at issue time.
-->
<changeSet id="0380-cheque-series-applicable-account-type" author="CBS">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_cheque_series" columnName="applicable_account_type"/></not>
</preConditions>
<addColumn tableName="m_cheque_series">
<column name="applicable_account_type" type="VARCHAR(10)" defaultValue="BOTH">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.1.xsd">
<!--
Cheque books are requested and issued in whole BOOKS of a fixed denomination (25-leaf, 50-leaf, ...),
not in an arbitrary leaf count. A requisition captures how many books the customer wants; the actual
leaves handed over are derived at issue time as books x series.leavesPerBook, and that leaf count plus
the per-leaf serials are persisted. no_of_leaves_requested becomes optional/legacy.
-->
<changeSet id="0381-cheque-book-no-of-books" author="CBS">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_cheque_book" columnName="no_of_books"/></not>
</preConditions>
<addColumn tableName="m_cheque_book">
<column name="no_of_books" type="INTEGER" defaultValueNumeric="1">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="0381-cheque-book-leaves-requested-optional" author="CBS">
<preConditions onFail="MARK_RAN">
<columnExists tableName="m_cheque_book" columnName="no_of_leaves_requested"/>
</preConditions>
<dropNotNullConstraint tableName="m_cheque_book" columnName="no_of_leaves_requested" columnDataType="INTEGER"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.1.xsd">
<!--
A manually-recorded USED status for a cheque leaf. This CBS has no clearing/presentment integration,
so the system cannot detect on its own when a specific leaf was honoured — a branch marks it USED
after seeing it clear on the bank/clearing statement (the same manual-recording pattern already used
for the settlement module's "actual bank settlement" entry, where automatic detection isn't feasible
either). This is a record-keeping status, not a fraud control, so it is not maker-checker.
-->
<changeSet id="0382-cheque-leaf-used-columns" author="CBS">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_cheque_leaf" columnName="used_reason"/></not>
</preConditions>
<addColumn tableName="m_cheque_leaf">
<column name="used_reason" type="VARCHAR(500)"/>
<column name="used_by" type="BIGINT"/>
<column name="used_on" type="TIMESTAMP WITH TIME ZONE"/>
</addColumn>
</changeSet>
<changeSet id="0382-permission" author="CBS" runOnChange="false">
<sql>
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'MARKUSED_CHEQUEBOOK', 'CHEQUEBOOK', 'MARKUSED', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'MARKUSED_CHEQUEBOOK');
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.1.xsd">
<!--
"BOTH" is removed as a cheque series applicability: every series must now be tagged explicitly as
SAVINGS or CURRENT (real banks stock physically different stationery per scheme, so "applies to
everything" was never a genuine stationery category). Any pre-existing BOTH-tagged row is narrowed
to SAVINGS — an operator can re-create a CURRENT-tagged series if that stock is also needed there.
-->
<changeSet id="0383-cheque-series-no-both-data" author="CBS" runOnChange="false">
<sql>
UPDATE m_cheque_series SET applicable_account_type = 'SAVINGS' WHERE applicable_account_type = 'BOTH';
</sql>
</changeSet>
<changeSet id="0383-cheque-series-no-both-default" author="CBS">
<preConditions onFail="MARK_RAN">
<columnExists tableName="m_cheque_series" columnName="applicable_account_type"/>
</preConditions>
<dropDefaultValue tableName="m_cheque_series" columnName="applicable_account_type"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.1.xsd">
<!--
Extend maker-checker to the three cheque-book creation actions (series master, stock receipt, book
requisition) so every request now needs a second user's approval before it takes effect, matching
the fraud-sensitive actions (stop-payment/report-lost/revoke) that already required it. No bespoke
approval logic is needed - can_maker_checker=true on the permission row is all the generic
command-processing pipeline needs; pending requests surface in the existing Checker Inbox screen.
-->
<changeSet id="0384-chequebook-maker-checker" author="CBS" runOnChange="false">
<sql>
UPDATE m_permission SET can_maker_checker = true
WHERE code IN ('CREATE_CHEQUESERIES', 'CREATE_CHEQUESTOCKLOT', 'CREATE_CHEQUEBOOK');
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.1.xsd">
<!--
Bespoke maker-checker for the three cheque-book creation actions (series master, stock receipt, book
requisition), mirroring the existing m_clerk_disbursement_request pattern rather than the native
Fineract command-approval engine: the native engine's maker-checker hold is bypassed by any user
whose role carries ALL_FUNCTIONS (Super user, Clerk in this tenant), which made it useless for this
CBS's actual admin accounts. This table instead holds a request as PENDING with its raw request JSON;
approval replays that JSON through the same creation logic used elsewhere in this module. The
submitting user (maker) can never approve/reject their own request (enforced in code, not just by
role) - any OTHER user with the same create permission can act as checker.
-->
<changeSet id="0385-chequebook-pending-requests-table" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_chequebook_pending_request"/></not>
</preConditions>
<createTable tableName="m_chequebook_pending_request">
<column name="id" type="BIGSERIAL">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="request_type" type="VARCHAR(30)">
<constraints nullable="false"/>
</column>
<column name="payload_json" type="TEXT">
<constraints nullable="false"/>
</column>
<column name="status" type="VARCHAR(20)" defaultValue="PENDING">
<constraints nullable="false"/>
</column>
<column name="maker_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="maker_name" type="VARCHAR(150)"/>
<column name="maker_username" type="VARCHAR(100)"/>
<column name="submitted_on" type="TIMESTAMP WITH TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="checker_id" type="BIGINT"/>
<column name="checker_name" type="VARCHAR(150)"/>
<column name="checker_username" type="VARCHAR(100)"/>
<column name="checked_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="rejection_reason" type="VARCHAR(500)"/>
<column name="created_resource_id" type="BIGINT"/>
<column name="activity_log" type="TEXT"/>
</createTable>
<sql>CREATE INDEX IF NOT EXISTS idx_cbpr_status ON m_chequebook_pending_request(status)</sql>
<sql>CREATE INDEX IF NOT EXISTS idx_cbpr_maker_id ON m_chequebook_pending_request(maker_id)</sql>
<sql>CREATE INDEX IF NOT EXISTS idx_cbpr_submitted_on ON m_chequebook_pending_request(submitted_on)</sql>
</changeSet>
<changeSet id="0385-chequebook-revert-native-maker-checker" author="CBS" runOnChange="false">
<!-- the native engine's hold is replaced by this table's bespoke workflow for these three actions -->
<sql>
UPDATE m_permission SET can_maker_checker = false
WHERE code IN ('CREATE_CHEQUESERIES', 'CREATE_CHEQUESTOCKLOT', 'CREATE_CHEQUEBOOK');
</sql>
</changeSet>
<changeSet id="0385-chequebook-pending-request-permission" author="CBS" runOnChange="false">
<sql>
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'READ_CHEQUEBOOKPENDINGREQUEST', 'CHEQUEBOOKPENDINGREQUEST', 'READ', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_CHEQUEBOOKPENDINGREQUEST');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'APPROVE_CHEQUEBOOKPENDINGREQUEST', 'CHEQUEBOOKPENDINGREQUEST', 'APPROVE', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'APPROVE_CHEQUEBOOKPENDINGREQUEST');
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
SELECT 'portfolio', 'REJECT_CHEQUEBOOKPENDINGREQUEST', 'CHEQUEBOOKPENDINGREQUEST', 'REJECT', false
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REJECT_CHEQUEBOOKPENDINGREQUEST');
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.1.xsd">
<!--
Fineract holds a per-permission can_maker_checker flag AND a single global "maker-checker" switch in
c_configuration - the global switch is checked FIRST (ConfigurationDomainServiceJpa.isMakerCheckerEnabledForTask),
and if it is disabled, every maker-checker-eligible permission in the whole system is bypassed
regardless of its own flag. It shipped disabled in this tenant, which meant the fraud-sensitive
actions built earlier (stop-payment/report-lost/revoke) were never actually holding for approval
either. Enabling it here turns on real maker-checker enforcement platform-wide, not just for the
cheque-book module. NB: a user whose role carries ALL_FUNCTIONS or CHECKER_SUPER_USER (Super user,
Clerk in this tenant) still bypasses the hold regardless of this switch - see AppUser.isCheckerSuperUser().
-->
<changeSet id="0386-enable-global-maker-checker" author="CBS" runOnChange="false">
<sql>
UPDATE c_configuration SET enabled = true WHERE name = 'maker-checker';
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<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.1.xsd">
<!--
leaf_serial_no was originally unique bank-wide, which matched the OLD global stock-numbering
design. Per-series numbering (0383) intentionally lets two different series share a serial number
(each is a separate stationery order from the press, numbered independently) - so a bare
leaf_serial_no can no longer be unique on its own once real leaves are materialized for two series
that happen to overlap. Uniqueness now needs to be scoped to the stock lot a leaf was drawn from
(every lot belongs to exactly one series, and lots within one series never overlap), not the whole
table.
-->
<changeSet id="0387-chequeleaf-serial-unique-per-lot" author="CBS" runOnChange="false">
<preConditions onFail="MARK_RAN">
<indexExists indexName="uk_cheque_leaf_serial_no"/>
</preConditions>
<dropUniqueConstraint tableName="m_cheque_leaf" constraintName="uk_cheque_leaf_serial_no"/>
</changeSet>
<changeSet id="0387-chequeleaf-serial-unique-per-lot-add" author="CBS" runOnChange="false">
<preConditions onFail="MARK_RAN">
<not>
<indexExists indexName="uk_cheque_leaf_lot_serial_no"/>
</not>
</preConditions>
<addUniqueConstraint tableName="m_cheque_leaf" columnNames="lot_id, leaf_serial_no"
constraintName="uk_cheque_leaf_lot_serial_no"/>
</changeSet>
</databaseChangeLog>