Skip to content
This repository was archived by the owner on Jan 1, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/package-python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12.10'
python-version: '3.12.11'

- name: Install build dependencies
run: pip install --user build
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/run-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ jobs:
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ['3.12.10', '3.x']
python-version: ['3.12.11', '3.x']

steps:
- name: Checkout code
Expand Down
19 changes: 19 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: FastAPI",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": [
"app.main:app",
"--reload"
],
"jinja": true
}
]
}
21 changes: 16 additions & 5 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,27 @@
"**/*.egg-info": true
},
"[python]": {
"editor.rulers": [80],
"editor.rulers": [
80
],
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.codeActionsOnSave": {
"source.organizeImports": "always"
"source.organizeImports": "always",
"source.unusedImports": "always"
}
},
"isort.args": ["--profile", "black"],
"isort.args": [
"--profile",
"black"
],
"triggerTaskOnSave.tasks": {
"Run file tests": ["tests/**/test_*.py"],
"Run all tests": ["app/**/*.py", "!tests/**"]
// "Run file tests": [
// "tests/**/test_*.py"
// ],
// "Run all tests": [
// "app/**/*.py",
// "!tests/**"
// ]
},
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
Expand Down
File renamed without changes.
File renamed without changes.
Empty file added app/api/models/category.py
Empty file.
Empty file added app/api/models/contact.py
Empty file.
Empty file added app/api/models/customization.py
Empty file.
Empty file added app/api/models/language.py
Empty file.
Empty file added app/api/models/product.py
Empty file.
Empty file added app/api/models/tag.py
Empty file.
Empty file added app/api/models/translation.py
Empty file.
Empty file added app/api/schemas/__init__.py
Empty file.
19 changes: 19 additions & 0 deletions app/api/schemas/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Generic, TypeVar

from pydantic import BaseModel, Field

T = TypeVar("T")


class PaginationInfo(BaseModel):
current_page: int = Field(..., description="Current page number")
page_size: int = Field(..., description="Number of items per page")
total_items: int = Field(..., description="Total number of items")
total_pages: int = Field(..., description="Total number of pages")
has_next: bool = Field(..., description="Whether there is a next page")
has_previous: bool = Field(..., description="Whether there is a previous page")


class PaginatedResponse(BaseModel, Generic[T]):
data: list[T] = Field(..., description="List of items")
pagination: PaginationInfo = Field(..., description="Pagination information")
Empty file added app/api/schemas/v1/__init__.py
Empty file.
Empty file added app/api/schemas/v1/attribute.py
Empty file.
Empty file added app/api/schemas/v1/category.py
Empty file.
Empty file added app/api/schemas/v1/common.py
Empty file.
Empty file.
Empty file.
Empty file added app/api/schemas/v1/pricing.py
Empty file.
204 changes: 204 additions & 0 deletions app/api/schemas/v1/product.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
from datetime import datetime
from decimal import Decimal
from enum import Enum
from typing import List, Optional

from pydantic import BaseModel, ConfigDict, Field, model_validator

from ....api.schemas.base import PaginationInfo


class ProductType(str, Enum):
STANDARD = "standard"
CONFIGURABLE = "configurable"
VARIANT_BASED = "variant_based"


class CategoryResponse(BaseModel):
category_id: int
category_name: str
category_color: str
category_description: Optional[str] = None


class ProductListItem(BaseModel):
"""Product item for listing views (homepage, category pages)"""

model_config = ConfigDict(from_attributes=True)

# Core product data
product_id: int
product_name: str
product_description: str
product_type: ProductType
price: Optional[float] = Field(
None, description="Price for standard/variant products"
)
base_price: Optional[float] = Field(
None, description="Base price for configurable products"
)
image_url: Optional[str] = None
preparation_time_hours: int
min_order_hours: int
serving_info: Optional[str] = None
is_customizable: bool
created_at: datetime

# Category information
category: CategoryResponse

# Variant indicators
has_variants: bool = Field(..., description="Whether product has variants")
default_variant_id: Optional[int] = Field(
None, description="Default variant ID if applicable"
)
variant_count: int = Field(0, description="Number of variants")

# Related data counts
attribute_count: int = Field(0, description="Number of attributes")
tag_count: int = Field(0, description="Number of tags assigned")
image_count: int = Field(0, description="Number of additional images")


class ProductListResponse(BaseModel):
"""Response for product listing endpoints"""

products: list[ProductListItem]
pagination: PaginationInfo


# Query parameter schemas
class ProductSortBy(str, Enum):
CREATED_AT = "created_at"
PRICE = "price"
NAME = "name"


class SortOrder(str, Enum):
ASC = "ASC"
DESC = "DESC"


class ProductListFilters(BaseModel):
"""Query parameters for filtering product lists"""

category_id: Optional[int] = Field(None, description="Filter by category")
tag_ids: Optional[list[int]] = Field(None, description="Filter by tag IDs")
sort_by: ProductSortBy = Field(ProductSortBy.CREATED_AT, description="Sort field")
sort_order: SortOrder = Field(SortOrder.DESC, description="Sort order")


class ProductAttributeInput(BaseModel):
"""Schema for product attribute input"""

name: str = Field(
...,
min_length=1,
max_length=100,
description="Attribute name (e.g., 'allergen')",
)
value: str = Field(
...,
min_length=1,
max_length=200,
description="Attribute value (e.g., 'gluten')",
)
color: Optional[str] = Field(
None, pattern=r"^#[0-9A-Fa-f]{6}$", description="Hex color (e.g., '#FF6B6B')"
)


class ProductTranslationInput(BaseModel):
"""Schema for product translation input"""

language_iso: str = Field(
..., pattern=r"^[a-z]{2}$", description="Language ISO code"
)
name: str = Field(
..., min_length=3, max_length=200, description="Translated product name"
)
description: Optional[str] = Field(
None, max_length=2000, description="Translated description"
)


class CreateProductRequest(BaseModel):
"""Request schema for creating a new product"""

model_config = ConfigDict(str_strip_whitespace=True)

# Required fields
product_name: str = Field(
...,
min_length=3,
max_length=200,
description="Product name",
pattern=r"^[^<>\'\"]+$", # Prevent XSS characters
)
product_description: str = Field(
..., min_length=1, max_length=2000, description="Product description"
)
product_type: ProductType = Field(..., description="Type of product")
category_id: int = Field(..., ge=1, description="Category ID")

# Pricing (conditional based on product_type)
price: Optional[Decimal] = Field(
None, ge=0, le=999999.99, description="Price for standard/variant products"
)
base_price: Optional[Decimal] = Field(
None, ge=0, le=999999.99, description="Base price for configurable products"
)

# Optional fields
image_url: Optional[str] = Field(
None,
pattern=r"^https?://[^\s]+\.(jpg|jpeg|png|webp)(\?[^\s]*)?$",
description="Product image URL",
)
preparation_time_hours: int = Field(
48, ge=0, le=24 * 365, description="Preparation time in hours"
)
min_order_hours: int = Field(
48, ge=0, le=24 * 365, description="Minimum order advance time in hours"
)
serving_info: Optional[str] = Field(
None, max_length=200, description="Serving information (e.g., '4-6 persons')"
)
is_customizable: bool = Field(False, description="Whether product is customizable")

# Related data
tag_ids: Optional[List[int]] = Field(None, description="List of tag IDs to assign")
attributes: Optional[List[ProductAttributeInput]] = Field(
None, description="Product attributes"
)
translations: Optional[List[ProductTranslationInput]] = Field(
None, description="Product translations"
)

@model_validator(mode="after")
def validate_pricing(self) -> "CreateProductRequest":
"""Validate pricing based on product type"""
if self.product_type == ProductType.CONFIGURABLE:
if self.base_price is None:
raise ValueError("Configurable products require a base_price")
if self.price is not None:
# Clear price for configurable products
self.price = None
else:
if self.price is None:
raise ValueError("Standard and variant-based products require a price")
if self.base_price is not None:
# Clear base_price for non-configurable products
self.base_price = None
return self


class CreateProductResponse(BaseModel):
"""Response schema for product creation"""

model_config = ConfigDict(from_attributes=True)

product_id: int = Field(..., description="Created product ID")
product_name: str = Field(..., description="Product name")
message: str = Field(..., description="Success message")
created_at: datetime = Field(..., description="Timestamp of creation")
Empty file added app/api/schemas/v1/search.py
Empty file.
Empty file added app/api/schemas/v1/tag.py
Empty file.
Empty file.
Empty file added app/api/schemas/v1/variant.py
Empty file.
Empty file added app/api/schemas/v2/__init__.py
Empty file.
Empty file added app/api/services/__init__.py
Empty file.
Empty file added app/api/services/base.py
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Loading
Loading