'decimal:2', 'total_csv' => 'decimal:2', 'fixed_cost_portion' => 'decimal:2', 'calculated_price' => 'decimal:2', 'contribution_margin' => 'decimal:2', 'margin_percentage' => 'decimal:2', 'competitor_price' => 'decimal:2', 'min_price' => 'decimal:2', 'max_price' => 'decimal:2', 'premium_multiplier' => 'decimal:4', 'target_margin' => 'decimal:2', 'psychological_pricing' => 'boolean', 'is_active' => 'boolean', ]; /** * 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 do serviço (insumos) */ public function items(): HasMany { return $this->hasMany(ServiceSheetItem::class); } /** * Calcula o CSV (Custo do Serviço Vendido) somando todos os itens * * @return float */ public function calculateTotalCsv(): float { return $this->items->sum(function ($item) { return $item->unit_cost * $item->quantity_used; }); } /** * Calcula o gasto fixo proporcional ao tempo do serviço * * @return float */ public function calculateFixedCostPortion(): float { $setting = $this->businessSetting; if (!$setting) { return 0; } return round($setting->fixed_cost_per_minute * $this->duration_minutes, 2); } /** * Calcula o preço de venda usando a fórmula de serviços: * Preço = (Gasto Fixo/Tempo + CSV) × Markup * * @return float */ public function calculatePrice(): float { $setting = $this->businessSetting; if (!$setting) { return 0; } $basePrice = $setting->calculateServicePrice( (float) $this->duration_minutes, (float) $this->total_csv ); // Aplicar multiplicador premium $price = $basePrice * (float) ($this->premium_multiplier ?? 1); // Aplicar estratégia de preço baseada no competidor if ($this->competitor_price && $this->competitor_price > 0) { switch ($this->pricing_strategy) { case 'aggressive': $strategicPrice = $this->competitor_price * 0.95; $price = min($price, $strategicPrice); break; case 'premium': $strategicPrice = $this->competitor_price * 1.10; $price = max($price, $strategicPrice); break; } } // Aplicar limites min/max if ($this->min_price && $price < $this->min_price) { $price = (float) $this->min_price; } if ($this->max_price && $price > $this->max_price) { $price = (float) $this->max_price; } // Aplicar redondeo psicológico if ($this->psychological_pricing) { $price = $this->applyPsychologicalPricing($price); } return round($price, 2); } /** * Aplica redondeo psicológico (€26.04 → €25.99) */ protected function applyPsychologicalPricing(float $price): float { $intPart = floor($price); $decimal = $price - $intPart; if ($decimal > 0 && $decimal <= 0.50) { return $intPart - 0.01; } elseif ($decimal > 0.50) { return $intPart + 0.99; } return $price - 0.01; } /** * Calcula a margem de contribuição * Margem = Preço - CSV - Gasto Fixo Proporcional - Deduções */ public function calculateContributionMargin(): float { $price = (float) $this->calculated_price; $csv = (float) $this->total_csv; $fixedCost = (float) $this->fixed_cost_portion; $setting = $this->businessSetting; if (!$setting) { return 0; } // Para B2C, remover IVA do preço antes de calcular margem if ($setting->price_includes_tax) { $vatRate = (float) ($setting->vat_rate ?? 0); $price = $price / (1 + $vatRate / 100); } // Deduções variáveis $deductions = $price * ($setting->total_variable_costs / 100); return round($price - $csv - $fixedCost - $deductions, 2); } /** * Recalcula todos os valores e salva */ public function recalculate(): self { $this->total_csv = $this->calculateTotalCsv(); $this->fixed_cost_portion = $this->calculateFixedCostPortion(); $this->calculated_price = $this->calculatePrice(); $this->contribution_margin = $this->calculateContributionMargin(); // Calcular margem percentual if ($this->calculated_price > 0) { $this->margin_percentage = round( ($this->contribution_margin / $this->calculated_price) * 100, 2 ); } else { $this->margin_percentage = 0; } $this->save(); return $this; } /** * Retorna o breakdown de custos para exibição */ public function getCostBreakdownAttribute(): array { $setting = $this->businessSetting; $price = (float) $this->calculated_price; $csv = (float) $this->total_csv; $fixedCost = (float) $this->fixed_cost_portion; // Calcular valores $vatAmount = 0; $priceWithoutVat = $price; if ($setting && $setting->price_includes_tax) { $vatRate = (float) ($setting->vat_rate ?? 0); $priceWithoutVat = $price / (1 + $vatRate / 100); $vatAmount = $price - $priceWithoutVat; } $deductions = $setting ? $priceWithoutVat * ($setting->total_variable_costs / 100) : 0; return [ 'duration_minutes' => (float) $this->duration_minutes, 'duration_hours' => round($this->duration_minutes / 60, 2), 'csv' => $csv, 'fixed_cost_portion' => $fixedCost, 'base_cost' => $csv + $fixedCost, 'markup' => $setting ? (float) $setting->markup_factor : 0, 'price_without_vat' => round($priceWithoutVat, 2), 'vat_amount' => round($vatAmount, 2), 'vat_rate' => $setting && $setting->price_includes_tax ? (float) $setting->vat_rate : 0, 'final_price' => $price, 'deductions' => round($deductions, 2), 'contribution_margin' => (float) $this->contribution_margin, 'margin_percentage' => (float) $this->margin_percentage, ]; } /** * Scope para buscar serviços de um usuário */ public function scopeOfUser($query, $userId) { return $query->where('user_id', $userId); } /** * Scope para buscar serviços ativos */ public function scopeActive($query) { return $query->where('is_active', true); } }