e3bc5da7e3
Co-authored-by: Copilot <copilot@github.com>
297 lines
12 KiB
PHP
297 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Models\Contract;
|
|
use App\Models\Email;
|
|
use App\Models\EmailLog;
|
|
use App\Models\EmailLogStatus;
|
|
use App\Models\EmailTemplate;
|
|
use App\Models\MailProfile;
|
|
use App\Models\Package;
|
|
use App\Models\PackageItem;
|
|
use App\Services\EmailSender;
|
|
use App\Services\EmailTemplateRenderer;
|
|
use Illuminate\Bus\Batchable;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Illuminate\Support\Str;
|
|
|
|
class PackageItemEmailJob implements ShouldQueue
|
|
{
|
|
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
public function __construct(public int $packageItemId)
|
|
{
|
|
$this->onQueue('email');
|
|
}
|
|
|
|
public function handle(EmailTemplateRenderer $renderer, EmailSender $sender): void
|
|
{
|
|
/** @var PackageItem|null $item */
|
|
$item = PackageItem::query()->find($this->packageItemId);
|
|
if (! $item) {
|
|
return;
|
|
}
|
|
|
|
/** @var Package $package */
|
|
$package = $item->package;
|
|
if (! $package || $package->status === Package::STATUS_CANCELED) {
|
|
return;
|
|
}
|
|
|
|
if (in_array($item->status, ['sent', 'failed', 'canceled', 'skipped'], true)) {
|
|
return;
|
|
}
|
|
|
|
if ($item->status === 'queued') {
|
|
$item->status = 'processing';
|
|
$item->save();
|
|
$package->increment('processing_count');
|
|
}
|
|
|
|
$payload = (array) $item->payload_json;
|
|
$target = (array) $item->target_json;
|
|
|
|
$to = $target['email'] ?? null;
|
|
if (! is_string($to) || ! filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
|
$item->status = 'failed';
|
|
$item->last_error = 'Missing or invalid recipient email.';
|
|
$item->save();
|
|
$this->updatePackageCounters($item, $package);
|
|
|
|
return;
|
|
}
|
|
|
|
$templateId = $payload['template_id'] ?? null;
|
|
$mailProfileId = $payload['mail_profile_id'] ?? null;
|
|
$variables = (array) ($payload['variables'] ?? []);
|
|
$subjectOverride = isset($payload['subject']) ? trim((string) $payload['subject']) : null;
|
|
if ($subjectOverride === '') {
|
|
$subjectOverride = null;
|
|
}
|
|
$bodyText = isset($payload['body_text']) ? (string) $payload['body_text'] : '';
|
|
|
|
// Enrich variables with contract/account context when available
|
|
$contract = null;
|
|
if (! empty($target['contract_id'])) {
|
|
$contract = Contract::query()->with(['clientCase.person', 'account.type'])->find($target['contract_id']);
|
|
if ($contract) {
|
|
$variables['contract'] = [
|
|
'id' => $contract->id,
|
|
'uuid' => $contract->uuid,
|
|
'reference' => $contract->reference,
|
|
'start_date' => (string) ($contract->start_date ?? ''),
|
|
'end_date' => (string) ($contract->end_date ?? ''),
|
|
];
|
|
if (is_array($contract->meta) && ! empty($contract->meta)) {
|
|
$variables['contract']['meta'] = $this->flattenMeta($contract->meta);
|
|
}
|
|
if ($contract->account) {
|
|
$initialRaw = (string) $contract->account->initial_amount;
|
|
$balanceRaw = (string) $contract->account->balance_amount;
|
|
$variables['account'] = [
|
|
'id' => $contract->account->id,
|
|
'reference' => $contract->account->reference,
|
|
'initial_amount' => $this->formatAmountEu($initialRaw),
|
|
'balance_amount' => $this->formatAmountEu($balanceRaw),
|
|
'initial_amount_raw' => $initialRaw,
|
|
'balance_amount_raw' => $balanceRaw,
|
|
'type' => $contract->account->type?->name,
|
|
];
|
|
}
|
|
if ($contract->clientCase?->person) {
|
|
$person = $contract->clientCase->person;
|
|
$variables['person'] = [
|
|
'full_name' => $person->full_name,
|
|
'first_name' => $person->first_name,
|
|
'last_name' => $person->last_name,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @var EmailTemplate|null $template */
|
|
$template = $templateId ? EmailTemplate::with(['action', 'decision'])->find((int) $templateId) : null;
|
|
|
|
/** @var MailProfile|null $mailProfile */
|
|
$mailProfile = $mailProfileId
|
|
? MailProfile::find((int) $mailProfileId)
|
|
: MailProfile::query()->where('active', true)->orderBy('priority')->orderBy('id')->first();
|
|
|
|
try {
|
|
if (! $template && ! $subjectOverride) {
|
|
throw new \RuntimeException('No email template or subject provided.');
|
|
}
|
|
|
|
$rendered = $template
|
|
? $renderer->render([
|
|
'subject' => $subjectOverride ?? (string) $template->subject_template,
|
|
'html' => (string) $template->html_template,
|
|
'text' => (string) $template->text_template,
|
|
], array_filter([
|
|
'contract' => $contract,
|
|
'person' => $contract?->clientCase?->person,
|
|
'client' => $contract?->clientCase?->client,
|
|
'client_case' => $contract?->clientCase,
|
|
'mail_profile' => $mailProfile,
|
|
'extra' => $variables,
|
|
'body_text' => $bodyText !== '' ? $bodyText : null,
|
|
]))
|
|
: [
|
|
'subject' => $subjectOverride ?? '',
|
|
'html' => null,
|
|
'text' => null,
|
|
];
|
|
|
|
$log = new EmailLog;
|
|
$log->fill([
|
|
'uuid' => (string) Str::uuid(),
|
|
'template_id' => $template?->id,
|
|
'mail_profile_id' => $mailProfile?->id,
|
|
'to_email' => $to,
|
|
'to_recipients' => [$to],
|
|
'subject' => $rendered['subject'],
|
|
'body_html_hash' => isset($rendered['html']) ? hash('sha256', (string) $rendered['html']) : null,
|
|
'body_text_preview' => isset($rendered['text']) ? mb_strimwidth((string) $rendered['text'], 0, 4096) : null,
|
|
'embed_mode' => 'base64',
|
|
'status' => EmailLogStatus::Queued,
|
|
'queued_at' => now(),
|
|
'contract_id' => $contract?->id,
|
|
'client_id' => $contract?->clientCase?->client?->id,
|
|
'client_case_id' => $contract?->clientCase?->id,
|
|
'extra_context' => ['package_id' => $item->package_id, 'package_item_id' => $item->id],
|
|
]);
|
|
$log->save();
|
|
|
|
$log->body()->create([
|
|
'body_html' => (string) ($rendered['html'] ?? ''),
|
|
'body_text' => (string) ($rendered['text'] ?? ''),
|
|
'inline_css' => true,
|
|
]);
|
|
|
|
// Send directly (synchronous within job context)
|
|
$start = microtime(true);
|
|
$log->status = EmailLogStatus::Sending;
|
|
$log->started_at = now();
|
|
$log->attempt = 1;
|
|
$log->save();
|
|
|
|
$sender->sendFromLog($log);
|
|
|
|
$log->status = EmailLogStatus::Sent;
|
|
$log->sent_at = now();
|
|
$log->duration_ms = (int) round((microtime(true) - $start) * 1000);
|
|
$log->save();
|
|
|
|
$item->status = 'sent';
|
|
$item->result_json = ['email_log_id' => $log->id, 'subject' => $rendered['subject']];
|
|
$item->last_error = null;
|
|
$item->save();
|
|
|
|
// Clear failed flag on successful delivery
|
|
Email::query()->where('value', $to)->where('failed', true)->update(['failed' => false]);
|
|
|
|
// Create activity if the template has action/decision configured
|
|
if ($template && ($template->action_id || $template->decision_id) && $contract && $contract->client_case_id) {
|
|
$activity = \App\Models\Activity::create(array_filter([
|
|
'client_case_id' => $contract->client_case_id,
|
|
'contract_id' => $contract->id,
|
|
'action_id' => $template->action_id,
|
|
'decision_id' => $template->decision_id,
|
|
'note' => 'Poslano: '.$to.', Uspešno'.($bodyText !== '' ? ' | Vsebina: '.mb_strimwidth($bodyText, 0, 500, '…') : ''),
|
|
]));
|
|
$activity->emailLogs()->attach($log->id);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$item->status = 'failed';
|
|
$item->last_error = $e->getMessage();
|
|
$item->save();
|
|
|
|
// Create activity for failed send if the template has action/decision configured
|
|
if ($template && ($template->action_id || $template->decision_id) && isset($contract) && $contract && $contract->client_case_id) {
|
|
$shortError = mb_strimwidth($e->getMessage(), 0, 120, '…');
|
|
$activity = \App\Models\Activity::create(array_filter([
|
|
'client_case_id' => $contract->client_case_id,
|
|
'contract_id' => $contract->id,
|
|
'action_id' => $template->action_id,
|
|
'decision_id' => $template->decision_id,
|
|
'note' => 'Poslano: '.$to.', Napaka pri pošiljanju: '.$shortError.($bodyText !== '' ? ' | Vsebina: '.mb_strimwidth($bodyText, 0, 500, '…') : ''),
|
|
]));
|
|
if (isset($log) && $log->exists) {
|
|
$activity->emailLogs()->attach($log->id);
|
|
}
|
|
}
|
|
|
|
// Mark the email address as failed in the DB.
|
|
if (isset($to)) {
|
|
Email::query()
|
|
->where('value', $to)
|
|
->update(['failed' => true]);
|
|
}
|
|
|
|
// Permanent SMTP rejection (550 user unknown, 551 not local, 553 invalid address)
|
|
// means the address definitively does not exist — also mark it invalid.
|
|
if ($e instanceof \Symfony\Component\Mailer\Exception\TransportExceptionInterface
|
|
&& preg_match('/\b55[013]\b/', $e->getMessage())
|
|
&& isset($to)) {
|
|
Email::query()
|
|
->where('value', $to)
|
|
->update(['valid' => false]);
|
|
}
|
|
}
|
|
|
|
$this->updatePackageCounters($item, $package);
|
|
}
|
|
|
|
private function updatePackageCounters(PackageItem $item, Package $package): void
|
|
{
|
|
if ($item->status === 'sent') {
|
|
$package->increment('sent_count');
|
|
} else {
|
|
$package->increment('failed_count');
|
|
}
|
|
|
|
$package->decrement('processing_count');
|
|
|
|
$package->refresh();
|
|
$done = $package->sent_count + $package->failed_count;
|
|
if ($done >= $package->total_items) {
|
|
$package->status = $package->failed_count > 0 ? Package::STATUS_FAILED : Package::STATUS_COMPLETED;
|
|
$package->finished_at = now();
|
|
$package->save();
|
|
}
|
|
}
|
|
|
|
private function flattenMeta(array $meta, string $prefix = ''): array
|
|
{
|
|
$result = [];
|
|
foreach ($meta as $key => $value) {
|
|
$newKey = $prefix === '' ? $key : "{$prefix}.{$key}";
|
|
if (is_array($value)) {
|
|
if (isset($value['value'])) {
|
|
$result[$newKey] = $value['value'];
|
|
} else {
|
|
$nested = $this->flattenMeta($value, $newKey);
|
|
$result = array_merge($result, $nested);
|
|
}
|
|
} else {
|
|
$result[$newKey] = $value;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
private function formatAmountEu(string $raw): string
|
|
{
|
|
$numeric = preg_replace('/[^0-9.]/', '', $raw);
|
|
$float = (float) $numeric;
|
|
|
|
return number_format($float, 2, ',', '.');
|
|
}
|
|
}
|