chore: run go fix for Go 1.26 modernization
CI / Go vulnerabilities (push) Successful in 1m10s
CI / Build & Vet (push) Successful in 1m39s
CI / Frontend build (gate) (push) Successful in 1m42s
CI / Frontend QC (audit) (push) Successful in 56s
CI / Frontend QC (typecheck) (push) Successful in 1m36s
CI / Frontend QC (lint) (push) Successful in 1m51s
CI / Tests (prod) (push) Has been cancelled
CI / Tests (dev) (push) Has been cancelled
CI / Race (prod) (push) Has been cancelled
CI / Race (dev) (push) Has been cancelled
CI / Go vulnerabilities (push) Successful in 1m10s
CI / Build & Vet (push) Successful in 1m39s
CI / Frontend build (gate) (push) Successful in 1m42s
CI / Frontend QC (audit) (push) Successful in 56s
CI / Frontend QC (typecheck) (push) Successful in 1m36s
CI / Frontend QC (lint) (push) Successful in 1m51s
CI / Tests (prod) (push) Has been cancelled
CI / Tests (dev) (push) Has been cancelled
CI / Race (prod) (push) Has been cancelled
CI / Race (dev) (push) Has been cancelled
106 files: interface{}→any, strings.Split→SplitSeq, CutPrefix/Cut, strings.Builder, slices.Contains, remove redundant // +build directives, gofmt import ordering and indentation.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -2,8 +2,8 @@ package bookings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crussell/db"
|
||||
"crussell/clock"
|
||||
"crussell/db"
|
||||
"crussell/handlers/notifications"
|
||||
"crussell/handlers/payments"
|
||||
"crussell/handlers/scheduling"
|
||||
@@ -254,20 +254,20 @@ type AdminBookingSummary struct {
|
||||
}
|
||||
|
||||
type UserSummary struct {
|
||||
ID string `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
FullName string `json:"full_name"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
ProfilePicURL *string `json:"profile_pic_url,omitempty"`
|
||||
DateOfBirth *string `json:"date_of_birth,omitempty"`
|
||||
AccountRole string `json:"account_role"`
|
||||
LoyaltyStamps *int `json:"loyalty_stamps,omitempty"`
|
||||
ReferralCode *string `json:"referral_code,omitempty"`
|
||||
ReferralCodeUses *int `json:"referral_code_uses,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000000"`
|
||||
ID string `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
FullName string `json:"full_name"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
ProfilePicURL *string `json:"profile_pic_url,omitempty"`
|
||||
DateOfBirth *string `json:"date_of_birth,omitempty"`
|
||||
AccountRole string `json:"account_role"`
|
||||
LoyaltyStamps *int `json:"loyalty_stamps,omitempty"`
|
||||
ReferralCode *string `json:"referral_code,omitempty"`
|
||||
ReferralCodeUses *int `json:"referral_code_uses,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000000"`
|
||||
PreviousFirstName *string `json:"previous_first_name,omitempty"`
|
||||
PreviousLastName *string `json:"previous_last_name,omitempty"`
|
||||
}
|
||||
@@ -387,7 +387,7 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Build WHERE conditions shared between count and data queries.
|
||||
whereClause := " WHERE b.user_id = $1"
|
||||
whereArgs := []interface{}{userID}
|
||||
whereArgs := []any{userID}
|
||||
paramCount := 2
|
||||
|
||||
if req.Status != nil {
|
||||
@@ -447,7 +447,7 @@ func GetAllUserBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
WHERE booking_id = b.id
|
||||
) pt ON true` + whereClause
|
||||
|
||||
dataArgs := make([]interface{}, len(whereArgs))
|
||||
dataArgs := make([]any, len(whereArgs))
|
||||
copy(dataArgs, whereArgs)
|
||||
dataParamCount := paramCount
|
||||
|
||||
@@ -683,7 +683,7 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
) pre_pay ON true
|
||||
`
|
||||
|
||||
var args []interface{}
|
||||
var args []any
|
||||
paramCount := 1
|
||||
|
||||
addWhereClause := func(condition string) {
|
||||
@@ -750,7 +750,7 @@ func GetAllAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var total int
|
||||
{
|
||||
countWhere := ""
|
||||
var countArgs []interface{}
|
||||
var countArgs []any
|
||||
cp := 1
|
||||
addCountWhere := func(cond string) {
|
||||
if cp == 1 {
|
||||
@@ -954,7 +954,7 @@ func GetAllBookingsByUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
FROM bookings b
|
||||
WHERE b.user_id = $1
|
||||
`
|
||||
var args []interface{}
|
||||
var args []any
|
||||
args = append(args, userID)
|
||||
paramCount := 2
|
||||
|
||||
@@ -1793,7 +1793,7 @@ func SearchAdminBookingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
WHERE bcs.booking_id = b.id AND cs.name ILIKE $1 ESCAPE '\'
|
||||
)
|
||||
`
|
||||
var args []interface{}
|
||||
var args []any
|
||||
args = append(args, searchPattern)
|
||||
paramCount := 2
|
||||
|
||||
@@ -2639,9 +2639,9 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if currentStatus == "completed" {
|
||||
log.Printf("Booking %s is already completed — skipping duplicate completion", bookingID)
|
||||
} else {
|
||||
// Collect patch test IDs first so the rows are consumed before INSERT operations.
|
||||
var patchTestIDs []string
|
||||
ptRows, err := tx.Query(r.Context(), `
|
||||
// Collect patch test IDs first so the rows are consumed before INSERT operations.
|
||||
var patchTestIDs []string
|
||||
ptRows, err := tx.Query(r.Context(), `
|
||||
SELECT DISTINCT pt.id
|
||||
FROM patch_tests pt
|
||||
JOIN booking_services bs ON bs.booking_id = $1
|
||||
@@ -2649,42 +2649,42 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
SELECT pt_inner.id FROM patch_tests pt_inner WHERE bs.service_id = ANY(pt_inner.service_ids)
|
||||
)
|
||||
`, bookingID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch patch tests for booking %s: %v", bookingID, err)
|
||||
} else {
|
||||
for ptRows.Next() {
|
||||
var ptID string
|
||||
if err := ptRows.Scan(&ptID); err == nil {
|
||||
patchTestIDs = append(patchTestIDs, ptID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to fetch patch tests for booking %s: %v", bookingID, err)
|
||||
} else {
|
||||
for ptRows.Next() {
|
||||
var ptID string
|
||||
if err := ptRows.Scan(&ptID); err == nil {
|
||||
patchTestIDs = append(patchTestIDs, ptID)
|
||||
}
|
||||
}
|
||||
ptRows.Close()
|
||||
}
|
||||
ptRows.Close()
|
||||
}
|
||||
for _, ptID := range patchTestIDs {
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
for _, ptID := range patchTestIDs {
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
INSERT INTO user_patch_tests (user_id, patch_test_id, tested_at)
|
||||
VALUES ($1, $2, NOW())
|
||||
ON CONFLICT (user_id, patch_test_id) DO UPDATE SET tested_at = NOW()
|
||||
`, booking.User.ID, ptID); err != nil {
|
||||
log.Printf("Failed to update patch test validity for user %s, patch test %s: %v", booking.User.ID, ptID, err)
|
||||
log.Printf("Failed to update patch test validity for user %s, patch test %s: %v", booking.User.ID, ptID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var bookingTotal float64
|
||||
if err := tx.QueryRow(r.Context(), `
|
||||
var bookingTotal float64
|
||||
if err := tx.QueryRow(r.Context(), `
|
||||
SELECT total_amount FROM bookings WHERE id = $1
|
||||
`, bookingID).Scan(&bookingTotal); err != nil {
|
||||
log.Printf("Failed to calculate booking total for %s: %v", bookingID, err)
|
||||
}
|
||||
log.Printf("Failed to calculate booking total for %s: %v", bookingID, err)
|
||||
}
|
||||
|
||||
// Don't award a stamp if this booking already used a loyalty redemption
|
||||
// (take or receive, never both).
|
||||
var loyaltyAppliedOnThisBooking bool
|
||||
tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'loyalty')`, bookingID).Scan(&loyaltyAppliedOnThisBooking)
|
||||
// Don't award a stamp if this booking already used a loyalty redemption
|
||||
// (take or receive, never both).
|
||||
var loyaltyAppliedOnThisBooking bool
|
||||
tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'loyalty')`, bookingID).Scan(&loyaltyAppliedOnThisBooking)
|
||||
|
||||
var newStampCount int
|
||||
if bookingTotal > 0 && !loyaltyAppliedOnThisBooking {
|
||||
if err := tx.QueryRow(r.Context(), `
|
||||
var newStampCount int
|
||||
if bookingTotal > 0 && !loyaltyAppliedOnThisBooking {
|
||||
if err := tx.QueryRow(r.Context(), `
|
||||
UPDATE users
|
||||
SET loyalty_stamps = loyalty_stamps + 1
|
||||
WHERE id = $1
|
||||
@@ -2697,108 +2697,108 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
)
|
||||
RETURNING loyalty_stamps
|
||||
`, booking.User.ID, bookingID).Scan(&newStampCount); err != nil {
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
log.Printf("Failed to add loyalty stamp for booking %s: %v", bookingID, err)
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
log.Printf("Failed to add loyalty stamp for booking %s: %v", bookingID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create pending redemption when stamps reach LoyaltyStampCost
|
||||
if newStampCount == payments.LoyaltyStampCost {
|
||||
_, err = tx.Exec(r.Context(), `
|
||||
// Create pending redemption when stamps reach LoyaltyStampCost
|
||||
if newStampCount == payments.LoyaltyStampCost {
|
||||
_, err = tx.Exec(r.Context(), `
|
||||
INSERT INTO loyalty_redemptions (user_id, stamps_redeemed, status, redeemed_at)
|
||||
VALUES ($1, $2, 'pending', NOW())
|
||||
`, booking.User.ID, payments.LoyaltyStampCost)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create loyalty redemption for user %s: %v", booking.User.ID, err)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create loyalty redemption for user %s: %v", booking.User.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip time-based campaign if already applied at payment time
|
||||
var timeBasedApplied bool
|
||||
tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'campaign' AND campaign_type = 'time_based')`, bookingID).Scan(&timeBasedApplied)
|
||||
if bookingTotal > 0 && !timeBasedApplied {
|
||||
var campaignID string
|
||||
var campaignPercent float64
|
||||
if err := tx.QueryRow(r.Context(), `
|
||||
// Skip time-based campaign if already applied at payment time
|
||||
var timeBasedApplied bool
|
||||
tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'campaign' AND campaign_type = 'time_based')`, bookingID).Scan(&timeBasedApplied)
|
||||
if bookingTotal > 0 && !timeBasedApplied {
|
||||
var campaignID string
|
||||
var campaignPercent float64
|
||||
if err := tx.QueryRow(r.Context(), `
|
||||
SELECT id, discount_percent 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)
|
||||
ORDER BY discount_percent DESC LIMIT 1
|
||||
`).Scan(&campaignID, &campaignPercent); err == nil && campaignID != "" {
|
||||
discountAmount := roundTo2(bookingTotal * campaignPercent / 100)
|
||||
discountAmount := roundTo2(bookingTotal * campaignPercent / 100)
|
||||
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
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, booking.User.ID, campaignID, campaignPercent, bookingTotal, discountAmount); err != nil {
|
||||
log.Printf("ALERT: failed to insert booking discount: %v", err)
|
||||
}
|
||||
log.Printf("ALERT: failed to insert booking discount: %v", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
|
||||
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
|
||||
`, bookingID, discountAmount, booking.User.ID); err != nil {
|
||||
log.Printf("ALERT: failed to insert payment record: %v", err)
|
||||
}
|
||||
log.Printf("ALERT: failed to insert payment record: %v", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
|
||||
`, campaignID); err != nil {
|
||||
log.Printf("ALERT: failed to update discount campaign usage: %v", err)
|
||||
log.Printf("ALERT: failed to update discount campaign usage: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bookingTotal > 0 {
|
||||
var userBookingCount int
|
||||
_ = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, booking.User.ID).Scan(&userBookingCount)
|
||||
if bookingTotal > 0 {
|
||||
var userBookingCount int
|
||||
_ = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, booking.User.ID).Scan(&userBookingCount)
|
||||
|
||||
var milestoneCampaignID string
|
||||
var milestonePercent float64
|
||||
_ = tx.QueryRow(r.Context(), `
|
||||
var milestoneCampaignID string
|
||||
var milestonePercent float64
|
||||
_ = tx.QueryRow(r.Context(), `
|
||||
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
|
||||
AND NOT EXISTS (SELECT 1 FROM booking_discounts WHERE user_id = $2 AND source_id = discount_campaigns.id)
|
||||
`, userBookingCount, booking.User.ID).Scan(&milestoneCampaignID, &milestonePercent)
|
||||
|
||||
if milestoneCampaignID != "" {
|
||||
discountAmount := roundTo2(bookingTotal * milestonePercent / 100)
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
if milestoneCampaignID != "" {
|
||||
discountAmount := roundTo2(bookingTotal * milestonePercent / 100)
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
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, booking.User.ID, milestoneCampaignID, milestonePercent, bookingTotal, discountAmount); err != nil {
|
||||
log.Printf("ALERT: failed to insert booking discount: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
log.Printf("ALERT: failed to insert booking discount: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
|
||||
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
|
||||
`, bookingID, discountAmount, booking.User.ID); err != nil {
|
||||
log.Printf("ALERT: failed to insert payment record: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
log.Printf("ALERT: failed to insert payment record: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
|
||||
`, milestoneCampaignID); err != nil {
|
||||
log.Printf("ALERT: failed to insert payment record: %v", err)
|
||||
log.Printf("ALERT: failed to insert payment record: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var globalMilestoneApplied bool
|
||||
tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'campaign' AND campaign_type = 'milestone' AND milestone_type = 'global_booking_count')`, bookingID).Scan(&globalMilestoneApplied)
|
||||
if !globalMilestoneApplied {
|
||||
var globalCount int
|
||||
_ = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE status = 'completed'`).Scan(&globalCount)
|
||||
var globalMilestoneApplied bool
|
||||
tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND discount_source = 'campaign' AND campaign_type = 'milestone' AND milestone_type = 'global_booking_count')`, bookingID).Scan(&globalMilestoneApplied)
|
||||
if !globalMilestoneApplied {
|
||||
var globalCount int
|
||||
_ = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM bookings WHERE status = 'completed'`).Scan(&globalCount)
|
||||
|
||||
var hasInPersonPayment bool
|
||||
tx.QueryRow(r.Context(), `
|
||||
var hasInPersonPayment bool
|
||||
tx.QueryRow(r.Context(), `
|
||||
SELECT EXISTS(SELECT 1 FROM payments WHERE booking_id = $1 AND payment_method = 'in_person_card')`, bookingID).Scan(&hasInPersonPayment)
|
||||
|
||||
if hasInPersonPayment {
|
||||
var globalCampaignID string
|
||||
var globalPercent float64
|
||||
_ = tx.QueryRow(r.Context(), `
|
||||
if hasInPersonPayment {
|
||||
var globalCampaignID string
|
||||
var globalPercent float64
|
||||
_ = tx.QueryRow(r.Context(), `
|
||||
SELECT id, discount_percent FROM discount_campaigns
|
||||
WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'global_booking_count'
|
||||
AND milestone_value <= $1
|
||||
@@ -2806,126 +2806,126 @@ func ProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ORDER BY milestone_value DESC LIMIT 1
|
||||
`, globalCount).Scan(&globalCampaignID, &globalPercent)
|
||||
|
||||
if globalCampaignID != "" {
|
||||
discountAmount := roundTo2(bookingTotal * globalPercent / 100)
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
if globalCampaignID != "" {
|
||||
discountAmount := roundTo2(bookingTotal * globalPercent / 100)
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
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, booking.User.ID, globalCampaignID, globalPercent, bookingTotal, discountAmount); err != nil {
|
||||
log.Printf("ALERT: failed to insert booking discount: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
log.Printf("ALERT: failed to insert booking discount: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
|
||||
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
|
||||
`, bookingID, discountAmount, booking.User.ID); err != nil {
|
||||
log.Printf("ALERT: failed to insert payment record: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
log.Printf("ALERT: failed to insert payment record: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
|
||||
`, globalCampaignID); err != nil {
|
||||
log.Printf("ALERT: failed to insert payment record: %v", err)
|
||||
log.Printf("ALERT: failed to insert payment record: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var firstVisitDate time.Time
|
||||
_ = tx.QueryRow(r.Context(), `SELECT MIN(start_time) FROM bookings WHERE user_id = $1 AND status = 'completed'`, booking.User.ID).Scan(&firstVisitDate)
|
||||
if !firstVisitDate.IsZero() {
|
||||
annRows, err := tx.Query(r.Context(), `
|
||||
var firstVisitDate time.Time
|
||||
_ = tx.QueryRow(r.Context(), `SELECT MIN(start_time) FROM bookings WHERE user_id = $1 AND status = 'completed'`, booking.User.ID).Scan(&firstVisitDate)
|
||||
if !firstVisitDate.IsZero() {
|
||||
annRows, err := tx.Query(r.Context(), `
|
||||
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')
|
||||
`, booking.User.ID)
|
||||
if err == nil {
|
||||
// Collect anniversary campaigns first to avoid interleaving rows with writes.
|
||||
type annCampaign struct {
|
||||
id string
|
||||
pct float64
|
||||
value int
|
||||
unit string
|
||||
}
|
||||
var campaigns []annCampaign
|
||||
for annRows.Next() {
|
||||
var c annCampaign
|
||||
if annRows.Scan(&c.id, &c.pct, &c.value, &c.unit) == nil {
|
||||
campaigns = append(campaigns, c)
|
||||
if err == nil {
|
||||
// Collect anniversary campaigns first to avoid interleaving rows with writes.
|
||||
type annCampaign struct {
|
||||
id string
|
||||
pct float64
|
||||
value int
|
||||
unit string
|
||||
}
|
||||
}
|
||||
annRows.Close()
|
||||
var campaigns []annCampaign
|
||||
for annRows.Next() {
|
||||
var c annCampaign
|
||||
if annRows.Scan(&c.id, &c.pct, &c.value, &c.unit) == nil {
|
||||
campaigns = append(campaigns, c)
|
||||
}
|
||||
}
|
||||
annRows.Close()
|
||||
|
||||
// Sort by milestone_value descending so we apply the longest anniversary only
|
||||
sort.Slice(campaigns, func(i, j int) bool {
|
||||
return campaigns[i].value > campaigns[j].value
|
||||
})
|
||||
for _, c := range campaigns {
|
||||
var matches bool
|
||||
elapsed := time.Since(firstVisitDate)
|
||||
switch c.unit {
|
||||
case "months":
|
||||
months := int(elapsed.Hours() / (30 * 24))
|
||||
matches = months >= c.value
|
||||
case "years":
|
||||
years := int(elapsed.Hours() / (365.25 * 24))
|
||||
matches = years >= c.value
|
||||
}
|
||||
if matches {
|
||||
discountAmount := roundTo2(bookingTotal * c.pct / 100)
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
// Sort by milestone_value descending so we apply the longest anniversary only
|
||||
sort.Slice(campaigns, func(i, j int) bool {
|
||||
return campaigns[i].value > campaigns[j].value
|
||||
})
|
||||
for _, c := range campaigns {
|
||||
var matches bool
|
||||
elapsed := time.Since(firstVisitDate)
|
||||
switch c.unit {
|
||||
case "months":
|
||||
months := int(elapsed.Hours() / (30 * 24))
|
||||
matches = months >= c.value
|
||||
case "years":
|
||||
years := int(elapsed.Hours() / (365.25 * 24))
|
||||
matches = years >= c.value
|
||||
}
|
||||
if matches {
|
||||
discountAmount := roundTo2(bookingTotal * c.pct / 100)
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
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, booking.User.ID, c.id, c.pct, bookingTotal, discountAmount); err != nil {
|
||||
log.Printf("ALERT: failed to insert booking discount: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
log.Printf("ALERT: failed to insert booking discount: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_by)
|
||||
VALUES ($1, 'partial', 'discount', $2, 'completed', $3)
|
||||
`, bookingID, discountAmount, booking.User.ID); err != nil {
|
||||
log.Printf("ALERT: failed to insert payment record: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
log.Printf("ALERT: failed to insert payment record: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1
|
||||
`, c.id); err != nil {
|
||||
log.Printf("ALERT: failed to insert payment record: %v", err)
|
||||
log.Printf("ALERT: failed to insert payment record: %v", err)
|
||||
}
|
||||
break // apply longest matching only
|
||||
}
|
||||
break // apply longest matching only
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var paymentExists bool
|
||||
if err := tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM payments WHERE booking_id = $1)`, bookingID).Scan(&paymentExists); err == nil && paymentExists {
|
||||
var newDepositsRequired int
|
||||
if err := tx.QueryRow(r.Context(), `
|
||||
var paymentExists bool
|
||||
if err := tx.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM payments WHERE booking_id = $1)`, bookingID).Scan(&paymentExists); err == nil && paymentExists {
|
||||
var newDepositsRequired int
|
||||
if err := tx.QueryRow(r.Context(), `
|
||||
UPDATE users SET deposits_required = GREATEST(0, deposits_required - 1)
|
||||
WHERE id = $1
|
||||
RETURNING deposits_required
|
||||
`, booking.User.ID).Scan(&newDepositsRequired); err != nil {
|
||||
log.Printf("ALERT: failed to update deposits_required: %v", err)
|
||||
} else if newDepositsRequired == 0 {
|
||||
// After 3 paid bookings, forget no-shows so the counter resets.
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
log.Printf("ALERT: failed to update deposits_required: %v", err)
|
||||
} else if newDepositsRequired == 0 {
|
||||
// After 3 paid bookings, forget no-shows so the counter resets.
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
INSERT INTO forgiven_no_shows (booking_id)
|
||||
SELECT id FROM bookings
|
||||
WHERE user_id = $1 AND status = 'no_show'
|
||||
AND start_time >= NOW() - INTERVAL '6 months'
|
||||
AND NOT EXISTS (SELECT 1 FROM forgiven_no_shows WHERE booking_id = bookings.id)
|
||||
`, booking.User.ID); err != nil {
|
||||
log.Printf("ALERT: failed to auto-forgive no-shows: %v", err)
|
||||
log.Printf("ALERT: failed to auto-forgive no-shows: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Consume unconsumed name_history entries — this booking is the "first post-name-change
|
||||
// booking" that completes. After this, we no longer show "formerly" on displays.
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
// Consume unconsumed name_history entries — this booking is the "first post-name-change
|
||||
// booking" that completes. After this, we no longer show "formerly" on displays.
|
||||
if _, err := tx.Exec(r.Context(), `
|
||||
UPDATE name_history SET booking_id = $1
|
||||
WHERE user_id = $2 AND booking_id IS NULL
|
||||
`, bookingID, booking.User.ID); err != nil {
|
||||
log.Printf("Failed to consume name_history for user %s: %v", booking.User.ID, err)
|
||||
}
|
||||
} // close the else from alreadyCompleted check
|
||||
log.Printf("Failed to consume name_history for user %s: %v", booking.User.ID, err)
|
||||
}
|
||||
} // close the else from alreadyCompleted check
|
||||
}
|
||||
|
||||
if err := tx.Commit(r.Context()); err != nil {
|
||||
@@ -3288,7 +3288,7 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
resp := map[string]interface{}{
|
||||
resp := map[string]any{
|
||||
"message": "Booking cancelled successfully",
|
||||
"id": bookingID,
|
||||
"status": req.Reason,
|
||||
@@ -3336,7 +3336,7 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"message": "Booking deleted successfully",
|
||||
"id": bookingID,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user