Add Welcome Kit Dispatch feature and Share Account summary API

This commit is contained in:
Anshul Rathore
2026-07-31 11:51:57 +05:30
parent ab5666c0f4
commit 67810bfaea
16 changed files with 852 additions and 0 deletions
@@ -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<ShareAccountClientSummaryData> 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<ShareAccountClientSummaryData> 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;
}
}
@@ -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;
}
}
@@ -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<ShareAccountData> retrieveAllShareAccountDataForDividends(Long productId, boolean fetchInActiveAccounts,
LocalDate startDate);
Collection<ShareAccountClientSummaryData> retrieveAllShareAccountClientSummaries(String searchTerm);
String retrieveAccountNumberByAccountId(Long accountId);
}
@@ -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<ShareAccountClientSummaryData> 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<Object> 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<ShareAccountClientSummaryData> 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<ShareAccountChargeData> convertChargesToShareAccountCharges(Collection<ChargeData> productCharges) {
final Collection<ShareAccountChargeData> savingsCharges = new ArrayList<>();
for (final ChargeData chargeData : productCharges) {
@@ -491,6 +515,63 @@ public class ShareAccountReadPlatformServiceImpl implements ShareAccountReadPlat
}
}
private static final class ShareAccountClientSummaryMapper implements RowMapper<ShareAccountClientSummaryData> {
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 {
@@ -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);
}
}
@@ -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);
}
}
@@ -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<WelcomeKitDispatchData> dispatches;
}
@@ -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;
}
@@ -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<Long> {
@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;
}
}
}
@@ -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<WelcomeKitDispatch, Long>, JpaSpecificationExecutor<WelcomeKitDispatch> {
List<WelcomeKitDispatch> findByClientIdOrderByCreatedDateDesc(Long clientId);
}
@@ -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);
}
@@ -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<WelcomeKitDispatchData> 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<WelcomeKitDispatchData> {
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();
}
}
}
@@ -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);
}
@@ -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();
}
}
@@ -435,4 +435,5 @@
<include file="parts/0412_activate_outward_clearing_batch_job.xml" relativeToChangelogFile="true"/>
<include file="parts/0413_inward_item_verify_signature_permission.xml" relativeToChangelogFile="true"/>
<include file="parts/0414_inward_item_full_source_details.xml" relativeToChangelogFile="true"/>
<include file="parts/0415_welcome_kit_dispatch.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="0415_welcome_kit_dispatch" author="CBS" runOnChange="false">
<sql>
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');
</sql>
</changeSet>
</databaseChangeLog>