fix(loyalty): correct stamp accumulation, one-per-day guard, and test coverage
- Handler: apply existing redemption BEFORE incrementing stamps (was creating
and applying redemption to same booking)
- Handler: guard stamp increment behind bookingTotal > 0 (free bookings don't
earn stamps)
- Handler: fix global milestone off-by-one (globalCount already includes
current booking since status updated before discount logic)
- Schema: chk_milestone constraint only requires milestone_unit for anniversary
type, not per_user_booking_count or global_booking_count
- Tests: rewrite from scratch with 12 focused tests:
- FullCycle, ExistingRedemptionApplies, OneStampPerDay, ZeroTotalNoStamp,
CycleRepeats, TimeBasedCampaign, PerUserMilestone, GlobalMilestone,
AnniversaryMilestone, LoyaltyPriority, NoDiscountOnZeroTotal,
CampaignMaxRedemptions
- Tests: fix path parameter extraction in makeProgressRequest
- Vendor: go mod tidy + vendor for testify dependency
This commit is contained in:
@@ -1626,32 +1626,6 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := db.DB.Exec(r.Context(), `
|
||||
UPDATE users
|
||||
SET loyalty_stamps = loyalty_stamps + 1
|
||||
WHERE id = $1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM bookings b
|
||||
WHERE b.user_id = users.id
|
||||
AND b.status = 'completed'
|
||||
AND b.updated_at >= CURRENT_DATE - INTERVAL '1 day'
|
||||
AND b.id != $2
|
||||
)
|
||||
`, booking.User.ID, bookingID); err != nil {
|
||||
log.Printf("Failed to add loyalty stamp for booking %s: %v", bookingID, err)
|
||||
}
|
||||
|
||||
var newStampCount int
|
||||
if err := db.DB.QueryRow(r.Context(), `SELECT loyalty_stamps FROM users WHERE id = $1`, booking.User.ID).Scan(&newStampCount); err == nil && newStampCount == 10 {
|
||||
_, err = db.DB.Exec(r.Context(), `
|
||||
INSERT INTO loyalty_redemptions (user_id, stamps_redeemed, status, redeemed_at)
|
||||
VALUES ($1, 10, 'pending', NOW())
|
||||
`, booking.User.ID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create loyalty redemption for user %s: %v", booking.User.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
var bookingTotal float64
|
||||
if err := db.DB.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(SUM(COALESCE(bs.override_price, s.price)), 0)
|
||||
@@ -1662,6 +1636,7 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("Failed to calculate booking total for %s: %v", bookingID, err)
|
||||
}
|
||||
|
||||
// Apply existing pending loyalty redemption (earned from previous 10 bookings)
|
||||
if bookingTotal > 0 {
|
||||
var redemptionID string
|
||||
if err := db.DB.QueryRow(r.Context(), `
|
||||
@@ -1692,6 +1667,36 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Increment stamps (max 1 per day, only for paid bookings)
|
||||
if bookingTotal > 0 {
|
||||
if _, err := db.DB.Exec(r.Context(), `
|
||||
UPDATE users
|
||||
SET loyalty_stamps = loyalty_stamps + 1
|
||||
WHERE id = $1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM bookings b
|
||||
WHERE b.user_id = users.id
|
||||
AND b.status = 'completed'
|
||||
AND b.updated_at >= CURRENT_DATE - INTERVAL '1 day'
|
||||
AND b.id != $2
|
||||
)
|
||||
`, booking.User.ID, bookingID); err != nil {
|
||||
log.Printf("Failed to add loyalty stamp for booking %s: %v", bookingID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create pending redemption when stamps reach 10
|
||||
var newStampCount int
|
||||
if err := db.DB.QueryRow(r.Context(), `SELECT loyalty_stamps FROM users WHERE id = $1`, booking.User.ID).Scan(&newStampCount); err == nil && newStampCount == 10 {
|
||||
_, err = db.DB.Exec(r.Context(), `
|
||||
INSERT INTO loyalty_redemptions (user_id, stamps_redeemed, status, redeemed_at)
|
||||
VALUES ($1, 10, 'pending', NOW())
|
||||
`, booking.User.ID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create loyalty redemption for user %s: %v", booking.User.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if bookingTotal > 0 {
|
||||
var hasLoyaltyDiscount bool
|
||||
_ = db.DB.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'loyalty')`, bookingID).Scan(&hasLoyaltyDiscount)
|
||||
@@ -1767,7 +1772,7 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'global_booking_count'
|
||||
AND milestone_value = $1
|
||||
AND (max_redemptions IS NULL OR times_redeemed < max_redemptions)
|
||||
`, globalCount+1).Scan(&globalCampaignID, &globalPercent)
|
||||
`, globalCount).Scan(&globalCampaignID, &globalPercent)
|
||||
|
||||
if globalCampaignID != "" {
|
||||
discountAmount := roundTo2(bookingTotal * globalPercent / 100)
|
||||
|
||||
Reference in New Issue
Block a user