Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
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
}