|
|
|
@@ -1122,6 +1122,616 @@ func TestAnonymizeStaleGuestAccounts_NoBookings(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Tests for CleanupExpiredFinancialRecords ---
|
|
|
|
|
|
|
|
|
|
// TestCleanupExpiredFinancialRecords_PaymentOlderThan7Years verifies that a
|
|
|
|
|
// payment older than 7 years is aggregated and deleted.
|
|
|
|
|
func TestCleanupExpiredFinancialRecords_PaymentOlderThan7Years(t *testing.T) {
|
|
|
|
|
resetTestData(t)
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
userID, err := fixtures.CreateTestUser(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create user: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serviceID, err := fixtures.CreateTestService(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create service: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
eightYearsAgo := time.Now().AddDate(-8, 0, 0)
|
|
|
|
|
_, err = db.DB.Exec(ctx, `
|
|
|
|
|
INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
|
|
|
|
|
VALUES ($1, 'full', 'cash', 'completed', 50.00, $2)
|
|
|
|
|
`, bookingID, eightYearsAgo)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create payment: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run cleanup
|
|
|
|
|
err = CleanupExpiredFinancialRecords(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CleanupExpiredFinancialRecords failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify payment was deleted
|
|
|
|
|
var paymentCount int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE booking_id = $1", bookingID).Scan(&paymentCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count payments: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if paymentCount != 0 {
|
|
|
|
|
t.Errorf("expected payment to be deleted, got %d payment(s)", paymentCount)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify financial_aggregates has 1 row
|
|
|
|
|
var aggCount int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM financial_aggregates").Scan(&aggCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count aggregates: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if aggCount != 1 {
|
|
|
|
|
t.Errorf("expected 1 financial_aggregates row, got %d", aggCount)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify aggregate has correct month and total
|
|
|
|
|
expectedMonth := time.Date(eightYearsAgo.Year(), eightYearsAgo.Month(), 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
|
var aggMonth time.Time
|
|
|
|
|
var totalPayments float64
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT month, total_payments FROM financial_aggregates").Scan(&aggMonth, &totalPayments)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to query aggregate: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !aggMonth.Equal(expectedMonth) {
|
|
|
|
|
t.Errorf("expected month %s, got %s", expectedMonth.Format("2006-01-02"), aggMonth.Format("2006-01-02"))
|
|
|
|
|
}
|
|
|
|
|
if totalPayments != 50.00 {
|
|
|
|
|
t.Errorf("expected total_payments 50.00, got %.2f", totalPayments)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestCleanupExpiredFinancialRecords_AnonUserWithin1YearBuffer verifies that
|
|
|
|
|
// an anonymized user's payment is NOT deleted when the 1-year buffer hasn't
|
|
|
|
|
// elapsed, even if the payment is older than 7 years.
|
|
|
|
|
func TestCleanupExpiredFinancialRecords_AnonUserWithin1YearBuffer(t *testing.T) {
|
|
|
|
|
resetTestData(t)
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
guestID, err := fixtures.CreateTestUser(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create user: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = db.DB.Exec(ctx, `
|
|
|
|
|
UPDATE users
|
|
|
|
|
SET account_role = 'guest',
|
|
|
|
|
email = 'anon-' || id || '@anon.invalid',
|
|
|
|
|
updated_at = NOW() - INTERVAL '6 months'
|
|
|
|
|
WHERE id = $1
|
|
|
|
|
`, guestID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to anonymize user: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serviceID, err := fixtures.CreateTestService(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create service: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bookingID, err := fixtures.CreateTestBooking(db.DB, guestID, serviceID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fourYearsAgo := time.Now().AddDate(-4, 0, 0)
|
|
|
|
|
var paymentID string
|
|
|
|
|
err = db.DB.QueryRow(ctx, `
|
|
|
|
|
INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
|
|
|
|
|
VALUES ($1, 'full', 'cash', 'completed', 75.00, $2)
|
|
|
|
|
RETURNING id
|
|
|
|
|
`, bookingID, fourYearsAgo).Scan(&paymentID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create payment: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run cleanup
|
|
|
|
|
err = CleanupExpiredFinancialRecords(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CleanupExpiredFinancialRecords failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify payment still exists (neither 7yr nor 1yr conditions satisfied)
|
|
|
|
|
var paymentCount int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE id = $1", paymentID).Scan(&paymentCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count payments: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if paymentCount != 1 {
|
|
|
|
|
t.Error("expected payment to still exist")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify no aggregate was created
|
|
|
|
|
var aggCount int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM financial_aggregates").Scan(&aggCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count aggregates: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if aggCount != 0 {
|
|
|
|
|
t.Errorf("expected no aggregates, got %d", aggCount)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestCleanupExpiredFinancialRecords_PaymentOlderThan9Years verifies that a
|
|
|
|
|
// payment older than 9 years is always deleted regardless of user status.
|
|
|
|
|
func TestCleanupExpiredFinancialRecords_PaymentOlderThan9Years(t *testing.T) {
|
|
|
|
|
resetTestData(t)
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
userID, err := fixtures.CreateTestUser(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create user: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serviceID, err := fixtures.CreateTestService(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create service: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Payment created 9 years ago
|
|
|
|
|
nineYearsAgo := time.Now().AddDate(-9, 0, 0)
|
|
|
|
|
_, err = db.DB.Exec(ctx, `
|
|
|
|
|
INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
|
|
|
|
|
VALUES ($1, 'full', 'cash', 'completed', 60.00, $2)
|
|
|
|
|
`, bookingID, nineYearsAgo)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create payment: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run cleanup
|
|
|
|
|
err = CleanupExpiredFinancialRecords(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CleanupExpiredFinancialRecords failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify payment was deleted
|
|
|
|
|
var paymentCount int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE booking_id = $1", bookingID).Scan(&paymentCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count payments: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if paymentCount != 0 {
|
|
|
|
|
t.Errorf("expected payment to be deleted, got %d payment(s)", paymentCount)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify financial_aggregates has 1 row
|
|
|
|
|
var aggCount int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM financial_aggregates").Scan(&aggCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count aggregates: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if aggCount != 1 {
|
|
|
|
|
t.Errorf("expected 1 financial_aggregates row, got %d", aggCount)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestCleanupExpiredFinancialRecords_AggregationCorrectTotals verifies that
|
|
|
|
|
// multiple payments in the same month are correctly aggregated by method and type.
|
|
|
|
|
func TestCleanupExpiredFinancialRecords_AggregationCorrectTotals(t *testing.T) {
|
|
|
|
|
resetTestData(t)
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
userID, err := fixtures.CreateTestUser(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create user: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serviceID, err := fixtures.CreateTestService(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create service: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3 payments 8 years ago, all in the same month
|
|
|
|
|
sameMonth := time.Now().AddDate(-8, 0, 0)
|
|
|
|
|
_ = sameMonth // used for all payments
|
|
|
|
|
|
|
|
|
|
// Payment 1: £50 cash
|
|
|
|
|
_, err = db.DB.Exec(ctx, `
|
|
|
|
|
INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, fees, created_at)
|
|
|
|
|
VALUES ($1, 'full', 'cash', 'completed', 50.00, 0, $2)
|
|
|
|
|
`, bookingID, sameMonth)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create cash payment: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Payment 2: £30 online_square with £2.50 fees
|
|
|
|
|
_, err = db.DB.Exec(ctx, `
|
|
|
|
|
INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, fees, created_at)
|
|
|
|
|
VALUES ($1, 'full', 'online_square', 'completed', 30.00, 2.50, $2)
|
|
|
|
|
`, bookingID, sameMonth)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create online payment: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Payment 3: £20 in_person_card
|
|
|
|
|
_, err = db.DB.Exec(ctx, `
|
|
|
|
|
INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, fees, created_at)
|
|
|
|
|
VALUES ($1, 'full', 'in_person_card', 'completed', 20.00, 0, $2)
|
|
|
|
|
`, bookingID, sameMonth)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create card payment: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run cleanup
|
|
|
|
|
err = CleanupExpiredFinancialRecords(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CleanupExpiredFinancialRecords failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify aggregate totals
|
|
|
|
|
var totalPayments, totalCash, totalOnline, totalInPerson, totalSquareFees float64
|
|
|
|
|
var bookingCount int
|
|
|
|
|
err = db.DB.QueryRow(ctx, `
|
|
|
|
|
SELECT total_payments, total_cash, total_online, total_in_person, total_square_fees, booking_count
|
|
|
|
|
FROM financial_aggregates
|
|
|
|
|
`).Scan(&totalPayments, &totalCash, &totalOnline, &totalInPerson, &totalSquareFees, &bookingCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to query aggregate: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if totalPayments != 100.00 {
|
|
|
|
|
t.Errorf("expected total_payments 100.00, got %.2f", totalPayments)
|
|
|
|
|
}
|
|
|
|
|
if totalCash != 50.00 {
|
|
|
|
|
t.Errorf("expected total_cash 50.00, got %.2f", totalCash)
|
|
|
|
|
}
|
|
|
|
|
if totalOnline != 30.00 {
|
|
|
|
|
t.Errorf("expected total_online 30.00, got %.2f", totalOnline)
|
|
|
|
|
}
|
|
|
|
|
if totalInPerson != 20.00 {
|
|
|
|
|
t.Errorf("expected total_in_person 20.00, got %.2f", totalInPerson)
|
|
|
|
|
}
|
|
|
|
|
if totalSquareFees != 2.50 {
|
|
|
|
|
t.Errorf("expected total_square_fees 2.50, got %.2f", totalSquareFees)
|
|
|
|
|
}
|
|
|
|
|
if bookingCount != 1 {
|
|
|
|
|
t.Errorf("expected booking_count 1, got %d", bookingCount)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestCleanupExpiredFinancialRecords_Idempotent verifies that running the
|
|
|
|
|
// cleanup function twice produces the same result (no double-counting).
|
|
|
|
|
func TestCleanupExpiredFinancialRecords_Idempotent(t *testing.T) {
|
|
|
|
|
resetTestData(t)
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
userID, err := fixtures.CreateTestUser(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create user: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serviceID, err := fixtures.CreateTestService(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create service: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Payment 8 years ago
|
|
|
|
|
eightYearsAgo := time.Now().AddDate(-8, 0, 0)
|
|
|
|
|
_, err = db.DB.Exec(ctx, `
|
|
|
|
|
INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
|
|
|
|
|
VALUES ($1, 'full', 'cash', 'completed', 100.00, $2)
|
|
|
|
|
`, bookingID, eightYearsAgo)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create payment: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// First run
|
|
|
|
|
err = CleanupExpiredFinancialRecords(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("first cleanup failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Capture aggregate values after first run
|
|
|
|
|
var aggCount1 int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM financial_aggregates").Scan(&aggCount1)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count aggregates: %v", err)
|
|
|
|
|
}
|
|
|
|
|
var totalPayments1 float64
|
|
|
|
|
var aggMonth1 time.Time
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT month, total_payments FROM financial_aggregates").Scan(&aggMonth1, &totalPayments1)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to query aggregate after first run: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Second run
|
|
|
|
|
err = CleanupExpiredFinancialRecords(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("second cleanup failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify same aggregate count and values
|
|
|
|
|
var aggCount2 int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM financial_aggregates").Scan(&aggCount2)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count aggregates: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if aggCount2 != aggCount1 {
|
|
|
|
|
t.Errorf("expected %d aggregates after second run, got %d", aggCount1, aggCount2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var totalPayments2 float64
|
|
|
|
|
var aggMonth2 time.Time
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT month, total_payments FROM financial_aggregates").Scan(&aggMonth2, &totalPayments2)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to query aggregate after second run: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if !aggMonth2.Equal(aggMonth1) {
|
|
|
|
|
t.Errorf("expected month %s, got %s", aggMonth1.Format("2006-01-02"), aggMonth2.Format("2006-01-02"))
|
|
|
|
|
}
|
|
|
|
|
if totalPayments2 != totalPayments1 {
|
|
|
|
|
t.Errorf("expected total_payments %.2f after second run, got %.2f", totalPayments1, totalPayments2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify payments are still deleted
|
|
|
|
|
var paymentCount int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE booking_id = $1", bookingID).Scan(&paymentCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count payments: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if paymentCount != 0 {
|
|
|
|
|
t.Errorf("expected payments to be deleted, got %d", paymentCount)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestCleanupExpiredFinancialRecords_ActiveUserWithin7Years verifies that
|
|
|
|
|
// a payment newer than 7 years for an active user is NOT deleted.
|
|
|
|
|
func TestCleanupExpiredFinancialRecords_ActiveUserWithin7Years(t *testing.T) {
|
|
|
|
|
resetTestData(t)
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
userID, err := fixtures.CreateTestUser(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create user: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serviceID, err := fixtures.CreateTestService(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create service: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Payment created 3 years ago (< 7 years)
|
|
|
|
|
threeYearsAgo := time.Now().AddDate(-3, 0, 0)
|
|
|
|
|
var paymentID string
|
|
|
|
|
err = db.DB.QueryRow(ctx, `
|
|
|
|
|
INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
|
|
|
|
|
VALUES ($1, 'full', 'cash', 'completed', 40.00, $2)
|
|
|
|
|
RETURNING id
|
|
|
|
|
`, bookingID, threeYearsAgo).Scan(&paymentID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create payment: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run cleanup
|
|
|
|
|
err = CleanupExpiredFinancialRecords(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CleanupExpiredFinancialRecords failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify payment still exists
|
|
|
|
|
var paymentCount int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE id = $1", paymentID).Scan(&paymentCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count payments: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if paymentCount != 1 {
|
|
|
|
|
t.Error("expected payment to still exist")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify no aggregate was created
|
|
|
|
|
var aggCount int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM financial_aggregates").Scan(&aggCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count aggregates: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if aggCount != 0 {
|
|
|
|
|
t.Errorf("expected no aggregates, got %d", aggCount)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestCleanupExpiredFinancialRecords_AnonUserBothThresholdsElapsed verifies that
|
|
|
|
|
// an anonymized user's payment IS deleted when BOTH the 7-year rule AND the
|
|
|
|
|
// 1-year post-anonymization buffer have elapsed.
|
|
|
|
|
func TestCleanupExpiredFinancialRecords_AnonUserBothThresholdsElapsed(t *testing.T) {
|
|
|
|
|
resetTestData(t)
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
guestID, err := fixtures.CreateTestUser(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create user: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Anonymize user 2 years ago
|
|
|
|
|
_, err = db.DB.Exec(ctx, `
|
|
|
|
|
UPDATE users
|
|
|
|
|
SET account_role = 'guest',
|
|
|
|
|
email = 'anon-' || id || '@anon.invalid',
|
|
|
|
|
updated_at = NOW() - INTERVAL '2 years'
|
|
|
|
|
WHERE id = $1
|
|
|
|
|
`, guestID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to anonymize user: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serviceID, err := fixtures.CreateTestService(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create service: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bookingID, err := fixtures.CreateTestBooking(db.DB, guestID, serviceID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Payment created 8 years ago (past 7yr rule)
|
|
|
|
|
// User anonymized 2 years ago (past 1yr buffer)
|
|
|
|
|
// Both conditions met → should be deleted
|
|
|
|
|
eightYearsAgo := time.Now().AddDate(-8, 0, 0)
|
|
|
|
|
var paymentID string
|
|
|
|
|
err = db.DB.QueryRow(ctx, `
|
|
|
|
|
INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
|
|
|
|
|
VALUES ($1, 'full', 'cash', 'completed', 75.00, $2)
|
|
|
|
|
RETURNING id
|
|
|
|
|
`, bookingID, eightYearsAgo).Scan(&paymentID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create payment: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run cleanup
|
|
|
|
|
err = CleanupExpiredFinancialRecords(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CleanupExpiredFinancialRecords failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify payment was deleted
|
|
|
|
|
var paymentCount int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE id = $1", paymentID).Scan(&paymentCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count payments: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if paymentCount != 0 {
|
|
|
|
|
t.Error("expected payment to be deleted (both 7yr and 1yr+ thresholds elapsed)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify aggregate was created
|
|
|
|
|
var aggCount int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM financial_aggregates").Scan(&aggCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count aggregates: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if aggCount != 1 {
|
|
|
|
|
t.Errorf("expected 1 financial_aggregates row, got %d", aggCount)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestCleanupExpiredFinancialRecords_RefundAggregatedAndDeleted verifies that
|
|
|
|
|
// refunds are aggregated and deleted alongside their parent payment when
|
|
|
|
|
// retention expires.
|
|
|
|
|
func TestCleanupExpiredFinancialRecords_RefundAggregatedAndDeleted(t *testing.T) {
|
|
|
|
|
resetTestData(t)
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
userID, err := fixtures.CreateTestUser(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create user: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serviceID, err := fixtures.CreateTestService(db.DB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create service: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bookingID, err := fixtures.CreateTestBooking(db.DB, userID, serviceID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Payment 8 years ago
|
|
|
|
|
eightYearsAgo := time.Now().AddDate(-8, 0, 0)
|
|
|
|
|
var paymentID string
|
|
|
|
|
err = db.DB.QueryRow(ctx, `
|
|
|
|
|
INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
|
|
|
|
|
VALUES ($1, 'full', 'cash', 'completed', 100.00, $2)
|
|
|
|
|
RETURNING id
|
|
|
|
|
`, bookingID, eightYearsAgo).Scan(&paymentID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create payment: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Refund 8 years ago (same month as payment)
|
|
|
|
|
var refundID string
|
|
|
|
|
err = db.DB.QueryRow(ctx, `
|
|
|
|
|
INSERT INTO refunds (payment_id, booking_id, amount, status, reason, created_at)
|
|
|
|
|
VALUES ($1, $2, 30.00, 'completed', 'test refund', $3)
|
|
|
|
|
RETURNING id
|
|
|
|
|
`, paymentID, bookingID, eightYearsAgo).Scan(&refundID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create refund: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run cleanup
|
|
|
|
|
err = CleanupExpiredFinancialRecords(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CleanupExpiredFinancialRecords failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify payment was deleted
|
|
|
|
|
var paymentCount int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE id = $1", paymentID).Scan(&paymentCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count payments: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if paymentCount != 0 {
|
|
|
|
|
t.Error("expected payment to be deleted")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify refund was deleted
|
|
|
|
|
var refundCount int
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT COUNT(*) FROM refunds WHERE id = $1", refundID).Scan(&refundCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to count refunds: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if refundCount != 0 {
|
|
|
|
|
t.Error("expected refund to be deleted")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify aggregate has both payment and refund totals
|
|
|
|
|
var totalPayments, totalRefunds float64
|
|
|
|
|
err = db.DB.QueryRow(ctx, "SELECT total_payments, total_refunds FROM financial_aggregates").Scan(&totalPayments, &totalRefunds)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to query aggregate: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if totalPayments != 100.00 {
|
|
|
|
|
t.Errorf("expected total_payments 100.00, got %.2f", totalPayments)
|
|
|
|
|
}
|
|
|
|
|
if totalRefunds != 30.00 {
|
|
|
|
|
t.Errorf("expected total_refunds 30.00, got %.2f", totalRefunds)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure bytes is used to avoid unused import error
|
|
|
|
|
var _ = bytes.Buffer{}
|
|
|
|
|
|
|
|
|
|