aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--thallium-backend/src/dto/__init__.py5
-rw-r--r--thallium-backend/src/dto/orders.py66
-rw-r--r--thallium-backend/src/routes/__init__.py2
-rw-r--r--thallium-backend/src/routes/orders.py38
4 files changed, 111 insertions, 0 deletions
diff --git a/thallium-backend/src/dto/__init__.py b/thallium-backend/src/dto/__init__.py
index e38135e..17907e2 100644
--- a/thallium-backend/src/dto/__init__.py
+++ b/thallium-backend/src/dto/__init__.py
@@ -1,4 +1,5 @@
from .login import JWTClaim, PasswordReset, UserClaim, UserLogin, VoucherClaim, VoucherLogin
+from .orders import Order, OrderCreate, OrderItem, OrderRecipient
from .templates import Template, TemplateWithVariant, Variant
from .users import User, UserPermission
from .vouchers import Voucher
@@ -6,6 +7,10 @@ from .vouchers import Voucher
__all__ = (
"LoginData",
"JWTClaim",
+ "OrderCreate",
+ "Order",
+ "OrderItem",
+ "OrderRecipient",
"User",
"UserPermission",
"Voucher",
diff --git a/thallium-backend/src/dto/orders.py b/thallium-backend/src/dto/orders.py
new file mode 100644
index 0000000..a3a046e
--- /dev/null
+++ b/thallium-backend/src/dto/orders.py
@@ -0,0 +1,66 @@
+from decimal import Decimal
+
+from pydantic import BaseModel
+
+
+class OrderRecipient(BaseModel):
+ """Information about the recipient of the order."""
+
+ name: str
+ company: str | None = None
+ address1: str
+ address2: str
+ city: str
+ state_code: str | None = None
+ state_name: str | None = None
+ country_code: str
+ country_name: str
+ zip: str
+ phone: str
+ email: str
+ tax_number: str | None = None
+
+
+class OrderItem(BaseModel):
+ """Information about the items in the order."""
+
+ product_template_id: int
+ variant_id: int
+
+
+class OrderCreate(BaseModel):
+ """Data required to create an order."""
+
+ recipient: OrderRecipient
+ items: list[OrderItem]
+
+ def as_printful_payload(self) -> dict:
+ """Return this order in the format used by Printful's API."""
+ return {
+ "recipient": self.recipient.model_dump(),
+ "items": [item.model_dump() for item in self.items],
+ }
+
+
+class OrderCosts(BaseModel):
+ """All costs associated with an order."""
+
+ currency: str
+ subtotal: Decimal
+ discount: Decimal
+ shipping: Decimal
+ digitization: Decimal
+ additional_fee: Decimal
+ fulfillment_fee: Decimal
+ retail_delivery_fee: Decimal
+ tax: Decimal
+ vat: Decimal
+ total: Decimal
+
+
+class Order(OrderCreate):
+ """The order as returned by printful."""
+
+ id: int
+ status: str
+ costs: OrderCosts
diff --git a/thallium-backend/src/routes/__init__.py b/thallium-backend/src/routes/__init__.py
index 0671816..a1fd732 100644
--- a/thallium-backend/src/routes/__init__.py
+++ b/thallium-backend/src/routes/__init__.py
@@ -3,6 +3,7 @@ from fastapi import APIRouter
from src.routes.admin import router as admin_router
from src.routes.debug import router as debug_router
from src.routes.login import router as login_router
+from src.routes.orders import router as order_router
from src.routes.templates import router as template_router
from src.routes.vouchers import router as voucher_router
from src.settings import CONFIG
@@ -10,6 +11,7 @@ from src.settings import CONFIG
top_level_router = APIRouter()
top_level_router.include_router(admin_router)
top_level_router.include_router(login_router)
+top_level_router.include_router(order_router)
top_level_router.include_router(template_router)
top_level_router.include_router(voucher_router)
if CONFIG.debug:
diff --git a/thallium-backend/src/routes/orders.py b/thallium-backend/src/routes/orders.py
new file mode 100644
index 0000000..c5126a8
--- /dev/null
+++ b/thallium-backend/src/routes/orders.py
@@ -0,0 +1,38 @@
+import logging
+
+from fastapi import APIRouter, Depends, HTTPException, Request
+from sqlalchemy import select
+
+from src.auth import TokenAuth
+from src.dto import Order, OrderCreate, Voucher
+from src.orm import Voucher as DBVoucher
+from src.settings import DBSession, PrintfulClient
+
+router = APIRouter(prefix="/orders", tags=["Orders"], dependencies=[Depends(TokenAuth(allow_vouchers=True))])
+
+log = logging.getLogger(__name__)
+
+
+async def create_order(request: Request, db: DBSession, client: PrintfulClient, order: OrderCreate) -> Order | None:
+ """
+ Create the order in printful and deduct the order cost from the voucher.
+
+ If the voucher does not have enough funds, the order is cancelled.
+ """
+ resp = await client.post("/orders", json=order.as_printful_payload(), params={"confirm": False})
+ resp.raise_for_status()
+ submitted_order = Order.model_validate(resp.json()["result"])
+
+ voucher: Voucher = request.state.voucher
+ stmt = select(DBVoucher).where(DBVoucher.id == voucher.id).with_for_update()
+ db_voucher = await db.scalar(stmt)
+ if submitted_order.costs.total > db_voucher.balance:
+ await client.delete(f"/orders/{submitted_order.id}")
+ raise HTTPException(
+ status_code=400,
+ detail=f"Order totals {submitted_order.costs.total}, only {db_voucher.balance} remaining on voucher.",
+ )
+
+ db_voucher.balance = db_voucher.balance - submitted_order.costs.total
+ return submitted_order