05becb04d0
gates / gates (push) Successful in 4m9s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
71 lines
2.7 KiB
Go
71 lines
2.7 KiB
Go
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
|
|
}
|