271 lines
8.2 KiB
Go
271 lines
8.2 KiB
Go
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
|
|
}
|