feat(loyalty-discount): implement loyalty and discount system
- Add discount campaign management and validation logic - Update booking handlers with discount application flow - Add customer relationship endpoints for loyalty tracking - Update frontend modals (booking, approval, payment, reschedule) - Add DiscountsManagement and loyalty reference documentation - Update dev scripts and database init for discount tables - Clean up completed plan files
This commit is contained in:
@@ -3121,3 +3121,89 @@ func TestGetBookingsByCreatedRange_OrderedByCreatedAt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAdminBooking_WithDiscounts(t *testing.T) {
|
||||
resetTestData(t)
|
||||
seedDefaultWorkingHours(t)
|
||||
|
||||
userID, err := fixtures.CreateTestUser(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test user: %v", err)
|
||||
}
|
||||
|
||||
serviceID, err := fixtures.CreateTestService(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test service: %v", err)
|
||||
}
|
||||
|
||||
// Create completed booking
|
||||
bookingTime := time.Now().Add(-24 * time.Hour)
|
||||
bookingID := createCompletedBookingWithTimeForAdmin(t, userID, serviceID, bookingTime, 50.00)
|
||||
|
||||
// Create a discount campaign and apply it
|
||||
var campaignID string
|
||||
err = db.DB.QueryRow(context.Background(), `
|
||||
INSERT INTO discount_campaigns (name, campaign_type, discount_percent, status, start_date, end_date)
|
||||
VALUES ($1, 'time_based', 10.0, 'active', NOW() - INTERVAL '2 days', NOW() + INTERVAL '2 days')
|
||||
RETURNING id
|
||||
`, "Admin Test Campaign").Scan(&campaignID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create campaign: %v", err)
|
||||
}
|
||||
|
||||
_, err = db.DB.Exec(context.Background(), `
|
||||
INSERT INTO booking_discounts (booking_id, user_id, discount_source, source_id, campaign_type, discount_percent, original_total, discount_amount)
|
||||
VALUES ($1, $2, 'campaign', $3, 'time_based', 10.0, 50.00, 5.00)
|
||||
`, bookingID, userID, campaignID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create booking discount: %v", err)
|
||||
}
|
||||
|
||||
// Call GetAdminBookingHandler
|
||||
handler := http.HandlerFunc(bookings.GetAdminBookingHandler)
|
||||
w := makeAdminRequest(handler, "GET", "/api/admin/bookings/"+bookingID, nil)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var booking bookings.Booking
|
||||
if err := parseResponseBody(w, &booking); err != nil {
|
||||
t.Fatalf("failed to parse booking: %v", err)
|
||||
}
|
||||
|
||||
if len(booking.Discounts) != 1 {
|
||||
t.Fatalf("expected 1 discount, got %d", len(booking.Discounts))
|
||||
}
|
||||
|
||||
d := booking.Discounts[0]
|
||||
if d.CampaignName == nil || *d.CampaignName != "Admin Test Campaign" {
|
||||
t.Errorf("expected campaign name 'Admin Test Campaign', got %v", d.CampaignName)
|
||||
}
|
||||
if d.DiscountAmount != 5.00 {
|
||||
t.Errorf("expected discount amount 5.00, got %.2f", d.DiscountAmount)
|
||||
}
|
||||
}
|
||||
|
||||
func createCompletedBookingWithTimeForAdmin(t *testing.T, userID, serviceID string, startTime time.Time, price float64) string {
|
||||
t.Helper()
|
||||
var bookingID string
|
||||
err := db.DB.QueryRow(context.Background(), `
|
||||
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 = db.DB.Exec(context.Background(), `
|
||||
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: %v", err)
|
||||
}
|
||||
|
||||
return bookingID
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user