diff --git a/app/core/myeclpay/coredata_myeclpay.py b/app/core/myeclpay/coredata_myeclpay.py new file mode 100644 index 0000000000..6b0944c31c --- /dev/null +++ b/app/core/myeclpay/coredata_myeclpay.py @@ -0,0 +1,9 @@ +from uuid import UUID + +from app.types.core_data import BaseCoreData + + +class MyECLPayBankAccountHolder(BaseCoreData): + """Bank account holder information for MyECLPay.""" + + holder_structure_id: UUID diff --git a/app/core/myeclpay/cruds_myeclpay.py b/app/core/myeclpay/cruds_myeclpay.py index bd07322f9e..14f178e3e2 100644 --- a/app/core/myeclpay/cruds_myeclpay.py +++ b/app/core/myeclpay/cruds_myeclpay.py @@ -6,7 +6,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import noload, selectinload -from app.core.memberships import schemas_memberships from app.core.myeclpay import models_myeclpay, schemas_myeclpay from app.core.myeclpay.exceptions_myeclpay import WalletNotFoundOnUpdateError from app.core.myeclpay.types_myeclpay import ( @@ -14,19 +13,33 @@ WalletDeviceStatus, WalletType, ) +from app.core.myeclpay.utils_myeclpay import ( + invoice_model_to_schema, + refund_model_to_schema, + structure_model_to_schema, +) from app.core.users import schemas_users async def create_structure( - structure: schemas_myeclpay.Structure, + structure: schemas_myeclpay.StructureSimple, db: AsyncSession, ) -> None: db.add( models_myeclpay.Structure( id=structure.id, + short_id=structure.short_id, name=structure.name, association_membership_id=structure.association_membership_id, manager_user_id=structure.manager_user_id, + siret=structure.siret, + siege_address_street=structure.siege_address_street, + siege_address_city=structure.siege_address_city, + siege_address_zipcode=structure.siege_address_zipcode, + siege_address_country=structure.siege_address_country, + iban=structure.iban, + bic=structure.bic, + creation=structure.creation, ), ) @@ -112,28 +125,7 @@ async def get_structures( ) -> Sequence[schemas_myeclpay.Structure]: result = await db.execute(select(models_myeclpay.Structure)) return [ - schemas_myeclpay.Structure( - name=structure.name, - association_membership_id=structure.association_membership_id, - association_membership=schemas_memberships.MembershipSimple( - id=structure.association_membership.id, - name=structure.association_membership.name, - manager_group_id=structure.association_membership.manager_group_id, - ) - if structure.association_membership - else None, - manager_user_id=structure.manager_user_id, - id=structure.id, - manager_user=schemas_users.CoreUserSimple( - id=structure.manager_user.id, - firstname=structure.manager_user.firstname, - name=structure.manager_user.name, - nickname=structure.manager_user.nickname, - account_type=structure.manager_user.account_type, - school_id=structure.manager_user.school_id, - ), - ) - for structure in result.scalars().all() + structure_model_to_schema(structure) for structure in result.scalars().all() ] @@ -152,31 +144,7 @@ async def get_structure_by_id( .scalars() .first() ) - return ( - schemas_myeclpay.Structure( - name=structure.name, - association_membership_id=structure.association_membership_id, - association_membership=schemas_memberships.MembershipSimple( - id=structure.association_membership.id, - name=structure.association_membership.name, - manager_group_id=structure.association_membership.manager_group_id, - ) - if structure.association_membership - else None, - manager_user_id=structure.manager_user_id, - id=structure.id, - manager_user=schemas_users.CoreUserSimple( - id=structure.manager_user.id, - firstname=structure.manager_user.firstname, - name=structure.manager_user.name, - nickname=structure.manager_user.nickname, - account_type=structure.manager_user.account_type, - school_id=structure.manager_user.school_id, - ), - ) - if structure - else None - ) + return structure_model_to_schema(structure) if structure else None async def create_store( @@ -822,47 +790,7 @@ async def get_refund_by_transaction_id( .scalars() .first() ) - return ( - schemas_myeclpay.Refund( - id=result.id, - transaction_id=result.transaction_id, - credited_wallet_id=result.credited_wallet_id, - debited_wallet_id=result.debited_wallet_id, - total=result.total, - creation=result.creation, - seller_user_id=result.seller_user_id, - transaction=schemas_myeclpay.Transaction( - id=result.transaction.id, - debited_wallet_id=result.transaction.debited_wallet_id, - credited_wallet_id=result.transaction.credited_wallet_id, - transaction_type=result.transaction.transaction_type, - seller_user_id=result.transaction.seller_user_id, - total=result.transaction.total, - creation=result.transaction.creation, - status=result.transaction.status, - ), - debited_wallet=schemas_myeclpay.WalletInfo( - id=result.debited_wallet.id, - type=result.debited_wallet.type, - owner_name=result.debited_wallet.store.name - if result.debited_wallet.store - else result.debited_wallet.user.full_name - if result.debited_wallet.user - else None, - ), - credited_wallet=schemas_myeclpay.WalletInfo( - id=result.credited_wallet.id, - type=result.credited_wallet.type, - owner_name=result.credited_wallet.store.name - if result.credited_wallet.store - else result.credited_wallet.user.full_name - if result.credited_wallet.user - else None, - ), - ) - if result - else None - ) + return refund_model_to_schema(result) if result else None async def get_refunds_by_wallet_id( @@ -891,46 +819,7 @@ async def get_refunds_by_wallet_id( .scalars() .all() ) - return [ - schemas_myeclpay.Refund( - id=refund.id, - transaction_id=refund.transaction_id, - credited_wallet_id=refund.credited_wallet_id, - debited_wallet_id=refund.debited_wallet_id, - total=refund.total, - creation=refund.creation, - seller_user_id=refund.seller_user_id, - transaction=schemas_myeclpay.Transaction( - id=refund.transaction.id, - debited_wallet_id=refund.transaction.debited_wallet_id, - credited_wallet_id=refund.transaction.credited_wallet_id, - transaction_type=refund.transaction.transaction_type, - seller_user_id=refund.transaction.seller_user_id, - total=refund.transaction.total, - creation=refund.transaction.creation, - status=refund.transaction.status, - ), - debited_wallet=schemas_myeclpay.WalletInfo( - id=refund.debited_wallet.id, - type=refund.debited_wallet.type, - owner_name=refund.debited_wallet.store.name - if refund.debited_wallet.store - else refund.debited_wallet.user.full_name - if refund.debited_wallet.user - else None, - ), - credited_wallet=schemas_myeclpay.WalletInfo( - id=refund.credited_wallet.id, - type=refund.credited_wallet.type, - owner_name=refund.credited_wallet.store.name - if refund.credited_wallet.store - else refund.credited_wallet.user.full_name - if refund.credited_wallet.user - else None, - ), - ) - for refund in result - ] + return [refund_model_to_schema(refund) for refund in result] async def get_store( @@ -981,3 +870,216 @@ async def delete_used_qrcode( models_myeclpay.UsedQRCode.qr_code_id == qr_code_id, ), ) + + +async def get_invoices( + db: AsyncSession, + skip: int | None = None, + limit: int | None = None, + structures_ids: list[UUID] | None = None, + start_date: datetime | None = None, + end_date: datetime | None = None, +) -> list[schemas_myeclpay.Invoice]: + select_command = ( + select(models_myeclpay.Invoice) + .where( + models_myeclpay.Invoice.end_date >= start_date + if start_date + else and_(True), + models_myeclpay.Invoice.end_date <= end_date if end_date else and_(True), + models_myeclpay.Invoice.structure_id.in_(structures_ids) + if structures_ids + else and_(True), + ) + .order_by( + models_myeclpay.Invoice.end_date.desc(), + ) + ) + if skip is not None: + select_command = select_command.offset(skip) + if limit is not None: + select_command = select_command.limit(limit) + result = await db.execute(select_command) + return [invoice_model_to_schema(invoice) for invoice in result.scalars().all()] + + +async def get_invoice_by_id( + invoice_id: UUID, + db: AsyncSession, +) -> schemas_myeclpay.Invoice | None: + result = await db.execute( + select(models_myeclpay.Invoice) + .where( + models_myeclpay.Invoice.id == invoice_id, + ) + .with_for_update(of=models_myeclpay.Invoice), + ) + invoice = result.scalars().first() + return invoice_model_to_schema(invoice) if invoice else None + + +async def get_pending_invoices_by_structure_id( + structure_id: UUID, + db: AsyncSession, +) -> list[schemas_myeclpay.Invoice]: + result = await db.execute( + select(models_myeclpay.Invoice).where( + models_myeclpay.Invoice.structure_id == structure_id, + models_myeclpay.Invoice.received.is_(False), + ), + ) + return [invoice_model_to_schema(invoice) for invoice in result.scalars().all()] + + +async def get_unreceived_invoices_by_store_id( + store_id: UUID, + db: AsyncSession, +) -> list[schemas_myeclpay.InvoiceDetailBase]: + result = await db.execute( + select(models_myeclpay.InvoiceDetail) + .join(models_myeclpay.Invoice) + .where( + models_myeclpay.InvoiceDetail.store_id == store_id, + models_myeclpay.Invoice.received.is_(False), + ), + ) + return [ + schemas_myeclpay.InvoiceDetailBase( + invoice_id=detail.invoice_id, + store_id=detail.store_id, + total=detail.total, + ) + for detail in result.scalars().all() + ] + + +async def create_invoice( + invoice: schemas_myeclpay.InvoiceInfo, + db: AsyncSession, +) -> None: + invoice_db = models_myeclpay.Invoice( + id=invoice.id, + reference=invoice.reference, + creation=invoice.creation, + start_date=invoice.start_date, + end_date=invoice.end_date, + total=invoice.total, + structure_id=invoice.structure_id, + received=invoice.received, + ) + db.add(invoice_db) + for detail in invoice.details: + detail_db = models_myeclpay.InvoiceDetail( + invoice_id=invoice.id, + store_id=detail.store_id, + total=detail.total, + ) + db.add(detail_db) + + +async def update_invoice_received_status( + invoice_id: UUID, + db: AsyncSession, +) -> None: + await db.execute( + update(models_myeclpay.Invoice) + .where(models_myeclpay.Invoice.id == invoice_id) + .values(received=True), + ) + + +async def update_invoice_paid_status( + invoice_id: UUID, + paid: bool, + db: AsyncSession, +) -> None: + await db.execute( + update(models_myeclpay.Invoice) + .where(models_myeclpay.Invoice.id == invoice_id) + .values(paid=paid), + ) + + +async def delete_invoice( + invoice_id: UUID, + db: AsyncSession, +) -> None: + await db.execute( + delete(models_myeclpay.InvoiceDetail).where( + models_myeclpay.InvoiceDetail.invoice_id == invoice_id, + ), + ) + await db.execute( + delete(models_myeclpay.Invoice).where( + models_myeclpay.Invoice.id == invoice_id, + ), + ) + + +async def get_last_structure_invoice( + structure_id: UUID, + db: AsyncSession, +) -> schemas_myeclpay.Invoice | None: + result = ( + ( + await db.execute( + select(models_myeclpay.Invoice) + .where( + models_myeclpay.Invoice.structure_id == structure_id, + ) + .order_by(models_myeclpay.Invoice.end_date.desc()) + .limit(1), + ) + ) + .scalars() + .first() + ) + return invoice_model_to_schema(result) if result else None + + +async def add_withdrawal( + withdrawal: schemas_myeclpay.Withdrawal, + db: AsyncSession, +) -> None: + withdrawal_db = models_myeclpay.Withdrawal( + id=withdrawal.id, + wallet_id=withdrawal.wallet_id, + total=withdrawal.total, + creation=withdrawal.creation, + ) + db.add(withdrawal_db) + + +async def get_withdrawals( + db: AsyncSession, +) -> list[schemas_myeclpay.Withdrawal]: + result = await db.execute(select(models_myeclpay.Withdrawal)) + return [ + schemas_myeclpay.Withdrawal( + id=withdrawal.id, + wallet_id=withdrawal.wallet_id, + total=withdrawal.total, + creation=withdrawal.creation, + ) + for withdrawal in result.scalars().all() + ] + + +async def get_withdrawals_by_wallet_id( + wallet_id: UUID, + db: AsyncSession, +) -> list[schemas_myeclpay.Withdrawal]: + result = await db.execute( + select(models_myeclpay.Withdrawal).where( + models_myeclpay.Withdrawal.wallet_id == wallet_id, + ), + ) + return [ + schemas_myeclpay.Withdrawal( + id=withdrawal.id, + wallet_id=withdrawal.wallet_id, + total=withdrawal.total, + creation=withdrawal.creation, + ) + for withdrawal in result.scalars().all() + ] diff --git a/app/core/myeclpay/dependencies_myeclpay.py b/app/core/myeclpay/dependencies_myeclpay.py new file mode 100644 index 0000000000..5d47a68e3f --- /dev/null +++ b/app/core/myeclpay/dependencies_myeclpay.py @@ -0,0 +1,34 @@ +from fastapi import Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.myeclpay.coredata_myeclpay import MyECLPayBankAccountHolder +from app.core.myeclpay.cruds_myeclpay import get_structure_by_id +from app.core.users.models_users import CoreUser +from app.dependencies import get_db, is_user +from app.utils.tools import get_core_data + + +async def is_user_bank_account_holder( + user: CoreUser = Depends(is_user()), + db: AsyncSession = Depends(get_db), +) -> CoreUser: + """Check if the user is a bank account holder.""" + account_holder = await get_core_data( + MyECLPayBankAccountHolder, + db=db, + ) + structure = await get_structure_by_id( + db=db, + structure_id=account_holder.holder_structure_id, + ) + if not structure: + raise HTTPException( + status_code=404, + detail="Structure not found for the bank account holder", + ) + if structure.manager_user_id != user.id: + raise HTTPException( + status_code=403, + detail="User is not the bank account holder", + ) + return user diff --git a/app/core/myeclpay/endpoints_myeclpay.py b/app/core/myeclpay/endpoints_myeclpay.py index 30f6e30749..0a9ad42e99 100644 --- a/app/core/myeclpay/endpoints_myeclpay.py +++ b/app/core/myeclpay/endpoints_myeclpay.py @@ -15,21 +15,29 @@ HTTPException, Query, ) -from fastapi.responses import RedirectResponse +from fastapi.responses import FileResponse, RedirectResponse from fastapi.templating import Jinja2Templates from sqlalchemy.ext.asyncio import AsyncSession +from app.core.auth import schemas_auth from app.core.core_endpoints import cruds_core from app.core.groups.groups_type import GroupType -from app.core.memberships import schemas_memberships from app.core.memberships.utils_memberships import ( get_user_active_membership_to_association_membership, ) from app.core.myeclpay import cruds_myeclpay, schemas_myeclpay +from app.core.myeclpay.coredata_myeclpay import MyECLPayBankAccountHolder +from app.core.myeclpay.dependencies_myeclpay import is_user_bank_account_holder +from app.core.myeclpay.exceptions_myeclpay import ( + InvoiceNotFoundAfterCreationError, + ReferencedStructureNotFoundError, +) +from app.core.myeclpay.factory_myeclpay import MyECLPayFactory from app.core.myeclpay.integrity_myeclpay import ( format_cancel_log, format_refund_log, format_transaction_log, + format_withdrawal_log, ) from app.core.myeclpay.models_myeclpay import Store, WalletDevice from app.core.myeclpay.types_myeclpay import ( @@ -45,6 +53,7 @@ LATEST_TOS, QRCODE_EXPIRATION, is_user_latest_tos_signed, + structure_model_to_schema, validate_transfer_callback, verify_signature, ) @@ -63,14 +72,23 @@ get_payment_tool, get_request_id, get_settings, + get_token_data, is_user, is_user_an_ecl_member, is_user_in, ) from app.types import standard_responses from app.types.module import CoreModule +from app.types.scopes_type import ScopeType +from app.utils.auth.auth_utils import get_user_id_from_token_with_scopes from app.utils.communication.notifications import NotificationTool from app.utils.mail.mailworker import send_email +from app.utils.tools import ( + generate_pdf_from_template, + get_core_data, + get_file_from_data, + set_core_data, +) router = APIRouter(tags=["MyECLPay"]) @@ -79,7 +97,7 @@ tag="MyECLPay", router=router, payment_callback=validate_transfer_callback, - factory=None, + factory=MyECLPayFactory(), ) templates = Jinja2Templates(directory="assets/templates") @@ -97,6 +115,63 @@ RETENTION_DURATION = 10 * 365 # 10 years in days +@router.get( + "/myeclpay/bank-account-holder", + response_model=schemas_myeclpay.Structure, + status_code=200, +) +async def get_bank_account_holder( + user: CoreUser = Depends(is_user()), + db: AsyncSession = Depends(get_db), +): + """ + Get the current bank account holder information. + """ + bank_account_holder = await get_core_data( + MyECLPayBankAccountHolder, + db=db, + ) + structure = await cruds_myeclpay.get_structure_by_id( + db=db, + structure_id=bank_account_holder.holder_structure_id, + ) + if structure is None: + raise ReferencedStructureNotFoundError( + structure_id=bank_account_holder.holder_structure_id, + ) + return structure + + +@router.post( + "/myeclpay/bank-account-holder", + response_model=schemas_myeclpay.Structure, + status_code=201, +) +async def set_bank_account_holder( + bank_account_info: MyECLPayBankAccountHolder, + db: AsyncSession = Depends(get_db), + user: CoreUser = Depends(is_user_in(GroupType.admin)), +): + """Set the bank account holder information.""" + + structure = await cruds_myeclpay.get_structure_by_id( + structure_id=bank_account_info.holder_structure_id, + db=db, + ) + if structure is None: + raise HTTPException( + status_code=404, + detail="Structure does not exist", + ) + + await set_core_data( + bank_account_info, + db=db, + ) + + return structure + + @router.get( "/myeclpay/structures", status_code=200, @@ -144,20 +219,20 @@ async def create_structure( status_code=404, detail="Manager user does not exist", ) - structure_db = schemas_myeclpay.Structure( + structure_db = schemas_myeclpay.StructureSimple( id=uuid.uuid4(), + short_id=structure.short_id, name=structure.name, association_membership_id=structure.association_membership_id, association_membership=None, manager_user_id=structure.manager_user_id, - manager_user=schemas_users.CoreUserSimple( - id=db_user.id, - name=db_user.name, - firstname=db_user.firstname, - nickname=db_user.nickname, - account_type=db_user.account_type, - school_id=db_user.school_id, - ), + siege_address_street=structure.siege_address_street, + siege_address_zipcode=structure.siege_address_zipcode, + siege_address_city=structure.siege_address_city, + siege_address_country=structure.siege_address_country, + iban=structure.iban, + bic=structure.bic, + creation=datetime.now(tz=UTC), ) await cruds_myeclpay.create_structure( structure=structure_db, @@ -462,6 +537,7 @@ async def create_store( name=store.name, structure_id=structure_id, wallet_id=wallet_id, + creation=datetime.now(tz=UTC), ) await cruds_myeclpay.create_store( store=store_db, @@ -493,6 +569,7 @@ async def create_store( name=store_db.name, structure_id=store_db.structure_id, wallet_id=store_db.wallet_id, + creation=store_db.creation, structure=structure, ) @@ -651,28 +728,9 @@ async def get_user_stores( schemas_myeclpay.UserStore( id=store.id, name=store.name, + creation=store.creation, structure_id=store.structure_id, - structure=schemas_myeclpay.Structure( - id=store.structure.id, - name=store.structure.name, - association_membership_id=store.structure.association_membership_id, - association_membership=schemas_memberships.MembershipSimple( - id=store.structure.association_membership.id, - name=store.structure.association_membership.name, - manager_group_id=store.structure.association_membership.manager_group_id, - ) - if store.structure.association_membership is not None - else None, - manager_user_id=store.structure.manager_user_id, - manager_user=schemas_users.CoreUserSimple( - id=store.structure.manager_user.id, - name=store.structure.manager_user.name, - firstname=store.structure.manager_user.firstname, - nickname=store.structure.manager_user.nickname, - account_type=store.structure.manager_user.account_type, - school_id=store.structure.manager_user.school_id, - ), - ), + structure=structure_model_to_schema(store.structure), wallet_id=store.wallet_id, can_bank=seller.can_bank, can_see_history=seller.can_see_history, @@ -2466,6 +2524,448 @@ async def cancel_transaction( ) +@router.get( + "/myeclpay/invoices", + response_model=list[schemas_myeclpay.Invoice], +) +async def get_invoices( + page: int | None = None, + page_size: int | None = None, + structures_ids: list[UUID] | None = Query(default=None), + start_date: datetime | None = None, + end_date: datetime | None = None, + db: AsyncSession = Depends(get_db), + user: CoreUser = Depends(is_user_bank_account_holder), +) -> list[schemas_myeclpay.Invoice]: + """ + Get all invoices. + + **The user must be authenticated to use this endpoint** + """ + return await cruds_myeclpay.get_invoices( + db=db, + skip=(page - 1) * page_size if page and page_size else None, + limit=page_size, + start_date=start_date, + end_date=end_date, + structures_ids=structures_ids, + ) + + +@router.get( + "/myeclpay/invoices/structures/{structure_id}", + response_model=list[schemas_myeclpay.Invoice], +) +async def get_structure_invoices( + structure_id: UUID, + page: int | None = None, + page_size: int | None = None, + start_date: datetime | None = None, + end_date: datetime | None = None, + db: AsyncSession = Depends(get_db), + user: CoreUser = Depends(is_user()), +) -> list[schemas_myeclpay.Invoice]: + """ + Get all invoices. + + **The user must be the structure manager** + """ + structure = await cruds_myeclpay.get_structure_by_id( + structure_id=structure_id, + db=db, + ) + if structure is None: + raise HTTPException( + status_code=404, + detail="Structure does not exist", + ) + if structure.manager_user_id != user.id: + raise HTTPException( + status_code=403, + detail="User is not allowed to access this structure invoices", + ) + + return await cruds_myeclpay.get_invoices( + db=db, + skip=(page - 1) * page_size if page and page_size else None, + limit=page_size, + start_date=start_date, + end_date=end_date, + structures_ids=[structure_id], + ) + + +@router.get( + "/myeclpay/invoices/{invoice_id}", + response_class=FileResponse, +) +async def download_invoice( + invoice_id: UUID, + db: AsyncSession = Depends(get_db), + user: CoreUser = Depends(is_user()), +): + invoice = await cruds_myeclpay.get_invoice_by_id(invoice_id, db) + if invoice is None: + raise HTTPException( + status_code=404, + detail="Invoice does not exist", + ) + structure = await cruds_myeclpay.get_structure_by_id( + structure_id=invoice.structure_id, + db=db, + ) + if structure is None: + raise HTTPException( + status_code=404, + detail="Structure does not exist", + ) + bank_account_info = await get_bank_account_holder( + user=user, + db=db, + ) + if user.id not in ( + structure.manager_user_id, + bank_account_info.manager_user_id, + ): + raise HTTPException( + status_code=403, + detail="User is not allowed to access this invoice", + ) + return get_file_from_data( + directory="myeclpay/invoices", + filename=str(invoice_id), + ) + + +@router.post( + "/myeclpay/invoices/structures/{structure_id}", + response_model=schemas_myeclpay.Invoice, + status_code=201, +) +async def create_structure_invoice( + structure_id: UUID, + db: AsyncSession = Depends(get_db), + token_data: schemas_auth.TokenData = Depends(get_token_data), +): + """ + Create an invoice for a structure. + + **The user must be the bank account holder** + """ + now = await cruds_core.start_isolation_mode(db) + # Database isolation requires to be the first statement of the transaction + # We can't use reguler dependencies to check user permissions as they access the database + user_id = get_user_id_from_token_with_scopes( + scopes=[[ScopeType.API]], + token_data=token_data, + ) + user = await cruds_users.get_user_by_id( + db=db, + user_id=user_id, + ) + if user is None: + raise HTTPException( + status_code=404, + detail="User does not exist", + ) + bank_holder_structure = await get_bank_account_holder( + user=user, + db=db, + ) + if bank_holder_structure.manager_user_id != user.id: + raise HTTPException( + status_code=403, + detail="User is not the bank account holder", + ) + structure = await cruds_myeclpay.get_structure_by_id( + structure_id=structure_id, + db=db, + ) + if structure is None: + raise HTTPException( + status_code=404, + detail="Structure does not exist", + ) + + stores = await cruds_myeclpay.get_stores_by_structure_id( + structure_id=structure_id, + db=db, + ) + invoice_details: list[schemas_myeclpay.InvoiceDetailBase] = [] + invoice_id = uuid.uuid4() + + # We use a 30 seconds delay to avoid unstable transactions + # as they can be canceled during the 30 seconds after their creation + security_now = now - timedelta(seconds=30) + to_substract_transactions = await cruds_myeclpay.get_transactions( + db=db, + start_date=security_now, + exclude_canceled=True, + ) + + for store in stores: + store_wallet_db = await cruds_myeclpay.get_wallet( + wallet_id=store.wallet_id, + db=db, + ) + if store_wallet_db is None: + hyperion_error_logger.error( + "MyECLPAy: Could not find wallet associated with a store, this should never happen", + ) + raise HTTPException( + status_code=500, + detail="Could not find wallet associated with the store", + ) + store_wallet = schemas_myeclpay.Wallet( + id=store_wallet_db.id, + type=store_wallet_db.type, + balance=store_wallet_db.balance, + user=None, + store=None, + ) + for transaction in to_substract_transactions: + if transaction.credited_wallet_id == store_wallet.id: + store_wallet.balance -= transaction.total + elif transaction.debited_wallet_id == store_wallet.id: + store_wallet.balance += transaction.total + store_pending_invoices = ( + await cruds_myeclpay.get_unreceived_invoices_by_store_id( + store_id=store.id, + db=db, + ) + ) + for pending_invoice in store_pending_invoices: + store_wallet.balance -= pending_invoice.total + if store_wallet.balance != 0: + invoice_details.append( + schemas_myeclpay.InvoiceDetailBase( + invoice_id=invoice_id, + store_id=store.id, + store_name=store.name, + wallet_id=store_wallet.id, + total=store_wallet.balance, + ), + ) + if not invoice_details: + raise HTTPException( + status_code=400, + detail="No invoice to create", + ) + last_structure_invoice = await cruds_myeclpay.get_last_structure_invoice( + structure_id=structure_id, + db=db, + ) + last_invoice_number = ( + int(last_structure_invoice.reference[-4:]) + if last_structure_invoice + and int(last_structure_invoice.reference[5:9]) == security_now.year + else 0 + ) + invoice = schemas_myeclpay.InvoiceInfo( + id=invoice_id, + reference=f"PAY{security_now.year}{structure.short_id}{last_invoice_number + 1:04d}", + structure_id=structure_id, + creation=datetime.now(UTC), + start_date=last_structure_invoice.end_date + if last_structure_invoice + else structure.creation, + end_date=security_now, + total=sum(detail.total for detail in invoice_details), + details=invoice_details, + ) + await cruds_myeclpay.create_invoice( + invoice=invoice, + db=db, + ) + invoice_db = await cruds_myeclpay.get_invoice_by_id( + invoice_id=invoice_id, + db=db, + ) + if invoice_db is None: + raise InvoiceNotFoundAfterCreationError(invoice_id=invoice_id) + + context = { + "invoice": invoice_db.model_dump(), + "payment_name": "MyECLPay", + "holder_coordinates": { + "name": bank_holder_structure.name, + "address_street": bank_holder_structure.siege_address_street, + "address_city": bank_holder_structure.siege_address_city, + "address_zipcode": bank_holder_structure.siege_address_zipcode, + "address_country": bank_holder_structure.siege_address_country, + "siret": bank_holder_structure.siret, + }, + } + await generate_pdf_from_template( + template_name="myeclpay_invoice.html", + directory="myeclpay/invoices", + filename=invoice.id, + context=context, + ) + return invoice_db + + +@router.patch( + "/myeclpay/invoices/{invoice_id}/paid", + status_code=204, +) +async def update_invoice_paid_status( + invoice_id: UUID, + paid: bool, + db: AsyncSession = Depends(get_db), + user: CoreUser = Depends(is_user_bank_account_holder), +): + """ + Update the paid status of a structure invoice. + + **The user must be the bank account holder** + """ + hyperion_error_logger.debug( + f"User {user.id} requested to update the paid status of invoice {invoice_id} to {paid}", + ) + invoice = await cruds_myeclpay.get_invoice_by_id( + invoice_id=invoice_id, + db=db, + ) + if invoice is None: + raise HTTPException( + status_code=404, + detail="Invoice does not exist", + ) + await cruds_myeclpay.update_invoice_paid_status( + invoice_id=invoice.id, + paid=paid, + db=db, + ) + + +@router.patch( + "/myeclpay/invoices/{invoice_id}/received", + status_code=204, +) +async def aknowledge_invoice_as_received( + invoice_id: UUID, + db: AsyncSession = Depends(get_db), + user: CoreUser = Depends(is_user()), +): + """ + Update the received status of a structure invoice. + + **The user must be the structure manager** + """ + invoice = await cruds_myeclpay.get_invoice_by_id( + invoice_id=invoice_id, + db=db, + ) + if invoice is None: + raise HTTPException( + status_code=404, + detail="Invoice does not exist", + ) + if not invoice.paid: + raise HTTPException( + status_code=400, + detail="Cannot mark an invoice as received if it is not paid", + ) + if invoice.received: + raise HTTPException( + status_code=400, + detail="Invoice is already marked as received", + ) + structure = await cruds_myeclpay.get_structure_by_id( + structure_id=invoice.structure_id, + db=db, + ) + if structure is None: + raise HTTPException( + status_code=500, + detail="Structure does not exist", + ) + if structure.manager_user_id != user.id: + raise HTTPException( + status_code=403, + detail="User is not allowed to edit this structure invoice", + ) + + await cruds_myeclpay.update_invoice_received_status( + invoice_id=invoice.id, + db=db, + ) + for detail in invoice.details: + store = await cruds_myeclpay.get_store( + store_id=detail.store_id, + db=db, + ) + if store is None: + hyperion_error_logger.error( + "MyECLPay: Could not find store associated with an invoice, this should never happen", + ) + raise HTTPException( + status_code=500, + detail="Could not find store associated with the invoice", + ) + await cruds_myeclpay.increment_wallet_balance( + wallet_id=store.wallet_id, + amount=-detail.total, + db=db, + ) + await cruds_myeclpay.add_withdrawal( + schemas_myeclpay.Withdrawal( + id=uuid.uuid4(), + wallet_id=store.wallet_id, + total=detail.total, + creation=datetime.now(UTC), + ), + db=db, + ) + + hyperion_myeclpay_logger.info( + format_withdrawal_log( + wallet_id=store.wallet_id, + total=detail.total, + ), + extra={ + "s3_subfolder": MYECLPAY_LOGS_S3_SUBFOLDER, + "s3_retention": RETENTION_DURATION, + }, + ) + + +@router.delete( + "/myeclpay/invoices/{invoice_id}", + status_code=204, +) +async def delete_structure_invoice( + invoice_id: UUID, + db: AsyncSession = Depends(get_db), + user: CoreUser = Depends(is_user_bank_account_holder), +): + """ + Delete a structure invoice. + + **The user must be the bank account holder** + """ + invoice = await cruds_myeclpay.get_invoice_by_id( + invoice_id=invoice_id, + db=db, + ) + if invoice is None: + raise HTTPException( + status_code=404, + detail="Invoice does not exist", + ) + if invoice.paid: + raise HTTPException( + status_code=400, + detail="Cannot delete an invoice that has already been paid", + ) + + await cruds_myeclpay.delete_invoice( + invoice_id=invoice.id, + db=db, + ) + + @router.get( "/myeclpay/integrity-check", status_code=200, diff --git a/app/core/myeclpay/exceptions_myeclpay.py b/app/core/myeclpay/exceptions_myeclpay.py index 95f720147f..1d639cc02d 100644 --- a/app/core/myeclpay/exceptions_myeclpay.py +++ b/app/core/myeclpay/exceptions_myeclpay.py @@ -9,3 +9,23 @@ class WalletNotFoundOnUpdateError(Exception): def __init__(self, wallet_id: UUID): super().__init__(f"Wallet {wallet_id} not found when updating") + + +class InvoiceNotFoundAfterCreationError(Exception): + """ + Exception raised when an invoice is not found after its creation. + This should lead to an internal server error response and a rollback of the transaction. + """ + + def __init__(self, invoice_id: UUID): + super().__init__(f"Invoice {invoice_id} not found after creation") + + +class ReferencedStructureNotFoundError(Exception): + """ + Exception raised when a referenced structure is not found. + This should lead to an internal server error response and a rollback of the transaction. + """ + + def __init__(self, structure_id: UUID): + super().__init__(f"Referenced structure {structure_id} not found") diff --git a/app/core/myeclpay/factory_myeclpay.py b/app/core/myeclpay/factory_myeclpay.py new file mode 100644 index 0000000000..c727af281b --- /dev/null +++ b/app/core/myeclpay/factory_myeclpay.py @@ -0,0 +1,131 @@ +import random +import uuid +from datetime import UTC, datetime, timedelta + +from faker import Faker +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.myeclpay import cruds_myeclpay, models_myeclpay, schemas_myeclpay +from app.core.myeclpay.types_myeclpay import WalletType +from app.core.users.factory_users import CoreUsersFactory +from app.core.utils.config import Settings +from app.types.factory import Factory + +faker = Faker() + +OTHER_STRUCTURES = 2 + + +class MyECLPayFactory(Factory): + depends_on = [CoreUsersFactory] + + demo_structures_id: list[uuid.UUID] + other_structures_id: list[uuid.UUID] + + other_stores_id: list[list[uuid.UUID]] = [] + other_stores_wallet_id: list[list[uuid.UUID]] = [] + + @classmethod + async def create_structures(cls, db: AsyncSession): + cls.demo_structures_id = [uuid.uuid4() for _ in CoreUsersFactory.demo_users_id] + for i, user_id in enumerate(CoreUsersFactory.demo_users_id): + await cruds_myeclpay.create_structure( + schemas_myeclpay.StructureSimple( + id=uuid.uuid4(), + short_id="".join(faker.random_letters(3)).upper(), + name=CoreUsersFactory.demo_users[i].nickname, + manager_user_id=user_id, + siege_address_street=faker.street_address(), + siege_address_city=faker.city(), + siege_address_zipcode=faker.postcode(), + siege_address_country=faker.country(), + iban=faker.iban(), + bic="".join(faker.random_letters(11)).upper(), + creation=datetime.now(UTC), + ), + db, + ) + cls.other_structures_id = [uuid.uuid4() for _ in range(OTHER_STRUCTURES)] + for i, structure_id in enumerate(cls.other_structures_id): + await cruds_myeclpay.create_structure( + schemas_myeclpay.StructureSimple( + id=structure_id, + short_id="".join(faker.random_letters(3)).upper(), + name=faker.company(), + manager_user_id=CoreUsersFactory.other_users_id[i], + siege_address_street=faker.street_address(), + siege_address_city=faker.city(), + siege_address_zipcode=faker.postcode(), + siege_address_country=faker.country(), + iban=faker.iban(), + bic="".join(faker.random_letters(11)).upper(), + creation=datetime.now(UTC), + ), + db, + ) + + @classmethod + async def create_other_structures_stores(cls, db: AsyncSession): + for structure_id in cls.other_structures_id: + structure_store_ids = [] + structure_wallet_ids = [] + for _ in range(random.randint(2, 4)): # noqa: S311 + store_id = uuid.uuid4() + wallet_id = uuid.uuid4() + structure_store_ids.append(store_id) + structure_wallet_ids.append(wallet_id) + await cruds_myeclpay.create_wallet( + wallet_id=wallet_id, + wallet_type=WalletType.STORE, + balance=100000, + db=db, + ) + await cruds_myeclpay.create_store( + models_myeclpay.Store( + id=store_id, + structure_id=structure_id, + name=faker.company(), + creation=datetime.now(UTC), + wallet_id=wallet_id, + ), + db, + ) + cls.other_stores_id.append(structure_store_ids) + cls.other_stores_wallet_id.append(structure_wallet_ids) + + @classmethod + async def create_other_structures_invoices(cls, db: AsyncSession): + for i, structure_id in enumerate(cls.other_structures_id): + invoice_id = uuid.uuid4() + await cruds_myeclpay.create_invoice( + schemas_myeclpay.InvoiceInfo( + id=invoice_id, + structure_id=structure_id, + reference=faker.bothify(text="MYPAY2025???####"), + total=1000 * len(cls.other_stores_id[i]), + paid=False, + received=False, + start_date=datetime.now(UTC) - timedelta(days=30), + end_date=datetime.now(UTC), + creation=datetime.now(UTC), + details=[ + schemas_myeclpay.InvoiceDetailBase( + invoice_id=invoice_id, + store_id=store_id, + total=1000, + ) + for store_id in cls.other_stores_id[i] + ], + ), + db, + ) + + @classmethod + async def run(cls, db: AsyncSession, settings: Settings) -> None: + await cls.create_structures(db) + await cls.create_other_structures_stores(db) + await cls.create_other_structures_invoices(db) + + @classmethod + async def should_run(cls, db: AsyncSession): + return len(await cruds_myeclpay.get_structures(db=db)) == 0 diff --git a/app/core/myeclpay/integrity_myeclpay.py b/app/core/myeclpay/integrity_myeclpay.py index 1a129f41b7..a296dc3bea 100644 --- a/app/core/myeclpay/integrity_myeclpay.py +++ b/app/core/myeclpay/integrity_myeclpay.py @@ -43,3 +43,10 @@ def format_cancel_log( transaction_id: UUID, ): return f"{ActionType.CANCEL.name} {transaction_id}" + + +def format_withdrawal_log( + wallet_id: UUID, + total: int, +): + return f"{ActionType.WITHDRAWAL.name} {wallet_id} {total}" diff --git a/app/core/myeclpay/models_myeclpay.py b/app/core/myeclpay/models_myeclpay.py index 9bad1b30ba..77dfb29a6b 100644 --- a/app/core/myeclpay/models_myeclpay.py +++ b/app/core/myeclpay/models_myeclpay.py @@ -120,8 +120,17 @@ class Structure(Base): __tablename__ = "myeclpay_structure" id: Mapped[PrimaryKey] + short_id: Mapped[str] = mapped_column(unique=True) name: Mapped[str] = mapped_column(unique=True) + siege_address_street: Mapped[str] + siege_address_city: Mapped[str] + siege_address_zipcode: Mapped[str] + siege_address_country: Mapped[str] + siret: Mapped[str | None] + iban: Mapped[str] + bic: Mapped[str] manager_user_id: Mapped[str] = mapped_column(ForeignKey("core_user.id")) + creation: Mapped[datetime] association_membership_id: Mapped[UUID | None] = mapped_column( ForeignKey("core_association_membership.id"), default=None, @@ -159,6 +168,7 @@ class Store(Base): ForeignKey("myeclpay_wallet.id"), unique=True, ) + creation: Mapped[datetime] structure: Mapped[Structure] = relationship(init=False, lazy="joined") @@ -240,3 +250,51 @@ class UsedQRCode(Base): qr_code_key: Mapped[UUID | None] qr_code_store: Mapped[bool | None] signature: Mapped[str | None] + + +class Invoice(Base): + __tablename__ = "myeclpay_invoice" + + id: Mapped[PrimaryKey] + reference: Mapped[str] = mapped_column(unique=True) + creation: Mapped[datetime] + start_date: Mapped[datetime] + end_date: Mapped[datetime] + total: Mapped[int] # Stored in cents + structure_id: Mapped[UUID] = mapped_column(ForeignKey("myeclpay_structure.id")) + paid: Mapped[bool] = mapped_column(default=False) + received: Mapped[bool] = mapped_column(default=False) + + details: Mapped[list["InvoiceDetail"]] = relationship( + init=False, + lazy="selectin", + ) + structure: Mapped[Structure] = relationship( + init=False, + lazy="joined", + ) + + +class InvoiceDetail(Base): + __tablename__ = "myeclpay_invoice_detail" + + invoice_id: Mapped[UUID] = mapped_column( + ForeignKey("myeclpay_invoice.id"), + primary_key=True, + ) + store_id: Mapped[UUID] = mapped_column( + ForeignKey("myeclpay_store.id"), + primary_key=True, + ) + total: Mapped[int] # Stored in cents + + store: Mapped[Store] = relationship(init=False, lazy="joined") + + +class Withdrawal(Base): + __tablename__ = "myeclpay_withdrawal" + + id: Mapped[PrimaryKey] + wallet_id: Mapped[UUID] = mapped_column(ForeignKey("myeclpay_wallet.id")) + total: Mapped[int] # Stored in cents + creation: Mapped[datetime] diff --git a/app/core/myeclpay/schemas_myeclpay.py b/app/core/myeclpay/schemas_myeclpay.py index c6e87fb494..fcb4ffd37f 100644 --- a/app/core/myeclpay/schemas_myeclpay.py +++ b/app/core/myeclpay/schemas_myeclpay.py @@ -4,6 +4,7 @@ from pydantic import ( BaseModel, Field, + model_validator, ) from app.core.memberships import schemas_memberships @@ -19,13 +20,29 @@ class StructureBase(BaseModel): + short_id: str = Field( + min_length=3, + max_length=3, + description="Short ID of the structure, used for invoices", + ) name: str association_membership_id: UUID | None = None manager_user_id: str + siege_address_street: str + siege_address_city: str + siege_address_zipcode: str + siege_address_country: str + siret: str | None = None + iban: str + bic: str -class Structure(StructureBase): +class StructureSimple(StructureBase): id: UUID + creation: datetime + + +class Structure(StructureSimple): manager_user: schemas_users.CoreUserSimple association_membership: schemas_memberships.MembershipSimple | None @@ -33,6 +50,13 @@ class Structure(StructureBase): class StructureUpdate(BaseModel): name: str | None = None association_membership_id: UUID | None = None + siret: str | None = None + siege_address_street: str | None = None + siege_address_city: str | None = None + siege_address_zipcode: str | None = None + siege_address_country: str | None = None + iban: str | None = None + bic: str | None = None class StructureTranfert(BaseModel): @@ -43,10 +67,14 @@ class StoreBase(BaseModel): name: str -class Store(StoreBase): +class StoreSimple(StoreBase): id: UUID structure_id: UUID wallet_id: UUID + creation: datetime + + +class Store(StoreSimple): structure: Structure @@ -259,3 +287,57 @@ class IntegrityCheckData(BaseModel): transactions: list[TransactionBase] transfers: list[Transfer] refunds: list[RefundBase] + + +class BankAccountHolderEdit(BaseModel): + holder_user_id: str + + +class InvoiceDetailBase(BaseModel): + invoice_id: UUID + store_id: UUID + total: int # Stored in cents + + +class InvoiceDetail(InvoiceDetailBase): + store: StoreSimple + + +class InvoiceBase(BaseModel): + id: UUID + reference: str + structure_id: UUID + creation: datetime + start_date: datetime + end_date: datetime + total: int # Stored in cents + paid: bool = False + received: bool = False + + +class InvoiceInfo(InvoiceBase): + details: list[InvoiceDetailBase] + + @model_validator(mode="after") + def validate_sum(self): + if sum(detail.total for detail in self.details) != self.total: + raise ValueError + return self + + +class Invoice(InvoiceBase): + structure: Structure + details: list[InvoiceDetail] + + @model_validator(mode="after") + def validate_details(self): + if sum(detail.total for detail in self.details) != self.total: + raise ValueError + return self + + +class Withdrawal(BaseModel): + id: UUID + wallet_id: UUID + total: int # Stored in cents + creation: datetime diff --git a/app/core/myeclpay/types_myeclpay.py b/app/core/myeclpay/types_myeclpay.py index d8d89545fe..2fce885bc4 100644 --- a/app/core/myeclpay/types_myeclpay.py +++ b/app/core/myeclpay/types_myeclpay.py @@ -56,6 +56,7 @@ class ActionType(str, Enum): REFUND = "refund" CANCEL = "cancel" TRANSACTION = "transaction" + WITHDRAWAL = "withdrawal" class UnexpectedError(Exception): diff --git a/app/core/myeclpay/utils_myeclpay.py b/app/core/myeclpay/utils_myeclpay.py index 94a49212ba..73464fd555 100644 --- a/app/core/myeclpay/utils_myeclpay.py +++ b/app/core/myeclpay/utils_myeclpay.py @@ -6,7 +6,8 @@ from cryptography.hazmat.primitives.asymmetric import ed25519 from sqlalchemy.ext.asyncio import AsyncSession -from app.core.myeclpay import cruds_myeclpay +from app.core.memberships import schemas_memberships +from app.core.myeclpay import cruds_myeclpay, models_myeclpay, schemas_myeclpay from app.core.myeclpay.integrity_myeclpay import format_transfer_log from app.core.myeclpay.models_myeclpay import UserPayment from app.core.myeclpay.schemas_myeclpay import ( @@ -18,6 +19,7 @@ TransferTotalDontMatchInCallbackError, ) from app.core.payment import schemas_payment +from app.core.users import schemas_users hyperion_security_logger = logging.getLogger("hyperion.security") hyperion_myeclpay_logger = logging.getLogger("hyperion.myeclpay") @@ -116,3 +118,121 @@ async def validate_transfer_callback( "s3_retention": RETENTION_DURATION, }, ) + + +def structure_model_to_schema( + structure: models_myeclpay.Structure, +) -> schemas_myeclpay.Structure: + """ + Convert a structure model to a schema. + """ + return schemas_myeclpay.Structure( + id=structure.id, + short_id=structure.short_id, + name=structure.name, + association_membership_id=structure.association_membership_id, + association_membership=schemas_memberships.MembershipSimple( + id=structure.association_membership.id, + name=structure.association_membership.name, + manager_group_id=structure.association_membership.manager_group_id, + ) + if structure.association_membership + else None, + manager_user_id=structure.manager_user_id, + manager_user=schemas_users.CoreUserSimple( + id=structure.manager_user.id, + firstname=structure.manager_user.firstname, + name=structure.manager_user.name, + nickname=structure.manager_user.nickname, + account_type=structure.manager_user.account_type, + school_id=structure.manager_user.school_id, + ), + siret=structure.siret, + siege_address_street=structure.siege_address_street, + siege_address_city=structure.siege_address_city, + siege_address_zipcode=structure.siege_address_zipcode, + siege_address_country=structure.siege_address_country, + iban=structure.iban, + bic=structure.bic, + creation=structure.creation, + ) + + +def refund_model_to_schema( + refund: models_myeclpay.Refund, +) -> schemas_myeclpay.Refund: + """ + Convert a refund model to a schema. + """ + return schemas_myeclpay.Refund( + id=refund.id, + transaction_id=refund.transaction_id, + credited_wallet_id=refund.credited_wallet_id, + debited_wallet_id=refund.debited_wallet_id, + total=refund.total, + creation=refund.creation, + seller_user_id=refund.seller_user_id, + transaction=schemas_myeclpay.Transaction( + id=refund.transaction.id, + debited_wallet_id=refund.transaction.debited_wallet_id, + credited_wallet_id=refund.transaction.credited_wallet_id, + transaction_type=refund.transaction.transaction_type, + seller_user_id=refund.transaction.seller_user_id, + total=refund.transaction.total, + creation=refund.transaction.creation, + status=refund.transaction.status, + ), + debited_wallet=schemas_myeclpay.WalletInfo( + id=refund.debited_wallet.id, + type=refund.debited_wallet.type, + owner_name=refund.debited_wallet.store.name + if refund.debited_wallet.store + else refund.debited_wallet.user.full_name + if refund.debited_wallet.user + else None, + ), + credited_wallet=schemas_myeclpay.WalletInfo( + id=refund.credited_wallet.id, + type=refund.credited_wallet.type, + owner_name=refund.credited_wallet.store.name + if refund.credited_wallet.store + else refund.credited_wallet.user.full_name + if refund.credited_wallet.user + else None, + ), + ) + + +def invoice_model_to_schema( + invoice: models_myeclpay.Invoice, +) -> schemas_myeclpay.Invoice: + """ + Convert an invoice model to a schema. + """ + return schemas_myeclpay.Invoice( + id=invoice.id, + reference=invoice.reference, + structure_id=invoice.structure_id, + creation=invoice.creation, + start_date=invoice.start_date, + end_date=invoice.end_date, + total=invoice.total, + paid=invoice.paid, + received=invoice.received, + structure=structure_model_to_schema(invoice.structure), + details=[ + schemas_myeclpay.InvoiceDetail( + invoice_id=invoice.id, + store_id=detail.store_id, + total=detail.total, + store=schemas_myeclpay.StoreSimple( + id=detail.store.id, + name=detail.store.name, + structure_id=detail.store.structure_id, + wallet_id=detail.store.wallet_id, + creation=detail.store.creation, + ), + ) + for detail in invoice.details + ], + ) diff --git a/app/dependencies.py b/app/dependencies.py index 07c32266a9..0564433ee9 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -334,7 +334,7 @@ async def get_current_user_id( The expected scopes are passed as list of list of scopes, each list of scopes is an "AND" condition, and the list of list of scopes is an "OR" condition. """ - return await auth_utils.get_user_id_from_token_with_scopes( + return auth_utils.get_user_id_from_token_with_scopes( scopes=scopes, token_data=token_data, ) diff --git a/app/types/websocket.py b/app/types/websocket.py index dd6ba0e750..b51c0d335f 100644 --- a/app/types/websocket.py +++ b/app/types/websocket.py @@ -257,7 +257,7 @@ async def manage_websocket( request_id="websocket", ) - user_id = await auth_utils.get_user_id_from_token_with_scopes( + user_id = auth_utils.get_user_id_from_token_with_scopes( scopes=[[ScopeType.API]], token_data=token_data, ) diff --git a/app/utils/auth/auth_utils.py b/app/utils/auth/auth_utils.py index 2ce76af9bd..3948f70e32 100644 --- a/app/utils/auth/auth_utils.py +++ b/app/utils/auth/auth_utils.py @@ -59,7 +59,7 @@ def get_token_data( return token_data -async def get_user_id_from_token_with_scopes( +def get_user_id_from_token_with_scopes( scopes: list[list[ScopeType]], token_data: schemas_auth.TokenData, ) -> str: diff --git a/assets/templates/myeclpay_invoice.html b/assets/templates/myeclpay_invoice.html new file mode 100644 index 0000000000..723686094a --- /dev/null +++ b/assets/templates/myeclpay_invoice.html @@ -0,0 +1,147 @@ + + +
+ +
+ {{invoice.structure.siege_address_street}}
+ {{invoice.structure.siege_address_zipcode}}
+ {{invoice.structure.siege_address_city}}
+ {{invoice.structure.siege_address_country}}
+ {% if invoice.structure.siret %} SIRET :
+ {{invoice.structure.siret}}
+ {% endif %}
+
Facture : {{invoice.reference}}
++ {{invoice.structure.siege_address_city}}, le + {{invoice.creation.strftime("%d/%m/%Y")}} +
+
+ {{holder_coordinates.name}}
+ {{holder_coordinates.address_street}}
+ {{holder_coordinates.address_zipcode}}
+ {{holder_coordinates.address_city}}
+ {{holder_coordinates.address_country}}
+ {% if holder_coordinates.siret %} SIRET: {{holder_coordinates.siret}}
+ {% endif %}
+
| Description | +Total | +
|---|---|
| + Solde {{payment_name}} du + {{invoice.start_date.strftime("%d/%m/%Y")}} au + {{invoice.end_date.strftime("%d/%m/%Y")}} + | +{{invoice.total/100}} € | +
| Total : | +{{invoice.total/100}} € | +
Par virement :
+| IBAN : | +{{invoice.structure.iban}} | +
| BIC : | +{{invoice.structure.bic}} | +
Délai de paiement : 30 jours
++ En cas de retard de paiement, seront exigibles, conformément à l'article + L 441-6 du code de commerce, une indemnité calculée sur la base de trois + fois le taux de l'intérêt légal en vigueur ainsi qu'une indemnité + forfaitaire pour frais de recouvrement de 40 euros. +
+Période du 10/01/2025 au 15/02/2025
+| Magasin | +Total | +
|---|---|
| {{detail.store.name}} | +{{detail.total/100}} | +