Files
portal-api/app/Console/Commands/ServiceExpiry.php
T

139 lines
5.8 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Enums\Invoice\InvoiceStatus;
use App\Helpers\OrderHelper;
use App\Jobs\VirtualMachine\Action\VMJobs;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Service;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ServiceExpiry extends Command
{
protected $signature = 'service:expiry';
protected $description = 'Suspend expired services, power off VMs, create renewal invoices, terminate after 30 days';
public function handle(): int
{
// Note: 'invoice' is NOT eager-loaded — createRenewalInvoiceIfMissing queries fresh
$expired = Service::query()
->with(['orderItem.product'])
->whereNotNull('expires_at')
->where('expires_at', '<', now())
->whereNotIn('status', ['terminated', 'cancelled'])
->get();
$this->info("service:expiry | Found {$expired->count()} expired service(s).");
foreach ($expired as $service) {
try {
// Fix: diffInDays is always positive; guard with greaterThan to avoid edge case at exact boundary
$daysExpired = now()->greaterThan($service->expires_at)
? (int) now()->diffInDays($service->expires_at)
: 0;
// --- 30-day threshold: terminate (idempotent) ---
if ($daysExpired >= 30) {
if ($service->status !== 'terminated') {
$service->update(['status' => 'terminated']);
Log::info('Service terminated', ['service_id' => $service->id, 'days_overdue' => $daysExpired]);
$this->line(" [terminated] Service #{$service->id}");
}
continue;
}
// Capture before transaction so dispatch is conditional and outside the transaction
$needsVmPowerOff = $service->status !== 'suspended' && $service->vm_id;
// --- Transaction: status update + invoice creation (atomic) ---
DB::transaction(function () use ($service) {
if ($service->status !== 'suspended') {
$service->update([
'status' => 'suspended',
'suspended_at' => now(),
]);
Log::info('Service suspended', ['service_id' => $service->id]);
$this->line(" [suspended] Service #{$service->id}");
}
// Optional: note auto_renew intent (auto-payment not implemented)
if ($service->auto_renew) {
Log::info('Service has auto_renew=true; renewal invoice will be created for manual or future auto-payment', ['service_id' => $service->id]);
}
$this->createRenewalInvoiceIfMissing($service);
});
// --- Dispatch VMJobs OUTSIDE transaction (safe: DB is committed) ---
if ($needsVmPowerOff) {
dispatch(new VMJobs((int) $service->vm_id, 'PowerOff', false));
Log::info('PowerOff dispatched', ['service_id' => $service->id, 'vm_id' => $service->vm_id]);
}
} catch (\Throwable $e) {
Log::error('service:expiry error', ['service_id' => $service->id, 'error' => $e->getMessage()]);
$this->error(" [error] Service #{$service->id}: " . $e->getMessage());
}
}
$this->info("service:expiry | Done.");
return Command::SUCCESS;
}
private function createRenewalInvoiceIfMissing(Service $service): void
{
// Fresh DB query via invoice_id FK — does NOT rely on stale eager-loaded relation.
// invoices table has no service_id column; service->invoice_id is the correct pointer.
$hasUnpaidRenewal = $service->invoice_id && Invoice::where('id', $service->invoice_id)
->where('invoice_type', 2)
->where('status', InvoiceStatus::UNPAID->value)
->exists();
if ($hasUnpaidRenewal) {
return;
}
$orderItem = $service->orderItem;
if (!$orderItem || (int) $orderItem->price <= 0) {
Log::warning('service:expiry | Cannot create renewal invoice: no order item or zero price', ['service_id' => $service->id]);
return;
}
$price = (int) $orderItem->price;
$invoice = Invoice::create([
'invoice_number' => OrderHelper::generateOrderNumber('INV'),
'customer_id' => $service->customer_id,
'order_id' => null,
'invoice_type' => 2, // renewal
'sub_total' => $price,
'discount_amount' => 0,
'tax' => 0,
'total' => $price,
'status' => InvoiceStatus::UNPAID->value,
'due_date' => now()->addDays(30)->toDateString(),
]);
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' => $orderItem->months ?? 1,
'quantity' => 1,
'unit_price' => $price,
'total_price' => $price,
]);
// Update service to point to the new renewal invoice
$service->update(['invoice_id' => $invoice->id]);
Log::info('Renewal invoice created', ['service_id' => $service->id, 'invoice_number' => $invoice->invoice_number]);
$this->line(" [invoice] Renewal {$invoice->invoice_number} for Service #{$service->id}.");
}
}