diff --git a/app/core/feed/cruds_feed.py b/app/core/feed/cruds_feed.py index f052d4c7f8..8bb9667bcf 100644 --- a/app/core/feed/cruds_feed.py +++ b/app/core/feed/cruds_feed.py @@ -92,3 +92,17 @@ async def edit_news_by_module_object_id( ) .values(**news_edit.model_dump(exclude_unset=True)), ) + + +async def get_news_by_module_object_id( + module: str, + module_object_id: UUID, + db: AsyncSession, +) -> models_feed.News | None: + result = await db.execute( + select(models_feed.News).where( + models_feed.News.module == module, + models_feed.News.module_object_id == module_object_id, + ), + ) + return result.scalars().first() diff --git a/app/core/feed/utils_feed.py b/app/core/feed/utils_feed.py index fd9586ca0b..13d0fca1e5 100644 --- a/app/core/feed/utils_feed.py +++ b/app/core/feed/utils_feed.py @@ -123,3 +123,16 @@ async def edit_feed_news( group_id=group, message=message, ) + + +async def check_if_module_object_id_is_linked_to_feed( + module: str, + module_object_id: uuid.UUID, + db: AsyncSession, +) -> bool: + result = await cruds_feed.get_news_by_module_object_id( + module=module, + module_object_id=module_object_id, + db=db, + ) + return result is not None diff --git a/app/core/mypayment/utils_mypayment.py b/app/core/mypayment/utils_mypayment.py index d31e36bd87..d9e3358a07 100644 --- a/app/core/mypayment/utils_mypayment.py +++ b/app/core/mypayment/utils_mypayment.py @@ -425,3 +425,18 @@ async def can_user_manage_events( db=db, ) return seller is not None and seller.can_manage_events + + +async def ensure_user_can_manage_events( + user_id: str, + store_id: UUID, + db: AsyncSession, +): + """ + Will raise a 403 HTTPException if the user is not authorized to manage events for the store. + """ + if not await can_user_manage_events(user_id, store_id, db): + raise HTTPException( + 403, + detail="User is not authorized to manage store's events", + ) diff --git a/app/core/tickets/cruds_tickets.py b/app/core/tickets/cruds_tickets.py index 7bf090e8f9..03efa80ac9 100644 --- a/app/core/tickets/cruds_tickets.py +++ b/app/core/tickets/cruds_tickets.py @@ -345,6 +345,24 @@ async def create_event_category( db.add(db_category) +async def create_event_question( + question_id: UUID, + event_id: UUID, + question: schemas_tickets.QuestionCreate, + db: AsyncSession, +): + db_question = models_tickets.Question( + id=question_id, + event_id=event_id, + question=question.question, + answer_type=question.answer_type, + price=question.price, + required=question.required, + disabled=False, + ) + db.add(db_question) + + async def get_category_by_id( category_id: UUID, db: AsyncSession, @@ -726,44 +744,48 @@ async def count_valid_checkouts_by_event_id( return result.scalar() or 0 -async def count_valid_checkouts_by_category_id( - category_id: UUID, +async def count_valid_checkouts_and_tickets_by_event_id( + event_id: UUID, db: AsyncSession, ) -> int: """ - Count only unpaid checkouts that are not expired + Count unpaid checkouts that are not expired and paid tickets """ result = await db.execute( select(func.count()).where( - models_tickets.Checkout.category_id == category_id, - models_tickets.Checkout.expiration >= datetime.now(UTC), - not_(models_tickets.Checkout.paid), + models_tickets.Checkout.event_id == event_id, + or_( + models_tickets.Checkout.paid, + models_tickets.Checkout.expiration >= datetime.now(UTC), + ), ), ) return result.scalar() or 0 -async def count_valid_checkouts_by_session_id( +async def count_valid_checkouts_and_tickets_by_session_id( session_id: UUID, db: AsyncSession, ) -> int: """ - Count only unpaid checkouts that are not expired + Count unpaid checkouts that are not expired and paid tickets """ result = await db.execute( select(func.count()).where( models_tickets.Checkout.session_id == session_id, - models_tickets.Checkout.expiration >= datetime.now(UTC), - not_(models_tickets.Checkout.paid), + or_( + models_tickets.Checkout.paid, + models_tickets.Checkout.expiration >= datetime.now(UTC), + ), ), ) return result.scalar() or 0 -async def count_valid_checkouts_and_tickets_by_event_id( - event_id: UUID, +async def count_valid_checkouts_and_tickets_by_category_id( + category_id: UUID, db: AsyncSession, ) -> int: """ @@ -771,7 +793,7 @@ async def count_valid_checkouts_and_tickets_by_event_id( """ result = await db.execute( select(func.count()).where( - models_tickets.Checkout.event_id == event_id, + models_tickets.Checkout.category_id == category_id, or_( models_tickets.Checkout.paid, models_tickets.Checkout.expiration >= datetime.now(UTC), @@ -782,40 +804,36 @@ async def count_valid_checkouts_and_tickets_by_event_id( return result.scalar() or 0 -async def count_valid_checkouts_and_tickets_by_category_id( +async def count_valid_checkouts_by_category_id( category_id: UUID, db: AsyncSession, ) -> int: """ - Count unpaid checkouts that are not expired and paid tickets + Count only unpaid checkouts that are not expired """ result = await db.execute( select(func.count()).where( models_tickets.Checkout.category_id == category_id, - or_( - models_tickets.Checkout.paid, - models_tickets.Checkout.expiration >= datetime.now(UTC), - ), + models_tickets.Checkout.expiration >= datetime.now(UTC), + not_(models_tickets.Checkout.paid), ), ) return result.scalar() or 0 -async def count_valid_checkouts_and_tickets_by_session_id( +async def count_valid_checkouts_by_session_id( session_id: UUID, db: AsyncSession, ) -> int: """ - Count unpaid checkouts that are not expired and paid tickets + Count only unpaid checkouts that are not expired """ result = await db.execute( select(func.count()).where( models_tickets.Checkout.session_id == session_id, - or_( - models_tickets.Checkout.paid, - models_tickets.Checkout.expiration >= datetime.now(UTC), - ), + models_tickets.Checkout.expiration >= datetime.now(UTC), + not_(models_tickets.Checkout.paid), ), ) @@ -926,3 +944,89 @@ async def get_ticket_change_over_invitation_by_token( new_user_id=invitation.new_user_id, token=invitation.token, ) + + +async def delete_question( + question_id: UUID, + db: AsyncSession, +): + await db.execute( + delete(models_tickets.Question).where( + models_tickets.Question.id == question_id, + ), + ) + + +async def delete_session( + session_id: UUID, + db: AsyncSession, +): + # Delete all expired unpaid checkouts for the session before deleting the session itself + await db.execute( + delete(models_tickets.Checkout).where( + models_tickets.Checkout.session_id == session_id, + not_(models_tickets.Checkout.paid), + models_tickets.Checkout.expiration < datetime.now(UTC), + ), + ) + + await db.execute( + delete(models_tickets.EventSession).where( + models_tickets.EventSession.id == session_id, + ), + ) + + +async def delete_category( + category_id: UUID, + db: AsyncSession, +): + # Delete all expired unpaid checkouts for the category before deleting the category itself + await db.execute( + delete(models_tickets.Checkout).where( + models_tickets.Checkout.category_id == category_id, + not_(models_tickets.Checkout.paid), + models_tickets.Checkout.expiration < datetime.now(UTC), + ), + ) + + await db.execute( + delete(models_tickets.Category).where( + models_tickets.Category.id == category_id, + ), + ) + + +async def delete_event( + event_id: UUID, + db: AsyncSession, +): + # Delete all expired unpaid checkouts for the event before deleting the event itself + await db.execute( + delete(models_tickets.Checkout).where( + models_tickets.Checkout.event_id == event_id, + not_(models_tickets.Checkout.paid), + models_tickets.Checkout.expiration < datetime.now(UTC), + ), + ) + + await db.execute( + delete(models_tickets.Question).where( + models_tickets.Question.event_id == event_id, + ), + ) + await db.execute( + delete(models_tickets.EventSession).where( + models_tickets.EventSession.event_id == event_id, + ), + ) + await db.execute( + delete(models_tickets.Category).where( + models_tickets.Category.event_id == event_id, + ), + ) + await db.execute( + delete(models_tickets.TicketEvent).where( + models_tickets.TicketEvent.id == event_id, + ), + ) diff --git a/app/core/tickets/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index fbc512a27c..2e89b0b9cc 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -415,6 +415,8 @@ async def ticket_request_change_over( giver_name=user.full_name, ) + confirmation_url = "No account exists for this email" + else: await cruds_tickets.create_ticket_change_over_invitation( ticket_id=ticket.id, @@ -431,18 +433,23 @@ async def ticket_request_change_over( confirmation_url=confirmation_url, ) - background_tasks.add_task( - send_email, - recipient=ticket_transfer.email, - subject=f"{settings.school.application_name} - Ticket transfer for {event.name}", - content=mail, - settings=settings, - ) + if settings.SMTP_ACTIVE: + background_tasks.add_task( + send_email, + recipient=ticket_transfer.email, + subject=f"{settings.school.application_name} - Ticket transfer for {event.name}", + content=mail, + settings=settings, + ) + else: + hyperion_security_logger.info( + f"You can confirm the transfer by clicking the following link: {confirmation_url}", + ) @router.get( "/tickets/user/me/tickets/change-over/accept", - status_code=200, + status_code=307, ) async def ticket_accept_change_over( token: str, @@ -505,15 +512,11 @@ async def get_event_admin( if event is None: raise HTTPException(404, "Event not found") - if not await utils_mypayment.can_user_manage_events( + await utils_mypayment.ensure_user_can_manage_events( user_id=user.id, store_id=event.store_id, db=db, - ): - raise HTTPException( - status_code=403, - detail="User is not authorized to manage store events", - ) + ) return await utils_tickets.convert_to_event_admin( event=event, @@ -538,15 +541,11 @@ async def create_event( **The user should have the right to manage the event seller** """ - if not await utils_mypayment.can_user_manage_events( + await utils_mypayment.ensure_user_can_manage_events( user_id=user.id, store_id=event_create.store_id, db=db, - ): - raise HTTPException( - status_code=403, - detail="User is not authorized to manage store events", - ) + ) if len(event_create.sessions) == 0 or len(event_create.categories) == 0: raise HTTPException( @@ -597,15 +596,11 @@ async def update_event( if event is None: raise HTTPException(404, "Event not found") - if not await utils_mypayment.can_user_manage_events( + await utils_mypayment.ensure_user_can_manage_events( user_id=user.id, store_id=event.store_id, db=db, - ): - raise HTTPException( - status_code=403, - detail="User is not authorized to manage store's events", - ) + ) if event_update.open_datetime is not None: # We want to update the datetime in the feed @@ -627,6 +622,59 @@ async def update_event( ) +@router.delete( + "/tickets/admin/events/{event_id}", + status_code=204, +) +async def delete_event( + event_id: UUID, + user: CoreUser = Depends( + is_user(), + ), + db: AsyncSession = Depends(get_db), +): + """ + Delete one event for admin + """ + event = await cruds_tickets.get_event_simple_by_id(event_id=event_id, db=db) + if event is None: + raise HTTPException(404, "Event not found") + + await utils_mypayment.ensure_user_can_manage_events( + user_id=user.id, + store_id=event.store_id, + db=db, + ) + + nb_checkouts_and_tickets = ( + await cruds_tickets.count_valid_checkouts_and_tickets_by_event_id( + event_id=event_id, + db=db, + ) + ) + if nb_checkouts_and_tickets > 0: + raise HTTPException( + 400, + "Cannot delete event with checkouts or tickets", + ) + + # We want to check if the event is linked to the feed + if await utils_feed.check_if_module_object_id_is_linked_to_feed( + module=core_module.root, + module_object_id=event.id, + db=db, + ): + raise HTTPException( + 400, + "Cannot delete event linked to the feed", + ) + + await cruds_tickets.delete_event( + event_id=event_id, + db=db, + ) + + @router.post( "/tickets/admin/events/{event_id}/sessions", response_model=schemas_tickets.SessionComplete, @@ -649,15 +697,11 @@ async def create_session( if event is None: raise HTTPException(404, "Event not found") - if not await utils_mypayment.can_user_manage_events( + await utils_mypayment.ensure_user_can_manage_events( user_id=user.id, store_id=event.store_id, db=db, - ): - raise HTTPException( - status_code=403, - detail="User is not authorized to manage store's events", - ) + ) session_id = uuid.uuid4() @@ -694,37 +738,66 @@ async def update_session( if event is None: raise HTTPException(404, "Event not found") - if not await utils_mypayment.can_user_manage_events( + await utils_mypayment.ensure_user_can_manage_events( user_id=user.id, store_id=event.store_id, db=db, - ): - raise HTTPException( - status_code=403, - detail="User is not authorized to manage store's events", - ) + ) session = await cruds_tickets.get_session_by_id(session_id=session_id, db=db) if session is None or session.event_id != event_id: raise HTTPException(404, "Session not found") - nb_checkouts = await cruds_tickets.count_valid_checkouts_by_session_id( + await cruds_tickets.update_session( session_id=session_id, + session_update=session_update, db=db, ) - nb_tickets = await cruds_tickets.count_tickets_by_session_id( - session_id=session_id, + + +@router.delete( + "/tickets/admin/events/{event_id}/sessions/{session_id}", + status_code=204, +) +async def delete_session( + event_id: UUID, + session_id: UUID, + user: CoreUser = Depends( + is_user(), + ), + db: AsyncSession = Depends(get_db), +): + """ + Delete one session for admin + """ + event = await cruds_tickets.get_event_simple_by_id(event_id=event_id, db=db) + if event is None: + raise HTTPException(404, "Event not found") + + await utils_mypayment.ensure_user_can_manage_events( + user_id=user.id, + store_id=event.store_id, db=db, ) - if nb_checkouts + nb_tickets > 0: + + session = await cruds_tickets.get_session_by_id(session_id=session_id, db=db) + if session is None or session.event_id != event_id: + raise HTTPException(404, "Session not found") + + nb_checkouts_and_tickets = ( + await cruds_tickets.count_valid_checkouts_and_tickets_by_session_id( + session_id=session_id, + db=db, + ) + ) + if nb_checkouts_and_tickets > 0: raise HTTPException( 400, - "Cannot update session with checkouts or tickets", + "Cannot delete session with checkouts or tickets", ) - await cruds_tickets.update_session( + await cruds_tickets.delete_session( session_id=session_id, - session_update=session_update, db=db, ) @@ -751,15 +824,11 @@ async def create_category( if event is None: raise HTTPException(404, "Event not found") - if not await utils_mypayment.can_user_manage_events( + await utils_mypayment.ensure_user_can_manage_events( user_id=user.id, store_id=event.store_id, db=db, - ): - raise HTTPException( - status_code=403, - detail="User is not authorized to manage store's events", - ) + ) category_id = uuid.uuid4() @@ -796,41 +865,134 @@ async def update_category( if event is None: raise HTTPException(404, "Event not found") - if not await utils_mypayment.can_user_manage_events( + await utils_mypayment.ensure_user_can_manage_events( user_id=user.id, store_id=event.store_id, db=db, - ): - raise HTTPException( - status_code=403, - detail="User is not authorized to manage store's events", - ) + ) category = await cruds_tickets.get_category_by_id(category_id=category_id, db=db) if category is None or category.event_id != event_id: raise HTTPException(404, "Category not found") - nb_checkouts = await cruds_tickets.count_valid_checkouts_by_category_id( + # Some fields cannot be updated if the category has checkouts or tickets + fields_to_update = category_update.model_dump(exclude_unset=True).keys() + if "price" in fields_to_update: + nb_checkouts_and_tickets = ( + await cruds_tickets.count_valid_checkouts_and_tickets_by_category_id( + category_id=category_id, + db=db, + ) + ) + if nb_checkouts_and_tickets > 0: + raise HTTPException( + 400, + "Cannot update category price or required_membership with checkouts or tickets", + ) + + await cruds_tickets.update_category( category_id=category_id, + category_update=category_update, db=db, ) - nb_tickets = await cruds_tickets.count_tickets_by_category_id( - category_id=category_id, + + +@router.delete( + "/tickets/admin/events/{event_id}/categories/{category_id}", + status_code=204, +) +async def delete_category( + event_id: UUID, + category_id: UUID, + user: CoreUser = Depends( + is_user(), + ), + db: AsyncSession = Depends(get_db), +): + """ + Delete one category for admin + """ + event = await cruds_tickets.get_event_simple_by_id(event_id=event_id, db=db) + if event is None: + raise HTTPException(404, "Event not found") + + await utils_mypayment.ensure_user_can_manage_events( + user_id=user.id, + store_id=event.store_id, db=db, ) - if nb_checkouts + nb_tickets > 0: + + category = await cruds_tickets.get_category_by_id(category_id=category_id, db=db) + if category is None or category.event_id != event_id: + raise HTTPException(404, "Category not found") + + nb_checkouts_and_tickets = ( + await cruds_tickets.count_valid_checkouts_and_tickets_by_category_id( + category_id=category_id, + db=db, + ) + ) + if nb_checkouts_and_tickets > 0: raise HTTPException( 400, - "Cannot update category with checkouts or tickets", + "Cannot delete category with checkouts or tickets", ) - await cruds_tickets.update_category( + await cruds_tickets.delete_category( category_id=category_id, - category_update=category_update, db=db, ) +@router.post( + "/tickets/admin/events/{event_id}/questions", + response_model=schemas_tickets.Question, + status_code=201, +) +async def create_question( + event_id: UUID, + question_create: schemas_tickets.QuestionCreate, + user: CoreUser = Depends( + is_user(), + ), + db: AsyncSession = Depends(get_db), +): + """ + Create a question for an event + + **The user should have the right to manage the event seller** + """ + event = await cruds_tickets.get_event_simple_by_id(event_id=event_id, db=db) + if event is None: + raise HTTPException(404, "Event not found") + + await utils_mypayment.ensure_user_can_manage_events( + user_id=user.id, + store_id=event.store_id, + db=db, + ) + + question_id = uuid.uuid4() + + await cruds_tickets.create_event_question( + question_id=question_id, + event_id=event_id, + question=question_create, + db=db, + ) + + question = await cruds_tickets.get_question_by_id( + question_id=question_id, + db=db, + ) + if question is None: + raise ObjectExpectedInDbNotFoundError( + object_name="Question", + object_id=question_id, + ) + return question + + @router.patch( "/tickets/admin/events/{event_id}/questions/{question_id}", status_code=204, @@ -851,15 +1013,60 @@ async def update_question( if event is None: raise HTTPException(404, "Event not found") - if not await utils_mypayment.can_user_manage_events( + await utils_mypayment.ensure_user_can_manage_events( user_id=user.id, store_id=event.store_id, db=db, - ): - raise HTTPException( - status_code=403, - detail="User is not authorized to manage store's events", + ) + + question = await cruds_tickets.get_question_by_id(question_id=question_id, db=db) + if question is None or question.event_id != event_id: + raise HTTPException(404, "Question not found") + + # Some fields cannot be updated if the question has answers + fields_to_update = question_update.model_dump(exclude_unset=True).keys() + if any(field in fields_to_update for field in ["answer_type", "price"]): + nb_answers = await cruds_tickets.count_answers_by_question_id( + question_id=question_id, + db=db, ) + if nb_answers > 0: + raise HTTPException( + 400, + "Cannot update answer_type or price for question with answers", + ) + + await cruds_tickets.update_question( + question_id=question_id, + question_update=question_update, + db=db, + ) + + +@router.delete( + "/tickets/admin/events/{event_id}/questions/{question_id}", + status_code=204, +) +async def delete_question( + event_id: UUID, + question_id: UUID, + user: CoreUser = Depends( + is_user(), + ), + db: AsyncSession = Depends(get_db), +): + """ + Delete one question for admin + """ + event = await cruds_tickets.get_event_simple_by_id(event_id=event_id, db=db) + if event is None: + raise HTTPException(404, "Event not found") + + await utils_mypayment.ensure_user_can_manage_events( + user_id=user.id, + store_id=event.store_id, + db=db, + ) question = await cruds_tickets.get_question_by_id(question_id=question_id, db=db) if question is None or question.event_id != event_id: @@ -872,12 +1079,11 @@ async def update_question( if nb_answers > 0: raise HTTPException( 400, - "Cannot update question with answers", + "Cannot delete question with answers", ) - await cruds_tickets.update_question( + await cruds_tickets.delete_question( question_id=question_id, - question_update=question_update, db=db, ) @@ -903,15 +1109,11 @@ async def get_event_tickets( if event is None: raise HTTPException(404, "Event not found") - if not await utils_mypayment.can_user_manage_events( + await utils_mypayment.ensure_user_can_manage_events( user_id=user.id, store_id=event.store_id, db=db, - ): - raise HTTPException( - status_code=403, - detail="User is not authorized to manage store events", - ) + ) return await cruds_tickets.get_paid_tickets_by_event_id(event_id=event_id, db=db) @@ -937,15 +1139,11 @@ async def get_event_tickets_csv( if event is None: raise HTTPException(404, "Event not found") - if not await utils_mypayment.can_user_manage_events( + await utils_mypayment.ensure_user_can_manage_events( user_id=user.id, store_id=event.store_id, db=db, - ): - raise HTTPException( - status_code=403, - detail="User is not authorized to manage store events", - ) + ) csv_io = StringIO() @@ -1050,15 +1248,11 @@ async def check_ticket( object_id=ticket.event_id, ) - if not await utils_mypayment.can_user_manage_events( + await utils_mypayment.ensure_user_can_manage_events( user_id=user.id, store_id=event.store_id, db=db, - ): - raise HTTPException( - status_code=403, - detail="User is not authorized to manage store events", - ) + ) return ticket @@ -1091,15 +1285,11 @@ async def scan_ticket( object_id=ticket.event_id, ) - if not await utils_mypayment.can_user_manage_events( + await utils_mypayment.ensure_user_can_manage_events( user_id=user.id, store_id=event.store_id, db=db, - ): - raise HTTPException( - status_code=403, - detail="User is not authorized to manage store events", - ) + ) if ticket.scanned: raise HTTPException( diff --git a/app/core/tickets/schemas_tickets.py b/app/core/tickets/schemas_tickets.py index a0f1826597..1835818720 100644 --- a/app/core/tickets/schemas_tickets.py +++ b/app/core/tickets/schemas_tickets.py @@ -92,6 +92,7 @@ def null_or_greater_than_one_euro(cls, v: int) -> int: class CategoryUpdate(BaseModel): name: str | None = None + # price can not be updated if there are already checkouts or tickets for this category price: int | None = None quota: int | None = None required_membership: UUID | None = None @@ -131,7 +132,9 @@ class QuestionCreate(BaseModel): class QuestionUpdate(BaseModel): question: str | None = None + # answer_type can not be updated if there are already answers for this question answer_type: AnswerType | None = None + # price can not be updated if there are already answers for this question price: int | None = None required: bool | None = None disabled: bool | None = None @@ -192,6 +195,7 @@ class EventUpdate(BaseModel): quota: int | None = None open_datetime: datetime | None = None close_datetime: datetime | None = None + disabled: bool | None = None class AnswerValue(BaseModel): diff --git a/app/core/tickets/utils_tickets.py b/app/core/tickets/utils_tickets.py index 91ad11a381..005e2a3706 100644 --- a/app/core/tickets/utils_tickets.py +++ b/app/core/tickets/utils_tickets.py @@ -159,15 +159,11 @@ async def get_events_from_store( user_id: str, db: AsyncSession, ) -> Sequence[schemas_tickets.EventSimple]: - if not await utils_mypayment.can_user_manage_events( + await utils_mypayment.ensure_user_can_manage_events( user_id=user_id, store_id=store_id, db=db, - ): - raise HTTPException( - status_code=403, - detail="User is not authorized to manage store events", - ) + ) return await cruds_tickets.get_events_by_store_id( store_id=store_id, diff --git a/tests/core/test_tickets.py b/tests/core/test_tickets.py index 3abb0df460..eeb2bee3bd 100644 --- a/tests/core/test_tickets.py +++ b/tests/core/test_tickets.py @@ -3,8 +3,10 @@ import pytest_asyncio from fastapi.testclient import TestClient +from pytest_mock import MockerFixture from app.core.associations.models_associations import CoreAssociation +from app.core.feed import models_feed, types_feed from app.core.groups.groups_type import GroupType from app.core.memberships import models_memberships from app.core.mypayment import models_mypayment @@ -55,6 +57,8 @@ ticket_for_user_with_answer: models_tickets.Checkout +event_linked_to_feed: models_tickets.TicketEvent + @pytest_asyncio.fixture(scope="module", autouse=True) async def init_objects() -> None: @@ -340,6 +344,36 @@ async def init_objects() -> None: ) await add_object_to_db(ticket_for_user_with_answer) + global event_linked_to_feed + event_linked_to_feed = models_tickets.TicketEvent( + id=uuid.uuid4(), + store_id=store.id, + name="Test Event Linked to Feed", + open_datetime=datetime.now(tz=UTC) - timedelta(days=1), + close_datetime=datetime.now(tz=UTC) + timedelta(days=1), + quota=10, + disabled=False, + sessions=[], + categories=[], + questions=[], + ) + await add_object_to_db(event_linked_to_feed) + feed = models_feed.News( + id=uuid.uuid4(), + title="Test Feed News", + module="tickets", + module_object_id=event_linked_to_feed.id, + start=datetime.now(tz=UTC) - timedelta(days=1), + end=datetime.now(tz=UTC) + timedelta(days=1), + entity="Test Entity", + location="Test Location", + action_start=datetime.now(tz=UTC) - timedelta(days=1), + image_directory="test_directory", + image_id=uuid.uuid4(), + status=types_feed.NewsStatus.PUBLISHED, + ) + await add_object_to_db(feed) + async def test_payment_callback(client: TestClient): async with get_TestingSessionLocal()() as db: @@ -1011,6 +1045,106 @@ def test_get_user_tickets(client: TestClient): assert len(ticket["answers"]) > 0 +# ticket_request_change_over + + +async def test_ticket_request_change_over_for_non_existing_ticket(client: TestClient): + response = client.post( + "/tickets/user/me/tickets/change-over/request", + headers={"Authorization": f"Bearer {user_token}"}, + json={ + "ticket_id": str(uuid.uuid4()), + "email": "test@test.fr", + }, + ) + assert response.status_code == 404 + assert response.json()["detail"] == "Ticket not found" + + +async def test_ticket_request_change_over_for_ticket_from_different_user( + client: TestClient, +): + response = client.post( + "/tickets/user/me/tickets/change-over/request", + headers={"Authorization": f"Bearer {user_token}"}, + json={ + "ticket_id": str(ticket_sold_out_event.id), + "email": "test@test.fr", + }, + ) + assert response.status_code == 403 + assert response.json()["detail"] == "User is not the owner of the ticket" + + +async def test_ticket_request_change_over_for_ticket_for_non_existing_user_email( + client: TestClient, +): + response = client.post( + "/tickets/user/me/tickets/change-over/request", + headers={"Authorization": f"Bearer {user_token}"}, + json={ + "ticket_id": str(ticket_for_user_with_answer.id), + "email": "non-existing@test.fr", + }, + ) + assert response.status_code == 204 + + +async def test_ticket_request_change_over( + client: TestClient, + mocker: MockerFixture, +): + ticket_to_transfer = models_tickets.Checkout( + id=uuid.uuid4(), + category_id=event_category.id, + session_id=event_session.id, + event_id=global_event.id, + user_id=user.id, + price=10, + scanned=False, + paid=True, + expiration=datetime.now(tz=UTC) + timedelta(hours=1), + answers=[], + ) + await add_object_to_db(ticket_to_transfer) + + generate_token_patch = mocker.patch( + "app.core.tickets.endpoints_tickets.security.generate_token", + return_value="token", + ) + + response = client.post( + "/tickets/user/me/tickets/change-over/request", + headers={"Authorization": f"Bearer {user_token}"}, + json={ + "ticket_id": str(ticket_to_transfer.id), + "email": seller_can_manage_event_user.email, + }, + ) + assert response.status_code == 204 + generate_token_patch.assert_called() + + response = client.get( + "/tickets/user/me/tickets/change-over/accept?token=token", + follow_redirects=False, + ) + + assert response.status_code == 307 + assert "message?type=ticket_change_over_success" in response.headers["location"] + + +# ticket_accept_change_over + + +def test_ticket_accept_change_over_with_invalid_token(client: TestClient): + response = client.get( + "/tickets/user/me/tickets/change-over/accept?token=invalid_token", + follow_redirects=False, + ) + assert response.status_code == 307 + assert "message?type=ticket_change_over_invalid" in response.headers["location"] + + # get_event_admin @@ -1029,7 +1163,9 @@ def test_get_event_admin_as_non_authorised_seller(client: TestClient): headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == 403 - assert response.json()["detail"] == "User is not authorized to manage store events" + assert ( + response.json()["detail"] == "User is not authorized to manage store's events" + ) def test_get_event_admin(client: TestClient): @@ -1065,7 +1201,9 @@ def test_create_event_as_non_authorised_seller(client: TestClient): }, ) assert response.status_code == 403 - assert response.json()["detail"] == "User is not authorized to manage store events" + assert ( + response.json()["detail"] == "User is not authorized to manage store's events" + ) def test_create_event_without_sessions(client: TestClient): @@ -1222,6 +1360,70 @@ def test_update_event(client: TestClient): assert response.status_code == 204 +def test_update_event_disable(client: TestClient): + create_response = client.post( + "/tickets/admin/events/", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + json={ + "store_id": str(store.id), + "name": "Test Event To Disable", + "open_datetime": (datetime.now(tz=UTC) - timedelta(days=1)).isoformat(), + "close_datetime": (datetime.now(tz=UTC) + timedelta(days=2)).isoformat(), + "quota": 10, + "sessions": [ + { + "name": "Test Session", + "start_datetime": ( + datetime.now(tz=UTC) + timedelta(days=1) + ).isoformat(), + "quota": 10, + }, + ], + "categories": [ + { + "name": "Test Category", + "price": 1000, + "quota": 10, + "required_membership": None, + }, + ], + "questions": [], + }, + ) + assert create_response.status_code == 201 + event_id = create_response.json()["id"] + + response = client.patch( + f"/tickets/admin/events/{event_id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + json={ + "disabled": True, + }, + ) + assert response.status_code == 204 + + admin_response = client.get( + f"/tickets/admin/events/{event_id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert admin_response.status_code == 200 + assert admin_response.json()["disabled"] is True + + public_response = client.get( + f"/tickets/events/{event_id}", + headers={"Authorization": f"Bearer {user_token}"}, + ) + assert public_response.status_code == 400 + assert public_response.json()["detail"] == "Event is disabled" + + open_events_response = client.get( + "/tickets/events", + headers={"Authorization": f"Bearer {user_token}"}, + ) + assert open_events_response.status_code == 200 + assert event_id not in {event["id"] for event in open_events_response.json()} + + # create_session @@ -1313,20 +1515,6 @@ def test_update_session_with_non_existing_session(client: TestClient): assert response.json()["detail"] == "Session not found" -def test_update_session_with_existing_tickets(client: TestClient): - response = client.patch( - f"/tickets/admin/events/{global_event.id}/sessions/{event_session.id}", - headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, - json={ - "name": "Updated Test Session", - }, - ) - assert response.status_code == 400 - assert ( - response.json()["detail"] == "Cannot update session with checkouts or tickets" - ) - - async def test_update_session(client: TestClient): session_without_tickets = models_tickets.EventSession( id=uuid.uuid4(), @@ -1334,7 +1522,7 @@ async def test_update_session(client: TestClient): name="Test Session without tickets", start_datetime=datetime.now(tz=UTC) - timedelta(days=1), quota=None, - disabled=False, + disabled=True, ) await add_object_to_db(session_without_tickets) response = client.patch( @@ -1342,6 +1530,7 @@ async def test_update_session(client: TestClient): headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, json={ "name": "Updated Test Session", + "disabled": True, }, ) assert response.status_code == 204 @@ -1418,6 +1607,62 @@ def test_create_category(client: TestClient): assert category["quota"] == 10 +# create_question + + +def test_create_question_with_non_existing_event(client: TestClient): + response = client.post( + f"/tickets/admin/events/{uuid.uuid4()}/questions/", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + json={ + "question": "Test Question", + "answer_type": "text", + "price": 100, + "required": False, + }, + ) + assert response.status_code == 404 + assert response.json()["detail"] == "Event not found" + + +def test_create_question_as_non_authorised_seller(client: TestClient): + response = client.post( + f"/tickets/admin/events/{global_event.id}/questions/", + headers={"Authorization": f"Bearer {user_token}"}, + json={ + "question": "Test Question", + "answer_type": "text", + "price": 100, + "required": False, + }, + ) + assert response.status_code == 403 + assert ( + response.json()["detail"] == "User is not authorized to manage store's events" + ) + + +def test_create_question(client: TestClient): + response = client.post( + f"/tickets/admin/events/{global_event.id}/questions/", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + json={ + "question": "New Test Question", + "answer_type": "text", + "price": 100, + "required": True, + }, + ) + assert response.status_code == 201 + question = response.json() + assert question["question"] == "New Test Question" + assert question["answer_type"] == "text" + assert question["price"] == 100 + assert question["required"] is True + assert question["disabled"] is False + assert question["event_id"] == str(global_event.id) + + # update_category @@ -1459,17 +1704,19 @@ def test_update_category_with_non_existing_category(client: TestClient): assert response.json()["detail"] == "Category not found" -def test_update_category_with_existing_tickets(client: TestClient): +def test_update_category_price_with_existing_tickets(client: TestClient): response = client.patch( f"/tickets/admin/events/{global_event.id}/categories/{event_category.id}", headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, json={ "name": "Updated Test Category", + "price": 2000, }, ) assert response.status_code == 400 assert ( - response.json()["detail"] == "Cannot update category with checkouts or tickets" + response.json()["detail"] + == "Cannot update category price or required_membership with checkouts or tickets" ) @@ -1505,6 +1752,7 @@ async def test_update_category(client: TestClient): headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, json={ "name": "Updated Test Category", + "disabled": True, }, ) assert response.status_code == 204 @@ -1551,16 +1799,73 @@ def test_update_question_with_non_existing_question(client: TestClient): assert response.json()["detail"] == "Question not found" -async def test_update_question_with_answer(client: TestClient): +async def test_update_question_answer_type_with_answer(client: TestClient): response = client.patch( f"/tickets/admin/events/{global_event.id}/questions/{global_event_optionnal_question_id}", headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, json={ "question": "Updated Test Question", + "answer_type": "number", }, ) assert response.status_code == 400 - assert response.json()["detail"] == "Cannot update question with answers" + assert ( + response.json()["detail"] + == "Cannot update answer_type or price for question with answers" + ) + + +async def test_update_question_price_with_answer(client: TestClient): + response = client.patch( + f"/tickets/admin/events/{global_event.id}/questions/{global_event_optionnal_question_id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + json={ + "question": "Updated Test Question", + "price": 100, + }, + ) + assert response.status_code == 400 + assert ( + response.json()["detail"] + == "Cannot update answer_type or price for question with answers" + ) + + +def test_update_question_disable_with_answers(client: TestClient): + response = client.patch( + f"/tickets/admin/events/{global_event.id}/questions/{global_event_optionnal_question_id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + json={ + "disabled": True, + "question": "Updated Test Question", + }, + ) + assert response.status_code == 204 + + checkout_response = client.post( + f"/tickets/events/{global_event.id}/checkout", + headers={"Authorization": f"Bearer {user_token}"}, + json={ + "category_id": str(free_event_category.id), + "session_id": str(event_session.id), + "answers": [ + { + "question_id": str(global_event_optionnal_question_id), + "answer": { + "answer_type": "text", + "answer": "Test Answer", + }, + }, + ], + "mypayment_request_method": "transfer_request", + "mypayment_transfer_redirect_url": "http://localhost:3000/payment_callback", + }, + ) + assert checkout_response.status_code == 400 + assert ( + checkout_response.json()["detail"] + == f"Question with id {global_event_optionnal_question_id} is disabled" + ) async def test_update_question(client: TestClient): @@ -1584,6 +1889,213 @@ async def test_update_question(client: TestClient): assert response.status_code == 204 +# delete_event + + +def test_delete_event_not_found(client: TestClient): + response = client.delete( + f"/tickets/admin/events/{uuid.uuid4()}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert response.status_code == 404 + assert response.json()["detail"] == "Event not found" + + +def test_delete_event_as_non_authorised_seller(client: TestClient): + response = client.delete( + f"/tickets/admin/events/{global_event.id}", + headers={"Authorization": f"Bearer {user_token}"}, + ) + assert response.status_code == 403 + assert ( + response.json()["detail"] == "User is not authorized to manage store's events" + ) + + +def test_delete_event_with_checkouts_or_tickets(client: TestClient): + response = client.delete( + f"/tickets/admin/events/{global_event.id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert response.status_code == 400 + assert response.json()["detail"] == "Cannot delete event with checkouts or tickets" + + +def test_delete_event_linked_to_feed(client: TestClient): + response = client.delete( + f"/tickets/admin/events/{event_linked_to_feed.id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert response.status_code == 400 + assert response.json()["detail"] == "Cannot delete event linked to the feed" + + +def test_delete_event(client: TestClient): + create_response = client.post( + "/tickets/admin/events/", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + json={ + "store_id": str(store.id), + "name": "Test Event To Delete", + "open_datetime": (datetime.now(tz=UTC) + timedelta(days=1)).isoformat(), + "close_datetime": (datetime.now(tz=UTC) + timedelta(days=2)).isoformat(), + "quota": 10, + "sessions": [ + { + "name": "Test Session", + "start_datetime": ( + datetime.now(tz=UTC) + timedelta(days=1) + ).isoformat(), + "quota": 10, + }, + ], + "categories": [ + { + "name": "Test Category", + "price": 1000, + "quota": 10, + "required_membership": None, + }, + ], + "questions": [], + }, + ) + assert create_response.status_code == 201 + event_id = create_response.json()["id"] + + response = client.delete( + f"/tickets/admin/events/{event_id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert response.status_code == 204 + + admin_response = client.get( + f"/tickets/admin/events/{event_id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert admin_response.status_code == 404 + + +# delete_session + + +def test_delete_session_with_checkouts_or_tickets(client: TestClient): + response = client.delete( + f"/tickets/admin/events/{global_event.id}/sessions/{event_session.id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert response.status_code == 400 + assert ( + response.json()["detail"] == "Cannot delete session with checkouts or tickets" + ) + + +async def test_delete_session(client: TestClient): + session_without_tickets = models_tickets.EventSession( + id=uuid.uuid4(), + event_id=global_event.id, + name="Test Session to delete", + start_datetime=datetime.now(tz=UTC) - timedelta(days=1), + quota=None, + disabled=False, + ) + await add_object_to_db(session_without_tickets) + + response = client.delete( + f"/tickets/admin/events/{global_event.id}/sessions/{session_without_tickets.id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert response.status_code == 204 + + admin_response = client.get( + f"/tickets/admin/events/{global_event.id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert admin_response.status_code == 200 + session_ids = {session["id"] for session in admin_response.json()["sessions"]} + assert str(session_without_tickets.id) not in session_ids + + +# delete_category + + +def test_delete_category_with_checkouts_or_tickets(client: TestClient): + response = client.delete( + f"/tickets/admin/events/{global_event.id}/categories/{event_category.id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert response.status_code == 400 + assert ( + response.json()["detail"] == "Cannot delete category with checkouts or tickets" + ) + + +async def test_delete_category(client: TestClient): + category_without_tickets = models_tickets.Category( + id=uuid.uuid4(), + event_id=global_event.id, + name="Test Category to delete", + quota=None, + disabled=False, + price=1000, + required_membership=None, + ) + await add_object_to_db(category_without_tickets) + + response = client.delete( + f"/tickets/admin/events/{global_event.id}/categories/{category_without_tickets.id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert response.status_code == 204 + + admin_response = client.get( + f"/tickets/admin/events/{global_event.id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert admin_response.status_code == 200 + category_ids = {category["id"] for category in admin_response.json()["categories"]} + assert str(category_without_tickets.id) not in category_ids + + +# delete_question + + +def test_delete_question_with_answers(client: TestClient): + response = client.delete( + f"/tickets/admin/events/{global_event.id}/questions/{global_event_optionnal_question_id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert response.status_code == 400 + assert response.json()["detail"] == "Cannot delete question with answers" + + +async def test_delete_question(client: TestClient): + question_without_answers = models_tickets.Question( + id=uuid.uuid4(), + event_id=global_event.id, + question="Test Question to delete", + answer_type=AnswerType.TEXT, + price=None, + required=False, + disabled=False, + ) + await add_object_to_db(question_without_answers) + + response = client.delete( + f"/tickets/admin/events/{global_event.id}/questions/{question_without_answers.id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert response.status_code == 204 + + admin_response = client.get( + f"/tickets/admin/events/{global_event.id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + ) + assert admin_response.status_code == 200 + question_ids = {question["id"] for question in admin_response.json()["questions"]} + assert str(question_without_answers.id) not in question_ids + + # get_event_tickets @@ -1602,7 +2114,9 @@ def test_get_event_tickets_as_non_authorised_seller(client: TestClient): headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == 403 - assert response.json()["detail"] == "User is not authorized to manage store events" + assert ( + response.json()["detail"] == "User is not authorized to manage store's events" + ) def test_get_event_tickets(client: TestClient): @@ -1634,7 +2148,9 @@ def test_get_event_tickets_csv_as_non_authorised_seller(client: TestClient): headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == 403 - assert response.json()["detail"] == "User is not authorized to manage store events" + assert ( + response.json()["detail"] == "User is not authorized to manage store's events" + ) def test_get_event_tickets_csv(client: TestClient): @@ -1663,7 +2179,9 @@ def test_check_ticket_as_non_authorised_seller(client: TestClient): headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == 403 - assert response.json()["detail"] == "User is not authorized to manage store events" + assert ( + response.json()["detail"] == "User is not authorized to manage store's events" + ) def test_check_ticket(client: TestClient): @@ -1695,7 +2213,9 @@ def test_scan_ticket_as_non_authorised_seller(client: TestClient): headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == 403 - assert response.json()["detail"] == "User is not authorized to manage store events" + assert ( + response.json()["detail"] == "User is not authorized to manage store's events" + ) def test_scan_ticket(client: TestClient): @@ -1758,7 +2278,9 @@ def test_get_events_by_association_as_non_authorised_seller(client: TestClient): headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == 403 - assert response.json()["detail"] == "User is not authorized to manage store events" + assert ( + response.json()["detail"] == "User is not authorized to manage store's events" + ) def test_get_events_by_association(client: TestClient):