feat: Reimplement order and invoice management with new service and repository layers, introduce compression middleware, and update database schema.
This commit is contained in:
@@ -2,78 +2,83 @@
|
||||
|
||||
namespace App\Repositories\Api\Invoice;
|
||||
|
||||
use App\Enums\Invoice\InvoiceStatus;
|
||||
use App\Helpers\OrderHelper;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use App\Models\Order;
|
||||
use App\Repositories\Api\BaseApiRepository;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
class InvoiceRepository extends BaseApiRepository implements InvoiceRepositoryInterface
|
||||
{
|
||||
public function getModel()
|
||||
public function getModel(): string
|
||||
{
|
||||
return Invoice::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lấy danh sách sản phẩm đang hoạt động
|
||||
*/
|
||||
public function getInvoice(int $limit = 10, $customer = null)
|
||||
// -------------------------------------------------------------------------
|
||||
// getInvoice
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function getInvoice(int $limit = 10, int $customerId = 0): LengthAwarePaginator
|
||||
{
|
||||
return $this->model->where('customer_id', $customer)->paginate($limit);
|
||||
return $this->model
|
||||
->where('customer_id', $customerId)
|
||||
->orderByDesc('created_at')
|
||||
->paginate($limit);
|
||||
}
|
||||
|
||||
public function createFromOrder($order)
|
||||
// -------------------------------------------------------------------------
|
||||
// createFromOrder
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function createFromOrder(Order $order): Invoice
|
||||
{
|
||||
$invoice = $this->model->create([
|
||||
'customer_id' => $order->customer_id,
|
||||
'order_id' => $order->id,
|
||||
'order_num' => \App\Helpers\OrderHelper::generateOrderNumber('INV'),
|
||||
'date' => now(),
|
||||
'due_date' => now()->addDays(3),
|
||||
'total' => $order->amount,
|
||||
'sub_total' => $order->amount,
|
||||
'tax' => 0,
|
||||
'status' => 1, // Unpaid
|
||||
'invoice_number' => OrderHelper::generateOrderNumber('INV'),
|
||||
'customer_id' => $order->customer_id,
|
||||
'order_id' => $order->id,
|
||||
'invoice_type' => 1, // new service
|
||||
'sub_total' => $order->total,
|
||||
'discount_amount' => $order->discount_amount ?? 0,
|
||||
'tax' => 0,
|
||||
'total' => $order->total,
|
||||
'status' => InvoiceStatus::UNPAID->value,
|
||||
'due_date' => now()->addDays(3)->toDateString(),
|
||||
]);
|
||||
|
||||
$order->update(['invoice_id' => $invoice->id]);
|
||||
|
||||
$orderItems = $order->orderItems;
|
||||
if ($orderItems->isEmpty()) {
|
||||
$orderItems = $order->orderItems()->get();
|
||||
}
|
||||
|
||||
foreach ($orderItems as $item) {
|
||||
$invoiceItem = \App\Models\InvoiceItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
// 'customer_id' removed as not in migration
|
||||
'product_id' => $item->product_id,
|
||||
// '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,
|
||||
// Create one invoice_item per order_item line
|
||||
foreach ($order->orderItems as $item) {
|
||||
InvoiceItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'product_id' => $item->product_id,
|
||||
'type' => 'service',
|
||||
'description' => $item->product?->name ?? 'Dịch vụ',
|
||||
'months' => $this->cycleToMonths($item->billing_cycle),
|
||||
'quantity' => $item->quantity ?? 1,
|
||||
'unit_price' => $item->price,
|
||||
'total_price' => $item->price * ($item->quantity ?? 1),
|
||||
]);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private function cycleToMonths(?string $cycle): int
|
||||
{
|
||||
return match ($cycle) {
|
||||
'monthly' => 1,
|
||||
'quarterly' => 3,
|
||||
'semi_annually' => 6,
|
||||
'annually' => 12,
|
||||
'biennially' => 24,
|
||||
'triennially' => 36,
|
||||
default => 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,20 @@
|
||||
|
||||
namespace App\Repositories\Api\Invoice;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Order;
|
||||
use App\Repositories\BaseRepositoryInterface;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
interface InvoiceRepositoryInterface extends BaseRepositoryInterface
|
||||
{
|
||||
public function getInvoice(int $limit = 10, $customer = null);
|
||||
/**
|
||||
* Paginated invoice list for a customer.
|
||||
*/
|
||||
public function getInvoice(int $limit, int $customerId): LengthAwarePaginator;
|
||||
|
||||
public function createFromOrder($order);
|
||||
/**
|
||||
* Create an invoice (+ invoice_items) from a completed Order.
|
||||
*/
|
||||
public function createFromOrder(Order $order): Invoice;
|
||||
}
|
||||
|
||||
@@ -2,84 +2,88 @@
|
||||
|
||||
namespace App\Repositories\Api\Order;
|
||||
|
||||
use App\Enums\Invoice\InvoiceStatus;
|
||||
use App\Enums\Order\OrderStatus;
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
use App\Repositories\Api\BaseApiRepository;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
class OrderRepository extends BaseApiRepository implements OrderRepositoryInterface
|
||||
{
|
||||
public function getModel()
|
||||
public function getModel(): string
|
||||
{
|
||||
return Order::class;
|
||||
}
|
||||
|
||||
public function createItem($orderId, array $data)
|
||||
// -------------------------------------------------------------------------
|
||||
// createItem
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function createItem(int $orderId, array $data): OrderItem
|
||||
{
|
||||
$addons = $data['addons'] ?? [];
|
||||
unset($data['addons']); // Remove 'addons' from data before creating OrderItem
|
||||
unset($data['addons']);
|
||||
|
||||
$orderItem = \App\Models\OrderItem::create(array_merge(
|
||||
return OrderItem::create(array_merge(
|
||||
['order_id' => $orderId],
|
||||
$data
|
||||
$data,
|
||||
// Preserve addon selections as JSON; read again at provisioning time
|
||||
// to build service_addons records.
|
||||
['addons_meta' => !empty($addons) ? $addons : null],
|
||||
));
|
||||
|
||||
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)
|
||||
// -------------------------------------------------------------------------
|
||||
// processPayment
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function processPayment(Order $order, string $method): Order
|
||||
{
|
||||
// Update Order Status
|
||||
$order->update([
|
||||
'status' => \App\Enums\Order\OrderStatus::ACTIVE->value,
|
||||
'status' => OrderStatus::ACTIVE->value,
|
||||
'payment_method' => $method,
|
||||
]);
|
||||
|
||||
// Update Invoice Status
|
||||
if ($order->invoice) {
|
||||
$order->invoice->update([
|
||||
'status' => 2, // Paid
|
||||
'date_paid' => now()
|
||||
'status' => InvoiceStatus::PAID->value,
|
||||
'payment_method' => $method,
|
||||
'paid_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $order;
|
||||
return $order->fresh(['orderItems', 'invoice', 'customer']);
|
||||
}
|
||||
|
||||
public function hasPendingOrder($customerId)
|
||||
// -------------------------------------------------------------------------
|
||||
// hasPendingOrder
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function hasPendingOrder(int $customerId): bool
|
||||
{
|
||||
return $this->model->where('customer_id', $customerId)
|
||||
->where('status', \App\Enums\Order\OrderStatus::PENDING->value)
|
||||
return $this->model
|
||||
->where('customer_id', $customerId)
|
||||
->where('status', OrderStatus::PENDING->value)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function getOrders($customerId, array $filters)
|
||||
// -------------------------------------------------------------------------
|
||||
// getOrders
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function getOrders(int $customerId, array $filters): LengthAwarePaginator
|
||||
{
|
||||
$query = $this->model->where('customer_id', $customerId);
|
||||
|
||||
if (isset($filters['total_price'])) {
|
||||
$query->where('amount', $filters['total_price']);
|
||||
if (!empty($filters['total_price'])) {
|
||||
$query->where('total', $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);
|
||||
}
|
||||
if (!empty($filters['pending']) && filter_var($filters['pending'], FILTER_VALIDATE_BOOLEAN)) {
|
||||
$query->where('status', OrderStatus::PENDING->value);
|
||||
}
|
||||
|
||||
return $query->orderBy('created_at', 'desc')->paginate(10);
|
||||
return $query->orderByDesc('created_at')->paginate(10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,31 @@
|
||||
|
||||
namespace App\Repositories\Api\Order;
|
||||
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
use App\Repositories\BaseRepositoryInterface;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
interface OrderRepositoryInterface extends BaseRepositoryInterface
|
||||
{
|
||||
public function createItem($orderId, array $data);
|
||||
/**
|
||||
* Create an order item for the given order.
|
||||
* Addon selections are serialised into addons_meta JSON.
|
||||
*/
|
||||
public function createItem(int $orderId, array $data): OrderItem;
|
||||
|
||||
public function processPayment($order, $method);
|
||||
/**
|
||||
* Mark order as completed, update the related invoice as paid.
|
||||
*/
|
||||
public function processPayment(Order $order, string $method): Order;
|
||||
|
||||
public function hasPendingOrder($customerId);
|
||||
/**
|
||||
* Return true if the customer has any pending orders.
|
||||
*/
|
||||
public function hasPendingOrder(int $customerId): bool;
|
||||
|
||||
public function getOrders($customerId, array $filters);
|
||||
/**
|
||||
* Paginated order list for a customer, with optional filters.
|
||||
*/
|
||||
public function getOrders(int $customerId, array $filters): LengthAwarePaginator;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class InvoiceRepository extends BaseRepository implements InvoiceRepositoryInter
|
||||
$model = $model->where("customer_id", $data['customer_id']);
|
||||
}
|
||||
if (isset($data['payment_method']) && $data['payment_method'] !== 'full') {
|
||||
$model = $model->where("payment_id", $data['payment_method']);
|
||||
$model = $model->where("payment_method", $data['payment_method']);
|
||||
}
|
||||
if (isset($data['invoice_id'])) {
|
||||
$model = $model->where("invoice_id", $data['invoice_id']);
|
||||
|
||||
@@ -54,4 +54,12 @@ class OrderCachedRepository extends BaseCachedRepository implements OrderReposit
|
||||
$this->clearCache();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function findByOrderNum(string $orderNum)
|
||||
{
|
||||
$cacheKey = $this->getCacheKey(md5(json_encode(['findByOrderNum' => $orderNum])));
|
||||
return Cache::remember($cacheKey, $this->cacheTime, function () use ($orderNum) {
|
||||
return $this->orderRepository->findByOrderNum($orderNum);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,4 +133,17 @@ class OrderRepository extends BaseRepository implements OrderRepositoryInterface
|
||||
}
|
||||
return $this->model->where($key, $value)->update($data);
|
||||
}
|
||||
|
||||
public function findByOrderNum(string $orderNum)
|
||||
{
|
||||
return Order::with([
|
||||
'orderItems.product',
|
||||
'orderItems.os',
|
||||
'user',
|
||||
'customer',
|
||||
'payment',
|
||||
'discount',
|
||||
'invoice',
|
||||
])->where('order_num', $orderNum)->first();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ interface OrderRepositoryInterface extends BaseRepositoryInterface{
|
||||
public function filter(array $data = [],array $relationships = [],array $withCount = [],array $select = ['*'], bool $returnQuery = false);
|
||||
public function findWithRelationships(int $id = 0, array $relationships = [],array $withCount = [], array $withSum = []);
|
||||
public function updateBy(string $key = '',string|int $value = '',array $data =[]);
|
||||
public function findByOrderNum(string $orderNum);
|
||||
}
|
||||
Reference in New Issue
Block a user