package handlers import ( "database/sql" "encoding/json" "log" "net/http" "go-crm/config" "go-crm/internal/db" "go-crm/internal/parser" ) // ingestRequest is the JSON body sent by the WhatsApp adapter. type ingestRequest struct { ClientID int64 `json:"client_id"` Name string `json:"name"` Phone string `json:"phone"` Message string `json:"message"` MessageID string `json:"message_id"` } // IngestLead is the internal-only endpoint called by the WhatsApp adapter. // POST /leads/ingest (X-Internal-Secret required) func (a *App) IngestLead(w http.ResponseWriter, r *http.Request) { if r.Header.Get("X-Internal-Secret") != config.InternalSecret() { http.Error(w, "forbidden", http.StatusForbidden) return } var req ingestRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } if req.ClientID == 0 || req.Phone == "" { http.Error(w, "client_id and phone required", http.StatusBadRequest) return } phoneNorm := parser.NormalizePhone(req.Phone) // Dedup: skip if this message was already processed. if req.MessageID != "" { processed, err := db.IsMessageProcessed(a.DB, req.ClientID, req.MessageID) if err != nil { log.Printf("IngestLead: db error checking processed message: %v", err) } if processed { w.WriteHeader(http.StatusOK) return } } // Check if lead already exists for this phone. existing, err := db.GetLeadByPhone(a.DB, req.ClientID, phoneNorm) if err != nil && err != sql.ErrNoRows { log.Printf("IngestLead: db error checking existing lead: %v", err) http.Error(w, "db error", http.StatusInternalServerError) return } if existing != nil { // Known lead — just update last contact timestamp. if terr := db.TouchLeadContact(a.DB, existing.LeadID); terr != nil { log.Printf("IngestLead: failed to touch lead %d: %v", existing.LeadID, terr) } if req.MessageID != "" { if err := db.RecordProcessedMessage(a.DB, req.ClientID, req.MessageID); err != nil { log.Printf("IngestLead: failed to record processed message: %v", err) } } w.WriteHeader(http.StatusOK) return } // New lead — extract service from message text. mapping, err := db.LoadKeywordMapping(a.DB, req.ClientID) if err != nil { log.Printf("IngestLead: failed to load keyword mapping: %v", err) mapping = map[string]string{} } serviceInterest := parser.ExtractService(req.Message, mapping) needsReview := serviceInterest == "Não especificou" lead := &db.Lead{ ClientID: req.ClientID, Name: req.Name, PhoneRaw: req.Phone, PhoneNormalized: phoneNorm, ServiceInterest: serviceInterest, Status: "Não agendou", NeedsReview: needsReview, PaymentStatus: "Não pago", } if err := db.CreateLead(a.DB, lead); err != nil { log.Printf("IngestLead: failed to create lead: %v", err) http.Error(w, "db error", http.StatusInternalServerError) return } if req.MessageID != "" { if err := db.RecordProcessedMessage(a.DB, req.ClientID, req.MessageID); err != nil { log.Printf("IngestLead: failed to record processed message: %v", err) } } // Seed default statuses/keywords for this client if first lead. go seedClientDefaults(a.DB, req.ClientID) w.WriteHeader(http.StatusCreated) } // seedClientDefaults seeds default statuses and keywords for a client on first lead. // It accepts the DB as a parameter so it can be called from both the App method // and the legacy package-level shim without touching global state. func seedClientDefaults(database *sql.DB, clientID int64) { statuses, _ := db.ListLeadStatuses(database, clientID) if len(statuses) == 0 { defaults := []string{"Não agendou", "Agendou", "Sem retorno / não evoluiu"} for _, s := range defaults { db.AddLeadStatus(database, clientID, s) } } kws, _ := db.ListServiceKeywords(database, clientID) if len(kws) == 0 { defaultKWs := map[string][]string{ "Head Spa": {"head spa", "head-spa", "headspa"}, "Massagem completa": {"massagem", "massagem completa"}, "Drenagem linfatica": {"drenagem", "linfática", "drenagem linfatica"}, "Hydra Boost": {"hydra", "hydra boost"}, "Design Henna": {"henna", "design henna"}, "Masculino": {"masculino", "masc"}, } for svc, keywords := range defaultKWs { for _, kw := range keywords { db.AddServiceKeyword(database, clientID, svc, kw) } } } } // --- package-level shim kept for existing tests --- func IngestLead(w http.ResponseWriter, r *http.Request) { (&App{DB: DB, WAConnector: WAConnector}).IngestLead(w, r) }