| Line 49... |
Line 49... |
| 49 |
import org.apache.commons.csv.CSVRecord;
|
49 |
import org.apache.commons.csv.CSVRecord;
|
| 50 |
import org.apache.commons.io.output.ByteArrayOutputStream;
|
50 |
import org.apache.commons.io.output.ByteArrayOutputStream;
|
| 51 |
import org.apache.logging.log4j.LogManager;
|
51 |
import org.apache.logging.log4j.LogManager;
|
| 52 |
import org.apache.logging.log4j.Logger;
|
52 |
import org.apache.logging.log4j.Logger;
|
| 53 |
import org.springframework.beans.factory.annotation.Autowired;
|
53 |
import org.springframework.beans.factory.annotation.Autowired;
|
| - |
|
54 |
import org.springframework.beans.factory.annotation.Value;
|
| 54 |
import org.springframework.format.annotation.DateTimeFormat;
|
55 |
import org.springframework.format.annotation.DateTimeFormat;
|
| 55 |
import org.springframework.http.HttpHeaders;
|
56 |
import org.springframework.http.HttpHeaders;
|
| 56 |
import org.springframework.http.HttpStatus;
|
57 |
import org.springframework.http.HttpStatus;
|
| 57 |
import org.springframework.http.MediaType;
|
58 |
import org.springframework.http.MediaType;
|
| 58 |
import org.springframework.http.ResponseEntity;
|
59 |
import org.springframework.http.ResponseEntity;
|
| Line 77... |
Line 78... |
| 77 |
|
78 |
|
| 78 |
@Controller
|
79 |
@Controller
|
| 79 |
@Transactional(rollbackFor = Throwable.class)
|
80 |
@Transactional(rollbackFor = Throwable.class)
|
| 80 |
public class LeadController {
|
81 |
public class LeadController {
|
| 81 |
private static final Logger LOGGER = LogManager.getLogger(LeadController.class);
|
82 |
private static final Logger LOGGER = LogManager.getLogger(LeadController.class);
|
| - |
|
83 |
|
| - |
|
84 |
/** Source/createdBy label for leads captured by the AI assistant. */
|
| - |
|
85 |
private static final String AI_LEAD_SOURCE = "AI Assistant";
|
| - |
|
86 |
|
| 82 |
@Autowired
|
87 |
@Autowired
|
| 83 |
private ResponseSender<?> responseSender;
|
88 |
private ResponseSender<?> responseSender;
|
| 84 |
|
89 |
|
| - |
|
90 |
@Value("${ai.lead.intake.token}")
|
| - |
|
91 |
private String aiLeadIntakeToken;
|
| - |
|
92 |
|
| 85 |
@Autowired
|
93 |
@Autowired
|
| 86 |
private AuthRepository authRepository;
|
94 |
private AuthRepository authRepository;
|
| 87 |
|
95 |
|
| 88 |
@Autowired
|
96 |
@Autowired
|
| 89 |
private LeadRepository leadRepository;
|
97 |
private LeadRepository leadRepository;
|
| Line 711... |
Line 719... |
| 711 |
|
719 |
|
| 712 |
return responseSender.ok(true);
|
720 |
return responseSender.ok(true);
|
| 713 |
|
721 |
|
| 714 |
}
|
722 |
}
|
| 715 |
|
723 |
|
| - |
|
724 |
/**
|
| - |
|
725 |
* Intake endpoint for leads captured by the AI assistant (chat + voice flows, external system).
|
| - |
|
726 |
* <p>
|
| - |
|
727 |
* The source is stamped server-side as {@value #AI_LEAD_SOURCE} so the caller cannot spoof it.
|
| - |
|
728 |
* Authenticated with a shared secret sent in the standard {@code Auth-Token} header; this path is
|
| - |
|
729 |
* excluded from the JWT {@code AuthenticationInterceptor} (see WebMVCConfig) so the secret is not
|
| - |
|
730 |
* treated as a user token. The call is idempotent: the AI side fires fire-and-forget with retry,
|
| - |
|
731 |
* so a repeat POST for a mobile that already has an AI lead returns OK without creating a
|
| - |
|
732 |
* duplicate. Leads from other sources (SD-WEB, manual) for the same mobile do not block creation.
|
| - |
|
733 |
*/
|
| - |
|
734 |
@RequestMapping(value = ProfitMandiConstants.URL_AI_LEAD_INTAKE, method = RequestMethod.POST)
|
| - |
|
735 |
public ResponseEntity<?> aiLead(HttpServletRequest request,
|
| - |
|
736 |
@RequestHeader(name = "Auth-Token", required = false) String authToken,
|
| - |
|
737 |
@RequestBody AiLeadRequest aiLeadRequest)
|
| - |
|
738 |
throws ProfitMandiBusinessException {
|
| - |
|
739 |
LOGGER.info("AI lead intake request: {}", aiLeadRequest);
|
| - |
|
740 |
|
| - |
|
741 |
if (aiLeadIntakeToken == null || aiLeadIntakeToken.trim().isEmpty()
|
| - |
|
742 |
|| authToken == null || !aiLeadIntakeToken.equals(authToken)) {
|
| - |
|
743 |
LOGGER.warn("AI lead intake rejected: invalid or missing Auth-Token");
|
| - |
|
744 |
return responseSender.forbidden(null);
|
| - |
|
745 |
}
|
| - |
|
746 |
|
| - |
|
747 |
// Normalise mobile to the 10-digit number the lead table stores (mobile column is length 10).
|
| - |
|
748 |
// The AI side sends digits-only 10-12 chars; strip any stray non-digits and drop a leading
|
| - |
|
749 |
// country code (e.g. 91) when present.
|
| - |
|
750 |
String rawMobile = aiLeadRequest.getMobile();
|
| - |
|
751 |
String mobile = rawMobile == null ? "" : rawMobile.replaceAll("\\D", "");
|
| - |
|
752 |
if (mobile.length() > 10) {
|
| - |
|
753 |
mobile = mobile.substring(mobile.length() - 10);
|
| - |
|
754 |
}
|
| - |
|
755 |
if (mobile.length() != 10) {
|
| - |
|
756 |
throw new ProfitMandiBusinessException("Mobile Number", String.valueOf(rawMobile),
|
| - |
|
757 |
"Mobile number must contain 10 digits");
|
| - |
|
758 |
}
|
| - |
|
759 |
|
| - |
|
760 |
String firstName = aiLeadRequest.getFirstName();
|
| - |
|
761 |
if (firstName == null || firstName.trim().isEmpty()) {
|
| - |
|
762 |
throw new ProfitMandiBusinessException("First Name", firstName, "First name is required");
|
| - |
|
763 |
}
|
| - |
|
764 |
|
| - |
|
765 |
// Idempotency: scoped to AI leads only - the AI side retries fire-and-forget posts. A lead
|
| - |
|
766 |
// for the same mobile under another source (e.g. SD-WEB, manual) does NOT block creation.
|
| - |
|
767 |
Lead existing = leadRepository.selectByMobileNumberAndSource(mobile, AI_LEAD_SOURCE);
|
| - |
|
768 |
if (existing != null) {
|
| - |
|
769 |
LOGGER.info("AI lead intake: AI lead already exists for mobile {} (id={}), skipping create",
|
| - |
|
770 |
mobile, existing.getId());
|
| - |
|
771 |
return responseSender.ok(existing.getId());
|
| - |
|
772 |
}
|
| - |
|
773 |
|
| - |
|
774 |
Lead lead = new Lead();
|
| - |
|
775 |
lead.setLeadMobile(mobile);
|
| - |
|
776 |
lead.setFirstName(firstName.trim());
|
| - |
|
777 |
lead.setLastName(aiLeadRequest.getLastName());
|
| - |
|
778 |
lead.setCity(aiLeadRequest.getCity());
|
| - |
|
779 |
lead.setOutLetName(aiLeadRequest.getOutletName());
|
| - |
|
780 |
// AI flow does not collect a separate address; mirror web capture and fall back to city
|
| - |
|
781 |
lead.setAddress(aiLeadRequest.getCity());
|
| - |
|
782 |
|
| - |
|
783 |
// Source is stamped server-side; the caller cannot override it
|
| - |
|
784 |
lead.setSource(AI_LEAD_SOURCE);
|
| - |
|
785 |
lead.setCreatedBy(AI_LEAD_SOURCE);
|
| - |
|
786 |
lead.setStatus(LeadStatus.followUp);
|
| - |
|
787 |
lead.setColor("yellow");
|
| - |
|
788 |
|
| - |
|
789 |
// Auto-assign using the same routing the public web capture uses today
|
| - |
|
790 |
lead.setAssignTo(53);
|
| - |
|
791 |
lead.setAuthId(lead.getAssignTo());
|
| - |
|
792 |
|
| - |
|
793 |
lead.setCreatedTimestamp(LocalDateTime.now());
|
| - |
|
794 |
lead.setUpdatedTimestamp(LocalDateTime.now());
|
| - |
|
795 |
leadRepository.persist(lead);
|
| - |
|
796 |
|
| - |
|
797 |
LOGGER.info("AI lead intake: created lead id={} for mobile {}", lead.getId(), mobile);
|
| - |
|
798 |
return responseSender.ok(lead.getId());
|
| - |
|
799 |
|
| - |
|
800 |
}
|
| - |
|
801 |
|
| 716 |
@RequestMapping(value = "/getPartnersList", method = RequestMethod.GET)
|
802 |
@RequestMapping(value = "/getPartnersList", method = RequestMethod.GET)
|
| 717 |
@ApiImplicitParams({@ApiImplicitParam(name = "Auth-Token", value = "Auth-Token", required = true, dataType = "string", paramType = "header")})
|
803 |
@ApiImplicitParams({@ApiImplicitParam(name = "Auth-Token", value = "Auth-Token", required = true, dataType = "string", paramType = "header")})
|
| 718 |
public ResponseEntity<?> getPartners(HttpServletRequest request, @RequestParam(name = "gmailId") String gmailId)
|
804 |
public ResponseEntity<?> getPartners(HttpServletRequest request, @RequestParam(name = "gmailId") String gmailId)
|
| 719 |
throws ProfitMandiBusinessException {
|
805 |
throws ProfitMandiBusinessException {
|
| 720 |
|
806 |
|