webmoney/backend/app/Models/PromotionalCampaign.php

420 lines
12 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Carbon\Carbon;
class PromotionalCampaign extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'name',
'code',
'description',
'discount_type',
'discount_value',
'start_date',
'end_date',
'start_time',
'end_time',
'is_active',
'apply_to_all_products',
'min_price',
'min_margin_percent',
'protect_against_loss',
'margin_warning_threshold',
'show_original_price',
'show_discount_badge',
'badge_text',
'badge_color',
'priority',
];
protected $casts = [
'start_date' => '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';
}
}