Scrub Square card and customer references on anonymization (GDPR)
anonymize_user, delete_guest_user, and AnonymizeStaleGuestAccounts now NULL square_card_id and square_customer_id on user_saved_cards (7-year retained_until soft-delete kept for financial records). Make square_card_id nullable in the schema. GDPR export refunds join fixed to include gift-card-purchase refunds. Add scrub assertions to the GDPR and stale-guest test suites.
This commit is contained in:
@@ -505,6 +505,31 @@ func AnonymizeStaleGuestAccounts(ctx context.Context) (int, error) {
|
||||
}
|
||||
totalRows += int(tag.RowsAffected())
|
||||
|
||||
// Scrub Square saved-card references for stale guests and soft-delete any
|
||||
// active cards (7-year financial retention; Square ids are external-system
|
||||
// identifiers and must be removed for GDPR storage limitation).
|
||||
// COALESCE keeps the original timestamps for cards soft-deleted by an
|
||||
// earlier run, so a re-run never extends the retention window.
|
||||
tag, err = tx.Exec(ctx, `
|
||||
UPDATE user_saved_cards
|
||||
SET square_card_id = NULL,
|
||||
square_customer_id = NULL,
|
||||
last_4 = 'XXXX',
|
||||
fingerprint = NULL,
|
||||
deleted_at = COALESCE(deleted_at, NOW()),
|
||||
retained_until = COALESCE(retained_until, NOW() + INTERVAL '7 years')
|
||||
WHERE user_id IN (
|
||||
SELECT id FROM users
|
||||
WHERE account_role = 'guest'
|
||||
AND n_first_name = 'Guest'
|
||||
AND n_last_name = 'Anonymized'
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
totalRows += int(tag.RowsAffected())
|
||||
|
||||
return totalRows, tx.Commit(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -1259,6 +1259,69 @@ func TestAnonymizeStaleGuestAccounts_NoBookings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestAnonymizeStaleGuestAccounts_ScrubsSavedCards verifies the saved-card
|
||||
// scrub for stale guests: square_card_id / square_customer_id are NULLed
|
||||
// (external-system references removed for GDPR storage limitation), the card
|
||||
// is soft-deleted, last_4 is masked, and the fingerprint is NULLed.
|
||||
func TestAnonymizeStaleGuestAccounts_ScrubsSavedCards(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, tx := resetTestData(t)
|
||||
|
||||
guestID, err := fixtures.CreateTestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create guest user: %v", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `UPDATE users SET account_role = 'guest' WHERE id = $1`, guestID); err != nil {
|
||||
t.Fatalf("failed to set guest role: %v", err)
|
||||
}
|
||||
|
||||
// Stale booking: completed > 6 months ago.
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO bookings (user_id, start_time, status, deposit_required)
|
||||
VALUES ($1, NOW() - INTERVAL '7 months', 'completed', false)
|
||||
`, guestID); err != nil {
|
||||
t.Fatalf("failed to create stale booking: %v", err)
|
||||
}
|
||||
|
||||
var cardID string
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO user_saved_cards (user_id, square_card_id, square_customer_id, brand, last_4, exp_month, exp_year, fingerprint, is_default)
|
||||
VALUES ($1, 'sq_card_stale', 'sq_customer_stale', 'Visa', '4242', 12, 2030, 'fp_stale123', true)
|
||||
RETURNING id
|
||||
`, guestID).Scan(&cardID); err != nil {
|
||||
t.Fatalf("failed to insert saved card: %v", err)
|
||||
}
|
||||
|
||||
if _, err := AnonymizeStaleGuestAccounts(ctx); err != nil {
|
||||
t.Fatalf("AnonymizeStaleGuestAccounts failed: %v", err)
|
||||
}
|
||||
|
||||
var deletedAt, fingerprint, squareCardID, squareCustomerID interface{}
|
||||
var last4 string
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT deleted_at, last_4, fingerprint, square_card_id, square_customer_id
|
||||
FROM user_saved_cards WHERE id = $1
|
||||
`, cardID).Scan(&deletedAt, &last4, &fingerprint, &squareCardID, &squareCustomerID); err != nil {
|
||||
t.Fatalf("failed to query saved card after anonymization: %v", err)
|
||||
}
|
||||
|
||||
if deletedAt == nil {
|
||||
t.Error("expected deleted_at to be set after anonymization")
|
||||
}
|
||||
if last4 != "XXXX" {
|
||||
t.Errorf("expected last_4 to be 'XXXX', got %q", last4)
|
||||
}
|
||||
if fingerprint != nil {
|
||||
t.Errorf("expected fingerprint to be NULL, got %v", fingerprint)
|
||||
}
|
||||
if squareCardID != nil {
|
||||
t.Errorf("expected square_card_id to be NULL (external-system reference), got %v", squareCardID)
|
||||
}
|
||||
if squareCustomerID != nil {
|
||||
t.Errorf("expected square_customer_id to be NULL, got %v", squareCustomerID)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests for CleanupExpiredFinancialRecords ---
|
||||
|
||||
// TestCleanupExpiredFinancialRecords_PaymentOlderThan7Years verifies that a
|
||||
|
||||
Reference in New Issue
Block a user