| 37759 |
vikas |
1 |
package com.spice.profitmandi.web.controller;
|
|
|
2 |
|
|
|
3 |
import com.spice.profitmandi.common.model.UserInfo;
|
|
|
4 |
import com.spice.profitmandi.common.web.util.ResponseSender;
|
|
|
5 |
import com.spice.profitmandi.dao.entity.auth.AuthUser;
|
|
|
6 |
import com.spice.profitmandi.dao.entity.user.Lead;
|
|
|
7 |
import com.spice.profitmandi.dao.model.lms.LeadDispositionRequest;
|
|
|
8 |
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
|
|
|
9 |
import com.spice.profitmandi.service.lms.LmsCallService;
|
|
|
10 |
import com.spice.profitmandi.service.lms.LmsDispositionService;
|
|
|
11 |
import com.spice.profitmandi.service.lms.LmsOutcome;
|
|
|
12 |
import org.apache.logging.log4j.LogManager;
|
|
|
13 |
import org.apache.logging.log4j.Logger;
|
|
|
14 |
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
15 |
import org.springframework.http.HttpStatus;
|
|
|
16 |
import org.springframework.http.MediaType;
|
|
|
17 |
import org.springframework.http.ResponseEntity;
|
|
|
18 |
import org.springframework.stereotype.Controller;
|
|
|
19 |
import org.springframework.transaction.annotation.Transactional;
|
|
|
20 |
import org.springframework.web.bind.annotation.RequestBody;
|
|
|
21 |
import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
22 |
import org.springframework.web.bind.annotation.RequestMethod;
|
|
|
23 |
import org.springframework.web.bind.annotation.RequestParam;
|
|
|
24 |
import org.springframework.web.bind.annotation.ResponseBody;
|
|
|
25 |
|
|
|
26 |
import javax.servlet.http.HttpServletRequest;
|
|
|
27 |
|
|
|
28 |
/**
|
|
|
29 |
* The field app's LMS dialer: place a recorded call to a lead from a beat stop, watch it, end it,
|
|
|
30 |
* and record what came of it.
|
|
|
31 |
*
|
|
|
32 |
* <p>Same calls as the desk console's {@code /lms/dialer/*}, and deliberately the same services
|
|
|
33 |
* underneath ({@link LmsCallService}, {@link LmsDispositionService}) so a rep's call is
|
|
|
34 |
* indistinguishable from a BGC agent's in every LMS report — same {@code user.lead_call} row, same
|
|
|
35 |
* recording archived to our own bucket, same first-contact SLA stamp, same trail entry. Only the
|
|
|
36 |
* identity step differs: the console reads a session cookie, this reads the app's {@code Auth-Token}.
|
|
|
37 |
*
|
|
|
38 |
* <p><b>Not under {@code /clickToCall}.</b> That prefix is excluded from {@code
|
|
|
39 |
* SimpleCORSInterceptor} in {@code WebMVCConfig} for the legacy Knowlarity endpoints, and a new
|
|
|
40 |
* path starting with it would silently inherit the exclusion.
|
|
|
41 |
*/
|
|
|
42 |
@Controller
|
|
|
43 |
@Transactional(rollbackFor = Throwable.class)
|
|
|
44 |
public class LmsAppCallController {
|
|
|
45 |
|
|
|
46 |
private static final Logger LOGGER = LogManager.getLogger(LmsAppCallController.class);
|
|
|
47 |
|
|
|
48 |
@Autowired
|
|
|
49 |
private LmsCallService lmsCallService;
|
|
|
50 |
|
|
|
51 |
@Autowired
|
|
|
52 |
private LmsDispositionService lmsDispositionService;
|
|
|
53 |
|
|
|
54 |
@Autowired
|
|
|
55 |
private AuthRepository authRepository;
|
|
|
56 |
|
|
|
57 |
@Autowired
|
|
|
58 |
private ResponseSender<?> responseSender;
|
|
|
59 |
|
|
|
60 |
/** Gate a dial before it happens: DND and TRAI calling hours. */
|
|
|
61 |
@RequestMapping(value = "/lms/call/precheck", method = RequestMethod.POST)
|
|
|
62 |
@ResponseBody
|
|
|
63 |
public ResponseEntity<?> precheck(HttpServletRequest request,
|
|
|
64 |
@RequestParam(name = "leadId") int leadId) {
|
|
|
65 |
AuthUser agent = currentUser(request);
|
|
|
66 |
if (agent == null) {
|
|
|
67 |
return unauthorized();
|
|
|
68 |
}
|
|
|
69 |
return renderAllowingRefusal(lmsCallService.precheck(leadId));
|
|
|
70 |
}
|
|
|
71 |
|
|
|
72 |
/**
|
|
|
73 |
* Place the call. The rep's own VBC endpoint rings first, then VBC dials the retailer.
|
|
|
74 |
*
|
|
|
75 |
* <p>{@code leadId} is a query parameter rather than a JSON body on purpose: {@code
|
|
|
76 |
* PostInterceptor} de-duplicates POSTs for 300 seconds by hashing the body when no
|
|
|
77 |
* {@code IdempotencyKey} is present, and two identical bodies would make a rep's second attempt
|
|
|
78 |
* at the same lead fail with "Duplicate request." instead of dialling.
|
|
|
79 |
*/
|
|
|
80 |
@RequestMapping(value = "/lms/call/dial", method = RequestMethod.POST)
|
|
|
81 |
@ResponseBody
|
|
|
82 |
public ResponseEntity<?> dial(HttpServletRequest request,
|
|
|
83 |
@RequestParam(name = "leadId") int leadId) {
|
|
|
84 |
AuthUser agent = currentUser(request);
|
|
|
85 |
if (agent == null) {
|
|
|
86 |
return unauthorized();
|
|
|
87 |
}
|
|
|
88 |
return renderAllowingRefusal(lmsCallService.dial(agent, leadId));
|
|
|
89 |
}
|
|
|
90 |
|
|
|
91 |
/**
|
|
|
92 |
* Poll one call's live state. Click2dial has no event webhook, so the app has to ask — and this
|
|
|
93 |
* is also what advances the stored row, so a call nobody polls never reaches a terminal state.
|
|
|
94 |
*/
|
|
|
95 |
@RequestMapping(value = "/lms/call/state", method = RequestMethod.GET)
|
|
|
96 |
@ResponseBody
|
|
|
97 |
public ResponseEntity<?> state(HttpServletRequest request,
|
|
|
98 |
@RequestParam(name = "leadCallId") int leadCallId) {
|
|
|
99 |
AuthUser agent = currentUser(request);
|
|
|
100 |
if (agent == null) {
|
|
|
101 |
return unauthorized();
|
|
|
102 |
}
|
|
|
103 |
return render(lmsCallService.state(agent, leadCallId));
|
|
|
104 |
}
|
|
|
105 |
|
|
|
106 |
/** Hang up a call the rep placed. */
|
|
|
107 |
@RequestMapping(value = "/lms/call/hangup", method = RequestMethod.POST)
|
|
|
108 |
@ResponseBody
|
|
|
109 |
public ResponseEntity<?> hangup(HttpServletRequest request,
|
|
|
110 |
@RequestParam(name = "leadCallId") int leadCallId) {
|
|
|
111 |
AuthUser agent = currentUser(request);
|
|
|
112 |
if (agent == null) {
|
|
|
113 |
return unauthorized();
|
|
|
114 |
}
|
|
|
115 |
return render(lmsCallService.hangup(agent, leadCallId));
|
|
|
116 |
}
|
|
|
117 |
|
|
|
118 |
/** Record what came of the call: disposition, stage move, SLA stamp, trail entry. */
|
|
|
119 |
@RequestMapping(value = "/lms/call/disposition", method = RequestMethod.POST,
|
|
|
120 |
consumes = MediaType.APPLICATION_JSON_VALUE)
|
|
|
121 |
@ResponseBody
|
|
|
122 |
public ResponseEntity<?> disposition(HttpServletRequest request,
|
|
|
123 |
@RequestBody LeadDispositionRequest body) {
|
|
|
124 |
AuthUser agent = currentUser(request);
|
|
|
125 |
if (agent == null) {
|
|
|
126 |
return unauthorized();
|
|
|
127 |
}
|
|
|
128 |
LmsOutcome outcome = lmsDispositionService.apply(agent, body);
|
|
|
129 |
if (outcome.isOk()) {
|
|
|
130 |
Lead lead = (Lead) outcome.data.get("lead");
|
|
|
131 |
java.util.Map<String, Object> out = new java.util.LinkedHashMap<>();
|
|
|
132 |
out.put("ok", true);
|
|
|
133 |
out.put("leadId", lead.getId());
|
|
|
134 |
out.put("stage", outcome.data.get("stage"));
|
|
|
135 |
out.put("leadCallId", outcome.data.get("leadCallId"));
|
|
|
136 |
return responseSender.ok(out);
|
|
|
137 |
}
|
|
|
138 |
return render(outcome);
|
|
|
139 |
}
|
|
|
140 |
|
|
|
141 |
/**
|
|
|
142 |
* As {@link #render}, but a refusal comes back as a 200 carrying {@code {ok:false, code, reason}}.
|
|
|
143 |
*
|
|
|
144 |
* <p>"This retailer is on the do-not-call register" and "it is past 21:00" are correct answers
|
|
|
145 |
* to a request to dial, not failures of it. They matter because the app's HTTP interceptor
|
|
|
146 |
* auto-toasts every 4xx in red: sending a 4xx here would show the rep an error for behaving
|
|
|
147 |
* exactly as the rules require, on top of the explanation the call sheet already gives them.
|
|
|
148 |
*/
|
|
|
149 |
private ResponseEntity<?> renderAllowingRefusal(LmsOutcome outcome) {
|
|
|
150 |
if (outcome.status == LmsOutcome.Status.REFUSED) {
|
|
|
151 |
return responseSender.ok(outcome.asRefusalBody());
|
|
|
152 |
}
|
|
|
153 |
return render(outcome);
|
|
|
154 |
}
|
|
|
155 |
|
|
|
156 |
/** Map a service outcome onto this module's response envelope. */
|
|
|
157 |
private ResponseEntity<?> render(LmsOutcome outcome) {
|
|
|
158 |
switch (outcome.status) {
|
|
|
159 |
case OK:
|
|
|
160 |
return responseSender.ok(outcome.data);
|
|
|
161 |
case NOT_FOUND:
|
|
|
162 |
return responseSender.notFound(outcome.message);
|
|
|
163 |
case UNAUTHORIZED:
|
|
|
164 |
return unauthorized();
|
|
|
165 |
case FORBIDDEN:
|
|
|
166 |
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
|
|
167 |
default:
|
|
|
168 |
return responseSender.badRequest(outcome.message);
|
|
|
169 |
}
|
|
|
170 |
}
|
|
|
171 |
|
|
|
172 |
private ResponseEntity<?> unauthorized() {
|
|
|
173 |
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
|
|
174 |
}
|
|
|
175 |
|
|
|
176 |
/**
|
|
|
177 |
* The field rep behind this request.
|
|
|
178 |
*
|
|
|
179 |
* <p>Follows the beat/PJP idiom ({@code BeatTrackingController.listBeats}): resolve from the
|
|
|
180 |
* token's email against {@code gmail_id}, never from a client-supplied user id. Null-checked by
|
|
|
181 |
* every caller because {@code AuthenticationInterceptor} lets a request through when the
|
|
|
182 |
* {@code Auth-Token} header is absent entirely — it only rejects a token it cannot parse.
|
|
|
183 |
*/
|
|
|
184 |
private AuthUser currentUser(HttpServletRequest request) {
|
|
|
185 |
try {
|
|
|
186 |
UserInfo userInfo = (UserInfo) request.getAttribute("userInfo");
|
|
|
187 |
if (userInfo == null || userInfo.getEmail() == null) {
|
|
|
188 |
return null;
|
|
|
189 |
}
|
|
|
190 |
return authRepository.selectByGmailId(userInfo.getEmail());
|
|
|
191 |
} catch (Exception e) {
|
|
|
192 |
LOGGER.warn("Could not resolve the user for an LMS call request", e);
|
|
|
193 |
return null;
|
|
|
194 |
}
|
|
|
195 |
}
|
|
|
196 |
}
|