Переглянути джерело

feat: motor de cobrança TBR via Asaas (Phase 3)

ebagabee 1 тиждень тому
батько
коміт
7eec42feee

+ 12 - 0
app/Enums/ReceivableStatus.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace App\Enums;
+
+enum ReceivableStatus: string
+{
+    case PENDING = 'pending';
+    case AWAITING_PAYMENT = 'awaiting_payment';
+    case PAID = 'paid';
+    case OVERDUE = 'overdue';
+    case CANCELLED = 'cancelled';
+}

+ 75 - 0
app/Jobs/SyncFranchiseeChargeJob.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Models\FranchiseeAccountReceive;
+use App\Enums\ReceivableStatus;
+use App\Services\Integrations\Asaas\AsaasClient;
+use App\Services\Integrations\Asaas\AsaasCustomerService;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Exception;
+use Illuminate\Support\Facades\Log;
+
+class SyncFranchiseeChargeJob implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public $receive;
+
+    /**
+     * Create a new job instance.
+     */
+    public function __construct(FranchiseeAccountReceive $receive)
+    {
+        $this->receive = $receive;
+    }
+
+    /**
+     * Execute the job.
+     */
+    public function handle(AsaasClient $client, AsaasCustomerService $customerService): void
+    {
+        // 1. Garantir que a Unidade é um Customer na conta-mãe do Asaas
+        $unit = $this->receive->unit;
+        if (!$unit) {
+            Log::error("Recebível #{$this->receive->id} sem Unidade vinculada. Abortando integração.");
+            return;
+        }
+
+        try {
+            $asaasCustomerId = $customerService->ensureFranchiseeCustomer($unit);
+            $this->receive->update(['asaas_customer_id' => $asaasCustomerId]);
+            
+            // 2. Gerar a cobrança no Asaas
+            $payload = [
+                'customer' => $asaasCustomerId,
+                'billingType' => 'UNDEFINED', // Deixa o pagador escolher PIX/Boleto
+                'value' => (float) $this->receive->value,
+                'dueDate' => $this->receive->due_date->format('Y-m-d'),
+                'description' => $this->receive->history ?? 'Cobrança Ginástica do Cérebro (TBR)',
+                'externalReference' => (string) $this->receive->id, // Para conciliarmos no webhook depois
+            ];
+
+            $response = $client->post('/payments', $payload);
+
+            // 3. Atualiza o banco com o link do boleto
+            $this->receive->update([
+                'asaas_id' => $response['id'],
+                'invoice_url' => $response['invoiceUrl'],
+                'billing_type' => $response['billingType'] ?? 'UNDEFINED',
+                'status' => ReceivableStatus::AWAITING_PAYMENT,
+                'asaas_status' => $response['status'],
+            ]);
+
+            Log::info("Cobrança do Asaas gerada com sucesso para Recebível #{$this->receive->id}");
+
+        } catch (Exception $e) {
+            Log::error("Erro ao gerar cobrança no Asaas para Recebível #{$this->receive->id}: " . $e->getMessage());
+            throw $e; // Re-joga a exceção para que o Job tente novamente depois
+        }
+    }
+}

+ 4 - 2
app/Models/FranchiseeAccountReceive.php

@@ -21,8 +21,10 @@ class FranchiseeAccountReceive extends Model
         'paid_value' => 'decimal:2',
         'discount'   => 'decimal:2',
         'fees'       => 'decimal:2',
-        'due_date'   => 'date',
-        'created_at' => 'datetime',
+        'due_date'     => 'date',
+        'payment_date' => 'datetime',
+        'status'       => \App\Enums\ReceivableStatus::class,
+        'created_at'   => 'datetime',
         'updated_at' => 'datetime',
     ];
 

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

@@ -0,0 +1,52 @@
+<?php
+
+namespace App\Services\Integrations\Asaas;
+
+use App\Models\Unit;
+use Exception;
+
+class AsaasCustomerService
+{
+    protected AsaasClient $client;
+
+    public function __construct(AsaasClient $client)
+    {
+        $this->client = $client;
+    }
+
+    /**
+     * Garante que a Franquia exista como Customer (Cliente) na conta-mãe.
+     * Retorna o ID do cliente no Asaas (cus_xxxxxx).
+     */
+    public function ensureFranchiseeCustomer(Unit $unit): string
+    {
+        $cpfCnpj = preg_replace('/[^0-9]/', '', $unit->cnpj);
+
+        if (empty($cpfCnpj)) {
+            throw new Exception("Unidade {$unit->fantasy_name} não possui CNPJ para ser cobrada.");
+        }
+
+        // 1. Tenta buscar no Asaas se já existe o customer com esse CNPJ
+        $existing = $this->client->get('/customers', ['cpfCnpj' => $cpfCnpj]);
+
+        if (isset($existing['data']) && count($existing['data']) > 0) {
+            return $existing['data'][0]['id'];
+        }
+
+        // 2. Se não existe, cria um novo
+        $payload = [
+            'name' => $unit->social_reason ?? $unit->fantasy_name ?? 'Franquia',
+            'cpfCnpj' => $cpfCnpj,
+            'email' => $unit->email,
+            'mobilePhone' => preg_replace('/[^0-9]/', '', $unit->cell_number ?? $unit->phone_number ?? ''),
+            'postalCode' => preg_replace('/[^0-9]/', '', $unit->postal_code),
+            'address' => $unit->street,
+            'addressNumber' => $unit->address_number ?? 'S/N',
+            'province' => $unit->neighborhood,
+        ];
+
+        $response = $this->client->post('/customers', $payload);
+
+        return $response['id'];
+    }
+}

+ 2 - 0
app/Services/TbrCalculationService.php

@@ -357,6 +357,8 @@ private function buildReceivable(TbrCalculation $calculation, ?FranchiseeContrac
 
         $calculation->update(['receivable_generated' => true]);
 
+        \App\Jobs\SyncFranchiseeChargeJob::dispatch($receive);
+
         return $receive->load('details');
     }
 

+ 38 - 0
database/migrations/2026_06_03_182336_add_asaas_columns_to_franchisee_account_receives_table.php

@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('franchisee_account_receives', function (Blueprint $table) {
+            $table->string('billing_type')->nullable()->after('due_date');
+            $table->string('asaas_status')->nullable()->after('billing_type');
+            $table->string('invoice_url')->nullable()->after('asaas_status');
+            $table->timestamp('payment_date')->nullable()->after('invoice_url');
+            $table->string('asaas_customer_id')->nullable()->after('payment_date');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('franchisee_account_receives', function (Blueprint $table) {
+            $table->dropColumn([
+                'billing_type',
+                'asaas_status',
+                'invoice_url',
+                'payment_date',
+                'asaas_customer_id',
+            ]);
+        });
+    }
+};