package repository import ( "context" "encoding/json" "errors" "strings" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" ) // BannerWriteInput is create/update payload for ops_banners. type BannerWriteInput struct { Code string Title string Placement string ImageURL *string LinkPath *string SortOrder int Active bool } // ListActiveBanners returns active banners for a placement (C-end). func (r *AdminRepo) ListActiveBanners(ctx context.Context, placement string) ([]BannerRow, error) { rows, err := r.Pool.Query(ctx, ` SELECT id, code, title, placement, image_url, link_path, sort_order, active, system, updated_at FROM ops_banners WHERE active = true AND placement = $1 ORDER BY sort_order ASC, code ASC LIMIT 100`, placement) if err != nil { return nil, err } defer rows.Close() var out []BannerRow for rows.Next() { var b BannerRow if err := rows.Scan( &b.ID, &b.Code, &b.Title, &b.Placement, &b.ImageURL, &b.LinkPath, &b.SortOrder, &b.Active, &b.System, &b.UpdatedAt, ); err != nil { return nil, err } out = append(out, b) } return out, rows.Err() } // CreateBannerWithAudit inserts a banner and audits. func (r *AdminRepo) CreateBannerWithAudit( ctx context.Context, adminID uuid.UUID, in BannerWriteInput, meta json.RawMessage, ) (*BannerRow, error) { tx, err := r.Pool.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) var b BannerRow err = tx.QueryRow(ctx, ` INSERT INTO ops_banners(code, title, placement, image_url, link_path, sort_order, active, system) VALUES ($1,$2,$3,$4,$5,$6,$7,false) RETURNING id, code, title, placement, image_url, link_path, sort_order, active, system, updated_at`, in.Code, in.Title, in.Placement, in.ImageURL, in.LinkPath, in.SortOrder, in.Active, ).Scan( &b.ID, &b.Code, &b.Title, &b.Placement, &b.ImageURL, &b.LinkPath, &b.SortOrder, &b.Active, &b.System, &b.UpdatedAt, ) if err != nil { return nil, mapBannerWriteErr(err) } if meta == nil { meta = json.RawMessage(`{}`) } if _, err := tx.Exec(ctx, ` INSERT INTO admin_audit_logs(admin_id, action, target_type, target_id, meta) VALUES ($1,'cms.banner.create','banner',$2,$3)`, adminID, b.ID.String(), meta, ); err != nil { return nil, err } if err := tx.Commit(ctx); err != nil { return nil, err } return &b, nil } // UpdateBannerWithAudit updates mutable fields and audits. func (r *AdminRepo) UpdateBannerWithAudit( ctx context.Context, adminID, id uuid.UUID, in BannerWriteInput, meta json.RawMessage, ) (*BannerRow, error) { tx, err := r.Pool.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) var system bool var oldCode string err = tx.QueryRow(ctx, `SELECT system, code FROM ops_banners WHERE id=$1`, id).Scan(&system, &oldCode) if errors.Is(err, pgx.ErrNoRows) { return nil, pgx.ErrNoRows } if err != nil { return nil, err } code := in.Code if system { code = oldCode } var b BannerRow err = tx.QueryRow(ctx, ` UPDATE ops_banners SET code=$2, title=$3, placement=$4, image_url=$5, link_path=$6, sort_order=$7, active=$8, updated_at=now() WHERE id=$1 RETURNING id, code, title, placement, image_url, link_path, sort_order, active, system, updated_at`, id, code, in.Title, in.Placement, in.ImageURL, in.LinkPath, in.SortOrder, in.Active, ).Scan( &b.ID, &b.Code, &b.Title, &b.Placement, &b.ImageURL, &b.LinkPath, &b.SortOrder, &b.Active, &b.System, &b.UpdatedAt, ) if err != nil { return nil, mapBannerWriteErr(err) } if meta == nil { meta = json.RawMessage(`{}`) } if _, err := tx.Exec(ctx, ` INSERT INTO admin_audit_logs(admin_id, action, target_type, target_id, meta) VALUES ($1,'cms.banner.update','banner',$2,$3)`, adminID, id.String(), meta, ); err != nil { return nil, err } if err := tx.Commit(ctx); err != nil { return nil, err } return &b, nil } func mapBannerWriteErr(err error) error { var pgErr *pgconn.PgError if errors.As(err, &pgErr) && pgErr.Code == "23505" { return errString("banner code conflict") } return err } // BannerCodeConflict reports unique code violation. func BannerCodeConflict(err error) bool { return err != nil && strings.Contains(err.Error(), "banner code conflict") }