2 Komitmen bad55403b1 ... b17d87b70f

Pembuat SHA1 Pesan Tanggal
  ebagabee b17d87b70f feat: integração subcontas white-label Asaas (Phase 2) 1 Minggu lalu
  ebagabee 6fe5f8a8cd feat: fundação da integração com Asaas (Phase 1) 1 Minggu lalu

+ 3 - 0
.env.example

@@ -64,3 +64,6 @@ AWS_BUCKET=
 AWS_URL=https://
 AWS_USE_PATH_STYLE_ENDPOINT=false
 
+ASAAS_BASE_URL=https://sandbox.asaas.com/api/v3
+ASAAS_API_KEY=
+ASAAS_WEBHOOK_TOKEN=

+ 51 - 0
app/Console/Commands/AsaasSmokeTestCommand.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Services\Integrations\Asaas\AsaasClient;
+use Illuminate\Console\Command;
+use Illuminate\Support\Str;
+
+class AsaasSmokeTestCommand extends Command
+{
+    protected $signature = 'asaas:smoke-test';
+    protected $description = 'Executa um teste de fumaça criando um cliente e uma cobrança no Asaas Sandbox';
+
+    public function handle()
+    {
+        $this->info('Iniciando teste de fumaça com o Asaas...');
+
+        $client = new AsaasClient();
+
+        try {
+            $this->info('1. Criando Customer de teste...');
+            $customer = $client->post('/customers', [
+                'name' => 'Teste Fumaça ' . Str::random(5),
+                'cpfCnpj' => '12345678909', // CPF matematicamente válido para testes
+                'email' => 'teste@ginasticacerebro.com.br',
+            ]);
+
+            $customerId = $customer['id'];
+            $this->info("Customer criado com sucesso: $customerId");
+
+            $this->info('2. Criando Cobrança (Payment) PIX...');
+            $payment = $client->post('/payments', [
+                'customer' => $customerId,
+                'billingType' => 'PIX',
+                'value' => 10.50,
+                'dueDate' => now()->addDays(5)->format('Y-m-d'),
+                'description' => 'Cobrança Teste de Fumaça',
+            ]);
+
+            $this->info('Cobrança criada com sucesso: ' . $payment['id']);
+            $this->info('Link de pagamento: ' . $payment['invoiceUrl']);
+
+            $this->info('Teste finalizado com sucesso! 🎉');
+        } catch (\Exception $e) {
+            $this->error('Falha no teste: ' . $e->getMessage());
+            return Command::FAILURE;
+        }
+
+        return Command::SUCCESS;
+    }
+}

+ 22 - 0
app/Exceptions/AsaasException.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Exceptions;
+
+use Exception;
+use Illuminate\Http\Client\Response;
+
+class AsaasException extends Exception
+{
+    public array $errors;
+
+    public function __construct(Response $response)
+    {
+        $status = $response->status();
+        $body = $response->json();
+
+        $this->errors = $body['errors'] ?? [];
+        $message = "Asaas API Error ($status): " . collect($this->errors)->pluck('description')->join(', ');
+
+        parent::__construct($message, $status);
+    }
+}

+ 21 - 0
app/Http/Controllers/UnitController.php

@@ -82,4 +82,25 @@ public function selectList(): JsonResponse
 
         return $this->successResponse(payload: $items);
     }
+
+    public function onboardAsaas(int $id, \App\Services\Integrations\Asaas\AsaasAccountService $asaasService): JsonResponse
+    {
+        $unit = $this->service->findById($id);
+
+        try {
+            $account = $asaasService->ensureSubaccount($unit);
+            
+            return $this->successResponse(
+                payload: $account,
+                message: 'Subconta do Asaas ativada com sucesso.',
+                code: 200
+            );
+            
+        } catch (\Exception $e) {
+            return $this->errorResponse(
+                message: 'Falha ao ativar subconta: ' . $e->getMessage(),
+                code: 400
+            );
+        }
+    }
 }

+ 5 - 0
app/Models/Unit.php

@@ -104,4 +104,9 @@ public function groups(): BelongsToMany
     {
         return $this->belongsToMany(Group::class, 'group_units');
     }
+
+    public function paymentAccount(): \Illuminate\Database\Eloquent\Relations\HasOne
+    {
+        return $this->hasOne(UnitPaymentAccount::class, 'unit_id');
+    }
 }

+ 26 - 0
app/Models/UnitPaymentAccount.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+class UnitPaymentAccount extends Model
+{
+    use HasFactory;
+
+    protected $table = 'unit_payment_accounts';
+
+    protected $guarded = ['id'];
+
+    protected $casts = [
+        'onboarded_at' => 'datetime',
+        'asaas_api_key' => 'encrypted', 
+    ];
+
+    public function unit(): BelongsTo
+    {
+        return $this->belongsTo(Unit::class);
+    }
+}

+ 55 - 0
app/Services/Integrations/Asaas/AsaasAccountService.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Services\Integrations\Asaas;
+
+use App\Models\Unit;
+use App\Models\UnitPaymentAccount;
+use Exception;
+
+class AsaasAccountService
+{
+    protected AsaasClient $client;
+
+    public function __construct(AsaasClient $client)
+    {
+        $this->client = $client;
+    }
+
+    public function ensureSubaccount(Unit $unit): UnitPaymentAccount
+    {
+        $existingAccount = UnitPaymentAccount::where('unit_id', $unit->id)->first();
+       
+        if ($existingAccount && $existingAccount->asaas_account_id) {
+            return $existingAccount;
+        }
+
+        if (empty($unit->cnpj)) {
+            throw new Exception("A unidade {$unit->fantasy_name} não possui um CNPJ cadastrado. O CNPJ é obrigatório para criar a subconta.");
+        }
+
+        $payload = [
+            'name' => $unit->fantasy_name ?? $unit->social_reason ?? 'Unidade Ginástica do Cérebro',
+            'email' => $unit->email,
+            'cpfCnpj' => preg_replace('/[^0-9]/', '', $unit->cnpj),
+            'mobilePhone' => preg_replace('/[^0-9]/', '', $unit->cell_number ?? $unit->phone_number ?? ''),
+            'address' => $unit->street,
+            'addressNumber' => $unit->address_number ?? 'S/N',
+            'province' => $unit->neighborhood,
+            'postalCode' => preg_replace('/[^0-9]/', '', $unit->postal_code),
+            'companyType' => 'LIMITED',
+        ];
+
+        $response = $this->client->post('/accounts', $payload);
+
+        return UnitPaymentAccount::updateOrCreate(
+            ['unit_id' => $unit->id],
+            [
+                'asaas_account_id' => $response['id'],
+                'asaas_wallet_id' => $response['walletId'],
+                'asaas_api_key' => $response['apiKey'],
+                'status' => 'ACTIVE',
+                'onboarded_at' => now(),
+            ]
+        );
+    }
+}

+ 49 - 0
app/Services/Integrations/Asaas/AsaasClient.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Services\Integrations\Asaas;
+
+use App\Exceptions\AsaasException;
+use Illuminate\Http\Client\PendingRequest;
+use Illuminate\Support\Facades\Http;
+
+class AsaasClient
+{
+    protected string $baseUrl;
+    protected string $apiKey;
+
+    public function __construct(?string $apiKey = null)
+    {
+        $this->baseUrl = config('services.asaas.base_url');
+        // Allows overriding the API key for subaccounts
+        $this->apiKey = $apiKey ?? config('services.asaas.api_key');
+    }
+
+    protected function request(): PendingRequest
+    {
+        return Http::withHeaders([
+            'access_token' => $this->apiKey,
+        ])->baseUrl($this->baseUrl);
+    }
+
+    public function get(string $endpoint, array $query = [])
+    {
+        $response = $this->request()->get($endpoint, $query);
+
+        if ($response->failed()) {
+            throw new AsaasException($response);
+        }
+
+        return $response->json();
+    }
+
+    public function post(string $endpoint, array $data = [])
+    {
+        $response = $this->request()->post($endpoint, $data);
+
+        if ($response->failed()) {
+            throw new AsaasException($response);
+        }
+
+        return $response->json();
+    }
+}

+ 6 - 0
config/services.php

@@ -35,4 +35,10 @@
         ],
     ],
 
+    'asaas' => [
+        'base_url' => env('ASAAS_BASE_URL', 'https://sandbox.asaas.com/api/v3'),
+        'api_key' => env('ASAAS_API_KEY'),
+        'webhook_token' => env('ASAAS_WEBHOOK_TOKEN'),
+    ],
+
 ];

+ 33 - 0
database/migrations/2026_06_03_173041_create_unit_payment_accounts_table.php

@@ -0,0 +1,33 @@
+<?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::create('unit_payment_accounts', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('unit_id')->constrained('units')->onDelete('cascade');
+            $table->string('asaas_account_id')->nullable();
+            $table->string('asaas_wallet_id')->nullable();
+            $table->text('asaas_api_key')->nullable();
+            $table->string('status')->default('PENDING');
+            $table->timestamp('onboarded_at')->nullable();
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('unit_payment_accounts');
+    }
+};

+ 3 - 0
routes/authRoutes/unit.php

@@ -23,6 +23,9 @@
     Route::get('/{id}', 'show')
         ->middleware('permission:unit,view');
 
+    Route::post('/{id}/asaas-onboard', 'onboardAsaas')
+        ->middleware('permission:unit,edit');
+
     Route::put('/{id}', 'update')
         ->middleware('permission:unit,edit');