From 79a794de58c0891e8ec495cbb53ddc3a8316ed69 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Fri, 8 May 2026 18:08:59 +0100 Subject: [PATCH] fix: remove discount_eligible from booking creation, fix modal scrolling, lower default to 5% - Remove all discount_eligible checks from CreateBookingHandler and AdminCreateBookingForUserHandler - Discounts are only calculated at completion/payment time, not at booking time - Remove discount_eligible from frontend Booking type and pending_redemption from BookingUser - Remove TestDiscount_EligibilityFlag test (no longer relevant) - Fix modal scrolling: add max-h-[90vh] overflow-y-auto to match ServicesManagement pattern - Lower default discount from 10% to 5% - Clarify max_redemptions label as 'campaign total' (per campaign, not per person) --- backend/handlers/bookings/bookings.go | 24 +------- backend/handlers/bookings/discount_test.go | 57 ------------------- backend/handlers/bookings/manage.go | 21 +------ .../admin/DiscountsManagement.svelte | 10 ++-- frontend/src/lib/types/booking.ts | 10 ---- 5 files changed, 10 insertions(+), 112 deletions(-) diff --git a/backend/handlers/bookings/bookings.go b/backend/handlers/bookings/bookings.go index 3290a21..0827ae0 100644 --- a/backend/handlers/bookings/bookings.go +++ b/backend/handlers/bookings/bookings.go @@ -1307,24 +1307,6 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) { createdBy = &creatorID } - var discountEligible bool - if !isGuest { - var hasPendingRedemption bool - _ = db.DB.QueryRow(r.Context(), ` - SELECT EXISTS(SELECT 1 FROM loyalty_redemptions WHERE user_id = $1 AND status = 'pending' AND expires_at > NOW()) - `, userID).Scan(&hasPendingRedemption) - - var activeCampaigns int - _ = db.DB.QueryRow(r.Context(), ` - SELECT COUNT(*) FROM discount_campaigns - WHERE status = 'active' AND campaign_type = 'time_based' - AND start_date <= NOW() AND end_date >= NOW() - AND (max_redemptions IS NULL OR times_redeemed < max_redemptions) - `).Scan(&activeCampaigns) - - discountEligible = hasPendingRedemption || activeCampaigns > 0 - } - tx, err := db.DB.Begin(r.Context()) if err != nil { log.Printf("Failed to start transaction: %v", err) @@ -1346,10 +1328,10 @@ func CreateBookingHandler(w http.ResponseWriter, r *http.Request) { var booking Booking booking.User = &UserSummary{} if err := tx.QueryRow(r.Context(), ` - INSERT INTO bookings (user_id, start_time, notes, created_by, deposit_required, status, idempotency_key, discount_eligible) - VALUES ($1, $2, $3::text, $4, $5, CASE WHEN $3::text IS NOT NULL AND $3::text != '' THEN 'pending'::booking_status ELSE 'confirmed'::booking_status END, $6, $7) + INSERT INTO bookings (user_id, start_time, notes, created_by, deposit_required, status, idempotency_key) + VALUES ($1, $2, $3::text, $4, $5, CASE WHEN $3::text IS NOT NULL AND $3::text != '' THEN 'pending'::booking_status ELSE 'confirmed'::booking_status END, $6) RETURNING id, user_id, start_time, status, notes, created_at, updated_at, created_by, deposit_required - `, userID, req.StartTime, req.Notes, createdBy, depositRequiredSnapshot, sql.NullString{String: idempotencyKey, Valid: idempotencyKey != ""}, discountEligible).Scan( + `, userID, req.StartTime, req.Notes, createdBy, depositRequiredSnapshot, sql.NullString{String: idempotencyKey, Valid: idempotencyKey != ""}).Scan( &booking.ID, &booking.User.ID, &booking.StartTime, &booking.Status, &booking.Notes, &booking.CreatedAt, &booking.UpdatedAt, &booking.CreatedBy, &booking.DepositRequired, diff --git a/backend/handlers/bookings/discount_test.go b/backend/handlers/bookings/discount_test.go index 4934488..a4151bc 100644 --- a/backend/handlers/bookings/discount_test.go +++ b/backend/handlers/bookings/discount_test.go @@ -688,63 +688,6 @@ func TestDiscount_CampaignMaxRedemptions(t *testing.T) { assert.Equal(t, 0, discountCount2, "Expected no discount on second booking (max reached)") } -// ============================================================================= -// Discount Eligibility Flag Tests -// ============================================================================= - -// TestDiscount_EligibilityFlag tests that booking.discount_eligible is set correctly -func TestDiscount_EligibilityFlag(t *testing.T) { - cleanup := setupTestDB(t) - defer cleanup() - - seedDefaultWorkingHours(t) - - // Test case 1: User with pending redemption should be eligible - userID1 := createTestUser(t, 10) - - // Create pending redemption - ctx := context.Background() - _, err := db.DB.Exec(ctx, ` - INSERT INTO loyalty_redemptions (user_id, stamps_redeemed, status, redeemed_at) - VALUES ($1, 10, 'pending', NOW()) - `, userID1) - require.NoError(t, err) - - serviceID := createTestService(t, 50.00) - - // Create a new booking - bookingID1 := createPendingBooking(t, userID1, serviceID, time.Now().Add(24*time.Hour)) - - // Check discount_eligible flag - var discountEligible1 bool - err = db.DB.QueryRow(context.Background(), `SELECT discount_eligible FROM bookings WHERE id = $1`, bookingID1).Scan(&discountEligible1) - require.NoError(t, err) - assert.True(t, discountEligible1, "Expected discount_eligible = true for user with pending redemption") - - // Test case 2: User with no pending redemption and no active campaigns should NOT be eligible - userID2 := createTestUser(t, 0) // No stamps, no pending redemption - - bookingID2 := createPendingBooking(t, userID2, serviceID, time.Now().Add(48*time.Hour)) - - var discountEligible2 bool - err = db.DB.QueryRow(context.Background(), `SELECT discount_eligible FROM bookings WHERE id = $1`, bookingID2).Scan(&discountEligible2) - require.NoError(t, err) - assert.False(t, discountEligible2, "Expected discount_eligible = false for user with no pending redemption and no campaigns") - - // Test case 3: User with active time-based campaign should be eligible - userID3 := createTestUser(t, 0) - - // Create active time-based campaign - _ = createTestCampaign(t, "Active Campaign", "time_based", 5.0, nil, nil, nil, nil) - - bookingID3 := createPendingBooking(t, userID3, serviceID, time.Now().Add(72*time.Hour)) - - var discountEligible3 bool - err = db.DB.QueryRow(context.Background(), `SELECT discount_eligible FROM bookings WHERE id = $1`, bookingID3).Scan(&discountEligible3) - require.NoError(t, err) - assert.True(t, discountEligible3, "Expected discount_eligible = true when active time-based campaign exists") -} - // ============================================================================= // Helper: Truncate discount-specific tables // ============================================================================= diff --git a/backend/handlers/bookings/manage.go b/backend/handlers/bookings/manage.go index 3b5bdbc..49b39e4 100644 --- a/backend/handlers/bookings/manage.go +++ b/backend/handlers/bookings/manage.go @@ -667,21 +667,6 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) { } - // Check discount eligibility - var discountEligible bool - var hasPendingRedemption bool - _ = db.DB.QueryRow(r.Context(), ` - SELECT EXISTS(SELECT 1 FROM loyalty_redemptions WHERE user_id = $1 AND status = 'pending' AND expires_at > NOW()) - `, req.UserID).Scan(&hasPendingRedemption) - var activeCampaigns int - _ = db.DB.QueryRow(r.Context(), ` - SELECT COUNT(*) FROM discount_campaigns - WHERE status = 'active' AND campaign_type = 'time_based' - AND start_date <= NOW() AND end_date >= NOW() - AND (max_redemptions IS NULL OR times_redeemed < max_redemptions) - `).Scan(&activeCampaigns) - discountEligible = hasPendingRedemption || activeCampaigns > 0 - tx, err := db.DB.Begin(r.Context()) if err != nil { log.Printf("Failed to start transaction: %v", err) @@ -698,10 +683,9 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) { status, notes, created_by, - idempotency_key, - discount_eligible + idempotency_key ) - VALUES ($1, $2, 'confirmed', $3, $4, $5, $6) + VALUES ($1, $2, 'confirmed', $3, $4, $5) RETURNING id, user_id, start_time, status, notes, created_at, updated_at, created_by ` @@ -716,7 +700,6 @@ func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) { req.Notes, adminID, sql.NullString{String: idempotencyKey, Valid: idempotencyKey != ""}, - discountEligible, ).Scan( &booking.ID, &booking.User.ID, diff --git a/frontend/src/lib/components/admin/DiscountsManagement.svelte b/frontend/src/lib/components/admin/DiscountsManagement.svelte index d4be52e..bf9b4e5 100644 --- a/frontend/src/lib/components/admin/DiscountsManagement.svelte +++ b/frontend/src/lib/components/admin/DiscountsManagement.svelte @@ -69,7 +69,7 @@ name: '', description: '', campaign_type: 'time_based' as 'time_based' | 'milestone', - discount_percent: 10, + discount_percent: 5, scope: 'all_bookings', start_date: '', end_date: '', @@ -124,7 +124,7 @@ name: '', description: '', campaign_type: 'time_based', - discount_percent: 10, + discount_percent: 5, scope: 'all_bookings', start_date: '', end_date: '', @@ -439,7 +439,7 @@ - + {editingCampaign ? 'Edit Campaign' : 'Create Campaign'} Configure discount rules and campaign parameters @@ -577,9 +577,9 @@
- Max Redemptions + Max Redemptions (campaign total) -

0 = unlimited

+

0 = unlimited across all users

diff --git a/frontend/src/lib/types/booking.ts b/frontend/src/lib/types/booking.ts index f609665..0639328 100644 --- a/frontend/src/lib/types/booking.ts +++ b/frontend/src/lib/types/booking.ts @@ -73,7 +73,6 @@ export interface BookingUser { date_of_birth?: string; account_role: string; loyalty_stamps?: number; - pending_redemption?: boolean; referral_code?: string; referral_code_uses?: number; created_at: string; @@ -121,15 +120,6 @@ export interface Booking { amount_paid: number; amount_due: number; duration_minutes: number; - discount_eligible?: boolean; -} - -export interface BookingListResponse { - bookings: Booking[]; - page: number; - per_page: number; - total: number; - total_pages: number; } export interface LoyaltyRedemption {