Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
37391 vikas 1
package com.spice.profitmandi.web.services;
2
 
3
import com.spice.profitmandi.common.enumuration.MessageType;
4
import com.spice.profitmandi.common.model.SendNotificationModel;
5
import com.spice.profitmandi.dao.entity.auth.AuthUser;
6
import com.spice.profitmandi.dao.entity.dtr.User;
7
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
8
import com.spice.profitmandi.dao.repository.dtr.UserRepository;
9
import com.spice.profitmandi.service.NotificationService;
10
import com.spice.profitmandi.service.mail.MailOutboxService;
11
import org.apache.logging.log4j.LogManager;
12
import org.apache.logging.log4j.Logger;
13
import org.springframework.beans.factory.annotation.Autowired;
14
import org.springframework.stereotype.Service;
15
import org.springframework.transaction.annotation.Propagation;
16
import org.springframework.transaction.annotation.Transactional;
17
 
18
import java.time.LocalDateTime;
19
import java.util.Arrays;
20
import java.util.List;
21
 
22
/**
23
 * Alerts the reporting manager when a checkout recycles one remark across agendas.
24
 *
25
 * REQUIRES_NEW on purpose: this runs mid-checkout, and the campaign/push rows it
26
 * writes must not be able to poison the checkout's own transaction. The alert gets
27
 * its own session — if it fails, the checkout still commits.
28
 *
29
 * Delivery is the existing queue: {@link NotificationService#sendNotification} writes
30
 * notification_campaign + pushnotifications rows, and the cron's FCM sweep
31
 * (ScheduledTasks.sendNotification) pushes them. Nothing here talks to FCM, so a
32
 * checkout never waits on Google.
33
 */
34
@Service
35
@Transactional(propagation = Propagation.REQUIRES_NEW)
36
public class RemarkAlertService {
37
 
38
    private static final Logger LOGGER = LogManager.getLogger(RemarkAlertService.class);
39
 
40
    /** Where the push lands when tapped — same target the other team alerts use. */
41
    private static final String NOTIFICATION_URL = "https://app.smartdukaan.com/pages/home/notifications";
42
 
43
    /** Pairs named in the push body before it collapses to "+N more". */
44
    private static final int PAIRS_IN_MESSAGE = 2;
45
 
46
    @Autowired
47
    private NotificationService notificationService;
48
 
49
    @Autowired
50
    private MailOutboxService mailOutboxService;
51
 
52
    @Autowired
53
    private AuthRepository authRepository;
54
 
55
    @Autowired
56
    private UserRepository userRepository;
57
 
58
    /**
59
     * @param trackingId     location_tracking.id of the checkout
60
     * @param asmUserId      location_tracking.user_id — the rep who filed the remarks
61
     * @param taskName       the visit's task_name ("<agendas> | <store>")
62
     * @param duplicatePairs human-readable agenda pairs, e.g. "Credit dues + Low purchase (0.75)"
63
     */
64
    public void notifyDuplicateRemarks(int trackingId, int asmUserId, String taskName,
65
                                       List<String> duplicatePairs) {
66
        if (duplicatePairs == null || duplicatePairs.isEmpty()) return;
67
        try {
68
            User asm = userRepository.selectById(asmUserId);
69
            if (asm == null) {
70
                LOGGER.warn("Duplicate-remark alert skipped: no dtr user {} (tracking id={})", asmUserId, trackingId);
71
                return;
72
            }
73
            AuthUser manager = resolveManager(asm);
74
            if (manager == null) {
75
                LOGGER.warn("Duplicate-remark alert skipped: no reporting manager for user {} (tracking id={})",
76
                        asmUserId, trackingId);
77
                return;
78
            }
79
 
80
            String store = storeName(taskName);
81
            pushToManager(asm, manager, store, trackingId, duplicatePairs);
82
            emailToManager(asm, manager, store, trackingId, duplicatePairs);
83
        } catch (Exception e) {
84
            // An alert is never worth failing a checkout for.
85
            LOGGER.warn("Duplicate-remark alert failed for tracking id={}: {}", trackingId, e.toString());
86
        }
87
    }
88
 
89
    /** In-app push, delivered by the cron's FCM sweep. */
90
    private void pushToManager(User asm, AuthUser manager, String store, int trackingId, List<String> pairs) {
91
        try {
92
            Integer managerUserId = managerDtrUserId(manager);
93
            if (managerUserId == null) {
94
                LOGGER.warn("Duplicate-remark push skipped: manager {} has no dtr user (tracking id={})",
95
                        manager.getId(), trackingId);
96
                return;
97
            }
98
            if (managerUserId == asm.getId()) {
99
                LOGGER.warn("Duplicate-remark push skipped: user {} reports to themselves", asm.getId());
100
                return;
101
            }
102
            SendNotificationModel model = new SendNotificationModel();
103
            model.setCampaignName("Duplicate visit remarks");
104
            model.setTitle("Duplicate remarks — " + store);
105
            model.setMessage(buildMessage(asm, store, pairs));
106
            model.setType("url");
107
            model.setUrl(NOTIFICATION_URL);
108
            model.setMessageType(MessageType.notification);
109
            model.setExpiresat(LocalDateTime.now().plusDays(2));
110
            model.setUserIds(Arrays.asList(managerUserId));
111
            notificationService.sendNotification(model);
112
            LOGGER.info("Duplicate-remark push queued for manager user {} (rep {}, tracking id={}, {} pair(s))",
113
                    managerUserId, asm.getId(), trackingId, pairs.size());
114
        } catch (Exception e) {
115
            LOGGER.warn("Duplicate-remark push failed for tracking id={}: {}", trackingId, e.toString());
116
        }
117
    }
118
 
119
    /**
120
     * Mail to the manager, queued through MailOutboxService rather than sent inline —
121
     * a checkout must not wait on SMTP, and the outbox redirects to the dev recipient
122
     * off prod so testing cannot mail a real manager.
123
     */
124
    private void emailToManager(User asm, AuthUser manager, String store, int trackingId, List<String> pairs) {
125
        try {
126
            String managerEmail = managerEmail(manager);
127
            if (managerEmail == null) {
128
                LOGGER.warn("Duplicate-remark email skipped: no address for manager {} (tracking id={})",
129
                        manager.getId(), trackingId);
130
                return;
131
            }
132
            String subject = "Duplicate visit remarks: " + repName(asm) + " (" + store + ")";
133
            mailOutboxService.queueMail(
134
                    new String[]{managerEmail},
135
                    null,
136
                    new String[]{"sdtech@smartdukaan.com"},
137
                    subject,
138
                    buildManagerEmailBody(asm, store, trackingId, pairs),
139
                    true,
140
                    "RemarkAlertService.notifyDuplicateRemarks");
141
            LOGGER.info("Duplicate-remark email queued to {} (rep {}, tracking id={})",
142
                    managerEmail, asm.getId(), trackingId);
143
        } catch (Exception e) {
144
            LOGGER.warn("Duplicate-remark email failed for tracking id={}: {}", trackingId, e.toString());
145
        }
146
    }
147
 
148
    private String buildManagerEmailBody(User asm, String store, int trackingId, List<String> pairs) {
149
        StringBuilder html = new StringBuilder();
150
        html.append("<p>Hello,</p>");
151
        html.append("<p><strong>").append(escape(repName(asm))).append("</strong>");
152
        if (asm.getEmailId() != null) html.append(" (").append(escape(asm.getEmailId())).append(")");
153
        html.append(" filed the same remark against different agendas while checking out of <strong>")
154
                .append(escape(store)).append("</strong>.</p>");
155
        html.append("<p>Agenda pairs whose remarks overlap:</p><ul>");
156
        for (String pair : pairs) {
157
            html.append("<li>").append(escape(pair)).append("</li>");
158
        }
159
        html.append("</ul>");
160
        html.append("<p>Overlap is measured on the remarks' content words; 1.0 means identical text. ")
161
                .append("Each agenda is meant to carry its own discussion, so please confirm what was ")
162
                .append("actually covered at this visit.</p>");
163
        html.append("<p style=\"color:#777\">Visit reference: location_tracking #").append(trackingId).append("</p>");
164
        return html.toString();
165
    }
166
 
167
    /**
168
     * The rep's reporting manager. The reporting line lives on auth.auth_user
169
     * (manager_id) while the app addresses people as dtr.users, so this hops
170
     * dtr user → auth_user (by email, active only) → manager_id. Null at any break.
171
     */
172
    private AuthUser resolveManager(User asm) {
173
        String asmEmail = asm.getEmailId();
174
        if (asmEmail == null || asmEmail.trim().isEmpty()) return null;
175
 
176
        AuthUser asmAuth = authRepository.selectByGmailId(asmEmail);
177
        if (asmAuth == null || asmAuth.getManagerId() <= 0) return null;
178
 
179
        return authRepository.selectById(asmAuth.getManagerId());
180
    }
181
 
182
    /**
183
     * email_id first: that is the column AuthRepository.selectByGmailId actually
184
     * matches on (despite the name) and the one that lines up with dtr.users.email.
185
     * The two differ on a minority of auth_user rows, so keep gmail_id as fallback.
186
     */
187
    private String managerEmail(AuthUser manager) {
188
        String email = manager.getEmailId() != null && !manager.getEmailId().trim().isEmpty()
189
                ? manager.getEmailId()
190
                : manager.getGmailId();
191
        return (email == null || email.trim().isEmpty()) ? null : email.trim();
192
    }
193
 
194
    /** Push notifications address dtr.users, so the manager needs a row there too. */
195
    private Integer managerDtrUserId(AuthUser manager) throws Exception {
196
        String email = managerEmail(manager);
197
        if (email == null) return null;
198
        User managerUser = userRepository.selectByEmailId(email);
199
        return managerUser != null ? managerUser.getId() : null;
200
    }
201
 
202
    /** Remarks and store names are user-typed and land in an HTML mail. */
203
    static String escape(String value) {
204
        if (value == null) return "";
205
        return value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
206
    }
207
 
208
    private String buildMessage(User asm, String store, List<String> pairs) {
209
        StringBuilder message = new StringBuilder();
210
        message.append(repName(asm)).append(" filed the same remark for different agendas at ").append(store);
211
        message.append(" — ");
212
        int named = Math.min(PAIRS_IN_MESSAGE, pairs.size());
213
        for (int i = 0; i < named; i++) {
214
            if (i > 0) message.append("; ");
215
            message.append(pairs.get(i));
216
        }
217
        if (pairs.size() > named) {
218
            message.append(" +").append(pairs.size() - named).append(" more");
219
        }
220
        message.append(". Please review.");
221
        return message.toString();
222
    }
223
 
224
    private String repName(User asm) {
225
        String first = asm.getFirstName() != null ? asm.getFirstName().trim() : "";
226
        String last = asm.getLastName() != null ? asm.getLastName().trim() : "";
227
        String name = (first + " " + last).trim();
228
        return name.isEmpty() ? ("User " + asm.getId()) : name;
229
    }
230
 
231
    /** Store is everything after the first " | " of task_name. */
232
    private String storeName(String taskName) {
233
        if (taskName == null) return "a partner store";
234
        int sep = taskName.indexOf(" | ");
235
        if (sep < 0) return taskName.trim().isEmpty() ? "a partner store" : taskName.trim();
236
        String store = taskName.substring(sep + 3).trim();
237
        // task_name can carry a trailing marker ("… | Store | SELF").
238
        int next = store.indexOf(" | ");
239
        if (next > -1) store = store.substring(0, next).trim();
240
        return store.isEmpty() ? "a partner store" : store;
241
    }
242
}