395 lines
16 KiB
PHP
395 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Customer;
|
|
|
|
use App\Helpers\OrderHelper;
|
|
use App\Models\Customer;
|
|
use App\Models\ModuleVmware;
|
|
use App\Models\Order;
|
|
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 App\Services\Order\ServiceOrderRecorder;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
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,
|
|
private readonly ServiceOrderRecorder $orderRecorder,
|
|
private readonly CustomerServiceAssignment $assignment,
|
|
) {}
|
|
|
|
public function getServices(string $uuid): Collection
|
|
{
|
|
$services = $this->serviceRepository->filter(
|
|
['customer_uuid' => $uuid],
|
|
[
|
|
'product',
|
|
'product.pricings' => fn ($query) => $query->where('is_active', 1)->select('id', 'product_id', 'billing_cycle'),
|
|
'orderItem',
|
|
'invoice',
|
|
'vmware.os.osGroup',
|
|
...Service::HOSTING_ACCOUNT_RELATIONS,
|
|
],
|
|
);
|
|
|
|
ModuleVmware::resolveVirtualMachineIds($services->pluck('vmware'));
|
|
|
|
return $services;
|
|
}
|
|
|
|
public function attachService(string $uuid, int $serviceId): Service
|
|
{
|
|
return $this->assignment->attach(Service::findOrFail($serviceId), $this->resolveCustomer($uuid));
|
|
}
|
|
|
|
public function bulkDeleteServices(string $uuid, array $ids): int
|
|
{
|
|
$customer = $this->resolveCustomer($uuid);
|
|
|
|
return DB::transaction(function () use ($customer, $ids) {
|
|
$services = Service::where('customer_id', $customer->id)->whereIn('id', $ids)->get();
|
|
|
|
foreach ($services as $service) {
|
|
$this->assignment->detach($service);
|
|
}
|
|
|
|
return $services->count();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 ($customer, $service, $data) {
|
|
$vmFields = ['virtual_machine_id', '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', 'billing_cycle', ...$vmFields])->all());
|
|
$service->save();
|
|
|
|
if (!empty($data['virtual_machine_id'])) {
|
|
$this->assignVirtualMachine($service, $customer, (int) $data['virtual_machine_id']);
|
|
}
|
|
|
|
$price = isset($data['price']) ? (int) $data['price'] : null;
|
|
|
|
if (!$service->order_item_id && ($price !== null || !empty($data['billing_cycle']))) {
|
|
$this->orderRecorder->ensureForService($service, $price, $data['billing_cycle'] ?? null);
|
|
$service->refresh();
|
|
}
|
|
|
|
if (!empty($data['billing_cycle']) && $service->orderItem) {
|
|
$service->orderItem->update([
|
|
'billing_cycle' => $data['billing_cycle'],
|
|
'months' => OrderHelper::cycleToMonths($data['billing_cycle']),
|
|
]);
|
|
}
|
|
|
|
if ($price !== null && $service->orderItem) {
|
|
$service->orderItem->update(['price' => $price]);
|
|
$this->orderRecorder->syncUnpaidInvoice($service->refresh(), $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;
|
|
});
|
|
}
|
|
|
|
private function assignVirtualMachine(Service $service, Customer $customer, int $virtualMachineId): void
|
|
{
|
|
$vm = VirtualMachines::with('datacenter')->findOrFail($virtualMachineId);
|
|
$current = $service->vmware;
|
|
|
|
if ($current && ($current->virtual_machine_id === $vm->id || $current->vmware_id === $vm->vm_id)) {
|
|
return;
|
|
}
|
|
|
|
$takenBy = DB::table('module_vmware as mv')
|
|
->join('services as s', 's.vm_id', '=', 'mv.id')
|
|
->where(fn($q) => $q->where('mv.virtual_machine_id', $vm->id)->orWhere('mv.vmware_id', $vm->vm_id))
|
|
->where('s.id', '!=', $service->id)
|
|
->value('s.id');
|
|
abort_if($takenBy, 422, "VM {$vm->name} đang gán cho dịch vụ #{$takenBy}.");
|
|
|
|
if ($current) {
|
|
$current->update([
|
|
'virtual_machine_id' => $vm->id,
|
|
'vmware_id' => $vm->vm_id,
|
|
'name' => $vm->name,
|
|
'vcenter_id' => $vm->datacenter->vcenter_id ?? null,
|
|
'cpu' => $vm->cpu,
|
|
'ram_mb' => $vm->ram_mb,
|
|
'ip' => $vm->ip,
|
|
]);
|
|
|
|
return;
|
|
}
|
|
|
|
$price = (int) ($service->orderItem->price ?? 0);
|
|
$vmId = $this->createModuleVmwareIfNeeded(
|
|
['virtual_machine_id' => $vm->id, 'server_name' => $service->server_name],
|
|
$customer,
|
|
$service->orderItem?->order,
|
|
$price,
|
|
$service->expires_at ?? now(),
|
|
);
|
|
|
|
ModuleVmware::where('id', $vmId)->update(['service_id' => $service->id]);
|
|
$service->update(['vm_id' => $vmId]);
|
|
}
|
|
|
|
/**
|
|
* 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::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);
|
|
|
|
// 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)
|
|
$listPrice = $this->orderRecorder->listPrice($product, $billingCycle, $data['server_name'] ?? null);
|
|
abort_if(
|
|
!isset($data['amount_paid']) && $listPrice === null && $product->product_type === 'domain',
|
|
422,
|
|
'Tên miền không có giá trong bảng giá tên miền, vui lòng nhập giá.',
|
|
);
|
|
$basePrice = isset($data['amount_paid']) ? (int) $data['amount_paid'] : (int) $listPrice;
|
|
|
|
$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) {
|
|
['order' => $order, 'orderItem' => $orderItem, 'invoice' => $invoice] = $this->orderRecorder->create(
|
|
$customer->id,
|
|
$product,
|
|
$billingCycle,
|
|
$basePrice,
|
|
$data['server_name'] ?? null,
|
|
paid: true,
|
|
paymentMethod: $paymentMethod,
|
|
clientIp: $clientIp,
|
|
);
|
|
|
|
// 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);
|
|
|
|
// module_vmware.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);
|
|
|
|
// 4) Payment processing
|
|
if ($paymentMethod === 'balance') {
|
|
$customer->total = (int) $customer->total - $basePrice;
|
|
$customer->save();
|
|
}
|
|
|
|
$proofPaths = $data['_proof_paths'] ?? [];
|
|
|
|
Transaction::create([
|
|
'uuid' => (string) Str::uuid(),
|
|
'public_code' => Transaction::newPublicCode('SVC'),
|
|
'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,
|
|
];
|
|
|
|
$replacedOrderItemId = Service::where($serviceLookup)->value('order_item_id');
|
|
|
|
$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,
|
|
]);
|
|
|
|
$this->orderRecorder->discardOrder($replacedOrderItemId);
|
|
|
|
// 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, int $page = 1, int $perPage = 200): array
|
|
{
|
|
$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');
|
|
|
|
$query = 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')
|
|
->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}%");
|
|
}));
|
|
|
|
$total = (clone $query)->count();
|
|
|
|
$data = $query
|
|
->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',
|
|
)
|
|
->orderBy('vm.name')
|
|
->forPage($page, $perPage)
|
|
->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;
|
|
});
|
|
|
|
return ['data' => $data, 'total' => $total];
|
|
}
|
|
}
|