From 04d65d857f53ce1f61ca580001743660ceb6667e Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Wed, 15 Jul 2026 11:13:06 +0530 Subject: [PATCH] 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. --- .../service/CommandWrapperBuilder.java | 152 ++++ .../CurrentAccountRepositoryWrapper.java | 47 ++ .../api/ChequeBookApiConstants.java | 35 + .../api/ChequeBookApiJsonInputParams.java | 113 +++ .../chequebook/api/ChequeBookApiResource.java | 130 +++ .../ChequeBookPendingRequestApiResource.java | 102 +++ .../chequebook/api/ChequeLeafApiResource.java | 56 ++ .../api/ChequeSeriesApiResource.java | 103 +++ .../api/ChequeStockLotApiResource.java | 102 +++ .../api/ChequeStockTransferApiResource.java | 103 +++ .../chequebook/data/ChequeBookData.java | 55 ++ .../data/ChequeBookPendingRequestData.java | 46 ++ .../chequebook/data/ChequeLeafData.java | 60 ++ .../chequebook/data/ChequeSeriesData.java | 38 + .../chequebook/data/ChequeStockLotData.java | 46 ++ .../data/ChequeStockTransferData.java | 48 ++ .../chequebook/domain/ChequeAccountType.java | 40 + .../chequebook/domain/ChequeBook.java | 184 +++++ .../domain/ChequeBookPendingRequest.java | 130 +++ .../ChequeBookPendingRequestRepository.java | 25 + ...ueBookPendingRequestRepositoryWrapper.java | 38 + .../ChequeBookPendingRequestStatus.java | 27 + .../domain/ChequeBookPendingRequestType.java | 28 + .../domain/ChequeBookRepository.java | 25 + .../domain/ChequeBookRepositoryWrapper.java | 38 + .../chequebook/domain/ChequeBookStatus.java | 27 + .../chequebook/domain/ChequeLeaf.java | 194 +++++ .../domain/ChequeLeafRepository.java | 33 + .../domain/ChequeLeafRepositoryWrapper.java | 38 + .../chequebook/domain/ChequeLeafStatus.java | 38 + .../chequebook/domain/ChequeSeries.java | 111 +++ .../domain/ChequeSeriesApplicability.java | 53 ++ .../domain/ChequeSeriesRepository.java | 26 + .../domain/ChequeSeriesRepositoryWrapper.java | 38 + .../chequebook/domain/ChequeStockLot.java | 192 +++++ .../domain/ChequeStockLotRepository.java | 39 + .../ChequeStockLotRepositoryWrapper.java | 38 + .../domain/ChequeStockLotSource.java | 26 + .../domain/ChequeStockLotStatus.java | 34 + .../domain/ChequeStockTransfer.java | 133 +++ .../domain/ChequeStockTransferRepository.java | 25 + .../ChequeStockTransferRepositoryWrapper.java | 38 + .../domain/ChequeStockTransferStatus.java | 26 + .../ChequeBookNotFoundException.java | 28 + ...ueBookPendingRequestNotFoundException.java | 29 + .../ChequeLeafNotFoundException.java | 28 + .../ChequeSeriesNotFoundException.java | 28 + .../ChequeStockLotNotFoundException.java | 28 + .../ChequeStockTransferNotFoundException.java | 28 + ...hequeBookPendingRequestCommandHandler.java | 42 + .../CancelLeafChequeBookCommandHandler.java | 42 + .../CreateChequeBookCommandHandler.java | 42 + .../CreateChequeSeriesCommandHandler.java | 42 + .../CreateChequeStockLotCommandHandler.java | 42 + ...eateChequeStockTransferCommandHandler.java | 42 + .../DeleteChequeSeriesCommandHandler.java | 42 + .../IssueChequeBookCommandHandler.java | 42 + .../MarkUsedChequeBookCommandHandler.java | 42 + ...eiveChequeStockTransferCommandHandler.java | 42 + .../RejectChequeBookCommandHandler.java | 42 + ...hequeBookPendingRequestCommandHandler.java | 42 + ...jectChequeStockTransferCommandHandler.java | 42 + .../ReportLostChequeBookCommandHandler.java | 42 + ...eportLostChequeStockLotCommandHandler.java | 42 + ...keStopPaymentChequeBookCommandHandler.java | 42 + .../StopPaymentChequeBookCommandHandler.java | 42 + .../UpdateChequeSeriesCommandHandler.java | 42 + .../WithdrawChequeBookCommandHandler.java | 42 + ...queBookCommandFromApiJsonDeserializer.java | 267 ++++++ .../ChequeBookReadPlatformService.java | 63 ++ .../ChequeBookReadPlatformServiceImpl.java | 498 ++++++++++++ .../ChequeBookWritePlatformService.java | 78 ++ .../ChequeBookWritePlatformServiceImpl.java | 767 ++++++++++++++++++ .../ChequeSeriesHardDeleteAttempter.java | 59 ++ .../src/main/resources/application.properties | 6 +- .../db/changelog/tenant/changelog-tenant.xml | 9 + .../tenant/parts/0379_chequebook_module.xml | 287 +++++++ .../parts/0380_cheque_series_account_type.xml | 24 + .../parts/0381_chequebook_no_of_books.xml | 31 + .../parts/0382_chequeleaf_used_status.xml | 33 + .../parts/0383_cheque_series_no_both.xml | 26 + .../parts/0384_chequebook_maker_checker.xml | 21 + .../0385_chequebook_pending_requests.xml | 77 ++ .../0386_enable_global_maker_checker.xml | 23 + .../0387_chequeleaf_serial_unique_per_lot.xml | 33 + 85 files changed, 6146 insertions(+), 3 deletions(-) create mode 100644 fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccountRepositoryWrapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookApiConstants.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookApiJsonInputParams.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookPendingRequestApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeLeafApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeSeriesApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeStockLotApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeStockTransferApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeBookData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeBookPendingRequestData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeLeafData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeSeriesData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeStockLotData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeStockTransferData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeAccountType.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBook.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequest.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestRepositoryWrapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestStatus.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestType.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookRepositoryWrapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookStatus.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeaf.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeafRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeafRepositoryWrapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeafStatus.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeries.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeriesApplicability.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeriesRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeriesRepositoryWrapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLot.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotRepositoryWrapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotSource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotStatus.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransfer.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransferRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransferRepositoryWrapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransferStatus.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeBookNotFoundException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeBookPendingRequestNotFoundException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeLeafNotFoundException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeSeriesNotFoundException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeStockLotNotFoundException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeStockTransferNotFoundException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ApproveChequeBookPendingRequestCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CancelLeafChequeBookCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeBookCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeSeriesCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeStockLotCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeStockTransferCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/DeleteChequeSeriesCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/IssueChequeBookCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/MarkUsedChequeBookCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ReceiveChequeStockTransferCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RejectChequeBookCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RejectChequeBookPendingRequestCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RejectChequeStockTransferCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ReportLostChequeBookCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ReportLostChequeStockLotCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RevokeStopPaymentChequeBookCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/StopPaymentChequeBookCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/UpdateChequeSeriesCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/WithdrawChequeBookCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/serialization/ChequeBookCommandFromApiJsonDeserializer.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookReadPlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookReadPlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookWritePlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookWritePlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeSeriesHardDeleteAttempter.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0379_chequebook_module.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0380_cheque_series_account_type.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0381_chequebook_no_of_books.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0382_chequeleaf_used_status.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0383_cheque_series_no_both.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0384_chequebook_maker_checker.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0385_chequebook_pending_requests.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0386_enable_global_maker_checker.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0387_chequeleaf_serial_unique_per_lot.xml diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index b95df2d30..398218dec 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -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"; diff --git a/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccountRepositoryWrapper.java b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccountRepositoryWrapper.java new file mode 100644 index 000000000..fc15692f1 --- /dev/null +++ b/fineract-current-product/src/main/java/org/apache/fineract/portfolio/currentaccount/domain/CurrentAccountRepositoryWrapper.java @@ -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; + +/** + *

+ * Wrapper for {@link CurrentAccountRepository} that is responsible for checking if {@link CurrentAccount} is + * returned when using findOne repository method and throwing an appropriate not found exception. + *

+ * + *

+ * This is to avoid need for checking and throwing in multiple areas of code base where + * {@link CurrentAccountRepository} is required. + *

+ */ +@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)); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookApiConstants.java new file mode 100644 index 000000000..a9b340521 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookApiConstants.java @@ -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"; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookApiJsonInputParams.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookApiJsonInputParams.java new file mode 100644 index 000000000..f02e15eca --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookApiJsonInputParams.java @@ -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 seriesCreateParams() { + return toSet(SERIES_CODE, NAME, LEAVES_PER_BOOK, APPLICABLE_ACCOUNT_TYPE, CTS_COMPLIANT, REORDER_LEVEL, LOCALE, DATE_FORMAT); + } + + public static Set seriesUpdateParams() { + return toSet(NAME, LEAVES_PER_BOOK, APPLICABLE_ACCOUNT_TYPE, CTS_COMPLIANT, REORDER_LEVEL, LOCALE, DATE_FORMAT); + } + + public static Set stockLotReceiveParams() { + return toSet(SERIES_ID, OFFICE_ID, RANGE_START, RANGE_END, BATCH_REF, LOCALE, DATE_FORMAT); + } + + public static Set stockLotReportLostParams() { + return toSet(FROM_SERIAL_NO, REASON, LOCALE, DATE_FORMAT); + } + + public static Set stockTransferDispatchParams() { + return toSet(SERIES_ID, FROM_OFFICE_ID, TO_OFFICE_ID, FROM_LOT_ID, NO_OF_LEAVES, LOCALE, DATE_FORMAT); + } + + public static Set bookRequestParams() { + return toSet(ACCOUNT_TYPE, ACCOUNT_ID, OFFICE_ID, SERIES_ID, NO_OF_BOOKS, LOCALE, DATE_FORMAT); + } + + public static Set bookIssueParams() { + return toSet(COLLECTION_ACKNOWLEDGED, LOCALE, DATE_FORMAT); + } + + public static Set bookRejectParams() { + return toSet(REJECTION_REASON, LOCALE, DATE_FORMAT); + } + + public static Set bookLeafActionParams() { + return toSet(FROM_LEAF_SEQ, TO_LEAF_SEQ, REASON, LOCALE, DATE_FORMAT); + } + + private static Set toSet(final ChequeBookApiJsonInputParams... params) { + return Arrays.stream(params).map(ChequeBookApiJsonInputParams::getValue).collect(Collectors.toCollection(HashSet::new)); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookApiResource.java new file mode 100644 index 000000000..bffbafa9e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookApiResource.java @@ -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 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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookPendingRequestApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookPendingRequestApiResource.java new file mode 100644 index 000000000..a6ed585ba --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeBookPendingRequestApiResource.java @@ -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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeLeafApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeLeafApiResource.java new file mode 100644 index 000000000..0412bff42 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeLeafApiResource.java @@ -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 lookup(@QueryParam("serialNo") @Parameter(description = "serialNo") final Long serialNo) { + this.context.authenticatedUser().validateHasReadPermission(ChequeBookApiConstants.ENTITY_BOOK); + return this.readPlatformService.retrieveLeafBySerialNo(serialNo); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeSeriesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeSeriesApiResource.java new file mode 100644 index 000000000..75a8c5054 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeSeriesApiResource.java @@ -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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeStockLotApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeStockLotApiResource.java new file mode 100644 index 000000000..86f1b6b81 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeStockLotApiResource.java @@ -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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeStockTransferApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeStockTransferApiResource.java new file mode 100644 index 000000000..0cf762e0b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/api/ChequeStockTransferApiResource.java @@ -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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeBookData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeBookData.java new file mode 100644 index 000000000..8e0d5b564 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeBookData.java @@ -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; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeBookPendingRequestData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeBookPendingRequestData.java new file mode 100644 index 000000000..d07fd39de --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeBookPendingRequestData.java @@ -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; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeLeafData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeLeafData.java new file mode 100644 index 000000000..bd3e23b7d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeLeafData.java @@ -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; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeSeriesData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeSeriesData.java new file mode 100644 index 000000000..8dac7fc9f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeSeriesData.java @@ -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; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeStockLotData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeStockLotData.java new file mode 100644 index 000000000..dfad4a260 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeStockLotData.java @@ -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; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeStockTransferData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeStockTransferData.java new file mode 100644 index 000000000..32253dca2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/data/ChequeStockTransferData.java @@ -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; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeAccountType.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeAccountType.java new file mode 100644 index 000000000..889880c54 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeAccountType.java @@ -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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBook.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBook.java new file mode 100644 index 000000000..6fbf6015d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBook.java @@ -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 { + + @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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequest.java new file mode 100644 index 000000000..5bc06507c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequest.java @@ -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 { + + @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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestRepository.java new file mode 100644 index 000000000..67e32e2de --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestRepository.java @@ -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 { + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestRepositoryWrapper.java new file mode 100644 index 000000000..db653614a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestRepositoryWrapper.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestStatus.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestStatus.java new file mode 100644 index 000000000..61da73a7f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestStatus.java @@ -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; // +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestType.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestType.java new file mode 100644 index 000000000..fd5a239f4 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookPendingRequestType.java @@ -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; // +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookRepository.java new file mode 100644 index 000000000..2091bd0b6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookRepository.java @@ -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 { + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookRepositoryWrapper.java new file mode 100644 index 000000000..7f7d15a47 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookRepositoryWrapper.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookStatus.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookStatus.java new file mode 100644 index 000000000..eeb8c4f0d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeBookStatus.java @@ -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; // +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeaf.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeaf.java new file mode 100644 index 000000000..16d8d6f7d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeaf.java @@ -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. + * + *

+ * Legal transitions: (created)->ACTIVE (issued) or (created, no book)->LOST (unissued theft report); + * ACTIVE<->STOP_PAYMENT (the one round-trip); ACTIVE->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 { + + @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(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeafRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeafRepository.java new file mode 100644 index 000000000..23443e58f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeafRepository.java @@ -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 { + + Optional findByLeafSerialNo(Long leafSerialNo); + + List findByChequeBookIdOrderByLeafSeqNoAsc(Long chequeBookId); + + List findByChequeBookIdAndLeafSeqNoBetweenOrderByLeafSeqNoAsc(Long chequeBookId, Integer fromSeqNo, + Integer toSeqNo); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeafRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeafRepositoryWrapper.java new file mode 100644 index 000000000..f8c2304f2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeafRepositoryWrapper.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeafStatus.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeafStatus.java new file mode 100644 index 000000000..4229dec45 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeLeafStatus.java @@ -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)->ACTIVE (issued) or (created, no book)->LOST (unissued stock reported + * stolen); ACTIVE<->STOP_PAYMENT (the one round-trip, via stop-payment/revoke); ACTIVE->CANCELLED + * (spoiled leaf voided); ACTIVE->LOST (reported lost/stolen post-issuance); ACTIVE->WITHDRAWN (account + * closed); ACTIVE->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; // +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeries.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeries.java new file mode 100644 index 000000000..5612e922d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeries.java @@ -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 { + + @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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeriesApplicability.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeriesApplicability.java new file mode 100644 index 000000000..d9fbb1466 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeriesApplicability.java @@ -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()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeriesRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeriesRepository.java new file mode 100644 index 000000000..9454a5379 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeriesRepository.java @@ -0,0 +1,26 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.chequebook.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChequeSeriesRepository extends JpaRepository { + + boolean existsBySeriesCode(String seriesCode); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeriesRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeriesRepositoryWrapper.java new file mode 100644 index 000000000..76cbd3644 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeSeriesRepositoryWrapper.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLot.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLot.java new file mode 100644 index 000000000..ad420c5e4 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLot.java @@ -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 { + + @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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotRepository.java new file mode 100644 index 000000000..0c6676ed6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotRepository.java @@ -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 { + + List 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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotRepositoryWrapper.java new file mode 100644 index 000000000..24e66b9ed --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotRepositoryWrapper.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotSource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotSource.java new file mode 100644 index 000000000..f5d82178b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotSource.java @@ -0,0 +1,26 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.chequebook.domain; + +/** How a {@link ChequeStockLot} came to exist at its current custodian office. */ +public enum ChequeStockLotSource { + + RECEIPT, // + TRANSFER_IN; // +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotStatus.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotStatus.java new file mode 100644 index 000000000..d73902f5a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockLotStatus.java @@ -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; // +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransfer.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransfer.java new file mode 100644 index 000000000..c40f5d454 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransfer.java @@ -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 { + + @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; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransferRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransferRepository.java new file mode 100644 index 000000000..c50ecfb39 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransferRepository.java @@ -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 { + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransferRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransferRepositoryWrapper.java new file mode 100644 index 000000000..f5192ecd7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransferRepositoryWrapper.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransferStatus.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransferStatus.java new file mode 100644 index 000000000..e339f8b7c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/domain/ChequeStockTransferStatus.java @@ -0,0 +1,26 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.chequebook.domain; + +public enum ChequeStockTransferStatus { + + DISPATCHED, // + RECEIVED, // + REJECTED; // +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeBookNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeBookNotFoundException.java new file mode 100644 index 000000000..e18818228 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeBookNotFoundException.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeBookPendingRequestNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeBookPendingRequestNotFoundException.java new file mode 100644 index 000000000..5b88d76d2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeBookPendingRequestNotFoundException.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeLeafNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeLeafNotFoundException.java new file mode 100644 index 000000000..356934ac8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeLeafNotFoundException.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeSeriesNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeSeriesNotFoundException.java new file mode 100644 index 000000000..ad6f3931e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeSeriesNotFoundException.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeStockLotNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeStockLotNotFoundException.java new file mode 100644 index 000000000..053bcf579 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeStockLotNotFoundException.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeStockTransferNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeStockTransferNotFoundException.java new file mode 100644 index 000000000..b8635b7a2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/exception/ChequeStockTransferNotFoundException.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ApproveChequeBookPendingRequestCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ApproveChequeBookPendingRequestCommandHandler.java new file mode 100644 index 000000000..777ef1867 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ApproveChequeBookPendingRequestCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CancelLeafChequeBookCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CancelLeafChequeBookCommandHandler.java new file mode 100644 index 000000000..98d9ff605 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CancelLeafChequeBookCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeBookCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeBookCommandHandler.java new file mode 100644 index 000000000..d0cdea088 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeBookCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeSeriesCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeSeriesCommandHandler.java new file mode 100644 index 000000000..7eb82df62 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeSeriesCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeStockLotCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeStockLotCommandHandler.java new file mode 100644 index 000000000..08b767ccc --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeStockLotCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeStockTransferCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeStockTransferCommandHandler.java new file mode 100644 index 000000000..2c1c14250 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/CreateChequeStockTransferCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/DeleteChequeSeriesCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/DeleteChequeSeriesCommandHandler.java new file mode 100644 index 000000000..a034517b3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/DeleteChequeSeriesCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/IssueChequeBookCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/IssueChequeBookCommandHandler.java new file mode 100644 index 000000000..a452cf3d1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/IssueChequeBookCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/MarkUsedChequeBookCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/MarkUsedChequeBookCommandHandler.java new file mode 100644 index 000000000..f8c17ddb1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/MarkUsedChequeBookCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ReceiveChequeStockTransferCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ReceiveChequeStockTransferCommandHandler.java new file mode 100644 index 000000000..450e653af --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ReceiveChequeStockTransferCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RejectChequeBookCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RejectChequeBookCommandHandler.java new file mode 100644 index 000000000..85e1fe8d1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RejectChequeBookCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RejectChequeBookPendingRequestCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RejectChequeBookPendingRequestCommandHandler.java new file mode 100644 index 000000000..34bb193ae --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RejectChequeBookPendingRequestCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RejectChequeStockTransferCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RejectChequeStockTransferCommandHandler.java new file mode 100644 index 000000000..37060e0ae --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RejectChequeStockTransferCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ReportLostChequeBookCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ReportLostChequeBookCommandHandler.java new file mode 100644 index 000000000..650eb38b3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ReportLostChequeBookCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ReportLostChequeStockLotCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ReportLostChequeStockLotCommandHandler.java new file mode 100644 index 000000000..abc823b05 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/ReportLostChequeStockLotCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RevokeStopPaymentChequeBookCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RevokeStopPaymentChequeBookCommandHandler.java new file mode 100644 index 000000000..e775dc9bf --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/RevokeStopPaymentChequeBookCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/StopPaymentChequeBookCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/StopPaymentChequeBookCommandHandler.java new file mode 100644 index 000000000..98790bcc6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/StopPaymentChequeBookCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/UpdateChequeSeriesCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/UpdateChequeSeriesCommandHandler.java new file mode 100644 index 000000000..07be4a828 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/UpdateChequeSeriesCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/WithdrawChequeBookCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/WithdrawChequeBookCommandHandler.java new file mode 100644 index 000000000..0719c3bbd --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/handler/WithdrawChequeBookCommandHandler.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/serialization/ChequeBookCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/serialization/ChequeBookCommandFromApiJsonDeserializer.java new file mode 100644 index 000000000..20934f115 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/serialization/ChequeBookCommandFromApiJsonDeserializer.java @@ -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 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 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 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 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 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 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 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 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 supportedParams) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParams); + return this.fromApiJsonHelper.parse(json); + } + + private void throwIfErrors(final List errors) { + if (!errors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookReadPlatformService.java new file mode 100644 index 000000000..20a00386f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookReadPlatformService.java @@ -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 retrieveAllSeries(String status, Integer offset, Integer limit); + + ChequeSeriesData retrieveSeries(Long id); + + Page retrieveStockLots(Long officeId, Long seriesId, String status, Integer offset, Integer limit); + + ChequeStockLotData retrieveStockLot(Long id); + + Page retrieveStockTransfers(String status, Integer offset, Integer limit); + + ChequeStockTransferData retrieveStockTransfer(Long id); + + Page retrieveAllBooks(String accountType, Long accountId, Long officeId, String status, Integer offset, + Integer limit); + + ChequeBookData retrieveBook(Long id); + + List 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 retrieveLeafBySerialNo(Long leafSerialNo); + + /** Bespoke maker-checker queue for series/stock/book creation requests — see {@code m_chequebook_pending_request}. */ + Page retrievePendingRequests(String status, String fromDate, String toDate, Integer offset, + Integer limit); + + ChequeBookPendingRequestData retrievePendingRequest(Long id); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookReadPlatformServiceImpl.java new file mode 100644 index 000000000..630a7694b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookReadPlatformServiceImpl.java @@ -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 retrieveAllSeries(final String status, final Integer offset, final Integer limit) { + final StringBuilder where = new StringBuilder(" where 1=1"); + final List 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 pageArgs = new ArrayList<>(args); + pageArgs.add(limit); + pageArgs.add(offset); + final List 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 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 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 pageArgs = new ArrayList<>(args); + pageArgs.add(limit); + pageArgs.add(offset); + final List 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 retrieveStockTransfers(final String status, final Integer offset, final Integer limit) { + final StringBuilder where = new StringBuilder(" where 1=1"); + final List 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 pageArgs = new ArrayList<>(args); + pageArgs.add(limit); + pageArgs.add(offset); + final List 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 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 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 pageArgs = new ArrayList<>(args); + pageArgs.add(limit); + pageArgs.add(offset); + final List books = this.jdbcTemplate.query(pageSql.toString(), this.bookMapper, pageArgs.toArray()); + final List 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 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 leaves = this.jdbcTemplate.query(sql, this.leafMapper, chequeBookId); + return leaves.stream().map(this::withAccountInfo).toList(); + } + + @Override + public List 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 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 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 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 pageArgs = new ArrayList<>(args); + pageArgs.add(limit); + pageArgs.add(offset); + final List 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 { + + 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 { + + 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 { + + 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 { + + 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 { + + 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 { + + 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(); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookWritePlatformService.java new file mode 100644 index 000000000..510c3e73b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookWritePlatformService.java @@ -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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookWritePlatformServiceImpl.java new file mode 100644 index 000000000..17edfd73a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeBookWritePlatformServiceImpl.java @@ -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 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 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 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 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 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 leaves = this.chequeLeafRepository.findByChequeBookIdOrderByLeafSeqNoAsc(chequeBookId); + final List 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 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 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 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 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 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 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 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); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeSeriesHardDeleteAttempter.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeSeriesHardDeleteAttempter.java new file mode 100644 index 000000000..e9821efc1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/chequebook/service/ChequeSeriesHardDeleteAttempter.java @@ -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; + } + } +} diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index a1b2caaf0..73358eebe 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -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} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 1b866e481..5ee01a0e0 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -399,4 +399,13 @@ + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0379_chequebook_module.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0379_chequebook_module.xml new file mode 100644 index 000000000..07fca282a --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0379_chequebook_module.xml @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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'); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0380_cheque_series_account_type.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0380_cheque_series_account_type.xml new file mode 100644 index 000000000..2377db70c --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0380_cheque_series_account_type.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0381_chequebook_no_of_books.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0381_chequebook_no_of_books.xml new file mode 100644 index 000000000..a93cc4f53 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0381_chequebook_no_of_books.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0382_chequeleaf_used_status.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0382_chequeleaf_used_status.xml new file mode 100644 index 000000000..51fad3e5a --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0382_chequeleaf_used_status.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + 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'); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0383_cheque_series_no_both.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0383_cheque_series_no_both.xml new file mode 100644 index 000000000..f4181352e --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0383_cheque_series_no_both.xml @@ -0,0 +1,26 @@ + + + + + + + + UPDATE m_cheque_series SET applicable_account_type = 'SAVINGS' WHERE applicable_account_type = 'BOTH'; + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0384_chequebook_maker_checker.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0384_chequebook_maker_checker.xml new file mode 100644 index 000000000..142f6c7b7 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0384_chequebook_maker_checker.xml @@ -0,0 +1,21 @@ + + + + + + + + UPDATE m_permission SET can_maker_checker = true + WHERE code IN ('CREATE_CHEQUESERIES', 'CREATE_CHEQUESTOCKLOT', 'CREATE_CHEQUEBOOK'); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0385_chequebook_pending_requests.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0385_chequebook_pending_requests.xml new file mode 100644 index 000000000..e0eaccee2 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0385_chequebook_pending_requests.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CREATE INDEX IF NOT EXISTS idx_cbpr_status ON m_chequebook_pending_request(status) + CREATE INDEX IF NOT EXISTS idx_cbpr_maker_id ON m_chequebook_pending_request(maker_id) + CREATE INDEX IF NOT EXISTS idx_cbpr_submitted_on ON m_chequebook_pending_request(submitted_on) + + + + + + UPDATE m_permission SET can_maker_checker = false + WHERE code IN ('CREATE_CHEQUESERIES', 'CREATE_CHEQUESTOCKLOT', 'CREATE_CHEQUEBOOK'); + + + + + + 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'); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0386_enable_global_maker_checker.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0386_enable_global_maker_checker.xml new file mode 100644 index 000000000..24df6512d --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0386_enable_global_maker_checker.xml @@ -0,0 +1,23 @@ + + + + + + + + UPDATE c_configuration SET enabled = true WHERE name = 'maker-checker'; + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0387_chequeleaf_serial_unique_per_lot.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0387_chequeleaf_serial_unique_per_lot.xml new file mode 100644 index 000000000..2880313bf --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0387_chequeleaf_serial_unique_per_lot.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + +