From 1bfc261056b9c137c645fd80b1d9f566e6d7fc37 Mon Sep 17 00:00:00 2001 From: Junior-Shyko Date: Tue, 20 May 2025 12:15:16 -0300 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=94=96=20Add=20vers=C3=A3o=20do=20pro?= =?UTF-8?q?jeto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- version.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 version.txt diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +1.0.0 From 29f6e3755809dee6390a2e0b6c554ecc8c6014e0 Mon Sep 17 00:00:00 2001 From: Ronny John Date: Tue, 10 Jun 2025 16:19:18 -0300 Subject: [PATCH 2/4] feat: Adiciona consumo de fila para envio de emails ao publicar pareceres --- .env.example | 14 ++-- .env.testing | 2 + Dockerfile | 2 +- .../Commands/ConsumePublishedOpinions.php | 75 +++++++++++++++++++ app/Mail/PublishedOpinions.php | 38 ++++++++++ app/Services/AmqpService.php | 47 ++++++++++++ composer.json | 4 +- composer.lock | 18 +++-- config/app.php | 4 + docker/php-fpm/99-xdebug.ini | 2 +- docker/php-fpm/supervisord.conf | 7 ++ .../views/emails/published-opinions.blade.php | 40 ++++++++++ 12 files changed, 235 insertions(+), 18 deletions(-) create mode 100644 app/Console/Commands/ConsumePublishedOpinions.php create mode 100644 app/Mail/PublishedOpinions.php create mode 100644 app/Services/AmqpService.php create mode 100644 resources/views/emails/published-opinions.blade.php diff --git a/.env.example b/.env.example index 4d35d06..26635e8 100644 --- a/.env.example +++ b/.env.example @@ -19,12 +19,12 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=sqlite -# DB_HOST=127.0.0.1 -# DB_PORT=3306 -# DB_DATABASE=laravel -# DB_USERNAME=root -# DB_PASSWORD= +DB_CONNECTION=pgsql +DB_HOST=db-email +DB_PORT=5432 +DB_DATABASE= +DB_USERNAME= +DB_PASSWORD= SESSION_DRIVER=database SESSION_LIFETIME=120 @@ -87,6 +87,8 @@ RABBITMQ_DEFAULT_HOST= RABBITMQ_DEFAULT_PORT= RABBITMQ_DEFAULT_USER= RABBITMQ_DEFAULT_PASS= +RABBITMQ_EXCHANGE_PLUGINS= +RABBITMQ_QUEUE_OPINIONS_PUBLISHED= RABBITMQ_QUEUE_PC= RABBITMQ_QUEUE_PC_ROUTE_KEY_PROP= RABBITMQ_QUEUE_PC_ROUTE_KEY_ADM= diff --git a/.env.testing b/.env.testing index 6306635..ca63c86 100644 --- a/.env.testing +++ b/.env.testing @@ -89,6 +89,8 @@ RABBITMQ_DEFAULT_HOST=rabbitmq RABBITMQ_DEFAULT_PORT=5672 RABBITMQ_DEFAULT_USER=mqadmin RABBITMQ_DEFAULT_PASS=Admin123XX_ +RABBITMQ_EXCHANGE_PLUGINS=pluginsTest +RABBITMQ_QUEUE_OPINIONS_PUBLISHED=opinionsPublishedTest RABBITMQ_QUEUE_PC=msgs RABBITMQ_QUEUE_PC_ROUTE_KEY_PROP=proponente RABBITMQ_QUEUE_PC_ROUTE_KEY_ADM=resposta diff --git a/Dockerfile b/Dockerfile index 74bee59..6323d13 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,4 +26,4 @@ RUN apk add --no-cache \ # Limpeza dos pacotes && docker-php-source delete -EXPOSE 9000 \ No newline at end of file +EXPOSE 9000 diff --git a/app/Console/Commands/ConsumePublishedOpinions.php b/app/Console/Commands/ConsumePublishedOpinions.php new file mode 100644 index 0000000..f9f805b --- /dev/null +++ b/app/Console/Commands/ConsumePublishedOpinions.php @@ -0,0 +1,75 @@ +info('🎯 Aguardando e-mails para envio...'); + $this->amqpService->consumeQueue($queue, $this->processMessage(...)); + + return Command::SUCCESS; + } + + private function processMessage(AMQPMessage $message): void + { + $data = json_decode($message->getBody(), true); + + foreach ($data['registrations'] as $item) { + $sent = $this->sendMail($item, $data['opportunity']); + + if (! $sent) { + // @todo: Decidir o que fazer em caso de erro no envio. + } + } + + $message->ack(); + } + + private function sendMail(array $registration, array $opportunity): bool + { + try { + $email = $registration['agent']['email']; + $name = $registration['agent']['name']; + + $sent = Mail::to($email, $name) + ->send(new PublishedOpinions([ + 'opportunity' => $opportunity, + 'registration' => $registration, + ])); + + $this->info("📧 E-mail enviado para: {$name} <{$email}>"); + } catch (\Exception $e) { + logger($e->getMessage()); + + $this->error("❌ Falha ao enviar e-mail para: {$name} <{$email}>"); + + return false; + } + + return (bool) $sent; + } +} diff --git a/app/Mail/PublishedOpinions.php b/app/Mail/PublishedOpinions.php new file mode 100644 index 0000000..fcbf450 --- /dev/null +++ b/app/Mail/PublishedOpinions.php @@ -0,0 +1,38 @@ + $this->data['opportunity'], + 'registration' => $this->data['registration'], + ], + ); + } +} diff --git a/app/Services/AmqpService.php b/app/Services/AmqpService.php new file mode 100644 index 0000000..c966f0a --- /dev/null +++ b/app/Services/AmqpService.php @@ -0,0 +1,47 @@ +connection = new AMQPStreamConnection( + config('app.rabbitmq.host'), + config('app.rabbitmq.port'), + config('app.rabbitmq.user'), + config('app.rabbitmq.pass'), + ); + + $this->channel = $this->connection->channel(); + } + + /** + * @throws \Exception + */ + public function consumeQueue(string $queue, callable $callback): void + { + $this->channel->queue_declare(queue: $queue, durable: true, auto_delete: false); + + $this->channel->basic_consume(queue: $queue, callback: $callback); + + while ($this->channel->is_consuming()) { + $this->channel->wait(); + } + + $this->channel->close(); + $this->connection->close(); + } +} diff --git a/composer.json b/composer.json index e3fd1bb..55cd587 100644 --- a/composer.json +++ b/composer.json @@ -6,14 +6,14 @@ "license": "MIT", "require": { "php": "^8.4", + "ext-pdo": "*", "guzzlehttp/guzzle": "^7.0", "juniorshyko/phpextensive": "^1.0", "laravel/framework": "^12.0", "laravel/sanctum": "^4.0", "laravel/tinker": "^2.9", "php-amqplib/php-amqplib": "^3.2", - "tymon/jwt-auth": "^2.1", - "ext-pdo": "*" + "tymon/jwt-auth": "^2.1" }, "require-dev": { "brianium/paratest": "^7.8", diff --git a/composer.lock b/composer.lock index 3a0a2c7..b8d764b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c517e5610ee4326849a3e0b32f2eadbe", + "content-hash": "c6bb3a2d5cf92b74e56339ef217e4da2", "packages": [ { "name": "brick/math", @@ -1699,16 +1699,16 @@ }, { "name": "league/commonmark", - "version": "2.6.1", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "d990688c91cedfb69753ffc2512727ec646df2ad" + "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d990688c91cedfb69753ffc2512727ec646df2ad", - "reference": "d990688c91cedfb69753ffc2512727ec646df2ad", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", + "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", "shasum": "" }, "require": { @@ -1745,7 +1745,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.7-dev" + "dev-main": "2.8-dev" } }, "autoload": { @@ -1802,7 +1802,7 @@ "type": "tidelift" } ], - "time": "2024-12-29T14:10:59+00:00" + "time": "2025-05-05T12:20:28+00:00" }, { "name": "league/config", @@ -9343,6 +9343,8 @@ "php": "^8.4", "ext-pdo": "*" }, - "platform-dev": {}, + "platform-dev": { + "ext-pcntl": "*" + }, "plugin-api-version": "2.6.0" } diff --git a/config/app.php b/config/app.php index 2dbd3c5..9f34966 100644 --- a/config/app.php +++ b/config/app.php @@ -133,6 +133,10 @@ 'queues' => [ 'accountability' => env('RABBITMQ_QUEUE_PC'), 'published_recourses' => env('RABBITMQ_QUEUE_PUBLISHED_RECOURSES'), + 'opinions_published' => env('RABBITMQ_QUEUE_OPINIONS_PUBLISHED'), + ], + 'exchanges' => [ + 'plugins' => env('RABBITMQ_EXCHANGE_PLUGINS'), ], 'route_key_prop' => env('RABBITMQ_QUEUE_PC_ROUTE_KEY_PROP'), 'route_key_adm' => env('RABBITMQ_QUEUE_PC_ROUTE_KEY_ADM'), diff --git a/docker/php-fpm/99-xdebug.ini b/docker/php-fpm/99-xdebug.ini index c07042c..1aadf86 100644 --- a/docker/php-fpm/99-xdebug.ini +++ b/docker/php-fpm/99-xdebug.ini @@ -1,5 +1,5 @@ zend_extension=xdebug.so -xdebug.mode=off +xdebug.mode=coverage xdebug.start_with_request=trigger xdebug.discover_client_host=1 xdebug.client_host=host.docker.internal diff --git a/docker/php-fpm/supervisord.conf b/docker/php-fpm/supervisord.conf index 214c70b..5afc4d7 100644 --- a/docker/php-fpm/supervisord.conf +++ b/docker/php-fpm/supervisord.conf @@ -16,6 +16,13 @@ autorestart=true stderr_logfile=/dev/stderr stdout_logfile=/dev/stdout +[program:published-opinions] +command=php artisan rabbitmq:consume-published-opinions-emails +autostart=true +autorestart=true +stderr_logfile=/dev/stderr +stdout_logfile=/dev/stdout + [program:accountability_queue] process_name=%(program_name)s command=php /var/www/html/artisan queue:work diff --git a/resources/views/emails/published-opinions.blade.php b/resources/views/emails/published-opinions.blade.php new file mode 100644 index 0000000..db0fb40 --- /dev/null +++ b/resources/views/emails/published-opinions.blade.php @@ -0,0 +1,40 @@ +@include('emails/header') + + + + + + + +@include('emails/footer') From 40def67e0bea1b022451a853fac34f3201f8925a Mon Sep 17 00:00:00 2001 From: Ronny John Date: Wed, 11 Jun 2025 14:15:58 -0300 Subject: [PATCH 3/4] tests: Add tests to Published Opinions command --- .../Commands/ConsumePublishedOpinionsTest.php | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 tests/Unit/Console/Commands/ConsumePublishedOpinionsTest.php diff --git a/tests/Unit/Console/Commands/ConsumePublishedOpinionsTest.php b/tests/Unit/Console/Commands/ConsumePublishedOpinionsTest.php new file mode 100644 index 0000000..b837095 --- /dev/null +++ b/tests/Unit/Console/Commands/ConsumePublishedOpinionsTest.php @@ -0,0 +1,144 @@ +start(); + + $this->assertTrue($process->isRunning()); + + $process->signal(SIGINT); + + sleep(1); + + $this->assertFalse($process->isRunning()); + $this->assertEquals(130, $process->getExitCode()); + } + + public function test_send_email_true(): void + { + Mail::fake(); + + $registration = [ + 'url' => 'https://example.com', + 'number' => 'te-320998499', + 'agent' => [ + 'name' => 'John Doe', + 'email' => 'john@doe.example', + ], + ]; + + $opportunity = [ + 'name' => 'John Doe', + 'url' => 'https://example.com', + ]; + + $command = new ConsumePublishedOpinions($this->createMock(AmqpService::class)); + $command->setOutput($this->createMock(OutputStyle::class)); + + $reflection = new \ReflectionClass($command); + $method = $reflection->getMethod('sendMail'); + $method->setAccessible(true); + $method->invoke($command, $registration, $opportunity); + + Mail::assertSent(PublishedOpinions::class); + } + + public function test_send_email_false(): void + { + Mail::fake(); + + $registration = [ + 'url' => 'https://example.com', + 'agent' => [ + 'name' => 'John Doe', + 'email' => 'john@doe.example', + ], + ]; + + $opportunity = [ + 'url' => 'https://example.com', + ]; + + $command = new ConsumePublishedOpinions($this->createMock(AmqpService::class)); + $command->setOutput($this->createMock(OutputStyle::class)); + + $reflection = new \ReflectionClass($command); + $method = $reflection->getMethod('sendMail'); + $method->setAccessible(true); + $send = $method->invoke($command, $registration, $opportunity); + + // Asserts that the email was not sent + $this->assertFalse($send); + Mail::assertNotSent(PublishedRecourse::class); + } + + public function test_process_message_with_success(): void + { + Mail::fake(); + + $body = json_encode([ + 'registrations' => [[ + 'url' => 'https://example.com', + 'number' => 'te-320998499', + 'agent' => [ + 'name' => 'John Doe', + 'email' => 'john@doe.example', + ], + ]], + 'opportunity' => [ + 'name' => 'John Doe', + 'url' => 'https://example.com', + ], + ]); + $msgMock = $this->createMockAMQPMessage($body); + $msgMock->shouldReceive('ack') + ->once() + ->andReturnNull(); + $msgMock->shouldReceive('getBody') + ->once() + ->andReturn($body); + + $command = new ConsumePublishedOpinions($this->createMock(AmqpService::class)); + $command->setOutput($this->createMock(OutputStyle::class)); + + $reflection = new \ReflectionClass($command); + $method = $reflection->getMethod('processMessage'); + $method->setAccessible(true); + $method->invoke($command, $msgMock); + + Mail::assertSent(PublishedOpinions::class); + } + + private function createMockAMQPMessage(string $body): MockInterface + { + $channelMock = Mockery::mock(AMQPChannel::class); + + $msgMock = Mockery::mock(AMQPMessage::class); + $reflection = new \ReflectionClass(AMQPMessage::class); + $reflection->getMethod('setBody')->invoke($msgMock, $body); + $reflection->getMethod('setDeliveryTag')->invoke($msgMock, 1); + $reflection->getMethod('setChannel')->invoke($msgMock, $channelMock); + + return $msgMock; + } +} From 423218968c28f827b71c746cc57344cc1d565d4c Mon Sep 17 00:00:00 2001 From: Ronny John Date: Fri, 4 Jul 2025 10:46:54 -0300 Subject: [PATCH 4/4] typo: Melhorando textos do template de e-mail --- resources/views/emails/published-opinions.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/emails/published-opinions.blade.php b/resources/views/emails/published-opinions.blade.php index db0fb40..552b86e 100644 --- a/resources/views/emails/published-opinions.blade.php +++ b/resources/views/emails/published-opinions.blade.php @@ -9,7 +9,7 @@

Prezado(a), espero que esteja bem.

- A Secult vem por meio deste informar que os pareceres referentes ao projeto inscrito na oportunidade + A plataforma {{ $appName ?? 'Mapa Cultural do Ceará' }} vem por meio deste informar que os pareceres referentes ao projeto inscrito na oportunidade {{ $opportunity['name'] }} com número de inscrição {{ $registration['number'] }} @@ -27,7 +27,7 @@ Cordialmente,

- Secretaria Estadual da Cultura do Ceará. + {{ $opportunity['owner']['name'] ?? 'Secretaria da Cultura do Estado do Ceará' }}.