webmoney/backend/app/Models/ProductSheet.php

338 lines
9.6 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\HasMany;
class ProductSheet extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'business_setting_id',
'name',
'sku',
'description',
'category',
'base_volume_ml',
'standard_portion_ml',
'currency',
'cmv_total',
'sale_price',
'markup_used',
'contribution_margin',
'is_active',
// Strategic pricing fields
'competitor_price',
'min_price',
'max_price',
'premium_multiplier',
'price_strategy',
'psychological_rounding',
'target_margin_percent',
'final_price',
'real_margin_percent',
];
protected $casts = [
'cmv_total' => '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');
}
/**
* Variantes de venda do produto (ex: Garrafa, Taça, Meia Taça, Degustar)
*/
public function variants(): HasMany
{
return $this->hasMany(ProductVariant::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)',
];
}
}