diff --git a/backend/app/api/routes/events/gifts.py b/backend/app/api/routes/events/gifts.py new file mode 100644 index 0000000..e50d03c --- /dev/null +++ b/backend/app/api/routes/events/gifts.py @@ -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) \ No newline at end of file diff --git a/backend/app/crud/gift.py b/backend/app/crud/gift.py new file mode 100644 index 0000000..2c27b0b --- /dev/null +++ b/backend/app/crud/gift.py @@ -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) \ No newline at end of file diff --git a/backend/app/schemas/gifts.py b/backend/app/schemas/gifts.py new file mode 100644 index 0000000..56670d7 --- /dev/null +++ b/backend/app/schemas/gifts.py @@ -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 \ No newline at end of file diff --git a/backend/tests/api/routes/events/test_gifts.py b/backend/tests/api/routes/events/test_gifts.py new file mode 100644 index 0000000..29e415e --- /dev/null +++ b/backend/tests/api/routes/events/test_gifts.py @@ -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" \ No newline at end of file diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index f316091..e23744f 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -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", diff --git a/backend/tests/crud/test_gift.py b/backend/tests/crud/test_gift.py new file mode 100644 index 0000000..3a9edde --- /dev/null +++ b/backend/tests/crud/test_gift.py @@ -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) \ No newline at end of file