Subversion Repositories SmartDukaan

Rev

Rev 36930 | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 36930 Rev 36957
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