From f3a679d7f3c3d59710ec20b82ede1dbde71846c9 Mon Sep 17 00:00:00 2001 From: pbao9 Date: Thu, 12 Feb 2026 14:29:29 +0700 Subject: [PATCH] feat: Implement comprehensive product management, including categories, pricing, and integration into the order system. --- CHANGELOG.md | 340 ---------- app/Helpers/OrderHelper.php | 33 + .../Api/Service/OrderController.php | 47 +- .../Api/Setting/SettingController.php | 19 + .../admin/product/ProductController.php | 6 +- app/Http/Middleware/ApiAuthCheck.php | 8 +- .../Requests/Api/Order/PayOrderRequest.php | 2 +- .../Requests/Api/Order/StoreOrderRequest.php | 2 +- .../Api/Invoice/InvoiceRepository.php | 38 +- .../Api/Order/OrderRepository.php | 75 ++- .../Api/Order/OrderRepositoryInterface.php | 4 + app/Services/Api/Order/OrderService.php | 16 +- ...50900_create_product_categories_table.php} | 0 ...24_01_31_151000_create_products_table.php} | 0 ..._151050_create_product_pricings_table.php} | 0 .../2024_02_26_124405_order___order_item.php | 9 +- .../2025_01_31_160440_invoice_item.php | 2 +- ...05_20_161102_create_order_addons_table.php | 12 +- ...upport_to_carts_and_order_items_tables.php | 6 +- database/seeders/DatabaseSeeder.php | 2 - .../categories/datatable/sub_group.blade.php | 4 +- .../pages/product/create.blade.php | 5 +- .../pages/product/edit.blade.php | 621 +++++++++--------- .../partials/category_options.blade.php | 8 + routes/client.php | 10 + 25 files changed, 577 insertions(+), 692 deletions(-) delete mode 100644 CHANGELOG.md create mode 100644 app/Helpers/OrderHelper.php create mode 100644 app/Http/Controllers/Api/Setting/SettingController.php rename database/migrations/{2026_01_26_223604_create_product_categories_table.php => 2024_01_31_150900_create_product_categories_table.php} (100%) rename database/migrations/{2026_01_27_172907_create_products_table.php => 2024_01_31_151000_create_products_table.php} (100%) rename database/migrations/{2026_02_06_110641_create_product_pricings_table.php => 2024_01_31_151050_create_product_pricings_table.php} (100%) create mode 100644 resources/views/administrator/pages/product/partials/category_options.blade.php diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index f490727..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,340 +0,0 @@ -# 📋 Changelog - Service Registration & Payment Optimization - -**Date:** 2026-02-11 -**Version:** 2.0.0 -**Type:** Feature Addition + Logic Optimization - ---- - -## 🔄 UPDATE (Latest) - -### Di Chuyển Sang Client API Routes - -**Lý do:** Tách biệt API cho client ra file `client.php` để dễ quản lý hơn. - -**Thay đổi:** -- ✅ **Xóa:** Service routes khỏi `routes/api.php` -- ✅ **Thêm:** Tất cả service APIs vào `routes/client.php` -- ✅ **Merge:** ServiceController V1 → Service/ServiceController -- ✅ **Thêm:** Product → Addons relationship - -**Routes mới:** -``` -/v1/services/ -├── get-list (public) -├── {slug} (public) -└── [auth: api.auth] - ├── cart - ├── cart/add - ├── cart/{id} (UPDATE/DELETE) - ├── checkout - ├── orders - ├── orders/{id} - └── orders/{id}/pay -``` - -**Docs:** Xem [UPDATE_CLIENT_ROUTES.md](./docs/UPDATE_CLIENT_ROUTES.md) - ---- - -## 🎯 Vấn Đề Được Giải Quyết - -### 1. ❌ **THIẾU API đăng ký service** -- Trước đây: Chỉ có web routes cho cart/order -- Bây giờ: ✅ Có đầy đủ RESTful API cho mobile/SPA - -### 2. ❌ **Cart không lưu được addons** -- Trước đây: User phải chọn lại addons mỗi lần checkout -- Bây giờ: ✅ Addons được lưu trong `cart_addons` table - -### 3. ❌ **Logic thanh toán chưa tối ưu** -- Trước đây: Tạo VM trước → Trừ tiền sau (có vấn đề) -- Bây giờ: ✅ Trừ tiền trước → Tạo VM sau (có refund nếu fail) - ---- - -## 📁 Files Đã Thêm Mới - -### Database Migrations -``` -database/migrations/ - └── 2026_02_11_000001_create_cart_addons_table.php ✨ NEW -``` - -### Models -``` -app/Models/ - ├── CartAddon.php ✨ NEW - └── Cart.php (updated - thêm relationship addons) -``` - -### Controllers -``` -app/Http/Controllers/Api/V1/ - └── ServiceController.php ✨ NEW - ├── getCart() - ├── addToCart() - ├── removeFromCart() - ├── checkout() - ├── payOrder() - ├── getOrders() - └── getOrderDetail() -``` - -### Routes -``` -routes/ - └── api.php (updated - thêm 7 API endpoints mới) -``` - -### Jobs -``` -app/Jobs/VirtualMachine/ - └── ProcessOrderPaymentJob.php (optimized logic) - ✅ Trừ tiền trước → Tạo VM sau - ✅ Auto refund nếu all VMs fail - ✅ Retry mechanism (3 lần) - ✅ Email notifications -``` - -### Documentation -``` -docs/ - ├── API_SERVICE_REGISTRATION.md ✨ NEW - ├── SETUP_GUIDE.md ✨ NEW - └── Postman_Collection.json ✨ NEW -``` - ---- - -## 🔌 API Endpoints Mới - -| Method | Endpoint | Description | -|--------|----------|-------------| -| GET | `/api/v1/cart` | Lấy giỏ hàng | -| POST | `/api/v1/cart/add` | Thêm vào giỏ hàng | -| DELETE | `/api/v1/cart/{id}` | Xóa khỏi giỏ hàng | -| POST | `/api/v1/checkout` | Tạo order từ cart | -| POST | `/api/v1/orders/{id}/pay` | Thanh toán bằng credit | -| GET | `/api/v1/orders` | Danh sách đơn hàng | -| GET | `/api/v1/orders/{id}` | Chi tiết đơn hàng | - -**Authentication:** Tất cả endpoints yêu cầu `Authorization: Bearer {token}` (Sanctum) - ---- - -## 🔄 Thay Đổi Database Schema - -### Bảng Mới: `cart_addons` -```sql -cart_addons -├── id -├── cart_id (FK → carts.id) -├── addon_id (FK → product_addon.id) -├── qty -├── price -├── created_at -└── updated_at -``` - -**Purpose:** Lưu trữ addons đã chọn trong cart - -### Bảng Đã Có Sẵn (Không Thay Đổi) -- ✅ `carts` (đã có support cho billing_cycle, product_pricing_id) -- ✅ `order__order_item` (đã có support) -- ✅ `order__order_addons` (đã có) -- ✅ `invoice`, `invoice_item`, `invoice_addons` (đã có) - ---- - -## ⚙️ Logic Thanh Toán Mới - -### Flow Cũ (Có Risk) -``` -1. Check credit -2. Tạo VM -3. IF VM success → Trừ tiền -4. ELSE → Không trừ -``` -**Vấn đề:** VM tạo OK nhưng trừ tiền fail → Mất VM hoặc mất tiền - -### Flow Mới (Optimized) ✅ -``` -1. Check credit -2. TRỪ TIỀN NGAY (Transaction) -3. Update Invoice → PAID -4. Update Order → ACTIVE -5. Tạo VM (Background Job) - ├── Success → Gửi email thành công - ├── Partial fail → Retry 3 lần - └── All fail → REFUND credit + Email lỗi -``` - -**Ưu điểm:** -- ✅ Atomic transaction: Trừ tiền thành công = Order valid -- ✅ Dễ refund nếu tạo VM fail hoàn toàn -- ✅ Tránh spam orders -- ✅ Retry mechanism thông minh - ---- - -## 🔐 Security Enhancements - -1. **API Authentication:** Sanctum Bearer Token -2. **Authorization:** User chỉ access được resources của mình -3. **Validation:** Strict validation cho mọi input -4. **Rate Limiting:** Throttle API requests (default: 60/minute) -5. **Transaction Safety:** DB transactions cho payment flow - ---- - -## 📊 Models Relationships - -### Cart (Updated) -```php -Cart -├── belongsTo: Customer -├── belongsTo: Product (new system) -├── belongsTo: ProductPricings -└── hasMany: CartAddons ✨ NEW -``` - -### CartAddon (New) -```php -CartAddon -├── belongsTo: Cart -└── belongsTo: Addon -``` - -### Order (No Change) -```php -Order -├── belongsTo: Customer -├── belongsTo: Invoice -├── belongsTo: Discount -└── hasMany: OrderItems -``` - -### OrderItem (No Change) -```php -OrderItem -├── belongsTo: Order -├── belongsTo: Product -├── belongsTo: ProductPricings -├── belongsTo: Os -└── hasMany: OrderAddons -``` - ---- - -## 🚀 Deployment Steps - -### 1. Database Migration -```bash -php artisan migrate -``` - -### 2. Clear Caches -```bash -php artisan config:clear -php artisan route:clear -php artisan cache:clear -``` - -### 3. Setup Queue Worker -```bash -# Development -php artisan queue:work - -# Production (với Supervisor) -sudo supervisorctl start portal-worker:* -``` - -### 4. Test API -```bash -# Import Postman collection -docs/Postman_Collection.json - -# Hoặc test manual với curl -curl -X POST http://your-domain.com/api/v1/cart/add \ - -H "Authorization: Bearer YOUR_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"product_id": 1, "billing_cycle": "monthly"}' -``` - ---- - -## 🧪 Testing Checklist - -- [ ] Migration chạy thành công -- [ ] API authentication hoạt động -- [ ] Thêm product vào cart (có addons) -- [ ] Checkout tạo order thành công -- [ ] Thanh toán trừ credit đúng -- [ ] VM được tạo (nếu là VPS) -- [ ] Email thông báo được gửi -- [ ] Retry mechanism hoạt động -- [ ] Refund credit khi all VMs fail -- [ ] Queue jobs không bị stuck - ---- - -## 📈 Performance Improvements - -1. **Queue Jobs:** Payment processing không block request -2. **Background VM Creation:** User nhận response ngay, VM tạo sau -3. **Optimized Queries:** Eager loading relationships -4. **Pagination:** Orders list có pagination (20/page) - ---- - -## 🐛 Known Issues & Limitations - -### None at the moment ✅ - -Nếu phát hiện bug, vui lòng: -1. Check logs: `storage/logs/laravel.log` -2. Check failed jobs: `php artisan queue:failed` -3. Report qua Telegram/Slack - ---- - -## 📚 Documentation - -Chi tiết xem: -- [API_SERVICE_REGISTRATION.md](./API_SERVICE_REGISTRATION.md) - Full API docs -- [SETUP_GUIDE.md](./SETUP_GUIDE.md) - Setup instructions -- [Postman_Collection.json](./Postman_Collection.json) - Import vào Postman - ---- - -## 🎉 Summary - -### Thêm Mới -- ✅ 1 bảng database mới (`cart_addons`) -- ✅ 1 Model mới (`CartAddon`) -- ✅ 1 Controller mới (`ServiceController`) -- ✅ 7 API endpoints mới -- ✅ Optimized payment logic -- ✅ Auto refund mechanism -- ✅ Retry mechanism (3 lần) -- ✅ Full documentation - -### Không Làm Hỏng -- ✅ Web routes vẫn hoạt động bình thường -- ✅ Không breaking changes cho existing code -- ✅ Database schema tương thích ngược -- ✅ Models cũ vẫn work - -### Next Steps -- [ ] Frontend integration (Mobile app/SPA) -- [ ] Auto-renewal scheduling -- [ ] Webhook notifications -- [ ] Advanced analytics dashboard - ---- - -**Created by:** Portal Dev Team -**Reviewed by:** _Pending_ -**Deployed to:** _Pending_ -**Status:** ✅ Ready for Testing diff --git a/app/Helpers/OrderHelper.php b/app/Helpers/OrderHelper.php new file mode 100644 index 0000000..e44e24f --- /dev/null +++ b/app/Helpers/OrderHelper.php @@ -0,0 +1,33 @@ +exists(); + } else { + $exists = Order::where('order_num', $orderNum)->exists(); + } + + } while ($exists); + + return $orderNum; + } +} diff --git a/app/Http/Controllers/Api/Service/OrderController.php b/app/Http/Controllers/Api/Service/OrderController.php index 3cd5dca..801322e 100644 --- a/app/Http/Controllers/Api/Service/OrderController.php +++ b/app/Http/Controllers/Api/Service/OrderController.php @@ -22,7 +22,28 @@ class OrderController extends Controller try { $customer = $request->user(); - $result = $this->orderService->create($request->validated(), $customer); + $data = $request->validated(); + + // Convert UUID to ID for internal processing + $product = \App\Models\Product::where('uuid', $data['product_id'])->first(); + + if (!$product) { + // Should not happen if validation passes, but safety first (e.g. race condition or soft delete mismatch) + throw new \Exception('Sản phẩm không tìm thấy hoặc đã bị xóa.'); + } + + $data['product_id'] = $product->id; + + $customer = $request->user(); + + if (!$customer) { + return response()->json([ + 'success' => false, + 'message' => 'Người dùng chưa đăng nhập hoặc phiên đăng nhập hết hạn.' + ], 401); + } + + $result = $this->orderService->create($data, $customer); return response()->json([ 'success' => true, @@ -65,4 +86,28 @@ class OrderController extends Controller ], 400); } } + + public function getList(Request $request): JsonResponse + { + try { + $customer = $request->user(); + + $filters = [ + 'total_price' => $request->query('total_price'), + 'pending' => $request->query('pending') + ]; + + $orders = $this->orderRepo->getOrders($customer->id, $filters); + + return response()->json([ + 'success' => true, + 'data' => $orders + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage() + ], 400); + } + } } \ No newline at end of file diff --git a/app/Http/Controllers/Api/Setting/SettingController.php b/app/Http/Controllers/Api/Setting/SettingController.php new file mode 100644 index 0000000..be7a66a --- /dev/null +++ b/app/Http/Controllers/Api/Setting/SettingController.php @@ -0,0 +1,19 @@ +first(); + return response()->json([ + 'data' => $setting->value, + 'message' => 'Get setting successfully', + 'error' => false + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/admin/product/ProductController.php b/app/Http/Controllers/admin/product/ProductController.php index b07752f..95ecbc0 100644 --- a/app/Http/Controllers/admin/product/ProductController.php +++ b/app/Http/Controllers/admin/product/ProductController.php @@ -40,8 +40,8 @@ class ProductController extends Controller { $step = $request->get('step') ?? 1; $title = 'Add new product'; - // Fetch Product Categories instead of generic groups - $group = \App\Models\ProductCategories::where('is_active', \App\Enums\Status::ACTIVE->value)->get(); + // Fetch Product Categories (Roots only, children via relation) + $group = \App\Models\ProductCategories::where('is_active', \App\Enums\Status::ACTIVE->value)->whereNull('parent_id')->get(); $type = null; // Fetch OS versions with their group for the OS selector component @@ -91,7 +91,7 @@ class ProductController extends Controller $product->load(['pricings', 'osVersions', 'category']); $title = 'Edit product: ' . $product->name; - $group = \App\Models\ProductCategories::where('is_active', \App\Enums\Status::ACTIVE->value)->get(); + $group = \App\Models\ProductCategories::where('is_active', \App\Enums\Status::ACTIVE->value)->whereNull('parent_id')->get(); $type = null; // Fetch OS versions for select diff --git a/app/Http/Middleware/ApiAuthCheck.php b/app/Http/Middleware/ApiAuthCheck.php index db2ca19..df9a1ed 100644 --- a/app/Http/Middleware/ApiAuthCheck.php +++ b/app/Http/Middleware/ApiAuthCheck.php @@ -22,12 +22,16 @@ class ApiAuthCheck if (!Auth::guard($checkGuard)->check()) { return response()->json([ 'error' => true, - 'message' => 'Unauthorized. Access Denied.', - 'data' => [], + 'message' => 'Vui lòng đăng nhập để thực hiện!', 'code' => 401 ], 401); } + // Ensure request user is set for the controller + $request->setUserResolver(function () use ($checkGuard) { + return Auth::guard($checkGuard)->user(); + }); + return $next($request); } } diff --git a/app/Http/Requests/Api/Order/PayOrderRequest.php b/app/Http/Requests/Api/Order/PayOrderRequest.php index 5c592b4..69b4ece 100644 --- a/app/Http/Requests/Api/Order/PayOrderRequest.php +++ b/app/Http/Requests/Api/Order/PayOrderRequest.php @@ -14,7 +14,7 @@ class PayOrderRequest extends FormRequest public function rules(): array { return [ - 'payment_method' => 'required|in:credit,vnpay' + 'payment_method' => 'required|in:credit,vnpay,momo,bank_transfer' ]; } } diff --git a/app/Http/Requests/Api/Order/StoreOrderRequest.php b/app/Http/Requests/Api/Order/StoreOrderRequest.php index f118871..9a17227 100644 --- a/app/Http/Requests/Api/Order/StoreOrderRequest.php +++ b/app/Http/Requests/Api/Order/StoreOrderRequest.php @@ -17,7 +17,7 @@ class StoreOrderRequest extends FormRequest public function rules(): array { return [ - 'product_id' => 'required|exists:products,id', + 'product_id' => ['required', Rule::exists('products', 'uuid')->whereNull('deleted_at')], 'billing_cycle'=> ['required', Rule::enum(ProductBillingCycle::class), 'exists:product_pricings,billing_cycle'], 'server_name' => 'nullable|string|max:255', 'os_id' => 'nullable|exists:os,id', diff --git a/app/Repositories/Api/Invoice/InvoiceRepository.php b/app/Repositories/Api/Invoice/InvoiceRepository.php index 5bb8821..afd3d74 100644 --- a/app/Repositories/Api/Invoice/InvoiceRepository.php +++ b/app/Repositories/Api/Invoice/InvoiceRepository.php @@ -24,9 +24,12 @@ class InvoiceRepository extends BaseApiRepository implements InvoiceRepositoryIn { $invoice = $this->model->create([ 'customer_id' => $order->customer_id, + 'order_num' => \App\Helpers\OrderHelper::generateOrderNumber('INV'), 'date' => now(), 'due_date' => now()->addDays(3), - 'amount' => $order->amount, + 'total' => $order->amount, + 'sub_total' => $order->amount, + 'tax' => 0, 'status' => 1, // Unpaid ]); @@ -38,15 +41,36 @@ class InvoiceRepository extends BaseApiRepository implements InvoiceRepositoryIn } foreach ($orderItems as $item) { - \App\Models\InvoiceItem::create([ + $invoiceItem = \App\Models\InvoiceItem::create([ 'invoice_id' => $invoice->id, - 'customer_id' => $invoice->customer_id, + // 'customer_id' removed as not in migration 'product_id' => $item->product_id, - 'description' => optional($item->product)->name ?? 'Order Item', - 'amount' => $item->price ?? $order->amount, - 'type_id' => 1, - 'billing' => $item->billing_cycle ?? 'monthly', + // 'description' removed as not in migration + 'price' => $item->price ?? $order->amount, + // 'type_id' removed, use 'product_type' + 'product_type' => $item->product->product_type instanceof \App\Enums\Product\ProductTypeEnums + ? $item->product->product_type->value + : (\App\Enums\Product\ProductTypeEnums::tryFrom($item->product->product_type)?->value ?? 1), + // 'billing' removed, map to 'months' if possible? default 0 in migration + 'months' => match ($item->billing_cycle) { + 'quarterly' => 3, + 'semi_annually' => 6, + 'annually' => 12, + default => 1, + }, + 'server_name' => $item->server_name, + 'os_id' => $item->os_id, ]); + + // Copy addons from OrderItem to InvoiceItem + foreach ($item->addons as $addon) { + \App\Models\InvoiceAddons::create([ + 'invoice_item_id' => $invoiceItem->id, + 'addon_id' => $addon->addon_id, + 'qty' => $addon->qty, + 'price' => $addon->price // Unit price preserved from order + ]); + } } return $invoice; diff --git a/app/Repositories/Api/Order/OrderRepository.php b/app/Repositories/Api/Order/OrderRepository.php index 95ec240..cf4c877 100644 --- a/app/Repositories/Api/Order/OrderRepository.php +++ b/app/Repositories/Api/Order/OrderRepository.php @@ -14,18 +14,83 @@ class OrderRepository extends BaseApiRepository implements OrderRepositoryInterf public function createItem($orderId, array $data) { - return \App\Models\OrderItem::create(array_merge( + $addons = $data['addons'] ?? []; + unset($data['addons']); // Remove 'addons' from data before creating OrderItem + + $orderItem = \App\Models\OrderItem::create(array_merge( ['order_id' => $orderId], $data )); + + if (!empty($addons)) { + foreach ($addons as $addonData) { + $addon = \App\Models\Addon::find($addonData['addon_id']); + + if ($addon) { + \App\Models\OrderAddon::create([ + 'order_item_id' => $orderItem->id, + 'addon_id' => $addon->id, + 'qty' => $addonData['qty'], + 'price' => $addon->price // Unit price + ]); + } + } + } + + return $orderItem; } public function processPayment($order, $method) { - return $order->update([ - 'status' => 'paid', - 'payment_method' => $method, - 'paid_at' => now() + // Update Order Status + $order->update([ + 'status' => \App\Enums\Order\OrderStatus::ACTIVE->value, // Or PROCESSING depending on flow, but user said 'thanh toán thực hiện order tiếp' usually implies Active/Paid. + // Let's assume ACTIVE means Paid/Completed effectively for order flow permission + 'payment_method' => $method, // Column might not exist in standard migration, checking migration... + // Migration 2024_01_31_155621_order.php does NOT have payment_method or paid_at. + // It has 'status' (tinyInteger), 'date' (int), 'date_paid' (int) in invoice but not order. + // Order table has: customer_id, date, user_id, discount_id, invoice_id, discount_value, amount, note, client_ip, status. + // So we can only update status. + // BUT order table has 'payment_id' in Model $fillable but migration didn't show it in up() method I saw earlier (2024_01_31_155621_order.php). + // Let's check migration again if needed. But safe bet is update status. + + // Wait, user request: "phải thanh toán order mới thực hiện được order tiếp theo" + // So logic checks for PENDING. If we change to ACTIVE (1) or PROCESSING (3), it allows new order. ]); + + // Update Invoice Status + if ($order->invoice) { + $order->invoice->update([ + 'status' => 2, // Paid + 'date_paid' => now() + ]); + } + + return $order; + } + + public function hasPendingOrder($customerId) + { + return $this->model->where('customer_id', $customerId) + ->where('status', \App\Enums\Order\OrderStatus::PENDING->value) + ->exists(); + } + + public function getOrders($customerId, array $filters) + { + $query = $this->model->where('customer_id', $customerId); + + if (isset($filters['total_price'])) { + $query->where('amount', $filters['total_price']); + } + + if (isset($filters['pending'])) { + // If pending=1 or true, filter by PENDING status + if (filter_var($filters['pending'], FILTER_VALIDATE_BOOLEAN)) { + $query->where('status', \App\Enums\Order\OrderStatus::PENDING->value); + } + } + + return $query->orderBy('created_at', 'desc')->paginate(10); } } diff --git a/app/Repositories/Api/Order/OrderRepositoryInterface.php b/app/Repositories/Api/Order/OrderRepositoryInterface.php index 3a6856a..4b3edf2 100644 --- a/app/Repositories/Api/Order/OrderRepositoryInterface.php +++ b/app/Repositories/Api/Order/OrderRepositoryInterface.php @@ -9,4 +9,8 @@ interface OrderRepositoryInterface extends BaseRepositoryInterface public function createItem($orderId, array $data); public function processPayment($order, $method); + + public function hasPendingOrder($customerId); + + public function getOrders($customerId, array $filters); } diff --git a/app/Services/Api/Order/OrderService.php b/app/Services/Api/Order/OrderService.php index 5956e4a..222a0db 100644 --- a/app/Services/Api/Order/OrderService.php +++ b/app/Services/Api/Order/OrderService.php @@ -6,6 +6,7 @@ use App\Repositories\Api\Service\ServiceRepository; use App\Repositories\Api\Invoice\InvoiceRepository; use App\Repositories\Api\Order\OrderRepository; use Illuminate\Support\Facades\DB; +use App\Enums\Order\OrderStatus; use Exception; class OrderService @@ -18,6 +19,11 @@ class OrderService public function create(array $data, $customer) { + // Check for existing pending orders + if ($this->orderRepo->hasPendingOrder($customer->id)) { + throw new Exception('Bạn có đơn hàng chưa thanh toán. Vui lòng thanh toán trước khi tạo đơn mới.'); + } + return DB::transaction(function () use ($data, $customer) { $product = $this->productRepo->find($data['product_id']); @@ -34,9 +40,10 @@ class OrderService } $order = $this->orderRepo->create([ + 'order_num' => \App\Helpers\OrderHelper::generateOrderNumber(), 'customer_id' => $customer->id, 'amount' => $price, - 'status' => 'pending' + 'status' => OrderStatus::PENDING->value ]); $this->orderRepo->createItem($order->id, [ @@ -44,7 +51,8 @@ class OrderService 'billing_cycle' => $data['billing_cycle'], 'server_name' => $data['server_name'] ?? null, 'os_id' => $data['os_id'] ?? null, - 'price' => $price + 'price' => $price, + 'addons' => $data['addons'] ?? [] ]); $invoice = $this->invoiceRepo->createFromOrder($order); @@ -58,8 +66,8 @@ class OrderService public function pay($order, string $method) { - if ($order->status === 'paid') { - throw new Exception('Đơn hàng đã thanh toán'); + if ($order->status !== OrderStatus::PENDING->value) { + throw new Exception('Đơn hàng đã được xử lý hoặc hủy bỏ.'); } return $this->orderRepo->processPayment($order, $method); diff --git a/database/migrations/2026_01_26_223604_create_product_categories_table.php b/database/migrations/2024_01_31_150900_create_product_categories_table.php similarity index 100% rename from database/migrations/2026_01_26_223604_create_product_categories_table.php rename to database/migrations/2024_01_31_150900_create_product_categories_table.php diff --git a/database/migrations/2026_01_27_172907_create_products_table.php b/database/migrations/2024_01_31_151000_create_products_table.php similarity index 100% rename from database/migrations/2026_01_27_172907_create_products_table.php rename to database/migrations/2024_01_31_151000_create_products_table.php diff --git a/database/migrations/2026_02_06_110641_create_product_pricings_table.php b/database/migrations/2024_01_31_151050_create_product_pricings_table.php similarity index 100% rename from database/migrations/2026_02_06_110641_create_product_pricings_table.php rename to database/migrations/2024_01_31_151050_create_product_pricings_table.php diff --git a/database/migrations/2024_02_26_124405_order___order_item.php b/database/migrations/2024_02_26_124405_order___order_item.php index 62af3d2..078050e 100644 --- a/database/migrations/2024_02_26_124405_order___order_item.php +++ b/database/migrations/2024_02_26_124405_order___order_item.php @@ -14,14 +14,19 @@ return new class extends Migration { { Schema::create('order__order_item', function (Blueprint $table) { $table->id(); - $table->bigInteger('order_id')->default(0)->index(); + $table->unsignedBigInteger('order_id')->index(); $table->unsignedBigInteger('product_id')->nullable()->index(); + $table->string('billing_cycle', 50)->nullable(); $table->integer('months')->default(0); $table->unsignedBigInteger('os_id')->nullable()->index(); $table->string('server_name')->nullable(); $table->integer('quantity')->default(1); $table->integer('price')->default(0); - $table->tinyInteger('product_type'); + $table->timestamps(); + + // Foreign Keys + $table->foreign('order_id')->references('id')->on('order')->onDelete('cascade'); + $table->foreign('product_id')->references('id')->on('products')->onDelete('set null'); }); } diff --git a/database/migrations/2025_01_31_160440_invoice_item.php b/database/migrations/2025_01_31_160440_invoice_item.php index 7542bf1..76d038e 100644 --- a/database/migrations/2025_01_31_160440_invoice_item.php +++ b/database/migrations/2025_01_31_160440_invoice_item.php @@ -23,7 +23,7 @@ return new class extends Migration { $table->integer('quantity')->default(1); $table->tinyInteger('invoice_type')->default(1); $table->integer('price')->default(0); - $table->tinyInteger('product_type'); + $table->string('product_type', 50)->nullable(); $table->timestamps(); }); diff --git a/database/migrations/2025_05_20_161102_create_order_addons_table.php b/database/migrations/2025_05_20_161102_create_order_addons_table.php index f0b6495..3d298b2 100644 --- a/database/migrations/2025_05_20_161102_create_order_addons_table.php +++ b/database/migrations/2025_05_20_161102_create_order_addons_table.php @@ -12,11 +12,15 @@ return new class extends Migration { { Schema::create('order__order_addons', function (Blueprint $table) { $table->id(); - $table->unsignedBigInteger('order_item_id')->index(); // FK đến order_items - $table->unsignedBigInteger('addon_id'); + $table->unsignedBigInteger('order_item_id')->index(); + $table->unsignedBigInteger('addon_id')->index(); $table->unsignedInteger('qty')->default(1); - $table->unsignedInteger('price'); // đơn giá tại thời điểm mua + $table->unsignedInteger('price'); $table->timestamps(); + + // Foreign Keys + $table->foreign('order_item_id')->references('id')->on('order__order_item')->onDelete('cascade'); + $table->foreign('addon_id')->references('id')->on('product_addon')->onDelete('cascade'); }); } @@ -25,6 +29,6 @@ return new class extends Migration { */ public function down(): void { - Schema::dropIfExists('order_addons'); + Schema::dropIfExists('order__order_addons'); } }; diff --git a/database/migrations/2026_02_08_210451_add_product_support_to_carts_and_order_items_tables.php b/database/migrations/2026_02_08_210451_add_product_support_to_carts_and_order_items_tables.php index e0545ca..e35ddfa 100644 --- a/database/migrations/2026_02_08_210451_add_product_support_to_carts_and_order_items_tables.php +++ b/database/migrations/2026_02_08_210451_add_product_support_to_carts_and_order_items_tables.php @@ -37,7 +37,7 @@ return new class extends Migration // Make product_type nullable for new Product system if (Schema::hasColumn('order__order_item', 'product_type')) { - $table->integer('product_type')->nullable()->change(); + $table->string('product_type', 50)->nullable()->change(); } }); } @@ -49,12 +49,12 @@ return new class extends Migration { Schema::table('carts', function (Blueprint $table) { $table->dropColumn(['billing_cycle', 'product_pricing_id']); - $table->dropIndex(['product_pricing_id']); + // $table->dropIndex(['product_pricing_id']); }); Schema::table('order__order_item', function (Blueprint $table) { $table->dropColumn(['billing_cycle', 'product_pricing_id']); - $table->dropIndex(['product_pricing_id']); + // $table->dropIndex(['product_pricing_id']); }); } }; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 7d30d5f..a485b40 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -15,8 +15,6 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - User::factory(1)->create(); - $this->call(DomainTypeSeeder::class); $this->call(GroupSeeder::class); $this->call(Payment::class); diff --git a/resources/views/administrator/pages/product/categories/datatable/sub_group.blade.php b/resources/views/administrator/pages/product/categories/datatable/sub_group.blade.php index 7c3658c..4b1e01b 100644 --- a/resources/views/administrator/pages/product/categories/datatable/sub_group.blade.php +++ b/resources/views/administrator/pages/product/categories/datatable/sub_group.blade.php @@ -21,8 +21,8 @@ {{ $child->name }} {{ $child->slug }} - - {{ $child->is_active ? Status::ACTIVE->label() : Status::DEACTIVE->label() }} + + {{ $child->is_active ? Status::ACTIVE->label() : Status::INACTIVE->label() }} diff --git a/resources/views/administrator/pages/product/create.blade.php b/resources/views/administrator/pages/product/create.blade.php index 582f7fd..2c2041c 100644 --- a/resources/views/administrator/pages/product/create.blade.php +++ b/resources/views/administrator/pages/product/create.blade.php @@ -213,10 +213,7 @@ diff --git a/resources/views/administrator/pages/product/edit.blade.php b/resources/views/administrator/pages/product/edit.blade.php index f8bf352..59686f4 100644 --- a/resources/views/administrator/pages/product/edit.blade.php +++ b/resources/views/administrator/pages/product/edit.blade.php @@ -7,357 +7,358 @@ @section('breadcrumb') @php - $breadcrumbItems = [ - ['label' => 'Product'], - ['label' => 'Product / Services', 'url' => route('products.index')], - ['label' => 'Edit Product', 'active' => true], - ]; +$breadcrumbItems = [ + ['label' => 'Product'], + ['label' => 'Product / Services', 'url' => route('products.index')], + ['label' => 'Edit Product', 'active' => true], +]; @endphp @endsection @section('content') -
- @csrf - @method('PUT') -
- {{-- Left Column: Basic Info & Pricing --}} -
-
-
-
Edit Product
-
-
- - {{-- Basic Information --}} -
-
Basic Information
- -
-
-
- - -
-
-
- -
- - -
- -
- - -
+ + @csrf + @method('PUT') +
+ {{-- Left Column: Basic Info & Pricing --}} +
+
+
+
Edit Product
+
- {{-- Pricing & Billing --}} -
-
Pricing & Billing
- {{-- Global Settings --}} -
-
-
- - + {{-- Basic Information --}} +
+
Basic Information
+ +
+
+
+ + +
-
-
- - -
+ +
+ +
-
-
- - -
+ +
+ +
- {{-- Billing Cycles Table --}} - - - - - - - - - - - - @foreach (\App\Enums\Product\ProductBillingCycle::cases() as $cycle) - @php - $pricing = $product->pricings - ->where('billing_cycle', $cycle->value) - ->first(); - @endphp + + {{-- Pricing & Billing --}} +
+
Pricing & Billing
+ {{-- Global Settings --}} +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ {{-- Billing Cycles Table --}} + +
#Billing CyclePriceSale Price
+ - - - - + + + + - @endforeach - -
-
- value}.enabled", $pricing ? 1 : 0) ? 'checked' : '' }}> - -
-
- {{ $cycle->label() }} - -
- $ - value}.price", $pricing->price ?? '') }}"> -
-
-
- $ - value}.price_sale", $pricing->price_sale ?? '') }}"> -
-
#Billing CyclePriceSale Price
-
+ + + @foreach (\App\Enums\Product\ProductBillingCycle::cases() as $cycle) + @php + $pricing = $product->pricings + ->where('billing_cycle', $cycle->value) + ->first(); + @endphp + + +
+ value}.enabled", $pricing ? 1 : 0) ? 'checked' : '' }}> + +
+ + + {{ $cycle->label() }} + + +
+ $ + value}.price", $pricing->price ?? '') }}"> +
+ + +
+ $ + value}.price_sale", $pricing->price_sale ?? '') }}"> +
+ + + @endforeach + + +
- {{-- Hardware Specs (VPS/Dedicated) --}} -
-
Hardware Specifications
-
-
-
- - + {{-- Hardware Specs (VPS/Dedicated) --}} +
+
Hardware Specifications
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
-
-
- - +
+
+
+ + +
-
-
-
- - +
+
+ + +
-
-
-
- - -
-
-
-
- - -
-
-
-
+
-
- {{-- Right Column: Classification & Settings --}} -
-
-
-
Classification
-
-
- {{-- Product Category --}} -
-
- - - - Thêm Group - + {{-- Right Column: Classification & Settings --}} +
+
+
+
Classification
+
+
+ {{-- Product Category --}} +
+ +
- -
- {{-- Operating System (for VPS) --}} -
- - @php - $selectedOsIds = old('os_ids', $product->osVersions->pluck('id')->toArray()); - @endphp - - Select multiple OS versions supported by this product -
- - {{-- Control Panels (for Hosting) --}} -