Blame | Last modification | View Log | RSS feed
package com.spice.profitmandi.web.services;import com.spice.profitmandi.common.enumuration.MessageType;import com.spice.profitmandi.common.model.SendNotificationModel;import com.spice.profitmandi.dao.entity.auth.AuthUser;import com.spice.profitmandi.dao.entity.dtr.User;import com.spice.profitmandi.dao.repository.auth.AuthRepository;import com.spice.profitmandi.dao.repository.dtr.UserRepository;import com.spice.profitmandi.service.NotificationService;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.stereotype.Service;import org.springframework.transaction.annotation.Propagation;import org.springframework.transaction.annotation.Transactional;import java.time.LocalDateTime;import java.util.Arrays;import java.util.List;/*** Alerts the reporting manager when a checkout recycles one remark across agendas.** REQUIRES_NEW on purpose: this runs mid-checkout, and the campaign/push rows it* writes must not be able to poison the checkout's own transaction. The alert gets* its own session — if it fails, the checkout still commits.** Delivery is the existing queue: {@link NotificationService#sendNotification} writes* notification_campaign + pushnotifications rows, and the cron's FCM sweep* (ScheduledTasks.sendNotification) pushes them. Nothing here talks to FCM, so a* checkout never waits on Google.*/@Service@Transactional(propagation = Propagation.REQUIRES_NEW)public class RemarkAlertService {private static final Logger LOGGER = LogManager.getLogger(RemarkAlertService.class);/** Where the push lands when tapped — same target the other team alerts use. */private static final String NOTIFICATION_URL = "https://app.smartdukaan.com/pages/home/notifications";/** Pairs named in the push body before it collapses to "+N more". */private static final int PAIRS_IN_MESSAGE = 2;@Autowiredprivate NotificationService notificationService;@Autowiredprivate MailOutboxService mailOutboxService;@Autowiredprivate AuthRepository authRepository;@Autowiredprivate UserRepository userRepository;/*** @param trackingId location_tracking.id of the checkout* @param asmUserId location_tracking.user_id — the rep who filed the remarks* @param taskName the visit's task_name ("<agendas> | <store>")* @param duplicatePairs human-readable agenda pairs, e.g. "Credit dues + Low purchase (0.75)"*/public void notifyDuplicateRemarks(int trackingId, int asmUserId, String taskName,List<String> duplicatePairs) {if (duplicatePairs == null || duplicatePairs.isEmpty()) return;try {User asm = userRepository.selectById(asmUserId);if (asm == null) {LOGGER.warn("Duplicate-remark alert skipped: no dtr user {} (tracking id={})", asmUserId, trackingId);return;}AuthUser manager = resolveManager(asm);if (manager == null) {LOGGER.warn("Duplicate-remark alert skipped: no reporting manager for user {} (tracking id={})",asmUserId, trackingId);return;}String store = storeName(taskName);pushToManager(asm, manager, store, trackingId, duplicatePairs);emailToManager(asm, manager, store, trackingId, duplicatePairs);} catch (Exception e) {// An alert is never worth failing a checkout for.LOGGER.warn("Duplicate-remark alert failed for tracking id={}: {}", trackingId, e.toString());}}/** In-app push, delivered by the cron's FCM sweep. */private void pushToManager(User asm, AuthUser manager, String store, int trackingId, List<String> pairs) {try {Integer managerUserId = managerDtrUserId(manager);if (managerUserId == null) {LOGGER.warn("Duplicate-remark push skipped: manager {} has no dtr user (tracking id={})",manager.getId(), trackingId);return;}if (managerUserId == asm.getId()) {LOGGER.warn("Duplicate-remark push skipped: user {} reports to themselves", asm.getId());return;}SendNotificationModel model = new SendNotificationModel();model.setCampaignName("Duplicate visit remarks");model.setTitle("Duplicate remarks — " + store);model.setMessage(buildMessage(asm, store, pairs));model.setType("url");model.setUrl(NOTIFICATION_URL);model.setMessageType(MessageType.notification);model.setExpiresat(LocalDateTime.now().plusDays(2));model.setUserIds(Arrays.asList(managerUserId));notificationService.sendNotification(model);LOGGER.info("Duplicate-remark push queued for manager user {} (rep {}, tracking id={}, {} pair(s))",managerUserId, asm.getId(), trackingId, pairs.size());} catch (Exception e) {LOGGER.warn("Duplicate-remark push failed for tracking id={}: {}", trackingId, e.toString());}}/*** Mail to the manager, queued through MailOutboxService rather than sent inline —* a checkout must not wait on SMTP, and the outbox redirects to the dev recipient* off prod so testing cannot mail a real manager.*/private void emailToManager(User asm, AuthUser manager, String store, int trackingId, List<String> pairs) {try {String managerEmail = managerEmail(manager);if (managerEmail == null) {LOGGER.warn("Duplicate-remark email skipped: no address for manager {} (tracking id={})",manager.getId(), trackingId);return;}String subject = "Duplicate visit remarks: " + repName(asm) + " (" + store + ")";mailOutboxService.queueMail(new String[]{managerEmail},null,new String[]{"sdtech@smartdukaan.com"},subject,buildManagerEmailBody(asm, store, trackingId, pairs),true,"RemarkAlertService.notifyDuplicateRemarks");LOGGER.info("Duplicate-remark email queued to {} (rep {}, tracking id={})",managerEmail, asm.getId(), trackingId);} catch (Exception e) {LOGGER.warn("Duplicate-remark email failed for tracking id={}: {}", trackingId, e.toString());}}private String buildManagerEmailBody(User asm, String store, int trackingId, List<String> pairs) {StringBuilder html = new StringBuilder();html.append("<p>Hello,</p>");html.append("<p><strong>").append(escape(repName(asm))).append("</strong>");if (asm.getEmailId() != null) html.append(" (").append(escape(asm.getEmailId())).append(")");html.append(" filed the same remark against different agendas while checking out of <strong>").append(escape(store)).append("</strong>.</p>");html.append("<p>Agenda pairs whose remarks overlap:</p><ul>");for (String pair : pairs) {html.append("<li>").append(escape(pair)).append("</li>");}html.append("</ul>");html.append("<p>Overlap is measured on the remarks' content words; 1.0 means identical text. ").append("Each agenda is meant to carry its own discussion, so please confirm what was ").append("actually covered at this visit.</p>");html.append("<p style=\"color:#777\">Visit reference: location_tracking #").append(trackingId).append("</p>");return html.toString();}/*** The rep's reporting manager. The reporting line lives on auth.auth_user* (manager_id) while the app addresses people as dtr.users, so this hops* dtr user → auth_user (by email, active only) → manager_id. Null at any break.*/private AuthUser resolveManager(User asm) {String asmEmail = asm.getEmailId();if (asmEmail == null || asmEmail.trim().isEmpty()) return null;AuthUser asmAuth = authRepository.selectByGmailId(asmEmail);if (asmAuth == null || asmAuth.getManagerId() <= 0) return null;return authRepository.selectById(asmAuth.getManagerId());}/*** email_id first: that is the column AuthRepository.selectByGmailId actually* matches on (despite the name) and the one that lines up with dtr.users.email.* The two differ on a minority of auth_user rows, so keep gmail_id as fallback.*/private String managerEmail(AuthUser manager) {String email = manager.getEmailId() != null && !manager.getEmailId().trim().isEmpty()? manager.getEmailId(): manager.getGmailId();return (email == null || email.trim().isEmpty()) ? null : email.trim();}/** Push notifications address dtr.users, so the manager needs a row there too. */private Integer managerDtrUserId(AuthUser manager) throws Exception {String email = managerEmail(manager);if (email == null) return null;User managerUser = userRepository.selectByEmailId(email);return managerUser != null ? managerUser.getId() : null;}/** Remarks and store names are user-typed and land in an HTML mail. */static String escape(String value) {if (value == null) return "";return value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);}private String buildMessage(User asm, String store, List<String> pairs) {StringBuilder message = new StringBuilder();message.append(repName(asm)).append(" filed the same remark for different agendas at ").append(store);message.append(" — ");int named = Math.min(PAIRS_IN_MESSAGE, pairs.size());for (int i = 0; i < named; i++) {if (i > 0) message.append("; ");message.append(pairs.get(i));}if (pairs.size() > named) {message.append(" +").append(pairs.size() - named).append(" more");}message.append(". Please review.");return message.toString();}private String repName(User asm) {String first = asm.getFirstName() != null ? asm.getFirstName().trim() : "";String last = asm.getLastName() != null ? asm.getLastName().trim() : "";String name = (first + " " + last).trim();return name.isEmpty() ? ("User " + asm.getId()) : name;}/** Store is everything after the first " | " of task_name. */private String storeName(String taskName) {if (taskName == null) return "a partner store";int sep = taskName.indexOf(" | ");if (sep < 0) return taskName.trim().isEmpty() ? "a partner store" : taskName.trim();String store = taskName.substring(sep + 3).trim();// task_name can carry a trailing marker ("… | Store | SELF").int next = store.indexOf(" | ");if (next > -1) store = store.substring(0, next).trim();return store.isEmpty() ? "a partner store" : store;}}