feat: Reorganize database migrations for module snapshots, VMware, and OS groups, and implement customer order creation email notifications.

This commit is contained in:
pbao9
2026-03-12 12:31:52 +07:00
parent 1934be9a19
commit 508d5186db
19 changed files with 350 additions and 195 deletions
+21 -3
View File
@@ -43,17 +43,28 @@ class OrderResource extends JsonResource
'status' => $this->invoice->status,
'date' => $this->invoice->date,
'due_date' => $this->invoice->due_date,
'date_paid' => $this->invoice->date_paid,
'paid_at' => $this->invoice->paid_at,
]),
'amount' => $this->amount,
'total' => $this->total,
'payment_method' => $this->whenLoaded('payment', fn() => $this->payment->name),
'payment_proof' => $this->payment_proof,
'note' => $this->note,
'client_ip' => $this->client_ip,
'status' => $this->status,
'date' => $this->date,
'paid_at' => $this->paid_at,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'vmwares' => $this->whenLoaded('moduleVmwares', fn() => $this->moduleVmwares->map(fn($vm) => [
'id' => $vm->id,
'vmware_id' => $vm->vmware_id,
'name' => $vm->name,
'hostname' => $vm->hostname,
'user' => $vm->user,
'ip' => $vm->ipAddress?->ip,
'os_id' => $vm->os_id,
'status' => $vm->status,
'next_due_date' => $vm->next_due_date,
])),
'order_items' => $this->whenLoaded('orderItems', fn() => $this->orderItems->map(function ($item) {
return [
'id' => $item->id,
@@ -73,6 +84,13 @@ class OrderResource extends JsonResource
'quantity' => $item->quantity,
'server_name' => $item->server_name,
'price' => $item->price,
'service' => $item->service ? [
'id' => $item->service->id,
'vm_id' => $item->service->vm_id,
'status' => $item->service->status,
'started_at' => $item->service->started_at,
'expires_at' => $item->service->expires_at,
] : null,
];
})),
];
@@ -0,0 +1,39 @@
<?php
namespace App\Jobs\Mail\Customer;
use App\Mail\Customer\OrderCreated;
use App\Models\Template;
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 OrderCreatedJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public readonly array $data) {}
public function handle(): void
{
$template = Template::where('name', 'Order Created')->where('status', 1)->first();
if (!$template) {
Log::warning('OrderCreatedJob: Template "Order Created" not found or inactive.');
return;
}
try {
Mail::to($this->data['customer_email'])->send(new OrderCreated($template, $this->data));
} catch (\Throwable $e) {
Log::error('OrderCreatedJob: Failed to send email. ' . $e->getMessage(), [
'customer_email' => $this->data['customer_email'],
'order_num' => $this->data['order_num'] ?? null,
]);
}
}
}
@@ -14,6 +14,7 @@ use App\Models\ModuleVmware;
use App\Models\ModuleIpAddress;
use App\Models\IpPool;
use App\Models\Log as CustomerLog;
use App\Models\Service;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -255,6 +256,22 @@ class CreateVirtualMachineJob implements ShouldQueue
AssignIPJob::dispatch($vmId, $moduleVmware->id, $moduleIpAddress->ip)
->delay(now()->addMinutes(5));
// Lưu Service record để track dịch vụ của khách hàng
Service::create([
'customer_id' => $customer->id,
'product_id' => $this->item->product_id,
'order_item_id' => $this->item->id,
'invoice_id' => $invoice?->id,
'vm_id' => $vmId,
'server_name' => $serverName,
'status' => 'active',
'auto_renew' => false,
'started_at' => now(),
'expires_at' => $this->item->months > 0
? now()->addMonths($this->item->months)
: null,
]);
} catch (\Exception $e) {
Log::error('❌ Lỗi khi lưu dữ liệu VM và IP address: ' . $e->getMessage(), [
'vmware_id' => $vmId,
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace App\Mail\Customer;
use App\Models\Template;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class OrderCreated extends Mailable
{
use Queueable, SerializesModels;
protected Template $template;
protected array $data;
public function __construct(Template $template, array $data)
{
$this->template = $template;
$this->data = $data;
}
public function envelope(): Envelope
{
return new Envelope(
subject: $this->template->subject,
);
}
public function content(): Content
{
$message = str_replace(
[
'{\$company_name}',
'{\$company_logo}',
'{\$client_name}',
'{\$order_num}',
'{\$order_total}',
'{\$invoice_num}',
],
[
$this->data['company_name'],
$this->data['company_logo'],
$this->data['client_name'],
$this->data['order_num'],
$this->data['order_total'],
$this->data['invoice_num'],
],
$this->template->message
);
return new Content(
view: 'emails.template',
with: ['template' => $message],
);
}
public function attachments(): array
{
return [];
}
}
+6
View File
@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\ModuleVmware;
class Order extends Model
{
@@ -59,4 +60,9 @@ class Order extends Model
{
return $this->belongsTo(Payment::class);
}
public function moduleVmwares()
{
return $this->hasMany(ModuleVmware::class, 'order_id');
}
}
+10
View File
@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class OrderItem extends Model
{
@@ -31,6 +32,15 @@ class OrderItem extends Model
return $this->belongsTo(Product::class, 'product_id');
}
public function getProductAttribute(): ?Product
{
if ($this->relationLoaded('product')) {
return $this->getRelation('product');
}
return $this->product()->first();
}
public function pricing()
{
return $this->belongsTo(ProductPricings::class, 'product_pricing_id');
@@ -139,6 +139,8 @@ class OrderRepository extends BaseRepository implements OrderRepositoryInterface
return Order::with([
'orderItems.product',
'orderItems.os',
'orderItems.service',
'moduleVmwares.ipAddress',
'user',
'customer',
'payment',
+28 -9
View File
@@ -5,6 +5,7 @@ namespace App\Services;
use App\Enums\Product\ProductBillingCycle;
use App\Http\Helpers\Helper;
use App\Jobs\Hosting\CreateHostingJobs;
use App\Jobs\Mail\Customer\OrderCreatedJob;
use App\Jobs\VirtualMachine\Create\CreateVirtualMachineJob;
use App\Models\Customer;
use App\Models\Order;
@@ -37,20 +38,25 @@ class OrderService
}
$paymentRecord = Payment::find((int) $data['payment_method']);
$isBankTransfer = $paymentRecord && (int) $paymentRecord->type === 1;
$isBalancePayment = $paymentRecord && (int) $paymentRecord->type === 0;
// Balance payment → auto-confirm order & invoice
$effectiveStatus = $isBalancePayment ? 1 : (int) $data['status'];
[$productItems, $relIdData, $total] = $this->buildProductItems($data);
$orderTotal = (int) ($total + ($total * (10 / 100)));
$invoiceData = [
'invoice_number' => Helper::generateUniqueCode(),
'due_date' => Carbon::today()->toDateString(),
'paid_at' => (int) $data['status'] === 1 ? Carbon::today()->toDateString() : null,
'paid_at' => $effectiveStatus === 1 ? Carbon::now() : null,
'sub_total' => $total,
'tax' => $total * (10 / 100),
'total' => $total + ($total * (10 / 100)),
'total' => $orderTotal,
'payment_method' => $paymentRecord?->id,
'customer_id' => $data['customer_id'],
'status' => $data['status'],
'status' => $effectiveStatus,
];
$orderData = [
@@ -58,15 +64,15 @@ class OrderService
'customer_id' => $data['customer_id'],
'discount_id' => $data['discount_id'] ?? null,
'sub_total' => $total,
'total' => $total + ($total * (10 / 100)),
'total' => $orderTotal,
'payment_method' => $paymentRecord?->code ?? null,
'payment_proof' => $data['payment_proof'] ?? null,
'user_id' => Auth::guard('admin')->id(),
'status' => $data['status'],
'status' => $effectiveStatus,
'client_ip' => request()->getClientIp(),
];
return DB::transaction(function () use ($invoiceData, $orderData, $productItems, $relIdData, $data, $total, $isBankTransfer) {
$order = DB::transaction(function () use ($invoiceData, $orderData, $productItems, $relIdData, $data, $orderTotal, $isBalancePayment) {
$invoice = $this->invoiceRepository->create($invoiceData);
$productItems = array_map(fn ($item) => array_merge($item, ['invoice_id' => $invoice->id]), $productItems);
@@ -77,6 +83,8 @@ class OrderService
$order = $this->orderRepository->create($orderData);
$invoice->update(['order_id' => $order->id]);
$relIdData = array_map(function ($item) use ($order) {
$item['data']['order_id'] = $order->id;
return $item;
@@ -116,8 +124,7 @@ class OrderService
}
}
if (!$isBankTransfer) {
$orderTotal = (int) ($total + ($total * (10 / 100)));
if ($isBalancePayment) {
$customer = Customer::lockForUpdate()->find($data['customer_id']);
if ($customer->total < $orderTotal) {
@@ -129,6 +136,18 @@ class OrderService
return $order;
});
OrderCreatedJob::dispatch([
'customer_email' => $customer->email,
'client_name' => $customer->fullname,
'order_num' => $orderData['order_num'],
'invoice_num' => $invoiceData['invoice_number'],
'order_total' => number_format($orderTotal, 0, ',', '.'),
'company_name' => setting('general.company_name') ?: config('app.name'),
'company_logo' => setting('general.company_logo') ?: '',
]);
return $order;
}
private function buildProductItems(array $data): array