Files
portal-api/app/Services/Customer/CustomerServiceProvisioningService.php
T
2026-09-09 16:56:22 +07:00

396 lines
16 KiB
PHP

<?php
namespace App\Services\Customer;
use App\Enums\Invoice\InvoiceStatus;
use App\Enums\Order\OrderStatus;
use App\Helpers\OrderHelper;
use App\Mail\ServiceRenewalReminderMail;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\ModuleVmware;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
use App\Models\Service;
use App\Models\Transaction;
use App\Models\VirtualMachines;
use App\Repositories\Customer\CustomerRepositoryInterface;
use App\Repositories\Module\Hosting\HostingRepositoryInterface as ModuleHostingRepositoryInterface;
use App\Repositories\Module\Vmware\VmwareRepositoryInterface;
use App\Repositories\Service\ServiceRepositoryInterface;
use App\Services\Customer\Concerns\ResolvesCustomer;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
class CustomerServiceProvisioningService
{
use ResolvesCustomer;
public function __construct(
private readonly CustomerRepositoryInterface $customerRepository,
private readonly ModuleHostingRepositoryInterface $moduleHostingRepository,
private readonly VmwareRepositoryInterface $vmwareRepository,
private readonly ServiceRepositoryInterface $serviceRepository,
) {}
public function getProducts(string $uuid): Collection
{
$customer = $this->resolveCustomer($uuid);
$hostingServices = $this->moduleHostingRepository->filter(['customer_id' => $customer->id], ['server']);
$vpsServices = $this->vmwareRepository->filter(
['customer_id' => $customer->id],
['ipaddress.host.datacenter.vCenter', 'os.osGroup'],
);
$allService = collect();
foreach ($hostingServices as $hosting) {
$allService->push([
'product_id' => $hosting->id,
'product_type' => 3,
'name' => $hosting->name,
'description' => $hosting->name . ' (Hosting)',
]);
}
foreach ($vpsServices as $vps) {
$allService->push([
'product_id' => $vps->id,
'product_type' => 4,
'name' => $vps->name,
'description' => $vps->name . ' (VPS)',
]);
}
return $allService->values();
}
public function getServices(string $uuid): Collection
{
$services = $this->serviceRepository->filter(
['customer_uuid' => $uuid],
['product', 'orderItem', 'invoice', 'vmware.os.osGroup'],
);
ModuleVmware::resolveVirtualMachineIds($services->pluck('vmware'));
return $services;
}
/**
* Xoá nhiều dịch vụ của khách hàng. Chỉ xoá bản ghi trong bảng `services`,
* không xoá VM (module_vmware / virtual_machines) liên kết.
*/
public function bulkDeleteServices(string $uuid, array $ids): int
{
$customer = $this->resolveCustomer($uuid);
return Service::where('customer_id', $customer->id)->whereIn('id', $ids)->delete();
}
/**
* Cập nhật thông tin dịch vụ của khách hàng (server_name/hostname, status, ngày,...).
* Đồng bộ hostname sang module_vmware nếu service đã gắn VM.
*/
public function updateService(string $uuid, int $serviceId, array $data): Service
{
$customer = $this->resolveCustomer($uuid);
$service = Service::where('customer_id', $customer->id)->where('id', $serviceId)->first();
abort_if(!$service, 404, 'Không tìm thấy dịch vụ');
return DB::transaction(function () use ($service, $data) {
$vmFields = ['os_id', 'vm_cpu', 'vm_ram_mb', 'vm_disk_gb', 'vm_ip', 'vm_dns', 'vm_user', 'vm_password', 'vm_port'];
$service->fill(collect($data)->except(['price', ...$vmFields])->all());
$service->save();
if (array_key_exists('price', $data) && $data['price'] !== null && $service->orderItem) {
$service->orderItem->update(['price' => $data['price']]);
}
// Đồng bộ sang module_vmware nếu có gán VM
if ($service->vm_id) {
$vmData = array_filter([
'hostname' => $data['server_name'] ?? null,
'os_id' => $data['os_id'] ?? null,
'cpu' => $data['vm_cpu'] ?? null,
'ram_mb' => $data['vm_ram_mb'] ?? null,
'disk_gb' => $data['vm_disk_gb'] ?? null,
'ip' => $data['vm_ip'] ?? null,
'dns' => $data['vm_dns'] ?? null,
'user' => $data['vm_user'] ?? null,
'password' => $data['vm_password'] ?? null,
'plain_password' => $data['vm_password'] ?? null,
'port' => $data['vm_port'] ?? null,
], fn($v) => $v !== null);
if ($vmData) {
ModuleVmware::findOrFail($service->vm_id)->update($vmData);
}
}
$service->load(['product', 'orderItem', 'invoice', 'vmware.os.osGroup']);
ModuleVmware::resolveVirtualMachineIds([$service->vmware]);
return $service;
});
}
/**
* Gửi email nhắc gia hạn dịch vụ cho khách hàng.
*/
public function notifyRenewal(string $uuid, int $serviceId): void
{
$customer = $this->resolveCustomer($uuid);
$service = Service::where('customer_id', $customer->id)->where('id', $serviceId)
->with(['product', 'orderItem'])
->first();
abort_if(!$service, 404, 'Không tìm thấy dịch vụ');
abort_if(!$customer->email, 422, 'Khách hàng chưa có email');
Mail::to($customer->email)->send(new ServiceRenewalReminderMail($customer, $service));
}
/**
* Create a service for a customer. For VPS products, accepts a
* `virtual_machine_id` from the vCenter inventory and auto-creates the
* matching module_vmware row so `services.vm_id` can be linked.
*/
public function createService(string $uuid, array $data, string $clientIp): Service
{
$customer = $this->resolveCustomer($uuid);
$product = Product::with('pricings')->find($data['product_id']);
abort_if(
$product->product_type === 'vps' && empty($data['virtual_machine_id']),
422,
'VPS service requires a virtual_machine_id',
);
$billingCycle = $data['billing_cycle'] ?? 'monthly';
$months = OrderHelper::cycleToMonths($billingCycle);
$pricing = $product->pricings->where('billing_cycle', $billingCycle)->first();
// Số tiền đã thanh toán: admin nhập tay để khớp với thực tế (có thể lệch giá niêm yết do khuyến mãi/thoả thuận)
$basePrice = isset($data['amount_paid']) ? (int) $data['amount_paid'] : (int) ($pricing->price ?? 0);
$paymentMethod = $data['payment_method'];
// Đối với balance: kiểm tra số dư trước khi vào transaction
abort_if(
$paymentMethod === 'balance' && (int) $customer->total < $basePrice,
422,
sprintf(
'Số dư khách không đủ. Hiện có: %s VNĐ, cần: %s VNĐ.',
number_format((int) $customer->total),
number_format($basePrice),
),
);
return DB::transaction(function () use ($customer, $product, $data, $billingCycle, $months, $basePrice, $paymentMethod, $clientIp) {
// 1) Order + OrderItem (mô phỏng flow customer đặt hàng)
$order = Order::create([
'order_num' => OrderHelper::generateOrderNumber(),
'customer_id' => $customer->id,
'date' => now()->toDateString(),
'sub_total' => $basePrice,
'discount_amount' => 0,
'total' => $basePrice,
'status' => OrderStatus::ACTIVE->value,
'payment_method' => $paymentMethod,
'client_ip' => $clientIp,
]);
$orderItem = OrderItem::create([
'order_id' => $order->id,
'product_id' => $product->id,
'billing_cycle' => $billingCycle,
'months' => $months,
'server_name' => $data['server_name'] ?? null,
'quantity' => 1,
'price' => $basePrice,
]);
// Ngày bắt đầu/hết hạn dùng chung cho cả module_vmware (next_due_date/next_invoice) và services.
$startedAt = $data['started_at'] ?? now();
$expiresAt = $data['expires_at'] ?? Carbon::parse($startedAt)->addMonths($months);
// 2) ModuleVmware: snapshot từ virtual_machines. `order_id` phải trỏ về
// order vừa tạo — client console-ticket/rebuild resolve VM qua
// service->orderItem->order->vmWare, không qua services.vm_id.
$vmId = $this->createModuleVmwareIfNeeded($data, $customer, $order, $basePrice, $expiresAt);
// 3) Invoice — đánh dấu PAID luôn vì admin đứng ra tạo & xác nhận thanh toán
$invoice = Invoice::create([
'invoice_number' => OrderHelper::generateOrderNumber('INV'),
'customer_id' => $customer->id,
'order_id' => $order->id,
'invoice_type' => 1,
'date' => now()->toDateString(),
'sub_total' => $basePrice,
'discount_amount' => 0,
'tax' => 0,
'total' => $basePrice,
'status' => InvoiceStatus::PAID->value,
'payment_method' => $paymentMethod,
'paid_at' => now(),
'due_date' => now()->addMonths($months)->toDateString(),
]);
InvoiceItem::create([
'invoice_id' => $invoice->id,
'product_id' => $product->id,
'type' => 'service',
'description' => $product->name ?? 'Dịch vụ',
'months' => $months,
'quantity' => 1,
'unit_price' => $basePrice,
'total_price' => $basePrice,
]);
// 4) Payment processing
if ($paymentMethod === 'balance') {
$customer->total = (int) $customer->total - $basePrice;
$customer->save();
}
$proofPaths = $data['_proof_paths'] ?? [];
Transaction::create([
'uuid' => (string) Str::uuid(),
'customer_id' => $customer->id,
'user_id' => Auth::id(),
'trans' => $order->order_num,
'amount' => $basePrice,
'type' => $paymentMethod === 'transfer' ? 'transfer' : 'balance',
'status' => 1, // paid
'note' => $paymentMethod === 'transfer'
? 'Admin tạo dịch vụ - thanh toán chuyển khoản'
: 'Admin tạo dịch vụ - trừ số dư khách hàng',
'proof_image' => !empty($proofPaths) ? json_encode($proofPaths) : null,
]);
// 5) Service — updateOrCreate để tránh trùng khi cùng VM được gán lại
$serviceLookup = $vmId
? ['customer_id' => $customer->id, 'vm_id' => $vmId]
: [
'customer_id' => $customer->id,
'product_id' => $product->id,
'vm_id' => null,
'server_name' => $data['server_name'] ?? null,
];
$service = Service::updateOrCreate($serviceLookup, [
'customer_id' => $customer->id,
'product_id' => $product->id,
'vm_id' => $vmId,
'order_item_id' => $orderItem->id,
'invoice_id' => $invoice->id,
'server_name' => $data['server_name'] ?? null,
'status' => $data['status'] ?? 'active',
'auto_renew' => (bool) ($data['auto_renew'] ?? false),
'started_at' => $startedAt,
'expires_at' => $expiresAt,
'note' => $data['note'] ?? null,
]);
// module_vmware.service_id chỉ biết được sau khi Service đã tồn tại.
if ($vmId) {
ModuleVmware::where('id', $vmId)->update(['service_id' => $service->id]);
}
return $service->load(['product', 'vmware.os.osGroup']);
});
}
private function createModuleVmwareIfNeeded(array $data, Customer $customer, Order $order, int $basePrice, string|Carbon $expiresAt): ?int
{
if (empty($data['virtual_machine_id'])) {
return null;
}
$vm = VirtualMachines::with('datacenter')->find($data['virtual_machine_id']);
$vcenterId = $vm->datacenter->vcenter_id ?? null;
$moduleVm = ModuleVmware::updateOrCreate(
['virtual_machine_id' => $vm->id],
[
'customer_id' => $customer->id,
'order_id' => $order->id,
'name' => $vm->name,
'vmware_id' => $vm->vm_id,
'vcenter_id' => $vcenterId,
'hostname' => $data['server_name'] ?? $vm->name,
'cpu' => $data['vm_cpu'] ?? $vm->cpu,
'ram_mb' => $data['vm_ram_mb'] ?? $vm->ram_mb,
'disk_gb' => $data['vm_disk_gb'] ?? null,
'os_id' => $data['os_id'] ?? null,
'ip' => $data['vm_ip'] ?? $vm->ip,
'dns' => $data['vm_dns'] ?? null,
'user' => $data['vm_user'] ?? null,
'password' => $data['vm_password'] ?? null,
'plain_password' => $data['vm_password'] ?? null,
'port' => $data['vm_port'] ?? null,
'status' => 1,
'first_payment' => $basePrice,
'next_payment' => $basePrice,
'next_due_date' => Carbon::parse($expiresAt)->timestamp,
'next_invoice' => Carbon::parse($expiresAt)->timestamp,
],
);
return $moduleVm->id;
}
/**
* List virtual machines from vCenter inventory for the VPS picker.
* Annotates each VM with `assigned_service_id` if already linked to a service.
*/
public function listAvailableVms(string $search): Collection
{
$assignments = DB::table('module_vmware as mv')
->join('services as s', 's.vm_id', '=', 'mv.id')
->whereNotNull('mv.virtual_machine_id')
->select('mv.virtual_machine_id', 's.id as service_id', 's.customer_id as service_customer_id')
->get()
->keyBy('virtual_machine_id');
return DB::table('virtual_machines as vm')
->leftJoin('module_datacenter as dc', 'dc.id', '=', 'vm.datacenter_id')
->leftJoin('module_host as h', 'h.id', '=', 'vm.host_id')
->leftJoin('module_vcenter as vc', 'vc.id', '=', 'dc.vcenter_id')
->select(
'vm.id', 'vm.vm_id', 'vm.name', 'vm.power_state', 'vm.cpu', 'vm.ram_mb', 'vm.ip',
'vc.host as vcenter_name',
'dc.name as datacenter_name',
'h.name as host_name',
)
->where('vm.is_template', 0)
->where('vm.is_system', 0)
->when($search, fn($q) => $q->where(function ($w) use ($search) {
$w->where('vm.name', 'like', "%{$search}%")
->orWhere('vm.vm_id', 'like', "%{$search}%")
->orWhere('vm.ip', 'like', "%{$search}%");
}))
->orderBy('vm.name')
->limit(200)
->get()
->map(function ($vm) use ($assignments) {
$a = $assignments->get($vm->id);
$vm->assigned_service_id = $a->service_id ?? null;
$vm->assigned_customer_id = $a->service_customer_id ?? null;
return $vm;
});
}
}