338 lines
9.6 KiB
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)',
|
|
];
|
|
}
|
|
}
|