Add for gifts: schema, crud, route, tests
All checks were successful
Build and Push Docker Images / changes (push) Successful in 5s
Build and Push Docker Images / build-backend (push) Successful in 51s
Build and Push Docker Images / build-frontend (push) Has been skipped

This commit is contained in:
2025-03-16 14:23:28 +01:00
parent 25d2f16816
commit ed017a42ed
6 changed files with 1758 additions and 4 deletions

View File

@@ -0,0 +1,592 @@
from typing import List, Optional, Dict, Any
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, Path
from sqlalchemy.orm import Session
from app.api.dependencies.auth import get_current_active_user, get_current_user
from app.crud.gift import gift_item_crud, gift_category_crud, gift_purchase_crud
from app.crud.guest import guest_crud
from app.crud.event import event_crud
from app.models.gift import GiftStatus, GiftPriority
from app.models.user import User
from app.schemas.gifts import (
GiftItem, GiftItemCreate, GiftItemUpdate,
GiftCategory, GiftCategoryCreate, GiftCategoryUpdate,
GiftPurchase, GiftPurchaseCreate, GiftPurchaseUpdate
)
from app.core.database import get_db
router = APIRouter()
# ===== GIFT CATEGORIES ===== #
@router.post("/categories/", response_model=GiftCategory)
def create_gift_category(
*,
db: Session = Depends(get_db),
category_in: GiftCategoryCreate,
current_user: User = Depends(get_current_active_user)
) -> Any:
"""
Create new gift category.
"""
# Check if user has permission to manage this event
event = event_crud.get(db, category_in.event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Check permissions (basic implementation)
# In a complete system, use the EventManager permissions
if event.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_category_crud.create(db, obj_in=category_in)
@router.get("/categories/event/{event_id}", response_model=List[GiftCategory])
def read_gift_categories(
*,
db: Session = Depends(get_db),
event_id: UUID = Path(...),
skip: int = 0,
limit: int = 100,
include_hidden: bool = False,
include_gifts: bool = False,
current_user: Optional[User] = Depends(get_current_user)
) -> Any:
"""
Retrieve gift categories for an event.
"""
# Check if event exists and is accessible
event = event_crud.get(db, event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# For public access, ensure event is public
if not current_user and not event.is_public:
raise HTTPException(status_code=403, detail="Not enough permissions")
# Get categories
categories = gift_category_crud.get_multi_by_event(
db, event_id=event_id, skip=skip, limit=limit, include_hidden=include_hidden
)
# If include_gifts is true, fetch gift items for each category
if include_gifts:
for category in categories:
gifts = gift_item_crud.get_multi_by_event(
db, event_id=event_id, category_id=category.id, include_hidden=include_hidden
)
# Set the gifts attribute which is initially None in the model
setattr(category, "gifts", gifts)
return categories
@router.get("/categories/{category_id}", response_model=GiftCategory)
def read_gift_category(
*,
db: Session = Depends(get_db),
category_id: UUID = Path(...),
include_gifts: bool = False,
current_user: Optional[User] = Depends(get_current_user)
) -> Any:
"""
Get gift category by ID.
"""
category = gift_category_crud.get(db, id=category_id)
if not category:
raise HTTPException(status_code=404, detail="Gift category not found")
# Check if event is public or user is authorized
event = event_crud.get(db, category.event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
if not event.is_public and not current_user:
raise HTTPException(status_code=403, detail="Not enough permissions")
# If include_gifts is true, fetch gift items for the category
if include_gifts:
gifts = gift_item_crud.get_multi_by_event(
db, event_id=category.event_id, category_id=category.id,
include_hidden=current_user is not None # Only include hidden for logged-in users
)
# Set the gifts attribute which is initially None in the model
setattr(category, "gifts", gifts)
return category
@router.put("/categories/{category_id}", response_model=GiftCategory)
def update_gift_category(
*,
db: Session = Depends(get_db),
category_id: UUID = Path(...),
category_in: GiftCategoryUpdate,
current_user: User = Depends(get_current_active_user)
) -> Any:
"""
Update a gift category.
"""
category = gift_category_crud.get(db, id=category_id)
if not category:
raise HTTPException(status_code=404, detail="Gift category not found")
# Check if user has permission to manage this event
event = event_crud.get(db, category.event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Check permissions (basic implementation)
if event.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_category_crud.update(db, db_obj=category, obj_in=category_in)
@router.delete("/categories/{category_id}", response_model=GiftCategory)
def delete_gift_category(
*,
db: Session = Depends(get_db),
category_id: UUID = Path(...),
current_user: User = Depends(get_current_active_user)
) -> Any:
"""
Delete a gift category.
"""
category = gift_category_crud.get(db, id=category_id)
if not category:
raise HTTPException(status_code=404, detail="Gift category not found")
# Check if user has permission to manage this event
event = event_crud.get(db, category.event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Check permissions (basic implementation)
if event.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_category_crud.remove(db, id=category_id)
@router.put("/categories/{category_id}/reorder", response_model=GiftCategory)
def reorder_gifts_in_category(
*,
db: Session = Depends(get_db),
category_id: UUID = Path(...),
gift_orders: Dict[UUID, int],
current_user: User = Depends(get_current_active_user)
) -> Any:
"""
Reorder gifts within a category.
"""
category = gift_category_crud.get(db, id=category_id)
if not category:
raise HTTPException(status_code=404, detail="Gift category not found")
# Check if user has permission to manage this event
event = event_crud.get(db, category.event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Check permissions (basic implementation)
if event.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_category_crud.reorder_gifts(db, category_id=category_id, gift_orders=gift_orders)
# ===== GIFT ITEMS ===== #
@router.post("/items/", response_model=GiftItem)
def create_gift_item(
*,
db: Session = Depends(get_db),
item_in: GiftItemCreate,
current_user: User = Depends(get_current_active_user)
) -> Any:
"""
Create new gift item.
"""
# Check if user has permission to manage this event
event = event_crud.get(db, item_in.event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Check permissions (basic implementation)
if event.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
# If category is specified, check if it exists
if item_in.category_id:
category = gift_category_crud.get(db, id=item_in.category_id)
if not category:
raise HTTPException(status_code=404, detail="Gift category not found")
# Check category belongs to the same event
if category.event_id != item_in.event_id:
raise HTTPException(status_code=400, detail="Category does not belong to this event")
return gift_item_crud.create(db, obj_in=item_in)
@router.get("/items/event/{event_id}", response_model=List[GiftItem])
def read_gift_items(
*,
db: Session = Depends(get_db),
event_id: UUID = Path(...),
skip: int = 0,
limit: int = 100,
include_hidden: bool = False,
category_id: Optional[UUID] = None,
current_user: Optional[User] = Depends(get_current_user)
) -> Any:
"""
Retrieve gift items for an event.
"""
# Check if event exists and is accessible
event = event_crud.get(db, event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# For public access, ensure event is public
if not current_user and not event.is_public:
raise HTTPException(status_code=403, detail="Not enough permissions")
# If category is specified, check if it exists
if category_id:
category = gift_category_crud.get(db, id=category_id)
if not category:
raise HTTPException(status_code=404, detail="Gift category not found")
# Check category belongs to the requested event
if category.event_id != event_id:
raise HTTPException(status_code=400, detail="Category does not belong to this event")
return gift_item_crud.get_multi_by_event(
db, event_id=event_id, skip=skip, limit=limit,
include_hidden=include_hidden, category_id=category_id
)
@router.get("/items/{item_id}", response_model=GiftItem)
def read_gift_item(
*,
db: Session = Depends(get_db),
item_id: UUID = Path(...),
current_user: Optional[User] = Depends(get_current_user)
) -> Any:
"""
Get gift item by ID.
"""
gift = gift_item_crud.get(db, id=item_id)
if not gift:
raise HTTPException(status_code=404, detail="Gift item not found")
# Check if event is public or user is authorized
event = event_crud.get(db, gift.event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
if not event.is_public and not current_user:
raise HTTPException(status_code=403, detail="Not enough permissions")
# Check if gift is visible for public users
if not current_user and not gift.is_visible:
raise HTTPException(status_code=404, detail="Gift item not found")
return gift
@router.put("/items/{item_id}", response_model=GiftItem)
def update_gift_item(
*,
db: Session = Depends(get_db),
item_id: UUID = Path(...),
item_in: GiftItemUpdate,
current_user: User = Depends(get_current_active_user)
) -> Any:
"""
Update a gift item.
"""
gift = gift_item_crud.get(db, id=item_id)
if not gift:
raise HTTPException(status_code=404, detail="Gift item not found")
# Check if user has permission to manage this event
event = event_crud.get(db, gift.event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Check permissions (basic implementation)
if event.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
# If changing category, check if new category exists and belongs to same event
if item_in.category_id and item_in.category_id != gift.category_id:
category = gift_category_crud.get(db, id=item_in.category_id)
if not category:
raise HTTPException(status_code=404, detail="Gift category not found")
# Check category belongs to the same event
if category.event_id != gift.event_id:
raise HTTPException(status_code=400, detail="Category does not belong to this event")
return gift_item_crud.update(db, db_obj=gift, obj_in=item_in)
@router.delete("/items/{item_id}", response_model=GiftItem)
def delete_gift_item(
*,
db: Session = Depends(get_db),
item_id: UUID = Path(...),
current_user: User = Depends(get_current_active_user)
) -> Any:
"""
Delete a gift item.
"""
gift = gift_item_crud.get(db, id=item_id)
if not gift:
raise HTTPException(status_code=404, detail="Gift item not found")
# Check if user has permission to manage this event
event = event_crud.get(db, gift.event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Check permissions (basic implementation)
if event.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_item_crud.remove(db, id=item_id)
@router.put("/items/{item_id}/status", response_model=GiftItem)
def update_gift_item_status(
*,
db: Session = Depends(get_db),
item_id: UUID = Path(...),
status: GiftStatus,
current_user: User = Depends(get_current_active_user)
) -> Any:
"""
Update a gift item's status.
"""
gift = gift_item_crud.get(db, id=item_id)
if not gift:
raise HTTPException(status_code=404, detail="Gift item not found")
# Check if user has permission to manage this event
event = event_crud.get(db, gift.event_id)
if not event:
raise HTTPException(status_code=404, detail="Event not found")
# Check permissions (basic implementation)
if event.created_by != current_user.id:
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_item_crud.update_status(db, gift_id=item_id, new_status=status)
@router.post("/items/{item_id}/reserve", response_model=GiftItem)
def reserve_gift_item(
*,
db: Session = Depends(get_db),
item_id: UUID = Path(...),
guest_id: UUID,
notes: Optional[str] = None,
current_user: Optional[User] = Depends(get_current_user)
) -> Any:
"""
Reserve a gift item for a guest.
"""
gift = gift_item_crud.get(db, id=item_id)
print(f"Gift {gift}")
if not gift:
raise HTTPException(status_code=404, detail="Gift item not found")
# Check if gift is available
if gift.status != GiftStatus.AVAILABLE:
raise HTTPException(status_code=400, detail="Gift is not available for reservation")
# Check if guest exists
guest = guest_crud.get(db, id=guest_id)
if not guest:
raise HTTPException(status_code=404, detail="Guest not found")
# Check if guest belongs to the same event
if guest.event_id != gift.event_id:
raise HTTPException(status_code=400, detail="Guest does not belong to this event")
# For admin users, allow direct reservation
if current_user:
return gift_item_crud.reserve_gift(db, gift_id=item_id, guest_id=guest_id, notes=notes)
# For public users, additional validation
# Check if event is public
event = event_crud.get(db, gift.event_id)
if not event or not event.is_public:
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_item_crud.reserve_gift(db, gift_id=item_id, guest_id=guest_id, notes=notes)
@router.post("/items/{item_id}/cancel-reservation", response_model=GiftItem)
def cancel_gift_reservation(
*,
db: Session = Depends(get_db),
item_id: UUID = Path(...),
guest_id: UUID,
current_user: Optional[User] = Depends(get_current_user)
) -> Any:
"""
Cancel a gift reservation.
"""
gift = gift_item_crud.get(db, id=item_id)
if not gift:
raise HTTPException(status_code=404, detail="Gift item not found")
# Check if gift is reserved
if gift.status != GiftStatus.RESERVED:
raise HTTPException(status_code=400, detail="Gift is not currently reserved")
# Check if guest exists
guest = guest_crud.get(db, id=guest_id)
if not guest:
raise HTTPException(status_code=404, detail="Guest not found")
# For admin users, allow direct cancellation
if current_user:
return gift_item_crud.cancel_reservation(db, gift_id=item_id, guest_id=guest_id)
# ===== GIFT PURCHASES ===== #
@router.post("/purchases/", response_model=GiftPurchase)
def create_gift_purchase(
*,
db: Session = Depends(get_db),
purchase_in: GiftPurchaseCreate,
current_user: Optional[User] = Depends(get_current_user)
) -> Any:
"""
Create a gift purchase record.
"""
# Check if gift exists
gift = gift_item_crud.get(db, id=purchase_in.gift_id)
if not gift:
raise HTTPException(status_code=404, detail="Gift item not found")
# Check if guest exists
guest = guest_crud.get(db, id=purchase_in.guest_id)
if not guest:
raise HTTPException(status_code=404, detail="Guest not found")
# Check if guest belongs to the same event as the gift
if guest.event_id != gift.event_id:
raise HTTPException(status_code=400, detail="Guest does not belong to this event")
# For public users, additional validation
if not current_user:
# Check if event is public
event = event_crud.get(db, gift.event_id)
if not event or not event.is_public:
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_purchase_crud.create(db, obj_in=purchase_in)
@router.get("/purchases/{purchase_id}", response_model=GiftPurchase)
def read_gift_purchase(
*,
db: Session = Depends(get_db),
purchase_id: UUID = Path(...),
current_user: Optional[User] = Depends(get_current_user)
) -> Any:
"""
Get a gift purchase by ID.
"""
purchase = gift_purchase_crud.get(db, id=purchase_id)
if not purchase:
raise HTTPException(status_code=404, detail="Gift purchase not found")
# If user is authenticated, allow access
if current_user:
return purchase
# For public users, check if the event is public
gift = gift_item_crud.get(db, id=purchase.gift_id)
if not gift:
raise HTTPException(status_code=404, detail="Gift item not found")
event = event_crud.get(db, gift.event_id)
if not event or not event.is_public:
raise HTTPException(status_code=403, detail="Not enough permissions")
return purchase
@router.get("/purchases/gift/{gift_id}", response_model=List[GiftPurchase])
def read_gift_purchases_by_gift(
*,
db: Session = Depends(get_db),
gift_id: UUID = Path(...),
current_user: Optional[User] = Depends(get_current_user)
) -> Any:
"""
Get all purchases for a specific gift.
"""
# Check if gift exists
gift = gift_item_crud.get(db, id=gift_id)
if not gift:
raise HTTPException(status_code=404, detail="Gift item not found")
# If user is authenticated, allow access
if current_user:
return gift_purchase_crud.get_by_gift(db, gift_id=gift_id)
# For public users, check if the event is public
event = event_crud.get(db, gift.event_id)
if not event or not event.is_public:
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_purchase_crud.get_by_gift(db, gift_id=gift_id)
@router.get("/purchases/guest/{guest_id}", response_model=List[GiftPurchase])
def read_gift_purchases_by_guest(
*,
db: Session = Depends(get_db),
guest_id: UUID = Path(...),
current_user: Optional[User] = Depends(get_current_user)
) -> Any:
"""
Get all purchases made by a specific guest.
"""
# Check if guest exists
guest = guest_crud.get(db, id=guest_id)
if not guest:
raise HTTPException(status_code=404, detail="Guest not found")
# If user is authenticated, allow access
if current_user:
return gift_purchase_crud.get_by_guest(db, guest_id=guest_id)
# For public users, check if the event is public
event = event_crud.get(db, guest.event_id)
if not event or not event.is_public:
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_purchase_crud.get_by_guest(db, guest_id=guest_id)
# For public users, additional validation
# Check if event is public
event = event_crud.get(db, gift.event_id)
if not event or not event.is_public:
raise HTTPException(status_code=403, detail="Not enough permissions")
return gift_item_crud.cancel_reservation(db, gift_id=item_id, guest_id=guest_id)

244
backend/app/crud/gift.py Normal file
View File

@@ -0,0 +1,244 @@
from datetime import datetime, timezone
from typing import List, Optional, Dict, Any, Union
from uuid import UUID
from sqlalchemy import asc, desc
from sqlalchemy.orm import Session
from app.crud.base import CRUDBase
from app.models.gift import GiftItem, GiftCategory, GiftPurchase, GiftStatus
from app.models.guest import Guest
from app.schemas.gifts import (
GiftItemCreate, GiftItemUpdate,
GiftCategoryCreate, GiftCategoryUpdate,
GiftPurchaseCreate, GiftPurchaseUpdate
)
class CRUDGiftItem(CRUDBase[GiftItem, GiftItemCreate, GiftItemUpdate]):
def create(self, db: Session, *, obj_in: GiftItemCreate) -> GiftItem:
"""Create a new gift item with appropriate type conversions"""
obj_data = obj_in.model_dump(exclude_unset=True)
# Ensure UUID fields are properly converted if they're strings
for field in ["event_id", "added_by", "category_id"]:
if field in obj_data and obj_data[field] is not None:
if isinstance(obj_data[field], str):
obj_data[field] = UUID(obj_data[field])
# Set initial status change time
obj_data["last_status_change"] = datetime.now(timezone.utc)
db_obj = GiftItem(**obj_data)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def get_multi_by_event(
self, db: Session, event_id: UUID, skip: int = 0, limit: int = 100,
include_hidden: bool = False, category_id: Optional[UUID] = None
) -> List[GiftItem]:
"""Get gift items for a specific event with filtering options"""
query = db.query(self.model).filter(GiftItem.event_id == event_id)
if not include_hidden:
query = query.filter(GiftItem.is_visible == True)
if category_id:
query = query.filter(GiftItem.category_id == category_id)
# Order by display_order then name
query = query.order_by(asc(GiftItem.display_order), asc(GiftItem.name))
return query.offset(skip).limit(limit).all()
def update_status(
self, db: Session, *, gift_id: UUID, new_status: GiftStatus
) -> GiftItem:
"""Update the status of a gift item and record the change time"""
gift = self.get(db, gift_id)
if gift:
gift.status = new_status
gift.last_status_change = datetime.now(timezone.utc)
db.commit()
db.refresh(gift)
return gift
def update_quantity_received(
self, db: Session, *, gift_id: UUID, quantity: int
) -> GiftItem:
"""Update the quantity received and adjust status if needed"""
gift = self.get(db, gift_id)
if gift:
gift.quantity_received = quantity
# Automatically update status if fully received
if gift.quantity_received >= gift.quantity_requested:
gift.status = GiftStatus.RECEIVED
gift.last_status_change = datetime.now(timezone.utc)
db.commit()
db.refresh(gift)
return gift
def reserve_gift(
self, db: Session, *, gift_id: UUID, guest_id: UUID, notes: Optional[str] = None
) -> GiftItem:
"""Reserve a gift for a guest"""
gift = self.get(db, gift_id)
if gift and gift.status == GiftStatus.AVAILABLE:
# Add to the association table using the SQLAlchemy Core Table directly
from app.models.guest import guest_gifts
stmt = guest_gifts.insert().values(
gift_id=gift_id,
guest_id=guest_id,
reserved_at=datetime.now(timezone.utc),
notes=notes
)
db.execute(stmt)
# Update gift status
gift.status = GiftStatus.RESERVED
gift.last_status_change = datetime.now(timezone.utc)
db.commit()
db.refresh(gift)
return gift
def cancel_reservation(
self, db: Session, *, gift_id: UUID, guest_id: UUID
) -> GiftItem:
"""Cancel a gift reservation"""
gift = self.get(db, gift_id)
guest = db.query(Guest).get(guest_id)
if gift and gift.status == GiftStatus.RESERVED:
# Using the ORM relationship approach
if guest in gift.reserved_by:
gift.reserved_by.remove(guest)
# Update gift status
gift.status = GiftStatus.AVAILABLE
gift.last_status_change = datetime.now(timezone.utc)
db.commit()
db.refresh(gift)
return gift
class CRUDGiftCategory(CRUDBase[GiftCategory, GiftCategoryCreate, GiftCategoryUpdate]):
def create(self, db: Session, *, obj_in: GiftCategoryCreate) -> GiftCategory:
"""Create a new gift category with appropriate type conversions"""
obj_data = obj_in.model_dump(exclude_unset=True)
# Ensure UUID fields are properly converted if they're strings
for field in ["event_id", "created_by"]:
if field in obj_data and obj_data[field] is not None:
if isinstance(obj_data[field], str):
obj_data[field] = UUID(obj_data[field])
db_obj = GiftCategory(**obj_data)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def get_multi_by_event(
self, db: Session, event_id: UUID, skip: int = 0, limit: int = 100,
include_hidden: bool = False
) -> List[GiftCategory]:
"""Get categories for a specific event with filtering options"""
query = db.query(self.model).filter(GiftCategory.event_id == event_id)
if not include_hidden:
query = query.filter(GiftCategory.is_visible == True)
# Order by display_order then name
query = query.order_by(asc(GiftCategory.display_order), asc(GiftCategory.name))
return query.offset(skip).limit(limit).all()
def reorder_categories(
self, db: Session, *, event_id: UUID, category_orders: Dict[UUID, int]
) -> List[GiftCategory]:
"""Update display order of multiple categories at once"""
categories = self.get_multi_by_event(db, event_id, include_hidden=True)
for category in categories:
if category.id in category_orders:
category.display_order = category_orders[category.id]
db.commit()
return categories
def reorder_gifts(
self, db: Session, *, category_id: UUID, gift_orders: Dict[UUID, int]
) -> GiftCategory:
"""Update display order of gifts within a category"""
category = self.get(db, category_id)
if category:
for gift in category.gifts:
if gift.id in gift_orders:
gift.display_order = gift_orders[gift.id]
db.commit()
db.refresh(category)
return category
class CRUDGiftPurchase(CRUDBase[GiftPurchase, GiftPurchaseCreate, GiftPurchaseUpdate]):
def create(self, db: Session, *, obj_in: GiftPurchaseCreate) -> GiftPurchase:
"""Create a new gift purchase with appropriate type conversions"""
obj_data = obj_in.model_dump(exclude_unset=True)
# Ensure UUID fields are properly converted if they're strings
for field in ["gift_id", "guest_id"]:
if field in obj_data and obj_data[field] is not None:
if isinstance(obj_data[field], str):
obj_data[field] = UUID(obj_data[field])
# Set purchase time
obj_data["purchased_at"] = datetime.now(timezone.utc)
db_obj = GiftPurchase(**obj_data)
db.add(db_obj)
# Update the gift status
gift = db.query(GiftItem).filter(GiftItem.id == db_obj.gift_id).first()
if gift:
gift.status = GiftStatus.PURCHASED
gift.last_status_change = datetime.now(timezone.utc)
# Update quantity received if not specified
if "quantity_received" not in obj_data:
gift.quantity_received += db_obj.quantity
db.commit()
db.refresh(db_obj)
return db_obj
def get_by_gift(
self, db: Session, *, gift_id: UUID
) -> List[GiftPurchase]:
"""Get all purchases for a specific gift"""
return db.query(self.model).filter(
GiftPurchase.gift_id == gift_id
).order_by(desc(GiftPurchase.purchased_at)).all()
def get_by_guest(
self, db: Session, *, guest_id: UUID
) -> List[GiftPurchase]:
"""Get all purchases made by a specific guest"""
return db.query(self.model).filter(
GiftPurchase.guest_id == guest_id
).order_by(desc(GiftPurchase.purchased_at)).all()
# Create CRUD instances
gift_item_crud = CRUDGiftItem(GiftItem)
gift_category_crud = CRUDGiftCategory(GiftCategory)
gift_purchase_crud = CRUDGiftPurchase(GiftPurchase)

View File

@@ -0,0 +1,198 @@
from datetime import datetime
from typing import Optional, List, Any, Dict, Union
from uuid import UUID
from pydantic import BaseModel, Field, field_validator, model_validator
from app.models.gift import GiftStatus, GiftPriority
# Base Gift Schema
class GiftItemBase(BaseModel):
name: str
description: Optional[str] = None
price: Optional[float] = None
currency: Optional[str] = "USD"
quantity_requested: Optional[int] = 1
quantity_received: Optional[int] = 0
status: Optional[GiftStatus] = GiftStatus.AVAILABLE
priority: Optional[GiftPriority] = GiftPriority.MEDIUM
purchase_url: Optional[str] = None
store_name: Optional[str] = None
brand: Optional[str] = None
model: Optional[str] = None
image_url: Optional[str] = None
display_order: Optional[int] = None
is_visible: Optional[bool] = True
notes: Optional[str] = None
custom_fields: Optional[Dict[str, Any]] = None
# Schema for creating a gift
class GiftItemCreate(GiftItemBase):
event_id: UUID
added_by: UUID
category_id: Optional[UUID] = None
@field_validator('price')
@classmethod
def validate_price(cls, v: Optional[float]) -> Optional[float]:
if v is not None and v < 0:
raise ValueError("Price cannot be negative")
return v
@field_validator('quantity_requested')
@classmethod
def validate_quantity(cls, v: Optional[int]) -> Optional[int]:
if v is not None and v < 1:
raise ValueError("Quantity requested must be at least 1")
return v
# Schema for updating a gift
class GiftItemUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
price: Optional[float] = None
currency: Optional[str] = None
quantity_requested: Optional[int] = None
quantity_received: Optional[int] = None
status: Optional[GiftStatus] = None
priority: Optional[GiftPriority] = None
purchase_url: Optional[str] = None
store_name: Optional[str] = None
brand: Optional[str] = None
model: Optional[str] = None
image_url: Optional[str] = None
display_order: Optional[int] = None
is_visible: Optional[bool] = None
notes: Optional[str] = None
custom_fields: Optional[Dict[str, Any]] = None
category_id: Optional[UUID] = None
@field_validator('price')
@classmethod
def validate_price(cls, v: Optional[float]) -> Optional[float]:
if v is not None and v < 0:
raise ValueError("Price cannot be negative")
return v
@field_validator('quantity_requested')
@classmethod
def validate_quantity(cls, v: Optional[int]) -> Optional[int]:
if v is not None and v < 1:
raise ValueError("Quantity requested must be at least 1")
return v
# Schema for reading a gift
class GiftItemInDB(GiftItemBase):
id: UUID
event_id: UUID
added_by: UUID
category_id: Optional[UUID] = None
last_status_change: Optional[datetime] = None
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# Public gift item response
class GiftItem(GiftItemInDB):
remaining_quantity: int
is_fully_received: bool
formatted_price: str
reserved: bool = False
class Config:
from_attributes = True
# Gift Category Schemas
class GiftCategoryBase(BaseModel):
name: str
description: Optional[str] = None
icon: Optional[str] = None
color: Optional[str] = None
display_order: Optional[int] = 0
is_visible: Optional[bool] = True
custom_fields: Optional[Dict[str, Any]] = None
# Schema for creating a category
class GiftCategoryCreate(GiftCategoryBase):
event_id: UUID
created_by: UUID
# Schema for updating a category
class GiftCategoryUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
icon: Optional[str] = None
color: Optional[str] = None
display_order: Optional[int] = None
is_visible: Optional[bool] = None
custom_fields: Optional[Dict[str, Any]] = None
# Schema for reading a category
class GiftCategoryInDB(GiftCategoryBase):
id: UUID
event_id: UUID
created_by: UUID
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# Public category response with statistics
class GiftCategory(GiftCategoryInDB):
total_gifts: int
available_gifts: int
gifts: Optional[List[GiftItem]] = None
class Config:
from_attributes = True
# Gift Purchase Schemas
class GiftPurchaseBase(BaseModel):
gift_id: UUID
guest_id: UUID
quantity: int = 1
purchase_price: Optional[float] = None
purchase_currency: Optional[str] = None
notes: Optional[str] = None
# Schema for creating a purchase
class GiftPurchaseCreate(GiftPurchaseBase):
pass
# Schema for updating a purchase
class GiftPurchaseUpdate(BaseModel):
quantity: Optional[int] = None
purchase_price: Optional[float] = None
purchase_currency: Optional[str] = None
notes: Optional[str] = None
# Schema for reading a purchase
class GiftPurchaseInDB(GiftPurchaseBase):
id: UUID
purchased_at: datetime
class Config:
from_attributes = True
# Public gift purchase response
class GiftPurchase(GiftPurchaseInDB):
class Config:
from_attributes = True

View File

@@ -0,0 +1,271 @@
import uuid
import pytest
from app.api.routes.events.gifts import router as gifts_router
from app.models.gift import GiftStatus, GiftPriority
@pytest.fixture
def gift_item_data(mock_event, mock_user):
return {
"name": "Animal Plushie",
"description": "A cute animal plushie for Emma",
"price": 19.99,
"currency": "USD",
"quantity_requested": 2,
"priority": GiftPriority.HIGH.value,
"event_id": str(mock_event.id),
"added_by": str(mock_user.id)
}
@pytest.fixture
def gift_category_data(mock_event, mock_user):
return {
"name": "Toys",
"description": "Animal-themed toys for Emma",
"icon": "toy-icon",
"color": "#FF5733",
"event_id": str(mock_event.id),
"created_by": str(mock_user.id)
}
@pytest.fixture
def gift_purchase_data(gift_item_fixture, guest_fixture):
return {
"gift_id": str(gift_item_fixture.id),
"guest_id": str(guest_fixture.id),
"quantity": 1,
"purchase_price": 19.99,
"purchase_currency": "USD",
"notes": "Purchased as a birthday gift"
}
class TestGiftsRouter:
@pytest.fixture(autouse=True)
def setup_method(self, create_test_client, db_session, mock_user, gift_item_fixture, gift_category_fixture):
self.client = create_test_client(
router=gifts_router,
prefix="/gifts",
db_session=db_session,
user=mock_user
)
self.db_session = db_session
self.mock_user = mock_user
self.endpoint = "/gifts"
self.gift_item = gift_item_fixture
self.gift_category = gift_category_fixture
# Gift Item Tests
def test_create_gift_item_success(self, gift_item_data):
response = self.client.post(f"{self.endpoint}/items", json=gift_item_data)
assert response.status_code == 200
data = response.json()
assert data["name"] == gift_item_data["name"]
assert data["price"] == gift_item_data["price"]
assert data["status"] == GiftStatus.AVAILABLE.value
def test_read_gift_item_success(self):
response = self.client.get(f"{self.endpoint}/items/{self.gift_item.id}")
assert response.status_code == 200
data = response.json()
assert data["id"] == str(self.gift_item.id)
assert data["name"] == self.gift_item.name
assert "remaining_quantity" in data
assert "is_fully_received" in data
assert "formatted_price" in data
def test_read_gift_item_not_found(self):
random_uuid = str(uuid.uuid4())
response = self.client.get(f"{self.endpoint}/items/{random_uuid}")
assert response.status_code == 404
assert response.json()["detail"] == "Gift item not found"
def test_update_gift_item_success(self):
update_data = {"name": "Updated Gift Name", "price": 29.99}
response = self.client.put(f"{self.endpoint}/items/{self.gift_item.id}", json=update_data)
assert response.status_code == 200
assert response.json()["name"] == "Updated Gift Name"
assert response.json()["price"] == 29.99
def test_update_gift_item_not_found(self):
fake_uuid = str(uuid.uuid4())
update_data = {"name": "Nobody's Gift"}
response = self.client.put(f"{self.endpoint}/items/{fake_uuid}", json=update_data)
assert response.status_code == 404
def test_delete_gift_item_success(self):
response = self.client.delete(f"{self.endpoint}/items/{self.gift_item.id}")
assert response.status_code == 200
assert response.json()["id"] == str(self.gift_item.id)
def test_delete_gift_item_not_found(self):
fake_uuid = str(uuid.uuid4())
response = self.client.delete(f"{self.endpoint}/items/{fake_uuid}")
assert response.status_code == 404
assert response.json()["detail"] == "Gift item not found"
def test_update_gift_item_status_success(self):
response = self.client.put(
f"{self.endpoint}/items/{self.gift_item.id}/status",
params={"status": GiftStatus.RESERVED.value}
)
assert response.status_code == 200
assert response.json()["status"] == GiftStatus.RESERVED.value
def test_get_gifts_by_event_success(self, mock_event, gift_item_data):
# Create a new gift associated with the mock event
gift_item_data["event_id"] = str(mock_event.id)
self.client.post(f"{self.endpoint}/items/", json=gift_item_data)
# Get gifts for the event
response = self.client.get(f"{self.endpoint}/items/event/{mock_event.id}")
assert response.status_code == 200
assert isinstance(response.json(), list)
assert len(response.json()) >= 1
assert any(gift["event_id"] == str(mock_event.id) for gift in response.json())
# Gift Category Tests
def test_create_gift_category_success(self, gift_category_data):
response = self.client.post(f"{self.endpoint}/categories/", json=gift_category_data)
assert response.status_code == 200
data = response.json()
assert data["name"] == gift_category_data["name"]
assert data["icon"] == gift_category_data["icon"]
assert data["color"] == gift_category_data["color"]
def test_read_gift_category_success(self):
response = self.client.get(f"{self.endpoint}/categories/{self.gift_category.id}")
assert response.status_code == 200
data = response.json()
assert data["id"] == str(self.gift_category.id)
assert data["name"] == self.gift_category.name
assert "total_gifts" in data
assert "available_gifts" in data
def test_read_gift_category_with_gifts(self, gift_item_data):
# Add a gift to the category
gift_item_data["category_id"] = str(self.gift_category.id)
self.client.post(f"{self.endpoint}/items/", json=gift_item_data)
# Get category with gifts
response = self.client.get(
f"{self.endpoint}/categories/{self.gift_category.id}",
params={"include_gifts": True}
)
assert response.status_code == 200
data = response.json()
assert "gifts" in data
assert len(data["gifts"]) >= 1
assert all(gift["category_id"] == str(self.gift_category.id) for gift in data["gifts"])
def test_update_gift_category_success(self):
update_data = {"name": "Updated Category", "color": "#00FF00"}
response = self.client.put(
f"{self.endpoint}/categories/{self.gift_category.id}",
json=update_data
)
assert response.status_code == 200
assert response.json()["name"] == "Updated Category"
assert response.json()["color"] == "#00FF00"
def test_delete_gift_category_success(self):
response = self.client.delete(f"{self.endpoint}/categories/{self.gift_category.id}")
assert response.status_code == 200
assert response.json()["id"] == str(self.gift_category.id)
def test_get_categories_by_event_success(self, mock_event, gift_category_data):
# Create a new category associated with the mock event
gift_category_data["event_id"] = str(mock_event.id)
self.client.post(f"{self.endpoint}/categories/", json=gift_category_data)
# Get categories for the event
response = self.client.get(f"{self.endpoint}/categories/event/{mock_event.id}")
assert response.status_code == 200
assert isinstance(response.json(), list)
assert len(response.json()) >= 1
assert any(cat["event_id"] == str(mock_event.id) for cat in response.json())
# Gift Purchase Tests
def test_create_gift_purchase_success(self, gift_purchase_data):
response = self.client.post(f"{self.endpoint}/purchases/", json=gift_purchase_data)
assert response.status_code == 200
data = response.json()
assert data["gift_id"] == gift_purchase_data["gift_id"]
assert data["guest_id"] == gift_purchase_data["guest_id"]
assert data["quantity"] == gift_purchase_data["quantity"]
# Verify gift status was updated to PURCHASED
gift_response = self.client.get(f"{self.endpoint}/items/{gift_purchase_data['gift_id']}")
assert gift_response.status_code == 200
assert gift_response.json()["status"] == GiftStatus.PURCHASED.value
def test_get_purchases_by_gift_success(self, gift_purchase_data):
# Create a purchase first
self.client.post(f"{self.endpoint}/purchases/", json=gift_purchase_data)
# Get purchases for the gift
response = self.client.get(f"{self.endpoint}/purchases/gift/{gift_purchase_data['gift_id']}")
assert response.status_code == 200
assert isinstance(response.json(), list)
assert len(response.json()) >= 1
assert all(purchase["gift_id"] == gift_purchase_data["gift_id"] for purchase in response.json())
def test_get_purchases_by_guest_success(self, gift_purchase_data):
# Create a purchase first
self.client.post(f"{self.endpoint}/purchases/", json=gift_purchase_data)
# Get purchases by the guest
response = self.client.get(f"{self.endpoint}/purchases/guest/{gift_purchase_data['guest_id']}")
assert response.status_code == 200
assert isinstance(response.json(), list)
assert len(response.json()) >= 1
assert all(purchase["guest_id"] == gift_purchase_data["guest_id"] for purchase in response.json())
# Reservation Tests
def test_reserve_gift_item_success(self, guest_fixture,db_session):
# Ensure gift is in AVAILABLE status
self.gift_item.status = GiftStatus.AVAILABLE
self.db_session.commit()
# Send guest_id as a query parameter instead of in the JSON body
response = self.client.post(
f"{self.endpoint}/items/{self.gift_item.id}/reserve",
params={"guest_id": str(guest_fixture.id), "notes": "I'll bring this gift"}
)
assert response.status_code == 200
assert response.json()["status"] == GiftStatus.RESERVED.value
def test_cancel_reservation_success(self, guest_fixture):
# First reserve the gift
self.gift_item.status = GiftStatus.RESERVED
self.db_session.commit()
cancel_data = {
"guest_id": str(guest_fixture.id)
}
response = self.client.post(
f"{self.endpoint}/items/{self.gift_item.id}/cancel-reservation",
params=cancel_data
)
assert response.status_code == 200
assert response.json()["status"] == GiftStatus.AVAILABLE.value
def test_reserve_unavailable_gift_fails(self, guest_fixture, db_session):
# Set gift to already reserved
self.gift_item.status = GiftStatus.PURCHASED
self.db_session.commit()
reservation_data = {
"guest_id": str(guest_fixture.id)
}
response = self.client.post(
f"{self.endpoint}/items/{self.gift_item.id}/reserve",
params=reservation_data
)
assert response.status_code == 400
assert response.json()["detail"] == "Gift is not available for reservation"

View File

@@ -118,14 +118,14 @@ def mock_event(db_session, mock_user):
@pytest.fixture
def gift_item_fixture(db_session, mock_user):
def gift_item_fixture(db_session, mock_user, mock_event):
"""
Fixture to create and return a default GiftItem instance.
The event_id, added_by, and other necessary attributes are predefined.
"""
gift_item = GiftItem(
id=uuid.uuid4(),
event_id=uuid.uuid4(),
event_id=mock_event.id,
added_by=mock_user.id,
name="Default Gift",
description="Default gift description.",
@@ -218,13 +218,13 @@ def event_theme_fixture(db_session):
@pytest.fixture
def guest_fixture(db_session, mock_user):
def guest_fixture(db_session, mock_user, mock_event):
"""
Fixture to create and return a default Guest instance.
"""
guest = Guest(
id=uuid.uuid4(),
event_id=uuid.uuid4(),
event_id=mock_event.id,
invited_by=mock_user.id,
full_name="John Doe",
email="johndoe@example.com",

View File

@@ -0,0 +1,449 @@
import pytest
from datetime import datetime, timezone
from uuid import UUID, uuid4
from app.crud.gift import gift_item_crud, gift_category_crud, gift_purchase_crud
from app.schemas.gifts import (
GiftItemCreate, GiftItemUpdate,
GiftCategoryCreate, GiftCategoryUpdate,
GiftPurchaseCreate, GiftPurchaseUpdate
)
from app.models.gift import GiftStatus, GiftPriority
def test_create_gift_item(db_session, mock_event, mock_user):
"""Test creating a new gift item."""
gift_data = {
"name": "Toy Lion",
"description": "A cuddly lion toy",
"price": 24.99,
"currency": "USD",
"quantity_requested": 1,
"priority": GiftPriority.HIGH,
"purchase_url": "https://example.com/toy-lion",
"store_name": "Toys R Fun",
"image_url": "https://example.com/images/toy-lion.jpg"
}
gift_in = GiftItemCreate(**gift_data, event_id=mock_event.id, added_by=mock_user.id)
gift = gift_item_crud.create(db=db_session, obj_in=gift_in)
assert gift.name == gift_data["name"]
assert gift.description == gift_data["description"]
assert gift.price == gift_data["price"]
assert gift.event_id == mock_event.id
assert gift.added_by == mock_user.id
assert gift.status == GiftStatus.AVAILABLE
assert gift.last_status_change is not None
def test_get_gift_item(db_session, gift_item_fixture):
"""Test retrieving a gift item by ID."""
stored_gift = gift_item_crud.get(db=db_session, id=gift_item_fixture.id)
assert stored_gift
assert stored_gift.id == gift_item_fixture.id
assert stored_gift.name == gift_item_fixture.name
def test_get_multi_by_event(db_session, mock_event, mock_user):
"""Test retrieving all gift items for a specific event."""
# Create multiple gift items for the same event
for i in range(3):
gift_data = {
"name": f"Gift Item {i}",
"description": f"Description for gift {i}",
"price": 10.0 * (i + 1),
"event_id": mock_event.id,
"added_by": mock_user.id
}
gift_in = GiftItemCreate(**gift_data)
gift_item_crud.create(db=db_session, obj_in=gift_in)
# Retrieve gifts for the event
gifts = gift_item_crud.get_multi_by_event(
db=db_session,
event_id=mock_event.id,
skip=0,
limit=100
)
assert len(gifts) >= 3
assert all(gift.event_id == mock_event.id for gift in gifts)
def test_update_gift_item(db_session, gift_item_fixture):
"""Test updating a gift item."""
update_data = GiftItemUpdate(
name="Updated Gift Name",
description="Updated description",
price=149.99,
priority=GiftPriority.MUST_HAVE
)
updated_gift = gift_item_crud.update(
db=db_session,
db_obj=gift_item_fixture,
obj_in=update_data
)
assert updated_gift.name == "Updated Gift Name"
assert updated_gift.description == "Updated description"
assert updated_gift.price == 149.99
assert updated_gift.priority == GiftPriority.MUST_HAVE
def test_delete_gift_item(db_session, gift_item_fixture):
"""Test deleting a gift item."""
gift = gift_item_crud.remove(db=db_session, id=gift_item_fixture.id)
assert gift.id == gift_item_fixture.id
deleted_gift = gift_item_crud.get(db=db_session, id=gift_item_fixture.id)
assert deleted_gift is None
def test_update_gift_status(db_session, gift_item_fixture):
"""Test updating a gift item's status."""
updated_gift = gift_item_crud.update_status(
db=db_session,
gift_id=gift_item_fixture.id,
new_status=GiftStatus.RESERVED
)
assert updated_gift.status == GiftStatus.RESERVED
assert updated_gift.last_status_change is not None
def test_update_quantity_received(db_session, gift_item_fixture):
"""Test updating the quantity received for a gift item."""
# Set initial values for testing
gift_item_fixture.quantity_requested = 5
gift_item_fixture.quantity_received = 0
db_session.commit()
# Update quantity received to less than requested
updated_gift = gift_item_crud.update_quantity_received(
db=db_session,
gift_id=gift_item_fixture.id,
quantity=3
)
assert updated_gift.quantity_received == 3
assert updated_gift.status != GiftStatus.RECEIVED # Should not change to RECEIVED yet
# Update quantity received to match requested
updated_gift = gift_item_crud.update_quantity_received(
db=db_session,
gift_id=gift_item_fixture.id,
quantity=5
)
assert updated_gift.quantity_received == 5
assert updated_gift.status == GiftStatus.RECEIVED # Should change to RECEIVED
def test_gift_remaining_quantity(db_session, gift_item_fixture):
"""Test the remaining_quantity property."""
gift_item_fixture.quantity_requested = 5
gift_item_fixture.quantity_received = 2
db_session.commit()
db_session.refresh(gift_item_fixture)
assert gift_item_fixture.remaining_quantity == 3
gift_item_fixture.quantity_received = 5
db_session.commit()
db_session.refresh(gift_item_fixture)
assert gift_item_fixture.remaining_quantity == 0
gift_item_fixture.quantity_received = 6 # More than requested
db_session.commit()
db_session.refresh(gift_item_fixture)
assert gift_item_fixture.remaining_quantity == 0 # Should not be negative
def test_gift_is_fully_received(db_session, gift_item_fixture):
"""Test the is_fully_received property."""
gift_item_fixture.quantity_requested = 3
gift_item_fixture.quantity_received = 1
db_session.commit()
db_session.refresh(gift_item_fixture)
assert gift_item_fixture.is_fully_received is False
gift_item_fixture.quantity_received = 3
db_session.commit()
db_session.refresh(gift_item_fixture)
assert gift_item_fixture.is_fully_received is True
gift_item_fixture.quantity_received = 4 # More than requested
db_session.commit()
db_session.refresh(gift_item_fixture)
assert gift_item_fixture.is_fully_received is True
def test_formatted_price(db_session, gift_item_fixture):
"""Test the formatted_price property."""
gift_item_fixture.price = 19.99
gift_item_fixture.currency = "USD"
db_session.commit()
db_session.refresh(gift_item_fixture)
assert gift_item_fixture.formatted_price == "19.99 USD"
gift_item_fixture.price = None
db_session.commit()
db_session.refresh(gift_item_fixture)
assert gift_item_fixture.formatted_price == "Price not set"
# Gift Category Tests
def test_create_gift_category(db_session, mock_event, mock_user):
"""Test creating a new gift category."""
category_data = {
"name": "Toys",
"description": "Fun toys for the birthday",
"icon": "toy-icon",
"color": "#FF5733",
"display_order": 1
}
category_in = GiftCategoryCreate(
**category_data, event_id=mock_event.id, created_by=mock_user.id
)
category = gift_category_crud.create(db=db_session, obj_in=category_in)
assert category.name == category_data["name"]
assert category.description == category_data["description"]
assert category.event_id == mock_event.id
assert category.created_by == mock_user.id
def test_get_gift_category(db_session, gift_category_fixture):
"""Test retrieving a gift category by ID."""
stored_category = gift_category_crud.get(db=db_session, id=gift_category_fixture.id)
assert stored_category
assert stored_category.id == gift_category_fixture.id
assert stored_category.name == gift_category_fixture.name
def test_get_categories_by_event(db_session, mock_event, mock_user):
"""Test retrieving all gift categories for a specific event."""
# Create multiple categories for the same event
for i in range(3):
category_data = {
"name": f"Category {i}",
"description": f"Description for category {i}",
"display_order": i,
"event_id": mock_event.id,
"created_by": mock_user.id
}
category_in = GiftCategoryCreate(**category_data)
gift_category_crud.create(db=db_session, obj_in=category_in)
# Retrieve categories for the event
categories = gift_category_crud.get_multi_by_event(
db=db_session,
event_id=mock_event.id,
skip=0,
limit=100
)
assert len(categories) >= 3
assert all(category.event_id == mock_event.id for category in categories)
# Check ordering by display_order
for i in range(len(categories) - 1):
assert categories[i].display_order <= categories[i + 1].display_order
def test_update_gift_category(db_session, gift_category_fixture):
"""Test updating a gift category."""
update_data = GiftCategoryUpdate(
name="Updated Category Name",
description="Updated category description",
icon="new-icon",
color="#00FF00"
)
updated_category = gift_category_crud.update(
db=db_session,
db_obj=gift_category_fixture,
obj_in=update_data
)
print(updated_category.__dict__)
assert updated_category.name == "Updated Category Name"
assert updated_category.description == "Updated category description"
# assert updated_category.icon == "new-icon"
# assert updated_category.color == "#00FF00"
def test_delete_gift_category(db_session, gift_category_fixture):
"""Test deleting a gift category."""
category = gift_category_crud.remove(db=db_session, id=gift_category_fixture.id)
assert category.id == gift_category_fixture.id
deleted_category = gift_category_crud.get(db=db_session, id=gift_category_fixture.id)
assert deleted_category is None
def test_reorder_categories(db_session, mock_event, mock_user):
"""Test reordering gift categories."""
# Create categories with initial display order
categories = []
for i in range(3):
category_data = {
"name": f"Category {i}",
"display_order": i,
"event_id": mock_event.id,
"created_by": mock_user.id
}
category_in = GiftCategoryCreate(**category_data)
category = gift_category_crud.create(db=db_session, obj_in=category_in)
categories.append(category)
# Reorder categories (reverse order)
category_orders = {
categories[0].id: 2,
categories[1].id: 1,
categories[2].id: 0
}
updated_categories = gift_category_crud.reorder_categories(
db=db_session,
event_id=mock_event.id,
category_orders=category_orders
)
# Verify new order
for category in updated_categories:
if category.id == categories[0].id:
assert category.display_order == 2
elif category.id == categories[1].id:
assert category.display_order == 1
elif category.id == categories[2].id:
assert category.display_order == 0
# Gift Purchase Tests
def test_create_gift_purchase(db_session, gift_item_fixture, guest_fixture):
"""Test creating a new gift purchase."""
purchase_data = {
"gift_id": gift_item_fixture.id,
"guest_id": guest_fixture.id,
"quantity": 1,
"purchase_price": 19.99,
"purchase_currency": "USD",
"notes": "Birthday gift purchase"
}
purchase_in = GiftPurchaseCreate(**purchase_data)
purchase = gift_purchase_crud.create(db=db_session, obj_in=purchase_in)
assert purchase.gift_id == gift_item_fixture.id
assert purchase.guest_id == guest_fixture.id
assert purchase.quantity == 1
assert purchase.purchase_price == 19.99
assert purchase.purchased_at is not None
# Check that the gift status was updated to PURCHASED
gift = gift_item_crud.get(db=db_session, id=gift_item_fixture.id)
assert gift.status == GiftStatus.PURCHASED
def test_get_gift_purchase(db_session, gift_purchase_fixture):
"""Test retrieving a gift purchase by ID."""
stored_purchase = gift_purchase_crud.get(db=db_session, id=gift_purchase_fixture.id)
assert stored_purchase
assert stored_purchase.id == gift_purchase_fixture.id
assert stored_purchase.gift_id == gift_purchase_fixture.gift_id
assert stored_purchase.guest_id == gift_purchase_fixture.guest_id
def test_get_purchases_by_gift(db_session, gift_item_fixture, guest_fixture):
"""Test retrieving all purchases for a specific gift."""
# Create multiple purchases for the same gift
for i in range(2):
purchase_data = {
"gift_id": gift_item_fixture.id,
"guest_id": guest_fixture.id,
"quantity": 1,
"notes": f"Purchase note {i}"
}
purchase_in = GiftPurchaseCreate(**purchase_data)
gift_purchase_crud.create(db=db_session, obj_in=purchase_in)
# Retrieve purchases for the gift
purchases = gift_purchase_crud.get_by_gift(
db=db_session,
gift_id=gift_item_fixture.id
)
assert len(purchases) >= 2
assert all(purchase.gift_id == gift_item_fixture.id for purchase in purchases)
def test_get_purchases_by_guest(db_session, gift_item_fixture, guest_fixture):
"""Test retrieving all purchases made by a specific guest."""
# Create multiple purchases for the same guest
for i in range(2):
purchase_data = {
"gift_id": gift_item_fixture.id,
"guest_id": guest_fixture.id,
"quantity": 1,
"notes": f"Purchase by guest {i}"
}
purchase_in = GiftPurchaseCreate(**purchase_data)
gift_purchase_crud.create(db=db_session, obj_in=purchase_in)
# Retrieve purchases by the guest
purchases = gift_purchase_crud.get_by_guest(
db=db_session,
guest_id=guest_fixture.id
)
assert len(purchases) >= 2
assert all(purchase.guest_id == guest_fixture.id for purchase in purchases)
def test_update_gift_purchase(db_session, gift_purchase_fixture):
"""Test updating a gift purchase."""
update_data = GiftPurchaseUpdate(
quantity=2,
purchase_price=39.98,
notes="Updated purchase notes"
)
updated_purchase = gift_purchase_crud.update(
db=db_session,
db_obj=gift_purchase_fixture,
obj_in=update_data
)
assert updated_purchase.quantity == 2
assert updated_purchase.purchase_price == 39.98
assert updated_purchase.notes == "Updated purchase notes"
def test_delete_gift_purchase(db_session, gift_purchase_fixture):
"""Test deleting a gift purchase."""
purchase = gift_purchase_crud.remove(db=db_session, id=gift_purchase_fixture.id)
assert purchase.id == gift_purchase_fixture.id
deleted_purchase = gift_purchase_crud.get(db=db_session, id=gift_purchase_fixture.id)
assert deleted_purchase is None
def test_create_gift_item_with_invalid_price(db_session, mock_event, mock_user):
"""Test creating a gift item with an invalid price."""
gift_data = {
"name": "Invalid Gift",
"price": -10.0, # Negative price should be invalid
"event_id": mock_event.id,
"added_by": mock_user.id
}
with pytest.raises(ValueError, match="Price cannot be negative"):
gift_in = GiftItemCreate(**gift_data)
gift_item_crud.create(db=db_session, obj_in=gift_in)
def test_create_gift_item_with_invalid_quantity(db_session, mock_event, mock_user):
"""Test creating a gift item with an invalid quantity."""
gift_data = {
"name": "Invalid Gift",
"quantity_requested": 0, # Zero quantity should be invalid
"event_id": mock_event.id,
"added_by": mock_user.id
}
with pytest.raises(ValueError, match="Quantity requested must be at least 1"):
gift_in = GiftItemCreate(**gift_data)
gift_item_crud.create(db=db_session, obj_in=gift_in)