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.
This commit is contained in:
154
advertising/advertising.go
Normal file
154
advertising/advertising.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Service advertising gestisce l'elenco delle inserzioni pubblicitarie (Advertising Directory).
|
||||
package advertising
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"encore.dev/beta/errs"
|
||||
"encore.dev/storage/sqldb"
|
||||
"encore.dev/types/uuid"
|
||||
)
|
||||
|
||||
var db = sqldb.NewDatabase("advertising", sqldb.DatabaseConfig{
|
||||
Migrations: "./migrations",
|
||||
})
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusActive Status = "active"
|
||||
StatusInactive Status = "inactive"
|
||||
)
|
||||
|
||||
type Advertising struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Nome string `json:"nome"`
|
||||
Email string `json:"email"`
|
||||
Status Status `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type CreateAdvertisingParams struct {
|
||||
Nome string `json:"nome"`
|
||||
Email string `json:"email"`
|
||||
Status Status `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
func (p *CreateAdvertisingParams) Validate() error {
|
||||
if p == nil {
|
||||
return &errs.Error{Code: errs.InvalidArgument, Message: "i parametri di creazione sono obbligatori"}
|
||||
}
|
||||
|
||||
email, err := normalizeEmailValue(p.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Email = email
|
||||
|
||||
nome := normalizeText(p.Nome)
|
||||
if nome == "" {
|
||||
return &errs.Error{Code: errs.InvalidArgument, Message: "il nome è obbligatorio"}
|
||||
}
|
||||
p.Nome = nome
|
||||
|
||||
if p.Status == "" {
|
||||
p.Status = StatusActive
|
||||
} else if p.Status != StatusActive && p.Status != StatusInactive {
|
||||
return &errs.Error{Code: errs.InvalidArgument, Message: "lo status deve essere 'active' o 'inactive'"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeEmailValue(email string) (string, error) {
|
||||
email = strings.TrimSpace(strings.ToLower(email))
|
||||
if email == "" {
|
||||
return "", &errs.Error{Code: errs.InvalidArgument, Message: "l'email è obbligatoria"}
|
||||
}
|
||||
parsed, err := mail.ParseAddress(email)
|
||||
if err != nil || !strings.Contains(parsed.Address, "@") || !strings.Contains(parsed.Address, ".") {
|
||||
return "", &errs.Error{Code: errs.InvalidArgument, Message: "indirizzo email non valido"}
|
||||
}
|
||||
return strings.TrimSpace(strings.ToLower(parsed.Address)), nil
|
||||
}
|
||||
|
||||
func normalizeText(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Fields(value)
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
type ListAdvertisingResponse struct {
|
||||
Advertising []*Advertising `json:"advertising"`
|
||||
}
|
||||
|
||||
// List ritorna l'intera lista delle pubblicità ordinate per data di creazione.
|
||||
//
|
||||
//encore:api public method=GET path=/advertising
|
||||
func List(ctx context.Context) (*ListAdvertisingResponse, error) {
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT id, nome, email, status, created_at
|
||||
FROM advertising
|
||||
ORDER BY created_at DESC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, errs.WrapCode(err, errs.Internal, "errore durante il recupero delle pubblicità")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var list []*Advertising
|
||||
for rows.Next() {
|
||||
var a Advertising
|
||||
if err := rows.Scan(&a.ID, &a.Nome, &a.Email, &a.Status, &a.CreatedAt); err != nil {
|
||||
return nil, errs.WrapCode(err, errs.Internal, "errore durante la lettura del record pubblicità")
|
||||
}
|
||||
list = append(list, &a)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, errs.WrapCode(err, errs.Internal, "errore durante l'iterazione delle pubblicità")
|
||||
}
|
||||
|
||||
if list == nil {
|
||||
list = []*Advertising{}
|
||||
}
|
||||
|
||||
return &ListAdvertisingResponse{Advertising: list}, nil
|
||||
}
|
||||
|
||||
// Create crea un nuovo cliente/pubblicità con validazione dell'email e dei campi.
|
||||
//
|
||||
//encore:api public method=POST path=/advertising
|
||||
func Create(ctx context.Context, p *CreateAdvertisingParams) (*Advertising, error) {
|
||||
if err := p.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return nil, errs.WrapCode(err, errs.Internal, "impossibile generare l'identificativo UUID")
|
||||
}
|
||||
|
||||
item := Advertising{
|
||||
ID: id,
|
||||
Nome: p.Nome,
|
||||
Email: p.Email,
|
||||
Status: p.Status,
|
||||
}
|
||||
|
||||
err = db.QueryRow(ctx, `
|
||||
INSERT INTO advertising (id, nome, email, status)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING created_at
|
||||
`, id, p.Nome, p.Email, p.Status).Scan(&item.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, errs.WrapCode(err, errs.Internal, "impossibile creare la pubblicità")
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
92
advertising/advertising_test.go
Normal file
92
advertising/advertising_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package advertising
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCreateAdvertisingParamsValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
params *CreateAdvertisingParams
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil params",
|
||||
params: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty email",
|
||||
params: &CreateAdvertisingParams{
|
||||
Nome: "Azienda Rossi",
|
||||
Email: "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid email format",
|
||||
params: &CreateAdvertisingParams{
|
||||
Nome: "Azienda Rossi",
|
||||
Email: "invalid-email-address",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "email missing domain dot",
|
||||
params: &CreateAdvertisingParams{
|
||||
Nome: "Azienda Rossi",
|
||||
Email: "test@domain",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty name",
|
||||
params: &CreateAdvertisingParams{
|
||||
Nome: " ",
|
||||
Email: "azienda@rossi.it",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid status",
|
||||
params: &CreateAdvertisingParams{
|
||||
Nome: "Azienda Rossi",
|
||||
Email: "azienda@rossi.it",
|
||||
Status: "pending",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid with default active status",
|
||||
params: &CreateAdvertisingParams{
|
||||
Nome: " Azienda Rossi ",
|
||||
Email: " AZIENDA@rossi.it ",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid with explicit inactive status",
|
||||
params: &CreateAdvertisingParams{
|
||||
Nome: "Azienda Bianchi",
|
||||
Email: "info@bianchi.com",
|
||||
Status: StatusInactive,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.params.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if err == nil {
|
||||
if tt.params.Status != StatusActive && tt.params.Status != StatusInactive {
|
||||
t.Errorf("expected Status to be active or inactive, got %s", tt.params.Status)
|
||||
}
|
||||
if tt.params.Email != "azienda@rossi.it" && tt.params.Email != "info@bianchi.com" {
|
||||
t.Errorf("expected email to be normalized, got %s", tt.params.Email)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
7
advertising/migrations/1_create_advertising.up.sql
Normal file
7
advertising/migrations/1_create_advertising.up.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE advertising (
|
||||
id UUID PRIMARY KEY,
|
||||
nome TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('active', 'inactive')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
7
advertising/migrations/2_create_promo.up.sql
Normal file
7
advertising/migrations/2_create_promo.up.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE promo (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
position INT NOT NULL CHECK (position IN (0, 1, 2)),
|
||||
item JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
active_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
1
advertising/migrations/3_add_section_to_promo.up.sql
Normal file
1
advertising/migrations/3_add_section_to_promo.up.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE promo ADD COLUMN section VARCHAR(50) NOT NULL DEFAULT '';
|
||||
2
advertising/migrations/4_add_weeks_to_promo.up.sql
Normal file
2
advertising/migrations/4_add_weeks_to_promo.up.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE promo ADD COLUMN year INT NOT NULL DEFAULT EXTRACT(YEAR FROM NOW());
|
||||
ALTER TABLE promo ADD COLUMN weeks JSONB NOT NULL DEFAULT '[]'::jsonb;
|
||||
348
advertising/promo.go
Normal file
348
advertising/promo.go
Normal file
@@ -0,0 +1,348 @@
|
||||
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
|
||||
}
|
||||
214
advertising/promo_test.go
Normal file
214
advertising/promo_test.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package advertising
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCreatePromoParamsValidate(t *testing.T) {
|
||||
now := time.Now()
|
||||
tests := []struct {
|
||||
name string
|
||||
params *CreatePromoParams
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil params",
|
||||
params: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid negative position",
|
||||
params: &CreatePromoParams{
|
||||
Position: -1,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid position > 2",
|
||||
params: &CreatePromoParams{
|
||||
Position: 3,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid PromoLeft with week section",
|
||||
params: &CreatePromoParams{
|
||||
Position: PromoLeft,
|
||||
Section: string(PromoLeftWeek),
|
||||
ActiveFrom: &now,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid PromoRight1 with show section",
|
||||
params: &CreatePromoParams{
|
||||
Position: PromoRight1,
|
||||
Section: string(PromoRight1Show),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid PromoRight2 with items and top section",
|
||||
params: &CreatePromoParams{
|
||||
Position: PromoRight2,
|
||||
Section: string(PromoRight2Top),
|
||||
Item: []PromoItem{
|
||||
{
|
||||
Title: "Banner Estivo",
|
||||
Content: []PromoContent{
|
||||
{
|
||||
Width: 300,
|
||||
Height: 250,
|
||||
ImageURL: "https://example.com/banner.jpg",
|
||||
URL: "https://example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid section for PromoLeft",
|
||||
params: &CreatePromoParams{
|
||||
Position: PromoLeft,
|
||||
Section: "invalid-section",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid section for PromoRight1",
|
||||
params: &CreatePromoParams{
|
||||
Position: PromoRight1,
|
||||
Section: string(PromoLeftWeek),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid section for PromoRight2",
|
||||
params: &CreatePromoParams{
|
||||
Position: PromoRight2,
|
||||
Section: string(PromoRight1Show),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid week < 1",
|
||||
params: &CreatePromoParams{
|
||||
Position: PromoLeft,
|
||||
Section: string(PromoLeftWeek),
|
||||
Weeks: []int{0},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid week > 53",
|
||||
params: &CreatePromoParams{
|
||||
Position: PromoLeft,
|
||||
Section: string(PromoLeftWeek),
|
||||
Weeks: []int{54},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid weeks",
|
||||
params: &CreatePromoParams{
|
||||
Position: PromoLeft,
|
||||
Section: string(PromoLeftWeek),
|
||||
Weeks: []int{1, 10, 52},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.params.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if err == nil && tt.params.Item == nil {
|
||||
t.Errorf("expected Item slice to be initialized, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePromoParamsValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
params *UpdatePromoParams
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil params",
|
||||
params: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid negative position",
|
||||
params: &UpdatePromoParams{
|
||||
Position: -1,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid PromoLeft week",
|
||||
params: &UpdatePromoParams{
|
||||
Position: PromoLeft,
|
||||
Section: string(PromoLeftWeek),
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid PromoRight1 show with items",
|
||||
params: &UpdatePromoParams{
|
||||
Position: PromoRight1,
|
||||
Section: string(PromoRight1Show),
|
||||
Item: []PromoItem{
|
||||
{Title: "Updated title"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid section for PromoRight1",
|
||||
params: &UpdatePromoParams{
|
||||
Position: PromoRight1,
|
||||
Section: "invalid",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid week in update",
|
||||
params: &UpdatePromoParams{
|
||||
Position: PromoRight1,
|
||||
Section: string(PromoRight1Show),
|
||||
Weeks: []int{0},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid weeks in update",
|
||||
params: &UpdatePromoParams{
|
||||
Position: PromoRight1,
|
||||
Section: string(PromoRight1Show),
|
||||
Weeks: []int{5, 6, 7},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.params.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if err == nil && tt.params.Item == nil {
|
||||
t.Errorf("expected Item slice to be initialized, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user