Browse Source

feat(asaas): cobrança da parcela do aluno na conta Asaas da própria unidade

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ebagabee 4 tuần trước cách đây
mục cha
commit
21de89e46d

+ 83 - 0
app/Jobs/SyncStudentChargeJob.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Models\StudentContractInstallment;
+use App\Models\UnitPaymentAccount;
+use App\Services\Integrations\Asaas\AsaasClient;
+use App\Services\Integrations\Asaas\AsaasCustomerService;
+use Exception;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * Emite a cobrança de uma parcela de aluno na conta Asaas PRÓPRIA da unidade.
+ *
+ * Asaas é opcional: se a unidade não tiver chave, o job apenas registra e sai —
+ * a parcela segue para baixa manual. externalReference = STU_{id}.
+ */
+class SyncStudentChargeJob implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public StudentContractInstallment $installment;
+
+    public function __construct(StudentContractInstallment $installment)
+    {
+        $this->installment = $installment;
+    }
+
+    public function handle(): void
+    {
+        $installment = $this->installment->fresh(['student']);
+
+        if (!$installment || $installment->status === 'cancelled') {
+            return;
+        }
+
+        if ($installment->asaas_id) {
+            Log::info("Parcela #{$installment->id} já possui cobrança Asaas. Ignorando.");
+            return;
+        }
+
+        $account = UnitPaymentAccount::where('unit_id', $installment->unit_id)->first();
+
+        if (!$account || empty($account->asaas_api_key)) {
+            Log::info("Unidade {$installment->unit_id} sem Asaas. Parcela #{$installment->id} segue para baixa manual.");
+            return;
+        }
+
+        $client          = new AsaasClient($account->asaas_api_key);
+        $customerService  = new AsaasCustomerService($client);
+
+        try {
+            $customerId = $customerService->ensureStudentCustomer($installment->student);
+
+            $response = $client->post('/payments', [
+                'customer' => $customerId,
+                'billingType' => 'UNDEFINED',
+                'value' => (float) $installment->value,
+                'dueDate' => $installment->due_date->format('Y-m-d'),
+                'description' => trim(($installment->history ?? 'Mensalidade') . ' ' . $installment->order),
+                'externalReference' => "STU_{$installment->id}",
+            ]);
+
+            $installment->update([
+                'asaas_customer_id' => $customerId,
+                'asaas_id'          => $response['id'],
+                'invoice_url'       => $response['invoiceUrl'] ?? null,
+                'billing_type'      => $response['billingType'] ?? 'UNDEFINED',
+                'asaas_status'      => $response['status'] ?? null,
+            ]);
+
+            Log::info("Cobrança Asaas gerada para parcela #{$installment->id}.");
+        } catch (Exception $e) {
+            Log::error("Erro ao gerar cobrança Asaas da parcela #{$installment->id}: " . $e->getMessage());
+            throw $e;
+        }
+    }
+}

+ 40 - 0
app/Services/Integrations/Asaas/AsaasCustomerService.php

@@ -2,6 +2,7 @@
 
 namespace App\Services\Integrations\Asaas;
 
+use App\Models\Student;
 use App\Models\Unit;
 use Exception;
 
@@ -54,4 +55,43 @@ public function ensureFranchiseeCustomer(Unit $unit): string
 
         return $response['id'];
     }
+
+    /**
+     * Garante que o Aluno exista como Customer na conta Asaas da unidade.
+     * Retorna o ID do cliente no Asaas (cus_xxxxxx).
+     */
+    public function ensureStudentCustomer(Student $student): string
+    {
+        $cpfCnpj = preg_replace('/[^0-9]/', '', $student->document_number ?? '');
+
+        if (empty($cpfCnpj)) {
+            throw new Exception("Aluno {$student->name} não possui CPF para ser cobrado.");
+        }
+
+        $existing = $this->client->get('/customers', ['cpfCnpj' => $cpfCnpj]);
+
+        if (isset($existing['data']) && count($existing['data']) > 0) {
+            return $existing['data'][0]['id'];
+        }
+
+        $payload = [
+            'name' => $student->payer_name ?? $student->name,
+            'cpfCnpj' => $cpfCnpj,
+            'email' => $student->email,
+            'postalCode' => preg_replace('/[^0-9]/', '', $student->postal_code ?? ''),
+            'address' => $student->street,
+            'addressNumber' => $student->address_number ?? 'S/N',
+            'province' => $student->neighborhood,
+        ];
+
+        $mobilePhone = preg_replace('/[^0-9]/', '', $student->phone ?? '');
+
+        if (strlen($mobilePhone) >= 10 && strlen($mobilePhone) <= 11) {
+            $payload['mobilePhone'] = $mobilePhone;
+        }
+
+        $response = $this->client->post('/customers', $payload);
+
+        return $response['id'];
+    }
 }