Blame | Last modification | View Log | RSS feed
package com.spice.profitmandi.web.controller;import com.spice.profitmandi.common.model.UserInfo;import com.spice.profitmandi.common.web.util.ResponseSender;import com.spice.profitmandi.dao.entity.auth.AuthUser;import com.spice.profitmandi.dao.entity.user.Lead;import com.spice.profitmandi.dao.model.lms.LeadDispositionRequest;import com.spice.profitmandi.dao.repository.auth.AuthRepository;import com.spice.profitmandi.service.lms.LmsCallService;import com.spice.profitmandi.service.lms.LmsDispositionService;import com.spice.profitmandi.service.lms.LmsOutcome;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Controller;import org.springframework.transaction.annotation.Transactional;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.http.HttpServletRequest;/*** The field app's LMS dialer: place a recorded call to a lead from a beat stop, watch it, end it,* and record what came of it.** <p>Same calls as the desk console's {@code /lms/dialer/*}, and deliberately the same services* underneath ({@link LmsCallService}, {@link LmsDispositionService}) so a rep's call is* indistinguishable from a BGC agent's in every LMS report — same {@code user.lead_call} row, same* recording archived to our own bucket, same first-contact SLA stamp, same trail entry. Only the* identity step differs: the console reads a session cookie, this reads the app's {@code Auth-Token}.** <p><b>Not under {@code /clickToCall}.</b> That prefix is excluded from {@code* SimpleCORSInterceptor} in {@code WebMVCConfig} for the legacy Knowlarity endpoints, and a new* path starting with it would silently inherit the exclusion.*/@Controller@Transactional(rollbackFor = Throwable.class)public class LmsAppCallController {private static final Logger LOGGER = LogManager.getLogger(LmsAppCallController.class);@Autowiredprivate LmsCallService lmsCallService;@Autowiredprivate LmsDispositionService lmsDispositionService;@Autowiredprivate AuthRepository authRepository;@Autowiredprivate ResponseSender<?> responseSender;/** Gate a dial before it happens: DND and TRAI calling hours. */@RequestMapping(value = "/lms/call/precheck", method = RequestMethod.POST)@ResponseBodypublic ResponseEntity<?> precheck(HttpServletRequest request,@RequestParam(name = "leadId") int leadId) {AuthUser agent = currentUser(request);if (agent == null) {return unauthorized();}return renderAllowingRefusal(lmsCallService.precheck(leadId));}/*** Place the call. The rep's own VBC endpoint rings first, then VBC dials the retailer.** <p>{@code leadId} is a query parameter rather than a JSON body on purpose: {@code* PostInterceptor} de-duplicates POSTs for 300 seconds by hashing the body when no* {@code IdempotencyKey} is present, and two identical bodies would make a rep's second attempt* at the same lead fail with "Duplicate request." instead of dialling.*/@RequestMapping(value = "/lms/call/dial", method = RequestMethod.POST)@ResponseBodypublic ResponseEntity<?> dial(HttpServletRequest request,@RequestParam(name = "leadId") int leadId) {AuthUser agent = currentUser(request);if (agent == null) {return unauthorized();}return renderAllowingRefusal(lmsCallService.dial(agent, leadId));}/*** Poll one call's live state. Click2dial has no event webhook, so the app has to ask — and this* is also what advances the stored row, so a call nobody polls never reaches a terminal state.*/@RequestMapping(value = "/lms/call/state", method = RequestMethod.GET)@ResponseBodypublic ResponseEntity<?> state(HttpServletRequest request,@RequestParam(name = "leadCallId") int leadCallId) {AuthUser agent = currentUser(request);if (agent == null) {return unauthorized();}return render(lmsCallService.state(agent, leadCallId));}/** Hang up a call the rep placed. */@RequestMapping(value = "/lms/call/hangup", method = RequestMethod.POST)@ResponseBodypublic ResponseEntity<?> hangup(HttpServletRequest request,@RequestParam(name = "leadCallId") int leadCallId) {AuthUser agent = currentUser(request);if (agent == null) {return unauthorized();}return render(lmsCallService.hangup(agent, leadCallId));}/** Record what came of the call: disposition, stage move, SLA stamp, trail entry. */@RequestMapping(value = "/lms/call/disposition", method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_VALUE)@ResponseBodypublic ResponseEntity<?> disposition(HttpServletRequest request,@RequestBody LeadDispositionRequest body) {AuthUser agent = currentUser(request);if (agent == null) {return unauthorized();}LmsOutcome outcome = lmsDispositionService.apply(agent, body);if (outcome.isOk()) {Lead lead = (Lead) outcome.data.get("lead");java.util.Map<String, Object> out = new java.util.LinkedHashMap<>();out.put("ok", true);out.put("leadId", lead.getId());out.put("stage", outcome.data.get("stage"));out.put("leadCallId", outcome.data.get("leadCallId"));return responseSender.ok(out);}return render(outcome);}/*** As {@link #render}, but a refusal comes back as a 200 carrying {@code {ok:false, code, reason}}.** <p>"This retailer is on the do-not-call register" and "it is past 21:00" are correct answers* to a request to dial, not failures of it. They matter because the app's HTTP interceptor* auto-toasts every 4xx in red: sending a 4xx here would show the rep an error for behaving* exactly as the rules require, on top of the explanation the call sheet already gives them.*/private ResponseEntity<?> renderAllowingRefusal(LmsOutcome outcome) {if (outcome.status == LmsOutcome.Status.REFUSED) {return responseSender.ok(outcome.asRefusalBody());}return render(outcome);}/** Map a service outcome onto this module's response envelope. */private ResponseEntity<?> render(LmsOutcome outcome) {switch (outcome.status) {case OK:return responseSender.ok(outcome.data);case NOT_FOUND:return responseSender.notFound(outcome.message);case UNAUTHORIZED:return unauthorized();case FORBIDDEN:return ResponseEntity.status(HttpStatus.FORBIDDEN).build();default:return responseSender.badRequest(outcome.message);}}private ResponseEntity<?> unauthorized() {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();}/*** The field rep behind this request.** <p>Follows the beat/PJP idiom ({@code BeatTrackingController.listBeats}): resolve from the* token's email against {@code gmail_id}, never from a client-supplied user id. Null-checked by* every caller because {@code AuthenticationInterceptor} lets a request through when the* {@code Auth-Token} header is absent entirely — it only rejects a token it cannot parse.*/private AuthUser currentUser(HttpServletRequest request) {try {UserInfo userInfo = (UserInfo) request.getAttribute("userInfo");if (userInfo == null || userInfo.getEmail() == null) {return null;}return authRepository.selectByGmailId(userInfo.getEmail());} catch (Exception e) {LOGGER.warn("Could not resolve the user for an LMS call request", e);return null;}}}