package admin import ( "context" "encoding/json" "errors" "regexp" "strings" "unicode/utf8" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/yuxingu/digital-psychology/apps/api/internal/repository" ) var ( ErrInvalidFeedSlot = errors.New("invalid feed slot") ErrFeedSlotConflict = errors.New("feed slot code conflict") slotCodeRe = regexp.MustCompile(`^[a-z][a-z0-9_]{1,62}$`) slotKeyRe = regexp.MustCompile(`^[a-z][a-z0-9_.]{1,62}$`) ) // FeedSlotWriteBody is JSON for create/update. type FeedSlotWriteBody struct { Code string `json:"code"` Title string `json:"title"` SlotKey string `json:"slot_key"` Placement string `json:"placement"` Active bool `json:"active"` } // CreateFeedSlot validates, inserts, audits. func (s *Service) CreateFeedSlot(ctx context.Context, adminID uuid.UUID, body FeedSlotWriteBody) (*repository.FeedSlotRow, error) { in, err := normalizeFeedSlotWrite(body) if err != nil { return nil, err } meta, _ := json.Marshal(map[string]any{"code": in.Code, "active": in.Active}) row, err := s.Repo.CreateFeedSlotWithAudit(ctx, adminID, in, meta) if repository.FeedSlotCodeConflict(err) { return nil, ErrFeedSlotConflict } return row, err } // UpdateFeedSlot validates, updates, audits. func (s *Service) UpdateFeedSlot(ctx context.Context, adminID, id uuid.UUID, body FeedSlotWriteBody) (*repository.FeedSlotRow, error) { in, err := normalizeFeedSlotWrite(body) if err != nil { return nil, err } meta, _ := json.Marshal(map[string]any{"code": in.Code, "active": in.Active}) row, err := s.Repo.UpdateFeedSlotWithAudit(ctx, adminID, id, in, meta) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrFeedSlotNotFound } if repository.FeedSlotCodeConflict(err) { return nil, ErrFeedSlotConflict } return row, err } func normalizeFeedSlotWrite(body FeedSlotWriteBody) (repository.FeedSlotWriteInput, error) { code := strings.TrimSpace(body.Code) title := strings.TrimSpace(body.Title) slotKey := strings.TrimSpace(body.SlotKey) placement := strings.TrimSpace(body.Placement) if !slotCodeRe.MatchString(code) || !slotKeyRe.MatchString(slotKey) { return repository.FeedSlotWriteInput{}, ErrInvalidFeedSlot } if title == "" || utf8.RuneCountInString(title) > 128 { return repository.FeedSlotWriteInput{}, ErrInvalidFeedSlot } if _, ok := bannerPlacements[placement]; !ok { return repository.FeedSlotWriteInput{}, ErrInvalidFeedSlot } return repository.FeedSlotWriteInput{ Code: code, Title: title, SlotKey: slotKey, Placement: placement, Active: body.Active, }, nil }