feat: implement persistent cart with database storage and proper exception handling

Implemented a complete shopping cart system with database persistence,
replacing the previous stub implementation. The cart now properly stores
items across sessions and follows the project's architecture patterns.

Database Changes:
- Add cart_items table with vendor_id, session_id, product_id, quantity, price_at_add
- Create unique constraint to prevent duplicate items per session
- Add indexes for session lookups and old cart cleanup
- Run migration a2064e1dfcd4 to create cart_items table

New Models & Schemas:
- models/database/cart.py: CartItem SQLAlchemy model with relationships
- models/schema/cart.py: Pydantic schemas for requests/responses
  * AddToCartRequest, UpdateCartItemRequest
  * CartResponse, CartItemResponse, CartOperationResponse, ClearCartResponse

Exception Handling:
- app/exceptions/cart.py: Cart-specific exceptions following project patterns
  * CartItemNotFoundException - item not found in cart
  * InsufficientInventoryForCartException - not enough inventory for cart operation
  * InvalidCartQuantityException - invalid quantity validation
  * CartValidationException - general cart validation
  * EmptyCartException - operations on empty cart
  * ProductNotAvailableForCartException - product unavailable
- Updated app/exceptions/__init__.py to export cart exceptions

Service Layer:
- Implement cart_service.get_cart() - fetch cart from database with totals
- Implement cart_service.add_to_cart() - create or update cart items with inventory checks
- Implement cart_service.update_cart_item() - update quantity with validation
- Implement cart_service.remove_from_cart() - delete cart item
- Implement cart_service.clear_cart() - remove all items for session
- Replace generic exceptions with cart-specific ones
- Fix InsufficientInventoryException usage (was using wrong parameters)

API Layer:
- Update app/api/v1/shop/cart.py to use Pydantic schemas
- Add response_model declarations to all endpoints
- Add return type hints for type safety
- Convert service dict responses to Pydantic models

Features:
- Cart items persist in database across server restarts
- Inventory validation before adding/updating items
- Price captured at time of adding to cart
- Duplicate items update quantity instead of creating new entries
- Full CRUD operations with proper error handling
- Type-safe API with auto-generated OpenAPI documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-23 20:17:16 +01:00
parent cf2aa2c5af
commit c100d839f1
9 changed files with 688 additions and 97 deletions

View File

@@ -152,6 +152,17 @@ try:
except ImportError as e:
print(f" ✗ Customer models failed: {e}")
# ----------------------------------------------------------------------------
# CART MODELS
# ----------------------------------------------------------------------------
try:
from models.database.cart import CartItem
print(" ✓ Cart models imported (1 model)")
print(" - CartItem")
except ImportError as e:
print(f" ✗ Cart models failed: {e}")
# ----------------------------------------------------------------------------
# ORDER MODELS
# ----------------------------------------------------------------------------

View File

@@ -0,0 +1,54 @@
"""add cart_items table
Revision ID: a2064e1dfcd4
Revises: f68d8da5315a
Create Date: 2025-11-23 19:52:40.509538
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'a2064e1dfcd4'
down_revision: Union[str, None] = 'f68d8da5315a'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Create cart_items table
op.create_table(
'cart_items',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('vendor_id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('session_id', sa.String(length=255), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=False),
sa.Column('price_at_add', sa.Float(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
sa.ForeignKeyConstraint(['vendor_id'], ['vendors.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('vendor_id', 'session_id', 'product_id', name='uq_cart_item')
)
# Create indexes
op.create_index('idx_cart_session', 'cart_items', ['vendor_id', 'session_id'], unique=False)
op.create_index('idx_cart_created', 'cart_items', ['created_at'], unique=False)
op.create_index(op.f('ix_cart_items_id'), 'cart_items', ['id'], unique=False)
op.create_index(op.f('ix_cart_items_session_id'), 'cart_items', ['session_id'], unique=False)
def downgrade() -> None:
# Drop indexes
op.drop_index(op.f('ix_cart_items_session_id'), table_name='cart_items')
op.drop_index(op.f('ix_cart_items_id'), table_name='cart_items')
op.drop_index('idx_cart_created', table_name='cart_items')
op.drop_index('idx_cart_session', table_name='cart_items')
# Drop table
op.drop_table('cart_items')