package comm import ( "database/sql" "encoding/json" "net/http" "time" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/rs/zerolog/log" "boc/middleware" ) // ContactHandler hanterar contact-identitet och deduplicering type ContactHandler struct { db *sql.DB } // NewContactHandler skapar en ny handler func NewContactHandler(db *sql.DB) *ContactHandler { return &ContactHandler{db: db} } // Contact representerar en kontakt type Contact struct { ID uuid.UUID `json:"id"` TenantID uuid.UUID `json:"tenant_id"` FirstName string `json:"first_name,omitempty"` LastName string `json:"last_name,omitempty"` Email string `json:"email,omitempty"` Phone string `json:"phone,omitempty"` Company string `json:"company,omitempty"` Tags []string `json:"tags,omitempty"` Status string `json:"status"` LeadScore int `json:"lead_score"` Source string `json:"source,omitempty"` Metadata json.RawMessage `json:"metadata,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // CreateContactRequest för att skapa kontakt type CreateContactRequest struct { FirstName string `json:"first_name"` LastName string `json:"last_name"` Email string `json:"email"` Phone string `json:"phone"` Company string `json:"company"` Source string `json:"source"` Tags []string `json:"tags,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` } // CreateContact skapar en ny kontakt med deduplicering func (h *ContactHandler) CreateContact(w http.ResponseWriter, r *http.Request) { var req CreateContactRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request") return } tenantID := getTenantID(r) if tenantID == uuid.Nil { writeError(w, http.StatusUnauthorized, "missing tenant") return } // Deduplicering: kolla om kontakt redan finns existingID, confidence := h.findExistingContact(tenantID, req.Email, req.Phone) if existingID != uuid.Nil && confidence > 0.8 { // Uppdatera befintlig kontakt h.updateContact(existingID, req) contact, _ := h.getContact(existingID) writeJSON(w, http.StatusOK, map[string]interface{}{ "contact": contact, "merged": true, "confidence": confidence, }) return } // Skapa ny kontakt contact, err := h.createNewContact(tenantID, req) if err != nil { log.Error().Err(err).Msg("Failed to create contact") writeError(w, http.StatusInternalServerError, "failed to create contact") return } writeJSON(w, http.StatusCreated, map[string]interface{}{ "contact": contact, "merged": false, "confidence": 1.0, }) } // GetContact hämtar en kontakt func (h *ContactHandler) GetContact(w http.ResponseWriter, r *http.Request) { id, err := uuid.Parse(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } contact, err := h.getContact(id) if err != nil { writeError(w, http.StatusNotFound, "contact not found") return } writeJSON(w, http.StatusOK, contact) } // ListContacts listar kontakter func (h *ContactHandler) ListContacts(w http.ResponseWriter, r *http.Request) { tenantID := getTenantID(r) if tenantID == uuid.Nil { writeError(w, http.StatusUnauthorized, "missing tenant") return } rows, err := h.db.Query(` SELECT id, first_name, last_name, email, phone, company, tags, status, lead_score, source, created_at, updated_at FROM contacts WHERE tenant_id = $1 AND merged_into IS NULL ORDER BY updated_at DESC LIMIT 100 `, tenantID) if err != nil { writeError(w, http.StatusInternalServerError, "database error") return } defer rows.Close() var contacts []Contact for rows.Next() { var c Contact rows.Scan(&c.ID, &c.FirstName, &c.LastName, &c.Email, &c.Phone, &c.Company, &c.Tags, &c.Status, &c.LeadScore, &c.Source, &c.CreatedAt, &c.UpdatedAt) contacts = append(contacts, c) } writeJSON(w, http.StatusOK, map[string]interface{}{"contacts": contacts}) } // MergeContacts slår ihop två kontakter func (h *ContactHandler) MergeContacts(w http.ResponseWriter, r *http.Request) { var req struct { PrimaryID uuid.UUID `json:"primary_id"` SecondaryID uuid.UUID `json:"secondary_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request") return } // Uppdatera secondary att peka på primary _, err := h.db.Exec(` UPDATE contacts SET merged_into = $1, status = 'merged' WHERE id = $2 `, req.PrimaryID, req.SecondaryID) if err != nil { writeError(w, http.StatusInternalServerError, "merge failed") return } // Uppdatera conversations och leads h.db.Exec(`UPDATE conversations SET contact_id = $1 WHERE contact_id = $2`, req.PrimaryID, req.SecondaryID) h.db.Exec(`UPDATE leads SET contact_id = $1 WHERE contact_id = $2`, req.PrimaryID, req.SecondaryID) writeJSON(w, http.StatusOK, map[string]interface{}{"merged": true}) } // Internal helpers func (h *ContactHandler) findExistingContact(tenantID uuid.UUID, email, phone string) (uuid.UUID, float64) { if email != "" { var id uuid.UUID err := h.db.QueryRow(` SELECT id FROM contacts WHERE tenant_id = $1 AND email = $2 AND merged_into IS NULL `, tenantID, email).Scan(&id) if err == nil { return id, 1.0 } } if phone != "" { var id uuid.UUID err := h.db.QueryRow(` SELECT id FROM contacts WHERE tenant_id = $1 AND phone = $2 AND merged_into IS NULL `, tenantID, phone).Scan(&id) if err == nil { return id, 0.9 } } return uuid.Nil, 0 } func (h *ContactHandler) createNewContact(tenantID uuid.UUID, req CreateContactRequest) (*Contact, error) { metadata, _ := json.Marshal(req.Metadata) var contact Contact err := h.db.QueryRow(` INSERT INTO contacts (tenant_id, first_name, last_name, email, phone, company, source, tags, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, tenant_id, first_name, last_name, email, phone, company, status, lead_score, source, created_at, updated_at `, tenantID, req.FirstName, req.LastName, req.Email, req.Phone, req.Company, req.Source, nullStringArray(req.Tags), metadata).Scan( &contact.ID, &contact.TenantID, &contact.FirstName, &contact.LastName, &contact.Email, &contact.Phone, &contact.Company, &contact.Status, &contact.LeadScore, &contact.Source, &contact.CreatedAt, &contact.UpdatedAt, ) return &contact, err } func (h *ContactHandler) updateContact(id uuid.UUID, req CreateContactRequest) error { _, err := h.db.Exec(` UPDATE contacts SET first_name = COALESCE(NULLIF($1, ''), first_name), last_name = COALESCE(NULLIF($2, ''), last_name), company = COALESCE(NULLIF($3, ''), company), updated_at = NOW() WHERE id = $4 `, req.FirstName, req.LastName, req.Company, id) return err } func (h *ContactHandler) getContact(id uuid.UUID) (*Contact, error) { var contact Contact err := h.db.QueryRow(` SELECT id, tenant_id, first_name, last_name, email, phone, company, tags, status, lead_score, source, created_at, updated_at FROM contacts WHERE id = $1 `, id).Scan( &contact.ID, &contact.TenantID, &contact.FirstName, &contact.LastName, &contact.Email, &contact.Phone, &contact.Company, &contact.Tags, &contact.Status, &contact.LeadScore, &contact.Source, &contact.CreatedAt, &contact.UpdatedAt, ) return &contact, err } // Helper functions func writeJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) } func writeError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, map[string]string{"error": message}) } func getTenantID(r *http.Request) uuid.UUID { // Hämta claims från context via middleware-paketet if claims, ok := middleware.FromContext(r.Context()); ok && claims.Sub != "" { if id, err := uuid.Parse(claims.Sub); err == nil { return id } } // Fallback: fixed tenant för utveckling return uuid.MustParse("3847477b-3d56-4975-9157-ae8f9ce52aa7") } func nullStringArray(arr []string) interface{} { if len(arr) == 0 { return nil } return arr }