Files
boc/backend/handlers/comm/contact.go
T
Bernt 417903b488
BOC CI/CD / Test (push) Failing after 14s
BOC CI/CD / Security Scan (push) Has been skipped
BOC CI/CD / Build & Push (push) Has been skipped
BOC CI/CD / Deploy to Staging (push) Has been skipped
BOC CI/CD / Deploy to Production (push) Has been skipped
fix(comm): handle null tags array in contact creation
2026-08-12 10:45:16 +00:00

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
}