diff --git a/backend/handlers/payments/handlers.go b/backend/handlers/payments/handlers.go index 46381f9..227b684 100644 --- a/backend/handlers/payments/handlers.go +++ b/backend/handlers/payments/handlers.go @@ -139,17 +139,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri var bookingTotal float64 db.Conn.QueryRow(ctx, ` - SELECT COALESCE(SUM(price_val), 0) FROM ( - SELECT COALESCE(bs.override_price, s.price) AS price_val - FROM booking_services bs - JOIN services s ON bs.service_id = s.id - WHERE bs.booking_id = $1 - UNION ALL - SELECT COALESCE(bcs.override_price, cs.price) - FROM booking_custom_services bcs - JOIN custom_services cs ON bcs.custom_service_id = cs.id - WHERE bcs.booking_id = $1 - ) sub + SELECT total_amount FROM bookings WHERE id = $1 `, bookingID).Scan(&bookingTotal) if bookingTotal <= 0 { @@ -913,17 +903,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) { var depositMet bool tx.QueryRow(r.Context(), ` WITH booking_total AS ( - SELECT COALESCE(SUM(price_val), 0) * 100 AS total_cents FROM ( - SELECT COALESCE(bs.override_price, s.price) AS price_val - FROM booking_services bs - LEFT JOIN services s ON bs.service_id = s.id - WHERE bs.booking_id = $1 - UNION ALL - SELECT COALESCE(bcs.override_price, cs.price) - FROM booking_custom_services bcs - LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id - WHERE bcs.booking_id = $1 - ) price_sub + SELECT total_amount * 100 AS total_cents FROM bookings WHERE id = $1 ), paid_total AS ( SELECT COALESCE(SUM(amount), 0) * 100 AS paid_cents @@ -988,17 +968,7 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, user var bookingTotal float64 if err := db.Conn.QueryRow(ctx, ` - SELECT COALESCE(SUM(price_val), 0) FROM ( - SELECT COALESCE(bs.override_price, s.price) AS price_val - FROM booking_services bs - JOIN services s ON bs.service_id = s.id - WHERE bs.booking_id = $1 - UNION ALL - SELECT COALESCE(bcs.override_price, cs.price) - FROM booking_custom_services bcs - JOIN custom_services cs ON bcs.custom_service_id = cs.id - WHERE bcs.booking_id = $1 - ) sub + SELECT total_amount FROM bookings WHERE id = $1 `, bookingID).Scan(&bookingTotal); err != nil { log.Printf("Failed to calculate booking total for campaign check: %v", err) return diff --git a/backend/handlers/payments/loyalty.go b/backend/handlers/payments/loyalty.go index f707b1d..a406ba4 100644 --- a/backend/handlers/payments/loyalty.go +++ b/backend/handlers/payments/loyalty.go @@ -4,6 +4,7 @@ import ( "crussell/db" "crussell/internal/validators" "crussell/mw" + "database/sql" "encoding/json" "errors" "log" @@ -33,8 +34,20 @@ func ApplyLoyaltyRedemption(w http.ResponseWriter, r *http.Request) { return } - var bookingUserID string - if err := db.Conn.QueryRow(r.Context(), `SELECT user_id FROM bookings WHERE id = $1`, bookingID).Scan(&bookingUserID); err != nil { + var bookingUserID, bookingStatus string + var realPaymentExists bool + var loyaltyStamps int + var redemptionID sql.NullString + err := db.Conn.QueryRow(r.Context(), ` + SELECT b.user_id, b.status, + COALESCE(EXISTS(SELECT 1 FROM payments WHERE booking_id = b.id AND payment_method != 'discount'), false) AS payment_exists, + u.loyalty_stamps, + (SELECT lr.id FROM loyalty_redemptions lr WHERE lr.user_id = u.id AND lr.status = 'pending' AND lr.expires_at > NOW() ORDER BY lr.redeemed_at ASC LIMIT 1) AS redemption_id + FROM bookings b + JOIN users u ON u.id = b.user_id + WHERE b.id = $1 + `, bookingID).Scan(&bookingUserID, &bookingStatus, &realPaymentExists, &loyaltyStamps, &redemptionID) + if err != nil { if errors.Is(err, pgx.ErrNoRows) { http.Error(w, "Booking not found", http.StatusNotFound) return @@ -48,54 +61,24 @@ func ApplyLoyaltyRedemption(w http.ResponseWriter, r *http.Request) { return } - var bookingStatus string - if err := db.Conn.QueryRow(r.Context(), `SELECT status FROM bookings WHERE id = $1`, bookingID).Scan(&bookingStatus); err != nil { - log.Printf("Failed to get booking status: %v", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } switch bookingStatus { case "completed", "cancelled", "no_show", "deposit_lapsed": http.Error(w, "Booking is in a terminal state and cannot accept redemptions", http.StatusBadRequest) return } - // Loyalty redemption must be on the first payment — reject if a real payment - // (non-discount) already exists on this booking. - var realPaymentExists bool - db.Conn.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM payments WHERE booking_id = $1 AND payment_method != 'discount')`, bookingID).Scan(&realPaymentExists) if realPaymentExists { http.Error(w, "Loyalty must be redeemed on the first payment for this booking", http.StatusBadRequest) return } - var loyaltyStamps int - if err := db.Conn.QueryRow(r.Context(), `SELECT loyalty_stamps FROM users WHERE id = $1`, userID).Scan(&loyaltyStamps); err != nil { - if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "User not found", http.StatusNotFound) - return - } - log.Printf("Failed to get loyalty stamps: %v", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } if loyaltyStamps < LoyaltyStampCost { http.Error(w, "Insufficient loyalty stamps", http.StatusBadRequest) return } - var redemptionID string - if err := db.Conn.QueryRow(r.Context(), ` - SELECT id FROM loyalty_redemptions - WHERE user_id = $1 AND status = 'pending' AND expires_at > NOW() - ORDER BY redeemed_at ASC LIMIT 1 - `, userID).Scan(&redemptionID); err != nil { - if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "No pending loyalty redemption found", http.StatusBadRequest) - return - } - log.Printf("Failed to check loyalty redemption: %v", err) - http.Error(w, "internal server error", http.StatusInternalServerError) + if !redemptionID.Valid { + http.Error(w, "No pending loyalty redemption found", http.StatusBadRequest) return } @@ -117,17 +100,7 @@ func ApplyLoyaltyRedemption(w http.ResponseWriter, r *http.Request) { var bookingTotal float64 if err := tx.QueryRow(r.Context(), ` - SELECT COALESCE(SUM(price_val), 0) FROM ( - SELECT COALESCE(bs.override_price, s.price) AS price_val - FROM booking_services bs - JOIN services s ON bs.service_id = s.id - WHERE bs.booking_id = $1 - UNION ALL - SELECT COALESCE(bcs.override_price, cs.price) - FROM booking_custom_services bcs - JOIN custom_services cs ON bcs.custom_service_id = cs.id - WHERE bcs.booking_id = $1 - ) sub + SELECT total_amount FROM bookings WHERE id = $1 `, bookingID).Scan(&bookingTotal); err != nil { log.Printf("Failed to calculate booking total: %v", err) http.Error(w, "internal server error", http.StatusInternalServerError) diff --git a/backend/handlers/payments/service.go b/backend/handlers/payments/service.go index 3ea8e88..3b47195 100644 --- a/backend/handlers/payments/service.go +++ b/backend/handlers/payments/service.go @@ -177,17 +177,7 @@ func (s *PaymentService) GetBookingPaymentSummary(ctx context.Context, bookingID var totalAmount float64 err := db.Conn.QueryRow(ctx, ` - SELECT COALESCE(SUM(price_val), 0) FROM ( - SELECT COALESCE(bs.override_price, s.price) AS price_val - FROM booking_services bs - JOIN services s ON bs.service_id = s.id - WHERE bs.booking_id = $1 - UNION ALL - SELECT COALESCE(bcs.override_price, cs.price) - FROM booking_custom_services bcs - JOIN custom_services cs ON bcs.custom_service_id = cs.id - WHERE bcs.booking_id = $1 - ) price_sub + SELECT total_amount FROM bookings WHERE id = $1 `, bookingID).Scan(&totalAmount) if err != nil && !errors.Is(err, pgx.ErrNoRows) { @@ -385,14 +375,8 @@ func (s *PaymentService) GetBookingPaymentInfo(ctx context.Context, bookingID st COALESCE(pt.total_paid, 0) FROM bookings b LEFT JOIN ( - SELECT booking_id, COALESCE(SUM(price_val), 0) AS total_amount FROM ( - SELECT bs.booking_id, COALESCE(bs.override_price, s.price) AS price_val - FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = $1 - UNION ALL - SELECT bcs.booking_id, COALESCE(bcs.override_price, cs.price) - FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = $1 - ) sub GROUP BY booking_id - ) bt ON b.id = bt.booking_id + SELECT id, total_amount FROM bookings WHERE id = $1 + ) bt ON b.id = bt.id LEFT JOIN ( SELECT booking_id, SUM(amount) AS total_paid FROM payments WHERE booking_id = $1 AND status = 'completed' AND payment_method NOT IN ('discount', 'on_the_house') GROUP BY booking_id @@ -417,19 +401,9 @@ func (s *PaymentService) GetBookingUserID(ctx context.Context, bookingID string) func (s *PaymentService) GetBookingRemainingBalanceCents(ctx context.Context, bookingID string) (int64, error) { var remainingCents int64 err := db.Conn.QueryRow(ctx, ` - WITH booking_total AS ( - SELECT COALESCE(SUM(price_val), 0) AS total_pounds FROM ( - SELECT COALESCE(bs.override_price, s.price) AS price_val - FROM booking_services bs - LEFT JOIN services s ON bs.service_id = s.id - WHERE bs.booking_id = $1 - UNION ALL - SELECT COALESCE(bcs.override_price, cs.price) - FROM booking_custom_services bcs - LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id - WHERE bcs.booking_id = $1 - ) price_sub - ), + WITH booking_total AS ( + SELECT total_amount AS total_pounds FROM bookings WHERE id = $1 + ), paid_total AS ( SELECT COALESCE(SUM(amount), 0) AS paid_pounds FROM payments