浏览代码

feat: Add manual receivable creation and management for franchisee accounts

- Introduced `FranchisorReceivableService` to handle manual creation, settling, and reopening of franchisee account receivables.
- Added `StoreFranchisorReceivableRequest` for validation of incoming data when creating receivables.
- Updated `FranchiseeAccountReceiveController` to include new endpoints for storing, settling, and reopening receivables.
- Enhanced `FranchiseeAccountReceiveResource` to include new fields and relationships.
- Modified `FranchiseeAccountReceive` model to support new fields and relationships.
- Updated database migration to add necessary fields for manual receivables.
- Adjusted financial dashboard services to account for new manual receivables in revenue calculations.
- Updated routes to include new endpoints for franchisee account receivables management.
ebagabee 1 周之前
父节点
当前提交
095958941d

+ 12 - 10
app/Enums/NotificationType.php

@@ -12,21 +12,23 @@ enum NotificationType: string
     use EnumHelper;
 
     case SUPPORT_TICKET_CREATED = 'support_ticket_created';
-    case SUPPORT_TICKET_REPLY   = 'support_ticket_reply';
-    case KANBAN_CREATED         = 'kanban_created';
-    case KANBAN_REPLY           = 'kanban_reply';
-    case KANBAN_STATUS_CHANGED  = 'kanban_status_changed';
-    case TBR_MONTH_CLOSED       = 'tbr_month_closed';
+    case SUPPORT_TICKET_REPLY = 'support_ticket_reply';
+    case KANBAN_CREATED = 'kanban_created';
+    case KANBAN_REPLY = 'kanban_reply';
+    case KANBAN_STATUS_CHANGED = 'kanban_status_changed';
+    case TBR_MONTH_CLOSED = 'tbr_month_closed';
+    case ACCOUNT_PAYABLE_CREATED = 'account_payable_created';
 
     public function label(): string
     {
         return match ($this) {
             self::SUPPORT_TICKET_CREATED => 'Novo chamado de suporte',
-            self::SUPPORT_TICKET_REPLY   => 'Nova resposta no chamado',
-            self::KANBAN_CREATED         => 'Nova atividade',
-            self::KANBAN_REPLY           => 'Novo comentário na atividade',
-            self::KANBAN_STATUS_CHANGED  => 'Status da atividade atualizado',
-            self::TBR_MONTH_CLOSED       => 'Fechamento do mês',
+            self::SUPPORT_TICKET_REPLY => 'Nova resposta no chamado',
+            self::KANBAN_CREATED => 'Nova atividade',
+            self::KANBAN_REPLY => 'Novo comentário na atividade',
+            self::KANBAN_STATUS_CHANGED => 'Status da atividade atualizado',
+            self::TBR_MONTH_CLOSED => 'Fechamento do mês',
+            self::ACCOUNT_PAYABLE_CREATED => 'Nova conta a pagar',
         };
     }
 }

+ 45 - 1
app/Http/Controllers/FranchiseeAccountReceiveController.php

@@ -2,13 +2,20 @@
 
 namespace App\Http\Controllers;
 
+use App\Http\Requests\SettlePayableRequest;
+use App\Http\Requests\StoreFranchisorReceivableRequest;
 use App\Http\Resources\FranchiseeAccountReceiveResource;
 use App\Models\FranchiseeAccountReceive;
+use App\Services\FranchisorReceivableService;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
 
 class FranchiseeAccountReceiveController extends Controller
 {
+    public function __construct(
+        protected FranchisorReceivableService $service,
+    ) {}
+
     /**
      * Lista, na visão consolidada do Franqueador, os títulos a receber das
      * franquias (TBR gerada). É o mesmo registro que, do lado da franquia,
@@ -16,7 +23,7 @@ class FranchiseeAccountReceiveController extends Controller
      */
     public function index(Request $request): JsonResponse
     {
-        $query = FranchiseeAccountReceive::with(['unit', 'details'])
+        $query = FranchiseeAccountReceive::with(['unit', 'details', 'planAccount'])
             ->orderByDesc('due_date')
             ->orderByDesc('id');
 
@@ -32,4 +39,41 @@ public function index(Request $request): JsonResponse
             payload: FranchiseeAccountReceiveResource::collection($query->get()),
         );
     }
+
+    public function store(StoreFranchisorReceivableRequest $request): JsonResponse
+    {
+        $item = $this->service->createManual($request->validated());
+
+        return $this->successResponse(
+            payload: new FranchiseeAccountReceiveResource($item),
+            message: __('messages.created'),
+            code: 201,
+        );
+    }
+
+    public function settle(SettlePayableRequest $request, int $id): JsonResponse
+    {
+        $item = FranchiseeAccountReceive::find($id);
+        if (! $item) {
+            return $this->errorResponse(message: 'Conta a receber não encontrada', code: 404);
+        }
+
+        return $this->successResponse(
+            payload: new FranchiseeAccountReceiveResource($this->service->settle($item, $request->validated())),
+            message: 'Baixa realizada com sucesso',
+        );
+    }
+
+    public function reopen(int $id): JsonResponse
+    {
+        $item = FranchiseeAccountReceive::find($id);
+        if (! $item) {
+            return $this->errorResponse(message: 'Conta a receber não encontrada', code: 404);
+        }
+
+        return $this->successResponse(
+            payload: new FranchiseeAccountReceiveResource($this->service->reopen($item)),
+            message: 'Conta reaberta',
+        );
+    }
 }

+ 27 - 0
app/Http/Requests/StoreFranchisorReceivableRequest.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
+
+class StoreFranchisorReceivableRequest extends FormRequest
+{
+    public function rules(): array
+    {
+        return [
+            'account_type' => ['required', Rule::in(['internal', 'unit'])],
+            'unit_id' => [
+                Rule::requiredIf($this->input('account_type') === 'unit'),
+                'nullable',
+                'integer',
+                'exists:units,id',
+            ],
+            'history' => ['required', 'string', 'max:255'],
+            'value' => ['required', 'numeric', 'min:0.01'],
+            'due_date' => ['required', 'date'],
+            'financial_plan_account_id' => ['nullable', 'integer', 'exists:financial_plan_accounts,id'],
+            'obs' => ['nullable', 'string', 'max:1000'],
+        ];
+    }
+}

+ 26 - 18
app/Http/Resources/FranchiseeAccountReceiveResource.php

@@ -12,27 +12,35 @@ class FranchiseeAccountReceiveResource extends JsonResource
     public function toArray(Request $request): array
     {
         return [
-            'id'                 => $this->id,
-            'unit_id'            => $this->unit_id,
-            'unit_name'          => $this->whenLoaded('unit', fn () => $this->unit->fantasy_name),
+            'id' => $this->id,
+            'unit_id' => $this->unit_id,
+            'unit_name' => $this->whenLoaded('unit', fn () => $this->unit?->fantasy_name),
             'tbr_calculation_id' => $this->tbr_calculation_id,
-            'order'              => $this->order,
-            'history'            => $this->history,
-            'value'              => $this->value,
-            'paid_value'         => $this->paid_value,
-            'due_date'           => $this->due_date?->format('Y-m-d'),
-            'discount'           => $this->discount,
-            'fees'               => $this->fees,
-            'obs'                => $this->obs,
-            'asaas_id'           => $this->asaas_id,
-            'status'             => $this->status,
-            'details'            => $this->whenLoaded('details', fn () => $this->details->map(fn ($d) => [
-                'id'      => $d->id,
-                'value'   => $d->value,
+            'origin' => $this->origin,
+            'financial_plan_account_id' => $this->financial_plan_account_id,
+            'plan_account' => $this->whenLoaded('planAccount', fn () => $this->planAccount ? [
+                'id' => $this->planAccount->id,
+                'code' => $this->planAccount->code,
+                'description' => $this->planAccount->description,
+            ] : null),
+            'order' => $this->order,
+            'history' => $this->history,
+            'value' => $this->value,
+            'paid_value' => $this->paid_value,
+            'due_date' => $this->due_date?->format('Y-m-d'),
+            'discount' => $this->discount,
+            'fees' => $this->fees,
+            'obs' => $this->obs,
+            'asaas_id' => $this->asaas_id,
+            'status' => $this->status,
+            'payment_date' => $this->payment_date?->format('Y-m-d'),
+            'details' => $this->whenLoaded('details', fn () => $this->details->map(fn ($d) => [
+                'id' => $d->id,
+                'value' => $d->value,
                 'history' => $d->history,
             ])),
-            'created_at'         => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
-            'updated_at'         => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
+            'created_at' => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
+            'updated_at' => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
         ];
     }
 

+ 11 - 6
app/Models/FranchiseeAccountReceive.php

@@ -17,14 +17,14 @@ class FranchiseeAccountReceive extends Model
     protected $guarded = ['id'];
 
     protected $casts = [
-        'value'      => 'decimal:2',
+        'value' => 'decimal:2',
         'paid_value' => 'decimal:2',
-        'discount'   => 'decimal:2',
-        'fees'       => 'decimal:2',
-        'due_date'     => 'date',
+        'discount' => 'decimal:2',
+        'fees' => 'decimal:2',
+        'due_date' => 'date',
         'payment_date' => 'datetime',
-        'status'       => \App\Enums\ReceivableStatus::class,
-        'created_at'   => 'datetime',
+        'status' => \App\Enums\ReceivableStatus::class,
+        'created_at' => 'datetime',
         'updated_at' => 'datetime',
     ];
 
@@ -38,6 +38,11 @@ public function tbrCalculation(): BelongsTo
         return $this->belongsTo(TbrCalculation::class, 'tbr_calculation_id');
     }
 
+    public function planAccount(): BelongsTo
+    {
+        return $this->belongsTo(FinancialPlanAccount::class, 'financial_plan_account_id');
+    }
+
     public function details(): HasMany
     {
         return $this->hasMany(FranchiseeAccountReceiveDetail::class, 'franchisee_account_receive_id');

+ 1 - 0
app/Services/FinancialDashboardService.php

@@ -74,6 +74,7 @@ private function tbrTotal(Carbon $start, Carbon $end): float
     {
         return (float) DB::table('franchisee_account_receives')
             ->whereNull('deleted_at')
+            ->whereNotNull('unit_id')
             ->whereBetween('due_date', [$start, $end])
             ->sum('value');
     }

+ 1 - 0
app/Services/FranchisorDashboardService.php

@@ -240,6 +240,7 @@ private function generalRevenue(array $unitIds): array
     {
         $base = DB::table('franchisee_account_receives')
             ->whereNull('deleted_at')
+            ->whereNotNull('unit_id')
             ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds));
 
         $value = (float) (clone $base)->sum('value');

+ 114 - 0
app/Services/FranchisorReceivableService.php

@@ -0,0 +1,114 @@
+<?php
+
+namespace App\Services;
+
+use App\Enums\NotificationType;
+use App\Models\FranchiseeAccountReceive;
+use App\Models\UnitAccountPayable;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Validation\ValidationException;
+
+class FranchisorReceivableService
+{
+    public function __construct(
+        protected NotificationService $notifications,
+    ) {}
+
+    public function createManual(array $data): FranchiseeAccountReceive
+    {
+        return DB::transaction(function () use ($data) {
+            $unitId = $data['account_type'] === 'unit' ? (int) $data['unit_id'] : null;
+
+            $receivable = FranchiseeAccountReceive::create([
+                'unit_id' => $unitId,
+                'tbr_calculation_id' => null,
+                'financial_plan_account_id' => $data['financial_plan_account_id'] ?? null,
+                'origin' => $unitId ? 'manual_unit' : 'manual_internal',
+                'history' => $data['history'],
+                'value' => $data['value'],
+                'paid_value' => 0,
+                'due_date' => $data['due_date'],
+                'discount' => 0,
+                'fees' => 0,
+                'obs' => $data['obs'] ?? null,
+                'status' => 'pending',
+            ]);
+
+            if ($unitId) {
+                UnitAccountPayable::create([
+                    'unit_id' => $unitId,
+                    'franchisee_account_receive_id' => $receivable->id,
+                    'origin' => UnitAccountPayable::ORIGIN_MANUAL,
+                    'history' => $receivable->history,
+                    'value' => $receivable->value,
+                    'paid_value' => 0,
+                    'discount' => 0,
+                    'fine' => 0,
+                    'due_date' => $receivable->due_date,
+                    'status' => 'pending',
+                    'obs' => $receivable->obs,
+                ]);
+
+                $this->notifications->dispatch([
+                    'origin_table' => 'franchisee_account_receives',
+                    'origin_id' => $receivable->id,
+                    'unit_id' => $unitId,
+                    'type' => NotificationType::ACCOUNT_PAYABLE_CREATED->value,
+                    'title' => 'Nova conta a pagar',
+                    'message' => "Uma nova conta a pagar foi criada: {$receivable->history}.",
+                    'url' => '/financial/accounts-payable',
+                    'action_text' => 'Ver Contas a Pagar',
+                    'priority' => 'normal',
+                    'recipient_user_ids' => $this->notifications->unitUserIds($unitId),
+                ]);
+            }
+
+            return $receivable->load(['unit', 'details', 'planAccount']);
+        });
+    }
+
+    public function settle(FranchiseeAccountReceive $receivable, array $data): FranchiseeAccountReceive
+    {
+        if ($receivable->status->value === 'paid') {
+            throw ValidationException::withMessages(['status' => 'Esta conta já está paga.']);
+        }
+
+        return DB::transaction(function () use ($receivable, $data) {
+            $receivable->update([
+                'status' => 'paid',
+                'paid_value' => $receivable->value,
+                'payment_date' => $data['payment_date'] ?? Carbon::today()->format('Y-m-d'),
+            ]);
+
+            UnitAccountPayable::where('franchisee_account_receive_id', $receivable->id)
+                ->update([
+                    'status' => 'paid',
+                    'paid_value' => $receivable->value,
+                    'payment_date' => $data['payment_date'] ?? Carbon::today()->format('Y-m-d'),
+                ]);
+
+            return $receivable->fresh(['unit', 'details', 'planAccount']);
+        });
+    }
+
+    public function reopen(FranchiseeAccountReceive $receivable): FranchiseeAccountReceive
+    {
+        if ($receivable->status->value !== 'paid') {
+            throw ValidationException::withMessages(['status' => 'Esta conta não está paga.']);
+        }
+
+        return DB::transaction(function () use ($receivable) {
+            $receivable->update([
+                'status' => 'pending',
+                'paid_value' => 0,
+                'payment_date' => null,
+            ]);
+
+            UnitAccountPayable::where('franchisee_account_receive_id', $receivable->id)
+                ->update(['status' => 'pending', 'paid_value' => 0, 'payment_date' => null]);
+
+            return $receivable->fresh(['unit', 'details', 'planAccount']);
+        });
+    }
+}

+ 162 - 147
app/Services/TbrCalculationService.php

@@ -20,9 +20,13 @@
 class TbrCalculationService
 {
     private const ROYALTIES_REVENUE_RATE = 0.08;
-    private const FNM_REVENUE_RATE       = 0.02;
+
+    private const FNM_REVENUE_RATE = 0.02;
+
     private const FNM_BRACKET_PERCENTAGE = 0.20;
-    private const MAINTENANCE_RATE       = 0.30;
+
+    private const MAINTENANCE_RATE = 0.30;
+
     private const EXEMPT_THRESHOLD_MONTH = 3;
 
     // Faturamento (competência): por padrão usa o próprio mês de referência
@@ -58,23 +62,32 @@ public function resolveRevenue(int $unitId, int $referenceYear, int $referenceMo
     public function resolveRevenueBetween(array $unitIds, Carbon $startDate, Carbon $endDate): float
     {
         $start = $startDate->toDateString();
-        $end   = $endDate->toDateString();
+        $end = $endDate->toDateString();
 
-        // Base de faturamento = parcelas de aluno + recebíveis avulsos (manuais)
-        // do mês de referência (decisão da franqueadora: avulsos compõem o royalty).
+        // Base de faturamento = parcelas de aluno + recebíveis avulsos da unidade
+        // + contas manuais que a franqueadora vinculou à unidade. Recebíveis de
+        // TBR ficam fora para não realimentar a própria base de cálculo.
         $installments = (float) StudentContractInstallment::whereNull('deleted_at')
-            ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
+            ->when(! empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
             ->where('status', '!=', 'cancelled')
             ->whereBetween('due_date', [$start, $end])
             ->sum('value');
 
         $manual = (float) UnitAccountReceivable::whereNull('deleted_at')
-            ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
+            ->when(! empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
+            ->where('status', '!=', 'cancelled')
+            ->whereBetween('due_date', [$start, $end])
+            ->sum('value');
+
+        $franchisorLinked = (float) FranchiseeAccountReceive::whereNull('deleted_at')
+            ->where('origin', 'manual_unit')
+            ->whereNotNull('unit_id')
+            ->when(! empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds))
             ->where('status', '!=', 'cancelled')
             ->whereBetween('due_date', [$start, $end])
             ->sum('value');
 
-        return $installments + $manual;
+        return $installments + $manual + $franchisorLinked;
     }
 
     /**
@@ -115,8 +128,8 @@ public function findById(int $id): ?TbrCalculation
     public function preview(array $data): array
     {
         $contract = $this->resolveContract($data['unit_id']);
-        $year     = (int) $data['reference_year'];
-        $month    = (int) $data['reference_month'];
+        $year = (int) $data['reference_year'];
+        $month = (int) $data['reference_month'];
 
         return $this->buildPreview($contract, $year, $month, $this->pickRevenue($data, $contract->unit_id, $year, $month));
     }
@@ -128,12 +141,13 @@ public function previewBatch(int $referenceYear, int $referenceMonth): array
         return $contracts->map(function (FranchiseeContract $contract) use ($referenceYear, $referenceMonth) {
             try {
                 $revenue = $this->resolveRevenue($contract->unit_id, $referenceYear, $referenceMonth);
+
                 return $this->buildPreview($contract, $referenceYear, $referenceMonth, $revenue);
             } catch (ValidationException $e) {
                 return [
-                    'unit_id'   => $contract->unit_id,
+                    'unit_id' => $contract->unit_id,
                     'unit_name' => $contract->unit?->fantasy_name,
-                    'error'     => collect($e->errors())->flatten()->first(),
+                    'error' => collect($e->errors())->flatten()->first(),
                 ];
             }
         })->values()->toArray();
@@ -143,9 +157,9 @@ public function calculate(array $data): TbrCalculation
     {
         return DB::transaction(function () use ($data) {
             $contract = $this->resolveContract($data['unit_id']);
-            $year     = (int) $data['reference_year'];
-            $month    = (int) $data['reference_month'];
-            $payload  = $this->buildPreview($contract, $year, $month, $this->pickRevenue($data, $contract->unit_id, $year, $month));
+            $year = (int) $data['reference_year'];
+            $month = (int) $data['reference_month'];
+            $payload = $this->buildPreview($contract, $year, $month, $this->pickRevenue($data, $contract->unit_id, $year, $month));
 
             return $this->persistCalculation($payload);
         });
@@ -171,7 +185,7 @@ public function generateReceivable(int $calculationId): FranchiseeAccountReceive
             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 . '.',
+                        .$calculation->contract_month_reference.'.',
                 ]);
             }
 
@@ -192,8 +206,8 @@ public function generateBatch(int $referenceYear, int $referenceMonth, ?array $u
         }
 
         $generated = [];
-        $skipped   = [];
-        $errors    = [];
+        $skipped = [];
+        $errors = [];
 
         foreach ($contracts as $contract) {
             try {
@@ -203,52 +217,53 @@ public function generateBatch(int $referenceYear, int $referenceMonth, ?array $u
 
                     if ($payload['receivable_already_generated']) {
                         $skipped[] = [
-                            'unit_id'   => $contract->unit_id,
+                            'unit_id' => $contract->unit_id,
                             'unit_name' => $payload['unit_name'],
-                            'reason'    => 'Já gerado para este mês de contrato.',
+                            'reason' => 'Já gerado para este mês de contrato.',
                         ];
+
                         return;
                     }
 
                     $calculation = $this->persistCalculation($payload);
-                    $receive     = $this->buildReceivable($calculation, $contract);
+                    $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'],
+                        '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_id' => $contract->unit_id,
                     'unit_name' => $contract->unit?->fantasy_name,
-                    'reason'    => collect($e->errors())->flatten()->first(),
+                    'reason' => collect($e->errors())->flatten()->first(),
                 ];
             } catch (\Throwable $e) {
                 $errors[] = [
-                    'unit_id'   => $contract->unit_id,
+                    'unit_id' => $contract->unit_id,
                     'unit_name' => $contract->unit?->fantasy_name,
-                    'reason'    => $e->getMessage(),
+                    'reason' => $e->getMessage(),
                 ];
             }
         }
 
         return [
             'generated_count' => count($generated),
-            'skipped_count'   => count($skipped),
-            'error_count'     => count($errors),
-            'generated'       => $generated,
-            'skipped'         => $skipped,
-            'errors'          => $errors,
+            '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();
+        $referenceLastDay = Carbon::createFromDate($referenceYear, $referenceMonth, 1)->endOfMonth()->toDateString();
         $referenceFirstDay = Carbon::createFromDate($referenceYear, $referenceMonth, 1)->startOfMonth()->toDateString();
 
         return FranchiseeContract::with(['unit', 'municipalitySize'])
@@ -274,13 +289,13 @@ private function resolveContract(int $unitId): FranchiseeContract
             ->orderByDesc('id')
             ->first();
 
-        if (!$contract) {
+        if (! $contract) {
             throw ValidationException::withMessages([
                 'unit_id' => 'Unidade não possui contrato cadastrado.',
             ]);
         }
 
-        if (!$contract->municipality_size_id) {
+        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.',
             ]);
@@ -317,144 +332,144 @@ private function buildPreview(FranchiseeContract $contract, int $referenceYear,
             : null;
         $royaltiesBracketPercentage = $royaltiesBracket ? (float) $royaltiesBracket->tbr_percentage : 0.0;
 
-        $fnmPercentage         = $chargeFnm ? $this->resolveFnmPercentage($contractMonth) : 0.0;
+        $fnmPercentage = $chargeFnm ? $this->resolveFnmPercentage($contractMonth) : 0.0;
         $maintenancePercentage = self::MAINTENANCE_RATE;
 
-        $royaltiesBracketValue   = round($royaltiesBracketPercentage * $tbrValue, 2);
-        $fnmBracketValue         = round($fnmPercentage * $tbrValue, 2);
+        $royaltiesBracketValue = round($royaltiesBracketPercentage * $tbrValue, 2);
+        $fnmBracketValue = round($fnmPercentage * $tbrValue, 2);
         $maintenanceBracketValue = round($maintenancePercentage * $tbrValue, 2);
 
         [$royaltiesEffectiveValue, $royaltiesEffectivePercentage, $royaltiesAppliedCriteria,
-         $fnmEffectiveValue, $fnmEffectivePercentage] = $this->resolveEffectiveValues(
-            $contractMonth,
-            $revenueValue,
-            $royaltiesBracketPercentage,
-            $royaltiesBracketValue,
-            $fnmPercentage,
-            $fnmBracketValue,
-        );
+            $fnmEffectiveValue, $fnmEffectivePercentage] = $this->resolveEffectiveValues(
+                $contractMonth,
+                $revenueValue,
+                $royaltiesBracketPercentage,
+                $royaltiesBracketValue,
+                $fnmPercentage,
+                $fnmBracketValue,
+            );
 
         // Cobrança desligada: zera o respectivo componente (fora do total).
-        if (!$chargeRoi) {
-            $royaltiesEffectiveValue      = 0.0;
+        if (! $chargeRoi) {
+            $royaltiesEffectiveValue = 0.0;
             $royaltiesEffectivePercentage = 0.0;
-            $royaltiesAppliedCriteria     = 'nao_cobrado';
+            $royaltiesAppliedCriteria = 'nao_cobrado';
         }
 
-        if (!$chargeFnm) {
-            $fnmEffectiveValue      = 0.0;
+        if (! $chargeFnm) {
+            $fnmEffectiveValue = 0.0;
             $fnmEffectivePercentage = 0.0;
         }
 
-        $maintenanceEffectiveValue      = $maintenanceBracketValue;
+        $maintenanceEffectiveValue = $maintenanceBracketValue;
         $maintenanceEffectivePercentage = $maintenancePercentage;
 
         $bracketSubtotal = round($royaltiesBracketValue + $fnmBracketValue + $maintenanceBracketValue, 2);
-        $subtotal        = round($royaltiesEffectiveValue + $fnmEffectiveValue + $maintenanceEffectiveValue, 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'     => $royaltiesBracketPercentage,
-            '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,
+            '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' => $royaltiesBracketPercentage,
+            '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),
+            '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'],
-            'reference_year'                   => $payload['reference_year'],
-            'reference_month'                  => $payload['reference_month'],
-            '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'],
+            'unit_id' => $payload['unit_id'],
+            'reference_year' => $payload['reference_year'],
+            'reference_month' => $payload['reference_month'],
+            '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,
+            '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
     {
         // Mês de competência = o que a pessoa escolheu ao gerar (não a data de geração).
-        $referenceDate  = $calculation->reference_year && $calculation->reference_month
+        $referenceDate = $calculation->reference_year && $calculation->reference_month
             ? Carbon::create($calculation->reference_year, $calculation->reference_month, 1)
             : Carbon::parse($calculation->created_at);
         $referenceLabel = $referenceDate->format('m/Y');
-        $dueDate        = $this->resolveDueDate($contract, $referenceDate);
+        $dueDate = $this->resolveDueDate($contract, $referenceDate);
 
         $receive = FranchiseeAccountReceive::create([
-            'unit_id'            => $calculation->unit_id,
+            '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',
+            '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,
+            'value' => $calculation->royalties_effective_value,
+            'history' => 'Royalties '.$referenceLabel,
         ]);
 
         FranchiseeAccountReceiveDetail::create([
             'franchisee_account_receive_id' => $receive->id,
-            'value'                         => $calculation->fnm_effective_value,
-            'history'                       => 'FNM ' . $referenceLabel,
+            '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,
+            'value' => $calculation->maintenance_effective_value,
+            'history' => 'Taxa Manutenção '.$referenceLabel,
         ]);
 
         $calculation->update(['receivable_generated' => true]);
@@ -462,16 +477,16 @@ private function buildReceivable(TbrCalculation $calculation, ?FranchiseeContrac
         // Espelho na unidade: o TBR que a matriz cobra vira uma Conta a Pagar da
         // franquia (ciclo próprio, com ou sem Asaas). A dívida é registrada sempre.
         \App\Models\UnitAccountPayable::create([
-            'unit_id'                       => $receive->unit_id,
+            'unit_id' => $receive->unit_id,
             'franchisee_account_receive_id' => $receive->id,
-            'origin'                        => \App\Models\UnitAccountPayable::ORIGIN_TBR,
-            'history'                       => $receive->history,
-            'value'                         => $receive->value,
-            'paid_value'                    => 0,
-            'discount'                      => 0,
-            'fine'                          => 0,
-            'due_date'                      => $receive->due_date,
-            'status'                        => 'pending',
+            'origin' => \App\Models\UnitAccountPayable::ORIGIN_TBR,
+            'history' => $receive->history,
+            'value' => $receive->value,
+            'paid_value' => 0,
+            'discount' => 0,
+            'fine' => 0,
+            'due_date' => $receive->due_date,
+            'status' => 'pending',
         ]);
 
         \App\Jobs\SyncFranchiseeChargeJob::dispatch($receive);
@@ -492,23 +507,23 @@ private function resolveEffectiveValues(
         }
 
         $royaltiesFromRevenue = round(self::ROYALTIES_REVENUE_RATE * $revenueValue, 2);
-        $fnmFromRevenue       = round(self::FNM_REVENUE_RATE * $revenueValue, 2);
+        $fnmFromRevenue = round(self::FNM_REVENUE_RATE * $revenueValue, 2);
 
         if ($royaltiesBracketValue >= $royaltiesFromRevenue) {
-            $royaltiesEffectiveValue      = $royaltiesBracketValue;
+            $royaltiesEffectiveValue = $royaltiesBracketValue;
             $royaltiesEffectivePercentage = $royaltiesBracketPercentage;
-            $royaltiesAppliedCriteria     = 'tbr_fixo';
+            $royaltiesAppliedCriteria = 'tbr_fixo';
         } else {
-            $royaltiesEffectiveValue      = $royaltiesFromRevenue;
+            $royaltiesEffectiveValue = $royaltiesFromRevenue;
             $royaltiesEffectivePercentage = self::ROYALTIES_REVENUE_RATE;
-            $royaltiesAppliedCriteria     = 'percentual_faturamento';
+            $royaltiesAppliedCriteria = 'percentual_faturamento';
         }
 
         if ($fnmBracketValue >= $fnmFromRevenue) {
-            $fnmEffectiveValue      = $fnmBracketValue;
+            $fnmEffectiveValue = $fnmBracketValue;
             $fnmEffectivePercentage = $fnmBracketPercentage;
         } else {
-            $fnmEffectiveValue      = $fnmFromRevenue;
+            $fnmEffectiveValue = $fnmFromRevenue;
             $fnmEffectivePercentage = self::FNM_REVENUE_RATE;
         }
 
@@ -517,13 +532,13 @@ private function resolveEffectiveValues(
 
     private function resolveContractMonth(?Carbon $startDate, int $year, int $month): int
     {
-        if (!$startDate) {
+        if (! $startDate) {
             return 1;
         }
 
-        $start     = $startDate->copy()->startOfMonth();
+        $start = $startDate->copy()->startOfMonth();
         $reference = Carbon::createFromDate($year, $month, 1)->startOfMonth();
-        $diff      = (int) $start->diffInMonths($reference);
+        $diff = (int) $start->diffInMonths($reference);
 
         return max(1, $diff + 1);
     }
@@ -539,7 +554,7 @@ private function findRoyaltiesBracket(int $municipalitySizeId, int $contractMont
             ->orderBy('start')
             ->first();
 
-        if (!$bracket) {
+        if (! $bracket) {
             throw ValidationException::withMessages([
                 'municipality_size_id' => 'Não foi encontrada faixa de royalties para o porte e mês de contrato informados.',
             ]);

+ 32 - 0
database/migrations/2026_07_22_000001_add_manual_fields_to_franchisee_account_receives.php

@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('franchisee_account_receives', function (Blueprint $table) {
+            $table->foreignId('unit_id')->nullable()->change();
+            $table->foreignId('tbr_calculation_id')->nullable()->change();
+            $table->foreignId('financial_plan_account_id')
+                ->nullable()
+                ->after('tbr_calculation_id')
+                ->constrained('financial_plan_accounts')
+                ->nullOnDelete();
+            $table->string('origin', 30)->default('tbr')->after('financial_plan_account_id');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('franchisee_account_receives', function (Blueprint $table) {
+            $table->dropConstrainedForeignId('financial_plan_account_id');
+            $table->dropColumn('origin');
+            $table->foreignId('unit_id')->nullable(false)->change();
+            $table->foreignId('tbr_calculation_id')->nullable(false)->change();
+        });
+    }
+};

+ 6 - 2
routes/authRoutes/franchisor_financial.php

@@ -32,8 +32,12 @@
     Route::post('/{id}/reopen', 'reopen')->whereNumber('id')->middleware('permission:franchisor_financial,edit');
 });
 
-Route::get('/franchisee-account-receive', [FranchiseeAccountReceiveController::class, 'index'])
-    ->middleware('permission:franchisor_financial,view');
+Route::controller(FranchiseeAccountReceiveController::class)->prefix('franchisee-account-receive')->group(function () {
+    Route::get('/', 'index')->middleware('permission:franchisor_financial,view');
+    Route::post('/', 'store')->middleware('permission:franchisor_financial,add');
+    Route::post('/{id}/settle', 'settle')->whereNumber('id')->middleware('permission:franchisor_financial,edit');
+    Route::post('/{id}/reopen', 'reopen')->whereNumber('id')->middleware('permission:franchisor_financial,edit');
+});
 
 Route::controller(FinancialPlanAccountController::class)->prefix('financial-plan-account')->group(function () {
     Route::get('/', 'index')->middleware('permission:franchisor_financial,view');