diff --git a/backend/gestion/models/product.go b/backend/gestion/models/product.go index 83b8edcc..9225c778 100644 --- a/backend/gestion/models/product.go +++ b/backend/gestion/models/product.go @@ -19,12 +19,19 @@ type Product struct { func (Product) TableName() string { return "products" } type ProductPrice struct { - ID int `json:"id" gorm:"primaryKey;autoIncrement"` - ProductID int `json:"product_id" gorm:"column:product_id;index"` - Quantity float64 `json:"quantity" gorm:"column:quantity" binding:"required"` - Price float64 `json:"price" gorm:"column:price" binding:"required"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` - ActivePrice bool `json:"active_price" gorm:"column:active_price;default:true"` + ID int `json:"id" gorm:"primaryKey;autoIncrement"` + ProductID int `json:"product_id" gorm:"column:product_id;index"` + Quantity float64 `json:"quantity" gorm:"column:quantity" binding:"required"` + Price float64 `json:"price" gorm:"column:price" binding:"required"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + // Pas de tag gorm "default:true" ici : GORM omet de l'INSERT tout champ + // dont la valeur Go est la valeur zéro (false) s'il porte un tag + // "default", laissant Postgres appliquer sa propre valeur par défaut + // (TRUE) à la place — un prix explicitement désactivé (false) revenait + // donc toujours actif après un Create(). La colonne a déjà son défaut + // TRUE posé au niveau SQL (db_init.go), ce tag Go était redondant et + // seulement source du bug. + ActivePrice bool `json:"active_price" gorm:"column:active_price"` } func (ProductPrice) TableName() string { return "product_prices" } diff --git a/backend/gestion/tests/stock_coverage_test.go b/backend/gestion/tests/stock_coverage_test.go index 678b2b05..76421889 100644 --- a/backend/gestion/tests/stock_coverage_test.go +++ b/backend/gestion/tests/stock_coverage_test.go @@ -278,6 +278,55 @@ func TestUpdateProduct_UpdatesStockWhenProvided(t *testing.T) { } } +// productPriceActiveFlags relit active_price par palier de quantité pour un +// produit, directement en base. +func productPriceActiveFlags(t *testing.T, productID int) map[float64]bool { + t.Helper() + var rows []struct { + Quantity float64 `gorm:"column:quantity"` + ActivePrice bool `gorm:"column:active_price"` + } + if err := testDB.GDB.Raw( + `SELECT quantity, active_price FROM product_prices WHERE product_id = ?`, productID, + ).Scan(&rows).Error; err != nil { + t.Fatalf("lecture active_price: %v", err) + } + out := make(map[float64]bool, len(rows)) + for _, r := range rows { + out[r.Quantity] = r.ActivePrice + } + return out +} + +// Un prix explicitement désactivé (active_price=false) doit rester désactivé +// après UpdateProduct — piège classique de GORM : un champ bool à sa valeur +// zéro (false) avec un tag gorm "default" est omis de l'INSERT, laissant la +// base appliquer son propre défaut (TRUE) à la place. Voir le commentaire sur +// ProductPrice.ActivePrice dans models/product.go. +func TestUpdateProduct_PersistsInactivePriceFlag(t *testing.T) { + cleanupStockTestData(t) + cat := newTestCategory(t, "CatInactive") + productID := newTestProduct(t, "UPInactive", 5) + + body := []byte(fmt.Sprintf( + `{"name":"UPInactive","category":%q,"description":"d2","unit":"g","stock":10,"prices":[{"quantity":1,"price":5,"active_price":false},{"quantity":5,"price":20,"active_price":true}]}`, + cat, + )) + c, rec := updateProductJSONContext("admin", body, productID) + handlers.UpdateProduct(c) + if rec.Code != http.StatusOK { + t.Fatalf("UpdateProduct valide doit réussir: got=%d body=%s", rec.Code, rec.Body.String()) + } + + flags := productPriceActiveFlags(t, productID) + if flags[1] != false { + t.Errorf("palier qty=1 doit rester désactivé après UpdateProduct: got active_price=%v", flags[1]) + } + if flags[5] != true { + t.Errorf("palier qty=5 doit rester actif après UpdateProduct: got active_price=%v", flags[5]) + } +} + // ── DeleteProduct ──────────────────────────────────────────────────────── func TestDeleteProduct_RemovesProductWithoutMedia(t *testing.T) {