Blame | Last modification | View Log | RSS feed
package com.spice.profitmandi.service.user;import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;import com.spice.profitmandi.common.model.CustomRetailer;import com.spice.profitmandi.common.model.ProfitMandiConstants;import com.spice.profitmandi.common.util.Utils;import com.spice.profitmandi.dao.entity.auth.AuthUser;import com.spice.profitmandi.dao.entity.cs.Position;import com.spice.profitmandi.dao.entity.dtr.Document;import com.spice.profitmandi.dao.entity.fofo.FofoStore;import com.spice.profitmandi.dao.entity.fofo.StoreClosure;import com.spice.profitmandi.dao.enumuration.cs.EscalationType;import com.spice.profitmandi.dao.enumuration.fofo.StoreClosureReason;import com.spice.profitmandi.dao.repository.auth.AuthRepository;import com.spice.profitmandi.dao.repository.cs.CsService;import com.spice.profitmandi.dao.repository.cs.PositionRepository;import com.spice.profitmandi.dao.repository.dtr.DocumentRepository;import com.spice.profitmandi.dao.repository.dtr.FofoStoreRepository;import com.spice.profitmandi.dao.repository.fofo.StoreClosureRepository;import com.spice.profitmandi.service.mail.MailOutboxService;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.core.io.FileSystemResource;import org.springframework.stereotype.Service;import java.io.File;import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;import java.util.*;import java.util.stream.Collectors;/*** Permanent partner store closure. A closure needs a reason, a remark and the approval mail* (uploaded via /document-upload), and queues a closure report with the approval attached to the partner's* Sales L2, the top three Sales levels, the top Accounts level and the closer.*/@Servicepublic class StoreClosureService {private static final Logger LOGGER = LogManager.getLogger(StoreClosureService.class);private static final DateTimeFormatter REPORT_TIME_FORMAT = DateTimeFormatter.ofPattern("dd-MMM-yyyy hh:mm a");@Autowiredprivate FofoStoreRepository fofoStoreRepository;@Autowiredprivate StoreClosureRepository storeClosureRepository;@Autowiredprivate DocumentRepository documentRepository;@Autowiredprivate PositionRepository positionRepository;@Autowiredprivate AuthRepository authRepository;@Autowiredprivate CsService csService;@Autowiredprivate RetailerService retailerService;@Autowiredprivate MailOutboxService mailOutboxService;public void closeStore(int fofoId, String storeCode, StoreClosureReason reason, String remark,int approvalDocumentId, String closedBy) throws ProfitMandiBusinessException {StoreAccess.check(StoreAccess.CLOSE, closedBy, "close store");FofoStore fofoStore = fofoStoreRepository.selectByRetailerId(fofoId);if (fofoStore.isClosed()) {throw new ProfitMandiBusinessException("Store Closed", "Store Already Closed","This store is already permanently closed");}if (storeCode == null || !storeCode.equals(fofoStore.getCode())) {throw new ProfitMandiBusinessException("Invalid Store Code", "Store Code Mismatch","Please type the correct store code to confirm closure");}if (reason == null) {throw new ProfitMandiBusinessException("Closure Reason", "Reason Missing","Please select the reason for closing the store");}if (remark == null || remark.trim().isEmpty()) {throw new ProfitMandiBusinessException("Closure Remark", "Remark Missing","Please describe why the store is being closed");}if (remark.trim().length() > 1000) {throw new ProfitMandiBusinessException("Closure Remark", "Remark Too Long","Please keep the remark within 1000 characters");}Document approvalDocument = approvalDocumentId > 0 ? documentRepository.selectById(approvalDocumentId) : null;if (approvalDocument == null) {throw new ProfitMandiBusinessException("Approval Mail", "Approval Missing","Please attach the approval mail for this closure");}File approvalFile = new File(approvalDocument.getPath(), approvalDocument.getName());if (!approvalFile.isFile()) {throw new ProfitMandiBusinessException("Approval Mail", "Approval File Missing","The uploaded approval mail could not be found, please upload it again");}LocalDateTime now = LocalDateTime.now();fofoStore.setActive(false);fofoStore.setClosed(true);fofoStore.setClosedTimestamp(now);StoreClosure storeClosure = new StoreClosure();storeClosure.setFofoId(fofoId);storeClosure.setReason(reason);storeClosure.setRemark(remark.trim());storeClosure.setApprovalDocumentId(approvalDocumentId);storeClosure.setClosedBy(closedBy);storeClosure.setCreateTimestamp(now);storeClosureRepository.persist(storeClosure);LOGGER.info("Store {} closed permanently by {} - {}", fofoStore.getCode(), closedBy, storeClosure);sendClosureReport(fofoStore, storeClosure, approvalDocument, approvalFile);}private void sendClosureReport(FofoStore fofoStore, StoreClosure storeClosure, Document approvalDocument,File approvalFile) throws ProfitMandiBusinessException {CustomRetailer retailer = retailerService.getFofoRetailer(fofoStore.getId());List<AuthUser> salesL2 = csService.getAuthUsersByPartnerIdAndCategory(fofoStore.getId(),ProfitMandiConstants.TICKET_CATEGORY_SALES, EscalationType.L2);Set<String> recipients = new LinkedHashSet<>();salesL2.stream().map(AuthUser::getEmailId).filter(Objects::nonNull).forEach(recipients::add);recipients.addAll(topLevelEmails(ProfitMandiConstants.TICKET_CATEGORY_SALES, 3));recipients.addAll(topLevelEmails(ProfitMandiConstants.TICKET_CATEGORY_ACCOUNTS, 1));recipients.add(storeClosure.getClosedBy());String businessName = retailer.getBusinessName();String city = retailer.getAddress() == null ? "-" : retailer.getAddress().getCity() + ", " + retailer.getAddress().getState();String subject = "Store Closure Report - " + fofoStore.getCode() + " - " + businessName;StringBuilder body = new StringBuilder();body.append("Dear Team,\n\nThe following partner store has been permanently closed.\n\n");body.append("Partner : ").append(businessName).append(" (Partner Id ").append(fofoStore.getId()).append(")\n");body.append("Store Code : ").append(fofoStore.getCode()).append("\n");body.append("GSTIN : ").append(Optional.ofNullable(retailer.getGstNumber()).orElse("-")).append("\n");body.append("Location : ").append(city).append("\n");body.append("Sales L2 : ").append(salesL2.isEmpty() ? "-" :salesL2.stream().map(AuthUser::getFullName).collect(Collectors.joining(", "))).append("\n");body.append("Closed By : ").append(storeClosure.getClosedBy()).append("\n");body.append("Closed On : ").append(storeClosure.getCreateTimestamp().format(REPORT_TIME_FORMAT)).append("\n");body.append("Reason : ").append(storeClosure.getReason().getLabel()).append("\n");body.append("Remark : ").append(storeClosure.getRemark()).append("\n\n");body.append("The approval mail for this closure is attached.\n\nRegards,\nSmartDukaan");String attachmentName = approvalDocument.getDisplayName() != null ? approvalDocument.getDisplayName() : approvalDocument.getName();mailOutboxService.queueMailWithAttachments(recipients.toArray(new String[0]), null, subject, body.toString(),"StoreClosureService.sendClosureReport", new Utils.Attachment(attachmentName, new FileSystemResource(approvalFile)));}/*** Emails of active auth users holding the highest {@code levels} distinct escalation levels held by active users* in a position category, resolved live so role changes in cs.position flow through without a code change.*/private Set<String> topLevelEmails(int categoryId, int levels) {List<Position> positions = positionRepository.selectPositionByCategoryId(categoryId).stream().filter(x -> x.getAuthUserId() > 0 && x.getEscalationType() != null).collect(Collectors.toList());if (positions.isEmpty()) {return new HashSet<>();}Map<Integer, AuthUser> activeUsers = authRepository.selectByIds(positions.stream().map(Position::getAuthUserId).distinct().collect(Collectors.toList())).stream().filter(x -> Boolean.TRUE.equals(x.getActive())).collect(Collectors.toMap(AuthUser::getId, x -> x));List<Position> activePositions = positions.stream().filter(x -> activeUsers.containsKey(x.getAuthUserId())).collect(Collectors.toList());List<EscalationType> topLevels = activePositions.stream().map(Position::getEscalationType).distinct().sorted(Comparator.reverseOrder()).limit(levels).collect(Collectors.toList());return activePositions.stream().filter(x -> topLevels.contains(x.getEscalationType())).map(x -> activeUsers.get(x.getAuthUserId()).getEmailId()).filter(Objects::nonNull).map(String::toLowerCase).collect(Collectors.toSet());}}