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

feat: :sparkles: feat (push aprovacao) criado push notification para aprovacao de cadastro do prestador

foi criado push notification para o prestador quando o cadastro dele for aprovado

fase:dev | origin:escopo
Gustavo Zanatta 1 день тому
батько
коміт
992bc6c94c

+ 1 - 0
app/Enums/PushNotificationCategoryEnum.php

@@ -12,4 +12,5 @@ enum PushNotificationCategoryEnum: string
     case EDUCATIVO_CONVERSAO  = 'educativo_conversao';
     case SOCIAL_PROOF         = 'social_proof';
     case CONTEXTUAL           = 'contextual';
+    case TRANSACIONAL         = 'transacional';
 }

+ 60 - 0
app/Notifications/Push/Prestador/Transacional/CadastroAprovadoPush.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Notifications\Push\Prestador\Transacional;
+
+use App\Enums\PushNotificationCategoryEnum;
+use App\Enums\PushNotificationTargetEnum;
+use App\Notifications\Push\BasePushNotification;
+use Illuminate\Database\Eloquent\Collection;
+
+/**
+ * Push transacional disparada no momento em que o cadastro do prestador é aprovado.
+ *
+ * Não é registrada no PushNotificationDispatcher: o envio parte do
+ * ProviderService, não da varredura do scheduler.
+ */
+class CadastroAprovadoPush extends BasePushNotification
+{
+    public function label(): string
+    {
+        return 'provider_cadastro_aprovado';
+    }
+
+    public function title(): string
+    {
+        return 'Cadastro aprovado! 🎉';
+    }
+
+    public function body(): string
+    {
+        return 'Boas-vindas ao diária app. Seu perfil foi aprovado e você já pode receber diárias.';
+    }
+
+    public function target(): PushNotificationTargetEnum
+    {
+        return PushNotificationTargetEnum::PRESTADOR;
+    }
+
+    public function category(): PushNotificationCategoryEnum
+    {
+        return PushNotificationCategoryEnum::TRANSACIONAL;
+    }
+
+    public function notificationCooldownDays(): int
+    {
+        return 0;
+    }
+
+    public function categoryCooldownDays(): int
+    {
+        return 0;
+    }
+
+    /**
+     * Nunca é elegível por varredura — o disparo é sempre pontual.
+     */
+    public function eligibleUsers(): Collection
+    {
+        return new Collection;
+    }
+}

+ 64 - 2
app/Services/ProviderService.php

@@ -6,11 +6,14 @@ use App\Enums\ApprovalStatusEnum;
 use App\Enums\UserTypeEnum;
 use App\Models\Address;
 use App\Models\City;
+use App\Models\DeviceToken;
 use App\Models\Provider;
 use App\Models\ProviderServicesType;
 use App\Models\ProviderWorkingDay;
+use App\Models\PushNotificationLog;
 use App\Models\State;
 use App\Models\User;
+use App\Notifications\Push\Prestador\Transacional\CadastroAprovadoPush;
 use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Http\UploadedFile;
 use Illuminate\Pagination\LengthAwarePaginator;
@@ -85,7 +88,7 @@ class ProviderService
         $provider = $model->fresh(['user', 'profileMedia']);
 
         if (! $wasAccepted && $provider->approval_status === ApprovalStatusEnum::ACCEPTED) {
-            $this->sendApprovedEmail($provider);
+            $this->notifyApproved($provider);
         }
 
         return $provider;
@@ -276,7 +279,7 @@ class ProviderService
         });
 
         if (! $wasAccepted) {
-            $this->sendApprovedEmail($provider);
+            $this->notifyApproved($provider);
         }
 
         return $provider;
@@ -401,6 +404,16 @@ class ProviderService
         return $digits === '' ? null : $digits;
     }
 
+    /**
+     * Efeitos colaterais da aprovação do cadastro do prestador.
+     * Chamado apenas na transição para ACCEPTED, sempre fora da transação.
+     */
+    private function notifyApproved(Provider $provider): void
+    {
+        $this->sendApprovedEmail($provider);
+        $this->sendApprovedPush($provider);
+    }
+
     private function sendApprovedEmail(Provider $provider): void
     {
         if (! empty($provider->user?->email)) {
@@ -427,4 +440,53 @@ class ProviderService
             'user_id'     => $provider->user?->id,
         ]);
     }
+
+    private function sendApprovedPush(Provider $provider): void
+    {
+        $user = $provider->user;
+
+        if (! $user) {
+            Log::warning('Push de aprovação do prestador ignorado: prestador sem usuário', [
+                'provider_id' => $provider->id,
+            ]);
+
+            return;
+        }
+
+        $notification = new CadastroAprovadoPush;
+
+        $alreadySent = PushNotificationLog::query()
+            ->where('user_id', $user->id)
+            ->where('label', $notification->label())
+            ->exists();
+
+        if ($alreadySent) {
+            return;
+        }
+
+        $hasActiveToken = DeviceToken::query()
+            ->where('user_id', $user->id)
+            ->where('app_type', $notification->target()->value)
+            ->where('active', true)
+            ->exists();
+
+        if (! $hasActiveToken) {
+            Log::warning('Push de aprovação do prestador ignorado: usuário não possui device token ativo', [
+                'provider_id' => $provider->id,
+                'user_id'     => $user->id,
+            ]);
+
+            return;
+        }
+
+        try {
+            app(PushNotificationService::class)->sendToUser($user, $notification);
+        } catch (\Throwable $exception) {
+            Log::error('Falha ao enviar push de aprovação do prestador', [
+                'provider_id' => $provider->id,
+                'user_id'     => $user->id,
+                'error'       => $exception->getMessage(),
+            ]);
+        }
+    }
 }

+ 206 - 0
tests/Feature/ProviderApprovalPushTest.php

@@ -0,0 +1,206 @@
+<?php
+
+namespace Tests\Feature;
+
+use App\Enums\ApprovalStatusEnum;
+use App\Enums\PushNotificationCategoryEnum;
+use App\Enums\PushNotificationTargetEnum;
+use App\Enums\UserTypeEnum;
+use App\Models\DeviceToken;
+use App\Models\Provider;
+use App\Models\PushNotificationLog;
+use App\Models\User;
+use App\Notifications\Push\Prestador\Transacional\CadastroAprovadoPush;
+use App\Services\ProviderService;
+use App\Services\PushNotificationService;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Mail;
+use Mockery\MockInterface;
+use Tests\TestCase;
+
+class ProviderApprovalPushTest extends TestCase
+{
+    use RefreshDatabase;
+
+    private ProviderService $service;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        Mail::fake();
+
+        $this->service = app(ProviderService::class);
+    }
+
+    public function test_aprovar_prestador_dispara_push_de_cadastro_aprovado(): void
+    {
+        $provider = $this->makeProvider();
+
+        $this->withDeviceToken($provider->user);
+
+        $push = $this->mockPushService();
+
+        $push->shouldReceive('sendToUser')
+            ->once()
+            ->withArgs(function (User $user, $notification) use ($provider) {
+                return $user->is($provider->user)
+                    && $notification instanceof CadastroAprovadoPush;
+            });
+
+        $approved = $this->service->approve($provider->id);
+
+        $this->assertSame(ApprovalStatusEnum::ACCEPTED->value, $approved->approval_status->value);
+    }
+
+    public function test_update_para_accepted_dispara_push(): void
+    {
+        $provider = $this->makeProvider();
+
+        $this->withDeviceToken($provider->user);
+
+        $push = $this->mockPushService();
+
+        $push->shouldReceive('sendToUser')->once();
+
+        $this->service->update($provider->id, [
+            'approval_status' => ApprovalStatusEnum::ACCEPTED->value,
+        ]);
+    }
+
+    public function test_update_que_nao_muda_o_status_nao_dispara_push(): void
+    {
+        $provider = $this->makeProvider();
+
+        $this->withDeviceToken($provider->user);
+
+        $push = $this->mockPushService();
+
+        $push->shouldNotReceive('sendToUser');
+
+        $this->service->update($provider->id, ['birth_date' => '1991-02-02']);
+    }
+
+    public function test_aprovar_prestador_ja_aprovado_nao_reenvia_push(): void
+    {
+        $provider = $this->makeProvider(ApprovalStatusEnum::ACCEPTED);
+
+        $this->withDeviceToken($provider->user);
+
+        $push = $this->mockPushService();
+
+        $push->shouldNotReceive('sendToUser');
+
+        $this->service->approve($provider->id);
+    }
+
+    /**
+     * Rede de seguranca extra: mesmo que a transicao seja detectada de novo
+     * (reprovar e reaprovar, por exemplo), o log impede o envio duplicado.
+     */
+    public function test_push_ja_registrada_no_log_nao_e_reenviada(): void
+    {
+        $provider = $this->makeProvider();
+
+        $this->withDeviceToken($provider->user);
+
+        PushNotificationLog::query()->create([
+            'label'    => (new CadastroAprovadoPush)->label(),
+            'user_id'  => $provider->user->id,
+            'target'   => PushNotificationTargetEnum::PRESTADOR->value,
+            'category' => PushNotificationCategoryEnum::TRANSACIONAL->value,
+            'sent_at'  => now()->subDay(),
+        ]);
+
+        $push = $this->mockPushService();
+
+        $push->shouldNotReceive('sendToUser');
+
+        $this->service->approve($provider->id);
+    }
+
+    public function test_prestador_sem_device_token_ativo_nao_dispara_push(): void
+    {
+        $provider = $this->makeProvider();
+
+        $this->withDeviceToken($provider->user, active: false);
+
+        $push = $this->mockPushService();
+
+        $push->shouldNotReceive('sendToUser');
+
+        $approved = $this->service->approve($provider->id);
+
+        $this->assertSame(ApprovalStatusEnum::ACCEPTED->value, $approved->approval_status->value);
+    }
+
+    public function test_token_de_outro_app_nao_dispara_push_do_prestador(): void
+    {
+        $provider = $this->makeProvider();
+
+        $this->withDeviceToken($provider->user, appType: PushNotificationTargetEnum::CLIENTE);
+
+        $push = $this->mockPushService();
+
+        $push->shouldNotReceive('sendToUser');
+
+        $this->service->approve($provider->id);
+    }
+
+    public function test_falha_no_envio_da_push_nao_impede_a_aprovacao(): void
+    {
+        $provider = $this->makeProvider();
+
+        $this->withDeviceToken($provider->user);
+
+        $push = $this->mockPushService();
+
+        $push->shouldReceive('sendToUser')
+            ->once()
+            ->andThrow(new \RuntimeException('FCM indisponivel'));
+
+        $approved = $this->service->approve($provider->id);
+
+        $this->assertSame(ApprovalStatusEnum::ACCEPTED->value, $approved->approval_status->value);
+    }
+
+    //
+
+    private function mockPushService(): MockInterface
+    {
+        return $this->mock(PushNotificationService::class);
+    }
+
+    private function makeProvider(?ApprovalStatusEnum $status = null): Provider
+    {
+        $user = User::query()->create([
+            'name'     => 'Prestador Teste',
+            'email'    => 'prestador'.uniqid().'@teste.com',
+            'password' => 'secret',
+            'type'     => UserTypeEnum::PROVIDER->value,
+        ]);
+
+        $provider = Provider::query()->create([
+            'user_id'         => $user->id,
+            'document'        => '06767310905',
+            'birth_date'      => '1990-01-01',
+            'approval_status' => ($status ?? ApprovalStatusEnum::PENDING)->value,
+        ]);
+
+        return $provider->load('user');
+    }
+
+    private function withDeviceToken(
+        User $user,
+        bool $active = true,
+        ?PushNotificationTargetEnum $appType = null,
+    ): DeviceToken {
+        return DeviceToken::query()->create([
+            'user_id'  => $user->id,
+            'token'    => 'tok_'.uniqid(),
+            'platform' => 'android',
+            'app_type' => ($appType ?? PushNotificationTargetEnum::PRESTADOR)->value,
+            'active'   => $active,
+        ]);
+    }
+}