Files
Crussell/backend/handlers/user/customer_relationship.go
T

147 lines
4.5 KiB
Go

package user
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"log"
"math"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"crussell/db"
"crussell/internal/validators"
)
type CustomerRelationship struct {
TotalSpend float64 `json:"totalSpend"`
TotalSaved float64 `json:"totalSaved"`
TotalTips float64 `json:"totalTips"`
TotalVisits int `json:"totalVisits"`
CustomerFor string `json:"customerFor"`
FirstVisitDate *string `json:"firstVisitDate,omitempty"`
LastVisitDate *string `json:"lastVisitDate,omitempty"`
TopServices []TopService `json:"topServices"`
}
type TopService struct {
Name string `json:"name"`
Count int `json:"count"`
}
// GET /api/admin/users/{id}/relationship
func GetCustomerRelationshipHandler(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "id")
if userID == "" || !validators.IsValidID(userID) {
http.Error(w, "user not found", http.StatusNotFound)
return
}
var result CustomerRelationship
var firstVisit sql.NullTime
var lastVisit sql.NullTime
var exists bool
err := db.DB.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)`, userID).Scan(&exists)
if err != nil {
log.Printf("Failed to check user existence for %s: %v", userID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if !exists {
http.Error(w, "user not found", http.StatusNotFound)
return
}
err = db.DB.QueryRow(r.Context(), `
SELECT
COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'completed' AND p.payment_type IN ('full','partial','balance','deposit') AND p.payment_method != 'discount'), 0),
COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'completed' AND p.payment_type IN ('full','partial','balance','deposit') AND p.payment_method = 'discount'), 0),
COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'completed' AND p.payment_type = 'tip'), 0),
COUNT(DISTINCT b.id) FILTER (WHERE b.status = 'completed'),
MIN(b.start_time) FILTER (WHERE b.status = 'completed'),
MAX(b.start_time) FILTER (WHERE b.status = 'completed')
FROM bookings b
LEFT JOIN payments p ON p.booking_id = b.id
WHERE b.user_id = $1
`, userID).Scan(&result.TotalSpend, &result.TotalSaved, &result.TotalTips, &result.TotalVisits, &firstVisit, &lastVisit)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
log.Printf("Failed to get customer relationship data for user %s: %v", userID, err)
}
if firstVisit.Valid {
firstVisitStr := firstVisit.Time.Format("2006-01-02T15:04:05Z07:00")
result.FirstVisitDate = &firstVisitStr
days := int(time.Since(firstVisit.Time).Hours() / 24)
switch {
case days < 7:
result.CustomerFor = formatPlural(days, "day")
case days < 30:
result.CustomerFor = formatPlural(int(math.Round(float64(days)/7)), "week")
case days < 365:
result.CustomerFor = formatPlural(int(math.Round(float64(days)/30)), "month")
default:
result.CustomerFor = formatPlural(int(math.Round(float64(days)/365.25)), "year")
}
}
if lastVisit.Valid {
lastVisitStr := lastVisit.Time.Format("2006-01-02T15:04:05Z07:00")
result.LastVisitDate = &lastVisitStr
}
rows, err := db.DB.Query(r.Context(), `
SELECT name, cnt FROM (
SELECT s.name, COUNT(*) as count, COUNT(*) as cnt
FROM booking_services bsvc
JOIN bookings b ON bsvc.booking_id = b.id
JOIN services s ON bsvc.service_id = s.id
WHERE b.user_id = $1 AND b.status = 'completed'
GROUP BY s.name
UNION ALL
SELECT cs.name, COUNT(*) as count, COUNT(*) as cnt
FROM booking_custom_services bcs
JOIN bookings b ON bcs.booking_id = b.id
JOIN custom_services cs ON bcs.custom_service_id = cs.id
WHERE b.user_id = $1 AND b.status = 'completed'
GROUP BY cs.name
) combined
ORDER BY count DESC
LIMIT 5
`, userID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
log.Printf("Failed to get top services for user %s: %v", userID, err)
} else {
defer rows.Close()
for rows.Next() {
var ts TopService
if err := rows.Scan(&ts.Name, &ts.Count); err != nil {
log.Printf("Failed to scan top service: %v", err)
continue
}
result.TopServices = append(result.TopServices, ts)
}
}
if result.TopServices == nil {
result.TopServices = []TopService{}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(result); err != nil {
log.Printf("Failed to encode customer relationship response: %v", err)
}
}
func formatPlural(n int, unit string) string {
if n == 1 {
return fmt.Sprintf("1 %s", unit)
}
return fmt.Sprintf("%d %ss", n, unit)
}