| 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("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
|
|
|
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 |
}
|