package repositories import ( "encoding/json" "sort" "time" "nearle/models" "gorm.io/gorm" ) // Where the assistant's audit rows land. // // Deliberately thin: one insert and one read. The registry decides what an entry // means; this only has to keep it. type AssistantAuditRepository interface { Record(entry models.AssistantAudit) error // Recent reads the trail back for one tenant, newest first. // // Scoped by tenant even though this is a review surface, because "who looked // at what" is itself a merchant's data — a trail readable across tenants // would be a nicer version of the hole the trail exists to detect. Recent(tenantID, limit int) ([]models.AssistantAudit, error) } type assistantAuditRepository struct{ db *gorm.DB } func NewAssistantAuditRepository(db *gorm.DB) AssistantAuditRepository { return &assistantAuditRepository{db: db} } func (r *assistantAuditRepository) Record(entry models.AssistantAudit) error { if r.db == nil { return nil } return r.db.Create(&entry).Error } func (r *assistantAuditRepository) Recent(tenantID, limit int) ([]models.AssistantAudit, error) { if r.db == nil { return nil, nil } if limit <= 0 || limit > 500 { limit = 100 } var rows []models.AssistantAudit err := r.db.Where("tenantid = ?", tenantID). Order("at DESC").Limit(limit).Find(&rows).Error return rows, err } // EncodeAuditArgs renders arguments for storage. // // Keys sorted, so two identical calls store identical JSON and a query looking // for one of them finds both. Go randomises map iteration, and without this the // same call would be unsearchable across rows. // // A value that will not encode becomes a string rather than failing the write: // losing the audit row entirely to save one unencodable argument is the wrong // trade, and the row is still the record that the call happened. func EncodeAuditArgs(args map[string]any) string { if len(args) == 0 { return "{}" } ordered := make(map[string]json.RawMessage, len(args)) keys := make([]string, 0, len(args)) for key := range args { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { raw, err := json.Marshal(args[key]) if err != nil { raw, _ = json.Marshal("") } ordered[key] = raw } // Re-marshalled through an ordered slice of pairs so the output is stable; // a map would be re-randomised on the way out. var out []byte out = append(out, '{') for i, key := range keys { if i > 0 { out = append(out, ',') } name, _ := json.Marshal(key) out = append(out, name...) out = append(out, ':') out = append(out, ordered[key]...) } out = append(out, '}') return string(out) } // AuditDuration converts a duration for storage, rounding to the millisecond. func AuditDuration(d time.Duration) int64 { return d.Round(time.Millisecond).Milliseconds() }