package store import ( "encoding/json" "fmt" "time" ) // R-901 (operator ruling 2026-10-08 09:04, `09` §3 decision 181): after a customer is DELETED, the hub's audit rows for // it — `events` and `notification_log` — are kept 1 year, then deleted. // // The delete cascade (web/customer_delete.go) deliberately leaves both tables („the audit trail outlives every // lifecycle tier"), and nothing ever pruned `notification_log`; `events` are pruned for EVERY customer at // retention.max_days (90 on the live hub), so in practice the year bites `notification_log`. Both are covered here so the // rule holds if max_days is ever raised. // // WHICH customers: the ones whose delete cascade COMPLETED — a `customer_resets` journal row with leg // `customer_delete` = `ok` and a `completed_at` stamp (a RESET has no such leg and is not a deletion). WHICH rows: only // those created AT OR BEFORE that completion — a customer id re-used after the deletion keeps everything it made since. // The journal row itself stays (F-14 provenance: an id and timestamps, no personal data). // // Pinned by TestR901_* (deleted_customer_audit_test.go). const DeletedCustomerAuditKeep = 365 * 24 * time.Hour // PruneDeletedCustomerAudit deletes the events and notification_log rows of customers whose deletion completed before // now-keep. Returns the rows deleted per table. func (s *Store) PruneDeletedCustomerAudit(now time.Time, keep time.Duration) (events, notifications int64, err error) { rows, err := s.db.Query(`SELECT customer_id, completed_at, legs_json FROM customer_resets WHERE completed_at IS NOT NULL`) if err != nil { return 0, 0, err } type del struct { id string at time.Time } var dels []del for rows.Next() { var id, at, legs string if err := rows.Scan(&id, &at, &legs); err != nil { rows.Close() return 0, 0, err } m := map[string]string{} if json.Unmarshal([]byte(legs), &m) != nil || m["customer_delete"] != "ok" { continue } t := parseSQLiteTime(at) if t.IsZero() || now.Sub(t) <= keep { continue } dels = append(dels, del{id, t.UTC()}) } rows.Close() for _, d := range dels { cutoff := d.at.Format("2006-01-02 15:04:05") r, err := s.db.Exec(`DELETE FROM events WHERE customer_id = ? AND created_at <= ?`, d.id, cutoff) if err != nil { return events, notifications, fmt.Errorf("PruneDeletedCustomerAudit %s: events: %w", d.id, err) } n, _ := r.RowsAffected() events += n r, err = s.db.Exec(`DELETE FROM notification_log WHERE customer_id = ? AND created_at <= ?`, d.id, cutoff) if err != nil { return events, notifications, fmt.Errorf("PruneDeletedCustomerAudit %s: notification_log: %w", d.id, err) } n, _ = r.RowsAffected() notifications += n } return events, notifications, nil }