feat(backend): add referral discount preview and transactional campaign safety

Add referral discount to payment preview for users with unused referral discounts.

- Check referral_discounts table for unused discounts in calculateDiscountPreview
- Wrap campaign discount application in a proper database transaction
- Apply eligibility guard for 2nd+ payments to prevent duplicate discount application

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-20 16:58:46 +01:00
co-authored by Sisyphus
parent 8efb8d93bf
commit 0beaa6e117
+92 -26
View File
@@ -265,6 +265,28 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri
}
}
// Check for referrer's unused referral discount
var rdID string
var rdPercent float64
if err := db.DB.QueryRow(ctx, `
SELECT id, discount_percent FROM referral_discounts
WHERE user_id = $1 AND used = FALSE
LIMIT 1
`, userID).Scan(&rdID, &rdPercent); err == nil && rdID != "" {
exists := 0
db.DB.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'referral' AND source_id = $2`, bookingID, rdID).Scan(&exists)
if exists == 0 {
amount := roundTo2(bookingTotal * rdPercent / 100)
resp.Discounts = append(resp.Discounts, DiscountPreview{
Source: "referral",
Name: "Referral Discount (10%)",
Percent: rdPercent,
Amount: amount,
})
discountTotal += amount
}
}
resp.Eligible = len(resp.Discounts) > 0
resp.DiscountedTotal = roundTo2(bookingTotal - discountTotal)
return resp
@@ -954,8 +976,12 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, user
SELECT COUNT(*) FROM payments
WHERE booking_id = $1 AND status = 'completed' AND payment_method NOT IN ('discount', 'on_the_house')
`, bookingID).Scan(&existingPayment)
// Only block if this is the 2nd+ payment — the first payment should still
// trigger discount application. Subsequent payments should not add new discounts.
// Only block if this is the 2nd+ real payment — the first payment should still
// trigger discount application (existingPayment counts already-completed payments).
// When called before payment commit (line 850), existingPayment=0 so discounts
// are applied. When called after commit (line 952), existingPayment=1 and the
// idempotency check handles it. At the 2nd+ payment attempt, this guard prevents
// applying any new discounts.
if existingPayment >= 2 {
return
}
@@ -981,9 +1007,16 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, user
return
}
tx, err := db.DB.Begin(ctx)
if err != nil {
log.Printf("Failed to begin discount application transaction: %v", err)
return
}
defer tx.Rollback(ctx)
var campaignID string
var campaignPercent float64
if err := db.DB.QueryRow(ctx, `
if err := tx.QueryRow(ctx, `
SELECT id, discount_percent FROM discount_campaigns
WHERE status = 'active' AND campaign_type = 'time_based'
AND start_date <= NOW() AND end_date >= NOW()
@@ -991,20 +1024,20 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, user
ORDER BY discount_percent DESC LIMIT 1
`).Scan(&campaignID, &campaignPercent); err == nil && campaignID != "" {
var exists int
db.DB.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, campaignID).Scan(&exists)
tx.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, campaignID).Scan(&exists)
if exists == 0 {
discountAmount := roundTo2(bookingTotal * campaignPercent / 100)
if _, err := db.DB.Exec(ctx, `
if _, err := tx.Exec(ctx, `
INSERT INTO booking_discounts (booking_id, user_id, discount_source, source_id, campaign_type, milestone_type, discount_percent, original_total, discount_amount)
VALUES ($1, $2, 'campaign', $3, 'time_based', NULL, $4, $5, $6)
`, bookingID, userID, campaignID, campaignPercent, bookingTotal, discountAmount); err != nil {
log.Printf("Failed to insert time-based campaign discount: %v", err)
} else {
db.DB.Exec(ctx, `
tx.Exec(ctx, `
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
`, bookingID, discountAmount, userID)
db.DB.Exec(ctx, `
tx.Exec(ctx, `
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
`, campaignID)
}
@@ -1012,11 +1045,11 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, user
}
var userBookingCount int
db.DB.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&userBookingCount)
tx.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&userBookingCount)
var milestoneCampaignID string
var milestonePercent float64
db.DB.QueryRow(ctx, `
tx.QueryRow(ctx, `
SELECT id, discount_percent FROM discount_campaigns
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'per_user_booking_count'
AND milestone_value = $1
@@ -1025,20 +1058,20 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, user
if milestoneCampaignID != "" {
var exists int
db.DB.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, milestoneCampaignID).Scan(&exists)
tx.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, milestoneCampaignID).Scan(&exists)
if exists == 0 {
discountAmount := roundTo2(bookingTotal * milestonePercent / 100)
if _, err := db.DB.Exec(ctx, `
if _, err := tx.Exec(ctx, `
INSERT INTO booking_discounts (booking_id, user_id, discount_source, source_id, campaign_type, milestone_type, discount_percent, original_total, discount_amount)
VALUES ($1, $2, 'campaign', $3, 'milestone', 'per_user_booking_count', $4, $5, $6)
`, bookingID, userID, milestoneCampaignID, milestonePercent, bookingTotal, discountAmount); err != nil {
log.Printf("Failed to insert per-user milestone discount: %v", err)
} else {
db.DB.Exec(ctx, `
tx.Exec(ctx, `
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
`, bookingID, discountAmount, userID)
db.DB.Exec(ctx, `
tx.Exec(ctx, `
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
`, milestoneCampaignID)
}
@@ -1046,9 +1079,9 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, user
}
var firstVisitDate time.Time
db.DB.QueryRow(ctx, `SELECT MIN(start_time) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&firstVisitDate)
tx.QueryRow(ctx, `SELECT MIN(start_time) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&firstVisitDate)
if !firstVisitDate.IsZero() {
annRows, err := db.DB.Query(ctx, `
annRows, err := tx.Query(ctx, `
SELECT id, discount_percent, milestone_value, milestone_unit FROM discount_campaigns
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'anniversary'
AND NOT EXISTS (SELECT 1 FROM booking_discounts WHERE user_id = $1 AND source_id = discount_campaigns.id AND milestone_type = 'anniversary')
@@ -1071,7 +1104,7 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, user
for _, c := range campaigns {
var exists int
db.DB.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, c.id).Scan(&exists)
tx.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, c.id).Scan(&exists)
if exists > 0 {
continue
}
@@ -1088,17 +1121,17 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, user
}
if matches {
discountAmount := roundTo2(bookingTotal * c.pct / 100)
if _, err := db.DB.Exec(ctx, `
if _, err := tx.Exec(ctx, `
INSERT INTO booking_discounts (booking_id, user_id, discount_source, source_id, campaign_type, milestone_type, discount_percent, original_total, discount_amount)
VALUES ($1, $2, 'campaign', $3, 'milestone', 'anniversary', $4, $5, $6)
`, bookingID, userID, c.id, c.pct, bookingTotal, discountAmount); err != nil {
log.Printf("Failed to insert anniversary discount: %v", err)
} else {
db.DB.Exec(ctx, `
tx.Exec(ctx, `
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
`, bookingID, discountAmount, userID)
db.DB.Exec(ctx, `
tx.Exec(ctx, `
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
`, c.id)
}
@@ -1111,15 +1144,15 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, user
}
var firstPaymentMethod string
if err := db.DB.QueryRow(ctx, `
if err := tx.QueryRow(ctx, `
SELECT payment_method FROM payments WHERE booking_id = $1 AND payment_method NOT IN ('discount', 'on_the_house') ORDER BY created_at ASC LIMIT 1
`, bookingID).Scan(&firstPaymentMethod); err == nil && firstPaymentMethod == "in_person_card" {
var globalCount int
db.DB.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE status = 'completed'`).Scan(&globalCount)
tx.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE status = 'completed'`).Scan(&globalCount)
var globalCampaignID string
var globalPercent float64
db.DB.QueryRow(ctx, `
tx.QueryRow(ctx, `
SELECT id, discount_percent FROM discount_campaigns
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'global_booking_count'
AND milestone_value <= $1
@@ -1130,26 +1163,59 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, bookingID string, user
if globalCampaignID != "" {
var exists int
db.DB.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, globalCampaignID).Scan(&exists)
tx.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, globalCampaignID).Scan(&exists)
if exists == 0 {
discountAmount := roundTo2(bookingTotal * globalPercent / 100)
if _, err := db.DB.Exec(ctx, `
if _, err := tx.Exec(ctx, `
INSERT INTO booking_discounts (booking_id, user_id, discount_source, source_id, campaign_type, milestone_type, discount_percent, original_total, discount_amount)
VALUES ($1, $2, 'campaign', $3, 'milestone', 'global_booking_count', $4, $5, $6)
`, bookingID, userID, globalCampaignID, globalPercent, bookingTotal, discountAmount); err != nil {
log.Printf("Failed to insert global milestone discount: %v", err)
} else {
db.DB.Exec(ctx, `
tx.Exec(ctx, `
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
`, bookingID, discountAmount, userID)
db.DB.Exec(ctx, `
tx.Exec(ctx, `
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
`, globalCampaignID)
}
}
}
}
// Apply referrer's referral discount if available
if bookingTotal > 0 {
var rdID string
var rdPercent float64
if err := tx.QueryRow(ctx, `
SELECT id, discount_percent FROM referral_discounts
WHERE user_id = $1 AND used = FALSE
LIMIT 1
`, userID).Scan(&rdID, &rdPercent); err == nil && rdID != "" {
exists := 0
tx.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'referral' AND source_id = $2`, bookingID, rdID).Scan(&exists)
if exists == 0 {
discountAmount := roundTo2(bookingTotal * rdPercent / 100)
if _, err := tx.Exec(ctx, `
INSERT INTO booking_discounts (booking_id, user_id, discount_source, source_id, campaign_type, milestone_type, discount_percent, original_total, discount_amount)
VALUES ($1, $2, 'referral', $3, NULL, NULL, $4, $5, $6)
`, bookingID, userID, rdID, rdPercent, bookingTotal, discountAmount); err == nil {
tx.Exec(ctx, `
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
`, bookingID, discountAmount, userID)
tx.Exec(ctx, `
UPDATE referral_discounts SET used = TRUE, used_at = NOW() WHERE id = $1
`, rdID)
}
}
}
}
if err := tx.Commit(ctx); err != nil {
log.Printf("Failed to commit discount application: %v", err)
}
}
// buildSplitRecords determines whether to split a single Square charge into