| Line 78... |
Line 78... |
| 78 |
private static final Logger LOGGER = LogManager.getLogger(LeadController.class);
|
78 |
private static final Logger LOGGER = LogManager.getLogger(LeadController.class);
|
| 79 |
|
79 |
|
| 80 |
/** Source / createdBy label for leads captured by the AI assistant (chat + voice). */
|
80 |
/** Source / createdBy label for leads captured by the AI assistant (chat + voice). */
|
| 81 |
private static final String AI_LEAD_SOURCE = "AI Assistant";
|
81 |
private static final String AI_LEAD_SOURCE = "AI Assistant";
|
| 82 |
|
82 |
|
| 83 |
// AI leads are auto-assigned round-robin (one by one) between these two auth users (was hardcoded 53 / Shankar Mushran).
|
83 |
// AI leads are auto-assigned to a random active BGC L1 (the lead desk). Previously a hardcoded
|
| 84 |
private static final int AI_ASSIGN_KHUSHBU = 134; // Khushbu Saxena (khushbu@smartdukaan.com)
|
84 |
// round-robin between Khushbu (134) and Archana (126); before that a fixed 53 / Shankar Mushran.
|
| 85 |
private static final int AI_ASSIGN_ARCHANA = 126; // Archana Ramola (archana@smartdukaan.com)
|
85 |
// 126/134 are themselves BGC L1, so they stay in the pool - it just widens to the whole desk.
|
| 86 |
private final java.util.concurrent.atomic.AtomicInteger aiAssignToggle = new java.util.concurrent.atomic.AtomicInteger(0);
|
86 |
private static final int AI_ASSIGN_FALLBACK = 134; // Khushbu Saxena - used only if no active BGC L1 resolves
|
| 87 |
|
87 |
|
| 88 |
@Value("${ai.lead.intake.token:}")
|
88 |
@Value("${ai.lead.intake.token:}")
|
| 89 |
private String aiLeadIntakeToken;
|
89 |
private String aiLeadIntakeToken;
|
| 90 |
|
90 |
|
| 91 |
/**
|
91 |
/**
|
| Line 152... |
Line 152... |
| 152 |
String recordingUrl = aiLeadRequest.getRecordingUrl();
|
152 |
String recordingUrl = aiLeadRequest.getRecordingUrl();
|
| 153 |
if (recordingUrl != null && !recordingUrl.trim().isEmpty()) {
|
153 |
if (recordingUrl != null && !recordingUrl.trim().isEmpty()) {
|
| 154 |
lead.setRecordingUrl(recordingUrl.trim());
|
154 |
lead.setRecordingUrl(recordingUrl.trim());
|
| 155 |
}
|
155 |
}
|
| 156 |
|
156 |
|
| 157 |
// Round-robin between Khushbu and Archana, one lead at a time (toggle only advances on a real create, not dedup hits).
|
157 |
// Random active BGC L1 - only on a real create, dedup hits return before this point.
|
| 158 |
int assignTo = (aiAssignToggle.getAndIncrement() % 2 == 0) ? AI_ASSIGN_KHUSHBU : AI_ASSIGN_ARCHANA;
|
158 |
int assignTo = resolveAiLeadAssignee();
|
| 159 |
lead.setAssignTo(assignTo);
|
159 |
lead.setAssignTo(assignTo);
|
| 160 |
lead.setAuthId(lead.getAssignTo());
|
160 |
lead.setAuthId(lead.getAssignTo());
|
| 161 |
lead.setCreatedTimestamp(LocalDateTime.now());
|
161 |
lead.setCreatedTimestamp(LocalDateTime.now());
|
| 162 |
lead.setUpdatedTimestamp(LocalDateTime.now());
|
162 |
lead.setUpdatedTimestamp(LocalDateTime.now());
|
| 163 |
leadRepository.persist(lead);
|
163 |
leadRepository.persist(lead);
|
| 164 |
|
164 |
|
| 165 |
LOGGER.info("AI lead intake: created lead id={} for mobile {}, assigned to authId={}", lead.getId(), mobile, assignTo);
|
165 |
LOGGER.info("AI lead intake: created lead id={} for mobile {}, assigned to authId={}", lead.getId(), mobile, assignTo);
|
| 166 |
return aiLeadResponse(HttpStatus.OK, "200", "OK", "SUCCESS", lead.getId());
|
166 |
return aiLeadResponse(HttpStatus.OK, "200", "OK", "SUCCESS", lead.getId());
|
| 167 |
}
|
167 |
}
|
| 168 |
|
168 |
|
| - |
|
169 |
/**
|
| - |
|
170 |
* Picks the owner for an incoming AI lead: a uniformly random <b>active</b> BGC L1 from
|
| - |
|
171 |
* cs.position (category {@link ProfitMandiConstants#TICKET_CATEGORY_BGC}, escalation L1).
|
| - |
|
172 |
* <p>
|
| - |
|
173 |
* BGC is the desk that works AI / inbound leads before they reach field sales, and BGC L1-L3 is
|
| - |
|
174 |
* already one of the groups offered in the Lead Management "Assign To" list, so a BGC L1 owner is
|
| - |
|
175 |
* a valid assignee the team can see and reassign. Field Sales (category
|
| - |
|
176 |
* {@link ProfitMandiConstants#TICKET_CATEGORY_SALES}) is deliberately NOT the pool.
|
| - |
|
177 |
* <p>
|
| - |
|
178 |
* Inactive users are filtered out because {@code getAuthUserByCategoryId(int, EscalationType)}
|
| - |
|
179 |
* does not filter on active, and an inactive owner would strand the lead. Ids are already
|
| - |
|
180 |
* distinct per user, so holding several regional positions does not raise a person's odds.
|
| - |
|
181 |
*/
|
| - |
|
182 |
private int resolveAiLeadAssignee() {
|
| - |
|
183 |
List<AuthUser> bgcL1 = csService.getAuthUserByCategoryId(
|
| - |
|
184 |
ProfitMandiConstants.TICKET_CATEGORY_BGC, EscalationType.L1);
|
| - |
|
185 |
|
| - |
|
186 |
List<AuthUser> eligible = bgcL1 == null ? Collections.<AuthUser>emptyList()
|
| - |
|
187 |
: bgcL1.stream()
|
| - |
|
188 |
.filter(x -> Boolean.TRUE.equals(x.isActive()))
|
| - |
|
189 |
.collect(Collectors.toList());
|
| - |
|
190 |
|
| - |
|
191 |
if (eligible.isEmpty()) {
|
| - |
|
192 |
LOGGER.warn("AI lead intake: no active BGC L1 in cs.position, falling back to auth id {}",
|
| - |
|
193 |
AI_ASSIGN_FALLBACK);
|
| - |
|
194 |
return AI_ASSIGN_FALLBACK;
|
| - |
|
195 |
}
|
| - |
|
196 |
|
| - |
|
197 |
AuthUser assignee = eligible.get(
|
| - |
|
198 |
java.util.concurrent.ThreadLocalRandom.current().nextInt(eligible.size()));
|
| - |
|
199 |
LOGGER.info("AI lead intake: assigned to BGC L1 {} ({}) picked from {} eligible",
|
| - |
|
200 |
assignee.getId(), assignee.getFullName(), eligible.size());
|
| - |
|
201 |
return assignee.getId();
|
| - |
|
202 |
}
|
| - |
|
203 |
|
| 169 |
private ResponseEntity<Map<String, Object>> aiLeadResponse(HttpStatus status, String statusCode,
|
204 |
private ResponseEntity<Map<String, Object>> aiLeadResponse(HttpStatus status, String statusCode,
|
| 170 |
String statusMessage, String responseStatus, Object response) {
|
205 |
String statusMessage, String responseStatus, Object response) {
|
| 171 |
Map<String, Object> body = new HashMap<>();
|
206 |
Map<String, Object> body = new HashMap<>();
|
| 172 |
body.put("statusCode", statusCode);
|
207 |
body.put("statusCode", statusCode);
|
| 173 |
body.put("statusMessage", statusMessage);
|
208 |
body.put("statusMessage", statusMessage);
|