refactor(payments): replace inline total_amount queries with booking field
Simplify payment handlers by using bookings.total_amount computed column instead of inline UNION sub-queries calculating price totals from booking_services and booking_custom_services. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -139,17 +139,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
|
|||||||
|
|
||||||
var bookingTotal float64
|
var bookingTotal float64
|
||||||
db.Conn.QueryRow(ctx, `
|
db.Conn.QueryRow(ctx, `
|
||||||
SELECT COALESCE(SUM(price_val), 0) FROM (
|
SELECT total_amount FROM bookings WHERE id = $1
|
||||||
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
|
|
||||||
`, bookingID).Scan(&bookingTotal)
|
`, bookingID).Scan(&bookingTotal)
|
||||||
|
|
||||||
if bookingTotal <= 0 {
|
if bookingTotal <= 0 {
|
||||||
@@ -913,17 +903,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
var depositMet bool
|
var depositMet bool
|
||||||
tx.QueryRow(r.Context(), `
|
tx.QueryRow(r.Context(), `
|
||||||
WITH booking_total AS (
|
WITH booking_total AS (
|
||||||
SELECT COALESCE(SUM(price_val), 0) * 100 AS total_cents FROM (
|
SELECT total_amount * 100 AS total_cents FROM bookings WHERE id = $1
|
||||||
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
|
|
||||||
),
|
),
|
||||||
paid_total AS (
|
paid_total AS (
|
||||||
SELECT COALESCE(SUM(amount), 0) * 100 AS paid_cents
|
SELECT COALESCE(SUM(amount), 0) * 100 AS paid_cents
|
||||||
@@ -988,17 +968,7 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, user
|
|||||||
|
|
||||||
var bookingTotal float64
|
var bookingTotal float64
|
||||||
if err := db.Conn.QueryRow(ctx, `
|
if err := db.Conn.QueryRow(ctx, `
|
||||||
SELECT COALESCE(SUM(price_val), 0) FROM (
|
SELECT total_amount FROM bookings WHERE id = $1
|
||||||
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
|
|
||||||
`, bookingID).Scan(&bookingTotal); err != nil {
|
`, bookingID).Scan(&bookingTotal); err != nil {
|
||||||
log.Printf("Failed to calculate booking total for campaign check: %v", err)
|
log.Printf("Failed to calculate booking total for campaign check: %v", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"crussell/db"
|
"crussell/db"
|
||||||
"crussell/internal/validators"
|
"crussell/internal/validators"
|
||||||
"crussell/mw"
|
"crussell/mw"
|
||||||
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
@@ -33,8 +34,20 @@ func ApplyLoyaltyRedemption(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var bookingUserID string
|
var bookingUserID, bookingStatus string
|
||||||
if err := db.Conn.QueryRow(r.Context(), `SELECT user_id FROM bookings WHERE id = $1`, bookingID).Scan(&bookingUserID); err != nil {
|
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) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
http.Error(w, "Booking not found", http.StatusNotFound)
|
http.Error(w, "Booking not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
@@ -48,54 +61,24 @@ func ApplyLoyaltyRedemption(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 {
|
switch bookingStatus {
|
||||||
case "completed", "cancelled", "no_show", "deposit_lapsed":
|
case "completed", "cancelled", "no_show", "deposit_lapsed":
|
||||||
http.Error(w, "Booking is in a terminal state and cannot accept redemptions", http.StatusBadRequest)
|
http.Error(w, "Booking is in a terminal state and cannot accept redemptions", http.StatusBadRequest)
|
||||||
return
|
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 {
|
if realPaymentExists {
|
||||||
http.Error(w, "Loyalty must be redeemed on the first payment for this booking", http.StatusBadRequest)
|
http.Error(w, "Loyalty must be redeemed on the first payment for this booking", http.StatusBadRequest)
|
||||||
return
|
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 {
|
if loyaltyStamps < LoyaltyStampCost {
|
||||||
http.Error(w, "Insufficient loyalty stamps", http.StatusBadRequest)
|
http.Error(w, "Insufficient loyalty stamps", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var redemptionID string
|
if !redemptionID.Valid {
|
||||||
if err := db.Conn.QueryRow(r.Context(), `
|
http.Error(w, "No pending loyalty redemption found", http.StatusBadRequest)
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,17 +100,7 @@ func ApplyLoyaltyRedemption(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var bookingTotal float64
|
var bookingTotal float64
|
||||||
if err := tx.QueryRow(r.Context(), `
|
if err := tx.QueryRow(r.Context(), `
|
||||||
SELECT COALESCE(SUM(price_val), 0) FROM (
|
SELECT total_amount FROM bookings WHERE id = $1
|
||||||
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
|
|
||||||
`, bookingID).Scan(&bookingTotal); err != nil {
|
`, bookingID).Scan(&bookingTotal); err != nil {
|
||||||
log.Printf("Failed to calculate booking total: %v", err)
|
log.Printf("Failed to calculate booking total: %v", err)
|
||||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||||
|
|||||||
@@ -177,17 +177,7 @@ func (s *PaymentService) GetBookingPaymentSummary(ctx context.Context, bookingID
|
|||||||
|
|
||||||
var totalAmount float64
|
var totalAmount float64
|
||||||
err := db.Conn.QueryRow(ctx, `
|
err := db.Conn.QueryRow(ctx, `
|
||||||
SELECT COALESCE(SUM(price_val), 0) FROM (
|
SELECT total_amount FROM bookings WHERE id = $1
|
||||||
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
|
|
||||||
`, bookingID).Scan(&totalAmount)
|
`, bookingID).Scan(&totalAmount)
|
||||||
|
|
||||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
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)
|
COALESCE(pt.total_paid, 0)
|
||||||
FROM bookings b
|
FROM bookings b
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT booking_id, COALESCE(SUM(price_val), 0) AS total_amount FROM (
|
SELECT id, total_amount FROM bookings WHERE id = $1
|
||||||
SELECT bs.booking_id, COALESCE(bs.override_price, s.price) AS price_val
|
) bt ON b.id = bt.id
|
||||||
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
|
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT booking_id, SUM(amount) AS total_paid
|
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
|
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) {
|
func (s *PaymentService) GetBookingRemainingBalanceCents(ctx context.Context, bookingID string) (int64, error) {
|
||||||
var remainingCents int64
|
var remainingCents int64
|
||||||
err := db.Conn.QueryRow(ctx, `
|
err := db.Conn.QueryRow(ctx, `
|
||||||
WITH booking_total AS (
|
WITH booking_total AS (
|
||||||
SELECT COALESCE(SUM(price_val), 0) AS total_pounds FROM (
|
SELECT total_amount AS total_pounds FROM bookings WHERE id = $1
|
||||||
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
|
|
||||||
),
|
|
||||||
paid_total AS (
|
paid_total AS (
|
||||||
SELECT COALESCE(SUM(amount), 0) AS paid_pounds
|
SELECT COALESCE(SUM(amount), 0) AS paid_pounds
|
||||||
FROM payments
|
FROM payments
|
||||||
|
|||||||
Reference in New Issue
Block a user