feat: Implement comprehensive product management, including categories, pricing, and integration into the order system.

This commit is contained in:
pbao9
2026-02-12 14:29:29 +07:00
parent 3b1beef66e
commit f3a679d7f3
25 changed files with 577 additions and 692 deletions
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Str;
use App\Models\Order;
class OrderHelper
{
/**
* Generate a unique order number with prefix.
*
* @param string $prefix
* @return string
*/
public static function generateOrderNumber(string $prefix = 'ORD'): string
{
do {
// Generate a random string of 8 uppercase characters/numbers
$random = strtoupper(Str::random(8));
$orderNum = $prefix . '-' . $random;
if ($prefix === 'INV') {
$exists = \App\Models\Invoice::where('order_num', $orderNum)->exists();
} else {
$exists = Order::where('order_num', $orderNum)->exists();
}
} while ($exists);
return $orderNum;
}
}
@@ -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);
}
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers\Api\Setting;
use App\Http\Controllers\Controller;
use App\Models\Setting;
class SettingController extends Controller
{
public function getSetting()
{
$setting = Setting::where('key', 'setting')->first();
return response()->json([
'data' => $setting->value,
'message' => 'Get setting successfully',
'error' => false
]);
}
}
@@ -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
+6 -2
View File
@@ -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);
}
}
@@ -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'
];
}
}
@@ -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',
@@ -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;
+70 -5
View File
@@ -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);
}
}
@@ -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);
}
+12 -4
View File
@@ -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);