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..552b86e
--- /dev/null
+++ b/resources/views/emails/published-opinions.blade.php
@@ -0,0 +1,40 @@
+@include('emails/header')
+
+
+
+
+
+
+ |
+
+ Prezado(a), espero que esteja bem.
+
+ 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'] }}
+ foram publicados.
+
+ Para visualizar, acesse o link a seguir da sua
+
+
+ página de inscrição.
+
+
+ {{ $registration['url'] }}
+
+
+ Cordialmente,
+
+
+ {{ $opportunity['owner']['name'] ?? 'Secretaria da Cultura do Estado do Ceará' }}.
+
+
+ |
+
+
+ |
+
+
+
+@include('emails/footer')
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;
+ }
+}