188 lines
5.7 KiB
Python
188 lines
5.7 KiB
Python
"""
|
|
@router OrderRouter (Backend/app/api/v1/routers/OrderRouter.py)
|
|
@purpose Database-driven order history and detail tracking for storefront customers, protected by JWT authentication.
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from pydantic import BaseModel
|
|
from typing import List, Optional
|
|
from sqlalchemy.orm import Session
|
|
from datetime import datetime
|
|
import ulid
|
|
|
|
from app.core.database.db_session import get_db
|
|
from app.api.v1.routers.CustomerProfileRouter import get_current_customer
|
|
from app.models.EcomCustomerModel import EcomCustomer
|
|
from app.models.OrderModel import Order, OrderItem, OrderStatusHistory
|
|
from app.services.InventoryService import record_ledger_entry
|
|
|
|
router = APIRouter(prefix="/api/v1/orders", tags=["Order Management"])
|
|
|
|
# --- Schemas ---
|
|
|
|
class OrderItemSchema(BaseModel):
|
|
product_name: str
|
|
sku: str
|
|
quantity: int
|
|
unit_price: float
|
|
total_price: float
|
|
|
|
class OrderDetailResponse(BaseModel):
|
|
order_id: str
|
|
order_no: str
|
|
total_amount: float
|
|
discount_amount: float
|
|
tax_amount: float
|
|
shipping_cost: float
|
|
final_amount: float
|
|
status: str
|
|
payment_status: str
|
|
fulfillment_status: str
|
|
shipping_address_json: Optional[str] = None
|
|
billing_address_json: Optional[str] = None
|
|
tracking_number: Optional[str] = None
|
|
courier_name: Optional[str] = None
|
|
created_at: datetime
|
|
items: List[OrderItemSchema]
|
|
|
|
class OrderListResponse(BaseModel):
|
|
order_id: str
|
|
order_no: str
|
|
final_amount: float
|
|
status: str
|
|
payment_status: str
|
|
created_at: datetime
|
|
|
|
# --- Endpoints ---
|
|
|
|
@router.get("", response_model=List[OrderListResponse])
|
|
def list_customer_orders(
|
|
customer: EcomCustomer = Depends(get_current_customer),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
List order history for the logged-in customer.
|
|
"""
|
|
orders = (
|
|
db.query(Order)
|
|
.filter(Order.customer_id == customer.customer_id)
|
|
.order_by(Order.created_at.desc())
|
|
.all()
|
|
)
|
|
return [
|
|
OrderListResponse(
|
|
order_id=o.order_id,
|
|
order_no=o.order_no,
|
|
final_amount=float(o.final_amount),
|
|
status=o.status,
|
|
payment_status=o.payment_status,
|
|
created_at=o.created_at
|
|
) for o in orders
|
|
]
|
|
|
|
@router.get("/{order_id}", response_model=OrderDetailResponse)
|
|
def get_order_details(
|
|
order_id: str,
|
|
customer: EcomCustomer = Depends(get_current_customer),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Retrieve full details for a specific order. Enforces customer ownership.
|
|
"""
|
|
order = (
|
|
db.query(Order)
|
|
.filter(Order.order_id == order_id, Order.customer_id == customer.customer_id)
|
|
.first()
|
|
)
|
|
if not order:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Order not found"
|
|
)
|
|
|
|
return OrderDetailResponse(
|
|
order_id=order.order_id,
|
|
order_no=order.order_no,
|
|
total_amount=float(order.total_amount),
|
|
discount_amount=float(order.discount_amount),
|
|
tax_amount=float(order.tax_amount),
|
|
shipping_cost=float(order.shipping_cost),
|
|
final_amount=float(order.final_amount),
|
|
status=order.status,
|
|
payment_status=order.payment_status,
|
|
fulfillment_status=order.fulfillment_status,
|
|
shipping_address_json=order.shipping_address_json,
|
|
billing_address_json=order.billing_address_json,
|
|
tracking_number=order.tracking_number,
|
|
courier_name=order.courier_name,
|
|
created_at=order.created_at,
|
|
items=[
|
|
OrderItemSchema(
|
|
product_name=item.product_name,
|
|
sku=item.sku,
|
|
quantity=item.quantity,
|
|
unit_price=float(item.unit_price),
|
|
total_price=float(item.total_price)
|
|
) for item in order.items
|
|
]
|
|
)
|
|
|
|
@router.post("/{order_id}/cancel")
|
|
def cancel_order(
|
|
order_id: str,
|
|
customer: EcomCustomer = Depends(get_current_customer),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Cancel an order if it is in an editable state (ORDER_CREATED or PAYMENT_PENDING).
|
|
Releases ONLINE_RESERVE inventory reservations.
|
|
"""
|
|
order = (
|
|
db.query(Order)
|
|
.filter(Order.order_id == order_id, Order.customer_id == customer.customer_id)
|
|
.first()
|
|
)
|
|
if not order:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Order not found"
|
|
)
|
|
|
|
if order.status not in ["ORDER_CREATED", "PAYMENT_PENDING"]:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Order cannot be cancelled. Current status is {order.status}."
|
|
)
|
|
|
|
previous_status = order.status
|
|
|
|
for item in order.items:
|
|
record_ledger_entry(
|
|
variant_id=item.variant_id,
|
|
event_type="ONLINE_RESERVE_RELEASE",
|
|
qty=item.quantity,
|
|
reference_id=order.order_id,
|
|
db=db,
|
|
notes=f"Released reservation for customer-cancelled order {order.order_no}",
|
|
commit=False,
|
|
)
|
|
|
|
history = OrderStatusHistory(
|
|
history_id=str(ulid.ULID()),
|
|
order_id=order.order_id,
|
|
previous_status=previous_status,
|
|
new_status="CANCELLED",
|
|
changed_by=customer.email,
|
|
reason="Cancelled by customer from profile portal"
|
|
)
|
|
db.add(history)
|
|
|
|
order.status = "CANCELLED"
|
|
order.fulfillment_status = "CANCELLED"
|
|
if order.payment_status == "PAYMENT_CAPTURED":
|
|
order.payment_status = "REFUNDED"
|
|
else:
|
|
order.payment_status = "PAYMENT_FAILED"
|
|
|
|
db.commit()
|
|
|
|
return {"message": "Order cancelled successfully", "order_id": order_id}
|