Files
portal-api/app/Http/Controllers/Api/Admin/Customer/Concerns/ManagesCustomerServices.php
T

256 lines
11 KiB
PHP

<?php
namespace App\Http\Controllers\Api\Admin\Customer\Concerns;
use App\Models\ActivityLog;
use App\Models\Invoice;
use App\Models\InvoiceAttachment;
use App\Models\Service;
use App\Services\Api\Order\OrderQueryService;
use App\Services\ServiceRenewalNotifier;
use App\Services\ServiceRenewalReminderService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
use App\Support\BillingCycleCatalog;
use App\Support\RenewalPeriodGuard;
trait ManagesCustomerServices
{
public function getServices(string $uuid): JsonResponse
{
return response()->json(['error' => false, 'data' => $this->serviceProvisioning->getServices($uuid)]);
}
public function attachService(string $uuid, Request $request): JsonResponse
{
$data = $request->validate(['service_id' => 'required|integer|exists:services,id']);
$service = $this->serviceProvisioning->attachService($uuid, $data['service_id']);
return response()->json(['error' => false, 'message' => 'Đã gán dịch vụ cho khách hàng', 'data' => $service]);
}
public function bulkDeleteServices(string $uuid, Request $request): JsonResponse
{
$data = $request->validate(['ids' => 'required|array|min:1', 'ids.*' => 'integer']);
$deleted = $this->serviceProvisioning->bulkDeleteServices($uuid, $data['ids']);
return response()->json(['error' => false, 'message' => "Đã gỡ {$deleted} dịch vụ khỏi khách hàng", 'data' => ['deleted' => $deleted]]);
}
public function updateService(string $uuid, int $serviceId, Request $request): JsonResponse
{
$data = $request->validate([
'server_name' => 'nullable|string|max:255',
'status' => 'nullable|in:pending,active,suspended,expired,cancelled,terminated',
'auto_renew' => 'nullable|boolean',
'started_at' => 'nullable|date',
'expires_at' => 'nullable|date',
'note' => 'nullable|string',
'requirements' => 'nullable|string',
'feedback' => 'nullable|string',
'price' => 'nullable|numeric|min:0',
'billing_cycle' => ['nullable', Rule::in(BillingCycleCatalog::codes())],
'virtual_machine_id' => 'nullable|integer|exists:virtual_machines,id',
'os_id' => 'nullable|integer|exists:os,id',
'vm_cpu' => 'nullable|integer|min:1',
'vm_ram_mb' => 'nullable|integer|min:1',
'vm_disk_gb' => 'nullable|integer|min:1',
'vm_ip' => 'nullable|string|max:45',
'vm_dns' => 'nullable|string|max:100',
'vm_user' => 'nullable|string|max:255',
'vm_password' => 'nullable|string|max:255',
'vm_port' => 'nullable|integer|min:1|max:65535',
]);
$service = $this->serviceProvisioning->updateService($uuid, $serviceId, $data);
return response()->json(['error' => false, 'message' => 'Cập nhật dịch vụ thành công', 'data' => $service]);
}
public function changeServiceIp(string $uuid, int $serviceId, Request $request): JsonResponse
{
$data = $request->validate(['ip' => 'required|ip']);
abort_if(!Service::where('id', $serviceId)->value('vm_id'), 422, 'Dịch vụ chưa gắn VM.');
$service = $this->serviceProvisioning->updateService($uuid, $serviceId, ['vm_ip' => $data['ip']]);
return response()->json(['error' => false, 'message' => 'Đã đổi IP thành công', 'data' => $service]);
}
public function changeServicePort(string $uuid, int $serviceId, Request $request): JsonResponse
{
$data = $request->validate(['port' => 'required|integer|min:1|max:65535']);
abort_if(!Service::where('id', $serviceId)->value('vm_id'), 422, 'Dịch vụ chưa gắn VM.');
$service = $this->serviceProvisioning->updateService($uuid, $serviceId, ['vm_port' => $data['port']]);
return response()->json(['error' => false, 'message' => 'Đã đổi port thành công', 'data' => $service]);
}
public function notifyServiceRenewal(
string $uuid,
int $serviceId,
ServiceRenewalReminderService $reminders,
): JsonResponse
{
$customer = $this->resolveCustomer($uuid);
$service = Service::query()
->where('customer_id', $customer->id)
->where('id', $serviceId)
->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');
abort_if(! $service->expires_at, 422, 'Dịch vụ chưa có ngày hết hạn');
$reminders->send([$service->id], Auth::guard('admin')->user());
return response()->json([
'error' => false,
'message' => 'Đã đưa email nhắc gia hạn vào hàng đợi gửi và ghi nhận lịch sử.',
]);
}
public function serviceRenewals(string $uuid, int $serviceId): JsonResponse
{
$service = Service::where('customer_id', $this->resolveCustomer($uuid)->id)->findOrFail($serviceId);
$renewals = ActivityLog::where('subject_type', Service::class)
->where('subject_id', $service->id)
->where('event', 'renewed')
->latest('id')
->limit(100)
->get(['id', 'causer_type', 'causer_name', 'old_values', 'new_values', 'created_at']);
return response()->json(['error' => false, 'data' => $renewals]);
}
public function renewService(string $uuid, int $serviceId, Request $request): JsonResponse
{
$data = $request->validate([
'months' => 'required|integer|min:1|max:120',
'payment' => 'required|in:balance,manual',
'note' => 'nullable|string|max:500',
'proof' => ['nullable', ...array_slice(explode('|', InvoiceAttachment::UPLOAD_RULE), 1)],
]);
$customer = $this->resolveCustomer($uuid);
$current = Service::where('customer_id', $customer->id)->find($serviceId);
$previousExpiresAt = $current?->expires_at;
if ($current && $violation = RenewalPeriodGuard::violation($current, (int) $data['months'])) {
return response()->json(['error' => true, 'message' => $violation], 422);
}
$result = app(OrderQueryService::class)->renewServiceByAdmin(
$serviceId,
$customer,
(int) $data['months'],
$data['payment'],
(int) auth()->id(),
(string) ($data['note'] ?? ''),
);
if (! $result['ok']) {
return response()->json(
['error' => true, 'message' => $result['message'], 'data' => $result['data'] ?? null],
$result['code'],
);
}
$this->notifyRenewal($serviceId, (int) $result['data']['invoice_id'], (int) $data['months'], $previousExpiresAt, $data['payment']);
if ($data['payment'] === 'manual' && $request->hasFile('proof')) {
$this->storeRenewalProof($request->file('proof'), (int) $result['data']['invoice_id']);
}
return response()->json(['error' => false, 'message' => $result['message'], 'data' => $result['data']]);
}
private function notifyRenewal(int $serviceId, int $invoiceId, int $months, ?\Carbon\CarbonInterface $previousExpiresAt, string $payment): void
{
$admin = auth()->user();
app(ServiceRenewalNotifier::class)->notifyAdminRenewal(
Service::with(['customer', 'product'])->findOrFail($serviceId),
Invoice::findOrFail($invoiceId),
$months,
$previousExpiresAt,
$payment,
(string) ($admin?->fullname ?? $admin?->username ?? 'Admin'),
$admin?->id,
);
}
private function storeRenewalProof(UploadedFile $file, int $invoiceId): void
{
InvoiceAttachment::create([
'invoice_id' => $invoiceId,
'type' => InvoiceAttachment::TYPE_PAYMENT_PROOF,
'filename' => $file->getClientOriginalName(),
'path' => $file->store("invoice-attachments/{$invoiceId}", 'public'),
'disk' => 'public',
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
'uploaded_by' => auth()->user()?->username ?? 'admin',
]);
}
public function createService(string $uuid, Request $request): JsonResponse
{
$data = $request->validate([
'product_id' => 'required|integer|exists:products,id',
'virtual_machine_id' => 'nullable|integer|exists:virtual_machines,id',
'server_name' => 'nullable|string|max:255',
'status' => 'nullable|in:pending,active,suspended,expired,cancelled,terminated',
'auto_renew' => 'nullable|boolean',
'started_at' => 'nullable|date',
'expires_at' => 'nullable|date',
'note' => 'nullable|string',
'billing_cycle' => ['nullable', Rule::in(BillingCycleCatalog::codes())],
'payment_method' => 'required|in:transfer,balance',
'amount_paid' => 'nullable|numeric|min:0',
'proof_images' => 'nullable|array',
'proof_images.*' => 'file|image|max:5120',
'os_id' => 'nullable|integer|exists:os,id',
'vm_cpu' => 'nullable|integer|min:1',
'vm_ram_mb' => 'nullable|integer|min:1',
'vm_disk_gb' => 'nullable|integer|min:1',
'vm_ip' => 'nullable|string|max:45',
'vm_dns' => 'nullable|string|max:100',
'vm_user' => 'nullable|string|max:255',
'vm_password' => 'nullable|string|max:255',
'vm_port' => 'nullable|integer|min:1|max:65535',
]);
// Upload proof images (chuyển khoản) — I/O ở tầng HTTP, service chỉ nhận đường dẫn đã lưu.
if ($data['payment_method'] === 'transfer' && $request->hasFile('proof_images')) {
$data['_proof_paths'] = collect($request->file('proof_images'))
->map(fn($file) => $file->store('payment-proofs', 'public'))
->all();
}
$service = $this->serviceProvisioning->createService($uuid, $data, $request->ip());
return response()->json(['error' => false, 'message' => 'Service created', 'data' => $service], 201);
}
public function listAvailableVms(Request $request): JsonResponse
{
$result = $this->serviceProvisioning->listAvailableVms(
trim((string) $request->get('q', '')),
max(1, (int) $request->get('page', 1)),
max(1, min((int) $request->get('per_page', 200), 200)),
);
return response()->json(['error' => false] + $result);
}
}