package service import ( "context" "encoding/json" "fmt" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" applogger "github.com/gochat/gochat/pkg/logger" ) // AuditService implements business logic for AuditLog operations. // Reference: Chatwoot enterprise audit log feature + P2B M11 spec type AuditService struct { repo *repository.AuditRepo } // AuditRecord captures the request/user metadata needed to create a Chatwoot-style audit row. type AuditRecord struct { AccountID uint UserID uint Username string AuditableType string AuditableID uint Action string AuditedChanges interface{} RemoteAddress string RequestUUID string Comment string } // NewAuditService creates a new AuditLog service. func NewAuditService(repo *repository.AuditRepo) *AuditService { return &AuditService{repo: repo} } // ListByAccount retrieves paginated audit log entries for an account with optional filters. // page and pageSize follow the project pagination convention (page >= 1, pageSize capped at 100). func (s *AuditService) ListByAccount(ctx context.Context, accountID uint, action string, auditableType string, page int, pageSize int) ([]model.Audit, int64, error) { if page < 1 { page = 1 } if pageSize < 1 { pageSize = 25 } if pageSize > 100 { pageSize = 100 } offset := (page - 1) * pageSize audits, total, err := s.repo.FindByAccount(ctx, accountID, action, auditableType, offset, pageSize) if err != nil { applogger.L().Errorf("AuditService.ListByAccount account=%d action=%s auditableType=%s: %v", accountID, action, auditableType, err) return nil, 0, fmt.Errorf("failed to list audit logs: %w", err) } return audits, total, nil } // CreateAudit creates a new audit log entry. func (s *AuditService) CreateAudit(ctx context.Context, audit *model.Audit) (*model.Audit, error) { if err := s.repo.Create(ctx, audit); err != nil { applogger.L().Errorf("AuditService.CreateAudit: %v", err) return nil, fmt.Errorf("failed to create audit log: %w", err) } return audit, nil } // Record creates an account-associated audit row for a mutating account resource. func (s *AuditService) Record(ctx context.Context, record AuditRecord) (*model.Audit, error) { if record.AccountID == 0 || record.AuditableType == "" || record.AuditableID == 0 || record.Action == "" { return nil, fmt.Errorf("invalid audit record") } changes, err := marshalAuditChanges(record.AuditedChanges) if err != nil { return nil, fmt.Errorf("invalid audited changes: %w", err) } accountID := record.AccountID audit := &model.Audit{ AccountID: &accountID, AuditableType: record.AuditableType, AuditableID: record.AuditableID, Action: record.Action, AuditedChanges: changes, AssociatedType: "Account", AssociatedID: &accountID, Username: record.Username, RemoteAddress: record.RemoteAddress, RequestUUID: record.RequestUUID, Comment: record.Comment, } if record.UserID != 0 { userID := record.UserID audit.UserID = &userID audit.UserType = "User" } return s.CreateAudit(ctx, audit) } // GetByID retrieves a single audit log entry by ID. func (s *AuditService) GetByID(ctx context.Context, id uint) (*model.Audit, error) { audit, err := s.repo.FindByID(ctx, id) if err != nil { applogger.L().Errorf("AuditService.GetByID id=%d: %v", id, err) return nil, fmt.Errorf("audit log not found: %w", err) } return audit, nil } // GetByIDForAccount retrieves a single audit log entry scoped to an account. func (s *AuditService) GetByIDForAccount(ctx context.Context, accountID uint, id uint) (*model.Audit, error) { audit, err := s.repo.FindByIDForAccount(ctx, accountID, id) if err != nil { applogger.L().Errorf("AuditService.GetByIDForAccount account=%d id=%d: %v", accountID, id, err) return nil, fmt.Errorf("audit log not found: %w", err) } return audit, nil } func marshalAuditChanges(changes interface{}) (json.RawMessage, error) { if changes == nil { return json.RawMessage(`{}`), nil } if raw, ok := changes.(json.RawMessage); ok { if len(raw) == 0 { return json.RawMessage(`{}`), nil } return raw, nil } data, err := json.Marshal(changes) if err != nil { return nil, err } return json.RawMessage(data), nil }