Add service renewal functionality and command for expired services

This commit is contained in:
pbao9
2026-04-15 20:52:11 +07:00
parent dba49c4374
commit 64a9d603e2
6 changed files with 289 additions and 1 deletions
@@ -4,10 +4,14 @@ namespace App\Http\Controllers\Api\Service;
use App\Enums\Invoice\InvoiceStatus;
use App\Enums\Order\OrderStatus;
use App\Helpers\OrderHelper;
use App\Http\Controllers\Controller;
use App\Jobs\VirtualMachine\Action\VMJobs;
use App\Models\Log as CustomerActivityLog;
use App\Http\Requests\Api\Order\PayOrderRequest;
use App\Http\Requests\Api\Order\StoreOrderRequest;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\ModuleVmware;
use App\Models\Order;
use App\Models\Product;
@@ -17,6 +21,7 @@ use App\Services\Api\Order\OrderService;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* @OA\Tag(name="Orders", description="Order & Invoice management")
@@ -442,6 +447,137 @@ class OrderController extends Controller
}
}
// =========================================================================
// Services — renewal
// =========================================================================
/**
* POST /v1/client/dich-vu/{id}/renew
*
* Pay-and-activate renewal in one step:
* 1. Validate balance
* 2. Debit customer balance (inside transaction)
* 3. Create PAID renewal invoice (inside transaction)
* 4. Extend service expires_at + set status = active (inside transaction)
* 5. Dispatch VMJobs 'Start' OUTSIDE transaction to power on suspended VM
*/
public function renewService(Request $request, int $id): JsonResponse
{
try {
$customer = $request->user();
$service = Service::query()
->with(['orderItem.product'])
->where('id', $id)
->where('customer_id', $customer->id)
->whereNotIn('status', ['terminated', 'cancelled'])
->first();
if (!$service) {
return $this->notFound('Không tìm thấy dịch vụ hoặc dịch vụ đã bị hủy.');
}
$orderItem = $service->orderItem;
if (!$orderItem || (int) $orderItem->price <= 0) {
return $this->error('Không thể xác định giá gia hạn dịch vụ.');
}
$validated = $request->validate(['months' => ['required', 'integer', 'min:1', 'max:24']]);
$price = (int) $orderItem->price * (int) $validated['months'];
$months = (int) $validated['months'];
// Validate balance before opening a transaction
if ($customer->total < $price) {
return $this->error(sprintf(
'Số dư không đủ. Hiện có: %s VNĐ, cần: %s VNĐ.',
number_format($customer->total),
number_format($price)
));
}
$needsVmStart = $service->vm_id !== null;
$invoice = DB::transaction(function () use ($customer, $service, $orderItem, $price, $months) {
// 1. Debit balance
$customer->total -= $price;
$customer->save();
// 2. Activity log
CustomerActivityLog::create([
'agent' => 'api',
'client_ip' => request()?->ip(),
'customer_id' => $customer->id,
'description' => sprintf(
'Gia hạn dịch vụ #%s. Số tiền: %s VNĐ. Số dư còn: %s VNĐ.',
$service->server_name ?? $service->id,
number_format($price),
number_format($customer->total)
),
]);
// 3. Create PAID renewal invoice
$invoice = Invoice::create([
'invoice_number' => OrderHelper::generateOrderNumber('INV'),
'customer_id' => $customer->id,
'order_id' => null,
'invoice_type' => 2, // renewal
'sub_total' => $price,
'discount_amount' => 0,
'tax' => 0,
'total' => $price,
'status' => InvoiceStatus::PAID->value,
'paid_at' => now(),
'payment_method' => 'credit',
]);
InvoiceItem::create([
'invoice_id' => $invoice->id,
'product_id' => $service->product_id,
'type' => 'renewal',
'description' => 'Gia hạn dịch vụ: ' . ($orderItem->product?->name ?? $service->server_name ?? ''),
'months' => $months,
'quantity' => 1,
'unit_price' => $price,
'total_price' => $price,
]);
// 4. Extend expires_at from today (or current expires_at if still in future)
$baseDate = $service->expires_at && $service->expires_at->isFuture()
? $service->expires_at
: now();
$expiresAt = Carbon::parse($baseDate)->addMonths($months);
$service->update([
'status' => 'active',
'expires_at' => $expiresAt,
'suspended_at' => null,
'invoice_id' => $invoice->id,
]);
return $invoice;
});
// 5. Start VM outside transaction (DB already committed)
if ($needsVmStart) {
dispatch(new VMJobs((int) $service->vm_id, 'Start', false))
->delay(now()->addSeconds(5));
}
return response()->json([
'success' => true,
'message' => 'Gia hạn thành công. Dịch vụ đã được kích hoạt lại.',
'data' => [
'invoice_number' => $invoice->invoice_number,
'total' => (float) $invoice->total,
'expires_at' => $service->fresh()?->expires_at?->toISOString(),
],
]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
// =========================================================================
// Private — formatters
// =========================================================================