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:
@@ -80,7 +80,7 @@ func DeleteAccountHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
rows, err := db.Conn.Query(ctx,
|
||||
`SELECT square_card_id FROM user_saved_cards WHERE user_id = $1 AND deleted_at IS NULL`, userID)
|
||||
`SELECT square_card_id FROM user_saved_cards WHERE user_id = $1 AND deleted_at IS NULL AND square_card_id IS NOT NULL`, userID)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to query saved cards for user %s: %v", userID, err)
|
||||
return
|
||||
|
||||
@@ -260,13 +260,13 @@ func TestAnonymizeUser_SoftDeletesSavedCards(t *testing.T) {
|
||||
t.Fatalf("anonymize_user failed: %v", err)
|
||||
}
|
||||
|
||||
var deletedAt, fingerprint interface{}
|
||||
var deletedAt, fingerprint, squareCardID interface{}
|
||||
var last4 string
|
||||
var expMonth, expYear int
|
||||
err = tx.QueryRow(ctx, `
|
||||
SELECT deleted_at, last_4, fingerprint, exp_month, exp_year
|
||||
SELECT deleted_at, last_4, fingerprint, exp_month, exp_year, square_card_id
|
||||
FROM user_saved_cards WHERE user_id = $1
|
||||
`, userID).Scan(&deletedAt, &last4, &fingerprint, &expMonth, &expYear)
|
||||
`, userID).Scan(&deletedAt, &last4, &fingerprint, &expMonth, &expYear, &squareCardID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to query saved card after anonymization: %v", err)
|
||||
}
|
||||
@@ -280,6 +280,9 @@ func TestAnonymizeUser_SoftDeletesSavedCards(t *testing.T) {
|
||||
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 after anonymization (external-system reference), got %v", squareCardID)
|
||||
}
|
||||
if expMonth != 1 {
|
||||
t.Errorf("expected exp_month to be 1, got %d", expMonth)
|
||||
}
|
||||
@@ -288,6 +291,72 @@ func TestAnonymizeUser_SoftDeletesSavedCards(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteGuestUser_ScrubsSavedCards(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
userID, err := fixtures.CreateTestGuestUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test guest user: %v", err)
|
||||
}
|
||||
|
||||
var cardID string
|
||||
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_guest', 'sq_customer_guest', 'Visa', '4242', 12, 2030, 'fp_guest123', true)
|
||||
RETURNING id
|
||||
`, userID).Scan(&cardID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to insert saved card: %v", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, `SELECT delete_guest_user($1)`, userID)
|
||||
if err != nil {
|
||||
t.Fatalf("delete_guest_user failed: %v", err)
|
||||
}
|
||||
|
||||
// The guest user row is fully deleted.
|
||||
var userCount int
|
||||
if err := tx.QueryRow(ctx, `SELECT COUNT(*) FROM users WHERE id = $1`, userID).Scan(&userCount); err != nil {
|
||||
t.Fatalf("failed to count users: %v", err)
|
||||
}
|
||||
if userCount != 0 {
|
||||
t.Errorf("expected guest user to be deleted, found %d rows", userCount)
|
||||
}
|
||||
|
||||
// The saved card is soft-deleted and scrubbed. delete_guest_user UNLINKS
|
||||
// the card (user_id = NULL), so query by the card id captured at insert.
|
||||
var deletedAt, fingerprint, squareCardID, squareCustomerID interface{}
|
||||
var last4 string
|
||||
var userIDCol interface{}
|
||||
err = tx.QueryRow(ctx, `
|
||||
SELECT user_id, deleted_at, last_4, fingerprint, square_card_id, square_customer_id
|
||||
FROM user_saved_cards WHERE id = $1
|
||||
`, cardID).Scan(&userIDCol, &deletedAt, &last4, &fingerprint, &squareCardID, &squareCustomerID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to query saved card after deletion: %v", err)
|
||||
}
|
||||
|
||||
if userIDCol != nil {
|
||||
t.Errorf("expected user_id to be NULL (card unlinked), got %v", userIDCol)
|
||||
}
|
||||
if deletedAt == nil {
|
||||
t.Error("expected deleted_at to be set after delete_guest_user")
|
||||
}
|
||||
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 after delete_guest_user (external-system reference), got %v", squareCardID)
|
||||
}
|
||||
if squareCustomerID != nil {
|
||||
t.Errorf("expected square_customer_id to be NULL after delete_guest_user, got %v", squareCustomerID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnonymizeUser_ExpiresVerificationCodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
Reference in New Issue
Block a user