Subversion Repositories SmartDukaan

Rev

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;

    @Autowired
    private NotificationService notificationService;

    @Autowired
    private MailOutboxService mailOutboxService;

    @Autowired
    private AuthRepository authRepository;

    @Autowired
    private 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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
    }

    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;
    }
}