chore: build
Backend - Build & Lint / build (push) Canceled after 11m16s
Frontend Admin - EAS Build / build (push) Canceled after 0s
Frontend Client - EAS Build / build (push) Canceled after 0s
Frontend Web - Build & Lint / build (push) Canceled after 0s

This commit is contained in:
Xor290
2026-09-08 17:44:28 +02:00
parent 06abfee274
commit 82f9a2fae9
37 changed files with 2164 additions and 598 deletions
+12 -3
View File
@@ -130,11 +130,14 @@ func (d *Database) HasOnlyRewardItems(username string) (bool, error) {
func (d *Database) AddToBasket(username string, productID int, quantity float64) (*models.Panier, error) {
var basket models.Panier
err := d.GDB.Transaction(func(tx *gorm.DB) error {
var currentStock float64
if err := tx.Raw(`SELECT stock FROM products WHERE id = ? FOR UPDATE`, productID).Scan(&currentStock).Error; err != nil {
var productInfo struct {
Stock float64 `gorm:"column:stock"`
Category string `gorm:"column:category"`
}
if err := tx.Raw(`SELECT stock, category FROM products WHERE id = ? FOR UPDATE`, productID).Scan(&productInfo).Error; err != nil {
return fmt.Errorf("erreur lecture stock: %w", err)
}
if currentStock < quantity {
if productInfo.Stock < quantity {
return fmt.Errorf("stock insuffisant")
}
@@ -148,6 +151,12 @@ func (d *Database) AddToBasket(username string, productID int, quantity float64)
productID, quantity).Scan(&priceResult).Error; err != nil || priceResult.Price == 0 {
return fmt.Errorf("prix introuvable pour product_id=%d qty=%.3f", productID, quantity)
}
// Une promotion active pour ce produit/quantité/catégorie s'applique
// automatiquement au prix facturé — indépendamment des points de
// fidélité (contrairement aux récompenses par palier).
if discounted, ok := d.ApplyPromotionToPrice(productID, productInfo.Category, quantity, priceResult.Price); ok {
priceResult.Price = discounted
}
var existing struct {
ID int `gorm:"column:id"`
+48
View File
@@ -0,0 +1,48 @@
package db
import (
"gestion/models"
"math"
)
// ResolvePromotionDiscount retourne le pourcentage de réduction actif pour un
// produit, sa catégorie catalogue et une quantité donnés, si une promotion
// configurée dans les settings couvre exactement ce couple (produit,
// quantité) — contrairement aux récompenses, aucun seuil de points n'entre
// en jeu : la promotion s'applique à toute commande de cette quantité.
func ResolvePromotionDiscount(settings *models.AppSettings, productID int, category string, quantity float64) (float64, bool) {
if settings == nil || !settings.PromotionsEnabled {
return 0, false
}
for _, promo := range settings.Promotions {
if promo.Category != category || promo.DiscountPercent <= 0 {
continue
}
if promo.AllProducts {
if promo.Quantity == quantity {
return promo.DiscountPercent, true
}
continue
}
for _, pq := range promo.Products {
if pq.ProductID == productID && pq.Quantity == quantity {
return promo.DiscountPercent, true
}
}
}
return 0, false
}
// ApplyPromotionToPrice applique la réduction (si une promotion couvre ce
// produit/quantité/catégorie) au prix catalogue donné, arrondi au centime.
func (d *Database) ApplyPromotionToPrice(productID int, category string, quantity, price float64) (float64, bool) {
settings, err := d.GetSettings()
if err != nil {
return price, false
}
discount, ok := ResolvePromotionDiscount(&settings, productID, category, quantity)
if !ok {
return price, false
}
return math.Round(price*(1-discount/100)*100) / 100, true
}
+65 -14
View File
@@ -72,19 +72,19 @@ func DefaultSettings() models.AppSettings {
Mode: "single",
CategoryRoutes: []models.CategoryRoute{},
},
AdminColorPrimary: "#7c3aed",
AdminColorSecondary: "#000000",
AdminColorSuccess: "#4ade80",
AdminColorDanger: "#ef4444",
AdminColorWarning: "#f59e0b",
ClientColorPrimary: "#7c3aed",
ClientColorSecondary: "#000000",
ClientColorSuccess: "#4ade80",
ClientColorDanger: "#ef4444",
ClientColorWarning: "#f59e0b",
AdminColorPrimary: "#7c3aed",
AdminColorSecondary: "#000000",
AdminColorSuccess: "#4ade80",
AdminColorDanger: "#ef4444",
AdminColorWarning: "#f59e0b",
ClientColorPrimary: "#7c3aed",
ClientColorSecondary: "#000000",
ClientColorSuccess: "#4ade80",
ClientColorDanger: "#ef4444",
ClientColorWarning: "#f59e0b",
ClientTitleGradientFrom: "#a78bfa",
ClientTitleGradientTo: "#22d3ee",
DeliverySchedule: DefaultDeliverySchedule(),
DeliverySchedule: DefaultDeliverySchedule(),
PostalZones: []models.PostalZone{
{Name: "Zone 30€", MinAmount: 30, Codes: []string{"44000", "44100", "44200", "44300"}},
{Name: "Zone 50€", MinAmount: 50, Codes: []string{
@@ -115,6 +115,11 @@ func (d *Database) GetSettings() (models.AppSettings, error) {
switch row.Key {
case "penalties_enabled":
settings.PenaltiesEnabled = row.Value == "true"
case "penalty_tiers":
var tiers []models.PenaltyTier
if err := json.Unmarshal([]byte(row.Value), &tiers); err == nil {
settings.PenaltyTiers = tiers
}
case "show_amende_score":
settings.ShowAmendeScore = row.Value == "true"
case "points_enabled":
@@ -125,9 +130,24 @@ func (d *Database) GetSettings() (models.AppSettings, error) {
settings.PointsPools = pools
}
case "points_reward":
var reward models.PointsReward
if err := json.Unmarshal([]byte(row.Value), &reward); err == nil {
settings.PointsReward = &reward
// row.Value peut valoir la chaîne littérale "null" (récompense
// désactivée puis sauvegardée : json.Marshal(nil *PointsReward)
// produit "null"). json.Unmarshal d'un null JSON dans une valeur
// non-pointeur est un no-op sans erreur (voir doc encoding/json),
// donc sans ce garde-fou &reward pointerait vers une struct vide
// mais non-nil, et la récompense réapparaîtrait activée.
if row.Value != "null" && row.Value != "" {
var reward models.PointsReward
if err := json.Unmarshal([]byte(row.Value), &reward); err == nil {
settings.PointsReward = &reward
}
}
case "promotions_enabled":
settings.PromotionsEnabled = row.Value == "true"
case "promotions":
var promotions []models.CategoryPromotionConfig
if err := json.Unmarshal([]byte(row.Value), &promotions); err == nil {
settings.Promotions = promotions
}
case "referral_enabled":
settings.ReferralEnabled = row.Value == "true"
@@ -213,6 +233,14 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
return "false"
}
if s.PenaltyTiers == nil {
s.PenaltyTiers = []models.PenaltyTier{}
}
tiersJSON, err := json.Marshal(s.PenaltyTiers)
if err != nil {
return fmt.Errorf("erreur sérialisation penalty_tiers: %w", err)
}
if s.PointsPools == nil {
s.PointsPools = []models.PointsPool{}
}
@@ -230,11 +258,31 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
return fmt.Errorf("erreur sérialisation pools: %w", err)
}
if s.PointsReward != nil {
for i := range s.PointsReward.CategoryConfigs {
if s.PointsReward.CategoryConfigs[i].Products == nil {
s.PointsReward.CategoryConfigs[i].Products = []models.RewardProductQuantity{}
}
}
}
rewardJSON, err := json.Marshal(s.PointsReward)
if err != nil {
return fmt.Errorf("erreur sérialisation points_reward: %w", err)
}
if s.Promotions == nil {
s.Promotions = []models.CategoryPromotionConfig{}
}
for i := range s.Promotions {
if s.Promotions[i].Products == nil {
s.Promotions[i].Products = []models.PromotionProductQuantity{}
}
}
promotionsJSON, err := json.Marshal(s.Promotions)
if err != nil {
return fmt.Errorf("erreur sérialisation promotions: %w", err)
}
if s.NowPaymentsCurrencies == nil {
s.NowPaymentsCurrencies = []string{}
}
@@ -269,10 +317,13 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
}
pairs := [][2]string{
{"penalties_enabled", boolStr(s.PenaltiesEnabled)},
{"penalty_tiers", string(tiersJSON)},
{"show_amende_score", boolStr(s.ShowAmendeScore)},
{"points_enabled", boolStr(s.PointsEnabled)},
{"points_pools", string(poolsJSON)},
{"points_reward", string(rewardJSON)},
{"promotions_enabled", boolStr(s.PromotionsEnabled)},
{"promotions", string(promotionsJSON)},
{"referral_enabled", boolStr(s.ReferralEnabled)},
{"referral_amount", strconv.FormatFloat(s.ReferralAmount, 'f', 2, 64)},
{"crypto_payment_enabled", boolStr(s.CryptoPaymentEnabled)},