Skip to content

Commit

Permalink
Merge pull request #70 from SuzuSys/develop2
Browse files Browse the repository at this point in the history
注文を発行したときにダイアログで注文番号を表示する
  • Loading branch information
exflikt authored Oct 5, 2024
2 parents 77c2b04 + 92c97b5 commit 11a6ba2
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 158 deletions.
136 changes: 43 additions & 93 deletions app/routers/orders.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,29 @@
from typing import Annotated
from uuid import UUID, uuid4
from uuid import UUID

from fastapi import APIRouter, Form, Header, HTTPException, Request, Response, status
from fastapi import APIRouter, Form, HTTPException, Request, Response, status
from fastapi.responses import HTMLResponse

from .. import templates
from ..store import PlacedItemTable, PlacementTable, Product, ProductTable
from ..store.product import ProductCompact
from ..store import PlacedItemTable, PlacementTable, ProductTable
from ..store.product import OrderSession

router = APIRouter()

# NOTE: Do NOT store this data in database (the data is transient and should be kept in memory)
# NOTE: Or should this be optionally stored in database?
order_sessions: dict[int, dict[UUID, Product]] = {}
order_sessions: dict[int, OrderSession] = {}
last_session_id = 0


def create_new_session() -> int:
global last_session_id
last_session_id += 1
new_session_id = last_session_id
order_sessions[new_session_id] = {}
order_sessions[new_session_id] = OrderSession()
return new_session_id


def compute_total_price(order_items: dict[UUID, Product]) -> str:
total_price = 0
for item in order_items.values():
total_price += item.price
return Product.to_price_str(total_price)


@router.post("/orders", response_class=Response)
async def create_new_order():
location = f"/orders/{create_new_session()}"
Expand All @@ -43,72 +36,54 @@ async def create_new_order():

@router.get("/orders/{session_id}", response_class=HTMLResponse)
async def get_order_session(request: Request, session_id: int):
if (order_items := order_sessions.get(session_id)) is None:
if (order_session := order_sessions.get(session_id)) is None:
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")

total_price = compute_total_price(order_items)
products = await ProductTable.select_all()
return HTMLResponse(
templates.orders(request, session_id, products, order_items, total_price)
)
return HTMLResponse(templates.orders(request, session_id, products, order_session))


@router.get("/orders/{session_id}/confirm", response_class=HTMLResponse)
async def get_order_session_to_confirm(request: Request, session_id: int):
if (order_items := order_sessions.get(session_id)) is None:
if (order_session := order_sessions.get(session_id)) is None:
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
if len(order_items) == 0:
total_price = Product.to_price_str(0)
placement_status = "エラー:商品が選択されていません"
if order_session.total_count == 0:
error_status = "エラー:商品が選択されていません"
else:
placement_status = ""
total_price = 0
products: dict[int, ProductCompact] = {}
for item in order_items.values():
total_price += item.price
if item.product_id in products:
products[item.product_id].count += 1
else:
products[item.product_id] = ProductCompact(item.name, item.price)
error_status = None
return HTMLResponse(
templates.components.order_confirm(
request,
session_id,
products,
len(order_items),
Product.to_price_str(total_price),
placement_status,
order_session,
error_status,
)
)


@router.post("/orders/{session_id}", response_class=HTMLResponse)
async def place_order(request: Request, session_id: int):
if (order_items := order_sessions.get(session_id)) is None:
if (order_session := order_sessions.get(session_id)) is None:
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")

if len(order_items) == 0:
total_price = Product.to_price_str(0)
placement_status = "エラー:商品が選択されていません"
order_frozen = False
if order_session.total_count == 0:
error_status = "エラー:商品が選択されていません"
placement_id = None
else:
error_status = None
order_sessions.pop(session_id)

total_price = compute_total_price(order_items)
product_ids = [item.product_id for item in order_items.values()]
product_ids = [item.product_id for item in order_session.products.values()]
placement_id = await PlacedItemTable.issue(product_ids)
# TODO: add a branch for out of stock error
await PlacementTable.insert(placement_id)
placement_status = f"注文番号: #{placement_id}"
order_frozen = True

return HTMLResponse(
templates.components.order_session(
templates.components.order_issued(
request,
session_id,
order_items,
total_price,
placement_status=placement_status,
order_frozen=order_frozen,
placement_id,
order_session,
error_status,
)
)

Expand All @@ -118,45 +93,33 @@ async def add_order_item(
request: Request,
session_id: int,
product_id: Annotated[int, Form()],
hx_request: Annotated[str | None, Header()] = None,
) -> Response:
if (product := await ProductTable.by_product_id(product_id)) is None:
raise HTTPException(status_code=404, detail=f"Product {product_id} not found")
if (order_items := order_sessions.get(session_id)) is None:
# NOTE: the branching below is a bit complicated so it might be changed in the future
if hx_request == "true":
# If requested by `hx-post`, respond with a new order session even when the `session_id` is not valid
new_session_id = create_new_session()
await add_order_item(request, new_session_id, product_id)
location = f"/orders/{new_session_id}"
return Response(
f"Session {session_id} not found; redirecting to a newly created order",
status_code=status.HTTP_200_OK,
headers={"location": location, "hx-redirect": location},
)
else:
# otherwise report back that the `session_id` is not valid
raise HTTPException(
status_code=404, detail=f"Session {session_id} not found"
)

order_items[uuid4()] = product
if (order_session := order_sessions.get(session_id)) is None:
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")

order_session.add(product)
return HTMLResponse(
templates.components.order_session(
request, session_id, order_items, compute_total_price(order_items)
request,
session_id,
order_session,
)
)


@router.delete("/orders/{session_id}/item/{index}", response_class=HTMLResponse)
async def delete_order_item(request: Request, session_id: int, index: UUID):
if (order_items := order_sessions.get(session_id)) is None:
if (order_session := order_sessions.get(session_id)) is None:
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")

order_items.pop(index)
order_session.delete(index)
return HTMLResponse(
templates.components.order_session(
request, session_id, order_items, compute_total_price(order_items)
request,
session_id,
order_session,
)
)

Expand All @@ -165,29 +128,16 @@ async def delete_order_item(request: Request, session_id: int, index: UUID):
async def clear_order_items(
request: Request,
session_id: int,
hx_request: Annotated[str | None, Header()] = None,
) -> Response:
if (order_items := order_sessions.get(session_id)) is None:
# NOTE: the branching below is a bit complicated so it might be changed in the future
if hx_request == "true":
# If requested by `hx-post`, respond with a new order session even when the `session_id` is not valid
location = f"/orders/{create_new_session()}"
return Response(
f"Session {session_id} not found; redirecting to a newly created order",
status_code=status.HTTP_200_OK,
headers={"location": location, "hx-redirect": location},
)
else:
# otherwise report back that the `session_id` is not valid
raise HTTPException(
status_code=404, detail=f"Session {session_id} not found"
)

order_items.clear()
if (order_session := order_sessions.get(session_id)) is None:
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")

order_session.clear()
return HTMLResponse(
templates.components.order_session(
request, session_id, {}, Product.to_price_str(0)
request,
session_id,
order_session,
)
)

Expand Down
47 changes: 42 additions & 5 deletions app/store/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from databases import Database
import sqlmodel

from uuid import UUID, uuid4


class Product(sqlmodel.SQLModel, table=True):
# NOTE: there are no Pydantic ways to set the generated table's name, as per https://github.com/fastapi/sqlmodel/issues/159
Expand All @@ -25,11 +27,46 @@ def to_price_str(price: int) -> str:
return f"¥{price:,}"


class ProductCompact:
def __init__(self, name: str, price: int):
self.name = name
self.price = Product.to_price_str(price)
self.count = 1
class OrderSession:
def __init__(self):
self.products: dict[UUID, Product] = {}
self.counted_products: dict[int, OrderSession.CountedProduct] = {}
self.total_count: int = 0
self.total_price: int = 0

def clear(self):
self.total_count = 0
self.total_price = 0
self.products = {}
self.counted_products = {}

def get_str_price(self) -> str:
return Product.to_price_str(self.total_price)

def add(self, p: Product):
self.total_count += 1
self.total_price += p.price
self.products[uuid4()] = p
if p.product_id in self.counted_products:
self.counted_products[p.product_id].count += 1
else:
self.counted_products[p.product_id] = self.CountedProduct(p.name, p.price)

def delete(self, id: UUID):
if id in self.products:
self.total_count -= 1
product = self.products.pop(id)
self.total_price -= product.price
if self.counted_products[product.product_id].count == 1:
self.counted_products.pop(product.product_id)
else:
self.counted_products[product.product_id].count -= 1

class CountedProduct:
def __init__(self, name: str, price: int):
self.name = name
self.price = Product.to_price_str(price)
self.count = 1


class Table:
Expand Down
30 changes: 15 additions & 15 deletions app/templates.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import os
from functools import wraps
from pathlib import Path
from typing import Any, Callable, Protocol
from uuid import UUID
from typing import Any, Callable, Protocol, Optional

import jinja2
from fastapi import Request
Expand All @@ -11,7 +10,7 @@

from .env import DEBUG
from .store import Product, placements_t
from .store.product import ProductCompact
from .store.product import OrderSession

TEMPLATES_DIR = Path("app/templates")

Expand Down Expand Up @@ -88,10 +87,7 @@ def products(products: list[Product]): ...
def orders(
session_id: int,
products: list[Product],
order_items: dict[UUID, Product],
total_price: str,
placement_status: str = "",
order_frozen: bool = False,
session: OrderSession,
): ...


Expand All @@ -117,10 +113,7 @@ def product_editor(product: Product | None): ...
@staticmethod
def order_session(
session_id: int,
order_items: dict[UUID, Product],
total_price: str,
placement_status: str = "",
order_frozen: bool = False,
session: OrderSession,
): ...

# @macro_template("components/incoming-placements.html")
Expand All @@ -131,8 +124,15 @@ def order_session(
@staticmethod
def order_confirm(
session_id: int,
products: dict[int, ProductCompact],
count: int,
total_price: str,
placement_status: str = "",
session: OrderSession,
error_status: Optional[str],
): ...

@macro_template("components/order-issued.html")
@staticmethod
def order_issued(
session_id: int,
placement_id: Optional[int],
session: OrderSession,
error_status: Optional[str],
): ...
Loading

0 comments on commit 11a6ba2

Please sign in to comment.