diff --git a/backend/app/routes/api.py b/backend/app/routes/api.py index feb26ba..1f989b8 100644 --- a/backend/app/routes/api.py +++ b/backend/app/routes/api.py @@ -1,10 +1,13 @@ import time +from decimal import Decimal +from pydantic import ValidationError from flask import Blueprint, request, jsonify from flask_jwt_extended import jwt_required, get_jwt_identity, create_access_token, create_refresh_token from app import db from app.models import User, Product, OrderItem, Order from app.celery import celery +from app.schemas import ProductCreateRequest, ProductResponse api_bp = Blueprint("api", __name__) @@ -104,21 +107,28 @@ def create_product(): if not user or not user.is_admin: return jsonify({"error": "Admin access required"}), 403 - data = request.get_json() - - product = Product( - name=data["name"], - description=data.get("description"), - price=data["price"], - stock=data.get("stock", 0), - image_url=data.get("image_url"), - category=data.get("category") - ) - - db.session.add(product) - db.session.commit() - - return jsonify(product.to_dict()), 201 + try: + # Validate request data using Pydantic schema + product_data = ProductCreateRequest(**request.get_json()) + + product = Product( + name=product_data.name, + description=product_data.description, + price=product_data.price, + stock=product_data.stock, + image_url=product_data.image_url, + category=product_data.category + ) + + db.session.add(product) + db.session.commit() + + # Use Pydantic schema for response + response = ProductResponse.model_validate(product) + return jsonify(response.model_dump()), 201 + + except ValidationError as e: + return jsonify({"error": "Validation error", "details": e.errors()}), 400 @api_bp.route("/products/", methods=["PUT"]) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..58f1ce0 --- /dev/null +++ b/backend/app/schemas/__init__.py @@ -0,0 +1,4 @@ +"""Pydantic schemas for request/response validation""" +from app.schemas.product import ProductCreateRequest, ProductResponse + +__all__ = ["ProductCreateRequest", "ProductResponse"] \ No newline at end of file diff --git a/backend/app/schemas/product.py b/backend/app/schemas/product.py new file mode 100644 index 0000000..304659e --- /dev/null +++ b/backend/app/schemas/product.py @@ -0,0 +1,64 @@ +"""Pydantic schemas for Product model""" +from pydantic import BaseModel, Field, field_validator +from decimal import Decimal +from datetime import datetime +from typing import Optional + + +class ProductCreateRequest(BaseModel): + """Schema for creating a new product""" + name: str = Field(..., min_length=1, max_length=200, description="Product name") + description: Optional[str] = Field(None, description="Product description") + price: Decimal = Field(..., gt=0, description="Product price (must be greater than 0)") + stock: int = Field(default=0, ge=0, description="Product stock quantity") + image_url: Optional[str] = Field(None, max_length=500, description="Product image URL") + category: Optional[str] = Field(None, description="Product category") + + class Config: + json_schema_extra = { + "example": { + "name": "Handcrafted Wooden Bowl", + "description": "A beautiful handcrafted bowl made from oak", + "price": 45.99, + "stock": 10, + "image_url": "https://example.com/bowl.jpg", + "category": "Woodwork" + } + } + + @field_validator("price") + @classmethod + def validate_price(cls, v: Decimal) -> Decimal: + """Validate that price has at most 2 decimal places""" + if v.as_tuple().exponent < -2: + raise ValueError("Price must have at most 2 decimal places") + return v + + +class ProductResponse(BaseModel): + """Schema for product response""" + id: int + name: str + description: Optional[str] = None + price: float + stock: int + image_url: Optional[str] = None + is_active: bool + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + json_schema_extra = { + "example": { + "id": 1, + "name": "Handcrafted Wooden Bowl", + "description": "A beautiful handcrafted bowl made from oak", + "price": 45.99, + "stock": 10, + "image_url": "https://example.com/bowl.jpg", + "is_active": True, + "created_at": "2024-01-15T10:30:00", + "updated_at": "2024-01-15T10:30:00" + } + } \ No newline at end of file diff --git a/backend/requirements/base.txt b/backend/requirements/base.txt index fa59b36..c60420f 100644 --- a/backend/requirements/base.txt +++ b/backend/requirements/base.txt @@ -8,3 +8,4 @@ python-dotenv==1.0.0 Werkzeug==3.0.1 SQLAlchemy==2.0.23 celery[redis]==5.3.6 +pydantic==2.5.3