Skip to content

Commit 8f26db6

Browse files
committed
feat: Implementa módulos Core, PanelAdmin e HomePage com gerenciamento de e-mails, finanças e autenticação, adiciona documentação completa e localização PT-BR.
1 parent fb8f2db commit 8f26db6

File tree

151 files changed

+5336
-3365
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

151 files changed

+5336
-3365
lines changed

.htaccess

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<IfModule mod_rewrite.c>
2+
RewriteEngine On
3+
# Redireciona todas as requisições para a pasta public/ (Laravel)
4+
# Use quando o DocumentRoot nao puder apontar para public/ (ex: shared hosting)
5+
RewriteRule ^(.*)$ public/$1 [L]
6+
RewriteRule ^$ public/ [L]
7+
</IfModule>

Config.md

Lines changed: 0 additions & 92 deletions
This file was deleted.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Modules\Core\Mail;
6+
7+
use Symfony\Component\Mime\Message;
8+
9+
/**
10+
* Serializable callable to add Vertex headers to a queued Mailable.
11+
* Using a class instead of a Closure allows the job to be serialized.
12+
*/
13+
final class AddVertexMailHeaders
14+
{
15+
public function __construct(
16+
public readonly string $fromAddress,
17+
public readonly string $entityRefId,
18+
public readonly string $templateKey,
19+
) {}
20+
21+
public function __invoke(Message $message): void
22+
{
23+
$message->getHeaders()->addTextHeader('List-Unsubscribe', '<mailto:' . $this->fromAddress . '>');
24+
$message->getHeaders()->addTextHeader('X-Template-Key', $this->templateKey);
25+
$message->getHeaders()->addTextHeader('X-Entity-Ref-ID', $this->entityRefId);
26+
}
27+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Modules\Core\Mail;
6+
7+
use Illuminate\Bus\Queueable;
8+
use Illuminate\Contracts\Queue\ShouldQueue;
9+
use Illuminate\Mail\Mailable;
10+
use Illuminate\Mail\Mailables\Address;
11+
use Illuminate\Mail\Mailables\Content;
12+
use Illuminate\Mail\Mailables\Envelope;
13+
use Illuminate\Queue\SerializesModels;
14+
use Modules\Core\Models\EmailLog;
15+
use Modules\Core\Models\EmailTemplate;
16+
17+
class VertexDynamicMail extends Mailable implements ShouldQueue
18+
{
19+
use Queueable, SerializesModels;
20+
21+
public int $tries = 3;
22+
23+
public string $templateKey;
24+
25+
public string $recipientEmail = '';
26+
27+
protected string $resolvedSubject;
28+
29+
protected string $renderedBody;
30+
31+
protected bool $isPlain = false;
32+
33+
public function __construct(string $templateKey, array $variables = [], ?string $recipientEmail = null)
34+
{
35+
$this->templateKey = $templateKey;
36+
$this->recipientEmail = $recipientEmail ?? '';
37+
38+
$template = EmailTemplate::where('key', $templateKey)->firstOrFail();
39+
$this->resolvedSubject = $template->subject;
40+
$this->isPlain = ! ($template->is_html ?? true);
41+
42+
$content = $this->substituteVariables($template->content_html ?? '', $variables);
43+
$this->renderedBody = $this->isPlain ? nl2br(e($content)) : $content;
44+
45+
$fromAddress = setting('mail_from_address', config('mail.from.address'));
46+
$entityRefId = 'mail_' . uniqid('', true);
47+
$this->withSymfonyMessage(new AddVertexMailHeaders($fromAddress, $entityRefId, $this->templateKey));
48+
}
49+
50+
public function envelope(): Envelope
51+
{
52+
$fromAddress = setting('mail_from_address', config('mail.from.address'));
53+
$fromName = setting('mail_from_name', config('mail.from.name'));
54+
$from = new Address($fromAddress, $fromName);
55+
56+
return new Envelope(
57+
subject: $this->resolvedSubject,
58+
from: $from,
59+
replyTo: [$fromAddress],
60+
);
61+
}
62+
63+
public function content(): Content
64+
{
65+
return new Content(
66+
view: 'emails.dynamic-content',
67+
with: [
68+
'bodyHtml' => $this->renderedBody,
69+
'isPlain' => $this->isPlain,
70+
],
71+
);
72+
}
73+
74+
protected function substituteVariables(string $html, array $variables): string
75+
{
76+
$urlKeys = ['reset_link', 'app_url', 'link'];
77+
$variables['app_url'] = $variables['app_url'] ?? config('app.url');
78+
79+
foreach ($variables as $key => $value) {
80+
$placeholder = '{{ ' . $key . ' }}';
81+
if (in_array($key, $urlKeys, true)) {
82+
$html = str_replace($placeholder, (string) $value, $html);
83+
} else {
84+
$html = str_replace($placeholder, e((string) $value), $html);
85+
}
86+
}
87+
88+
return $html;
89+
}
90+
91+
public function failed(\Throwable $e): void
92+
{
93+
EmailLog::create([
94+
'user_id' => null,
95+
'recipient_email' => $this->recipientEmail ?: 'unknown',
96+
'template_key' => $this->templateKey,
97+
'status' => 'failed',
98+
'error_details' => $e->getMessage(),
99+
'sent_at' => null,
100+
]);
101+
102+
$this->notifyAdminOfFailure();
103+
}
104+
105+
protected function notifyAdminOfFailure(): void
106+
{
107+
try {
108+
$admins = \App\Models\User::role('admin')->get();
109+
$message = sprintf(
110+
'E-mail falhou após 3 tentativas: template "%s", destinatário "%s". Verifique os Logs de Mensageria.',
111+
$this->templateKey,
112+
$this->recipientEmail
113+
);
114+
foreach ($admins as $admin) {
115+
$admin->notify(new \Modules\Notifications\Notifications\SystemNotification(
116+
'Falha no envio de e-mail',
117+
$message,
118+
null,
119+
'danger'
120+
));
121+
}
122+
} catch (\Throwable $e) {
123+
report($e);
124+
}
125+
}
126+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Modules\Core\Models;
6+
7+
use App\Models\User;
8+
use Illuminate\Database\Eloquent\Model;
9+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
10+
11+
class EmailLog extends Model
12+
{
13+
protected $table = 'email_logs';
14+
15+
protected $fillable = [
16+
'user_id',
17+
'recipient_email',
18+
'template_key',
19+
'status',
20+
'smtp_response',
21+
'error_details',
22+
'body_snapshot',
23+
'sent_at',
24+
];
25+
26+
protected function casts(): array
27+
{
28+
return [
29+
'sent_at' => 'datetime',
30+
];
31+
}
32+
33+
public function user(): BelongsTo
34+
{
35+
return $this->belongsTo(User::class);
36+
}
37+
38+
public function isFailed(): bool
39+
{
40+
return $this->status === 'failed';
41+
}
42+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Modules\Core\Models;
6+
7+
use Illuminate\Database\Eloquent\Model;
8+
9+
class EmailTemplate extends Model
10+
{
11+
protected $table = 'email_templates';
12+
13+
protected $fillable = [
14+
'key',
15+
'subject',
16+
'content_html',
17+
'variables_hint',
18+
'is_html',
19+
'description',
20+
];
21+
22+
protected function casts(): array
23+
{
24+
return [
25+
'is_html' => 'boolean',
26+
];
27+
}
28+
}

Modules/Core/app/Providers/CoreServiceProvider.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,20 @@ protected function overrideConfigsFromDatabase(): void
150150
date_default_timezone_set($timezone);
151151
\Carbon\Carbon::setLocale($locale);
152152

153-
// Override mail configs
153+
// Override mail configs (Admin > Configurações > E-mail). Encryption "null" = none.
154+
// All app emails use this config: WelcomeEmail, ResetPasswordEmail, ProSubscriptionConfirmation,
155+
// test mail (Settings), and Logs re-send. Queue workers load the same config on boot.
156+
$mailEncryption = $settings->get('mail_encryption', env('MAIL_ENCRYPTION', 'tls'));
157+
if ($mailEncryption === 'null' || $mailEncryption === '') {
158+
$mailEncryption = null;
159+
}
154160
config([
155161
'mail.default' => $settings->get('mail_mailer', env('MAIL_MAILER', 'log')),
156162
'mail.mailers.smtp.host' => $settings->get('mail_host', env('MAIL_HOST', '127.0.0.1')),
157-
'mail.mailers.smtp.port' => $settings->get('mail_port', env('MAIL_PORT', 2525)),
163+
'mail.mailers.smtp.port' => (int) $settings->get('mail_port', env('MAIL_PORT', 587)),
158164
'mail.mailers.smtp.username' => $settings->get('mail_username', env('MAIL_USERNAME')),
159165
'mail.mailers.smtp.password' => $settings->get('mail_password', env('MAIL_PASSWORD')),
160-
'mail.mailers.smtp.encryption' => $settings->get('mail_encryption', env('MAIL_ENCRYPTION', 'tls')),
166+
'mail.mailers.smtp.encryption' => $mailEncryption,
161167
'mail.from.address' => $settings->get('mail_from_address', env('MAIL_FROM_ADDRESS', 'hello@example.com')),
162168
'mail.from.name' => $settings->get('mail_from_name', env('MAIL_FROM_NAME', env('APP_NAME', 'Vertex Contas'))),
163169
]);

Modules/Core/app/Services/FinancialHealthService.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,19 @@ public function syncUserPlanning(\App\Models\User $user, array $incomes, array $
584584
foreach ($expenses as $item) {
585585
$this->createRecurringFromBaseline($user, $item, 'expense');
586586
}
587+
588+
// 4. Auto-create first account when user has 0 accounts (FREE flow)
589+
if (Account::where('user_id', $user->id)->count() === 0) {
590+
$limitService = app(SubscriptionLimitService::class);
591+
if ($limitService->canCreate($user, 'account')) {
592+
Account::create([
593+
'user_id' => $user->id,
594+
'name' => 'Minha conta',
595+
'type' => 'checking',
596+
'balance' => 0,
597+
]);
598+
}
599+
}
587600
});
588601
}
589602

0 commit comments

Comments
 (0)