This commit is contained in:
pbao9
2026-01-27 17:24:48 +07:00
parent 057ba58760
commit d2532bcddd
20 changed files with 870 additions and 189 deletions
@@ -23,6 +23,7 @@ use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Validator as ValidatorInstance;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Validator as FacadesValidator;
use Illuminate\Support\Facades\DB;
class ServiceController extends Controller
{
@@ -628,78 +629,7 @@ class ServiceController extends Controller
}
}
/**
* Renew service for selected months
*/
public function renew(Request $request)
{
try {
$user = customer();
if (!$user) {
return response()->json([
'error' => true,
'message' => __('Chưa đăng nhập. Vui lòng đăng nhập để sử dụng tính năng này.'),
], 401);
}
$validated = FacadesValidator::make($request->all(), [
'product_id' => 'required|string|max:255',
'plan' => 'required|string|in:3,6,12,24,other',
'months' => 'nullable|integer|min:1|max:60',
]);
if ($validated->fails()) {
return response()->json([
'error' => true,
'message' => __('Dữ liệu không hợp lệ'),
'errors' => $validated->errors(),
], 422);
}
$months = $request->plan === 'other' ? (int) ($request->months ?? 1) : (int) $request->plan;
$productId = $this->sanitizeInput($request->product_id);
// Fetch product price based on invoice item link
$invoiceItem = InvoiceItem::whereHas('invoice', function ($q) use ($user) {
$q->where('customer_id', $user->id);
})
->where('product_type', 4) // VPS
->whereHas('invoice', fn($q) => $q->where('status', '!=', 0))
->first();
$product = $invoiceItem?->getProductAttribute();
$unitPrice = (int) ($product?->price_sale ?? $product?->price ?? 0);
$total = $unitPrice * max($months, 1);
Log::info('Khách yêu cầu gia hạn dịch vụ', [
'user_id' => $user->id,
'product_vmware_id' => $productId,
'months' => $months,
'unit_price' => $unitPrice,
'total' => $total,
]);
// TODO: implement actual renewal billing/workflow
return response()->json([
'error' => false,
'message' => __('Yêu cầu gia hạn đã được ghi nhận'),
'data' => [
'unit_price' => $unitPrice,
'months' => $months,
'total' => $total,
],
]);
} catch (\Exception $e) {
Log::error('Lỗi renew', [
'user_id' => customer()?->id,
'error' => $e->getMessage(),
]);
return response()->json([
'error' => true,
'message' => __('Có lỗi xảy ra khi gia hạn'),
], 500);
}
}
/**
* Secure remove snapshot using session authentication - NO BEARER TOKEN
@@ -1316,4 +1246,238 @@ class ServiceController extends Controller
return $processed;
}
/**
* Calculate renewal price (AJAX)
*/
public function renewPrice(Request $request)
{
try {
$user = customer();
if (!$user) {
return response()->json(['error' => true, 'message' => __('Vui lòng đăng nhập.')], 401);
}
$vmware_id = $this->sanitizeInput($request->product_id);
$plan = $request->input('plan');
$months = $plan === 'other' ? (int) $request->input('months') : (int) $plan;
if ($months < 1)
$months = 1;
$vm = ModuleVmware::where('vmware_id', $vmware_id)
->where('customer_id', $user->id)
->first();
if (!$vm) {
return response()->json(['error' => true, 'message' => __('Không tìm thấy dịch vụ.')], 404);
}
$order = Order::find($vm->order_id);
if (!$order) {
return response()->json(['error' => true, 'message' => __('Không tìm thấy đơn hàng gốc.')], 404);
}
// Find OrderItem in order__order_item
// Rule: product_type = 4 (VPS) or 3 (Hosting)
// Ideally we match the product_id too, but ModuleVmware may not store original product_id directly except via relation?
// ModuleVmware has no 'product_id' column in the schema we saw, but it has 'order_id'.
// Let's assume we look for the item that created this VM.
// Since we are in ServiceController for VM, we look for Type 4 (VPS).
$orderItem = \App\Models\OrderItem::where('order_id', $order->id)
->where('product_type', 4) // VPS
->first();
if (!$orderItem) {
// Fallback or error
return response()->json(['error' => true, 'message' => __('Không tìm thấy thông tin sản phẩm trong đơn hàng.')], 404);
}
// Get Product Price
$product = null;
if ($orderItem->product_type == 4) {
$product = \App\Models\VPS::find($orderItem->product_id);
} elseif ($orderItem->product_type == 3) {
$product = \App\Models\Hosting::find($orderItem->product_id);
}
if (!$product) {
return response()->json(['error' => true, 'message' => __('Sản phẩm không còn tồn tại.')], 404);
}
// Priority: Price Sale -> Price
$base_unit_price = (float) ($product->price_sale > 0 ? $product->price_sale : $product->price);
// Calculate Addons Price
// Look into order__order_addons via OrderItem relationship
$addons_unit_price = 0;
if ($orderItem->addons && $orderItem->addons->count() > 0) {
foreach ($orderItem->addons as $addonPivot) {
// $addonPivot is OrderAddon model
// We need current price of the addon? User said "lấy giá đã lưu" (take stored price)
// BUT for renewal, usually we take CURRENT price of the addon or the STORED price?
// User said: "lấy sản order hiện tại kèm các addon nếu có lấy giá đã lưu * với số tháng"
// "giá đã lưu" implies the price stored in the order/product table?
// But then user said: "nếu product có price_sale thì ưu tiên lấy price_sale còn lại thì lấy price".
// This implies looking at the ACTUAL Product table for the base Item.
// For Addons: "kèm các addon nếu có".
// Let's look at the Addon model (Current Price).
if ($addonPivot->addonItems) {
$addons_unit_price += (float) ($addonPivot->addonItems->price);
}
}
}
$total_unit_price = $base_unit_price + $addons_unit_price;
$total = $total_unit_price * $months;
return response()->json([
'error' => false,
'data' => [
'unit_price' => $total_unit_price,
'months' => $months,
'total' => $total
]
]);
} catch (\Exception $e) {
Log::error("Error calculating renew price: " . $e->getMessage());
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
}
}
/**
* Renew VPS Service
*/
public function renew(Request $request)
{
try {
$user = customer();
if (!$user) {
return redirect()->route('user.login')->with('error', __('Vui lòng đăng nhập.'));
}
// 1. Validate Input
$validator = FacadesValidator::make($request->all(), [
'product_id' => 'required|string',
'plan' => 'required',
'months' => 'nullable|integer|min:2|max:60'
]);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput()->with('error', __('Dữ liệu không hợp lệ.'));
}
$vmware_id = $this->sanitizeInput($request->product_id);
$plan = $request->input('plan');
$months = $plan === 'other' ? (int) $request->input('months') : (int) $plan;
if ($months < 2) {
return redirect()->back()->with('error', __('Thời hạn gia hạn tối thiểu là 2 tháng.'));
}
// 2. Find Service & Verify Ownership
$vm = ModuleVmware::where('vmware_id', $vmware_id)
->where('customer_id', $user->id)
->first();
if (!$vm) {
return redirect()->back()->with('error', __('Không tìm thấy dịch vụ hoặc bạn không có quyền truy cập.'));
}
// 3. Calculate Price based on Original Order -> OrderItem
$order = Order::find($vm->order_id);
if (!$order) {
return redirect()->back()->with('error', __('Không tìm thấy đơn hàng gốc của dịch vụ này.'));
}
$orderItem = \App\Models\OrderItem::where('order_id', $order->id)
->where('product_type', 4) // VPS
->first();
if (!$orderItem) {
return redirect()->back()->with('error', __('Không tìm thấy thông tin sản phẩm trong đơn hàng.'));
}
// Get Product Price
$product = null;
if ($orderItem->product_type == 4) {
$product = \App\Models\VPS::find($orderItem->product_id);
} elseif ($orderItem->product_type == 3) {
$product = \App\Models\Hosting::find($orderItem->product_id);
}
if (!$product) {
return redirect()->back()->with('error', __('Sản phẩm không còn tồn tại.'));
}
// Priority: Price Sale -> Price
$base_unit_price = (float) ($product->price_sale > 0 ? $product->price_sale : $product->price);
// Calculate Addons Price
$addons_unit_price = 0;
if ($orderItem->addons && $orderItem->addons->count() > 0) {
foreach ($orderItem->addons as $addonPivot) {
if ($addonPivot->addonItems) {
$addons_unit_price += (float) ($addonPivot->addonItems->price);
}
}
}
$total_unit_price = $base_unit_price + $addons_unit_price;
$total_cost = $total_unit_price * $months;
// 4. Check Balance
if ($user->credit < $total_cost) {
return redirect()->back()->with('error', __('Số dư tài khoản không đủ để gia hạn (Cần: :amount). Vui lòng nạp thêm tiền.', ['amount' => number_format($total_cost) . ' VNĐ']));
}
// 5. Process Renewal
DB::beginTransaction();
try {
// Deduct Credit
$user->decrement('credit', $total_cost);
// Create Renewal Invoice (Paid)
$newInvoice = Invoice::create([
'customer_id' => $user->id,
'status' => 1, // Paid
'date' => now()->timestamp, // Using timestamp as per schema
'due_date' => now()->timestamp,
'total' => $total_cost,
// 'payment_method' => 'credit_balance', // Schema doesn't have this, maybe note?
'note' => "Gia hạn dịch vụ VPS #{$vm->name} ({$months} tháng)"
]);
// Update VM Expiry
$current_due = $vm->next_due_date ? \Carbon\Carbon::parse($vm->next_due_date) : now();
$new_due_date = $current_due->copy()->addMonths($months);
$vm->update([
'next_due_date' => $new_due_date,
'status' => 1,
]);
// TODO: Update OrderItem duration/dates if needed? Or just create new invoice record?
// The prompt was about "gia hạn thêm sản phẩm" (add renewal product?), but generally it just extends the service.
DB::commit();
// Clear cache
Cache::forget("vm_info_{$vm->vmware_id}");
return redirect()->back()->with('success', __('Gia hạn dịch vụ thành công! Hạn sử dụng mới: :date', ['date' => $new_due_date->format('d/m/Y')]));
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
} catch (\Exception $e) {
Log::error("Error renewing VM: " . $e->getMessage());
return redirect()->back()->with('error', __('Có lỗi xảy ra khi gia hạn dịch vụ. Vui lòng thử lại sau.'));
}
}
}
@@ -0,0 +1,158 @@
/**
* Renew VPS Service
*/
public function renew(Request $request)
{
try {
$user = customer();
if (!$user) {
return redirect()->route('user.login')->with('error', __('Vui lòng đăng nhập.'));
}
// 1. Validate Input
$validator = FacadesValidator::make($request->all(), [
'product_id' => 'required|string',
'plan' => 'required',
'months' => 'nullable|integer|min:2|max:60'
]);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput()->with('error', __('Dữ liệu không hợp lệ.'));
}
$vmware_id = $this->sanitizeInput($request->product_id);
$plan = $request->input('plan');
$months = $plan === 'other' ? (int)$request->input('months') : (int)$plan;
if ($months < 2) {
return redirect()->back()->with('error', __('Thời hạn gia hạn tối thiểu là 2 tháng.'));
}
// 2. Find Service & Verify Ownership
$vm = ModuleVmware::where('vmware_id', $vmware_id)
->where('customer_id', $user->id)
->first();
if (!$vm) {
return redirect()->back()->with('error', __('Không tìm thấy dịch vụ hoặc bạn không có quyền truy cập.'));
}
// 3. Calculate Price based on Original Order/InvoiceItem
// Strategy: Find the InvoiceItem from the Order associated with this VM.
// If multiple items, try to match type VPS (4).
$order = Order::find($vm->order_id);
if (!$order) {
// Fallback if order missing (rare)
// Try finding by invoice_id if stored on VM? ModuleVmware has next_invoice but not current_invoice.
return redirect()->back()->with('error', __('Không tìm thấy đơn hàng gốc của dịch vụ này.'));
}
// Find InvoiceItem
// Order has relationship to Invoice (order->invoice)
// Or Order has orderItems (which are InvoiceItems)
$invoiceItem = null;
if ($order->invoice) {
// Try finding item in the invoice related to this order
// Note: The modal uses $invoice->invoiceItem->first(). We should try to be specific.
// We don't have a direct link from ModuleVmware to InvoiceItem.
// But ModuleVmware links to Order, which links to items.
// Let's take the first item of type 4 (VPS) from the order's invoice.
$invoiceItem = $order->invoice->invoiceItem->where('type_id', 4)->first();
if (!$invoiceItem) {
$invoiceItem = $order->invoice->invoiceItem->first();
}
}
if (!$invoiceItem) {
// Extreme fallback or failure
return redirect()->back()->with('error', __('Không tìm thấy thông tin giá của dịch vụ.'));
}
// Calculate Unit Price (Monthly)
// 'amount' in InvoiceItem is total price for 'months' duration.
$billing_months = $invoiceItem->months > 0 ? $invoiceItem->months : 1;
if ($invoiceItem->amount <= 0) {
// If amount is 0 (free?), unit price is 0.
$base_unit_price = 0;
} else {
$base_unit_price = $invoiceItem->amount / $billing_months;
}
// Calculate Addons Unit Price
$addons_unit_price = 0;
if ($invoiceItem->addons && $invoiceItem->addons->count() > 0) {
foreach ($invoiceItem->addons as $addonPivot) {
// Try to find price. Assuming Addon model has current price.
// Ideally we should use the price stored in pivot if available, but pivot structure is unknown.
// We'll use the Addon catalog price as fallback or primary if pivot doesn't store it.
// $addonPivot->addonItems returns the Addon model.
if ($addonPivot->addonItems) {
$addons_unit_price += $addonPivot->addonItems->price;
}
}
}
$total_unit_price = $base_unit_price + $addons_unit_price;
$total_cost = $total_unit_price * $months;
// 4. Check Balance
if ($user->credit < $total_cost) {
return redirect()->back()->with('error', __('Số dư tài khoản không đủ để gia hạn (Cần: :amount). Vui lòng nạp thêm tiền.', ['amount' => number_format($total_cost) . ' VNĐ']));
}
// 5. Process Renewal
DB::beginTransaction();
try {
// Deduct Credit
$user->decrement('credit', $total_cost);
// Create Renewal Invoice (Paid) - Optional but good for history
$newInvoice = Invoice::create([
'customer_id' => $user->id,
'status' => 1, // Paid
'date' => now(),
'due_date' => now(),
'total' => $total_cost,
'payment_method' => 'credit_balance', // or similar
'description' => "Gia hạn dịch vụ VPS #{$vm->name} ({$months} tháng)"
]);
// Update VM Expiry
// If current expiry is in the past, renew from NOW? Or from expiry?
// Usually from expiry if it's recent grace period. If long expired, maybe from now.
// Let's simply add to next_due_date.
$current_due = $vm->next_due_date ? \Carbon\Carbon::parse($vm->next_due_date) : now();
if ($current_due->isPast()) {
// If expired, maybe we should start from today?
// Or just extend the gap. User usually pays for the gap.
// Let's extend from current_due to maintain billing cycle.
}
$new_due_date = $current_due->copy()->addMonths($months);
$vm->update([
'next_due_date' => $new_due_date,
'status' => 1, // Ensure active if it was suspended
]);
// Log or Notify (Optional)
DB::commit();
// Clear cache
Cache::forget("vm_info_{$vm->vmware_id}");
return redirect()->back()->with('success', __('Gia hạn dịch vụ thành công! Hạn sử dụng mới: :date', ['date' => $new_due_date->format('d/m/Y')]));
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
} catch (\Exception $e) {
Log::error("Error renewing VM: " . $e->getMessage());
return redirect()->back()->with('error', __('Có lỗi xảy ra khi gia hạn dịch vụ. Vui lòng thử lại sau.'));
}
}
@@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class WaitStatusVirtualMachine implements ShouldQueue
{
@@ -39,18 +40,75 @@ class WaitStatusVirtualMachine implements ShouldQueue
*/
public function handle()
{
Log::info("WaitStatusVirtualMachine: Started for VM {$this->vmware_id}, Target Status: " . ($this->status ? 'True' : 'False'));
$status = $this->serviceVCetner->CheckVirtualMachine($this->vmware_id);
if ($status['error'] === true) {
Log::error("WaitStatusVirtualMachine: Error checking status for VM {$this->vmware_id}: " . json_encode($status['message']));
throw new \Exception(json_encode($status['message']));
}
Log::info("WaitStatusVirtualMachine: Initial operations_ready: " . ($status['data']['operations_ready'] ? 'True' : 'False'));
// Wait loop: "Wait while operations_ready matches $this->status"
// Example: Stop Job passes true. We wait while it is true. When it becomes false (Off), we exit.
if ($status['data']['operations_ready'] === $this->status) {
Log::info("WaitStatusVirtualMachine: Entering wait loop...");
do {
sleep(10);
$status = $this->serviceVCetner->CheckVirtualMachine($this->vmware_id);
if ($status['error'] === true) {
Log::warning("WaitStatusVirtualMachine: Failed to check status in loop. Breaking.");
// If check fails, maybe network glitch? We can break or continue.
// Let's break to avoid infinite loop on error
break;
}
Log::info("WaitStatusVirtualMachine: Loop check - operations_ready: " . ($status['data']['operations_ready'] ? 'True' : 'False'));
} while ($status['data']['operations_ready'] === $this->status);
} else {
Log::info("WaitStatusVirtualMachine: Skipping wait loop, condition already met.");
}
Event(new VirtualMachine_Panel_Status('Yêu cầu được xử lý thành công',$this->user));
// Action Completed
// 1. Clear Cache again (to ensure next fetch gets new state)
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $this->vmware_id);
// 2. Update DB Status and Dispatch Event
$product = \App\Models\ModuleVmware::where('vmware_id', $this->vmware_id)->first();
if ($product) {
Log::info("WaitStatusVirtualMachine: Product found. Updating status...");
// If waiting for True (On) to become False (Off) -> New Status 0 (Off)
// If waiting for False (Off) to become True (On) -> New Status 1 (Active)
$newStatus = ($this->status === true) ? 0 : 1;
Log::info("WaitStatusVirtualMachine: Setting status to $newStatus");
$product->update(['status' => $newStatus]);
$msg = ($newStatus == 1)
? __('Máy ảo đã hoạt động trở lại.')
: __('Máy ảo đã tắt hoàn toàn.');
$statusStr = ($newStatus == 1) ? 'running' : 'stopped';
Log::info("WaitStatusVirtualMachine: Dispatching VirtualMachineStatusChanged event.");
\App\Events\VirtualMachineStatusChanged::dispatch(
$this->user,
$this->vmware_id,
$statusStr,
$msg
);
} else {
Log::error("WaitStatusVirtualMachine: Product not found for vmware_id: {$this->vmware_id}");
}
// Legacy event (optional, keep to avoid breaking other things if they listen to it)
Event(new VirtualMachine_Panel_Status('Yêu cầu được xử lý thành công', $this->user));
}
}
@@ -59,7 +59,15 @@ class RebootVmJob implements ShouldQueue
// Clear VM info cache
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $product->vmware_id);
$product->update(['status' => 3]); // Processing
// Dispatch Event to Frontend
\App\Events\VirtualMachineStatusChanged::dispatch(
$this->userId,
$product->vmware_id,
'rebooted',
__('Máy ảo đã được khởi động lại thành công.')
);
$product->update(['status' => 3]); // Processing
// In VMJobs: $jobs = new WaitStatusVirtualMachine($product->vmware_id,$serviceVCenter,$product->customer_id,false);
+10 -2
View File
@@ -60,8 +60,16 @@ class StartVmJob implements ShouldQueue
// Clear VM info cache
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $product->vmware_id);
// Update status to 'processing' (e.g., 3) if needed, or leave it to WaitStatus
// Dispatch Event to Frontend
\App\Events\VirtualMachineStatusChanged::dispatch(
$this->userId,
$product->vmware_id,
'running',
__('Máy ảo đã được khởi động thành công.')
);
// Update status to 'processing' (e.g., 3) if needed, or leave it to WaitStatus
$product->update(['status' => 3]); // 3 = Processing/Pending
// Dispatch wait status job
+9 -1
View File
@@ -59,7 +59,15 @@ class StopVmJob implements ShouldQueue
// Clear VM info cache
\Illuminate\Support\Facades\Cache::forget('vm_info_' . $product->vmware_id);
$product->update(['status' => 3]); // Processing
// Dispatch Event to Frontend
\App\Events\VirtualMachineStatusChanged::dispatch(
$this->userId,
$product->vmware_id,
'stopped',
__('Máy ảo đã được tắt thành công.')
);
$product->update(['status' => 3]); // Processing
dispatch(new WaitStatusVirtualMachine(
$product->vmware_id,