package advertising import ( "context" "encoding/json" "errors" "sort" "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 } // PromoLeftGroup raggruppa le promozioni di PromoLeft per sezione ordinata. type PromoLeftGroup struct { Week []*Promo `json:"week"` Raccomended []*Promo `json:"raccomended"` Discover []*Promo `json:"discover"` } // PromoRight1Group raggruppa le promozioni di PromoRight1 per sezione ordinata. type PromoRight1Group struct { Show []*Promo `json:"show"` Service []*Promo `json:"service"` } // PromoRight2Group raggruppa le promozioni di PromoRight2 per sezione ordinata. type PromoRight2Group struct { Top []*Promo `json:"top"` Selected []*Promo `json:"selected"` } // WeekPromoBySection contiene i tre tipi di promo suddivisi per sezioni ordinate. type WeekPromoBySection struct { PromoLeft PromoLeftGroup `json:"promo_left"` PromoRight1 PromoRight1Group `json:"promo_right1"` PromoRight2 PromoRight2Group `json:"promo_right2"` } // WeekPromoResponse rappresenta il payload JSON restituito dall'endpoint GetWeekPromo. type WeekPromoResponse struct { Week int `json:"week"` Year int `json:"year"` PromoLeft []*Promo `json:"promo_left"` PromoRight1 []*Promo `json:"promo_right1"` PromoRight2 []*Promo `json:"promo_right2"` BySection *WeekPromoBySection `json:"by_section"` } // GetWeekPromoParams definisce i parametri query opzionali per GetWeekPromo. type GetWeekPromoParams struct { Year int `json:"year,omitempty" query:"year"` } // GetWeekPromoQueryParams definisce i parametri di query per GetCurrentWeekPromo. type GetWeekPromoQueryParams struct { Week int `json:"week,omitempty" query:"week"` Year int `json:"year,omitempty" query:"year"` } // getSectionOrder ritorna la priorità numerica della sezione basata sull'ordine delle costanti in promo.go. func getSectionOrder(pos PromoPosition, section string) int { switch pos { case PromoLeft: switch PromoLeftSections(section) { case PromoLeftWeek: return 1 case PromoLeftRaccomended: return 2 case PromoLeftDiscover: return 3 } case PromoRight1: switch PromoRight1Sections(section) { case PromoRight1Show: return 1 case PromoRight1Service: return 2 } case PromoRight2: switch PromoRight2Sections(section) { case PromoRight2Top: return 1 case PromoRight2Selected: return 2 } } return 99 } // isPromoActiveInWeek verifica se una promozione è valida per la settimana e l'anno specificati. func isPromoActiveInWeek(p *Promo, targetYear int, targetWeek int) bool { if targetYear > 0 && p.Year > 0 && p.Year != targetYear { return false } // Se l'array settimane è vuoto, la promozione è attiva per tutto l'anno if len(p.Weeks) == 0 { return true } for _, w := range p.Weeks { if w == targetWeek { return true } } return false } // buildWeekPromoResponse costruisce la risposta con i tre tipi di promo ordinati per sezione. func buildWeekPromoResponse(ctx context.Context, week int, year int) (*WeekPromoResponse, error) { if week < 1 || week > 53 { return nil, &errs.Error{ Code: errs.InvalidArgument, Message: "settimana non valida: deve essere compresa tra 1 e 53", } } if year <= 0 { nowYear, _ := time.Now().ISOWeek() year = nowYear } allPromos, err := queryPromos(ctx) if err != nil { return nil, err } resp := &WeekPromoResponse{ Week: week, Year: year, PromoLeft: make([]*Promo, 0), PromoRight1: make([]*Promo, 0), PromoRight2: make([]*Promo, 0), BySection: &WeekPromoBySection{ PromoLeft: PromoLeftGroup{ Week: make([]*Promo, 0), Raccomended: make([]*Promo, 0), Discover: make([]*Promo, 0), }, PromoRight1: PromoRight1Group{ Show: make([]*Promo, 0), Service: make([]*Promo, 0), }, PromoRight2: PromoRight2Group{ Top: make([]*Promo, 0), Selected: make([]*Promo, 0), }, }, } for _, p := range allPromos { if !isPromoActiveInWeek(p, year, week) { continue } switch p.Position { case PromoLeft: resp.PromoLeft = append(resp.PromoLeft, p) switch PromoLeftSections(p.Section) { case PromoLeftWeek: resp.BySection.PromoLeft.Week = append(resp.BySection.PromoLeft.Week, p) case PromoLeftRaccomended: resp.BySection.PromoLeft.Raccomended = append(resp.BySection.PromoLeft.Raccomended, p) case PromoLeftDiscover: resp.BySection.PromoLeft.Discover = append(resp.BySection.PromoLeft.Discover, p) } case PromoRight1: resp.PromoRight1 = append(resp.PromoRight1, p) switch PromoRight1Sections(p.Section) { case PromoRight1Show: resp.BySection.PromoRight1.Show = append(resp.BySection.PromoRight1.Show, p) case PromoRight1Service: resp.BySection.PromoRight1.Service = append(resp.BySection.PromoRight1.Service, p) } case PromoRight2: resp.PromoRight2 = append(resp.PromoRight2, p) switch PromoRight2Sections(p.Section) { case PromoRight2Top: resp.BySection.PromoRight2.Top = append(resp.BySection.PromoRight2.Top, p) case PromoRight2Selected: resp.BySection.PromoRight2.Selected = append(resp.BySection.PromoRight2.Selected, p) } } } sortPromosBySection := func(promos []*Promo, pos PromoPosition) { sort.SliceStable(promos, func(i, j int) bool { ordI := getSectionOrder(pos, promos[i].Section) ordJ := getSectionOrder(pos, promos[j].Section) if ordI != ordJ { return ordI < ordJ } return promos[i].ID < promos[j].ID }) } sortPromosBySection(resp.PromoLeft, PromoLeft) sortPromosBySection(resp.PromoRight1, PromoRight1) sortPromosBySection(resp.PromoRight2, PromoRight2) return resp, nil } // GetWeekPromo ritorna un JSON con i tre tipi di promo (PromoLeft, PromoRight1, PromoRight2) // filtrati per la settimana e l'anno specificati, ordinati per le sezioni dichiarate in promo.go. // //encore:api public method=GET path=/promo/week/:week func GetWeekPromo(ctx context.Context, week int, params *GetWeekPromoParams) (*WeekPromoResponse, error) { year := 0 if params != nil { year = params.Year } return buildWeekPromoResponse(ctx, week, year) } // GetCurrentWeekPromo ritorna le promozioni della settimana (con settimana e anno opzionali via query ?week=W&year=Y). // //encore:api public method=GET path=/promo/week func GetCurrentWeekPromo(ctx context.Context, params *GetWeekPromoQueryParams) (*WeekPromoResponse, error) { nowYear, nowWeek := time.Now().ISOWeek() week := nowWeek year := nowYear if params != nil { if params.Week > 0 { week = params.Week } if params.Year > 0 { year = params.Year } } return buildWeekPromoResponse(ctx, week, year) }