package processor import ( "context" "encoding/json" "errors" "fmt" "strings" "github.com/google/uuid" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/entities" "apskel-pos-be/internal/repository" ) // ErrInvalidAuditEntry means an audit entry lacks what every row must say. var ErrInvalidAuditEntry = errors.New("invalid audit entry") // AuditEntry is one change to record. Before and After are marshalled to JSON; leave // one nil when there was nothing before (a create) or after (a delete). type AuditEntry struct { OrganizationID uuid.UUID // USER or SYSTEM. A USER entry needs ActorID. ActorType string ActorID *uuid.UUID EntityType string EntityID uuid.UUID Action string Before any After any Reason *string Source string } // AuditLogger writes audit_logs (docs/rfc-enakgame.md ยง13). It must be called inside // the transaction that makes the change, so the change and its row commit or roll // back together: outside one it returns repository.ErrAuditTxRequired. type AuditLogger struct { repo repository.AuditLogRepository } func NewAuditLogger(repo repository.AuditLogRepository) *AuditLogger { return &AuditLogger{repo: repo} } func (l *AuditLogger) Record(ctx context.Context, e AuditEntry) error { invalid := func(format string, args ...any) error { return fmt.Errorf("%w: %s", ErrInvalidAuditEntry, fmt.Sprintf(format, args...)) } switch { case e.OrganizationID == uuid.Nil: return invalid("organization is required") case e.ActorType != constants.AuditActorUser && e.ActorType != constants.AuditActorSystem: return invalid("unknown actor type %q", e.ActorType) case e.ActorType == constants.AuditActorUser && isNilID(e.ActorID): return invalid("a USER entry requires the actor") case strings.TrimSpace(e.EntityType) == "" || e.EntityID == uuid.Nil: return invalid("entity is required") case strings.TrimSpace(e.Action) == "": return invalid("action is required") case strings.TrimSpace(e.Source) == "": return invalid("source is required") } before, err := auditJSON(e.Before) if err != nil { return invalid("before: %v", err) } after, err := auditJSON(e.After) if err != nil { return invalid("after: %v", err) } return l.repo.Insert(ctx, &entities.AuditLog{ OrganizationID: e.OrganizationID, ActorType: e.ActorType, ActorID: e.ActorID, EntityType: e.EntityType, EntityID: e.EntityID, Action: e.Action, Before: before, After: after, Reason: e.Reason, Source: e.Source, }) } func auditJSON(v any) (json.RawMessage, error) { if v == nil { return nil, nil } raw, ok := v.(json.RawMessage) if !ok { var err error if raw, err = json.Marshal(v); err != nil { return nil, err } } // A nil pointer is nothing, the same as nil. if string(raw) == "null" { return nil, nil } return raw, nil }