Files
encore-test/advertising/promo.go
fabio 664db8f7a0 feat(advertising): implement advertising service with CRUD operations and validation
- Added advertising.go for managing advertising entries with Create and List functionalities.
- Implemented CreateAdvertisingParams with validation for name, email, and status.
- Created migrations for advertising table structure.
- Added tests for advertising parameters validation.

feat(promo): introduce promo management with validation and CRUD operations

- Added promo.go for managing promotional entries with Create, Update, and List functionalities.
- Implemented CreatePromoParams and UpdatePromoParams with validation for position, section, and weeks.
- Created migrations for promo table structure.
- Added tests for promo parameters validation.

feat(images): add image management service with upload and retrieval

- Added images.go for managing image uploads and retrieval from storage.
- Implemented SaveImageParams with validation for image description and data.
- Created migrations for images table structure.
- Added tests for image parameters validation and data parsing.
2026-10-01 20:19:07 +02:00

349 lines
9.2 KiB
Go

package advertising
import (
"context"
"encoding/json"
"errors"
"time"
"encore.dev/beta/errs"
"encore.dev/storage/sqldb"
)
type PromoPosition int
const (
PromoLeft PromoPosition = 0
PromoRight1 PromoPosition = 1
PromoRight2 PromoPosition = 2
)
type PromoLeftSections string
const (
PromoLeftWeek PromoLeftSections = "week"
PromoLeftRaccomended PromoLeftSections = "raccomended"
PromoLeftDiscover PromoLeftSections = "discover"
)
type PromoImage struct {
Width int `json:"width"`
Height int `json:"height"`
Proportion string `json:"proportion"`
}
var PromoLeftImageSize = PromoImage{
Width: 230,
Height: 0,
Proportion: "2:3",
}
var PromoRight1ImageSize = PromoImage{
Width: 230,
Height: 0,
Proportion: "2:3",
}
var PromoRight2ImageSize = PromoImage{
Width: 230,
Height: 345,
Proportion: "",
}
type PromoRight1Sections string
const (
PromoRight1Show PromoRight1Sections = "show"
PromoRight1Service PromoRight1Sections = "service"
)
type PromoRight2Sections string
const (
PromoRight2Top PromoRight2Sections = "top"
PromoRight2Selected PromoRight2Sections = "selected"
)
type PromoContent struct {
Width int `json:"width"`
Height int `json:"height"`
MobileOnly bool `json:"mobile_only"`
MobileImageURL string `json:"mobile_image_url"`
NewWindow bool `json:"new_window"`
ImageURL string `json:"image_url"`
URL string `json:"url"`
}
type PromoItem struct {
Title string `json:"title"`
Content []PromoContent `json:"content"`
}
type Promo struct {
ID int64 `json:"id"`
Position PromoPosition `json:"position"`
Section string `json:"section"`
Item []PromoItem `json:"item"`
Year int `json:"year"`
Weeks []int `json:"weeks"`
ActiveFrom time.Time `json:"active_from"`
CreatedAt time.Time `json:"created_at"`
}
type CreatePromoParams struct {
Position PromoPosition `json:"position"`
Section string `json:"section"`
Item []PromoItem `json:"item"`
Year int `json:"year,omitempty"`
Weeks []int `json:"weeks,omitempty"`
ActiveFrom *time.Time `json:"active_from,omitempty"`
}
func validatePromoFields(position PromoPosition, section string) error {
if position < PromoLeft || position > PromoRight2 {
return &errs.Error{Code: errs.InvalidArgument, Message: "posizione promo non valida, ammessi solo 0 (PromoLeft), 1 (PromoRight1), 2 (PromoRight2)"}
}
switch position {
case PromoLeft:
if section != string(PromoLeftWeek) && section != string(PromoLeftRaccomended) && section != string(PromoLeftDiscover) {
return &errs.Error{Code: errs.InvalidArgument, Message: "sezione non valida per PromoLeft, ammessi solo: 'week', 'raccomended', 'discover'"}
}
case PromoRight1:
if section != string(PromoRight1Show) && section != string(PromoRight1Service) {
return &errs.Error{Code: errs.InvalidArgument, Message: "sezione non valida per PromoRight1, ammessi solo: 'show', 'service'"}
}
case PromoRight2:
if section != string(PromoRight2Top) && section != string(PromoRight2Selected) {
return &errs.Error{Code: errs.InvalidArgument, Message: "sezione non valida per PromoRight2, ammessi solo: 'top', 'selected'"}
}
}
return nil
}
func validatePromoWeeks(weeks []int) error {
for _, w := range weeks {
if w < 1 || w > 53 {
return &errs.Error{Code: errs.InvalidArgument, Message: "numero settimana non valido, deve essere compreso tra 1 e 53"}
}
}
return nil
}
func (p *CreatePromoParams) Validate() error {
if p == nil {
return &errs.Error{Code: errs.InvalidArgument, Message: "i parametri di creazione sono obbligatori"}
}
if err := validatePromoFields(p.Position, p.Section); err != nil {
return err
}
if p.Item == nil {
p.Item = []PromoItem{}
}
if p.Weeks == nil {
p.Weeks = []int{}
}
if err := validatePromoWeeks(p.Weeks); err != nil {
return err
}
if p.Year == 0 {
p.Year = time.Now().Year()
}
return nil
}
type UpdatePromoParams struct {
Position PromoPosition `json:"position"`
Section string `json:"section"`
Item []PromoItem `json:"item"`
Year int `json:"year,omitempty"`
Weeks []int `json:"weeks,omitempty"`
ActiveFrom *time.Time `json:"active_from,omitempty"`
}
func (p *UpdatePromoParams) Validate() error {
if p == nil {
return &errs.Error{Code: errs.InvalidArgument, Message: "i parametri di aggiornamento sono obbligatori"}
}
if err := validatePromoFields(p.Position, p.Section); err != nil {
return err
}
if p.Item == nil {
p.Item = []PromoItem{}
}
if p.Weeks == nil {
p.Weeks = []int{}
}
if err := validatePromoWeeks(p.Weeks); err != nil {
return err
}
if p.Year == 0 {
p.Year = time.Now().Year()
}
return nil
}
type ListPromoResponse struct {
Promos []*Promo `json:"promos"`
}
type ListPromotionsResponse struct {
Promotions []*Promo `json:"promotions"`
}
func queryPromos(ctx context.Context) ([]*Promo, error) {
rows, err := db.Query(ctx, `
SELECT id, position, section, item, year, weeks, active_from, created_at
FROM promo
ORDER BY active_from DESC, id DESC
`)
if err != nil {
return nil, errs.WrapCode(err, errs.Internal, "errore durante il recupero delle promozioni")
}
defer rows.Close()
var promos []*Promo
for rows.Next() {
var (
p Promo
itemBytes []byte
weeksBytes []byte
)
if err := rows.Scan(&p.ID, &p.Position, &p.Section, &itemBytes, &p.Year, &weeksBytes, &p.ActiveFrom, &p.CreatedAt); err != nil {
return nil, errs.WrapCode(err, errs.Internal, "errore durante la lettura del record promo")
}
if len(itemBytes) > 0 {
if err := json.Unmarshal(itemBytes, &p.Item); err != nil {
p.Item = []PromoItem{}
}
} else {
p.Item = []PromoItem{}
}
if len(weeksBytes) > 0 {
if err := json.Unmarshal(weeksBytes, &p.Weeks); err != nil {
p.Weeks = []int{}
}
} else {
p.Weeks = []int{}
}
promos = append(promos, &p)
}
if err := rows.Err(); err != nil {
return nil, errs.WrapCode(err, errs.Internal, "errore durante l'iterazione delle promozioni")
}
if promos == nil {
promos = []*Promo{}
}
return promos, nil
}
// ListPromos ritorna la lista completa di tutte le promozioni.
//
//encore:api public method=GET path=/promo
func ListPromos(ctx context.Context) (*ListPromoResponse, error) {
promos, err := queryPromos(ctx)
if err != nil {
return nil, err
}
return &ListPromoResponse{Promos: promos}, nil
}
// ListPromotions ritorna la lista completa delle promozioni arricchite con le settimane lavorative attive.
//
//encore:api public method=GET path=/promotions
func ListPromotions(ctx context.Context) (*ListPromotionsResponse, error) {
promos, err := queryPromos(ctx)
if err != nil {
return nil, err
}
return &ListPromotionsResponse{Promotions: promos}, nil
}
// CreatePromo inserisce una nuova promozione nel database.
//
//encore:api public method=POST path=/promo
func CreatePromo(ctx context.Context, p *CreatePromoParams) (*Promo, error) {
if err := p.Validate(); err != nil {
return nil, err
}
itemJSON, err := json.Marshal(p.Item)
if err != nil {
return nil, errs.WrapCode(err, errs.InvalidArgument, "formato item non valido")
}
weeksJSON, err := json.Marshal(p.Weeks)
if err != nil {
return nil, errs.WrapCode(err, errs.InvalidArgument, "formato settimane non valido")
}
activeFrom := time.Now()
if p.ActiveFrom != nil && !p.ActiveFrom.IsZero() {
activeFrom = *p.ActiveFrom
}
var promo Promo
promo.Position = p.Position
promo.Section = p.Section
promo.Item = p.Item
promo.Year = p.Year
promo.Weeks = p.Weeks
promo.ActiveFrom = activeFrom
err = db.QueryRow(ctx, `
INSERT INTO promo (position, section, item, year, weeks, active_from)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, created_at
`, p.Position, p.Section, itemJSON, p.Year, weeksJSON, activeFrom).Scan(&promo.ID, &promo.CreatedAt)
if err != nil {
return nil, errs.WrapCode(err, errs.Internal, "impossibile creare la promozione")
}
return &promo, nil
}
// UpdatePromo aggiorna una promozione esistente nel database.
//
//encore:api public method=PUT path=/promo/:id
func UpdatePromo(ctx context.Context, id int64, p *UpdatePromoParams) (*Promo, error) {
if err := p.Validate(); err != nil {
return nil, err
}
itemJSON, err := json.Marshal(p.Item)
if err != nil {
return nil, errs.WrapCode(err, errs.InvalidArgument, "formato item non valido")
}
weeksJSON, err := json.Marshal(p.Weeks)
if err != nil {
return nil, errs.WrapCode(err, errs.InvalidArgument, "formato settimane non valido")
}
var promo Promo
promo.ID = id
promo.Position = p.Position
promo.Section = p.Section
promo.Item = p.Item
promo.Year = p.Year
promo.Weeks = p.Weeks
query := `
UPDATE promo
SET position = $1, section = $2, item = $3, year = $4, weeks = $5,
active_from = COALESCE($6, active_from)
WHERE id = $7
RETURNING active_from, created_at
`
err = db.QueryRow(ctx, query, p.Position, p.Section, itemJSON, p.Year, weeksJSON, p.ActiveFrom, id).Scan(&promo.ActiveFrom, &promo.CreatedAt)
if errors.Is(err, sqldb.ErrNoRows) {
return nil, &errs.Error{Code: errs.NotFound, Message: "promozione non trovata"}
}
if err != nil {
return nil, errs.WrapCode(err, errs.Internal, "impossibile aggiornare la promozione")
}
return &promo, nil
}