'date', 'end_date' => 'date', 'is_active' => 'boolean', 'apply_to_all_products' => 'boolean', 'protect_against_loss' => 'boolean', 'show_original_price' => 'boolean', 'show_discount_badge' => 'boolean', 'discount_value' => 'decimal:2', 'min_price' => 'decimal:2', 'min_margin_percent' => 'decimal:2', 'margin_warning_threshold' => 'decimal:2', 'priority' => 'integer', ]; // Tipos de desconto const TYPE_PERCENTAGE = 'percentage'; const TYPE_FIXED = 'fixed'; const TYPE_PRICE_OVERRIDE = 'price_override'; // Presets de campanhas const PRESETS = [ 'black_friday' => [ 'name' => 'Black Friday', 'discount_type' => 'percentage', 'discount_value' => 30, 'badge_text' => 'BLACK FRIDAY', 'badge_color' => '#000000', 'show_discount_badge' => true, ], 'cyber_monday' => [ 'name' => 'Cyber Monday', 'discount_type' => 'percentage', 'discount_value' => 25, 'badge_text' => 'CYBER MONDAY', 'badge_color' => '#3b82f6', 'show_discount_badge' => true, ], 'christmas' => [ 'name' => 'Natal', 'discount_type' => 'percentage', 'discount_value' => 20, 'badge_text' => 'šŸŽ„ NATAL', 'badge_color' => '#22c55e', 'show_discount_badge' => true, ], 'new_year' => [ 'name' => 'Ano Novo', 'discount_type' => 'percentage', 'discount_value' => 15, 'badge_text' => 'šŸŽ† ANO NOVO', 'badge_color' => '#f59e0b', 'show_discount_badge' => true, ], 'summer_sale' => [ 'name' => 'Liquidação de VerĆ£o', 'discount_type' => 'percentage', 'discount_value' => 40, 'badge_text' => 'LIQUIDAƇƃO', 'badge_color' => '#ef4444', 'show_discount_badge' => true, ], 'flash_sale' => [ 'name' => 'Flash Sale', 'discount_type' => 'percentage', 'discount_value' => 50, 'badge_text' => '⚔ FLASH SALE', 'badge_color' => '#8b5cf6', 'show_discount_badge' => true, ], ]; /** * UsuĆ”rio dono da campanha */ public function user(): BelongsTo { return $this->belongsTo(User::class); } /** * Produtos na campanha */ public function products(): BelongsToMany { return $this->belongsToMany(ProductSheet::class, 'campaign_products', 'campaign_id', 'product_sheet_id') ->withPivot(['discount_type', 'discount_value', 'promotional_price', 'promo_margin', 'promo_margin_percent', 'is_profitable']) ->withTimestamps(); } /** * Verifica se a campanha estĆ” ativa agora */ public function isCurrentlyActive(): bool { if (!$this->is_active) { return false; } $now = Carbon::now(); $today = $now->toDateString(); // Verificar data if ($today < $this->start_date->toDateString() || $today > $this->end_date->toDateString()) { return false; } // Verificar hora se definida if ($this->start_time && $this->end_time) { $currentTime = $now->format('H:i:s'); // Se for o primeiro dia, verificar hora de inĆ­cio if ($today === $this->start_date->toDateString() && $currentTime < $this->start_time) { return false; } // Se for o Ćŗltimo dia, verificar hora de fim if ($today === $this->end_date->toDateString() && $currentTime > $this->end_time) { return false; } } return true; } /** * Calcula o preƧo promocional para um produto * COM PROTEƇƃO DE RENTABILIDADE */ public function calculatePromotionalPrice(ProductSheet $product, ?array $override = null): float { $basePrice = (float) ($product->final_price ?? $product->sale_price); if ($basePrice <= 0) { return 0; } $discountType = $override['discount_type'] ?? $this->discount_type; $discountValue = (float) ($override['discount_value'] ?? $this->discount_value); $promoPrice = $basePrice; switch ($discountType) { case self::TYPE_PERCENTAGE: $promoPrice = $basePrice * (1 - ($discountValue / 100)); break; case self::TYPE_FIXED: $promoPrice = $basePrice - $discountValue; break; case self::TYPE_PRICE_OVERRIDE: $promoPrice = $discountValue; break; } // === PROTEƇƃO DE RENTABILIDADE === // 1. Aplicar preƧo mĆ­nimo da CAMPANHA se definido if ($this->min_price && $promoPrice < $this->min_price) { $promoPrice = (float) $this->min_price; } // 2. Aplicar preƧo mĆ­nimo do PRODUTO se definido (proteção individual) if ($product->min_price && $promoPrice < (float) $product->min_price) { $promoPrice = (float) $product->min_price; } // 3. NUNCA vender abaixo do CMV (Custo) - Proteção contra prejuĆ­zo $cmv = (float) ($product->cmv_total ?? 0); if ($cmv > 0 && $promoPrice < $cmv) { $promoPrice = $cmv; // PreƧo mĆ­nimo = custo (margem zero, sem prejuĆ­zo) } // 4. Aplicar margem mĆ­nima de seguranƧa se configurada na campanha if ($this->min_margin_percent && $cmv > 0) { $minPriceWithMargin = $cmv * (1 + ($this->min_margin_percent / 100)); if ($promoPrice < $minPriceWithMargin) { $promoPrice = $minPriceWithMargin; } } // NĆ£o pode ser negativo return max(0, round($promoPrice, 2)); } /** * Verifica se o preƧo promocional gera lucro */ public function isProfitable(ProductSheet $product): bool { $promoPrice = $this->calculatePromotionalPrice($product); $cmv = (float) ($product->cmv_total ?? 0); return $promoPrice > $cmv; } /** * Calcula a margem real durante a promoção */ public function getPromotionalMargin(ProductSheet $product): array { $promoPrice = $this->calculatePromotionalPrice($product); $cmv = (float) ($product->cmv_total ?? 0); $originalPrice = (float) ($product->final_price ?? $product->sale_price); $promoMargin = $promoPrice - $cmv; $promoMarginPercent = $promoPrice > 0 ? ($promoMargin / $promoPrice) * 100 : 0; $originalMargin = $originalPrice - $cmv; $originalMarginPercent = $originalPrice > 0 ? ($originalMargin / $originalPrice) * 100 : 0; return [ 'promo_price' => $promoPrice, 'cmv' => $cmv, 'promo_margin' => round($promoMargin, 2), 'promo_margin_percent' => round($promoMarginPercent, 1), 'original_margin' => round($originalMargin, 2), 'original_margin_percent' => round($originalMarginPercent, 1), 'margin_reduction' => round($originalMarginPercent - $promoMarginPercent, 1), 'is_profitable' => $promoMargin > 0, 'is_protected' => $promoPrice >= $cmv, ]; } /** * Calcula o percentual de desconto */ public function getDiscountPercentage(float $originalPrice, float $promoPrice): float { if ($originalPrice <= 0) { return 0; } return round((($originalPrice - $promoPrice) / $originalPrice) * 100, 1); } /** * Retorna informaƧƵes do badge para exibição */ public function getBadgeInfo(float $originalPrice, float $promoPrice): ?array { if (!$this->show_discount_badge) { return null; } $discountPercent = $this->getDiscountPercentage($originalPrice, $promoPrice); return [ 'text' => $this->badge_text ?? "-{$discountPercent}%", 'color' => $this->badge_color ?? '#ef4444', 'discount_percent' => $discountPercent, ]; } /** * Aplica a campanha a todos os produtos do usuĆ”rio */ public function applyToAllProducts(): int { if (!$this->apply_to_all_products) { return 0; } $products = ProductSheet::where('user_id', $this->user_id) ->where('is_active', true) ->get(); $count = 0; foreach ($products as $product) { $promoPrice = $this->calculatePromotionalPrice($product); $this->products()->syncWithoutDetaching([ $product->id => [ 'promotional_price' => $promoPrice, ] ]); $count++; } return $count; } /** * Atualiza preƧos promocionais de todos os produtos da campanha */ public function recalculateAllPrices(): void { foreach ($this->products as $product) { $override = null; if ($product->pivot->discount_type) { $override = [ 'discount_type' => $product->pivot->discount_type, 'discount_value' => $product->pivot->discount_value, ]; } $promoPrice = $this->calculatePromotionalPrice($product, $override); $this->products()->updateExistingPivot($product->id, [ 'promotional_price' => $promoPrice, ]); } } /** * Scope para campanhas do usuĆ”rio */ public function scopeOfUser($query, $userId) { return $query->where('user_id', $userId); } /** * Scope para campanhas ativas */ public function scopeActive($query) { return $query->where('is_active', true); } /** * Scope para campanhas atuais (dentro do perĆ­odo) */ public function scopeCurrent($query) { $now = Carbon::now(); return $query->where('start_date', '<=', $now) ->where('end_date', '>=', $now); } /** * Scope para campanhas futuras */ public function scopeUpcoming($query) { return $query->where('start_date', '>', Carbon::now()); } /** * Lista de presets disponĆ­veis */ public static function getPresets(): array { return self::PRESETS; } /** * Dias restantes da campanha */ public function getDaysRemainingAttribute(): int { if (!$this->isCurrentlyActive()) { return 0; } return max(0, Carbon::now()->diffInDays($this->end_date, false)); } /** * Status da campanha */ public function getStatusAttribute(): string { if (!$this->is_active) { return 'inactive'; } $now = Carbon::now(); if ($now < $this->start_date) { return 'scheduled'; } if ($now > $this->end_date) { return 'ended'; } return 'active'; } }