Compare commits

...
Author SHA1 Message Date
tarunmalviya aa3cb6ad25 Locker module: class master, locker list, waiting list, allotment (tables + client details API)
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
2026-10-05 15:18:52 +05:30
43 changed files with 3313 additions and 0 deletions
@@ -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";
@@ -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);
}
}
@@ -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;
}
@@ -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<String> setOf(final LockerApiJsonInputParams... params) {
return Arrays.stream(params).map(LockerApiJsonInputParams::getValue).collect(Collectors.toCollection(HashSet::new));
}
public static Set<String> 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<String> classUpdateParams() {
return setOf(CLASS_NAME, AREA, RENT, MEMBER_RENT, DEPOSIT, MEMBER_DEPOSIT, EFFECTIVE_FROM_DATE, EFFECTIVE_TO_DATE, LOCALE,
DATE_FORMAT);
}
public static Set<String> waitingListCreateParams() {
final Set<String> params = waitingListUpdateParams();
params.add(OFFICE_ID.getValue());
return params;
}
/** Branch is fixed once the entry is taken. */
public static Set<String> 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<String> waitingListCancelParams() {
return setOf(ADDITIONAL_REMARKS, LOCALE, DATE_FORMAT);
}
}
@@ -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<LockerData> 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);
}
}
@@ -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<LockerClassData> 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);
}
}
@@ -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<LockerWaitingListData> 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);
}
}
@@ -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<SavingsOption> savingsAccounts;
private final List<FixedDepositOption> 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;
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<String> preferredClasses;
private final String remarks;
private final String status;
private final String additionalRemarks;
}
@@ -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<Long> {
@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;
}
}
@@ -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<Long> {
@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();
}
}
@@ -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
}
@@ -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<LockerClass, Long> {
/** 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<LockerClass> 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<String> findActiveClassCodes(@Param("officeId") Long officeId);
}
@@ -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);
}
}
@@ -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<Locker, Long> {
/** 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);
}
@@ -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
}
@@ -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<Long> {
@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();
}
}
@@ -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<LockerWaitingList, Long> {}
@@ -0,0 +1,38 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.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);
}
}
@@ -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
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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());
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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<ApiParameterError> 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<ApiParameterError> 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<ApiParameterError> 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<ApiParameterError> 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<ApiParameterError> 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<String> 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<String> extractPreferredClasses(final JsonElement element) {
final JsonArray array = this.fromApiJsonHelper.extractJsonArrayNamed(LockerApiJsonInputParams.PREFERRED_CLASSES.getValue(),
element);
final List<String> 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<String> supportedParams) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, supportedParams);
return this.fromApiJsonHelper.parse(json);
}
private void throwIfErrors(final List<ApiParameterError> errors) {
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors);
}
}
}
@@ -0,0 +1,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.");
}
}
}
@@ -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<LockerClassData> retrieveAllClasses(String status, Long officeId, Integer offset, Integer limit);
LockerClassData retrieveClass(Long id);
Page<LockerData> retrieveLockers(Long officeId, String classCode, String status, Integer lockerNo, Integer offset, Integer limit);
LockerSummaryData retrieveLockerSummary(Long officeId);
Page<LockerWaitingListData> retrieveWaitingList(String status, Long officeId, Integer offset, Integer limit);
LockerWaitingListData retrieveWaitingListEntry(Long id);
LockerClientContactData retrieveClientContact(Long clientId);
LockerAllotmentClientData retrieveAllotmentClientDetails(Long clientId);
}
@@ -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<LockerClassData> retrieveAllClasses(final String status, final Long officeId, final Integer offset, final Integer limit) {
final StringBuilder where = new StringBuilder(" where 1=1");
final List<Object> args = new ArrayList<>();
if ("ACTIVE".equalsIgnoreCase(status)) {
where.append(" and 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<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(limit);
pageArgs.add(offset);
final List<LockerClassData> 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<LockerData> 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<Object> 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<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(limit);
pageArgs.add(offset);
final List<LockerData> 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<LockerWaitingListData> retrieveWaitingList(final String status, final Long officeId, final Integer offset,
final Integer limit) {
final StringBuilder where = new StringBuilder(" where 1=1");
final List<Object> 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<Object> pageArgs = new ArrayList<>(args);
pageArgs.add(limit);
pageArgs.add(offset);
final List<LockerWaitingListData> 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<String, Object> 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<String, Object> 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<Map<String, Object>> 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<LockerAllotmentClientData.SavingsOption> 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<LockerAllotmentClientData.FixedDepositOption> 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<LockerData> {
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<LockerWaitingListData> {
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<LockerClassData> {
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();
}
}
}
@@ -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);
}
@@ -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<Locker> 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<ApiParameterError> 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<String> chosen = this.validator.extractPreferredClasses(command.parsedJson());
final List<String> available = this.lockerClassRepository.findActiveClassCodes(officeId);
final List<String> 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<LockerClass> 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;
}
}
@@ -116,4 +116,8 @@
<include file="parts/0568_add_settlement_mode_to_clerk_share_withdrawal_request.xml" relativeToChangelogFile="true"/>
<include file="parts/0569_aml_alert_release_stale_under_review.xml" relativeToChangelogFile="true"/>
<include file="parts/0570_add_dd_payable_gl_to_sponsor_bank.xml" relativeToChangelogFile="true"/>
<include file="parts/0571_locker_class_master.xml" relativeToChangelogFile="true"/>
<include file="parts/0572_locker_waiting_list.xml" relativeToChangelogFile="true"/>
<include file="parts/0573_locker_waiting_list_client_link.xml" relativeToChangelogFile="true"/>
<include file="parts/0574_locker_allotment.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,155 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">
<!--
Safe Deposit Locker - class master and locker inventory.
A class row is one locker-number range of a class at a branch (a class such as A1 can have several
ranges), with member and non-member rent and deposit and an effective period. Saving a class row
materialises one m_locker row per number in its range, so every physical box exists as a record
that allotment, rent and the operation register can point at.
A locker is unique per branch + class + number. B1 and C1 may both have a locker 1; A1 may not have
two. Occupied is never stored on the class: it is counted from m_locker.status = 'ALLOTTED'.
-->
<changeSet id="0571-1-locker-class" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_locker_class"/></not>
</preConditions>
<createTable tableName="m_locker_class">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="office_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_locker_class_office" references="m_office(id)"/>
</column>
<column name="class_code" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="class_name" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="area" type="INTEGER">
<constraints nullable="false"/>
</column>
<column name="rent" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="member_rent" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="deposit" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="member_deposit" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="start_locker_no" type="INTEGER">
<constraints nullable="false"/>
</column>
<column name="end_locker_no" type="INTEGER">
<constraints nullable="false"/>
</column>
<column name="effective_from_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="effective_to_date" type="DATE"/>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="lastmodified_by" type="BIGINT"/>
<column name="lastmodified_on" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
<createIndex tableName="m_locker_class" indexName="idx_locker_class_office_code">
<column name="office_id"/>
<column name="class_code"/>
</createIndex>
</changeSet>
<changeSet id="0571-2-locker" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_locker"/></not>
</preConditions>
<createTable tableName="m_locker">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="office_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_locker_office" references="m_office(id)"/>
</column>
<column name="locker_class_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_locker_class" references="m_locker_class(id)"/>
</column>
<column name="class_code" type="VARCHAR(10)">
<constraints nullable="false"/>
</column>
<column name="locker_no" type="INTEGER">
<constraints nullable="false"/>
</column>
<column name="key_no" type="VARCHAR(20)"/>
<!-- VACANT / ALLOTTED / BLOCKED -->
<column name="status" type="VARCHAR(20)" defaultValue="VACANT">
<constraints nullable="false"/>
</column>
</createTable>
<addUniqueConstraint tableName="m_locker" columnNames="office_id, class_code, locker_no"
constraintName="uk_locker_office_class_no"/>
<createIndex tableName="m_locker" indexName="idx_locker_class_status">
<column name="locker_class_id"/>
<column name="status"/>
</createIndex>
</changeSet>
<!--
Only a Branch Manager (own branch) or the Super user (every branch, through ALL_FUNCTIONS) may create,
edit or deactivate a class. Read goes to Branch Manager too; other roles that hold ALL_FUNCTIONS_READ
(Clerk) can already see it.
-->
<changeSet id="0571-3-locker-class-permissions" author="CBS">
<sql>
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');
</sql>
<sql>
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);
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">
<!--
Safe Deposit Locker - locker list read access and the waiting list.
Waiting list: an inquiry for a locker, taken at the counter from anyone (the inquirer need not be a client
yet), with one or more preferred classes. Its id is the waiting number given to the applicant, as the RBI
locker directions require. Allotment will later move an entry to ALLOTTED; the counter can CANCEL it.
-->
<changeSet id="0572-1-locker-waiting-list" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_locker_waiting_list"/></not>
</preConditions>
<createTable tableName="m_locker_waiting_list">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="office_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_locker_waiting_office" references="m_office(id)"/>
</column>
<column name="inquiry_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="customer_name" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="address_line1" type="VARCHAR(150)">
<constraints nullable="false"/>
</column>
<column name="address_line2" type="VARCHAR(150)"/>
<column name="address_line3" type="VARCHAR(150)"/>
<column name="city" type="VARCHAR(60)">
<constraints nullable="false"/>
</column>
<column name="pincode" type="VARCHAR(6)">
<constraints nullable="false"/>
</column>
<column name="phone" type="VARCHAR(15)"/>
<column name="email" type="VARCHAR(100)"/>
<!-- comma-separated class codes, e.g. A1,B1 -->
<column name="preferred_classes" type="VARCHAR(200)">
<constraints nullable="false"/>
</column>
<column name="remarks" type="VARCHAR(500)"/>
<!-- PENDING / ALLOTTED / CANCELLED -->
<column name="status" type="VARCHAR(20)" defaultValue="PENDING">
<constraints nullable="false"/>
</column>
<column name="additional_remarks" type="VARCHAR(500)"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="lastmodified_by" type="BIGINT"/>
<column name="lastmodified_on" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
<createIndex tableName="m_locker_waiting_list" indexName="idx_locker_waiting_office_status">
<column name="office_id"/>
<column name="status"/>
</createIndex>
</changeSet>
<!-- Locker list and waiting list are counter work: Branch Manager and Clerk, each confined to their own branch. -->
<changeSet id="0572-2-locker-permissions" author="CBS">
<sql>
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');
</sql>
<sql>
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);
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">
<!--
Links a waiting-list inquiry to an existing client when the inquirer already banks here, so the counter can
pick the client instead of retyping name, address and contact, and allotment can later start from the same
client. Nullable: an inquirer who is not yet a client is still taken by name, as before.
-->
<changeSet id="0573-1-locker-waiting-list-client" author="CBS">
<preConditions onFail="MARK_RAN">
<and>
<tableExists tableName="m_locker_waiting_list"/>
<not><columnExists tableName="m_locker_waiting_list" columnName="client_id"/></not>
</and>
</preConditions>
<addColumn tableName="m_locker_waiting_list">
<column name="client_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_locker_waiting_client" references="m_client(id)"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,215 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">
<!--
Safe Deposit Locker - allotment (the legacy "A/c Master Maintenance": application master, joint members,
other entries, nominees).
An application is submitted PENDING_APPROVAL and holds its locker so no one else can take it; the Branch
Manager approves it (locker ALLOTTED, deposits under lien, waiting-list entry ALLOTTED) or rejects it
(locker vacant again). Its id is the application number. Lockers are only ever allotted to existing clients.
The partial unique index on locker_id is the database's own guarantee that one locker cannot be held by two
live applications at once, whatever happens in the application code.
Security deposit is optional. A fixed deposit given against a locker is recorded with a lien status: PENDING
while the application waits, ACTIVE once approved, RELEASED on reject or surrender.
-->
<changeSet id="0574-1-locker-allotment" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_locker_allotment"/></not>
</preConditions>
<createTable tableName="m_locker_allotment">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="office_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_locker_allot_office" references="m_office(id)"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_locker_allot_client" references="m_client(id)"/>
</column>
<column name="locker_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_locker_allot_locker" references="m_locker(id)"/>
</column>
<column name="key_no" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<!-- one-way hash, never the password itself -->
<column name="password_hash" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<!-- MEMBER / NON_MEMBER / STAFF -->
<column name="category" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<!-- SELF / EITHER_OR_SURVIVOR / FORMER_OR_SURVIVOR / ANYONE_OR_SURVIVOR / JOINTLY -->
<column name="pay_mode" type="VARCHAR(30)">
<constraints nullable="false"/>
</column>
<column name="dual_auth" type="BOOLEAN" defaultValueBoolean="false"><constraints nullable="false"/></column>
<column name="sms_active" type="BOOLEAN" defaultValueBoolean="true"><constraints nullable="false"/></column>
<column name="email_active" type="BOOLEAN" defaultValueBoolean="false"><constraints nullable="false"/></column>
<column name="rent_exemption" type="BOOLEAN" defaultValueBoolean="false"><constraints nullable="false"/></column>
<column name="rent_account_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_locker_allot_rent_account" references="m_savings_account(id)"/>
</column>
<column name="risk" type="VARCHAR(10)"/>
<column name="is_frozen" type="BOOLEAN" defaultValueBoolean="false"><constraints nullable="false"/></column>
<column name="freeze_remark" type="VARCHAR(200)"/>
<column name="waiting_list_id" type="BIGINT">
<constraints nullable="true" foreignKeyName="fk_locker_allot_waiting" references="m_locker_waiting_list(id)"/>
</column>
<column name="phone2" type="VARCHAR(15)"/>
<column name="photo_received" type="BOOLEAN" defaultValueBoolean="false"><constraints nullable="false"/></column>
<column name="address_proof_received" type="BOOLEAN" defaultValueBoolean="false"><constraints nullable="false"/></column>
<column name="remarks" type="VARCHAR(500)"/>
<column name="transaction_date" type="DATE"><constraints nullable="false"/></column>
<column name="opening_date" type="DATE"/>
<column name="close_date" type="DATE"/>
<column name="rent_up_to_date" type="DATE"/>
<!-- PENDING_APPROVAL / ACTIVE / REJECTED / CLOSED -->
<column name="status" type="VARCHAR(20)" defaultValue="PENDING_APPROVAL"><constraints nullable="false"/></column>
<column name="rejection_reason" type="VARCHAR(500)"/>
<column name="submitted_by" type="BIGINT"/>
<column name="submitted_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="approved_by" type="BIGINT"/>
<column name="approved_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="rejected_by" type="BIGINT"/>
<column name="rejected_on" type="TIMESTAMP WITH TIME ZONE"/>
<column name="lastmodified_by" type="BIGINT"/>
<column name="lastmodified_on" type="TIMESTAMP WITH TIME ZONE"/>
</createTable>
<createIndex tableName="m_locker_allotment" indexName="idx_locker_allot_office_status">
<column name="office_id"/>
<column name="status"/>
</createIndex>
<createIndex tableName="m_locker_allotment" indexName="idx_locker_allot_client">
<column name="client_id"/>
</createIndex>
<sql>
CREATE UNIQUE INDEX uk_locker_allot_live_locker ON m_locker_allotment (locker_id)
WHERE status IN ('PENDING_APPROVAL', 'ACTIVE');
</sql>
</changeSet>
<changeSet id="0574-2-locker-allotment-children" author="CBS">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_locker_allotment_joint"/></not>
</preConditions>
<createTable tableName="m_locker_allotment_joint">
<column name="id" type="BIGINT" autoIncrement="true"><constraints primaryKey="true" nullable="false"/></column>
<column name="allotment_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_locker_joint_allot" references="m_locker_allotment(id)"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_locker_joint_client" references="m_client(id)"/>
</column>
<!-- JOINT_HOLDER / MANDATE_HOLDER / AUTHORISED_SIGNATORY -->
<column name="join_category" type="VARCHAR(30)"><constraints nullable="false"/></column>
</createTable>
<createTable tableName="m_locker_allotment_guarantor">
<column name="id" type="BIGINT" autoIncrement="true"><constraints primaryKey="true" nullable="false"/></column>
<column name="allotment_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_locker_guar_allot" references="m_locker_allotment(id)"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_locker_guar_client" references="m_client(id)"/>
</column>
</createTable>
<createTable tableName="m_locker_allotment_nominee">
<column name="id" type="BIGINT" autoIncrement="true"><constraints primaryKey="true" nullable="false"/></column>
<column name="allotment_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_locker_nom_allot" references="m_locker_allotment(id)"/>
</column>
<column name="nomination_date" type="DATE"><constraints nullable="false"/></column>
<column name="name" type="VARCHAR(100)"><constraints nullable="false"/></column>
<column name="mobile_no" type="VARCHAR(15)"/>
<column name="date_of_birth" type="DATE"/>
<column name="relation" type="VARCHAR(20)"><constraints nullable="false"/></column>
<column name="gender" type="VARCHAR(10)"><constraints nullable="false"/></column>
<column name="guardian_name" type="VARCHAR(100)"/>
</createTable>
<createTable tableName="m_locker_allotment_deposit">
<column name="id" type="BIGINT" autoIncrement="true"><constraints primaryKey="true" nullable="false"/></column>
<column name="allotment_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_locker_dep_allot" references="m_locker_allotment(id)"/>
</column>
<column name="savings_account_id" type="BIGINT">
<constraints nullable="false" foreignKeyName="fk_locker_dep_account" references="m_savings_account(id)"/>
</column>
<!-- deposit amount when it was given, kept even if the account later changes -->
<column name="deposit_amount" type="DECIMAL(19,6)"><constraints nullable="false"/></column>
<!-- PENDING / ACTIVE / RELEASED -->
<column name="lien_status" type="VARCHAR(20)" defaultValue="PENDING"><constraints nullable="false"/></column>
<column name="lien_marked_on" type="DATE"/>
<column name="lien_released_on" type="DATE"/>
</createTable>
<sql>
CREATE UNIQUE INDEX uk_locker_dep_live_account ON m_locker_allotment_deposit (savings_account_id)
WHERE lien_status IN ('PENDING', 'ACTIVE');
</sql>
</changeSet>
<!--
Application entry and edits are counter work (Clerk, Branch Manager); approval and rejection are the Branch
Manager's alone. The Super user holds ALL_FUNCTIONS. Submitter and approver must be different users.
-->
<changeSet id="0574-3-locker-allotment-permissions" author="CBS">
<sql>
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');
</sql>
<sql>
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);
</sql>
</changeSet>
</databaseChangeLog>