From 4f8ed610aa21e629928202fd1b803184442ed681 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:16:53 +0000 Subject: [PATCH 01/24] Ticket change over # Conflicts: # app/core/tickets/endpoints_tickets.py --- app/core/tickets/cruds_tickets.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/core/tickets/cruds_tickets.py b/app/core/tickets/cruds_tickets.py index 7bf090e8f9..cfdd7a1dbb 100644 --- a/app/core/tickets/cruds_tickets.py +++ b/app/core/tickets/cruds_tickets.py @@ -581,6 +581,28 @@ async def get_paid_tickets_by_event_id( ] +async def get_ticket_simple_by_id( + ticket_id: UUID, + db: AsyncSession, +) -> schemas_tickets.TicketSimple | None: + result = await db.execute( + select(models_tickets.Checkout).where(models_tickets.Checkout.id == ticket_id), + ) + ticket = result.scalars().first() + if ticket is None: + return None + + return schemas_tickets.TicketSimple( + id=ticket.id, + category_id=ticket.category_id, + session_id=ticket.session_id, + event_id=ticket.event_id, + scanned=ticket.scanned, + user_id=ticket.user_id, + price=ticket.price, + ) + + async def get_ticket_by_id( ticket_id: UUID, db: AsyncSession, From e454a56749f51c9d4b0d64e99b7e617635155041 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Fri, 1 May 2026 15:10:53 +0200 Subject: [PATCH 02/24] Remove unused and fix --- app/core/tickets/cruds_tickets.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/app/core/tickets/cruds_tickets.py b/app/core/tickets/cruds_tickets.py index cfdd7a1dbb..7bf090e8f9 100644 --- a/app/core/tickets/cruds_tickets.py +++ b/app/core/tickets/cruds_tickets.py @@ -581,28 +581,6 @@ async def get_paid_tickets_by_event_id( ] -async def get_ticket_simple_by_id( - ticket_id: UUID, - db: AsyncSession, -) -> schemas_tickets.TicketSimple | None: - result = await db.execute( - select(models_tickets.Checkout).where(models_tickets.Checkout.id == ticket_id), - ) - ticket = result.scalars().first() - if ticket is None: - return None - - return schemas_tickets.TicketSimple( - id=ticket.id, - category_id=ticket.category_id, - session_id=ticket.session_id, - event_id=ticket.event_id, - scanned=ticket.scanned, - user_id=ticket.user_id, - price=ticket.price, - ) - - async def get_ticket_by_id( ticket_id: UUID, db: AsyncSession, From 8a72eddac6c66de3b152dc31aa820dbf5d89230d Mon Sep 17 00:00:00 2001 From: Foucauld Bellanger <63885990+Foukki@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:30:52 +0200 Subject: [PATCH 03/24] Disable event --- app/core/tickets/schemas_tickets.py | 1 + tests/core/test_tickets.py | 64 +++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/app/core/tickets/schemas_tickets.py b/app/core/tickets/schemas_tickets.py index a0f1826597..b6bc27fdfb 100644 --- a/app/core/tickets/schemas_tickets.py +++ b/app/core/tickets/schemas_tickets.py @@ -192,6 +192,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/tests/core/test_tickets.py b/tests/core/test_tickets.py index 3abb0df460..01028642ae 100644 --- a/tests/core/test_tickets.py +++ b/tests/core/test_tickets.py @@ -1222,6 +1222,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 From c0089f80e7cb6a729235c309be8d7fe54f9bd292 Mon Sep 17 00:00:00 2001 From: Foucauld Bellanger <63885990+Foukki@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:32:16 +0200 Subject: [PATCH 04/24] authorize disabling session and category with sold ticket --- app/core/tickets/endpoints_tickets.py | 8 ++- app/core/tickets/utils_tickets.py | 5 ++ tests/core/test_tickets.py | 72 +++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/app/core/tickets/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index fbc512a27c..6f10da249e 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -716,7 +716,9 @@ async def update_session( session_id=session_id, db=db, ) - if nb_checkouts + nb_tickets > 0: + if nb_checkouts + nb_tickets > 0 and not utils_tickets.allows_update_with_checkouts_or_tickets( + session_update, + ): raise HTTPException( 400, "Cannot update session with checkouts or tickets", @@ -818,7 +820,9 @@ async def update_category( category_id=category_id, db=db, ) - if nb_checkouts + nb_tickets > 0: + if nb_checkouts + nb_tickets > 0 and not utils_tickets.allows_update_with_checkouts_or_tickets( + category_update, + ): raise HTTPException( 400, "Cannot update category with checkouts or tickets", diff --git a/app/core/tickets/utils_tickets.py b/app/core/tickets/utils_tickets.py index 91ad11a381..76221988f8 100644 --- a/app/core/tickets/utils_tickets.py +++ b/app/core/tickets/utils_tickets.py @@ -5,12 +5,17 @@ from fastapi import ( HTTPException, ) +from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from app.core.mypayment import utils_mypayment from app.core.tickets import cruds_tickets, schemas_tickets +def allows_update_with_checkouts_or_tickets(update: BaseModel) -> bool: + return set(update.model_dump(exclude_unset=True).keys()) <= {"disabled"} + + async def mypayment_callback_callback( checkout_id: UUID, db: AsyncSession, diff --git a/tests/core/test_tickets.py b/tests/core/test_tickets.py index 01028642ae..91377bfe0a 100644 --- a/tests/core/test_tickets.py +++ b/tests/core/test_tickets.py @@ -1391,6 +1391,42 @@ def test_update_session_with_existing_tickets(client: TestClient): ) +def test_update_session_disable_with_existing_tickets(client: TestClient): + response = client.patch( + f"/tickets/admin/events/{global_event.id}/sessions/{event_sold_out_session.id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + json={ + "disabled": True, + }, + ) + assert response.status_code == 204 + + public_response = client.get( + f"/tickets/events/{global_event.id}", + headers={"Authorization": f"Bearer {user_token}"}, + ) + assert public_response.status_code == 200 + session_ids = {session["id"] for session in public_response.json()["sessions"]} + assert str(event_sold_out_session.id) not in session_ids + + +def test_update_session_disable_with_existing_tickets_and_other_field( + client: TestClient, +): + response = client.patch( + f"/tickets/admin/events/{global_event.id}/sessions/{event_sold_out_session.id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + json={ + "disabled": True, + "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(), @@ -1537,6 +1573,42 @@ def test_update_category_with_existing_tickets(client: TestClient): ) +def test_update_category_disable_with_existing_tickets(client: TestClient): + response = client.patch( + f"/tickets/admin/events/{global_event.id}/categories/{event_sold_out_category.id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + json={ + "disabled": True, + }, + ) + assert response.status_code == 204 + + public_response = client.get( + f"/tickets/events/{global_event.id}", + headers={"Authorization": f"Bearer {user_token}"}, + ) + assert public_response.status_code == 200 + category_ids = {category["id"] for category in public_response.json()["categories"]} + assert str(event_sold_out_category.id) not in category_ids + + +def test_update_category_disable_with_existing_tickets_and_other_field( + client: TestClient, +): + response = client.patch( + f"/tickets/admin/events/{global_event.id}/categories/{event_sold_out_category.id}", + headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, + json={ + "disabled": True, + "name": "Updated Test Category", + }, + ) + assert response.status_code == 400 + assert ( + response.json()["detail"] == "Cannot update category with checkouts or tickets" + ) + + async def test_update_category_with_price_to_low(client: TestClient): response = client.patch( f"/tickets/admin/events/{global_event.id}/categories/{event_category.id}", From 6d48baa08d0dc65abc474d41a0f44bba75f24add Mon Sep 17 00:00:00 2001 From: Foucauld Bellanger <63885990+Foukki@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:33:11 +0200 Subject: [PATCH 05/24] being able to disable questions with answer --- app/core/tickets/endpoints_tickets.py | 4 ++- tests/core/test_tickets.py | 49 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/app/core/tickets/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index 6f10da249e..4e9f2a3cd3 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -873,7 +873,9 @@ async def update_question( question_id=question_id, db=db, ) - if nb_answers > 0: + if nb_answers > 0 and not utils_tickets.allows_update_with_checkouts_or_tickets( + question_update, + ): raise HTTPException( 400, "Cannot update question with answers", diff --git a/tests/core/test_tickets.py b/tests/core/test_tickets.py index 91377bfe0a..db238f30f1 100644 --- a/tests/core/test_tickets.py +++ b/tests/core/test_tickets.py @@ -1699,6 +1699,55 @@ async def test_update_question_with_answer(client: TestClient): assert response.json()["detail"] == "Cannot update question with answers" +def test_update_question_disable_with_answers_and_other_field(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 == 400 + assert response.json()["detail"] == "Cannot update 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, + }, + ) + 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): question_without_tickets = models_tickets.Question( id=uuid.uuid4(), From 1c29a4001e7ee67302fc8dbd2abc78b2905a9444 Mon Sep 17 00:00:00 2001 From: Foucauld Bellanger <63885990+Foukki@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:35:03 +0200 Subject: [PATCH 06/24] delete event --- app/core/tickets/cruds_tickets.py | 137 +++++++++++++++++ app/core/tickets/endpoints_tickets.py | 203 ++++++++++++++++++++++++++ app/core/tickets/utils_tickets.py | 4 + tests/core/test_tickets.py | 202 +++++++++++++++++++++++++ 4 files changed, 546 insertions(+) diff --git a/app/core/tickets/cruds_tickets.py b/app/core/tickets/cruds_tickets.py index 7bf090e8f9..3e1c96325d 100644 --- a/app/core/tickets/cruds_tickets.py +++ b/app/core/tickets/cruds_tickets.py @@ -926,3 +926,140 @@ async def get_ticket_change_over_invitation_by_token( new_user_id=invitation.new_user_id, token=invitation.token, ) + + +async def _delete_checkouts_by_ids( + checkout_ids: Sequence[UUID], + db: AsyncSession, +): + if len(checkout_ids) == 0: + return + + await db.execute( + delete(models_tickets.TicketChangeOverInvitation).where( + models_tickets.TicketChangeOverInvitation.ticket_id.in_(checkout_ids), + ), + ) + await db.execute( + delete(models_tickets.Answer).where( + models_tickets.Answer.checkout_id.in_(checkout_ids), + ), + ) + await db.execute( + delete(models_tickets.Checkout).where( + models_tickets.Checkout.id.in_(checkout_ids), + ), + ) + + +async def delete_expired_unpaid_checkouts_by_event_id( + event_id: UUID, + db: AsyncSession, +): + result = await db.execute( + select(models_tickets.Checkout.id).where( + models_tickets.Checkout.event_id == event_id, + not_(models_tickets.Checkout.paid), + models_tickets.Checkout.expiration < datetime.now(UTC), + ), + ) + await _delete_checkouts_by_ids(result.scalars().all(), db) + + +async def delete_expired_unpaid_checkouts_by_session_id( + session_id: UUID, + db: AsyncSession, +): + result = await db.execute( + select(models_tickets.Checkout.id).where( + models_tickets.Checkout.session_id == session_id, + not_(models_tickets.Checkout.paid), + models_tickets.Checkout.expiration < datetime.now(UTC), + ), + ) + await _delete_checkouts_by_ids(result.scalars().all(), db) + + +async def delete_expired_unpaid_checkouts_by_category_id( + category_id: UUID, + db: AsyncSession, +): + result = await db.execute( + select(models_tickets.Checkout.id).where( + models_tickets.Checkout.category_id == category_id, + not_(models_tickets.Checkout.paid), + models_tickets.Checkout.expiration < datetime.now(UTC), + ), + ) + await _delete_checkouts_by_ids(result.scalars().all(), db) + + +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, +): + await delete_expired_unpaid_checkouts_by_session_id( + session_id=session_id, + db=db, + ) + await db.execute( + delete(models_tickets.EventSession).where( + models_tickets.EventSession.id == session_id, + ), + ) + + +async def delete_category( + category_id: UUID, + db: AsyncSession, +): + await delete_expired_unpaid_checkouts_by_category_id( + category_id=category_id, + db=db, + ) + await db.execute( + delete(models_tickets.Category).where( + models_tickets.Category.id == category_id, + ), + ) + + +async def delete_event( + event_id: UUID, + db: AsyncSession, +): + await delete_expired_unpaid_checkouts_by_event_id( + event_id=event_id, + db=db, + ) + 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 4e9f2a3cd3..44c85e9948 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -627,6 +627,54 @@ 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") + + if not await utils_mypayment.can_user_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", + ) + + nb_checkouts = await cruds_tickets.count_valid_checkouts_by_event_id( + event_id=event_id, + db=db, + ) + nb_tickets = await cruds_tickets.count_tickets_by_event_id( + event_id=event_id, + db=db, + ) + if utils_tickets.has_checkouts_or_tickets(nb_checkouts, nb_tickets): + raise HTTPException( + 400, + "Cannot delete event with checkouts or tickets", + ) + + await cruds_tickets.delete_event( + event_id=event_id, + db=db, + ) + + @router.post( "/tickets/admin/events/{event_id}/sessions", response_model=schemas_tickets.SessionComplete, @@ -731,6 +779,59 @@ async def update_session( ) +@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") + + if not await utils_mypayment.can_user_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( + session_id=session_id, + db=db, + ) + nb_tickets = await cruds_tickets.count_tickets_by_session_id( + session_id=session_id, + db=db, + ) + if utils_tickets.has_checkouts_or_tickets(nb_checkouts, nb_tickets): + raise HTTPException( + 400, + "Cannot delete session with checkouts or tickets", + ) + + await cruds_tickets.delete_session( + session_id=session_id, + db=db, + ) + + @router.post( "/tickets/admin/events/{event_id}/categories", response_model=schemas_tickets.CategoryComplete, @@ -835,6 +936,59 @@ async def update_category( ) +@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") + + if not await utils_mypayment.can_user_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( + category_id=category_id, + db=db, + ) + nb_tickets = await cruds_tickets.count_tickets_by_category_id( + category_id=category_id, + db=db, + ) + if utils_tickets.has_checkouts_or_tickets(nb_checkouts, nb_tickets): + raise HTTPException( + 400, + "Cannot delete category with checkouts or tickets", + ) + + await cruds_tickets.delete_category( + category_id=category_id, + db=db, + ) + + @router.patch( "/tickets/admin/events/{event_id}/questions/{question_id}", status_code=204, @@ -888,6 +1042,55 @@ async def update_question( ) +@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") + + if not await utils_mypayment.can_user_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") + + nb_answers = await cruds_tickets.count_answers_by_question_id( + question_id=question_id, + db=db, + ) + if nb_answers > 0: + raise HTTPException( + 400, + "Cannot delete question with answers", + ) + + await cruds_tickets.delete_question( + question_id=question_id, + db=db, + ) + + @router.get( "/tickets/admin/events/{event_id}/tickets", response_model=list[schemas_tickets.Ticket], diff --git a/app/core/tickets/utils_tickets.py b/app/core/tickets/utils_tickets.py index 76221988f8..b382099433 100644 --- a/app/core/tickets/utils_tickets.py +++ b/app/core/tickets/utils_tickets.py @@ -16,6 +16,10 @@ def allows_update_with_checkouts_or_tickets(update: BaseModel) -> bool: return set(update.model_dump(exclude_unset=True).keys()) <= {"disabled"} +def has_checkouts_or_tickets(nb_checkouts: int, nb_tickets: int) -> bool: + return nb_checkouts + nb_tickets > 0 + + async def mypayment_callback_callback( checkout_id: UUID, db: AsyncSession, diff --git a/tests/core/test_tickets.py b/tests/core/test_tickets.py index db238f30f1..3c6c0a32e2 100644 --- a/tests/core/test_tickets.py +++ b/tests/core/test_tickets.py @@ -1769,6 +1769,208 @@ 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(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 From 828f735da91ce4935a27ea9a56fce7dd838c0167 Mon Sep 17 00:00:00 2001 From: Foucauld Bellanger <63885990+Foukki@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:35:51 +0200 Subject: [PATCH 07/24] add question after creation --- app/core/tickets/cruds_tickets.py | 18 +++++++++ app/core/tickets/endpoints_tickets.py | 53 +++++++++++++++++++++++++ tests/core/test_tickets.py | 56 +++++++++++++++++++++++++++ 3 files changed, 127 insertions(+) diff --git a/app/core/tickets/cruds_tickets.py b/app/core/tickets/cruds_tickets.py index 3e1c96325d..42519c5005 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, diff --git a/app/core/tickets/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index 44c85e9948..1cbb599123 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -989,6 +989,59 @@ async def delete_category( ) +@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") + + if not await utils_mypayment.can_user_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_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, diff --git a/tests/core/test_tickets.py b/tests/core/test_tickets.py index 3c6c0a32e2..4475810eac 100644 --- a/tests/core/test_tickets.py +++ b/tests/core/test_tickets.py @@ -1518,6 +1518,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 From becd0b5e5790ba583fab0d1f84a1fab6b8ebe33b Mon Sep 17 00:00:00 2001 From: Foucauld Bellanger <63885990+Foukki@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:45:14 +0200 Subject: [PATCH 08/24] lint and format --- app/core/tickets/endpoints_tickets.py | 14 ++++++++++---- tests/core/test_tickets.py | 8 ++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/core/tickets/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index 1cbb599123..7061f6cecd 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -764,8 +764,11 @@ async def update_session( session_id=session_id, db=db, ) - if nb_checkouts + nb_tickets > 0 and not utils_tickets.allows_update_with_checkouts_or_tickets( - session_update, + if ( + nb_checkouts + nb_tickets > 0 + and not utils_tickets.allows_update_with_checkouts_or_tickets( + session_update, + ) ): raise HTTPException( 400, @@ -921,8 +924,11 @@ async def update_category( category_id=category_id, db=db, ) - if nb_checkouts + nb_tickets > 0 and not utils_tickets.allows_update_with_checkouts_or_tickets( - category_update, + if ( + nb_checkouts + nb_tickets > 0 + and not utils_tickets.allows_update_with_checkouts_or_tickets( + category_update, + ) ): raise HTTPException( 400, diff --git a/tests/core/test_tickets.py b/tests/core/test_tickets.py index 4475810eac..7cf8fc4f3d 100644 --- a/tests/core/test_tickets.py +++ b/tests/core/test_tickets.py @@ -1980,9 +1980,7 @@ async def test_delete_category(client: TestClient): 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"] - } + category_ids = {category["id"] for category in admin_response.json()["categories"]} assert str(category_without_tickets.id) not in category_ids @@ -2021,9 +2019,7 @@ async def test_delete_question(client: TestClient): 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"] - } + question_ids = {question["id"] for question in admin_response.json()["questions"]} assert str(question_without_answers.id) not in question_ids From 841b4234560137779cb28f534dafea383f4d4225 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 14 Jun 2026 15:58:13 +0200 Subject: [PATCH 09/24] Fastapi 0.137.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f487459191..3183da41c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ broadcaster==0.3.1 # Working with websockets with multiple work calypsso-proximapp==2.8.0 email-validator==2.3.0 Faker==40.21.0 -fastapi[standard]==0.136.3 +fastapi[standard]==0.137.0 firebase-admin==7.4.0 # Firebase is used for push notification google-api-python-client==2.197.0 google-auth-oauthlib==1.4.0 From 9a637ca56ebb48b9b8c09c33f501b91cf3738d34 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:03:45 +0200 Subject: [PATCH 10/24] Revert "Fastapi 0.137.0" This reverts commit 9818f0b82e29620476c5c2d3adad272302a90b8a. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3183da41c8..f487459191 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ broadcaster==0.3.1 # Working with websockets with multiple work calypsso-proximapp==2.8.0 email-validator==2.3.0 Faker==40.21.0 -fastapi[standard]==0.137.0 +fastapi[standard]==0.136.3 firebase-admin==7.4.0 # Firebase is used for push notification google-api-python-client==2.197.0 google-auth-oauthlib==1.4.0 From a2a0333ccc204dc6a757b39ef930d6d6de78d5d3 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:47:00 +0200 Subject: [PATCH 11/24] Ticket change over tests --- app/core/tickets/endpoints_tickets.py | 21 ++++--- tests/core/test_tickets.py | 85 +++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 7 deletions(-) diff --git a/app/core/tickets/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index 7061f6cecd..3ae1e617dc 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,13 +433,18 @@ 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( diff --git a/tests/core/test_tickets.py b/tests/core/test_tickets.py index 7cf8fc4f3d..8beccc4d49 100644 --- a/tests/core/test_tickets.py +++ b/tests/core/test_tickets.py @@ -3,6 +3,7 @@ import pytest_asyncio from fastapi.testclient import TestClient +from pytest_mock import MockerFixture from app.core.associations.models_associations import CoreAssociation from app.core.groups.groups_type import GroupType @@ -1011,6 +1012,90 @@ 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) + + 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 + + mocker.patch( + "app.core.tickets.endpoints_tickets.security.generate_token", + return_value="token", + ) + + response = client.get( + "/tickets/user/me/tickets/change-over/accept?token=token", + ) + assert response.status_code == 200 + + # get_event_admin From c870f4d76ae8342fa5d40805ea851899b9583f58 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:19:26 +0200 Subject: [PATCH 12/24] Test ticket change over --- app/core/tickets/endpoints_tickets.py | 1 - tests/core/test_tickets.py | 26 +++++++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/core/tickets/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index 3ae1e617dc..0ea6007266 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -449,7 +449,6 @@ async def ticket_request_change_over( @router.get( "/tickets/user/me/tickets/change-over/accept", - status_code=200, ) async def ticket_accept_change_over( token: str, diff --git a/tests/core/test_tickets.py b/tests/core/test_tickets.py index 8beccc4d49..7bbadcf2b2 100644 --- a/tests/core/test_tickets.py +++ b/tests/core/test_tickets.py @@ -1075,6 +1075,11 @@ async def test_ticket_request_change_over( ) 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}"}, @@ -1084,16 +1089,27 @@ async def test_ticket_request_change_over( }, ) assert response.status_code == 204 + generate_token_patch.assert_called() - mocker.patch( - "app.core.tickets.endpoints_tickets.security.generate_token", - return_value="token", + 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=token", + "/tickets/user/me/tickets/change-over/accept?token=invalid_token", + follow_redirects=False, ) - assert response.status_code == 200 + assert response.status_code == 307 + assert "message?type=ticket_change_over_invalid" in response.headers["location"] # get_event_admin From f0790c6453d08c30ee7b2018ec17ef11c719712a Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:27:39 +0200 Subject: [PATCH 13/24] Remove allows_update_with_checkouts_or_tickets and has_checkouts_or_tickets --- app/core/tickets/endpoints_tickets.py | 24 ++++++------------------ app/core/tickets/utils_tickets.py | 9 --------- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/app/core/tickets/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index 0ea6007266..df787ee882 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -669,7 +669,7 @@ async def delete_event( event_id=event_id, db=db, ) - if utils_tickets.has_checkouts_or_tickets(nb_checkouts, nb_tickets): + if nb_checkouts + nb_tickets > 0: raise HTTPException( 400, "Cannot delete event with checkouts or tickets", @@ -770,12 +770,7 @@ async def update_session( session_id=session_id, db=db, ) - if ( - nb_checkouts + nb_tickets > 0 - and not utils_tickets.allows_update_with_checkouts_or_tickets( - session_update, - ) - ): + if nb_checkouts + nb_tickets > 0: raise HTTPException( 400, "Cannot update session with checkouts or tickets", @@ -829,7 +824,7 @@ async def delete_session( session_id=session_id, db=db, ) - if utils_tickets.has_checkouts_or_tickets(nb_checkouts, nb_tickets): + if nb_checkouts + nb_tickets > 0: raise HTTPException( 400, "Cannot delete session with checkouts or tickets", @@ -930,12 +925,7 @@ async def update_category( category_id=category_id, db=db, ) - if ( - nb_checkouts + nb_tickets > 0 - and not utils_tickets.allows_update_with_checkouts_or_tickets( - category_update, - ) - ): + if nb_checkouts + nb_tickets > 0: raise HTTPException( 400, "Cannot update category with checkouts or tickets", @@ -989,7 +979,7 @@ async def delete_category( category_id=category_id, db=db, ) - if utils_tickets.has_checkouts_or_tickets(nb_checkouts, nb_tickets): + if nb_checkouts + nb_tickets > 0: raise HTTPException( 400, "Cannot delete category with checkouts or tickets", @@ -1092,9 +1082,7 @@ async def update_question( question_id=question_id, db=db, ) - if nb_answers > 0 and not utils_tickets.allows_update_with_checkouts_or_tickets( - question_update, - ): + if nb_answers > 0: raise HTTPException( 400, "Cannot update question with answers", diff --git a/app/core/tickets/utils_tickets.py b/app/core/tickets/utils_tickets.py index b382099433..91ad11a381 100644 --- a/app/core/tickets/utils_tickets.py +++ b/app/core/tickets/utils_tickets.py @@ -5,21 +5,12 @@ from fastapi import ( HTTPException, ) -from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from app.core.mypayment import utils_mypayment from app.core.tickets import cruds_tickets, schemas_tickets -def allows_update_with_checkouts_or_tickets(update: BaseModel) -> bool: - return set(update.model_dump(exclude_unset=True).keys()) <= {"disabled"} - - -def has_checkouts_or_tickets(nb_checkouts: int, nb_tickets: int) -> bool: - return nb_checkouts + nb_tickets > 0 - - async def mypayment_callback_callback( checkout_id: UUID, db: AsyncSession, From 5fa6803c152755d3643a5f71864c81b60ba45453 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 27 Jun 2026 18:52:01 +0200 Subject: [PATCH 14/24] Only prevent from updating some fields when there are tickets, checkouts or answers --- app/core/tickets/endpoints_tickets.py | 60 +++++++------- app/core/tickets/schemas_tickets.py | 4 + tests/core/test_tickets.py | 108 ++++++-------------------- 3 files changed, 55 insertions(+), 117 deletions(-) diff --git a/app/core/tickets/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index df787ee882..31d2574fad 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -762,20 +762,6 @@ async def update_session( 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( - session_id=session_id, - db=db, - ) - nb_tickets = await cruds_tickets.count_tickets_by_session_id( - session_id=session_id, - db=db, - ) - if nb_checkouts + nb_tickets > 0: - raise HTTPException( - 400, - "Cannot update session with checkouts or tickets", - ) - await cruds_tickets.update_session( session_id=session_id, session_update=session_update, @@ -917,19 +903,22 @@ async def update_category( 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( - category_id=category_id, - db=db, - ) - nb_tickets = await cruds_tickets.count_tickets_by_category_id( - category_id=category_id, - db=db, - ) - if nb_checkouts + nb_tickets > 0: - raise HTTPException( - 400, - "Cannot update category with checkouts or tickets", + # 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 or "required_membership" in fields_to_update: + nb_checkouts = await cruds_tickets.count_valid_checkouts_by_category_id( + category_id=category_id, + db=db, + ) + nb_tickets = await cruds_tickets.count_tickets_by_category_id( + category_id=category_id, + db=db, ) + if nb_checkouts + nb_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, @@ -1078,15 +1067,18 @@ async def update_question( if question is None or question.event_id != event_id: raise HTTPException(404, "Question not found") - 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 question with answers", + # Some fields cannot be updated if the question has answers + fields_to_update = question_update.model_dump(exclude_unset=True).keys() + if "answer_type" in fields_to_update or "price" in fields_to_update: + 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, diff --git a/app/core/tickets/schemas_tickets.py b/app/core/tickets/schemas_tickets.py index b6bc27fdfb..4227de5328 100644 --- a/app/core/tickets/schemas_tickets.py +++ b/app/core/tickets/schemas_tickets.py @@ -92,8 +92,10 @@ 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 can not be updated if there are already checkouts or tickets for this category required_membership: UUID | None = None disabled: bool | None = None @@ -131,7 +133,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 diff --git a/tests/core/test_tickets.py b/tests/core/test_tickets.py index 7bbadcf2b2..1579b521e9 100644 --- a/tests/core/test_tickets.py +++ b/tests/core/test_tickets.py @@ -1478,56 +1478,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" - ) - - -def test_update_session_disable_with_existing_tickets(client: TestClient): - response = client.patch( - f"/tickets/admin/events/{global_event.id}/sessions/{event_sold_out_session.id}", - headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, - json={ - "disabled": True, - }, - ) - assert response.status_code == 204 - - public_response = client.get( - f"/tickets/events/{global_event.id}", - headers={"Authorization": f"Bearer {user_token}"}, - ) - assert public_response.status_code == 200 - session_ids = {session["id"] for session in public_response.json()["sessions"]} - assert str(event_sold_out_session.id) not in session_ids - - -def test_update_session_disable_with_existing_tickets_and_other_field( - client: TestClient, -): - response = client.patch( - f"/tickets/admin/events/{global_event.id}/sessions/{event_sold_out_session.id}", - headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, - json={ - "disabled": True, - "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(), @@ -1535,7 +1485,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( @@ -1543,6 +1493,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 @@ -1716,53 +1667,35 @@ 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" - ) - - -def test_update_category_disable_with_existing_tickets(client: TestClient): - response = client.patch( - f"/tickets/admin/events/{global_event.id}/categories/{event_sold_out_category.id}", - headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, - json={ - "disabled": True, - }, - ) - assert response.status_code == 204 - - public_response = client.get( - f"/tickets/events/{global_event.id}", - headers={"Authorization": f"Bearer {user_token}"}, + response.json()["detail"] + == "Cannot update category price or required_membership with checkouts or tickets" ) - assert public_response.status_code == 200 - category_ids = {category["id"] for category in public_response.json()["categories"]} - assert str(event_sold_out_category.id) not in category_ids -def test_update_category_disable_with_existing_tickets_and_other_field( - client: TestClient, -): +def test_update_category_required_membership_with_existing_tickets(client: TestClient): response = client.patch( - f"/tickets/admin/events/{global_event.id}/categories/{event_sold_out_category.id}", + f"/tickets/admin/events/{global_event.id}/categories/{event_category.id}", headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, json={ - "disabled": True, "name": "Updated Test Category", + "required_membership": str(uuid.uuid4()), }, ) 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" ) @@ -1798,6 +1731,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 @@ -1844,29 +1778,36 @@ 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" + ) -def test_update_question_disable_with_answers_and_other_field(client: TestClient): +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={ - "disabled": True, "question": "Updated Test Question", + "price": 100, }, ) 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" + ) def test_update_question_disable_with_answers(client: TestClient): @@ -1875,6 +1816,7 @@ def test_update_question_disable_with_answers(client: TestClient): headers={"Authorization": f"Bearer {seller_can_manage_event_user_token}"}, json={ "disabled": True, + "question": "Updated Test Question", }, ) assert response.status_code == 204 From 44deb963bc00230ec03e9398fbb85cc1849cd006 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 27 Jun 2026 19:10:42 +0200 Subject: [PATCH 15/24] Delete sessions, categories, questions and events --- app/core/feed/cruds_feed.py | 14 ++++ app/core/feed/utils_feed.py | 13 ++++ app/core/tickets/cruds_tickets.py | 99 +++++++-------------------- app/core/tickets/endpoints_tickets.py | 11 +++ tests/core/test_tickets.py | 42 ++++++++++++ 5 files changed, 104 insertions(+), 75 deletions(-) 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/tickets/cruds_tickets.py b/app/core/tickets/cruds_tickets.py index 42519c5005..dcdbf620c2 100644 --- a/app/core/tickets/cruds_tickets.py +++ b/app/core/tickets/cruds_tickets.py @@ -946,72 +946,6 @@ async def get_ticket_change_over_invitation_by_token( ) -async def _delete_checkouts_by_ids( - checkout_ids: Sequence[UUID], - db: AsyncSession, -): - if len(checkout_ids) == 0: - return - - await db.execute( - delete(models_tickets.TicketChangeOverInvitation).where( - models_tickets.TicketChangeOverInvitation.ticket_id.in_(checkout_ids), - ), - ) - await db.execute( - delete(models_tickets.Answer).where( - models_tickets.Answer.checkout_id.in_(checkout_ids), - ), - ) - await db.execute( - delete(models_tickets.Checkout).where( - models_tickets.Checkout.id.in_(checkout_ids), - ), - ) - - -async def delete_expired_unpaid_checkouts_by_event_id( - event_id: UUID, - db: AsyncSession, -): - result = await db.execute( - select(models_tickets.Checkout.id).where( - models_tickets.Checkout.event_id == event_id, - not_(models_tickets.Checkout.paid), - models_tickets.Checkout.expiration < datetime.now(UTC), - ), - ) - await _delete_checkouts_by_ids(result.scalars().all(), db) - - -async def delete_expired_unpaid_checkouts_by_session_id( - session_id: UUID, - db: AsyncSession, -): - result = await db.execute( - select(models_tickets.Checkout.id).where( - models_tickets.Checkout.session_id == session_id, - not_(models_tickets.Checkout.paid), - models_tickets.Checkout.expiration < datetime.now(UTC), - ), - ) - await _delete_checkouts_by_ids(result.scalars().all(), db) - - -async def delete_expired_unpaid_checkouts_by_category_id( - category_id: UUID, - db: AsyncSession, -): - result = await db.execute( - select(models_tickets.Checkout.id).where( - models_tickets.Checkout.category_id == category_id, - not_(models_tickets.Checkout.paid), - models_tickets.Checkout.expiration < datetime.now(UTC), - ), - ) - await _delete_checkouts_by_ids(result.scalars().all(), db) - - async def delete_question( question_id: UUID, db: AsyncSession, @@ -1027,10 +961,15 @@ async def delete_session( session_id: UUID, db: AsyncSession, ): - await delete_expired_unpaid_checkouts_by_session_id( - session_id=session_id, - db=db, + # 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, @@ -1042,10 +981,15 @@ async def delete_category( category_id: UUID, db: AsyncSession, ): - await delete_expired_unpaid_checkouts_by_category_id( - category_id=category_id, - db=db, + # 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, @@ -1057,10 +1001,15 @@ async def delete_event( event_id: UUID, db: AsyncSession, ): - await delete_expired_unpaid_checkouts_by_event_id( - event_id=event_id, - db=db, + # 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, diff --git a/app/core/tickets/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index 31d2574fad..0ed9c226e5 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -675,6 +675,17 @@ async def delete_event( "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, diff --git a/tests/core/test_tickets.py b/tests/core/test_tickets.py index 1579b521e9..cb052aac21 100644 --- a/tests/core/test_tickets.py +++ b/tests/core/test_tickets.py @@ -6,6 +6,7 @@ from pytest_mock import MockerFixture from app.core.associations.models_associations import CoreAssociation +from app.core.feed import models_feed from app.core.groups.groups_type import GroupType from app.core.memberships import models_memberships from app.core.mypayment import models_mypayment @@ -56,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: @@ -341,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=models_feed.NewsStatus.PUBLISHED, + ) + await add_object_to_db(feed) + async def test_payment_callback(client: TestClient): async with get_TestingSessionLocal()() as db: @@ -1900,6 +1933,15 @@ def test_delete_event_with_checkouts_or_tickets(client: TestClient): 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/", From cb3a919600e81955e5aa6800235cc06912ddf453 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 28 Jun 2026 10:57:23 +0200 Subject: [PATCH 16/24] Fix types_feed import --- tests/core/test_tickets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/test_tickets.py b/tests/core/test_tickets.py index cb052aac21..2714edbd35 100644 --- a/tests/core/test_tickets.py +++ b/tests/core/test_tickets.py @@ -6,7 +6,7 @@ from pytest_mock import MockerFixture from app.core.associations.models_associations import CoreAssociation -from app.core.feed import models_feed +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 @@ -370,7 +370,7 @@ async def init_objects() -> None: action_start=datetime.now(tz=UTC) - timedelta(days=1), image_directory="test_directory", image_id=uuid.uuid4(), - status=models_feed.NewsStatus.PUBLISHED, + status=types_feed.NewsStatus.PUBLISHED, ) await add_object_to_db(feed) From 9e79e563e21a309b6104da59a852eb57924c415f Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 28 Jun 2026 11:02:18 +0200 Subject: [PATCH 17/24] Allow to patch required_membership --- app/core/tickets/endpoints_tickets.py | 2 +- app/core/tickets/schemas_tickets.py | 1 - tests/core/test_tickets.py | 16 ---------------- 3 files changed, 1 insertion(+), 18 deletions(-) diff --git a/app/core/tickets/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index 0ed9c226e5..cf29e393b6 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -916,7 +916,7 @@ async def update_category( # 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 or "required_membership" in fields_to_update: + if "price" in fields_to_update: nb_checkouts = await cruds_tickets.count_valid_checkouts_by_category_id( category_id=category_id, db=db, diff --git a/app/core/tickets/schemas_tickets.py b/app/core/tickets/schemas_tickets.py index 4227de5328..1835818720 100644 --- a/app/core/tickets/schemas_tickets.py +++ b/app/core/tickets/schemas_tickets.py @@ -95,7 +95,6 @@ class CategoryUpdate(BaseModel): # 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 can not be updated if there are already checkouts or tickets for this category required_membership: UUID | None = None disabled: bool | None = None diff --git a/tests/core/test_tickets.py b/tests/core/test_tickets.py index 2714edbd35..0f1b4c66a4 100644 --- a/tests/core/test_tickets.py +++ b/tests/core/test_tickets.py @@ -1716,22 +1716,6 @@ def test_update_category_price_with_existing_tickets(client: TestClient): ) -def test_update_category_required_membership_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", - "required_membership": str(uuid.uuid4()), - }, - ) - assert response.status_code == 400 - assert ( - response.json()["detail"] - == "Cannot update category price or required_membership with checkouts or tickets" - ) - - async def test_update_category_with_price_to_low(client: TestClient): response = client.patch( f"/tickets/admin/events/{global_event.id}/categories/{event_category.id}", From 02e397daa46edf001d513e88b07805b86c795f11 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 28 Jun 2026 17:10:16 +0200 Subject: [PATCH 18/24] count_valid_checkouts_and_tickets_by_event_id --- app/core/tickets/cruds_tickets.py | 20 ++++++++++ app/core/tickets/endpoints_tickets.py | 56 ++++++++++++--------------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/app/core/tickets/cruds_tickets.py b/app/core/tickets/cruds_tickets.py index dcdbf620c2..984b7808f5 100644 --- a/app/core/tickets/cruds_tickets.py +++ b/app/core/tickets/cruds_tickets.py @@ -744,6 +744,26 @@ async def count_valid_checkouts_by_event_id( return result.scalar() or 0 +async def count_valid_checkouts_and_tickets_by_event_id( + event_id: UUID, + db: AsyncSession, +) -> int: + """ + Count unpaid checkouts that are not expired and paid tickets + """ + result = await db.execute( + select(func.count()).where( + 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_category_id( category_id: UUID, db: AsyncSession, diff --git a/app/core/tickets/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index cf29e393b6..f3919bb634 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -661,15 +661,15 @@ async def delete_event( detail="User is not authorized to manage store's events", ) - nb_checkouts = await cruds_tickets.count_valid_checkouts_by_event_id( - event_id=event_id, - db=db, ) - nb_tickets = await cruds_tickets.count_tickets_by_event_id( - event_id=event_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 + nb_tickets > 0: + if nb_checkouts_and_tickets > 0: raise HTTPException( 400, "Cannot delete event with checkouts or tickets", @@ -813,15 +813,13 @@ async def delete_session( 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( - session_id=session_id, - db=db, - ) - nb_tickets = await cruds_tickets.count_tickets_by_session_id( - session_id=session_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 + nb_tickets > 0: + if nb_checkouts_and_tickets > 0: raise HTTPException( 400, "Cannot delete session with checkouts or tickets", @@ -917,15 +915,13 @@ async def update_category( # 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 = await cruds_tickets.count_valid_checkouts_by_category_id( - category_id=category_id, - db=db, - ) - nb_tickets = await cruds_tickets.count_tickets_by_category_id( - category_id=category_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 + nb_tickets > 0: + if nb_checkouts_and_tickets > 0: raise HTTPException( 400, "Cannot update category price or required_membership with checkouts or tickets", @@ -971,15 +967,13 @@ async def delete_category( 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( - category_id=category_id, - db=db, - ) - nb_tickets = await cruds_tickets.count_tickets_by_category_id( - category_id=category_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 + nb_tickets > 0: + if nb_checkouts_and_tickets > 0: raise HTTPException( 400, "Cannot delete category with checkouts or tickets", From 6cd86931f2dd404ae338e090231207800b1d41ce Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 28 Jun 2026 17:10:55 +0200 Subject: [PATCH 19/24] ensure_user_can_manage_events --- app/core/mypayment/utils_mypayment.py | 15 +++ app/core/tickets/endpoints_tickets.py | 136 +++++++------------------- app/core/tickets/utils_tickets.py | 8 +- 3 files changed, 50 insertions(+), 109 deletions(-) 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/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index f3919bb634..46805d1977 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -511,15 +511,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, @@ -544,15 +540,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( @@ -603,15 +595,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 @@ -651,16 +639,10 @@ async def delete_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", - ) - ) nb_checkouts_and_tickets = ( @@ -714,15 +696,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() @@ -759,15 +737,11 @@ 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: @@ -799,15 +773,11 @@ async def delete_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: @@ -853,15 +823,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() @@ -898,15 +864,11 @@ 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: @@ -953,15 +915,11 @@ async def delete_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: @@ -1007,15 +965,11 @@ async def create_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_id = uuid.uuid4() @@ -1058,15 +1012,11 @@ 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: @@ -1111,15 +1061,11 @@ async def delete_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: @@ -1162,15 +1108,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) @@ -1196,15 +1138,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() @@ -1309,15 +1247,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 @@ -1350,15 +1284,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/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, From 7d9c802556110cc79e223d655d12ce32f6852d37 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 28 Jun 2026 17:11:05 +0200 Subject: [PATCH 20/24] 307 status_code and any condition --- app/core/tickets/endpoints_tickets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/core/tickets/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index 46805d1977..33afe6d6f6 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -449,6 +449,7 @@ async def ticket_request_change_over( @router.get( "/tickets/user/me/tickets/change-over/accept", + status_code=307, ) async def ticket_accept_change_over( token: str, @@ -1024,7 +1025,7 @@ async def update_question( # Some fields cannot be updated if the question has answers fields_to_update = question_update.model_dump(exclude_unset=True).keys() - if "answer_type" in fields_to_update or "price" in fields_to_update: + 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, From 3fb343db247ef9ac0b84a454640a3af80e86209b Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 28 Jun 2026 17:14:31 +0200 Subject: [PATCH 21/24] Remove duplicated count_valid_checkouts_and_tickets_by_event_id definition --- app/core/tickets/cruds_tickets.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/app/core/tickets/cruds_tickets.py b/app/core/tickets/cruds_tickets.py index 984b7808f5..a804736bb9 100644 --- a/app/core/tickets/cruds_tickets.py +++ b/app/core/tickets/cruds_tickets.py @@ -800,26 +800,6 @@ async def count_valid_checkouts_by_session_id( return result.scalar() or 0 -async def count_valid_checkouts_and_tickets_by_event_id( - event_id: UUID, - db: AsyncSession, -) -> int: - """ - Count unpaid checkouts that are not expired and paid tickets - """ - result = await db.execute( - select(func.count()).where( - 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_and_tickets_by_category_id( category_id: UUID, db: AsyncSession, From b4c2c3f04e5697c99fda0c089811f1c3a4280e31 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:17:07 +0200 Subject: [PATCH 22/24] Fix tickets auth tests --- tests/core/test_tickets.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/core/test_tickets.py b/tests/core/test_tickets.py index 0f1b4c66a4..eeb2bee3bd 100644 --- a/tests/core/test_tickets.py +++ b/tests/core/test_tickets.py @@ -1163,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): @@ -1199,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): @@ -2110,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): @@ -2142,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): @@ -2171,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): @@ -2203,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): @@ -2266,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): From 5b0e03ccfb677ac323d81c0778fd51bdfad415f6 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:23:56 +0200 Subject: [PATCH 23/24] Fix deletion rules --- app/core/tickets/cruds_tickets.py | 40 +++++++++++++++++++++++++++ app/core/tickets/endpoints_tickets.py | 12 ++++---- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/app/core/tickets/cruds_tickets.py b/app/core/tickets/cruds_tickets.py index a804736bb9..62b9d083e5 100644 --- a/app/core/tickets/cruds_tickets.py +++ b/app/core/tickets/cruds_tickets.py @@ -764,6 +764,46 @@ async def count_valid_checkouts_and_tickets_by_event_id( return result.scalar() or 0 +async def count_valid_checkouts_and_tickets_by_session_id( + session_id: UUID, + db: AsyncSession, +) -> int: + """ + 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, + 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_category_id( + category_id: UUID, + db: AsyncSession, +) -> int: + """ + 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, + or_( + models_tickets.Checkout.paid, + models_tickets.Checkout.expiration >= datetime.now(UTC), + ), + ), + ) + + return result.scalar() or 0 + + async def count_valid_checkouts_by_category_id( category_id: UUID, db: AsyncSession, diff --git a/app/core/tickets/endpoints_tickets.py b/app/core/tickets/endpoints_tickets.py index 33afe6d6f6..2e89b0b9cc 100644 --- a/app/core/tickets/endpoints_tickets.py +++ b/app/core/tickets/endpoints_tickets.py @@ -785,8 +785,8 @@ async def delete_session( raise HTTPException(404, "Session not found") nb_checkouts_and_tickets = ( - await cruds_tickets.count_valid_checkouts_and_tickets_by_event_id( - event_id=event_id, + await cruds_tickets.count_valid_checkouts_and_tickets_by_session_id( + session_id=session_id, db=db, ) ) @@ -879,8 +879,8 @@ async def update_category( 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_event_id( - event_id=event_id, + await cruds_tickets.count_valid_checkouts_and_tickets_by_category_id( + category_id=category_id, db=db, ) ) @@ -927,8 +927,8 @@ async def delete_category( raise HTTPException(404, "Category not found") nb_checkouts_and_tickets = ( - await cruds_tickets.count_valid_checkouts_and_tickets_by_event_id( - event_id=event_id, + await cruds_tickets.count_valid_checkouts_and_tickets_by_category_id( + category_id=category_id, db=db, ) ) From 33a3bcfb7b4c06f7b95a5f8eb2ad9a396da4dd66 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:26:24 +0200 Subject: [PATCH 24/24] Remove duplicated definitions --- app/core/tickets/cruds_tickets.py | 40 ------------------------------- 1 file changed, 40 deletions(-) diff --git a/app/core/tickets/cruds_tickets.py b/app/core/tickets/cruds_tickets.py index 62b9d083e5..03efa80ac9 100644 --- a/app/core/tickets/cruds_tickets.py +++ b/app/core/tickets/cruds_tickets.py @@ -840,46 +840,6 @@ async def count_valid_checkouts_by_session_id( return result.scalar() or 0 -async def count_valid_checkouts_and_tickets_by_category_id( - category_id: UUID, - db: AsyncSession, -) -> int: - """ - 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, - 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_session_id( - session_id: UUID, - db: AsyncSession, -) -> int: - """ - 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, - or_( - models_tickets.Checkout.paid, - models_tickets.Checkout.expiration >= datetime.now(UTC), - ), - ), - ) - - return result.scalar() or 0 - - async def count_answers_by_question_id( question_id: UUID, db: AsyncSession,