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)
This commit is contained in:
2026-05-08 18:08:59 +01:00
parent 5c4041ded9
commit 79a794de58
5 changed files with 10 additions and 112 deletions
+3 -21
View File
@@ -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,
@@ -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
// =============================================================================
+2 -19
View File
@@ -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,