//go:build test package user import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "crussell/db" "crussell/testutils" "crussell/testutils/fixtures" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" ) func TestCustomerRelationship_Success(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) } svc1, err := fixtures.CreateTestService(tx) if err != nil { t.Fatalf("failed to create service 1: %v", err) } svc2ID, err := createService(ctx, tx, "Gel Manicure", 35.00) if err != nil { t.Fatalf("failed to create service 2: %v", err) } booking1 := createCompletedBooking(t, ctx, tx, userID, svc1, "2024-01-15 10:00:00+00", 50.00) booking2 := createCompletedBooking(t, ctx, tx, userID, svc1, "2024-06-20 14:00:00+00", 50.00) booking3 := createCompletedBooking(t, ctx, tx, userID, svc2ID, "2024-12-01 11:00:00+00", 35.00) createPayment(t, ctx, tx, booking1, "full", 50.00) createPayment(t, ctx, tx, booking2, "full", 50.00) createPayment(t, ctx, tx, booking3, "full", 35.00) req := newAdminRequest("GET", "/api/admin/users/"+userID+"/relationship", userID, ctx) rr := httptest.NewRecorder() GetCustomerRelationshipHandler(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected status 200, got %d. body: %s", rr.Code, rr.Body.String()) } var result CustomerRelationship if err := json.Unmarshal(rr.Body.Bytes(), &result); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } if result.TotalSpend != 135.00 { t.Errorf("expected total spend 135.00, got %.2f", result.TotalSpend) } if result.TotalTips != 0 { t.Errorf("expected total tips 0, got %.2f", result.TotalTips) } if result.TotalVisits != 3 { t.Errorf("expected total visits 3, got %d", result.TotalVisits) } if result.CustomerFor == "" { t.Error("expected customerFor to be set") } if result.FirstVisitDate == nil { t.Fatal("expected firstVisitDate to be set") } if !strings.Contains(*result.FirstVisitDate, "2024-01-15") { t.Errorf("expected first visit date to contain 2024-01-15, got %s", *result.FirstVisitDate) } if result.LastVisitDate == nil { t.Fatal("expected lastVisitDate to be set") } if !strings.Contains(*result.LastVisitDate, "2024-12-01") { t.Errorf("expected last visit date to contain 2024-12-01, got %s", *result.LastVisitDate) } if len(result.TopServices) != 2 { t.Fatalf("expected 2 top services, got %d", len(result.TopServices)) } if result.TopServices[0].Count != 2 { t.Errorf("expected top service count 2, got %d", result.TopServices[0].Count) } if result.TopServices[1].Count != 1 { t.Errorf("expected second service count 1, got %d", result.TopServices[1].Count) } } func TestCustomerRelationship_NoBookings(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) } req := newAdminRequest("GET", "/api/admin/users/"+userID+"/relationship", userID, ctx) rr := httptest.NewRecorder() GetCustomerRelationshipHandler(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected status 200, got %d. body: %s", rr.Code, rr.Body.String()) } var result CustomerRelationship if err := json.Unmarshal(rr.Body.Bytes(), &result); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } if result.TotalSpend != 0 { t.Errorf("expected total spend 0, got %.2f", result.TotalSpend) } if result.TotalTips != 0 { t.Errorf("expected total tips 0, got %.2f", result.TotalTips) } if result.TotalVisits != 0 { t.Errorf("expected total visits 0, got %d", result.TotalVisits) } if result.CustomerFor != "" { t.Errorf("expected empty customerFor, got %s", result.CustomerFor) } if result.FirstVisitDate != nil { t.Errorf("expected firstVisitDate to be nil, got %s", *result.FirstVisitDate) } if result.LastVisitDate != nil { t.Errorf("expected lastVisitDate to be nil, got %s", *result.LastVisitDate) } if len(result.TopServices) != 0 { t.Errorf("expected empty top services, got %d items", len(result.TopServices)) } } func TestCustomerRelationship_UserNotFound(t *testing.T) { t.Parallel() req := newAdminRequest("GET", "/api/admin/users/000000000000/relationship", "000000000000", context.Background()) rr := httptest.NewRecorder() GetCustomerRelationshipHandler(rr, req) if rr.Code != http.StatusNotFound { t.Errorf("expected status 404, got %d", rr.Code) } } func TestCustomerRelationship_InvalidID(t *testing.T) { t.Parallel() tests := []struct { name string id string }{ {"too_short", "abc"}, {"too_long", "abcdef1234567"}, {"empty", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := newAdminRequest("GET", "/api/admin/users/"+tt.id+"/relationship", tt.id, context.Background()) rr := httptest.NewRecorder() GetCustomerRelationshipHandler(rr, req) if rr.Code != http.StatusNotFound { t.Errorf("expected status 404 for id %q, got %d", tt.id, rr.Code) } }) } } func TestCustomerRelationship_OnlyPendingBookings(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) } svcID, err := fixtures.CreateTestService(tx) if err != nil { t.Fatalf("failed to create service: %v", err) } bookingID, err := fixtures.CreateTestBooking(tx, userID, svcID) if err != nil { t.Fatalf("failed to create booking: %v", err) } _ = bookingID req := newAdminRequest("GET", "/api/admin/users/"+userID+"/relationship", userID, ctx) rr := httptest.NewRecorder() GetCustomerRelationshipHandler(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected status 200, got %d. body: %s", rr.Code, rr.Body.String()) } var result CustomerRelationship if err := json.Unmarshal(rr.Body.Bytes(), &result); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } if result.TotalVisits != 0 { t.Errorf("expected 0 visits (pending booking), got %d", result.TotalVisits) } if result.TotalSpend != 0 { t.Errorf("expected 0 spend (no completed payments), got %.2f", result.TotalSpend) } if result.TotalTips != 0 { t.Errorf("expected 0 tips, got %.2f", result.TotalTips) } } func TestCustomerRelationship_PartialPayments(t *testing.T) { t.Parallel() ctx, tx := testutils.SetupTestTx(t) userID, err := fixtures.CreateTestUser(tx) if err != nil { t.Fatalf("failed to create test user: %v", err) } svcID, err := createService(ctx, tx, "Test Service", 100.00) if err != nil { t.Fatalf("failed to create service: %v", err) } bookingID := createCompletedBooking(t, ctx, tx, userID, svcID, "2024-03-01 10:00:00+00", 100.00) createPayment(t, ctx, tx, bookingID, "full", 80.00) createPayment(t, ctx, tx, bookingID, "tip", 10.00) createPayment(t, ctx, tx, bookingID, "deposit", 20.00) req := newAdminRequest("GET", "/api/admin/users/"+userID+"/relationship", userID, ctx) rr := httptest.NewRecorder() GetCustomerRelationshipHandler(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected status 200, got %d. body: %s", rr.Code, rr.Body.String()) } var result CustomerRelationship if err := json.Unmarshal(rr.Body.Bytes(), &result); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } if result.TotalSpend != 100.00 { t.Errorf("expected total spend 100.00 (full + deposit), got %.2f", result.TotalSpend) } if result.TotalTips != 10.00 { t.Errorf("expected total tips 10.00, got %.2f", result.TotalTips) } } func newAdminRequest(method, path, userID string, ctx context.Context) *http.Request { req := httptest.NewRequest(method, path, nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", userID) chiCtx := context.WithValue(ctx, chi.RouteCtxKey, rctx) return req.WithContext(chiCtx) } func createService(ctx context.Context, q db.Querier, name string, price float64) (string, error) { var id string err := q.QueryRow(ctx, ` INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id `, name, "Test service", price, 60, true, 16).Scan(&id) return id, err } func createCompletedBooking(t *testing.T, ctx context.Context, q db.Querier, userID, serviceID, startTime string, price float64) string { t.Helper() var bookingID string err := q.QueryRow(ctx, ` INSERT INTO bookings (user_id, start_time, status) VALUES ($1, $2, 'completed') RETURNING id `, userID, startTime).Scan(&bookingID) if err != nil { t.Fatalf("failed to create completed booking: %v", err) } _, err = q.Exec(ctx, ` INSERT INTO booking_services (booking_id, service_id, override_price) VALUES ($1, $2, $3) `, bookingID, serviceID, price) if err != nil { t.Fatalf("failed to link service to booking: %v", err) } return bookingID } func createPayment(t *testing.T, ctx context.Context, q db.Querier, bookingID, paymentType string, amount float64) { t.Helper() _, err := q.Exec(ctx, ` INSERT INTO payments (booking_id, payment_type, payment_method, amount, status) VALUES ($1, $2, 'in_person_card', $3, 'completed') `, bookingID, paymentType, amount) if err != nil { t.Fatalf("failed to create payment: %v", err) } } func TestFormatPlural(t *testing.T) { assert.Equal(t, "1 day", formatPlural(1, "day")) assert.Equal(t, "2 days", formatPlural(2, "day")) assert.Equal(t, "0 days", formatPlural(0, "day")) }