package handlers import ( "database/sql" "encoding/json" "net/http" "time" "github.com/go-chi/chi/v5" ) type SalesHandler struct { DB *sql.DB } func NewSalesHandler(db *sql.DB) *SalesHandler { return &SalesHandler{DB: db} } type Deal struct { ID string `json:"id"` CustomerID string `json:"customer_id"` ContactID *string `json:"contact_id"` Name string `json:"name"` Description string `json:"description"` Value float64 `json:"value"` Currency string `json:"currency"` Status string `json:"status"` Stage string `json:"stage"` Probability int `json:"probability"` ExpectedClose *time.Time `json:"expected_close"` ActualClose *time.Time `json:"actual_close"` AssignedTo *string `json:"assigned_to"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type Product struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` SKU string `json:"sku"` Price float64 `json:"price"` Currency string `json:"currency"` Unit string `json:"unit"` IsRecurring bool `json:"is_recurring"` BillingPeriod string `json:"billing_period"` Status string `json:"status"` } func (h *SalesHandler) ListDeals(w http.ResponseWriter, r *http.Request) { status := r.URL.Query().Get("status") if status == "" { status = "open" } rows, err := h.DB.Query(` SELECT id, customer_id, contact_id, name, description, value, currency, status, stage, probability, expected_close, actual_close, assigned_to, created_at, updated_at FROM boc_deals WHERE status = $1 ORDER BY created_at DESC LIMIT 100 `, status) if err != nil { writeError(w, http.StatusInternalServerError, "database error") return } defer rows.Close() deals := []Deal{} for rows.Next() { var d Deal if err := rows.Scan(&d.ID, &d.CustomerID, &d.ContactID, &d.Name, &d.Description, &d.Value, &d.Currency, &d.Status, &d.Stage, &d.Probability, &d.ExpectedClose, &d.ActualClose, &d.AssignedTo, &d.CreatedAt, &d.UpdatedAt); err != nil { continue } deals = append(deals, d) } writeJSON(w, http.StatusOK, map[string]interface{}{ "deals": deals, "total": len(deals), }) } func (h *SalesHandler) CreateDeal(w http.ResponseWriter, r *http.Request) { var req Deal if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request") return } var id string err := h.DB.QueryRow(` INSERT INTO boc_deals (customer_id, contact_id, name, description, value, currency, status, stage, probability, expected_close) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id `, req.CustomerID, req.ContactID, req.Name, req.Description, req.Value, req.Currency, req.Status, req.Stage, req.Probability, req.ExpectedClose).Scan(&id) if err != nil { writeError(w, http.StatusInternalServerError, "failed to create deal") return } writeJSON(w, http.StatusCreated, map[string]interface{}{ "id": id, "message": "Deal created", }) } func (h *SalesHandler) GetDeal(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") var d Deal err := h.DB.QueryRow(` SELECT id, customer_id, contact_id, name, description, value, currency, status, stage, probability, expected_close, actual_close, assigned_to, created_at, updated_at FROM boc_deals WHERE id = $1 `, id).Scan(&d.ID, &d.CustomerID, &d.ContactID, &d.Name, &d.Description, &d.Value, &d.Currency, &d.Status, &d.Stage, &d.Probability, &d.ExpectedClose, &d.ActualClose, &d.AssignedTo, &d.CreatedAt, &d.UpdatedAt) if err == sql.ErrNoRows { writeError(w, http.StatusNotFound, "deal not found") return } if err != nil { writeError(w, http.StatusInternalServerError, "database error") return } writeJSON(w, http.StatusOK, d) } func (h *SalesHandler) UpdateDeal(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") var req Deal if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request") return } _, err := h.DB.Exec(` UPDATE boc_deals SET customer_id = $1, contact_id = $2, name = $3, description = $4, value = $5, currency = $6, status = $7, stage = $8, probability = $9, expected_close = $10, actual_close = $11, assigned_to = $12 WHERE id = $13 `, req.CustomerID, req.ContactID, req.Name, req.Description, req.Value, req.Currency, req.Status, req.Stage, req.Probability, req.ExpectedClose, req.ActualClose, req.AssignedTo, id) if err != nil { writeError(w, http.StatusInternalServerError, "failed to update deal") return } writeJSON(w, http.StatusOK, map[string]interface{}{ "message": "Deal updated", }) } func (h *SalesHandler) GetMRR(w http.ResponseWriter, r *http.Request) { var mrr float64 err := h.DB.QueryRow(` SELECT COALESCE(SUM(value), 0) FROM boc_deals WHERE status = 'closed_won' AND created_at >= NOW() - INTERVAL '1 month' `).Scan(&mrr) if err != nil { mrr = 0 } writeJSON(w, http.StatusOK, map[string]interface{}{ "mrr": mrr, "currency": "USD", }) } func (h *SalesHandler) GetARR(w http.ResponseWriter, r *http.Request) { var arr float64 err := h.DB.QueryRow(` SELECT COALESCE(SUM(value), 0) FROM boc_deals WHERE status = 'closed_won' AND created_at >= NOW() - INTERVAL '1 year' `).Scan(&arr) if err != nil { arr = 0 } writeJSON(w, http.StatusOK, map[string]interface{}{ "arr": arr, "currency": "USD", }) } func (h *SalesHandler) ListProducts(w http.ResponseWriter, r *http.Request) { rows, err := h.DB.Query(` SELECT id, name, description, sku, price, currency, unit, is_recurring, billing_period, status FROM boc_products WHERE status = 'active' ORDER BY name `) if err != nil { writeError(w, http.StatusInternalServerError, "database error") return } defer rows.Close() products := []Product{} for rows.Next() { var p Product if err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.SKU, &p.Price, &p.Currency, &p.Unit, &p.IsRecurring, &p.BillingPeriod, &p.Status); err != nil { continue } products = append(products, p) } writeJSON(w, http.StatusOK, map[string]interface{}{ "products": products, "total": len(products), }) } func (h *SalesHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { var req Product if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request") return } var id string err := h.DB.QueryRow(` INSERT INTO boc_products (name, description, sku, price, currency, unit, is_recurring, billing_period, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'active') RETURNING id `, req.Name, req.Description, req.SKU, req.Price, req.Currency, req.Unit, req.IsRecurring, req.BillingPeriod).Scan(&id) if err != nil { writeError(w, http.StatusInternalServerError, "failed to create product") return } writeJSON(w, http.StatusCreated, map[string]interface{}{ "id": id, "message": "Product created", }) }