feat: Enhance product support in Cart and OrderItem models

- Added new fields 'billing_cycle' and 'product_pricing_id' to Cart and OrderItem models.
- Updated product retrieval logic to support both new and legacy systems.
- Introduced relationships for Product and ProductPricings in Cart and OrderItem models.
- Created migration to add new fields to the carts and order_item tables.
- Updated frontend service index to handle new pricing structure and billing cycles.
- Improved UI components for better user experience in product selection and pricing display.
- Refactored action buttons in various views for consistency and improved styling.
This commit is contained in:
pbao9
2026-02-09 08:04:56 +07:00
parent 4d4bf020d4
commit 057353a2e1
15 changed files with 590 additions and 250 deletions
@@ -14,7 +14,10 @@ use App\Models\InvoiceItem;
use App\Models\ModuleHosting;
use App\Models\ModuleVmware;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Os;
use App\Models\Product;
use App\Models\ProductPricings;
use App\Models\VPS;
use Exception;
use Illuminate\Http\Request;
@@ -36,36 +39,85 @@ class CartController extends Controller
public function addCart(Request $request)
{
$userId = customer()?->id;
$months = $request->input('months');
$productId = $request->product_id;
$typeId = $request->type_id;
if (!$userId) {
return back()->with('error', 'Bạn cần đăng nhập để thêm sản phẩm vào giỏ hàng.');
}
// Validate input data
if (!$productId || !$typeId || !$months) {
return back()->with('error', 'Dữ liệu không hợp lệ.');
// Get request data
$productId = $request->product_id;
$typeId = $request->type_id; // Can be null for new Product system
$months = $request->input('months');
$billingCycle = $request->input('billing_cycle'); // New: billing cycle
$productPrice = $request->input('product_price'); // Price from form
// Validate basic input
if (!$productId) {
return back()->with('error', 'Sản phẩm không hợp lệ.');
}
// Use updateOrCreate instead of delete + create for better performance
try {
CartModel::updateOrCreate(
['customer_id' => $userId], // Only one cart item per user
[
'product_id' => $productId,
'type_product' => $typeId,
'months' => $months,
]
);
// Check if this is new Product system or legacy (Hosting/VPS)
$product = Product::find($productId);
return redirect()->route('cart.index')->with('success', 'Sản phẩm đã được thêm vào giỏ hàng!');
if ($product) {
// NEW FLOW: Using Product table
$pricingId = null;
$finalPrice = $productPrice;
// Find the pricing based on billing_cycle
if ($billingCycle) {
$pricing = ProductPricings::where('product_id', $productId)
->where('billing_cycle', $billingCycle)
->where('is_active', 1)
->first();
if ($pricing) {
$pricingId = $pricing->id;
$finalPrice = $pricing->price_sale ?: $pricing->price;
}
}
// Add to cart
CartModel::updateOrCreate(
['customer_id' => $userId],
[
'product_id' => $productId,
'type_product' => null, // New system doesn't use type_product
'months' => $months,
'billing_cycle' => $billingCycle,
'product_pricing_id' => $pricingId,
'price' => $finalPrice,
'quantity' => 1,
]
);
return redirect()->route('cart.index')->with('success', 'Sản phẩm đã được thêm vào giỏ hàng!');
} else {
// LEGACY FLOW: Using old Hosting/VPS system
if (!$typeId || !$months) {
return back()->with('error', 'Dữ liệu không hợp lệ.');
}
CartModel::updateOrCreate(
['customer_id' => $userId],
[
'product_id' => $productId,
'type_product' => $typeId,
'months' => $months,
'price' => $productPrice,
]
);
return redirect()->route('cart.index')->with('success', 'Sản phẩm đã được thêm vào giỏ hàng!');
}
} catch (\Exception $e) {
Log::error('Cart add error: ' . $e->getMessage(), [
'user_id' => $userId,
'product_id' => $productId,
'type_id' => $typeId
'type_id' => $typeId,
'billing_cycle' => $billingCycle ?? null,
'trace' => $e->getTraceAsString()
]);
return back()->with('error', 'Có lỗi xảy ra khi thêm sản phẩm vào giỏ hàng.');
}
@@ -11,6 +11,7 @@ use App\Models\Hosting;
use App\Models\Group;
use App\Models\Type;
use App\Models\VPS;
use App\Models\ProductCategories;
use App\Repositories\Addon\AddonRepositoryInterface;
use App\Repositories\Domain\DomainRepositoryInterface;
use App\Repositories\Group\GroupRepositoryInterface;
@@ -18,7 +19,6 @@ use App\Repositories\Hosting\HostingRepositoryInterface;
use App\Repositories\Type\TypeRepositoryInterface;
use App\Repositories\VPS\VPSRepositoryInterface;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class ShopController extends Controller
{
@@ -34,24 +34,22 @@ class ShopController extends Controller
}
/**
* Display the shop index page with optimized queries
* Display the shop index page
*/
public function index(Request $request)
{
$title = "Dịch vụ của chúng tôi";
// Cache types with groups for better performance
$type = Cache::remember('shop_types_with_groups', 3600, function () {
return Type::with(['group' => function ($query) {
$query->where('status', 1); // Only active groups
}])
// Get types with groups
$type = Type::with(['group' => function ($query) {
$query->where('status', 1); // Only active groups
}])
->whereNotIn('id', [1, 2])
->where('status', 1) // Only active types
->latest('id')
->get();
});
// Handle product filtering with optimized queries
// Handle product filtering
if ($request->type && $request->group) {
$product = $this->getProductsByTypeAndGroup($request->type, $request->group);
$group = $this->getGroupsByType($request->type);
@@ -84,76 +82,67 @@ class ShopController extends Controller
}
/**
* Display products by group slug with pagination and optimization
* Display products by category slug (ProductCategories)
*/
public function show(Request $request, string $slug)
{
// Cache group data for better performance
$page = (int) $request->get('page', 1);
$cacheKey = "group_products_{$slug}_page_{$page}";
$data = Cache::remember($cacheKey, 1800, function () use ($slug, $page) {
// Find group with optimized query
$group = Group::with(['type'])
->where('slug', $slug)
->where('status', 1) // Only active groups
->first();
// Find category by slug
$category = ProductCategories::with(['parent', 'children'])
->where('slug', $slug)
->where('is_active', 1)
->first();
if (!$group) {
return null;
}
// Get products with pagination based on group type
$products = $this->getPaginatedProducts($group, $page);
return [
'group' => $group,
'products' => $products,
];
});
if (!$data) {
abort(404, 'Nhóm sản phẩm không tồn tại');
if (!$category) {
abort(404, 'Danh mục sản phẩm không tồn tại');
}
// Get products with pagination and pricing
$products = $category->products()
->with([
'pricings' => function ($query) {
$query->where('is_active', 1)->orderBy('price');
},
'osVersions.osGroup',
'controlPanels'
])
->where('is_active', 1)
->orderBy('created_at', 'desc')
->paginate(12);
return view('frontend.service.index', [
'group' => $data['group'],
'products' => $data['products'],
'breadcrumbs' => $this->crums->add(__('Dashboard'))->add('Sản phẩm thuộc ' . $data['group']->name),
'category' => $category,
'products' => $products,
'breadcrumbs' => $this->crums->add(__('Dashboard'))->add('Sản phẩm thuộc ' . $category->name),
]);
}
/**
* Get products by type and group with optimization
* Get products by type and group
*/
private function getProductsByTypeAndGroup(int $typeId, int $groupId)
{
return Cache::remember("products_type_{$typeId}_group_{$groupId}", 1800, function () use ($typeId, $groupId) {
return match ($typeId) {
3 => Hosting::where('group_id', $groupId)
->where('status', 1)
->with(['group', 'type'])
->get(),
4 => VPS::where('group_id', $groupId)
->where('status', 1)
->with(['group', 'type', 'operatingSystems.osGroup'])
->get(),
default => collect()
};
});
return match ($typeId) {
3 => Hosting::where('group_id', $groupId)
->where('status', 1)
->with(['group', 'type'])
->get(),
4 => VPS::where('group_id', $groupId)
->where('status', 1)
->with(['group', 'type', 'operatingSystems.osGroup'])
->get(),
default => collect()
};
}
/**
* Get groups by type with optimization
* Get groups by type
*/
private function getGroupsByType(int $typeId)
{
return Cache::remember("groups_type_{$typeId}", 3600, function () use ($typeId) {
return Group::with('type')
->where('type_id', $typeId)
->where('status', 1)
->get();
});
return Group::with('type')
->where('type_id', $typeId)
->where('status', 1)
->get();
}
/**
@@ -171,34 +160,4 @@ class ShopController extends Controller
default => null
};
}
/**
* Get paginated products based on group type
*/
private function getPaginatedProducts(Group $group, int $page = 1, int $perPage = 12)
{
$query = null;
// Determine which model to query based on group type
switch ($group->type_id) {
case 3: // Hosting
$query = Hosting::where('group_id', $group->id)
->where('status', 1)
->with(['group', 'type'])
->orderBy('created_at', 'desc');
break;
case 4: // VPS
$query = VPS::where('group_id', $group->id)
->where('status', 1)
->with(['group', 'type'])
->orderBy('created_at', 'desc');
break;
default:
return collect()->paginate($perPage);
}
return $query->paginate($perPage, ['*'], 'page', $page);
}
}