Subversion Repositories SmartDukaan

Rev

Rev 37003 | Rev 37036 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 37003 Rev 37035
Line 40... Line 40...
40
import org.apache.commons.csv.CSVRecord;
40
import org.apache.commons.csv.CSVRecord;
41
import org.apache.commons.io.output.ByteArrayOutputStream;
41
import org.apache.commons.io.output.ByteArrayOutputStream;
42
import org.apache.logging.log4j.LogManager;
42
import org.apache.logging.log4j.LogManager;
43
import org.apache.logging.log4j.Logger;
43
import org.apache.logging.log4j.Logger;
44
import org.springframework.beans.factory.annotation.Autowired;
44
import org.springframework.beans.factory.annotation.Autowired;
-
 
45
import org.springframework.beans.factory.annotation.Value;
45
import org.springframework.core.io.InputStreamResource;
46
import org.springframework.core.io.InputStreamResource;
46
import org.springframework.http.HttpHeaders;
47
import org.springframework.http.HttpHeaders;
47
import org.springframework.http.HttpStatus;
48
import org.springframework.http.HttpStatus;
48
import org.springframework.http.MediaType;
49
import org.springframework.http.MediaType;
49
import org.springframework.http.ResponseEntity;
50
import org.springframework.http.ResponseEntity;
Line 74... Line 75...
74
@Controller
75
@Controller
75
@Transactional(rollbackFor = Throwable.class)
76
@Transactional(rollbackFor = Throwable.class)
76
public class LeadController {
77
public class LeadController {
77
    private static final Logger LOGGER = LogManager.getLogger(LeadController.class);
78
    private static final Logger LOGGER = LogManager.getLogger(LeadController.class);
78
 
79
 
-
 
80
    /** Source / createdBy label for leads captured by the AI assistant (chat + voice). */
-
 
81
    private static final String AI_LEAD_SOURCE = "AI Assistant";
-
 
82
 
-
 
83
    @Value("${ai.lead.intake.token:}")
-
 
84
    private String aiLeadIntakeToken;
-
 
85
 
-
 
86
    /**
-
 
87
     * AI-assistant lead intake, hosted in the fofo app so it deploys via {@code fofo_deploy}. Mirrors the
-
 
88
     * profitmandi-web /add/lead contract: shared-secret {@code Auth-Token}, source stamped server-side,
-
 
89
     * idempotent by mobile+source, NOT NULL columns coalesced (so a minimal name+number lead never
-
 
90
     * crashes), stores {@code state} + {@code recordingUrl}, status = pending (Open/New). This path is
-
 
91
     * excluded from fofo's authentication/role/post interceptors in WebConfig.
-
 
92
     */
-
 
93
    @RequestMapping(value = ProfitMandiConstants.URL_AI_LEAD_INTAKE, method = RequestMethod.POST)
-
 
94
    @ResponseBody
-
 
95
    public ResponseEntity<?> aiLead(HttpServletRequest request,
-
 
96
                                    @RequestHeader(name = "Auth-Token", required = false) String authToken,
-
 
97
                                    @RequestBody AiLeadRequest aiLeadRequest) {
-
 
98
        LOGGER.info("AI lead intake request: {}", aiLeadRequest);
-
 
99
 
-
 
100
        if (aiLeadIntakeToken == null || aiLeadIntakeToken.trim().isEmpty()
-
 
101
                || authToken == null || !aiLeadIntakeToken.equals(authToken)) {
-
 
102
            LOGGER.warn("AI lead intake rejected: invalid or missing Auth-Token");
-
 
103
            return aiLeadResponse(HttpStatus.UNAUTHORIZED, "401", "UNAUTHORIZED", "FAILURE", null);
-
 
104
        }
-
 
105
 
-
 
106
        String rawMobile = aiLeadRequest.getMobile();
-
 
107
        String mobile = rawMobile == null ? "" : rawMobile.replaceAll("\\D", "");
-
 
108
        if (mobile.length() > 10) {
-
 
109
            mobile = mobile.substring(mobile.length() - 10);
-
 
110
        }
-
 
111
        if (mobile.length() != 10) {
-
 
112
            return aiLeadResponse(HttpStatus.BAD_REQUEST, "400", "Mobile number must contain 10 digits", "FAILURE", null);
-
 
113
        }
-
 
114
 
-
 
115
        String firstName = aiLeadRequest.getFirstName();
-
 
116
        if (firstName == null || firstName.trim().isEmpty()) {
-
 
117
            return aiLeadResponse(HttpStatus.BAD_REQUEST, "400", "First name is required", "FAILURE", null);
-
 
118
        }
-
 
119
 
-
 
120
        // Idempotency scoped to AI leads only (the AI side retries fire-and-forget posts).
-
 
121
        Lead existing = leadRepository.selectByMobileNumberAndSource(mobile, AI_LEAD_SOURCE);
-
 
122
        if (existing != null) {
-
 
123
            LOGGER.info("AI lead intake: AI lead already exists for mobile {} (id={}), skipping create", mobile, existing.getId());
-
 
124
            return aiLeadResponse(HttpStatus.OK, "200", "OK", "SUCCESS", existing.getId());
-
 
125
        }
-
 
126
 
-
 
127
        Lead lead = new Lead();
-
 
128
        lead.setLeadMobile(mobile);
-
 
129
        lead.setFirstName(firstName.trim());
-
 
130
        // last_name / city / address / state are NOT NULL in user.lead; coalesce nulls to "" so a minimal lead never crashes.
-
 
131
        lead.setLastName(aiLeadRequest.getLastName() == null ? "" : aiLeadRequest.getLastName().trim());
-
 
132
        String city = aiLeadRequest.getCity() == null ? "" : aiLeadRequest.getCity().trim();
-
 
133
        lead.setCity(city);
-
 
134
        lead.setOutLetName(aiLeadRequest.getOutletName());
-
 
135
        // AI does not collect a separate address; mirror web capture and fall back to city.
-
 
136
        lead.setAddress(city);
-
 
137
        String state = aiLeadRequest.getState();
-
 
138
        lead.setState(state == null ? "" : state.trim());
-
 
139
 
-
 
140
        // Source stamped server-side; caller cannot override it.
-
 
141
        lead.setSource(AI_LEAD_SOURCE);
-
 
142
        lead.setCreatedBy(AI_LEAD_SOURCE);
-
 
143
        lead.setStatus(LeadStatus.pending);
-
 
144
        lead.setColor("yellow");
-
 
145
 
-
 
146
        // Optional call-recording URL (voice leads only); store null when absent.
-
 
147
        String recordingUrl = aiLeadRequest.getRecordingUrl();
-
 
148
        if (recordingUrl != null && !recordingUrl.trim().isEmpty()) {
-
 
149
            lead.setRecordingUrl(recordingUrl.trim());
-
 
150
        }
-
 
151
 
-
 
152
        lead.setAssignTo(53);
-
 
153
        lead.setAuthId(lead.getAssignTo());
-
 
154
        lead.setCreatedTimestamp(LocalDateTime.now());
-
 
155
        lead.setUpdatedTimestamp(LocalDateTime.now());
-
 
156
        leadRepository.persist(lead);
-
 
157
 
-
 
158
        LOGGER.info("AI lead intake: created lead id={} for mobile {}", lead.getId(), mobile);
-
 
159
        return aiLeadResponse(HttpStatus.OK, "200", "OK", "SUCCESS", lead.getId());
-
 
160
    }
-
 
161
 
-
 
162
    private ResponseEntity<Map<String, Object>> aiLeadResponse(HttpStatus status, String statusCode,
-
 
163
                                                               String statusMessage, String responseStatus, Object response) {
-
 
164
        Map<String, Object> body = new HashMap<>();
-
 
165
        body.put("statusCode", statusCode);
-
 
166
        body.put("statusMessage", statusMessage);
-
 
167
        body.put("response", response);
-
 
168
        body.put("responseStatus", responseStatus);
-
 
169
        return ResponseEntity.status(status).body(body);
-
 
170
    }
-
 
171
 
79
    @Autowired
172
    @Autowired
80
    private LeadRepository leadRepository;
173
    private LeadRepository leadRepository;
81
 
174
 
82
    @Autowired
175
    @Autowired
83
    private LeadActivityRepository leadActivityRepository;
176
    private LeadActivityRepository leadActivityRepository;