orderBy('created_at', 'desc') ->paginate($perPage); } public function listAll(int $limit = 100): \Illuminate\Database\Eloquent\Collection { return TbrCalculation::with(['unit', 'user']) ->orderBy('created_at', 'desc') ->limit($limit) ->get(); } public function findById(int $id): ?TbrCalculation { return TbrCalculation::with([ 'unit', 'user', 'royaltiesBracket', ])->find($id); } public function preview(array $data): array { $contract = $this->resolveContract($data['unit_id']); return $this->buildPreview($contract, (int) $data['reference_year'], (int) $data['reference_month'], (float) ($data['revenue_value'] ?? 0)); } public function previewBatch(int $referenceYear, int $referenceMonth): array { $contracts = $this->loadActiveContracts($referenceYear, $referenceMonth); return $contracts->map(function (FranchiseeContract $contract) use ($referenceYear, $referenceMonth) { try { return $this->buildPreview($contract, $referenceYear, $referenceMonth, 0); } catch (ValidationException $e) { return [ 'unit_id' => $contract->unit_id, 'unit_name' => $contract->unit?->fantasy_name, 'error' => collect($e->errors())->flatten()->first(), ]; } })->values()->toArray(); } public function calculate(array $data): TbrCalculation { return DB::transaction(function () use ($data) { $contract = $this->resolveContract($data['unit_id']); $payload = $this->buildPreview($contract, (int) $data['reference_year'], (int) $data['reference_month'], (float) ($data['revenue_value'] ?? 0)); return $this->persistCalculation($payload); }); } public function generateReceivable(int $calculationId): FranchiseeAccountReceive { return DB::transaction(function () use ($calculationId) { $calculation = TbrCalculation::lockForUpdate()->findOrFail($calculationId); if ($calculation->receivable_generated) { throw ValidationException::withMessages([ 'tbr_calculation_id' => 'Já existe um título gerado para este cálculo.', ]); } $duplicate = TbrCalculation::where('unit_id', $calculation->unit_id) ->where('contract_month_reference', $calculation->contract_month_reference) ->where('receivable_generated', true) ->where('id', '!=', $calculation->id) ->exists(); if ($duplicate) { throw ValidationException::withMessages([ 'tbr_calculation_id' => 'Já existe um título gerado para esta unidade no mês de contrato ' . $calculation->contract_month_reference . '.', ]); } $contract = FranchiseeContract::where('unit_id', $calculation->unit_id) ->orderByDesc('start_date') ->first(); return $this->buildReceivable($calculation, $contract); }); } public function generateBatch(int $referenceYear, int $referenceMonth, ?array $unitIds = null): array { $contracts = $this->loadActiveContracts($referenceYear, $referenceMonth); if ($unitIds !== null) { $contracts = $contracts->filter(fn ($c) => in_array($c->unit_id, $unitIds, true))->values(); } $generated = []; $skipped = []; $errors = []; foreach ($contracts as $contract) { try { DB::transaction(function () use ($contract, $referenceYear, $referenceMonth, &$generated, &$skipped) { $payload = $this->buildPreview($contract, $referenceYear, $referenceMonth, 0); if ($payload['receivable_already_generated']) { $skipped[] = [ 'unit_id' => $contract->unit_id, 'unit_name' => $payload['unit_name'], 'reason' => 'Já gerado para este mês de contrato.', ]; return; } $calculation = $this->persistCalculation($payload); $receive = $this->buildReceivable($calculation, $contract); $generated[] = [ 'unit_id' => $contract->unit_id, 'unit_name' => $payload['unit_name'], 'tbr_calculation_id' => $calculation->id, 'receivable_id' => $receive->id, 'total' => $payload['final_value'], ]; }); } catch (ValidationException $e) { $errors[] = [ 'unit_id' => $contract->unit_id, 'unit_name' => $contract->unit?->fantasy_name, 'reason' => collect($e->errors())->flatten()->first(), ]; } catch (\Throwable $e) { $errors[] = [ 'unit_id' => $contract->unit_id, 'unit_name' => $contract->unit?->fantasy_name, 'reason' => $e->getMessage(), ]; } } return [ 'generated_count' => count($generated), 'skipped_count' => count($skipped), 'error_count' => count($errors), 'generated' => $generated, 'skipped' => $skipped, 'errors' => $errors, ]; } private function loadActiveContracts(int $referenceYear, int $referenceMonth): \Illuminate\Support\Collection { $referenceLastDay = Carbon::createFromDate($referenceYear, $referenceMonth, 1)->endOfMonth()->toDateString(); $referenceFirstDay = Carbon::createFromDate($referenceYear, $referenceMonth, 1)->startOfMonth()->toDateString(); return FranchiseeContract::with(['unit', 'municipalitySize']) ->whereNotNull('start_date') ->whereNotNull('municipality_size_id') ->where('start_date', '<=', $referenceLastDay) ->where(function ($q) use ($referenceFirstDay) { $q->whereNull('end_date')->orWhere('end_date', '>=', $referenceFirstDay); }) ->orderByDesc('start_date') ->orderByDesc('id') ->get() ->unique('unit_id') ->values(); } private function resolveContract(int $unitId): FranchiseeContract { $contract = FranchiseeContract::with(['unit', 'municipalitySize']) ->where('unit_id', $unitId) ->whereNotNull('start_date') ->orderByDesc('start_date') ->orderByDesc('id') ->first(); if (!$contract) { throw ValidationException::withMessages([ 'unit_id' => 'Unidade não possui contrato cadastrado.', ]); } if (!$contract->municipality_size_id) { throw ValidationException::withMessages([ 'unit_id' => 'O contrato da unidade não tem a faixa de habitantes definida. Edite o contrato para informá-la.', ]); } return $contract; } private function buildPreview(FranchiseeContract $contract, int $referenceYear, int $referenceMonth, float $revenueValue): array { $tbrValue = (float) ($contract->tbr_fixed_value ?? 0); if ($tbrValue <= 0) { $tbrValue = (float) (Tbr::where('year', $referenceYear)->orderByDesc('id')->value('tbr_value') ?? 0); } if ($tbrValue <= 0) { throw ValidationException::withMessages([ 'unit_id' => 'TBR não definida para o contrato nem para o ano de referência.', ]); } $contractMonth = $this->resolveContractMonth($contract->start_date, $referenceYear, $referenceMonth); $municipalitySizeId = (int) $contract->municipality_size_id; $royaltiesBracket = $this->findRoyaltiesBracket($municipalitySizeId, $contractMonth); $fnmPercentage = $this->resolveFnmPercentage($contractMonth); $maintenancePercentage = self::MAINTENANCE_RATE; $royaltiesBracketValue = round((float) $royaltiesBracket->tbr_percentage * $tbrValue, 2); $fnmBracketValue = round($fnmPercentage * $tbrValue, 2); $maintenanceBracketValue = round($maintenancePercentage * $tbrValue, 2); [$royaltiesEffectiveValue, $royaltiesEffectivePercentage, $royaltiesAppliedCriteria, $fnmEffectiveValue, $fnmEffectivePercentage] = $this->resolveEffectiveValues( $contractMonth, $revenueValue, (float) $royaltiesBracket->tbr_percentage, $royaltiesBracketValue, $fnmPercentage, $fnmBracketValue, ); $maintenanceEffectiveValue = $maintenanceBracketValue; $maintenanceEffectivePercentage = $maintenancePercentage; $bracketSubtotal = round($royaltiesBracketValue + $fnmBracketValue + $maintenanceBracketValue, 2); $subtotal = round($royaltiesEffectiveValue + $fnmEffectiveValue + $maintenanceEffectiveValue, 2); return [ 'unit_id' => $contract->unit_id, 'unit_name' => $contract->unit?->fantasy_name, 'contract_id' => $contract->id, 'reference_year' => $referenceYear, 'reference_month' => $referenceMonth, 'contract_month_reference' => $contractMonth, 'revenue_value' => $revenueValue, 'tbr_value' => $tbrValue, 'municipality_size_id' => $municipalitySizeId, 'municipality_size_name' => $contract->municipalitySize?->description, 'royalties_bracket_id' => $royaltiesBracket->id, 'royalties_bracket_percentage' => (float) $royaltiesBracket->tbr_percentage, 'royalties_bracket_value' => $royaltiesBracketValue, 'fnm_bracket_percentage' => $fnmPercentage, 'fnm_bracket_value' => $fnmBracketValue, 'maintenance_bracket_percentage' => $maintenancePercentage, 'maintenance_bracket_value' => $maintenanceBracketValue, 'royalties_effective_percentage' => $royaltiesEffectivePercentage, 'royalties_effective_value' => $royaltiesEffectiveValue, 'fnm_effective_percentage' => $fnmEffectivePercentage, 'fnm_effective_value' => $fnmEffectiveValue, 'maintenance_effective_percentage' => $maintenanceEffectivePercentage, 'maintenance_effective_value' => $maintenanceEffectiveValue, 'bracket_subtotal' => $bracketSubtotal, 'subtotal' => $subtotal, 'final_value' => $subtotal, 'royalties_applied_criteria' => $royaltiesAppliedCriteria, 'receivable_already_generated' => $this->existingReceivable($contract->unit_id, $contractMonth), ]; } private function persistCalculation(array $payload): TbrCalculation { return TbrCalculation::create([ 'unit_id' => $payload['unit_id'], 'revenue_value' => $payload['revenue_value'], 'contract_month_reference' => $payload['contract_month_reference'], 'tbr_value' => $payload['tbr_value'], 'royalties_bracket_id' => $payload['royalties_bracket_id'], 'royalties_bracket_percentage' => $payload['royalties_bracket_percentage'], 'royalties_bracket_value' => $payload['royalties_bracket_value'], 'fnm_bracket_percentage' => $payload['fnm_bracket_percentage'], 'fnm_bracket_value' => $payload['fnm_bracket_value'], 'maintenance_bracket_percentage' => $payload['maintenance_bracket_percentage'], 'maintenance_bracket_value' => $payload['maintenance_bracket_value'], 'royalties_effective_percentage' => $payload['royalties_effective_percentage'], 'royalties_effective_value' => $payload['royalties_effective_value'], 'fnm_effective_percentage' => $payload['fnm_effective_percentage'], 'fnm_effective_value' => $payload['fnm_effective_value'], 'maintenance_effective_percentage' => $payload['maintenance_effective_percentage'], 'maintenance_effective_value' => $payload['maintenance_effective_value'], 'bracket_subtotal' => $payload['bracket_subtotal'], 'subtotal' => $payload['subtotal'], 'final_value' => $payload['final_value'], 'user_id' => Auth::id(), 'royalties_applied_criteria' => $payload['royalties_applied_criteria'], 'receivable_generated' => false, ]); } private function buildReceivable(TbrCalculation $calculation, ?FranchiseeContract $contract): FranchiseeAccountReceive { $referenceDate = Carbon::parse($calculation->created_at); $referenceLabel = $referenceDate->format('m/Y'); $dueDate = $this->resolveDueDate($contract, $referenceDate); $receive = FranchiseeAccountReceive::create([ 'unit_id' => $calculation->unit_id, 'tbr_calculation_id' => $calculation->id, 'order' => $calculation->contract_month_reference, 'history' => 'Royalties / FNM / Manutenção — ' . $referenceLabel, 'value' => $calculation->final_value, 'paid_value' => 0, 'due_date' => $dueDate, 'discount' => 0, 'fees' => 0, 'obs' => null, 'asaas_id' => null, 'status' => 'pending', ]); FranchiseeAccountReceiveDetail::create([ 'franchisee_account_receive_id' => $receive->id, 'value' => $calculation->royalties_effective_value, 'history' => 'Royalties ' . $referenceLabel, ]); FranchiseeAccountReceiveDetail::create([ 'franchisee_account_receive_id' => $receive->id, 'value' => $calculation->fnm_effective_value, 'history' => 'FNM ' . $referenceLabel, ]); FranchiseeAccountReceiveDetail::create([ 'franchisee_account_receive_id' => $receive->id, 'value' => $calculation->maintenance_effective_value, 'history' => 'Taxa Manutenção ' . $referenceLabel, ]); $calculation->update(['receivable_generated' => true]); return $receive->load('details'); } private function resolveEffectiveValues( int $contractMonth, float $revenueValue, float $royaltiesBracketPercentage, float $royaltiesBracketValue, float $fnmBracketPercentage, float $fnmBracketValue, ): array { if ($contractMonth <= self::EXEMPT_THRESHOLD_MONTH) { return [0.0, 0.0, 'tbr_fixo', 0.0, 0.0]; } $royaltiesFromRevenue = round(self::ROYALTIES_REVENUE_RATE * $revenueValue, 2); $fnmFromRevenue = round(self::FNM_REVENUE_RATE * $revenueValue, 2); if ($royaltiesBracketValue >= $royaltiesFromRevenue) { $royaltiesEffectiveValue = $royaltiesBracketValue; $royaltiesEffectivePercentage = $royaltiesBracketPercentage; $royaltiesAppliedCriteria = 'tbr_fixo'; } else { $royaltiesEffectiveValue = $royaltiesFromRevenue; $royaltiesEffectivePercentage = self::ROYALTIES_REVENUE_RATE; $royaltiesAppliedCriteria = 'percentual_faturamento'; } if ($fnmBracketValue >= $fnmFromRevenue) { $fnmEffectiveValue = $fnmBracketValue; $fnmEffectivePercentage = $fnmBracketPercentage; } else { $fnmEffectiveValue = $fnmFromRevenue; $fnmEffectivePercentage = self::FNM_REVENUE_RATE; } return [$royaltiesEffectiveValue, $royaltiesEffectivePercentage, $royaltiesAppliedCriteria, $fnmEffectiveValue, $fnmEffectivePercentage]; } private function resolveContractMonth(?Carbon $startDate, int $year, int $month): int { if (!$startDate) { return 1; } $start = $startDate->copy()->startOfMonth(); $reference = Carbon::createFromDate($year, $month, 1)->startOfMonth(); $diff = (int) $start->diffInMonths($reference); return max(1, $diff + 1); } private function findRoyaltiesBracket(int $municipalitySizeId, int $contractMonth): InhabitantClassification { $bracket = InhabitantClassification::where('municipality_size_id', $municipalitySizeId) ->where('is_renewal', false) ->where('start', '<=', $contractMonth) ->where(function ($q) use ($contractMonth) { $q->whereNull('end')->orWhere('end', '>=', $contractMonth); }) ->orderBy('start') ->first(); if (!$bracket) { throw ValidationException::withMessages([ 'municipality_size_id' => 'Não foi encontrada faixa de royalties para o porte e mês de contrato informados.', ]); } return $bracket; } private function resolveFnmPercentage(int $contractMonth): float { return $contractMonth <= self::EXEMPT_THRESHOLD_MONTH ? 0.0 : self::FNM_BRACKET_PERCENTAGE; } private function resolveDueDate(?FranchiseeContract $contract, Carbon $referenceDate): Carbon { $dueDay = (int) ($contract?->invoice_due_date ?? 10); $dueDay = max(1, min(28, $dueDay)); return $referenceDate->copy()->addMonthNoOverflow()->day($dueDay); } private function existingReceivable(int $unitId, int $contractMonth): bool { return TbrCalculation::where('unit_id', $unitId) ->where('contract_month_reference', $contractMonth) ->where('receivable_generated', true) ->exists(); } }