package repository import ( "context" "database/sql" "time" ) // Deal represents a sales opportunity 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"` } // Product represents a sellable product/service 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"` } // SalesRepository handles all sales-related database operations type SalesRepository struct { db *sql.DB } func NewSalesRepository(db *sql.DB) *SalesRepository { return &SalesRepository{db: db} } // ListDeals returns all deals filtered by status func (r *SalesRepository) ListDeals(ctx context.Context, status string) ([]Deal, error) { if status == "" { status = "active" } rows, err := r.db.QueryContext(ctx, ` 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 { return nil, err } defer rows.Close() var 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) } return deals, rows.Err() } // GetDeal returns a single deal by ID func (r *SalesRepository) GetDeal(ctx context.Context, id string) (*Deal, error) { var d Deal err := r.db.QueryRowContext(ctx, ` 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 != nil { return nil, err } return &d, nil } // CreateDeal creates a new deal func (r *SalesRepository) CreateDeal(ctx context.Context, d *Deal) error { return r.db.QueryRowContext(ctx, ` INSERT INTO boc_deals (customer_id, contact_id, name, description, value, currency, status, stage, probability, expected_close, assigned_to) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, updated_at `, d.CustomerID, d.ContactID, d.Name, d.Description, d.Value, d.Currency, d.Status, d.Stage, d.Probability, d.ExpectedClose, d.AssignedTo).Scan(&d.ID, &d.CreatedAt, &d.UpdatedAt) } // UpdateDeal updates an existing deal func (r *SalesRepository) UpdateDeal(ctx context.Context, id string, d *Deal) error { _, err := r.db.ExecContext(ctx, ` 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, assigned_to = $11, updated_at = NOW() WHERE id = $12 `, d.CustomerID, d.ContactID, d.Name, d.Description, d.Value, d.Currency, d.Status, d.Stage, d.Probability, d.ExpectedClose, d.AssignedTo, id) return err } // ListProducts returns all active products func (r *SalesRepository) ListProducts(ctx context.Context) ([]Product, error) { rows, err := r.db.QueryContext(ctx, ` 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 { return nil, err } defer rows.Close() var 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) } return products, rows.Err() } // GetMRR calculates monthly recurring revenue func (r *SalesRepository) GetMRR(ctx context.Context) (float64, error) { var mrr float64 err := r.db.QueryRowContext(ctx, ` SELECT COALESCE(SUM(value * probability / 100.0), 0) FROM boc_deals WHERE status = 'open' AND stage IN ('negotiation', 'proposal') `).Scan(&mrr) return mrr, err } // GetARR calculates annual recurring revenue func (r *SalesRepository) GetARR(ctx context.Context) (float64, error) { var arr float64 err := r.db.QueryRowContext(ctx, ` SELECT COALESCE(SUM(value * probability / 100.0) * 12, 0) FROM boc_deals WHERE status = 'open' AND stage IN ('negotiation', 'proposal') `).Scan(&arr) return arr, err }