From aa3cb6ad25b1bfea8abe91f119515c399ff2c86f Mon Sep 17 00:00:00 2001 From: tarunmalviya Date: Mon, 5 Oct 2026 15:18:52 +0530 Subject: [PATCH] Locker module: class master, locker list, waiting list, allotment (tables + client details API) --- .../service/CommandWrapperBuilder.java | 48 +++ .../api/LockerAllotmentApiResource.java | 53 +++ .../locker/api/LockerApiConstants.java | 35 ++ .../locker/api/LockerApiJsonInputParams.java | 99 +++++ .../locker/api/LockerApiResource.java | 66 ++++ .../locker/api/LockerClassApiResource.java | 103 ++++++ .../api/LockerWaitingListApiResource.java | 119 ++++++ .../data/LockerAllotmentClientData.java | 83 +++++ .../locker/data/LockerClassData.java | 50 +++ .../locker/data/LockerClientContactData.java | 40 ++ .../portfolio/locker/data/LockerData.java | 39 ++ .../locker/data/LockerSummaryData.java | 33 ++ .../locker/data/LockerWaitingListData.java | 52 +++ .../portfolio/locker/domain/Locker.java | 63 ++++ .../portfolio/locker/domain/LockerClass.java | 154 ++++++++ .../locker/domain/LockerClassName.java | 24 ++ .../locker/domain/LockerClassRepository.java | 37 ++ .../domain/LockerClassRepositoryWrapper.java | 38 ++ .../locker/domain/LockerRepository.java | 39 ++ .../portfolio/locker/domain/LockerStatus.java | 24 ++ .../locker/domain/LockerWaitingList.java | 158 ++++++++ .../domain/LockerWaitingListRepository.java | 23 ++ .../LockerWaitingListRepositoryWrapper.java | 38 ++ .../domain/LockerWaitingListStatus.java | 24 ++ .../LockerClassNotFoundException.java | 28 ++ .../LockerWaitingListNotFoundException.java | 28 ++ ...CancelLockerWaitingListCommandHandler.java | 42 +++ .../CreateLockerClassCommandHandler.java | 42 +++ ...CreateLockerWaitingListCommandHandler.java | 42 +++ .../DeleteLockerClassCommandHandler.java | 42 +++ .../UpdateLockerClassCommandHandler.java | 42 +++ ...UpdateLockerWaitingListCommandHandler.java | 42 +++ .../LockerCommandFromApiJsonDeserializer.java | 266 ++++++++++++++ .../locker/service/LockerOfficeAccess.java | 76 ++++ .../service/LockerReadPlatformService.java | 46 +++ .../LockerReadPlatformServiceImpl.java | 344 ++++++++++++++++++ .../service/LockerWritePlatformService.java | 37 ++ .../LockerWritePlatformServiceImpl.java | 265 ++++++++++++++ .../tenant/final-changelog-tenant.xml | 4 + .../tenant/parts/0571_locker_class_master.xml | 155 ++++++++ .../tenant/parts/0572_locker_waiting_list.xml | 112 ++++++ .../0573_locker_waiting_list_client_link.xml | 43 +++ .../tenant/parts/0574_locker_allotment.xml | 215 +++++++++++ 43 files changed, 3313 insertions(+) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerAllotmentApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerApiConstants.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerApiJsonInputParams.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerClassApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerWaitingListApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerAllotmentClientData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerClassData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerClientContactData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerSummaryData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerWaitingListData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/Locker.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClass.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClassName.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClassRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClassRepositoryWrapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerStatus.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingList.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingListRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingListRepositoryWrapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingListStatus.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/exception/LockerClassNotFoundException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/exception/LockerWaitingListNotFoundException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/CancelLockerWaitingListCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/CreateLockerClassCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/CreateLockerWaitingListCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/DeleteLockerClassCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/UpdateLockerClassCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/UpdateLockerWaitingListCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/serialization/LockerCommandFromApiJsonDeserializer.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerOfficeAccess.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerReadPlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerReadPlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerWritePlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerWritePlatformServiceImpl.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0571_locker_class_master.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0572_locker_waiting_list.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0573_locker_waiting_list_client_link.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0574_locker_allotment.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 a25630c39..6e208bbe1 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 @@ -95,6 +95,54 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder createLockerClass() { + this.actionName = "CREATE"; + this.entityName = "LOCKERCLASS"; + this.entityId = null; + this.href = "/lockerclasses"; + return this; + } + + public CommandWrapperBuilder updateLockerClass(final Long lockerClassId) { + this.actionName = "UPDATE"; + this.entityName = "LOCKERCLASS"; + this.entityId = lockerClassId; + this.href = "/lockerclasses/" + lockerClassId; + return this; + } + + public CommandWrapperBuilder deleteLockerClass(final Long lockerClassId) { + this.actionName = "DELETE"; + this.entityName = "LOCKERCLASS"; + this.entityId = lockerClassId; + this.href = "/lockerclasses/" + lockerClassId; + return this; + } + + public CommandWrapperBuilder createLockerWaitingList() { + this.actionName = "CREATE"; + this.entityName = "LOCKERWAITINGLIST"; + this.entityId = null; + this.href = "/lockerwaitinglist"; + return this; + } + + public CommandWrapperBuilder updateLockerWaitingList(final Long waitingListId) { + this.actionName = "UPDATE"; + this.entityName = "LOCKERWAITINGLIST"; + this.entityId = waitingListId; + this.href = "/lockerwaitinglist/" + waitingListId; + return this; + } + + public CommandWrapperBuilder cancelLockerWaitingList(final Long waitingListId) { + this.actionName = "CANCEL"; + this.entityName = "LOCKERWAITINGLIST"; + this.entityId = waitingListId; + this.href = "/lockerwaitinglist/" + waitingListId + "?command=cancel"; + return this; + } + public CommandWrapperBuilder createChequeStockLot() { this.actionName = "CREATE"; this.entityName = "CHEQUESTOCKLOT"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerAllotmentApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerAllotmentApiResource.java new file mode 100644 index 000000000..ee156b35a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerAllotmentApiResource.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.locker.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.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.locker.data.LockerAllotmentClientData; +import org.apache.fineract.portfolio.locker.service.LockerReadPlatformService; +import org.springframework.stereotype.Component; + +/** Locker allotment — applications for a locker by an existing client, approved by the Branch Manager. */ +@Path("/v1/lockerallotments") +@Component +@Tag(name = "Locker Allotment", description = "Locker applications: client, locker, joint holders, security deposit, nominees; Branch Manager approval.") +@RequiredArgsConstructor +public class LockerAllotmentApiResource { + + private final PlatformSecurityContext context; + private final LockerReadPlatformService readPlatformService; + + @GET + @Path("clientdetails/{clientId}") + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Client details for an allotment", description = "Contact, address, gender, occupation, PAN, masked Aadhaar, membership, active savings accounts and fixed deposits. Only clients within the user's branch scope.") + public LockerAllotmentClientData retrieveClientDetails(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId) { + this.context.authenticatedUser().validateHasReadPermission(LockerApiConstants.ENTITY_ALLOTMENT); + return this.readPlatformService.retrieveAllotmentClientDetails(clientId); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerApiConstants.java new file mode 100644 index 000000000..7d1733b9f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerApiConstants.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.locker.api; + +public final class LockerApiConstants { + + private LockerApiConstants() {} + + public static final String ENTITY_CLASS = "LOCKERCLASS"; + public static final String ENTITY_LOCKER = "LOCKER"; + public static final String ENTITY_WAITING_LIST = "LOCKERWAITINGLIST"; + public static final String ENTITY_ALLOTMENT = "LOCKERALLOTMENT"; + + public static final String RESOURCE_CLASS = "lockerclass"; + public static final String RESOURCE_WAITING_LIST = "lockerwaitinglist"; + + /** Upper bound on one range, so a typo such as 1-100000 cannot insert a hundred thousand locker rows. */ + public static final int MAX_LOCKERS_PER_RANGE = 1000; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerApiJsonInputParams.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerApiJsonInputParams.java new file mode 100644 index 000000000..2e9991dac --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerApiJsonInputParams.java @@ -0,0 +1,99 @@ +/** + * 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.locker.api; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +public enum LockerApiJsonInputParams { + + // class create/update + OFFICE_ID("officeId"), // + CLASS_CODE("classCode"), // + CLASS_NAME("className"), // + AREA("area"), // + RENT("rent"), // + MEMBER_RENT("memberRent"), // + DEPOSIT("deposit"), // + MEMBER_DEPOSIT("memberDeposit"), // + START_LOCKER_NO("startLockerNo"), // + END_LOCKER_NO("endLockerNo"), // + EFFECTIVE_FROM_DATE("effectiveFromDate"), // + EFFECTIVE_TO_DATE("effectiveToDate"), // + // waiting list create/update/cancel + CLIENT_ID("clientId"), // + INQUIRY_DATE("inquiryDate"), // + CUSTOMER_NAME("customerName"), // + ADDRESS_LINE1("addressLine1"), // + ADDRESS_LINE2("addressLine2"), // + ADDRESS_LINE3("addressLine3"), // + CITY("city"), // + PINCODE("pincode"), // + PHONE("phone"), // + EMAIL("email"), // + PREFERRED_CLASSES("preferredClasses"), // + REMARKS("remarks"), // + ADDITIONAL_REMARKS("additionalRemarks"), // + // common — the web client always sends these on command payloads + LOCALE("locale"), // + DATE_FORMAT("dateFormat"); + + private final String value; + + LockerApiJsonInputParams(final String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + private static Set setOf(final LockerApiJsonInputParams... params) { + return Arrays.stream(params).map(LockerApiJsonInputParams::getValue).collect(Collectors.toCollection(HashSet::new)); + } + + public static Set classCreateParams() { + return setOf(OFFICE_ID, CLASS_CODE, CLASS_NAME, AREA, RENT, MEMBER_RENT, DEPOSIT, MEMBER_DEPOSIT, START_LOCKER_NO, END_LOCKER_NO, + EFFECTIVE_FROM_DATE, EFFECTIVE_TO_DATE, LOCALE, DATE_FORMAT); + } + + /** Class code, branch and locker range are fixed once saved — a different range is a new row. */ + public static Set classUpdateParams() { + return setOf(CLASS_NAME, AREA, RENT, MEMBER_RENT, DEPOSIT, MEMBER_DEPOSIT, EFFECTIVE_FROM_DATE, EFFECTIVE_TO_DATE, LOCALE, + DATE_FORMAT); + } + + public static Set waitingListCreateParams() { + final Set params = waitingListUpdateParams(); + params.add(OFFICE_ID.getValue()); + return params; + } + + /** Branch is fixed once the entry is taken. */ + public static Set waitingListUpdateParams() { + return setOf(CLIENT_ID, INQUIRY_DATE, CUSTOMER_NAME, ADDRESS_LINE1, ADDRESS_LINE2, ADDRESS_LINE3, CITY, PINCODE, PHONE, EMAIL, + PREFERRED_CLASSES, REMARKS, LOCALE, DATE_FORMAT); + } + + public static Set waitingListCancelParams() { + return setOf(ADDITIONAL_REMARKS, LOCALE, DATE_FORMAT); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerApiResource.java new file mode 100644 index 000000000..c127e0f8b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerApiResource.java @@ -0,0 +1,66 @@ +/** + * 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.locker.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.DefaultValue; +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 lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.locker.data.LockerData; +import org.apache.fineract.portfolio.locker.data.LockerSummaryData; +import org.apache.fineract.portfolio.locker.service.LockerReadPlatformService; +import org.springframework.stereotype.Component; + +/** Read-only locker inventory — the boxes created from class master ranges, with their status. */ +@Path("/v1/lockers") +@Component +@Tag(name = "Lockers", description = "Safe deposit locker inventory — every locker of the caller's branch with class, key number and status.") +@RequiredArgsConstructor +public class LockerApiResource { + + private final PlatformSecurityContext context; + private final LockerReadPlatformService readPlatformService; + + @GET + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List lockers", description = "Filter by class code, status (VACANT / ALLOTTED / BLOCKED) and locker number.") + public Page retrieveAll(@QueryParam("officeId") final Long officeId, @QueryParam("classCode") final String classCode, + @QueryParam("status") final String status, @QueryParam("lockerNo") final Integer lockerNo, + @QueryParam("offset") @DefaultValue("0") final Integer offset, @QueryParam("limit") @DefaultValue("20") final Integer limit) { + this.context.authenticatedUser().validateHasReadPermission(LockerApiConstants.ENTITY_LOCKER); + final int safeLimit = Math.min(limit, 500); + return this.readPlatformService.retrieveLockers(officeId, classCode, status, lockerNo, offset, safeLimit); + } + + @GET + @Path("summary") + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Locker counts by status") + public LockerSummaryData retrieveSummary(@QueryParam("officeId") final Long officeId) { + this.context.authenticatedUser().validateHasReadPermission(LockerApiConstants.ENTITY_LOCKER); + return this.readPlatformService.retrieveLockerSummary(officeId); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerClassApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerClassApiResource.java new file mode 100644 index 000000000..37fdb0d5a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerClassApiResource.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.locker.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.locker.data.LockerClassData; +import org.apache.fineract.portfolio.locker.service.LockerReadPlatformService; +import org.springframework.stereotype.Component; + +@Path("/v1/lockerclasses") +@Component +@Tag(name = "Locker Class Master", description = "Safe deposit locker classes — member / non-member rent and deposit per locker number range of a branch.") +@RequiredArgsConstructor +public class LockerClassApiResource { + + private final PlatformSecurityContext context; + private final LockerReadPlatformService readPlatformService; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + + @GET + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List locker classes", description = "A Super user sees every branch (optionally filtered by officeId); everyone else sees only their own branch.") + public Page retrieveAll(@QueryParam("status") final String status, @QueryParam("officeId") final Long officeId, + @QueryParam("offset") @DefaultValue("0") final Integer offset, @QueryParam("limit") @DefaultValue("20") final Integer limit) { + this.context.authenticatedUser().validateHasReadPermission(LockerApiConstants.ENTITY_CLASS); + final int safeLimit = Math.min(limit, 500); + return this.readPlatformService.retrieveAllClasses(status, officeId, offset, safeLimit); + } + + @GET + @Path("{id}") + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a locker class") + public LockerClassData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) { + this.context.authenticatedUser().validateHasReadPermission(LockerApiConstants.ENTITY_CLASS); + return this.readPlatformService.retrieveClass(id); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Create a locker class", description = "Also creates one VACANT locker for every number in the range.") + public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) { + final CommandWrapper request = new CommandWrapperBuilder().createLockerClass().withJson(apiRequestBodyAsJson).build(); + return this.commandsSourceWritePlatformService.logCommandSource(request); + } + + @PUT + @Path("{id}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Update a locker class", description = "Rates, class name, area and effective dates only — class code and range are fixed.") + public CommandProcessingResult update(@PathParam("id") @Parameter(description = "id") final Long id, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + final CommandWrapper request = new CommandWrapperBuilder().updateLockerClass(id).withJson(apiRequestBodyAsJson).build(); + return this.commandsSourceWritePlatformService.logCommandSource(request); + } + + @DELETE + @Path("{id}") + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Deactivate a locker class", description = "Refused while any of its lockers is allotted.") + public CommandProcessingResult delete(@PathParam("id") @Parameter(description = "id") final Long id) { + final CommandWrapper request = new CommandWrapperBuilder().deleteLockerClass(id).build(); + return this.commandsSourceWritePlatformService.logCommandSource(request); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerWaitingListApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerWaitingListApiResource.java new file mode 100644 index 000000000..cc02b5b7c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/api/LockerWaitingListApiResource.java @@ -0,0 +1,119 @@ +/** + * 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.locker.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.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.exception.UnrecognizedQueryParamException; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.locker.data.LockerClientContactData; +import org.apache.fineract.portfolio.locker.data.LockerWaitingListData; +import org.apache.fineract.portfolio.locker.service.LockerReadPlatformService; +import org.springframework.stereotype.Component; + +@Path("/v1/lockerwaitinglist") +@Component +@Tag(name = "Locker Waiting List", description = "Locker inquiries in branch queue order — the id is the waiting number given to the applicant.") +@RequiredArgsConstructor +public class LockerWaitingListApiResource { + + private final PlatformSecurityContext context; + private final LockerReadPlatformService readPlatformService; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + + @GET + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List waiting list entries", description = "Oldest inquiry first. status: PENDING / ALLOTTED / CANCELLED.") + public Page retrieveAll(@QueryParam("status") final String status, @QueryParam("officeId") final Long officeId, + @QueryParam("offset") @DefaultValue("0") final Integer offset, @QueryParam("limit") @DefaultValue("20") final Integer limit) { + this.context.authenticatedUser().validateHasReadPermission(LockerApiConstants.ENTITY_WAITING_LIST); + final int safeLimit = Math.min(limit, 500); + return this.readPlatformService.retrieveWaitingList(status, officeId, offset, safeLimit); + } + + @GET + @Path("{id}") + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a waiting list entry") + public LockerWaitingListData retrieveOne(@PathParam("id") @Parameter(description = "id") final Long id) { + this.context.authenticatedUser().validateHasReadPermission(LockerApiConstants.ENTITY_WAITING_LIST); + return this.readPlatformService.retrieveWaitingListEntry(id); + } + + @GET + @Path("clientcontact/{clientId}") + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Client name, contact and address to pre-fill an inquiry", description = "Only clients within the user's branch scope.") + public LockerClientContactData retrieveClientContact(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId) { + this.context.authenticatedUser().validateHasReadPermission(LockerApiConstants.ENTITY_WAITING_LIST); + return this.readPlatformService.retrieveClientContact(clientId); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Add a waiting list entry") + public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) { + final CommandWrapper request = new CommandWrapperBuilder().createLockerWaitingList().withJson(apiRequestBodyAsJson).build(); + return this.commandsSourceWritePlatformService.logCommandSource(request); + } + + @PUT + @Path("{id}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Update a pending waiting list entry") + public CommandProcessingResult update(@PathParam("id") @Parameter(description = "id") final Long id, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + final CommandWrapper request = new CommandWrapperBuilder().updateLockerWaitingList(id).withJson(apiRequestBodyAsJson).build(); + return this.commandsSourceWritePlatformService.logCommandSource(request); + } + + @POST + @Path("{id}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Cancel a pending waiting list entry", description = "command=cancel, optional additionalRemarks as the reason.") + public CommandProcessingResult action(@PathParam("id") @Parameter(description = "id") final Long id, + @QueryParam("command") final String command, @Parameter(hidden = true) final String apiRequestBodyAsJson) { + if (!"cancel".equalsIgnoreCase(command)) { + throw new UnrecognizedQueryParamException("command", command); + } + final String json = apiRequestBodyAsJson == null || apiRequestBodyAsJson.isBlank() ? "{}" : apiRequestBodyAsJson; + final CommandWrapper request = new CommandWrapperBuilder().cancelLockerWaitingList(id).withJson(json).build(); + return this.commandsSourceWritePlatformService.logCommandSource(request); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerAllotmentClientData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerAllotmentClientData.java new file mode 100644 index 000000000..18f700e0e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerAllotmentClientData.java @@ -0,0 +1,83 @@ +/** + * 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.locker.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +/** + * Everything the allotment form fills from the client: contact, address, gender, occupation, PAN, masked Aadhaar, + * membership (decides member rates), active savings accounts (rent debit) and active fixed deposits (security + * deposit, with whether each is already under a locker lien). + */ +@Getter +@Builder +@AllArgsConstructor +public class LockerAllotmentClientData { + + private final Long clientId; + private final String displayName; + private final String gender; + private final String occupation; + private final String mobileNo; + private final String alternateMobileNo; + private final String email; + private final String addressLine1; + private final String addressLine2; + private final String addressLine3; + private final String city; + private final String pincode; + private final String panNumber; + /** Only the last four digits are ever sent. */ + private final String aadhaarMasked; + private final boolean member; + private final String membershipNo; + private final Long totalShares; + private final String riskRating; + private final List savingsAccounts; + private final List fixedDeposits; + + @Getter + @AllArgsConstructor + public static class SavingsOption { + + private final Long id; + private final String accountNo; + private final String productName; + private final BigDecimal balance; + } + + @Getter + @AllArgsConstructor + public static class FixedDepositOption { + + private final Long id; + private final String accountNo; + private final String productName; + private final BigDecimal depositAmount; + private final LocalDate depositDate; + private final LocalDate maturityDate; + /** Already given as security for another locker application. */ + private final boolean onHold; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerClassData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerClassData.java new file mode 100644 index 000000000..35b0e723f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerClassData.java @@ -0,0 +1,50 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.locker.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class LockerClassData { + + private final Long id; + private final Long officeId; + private final String officeName; + private final String classCode; + private final String className; + private final Integer area; + private final BigDecimal rent; + private final BigDecimal memberRent; + private final BigDecimal deposit; + private final BigDecimal memberDeposit; + private final Integer startLockerNo; + private final Integer endLockerNo; + private final Integer totalLockers; + /** Counted from allotted lockers at read time — never stored. */ + private final Integer occupied; + private final LocalDate effectiveFromDate; + private final LocalDate effectiveToDate; + private final boolean active; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerClientContactData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerClientContactData.java new file mode 100644 index 000000000..10238cc71 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerClientContactData.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.locker.data; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +/** A client's name, contact and first active address — what the waiting-list form fills in when a client is picked. */ +@Getter +@Builder +@AllArgsConstructor +public class LockerClientContactData { + + private final Long clientId; + private final String displayName; + private final String mobileNo; + private final String email; + private final String addressLine1; + private final String addressLine2; + private final String addressLine3; + private final String city; + private final String pincode; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerData.java new file mode 100644 index 000000000..5c1d22927 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerData.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.locker.data; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class LockerData { + + private final Long id; + private final Long officeId; + private final String officeName; + private final Long lockerClassId; + private final String classCode; + private final String className; + private final Integer lockerNo; + private final String keyNo; + private final String status; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerSummaryData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerSummaryData.java new file mode 100644 index 000000000..41cf0bb68 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerSummaryData.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.locker.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** Locker counts by status for the branches the caller may see. */ +@Getter +@AllArgsConstructor +public class LockerSummaryData { + + private final long total; + private final long vacant; + private final long allotted; + private final long blocked; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerWaitingListData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerWaitingListData.java new file mode 100644 index 000000000..e8ac9dd7a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/data/LockerWaitingListData.java @@ -0,0 +1,52 @@ +/** + * 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.locker.data; + +import java.time.LocalDate; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class LockerWaitingListData { + + /** Also the waiting number given to the applicant. */ + private final Long id; + private final Long officeId; + private final String officeName; + /** Null when the inquirer is not a client. */ + private final Long clientId; + private final String clientName; + private final LocalDate inquiryDate; + private final String customerName; + private final String addressLine1; + private final String addressLine2; + private final String addressLine3; + private final String city; + private final String pincode; + private final String phone; + private final String email; + private final List preferredClasses; + private final String remarks; + private final String status; + private final String additionalRemarks; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/Locker.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/Locker.java new file mode 100644 index 000000000..ea4c504f4 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/Locker.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.locker.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +/** One physical locker box, created from its class row's range. Unique per branch + class code + number. */ +@Entity +@Table(name = "m_locker") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Locker extends AbstractPersistableCustom { + + @Column(name = "office_id", nullable = false) + private Long officeId; + + @Column(name = "locker_class_id", nullable = false) + private Long lockerClassId; + + @Column(name = "class_code", nullable = false, length = 10) + private String classCode; + + @Column(name = "locker_no", nullable = false) + private Integer lockerNo; + + @Column(name = "key_no", length = 20) + private String keyNo; + + @Column(name = "status", nullable = false, length = 20) + private String status; + + public static Locker createVacant(final LockerClass lockerClass, final Integer lockerNo) { + final Locker locker = new Locker(); + locker.officeId = lockerClass.getOfficeId(); + locker.lockerClassId = lockerClass.getId(); + locker.classCode = lockerClass.getClassCode(); + locker.lockerNo = lockerNo; + locker.status = LockerStatus.VACANT.name(); + return locker; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClass.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClass.java new file mode 100644 index 000000000..968ac3aaf --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClass.java @@ -0,0 +1,154 @@ +/** + * 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.locker.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import java.math.BigDecimal; +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.service.DateUtils; + +/** + * One locker-number range of a class at a branch — a class such as A1 can have several rows — with member and + * non-member rent and deposit and an effective period. Class code, branch and range never change once saved. + */ +@Entity +@Table(name = "m_locker_class") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class LockerClass extends AbstractPersistableCustom { + + @Column(name = "office_id", nullable = false) + private Long officeId; + + @Column(name = "class_code", nullable = false, length = 10) + private String classCode; + + @Column(name = "class_name", nullable = false, length = 10) + private String className; + + @Column(name = "area", nullable = false) + private Integer area; + + @Column(name = "rent", nullable = false, scale = 6, precision = 19) + private BigDecimal rent; + + @Column(name = "member_rent", nullable = false, scale = 6, precision = 19) + private BigDecimal memberRent; + + @Column(name = "deposit", nullable = false, scale = 6, precision = 19) + private BigDecimal deposit; + + @Column(name = "member_deposit", nullable = false, scale = 6, precision = 19) + private BigDecimal memberDeposit; + + @Column(name = "start_locker_no", nullable = false) + private Integer startLockerNo; + + @Column(name = "end_locker_no", nullable = false) + private Integer endLockerNo; + + @Column(name = "effective_from_date", nullable = false) + private LocalDate effectiveFromDate; + + @Column(name = "effective_to_date") + private LocalDate effectiveToDate; + + @Column(name = "is_active", nullable = false) + private boolean active; + + @Column(name = "created_by") + private Long createdBy; + + @Column(name = "created_on") + private OffsetDateTime createdOn; + + @Column(name = "lastmodified_by") + private Long lastModifiedBy; + + @Column(name = "lastmodified_on") + private OffsetDateTime lastModifiedOn; + + public static LockerClass createNew(final Long officeId, final String classCode, final LockerClassName className, final Integer area, + final BigDecimal rent, final BigDecimal memberRent, final BigDecimal deposit, final BigDecimal memberDeposit, + final Integer startLockerNo, final Integer endLockerNo, final LocalDate effectiveFromDate, final LocalDate effectiveToDate, + final Long createdBy) { + final LockerClass lockerClass = new LockerClass(); + lockerClass.officeId = officeId; + lockerClass.classCode = classCode; + lockerClass.className = className.name(); + lockerClass.area = area; + lockerClass.rent = rent; + lockerClass.memberRent = memberRent; + lockerClass.deposit = deposit; + lockerClass.memberDeposit = memberDeposit; + lockerClass.startLockerNo = startLockerNo; + lockerClass.endLockerNo = endLockerNo; + lockerClass.effectiveFromDate = effectiveFromDate; + lockerClass.effectiveToDate = effectiveToDate; + lockerClass.active = true; + lockerClass.createdBy = createdBy; + lockerClass.createdOn = DateUtils.getOffsetDateTimeOfTenant(); + return lockerClass; + } + + /** Null arguments leave the field unchanged, except effective-to, which the caller sets only when sent. */ + public void updateDetails(final LockerClassName className, final Integer area, final BigDecimal rent, final BigDecimal memberRent, + final BigDecimal deposit, final BigDecimal memberDeposit, final LocalDate effectiveFromDate, final Long modifiedBy) { + if (className != null) { + this.className = className.name(); + } + if (area != null) { + this.area = area; + } + if (rent != null) { + this.rent = rent; + } + if (memberRent != null) { + this.memberRent = memberRent; + } + if (deposit != null) { + this.deposit = deposit; + } + if (memberDeposit != null) { + this.memberDeposit = memberDeposit; + } + if (effectiveFromDate != null) { + this.effectiveFromDate = effectiveFromDate; + } + this.lastModifiedBy = modifiedBy; + this.lastModifiedOn = DateUtils.getOffsetDateTimeOfTenant(); + } + + public void updateEffectiveToDate(final LocalDate effectiveToDate) { + this.effectiveToDate = effectiveToDate; + } + + public void deactivate(final Long modifiedBy) { + this.active = false; + this.lastModifiedBy = modifiedBy; + this.lastModifiedOn = DateUtils.getOffsetDateTimeOfTenant(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClassName.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClassName.java new file mode 100644 index 000000000..5edcf2d81 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClassName.java @@ -0,0 +1,24 @@ +/** + * 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.locker.domain; + +/** Size bucket of a locker class, as named on the legacy class master. */ +public enum LockerClassName { + SMALL, MEDIUM, LARGE +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClassRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClassRepository.java new file mode 100644 index 000000000..d035009b6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClassRepository.java @@ -0,0 +1,37 @@ +/** + * 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.locker.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 LockerClassRepository extends JpaRepository { + + /** Active rows of the same branch and class whose range shares at least one number with [start, end]. */ + @Query("select c from LockerClass c where c.active = true and c.officeId = :officeId and upper(c.classCode) = upper(:classCode)" + + " and c.startLockerNo <= :endNo and c.endLockerNo >= :startNo") + List findActiveOverlapping(@Param("officeId") Long officeId, @Param("classCode") String classCode, + @Param("startNo") Integer startNo, @Param("endNo") Integer endNo); + + /** Class codes a waiting-list inquiry at this branch may ask for. */ + @Query("select distinct upper(c.classCode) from LockerClass c where c.active = true and c.officeId = :officeId") + List findActiveClassCodes(@Param("officeId") Long officeId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClassRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClassRepositoryWrapper.java new file mode 100644 index 000000000..5452431c0 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerClassRepositoryWrapper.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.locker.domain; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.portfolio.locker.exception.LockerClassNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LockerClassRepositoryWrapper { + + private final LockerClassRepository repository; + + public LockerClass findOneWithNotFoundDetection(final Long id) { + return this.repository.findById(id).orElseThrow(() -> new LockerClassNotFoundException(id)); + } + + public LockerClass saveAndFlush(final LockerClass lockerClass) { + return this.repository.saveAndFlush(lockerClass); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerRepository.java new file mode 100644 index 000000000..cfddd7bbc --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerRepository.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.locker.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface LockerRepository extends JpaRepository { + + /** Any locker (of any class row, active or not) already holding a number in [start, end] for this branch and class. */ + @Query("select count(l) from Locker l where l.officeId = :officeId and upper(l.classCode) = upper(:classCode)" + + " and l.lockerNo between :startNo and :endNo") + long countExistingInRange(@Param("officeId") Long officeId, @Param("classCode") String classCode, @Param("startNo") Integer startNo, + @Param("endNo") Integer endNo); + + long countByLockerClassIdAndStatus(Long lockerClassId, String status); + + @Modifying + @Query("delete from Locker l where l.lockerClassId = :lockerClassId and l.status <> 'ALLOTTED'") + int deleteUnallottedByLockerClassId(@Param("lockerClassId") Long lockerClassId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerStatus.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerStatus.java new file mode 100644 index 000000000..59d703b07 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerStatus.java @@ -0,0 +1,24 @@ +/** + * 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.locker.domain; + +/** VACANT can be allotted, ALLOTTED is with a customer, BLOCKED is out of use (repair, freeze of the box). */ +public enum LockerStatus { + VACANT, ALLOTTED, BLOCKED +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingList.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingList.java new file mode 100644 index 000000000..6299a92e7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingList.java @@ -0,0 +1,158 @@ +/** + * 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.locker.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +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.service.DateUtils; + +/** + * A locker inquiry in the branch queue. The inquirer need not be a client yet; the id is the waiting number handed + * to them. Editable only while PENDING. + */ +@Entity +@Table(name = "m_locker_waiting_list") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class LockerWaitingList extends AbstractPersistableCustom { + + @Column(name = "office_id", nullable = false) + private Long officeId; + + /** Set when the inquirer is already a client; null for a walk-in who is not. */ + @Column(name = "client_id") + private Long clientId; + + @Column(name = "inquiry_date", nullable = false) + private LocalDate inquiryDate; + + @Column(name = "customer_name", nullable = false, length = 100) + private String customerName; + + @Column(name = "address_line1", nullable = false, length = 150) + private String addressLine1; + + @Column(name = "address_line2", length = 150) + private String addressLine2; + + @Column(name = "address_line3", length = 150) + private String addressLine3; + + @Column(name = "city", nullable = false, length = 60) + private String city; + + @Column(name = "pincode", nullable = false, length = 6) + private String pincode; + + @Column(name = "phone", length = 15) + private String phone; + + @Column(name = "email", length = 100) + private String email; + + /** Comma-separated class codes, e.g. "A1,B1". */ + @Column(name = "preferred_classes", nullable = false, length = 200) + private String preferredClasses; + + @Column(name = "remarks", length = 500) + private String remarks; + + @Column(name = "status", nullable = false, length = 20) + private String status; + + @Column(name = "additional_remarks", length = 500) + private String additionalRemarks; + + @Column(name = "created_by") + private Long createdBy; + + @Column(name = "created_on") + private OffsetDateTime createdOn; + + @Column(name = "lastmodified_by") + private Long lastModifiedBy; + + @Column(name = "lastmodified_on") + private OffsetDateTime lastModifiedOn; + + public static LockerWaitingList createNew(final Long officeId, final Long clientId, final LocalDate inquiryDate, + final String customerName, final String addressLine1, final String addressLine2, final String addressLine3, final String city, + final String pincode, final String phone, final String email, final String preferredClasses, final String remarks, + final Long createdBy) { + final LockerWaitingList entry = new LockerWaitingList(); + entry.officeId = officeId; + entry.clientId = clientId; + entry.status = LockerWaitingListStatus.PENDING.name(); + entry.createdBy = createdBy; + entry.createdOn = DateUtils.getOffsetDateTimeOfTenant(); + entry.applyDetails(inquiryDate, customerName, addressLine1, addressLine2, addressLine3, city, pincode, phone, email, + preferredClasses, remarks); + return entry; + } + + /** Replaces every inquiry detail — the counter form always sends the whole entry. */ + public void updateDetails(final Long clientId, final LocalDate inquiryDate, final String customerName, final String addressLine1, + final String addressLine2, final String addressLine3, final String city, final String pincode, final String phone, + final String email, final String preferredClasses, final String remarks, final Long modifiedBy) { + this.clientId = clientId; + applyDetails(inquiryDate, customerName, addressLine1, addressLine2, addressLine3, city, pincode, phone, email, preferredClasses, + remarks); + touch(modifiedBy); + } + + public void cancel(final String additionalRemarks, final Long modifiedBy) { + this.status = LockerWaitingListStatus.CANCELLED.name(); + if (additionalRemarks != null) { + this.additionalRemarks = additionalRemarks; + } + touch(modifiedBy); + } + + public boolean isPending() { + return LockerWaitingListStatus.PENDING.name().equals(this.status); + } + + private void applyDetails(final LocalDate inquiryDate, final String customerName, final String addressLine1, final String addressLine2, + final String addressLine3, final String city, final String pincode, final String phone, final String email, + final String preferredClasses, final String remarks) { + this.inquiryDate = inquiryDate; + this.customerName = customerName; + this.addressLine1 = addressLine1; + this.addressLine2 = addressLine2; + this.addressLine3 = addressLine3; + this.city = city; + this.pincode = pincode; + this.phone = phone; + this.email = email; + this.preferredClasses = preferredClasses; + this.remarks = remarks; + } + + private void touch(final Long modifiedBy) { + this.lastModifiedBy = modifiedBy; + this.lastModifiedOn = DateUtils.getOffsetDateTimeOfTenant(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingListRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingListRepository.java new file mode 100644 index 000000000..f50067262 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingListRepository.java @@ -0,0 +1,23 @@ +/** + * 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.locker.domain; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LockerWaitingListRepository extends JpaRepository {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingListRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingListRepositoryWrapper.java new file mode 100644 index 000000000..0e51fa738 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingListRepositoryWrapper.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.locker.domain; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.portfolio.locker.exception.LockerWaitingListNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LockerWaitingListRepositoryWrapper { + + private final LockerWaitingListRepository repository; + + public LockerWaitingList findOneWithNotFoundDetection(final Long id) { + return this.repository.findById(id).orElseThrow(() -> new LockerWaitingListNotFoundException(id)); + } + + public LockerWaitingList saveAndFlush(final LockerWaitingList entry) { + return this.repository.saveAndFlush(entry); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingListStatus.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingListStatus.java new file mode 100644 index 000000000..adae23626 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/domain/LockerWaitingListStatus.java @@ -0,0 +1,24 @@ +/** + * 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.locker.domain; + +/** PENDING is in the queue; ALLOTTED is set by allotment; CANCELLED is withdrawn at the counter. */ +public enum LockerWaitingListStatus { + PENDING, ALLOTTED, CANCELLED +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/exception/LockerClassNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/exception/LockerClassNotFoundException.java new file mode 100644 index 000000000..cae1239a2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/exception/LockerClassNotFoundException.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.locker.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class LockerClassNotFoundException extends AbstractPlatformResourceNotFoundException { + + public LockerClassNotFoundException(final Long id) { + super("error.msg.lockerclass.id.invalid", "Locker class with identifier " + id + " does not exist", id); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/exception/LockerWaitingListNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/exception/LockerWaitingListNotFoundException.java new file mode 100644 index 000000000..4b05a85c1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/exception/LockerWaitingListNotFoundException.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.locker.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class LockerWaitingListNotFoundException extends AbstractPlatformResourceNotFoundException { + + public LockerWaitingListNotFoundException(final Long id) { + super("error.msg.lockerwaitinglist.id.invalid", "Locker waiting list entry " + id + " does not exist", id); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/CancelLockerWaitingListCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/CancelLockerWaitingListCommandHandler.java new file mode 100644 index 000000000..3292b9beb --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/CancelLockerWaitingListCommandHandler.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.locker.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.locker.service.LockerWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "LOCKERWAITINGLIST", action = "CANCEL") +@RequiredArgsConstructor +public class CancelLockerWaitingListCommandHandler implements NewCommandSourceHandler { + + private final LockerWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.cancelWaitingListEntry(command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/CreateLockerClassCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/CreateLockerClassCommandHandler.java new file mode 100644 index 000000000..c2ff1ff06 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/CreateLockerClassCommandHandler.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.locker.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.locker.service.LockerWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "LOCKERCLASS", action = "CREATE") +@RequiredArgsConstructor +public class CreateLockerClassCommandHandler implements NewCommandSourceHandler { + + private final LockerWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.createClass(command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/CreateLockerWaitingListCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/CreateLockerWaitingListCommandHandler.java new file mode 100644 index 000000000..1987913e9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/CreateLockerWaitingListCommandHandler.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.locker.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.locker.service.LockerWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "LOCKERWAITINGLIST", action = "CREATE") +@RequiredArgsConstructor +public class CreateLockerWaitingListCommandHandler implements NewCommandSourceHandler { + + private final LockerWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.createWaitingListEntry(command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/DeleteLockerClassCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/DeleteLockerClassCommandHandler.java new file mode 100644 index 000000000..5f868e982 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/DeleteLockerClassCommandHandler.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.locker.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.locker.service.LockerWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "LOCKERCLASS", action = "DELETE") +@RequiredArgsConstructor +public class DeleteLockerClassCommandHandler implements NewCommandSourceHandler { + + private final LockerWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.deactivateClass(command.entityId()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/UpdateLockerClassCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/UpdateLockerClassCommandHandler.java new file mode 100644 index 000000000..c06b63695 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/UpdateLockerClassCommandHandler.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.locker.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.locker.service.LockerWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "LOCKERCLASS", action = "UPDATE") +@RequiredArgsConstructor +public class UpdateLockerClassCommandHandler implements NewCommandSourceHandler { + + private final LockerWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.updateClass(command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/UpdateLockerWaitingListCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/UpdateLockerWaitingListCommandHandler.java new file mode 100644 index 000000000..b33d36df1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/handler/UpdateLockerWaitingListCommandHandler.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.locker.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.locker.service.LockerWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@CommandType(entity = "LOCKERWAITINGLIST", action = "UPDATE") +@RequiredArgsConstructor +public class UpdateLockerWaitingListCommandHandler implements NewCommandSourceHandler { + + private final LockerWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.updateWaitingListEntry(command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/serialization/LockerCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/serialization/LockerCommandFromApiJsonDeserializer.java new file mode 100644 index 000000000..a00aa7d1a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/serialization/LockerCommandFromApiJsonDeserializer.java @@ -0,0 +1,266 @@ +/** + * 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.locker.serialization; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.locker.api.LockerApiConstants; +import org.apache.fineract.portfolio.locker.api.LockerApiJsonInputParams; +import org.apache.fineract.portfolio.locker.domain.LockerClassName; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public final class LockerCommandFromApiJsonDeserializer { + + private static final String CLASS_CODE_PATTERN = "^[A-Za-z0-9]+$"; + private static final String PINCODE_PATTERN = "^[0-9]{6}$"; + private static final String PHONE_PATTERN = "^[0-9]{10,15}$"; + private static final String EMAIL_PATTERN = "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"; + + private final FromJsonHelper fromApiJsonHelper; + + public void validateForCreateClass(final String json) { + final JsonElement element = parse(json, LockerApiJsonInputParams.classCreateParams()); + final List errors = new ArrayList<>(); + final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(LockerApiConstants.RESOURCE_CLASS); + + final Long officeId = this.fromApiJsonHelper.extractLongNamed(LockerApiJsonInputParams.OFFICE_ID.getValue(), element); + v.reset().parameter(LockerApiJsonInputParams.OFFICE_ID.getValue()).value(officeId).ignoreIfNull().longGreaterThanZero(); + + final String classCode = this.fromApiJsonHelper.extractStringNamed(LockerApiJsonInputParams.CLASS_CODE.getValue(), element); + v.reset().parameter(LockerApiJsonInputParams.CLASS_CODE.getValue()).value(classCode).notBlank().notExceedingLengthOf(10) + .matchesRegularExpression(CLASS_CODE_PATTERN, "Class may contain only letters and digits"); + + validateClassName(element, v, true); + validateArea(element, v, true); + validateAmounts(element, v, true); + + final Integer startLockerNo = this.fromApiJsonHelper + .extractIntegerSansLocaleNamed(LockerApiJsonInputParams.START_LOCKER_NO.getValue(), element); + v.reset().parameter(LockerApiJsonInputParams.START_LOCKER_NO.getValue()).value(startLockerNo).notNull().integerGreaterThanZero(); + final Integer endLockerNo = this.fromApiJsonHelper + .extractIntegerSansLocaleNamed(LockerApiJsonInputParams.END_LOCKER_NO.getValue(), element); + v.reset().parameter(LockerApiJsonInputParams.END_LOCKER_NO.getValue()).value(endLockerNo).notNull().integerGreaterThanZero(); + if (startLockerNo != null && endLockerNo != null) { + if (endLockerNo < startLockerNo) { + v.reset().parameter(LockerApiJsonInputParams.END_LOCKER_NO.getValue()).value(endLockerNo) + .failWithCodeAndMessage("less.than.start", "End Locker No cannot be less than Start Locker No"); + } else if (endLockerNo - startLockerNo + 1 > LockerApiConstants.MAX_LOCKERS_PER_RANGE) { + v.reset().parameter(LockerApiJsonInputParams.END_LOCKER_NO.getValue()).value(endLockerNo).failWithCodeAndMessage("range.too.large", + "One range can have at most " + LockerApiConstants.MAX_LOCKERS_PER_RANGE + " lockers"); + } + } + + final LocalDate from = this.fromApiJsonHelper.extractLocalDateNamed(LockerApiJsonInputParams.EFFECTIVE_FROM_DATE.getValue(), + element); + v.reset().parameter(LockerApiJsonInputParams.EFFECTIVE_FROM_DATE.getValue()).value(from).notNull(); + final LocalDate to = this.fromApiJsonHelper.extractLocalDateNamed(LockerApiJsonInputParams.EFFECTIVE_TO_DATE.getValue(), element); + validateEffectivePeriod(from, to, v); + + throwIfErrors(errors); + } + + public void validateForUpdateClass(final String json) { + final JsonElement element = parse(json, LockerApiJsonInputParams.classUpdateParams()); + final List errors = new ArrayList<>(); + final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(LockerApiConstants.RESOURCE_CLASS); + + validateClassName(element, v, false); + validateArea(element, v, false); + validateAmounts(element, v, false); + + final LocalDate from = this.fromApiJsonHelper.extractLocalDateNamed(LockerApiJsonInputParams.EFFECTIVE_FROM_DATE.getValue(), + element); + final LocalDate to = this.fromApiJsonHelper.extractLocalDateNamed(LockerApiJsonInputParams.EFFECTIVE_TO_DATE.getValue(), element); + validateEffectivePeriod(from, to, v); + + throwIfErrors(errors); + } + + public void validateForCreateWaitingList(final String json) { + final JsonElement element = parse(json, LockerApiJsonInputParams.waitingListCreateParams()); + final List errors = new ArrayList<>(); + final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(LockerApiConstants.RESOURCE_WAITING_LIST); + final Long officeId = this.fromApiJsonHelper.extractLongNamed(LockerApiJsonInputParams.OFFICE_ID.getValue(), element); + v.reset().parameter(LockerApiJsonInputParams.OFFICE_ID.getValue()).value(officeId).ignoreIfNull().longGreaterThanZero(); + validateWaitingListDetails(element, v); + throwIfErrors(errors); + } + + public void validateForUpdateWaitingList(final String json) { + final JsonElement element = parse(json, LockerApiJsonInputParams.waitingListUpdateParams()); + final List errors = new ArrayList<>(); + final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(LockerApiConstants.RESOURCE_WAITING_LIST); + validateWaitingListDetails(element, v); + throwIfErrors(errors); + } + + public void validateForCancelWaitingList(final String json) { + final JsonElement element = parse(json, LockerApiJsonInputParams.waitingListCancelParams()); + final List errors = new ArrayList<>(); + final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(LockerApiConstants.RESOURCE_WAITING_LIST); + validateText(element, v, LockerApiJsonInputParams.ADDITIONAL_REMARKS, false, 500); + throwIfErrors(errors); + } + + /** Same shape on create and update — the counter form always sends the whole inquiry. */ + private void validateWaitingListDetails(final JsonElement element, final DataValidatorBuilder v) { + final Long clientId = this.fromApiJsonHelper.extractLongNamed(LockerApiJsonInputParams.CLIENT_ID.getValue(), element); + v.reset().parameter(LockerApiJsonInputParams.CLIENT_ID.getValue()).value(clientId).ignoreIfNull().longGreaterThanZero(); + + final LocalDate inquiryDate = this.fromApiJsonHelper.extractLocalDateNamed(LockerApiJsonInputParams.INQUIRY_DATE.getValue(), + element); + v.reset().parameter(LockerApiJsonInputParams.INQUIRY_DATE.getValue()).value(inquiryDate).notNull(); + if (inquiryDate != null && DateUtils.isAfterBusinessDate(inquiryDate)) { + v.reset().parameter(LockerApiJsonInputParams.INQUIRY_DATE.getValue()).value(inquiryDate).failWithCodeAndMessage("in.future", + "Inquiry Date cannot be in the future"); + } + + validateText(element, v, LockerApiJsonInputParams.CUSTOMER_NAME, true, 100); + validateText(element, v, LockerApiJsonInputParams.ADDRESS_LINE1, true, 150); + validateText(element, v, LockerApiJsonInputParams.ADDRESS_LINE2, false, 150); + validateText(element, v, LockerApiJsonInputParams.ADDRESS_LINE3, false, 150); + validateText(element, v, LockerApiJsonInputParams.CITY, true, 60); + validateText(element, v, LockerApiJsonInputParams.REMARKS, false, 500); + + final String pincode = this.fromApiJsonHelper.extractStringNamed(LockerApiJsonInputParams.PINCODE.getValue(), element); + v.reset().parameter(LockerApiJsonInputParams.PINCODE.getValue()).value(pincode).notBlank() + .matchesRegularExpression(PINCODE_PATTERN, "Pincode must be 6 digits"); + + final String phone = this.fromApiJsonHelper.extractStringNamed(LockerApiJsonInputParams.PHONE.getValue(), element); + if (StringUtils.isNotBlank(phone)) { + v.reset().parameter(LockerApiJsonInputParams.PHONE.getValue()).value(phone.trim()).matchesRegularExpression(PHONE_PATTERN, + "Phone must be 10 to 15 digits"); + } + + final String email = this.fromApiJsonHelper.extractStringNamed(LockerApiJsonInputParams.EMAIL.getValue(), element); + if (StringUtils.isNotBlank(email)) { + v.reset().parameter(LockerApiJsonInputParams.EMAIL.getValue()).value(email.trim()).notExceedingLengthOf(100) + .matchesRegularExpression(EMAIL_PATTERN, "Email is not valid"); + } + + final List classes = extractPreferredClasses(element); + if (classes.isEmpty()) { + v.reset().parameter(LockerApiJsonInputParams.PREFERRED_CLASSES.getValue()).value(null).failWithCodeAndMessage("empty", + "Choose at least one locker class"); + } else if (String.join(",", classes).length() > 200) { + v.reset().parameter(LockerApiJsonInputParams.PREFERRED_CLASSES.getValue()).value(classes).failWithCodeAndMessage("too.many", + "Too many locker classes chosen"); + } + } + + /** Upper-cased, de-duplicated class codes from the preferredClasses array; blanks dropped. */ + public List extractPreferredClasses(final JsonElement element) { + final JsonArray array = this.fromApiJsonHelper.extractJsonArrayNamed(LockerApiJsonInputParams.PREFERRED_CLASSES.getValue(), + element); + final List classes = new ArrayList<>(); + if (array != null) { + array.forEach(item -> { + final String code = item.isJsonNull() ? null : item.getAsString().trim().toUpperCase(); + if (StringUtils.isNotBlank(code) && !classes.contains(code)) { + classes.add(code); + } + }); + } + return classes; + } + + private void validateText(final JsonElement element, final DataValidatorBuilder v, final LockerApiJsonInputParams param, + final boolean required, final int maxLength) { + final String value = this.fromApiJsonHelper.extractStringNamed(param.getValue(), element); + final DataValidatorBuilder b = v.reset().parameter(param.getValue()).value(value); + (required ? b.notBlank() : b.ignoreIfNull()).notExceedingLengthOf(maxLength); + } + + /** Effective-to may be blank (open-ended) but never before effective-from. */ + public void validateEffectivePeriod(final LocalDate from, final LocalDate to, final DataValidatorBuilder v) { + if (from != null && to != null && to.isBefore(from)) { + v.reset().parameter(LockerApiJsonInputParams.EFFECTIVE_TO_DATE.getValue()).value(to).failWithCodeAndMessage("before.effective.from", + "Effective To cannot be before Effective From"); + } + } + + private void validateClassName(final JsonElement element, final DataValidatorBuilder v, final boolean required) { + final String className = this.fromApiJsonHelper.extractStringNamed(LockerApiJsonInputParams.CLASS_NAME.getValue(), element); + final DataValidatorBuilder b = v.reset().parameter(LockerApiJsonInputParams.CLASS_NAME.getValue()).value(className); + if (required) { + b.notBlank(); + } else { + b.ignoreIfNull(); + } + if (StringUtils.isNotBlank(className)) { + v.reset().parameter(LockerApiJsonInputParams.CLASS_NAME.getValue()).value(className.toUpperCase()) + .isOneOfTheseStringValues(Arrays.stream(LockerClassName.values()).map(Enum::name).toList()); + } + } + + private void validateArea(final JsonElement element, final DataValidatorBuilder v, final boolean required) { + final Integer area = this.fromApiJsonHelper.extractIntegerSansLocaleNamed(LockerApiJsonInputParams.AREA.getValue(), element); + final DataValidatorBuilder b = v.reset().parameter(LockerApiJsonInputParams.AREA.getValue()).value(area); + (required ? b.notNull() : b.ignoreIfNull()).integerGreaterThanZero(); + } + + /** Rent and deposit are mandatory on create; the member rates are optional and fall back to them. */ + private void validateAmounts(final JsonElement element, final DataValidatorBuilder v, final boolean required) { + validateAmount(element, v, LockerApiJsonInputParams.RENT, required); + validateAmount(element, v, LockerApiJsonInputParams.DEPOSIT, required); + validateAmount(element, v, LockerApiJsonInputParams.MEMBER_RENT, false); + validateAmount(element, v, LockerApiJsonInputParams.MEMBER_DEPOSIT, false); + } + + private void validateAmount(final JsonElement element, final DataValidatorBuilder v, final LockerApiJsonInputParams param, + final boolean required) { + final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(param.getValue(), element); + final DataValidatorBuilder b = v.reset().parameter(param.getValue()).value(amount); + (required ? b.notNull() : b.ignoreIfNull()).zeroOrPositiveAmount(); + } + + 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/locker/service/LockerOfficeAccess.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerOfficeAccess.java new file mode 100644 index 000000000..4e71e6445 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerOfficeAccess.java @@ -0,0 +1,76 @@ +/** + * 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.locker.service; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper; +import org.apache.fineract.useradministration.domain.AppUser; +import org.springframework.stereotype.Component; + +/** + * Branch rule for lockers: the Super user (ALL_FUNCTIONS) works across every branch; everyone else — a Branch Manager + * creating or editing, or any role reading — is confined to their own branch. + */ +@Component +@RequiredArgsConstructor +public class LockerOfficeAccess { + + private final PlatformSecurityContext context; + private final OfficeRepositoryWrapper officeRepositoryWrapper; + + public AppUser currentUser() { + return this.context.authenticatedUser(); + } + + public boolean isSuperUser(final AppUser user) { + return !user.hasNotPermissionForAnyOf("ALL_FUNCTIONS"); + } + + /** Branch a list is confined to: the requested one (or none = all) for a Super user, otherwise the user's own. */ + public Long officeForRead(final Long requestedOfficeId) { + final AppUser user = currentUser(); + return isSuperUser(user) ? requestedOfficeId : user.getOffice().getId(); + } + + /** Branch a new class belongs to: a Super user may name any branch; anyone else gets their own and may not name another. */ + public Long officeForCreate(final Long requestedOfficeId) { + final AppUser user = currentUser(); + final Long ownOfficeId = user.getOffice().getId(); + if (requestedOfficeId == null) { + return ownOfficeId; + } + if (isSuperUser(user)) { + return this.officeRepositoryWrapper.findOneWithNotFoundDetection(requestedOfficeId).getId(); + } + if (!ownOfficeId.equals(requestedOfficeId)) { + throw new NoAuthorizationException("You can only manage lockers of your own branch."); + } + return ownOfficeId; + } + + /** Refuses a read or change of another branch's record for anyone but the Super user. */ + public void assertOwnBranch(final Long recordOfficeId) { + final AppUser user = currentUser(); + if (!isSuperUser(user) && !user.getOffice().getId().equals(recordOfficeId)) { + throw new NoAuthorizationException("You can only manage lockers of your own branch."); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerReadPlatformService.java new file mode 100644 index 000000000..b0d607045 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerReadPlatformService.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.locker.service; + +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.portfolio.locker.data.LockerAllotmentClientData; +import org.apache.fineract.portfolio.locker.data.LockerClassData; +import org.apache.fineract.portfolio.locker.data.LockerClientContactData; +import org.apache.fineract.portfolio.locker.data.LockerData; +import org.apache.fineract.portfolio.locker.data.LockerSummaryData; +import org.apache.fineract.portfolio.locker.data.LockerWaitingListData; + +public interface LockerReadPlatformService { + + Page retrieveAllClasses(String status, Long officeId, Integer offset, Integer limit); + + LockerClassData retrieveClass(Long id); + + Page retrieveLockers(Long officeId, String classCode, String status, Integer lockerNo, Integer offset, Integer limit); + + LockerSummaryData retrieveLockerSummary(Long officeId); + + Page retrieveWaitingList(String status, Long officeId, Integer offset, Integer limit); + + LockerWaitingListData retrieveWaitingListEntry(Long id); + + LockerClientContactData retrieveClientContact(Long clientId); + + LockerAllotmentClientData retrieveAllotmentClientDetails(Long clientId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerReadPlatformServiceImpl.java new file mode 100644 index 000000000..3a6a54b4d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerReadPlatformServiceImpl.java @@ -0,0 +1,344 @@ +/** + * 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.locker.service; + +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.service.Page; +import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; +import org.apache.fineract.portfolio.locker.data.LockerAllotmentClientData; +import org.apache.fineract.portfolio.locker.data.LockerClassData; +import org.apache.fineract.portfolio.locker.data.LockerClientContactData; +import org.apache.fineract.portfolio.locker.data.LockerData; +import org.apache.fineract.portfolio.locker.data.LockerSummaryData; +import org.apache.fineract.portfolio.locker.data.LockerWaitingListData; +import org.apache.fineract.portfolio.locker.exception.LockerClassNotFoundException; +import org.apache.fineract.portfolio.locker.exception.LockerWaitingListNotFoundException; +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 LockerReadPlatformServiceImpl implements LockerReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + private final LockerOfficeAccess officeAccess; + private final ClientRepositoryWrapper clientRepositoryWrapper; + private final ClassMapper classMapper = new ClassMapper(); + private final LockerMapper lockerMapper = new LockerMapper(); + private final WaitingListMapper waitingListMapper = new WaitingListMapper(); + + @Override + public Page retrieveAllClasses(final String status, final Long officeId, 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 lc.is_active = true"); + } else if ("INACTIVE".equalsIgnoreCase(status)) { + where.append(" and lc.is_active = false"); + } + final Long scopedOfficeId = this.officeAccess.officeForRead(officeId); + if (scopedOfficeId != null) { + where.append(" and lc.office_id = ?"); + args.add(scopedOfficeId); + } + + final String countSql = "select count(*) from m_locker_class lc" + where; + final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray()); + + final StringBuilder pageSql = new StringBuilder("select ").append(this.classMapper.schema()).append(where) + .append(" order by o.name, lc.class_code, lc.start_locker_no limit ? offset ?"); + final List pageArgs = new ArrayList<>(args); + pageArgs.add(limit); + pageArgs.add(offset); + final List classes = this.jdbcTemplate.query(pageSql.toString(), this.classMapper, pageArgs.toArray()); + return new Page<>(classes, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue()); + } + + @Override + public LockerClassData retrieveClass(final Long id) { + final LockerClassData data; + try { + final String sql = "select " + this.classMapper.schema() + " where lc.id = ?"; + data = this.jdbcTemplate.queryForObject(sql, this.classMapper, id); + } catch (final EmptyResultDataAccessException e) { + throw new LockerClassNotFoundException(id); + } + this.officeAccess.assertOwnBranch(data.getOfficeId()); + return data; + } + + @Override + public Page retrieveLockers(final Long officeId, final String classCode, final String status, final Integer lockerNo, + final Integer offset, final Integer limit) { + final StringBuilder where = new StringBuilder(" where 1=1"); + final List args = new ArrayList<>(); + final Long scopedOfficeId = this.officeAccess.officeForRead(officeId); + if (scopedOfficeId != null) { + where.append(" and l.office_id = ?"); + args.add(scopedOfficeId); + } + if (StringUtils.isNotBlank(classCode)) { + where.append(" and upper(l.class_code) = ?"); + args.add(classCode.trim().toUpperCase()); + } + if (StringUtils.isNotBlank(status)) { + where.append(" and l.status = ?"); + args.add(status.trim().toUpperCase()); + } + if (lockerNo != null) { + where.append(" and l.locker_no = ?"); + args.add(lockerNo); + } + + final String countSql = "select count(*) from m_locker l" + where; + final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray()); + + final StringBuilder pageSql = new StringBuilder("select ").append(this.lockerMapper.schema()).append(where) + .append(" order by o.name, l.class_code, l.locker_no limit ? offset ?"); + final List pageArgs = new ArrayList<>(args); + pageArgs.add(limit); + pageArgs.add(offset); + final List lockers = this.jdbcTemplate.query(pageSql.toString(), this.lockerMapper, pageArgs.toArray()); + return new Page<>(lockers, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue()); + } + + @Override + public LockerSummaryData retrieveLockerSummary(final Long officeId) { + final Long scopedOfficeId = this.officeAccess.officeForRead(officeId); + final String sql = "select count(*) as total, count(*) filter (where status = 'VACANT') as vacant," + + " count(*) filter (where status = 'ALLOTTED') as allotted, count(*) filter (where status = 'BLOCKED') as blocked" + + " from m_locker" + (scopedOfficeId != null ? " where office_id = ?" : ""); + final Object[] args = scopedOfficeId != null ? new Object[] { scopedOfficeId } : new Object[0]; + return this.jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new LockerSummaryData(rs.getLong("total"), rs.getLong("vacant"), + rs.getLong("allotted"), rs.getLong("blocked")), args); + } + + @Override + public Page retrieveWaitingList(final String status, final Long officeId, final Integer offset, + final Integer limit) { + final StringBuilder where = new StringBuilder(" where 1=1"); + final List args = new ArrayList<>(); + final Long scopedOfficeId = this.officeAccess.officeForRead(officeId); + if (scopedOfficeId != null) { + where.append(" and w.office_id = ?"); + args.add(scopedOfficeId); + } + if (StringUtils.isNotBlank(status)) { + where.append(" and w.status = ?"); + args.add(status.trim().toUpperCase()); + } + + final String countSql = "select count(*) from m_locker_waiting_list w" + where; + final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray()); + + // Queue order: whoever asked first is served first. + final StringBuilder pageSql = new StringBuilder("select ").append(this.waitingListMapper.schema()).append(where) + .append(" order by w.inquiry_date, w.id limit ? offset ?"); + final List pageArgs = new ArrayList<>(args); + pageArgs.add(limit); + pageArgs.add(offset); + final List entries = this.jdbcTemplate.query(pageSql.toString(), this.waitingListMapper, + pageArgs.toArray()); + return new Page<>(entries, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue()); + } + + @Override + public LockerWaitingListData retrieveWaitingListEntry(final Long id) { + final LockerWaitingListData data; + try { + final String sql = "select " + this.waitingListMapper.schema() + " where w.id = ?"; + data = this.jdbcTemplate.queryForObject(sql, this.waitingListMapper, id); + } catch (final EmptyResultDataAccessException e) { + throw new LockerWaitingListNotFoundException(id); + } + this.officeAccess.assertOwnBranch(data.getOfficeId()); + return data; + } + + /** Checked against the user's branch scope first, then name, contact and the first active, non-deleted address. */ + @Override + public LockerClientContactData retrieveClientContact(final Long clientId) { + this.clientRepositoryWrapper.getActiveClientInUserScope(clientId); + final String sql = "select c.id, c.display_name, c.mobile_no, c.email_address, a.address_line_1, a.address_line_2," + + " a.address_line_3, coalesce(a.city, a.town_village) as city, a.postal_code from m_client c" + + " left join lateral (select ad.* from m_client_address ca join m_address ad on ad.id = ca.address_id" + + " where ca.client_id = c.id and ca.is_active = true and coalesce(ca.is_deleted, false) = false" + + " and coalesce(ad.is_deleted, false) = false order by ca.id limit 1) a on true where c.id = ?"; + return this.jdbcTemplate.queryForObject(sql, + (rs, rowNum) -> LockerClientContactData.builder().clientId(rs.getLong("id")).displayName(rs.getString("display_name")) + .mobileNo(rs.getString("mobile_no")).email(rs.getString("email_address")) + .addressLine1(rs.getString("address_line_1")).addressLine2(rs.getString("address_line_2")) + .addressLine3(rs.getString("address_line_3")).city(rs.getString("city")).pincode(rs.getString("postal_code")) + .build(), + clientId); + } + + /** + * Branch scope and active status first (through the client repository), then each part from its own table: the + * client row, the first active address, PAN / Aadhaar identifiers, active share accounts (membership), active + * savings accounts and active fixed deposits. + */ + @Override + public LockerAllotmentClientData retrieveAllotmentClientDetails(final Long clientId) { + final LockerClientContactData contact = retrieveClientContact(clientId); + + final Map client = this.jdbcTemplate.queryForMap( + "select c.alternate_mobile_no, g.code_value as gender, oc.code_value as occupation from m_client c" + + " left join m_code_value g on g.id = c.gender_cv_id left join m_code_value oc on oc.id = c.occupation_cv_id" + + " where c.id = ?", + clientId); + + String pan = null; + String aadhaar = null; + for (final Map row : this.jdbcTemplate.queryForList( + "select cv.code_value as doc_type, ci.document_key from m_client_identifier ci" + + " join m_code_value cv on cv.id = ci.document_type_id where ci.client_id = ? order by ci.id", + clientId)) { + final String type = String.valueOf(row.get("doc_type")).toUpperCase(); + final String key = (String) row.get("document_key"); + if (pan == null && type.contains("PAN")) { + pan = key; + } else if (aadhaar == null && (type.contains("AADHAAR") || type.contains("AADHAR"))) { + aadhaar = key; + } + } + + final List> shares = this.jdbcTemplate.queryForList( + "select account_no, coalesce(total_approved_shares, 0) as shares from m_share_account" + + " where client_id = ? and status_enum = 300 order by id", + clientId); + final long totalShares = shares.stream().mapToLong(s -> ((Number) s.get("shares")).longValue()).sum(); + + final List savings = this.jdbcTemplate.query( + "select sa.id, sa.account_no, sp.name as product_name, sa.account_balance_derived from m_savings_account sa" + + " join m_savings_product sp on sp.id = sa.product_id" + + " where sa.client_id = ? and sa.deposit_type_enum = 100 and sa.status_enum = 300 order by sa.id", + (rs, rowNum) -> new LockerAllotmentClientData.SavingsOption(rs.getLong("id"), rs.getString("account_no"), + rs.getString("product_name"), rs.getBigDecimal("account_balance_derived")), + clientId); + + final List fixedDeposits = this.jdbcTemplate.query( + "select sa.id, sa.account_no, sp.name as product_name, t.deposit_amount, sa.activatedon_date, t.maturity_date," + + " exists (select 1 from m_locker_allotment_deposit d where d.savings_account_id = sa.id" + + " and d.lien_status in ('PENDING', 'ACTIVE')) as on_hold" + + " from m_savings_account sa join m_savings_product sp on sp.id = sa.product_id" + + " join m_deposit_account_term_and_preclosure t on t.savings_account_id = sa.id" + + " where sa.client_id = ? and sa.deposit_type_enum = 200 and sa.status_enum = 300 order by sa.id", + (rs, rowNum) -> { + final java.sql.Date opened = rs.getDate("activatedon_date"); + final java.sql.Date matures = rs.getDate("maturity_date"); + return new LockerAllotmentClientData.FixedDepositOption(rs.getLong("id"), rs.getString("account_no"), + rs.getString("product_name"), rs.getBigDecimal("deposit_amount"), opened == null ? null : opened.toLocalDate(), + matures == null ? null : matures.toLocalDate(), rs.getBoolean("on_hold")); + }, + clientId); + + return LockerAllotmentClientData.builder().clientId(contact.getClientId()).displayName(contact.getDisplayName()) + .gender((String) client.get("gender")).occupation((String) client.get("occupation")).mobileNo(contact.getMobileNo()) + .alternateMobileNo((String) client.get("alternate_mobile_no")).email(contact.getEmail()) + .addressLine1(contact.getAddressLine1()).addressLine2(contact.getAddressLine2()).addressLine3(contact.getAddressLine3()) + .city(contact.getCity()).pincode(contact.getPincode()).panNumber(pan).aadhaarMasked(maskAadhaar(aadhaar)) + .member(totalShares > 0).membershipNo(shares.isEmpty() ? null : (String) shares.get(0).get("account_no")) + .totalShares(totalShares).riskRating(null).savingsAccounts(savings).fixedDeposits(fixedDeposits).build(); + } + + /** "XXXX XXXX 1234" — the full number never leaves the server. */ + private static String maskAadhaar(final String aadhaar) { + if (StringUtils.isBlank(aadhaar)) { + return null; + } + final String digits = aadhaar.replaceAll("\\D", ""); + return digits.length() < 4 ? "XXXX" : "XXXX XXXX " + digits.substring(digits.length() - 4); + } + + private static final class LockerMapper implements RowMapper { + + String schema() { + return " l.id, l.office_id, o.name as office_name, l.locker_class_id, l.class_code, lc.class_name, l.locker_no, l.key_no," + + " l.status from m_locker l join m_locker_class lc on lc.id = l.locker_class_id join m_office o on o.id = l.office_id"; + } + + @Override + public LockerData mapRow(final ResultSet rs, final int rowNum) throws SQLException { + return LockerData.builder().id(rs.getLong("id")).officeId(rs.getLong("office_id")).officeName(rs.getString("office_name")) + .lockerClassId(rs.getLong("locker_class_id")).classCode(rs.getString("class_code")) + .className(rs.getString("class_name")).lockerNo(rs.getInt("locker_no")).keyNo(rs.getString("key_no")) + .status(rs.getString("status")).build(); + } + } + + private static final class WaitingListMapper implements RowMapper { + + String schema() { + return " w.id, w.office_id, o.name as office_name, w.client_id, c.display_name as client_name, w.inquiry_date," + + " w.customer_name, w.address_line1, w.address_line2, w.address_line3, w.city, w.pincode, w.phone, w.email," + + " w.preferred_classes, w.remarks, w.status, w.additional_remarks from m_locker_waiting_list w" + + " join m_office o on o.id = w.office_id left join m_client c on c.id = w.client_id"; + } + + @Override + public LockerWaitingListData mapRow(final ResultSet rs, final int rowNum) throws SQLException { + final String classes = rs.getString("preferred_classes"); + return LockerWaitingListData.builder().id(rs.getLong("id")).officeId(rs.getLong("office_id")) + .officeName(rs.getString("office_name")).clientId((Long) rs.getObject("client_id", Long.class)) + .clientName(rs.getString("client_name")).inquiryDate(rs.getDate("inquiry_date").toLocalDate()) + .customerName(rs.getString("customer_name")).addressLine1(rs.getString("address_line1")) + .addressLine2(rs.getString("address_line2")).addressLine3(rs.getString("address_line3")).city(rs.getString("city")) + .pincode(rs.getString("pincode")).phone(rs.getString("phone")).email(rs.getString("email")) + .preferredClasses(StringUtils.isBlank(classes) ? List.of() : Arrays.asList(classes.split(","))) + .remarks(rs.getString("remarks")).status(rs.getString("status")).additionalRemarks(rs.getString("additional_remarks")) + .build(); + } + } + + private static final class ClassMapper implements RowMapper { + + String schema() { + return " lc.id, lc.office_id, o.name as office_name, lc.class_code, lc.class_name, lc.area, lc.rent, lc.member_rent," + + " lc.deposit, lc.member_deposit, lc.start_locker_no, lc.end_locker_no, lc.effective_from_date," + + " lc.effective_to_date, lc.is_active," + + " (select count(*) from m_locker l where l.locker_class_id = lc.id and l.status = 'ALLOTTED') as occupied" + + " from m_locker_class lc join m_office o on o.id = lc.office_id"; + } + + @Override + public LockerClassData mapRow(final ResultSet rs, final int rowNum) throws SQLException { + final int startLockerNo = rs.getInt("start_locker_no"); + final int endLockerNo = rs.getInt("end_locker_no"); + final Date effectiveTo = rs.getDate("effective_to_date"); + return LockerClassData.builder().id(rs.getLong("id")).officeId(rs.getLong("office_id")).officeName(rs.getString("office_name")) + .classCode(rs.getString("class_code")).className(rs.getString("class_name")).area(rs.getInt("area")) + .rent(rs.getBigDecimal("rent")).memberRent(rs.getBigDecimal("member_rent")).deposit(rs.getBigDecimal("deposit")) + .memberDeposit(rs.getBigDecimal("member_deposit")).startLockerNo(startLockerNo).endLockerNo(endLockerNo) + .totalLockers(endLockerNo - startLockerNo + 1).occupied(rs.getInt("occupied")) + .effectiveFromDate(rs.getDate("effective_from_date").toLocalDate()) + .effectiveToDate(effectiveTo == null ? null : effectiveTo.toLocalDate()).active(rs.getBoolean("is_active")).build(); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerWritePlatformService.java new file mode 100644 index 000000000..a1bcbdf0c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerWritePlatformService.java @@ -0,0 +1,37 @@ +/** + * 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.locker.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface LockerWritePlatformService { + + CommandProcessingResult createClass(JsonCommand command); + + CommandProcessingResult updateClass(Long id, JsonCommand command); + + CommandProcessingResult deactivateClass(Long id); + + CommandProcessingResult createWaitingListEntry(JsonCommand command); + + CommandProcessingResult updateWaitingListEntry(Long id, JsonCommand command); + + CommandProcessingResult cancelWaitingListEntry(Long id, JsonCommand command); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerWritePlatformServiceImpl.java new file mode 100644 index 000000000..abfa435dc --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/locker/service/LockerWritePlatformServiceImpl.java @@ -0,0 +1,265 @@ +/** + * 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.locker.service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; +import org.apache.fineract.portfolio.locker.api.LockerApiConstants; +import org.apache.fineract.portfolio.locker.api.LockerApiJsonInputParams; +import org.apache.fineract.portfolio.locker.domain.Locker; +import org.apache.fineract.portfolio.locker.domain.LockerClass; +import org.apache.fineract.portfolio.locker.domain.LockerClassName; +import org.apache.fineract.portfolio.locker.domain.LockerClassRepository; +import org.apache.fineract.portfolio.locker.domain.LockerClassRepositoryWrapper; +import org.apache.fineract.portfolio.locker.domain.LockerRepository; +import org.apache.fineract.portfolio.locker.domain.LockerStatus; +import org.apache.fineract.portfolio.locker.domain.LockerWaitingList; +import org.apache.fineract.portfolio.locker.domain.LockerWaitingListRepositoryWrapper; +import org.apache.fineract.portfolio.locker.serialization.LockerCommandFromApiJsonDeserializer; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class LockerWritePlatformServiceImpl implements LockerWritePlatformService { + + private final LockerCommandFromApiJsonDeserializer validator; + private final LockerOfficeAccess officeAccess; + private final LockerClassRepository lockerClassRepository; + private final LockerClassRepositoryWrapper lockerClassRepositoryWrapper; + private final LockerRepository lockerRepository; + private final LockerWaitingListRepositoryWrapper lockerWaitingListRepositoryWrapper; + private final ClientRepositoryWrapper clientRepositoryWrapper; + + /** Saves the class row and creates one VACANT locker per number in its range — all or nothing. */ + @Transactional + @Override + public CommandProcessingResult createClass(final JsonCommand command) { + this.validator.validateForCreateClass(command.json()); + + final Long officeId = this.officeAccess + .officeForCreate(command.longValueOfParameterNamed(LockerApiJsonInputParams.OFFICE_ID.getValue())); + final String classCode = command.stringValueOfParameterNamed(LockerApiJsonInputParams.CLASS_CODE.getValue()).trim() + .toUpperCase(); + final Integer startLockerNo = command.integerValueSansLocaleOfParameterNamed(LockerApiJsonInputParams.START_LOCKER_NO.getValue()); + final Integer endLockerNo = command.integerValueSansLocaleOfParameterNamed(LockerApiJsonInputParams.END_LOCKER_NO.getValue()); + assertRangeFree(officeId, classCode, startLockerNo, endLockerNo); + + final LockerClassName className = LockerClassName + .valueOf(command.stringValueOfParameterNamed(LockerApiJsonInputParams.CLASS_NAME.getValue()).toUpperCase()); + final Integer area = command.integerValueSansLocaleOfParameterNamed(LockerApiJsonInputParams.AREA.getValue()); + final BigDecimal rent = command.bigDecimalValueOfParameterNamed(LockerApiJsonInputParams.RENT.getValue()); + final BigDecimal deposit = command.bigDecimalValueOfParameterNamed(LockerApiJsonInputParams.DEPOSIT.getValue()); + // A blank member rate means members pay the normal rate, as on the legacy class master. + final BigDecimal memberRent = valueOrDefault(command, LockerApiJsonInputParams.MEMBER_RENT, rent); + final BigDecimal memberDeposit = valueOrDefault(command, LockerApiJsonInputParams.MEMBER_DEPOSIT, deposit); + final LocalDate effectiveFrom = command.localDateValueOfParameterNamed(LockerApiJsonInputParams.EFFECTIVE_FROM_DATE.getValue()); + final LocalDate effectiveTo = command.localDateValueOfParameterNamed(LockerApiJsonInputParams.EFFECTIVE_TO_DATE.getValue()); + + final LockerClass lockerClass = this.lockerClassRepositoryWrapper.saveAndFlush(LockerClass.createNew(officeId, classCode, className, + area, rent, memberRent, deposit, memberDeposit, startLockerNo, endLockerNo, effectiveFrom, effectiveTo, + this.officeAccess.currentUser().getId())); + + final List lockers = new ArrayList<>(endLockerNo - startLockerNo + 1); + for (int lockerNo = startLockerNo; lockerNo <= endLockerNo; lockerNo++) { + lockers.add(Locker.createVacant(lockerClass, lockerNo)); + } + this.lockerRepository.saveAll(lockers); + this.lockerRepository.flush(); + + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(lockerClass.getId()) + .withOfficeId(officeId).build(); + } + + @Transactional + @Override + public CommandProcessingResult updateClass(final Long id, final JsonCommand command) { + this.validator.validateForUpdateClass(command.json()); + final LockerClass lockerClass = this.lockerClassRepositoryWrapper.findOneWithNotFoundDetection(id); + this.officeAccess.assertOwnBranch(lockerClass.getOfficeId()); + if (!lockerClass.isActive()) { + throw new GeneralPlatformDomainRuleException("error.msg.lockerclass.inactive", + "Locker class " + lockerClass.getClassCode() + " is inactive and cannot be edited.", id); + } + + final String classNameValue = command.stringValueOfParameterNamed(LockerApiJsonInputParams.CLASS_NAME.getValue()); + final LockerClassName className = classNameValue == null || classNameValue.isBlank() ? null + : LockerClassName.valueOf(classNameValue.toUpperCase()); + final LocalDate effectiveFrom = command.localDateValueOfParameterNamed(LockerApiJsonInputParams.EFFECTIVE_FROM_DATE.getValue()); + lockerClass.updateDetails(className, command.integerValueSansLocaleOfParameterNamed(LockerApiJsonInputParams.AREA.getValue()), + command.bigDecimalValueOfParameterNamed(LockerApiJsonInputParams.RENT.getValue()), + command.bigDecimalValueOfParameterNamed(LockerApiJsonInputParams.MEMBER_RENT.getValue()), + command.bigDecimalValueOfParameterNamed(LockerApiJsonInputParams.DEPOSIT.getValue()), + command.bigDecimalValueOfParameterNamed(LockerApiJsonInputParams.MEMBER_DEPOSIT.getValue()), effectiveFrom, + this.officeAccess.currentUser().getId()); + // Effective-to is cleared by sending it blank, so it is applied whenever the key is present. + if (command.parameterExists(LockerApiJsonInputParams.EFFECTIVE_TO_DATE.getValue())) { + lockerClass.updateEffectiveToDate( + command.localDateValueOfParameterNamed(LockerApiJsonInputParams.EFFECTIVE_TO_DATE.getValue())); + } + + // Only one side may have been sent, so the pair is checked again against the saved values. + final List errors = new ArrayList<>(); + this.validator.validateEffectivePeriod(lockerClass.getEffectiveFromDate(), lockerClass.getEffectiveToDate(), + new DataValidatorBuilder(errors).resource(LockerApiConstants.RESOURCE_CLASS)); + if (!errors.isEmpty()) { + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors); + } + + this.lockerClassRepositoryWrapper.saveAndFlush(lockerClass); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(id) + .withOfficeId(lockerClass.getOfficeId()).build(); + } + + /** + * Refused while any locker of the class is allotted. Otherwise the class goes inactive and its unallotted lockers + * are removed, so the same numbers can be set up again as a fresh row (for example with a new rate). + */ + @Transactional + @Override + public CommandProcessingResult deactivateClass(final Long id) { + final LockerClass lockerClass = this.lockerClassRepositoryWrapper.findOneWithNotFoundDetection(id); + this.officeAccess.assertOwnBranch(lockerClass.getOfficeId()); + if (!lockerClass.isActive()) { + throw new GeneralPlatformDomainRuleException("error.msg.lockerclass.already.inactive", + "Locker class " + lockerClass.getClassCode() + " is already inactive.", id); + } + final long allotted = this.lockerRepository.countByLockerClassIdAndStatus(id, LockerStatus.ALLOTTED.name()); + if (allotted > 0) { + throw new GeneralPlatformDomainRuleException("error.msg.lockerclass.has.allotted.lockers", + "Cannot deactivate class " + lockerClass.getClassCode() + " — " + allotted + " of its lockers are allotted.", id, + allotted); + } + this.lockerRepository.deleteUnallottedByLockerClassId(id); + lockerClass.deactivate(this.officeAccess.currentUser().getId()); + this.lockerClassRepositoryWrapper.saveAndFlush(lockerClass); + return new CommandProcessingResultBuilder().withEntityId(id).withOfficeId(lockerClass.getOfficeId()).build(); + } + + @Transactional + @Override + public CommandProcessingResult createWaitingListEntry(final JsonCommand command) { + this.validator.validateForCreateWaitingList(command.json()); + final Long officeId = this.officeAccess + .officeForCreate(command.longValueOfParameterNamed(LockerApiJsonInputParams.OFFICE_ID.getValue())); + final String preferredClasses = resolvePreferredClasses(officeId, command); + + final LockerWaitingList entry = this.lockerWaitingListRepositoryWrapper.saveAndFlush(LockerWaitingList.createNew(officeId, + resolveClientId(command), command.localDateValueOfParameterNamed(LockerApiJsonInputParams.INQUIRY_DATE.getValue()), + trimmed(command, LockerApiJsonInputParams.CUSTOMER_NAME), trimmed(command, LockerApiJsonInputParams.ADDRESS_LINE1), + trimmed(command, LockerApiJsonInputParams.ADDRESS_LINE2), trimmed(command, LockerApiJsonInputParams.ADDRESS_LINE3), + trimmed(command, LockerApiJsonInputParams.CITY), trimmed(command, LockerApiJsonInputParams.PINCODE), + trimmed(command, LockerApiJsonInputParams.PHONE), trimmed(command, LockerApiJsonInputParams.EMAIL), preferredClasses, + trimmed(command, LockerApiJsonInputParams.REMARKS), this.officeAccess.currentUser().getId())); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(entry.getId()).withOfficeId(officeId) + .build(); + } + + @Transactional + @Override + public CommandProcessingResult updateWaitingListEntry(final Long id, final JsonCommand command) { + this.validator.validateForUpdateWaitingList(command.json()); + final LockerWaitingList entry = findPendingWaitingListEntry(id, "edited"); + final String preferredClasses = resolvePreferredClasses(entry.getOfficeId(), command); + entry.updateDetails(resolveClientId(command), command.localDateValueOfParameterNamed(LockerApiJsonInputParams.INQUIRY_DATE.getValue()), + trimmed(command, LockerApiJsonInputParams.CUSTOMER_NAME), trimmed(command, LockerApiJsonInputParams.ADDRESS_LINE1), + trimmed(command, LockerApiJsonInputParams.ADDRESS_LINE2), trimmed(command, LockerApiJsonInputParams.ADDRESS_LINE3), + trimmed(command, LockerApiJsonInputParams.CITY), trimmed(command, LockerApiJsonInputParams.PINCODE), + trimmed(command, LockerApiJsonInputParams.PHONE), trimmed(command, LockerApiJsonInputParams.EMAIL), preferredClasses, + trimmed(command, LockerApiJsonInputParams.REMARKS), this.officeAccess.currentUser().getId()); + this.lockerWaitingListRepositoryWrapper.saveAndFlush(entry); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(id) + .withOfficeId(entry.getOfficeId()).build(); + } + + @Transactional + @Override + public CommandProcessingResult cancelWaitingListEntry(final Long id, final JsonCommand command) { + this.validator.validateForCancelWaitingList(command.json()); + final LockerWaitingList entry = findPendingWaitingListEntry(id, "cancelled"); + entry.cancel(trimmed(command, LockerApiJsonInputParams.ADDITIONAL_REMARKS), this.officeAccess.currentUser().getId()); + this.lockerWaitingListRepositoryWrapper.saveAndFlush(entry); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(id) + .withOfficeId(entry.getOfficeId()).build(); + } + + /** Own branch only, and only while still in the queue — an allotted or cancelled entry is history. */ + private LockerWaitingList findPendingWaitingListEntry(final Long id, final String action) { + final LockerWaitingList entry = this.lockerWaitingListRepositoryWrapper.findOneWithNotFoundDetection(id); + this.officeAccess.assertOwnBranch(entry.getOfficeId()); + if (!entry.isPending()) { + throw new GeneralPlatformDomainRuleException("error.msg.lockerwaitinglist.not.pending", + "Waiting list entry " + id + " is " + entry.getStatus() + " and cannot be " + action + ".", id); + } + return entry; + } + + /** A linked client must exist, be active and be within the user's branch scope; no client id means a walk-in. */ + private Long resolveClientId(final JsonCommand command) { + final Long clientId = command.longValueOfParameterNamed(LockerApiJsonInputParams.CLIENT_ID.getValue()); + return clientId == null ? null : this.clientRepositoryWrapper.getActiveClientInUserScope(clientId).getId(); + } + + /** Every chosen class must be an active class at the entry's branch; stored as "A1,B1". */ + private String resolvePreferredClasses(final Long officeId, final JsonCommand command) { + final List chosen = this.validator.extractPreferredClasses(command.parsedJson()); + final List available = this.lockerClassRepository.findActiveClassCodes(officeId); + final List unknown = chosen.stream().filter(code -> !available.contains(code)).toList(); + if (!unknown.isEmpty()) { + throw new GeneralPlatformDomainRuleException("error.msg.lockerwaitinglist.class.unknown", + "Locker class " + String.join(", ", unknown) + " does not exist at this branch.", String.join(",", unknown)); + } + return String.join(",", chosen); + } + + /** Blank text is stored as null, so an empty optional field never shows as a stray space. */ + private String trimmed(final JsonCommand command, final LockerApiJsonInputParams param) { + final String value = command.stringValueOfParameterNamedAllowingNull(param.getValue()); + return value == null || value.isBlank() ? null : value.trim(); + } + + /** A number already used by this branch's class — in any row — cannot be created a second time. */ + private void assertRangeFree(final Long officeId, final String classCode, final Integer startLockerNo, final Integer endLockerNo) { + if (this.lockerRepository.countExistingInRange(officeId, classCode, startLockerNo, endLockerNo) == 0) { + return; + } + final List clashes = this.lockerClassRepository.findActiveOverlapping(officeId, classCode, startLockerNo, + endLockerNo); + final String existing = clashes.isEmpty() ? "some of these numbers" + : "lockers " + clashes.get(0).getStartLockerNo() + "–" + clashes.get(0).getEndLockerNo(); + throw new GeneralPlatformDomainRuleException("error.msg.lockerclass.range.overlap", + "Class " + classCode + " already has " + existing + " at this branch.", classCode, startLockerNo, endLockerNo); + } + + private BigDecimal valueOrDefault(final JsonCommand command, final LockerApiJsonInputParams param, final BigDecimal fallback) { + final BigDecimal value = command.bigDecimalValueOfParameterNamed(param.getValue()); + return value == null ? fallback : value; + } +} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml index 545b6450b..466e26a77 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/final-changelog-tenant.xml @@ -116,4 +116,8 @@ + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0571_locker_class_master.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0571_locker_class_master.xml new file mode 100644 index 000000000..7597fcf4a --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0571_locker_class_master.xml @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'READ_LOCKERCLASS', 'LOCKERCLASS', 'READ', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_LOCKERCLASS'); + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'CREATE_LOCKERCLASS', 'LOCKERCLASS', 'CREATE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_LOCKERCLASS'); + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'UPDATE_LOCKERCLASS', 'LOCKERCLASS', 'UPDATE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'UPDATE_LOCKERCLASS'); + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'DELETE_LOCKERCLASS', 'LOCKERCLASS', 'DELETE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'DELETE_LOCKERCLASS'); + + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + JOIN m_permission p ON p.code IN ('READ_LOCKERCLASS', 'CREATE_LOCKERCLASS', 'UPDATE_LOCKERCLASS', 'DELETE_LOCKERCLASS') + WHERE r.name = 'Branch Manager' + AND NOT EXISTS (SELECT 1 FROM m_role_permission rp WHERE rp.role_id = r.id AND rp.permission_id = p.id); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0572_locker_waiting_list.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0572_locker_waiting_list.xml new file mode 100644 index 000000000..306b83619 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0572_locker_waiting_list.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'READ_LOCKER', 'LOCKER', 'READ', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_LOCKER'); + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'READ_LOCKERWAITINGLIST', 'LOCKERWAITINGLIST', 'READ', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_LOCKERWAITINGLIST'); + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'CREATE_LOCKERWAITINGLIST', 'LOCKERWAITINGLIST', 'CREATE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_LOCKERWAITINGLIST'); + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'UPDATE_LOCKERWAITINGLIST', 'LOCKERWAITINGLIST', 'UPDATE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'UPDATE_LOCKERWAITINGLIST'); + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'CANCEL_LOCKERWAITINGLIST', 'LOCKERWAITINGLIST', 'CANCEL', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CANCEL_LOCKERWAITINGLIST'); + + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + JOIN m_permission p ON p.code IN ('READ_LOCKER', 'READ_LOCKERWAITINGLIST', 'CREATE_LOCKERWAITINGLIST', + 'UPDATE_LOCKERWAITINGLIST', 'CANCEL_LOCKERWAITINGLIST') + WHERE r.name IN ('Branch Manager', 'Clerk') + AND NOT EXISTS (SELECT 1 FROM m_role_permission rp WHERE rp.role_id = r.id AND rp.permission_id = p.id); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0573_locker_waiting_list_client_link.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0573_locker_waiting_list_client_link.xml new file mode 100644 index 000000000..377b1b89d --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0573_locker_waiting_list_client_link.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0574_locker_allotment.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0574_locker_allotment.xml new file mode 100644 index 000000000..a3ff2a630 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0574_locker_allotment.xml @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CREATE UNIQUE INDEX uk_locker_allot_live_locker ON m_locker_allotment (locker_id) + WHERE status IN ('PENDING_APPROVAL', 'ACTIVE'); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CREATE UNIQUE INDEX uk_locker_dep_live_account ON m_locker_allotment_deposit (savings_account_id) + WHERE lien_status IN ('PENDING', 'ACTIVE'); + + + + + + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'READ_LOCKERALLOTMENT', 'LOCKERALLOTMENT', 'READ', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_LOCKERALLOTMENT'); + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'CREATE_LOCKERALLOTMENT', 'LOCKERALLOTMENT', 'CREATE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_LOCKERALLOTMENT'); + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'UPDATE_LOCKERALLOTMENT', 'LOCKERALLOTMENT', 'UPDATE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'UPDATE_LOCKERALLOTMENT'); + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'APPROVE_LOCKERALLOTMENT', 'LOCKERALLOTMENT', 'APPROVE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'APPROVE_LOCKERALLOTMENT'); + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'REJECT_LOCKERALLOTMENT', 'LOCKERALLOTMENT', 'REJECT', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REJECT_LOCKERALLOTMENT'); + + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + JOIN m_permission p ON p.code IN ('READ_LOCKERALLOTMENT', 'CREATE_LOCKERALLOTMENT', 'UPDATE_LOCKERALLOTMENT') + WHERE r.name IN ('Branch Manager', 'Clerk') + AND NOT EXISTS (SELECT 1 FROM m_role_permission rp WHERE rp.role_id = r.id AND rp.permission_id = p.id); + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + JOIN m_permission p ON p.code IN ('APPROVE_LOCKERALLOTMENT', 'REJECT_LOCKERALLOTMENT') + WHERE r.name = 'Branch Manager' + AND NOT EXISTS (SELECT 1 FROM m_role_permission rp WHERE rp.role_id = r.id AND rp.permission_id = p.id); + + + +