Skip to content

Commit

Permalink
Add an endpoint to submit an order to printful
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisLovering committed Sep 13, 2024
1 parent 8458a9f commit da8fdd3
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 0 deletions.
5 changes: 5 additions & 0 deletions thallium-backend/src/dto/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
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

__all__ = (
"LoginData",
"JWTClaim",
"OrderCreate",
"Order",
"OrderItem",
"OrderRecipient",
"User",
"UserPermission",
"Voucher",
Expand Down
66 changes: 66 additions & 0 deletions thallium-backend/src/dto/orders.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions thallium-backend/src/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
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

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:
Expand Down
38 changes: 38 additions & 0 deletions thallium-backend/src/routes/orders.py
Original file line number Diff line number Diff line change
@@ -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__)


@router.post("/")
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

0 comments on commit da8fdd3

Please sign in to comment.