feat: implement admin SMTP settings management and automated order notification system
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Setting;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Facade\SignRSA;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class SettingController extends Controller
|
||||
{
|
||||
/**
|
||||
* GET /administrator/settings
|
||||
* Returns all setting keys: setting, smtp.
|
||||
*/
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$setting = Setting::where('key', 'setting')->first();
|
||||
$smtp = Setting::where('key', 'smtp')->first();
|
||||
|
||||
$settingValue = $setting ? json_decode((string) $setting->value, true) : [];
|
||||
$smtpValue = $smtp ? json_decode((string) $smtp->value, true) : [];
|
||||
|
||||
// Never expose raw smtp password
|
||||
if (isset($smtpValue['password']) && !empty($smtpValue['password'])) {
|
||||
$smtpValue['password'] = '********';
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'setting' => $settingValue,
|
||||
'smtp' => $smtpValue,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /administrator/settings
|
||||
* Accepts: { setting: {...}, smtp: {...} }
|
||||
*/
|
||||
public function update(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'setting' => 'sometimes|array',
|
||||
'smtp' => 'sometimes|array',
|
||||
'smtp.host' => 'sometimes|string',
|
||||
'smtp.port' => 'sometimes|integer',
|
||||
'smtp.user' => 'sometimes|email',
|
||||
'smtp.encryption' => 'sometimes|in:ssl,tls,starttls,',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['error' => true, 'message' => $validator->errors()->first()], 422);
|
||||
}
|
||||
|
||||
if ($request->has('setting')) {
|
||||
$current = Setting::where('key', 'setting')->first();
|
||||
$existing = $current ? json_decode((string) $current->value, true) : [];
|
||||
$merged = array_merge($existing, $request->input('setting'));
|
||||
|
||||
Setting::updateOrCreate(
|
||||
['key' => 'setting'],
|
||||
['value' => json_encode($merged, JSON_UNESCAPED_UNICODE)]
|
||||
);
|
||||
}
|
||||
|
||||
if ($request->has('smtp')) {
|
||||
$smtpData = $request->input('smtp');
|
||||
$current = Setting::where('key', 'smtp')->first();
|
||||
$existing = $current ? json_decode((string) $current->value, true) : [];
|
||||
|
||||
// Keep existing password if placeholder sent (not changed)
|
||||
if (isset($smtpData['password']) && $smtpData['password'] === '********') {
|
||||
$smtpData['password'] = $existing['password'] ?? '';
|
||||
} elseif (isset($smtpData['password']) && !empty($smtpData['password'])) {
|
||||
$smtpData['password'] = SignRSA::EncryptFunc($smtpData['password']);
|
||||
}
|
||||
|
||||
$merged = array_merge($existing, $smtpData);
|
||||
|
||||
Setting::updateOrCreate(
|
||||
['key' => 'smtp'],
|
||||
['value' => json_encode($merged, JSON_UNESCAPED_UNICODE)]
|
||||
);
|
||||
|
||||
// Flush mail config cache so new SMTP settings take effect immediately
|
||||
Cache::forget('mail_config');
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Cập nhật cài đặt thành công.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs\Mail\Admin;
|
||||
|
||||
use App\Mail\Admin\AdminOrderNotification;
|
||||
use App\Models\Setting;
|
||||
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\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class AdminOrderNotificationJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
public int $timeout = 60;
|
||||
|
||||
public function __construct(public readonly array $data) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$setting = Setting::where('key', 'setting')->first();
|
||||
$settingValue = $setting ? json_decode((string) $setting->value, true) : [];
|
||||
|
||||
$adminEmail = $settingValue['admin_email'] ?? null;
|
||||
|
||||
if (empty($adminEmail)) {
|
||||
Log::warning('AdminOrderNotificationJob: admin_email not configured in settings.');
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = array_merge($this->data, [
|
||||
'company_name' => $settingValue['company'] ?? 'Portal Admin',
|
||||
]);
|
||||
|
||||
try {
|
||||
Mail::to($adminEmail)->send(new AdminOrderNotification($payload));
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('AdminOrderNotificationJob: failed to send email. ' . $e->getMessage(), [
|
||||
'admin_email' => $adminEmail,
|
||||
'order_num' => $this->data['order_num'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function failed(\Throwable $e): void
|
||||
{
|
||||
Log::error('AdminOrderNotificationJob: job failed.', [
|
||||
'error' => $e->getMessage(),
|
||||
'order_num' => $this->data['order_num'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mail\Admin;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class AdminOrderNotification extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public readonly array $data) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: '[ADMIN] Đơn hàng mới #' . ($this->data['order_num'] ?? 'N/A')
|
||||
. ' — ' . ($this->data['customer_email'] ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.admin.order-notification',
|
||||
with: $this->data,
|
||||
);
|
||||
}
|
||||
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Services\Api\Order;
|
||||
|
||||
use App\Enums\Order\OrderStatus;
|
||||
use App\Helpers\OrderHelper;
|
||||
use App\Jobs\Mail\Admin\AdminOrderNotificationJob;
|
||||
use App\Jobs\VirtualMachine\Create\CreateVirtualMachineJob;
|
||||
use App\Models\Addon;
|
||||
use App\Models\Log as CustomerActivityLog;
|
||||
@@ -151,6 +152,8 @@ class OrderService
|
||||
$this->provisionService($processedOrder, $item, $customer);
|
||||
}
|
||||
|
||||
$this->dispatchAdminNotification($processedOrder, $customer);
|
||||
|
||||
return $processedOrder;
|
||||
});
|
||||
}
|
||||
@@ -268,6 +271,32 @@ class OrderService
|
||||
};
|
||||
}
|
||||
|
||||
private function dispatchAdminNotification(Order $order, $customer): void
|
||||
{
|
||||
try {
|
||||
$item = $order->orderItems->first();
|
||||
$product = $item?->product;
|
||||
$productType = $product?->category?->name ?? $product?->type ?? 'N/A';
|
||||
|
||||
AdminOrderNotificationJob::dispatch([
|
||||
'order_num' => $order->order_num,
|
||||
'customer_name' => trim($customer->firstname . ' ' . $customer->lastname),
|
||||
'customer_email' => $customer->email,
|
||||
'client_ip' => $order->client_ip ?? request()?->ip(),
|
||||
'product_name' => $product?->name ?? 'N/A',
|
||||
'product_type' => $productType,
|
||||
'billing_cycle' => $item?->billing_cycle ?? 'N/A',
|
||||
'total' => $order->total,
|
||||
'created_at' => now()->format('d/m/Y H:i:s'),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
Log::error('OrderService: failed to dispatch admin notification', [
|
||||
'order_id' => $order->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich raw addon entries with resolved price & subtotal.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user