From 27c2a5489ddf60ccb68d321cb0f6cfff1585611e Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 1 Mar 2026 11:28:23 +0100 Subject: [PATCH 01/18] Add CoreAssociation relationship to stores --- app/core/mypayment/endpoints_mypayment.py | 24 +++++++++++++++++++++++ app/core/mypayment/factory_mypayment.py | 4 +++- app/core/mypayment/models_mypayment.py | 4 ++++ app/core/mypayment/schemas_mypayment.py | 1 + app/core/mypayment/utils_mypayment.py | 1 + tests/core/test_mypayment.py | 7 +++++++ 6 files changed, 40 insertions(+), 1 deletion(-) diff --git a/app/core/mypayment/endpoints_mypayment.py b/app/core/mypayment/endpoints_mypayment.py index 405c64ef13..0f0ba1021d 100644 --- a/app/core/mypayment/endpoints_mypayment.py +++ b/app/core/mypayment/endpoints_mypayment.py @@ -20,6 +20,7 @@ from fastapi.responses import FileResponse, RedirectResponse from sqlalchemy.ext.asyncio import AsyncSession +from app.core.associations import cruds_associations from app.core.auth import schemas_auth from app.core.checkout import schemas_checkout from app.core.checkout.payment_tool import PaymentTool @@ -91,6 +92,7 @@ generate_pdf_from_template, get_core_data, get_file_from_data, + is_user_member_of_an_association, patch_identity_in_text, set_core_data, ) @@ -500,6 +502,7 @@ async def create_store( Stores name should be unique, as an user need to be able to identify a store by its name. **The user must be the manager for this structure** + **The user must be a member of the associated CoreAssociation** """ structure = await cruds_mypayment.get_structure_by_id( structure_id=structure_id, @@ -526,6 +529,24 @@ async def create_store( detail="Store with this name already exists in this structure", ) + association = await cruds_associations.get_association_by_id( + db=db, + association_id=store.association_id, + ) + if not association: + raise HTTPException( + status_code=404, + detail="Association not found", + ) + if not is_user_member_of_an_association( + user=user, + association=association, + ): + raise HTTPException( + status_code=403, + detail="You are not allowed to create stores for this association", + ) + # Create new wallet for store wallet_id = uuid.uuid4() await cruds_mypayment.create_wallet( @@ -541,6 +562,7 @@ async def create_store( structure_id=structure_id, wallet_id=wallet_id, creation=datetime.now(tz=UTC), + association_id=store.association_id, ) await cruds_mypayment.create_store( store=store_db, @@ -574,6 +596,7 @@ async def create_store( wallet_id=store_db.wallet_id, creation=store_db.creation, structure=structure, + association_id=store_db.association_id, ) @@ -845,6 +868,7 @@ async def get_user_stores( can_see_history=seller.can_see_history, can_cancel=seller.can_cancel, can_manage_sellers=seller.can_manage_sellers, + association_id=store.association_id, ), ) diff --git a/app/core/mypayment/factory_mypayment.py b/app/core/mypayment/factory_mypayment.py index dec2297102..f7aad4bdf4 100644 --- a/app/core/mypayment/factory_mypayment.py +++ b/app/core/mypayment/factory_mypayment.py @@ -5,6 +5,7 @@ from faker import Faker from sqlalchemy.ext.asyncio import AsyncSession +from app.core.associations.factory_associations import AssociationsFactory from app.core.mypayment import cruds_mypayment, models_mypayment, schemas_mypayment from app.core.mypayment.types_mypayment import WalletType from app.core.users.factory_users import CoreUsersFactory @@ -17,7 +18,7 @@ class MyPaymentFactory(Factory): - depends_on = [CoreUsersFactory] + depends_on = [CoreUsersFactory, AssociationsFactory] demo_structures_id: list[uuid.UUID] other_structures_id: list[uuid.UUID] @@ -88,6 +89,7 @@ async def create_other_structures_stores(cls, db: AsyncSession): name=faker.company(), creation=datetime.now(UTC), wallet_id=wallet_id, + association_id=AssociationsFactory.association_ids[0], ), db, ) diff --git a/app/core/mypayment/models_mypayment.py b/app/core/mypayment/models_mypayment.py index b6af28ce2f..12edb11655 100644 --- a/app/core/mypayment/models_mypayment.py +++ b/app/core/mypayment/models_mypayment.py @@ -170,6 +170,10 @@ class Store(Base): ) creation: Mapped[datetime] + association_id: Mapped[UUID] = mapped_column( + ForeignKey("core_association.id"), + ) + structure: Mapped[Structure] = relationship(init=False, lazy="joined") diff --git a/app/core/mypayment/schemas_mypayment.py b/app/core/mypayment/schemas_mypayment.py index 8825a457de..6da648856b 100644 --- a/app/core/mypayment/schemas_mypayment.py +++ b/app/core/mypayment/schemas_mypayment.py @@ -66,6 +66,7 @@ class StructureTranfert(BaseModel): class StoreBase(BaseModel): name: str + association_id: UUID class StoreSimple(StoreBase): diff --git a/app/core/mypayment/utils_mypayment.py b/app/core/mypayment/utils_mypayment.py index 01b613f86c..2cd133caba 100644 --- a/app/core/mypayment/utils_mypayment.py +++ b/app/core/mypayment/utils_mypayment.py @@ -231,6 +231,7 @@ def invoice_model_to_schema( structure_id=detail.store.structure_id, wallet_id=detail.store.wallet_id, creation=detail.store.creation, + association_id=detail.store.association_id, ), ) for detail in invoice.details diff --git a/tests/core/test_mypayment.py b/tests/core/test_mypayment.py index 9820d8a611..e73e0e9b7a 100644 --- a/tests/core/test_mypayment.py +++ b/tests/core/test_mypayment.py @@ -306,6 +306,7 @@ async def init_objects() -> None: name="Test Store", structure_id=structure.id, creation=datetime.now(UTC), + association_id=association_membership.id, ) await add_object_to_db(store) store2 = models_mypayment.Store( @@ -314,6 +315,7 @@ async def init_objects() -> None: name="Test Store 2", structure_id=structure2.id, creation=datetime.now(UTC), + association_id=association_membership.id, ) await add_object_to_db(store2) store3 = models_mypayment.Store( @@ -322,6 +324,7 @@ async def init_objects() -> None: name="Test Store 3", structure_id=structure2.id, creation=datetime.now(UTC), + association_id=association_membership.id, ) await add_object_to_db(store3) @@ -798,6 +801,7 @@ async def test_transfer_structure_manager_as_manager( wallet_id=new_wallet.id, name="Test Store Structure 2", structure_id=new_structure.id, + association_id=association_membership.id, ) await add_object_to_db(new_store) new_wallet2 = models_mypayment.Wallet( @@ -812,6 +816,7 @@ async def test_transfer_structure_manager_as_manager( wallet_id=new_wallet2.id, name="Test Store Structure 2 Where New Manager Already Seller", structure_id=new_structure.id, + association_id=association_membership.id, ) await add_object_to_db(new_store2_where_new_manager_already_seller) seller = models_mypayment.Seller( @@ -1241,6 +1246,7 @@ async def test_delete_store(client: TestClient): wallet_id=new_wallet.id, name="Test Store to Delete", structure_id=structure.id, + association_id=association_membership.id, ) await add_object_to_db(new_store) sellet = models_mypayment.Seller( @@ -1275,6 +1281,7 @@ async def test_update_store(client: TestClient): wallet_id=new_wallet.id, name="Test Store Update", structure_id=structure.id, + association_id=association_membership.id, ) await add_object_to_db(new_store) response = client.patch( From eb4ed55abb6132d1dac340148438c4250cf2f9f0 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 1 Mar 2026 11:50:47 +0100 Subject: [PATCH 02/18] Migration --- app/core/mypayment/models_mypayment.py | 2 +- .../versions/50-stores_coreassociations.py | 78 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 migrations/versions/50-stores_coreassociations.py diff --git a/app/core/mypayment/models_mypayment.py b/app/core/mypayment/models_mypayment.py index 12edb11655..b92494c297 100644 --- a/app/core/mypayment/models_mypayment.py +++ b/app/core/mypayment/models_mypayment.py @@ -171,7 +171,7 @@ class Store(Base): creation: Mapped[datetime] association_id: Mapped[UUID] = mapped_column( - ForeignKey("core_association.id"), + ForeignKey("associations_associations.id"), ) structure: Mapped[Structure] = relationship(init=False, lazy="joined") diff --git a/migrations/versions/50-stores_coreassociations.py b/migrations/versions/50-stores_coreassociations.py new file mode 100644 index 0000000000..2b730d8e06 --- /dev/null +++ b/migrations/versions/50-stores_coreassociations.py @@ -0,0 +1,78 @@ +"""empty message + +Create Date: 2026-03-01 11:41:22.994301 +""" + +import uuid +from collections.abc import Sequence +from typing import TYPE_CHECKING, Union + +if TYPE_CHECKING: + from pytest_alembic import MigrationContext + +import sqlalchemy as sa +from alembic import op + +from app.types.sqlalchemy import TZDateTime + +# revision identifiers, used by Alembic. +revision: str = "146db8dcb23e" +down_revision: str | None = "1ec573d854a1" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + store_table = sa.table( + "mypayment_store", + sa.column("id", sa.String), + ) + + conn = op.get_bind() + stores = conn.execute( + sa.select(store_table.c.id), + ).fetchall() + + if len(stores) > 0: + raise Exception( + "There are already stores in database, we cannot safely migrate to add association_id to store", + ) + + op.add_column( + "mypayment_store", + sa.Column( + "association_id", + sa.Uuid(), + nullable=False, + ), + ) + op.create_foreign_key( + None, + "mypayment_store", + "associations_associations", + ["association_id"], + ["id"], + ) + + +def downgrade() -> None: + op.drop_constraint( + "mypayment_store_association_id_fkey", + "mypayment_store", + type_="foreignkey", + ) + op.drop_column("mypayment_store", "association_id") + + +def pre_test_upgrade( + alembic_runner: "MigrationContext", + alembic_connection: sa.Connection, +) -> None: + pass + + +def test_upgrade( + alembic_runner: "MigrationContext", + alembic_connection: sa.Connection, +) -> None: + pass From 209614bded8fbbc620167c3ae9979914bde5b0c5 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 1 Mar 2026 11:51:09 +0100 Subject: [PATCH 03/18] Don't print all image in migration 49 Compress images --- migrations/versions/49-compress_images.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/migrations/versions/49-compress_images.py b/migrations/versions/49-compress_images.py index dc7ac39860..013d454f25 100644 --- a/migrations/versions/49-compress_images.py +++ b/migrations/versions/49-compress_images.py @@ -70,15 +70,14 @@ def upgrade() -> None: for data_folder, params in data_sources.items(): - print("__________________________________________") # noqa: T201 - print(f"Processing folder: {data_folder}") # noqa: T201 + print(f"\nCompressing images from folder: {data_folder}", end="") # noqa: T201 height = params.get("height") width = params.get("width") quality = params.get("quality", 85) fit = bool(params.get("fit", 0)) if Path("data/" + data_folder).exists(): for file_path in Path("data/" + data_folder).iterdir(): - print(" - ", file_path) # noqa: T201 + print(".", end="") # noqa: T201 if file_path.suffix in (".png", ".jpg", ".webp"): file_bytes = file_path.read_bytes() @@ -105,8 +104,11 @@ def upgrade() -> None: Path(f"data/{data_folder}/{file_path.stem}.webp").write_bytes(res) - # Delete the original file - Path(f"data/{data_folder}/{file_path.name}").unlink() + with Path(f"data/{data_folder}/{file_path.stem}.webp").open( + "wb", + ) as out_file: + out_file.write(res) + print() # noqa: T201 def downgrade() -> None: From 5e19dcb04ccadfd4923814b148614326b1778d10 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 1 Mar 2026 11:52:42 +0100 Subject: [PATCH 04/18] Lint --- migrations/versions/50-stores_coreassociations.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/migrations/versions/50-stores_coreassociations.py b/migrations/versions/50-stores_coreassociations.py index 2b730d8e06..4dc95642f9 100644 --- a/migrations/versions/50-stores_coreassociations.py +++ b/migrations/versions/50-stores_coreassociations.py @@ -3,9 +3,8 @@ Create Date: 2026-03-01 11:41:22.994301 """ -import uuid from collections.abc import Sequence -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING if TYPE_CHECKING: from pytest_alembic import MigrationContext @@ -13,8 +12,6 @@ import sqlalchemy as sa from alembic import op -from app.types.sqlalchemy import TZDateTime - # revision identifiers, used by Alembic. revision: str = "146db8dcb23e" down_revision: str | None = "1ec573d854a1" @@ -34,7 +31,7 @@ def upgrade() -> None: ).fetchall() if len(stores) > 0: - raise Exception( + raise Exception( # noqa: TRY002, TRY003 "There are already stores in database, we cannot safely migrate to add association_id to store", ) From 17b7f1624d930a97cf5bdd5b0c057d860b0f1c6d Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:07:33 +0100 Subject: [PATCH 05/18] Fix tests --- tests/core/test_mypayment.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/tests/core/test_mypayment.py b/tests/core/test_mypayment.py index e73e0e9b7a..4b6f7ffb11 100644 --- a/tests/core/test_mypayment.py +++ b/tests/core/test_mypayment.py @@ -11,6 +11,7 @@ from fastapi.testclient import TestClient from pytest_mock import MockerFixture +from app.core.associations import models_associations from app.core.groups import models_groups from app.core.groups.groups_type import GroupType from app.core.memberships import models_memberships @@ -62,6 +63,8 @@ ecl_user2_wallet_device: models_mypayment.WalletDevice ecl_user2_payment: models_mypayment.UserPayment +core_association: models_associations.CoreAssociation + association_membership: models_memberships.CoreAssociationMembership association_membership_user: models_memberships.CoreAssociationUserMembership structure: models_mypayment.Structure @@ -113,6 +116,13 @@ async def init_objects() -> None: admin_user = await create_user_with_groups(groups=[GroupType.admin]) admin_user_token = create_api_access_token(admin_user) + global core_association + core_association = models_associations.CoreAssociation( + id=uuid4(), + name="Association", + group_id=GroupType.admin, + ) + global association_membership association_membership = models_memberships.CoreAssociationMembership( id=uuid4(), @@ -306,7 +316,7 @@ async def init_objects() -> None: name="Test Store", structure_id=structure.id, creation=datetime.now(UTC), - association_id=association_membership.id, + association_id=core_association.id, ) await add_object_to_db(store) store2 = models_mypayment.Store( @@ -315,7 +325,7 @@ async def init_objects() -> None: name="Test Store 2", structure_id=structure2.id, creation=datetime.now(UTC), - association_id=association_membership.id, + association_id=core_association.id, ) await add_object_to_db(store2) store3 = models_mypayment.Store( @@ -324,7 +334,7 @@ async def init_objects() -> None: name="Test Store 3", structure_id=structure2.id, creation=datetime.now(UTC), - association_id=association_membership.id, + association_id=core_association.id, ) await add_object_to_db(store3) @@ -801,7 +811,7 @@ async def test_transfer_structure_manager_as_manager( wallet_id=new_wallet.id, name="Test Store Structure 2", structure_id=new_structure.id, - association_id=association_membership.id, + association_id=core_association.id, ) await add_object_to_db(new_store) new_wallet2 = models_mypayment.Wallet( @@ -816,7 +826,7 @@ async def test_transfer_structure_manager_as_manager( wallet_id=new_wallet2.id, name="Test Store Structure 2 Where New Manager Already Seller", structure_id=new_structure.id, - association_id=association_membership.id, + association_id=core_association.id, ) await add_object_to_db(new_store2_where_new_manager_already_seller) seller = models_mypayment.Seller( @@ -1246,7 +1256,7 @@ async def test_delete_store(client: TestClient): wallet_id=new_wallet.id, name="Test Store to Delete", structure_id=structure.id, - association_id=association_membership.id, + association_id=core_association.id, ) await add_object_to_db(new_store) sellet = models_mypayment.Seller( @@ -1281,7 +1291,7 @@ async def test_update_store(client: TestClient): wallet_id=new_wallet.id, name="Test Store Update", structure_id=structure.id, - association_id=association_membership.id, + association_id=core_association.id, ) await add_object_to_db(new_store) response = client.patch( From 9795b2d3249b85a526eb93b75d0d08c257bbd4b7 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:20:05 +0100 Subject: [PATCH 06/18] fixup --- migrations/versions/49-compress_images.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/migrations/versions/49-compress_images.py b/migrations/versions/49-compress_images.py index 013d454f25..da7b725d1f 100644 --- a/migrations/versions/49-compress_images.py +++ b/migrations/versions/49-compress_images.py @@ -104,10 +104,10 @@ def upgrade() -> None: Path(f"data/{data_folder}/{file_path.stem}.webp").write_bytes(res) - with Path(f"data/{data_folder}/{file_path.stem}.webp").open( - "wb", - ) as out_file: - out_file.write(res) + with Path(f"data/{data_folder}/{file_path.stem}.webp").open( + "wb", + ) as out_file: + out_file.write(res) print() # noqa: T201 From cadb9c2726e99f8a77707c4d145b64675b82b869 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:21:24 +0100 Subject: [PATCH 07/18] fixup --- migrations/versions/49-compress_images.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/migrations/versions/49-compress_images.py b/migrations/versions/49-compress_images.py index da7b725d1f..45dd3a5b2b 100644 --- a/migrations/versions/49-compress_images.py +++ b/migrations/versions/49-compress_images.py @@ -104,10 +104,6 @@ def upgrade() -> None: Path(f"data/{data_folder}/{file_path.stem}.webp").write_bytes(res) - with Path(f"data/{data_folder}/{file_path.stem}.webp").open( - "wb", - ) as out_file: - out_file.write(res) print() # noqa: T201 From b8d45d8b61ec87716b576267aab21c7678a17701 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:25:01 +0100 Subject: [PATCH 08/18] Migration rebase --- ...stores_coreassociations.py => 66-stores_coreassociations.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename migrations/versions/{50-stores_coreassociations.py => 66-stores_coreassociations.py} (97%) diff --git a/migrations/versions/50-stores_coreassociations.py b/migrations/versions/66-stores_coreassociations.py similarity index 97% rename from migrations/versions/50-stores_coreassociations.py rename to migrations/versions/66-stores_coreassociations.py index 4dc95642f9..f440541df6 100644 --- a/migrations/versions/50-stores_coreassociations.py +++ b/migrations/versions/66-stores_coreassociations.py @@ -14,7 +14,7 @@ # revision identifiers, used by Alembic. revision: str = "146db8dcb23e" -down_revision: str | None = "1ec573d854a1" +down_revision: str | None = "562adbd796ae" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None From 504bbcd41dda5452307aaf63119459d94d725ce9 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:29:16 +0100 Subject: [PATCH 09/18] get_store_by_association_id --- app/core/mypayment/cruds_mypayment.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/core/mypayment/cruds_mypayment.py b/app/core/mypayment/cruds_mypayment.py index a4321adcc5..692c39fbfe 100644 --- a/app/core/mypayment/cruds_mypayment.py +++ b/app/core/mypayment/cruds_mypayment.py @@ -194,6 +194,18 @@ async def get_store_by_name( return result.scalars().first() +async def get_store_by_association_id( + association_id: UUID, + db: AsyncSession, +) -> models_mypayment.Store | None: + result = await db.execute( + select(models_mypayment.Store).where( + models_mypayment.Store.association_id == association_id, + ), + ) + return result.scalars().first() + + async def get_stores_by_structure_id( db: AsyncSession, structure_id: UUID, From ba74726735d129e6d4b8db109ec76e239fdadeab Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:21:39 +0100 Subject: [PATCH 10/18] fix tests --- tests/core/test_mypayment.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/core/test_mypayment.py b/tests/core/test_mypayment.py index 4b6f7ffb11..7213270d6d 100644 --- a/tests/core/test_mypayment.py +++ b/tests/core/test_mypayment.py @@ -122,6 +122,7 @@ async def init_objects() -> None: name="Association", group_id=GroupType.admin, ) + await add_object_to_db(core_association) global association_membership association_membership = models_memberships.CoreAssociationMembership( @@ -885,6 +886,7 @@ async def test_create_store_for_non_existing_structure(client: TestClient): headers={"Authorization": f"Bearer {structure_manager_user_token}"}, json={ "name": "test_create_store Test Store", + "association_id": str(core_association.id), }, ) assert response.status_code == 404 @@ -897,6 +899,7 @@ async def test_create_store(client: TestClient): headers={"Authorization": f"Bearer {structure_manager_user_token}"}, json={ "name": "test_create_store Test Store", + "association_id": str(core_association.id), }, ) assert response.status_code == 201 @@ -916,6 +919,7 @@ async def test_create_store_when_user_not_manager_of_structure(client: TestClien headers={"Authorization": f"Bearer {ecl_user_access_token}"}, json={ "name": "test_create_store Test Store", + "association_id": str(core_association.id), }, ) assert response.status_code == 403 @@ -928,6 +932,7 @@ async def test_create_store_with_name_already_exist(client: TestClient): headers={"Authorization": f"Bearer {structure_manager_user_token}"}, json={ "name": "Test Store", + "association_id": str(core_association.id), }, ) assert response.status_code == 400 From 95c5d023a70560856e4bfa2c3b6afac4ce1b1ad0 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:43:20 +0100 Subject: [PATCH 11/18] Fix tests --- tests/core/test_mypayment.py | 73 +++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/tests/core/test_mypayment.py b/tests/core/test_mypayment.py index 7213270d6d..9c18cfe8a2 100644 --- a/tests/core/test_mypayment.py +++ b/tests/core/test_mypayment.py @@ -63,6 +63,7 @@ ecl_user2_wallet_device: models_mypayment.WalletDevice ecl_user2_payment: models_mypayment.UserPayment +core_association_group: models_groups.CoreGroup core_association: models_associations.CoreAssociation association_membership: models_memberships.CoreAssociationMembership @@ -116,11 +117,15 @@ async def init_objects() -> None: admin_user = await create_user_with_groups(groups=[GroupType.admin]) admin_user_token = create_api_access_token(admin_user) - global core_association + global core_association_group, core_association + core_association_group = await create_groups_with_permissions( + group_name="Core Association Group", + permissions=[], + ) core_association = models_associations.CoreAssociation( id=uuid4(), name="Association", - group_id=GroupType.admin, + group_id=core_association_group.id, ) await add_object_to_db(core_association) @@ -893,10 +898,66 @@ async def test_create_store_for_non_existing_structure(client: TestClient): assert response.json()["detail"] == "Structure does not exist" -async def test_create_store(client: TestClient): +async def test_create_store_for_non_existing_association(client: TestClient): response = client.post( f"/mypayment/structures/{structure.id}/stores", headers={"Authorization": f"Bearer {structure_manager_user_token}"}, + json={ + "name": "test_create_store Test Store", + "association_id": str(uuid4()), + }, + ) + assert response.status_code == 404 + assert response.json()["detail"] == "Association not found" + + +async def test_create_store_as_non_association_manager_member(client: TestClient): + response = client.post( + f"/mypayment/structures/{structure.id}/stores", + headers={ + "Authorization": f"Bearer {structure_manager_user_token}", + }, + json={ + "name": "test_create_store Test Store", + "association_id": str(core_association.id), + }, + ) + assert response.status_code == 403 + assert ( + response.json()["detail"] + == "You are not allowed to create stores for this association" + ) + + +async def test_create_store(client: TestClient): + structure_manager_and_association_member_user = await create_user_with_groups( + groups=[core_association_group.id], + ) + structure_manager_and_association_member_user_token = create_api_access_token( + structure_manager_and_association_member_user, + ) + structure = models_mypayment.Structure( + id=uuid4(), + name="Test Structure", + creation=datetime.now(UTC), + association_membership_id=association_membership.id, + manager_user_id=structure_manager_and_association_member_user.id, + short_id="DEF", + siege_address_street="123 Test Street", + siege_address_city="Test City", + siege_address_zipcode="12345", + siege_address_country="Test Country", + siret="12345678901234", + iban="FR76 1234 5678 9012 3456 7890 123", + bic="AZERTYUIOP", + ) + await add_object_to_db(structure) + + response = client.post( + f"/mypayment/structures/{structure.id}/stores", + headers={ + "Authorization": f"Bearer {structure_manager_and_association_member_user_token}" + }, json={ "name": "test_create_store Test Store", "association_id": str(core_association.id), @@ -907,7 +968,9 @@ async def test_create_store(client: TestClient): stores = client.get( "/mypayment/users/me/stores", - headers={"Authorization": f"Bearer {structure_manager_user_token}"}, + headers={ + "Authorization": f"Bearer {structure_manager_and_association_member_user_token}" + }, ) stores_ids = [store["id"] for store in stores.json()] assert response.json()["id"] in stores_ids @@ -1180,7 +1243,7 @@ async def test_get_stores_as_manager(client: TestClient): headers={"Authorization": f"Bearer {structure_manager_user_token}"}, ) assert response.status_code == 200 - assert len(response.json()) > 1 + assert len(response.json()) == 1 async def test_update_store_non_existing(client: TestClient): From cd651361ba332672b8adc3fb6cc556d5a400adbe Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:43:55 +0100 Subject: [PATCH 12/18] Lint --- tests/core/test_mypayment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/test_mypayment.py b/tests/core/test_mypayment.py index 9c18cfe8a2..0f086ee6e8 100644 --- a/tests/core/test_mypayment.py +++ b/tests/core/test_mypayment.py @@ -956,7 +956,7 @@ async def test_create_store(client: TestClient): response = client.post( f"/mypayment/structures/{structure.id}/stores", headers={ - "Authorization": f"Bearer {structure_manager_and_association_member_user_token}" + "Authorization": f"Bearer {structure_manager_and_association_member_user_token}", }, json={ "name": "test_create_store Test Store", @@ -969,7 +969,7 @@ async def test_create_store(client: TestClient): stores = client.get( "/mypayment/users/me/stores", headers={ - "Authorization": f"Bearer {structure_manager_and_association_member_user_token}" + "Authorization": f"Bearer {structure_manager_and_association_member_user_token}", }, ) stores_ids = [store["id"] for store in stores.json()] From 4ad20548ec087ae0361f60ba584bd9f9cfdeb539 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:50:10 +0100 Subject: [PATCH 13/18] Unique constraint for CoreAssociation and Store relationship --- app/core/mypayment/models_mypayment.py | 1 + migrations/versions/66-stores_coreassociations.py | 1 + 2 files changed, 2 insertions(+) diff --git a/app/core/mypayment/models_mypayment.py b/app/core/mypayment/models_mypayment.py index b92494c297..1931a34db7 100644 --- a/app/core/mypayment/models_mypayment.py +++ b/app/core/mypayment/models_mypayment.py @@ -172,6 +172,7 @@ class Store(Base): association_id: Mapped[UUID] = mapped_column( ForeignKey("associations_associations.id"), + unique=True, ) structure: Mapped[Structure] = relationship(init=False, lazy="joined") diff --git a/migrations/versions/66-stores_coreassociations.py b/migrations/versions/66-stores_coreassociations.py index f440541df6..51aa3b1e6a 100644 --- a/migrations/versions/66-stores_coreassociations.py +++ b/migrations/versions/66-stores_coreassociations.py @@ -50,6 +50,7 @@ def upgrade() -> None: ["association_id"], ["id"], ) + op.create_unique_constraint(None, "mypayment_store", ["association_id"]) def downgrade() -> None: From f8d08da65d7f27599275ad639d59cbea60d4c25d Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:05:22 +0100 Subject: [PATCH 14/18] Unique constraint --- app/core/mypayment/endpoints_mypayment.py | 9 ++++ tests/core/test_mypayment.py | 57 ++++++++++++++++++++--- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/app/core/mypayment/endpoints_mypayment.py b/app/core/mypayment/endpoints_mypayment.py index 0f0ba1021d..f4990cbc45 100644 --- a/app/core/mypayment/endpoints_mypayment.py +++ b/app/core/mypayment/endpoints_mypayment.py @@ -546,6 +546,15 @@ async def create_store( status_code=403, detail="You are not allowed to create stores for this association", ) + existing_store_for_association = await cruds_mypayment.get_store_by_association_id( + association_id=store.association_id, + db=db, + ) + if existing_store_for_association is not None: + raise HTTPException( + status_code=400, + detail="Store for this association already exists", + ) # Create new wallet for store wallet_id = uuid.uuid4() diff --git a/tests/core/test_mypayment.py b/tests/core/test_mypayment.py index 0f086ee6e8..804f1528f4 100644 --- a/tests/core/test_mypayment.py +++ b/tests/core/test_mypayment.py @@ -19,6 +19,7 @@ from app.core.mypayment.coredata_mypayment import ( MyPaymentBankAccountHolder, ) +from app.core.mypayment.cruds_mypayment import delete_store from app.core.mypayment.schemas_mypayment import QRCodeContentData from app.core.mypayment.types_mypayment import ( TransactionStatus, @@ -325,22 +326,34 @@ async def init_objects() -> None: association_id=core_association.id, ) await add_object_to_db(store) + core_association2 = models_associations.CoreAssociation( + id=uuid4(), + name="core_association2", + group_id=core_association_group.id, + ) + await add_object_to_db(core_association2) store2 = models_mypayment.Store( id=uuid4(), wallet_id=store2_wallet.id, name="Test Store 2", structure_id=structure2.id, creation=datetime.now(UTC), - association_id=core_association.id, + association_id=core_association2.id, ) await add_object_to_db(store2) + core_association3 = models_associations.CoreAssociation( + id=uuid4(), + name="core_association3", + group_id=core_association_group.id, + ) + await add_object_to_db(core_association3) store3 = models_mypayment.Store( id=uuid4(), wallet_id=store3_wallet.id, name="Test Store 3", structure_id=structure2.id, creation=datetime.now(UTC), - association_id=core_association.id, + association_id=core_association3.id, ) await add_object_to_db(store3) @@ -811,13 +824,19 @@ async def test_transfer_structure_manager_as_manager( balance=5000, ) await add_object_to_db(new_wallet) + new_core_association = models_associations.CoreAssociation( + id=uuid4(), + name="new_core_association", + group_id=core_association_group.id, + ) + await add_object_to_db(new_core_association) new_store = models_mypayment.Store( id=uuid4(), creation=datetime.now(UTC), wallet_id=new_wallet.id, name="Test Store Structure 2", structure_id=new_structure.id, - association_id=core_association.id, + association_id=new_core_association.id, ) await add_object_to_db(new_store) new_wallet2 = models_mypayment.Wallet( @@ -826,13 +845,19 @@ async def test_transfer_structure_manager_as_manager( balance=5000, ) await add_object_to_db(new_wallet2) + new2_core_association = models_associations.CoreAssociation( + id=uuid4(), + name="new2_core_association", + group_id=core_association_group.id, + ) + await add_object_to_db(new2_core_association) new_store2_where_new_manager_already_seller = models_mypayment.Store( id=uuid4(), creation=datetime.now(UTC), wallet_id=new_wallet2.id, name="Test Store Structure 2 Where New Manager Already Seller", structure_id=new_structure.id, - association_id=core_association.id, + association_id=new2_core_association.id, ) await add_object_to_db(new_store2_where_new_manager_already_seller) seller = models_mypayment.Seller( @@ -952,6 +977,12 @@ async def test_create_store(client: TestClient): bic="AZERTYUIOP", ) await add_object_to_db(structure) + create_store_core_association = models_associations.CoreAssociation( + id=uuid4(), + name="create_store_core_association", + group_id=core_association_group.id, + ) + await add_object_to_db(create_store_core_association) response = client.post( f"/mypayment/structures/{structure.id}/stores", @@ -960,7 +991,7 @@ async def test_create_store(client: TestClient): }, json={ "name": "test_create_store Test Store", - "association_id": str(core_association.id), + "association_id": str(create_store_core_association.id), }, ) assert response.status_code == 201 @@ -1318,13 +1349,19 @@ async def test_delete_store(client: TestClient): balance=5000, ) await add_object_to_db(new_wallet) + delete_store_core_association = models_associations.CoreAssociation( + id=uuid4(), + name="delete_store_core_association", + group_id=core_association_group.id, + ) + await add_object_to_db(delete_store_core_association) new_store = models_mypayment.Store( id=store_id, creation=datetime.now(UTC), wallet_id=new_wallet.id, name="Test Store to Delete", structure_id=structure.id, - association_id=core_association.id, + association_id=delete_store_core_association.id, ) await add_object_to_db(new_store) sellet = models_mypayment.Seller( @@ -1353,13 +1390,19 @@ async def test_update_store(client: TestClient): balance=5000, ) await add_object_to_db(new_wallet) + update_store_core_association = models_associations.CoreAssociation( + id=uuid4(), + name="update_store_core_association", + group_id=core_association_group.id, + ) + await add_object_to_db(update_store_core_association) new_store = models_mypayment.Store( id=uuid4(), creation=datetime.now(UTC), wallet_id=new_wallet.id, name="Test Store Update", structure_id=structure.id, - association_id=core_association.id, + association_id=update_store_core_association.id, ) await add_object_to_db(new_store) response = client.patch( From 8f922ebc3544dcb9d88c790dfc7b543b193d8400 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:06:01 +0100 Subject: [PATCH 15/18] lint --- tests/core/test_mypayment.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/core/test_mypayment.py b/tests/core/test_mypayment.py index 804f1528f4..4074d7b447 100644 --- a/tests/core/test_mypayment.py +++ b/tests/core/test_mypayment.py @@ -19,7 +19,6 @@ from app.core.mypayment.coredata_mypayment import ( MyPaymentBankAccountHolder, ) -from app.core.mypayment.cruds_mypayment import delete_store from app.core.mypayment.schemas_mypayment import QRCodeContentData from app.core.mypayment.types_mypayment import ( TransactionStatus, From 8634c40193a5d7f7ebe5f1777314e41b9118267c Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:29:03 +0100 Subject: [PATCH 16/18] Fix factories --- app/core/associations/factory_associations.py | 12 ++------ app/core/groups/factory_groups.py | 28 +++++++++++++------ app/core/mypayment/factory_mypayment.py | 6 +++- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/app/core/associations/factory_associations.py b/app/core/associations/factory_associations.py index faa34214de..8332fc767a 100644 --- a/app/core/associations/factory_associations.py +++ b/app/core/associations/factory_associations.py @@ -11,22 +11,14 @@ class AssociationsFactory(Factory): - association_ids = [ - uuid.uuid4(), - uuid.uuid4(), - uuid.uuid4(), - uuid.uuid4(), - ] + association_ids = [uuid.uuid4() for _ in range(len(CoreGroupsFactory.groups_ids))] depends_on = [CoreGroupsFactory] @classmethod async def create_associations(cls, db: AsyncSession): descriptions = [ - "Association 1", - "Association 2", - "Association 3", - "Association 4", + f"Association {i + 1}" for i in range(len(CoreGroupsFactory.groups_ids)) ] for i in range(len(CoreGroupsFactory.groups_ids)): await cruds_associations.create_association( diff --git a/app/core/groups/factory_groups.py b/app/core/groups/factory_groups.py index e7db9846f0..407c7758de 100644 --- a/app/core/groups/factory_groups.py +++ b/app/core/groups/factory_groups.py @@ -12,23 +12,35 @@ class CoreGroupsFactory(Factory): - groups_ids = [ - str(uuid.uuid4()), - str(uuid.uuid4()), - str(uuid.uuid4()), - str(uuid.uuid4()), - ] + groups_ids = [str(uuid.uuid4()) for _ in range(10)] depends_on = [CoreUsersFactory] @classmethod async def create_core_groups(cls, db: AsyncSession): - groups = ["BDE", "BDS", "Oui", "Pixels"] + groups = [ + "BDE", + "BDS", + "Pixels", + "Commuz", + "Musique", + "Fablab", + "BDA", + "Fouquette", + "Soli", + "SDeC", + ] descriptions = [ "Bureau des élèves", "Bureau des sports", - "Association d'entraide", "Association de photographie", + "", + "", + "", + "", + "", + "", + "", ] for i in range(len(groups)): await cruds_groups.create_group( diff --git a/app/core/mypayment/factory_mypayment.py b/app/core/mypayment/factory_mypayment.py index f7aad4bdf4..a8c4c57b3c 100644 --- a/app/core/mypayment/factory_mypayment.py +++ b/app/core/mypayment/factory_mypayment.py @@ -68,6 +68,7 @@ async def create_structures(cls, db: AsyncSession): @classmethod async def create_other_structures_stores(cls, db: AsyncSession): + association_id_index = 0 for structure_id in cls.other_structures_id: structure_store_ids = [] structure_wallet_ids = [] @@ -89,10 +90,13 @@ async def create_other_structures_stores(cls, db: AsyncSession): name=faker.company(), creation=datetime.now(UTC), wallet_id=wallet_id, - association_id=AssociationsFactory.association_ids[0], + association_id=AssociationsFactory.association_ids[ + association_id_index + ], ), db, ) + association_id_index += 1 cls.other_stores_id.append(structure_store_ids) cls.other_stores_wallet_id.append(structure_wallet_ids) From c678d6afa1c25d2d772b3d5630e7842093aaeda0 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:49:16 +0200 Subject: [PATCH 17/18] Optional association_id --- app/core/mypayment/models_mypayment.py | 2 +- app/core/mypayment/schemas_mypayment.py | 2 +- .../versions/66-stores_coreassociations.py | 17 +---------------- tests/core/test_mypayment.py | 17 +++-------------- 4 files changed, 6 insertions(+), 32 deletions(-) diff --git a/app/core/mypayment/models_mypayment.py b/app/core/mypayment/models_mypayment.py index 1931a34db7..2d17325a51 100644 --- a/app/core/mypayment/models_mypayment.py +++ b/app/core/mypayment/models_mypayment.py @@ -170,7 +170,7 @@ class Store(Base): ) creation: Mapped[datetime] - association_id: Mapped[UUID] = mapped_column( + association_id: Mapped[UUID | None] = mapped_column( ForeignKey("associations_associations.id"), unique=True, ) diff --git a/app/core/mypayment/schemas_mypayment.py b/app/core/mypayment/schemas_mypayment.py index 6da648856b..9c1698f347 100644 --- a/app/core/mypayment/schemas_mypayment.py +++ b/app/core/mypayment/schemas_mypayment.py @@ -66,7 +66,7 @@ class StructureTranfert(BaseModel): class StoreBase(BaseModel): name: str - association_id: UUID + association_id: UUID | None = None class StoreSimple(StoreBase): diff --git a/migrations/versions/66-stores_coreassociations.py b/migrations/versions/66-stores_coreassociations.py index 51aa3b1e6a..e30966c34e 100644 --- a/migrations/versions/66-stores_coreassociations.py +++ b/migrations/versions/66-stores_coreassociations.py @@ -20,27 +20,12 @@ def upgrade() -> None: - store_table = sa.table( - "mypayment_store", - sa.column("id", sa.String), - ) - - conn = op.get_bind() - stores = conn.execute( - sa.select(store_table.c.id), - ).fetchall() - - if len(stores) > 0: - raise Exception( # noqa: TRY002, TRY003 - "There are already stores in database, we cannot safely migrate to add association_id to store", - ) - op.add_column( "mypayment_store", sa.Column( "association_id", sa.Uuid(), - nullable=False, + nullable=True, ), ) op.create_foreign_key( diff --git a/tests/core/test_mypayment.py b/tests/core/test_mypayment.py index 4074d7b447..771af6694a 100644 --- a/tests/core/test_mypayment.py +++ b/tests/core/test_mypayment.py @@ -325,34 +325,23 @@ async def init_objects() -> None: association_id=core_association.id, ) await add_object_to_db(store) - core_association2 = models_associations.CoreAssociation( - id=uuid4(), - name="core_association2", - group_id=core_association_group.id, - ) - await add_object_to_db(core_association2) store2 = models_mypayment.Store( id=uuid4(), wallet_id=store2_wallet.id, name="Test Store 2", structure_id=structure2.id, creation=datetime.now(UTC), - association_id=core_association2.id, + association_id=None, ) await add_object_to_db(store2) - core_association3 = models_associations.CoreAssociation( - id=uuid4(), - name="core_association3", - group_id=core_association_group.id, - ) - await add_object_to_db(core_association3) + store3 = models_mypayment.Store( id=uuid4(), wallet_id=store3_wallet.id, name="Test Store 3", structure_id=structure2.id, creation=datetime.now(UTC), - association_id=core_association3.id, + association_id=None, ) await add_object_to_db(store3) From af867b24587b25c550ad61cf5080843b632a1a01 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:52:10 +0200 Subject: [PATCH 18/18] fix --- app/core/mypayment/endpoints_mypayment.py | 51 ++++++++++++----------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/app/core/mypayment/endpoints_mypayment.py b/app/core/mypayment/endpoints_mypayment.py index f4990cbc45..5c2cb9a23d 100644 --- a/app/core/mypayment/endpoints_mypayment.py +++ b/app/core/mypayment/endpoints_mypayment.py @@ -529,32 +529,35 @@ async def create_store( detail="Store with this name already exists in this structure", ) - association = await cruds_associations.get_association_by_id( - db=db, - association_id=store.association_id, - ) - if not association: - raise HTTPException( - status_code=404, - detail="Association not found", - ) - if not is_user_member_of_an_association( - user=user, - association=association, - ): - raise HTTPException( - status_code=403, - detail="You are not allowed to create stores for this association", + if store.association_id is not None: + association = await cruds_associations.get_association_by_id( + db=db, + association_id=store.association_id, ) - existing_store_for_association = await cruds_mypayment.get_store_by_association_id( - association_id=store.association_id, - db=db, - ) - if existing_store_for_association is not None: - raise HTTPException( - status_code=400, - detail="Store for this association already exists", + if not association: + raise HTTPException( + status_code=404, + detail="Association not found", + ) + if not is_user_member_of_an_association( + user=user, + association=association, + ): + raise HTTPException( + status_code=403, + detail="You are not allowed to create stores for this association", + ) + existing_store_for_association = ( + await cruds_mypayment.get_store_by_association_id( + association_id=store.association_id, + db=db, + ) ) + if existing_store_for_association is not None: + raise HTTPException( + status_code=400, + detail="Store for this association already exists", + ) # Create new wallet for store wallet_id = uuid.uuid4()