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