Files
backend_fiesta/repositories/assistantAuditRepository.go
2026-09-23 17:26:13 +05:30

100 lines
2.8 KiB
Go

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("<unencodable>")
}
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() }