From a4a80246d1e06e304245b2e4b6f9d8486395a7e4 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Fri, 10 Jul 2026 20:39:17 +0100 Subject: [PATCH] fix: restore S3 error stubs, add CI timeouts, gofmt handlers.go, fix comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - s3.go: restore error returns on prod Upload/Download/Delete stubs (silent nil was dangerous — callers would think writes succeeded) - CI: add -timeout 300s to tests, -timeout 480s to race, GO_TESTING to race env - handlers.go: gofmt indentation fix for GetCheckoutStatus struct literal - validators.go: fix comment to include A-F (matched the regex) --- .gitea/workflows/ci.yaml | 5 +- backend/handlers/payments/handlers.go | 121 +++++++++++----------- backend/internal/s3/s3.go | 10 +- backend/internal/validators/validators.go | 2 +- 4 files changed, 68 insertions(+), 70 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 036c734..54b2fb3 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -449,7 +449,7 @@ jobs: - name: Run tests working-directory: backend - run: go test -tags "${{ matrix.gotags }}" -count=1 ${{ matrix.verbose }} ${{ matrix.coverflags }} ./... + run: go test -tags "${{ matrix.gotags }}" -count=1 -timeout 300s ${{ matrix.verbose }} ${{ matrix.coverflags }} ./... env: GO_TESTING: "1" POSTGRES_HOST: postgres @@ -531,8 +531,9 @@ jobs: - name: Run race detector working-directory: backend - run: go test -tags "${{ matrix.gotags }}" -race -count=1 ./... + run: go test -tags "${{ matrix.gotags }}" -race -count=1 -timeout 480s ./... env: + GO_TESTING: "1" POSTGRES_HOST: postgres TEST_DB_HOST: postgres CGO_ENABLED: "1" diff --git a/backend/handlers/payments/handlers.go b/backend/handlers/payments/handlers.go index 3a24a9c..1cee950 100644 --- a/backend/handlers/payments/handlers.go +++ b/backend/handlers/payments/handlers.go @@ -141,7 +141,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri } var bookingTotal float64 -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = db.Conn.QueryRow(ctx, ` SELECT total_amount FROM bookings WHERE id = $1 `, bookingID).Scan(&bookingTotal) @@ -164,7 +164,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri ORDER BY discount_percent DESC LIMIT 1 `).Scan(&campaignID, &campaignPercent, &campaignName); err == nil && campaignID != "" { var exists int -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, campaignID).Scan(&exists) if exists == 0 { amount := roundTo2(bookingTotal * campaignPercent / 100) @@ -179,13 +179,13 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri } var userBookingCount int -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = db.Conn.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&userBookingCount) var milestoneCampaignID string var milestonePercent float64 var milestoneName string -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = db.Conn.QueryRow(ctx, ` SELECT id, discount_percent, name FROM discount_campaigns WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'per_user_booking_count' @@ -195,7 +195,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri if milestoneCampaignID != "" { var exists int -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, milestoneCampaignID).Scan(&exists) if exists == 0 { amount := roundTo2(bookingTotal * milestonePercent / 100) @@ -210,7 +210,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri } var firstVisitDate time.Time -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = db.Conn.QueryRow(ctx, `SELECT MIN(start_time) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&firstVisitDate) if !firstVisitDate.IsZero() { type annCamp struct { @@ -237,7 +237,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri for _, c := range campaigns { var exists int -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = db.Conn.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, c.id).Scan(&exists) if exists > 0 { continue @@ -274,7 +274,7 @@ func calculateDiscountPreview(ctx context.Context, bookingID string, userID stri LIMIT 1 `, userID).Scan(&rdID, &rdPercent); err == nil && rdID != "" { exists := 0 -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = db.Conn.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) @@ -351,10 +351,10 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) { return } defer func() { - if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { - slog.Error("failed to rollback transaction", "err", err) - } -}() + if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { + slog.Error("failed to rollback transaction", "err", err) + } + }() // Check booking status inside the transaction. var status string @@ -623,10 +623,10 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) { return } defer func() { - if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { - slog.Error("failed to rollback transaction", "err", err) - } -}() + if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { + slog.Error("failed to rollback transaction", "err", err) + } + }() // Check for existing payment inside the transaction. var existingID string @@ -636,10 +636,10 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) { WHERE booking_id = $1 AND idempotency_key = $2 `, bookingID, idempotencyKey).Scan(&existingID, &existingSquarePayID); err == nil { if existingSquarePayID.Valid && existingSquarePayID.String == paymentResult.SquarePayID { - _ = json.NewEncoder(w).Encode(PaymentStatusResponse{ - Status: "COMPLETED", - PaymentID: existingID, - Amount: paymentResult.Amount, + _ = json.NewEncoder(w).Encode(PaymentStatusResponse{ + Status: "COMPLETED", + PaymentID: existingID, + Amount: paymentResult.Amount, CardBrand: paymentResult.CardBrand, CardLast4: paymentResult.CardLast4, ReceiptURL: paymentResult.ReceiptURL, @@ -822,10 +822,10 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) { return } defer func() { - if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { - slog.Error("failed to rollback transaction", "err", err) - } -}() + if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { + slog.Error("failed to rollback transaction", "err", err) + } + }() var status string if err := tx.QueryRow(r.Context(), `SELECT status FROM bookings WHERE id = $1`, bookingID).Scan(&status); err != nil { @@ -1021,7 +1021,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) { // Promote deposit to confirmed if total paid meets the 20% threshold. // Check is inside the transaction so it sees the just-inserted payments. var depositMet bool -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = tx.QueryRow(r.Context(), ` WITH booking_total AS ( SELECT total_amount * 100 AS total_cents FROM bookings WHERE id = $1 @@ -1077,7 +1077,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) { // would create a credit balance or require a refund. func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingID string, userID string) { var existingPayment int -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = q.QueryRow(ctx, ` SELECT COUNT(*) FROM payments WHERE booking_id = $1 AND status = 'completed' AND payment_method NOT IN ('discount', 'on_the_house') @@ -1110,7 +1110,7 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI ORDER BY discount_percent DESC LIMIT 1 `).Scan(&campaignID, &campaignPercent); err == nil && campaignID != "" { var exists int -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = q.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) @@ -1120,12 +1120,12 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI `, bookingID, userID, campaignID, campaignPercent, bookingTotal, discountAmount); err != nil { log.Printf("Failed to insert time-based campaign discount: %v", err) } else { -//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates + //nolint:errcheck // exec errors are non-critical; best-effort inserts/updates _, _ = q.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) -//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates + //nolint:errcheck // exec errors are non-critical; best-effort inserts/updates _, _ = q.Exec(ctx, ` UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1 `, campaignID) @@ -1134,12 +1134,12 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI } var userBookingCount int -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = q.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&userBookingCount) var milestoneCampaignID string var milestonePercent float64 -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = q.QueryRow(ctx, ` SELECT id, discount_percent FROM discount_campaigns WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'per_user_booking_count' @@ -1149,7 +1149,7 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI if milestoneCampaignID != "" { var exists int -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = q.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) @@ -1159,12 +1159,12 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI `, bookingID, userID, milestoneCampaignID, milestonePercent, bookingTotal, discountAmount); err != nil { log.Printf("Failed to insert per-user milestone discount: %v", err) } else { -//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates + //nolint:errcheck // exec errors are non-critical; best-effort inserts/updates _, _ = q.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) -//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates + //nolint:errcheck // exec errors are non-critical; best-effort inserts/updates _, _ = q.Exec(ctx, ` UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1 `, milestoneCampaignID) @@ -1173,7 +1173,7 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI } var firstVisitDate time.Time -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = q.QueryRow(ctx, `SELECT MIN(start_time) FROM bookings WHERE user_id = $1 AND status = 'completed'`, userID).Scan(&firstVisitDate) if !firstVisitDate.IsZero() { annRows, err := q.Query(ctx, ` @@ -1199,7 +1199,7 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI for _, c := range campaigns { var exists int -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = q.QueryRow(ctx, `SELECT 1 FROM booking_discounts WHERE booking_id = $1 AND source_id = $2`, bookingID, c.id).Scan(&exists) if exists > 0 { continue @@ -1223,12 +1223,12 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI `, bookingID, userID, c.id, c.pct, bookingTotal, discountAmount); err != nil { log.Printf("Failed to insert anniversary discount: %v", err) } else { -//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates + //nolint:errcheck // exec errors are non-critical; best-effort inserts/updates _, _ = q.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) -//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates + //nolint:errcheck // exec errors are non-critical; best-effort inserts/updates _, _ = q.Exec(ctx, ` UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1 `, c.id) @@ -1246,12 +1246,12 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI 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 -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = q.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE status = 'completed'`).Scan(&globalCount) var globalCampaignID string var globalPercent float64 -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = q.QueryRow(ctx, ` SELECT id, discount_percent FROM discount_campaigns WHERE status = 'active' AND campaign_type = 'milestone' AND milestone_type = 'global_booking_count' @@ -1263,7 +1263,7 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI if globalCampaignID != "" { var exists int -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = q.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) @@ -1273,13 +1273,13 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI `, bookingID, userID, globalCampaignID, globalPercent, bookingTotal, discountAmount); err != nil { log.Printf("Failed to insert global milestone discount: %v", err) } else { -//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates - _, _ = q.Exec(ctx, ` + //nolint:errcheck // exec errors are non-critical; best-effort inserts/updates + _, _ = q.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) -//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates - _, _ = q.Exec(ctx, ` + //nolint:errcheck // exec errors are non-critical; best-effort inserts/updates + _, _ = q.Exec(ctx, ` UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = $1 `, globalCampaignID) } @@ -1297,7 +1297,7 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI LIMIT 1 `, userID).Scan(&rdID, &rdPercent); err == nil && rdID != "" { exists := 0 -//nolint:errcheck // zero value is acceptable fallback on scan failure + //nolint:errcheck // zero value is acceptable fallback on scan failure _ = q.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) @@ -1305,12 +1305,12 @@ func applyEligibleCampaignsAtPayment(ctx context.Context, q db.Querier, bookingI 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 { -//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates + //nolint:errcheck // exec errors are non-critical; best-effort inserts/updates _, _ = q.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) -//nolint:errcheck // exec errors are non-critical; best-effort inserts/updates + //nolint:errcheck // exec errors are non-critical; best-effort inserts/updates _, _ = q.Exec(ctx, ` UPDATE referral_discounts SET used = TRUE, used_at = NOW() WHERE id = $1 `, rdID) @@ -1422,6 +1422,7 @@ func buildSplitRecords(primary PaymentRecord, reqPaymentType string, info *Booki // split payment, following the same rules as the frontend's handlePayFull: // 'balance' when some payment already exists, 'full' when covering everything, // 'partial' when leaving a remainder. +// //lint:ignore U1000 reserved for future use func nonDepositPaymentType(reqType string, totalPaidAfterThis float64, thisPortion float64, bookingTotal float64) string { if totalPaidAfterThis >= bookingTotal { @@ -1619,10 +1620,10 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) { return } defer func() { - if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { - slog.Error("failed to rollback transaction", "err", err) - } -}() + if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { + slog.Error("failed to rollback transaction", "err", err) + } + }() refundReq := square.RefundPaymentReq{ PaymentID: *payment.SquarePaymentID, @@ -1965,10 +1966,10 @@ func AcquirePaymentLock(w http.ResponseWriter, r *http.Request) { return } defer func() { - if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { - slog.Error("failed to rollback transaction", "err", err) - } -}() + if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { + slog.Error("failed to rollback transaction", "err", err) + } + }() if _, err := tx.Exec(r.Context(), ` DELETE FROM time_blockers @@ -2016,10 +2017,10 @@ func ReleasePaymentLock(w http.ResponseWriter, r *http.Request) { return } defer func() { - if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { - slog.Error("failed to rollback transaction", "err", err) - } -}() + if err := tx.Rollback(r.Context()); err != nil && err.Error() != "tx is closed" { + slog.Error("failed to rollback transaction", "err", err) + } + }() if _, err := tx.Exec(r.Context(), ` DELETE FROM time_blockers diff --git a/backend/internal/s3/s3.go b/backend/internal/s3/s3.go index facadb4..221621e 100644 --- a/backend/internal/s3/s3.go +++ b/backend/internal/s3/s3.go @@ -50,19 +50,15 @@ func Connect() error { } func (s *S3Client) Upload(ctx context.Context, bucket, key string, body io.Reader, contentType string) error { - // TODO: Wire in AWS SDK v2 for production S3 uploads. - // Currently only available under //go:build dev via RUSTFS. - return nil + return fmt.Errorf("not implemented: production S3 upload requires AWS SDK v2 (use RUSTFS in dev)") } func (s *S3Client) Download(ctx context.Context, bucket, key string, w io.Writer) error { - // TODO: Wire in AWS SDK v2 for production S3 downloads. - return nil + return fmt.Errorf("not implemented: production S3 download requires AWS SDK v2 (use RUSTFS in dev)") } func (s *S3Client) Delete(ctx context.Context, bucket, key string) error { - // TODO: Wire in AWS SDK v2 for production S3 deletes. - return nil + return fmt.Errorf("not implemented: production S3 delete requires AWS SDK v2 (use RUSTFS in dev)") } func (s *S3Client) GetURL(ctx context.Context, bucket, key string) (string, error) { diff --git a/backend/internal/validators/validators.go b/backend/internal/validators/validators.go index 2b163c3..5dee1e0 100644 --- a/backend/internal/validators/validators.go +++ b/backend/internal/validators/validators.go @@ -27,7 +27,7 @@ func init() { var validIDRegex = regexp.MustCompile(`^[0-9a-fA-F]{12}$`) // IsValidID checks if an ID is valid based on the database constraint (CHAR(12) hex string) -// Valid IDs are exactly 12 hexadecimal characters (0-9, a-f) +// Valid IDs are exactly 12 hexadecimal characters (0-9, a-f, A-F) func IsValidID(id string) bool { if id == "" { return false