//go:build test && dev package payments import ( "context" "testing" "crussell/db" "crussell/testutils" "crussell/testutils/fixtures" ) // TestCompleteActiveBookingFromPayment_RefusesCancelledBooking locks the M2 // money-safety boundary of the sweep rescue's completion side-effect: a // cancelled / lapsed / no-show booking must NEVER be auto-completed by the // payment path — a charge landing on such a booking is failed + auto-refunded // by the sweep's F3 gate, and completing the booking would record money against // a booking the cancellation flow already closed. func TestCompleteActiveBookingFromPayment_RefusesCancelledBooking(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) _, bookingID, _ := setupTestData(t, ctx, tx) if _, err := tx.Exec(ctx, "UPDATE bookings SET status = 'we_cancelled' WHERE id = $1", bookingID); err != nil { t.Fatalf("failed to set booking we_cancelled: %v", err) } pgxTx := db.TxFromContext(ctx) if pgxTx == nil { t.Fatal("no transaction in context") } completeActiveBookingFromPayment(ctx, pgxTx, bookingID) var status string if err := tx.QueryRow(ctx, "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status); err != nil { t.Fatalf("failed to query booking status: %v", err) } if status != "we_cancelled" { t.Errorf("expected the cancelled booking NOT auto-completed, got %q", status) } // The setup tx is rolled back at test end, so no pool-level cleanup is needed. } // TestCompleteFullyPaidBooking_CompletesPayableBooking locks the sweep rescue's // completion side-effect (applyStaleRescueRecords → bookingIsFullyPaid → // completeActiveBookingFromPayment): a PAYABLE booking fully covered by // completed real money is completed by the rescue-completion path, exactly as // the live payment path would. func TestCompleteFullyPaidBooking_CompletesPayableBooking(t *testing.T) { ctx, tx := testutils.SetupTestTx(t) userID, bookingID, serviceID := setupTestData(t, ctx, tx) // A full £50 completed payment covers the £50 booking total. payID, err := fixtures.CreateTestPayment(tx, bookingID, 50.00, "online_square", "full", "completed") if err != nil { t.Fatalf("failed to create completed payment: %v", err) } pgxTx := db.TxFromContext(ctx) if pgxTx == nil { t.Fatal("no transaction in context") } if err := pgxTx.Commit(ctx); err != nil { t.Fatalf("failed to commit setup tx: %v", err) } pool := context.Background() t.Cleanup(func() { _, _ = db.Conn.Exec(pool, `DELETE FROM booking_discounts WHERE booking_id = $1`, bookingID) _, _ = db.Conn.Exec(pool, `DELETE FROM payments WHERE id = $1`, payID) _, _ = db.Conn.Exec(pool, `DELETE FROM bookings WHERE id = $1`, bookingID) _, _ = db.Conn.Exec(pool, `DELETE FROM services WHERE id = $1`, serviceID) _, _ = db.Conn.Exec(pool, `DELETE FROM users WHERE id = $1`, userID) }) completeFullyPaidBooking(pool, bookingID) var status string if err := db.Conn.QueryRow(pool, "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status); err != nil { t.Fatalf("failed to query booking status: %v", err) } if status != "completed" { t.Errorf("expected the fully-paid payable booking completed, got %q", status) } }