From 67810bfaea864e84235c5770c19b20796eb297ed Mon Sep 17 00:00:00 2001 From: Anshul Rathore Date: Fri, 31 Jul 2026 11:51:57 +0530 Subject: [PATCH] Add Welcome Kit Dispatch feature and Share Account summary API --- .../api/ShareAccountSummaryApiResource.java | 46 +++++++ .../data/ShareAccountClientSummaryData.java | 54 ++++++++ .../ShareAccountReadPlatformService.java | 3 + .../ShareAccountReadPlatformServiceImpl.java | 81 ++++++++++++ .../api/ClientWelcomeKitApiResource.java | 73 +++++++++++ .../welcomekit/api/WelcomeKitApiResource.java | 67 ++++++++++ .../welcomekit/data/ClientWelcomeKitData.java | 33 +++++ .../data/WelcomeKitDispatchData.java | 40 ++++++ .../welcomekit/domain/WelcomeKitDispatch.java | 118 ++++++++++++++++++ .../domain/WelcomeKitDispatchRepository.java | 28 +++++ .../WelcomeKitReadPlatformService.java | 26 ++++ .../WelcomeKitReadPlatformServiceImpl.java | 103 +++++++++++++++ .../WelcomeKitWritePlatformService.java | 29 +++++ .../WelcomeKitWritePlatformServiceImpl.java | 112 +++++++++++++++++ .../db/changelog/tenant/changelog-tenant.xml | 1 + .../parts/0415_welcome_kit_dispatch.xml | 38 ++++++ 16 files changed, 852 insertions(+) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/api/ShareAccountSummaryApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/data/ShareAccountClientSummaryData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/api/ClientWelcomeKitApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/api/WelcomeKitApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/data/ClientWelcomeKitData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/data/WelcomeKitDispatchData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/domain/WelcomeKitDispatch.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/domain/WelcomeKitDispatchRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitReadPlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitReadPlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitWritePlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitWritePlatformServiceImpl.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0415_welcome_kit_dispatch.xml diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/api/ShareAccountSummaryApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/api/ShareAccountSummaryApiResource.java new file mode 100644 index 000000000..9739ef49d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/api/ShareAccountSummaryApiResource.java @@ -0,0 +1,46 @@ +package org.apache.fineract.portfolio.shareaccounts.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import java.util.Collection; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountClientSummaryData; +import org.apache.fineract.portfolio.shareaccounts.service.ShareAccountReadPlatformService; +import org.springframework.stereotype.Component; + +@Path("/v1/shareaccounts/summary") +@Component +@Tag(name = "Share Account Summary", description = "Provides a highly optimized summary of all share accounts linked with their respective clients.") +@RequiredArgsConstructor +public class ShareAccountSummaryApiResource { + + private final PlatformSecurityContext platformSecurityContext; + private final ShareAccountReadPlatformService shareAccountReadPlatformService; + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List all share accounts with client details", description = "Retrieves all share accounts along with their associated client details in a single optimized query.\n\n" + + "Example Requests:\n\n" + "shareaccounts/summary") + @ApiResponse(responseCode = "200", description = "OK") + public Collection retrieveAllSummaries(@QueryParam("searchTerm") final String searchTerm) { + long start = System.currentTimeMillis(); + System.out.println("[TIMING] Start of retrieveAllSummaries (before auth)"); + this.platformSecurityContext.authenticatedUser(); + long afterAuth = System.currentTimeMillis(); + System.out.println("[TIMING] Auth check: " + (afterAuth - start) + "ms"); + Collection result = this.shareAccountReadPlatformService.retrieveAllShareAccountClientSummaries(searchTerm); + long afterService = System.currentTimeMillis(); + System.out.println("[TIMING] Service call total (including query): " + (afterService - afterAuth) + "ms"); + System.out.println("[TIMING] Right before response returned: " + (System.currentTimeMillis() - start) + "ms total so far"); + return result; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/data/ShareAccountClientSummaryData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/data/ShareAccountClientSummaryData.java new file mode 100644 index 000000000..36b001f00 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/data/ShareAccountClientSummaryData.java @@ -0,0 +1,54 @@ +package org.apache.fineract.portfolio.shareaccounts.data; + +import java.io.Serializable; +import java.time.LocalDate; +import lombok.Getter; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountStatusEnumData; + +@Getter +public class ShareAccountClientSummaryData implements Serializable { + + private final Long id; + private final Long clientId; + private final String clientName; + private final Long officeId; + private final String officeName; + private final String accountNo; + private final Long productId; + private final String productName; + private final Long totalApprovedShares; + private final Long totalPendingForApprovalShares; + private final CurrencyData currency; + private final LocalDate activatedDate; + private final ShareAccountStatusEnumData status; + + public ShareAccountClientSummaryData(final Long id, final Long clientId, final String clientName, final Long officeId, final String officeName, final String accountNo, + final Long productId, final String productName, final Long totalApprovedShares, final Long totalPendingForApprovalShares, + final CurrencyData currency, final LocalDate activatedDate, final ShareAccountStatusEnumData status) { + this.id = id; + this.clientId = clientId; + this.clientName = clientName; + this.officeId = officeId; + this.officeName = officeName; + this.accountNo = accountNo; + this.productId = productId; + this.productName = productName; + + if (totalApprovedShares == null) { + this.totalApprovedShares = Long.valueOf(0); + } else { + this.totalApprovedShares = totalApprovedShares; + } + + if (totalPendingForApprovalShares == null) { + this.totalPendingForApprovalShares = Long.valueOf(0); + } else { + this.totalPendingForApprovalShares = totalPendingForApprovalShares; + } + + this.currency = currency; + this.activatedDate = activatedDate; + this.status = status; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountReadPlatformService.java index 963d46508..f81e77a31 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountReadPlatformService.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.Set; import org.apache.fineract.portfolio.accounts.service.AccountReadPlatformService; import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountData; +import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountClientSummaryData; public interface ShareAccountReadPlatformService extends AccountReadPlatformService { @@ -38,5 +39,7 @@ public interface ShareAccountReadPlatformService extends AccountReadPlatformServ Collection retrieveAllShareAccountDataForDividends(Long productId, boolean fetchInActiveAccounts, LocalDate startDate); + Collection retrieveAllShareAccountClientSummaries(String searchTerm); + String retrieveAccountNumberByAccountId(Long accountId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountReadPlatformServiceImpl.java index 7124ff789..a7126f6c9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/shareaccounts/service/ShareAccountReadPlatformServiceImpl.java @@ -62,6 +62,7 @@ import org.apache.fineract.portfolio.shareaccounts.domain.ShareAccountStatusType import org.apache.fineract.portfolio.shareproducts.data.ShareProductData; import org.apache.fineract.portfolio.shareproducts.data.ShareProductMarketPriceData; import org.apache.fineract.portfolio.shareproducts.service.ShareProductDropdownReadPlatformService; +import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountClientSummaryData; import org.springframework.context.ApplicationContext; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -221,6 +222,29 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat }, mapper); } + @Override + public Collection retrieveAllShareAccountClientSummaries(String searchTerm) { + long start = System.currentTimeMillis(); + System.out.println("[TIMING] Inside retrieveAllShareAccountClientSummaries, before query builder setup"); + ShareAccountClientSummaryMapper mapper = new ShareAccountClientSummaryMapper(); + StringBuilder queryBuilder = new StringBuilder("select ").append(mapper.schema()); + List paramList = new ArrayList<>(); + + if (searchTerm != null && !searchTerm.trim().isEmpty()) { + queryBuilder.append(" where (c.display_name ILIKE ? OR sa.account_no ILIKE ?)"); + String searchPattern = "%" + searchTerm + "%"; + paramList.add(searchPattern); + paramList.add(searchPattern); + } + + long beforeQuery = System.currentTimeMillis(); + System.out.println("[TIMING] Before actual SQL query call. Time since service start: " + (beforeQuery - start) + "ms"); + Collection result = this.jdbcTemplate.query(queryBuilder.toString(), mapper, paramList.toArray()); + long afterQuery = System.currentTimeMillis(); + System.out.println("[TIMING] Query execution (JdbcTemplate): " + (afterQuery - beforeQuery) + "ms"); + return result; + } + public Collection convertChargesToShareAccountCharges(Collection productCharges) { final Collection savingsCharges = new ArrayList<>(); for (final ChargeData chargeData : productCharges) { @@ -491,6 +515,63 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat } } + private static final class ShareAccountClientSummaryMapper implements RowMapper { + + private final String schema; + + ShareAccountClientSummaryMapper() { + StringBuilder buff = new StringBuilder().append("sa.id as id, c.id as clientId, c.display_name as clientName, ") + .append("sa.account_no as accountNo, p.id as productId, p.name as productName, ") + .append("sa.total_approved_shares as approvedShares, sa.total_pending_shares as pendingShares, ") + .append("sa.currency_code as currencyCode, sa.currency_digits as currencyDigits, sa.currency_multiplesof as inMultiplesOf, ") + .append("curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ") + .append("curr.display_symbol as currencyDisplaySymbol, sa.activated_date as activatedDate, ") + .append("sa.status_enum as statusEnum, o.id as officeId, o.name as officeName ") + .append("from m_share_account sa ") + .append("inner join m_client c ON c.id = sa.client_id ") + .append("inner join m_office o ON o.id = c.office_id ") + .append("inner join m_share_product p on p.id = sa.product_id ") + .append("inner join m_currency curr on curr.code = sa.currency_code"); + this.schema = buff.toString(); + } + + @Override + public ShareAccountClientSummaryData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException { + final Long id = rs.getLong("id"); + final Long clientId = rs.getLong("clientId"); + final String clientName = rs.getString("clientName"); + final String accountNo = rs.getString("accountNo"); + final Long productId = rs.getLong("productId"); + final String productName = rs.getString("productName"); + final Long totalApprovedShares = JdbcSupport.getLong(rs, "approvedShares"); + final Long totalPendingForApprovalShares = JdbcSupport.getLong(rs, "pendingShares"); + + final String currencyCode = rs.getString("currencyCode"); + final String currencyName = rs.getString("currencyName"); + final String currencyNameCode = rs.getString("currencyNameCode"); + final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol"); + final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits"); + final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf"); + final CurrencyData currency = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf, + currencyDisplaySymbol, currencyNameCode); + + final LocalDate activatedDate = JdbcSupport.getLocalDate(rs, "activatedDate"); + + final Integer statusEnum = rs.getInt("statusEnum"); + final ShareAccountStatusEnumData status = SharesEnumerations.status(statusEnum); + + final Long officeId = rs.getLong("officeId"); + final String officeName = rs.getString("officeName"); + + return new ShareAccountClientSummaryData(id, clientId, clientName, officeId, officeName, accountNo, productId, productName, + totalApprovedShares, totalPendingForApprovalShares, currency, activatedDate, status); + } + + public String schema() { + return this.schema; + } + } + @Override public String retrieveAccountNumberByAccountId(Long accountId) { try { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/api/ClientWelcomeKitApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/api/ClientWelcomeKitApiResource.java new file mode 100644 index 000000000..16063c582 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/api/ClientWelcomeKitApiResource.java @@ -0,0 +1,73 @@ +/** + * 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.welcomekit.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.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.welcomekit.data.ClientWelcomeKitData; +import org.apache.fineract.portfolio.welcomekit.service.WelcomeKitReadPlatformService; +import org.apache.fineract.portfolio.welcomekit.service.WelcomeKitWritePlatformService; +import org.springframework.stereotype.Component; + +@Path("/v1/clients/{clientId}/welcomekit") +@Component +@Tag(name = "Welcome Kit Dispatch", description = "Manages the dispatching and tracking of welcome kits for clients.") +@RequiredArgsConstructor +public class ClientWelcomeKitApiResource { + + private static final String RESOURCE_NAME_FOR_PERMISSIONS = "WELCOMEKIT"; + private final PlatformSecurityContext context; + private final WelcomeKitReadPlatformService readPlatformService; + private final WelcomeKitWritePlatformService writePlatformService; + private final FromJsonHelper fromJsonHelper; + + @GET + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "List all welcome kits for a Client") + public ClientWelcomeKitData getClientWelcomeKits(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId) { + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); + return this.readPlatformService.retrieveWelcomeKitsByClientId(clientId); + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Create a new welcome kit dispatch") + public CommandProcessingResult dispatchWelcomeKit(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + this.context.authenticatedUser().validateHasPermissionTo("CREATE_" + RESOURCE_NAME_FOR_PERMISSIONS); + + final JsonCommand command = JsonCommand.fromJsonElement(clientId, this.fromJsonHelper.parse(apiRequestBodyAsJson), this.fromJsonHelper); + + return this.writePlatformService.dispatchWelcomeKit(clientId, command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/api/WelcomeKitApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/api/WelcomeKitApiResource.java new file mode 100644 index 000000000..b7b56f843 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/api/WelcomeKitApiResource.java @@ -0,0 +1,67 @@ +/** + * 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.welcomekit.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.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.core.MediaType; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.welcomekit.data.ClientWelcomeKitData; +import org.apache.fineract.portfolio.welcomekit.service.WelcomeKitReadPlatformService; +import org.apache.fineract.portfolio.welcomekit.service.WelcomeKitWritePlatformService; +import org.springframework.stereotype.Component; + +@Path("/v1/welcomekit") +@Component +@Tag(name = "Welcome Kit Dispatch", description = "Manages the dispatching and tracking of welcome kits for clients.") +@RequiredArgsConstructor +public class WelcomeKitApiResource { + + private static final String RESOURCE_NAME_FOR_PERMISSIONS = "WELCOMEKIT"; + private final PlatformSecurityContext context; + private final WelcomeKitWritePlatformService writePlatformService; + private final FromJsonHelper fromJsonHelper; + + @PUT + @Path("{welcomeKitId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Update the status of an existing dispatch") + public CommandProcessingResult updateWelcomeKitStatus( + @PathParam("welcomeKitId") @Parameter(description = "welcomeKitId") final Long welcomeKitId, + @Parameter(hidden = true) final String apiRequestBodyAsJson) { + this.context.authenticatedUser().validateHasPermissionTo("UPDATE_" + RESOURCE_NAME_FOR_PERMISSIONS); + + final JsonCommand command = JsonCommand.fromJsonElement(welcomeKitId, this.fromJsonHelper.parse(apiRequestBodyAsJson), this.fromJsonHelper); + + return this.writePlatformService.updateWelcomeKitStatus(welcomeKitId, command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/data/ClientWelcomeKitData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/data/ClientWelcomeKitData.java new file mode 100644 index 000000000..74b68009c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/data/ClientWelcomeKitData.java @@ -0,0 +1,33 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.welcomekit.data; + +import java.io.Serializable; +import java.util.List; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ClientWelcomeKitData implements Serializable { + + private final String clientName; + private final boolean canDispatchNew; + private final List dispatches; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/data/WelcomeKitDispatchData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/data/WelcomeKitDispatchData.java new file mode 100644 index 000000000..37f381e45 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/data/WelcomeKitDispatchData.java @@ -0,0 +1,40 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.welcomekit.data; + +import java.io.Serializable; +import java.time.LocalDate; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class WelcomeKitDispatchData implements Serializable { + + private final Long id; + private final Long clientId; + private final String clientName; + private final String deliveryAddress; + private final String deliveryMode; + private final String deliveryModeOtherText; + private final String trackingId; + private final String status; + private final LocalDate dispatchedOnDate; + private final LocalDate deliveredOnDate; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/domain/WelcomeKitDispatch.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/domain/WelcomeKitDispatch.java new file mode 100644 index 000000000..1f2f881c7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/domain/WelcomeKitDispatch.java @@ -0,0 +1,118 @@ +/** + * 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.welcomekit.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.LocalDate; +import java.time.LocalDateTime; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.portfolio.client.domain.Client; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Entity +@Table(name = "m_welcome_kit_dispatch") +@EntityListeners(AuditingEntityListener.class) +public class WelcomeKitDispatch extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "client_id", nullable = false) + private Client client; + + @Column(name = "delivery_address", length = 500, nullable = false) + private String deliveryAddress; + + @Column(name = "delivery_mode", length = 50, nullable = false) + private String deliveryMode; + + @Column(name = "delivery_mode_other_text", length = 255) + private String deliveryModeOtherText; + + @Column(name = "tracking_id", length = 100, nullable = false, unique = true) + private String trackingId; + + @Column(name = "status", length = 30, nullable = false) + private String status; + + @Column(name = "dispatched_on_date", nullable = false) + private LocalDate dispatchedOnDate; + + @Column(name = "delivered_on_date") + private LocalDate deliveredOnDate; + + @CreatedBy + @Column(name = "created_by", updatable = false) + private Long createdBy; + + @CreatedDate + @Column(name = "created_date", updatable = false) + private LocalDateTime createdDate; + + @LastModifiedBy + @Column(name = "last_modified_by") + private Long lastModifiedBy; + + @LastModifiedDate + @Column(name = "last_modified_date") + private LocalDateTime lastModifiedDate; + + protected WelcomeKitDispatch() { + // Default constructor for JPA + } + + private WelcomeKitDispatch(final Client client, final String deliveryAddress, final String deliveryMode, + final String deliveryModeOtherText, final String trackingId, final String status, + final LocalDate dispatchedOnDate) { + this.client = client; + this.deliveryAddress = deliveryAddress; + this.deliveryMode = deliveryMode; + this.deliveryModeOtherText = deliveryModeOtherText; + this.trackingId = trackingId; + this.status = status; + this.dispatchedOnDate = dispatchedOnDate; + } + + public static WelcomeKitDispatch createNew(final Client client, final String deliveryAddress, final String deliveryMode, + final String deliveryModeOtherText, final String trackingId, final LocalDate dispatchedOnDate) { + return new WelcomeKitDispatch(client, deliveryAddress, deliveryMode, deliveryModeOtherText, trackingId, "DISPATCHED", dispatchedOnDate); + } + + public Client getClient() { + return this.client; + } + + public String getStatus() { + return this.status; + } + + public void updateStatus(final String newStatus, final LocalDate deliveredOnDate) { + this.status = newStatus; + if ("DELIVERED".equals(newStatus)) { + this.deliveredOnDate = deliveredOnDate; + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/domain/WelcomeKitDispatchRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/domain/WelcomeKitDispatchRepository.java new file mode 100644 index 000000000..d16b4a2d7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/domain/WelcomeKitDispatchRepository.java @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.welcomekit.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface WelcomeKitDispatchRepository extends JpaRepository, JpaSpecificationExecutor { + + List findByClientIdOrderByCreatedDateDesc(Long clientId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitReadPlatformService.java new file mode 100644 index 000000000..c92836ad6 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitReadPlatformService.java @@ -0,0 +1,26 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.welcomekit.service; + +import org.apache.fineract.portfolio.welcomekit.data.ClientWelcomeKitData; + +public interface WelcomeKitReadPlatformService { + + ClientWelcomeKitData retrieveWelcomeKitsByClientId(Long clientId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitReadPlatformServiceImpl.java new file mode 100644 index 000000000..65cca504d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitReadPlatformServiceImpl.java @@ -0,0 +1,103 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.welcomekit.service; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.portfolio.welcomekit.data.ClientWelcomeKitData; +import org.apache.fineract.portfolio.welcomekit.data.WelcomeKitDispatchData; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class WelcomeKitReadPlatformServiceImpl implements WelcomeKitReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + + @Override + public ClientWelcomeKitData retrieveWelcomeKitsByClientId(Long clientId) { + final WelcomeKitDispatchMapper rm = new WelcomeKitDispatchMapper(); + final String sql = "SELECT " + rm.schema() + " WHERE w.client_id = ? ORDER BY w.created_date DESC"; + + List dispatches = this.jdbcTemplate.query(sql, rm, clientId); + + String clientName = this.jdbcTemplate.queryForObject("SELECT display_name FROM m_client WHERE id = ?", String.class, clientId); + + boolean canDispatchNew = true; + if (!dispatches.isEmpty()) { + WelcomeKitDispatchData latest = dispatches.get(0); + if ("RETURNED".equals(latest.getStatus())) { + canDispatchNew = true; + } else { + canDispatchNew = false; + } + } + + return ClientWelcomeKitData.builder() + .clientName(clientName) + .canDispatchNew(canDispatchNew) + .dispatches(dispatches) + .build(); + } + + private static final class WelcomeKitDispatchMapper implements RowMapper { + + public String schema() { + return " w.id as id, w.client_id as clientId, c.display_name as clientName, " + + " w.delivery_address as deliveryAddress, w.delivery_mode as deliveryMode, " + + " w.delivery_mode_other_text as deliveryModeOtherText, w.tracking_id as trackingId, " + + " w.status as status, w.dispatched_on_date as dispatchedOnDate, w.delivered_on_date as deliveredOnDate " + + " FROM m_welcome_kit_dispatch w " + + " JOIN m_client c ON c.id = w.client_id "; + } + + @Override + public WelcomeKitDispatchData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + final Long id = rs.getLong("id"); + final Long clientId = rs.getLong("clientId"); + final String clientName = rs.getString("clientName"); + final String deliveryAddress = rs.getString("deliveryAddress"); + final String deliveryMode = rs.getString("deliveryMode"); + final String deliveryModeOtherText = rs.getString("deliveryModeOtherText"); + final String trackingId = rs.getString("trackingId"); + final String status = rs.getString("status"); + final LocalDate dispatchedOnDate = JdbcSupport.getLocalDate(rs, "dispatchedOnDate"); + final LocalDate deliveredOnDate = JdbcSupport.getLocalDate(rs, "deliveredOnDate"); + + return WelcomeKitDispatchData.builder() + .id(id) + .clientId(clientId) + .clientName(clientName) + .deliveryAddress(deliveryAddress) + .deliveryMode(deliveryMode) + .deliveryModeOtherText(deliveryModeOtherText) + .trackingId(trackingId) + .status(status) + .dispatchedOnDate(dispatchedOnDate) + .deliveredOnDate(deliveredOnDate) + .build(); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitWritePlatformService.java new file mode 100644 index 000000000..3b09caa73 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitWritePlatformService.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.welcomekit.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface WelcomeKitWritePlatformService { + + CommandProcessingResult dispatchWelcomeKit(Long clientId, JsonCommand command); + + CommandProcessingResult updateWelcomeKitStatus(Long welcomeKitId, JsonCommand command); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitWritePlatformServiceImpl.java new file mode 100644 index 000000000..bc63bb6cd --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/welcomekit/service/WelcomeKitWritePlatformServiceImpl.java @@ -0,0 +1,112 @@ +/** + * 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.welcomekit.service; + +import java.time.LocalDate; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; +import org.apache.fineract.portfolio.welcomekit.domain.WelcomeKitDispatch; +import org.apache.fineract.portfolio.welcomekit.domain.WelcomeKitDispatchRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class WelcomeKitWritePlatformServiceImpl implements WelcomeKitWritePlatformService { + + private final PlatformSecurityContext context; + private final ClientRepositoryWrapper clientRepository; + private final WelcomeKitDispatchRepository welcomeKitDispatchRepository; + + @Transactional + @Override + public CommandProcessingResult dispatchWelcomeKit(final Long clientId, final JsonCommand command) { + this.context.authenticatedUser(); + + final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId); + + final String deliveryAddress = command.stringValueOfParameterNamed("deliveryAddress"); + final String deliveryMode = command.stringValueOfParameterNamed("deliveryMode"); + final String deliveryModeOtherText = command.stringValueOfParameterNamed("deliveryModeOtherText"); + final LocalDate dispatchedOnDate = command.localDateValueOfParameterNamed("dispatchedOnDate"); + + if (deliveryAddress == null || deliveryAddress.trim().isEmpty()) { + throw new PlatformDataIntegrityException("error.msg.welcomekit.deliveryAddress.cannot.be.blank", + "Delivery address cannot be blank."); + } + if (deliveryMode == null || deliveryMode.trim().isEmpty()) { + throw new PlatformDataIntegrityException("error.msg.welcomekit.deliveryMode.cannot.be.blank", + "Delivery mode cannot be blank."); + } + if (dispatchedOnDate == null) { + throw new PlatformDataIntegrityException("error.msg.welcomekit.dispatchedOnDate.cannot.be.blank", + "Dispatched on date cannot be blank."); + } + + final String trackingId = "WK-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + + final WelcomeKitDispatch newDispatch = WelcomeKitDispatch.createNew(client, deliveryAddress, deliveryMode, deliveryModeOtherText, + trackingId, dispatchedOnDate); + + this.welcomeKitDispatchRepository.saveAndFlush(newDispatch); + + return new CommandProcessingResultBuilder() + .withCommandId(command.commandId()) + .withClientId(clientId) + .withEntityId(newDispatch.getId()) + .build(); + } + + @Transactional + @Override + public CommandProcessingResult updateWelcomeKitStatus(final Long welcomeKitId, final JsonCommand command) { + this.context.authenticatedUser(); + + final WelcomeKitDispatch dispatch = this.welcomeKitDispatchRepository.findById(welcomeKitId).orElseThrow( + () -> new PlatformDataIntegrityException("error.msg.welcomekit.not.found", "Welcome kit not found.")); + + final String status = command.stringValueOfParameterNamed("status"); + + if (status == null || status.trim().isEmpty()) { + throw new PlatformDataIntegrityException("error.msg.welcomekit.status.cannot.be.blank", "Status cannot be blank."); + } + + LocalDate deliveredOnDate = null; + if ("DELIVERED".equals(status)) { + deliveredOnDate = LocalDate.now(); + } + + dispatch.updateStatus(status, deliveredOnDate); + + this.welcomeKitDispatchRepository.saveAndFlush(dispatch); + + return new CommandProcessingResultBuilder() + .withCommandId(command.commandId()) + .withClientId(dispatch.getClient().getId()) + .withEntityId(dispatch.getId()) + .build(); + } +} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index f59a3be7a..fe46285d1 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -435,4 +435,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0415_welcome_kit_dispatch.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0415_welcome_kit_dispatch.xml new file mode 100644 index 000000000..e877714fb --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0415_welcome_kit_dispatch.xml @@ -0,0 +1,38 @@ + + + + + + CREATE TABLE m_welcome_kit_dispatch ( + id BIGSERIAL PRIMARY KEY, + client_id BIGINT NOT NULL REFERENCES m_client(id), + delivery_address VARCHAR(500) NOT NULL, + delivery_mode VARCHAR(50) NOT NULL, + delivery_mode_other_text VARCHAR(255), + tracking_id VARCHAR(100) NOT NULL UNIQUE, + status VARCHAR(30) NOT NULL DEFAULT 'DISPATCHED', + dispatched_on_date DATE NOT NULL, + delivered_on_date DATE, + created_by BIGINT, + created_date TIMESTAMP NOT NULL DEFAULT NOW(), + last_modified_by BIGINT, + last_modified_date TIMESTAMP + ); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'READ_WELCOMEKIT', 'WELCOMEKIT', 'READ', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_WELCOMEKIT'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'CREATE_WELCOMEKIT', 'WELCOMEKIT', 'CREATE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_WELCOMEKIT'); + + INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker) + SELECT 'portfolio', 'UPDATE_WELCOMEKIT', 'WELCOMEKIT', 'UPDATE', false + WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'UPDATE_WELCOMEKIT'); + + + +