'decimal:2', 'sale_price' => 'decimal:2', 'markup_used' => 'decimal:4', 'contribution_margin' => 'decimal:2', 'is_active' => 'boolean', // Strategic pricing casts 'competitor_price' => 'decimal:2', 'min_price' => 'decimal:2', 'max_price' => 'decimal:2', 'premium_multiplier' => 'decimal:2', 'psychological_rounding' => 'boolean', 'target_margin_percent' => 'decimal:2', 'final_price' => 'decimal:2', 'real_margin_percent' => 'decimal:2', ]; // Price strategies const STRATEGY_AGGRESSIVE = 'aggressive'; const STRATEGY_NEUTRAL = 'neutral'; const STRATEGY_PREMIUM = 'premium'; /** * Relacionamento com usuário */ public function user(): BelongsTo { return $this->belongsTo(User::class); } /** * Relacionamento com configuração de negócio */ public function businessSetting(): BelongsTo { return $this->belongsTo(BusinessSetting::class); } /** * Itens/componentes de custo desta ficha técnica */ public function items(): HasMany { return $this->hasMany(ProductSheetItem::class)->orderBy('sort_order'); } /** * Recalcula o CMV total baseado nos itens * * @return float */ public function recalculateCmv(): float { $this->cmv_total = $this->items()->sum('unit_cost'); $this->save(); return $this->cmv_total; } /** * Calcula e atualiza o preço de venda usando o Markup da configuração * * @param BusinessSetting|null $businessSetting * @return float */ public function calculateSalePrice(?BusinessSetting $businessSetting = null): float { $setting = $businessSetting ?? $this->businessSetting; if (!$setting) { return 0; } $markup = $setting->markup_factor ?? $setting->calculateMarkup(); if ($markup <= 0) { return 0; } $this->markup_used = $markup; $this->sale_price = round($this->cmv_total * $markup, 2); $this->contribution_margin = $this->sale_price - $this->cmv_total; $this->save(); return $this->sale_price; } /** * Retorna a margem de contribuição percentual * * @return float */ public function getContributionMarginPercentAttribute(): float { if ($this->sale_price <= 0) { return 0; } return round(($this->contribution_margin / $this->sale_price) * 100, 2); } /** * Scope para buscar fichas do usuário */ public function scopeOfUser($query, $userId) { return $query->where('user_id', $userId); } /** * Scope para buscar apenas fichas ativas */ public function scopeActive($query) { return $query->where('is_active', true); } /** * Scope para buscar por categoria */ public function scopeByCategory($query, $category) { return $query->where('category', $category); } /** * Calcula o preço final aplicando estratégias de precificação * * @return float */ public function calculateFinalPrice(): float { // Começar com o preço base (markup) $basePrice = (float) $this->sale_price; if ($basePrice <= 0) { return 0; } $finalPrice = $basePrice; // 1. Aplicar multiplicador premium $premiumMultiplier = (float) ($this->premium_multiplier ?? 1.0); $finalPrice = $finalPrice * $premiumMultiplier; // 2. Aplicar estratégia de preço baseada no concorrente if ($this->competitor_price && $this->competitor_price > 0) { $competitorPrice = (float) $this->competitor_price; switch ($this->price_strategy) { case self::STRATEGY_AGGRESSIVE: // 5% abaixo do concorrente $targetPrice = $competitorPrice * 0.95; $finalPrice = min($finalPrice, $targetPrice); break; case self::STRATEGY_PREMIUM: // 10% acima do concorrente $targetPrice = $competitorPrice * 1.10; $finalPrice = max($finalPrice, $targetPrice); break; case self::STRATEGY_NEUTRAL: default: // Manter preço calculado, mas não muito diferente do concorrente break; } } // 3. Aplicar margem alvo específica se definida if ($this->target_margin_percent !== null && $this->target_margin_percent > 0) { $targetMargin = (float) $this->target_margin_percent / 100; $cmv = (float) $this->cmv_total; // Preço = CMV / (1 - margem) $targetPrice = $cmv / (1 - $targetMargin); $finalPrice = $targetPrice; } // 4. Aplicar limites min/max if ($this->min_price && $finalPrice < $this->min_price) { $finalPrice = (float) $this->min_price; } if ($this->max_price && $finalPrice > $this->max_price) { $finalPrice = (float) $this->max_price; } // 5. Aplicar arredondamento psicológico if ($this->psychological_rounding) { $finalPrice = $this->applyPsychologicalRounding($finalPrice); } // Salvar resultado $this->final_price = round($finalPrice, 2); $this->real_margin_percent = $this->calculateRealMargin($this->final_price); $this->save(); return $this->final_price; } /** * Aplica arredondamento psicológico ao preço * Ex: 26.04 -> 25.99, 135.41 -> 134.99 * * @param float $price * @return float */ protected function applyPsychologicalRounding(float $price): float { if ($price < 10) { // Preços baixos: arredondar para .99 return floor($price) + 0.99; } elseif ($price < 100) { // Preços médios: arredondar para X9.99 $tens = floor($price / 10) * 10; $remainder = $price - $tens; if ($remainder >= 5) { return $tens + 9.99; } else { return $tens - 0.01; } } else { // Preços altos: arredondar para XX9.99 ou XX4.99 $hundreds = floor($price / 100) * 100; $remainder = $price - $hundreds; if ($remainder >= 75) { return $hundreds + 99.99; } elseif ($remainder >= 25) { return $hundreds + 49.99; } else { return $hundreds - 0.01; } } } /** * Calcula a margem real baseada no preço final * * @param float $finalPrice * @return float */ protected function calculateRealMargin(float $finalPrice): float { if ($finalPrice <= 0) { return 0; } $cmv = (float) $this->cmv_total; $margin = (($finalPrice - $cmv) / $finalPrice) * 100; return round($margin, 2); } /** * Retorna comparação com concorrente * * @return array|null */ public function getCompetitorComparisonAttribute(): ?array { if (!$this->competitor_price || $this->competitor_price <= 0) { return null; } $finalPrice = (float) ($this->final_price ?? $this->sale_price); $competitorPrice = (float) $this->competitor_price; $difference = $finalPrice - $competitorPrice; $percentDiff = (($finalPrice - $competitorPrice) / $competitorPrice) * 100; return [ 'competitor_price' => $competitorPrice, 'our_price' => $finalPrice, 'difference' => round($difference, 2), 'percent_difference' => round($percentDiff, 2), 'position' => $difference > 0 ? 'above' : ($difference < 0 ? 'below' : 'equal'), ]; } /** * Lista de estratégias disponíveis */ public static function getStrategies(): array { return [ self::STRATEGY_AGGRESSIVE => 'Agressivo (5% abaixo do concorrente)', self::STRATEGY_NEUTRAL => 'Neutro (preço calculado)', self::STRATEGY_PREMIUM => 'Premium (10% acima do concorrente)', ]; } }