feat: Add IT Support catalog and related migrations, seeders, and email templates

- Created migration to add 'group_label' to 'product_type_fields' table.
- Implemented ItSupportCatalogSeeder to populate IT Support features and tiers.
- Added ProductCategorySeeder for initial product categories.
- Developed ProductMockupSeeder for mockup products in various categories.
- Introduced ProductTypeFieldSeeder to define fields for different product types.
- Created email templates for VPS service information and service renewal reminders.
This commit is contained in:
baodev0
2026-08-26 16:19:38 +07:00
parent 382b7c0ef8
commit c917e3bf5e
47 changed files with 2992 additions and 1759 deletions
File diff suppressed because it is too large Load Diff
@@ -6,37 +6,17 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\OrderRequest;
use App\Http\Requests\UpdateOrderRequest;
use App\Http\Resources\Api\Admin\OrderResource;
use App\Jobs\Hosting\CreateHostingJobs;
use App\Jobs\VirtualMachine\Create\CreateVMJobs;
use App\Jobs\VirtualMachine\Upgrade\UpgradeVirtualMachine;
use App\Models\Order;
use App\Repositories\Addon\AddonRepositoryInterface;
use App\Repositories\Invoice\InvoiceRepositoryInterface;
use App\Repositories\InvoiceItem\InvoiceItemRepositoryInterface;
use App\Repositories\Module\Hosting\HostingRepositoryInterface as ModuleHostingRepositoryInterface;
use App\Repositories\Module\HostingServer\HostingServerRepositoryInterface;
use App\Repositories\Module\IpAddress\IpAddressRepositoryInterface;
use App\Repositories\Module\Vmware\VmwareRepositoryInterface;
use App\Repositories\Order\OrderRepositoryInterface;
use App\Repositories\Os\OsRepositoryInterface;
use App\Services\OrderService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class OrderController extends Controller
{
public function __construct(
private readonly OrderRepositoryInterface $orderRepository,
private readonly InvoiceRepositoryInterface $invoiceRepository,
private readonly InvoiceItemRepositoryInterface $invoiceItemRepository,
private readonly ModuleHostingRepositoryInterface $moduleHostingRepository,
private readonly VmwareRepositoryInterface $moduleVmwareRepository,
private readonly HostingServerRepositoryInterface $moduleHostingServerRepository,
private readonly OsRepositoryInterface $osRepository,
private readonly IpAddressRepositoryInterface $ipAddressRepository,
private readonly AddonRepositoryInterface $addonRepository,
private readonly OrderService $orderService,
) {}
@@ -83,12 +63,7 @@ class OrderController extends Controller
{
$order = $this->orderRepository->findByOrderNum($order_num);
if (!$order) {
return response()->json([
'error' => true,
'message' => "Can't find order by ID #" . $order_num,
], 404);
}
abort_if(!$order, 404, "Can't find order by ID #{$order_num}");
return response()->json([
'error' => false,
@@ -100,173 +75,48 @@ class OrderController extends Controller
{
$order = $this->orderRepository->findWithRelationships($id, ['invoice', 'orderItem']);
if (!$order) {
return response()->json([
'error' => true,
'message' => "Can't find order by ID #" . $id,
], 404);
}
abort_if(!$order, 404, "Can't find order by ID #{$id}");
$status = match ($request->action) {
"accept" => 1,
"cancel" => 2,
default => 0
match ($request->action) {
'accept' => $this->orderService->confirmOrder($order, (bool) $request->module),
'cancel' => $this->orderService->setStatus($order, 2),
default => $this->orderService->setStatus($order, 0),
};
try {
DB::beginTransaction();
$this->orderRepository->update($id, ['status' => $status]);
if ($status === 1 && $request->module == 1) {
$jobs = [];
$addon = [];
$product = 0;
foreach ($order->orderItem as $key => $item) {
switch ((int) $item->type_id) {
case 1:
$itemData = $this->addonRepository->find($item->relid);
if ($itemData) {
$price = match ((int) $item->billing) {
2 => $item->amount / 1,
3 => $item->amount / 3,
4 => $item->amount / 6,
5 => $item->amount / 12,
default => 0
};
$addon[strtolower($itemData->name)] = (int) $price / $itemData->price;
$product = $item->parent_id;
}
break;
case 3:
$hostingServer = $this->moduleHostingServerRepository->get_active();
if ($hostingServer) {
$this->moduleHostingRepository->update($item->relid, ['module_server_id' => $hostingServer->first()->id]);
$jobs[] = new CreateHostingJobs($item->relid);
}
break;
case 4:
$product = $this->moduleVmwareRepository->find($item->relid);
if ($product && $product->os_id) {
$os = $this->osRepository->findWithRelationships($product->os_id, ['osGroup']);
if (!$os || !$os->osGroup->isNotEmpty()) break;
$ip_address = $this->ipAddressRepository->filter([
'module_host_id' => $os->osGroup->module_host_id,
'status' => 0,
]);
if (count($ip_address) > 0) {
$this->moduleVmwareRepository->update($item->relid, [
'module_ipaddress_id' => $ip_address->first()->id,
]);
$this->ipAddressRepository->update($ip_address->first()->id, ['status' => 1]);
$jobs[] = new CreateVMJobs($product->id);
}
}
break;
}
}
if (count($order->orderItem) === count($addon)) {
dispatch(new UpgradeVirtualMachine($product, $addon, $order->id))->delay(now()->addSeconds(10));
}
foreach ($jobs as $item) {
dispatch($item)->delay(now()->addSeconds(10));
}
}
DB::commit();
return response()->json(['error' => false, 'message' => 'Order updated successfully']);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
}
return response()->json(['error' => false, 'message' => 'Order updated successfully']);
}
public function destroy(int $id): JsonResponse
{
$order = Order::with('Invoice', 'Invoice.InvoiceItem')->find($id);
$order = Order::with('invoice', 'invoice.invoiceItem')->find($id);
if (!$order) {
return response()->json([
'error' => true,
'message' => "Can't find order by ID #" . $id,
], 404);
}
abort_if(!$order, 404, "Can't find order by ID #{$id}");
try {
DB::beginTransaction();
$this->orderService->deleteOrder($order);
if ($order->Invoice) {
$order->Invoice->delete();
if ($order->Invoice->InvoiceItem->count()) {
foreach ($order->Invoice->InvoiceItem->all() as $item) {
$item->delete();
}
}
}
$order->delete();
DB::commit();
return response()->json(['error' => false, 'message' => 'Order deleted successfully']);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
}
return response()->json(['error' => false, 'message' => 'Order deleted successfully']);
}
public function accept(int $id): JsonResponse
{
$order = $this->orderRepository->findWithRelationships($id, ['invoice', 'orderItem']);
$order = $this->orderRepository->find($id);
if (!$order) {
return response()->json([
'error' => true,
'message' => "Can't find order by ID #" . $id,
], 404);
}
abort_if(!$order, 404, "Can't find order by ID #{$id}");
try {
DB::beginTransaction();
$this->orderRepository->update($id, ['status' => 1]);
DB::commit();
$this->orderService->setStatus($order, 1);
return response()->json(['error' => false, 'message' => 'Order accepted successfully']);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
}
return response()->json(['error' => false, 'message' => 'Order accepted successfully']);
}
public function cancel(int $id): JsonResponse
{
$order = $this->orderRepository->find($id);
if (!$order) {
return response()->json([
'error' => true,
'message' => "Can't find order by ID #" . $id,
], 404);
}
abort_if(!$order, 404, "Can't find order by ID #{$id}");
try {
DB::beginTransaction();
$this->orderRepository->update($id, ['status' => 2]);
DB::commit();
$this->orderService->setStatus($order, 2);
return response()->json(['error' => false, 'message' => 'Order cancelled successfully']);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
}
return response()->json(['error' => false, 'message' => 'Order cancelled successfully']);
}
public function uploadProof(Request $request): JsonResponse
@@ -72,6 +72,7 @@ class ProductController extends Controller
'ram' => 'nullable|numeric|min:0',
'disk' => 'nullable|numeric|min:0',
'bandwidth' => 'nullable|numeric|min:0',
'domain_count' => 'nullable|integer|min:0',
'billing_type' => 'nullable|string|in:recurring,onetime',
'currency' => 'nullable|string|max:10',
'price_monthly' => 'nullable|numeric|min:0',
@@ -81,6 +82,7 @@ class ProductController extends Controller
'price_biennially' => 'nullable|numeric|min:0',
'price_triennially' => 'nullable|numeric|min:0',
'price_onetime' => 'nullable|numeric|min:0',
'features' => 'nullable|array',
]);
$productType = $validated['product_type'] ?? null;
@@ -103,6 +105,8 @@ class ProductController extends Controller
'ram' => $validated['ram'] ?? null,
'disk' => $validated['disk'] ?? null,
'bandwidth' => $validated['bandwidth'] ?? null,
'domain_count' => $validated['domain_count'] ?? null,
'features' => $validated['features'] ?? null,
]);
$this->savePricings($product, $validated);
@@ -135,6 +139,7 @@ class ProductController extends Controller
'ram' => 'nullable|numeric|min:0',
'disk' => 'nullable|numeric|min:0',
'bandwidth' => 'nullable|numeric|min:0',
'domain_count' => 'nullable|integer|min:0',
'os_ids' => 'nullable|array',
'os_ids.*' => 'integer|exists:os,id',
'module_host_id' => 'nullable|integer|exists:module_host,id',
@@ -148,6 +153,7 @@ class ProductController extends Controller
'price_biennially' => 'nullable|numeric|min:0',
'price_triennially' => 'nullable|numeric|min:0',
'price_onetime' => 'nullable|numeric|min:0',
'features' => 'nullable|array',
]);
if (isset($validated['name']) && $validated['name'] !== $product->name) {
@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Api\Admin\Product;
use App\Http\Controllers\Controller;
use App\Http\Requests\ProductTypeField\ProductTypeFieldRequest;
use App\Http\Resources\Api\Admin\ProductTypeFieldResource;
use App\Models\ProductTypeField;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ProductTypeFieldController extends Controller
{
public function index(Request $request): JsonResponse
{
$items = ProductTypeField::query()
->when($request->get('product_type'), fn ($q, $type) => $q->where('product_type', $type))
->orderBy('sort_order')
->orderBy('id')
->get();
return response()->json([
'error' => false,
'data' => ProductTypeFieldResource::collection($items),
]);
}
public function store(ProductTypeFieldRequest $request): JsonResponse
{
$field = ProductTypeField::create($request->validated());
return response()->json([
'error' => false,
'message' => 'Tạo field thành công.',
'data' => new ProductTypeFieldResource($field),
], 201);
}
public function update(ProductTypeFieldRequest $request, int $id): JsonResponse
{
$field = ProductTypeField::find($id);
if (!$field) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy field.'], 404);
}
$field->update($request->validated());
return response()->json([
'error' => false,
'message' => 'Cập nhật field thành công.',
'data' => new ProductTypeFieldResource($field),
]);
}
public function destroy(int $id): JsonResponse
{
$field = ProductTypeField::find($id);
if (!$field) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy field.'], 404);
}
$field->delete();
return response()->json([
'error' => false,
'message' => 'Xoá field thành công.',
]);
}
}
@@ -3,6 +3,8 @@
namespace App\Http\Controllers\Api\Admin\Service;
use App\Http\Controllers\Controller;
use App\Jobs\VirtualMachine\Provision\ResendVmCredentialsEmailJob;
use App\Models\ModuleVmware;
use App\Models\Service;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -34,9 +36,11 @@ class ServiceController extends Controller
public function show(int $id): JsonResponse
{
$service = Service::with(['customer', 'product', 'orderItem', 'invoice', 'vmware', 'addons'])
$service = Service::with(['customer', 'product', 'orderItem', 'invoice', 'vmware.os.osGroup', 'addons'])
->findOrFail($id);
ModuleVmware::resolveVirtualMachineIds([$service->vmware]);
return response()->json(['error' => false, 'data' => $service]);
}
@@ -47,4 +51,15 @@ class ServiceController extends Controller
return response()->json(['error' => false, 'message' => 'Đã gỡ khách hàng khỏi dịch vụ.']);
}
public function resendCredentials(int $id): JsonResponse
{
$service = Service::findOrFail($id);
abort_if(!$service->vm_id, 422, 'Dịch vụ chưa gắn VM.');
ResendVmCredentialsEmailJob::dispatch($service->vm_id);
return response()->json(['error' => false, 'message' => 'Đã xếp hàng gửi lại thông tin VPS cho khách hàng.']);
}
}