feat: email gửi tay/nhắc gia hạn, gói hosting, sửa dữ liệu sản phẩm
- Gửi email theo template cho 1 khách, tab lịch sử email, nhắc gia hạn dịch vụ hàng loạt - Mail lưu from/bcc/sent_by; mẫu MARKETING và mẫu cũ trong TemplateSeeder - Gói hosting: giới hạn, trạng thái, lấy plan Plesk qua CLI gateway - Backfill payment_method và ngày đơn hàng còn thiếu - Bổ sung field CPU/RAM/Disk/Băng thông cho loại sản phẩm vps và cloud Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8651dde6b1
commit
30898ea46f
@@ -2,8 +2,9 @@
|
||||
|
||||
namespace App\Services\Customer;
|
||||
|
||||
use App\Support\EmailRecipients;
|
||||
use App\Exceptions\InvalidTemplateVariableException;
|
||||
use App\Mail\Customer\ManualCustomerEmail;
|
||||
use App\Mail\GenericTemplateMail;
|
||||
use App\Mail\Customer\PasswordResetByAdmin;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Log;
|
||||
@@ -12,6 +13,11 @@ use App\Models\Template;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Customer\CustomerRepositoryInterface;
|
||||
use App\Services\Customer\Concerns\ResolvesCustomer;
|
||||
use App\Services\Email\EmailTemplateRenderer;
|
||||
use App\Support\AutoEmailGate;
|
||||
use App\Support\EmailRecipients;
|
||||
use App\Support\MailGlobals;
|
||||
use App\Support\MailPlaceholders;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
@@ -22,8 +28,62 @@ class CustomerMailService
|
||||
|
||||
public function __construct(
|
||||
private readonly CustomerRepositoryInterface $customerRepository,
|
||||
private readonly EmailTemplateRenderer $renderer,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{subject: string, html: string, cc: array<int, string>, bcc: array<int, string>}
|
||||
*/
|
||||
public function previewTemplateEmail(string $uuid, int $templateId): array
|
||||
{
|
||||
$customer = $this->resolveCustomer($uuid);
|
||||
$template = $this->sendableTemplate($templateId);
|
||||
$config = $template->mailConfig();
|
||||
|
||||
return [
|
||||
...$this->renderForCustomer($template, $customer),
|
||||
'cc' => $config['cc'] ?? [],
|
||||
'bcc' => $config['bcc'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
public function wrapWithLayout(string $uuid, string $contentHtml): string
|
||||
{
|
||||
$customer = $this->resolveCustomer($uuid);
|
||||
|
||||
return MailPlaceholders::fill(Template::layoutBody($contentHtml), [
|
||||
...MailGlobals::values(),
|
||||
...MailPlaceholders::recipientValues($customer, $customer->email),
|
||||
]);
|
||||
}
|
||||
|
||||
private function sendableTemplate(int $templateId): Template
|
||||
{
|
||||
$template = Template::query()->whereKey($templateId)->where('status', 1)->first();
|
||||
abort_if(!$template, 422, 'Template không tồn tại hoặc đang bị tắt.');
|
||||
abort_if($template->format === 'json', 422, 'Không thể gửi template JSON (hoá đơn).');
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{subject: string, html: string}
|
||||
*/
|
||||
private function renderForCustomer(Template $template, Customer $customer): array
|
||||
{
|
||||
$values = collect($template->variables())
|
||||
->map(fn ($definition) => $definition['example'] ?? '')
|
||||
->merge(MailGlobals::values())
|
||||
->merge(MailPlaceholders::recipientValues($customer, $customer->email))
|
||||
->all();
|
||||
|
||||
try {
|
||||
return $this->renderer->render($template, $values);
|
||||
} catch (InvalidTemplateVariableException $e) {
|
||||
abort(422, $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset về mật khẩu ngẫu nhiên và gửi mail cho khách hàng.
|
||||
*/
|
||||
@@ -80,16 +140,16 @@ class CustomerMailService
|
||||
{
|
||||
$customer = $this->resolveCustomer($uuid);
|
||||
|
||||
$template = null;
|
||||
abort_if(!AutoEmailGate::mailEnabled(), 422, 'Hệ thống đang tắt gửi email (Mẫu email → Cài đặt → Bật gửi email).');
|
||||
|
||||
$template = empty($data['template_id']) ? null : $this->sendableTemplate((int) $data['template_id']);
|
||||
$subject = trim((string) ($data['subject'] ?? ''));
|
||||
$message = trim((string) ($data['message'] ?? ''));
|
||||
|
||||
if (!empty($data['template_id'])) {
|
||||
$template = Template::query()->whereKey($data['template_id'])->where('status', 1)->first();
|
||||
abort_if(!$template, 422, 'Template không tồn tại hoặc đang bị tắt.');
|
||||
|
||||
$subject = trim((string) $template->subject);
|
||||
$message = trim((string) $template->message);
|
||||
if ($template && ($subject === '' || $message === '')) {
|
||||
$rendered = $this->renderForCustomer($template, $customer);
|
||||
$subject = $subject ?: $rendered['subject'];
|
||||
$message = $message ?: $rendered['html'];
|
||||
}
|
||||
|
||||
abort_if($subject === '' || $message === '', 422, 'Subject và content không được để trống.');
|
||||
@@ -107,14 +167,19 @@ class CustomerMailService
|
||||
if ($bcc !== []) {
|
||||
$mailer->bcc($bcc);
|
||||
}
|
||||
$mailer->send(new ManualCustomerEmail($subject, $message));
|
||||
$mailer->send($template
|
||||
? new GenericTemplateMail($subject, $message, array_diff_key($template->mailConfig(), array_flip(['cc', 'bcc'])))
|
||||
: new ManualCustomerEmail($subject, $message, wrapLayout: (bool) ($data['with_layout'] ?? true)));
|
||||
|
||||
MailLog::create([
|
||||
'template_id' => $template->id ?? 0,
|
||||
'subject' => $subject,
|
||||
'from_address' => $template?->mailConfig()['from_email'] ?? config('mail.from.address'),
|
||||
'to' => implode(', ', $to),
|
||||
'cc' => implode(', ', $cc),
|
||||
'bcc' => implode(', ', $bcc),
|
||||
'customer_id' => $customer->id,
|
||||
'sent_by' => $admin->id,
|
||||
'message' => $message,
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user