Răsfoiți Sursa

feat(financial): generate Asaas charges and lock paid status

ebagabee 5 zile în urmă
părinte
comite
130e983194

+ 13 - 0
app/Http/Controllers/FranchiseeAccountReceiveController.php

@@ -64,6 +64,19 @@ public function settle(SettlePayableRequest $request, int $id): JsonResponse
         );
     }
 
+    public function charge(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->generateCharge($item)),
+            message: 'Cobrança gerada no Asaas',
+        );
+    }
+
     public function reopen(int $id): JsonResponse
     {
         $item = FranchiseeAccountReceive::find($id);

+ 2 - 0
app/Http/Resources/FranchiseeAccountReceiveResource.php

@@ -32,6 +32,8 @@ public function toArray(Request $request): array
             'fees' => $this->fees,
             'obs' => $this->obs,
             'asaas_id' => $this->asaas_id,
+            'invoice_url' => $this->invoice_url,
+            'asaas_status' => $this->asaas_status,
             'status' => $this->status,
             'payment_date' => $this->payment_date?->format('Y-m-d'),
             'details' => $this->whenLoaded('details', fn () => $this->details->map(fn ($d) => [

+ 1 - 0
app/Http/Resources/UnitAccountPayableResource.php

@@ -22,6 +22,7 @@ public function toArray(Request $request): array
             'payment_date'                  => $this->payment_date?->format('Y-m-d'),
             'status'                        => $this->status,
             'obs'                           => $this->obs,
+            'invoice_url'                   => $this->franchiseeAccountReceive?->invoice_url,
         ];
     }
 }

+ 7 - 6
app/Jobs/ProcessAsaasWebhookJob.php

@@ -101,9 +101,10 @@ public function handle(NotificationService $notifications): void
         $currentStatus = $receive->status instanceof ReceivableStatus ? $receive->status->value : $receive->status;
         $newStatusValue = $newStatus instanceof ReceivableStatus ? $newStatus->value : $newStatus;
 
-        // Idempotência
-        if ($currentStatus === ReceivableStatus::PAID->value && $newStatusValue === ReceivableStatus::PAID->value) {
-            Log::info("Asaas Webhook Job: registro {$type} #{$receive->id} já está pago. Ignorando duplicata.");
+        // Pagamento é um estado terminal: eventos posteriores do Asaas não podem
+        // desfazer uma baixa manual ou automática já confirmada.
+        if ($currentStatus === ReceivableStatus::PAID->value) {
+            Log::info("Asaas Webhook Job: registro {$type} #{$receive->id} já está pago. Ignorando alteração de status.");
 
             return;
         }
@@ -186,9 +187,9 @@ private function handleStudentInstallment(string $externalReference, array $paym
         ];
         $installmentStatus = $statusMap[$newStatus->value] ?? $installment->status;
 
-        // Idempotência: já pago e continua pago → ignora duplicata.
-        if ($installment->status === 'paid' && $installmentStatus === 'paid') {
-            Log::info("Asaas Webhook Job: parcela #{$installment->id} já paga. Ignorando duplicata.");
+        // Pagamento é um estado terminal, inclusive quando a baixa foi manual.
+        if ($installment->status === 'paid') {
+            Log::info("Asaas Webhook Job: parcela #{$installment->id} já paga. Ignorando alteração de status.");
 
             return;
         }

+ 2 - 12
app/Services/CompanyAccountPayableService.php

@@ -76,18 +76,8 @@ public function settle(CompanyAccountPayable $payable, array $data): CompanyAcco
 
     public function reopen(CompanyAccountPayable $payable): CompanyAccountPayable
     {
-        if ($payable->status !== 'paid') {
-            throw ValidationException::withMessages([
-                'status' => 'Só é possível reabrir contas que estão pagas.',
-            ]);
-        }
-
-        $payable->update([
-            'status'       => 'pending',
-            'paid_value'   => 0,
-            'payment_date' => null,
+        throw ValidationException::withMessages([
+            'status' => 'Uma conta paga não pode voltar para um status anterior.',
         ]);
-
-        return $payable->fresh('planAccount:id,code,description');
     }
 }

+ 39 - 12
app/Services/FranchisorReceivableService.php

@@ -3,9 +3,12 @@
 namespace App\Services;
 
 use App\Enums\NotificationType;
+use App\Jobs\SyncFranchiseeChargeJob;
+use App\Models\CompanyPaymentAccount;
 use App\Models\FranchiseeAccountReceive;
 use App\Models\UnitAccountPayable;
 use Carbon\Carbon;
+use Exception;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Validation\ValidationException;
 
@@ -107,23 +110,47 @@ public function settle(FranchiseeAccountReceive $receivable, array $data): Franc
         });
     }
 
-    public function reopen(FranchiseeAccountReceive $receivable): FranchiseeAccountReceive
+    public function generateCharge(FranchiseeAccountReceive $receivable): FranchiseeAccountReceive
     {
-        if ($receivable->status->value !== 'paid') {
-            throw ValidationException::withMessages(['status' => 'Esta conta não está paga.']);
+        if (! $receivable->unit_id) {
+            throw ValidationException::withMessages([
+                'unit' => 'Cobranças Asaas só podem ser geradas para contas vinculadas a uma unidade.',
+            ]);
         }
 
-        return DB::transaction(function () use ($receivable) {
-            $receivable->update([
-                'status' => 'pending',
-                'paid_value' => 0,
-                'payment_date' => null,
+        if (in_array($receivable->status->value, ['paid', 'cancelled'], true)) {
+            throw ValidationException::withMessages([
+                'status' => 'Contas pagas ou canceladas não podem gerar cobrança.',
             ]);
+        }
 
-            UnitAccountPayable::where('franchisee_account_receive_id', $receivable->id)
-                ->update(['status' => 'pending', 'paid_value' => 0, 'payment_date' => null]);
+        if ($receivable->asaas_id) {
+            throw ValidationException::withMessages([
+                'asaas' => 'Esta conta já possui cobrança gerada.',
+            ]);
+        }
 
-            return $receivable->fresh(['unit', 'details', 'planAccount']);
-        });
+        if (empty(CompanyPaymentAccount::resolveApiKey())) {
+            throw ValidationException::withMessages([
+                'asaas' => 'A franqueadora não possui chave Asaas configurada.',
+            ]);
+        }
+
+        try {
+            SyncFranchiseeChargeJob::dispatchSync($receivable);
+        } catch (Exception) {
+            throw ValidationException::withMessages([
+                'asaas' => 'Não foi possível gerar a cobrança na Asaas. Tente novamente.',
+            ]);
+        }
+
+        return $receivable->fresh(['unit', 'details', 'planAccount']);
+    }
+
+    public function reopen(FranchiseeAccountReceive $receivable): FranchiseeAccountReceive
+    {
+        throw ValidationException::withMessages([
+            'status' => 'Uma conta paga não pode voltar para um status anterior.',
+        ]);
     }
 }

+ 4 - 13
app/Services/UnitAccountPayableService.php

@@ -18,7 +18,8 @@ class UnitAccountPayableService
 {
     public function list(int $unitId, array $filters = []): Collection
     {
-        return UnitAccountPayable::where('unit_id', $unitId)
+        return UnitAccountPayable::with('franchiseeAccountReceive')
+            ->where('unit_id', $unitId)
             ->when(
                 isset($filters['status']),
                 fn ($q) => $q->where('status', $filters['status']),
@@ -94,18 +95,8 @@ public function settle(UnitAccountPayable $payable, array $data): UnitAccountPay
      */
     public function reopen(UnitAccountPayable $payable): UnitAccountPayable
     {
-        if ($payable->status !== 'paid') {
-            throw ValidationException::withMessages([
-                'status' => 'Só é possível reabrir contas que estão pagas.',
-            ]);
-        }
-
-        $payable->update([
-            'status'       => 'pending',
-            'paid_value'   => 0,
-            'payment_date' => null,
+        throw ValidationException::withMessages([
+            'status' => 'Uma conta paga não pode voltar para um status anterior.',
         ]);
-
-        return $payable->fresh();
     }
 }

+ 4 - 24
app/Services/UnitReceivableService.php

@@ -125,19 +125,9 @@ public function generateCharge(StudentContractInstallment $installment): Student
      */
     public function reopen(StudentContractInstallment $installment): StudentContractInstallment
     {
-        if ($installment->status !== 'paid') {
-            throw ValidationException::withMessages([
-                'status' => 'Só é possível reabrir parcelas que estão pagas.',
-            ]);
-        }
-
-        $installment->update([
-            'status'       => 'pending',
-            'paid_value'   => 0,
-            'payment_date' => null,
+        throw ValidationException::withMessages([
+            'status' => 'Uma parcela paga não pode voltar para um status anterior.',
         ]);
-
-        return $installment->fresh(['student']);
     }
 
     // ------------------------------------------------------------------
@@ -218,18 +208,8 @@ public function settleManual(UnitAccountReceivable $receivable, array $data): Un
 
     public function reopenManual(UnitAccountReceivable $receivable): UnitAccountReceivable
     {
-        if ($receivable->status !== 'paid') {
-            throw ValidationException::withMessages([
-                'status' => 'Só é possível reabrir recebíveis que estão pagos.',
-            ]);
-        }
-
-        $receivable->update([
-            'status'       => 'pending',
-            'paid_value'   => 0,
-            'payment_date' => null,
+        throw ValidationException::withMessages([
+            'status' => 'Um recebível pago não pode voltar para um status anterior.',
         ]);
-
-        return $receivable->fresh('student');
     }
 }

+ 1 - 0
routes/authRoutes/franchisor_financial.php

@@ -35,6 +35,7 @@
 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}/charge', 'charge')->whereNumber('id')->middleware('permission:franchisor_financial,edit');
     Route::post('/{id}/settle', 'settle')->whereNumber('id')->middleware('permission:franchisor_financial,edit');
     Route::post('/{id}/reopen', 'reopen')->whereNumber('id')->middleware('permission:franchisor_financial,edit');
 });