Sfoglia il codice sorgente

feat(plan-account): hierarquia parent_id + visibilidade matriz/unidade

- parent_id (self-FK) no financial_plan_accounts + relações parent/children.
- Seed com raízes por tipo (Ativo/Passivo/Receitas/Despesas) e filhas
  linkadas pelo código; conta filha herda o chart_type do pai.
- Visibilidade: matriz vê só o próprio plano; unidade vê o da matriz
  (somente leitura) + cria/edita o seu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ebagabee 3 settimane fa
parent
commit
8e9f21abdc

+ 4 - 4
app/Http/Controllers/FinancialPlanAccountController.php

@@ -7,7 +7,6 @@
 use App\Http\Requests\FinancialPlanAccountRequest;
 use App\Http\Resources\FinancialPlanAccountResource;
 use Illuminate\Http\JsonResponse;
-use Illuminate\Http\Request;
 
 class FinancialPlanAccountController extends Controller
 {
@@ -73,10 +72,11 @@ public function destroyMe(int $id): JsonResponse
         return $this->successResponse(message: __('messages.deleted'), code: 204);
     }
 
-    public function index(Request $request): JsonResponse
+    public function index(): JsonResponse
     {
-        $unitId = $request->filled('unit_id') ? (int) $request->input('unit_id') : null;
-        $items = $this->service->getAll($unitId);
+        // Matriz enxerga apenas o próprio plano de contas (unit_id null);
+        // nunca os planos criados pelas unidades.
+        $items = $this->service->getAll(null);
         return $this->successResponse(payload: FinancialPlanAccountResource::collection($items));
     }
 

+ 8 - 1
app/Http/Requests/FinancialPlanAccountRequest.php

@@ -13,7 +13,14 @@ public function rules(): array
         return [
             'code'        => [$isCreate ? 'required' : 'sometimes', 'string', 'max:50'],
             'description' => [$isCreate ? 'required' : 'sometimes', 'string', 'max:255'],
-            'chart_type'  => [$isCreate ? 'required' : 'sometimes', 'string', 'max:50'],
+            // chart_type é obrigatório só quando NÃO há pai; com pai, é herdado.
+            'chart_type'  => [
+                $isCreate ? 'required_without:parent_id' : 'sometimes',
+                'nullable',
+                'string',
+                'max:50',
+            ],
+            'parent_id'   => ['nullable', 'integer', 'exists:financial_plan_accounts,id'],
             'order'       => ['nullable', 'string', 'max:50'],
             'unit_id'     => ['nullable', 'integer', 'exists:units,id'],
         ];

+ 7 - 0
app/Http/Resources/FinancialPlanAccountResource.php

@@ -20,6 +20,13 @@ public function toArray(Request $request): array
         return [
             'id' => $this->id,
             'unit_id' => $this->unit_id,
+            'parent_id' => $this->parent_id,
+            'parent' => $this->whenLoaded('parent', fn () => $this->parent ? [
+                'id'          => $this->parent->id,
+                'code'        => $this->parent->code,
+                'description' => $this->parent->description,
+                'chart_type'  => $this->parent->chart_type,
+            ] : null),
             'code' => $this->code,
             'order' => $this->order,
             'description' => $this->description,

+ 16 - 1
app/Models/FinancialPlanAccount.php

@@ -42,13 +42,28 @@ class FinancialPlanAccount extends Model
     protected $casts = [
         'created_at' => 'datetime',
         'updated_at' => 'datetime',
-        // Add your casts here (e.g., 'is_active' => 'boolean')
     ];
 
     // Relationships
 
+    public function parent(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+    {
+        return $this->belongsTo(self::class, 'parent_id');
+    }
+
+    public function children(): \Illuminate\Database\Eloquent\Relations\HasMany
+    {
+        return $this->hasMany(self::class, 'parent_id');
+    }
+
     // Business Logic Methods
 
+    /** Conta raiz (sem pai). */
+    public function isRoot(): bool
+    {
+        return $this->parent_id === null;
+    }
+
     // Custom Finders
 
     // Query Scopes

+ 29 - 5
app/Services/FinancialPlanAccountService.php

@@ -14,9 +14,14 @@ class FinancialPlanAccountService
     public function getAll(?int $unitId = null): Collection
     {
         return FinancialPlanAccount::query()
+            ->with('parent:id,code,description,chart_type')
             ->when(
                 $unitId !== null,
-                fn ($query) => $query->where('unit_id', $unitId),
+                // Unidade: vê o próprio plano (editável) + o plano da matriz (unit_id null, só leitura/uso).
+                fn ($query) => $query->where(
+                    fn ($sub) => $sub->where('unit_id', $unitId)->orWhereNull('unit_id'),
+                ),
+                // Matriz: vê apenas o próprio plano; nunca enxerga o plano das unidades.
                 fn ($query) => $query->whereNull('unit_id'),
             )
             ->orderBy('code')
@@ -35,10 +40,12 @@ public function findByIdForUnit(int $id, int $unitId): ?FinancialPlanAccount
 
     public function create(array $data): FinancialPlanAccount
     {
-        $data['unit_id'] = $data['unit_id'] ?? null;
-        $data['order'] = $data['order'] ?? $data['code'];
+        $data['unit_id']   = $data['unit_id'] ?? null;
+        $data['order']     = $data['order'] ?? $data['code'];
+        $data['parent_id'] = $data['parent_id'] ?? null;
+        $data = $this->applyParentChartType($data);
 
-        return FinancialPlanAccount::create($data);
+        return FinancialPlanAccount::create($data)->load('parent:id,code,description,chart_type');
     }
 
     public function update(int $id, array $data): ?FinancialPlanAccount
@@ -49,8 +56,25 @@ public function update(int $id, array $data): ?FinancialPlanAccount
             return null;
         }
 
+        $data = $this->applyParentChartType($data);
+
         $model->update($data);
-        return $model->fresh();
+        return $model->fresh('parent:id,code,description,chart_type');
+    }
+
+    /**
+     * Conta filha herda o chart_type do pai (a UI não envia chart_type nesse caso).
+     */
+    private function applyParentChartType(array $data): array
+    {
+        if (!empty($data['parent_id'])) {
+            $parent = FinancialPlanAccount::find($data['parent_id']);
+            if ($parent) {
+                $data['chart_type'] = $parent->chart_type;
+            }
+        }
+
+        return $data;
     }
 
     public function delete(int $id): bool

+ 32 - 0
database/migrations/2026_07_02_000002_add_parent_id_to_financial_plan_accounts.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
+{
+    /**
+     * Hierarquia do plano de contas: conta pai (raiz, parent_id null) e
+     * contas filhas apontando para o pai. A conta filha herda o chart_type do pai.
+     */
+    public function up(): void
+    {
+        Schema::table('financial_plan_accounts', function (Blueprint $table) {
+            $table->foreignId('parent_id')
+                ->nullable()
+                ->after('unit_id')
+                ->constrained('financial_plan_accounts')
+                ->nullOnDelete();
+
+            $table->index('parent_id');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('financial_plan_accounts', function (Blueprint $table) {
+            $table->dropConstrainedForeignId('parent_id');
+        });
+    }
+};

+ 35 - 3
database/seeders/FinancialPlanAccountSeeder.php

@@ -10,6 +10,14 @@ class FinancialPlanAccountSeeder extends Seeder
     public function run(): void
     {
         // Plano de contas padrão da matriz (franqueador): unit_id null.
+        // Raízes (contas pai) por tipo — parent_id null. As filhas herdam o chart_type.
+        $roots = [
+            ['code' => '1', 'description' => 'Ativo',    'chart_type' => 'Ativo'],
+            ['code' => '2', 'description' => 'Passivo',  'chart_type' => 'Passivo'],
+            ['code' => '3', 'description' => 'Receitas', 'chart_type' => 'Receitas'],
+            ['code' => '4', 'description' => 'Despesas', 'chart_type' => 'Despesas'],
+        ];
+
         $accounts = [
             ['code' => '1.1',   'description' => 'Ativo Circulante',             'chart_type' => 'Ativo'],
             ['code' => '1.1.1', 'description' => 'Caixas e Equivalentes',        'chart_type' => 'Ativo'],
@@ -43,15 +51,39 @@ public function run(): void
             ['code' => '4.2.2', 'description' => 'Taxas Bancárias',              'chart_type' => 'Despesas'],
         ];
 
+        // code => id, para resolver o parent_id ao criar as filhas.
+        $idByCode = [];
+
+        // Raízes primeiro (parent_id null).
+        foreach ($roots as $root) {
+            $model = FinancialPlanAccount::updateOrCreate(
+                ['code' => $root['code'], 'unit_id' => null],
+                [
+                    'parent_id'   => null,
+                    'order'       => $root['code'],
+                    'description' => $root['description'],
+                    'chart_type'  => $root['chart_type'],
+                ],
+            );
+            $idByCode[$root['code']] = $model->id;
+        }
+
+        // Demais contas — pai = código sem o último segmento (ex.: 1.1.1 -> 1.1).
         foreach ($accounts as $account) {
-            FinancialPlanAccount::firstOrCreate(
+            $segments = explode('.', $account['code']);
+            array_pop($segments);
+            $parentCode = implode('.', $segments);
+
+            $model = FinancialPlanAccount::updateOrCreate(
                 ['code' => $account['code'], 'unit_id' => null],
                 [
-                    'order' => $account['code'],
+                    'parent_id'   => $idByCode[$parentCode] ?? null,
+                    'order'       => $account['code'],
                     'description' => $account['description'],
-                    'chart_type' => $account['chart_type'],
+                    'chart_type'  => $account['chart_type'],
                 ],
             );
+            $idByCode[$account['code']] = $model->id;
         }
     }
 }