diff --git a/app/Http/Controllers/Api/Admin/Setting/SettingController.php b/app/Http/Controllers/Api/Admin/Setting/SettingController.php new file mode 100644 index 0000000..ee4ece7 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/Setting/SettingController.php @@ -0,0 +1,101 @@ +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.', + ]); + } +} diff --git a/app/Jobs/Mail/Admin/AdminOrderNotificationJob.php b/app/Jobs/Mail/Admin/AdminOrderNotificationJob.php new file mode 100644 index 0000000..9211781 --- /dev/null +++ b/app/Jobs/Mail/Admin/AdminOrderNotificationJob.php @@ -0,0 +1,59 @@ +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, + ]); + } +} diff --git a/app/Mail/Admin/AdminOrderNotification.php b/app/Mail/Admin/AdminOrderNotification.php new file mode 100644 index 0000000..d6aa119 --- /dev/null +++ b/app/Mail/Admin/AdminOrderNotification.php @@ -0,0 +1,39 @@ +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 []; + } +} diff --git a/app/Services/Api/Order/OrderService.php b/app/Services/Api/Order/OrderService.php index a260af9..345bb5a 100644 --- a/app/Services/Api/Order/OrderService.php +++ b/app/Services/Api/Order/OrderService.php @@ -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. * diff --git a/config/cors.php b/config/cors.php index 57f0207..ed99d99 100644 --- a/config/cors.php +++ b/config/cors.php @@ -14,7 +14,7 @@ return [ | */ - 'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout'], + 'paths' => ['api/*', 'v1/*', 'sanctum/csrf-cookie', 'login', 'logout'], 'allowed_methods' => ['*'], diff --git a/database/seeders/Setting.php b/database/seeders/Setting.php index 1046249..d74475b 100644 --- a/database/seeders/Setting.php +++ b/database/seeders/Setting.php @@ -29,8 +29,11 @@ class Setting extends Seeder 'company' => 'Thế giới số Portal', 'bot_id' => env('TELEGRAM_BOT_ID'), 'bot_token' => env('TELEGRAM_BOT_TOKEN'), + 'telegram_group_id' => env('TELEGRAM_GROUP_ID', ''), 'status' => 1, 'address' => 'HCM', + 'hotline' => '', + 'admin_email' => env('ADMIN_NOTIFY_EMAIL', ''), 'support' => null, 'brand_color' => '#EF6824' ], JSON_THROW_ON_ERROR) diff --git a/resources/views/emails/admin/order-notification.blade.php b/resources/views/emails/admin/order-notification.blade.php new file mode 100644 index 0000000..5972feb --- /dev/null +++ b/resources/views/emails/admin/order-notification.blade.php @@ -0,0 +1,127 @@ + + +
+ + +Một đơn hàng mới vừa được khách hàng tạo và thanh toán thành công trên hệ thống.
+ +| Tên khách hàng | +{{ $customer_name ?? 'N/A' }} | +
| {{ $customer_email ?? 'N/A' }} | +|
| IP đặt hàng | +{{ $client_ip ?? 'N/A' }} | +
| Mã đơn hàng | +#{{ $order_num ?? 'N/A' }} | +
| Sản phẩm | +{{ $product_name ?? 'N/A' }} | +
| Loại dịch vụ | +{{ $product_type ?? 'N/A' }} | +
| Chu kỳ thanh toán | +{{ $billing_cycle ?? 'N/A' }} | +
| Tổng tiền | +{{ number_format($total ?? 0, 0, ',', '.') }} VNĐ | +
| Trạng thái | +Đã thanh toán | +
| Thời gian | +{{ $created_at ?? now()->format('d/m/Y H:i:s') }} | +
| Username | +{{ $username }} | +
| Password | +{{ $password }} | +
| IP Address | +{{ $ip }} | +
+ Email này được gửi tự động bởi hệ thống. Vui lòng không trả lời email này. +
+