Files
felhom.eu/hub/internal/store/deleted_customer_audit.go
T

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
}