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/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, diff --git a/app/core/mypayment/endpoints_mypayment.py b/app/core/mypayment/endpoints_mypayment.py index 405c64ef13..5c2cb9a23d 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,36 @@ async def create_store( detail="Store with this name already exists in this structure", ) + if store.association_id is not None: + 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", + ) + 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() await cruds_mypayment.create_wallet( @@ -541,6 +574,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 +608,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 +880,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..a8c4c57b3c 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] @@ -67,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 = [] @@ -88,9 +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[ + 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) diff --git a/app/core/mypayment/models_mypayment.py b/app/core/mypayment/models_mypayment.py index b6af28ce2f..2d17325a51 100644 --- a/app/core/mypayment/models_mypayment.py +++ b/app/core/mypayment/models_mypayment.py @@ -170,6 +170,11 @@ class Store(Base): ) creation: Mapped[datetime] + association_id: Mapped[UUID | None] = mapped_column( + ForeignKey("associations_associations.id"), + unique=True, + ) + 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..9c1698f347 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 | None = None 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/migrations/versions/49-compress_images.py b/migrations/versions/49-compress_images.py index dc7ac39860..45dd3a5b2b 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,7 @@ 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() + print() # noqa: T201 def downgrade() -> None: diff --git a/migrations/versions/66-stores_coreassociations.py b/migrations/versions/66-stores_coreassociations.py new file mode 100644 index 0000000000..e30966c34e --- /dev/null +++ b/migrations/versions/66-stores_coreassociations.py @@ -0,0 +1,61 @@ +"""empty message + +Create Date: 2026-03-01 11:41:22.994301 +""" + +from collections.abc import Sequence +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pytest_alembic import MigrationContext + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "146db8dcb23e" +down_revision: str | None = "562adbd796ae" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column( + "mypayment_store", + sa.Column( + "association_id", + sa.Uuid(), + nullable=True, + ), + ) + op.create_foreign_key( + None, + "mypayment_store", + "associations_associations", + ["association_id"], + ["id"], + ) + op.create_unique_constraint(None, "mypayment_store", ["association_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 diff --git a/tests/core/test_mypayment.py b/tests/core/test_mypayment.py index 9820d8a611..771af6694a 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,9 @@ 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 association_membership_user: models_memberships.CoreAssociationUserMembership structure: models_mypayment.Structure @@ -113,6 +117,18 @@ 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_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=core_association_group.id, + ) + await add_object_to_db(core_association) + global association_membership association_membership = models_memberships.CoreAssociationMembership( id=uuid4(), @@ -306,6 +322,7 @@ async def init_objects() -> None: name="Test Store", structure_id=structure.id, creation=datetime.now(UTC), + association_id=core_association.id, ) await add_object_to_db(store) store2 = models_mypayment.Store( @@ -314,14 +331,17 @@ async def init_objects() -> None: name="Test Store 2", structure_id=structure2.id, creation=datetime.now(UTC), + association_id=None, ) await add_object_to_db(store2) + 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=None, ) await add_object_to_db(store3) @@ -792,12 +812,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=new_core_association.id, ) await add_object_to_db(new_store) new_wallet2 = models_mypayment.Wallet( @@ -806,12 +833,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=new2_core_association.id, ) await add_object_to_db(new_store2_where_new_manager_already_seller) seller = models_mypayment.Seller( @@ -870,18 +904,82 @@ 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 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) + 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", + headers={ + "Authorization": f"Bearer {structure_manager_and_association_member_user_token}", + }, + json={ + "name": "test_create_store Test Store", + "association_id": str(create_store_core_association.id), }, ) assert response.status_code == 201 @@ -889,7 +987,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 @@ -901,6 +1001,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 @@ -913,6 +1014,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 @@ -1160,7 +1262,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): @@ -1235,12 +1337,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=delete_store_core_association.id, ) await add_object_to_db(new_store) sellet = models_mypayment.Seller( @@ -1269,12 +1378,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=update_store_core_association.id, ) await add_object_to_db(new_store) response = client.patch(