diff --git a/app/Console/Commands/UpdateExpiryDate.php b/app/Console/Commands/UpdateExpiryDate.php new file mode 100644 index 0000000..743c0b7 --- /dev/null +++ b/app/Console/Commands/UpdateExpiryDate.php @@ -0,0 +1,54 @@ +option('dry-run'); + + $query = OrderItem::whereNull('expiry_date') + ->whereNotNull('months') + ->where('months', '>', 0); + + $count = $query->count(); + + if ($count === 0) { + $this->info('No order items need to be updated.'); + return 0; + } + + $this->info("Found {$count} order items to update."); + + if ($dryRun) { + $this->warn('DRY RUN - No changes will be made.'); + $query->chunk(100, function ($items) { + foreach ($items as $item) { + $expiryDate = Carbon::parse($item->created_at)->addMonths($item->months); + $this->line("Order #{$item->order_id}, Item #{$item->id}: would set expiry_date to {$expiryDate}"); + } + }); + } else { + $updated = 0; + $query->chunk(100, function ($items) use (&$updated) { + foreach ($items as $item) { + $expiryDate = Carbon::parse($item->created_at)->addMonths($item->months); + $item->update(['expiry_date' => $expiryDate]); + $updated++; + } + }); + $this->info("Updated {$updated} order items."); + } + + return 0; + } +} diff --git a/app/Http/Controllers/Api/Admin/Order/OrderController.php b/app/Http/Controllers/Api/Admin/Order/OrderController.php index 5d1e874..2739d32 100644 --- a/app/Http/Controllers/Api/Admin/Order/OrderController.php +++ b/app/Http/Controllers/Api/Admin/Order/OrderController.php @@ -22,11 +22,13 @@ use App\Repositories\Module\Vmware\VmwareRepositoryInterface; use App\Repositories\Order\OrderRepositoryInterface; use App\Repositories\Os\OsRepositoryInterface; use App\Repositories\VPS\VPSRepositoryInterface; +use App\Models\Customer; use Carbon\Carbon; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Validator; use Yajra\DataTables\Facades\DataTables; @@ -95,6 +97,14 @@ class OrderController extends Controller { $request->validated(); + $customer = Customer::find($request->customer_id); + if (!$customer || (int) $customer->status !== 1) { + return response()->json([ + 'error' => true, + 'message' => 'Tài khoản khách hàng không hoạt động. Không thể tạo đơn hàng.', + ], 422); + } + $total = 0; $product_item = []; $relid_data = []; @@ -167,28 +177,32 @@ class OrderController extends Controller $total += $item['quantity'] * ($item['price_custom'] ?? ($product->price_sale ?? $product->price)) * $billingDetails['billing']; } + $paymentRecord = \App\Models\Payment::find((int) $request->payment_method); + $isBankTransfer = $paymentRecord && (int) $paymentRecord->type === 1; + $order_item = [ - 'order_num' => Helper::generateUniqueCode(), - 'customer_id' => $request->customer_id, - 'invoice_id' => 0, - 'discount_id' => $request->discount_id ?: null, - 'amount' => $total + ($total * (10 / 100)), - 'payment_method' => $request->payment_method, - 'user_id' => Auth::guard('admin')->id(), - 'status' => $request->status, - 'client_ip' => $request->getClientIp() + 'order_num' => Helper::generateUniqueCode(), + 'customer_id' => $request->customer_id, + 'invoice_id' => 0, + 'discount_id' => $request->discount_id ?: null, + 'amount' => $total + ($total * (10 / 100)), + 'payment_id' => $paymentRecord?->id, + 'payment_proof' => $request->payment_proof ?? null, + 'user_id' => Auth::guard('admin')->id(), + 'status' => $request->status, + 'client_ip' => $request->getClientIp(), ]; $invoice_data = [ - 'date' => Carbon::today()->toDateString(), - 'due_date' => Carbon::today()->toDateString(), - 'date_paid' => (int)$request->status === 1 ? Carbon::today()->toDateString() : null, - 'sub_total' => $total, - 'tax' => $total * (10 / 100), - 'total' => $total + ($total * (10 / 100)), - 'payment_method' => $request->payment_method, + 'date' => Carbon::today()->toDateString(), + 'due_date' => Carbon::today()->toDateString(), + 'date_paid' => (int)$request->status === 1 ? Carbon::today()->toDateString() : null, + 'sub_total' => $total, + 'tax' => $total * (10 / 100), + 'total' => $total + ($total * (10 / 100)), + 'payment_id' => $paymentRecord?->id, 'customer_id' => $request->create_invoice ? $request->customer_id : 0, - 'status' => $request->status + 'status' => $request->status ]; DB::beginTransaction(); @@ -242,30 +256,39 @@ class OrderController extends Controller $this->invoiceItemRepository->insert($invoiceItemData); $now = now(); - \App\Models\OrderItem::insert(array_map(fn($item) => [ - 'order_id' => $order->id, - 'product_id' => $item['product_id'], - 'billing_cycle' => $item['billing_cycle'], - 'months' => $item['months'], - 'os_id' => $item['os_id'], - 'quantity' => $item['quantity'], - 'server_name' => $item['server_name'], - 'price' => $item['price'], - 'created_at' => $now, - 'updated_at' => $now, - ], $product_item)); + \App\Models\OrderItem::insert(array_map(function ($item) use ($now) { + $expiryDate = $now->copy()->addMonths($item['months']); + return [ + 'order_id' => $order->id, + 'product_id' => $item['product_id'], + 'billing_cycle' => $item['billing_cycle'], + 'months' => $item['months'], + 'expiry_date' => $expiryDate, + 'os_id' => $item['os_id'], + 'quantity' => $item['quantity'], + 'server_name' => $item['server_name'], + 'price' => $item['price'], + 'created_at' => $now, + 'updated_at' => $now, + ]; + }, $product_item)); // Dispatch VM creation job for VPS/cloud products $vpsTypes = ['vps', 'cloud']; $createdOrderItems = \App\Models\OrderItem::where('order_id', $order->id)->get(); foreach ($createdOrderItems as $orderItem) { - $pt = $orderItem->product?->product_type ?? $orderItem->billing_cycle; if (in_array($orderItem->product?->product_type, $vpsTypes)) { \App\Jobs\VirtualMachine\Create\CreateVirtualMachineJob::dispatch($orderItem) ->delay(now()->addSeconds(10)); } } + // Deduct customer balance for non-bank-transfer payments + if (!$isBankTransfer) { + Customer::where('id', $request->customer_id) + ->decrement('total', (int) ($total + ($total * (10 / 100)))); + } + DB::commit(); return response()->json([ @@ -284,10 +307,15 @@ class OrderController extends Controller public function show(int $id): JsonResponse { - $order = $this->orderRepository->findWithRelationships( - $id, - ['orderItem', 'user', 'customer.invoiceItem.type', 'payment', 'discount', 'invoice.invoiceItem'] - ); + $order = Order::with([ + 'orderItems.product', + 'orderItems.os', + 'user', + 'customer', + 'payment', + 'discount', + 'invoice' + ])->find($id); if (!$order) { return response()->json([ @@ -501,6 +529,19 @@ class OrderController extends Controller } } + public function uploadProof(Request $request): JsonResponse + { + $request->validate(['file' => 'required|image|max:5120']); + + $path = $request->file('file')->store('payment-proofs', 'public'); + + return response()->json([ + 'error' => false, + 'url' => Storage::disk('public')->url($path), + 'path' => $path, + ]); + } + private function calculateBillingDetails($billing): array { $months = match ($billing) { diff --git a/app/Http/Controllers/Api/Admin/OsManager/OsController.php b/app/Http/Controllers/Api/Admin/OsManager/OsController.php new file mode 100644 index 0000000..7ad38e9 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/OsManager/OsController.php @@ -0,0 +1,90 @@ +validate([ + 'version' => 'required|string|max:100', + 'os_group_id' => 'required|integer|exists:os_group,id', + 'template_id' => 'nullable|string|max:255', + 'is_active' => 'boolean', + ]); + + $os = Os::create([ + 'version' => $validated['version'], + 'os_group_id' => $validated['os_group_id'], + 'template_id' => $validated['template_id'] ?? null, + 'status' => ($validated['is_active'] ?? true) ? 1 : 0, + ]); + + return response()->json([ + 'error' => false, + 'message' => 'Thêm OS thành công.', + 'data' => $this->formatOs($os), + ], 201); + } + + public function update(Request $request, int $id): JsonResponse + { + $os = Os::find($id); + + if (! $os) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy OS.'], 404); + } + + $validated = $request->validate([ + 'version' => 'sometimes|required|string|max:100', + 'os_group_id' => 'sometimes|integer|exists:os_group,id', + 'template_id' => 'nullable|string|max:255', + 'is_active' => 'boolean', + ]); + + $os->update([ + 'version' => $validated['version'] ?? $os->version, + 'os_group_id' => $validated['os_group_id'] ?? $os->os_group_id, + 'template_id' => array_key_exists('template_id', $validated) ? $validated['template_id'] : $os->template_id, + 'status' => isset($validated['is_active']) ? ($validated['is_active'] ? 1 : 0) : $os->status, + ]); + + return response()->json([ + 'error' => false, + 'message' => 'Cập nhật OS thành công.', + 'data' => $this->formatOs($os->fresh()), + ]); + } + + public function destroy(int $id): JsonResponse + { + $os = Os::find($id); + + if (! $os) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy OS.'], 404); + } + + $os->delete(); + + return response()->json(['error' => false, 'message' => 'Xoá OS thành công.']); + } + + // ── helpers ────────────────────────────────────────────────────────────── + + private function formatOs(Os $os): array + { + return [ + 'id' => $os->id, + 'version' => $os->version, + 'template_id' => $os->template_id, + 'is_active' => (bool) $os->status, + 'os_group_id' => $os->os_group_id, + ]; + } +} diff --git a/app/Http/Controllers/Api/Admin/OsManager/OsGroupController.php b/app/Http/Controllers/Api/Admin/OsManager/OsGroupController.php new file mode 100644 index 0000000..09c3204 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/OsManager/OsGroupController.php @@ -0,0 +1,152 @@ +with(['os' => fn($q) => $q->orderBy('version')]) + ->when( + $request->get('filter'), + fn($q, $f) => $q->where('name', 'like', "%{$f}%") + ) + ->orderBy('name'); + + if ($request->boolean('all')) { + $items = $query->get(); + + return response()->json([ + 'error' => false, + 'data' => $items->map(fn($g) => $this->formatGroup($g)), + ]); + } + + $perPage = min((int) $request->get('per_page', 15), 100); + $paginator = $query->paginate($perPage); + + return response()->json([ + 'error' => false, + 'data' => [ + 'items' => collect($paginator->items())->map(fn($g) => $this->formatGroup($g)), + 'total' => $paginator->total(), + 'per_page' => $paginator->perPage(), + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + ], + ]); + } + + public function show(int $id): JsonResponse + { + $group = OsGroup::withCount('os') + ->with(['os' => fn($q) => $q->orderBy('version')]) + ->find($id); + + if (! $group) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy nhóm OS.'], 404); + } + + return response()->json(['error' => false, 'data' => $this->formatGroup($group)]); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'is_active' => 'boolean', + ]); + + $group = OsGroup::create([ + 'name' => $validated['name'], + 'status' => ($validated['is_active'] ?? true) ? 1 : 0, + ]); + + return response()->json([ + 'error' => false, + 'message' => 'Tạo nhóm OS thành công.', + 'data' => $this->formatGroup($group->load('os')), + ], 201); + } + + public function update(Request $request, int $id): JsonResponse + { + $group = OsGroup::find($id); + + if (! $group) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy nhóm OS.'], 404); + } + + $validated = $request->validate([ + 'name' => 'sometimes|required|string|max:255', + 'is_active' => 'boolean', + ]); + + $group->update([ + 'name' => $validated['name'] ?? $group->name, + 'status' => isset($validated['is_active']) ? ($validated['is_active'] ? 1 : 0) : $group->status, + ]); + + return response()->json([ + 'error' => false, + 'message' => 'Cập nhật nhóm OS thành công.', + 'data' => $this->formatGroup($group->fresh(['os'])), + ]); + } + + public function destroy(int $id): JsonResponse + { + $group = OsGroup::withCount('os')->find($id); + + if (! $group) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy nhóm OS.'], 404); + } + + if ($group->os_count > 0) { + return response()->json([ + 'error' => true, + 'message' => "Không thể xoá nhóm đang chứa {$group->os_count} OS. Hãy xoá các OS trước.", + ], 422); + } + + $group->delete(); + + return response()->json(['error' => false, 'message' => 'Xoá nhóm OS thành công.']); + } + + // ── helpers ────────────────────────────────────────────────────────────── + + private function formatGroup(OsGroup $group): array + { + return [ + 'id' => $group->id, + 'name' => $group->name, + 'is_active' => (bool) $group->status, + 'os_count' => $group->os_count ?? $group->os->count(), + 'os_list' => $group->relationLoaded('os') + ? $group->os->map(fn($o) => $this->formatOs($o))->values() + : [], + ]; + } + + private function formatOs(\App\Models\Os $os): array + { + return [ + 'id' => $os->id, + 'version' => $os->version, + 'template_id' => $os->template_id, + 'is_active' => (bool) $os->status, + 'os_group_id' => $os->os_group_id, + ]; + } +} diff --git a/app/Http/Controllers/Api/Admin/Payment/PaymentController.php b/app/Http/Controllers/Api/Admin/Payment/PaymentController.php index 5441533..06901a0 100644 --- a/app/Http/Controllers/Api/Admin/Payment/PaymentController.php +++ b/app/Http/Controllers/Api/Admin/Payment/PaymentController.php @@ -26,6 +26,21 @@ class PaymentController extends Controller ]); } + public function forSelect(): JsonResponse + { + $payments = $this->paymentRepository->filter(['status' => 1]); + + return response()->json([ + 'error' => false, + 'data' => $payments->map(fn($p) => [ + 'id' => $p->id, + 'name' => $p->name, + 'type' => (int) $p->type, + 'status' => (int) $p->status, + ])->values(), + ]); + } + public function store(PaymentRequest $request): JsonResponse { $request->validated(); diff --git a/app/Http/Controllers/Api/Admin/Product/ProductController.php b/app/Http/Controllers/Api/Admin/Product/ProductController.php index 8d1cfd4..986d7f5 100644 --- a/app/Http/Controllers/Api/Admin/Product/ProductController.php +++ b/app/Http/Controllers/Api/Admin/Product/ProductController.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Controller; use App\Http\Resources\Api\Admin\ProductResource; use App\Models\Product; use App\Repositories\Admin\Product\AdminProductRepositoryInterface; +use App\Models\Os; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Str; @@ -44,6 +45,9 @@ class ProductController extends Controller return response()->json(['error' => true, 'message' => 'Không tìm thấy sản phẩm.'], 404); } + // Load operatingSystems relationship (product_os table) + $product->load('operatingSystems'); + return response()->json([ 'error' => false, 'data' => new ProductResource($product), @@ -105,11 +109,13 @@ class ProductController extends Controller 'product_categories_id' => 'nullable|integer|exists:product_categories,id', 'product_type' => 'nullable|string', 'is_active' => 'boolean', - 'setup_fee' => 'nullable|numeric|min:0', - 'cpu' => 'nullable|numeric|min:0', - 'ram' => 'nullable|numeric|min:0', - 'disk' => 'nullable|numeric|min:0', - 'bandwidth' => 'nullable|numeric|min:0', + 'setup_fee' => 'nullable|numeric|min:0', + 'cpu' => 'nullable|numeric|min:0', + 'ram' => 'nullable|numeric|min:0', + 'disk' => 'nullable|numeric|min:0', + 'bandwidth' => 'nullable|numeric|min:0', + 'os_ids' => 'nullable|array', + 'os_ids.*' => 'integer|exists:os,id', ]); if (isset($validated['name']) && $validated['name'] !== $product->name) { @@ -121,8 +127,17 @@ class ProductController extends Controller $validated['product_type'] = $category?->type; } + // Handle OS associations via product_os table + $osIds = $validated['os_ids'] ?? null; + unset($validated['os_ids']); + $product->update($validated); + // Sync OS associations in product_os table + if ($osIds !== null) { + $product->operatingSystems()->sync($osIds); + } + return response()->json([ 'error' => false, 'message' => 'Cập nhật sản phẩm thành công.', @@ -151,7 +166,7 @@ class ProductController extends Controller $product = Product::find($id); if (!$product) { - return response()->json(['error' => true, 'message' => 'Không tìm thấy sản phẩm.'], 404); + return response()->json(['error' => true, 'message' => 'Không tìm thấy sản phờng trình.'], 404); } $osList = $product->osVersions() @@ -171,4 +186,49 @@ class ProductController extends Controller 'data' => $osList, ]); } + + public function getOsList(): JsonResponse + { + $osList = Os::with('osGroup') + ->where('status', 1) + ->get() + ->map(fn($os) => [ + 'id' => $os->id, + 'name' => $os->name, + 'version' => $os->version, + 'os_group_name' => $os->osGroup?->name, + 'template_id' => $os->template_id, + ]); + + return response()->json([ + 'error' => false, + 'data' => $osList, + ]); + } + + public function getOperatingSystems(int $id): JsonResponse + { + $product = Product::find($id); + + if (!$product) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy sản phẩm.'], 404); + } + + $osList = $product->operatingSystems() + ->with('osGroup') + ->where('status', 1) + ->get() + ->map(fn($os) => [ + 'id' => $os->id, + 'name' => $os->name, + 'version' => $os->version, + 'os_group_name' => $os->osGroup?->name, + 'template_id' => $os->template_id, + ]); + + return response()->json([ + 'error' => false, + 'data' => $osList, + ]); + } } diff --git a/app/Http/Requests/OrderRequest.php b/app/Http/Requests/OrderRequest.php index 95907b3..71744b9 100644 --- a/app/Http/Requests/OrderRequest.php +++ b/app/Http/Requests/OrderRequest.php @@ -23,7 +23,7 @@ class OrderRequest extends FormRequest { return [ 'customer_id' => 'required', - 'payment_method' => 'required', + 'payment_method' => 'required|integer|min:1', 'status' => 'required', 'product.*.product_id' => 'required|numeric|not__in:0', 'product.*.os_id' => 'nullable|integer', diff --git a/app/Http/Resources/Api/Admin/OrderResource.php b/app/Http/Resources/Api/Admin/OrderResource.php index b9d246d..48aa0cd 100644 --- a/app/Http/Resources/Api/Admin/OrderResource.php +++ b/app/Http/Resources/Api/Admin/OrderResource.php @@ -10,17 +10,71 @@ class OrderResource extends JsonResource public function toArray(Request $request): array { return [ - 'id' => $this->id, + 'id' => $this->id, 'order_num' => $this->order_num, - 'customer' => $this->whenLoaded('customer', fn() => [ + 'customer' => $this->whenLoaded('customer', fn() => [ 'id' => $this->customer->id, 'fullname' => $this->customer->fullname, 'email' => $this->customer->email, + 'phone' => $this->customer->phone ?? null, ]), - 'amount' => $this->amount, - 'status' => $this->status, - 'date' => $this->date, + 'user' => $this->whenLoaded('user', fn() => [ + 'id' => $this->user->id, + 'firstname' => $this->user->firstname, + 'lastname' => $this->user->lastname, + 'email' => $this->user->email, + ]), + 'payment' => $this->whenLoaded('payment', fn() => [ + 'id' => $this->payment->id, + 'name' => $this->payment->name, + 'type' => $this->payment->type, + ]), + 'discount' => $this->whenLoaded('discount', fn() => [ + 'id' => $this->discount->id, + 'code' => $this->discount->code, + 'value' => $this->discount->value, + ]), + 'invoice' => $this->whenLoaded('invoice', fn() => [ + 'id' => $this->invoice->id, + 'order_num' => $this->invoice->order_num, + 'sub_total' => $this->invoice->sub_total, + 'tax' => $this->invoice->tax, + 'total' => $this->invoice->total, + 'status' => $this->invoice->status, + 'date' => $this->invoice->date, + 'due_date' => $this->invoice->due_date, + 'date_paid' => $this->invoice->date_paid, + ]), + 'amount' => $this->amount, + 'payment_method' => $this->whenLoaded('payment', fn() => $this->payment->name), + 'payment_proof' => $this->payment_proof, + 'note' => $this->note, + 'client_ip' => $this->client_ip, + 'status' => $this->status, + 'date' => $this->date, 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'order_items' => $this->whenLoaded('orderItems', fn() => $this->orderItems->map(function ($item) { + return [ + 'id' => $item->id, + 'product' => $item->product ? [ + 'id' => $item->product->id, + 'name' => $item->product->name, + 'product_type' => $item->product->product_type, + ] : null, + 'os' => $item->os ? [ + 'id' => $item->os->id, + 'name' => $item->os->name, + 'version' => $item->os->version, + ] : null, + 'billing_cycle' => $item->billing_cycle, + 'months' => $item->months, + 'expiry_date' => $item->expiry_date, + 'quantity' => $item->quantity, + 'server_name' => $item->server_name, + 'price' => $item->price, + ]; + })), ]; } } diff --git a/app/Http/Resources/Api/Admin/ProductResource.php b/app/Http/Resources/Api/Admin/ProductResource.php index a295171..316d6eb 100644 --- a/app/Http/Resources/Api/Admin/ProductResource.php +++ b/app/Http/Resources/Api/Admin/ProductResource.php @@ -13,6 +13,10 @@ class ProductResource extends JsonResource return $this->pricings->min('price'); }); + $osIds = $this->whenLoaded('operatingSystems', function () { + return $this->operatingSystems->pluck('id')->toArray(); + }); + return [ 'id' => $this->id, 'uuid' => $this->uuid, @@ -30,6 +34,7 @@ class ProductResource extends JsonResource 'min_price' => $minPrice, 'is_active' => $this->is_active, 'created_at' => $this->created_at, + 'os_ids' => $osIds, ]; } diff --git a/app/Models/Order.php b/app/Models/Order.php index 1c10913..5bc8292 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -46,7 +46,7 @@ class Order extends Model public function payment() { - return $this->belongsTo(\App\Models\Payment::class); + return $this->belongsTo(\App\Models\Payment::class, 'payment_method', 'id'); } public function user() @@ -56,7 +56,7 @@ class Order extends Model public function orderItem() { - return $this->belongsToMany(\App\Models\InvoiceItem::class, 'order__order_item', 'order_id', 'invoice_item_id'); + return $this->belongsToMany(\App\Models\InvoiceItem::class, 'order_items', 'order_id', 'invoice_item_id'); } public function orderItems() diff --git a/app/Models/OrderItem.php b/app/Models/OrderItem.php index 4b88bbd..40c20c1 100644 --- a/app/Models/OrderItem.php +++ b/app/Models/OrderItem.php @@ -9,9 +9,10 @@ class OrderItem extends Model { use HasFactory; - protected $table = 'order__order_item'; + protected $table = 'order_items'; protected $guarded = []; public $timestamps = false; + public function order() { return $this->belongsTo(Order::class, 'order_id'); diff --git a/app/Models/Payment.php b/app/Models/Payment.php index a6bd450..e19d5f5 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -24,6 +24,10 @@ class Payment extends Model { return $this->belongsTo(\App\Models\Invoice::class); } + public function order() + { + return $this->belongsTo(\App\Models\Order::class); + } public function getStatus() { return match ($this->attributes['status']) { diff --git a/app/Models/Product.php b/app/Models/Product.php index cc46a80..6b4bc6d 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -75,6 +75,15 @@ class Product extends Model ->withTimestamps(); } + /** + * Relationship: Product has many OS via product_os table (VPS OS selection) + */ + public function operatingSystems() + { + return $this->belongsToMany(Os::class, 'product_os', 'product_id', 'os_id') + ->withTimestamps(); + } + /** * Relationship: Product has many Control Panels (for Hosting) */ diff --git a/database/migrations/2026_03_11_000002_add_payment_proof_to_order_table.php b/database/migrations/2026_03_11_000002_add_payment_proof_to_order_table.php new file mode 100644 index 0000000..2d79eef --- /dev/null +++ b/database/migrations/2026_03_11_000002_add_payment_proof_to_order_table.php @@ -0,0 +1,21 @@ +string('payment_proof')->nullable()->after('payment_method'); + }); + } + + public function down(): void + { + Schema::table('order', function (Blueprint $table) { + $table->dropColumn('payment_proof'); + }); + } +}; diff --git a/database/migrations/2026_03_11_144540_rename_order_items_table.php b/database/migrations/2026_03_11_144540_rename_order_items_table.php new file mode 100644 index 0000000..de2224c --- /dev/null +++ b/database/migrations/2026_03_11_144540_rename_order_items_table.php @@ -0,0 +1,24 @@ +timestamp('expiry_date')->nullable()->after('months'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('order_items', function (Blueprint $table) { + $table->dropColumn('expiry_date'); + }); + } +}; diff --git a/database/seeders/BankTransferPaymentSeeder.php b/database/seeders/BankTransferPaymentSeeder.php new file mode 100644 index 0000000..3557fd8 --- /dev/null +++ b/database/seeders/BankTransferPaymentSeeder.php @@ -0,0 +1,69 @@ + 'Số dư tài khoản', + 'type' => 0, + 'accountNo' => '', + 'bankName' => '', + 'bankCode' => '', + 'logo' => '', + 'status' => 1, + ], + [ + 'name' => 'Chuyển khoản ngân hàng', + 'type' => 1, + 'accountNo' => '', + 'bankName' => '', + 'bankCode' => '', + 'logo' => '', + 'status' => 1, + ], + [ + 'name' => 'Chuyển khoản VNPAY', + 'type' => 1, + 'accountNo' => '', + 'bankName' => 'VNPAY', + 'bankCode' => 'VNPAY', + 'logo' => '', + 'status' => 1, + ], + [ + 'name' => 'Chuyển khoản ACB', + 'type' => 1, + 'accountNo' => '', + 'bankName' => 'Ngân hàng ACB', + 'bankCode' => 'ACB', + 'logo' => '', + 'status' => 1, + ], + [ + 'name' => 'Chuyển khoản MB', + 'type' => 1, + 'accountNo' => '', + 'bankName' => 'Ngân hàng MB', + 'bankCode' => 'MB', + 'logo' => '', + 'status' => 1, + ], + ]; + + foreach ($methods as $method) { + DB::table('payment') + ->where('name', $method['name']) + ->exists() ?: DB::table('payment')->insert(array_merge($method, [ + 'created_at' => now(), + 'updated_at' => now(), + ])); + } + } +} diff --git a/routes/api.php b/routes/api.php index 3d1aa91..7afc416 100644 --- a/routes/api.php +++ b/routes/api.php @@ -15,6 +15,8 @@ use App\Http\Controllers\Api\Admin\VPS\VPSController; use App\Http\Controllers\Api\Admin\VCenter\VCenterController; use App\Http\Controllers\Api\Admin\DnsManager\ServerController as DnsServerController; use App\Http\Controllers\Api\Admin\DnsManager\DomainController as DnsDomainController; +use App\Http\Controllers\Api\Admin\OsManager\OsGroupController; +use App\Http\Controllers\Api\Admin\OsManager\OsController; use App\Http\Services\Media; use App\Http\Services\vCenter as ServiceVCenter; use App\Models\ModuleVmware; @@ -92,8 +94,13 @@ Route::prefix('v1') Route::get('/', [ProductController::class, 'index'])->middleware('perm:product-service.view'); Route::post('/', [ProductController::class, 'store'])->middleware('perm:product-service.create'); Route::post('import', [ProductController::class, 'import'])->middleware('perm:product-service.create'); + + // OS list for product configuration - must be before {id} routes + Route::get('os', [ProductController::class, 'getOsList'])->middleware('perm:product-service.view'); + Route::get('{id}', [ProductController::class, 'show'])->middleware('perm:product-service.view'); Route::get('{id}/os', [ProductController::class, 'getOs'])->middleware('perm:product-service.view'); + Route::get('{id}/operating-systems', [ProductController::class, 'getOperatingSystems'])->middleware('perm:product-service.view'); Route::put('{id}', [ProductController::class, 'update'])->middleware('perm:product-service.edit'); Route::delete('{id}', [ProductController::class, 'destroy'])->middleware('perm:product-service.destroy'); Route::post('{id}/duplicate', [ProductController::class, 'duplicate'])->middleware('perm:product-service.create'); @@ -115,6 +122,7 @@ Route::prefix('v1') Route::prefix('orders')->group(function () { Route::get('/', [OrderController::class, 'index'])->middleware('perm:order.view'); Route::get('/datatable', [OrderController::class, 'datatable'])->middleware('perm:order.view'); + Route::post('/upload-proof', [OrderController::class, 'uploadProof'])->middleware('perm:order.create'); Route::post('/', [OrderController::class, 'store'])->middleware('perm:order.create'); Route::get('{id}', [OrderController::class, 'show'])->middleware('perm:order.view'); Route::put('{id}', [OrderController::class, 'update'])->middleware('perm:order.edit'); @@ -143,6 +151,7 @@ Route::prefix('v1') // Payments (module: payment) Route::prefix('payments')->group(function () { + Route::get('for-select', [PaymentController::class, 'forSelect']); Route::get('/', [PaymentController::class, 'index'])->middleware('perm:payment.view'); Route::post('/', [PaymentController::class, 'store'])->middleware('perm:payment.create'); Route::get('{id}', [PaymentController::class, 'show'])->middleware('perm:payment.view'); @@ -150,6 +159,27 @@ Route::prefix('v1') Route::delete('{id}', [PaymentController::class, 'destroy'])->middleware('perm:payment.destroy'); }); + // OS Groups & OS Entries (module: module-vcenter) + Route::prefix('os-groups') + ->controller(OsGroupController::class) + ->as('os-groups.') + ->group(function () { + Route::get('/', 'index')->middleware('perm:module-vcenter.manager')->name('index'); + Route::post('/', 'store')->middleware('perm:module-vcenter.manager')->name('store'); + Route::get('{id}', 'show')->middleware('perm:module-vcenter.manager')->name('show'); + Route::put('{id}', 'update')->middleware('perm:module-vcenter.manager')->name('update'); + Route::delete('{id}', 'destroy')->middleware('perm:module-vcenter.manager')->name('destroy'); + }); + + Route::prefix('os') + ->controller(OsController::class) + ->as('os.') + ->group(function () { + Route::post('/', 'store')->middleware('perm:module-vcenter.manager')->name('store'); + Route::put('{id}', 'update')->middleware('perm:module-vcenter.manager')->name('update'); + Route::delete('{id}', 'destroy')->middleware('perm:module-vcenter.manager')->name('destroy'); + }); + // VPS (module: module-vcenter) Route::prefix('vps')->group(function () { Route::get('/', [VPSController::class, 'index'])->middleware('perm:module-vcenter.view'); diff --git a/storage/app/public/payment-proofs/48lYy8R6JFgbqasJy5dfSLxWR1miOvpUIjejIPle.jpg b/storage/app/public/payment-proofs/48lYy8R6JFgbqasJy5dfSLxWR1miOvpUIjejIPle.jpg new file mode 100644 index 0000000..8322426 Binary files /dev/null and b/storage/app/public/payment-proofs/48lYy8R6JFgbqasJy5dfSLxWR1miOvpUIjejIPle.jpg differ