Subversion Repositories SmartDukaan

Rev

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

    @Autowired
    private LmsCallService lmsCallService;

    @Autowired
    private LmsDispositionService lmsDispositionService;

    @Autowired
    private AuthRepository authRepository;

    @Autowired
    private ResponseSender<?> responseSender;

    /** Gate a dial before it happens: DND and TRAI calling hours. */
    @RequestMapping(value = "/lms/call/precheck", method = RequestMethod.POST)
    @ResponseBody
    public 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)
    @ResponseBody
    public 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)
    @ResponseBody
    public 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)
    @ResponseBody
    public 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)
    @ResponseBody
    public 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;
        }
    }
}