Просмотр исходного кода

refactor: idempotency keys + comando para gerar refresh dos dados pagarme (fase de teste apenas)

Gustavo Mantovani 7 часов назад
Родитель
Сommit
a8b24778e1

+ 204 - 0
app/Commands/RefreshPagarmeEntities.php

@@ -0,0 +1,204 @@
+<?php
+
+namespace App\Commands;
+
+use App\Models\Address;
+use App\Models\Client;
+use App\Models\Provider;
+use App\Services\Pagarme\PagarmeCustomerService;
+use App\Services\Pagarme\PagarmeRecipientService;
+use Illuminate\Console\Command;
+
+class RefreshPagarmeEntities extends Command
+{
+    protected $signature = 'pagarme:refresh-entities';
+
+    protected $description = 'Limpa os recipients e clients locais e os recria no Pagar.me com os dados do banco local';
+
+    public function __construct(
+        protected PagarmeRecipientService $recipientService,
+        protected PagarmeCustomerService $customerService,
+    ) {
+        parent::__construct();
+    }
+
+    public function handle(): int
+    {
+        $this->warn('ATENCAO: Este comando vai limpar os IDs de gateway dos recipients (providers) e customers (clients)');
+        $this->warn('e recria-los no Pagar.me com os dados do banco local. Os registros antigos no Pagar.me');
+        $this->warn('ficarao orfaos (nao serao deletados).');
+        $this->newLine();
+
+        $providers = Provider::query()
+            ->whereNotNull('recipient_id')
+            ->count();
+
+        $clients = Client::query()
+            ->whereNotNull('gateway_customer_id')
+            ->count();
+
+        $this->info("Providers com recipient_id: {$providers}");
+        $this->info("Clients com gateway_customer_id: {$clients}");
+        $this->newLine();
+
+        if ($providers + $clients === 0) {
+            $this->info('Nenhum registro para processar.');
+
+            return Command::SUCCESS;
+        }
+
+        if (! $this->confirm('Deseja continuar com a limpeza e recriacao?')) {
+            $this->info('Operacao cancelada.');
+
+            return Command::SUCCESS;
+        }
+
+        $this->newLine();
+
+        if ($providers > 0) {
+            $this->processProviders();
+        }
+
+        if ($clients > 0) {
+            $this->processClients();
+        }
+
+        $this->newLine();
+        $this->info('Operacao concluida.');
+
+        return Command::SUCCESS;
+    }
+
+    private function processProviders(): void
+    {
+        $this->info('--- Processando providers ---');
+        $this->newLine();
+
+        $providers = Provider::query()
+            ->whereNotNull('recipient_id')
+            ->with(['user', 'addresses.city.state', 'addresses.state'])
+            ->get();
+
+        $bar = $this->output->createProgressBar($providers->count());
+        $bar->start();
+
+        foreach ($providers as $provider) {
+            $address = $provider->addresses->where('is_primary', true)->first()
+                ?? $provider->addresses->first();
+
+            $data = [
+                'recipient_name'                 => $provider->recipient_name,
+                'recipient_email'                => $provider->recipient_email,
+                'recipient_document'             => $provider->recipient_document,
+                'recipient_type'                 => $provider->recipient_type,
+                'recipient_payment_mode'         => $provider->recipient_payment_mode,
+                'recipient_description'          => $provider->recipient_description,
+                'recipient_metadata'             => $provider->recipient_metadata ?? [],
+                'recipient_default_bank_account' => $provider->recipient_default_bank_account ?? [],
+                'birth_date'                     => $provider->birth_date?->format('Y-m-d'),
+                'professional_occupation'        => 'autonomo',
+                'phone'                          => $provider->user?->phone,
+                'address'                        => $address?->address ?? '',
+                'number'                         => $address?->number ?? '',
+                'district'                       => $address?->district ?? '',
+                'city'                           => $address?->city?->name ?? '',
+                'state'                          => $address?->state?->code ?? '',
+                'zip_code'                       => $address?->zip_code ?? '',
+                'complement'                     => $address?->complement ?? '',
+            ];
+
+            $provider->forceFill([
+                'recipient_id'    => null,
+                'idempotency_key' => null,
+                'recipient_code'  => null,
+            ])->save();
+
+            try {
+                $this->recipientService->createRecipientForProvider($provider, $data);
+            } catch (\Throwable $e) {
+                $this->newLine();
+                $this->error("Erro ao recriar recipient do provider {$provider->id}: {$e->getMessage()}");
+
+                $provider->forceFill([
+                    'recipient_id'    => null,
+                    'idempotency_key' => null,
+                    'recipient_code'  => null,
+                ])->save();
+            }
+
+            $bar->advance();
+        }
+
+        $bar->finish();
+        $this->newLine();
+        $this->newLine();
+    }
+
+    private function processClients(): void
+    {
+        $this->info('--- Processando clients ---');
+        $this->newLine();
+
+        $clients = Client::query()
+            ->whereNotNull('gateway_customer_id')
+            ->with('user')
+            ->get();
+
+        $bar = $this->output->createProgressBar($clients->count());
+        $bar->start();
+
+        foreach ($clients as $client) {
+            $address = Address::query()
+                ->with(['city', 'state'])
+                ->where('source', 'client')
+                ->where('source_id', $client->id)
+                ->where('is_primary', true)
+                ->first()
+                ?? Address::query()
+                    ->with(['city', 'state'])
+                    ->where('source', 'client')
+                    ->where('source_id', $client->id)
+                    ->first();
+
+            $data = [
+                'name'       => $client->user?->name,
+                'email'      => $client->user?->email,
+                'phone'      => $client->user?->phone,
+                'document'   => $client->document,
+                'address'    => $address?->address ?? '',
+                'number'     => $address?->number ?? '',
+                'district'   => $address?->district ?? '',
+                'city'       => $address?->city?->name ?? '',
+                'state'      => $address?->state?->code ?? '',
+                'zip_code'   => $address?->zip_code ?? '',
+                'complement' => $address?->complement ?? '',
+            ];
+
+            $client->forceFill([
+                'gateway_customer_id'   => null,
+                'gateway_customer_code' => null,
+                'idempotency_key'       => null,
+            ])->save();
+
+            try {
+                $this->customerService->createCustomerForClient($client, $data);
+            } catch (\Throwable $e) {
+                $this->newLine();
+                $this->error("Erro ao recriar customer do client {$client->id}: {$e->getMessage()}");
+
+                $client->forceFill([
+                    'gateway_customer_id'   => null,
+                    'gateway_customer_code' => null,
+                    'idempotency_key'       => null,
+                ])->save();
+            }
+
+            $bar->advance();
+        }
+
+        $bar->finish();
+
+        $this->newLine();
+        $this->newLine();
+    }
+}

+ 9 - 0
app/Exceptions/PaymentMissingDataException.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace App\Exceptions;
+
+use RuntimeException;
+
+class PaymentMissingDataException extends RuntimeException
+{
+}

+ 14 - 1
app/Http/Controllers/PaymentController.php

@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
 
 use App\Exceptions\PaymentFailedException;
 use App\Exceptions\PaymentException;
+use App\Exceptions\PaymentMissingDataException;
 use App\Http\Requests\PayServicePackageRequest;
 use App\Http\Requests\PaymentRequest;
 use App\Http\Resources\PaymentResource;
@@ -74,6 +75,11 @@ class PaymentController extends Controller
                     'card_id' => data_get($validated, 'card_id'),
                 ],
             );
+        } catch (PaymentMissingDataException $e) {
+            return response()->json([
+                'message' => $e->getMessage(),
+                'error'   => 'missing_payment_data',
+            ], 422);
         } catch (PaymentFailedException) {
             return $this->errorResponse(message: __('messages.payment_not_confirmed'), code: 422);
         } catch (PaymentException) {
@@ -95,7 +101,14 @@ class PaymentController extends Controller
             abort(403);
         }
 
-        $item = $this->service->getOrCreateServicePackagePixPayment($servicePackage);
+        try {
+            $item = $this->service->getOrCreateServicePackagePixPayment($servicePackage);
+        } catch (PaymentMissingDataException $e) {
+            return response()->json([
+                'message' => $e->getMessage(),
+                'error'   => 'missing_payment_data',
+            ], 422);
+        }
 
         return $this->successResponse(payload: new PaymentResource($item));
     }

+ 9 - 4
app/Http/Requests/RegisterClientRequest.php

@@ -3,14 +3,20 @@
 namespace App\Http\Requests;
 
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
 
 class RegisterClientRequest extends FormRequest
 {
     public function rules(): array
     {
         return [
-            'code'           => 'required|string|max:6',
-            'email'          => 'required|string|email|max:255',
+            'code' => 'required|string|max:6',
+
+            'email' => [
+                Rule::requiredIf(fn () => ! config('services.sms.enabled') || ! $this->filled('phone')),
+                'string', 'email', 'max:255',
+            ],
+
             'name'           => 'required|string|max:255',
             'phone'          => 'required|string|max:20',
             'document'       => 'required|string|max:20',
@@ -21,8 +27,7 @@ class RegisterClientRequest extends FormRequest
             'city'           => 'required|string|max:255',
             'state'          => 'required|string|max:10',
             'has_complement' => 'required|boolean',
-
-            'complement'     => 'sometimes|nullable|string|max:255',
+            'complement'     => 'required|string|max:255',
             'nickname'       => 'sometimes|nullable|string|max:255',
             'instructions'   => 'sometimes|nullable|string|max:500',
             'address_type'   => 'sometimes|nullable|string|in:home,commercial,other',

+ 9 - 6
app/Http/Requests/RegisterProviderRequest.php

@@ -5,6 +5,7 @@ namespace App\Http\Requests;
 use App\Enums\GenderEnum;
 use App\Enums\WorkingPeriodEnum;
 use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
 
 class RegisterProviderRequest extends FormRequest
 {
@@ -13,15 +14,19 @@ class RegisterProviderRequest extends FormRequest
     $dailyPriceMin = config('provider.daily_price_min');
 
     $rules = [
-      'code'            => 'required|string|max:6',
-      'email'           => 'required|string|email|max:255',
+      'code' => 'required|string|max:6',
+
+      'email' => [
+        Rule::requiredIf(fn () => ! config('services.sms.enabled') || ! $this->filled('phone')),
+        'string', 'email', 'max:255',
+      ],
+
       'name'            => 'required|string|max:255',
       'phone'           => 'required|string|max:20',
       'document'        => 'required|string|max:20',
       'rg'              => 'required|string|max:20',
       'gender'          => 'required|string|in:'.implode(',', GenderEnum::toArray()),
       'birth_date'      => 'required|date|before:today',
-
       'zip_code'        => 'required|string|max:20',
       'number'          => 'required|string|max:20',
       'address'         => 'required|string|max:255',
@@ -30,12 +35,10 @@ class RegisterProviderRequest extends FormRequest
       'state'           => 'required|string|max:2',
       'address_type'    => 'required|string|in:home,commercial,other',
       'has_complement'  => 'required|boolean',
-
       'daily_price_8h'  => "required|numeric|min:{$dailyPriceMin}|max:500",
       'daily_price_6h'  => 'required|numeric|min:0',
       'daily_price_4h'  => 'required|numeric|min:0',
       'daily_price_2h'  => 'required|numeric|min:0',
-
       'selfie'          => 'required|file|image|mimes:jpg,jpeg,png,webp|max:5120',
       'document_front'  => 'required|file|image|mimes:jpg,jpeg,png,webp|max:10240',
       'document_back'   => 'required|file|image|mimes:jpg,jpeg,png,webp|max:10240',
@@ -44,7 +47,7 @@ class RegisterProviderRequest extends FormRequest
       'working_days.*.day'    => 'required|integer|min:0|max:6',
       'working_days.*.period' => 'required|string|in:'.implode(',', array_column(WorkingPeriodEnum::cases(), 'value')),
 
-      'complement'   => 'sometimes|nullable|string|max:255',
+      'complement'   => 'required|string|max:255',
       'nickname'     => 'sometimes|nullable|string|max:255',
       'latitude'     => 'sometimes|nullable|numeric',
       'longitude'    => 'sometimes|nullable|numeric',

+ 2 - 1
app/Models/Schedule.php

@@ -2,6 +2,7 @@
 
 namespace App\Models;
 
+use App\Exceptions\PaymentMissingDataException;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\SoftDeletes;
@@ -164,7 +165,7 @@ class Schedule extends Model
             return;
         }
 
-        throw new \InvalidArgumentException(
+        throw new PaymentMissingDataException(
             'Voce precisa cadastrar um numero de celular valido no seu perfil para concluir o pagamento.'
         );
     }

+ 2 - 0
app/Services/Pagarme/PagarmeCardService.php

@@ -82,6 +82,8 @@ class PagarmeCardService
 
     // evita criacao duplicada de cartao
 
+    // formato: card-{email}-{ultimos-4-digitos}-{bandeira}
+
     private function idempotencyKey(ClientPaymentMethod $paymentMethod): string
     {
         if (! empty($paymentMethod->idempotency_key)) {

+ 2 - 0
app/Services/Pagarme/PagarmeCustomerService.php

@@ -123,6 +123,8 @@ class PagarmeCustomerService
 
     // evita criacao duplicada de customer
 
+    // formato: customer-{email}-{documento}
+
     private function idempotencyKey(Client $client): string
     {
         if (! empty($client->idempotency_key)) {

+ 10 - 10
app/Services/Pagarme/PagarmePaymentService.php

@@ -728,25 +728,25 @@ class PagarmePaymentService
 
     // evita criacao duplicada de payment
 
+    // formato: order-{data}-{gateway-code-prestador}-{gateway-code-cliente} (tudo minusculo e sem acento)
+
     private function idempotencyKey(Payment $payment): string
     {
         if (! empty($payment->idempotency_key)) {
             return $payment->idempotency_key;
         }
 
-        $payment->loadMissing(['client.user', 'provider.user', 'schedule', 'servicePackage']);
+        $payment->loadMissing(['client', 'provider']);
+
+        $date = $payment->created_at?->format('Y-m-d') ?: now()->format('Y-m-d');
 
-        $amountCents = (int) round((float) $payment->gross_amount * 100);
+        $providerCode = $payment->provider?->ensureGatewayCode() ?: "provider-{$payment->provider_id}";
+        $clientCode   = $payment->client?->ensureGatewayCode() ?: "client-{$payment->client_id}";
 
         $key = $this->pagarmeIdempotencyKey('order', [
-            "payment-{$payment->id}",
-            $payment->service_package_id
-                ? "service-package-{$payment->service_package_id}"
-                : "schedule-{$payment->schedule_id}",
-            $payment->client?->user?->name ?: "client-{$payment->client_id}",
-            $payment->provider?->user?->name ?: ($payment->provider_id ? "provider-{$payment->provider_id}" : 'provider-none'),
-            $payment->payment_method,
-            "amount-{$amountCents}",
+            $date,
+            $providerCode,
+            $clientCode,
         ]);
 
         $payment->forceFill(['idempotency_key' => $key])->save();

+ 2 - 0
app/Services/Pagarme/PagarmeRecipientService.php

@@ -237,6 +237,8 @@ class PagarmeRecipientService
 
     // evita criacao duplica de recipient
 
+    // formato: recipient-{email}-{documento} (tudo minusculo e sem acento)
+
     private function idempotencyKey(Provider $provider, string $suffix = '', array $data = []): string
     {
         $baseKey = $provider->idempotency_key;

+ 2 - 0
app/Services/PaymentService.php

@@ -387,6 +387,8 @@ class PaymentService
         $schedules = data_get($paymentData, 'schedules');
 
         try {
+            $schedules->first()->ensureCustomerPhone();
+
             $orderResponse = $this->pagarmePaymentService->processServicePackagePayment(
                 payment:       $payment,
                 schedules:     $schedules,

+ 1 - 0
lang/en/messages.php

@@ -48,6 +48,7 @@ return [
     'schedule_status_update_failed'                => 'The appointment status could not be updated.',
     'cancellation_not_allowed_for_status'          => 'Cancellation is not allowed for the current status.',
     'payment_error'                                => 'The payment could not be processed.',
+    'missing_payment_data'                         => 'Insufficient data to process payment. Check your profile.',
     'schedule_cancellation_failed'                 => 'The appointment could not be cancelled.',
     'invalid_schedule_period'                      => 'Invalid appointment period.',
     'withdrawal_already_in_progress'               => 'You already have a withdrawal in progress.',

+ 1 - 0
lang/es/messages.php

@@ -48,6 +48,7 @@ return [
     'schedule_status_update_failed'                => 'No fue posible actualizar el estado del servicio programado.',
     'cancellation_not_allowed_for_status'          => 'La cancelación no está permitida para el estado actual.',
     'payment_error'                                => 'No fue posible procesar el pago.',
+    'missing_payment_data'                         => 'Datos insuficientes para procesar el pago. Verifique su perfil.',
     'schedule_cancellation_failed'                 => 'No fue posible cancelar el servicio programado.',
     'invalid_schedule_period'                      => 'Período del servicio programado inválido.',
     'withdrawal_already_in_progress'               => 'Ya tienes un retiro en curso.',

+ 1 - 0
lang/pt/messages.php

@@ -48,6 +48,7 @@ return [
     'schedule_status_update_failed'                => 'Não foi possível atualizar o status do agendamento.',
     'cancellation_not_allowed_for_status'          => 'Cancelamento não permitido para o status atual.',
     'payment_error'                                => 'Não foi possível processar o pagamento.',
+    'missing_payment_data'                         => 'Dados insuficientes para processar o pagamento. Verifique seu cadastro.',
     'schedule_cancellation_failed'                 => 'Não foi possível cancelar o agendamento.',
     'invalid_schedule_period'                      => 'Período do agendamento inválido.',
     'withdrawal_already_in_progress'               => 'Você já possui um saque em andamento.',

+ 5 - 0
routes/console.php

@@ -1,6 +1,7 @@
 <?php
 
 use App\Commands\CreateCrud;
+use App\Commands\RefreshPagarmeEntities;
 use App\Commands\RefreshPermissions;
 use App\Commands\TestWebsocketEvent;
 use App\Models\Provider;
@@ -31,6 +32,10 @@ Artisan::command('permissions:refresh', function () {
     $this->call(RefreshPermissions::class);
 })->purpose('Refresh all permissions and user type permissions');
 
+Artisan::command('pagarme:refresh-entities', function () {
+    $this->call(RefreshPagarmeEntities::class);
+})->purpose('Limpa os recipients e clients locais e os recria no Pagar.me');
+
 Artisan::command('websocket:test {room} {--event=test-event} {--data=}', function () {
     $this->call(TestWebsocketEvent::class, [
         'room' => $this->argument('room'),