Files
flipventory_v2/app/schemas/item.py
Martin Hohenberg 4e0d0d3962 feat: initial FastAPI stub with auth, user management, and inventory CRUD
Sets up the complete application skeleton for Flipventory v2, an inventory
management tool for small/medium resellers using chaotic stockpiling.

Architecture
------------
- app/config.py         pydantic-settings; reads .env; holds admin_email guard
- app/database.py       SQLAlchemy 2.x engine, SessionLocal, Base, get_db dep
- app/models/user.py    User (email, bcrypt hash, is_admin bool, is_active)
- app/models/item.py    StorageBin + Item with FOUND/LISTED/SOLD/ARCHIVED enum
- app/schemas/          Pydantic request/response shapes (separate from models)
- app/auth/security.py  bcrypt password hashing + HS256 JWT creation/validation
- app/routers/auth.py   POST /auth/register, POST /auth/login, GET /auth/me
- app/routers/users.py  Admin-only CRUD for user accounts
- app/routers/items.py  /bins and /items CRUD with role-based access control

Key design decisions
--------------------
- martin@hohenberg.jp is ALWAYS admin: enforced on every login, cannot be
  demoted, cannot be deactivated. Prevents accidental lockout.
- All other registered e-mails are customers (is_admin=False).
- Customers can manage their own bins and items; admins see everything.
- Item hard-delete is admin-only; customers archive instead (audit safety).
- SQLite default for zero-infra local dev; any SQLAlchemy URL works in prod.
- Tables are created on startup via create_tables(); swap for Alembic later.

Addresses v1 Gitea issues
--------------------------
- Issue #1 (storage bins): StorageBin model + /bins CRUD endpoints
- Issue #2 (item CRUD): Item model + /items CRUD endpoints
- Issue #3 (audit logging): created_by_id / updated_by_id on every item row

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 10:40:53 +02:00

113 lines
3.0 KiB
Python

"""
Pydantic schemas for Item and StorageBin resources.
Design choices
---------------
- All price fields are optional: items can be added quickly without price info.
- ``status`` uses the ItemStatus enum so the API documents the allowed values.
- The ``*Read`` schemas include nested owner / bin info so clients don't need
to make extra requests.
"""
from datetime import datetime
from pydantic import BaseModel, ConfigDict, HttpUrl
from app.models.item import ItemStatus
# ---------------------------------------------------------------------------
# StorageBin schemas
# ---------------------------------------------------------------------------
class StorageBinCreate(BaseModel):
"""Request body for POST /bins."""
name: str
description: str | None = None
class StorageBinRead(BaseModel):
"""Response schema for a storage bin."""
model_config = ConfigDict(from_attributes=True)
id: int
name: str
description: str | None
owner_id: int
created_at: datetime
class StorageBinUpdate(BaseModel):
"""Request body for PATCH /bins/{id}."""
name: str | None = None
description: str | None = None
# ---------------------------------------------------------------------------
# Item schemas
# ---------------------------------------------------------------------------
class ItemCreate(BaseModel):
"""
Request body for POST /items.
Only ``title`` is required so that items can be added quickly during a
"chaos-dump" session. Everything else can be filled in later.
"""
title: str
description: str | None = None
status: ItemStatus = ItemStatus.FOUND
purchase_price: float | None = None
listing_price: float | None = None
sale_price: float | None = None
listing_url: str | None = None
sku: str | None = None
storage_bin_id: int | None = None
class ItemRead(BaseModel):
"""Full response schema for an item."""
model_config = ConfigDict(from_attributes=True)
id: int
title: str
description: str | None
status: ItemStatus
purchase_price: float | None
listing_price: float | None
sale_price: float | None
listing_url: str | None
sku: str | None
storage_bin_id: int | None
created_by_id: int | None
updated_by_id: int | None
created_at: datetime
updated_at: datetime
class ItemUpdate(BaseModel):
"""
Request body for PATCH /items/{id}.
All fields optional so callers can update just one field at a time
(e.g., only the status when marking something sold).
"""
title: str | None = None
description: str | None = None
status: ItemStatus | None = None
purchase_price: float | None = None
listing_price: float | None = None
sale_price: float | None = None
listing_url: str | None = None
sku: str | None = None
storage_bin_id: int | None = None
class ItemListResponse(BaseModel):
"""Paginated list response for GET /items."""
total: int
page: int
page_size: int
items: list[ItemRead]