Skip to content
3 changes: 3 additions & 0 deletions .github/workflows/lintandformat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ jobs:
ruff check --output-format=github
ruff format --check

- name: Type checking using Ty
run: ty check --output-format=github --error=all

- name: Cache .mypy_cache folder
id: mypy_cache
uses: actions/cache@v4.3.0
Expand Down
2 changes: 1 addition & 1 deletion app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[LifespanState]:
use_route_path_as_operation_ids(app)

app.add_middleware(
CORSMiddleware, # ty:ignore[invalid-argument-type]
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
Expand Down
178 changes: 85 additions & 93 deletions app/core/checkout/endpoints_checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
import uuid

from fastapi import APIRouter, Depends, HTTPException, Request
from helloasso_python.models.hello_asso_api_v5_models_api_notifications_api_notification_type import (
HelloAssoApiV5ModelsApiNotificationsApiNotificationType,
)
from pydantic import TypeAdapter, ValidationError
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.checkout import cruds_checkout, models_checkout, schemas_checkout
from app.core.checkout.types_checkout import (
FormNotificationResultContent,
NotificationResultContent,
OrderNotificationResultContent,
OrganizationNotificationResultContent,
PayementNotificationResultContent,
)
from app.dependencies import get_db
from app.module import all_modules
Expand Down Expand Up @@ -62,103 +63,94 @@ async def webhook(
status_code=400,
detail="Could not validate the webhook body",
)
if (
content.eventType
== HelloAssoApiV5ModelsApiNotificationsApiNotificationType.ORDER
):
pass
if (
content.eventType
== HelloAssoApiV5ModelsApiNotificationsApiNotificationType.PAYMENT
):
# We may receive the webhook multiple times, we only want to save a CheckoutPayment
# in the database the first time
existing_checkout_payment_model = (
await cruds_checkout.get_checkout_payment_by_hello_asso_payment_id(
hello_asso_payment_id=content.data.id, # ty:ignore[unresolved-attribute]
db=db,
)
)
if existing_checkout_payment_model is not None:
hyperion_error_logger.debug(
f"Payment: ignoring webhook call for helloasso checkout payment id {content.data.id} as it already exists in the database", # ty:ignore[unresolved-attribute]
match content:
case OrganizationNotificationResultContent():
pass
case OrderNotificationResultContent():
pass
case FormNotificationResultContent():
pass
case PayementNotificationResultContent():
# We may receive the webhook multiple times, we only want to save a CheckoutPayment
# in the database the first time
existing_checkout_payment_model = (
await cruds_checkout.get_checkout_payment_by_hello_asso_payment_id(
hello_asso_payment_id=content.data.id,
db=db,
)
)
return
if existing_checkout_payment_model is not None:
hyperion_error_logger.debug(
f"Payment: ignoring webhook call for helloasso checkout payment id {content.data.id} as it already exists in the database",
)
return

# If no metadata are included, this should not be a checkout we initiated
if not checkout_metadata:
hyperion_error_logger.info(
"Payment: missing checkout_metadata",
)
return
# If no metadata are included, this should not be a checkout we initiated
if not checkout_metadata:
hyperion_error_logger.info(
"Payment: missing checkout_metadata",
)
return

checkout = await cruds_checkout.get_checkout_by_id(
checkout_id=uuid.UUID(checkout_metadata.hyperion_checkout_id),
db=db,
)
# If a metadata with a checkout was present in the request but we can not find the checkout,
# we should raise an error
if not checkout:
hyperion_error_logger.error(
f"Payment: could not find checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}) in database for payment HelloAsso payment_id: {content.data.id}", # ty:ignore[unresolved-attribute]
)
raise HTTPException(
status_code=400,
detail=f"Could not find checkout {checkout_metadata.hyperion_checkout_id} in database",
checkout = await cruds_checkout.get_checkout_by_id(
checkout_id=uuid.UUID(checkout_metadata.hyperion_checkout_id),
db=db,
)
# If a metadata with a checkout was present in the request but we can not find the checkout,
# we should raise an error
if not checkout:
hyperion_error_logger.error(
f"Payment: could not find checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}) in database for payment HelloAsso payment_id: {content.data.id}",
)
raise HTTPException(
status_code=400,
detail=f"Could not find checkout {checkout_metadata.hyperion_checkout_id} in database",
)

if checkout.secret != checkout_metadata.secret:
hyperion_error_logger.error(
f"Payment: secret mismatch for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id})",
if checkout.secret != checkout_metadata.secret:
hyperion_error_logger.error(
f"Payment: secret mismatch for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id})",
)
raise HTTPException(
status_code=400,
detail="Secret mismatch",
)

checkout_payment_model = models_checkout.CheckoutPayment(
id=uuid.uuid4(),
checkout_id=checkout.id,
paid_amount=content.data.amount,
tip_amount=content.data.amountTip,
hello_asso_payment_id=content.data.id,
)
raise HTTPException(
status_code=400,
detail="Secret mismatch",
await cruds_checkout.create_checkout_payment(
checkout_payment=checkout_payment_model,
db=db,
)

checkout_payment_model = models_checkout.CheckoutPayment(
id=uuid.uuid4(),
checkout_id=checkout.id,
paid_amount=content.data.amount, # ty:ignore[unresolved-attribute]
tip_amount=content.data.amountTip, # ty:ignore[unresolved-attribute]
hello_asso_payment_id=content.data.id, # ty:ignore[unresolved-attribute]
)
await cruds_checkout.create_checkout_payment(
checkout_payment=checkout_payment_model,
db=db,
)

hyperion_error_logger.info(
f"Payment: checkout payment added to db for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id})",
)

# If a callback is defined for the module, we want to call it
try:
for module in all_modules:
if module.root == checkout.module:
if module.checkout_callback is None:
hyperion_error_logger.info(
f"Payment: module {checkout.module} does not define a payment callback for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id})",
)
return
hyperion_error_logger.info(
f"Payment: calling module {checkout.module} payment callback",
)
checkout_payment_schema = schemas_checkout.CheckoutPayment(
id=checkout_payment_model.id,
paid_amount=checkout_payment_model.paid_amount,
checkout_id=checkout_payment_model.checkout_id,
)
await module.checkout_callback(checkout_payment_schema, db)
hyperion_error_logger.info(
f"Payment: call to module {checkout.module} payment callback for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id}) succeeded",
)
return

hyperion_error_logger.info(
f"Payment: callback for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id}) was not called for module {checkout.module}",
)
except Exception:
hyperion_error_logger.exception(
f"Payment: call to module {checkout.module} payment callback for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id}) failed",
f"Payment: checkout payment added to db for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id})",
)

# If a callback is defined for the module, we want to call it
try:
for module in all_modules:
if module.root == checkout.module:
if module.checkout_callback is not None:
hyperion_error_logger.info(
f"Payment: calling module {checkout.module} payment callback",
)
checkout_payment_schema = (
schemas_checkout.CheckoutPayment.model_validate(
checkout_payment_model.__dict__,
)
)
await module.checkout_callback(checkout_payment_schema, db)
hyperion_error_logger.info(
f"Payment: call to module {checkout.module} payment callback for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id}) succeeded",
)
return
except Exception:
hyperion_error_logger.exception(
f"Payment: call to module {checkout.module} payment callback for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id}) failed",
)
45 changes: 24 additions & 21 deletions app/core/checkout/payment_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,16 @@ async def init_checkout(
# Thus we catch any exception and log it, then reraise it
try:
payer: HelloAssoApiV5ModelsCartsCheckoutPayer | None = None
if payer_user:
if payer_user is not None:
payer = HelloAssoApiV5ModelsCartsCheckoutPayer(
first_name=payer_user.firstname,
last_name=payer_user.name,
email=payer_user.email,
date_of_birth=datetime.combine(
payer_user.birthday,
datetime.min.time(),
).replace(tzinfo=UTC)
tzinfo=UTC,
)
if payer_user.birthday
else None,
)
Expand All @@ -193,7 +194,7 @@ async def init_checkout(
secret=secret,
hyperion_checkout_id=str(checkout_model_id),
).model_dump(),
) # ty:ignore[missing-argument]
) # ty:ignore[missing-argument] # See https://github.com/astral-sh/ty/issues/1438

response: HelloAssoApiV5ModelsCartsInitCheckoutResponse
with ApiClient(configuration) as api_client:
Expand All @@ -211,25 +212,27 @@ async def init_checkout(
hyperion_error_logger.exception(
f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name} (no payer info provided).",
)
else:
payer_user_name = f"{payer_user.firstname} {payer_user.name}"
hyperion_error_logger.warning(
f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}. Retrying without payer infos for {payer_user_name}",
raise

payer_user_name = f"{payer_user.firstname} {payer_user.name}"
hyperion_error_logger.warning(
f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}. Retrying without payer infos for {payer_user_name}",
)

init_checkout_body.payer = None
try:
response = checkout_api.organizations_organization_slug_checkout_intents_post(
self._helloasso_slug,
init_checkout_body,
)
except UnauthorizedException:
# HelloAsso returned a 401 unauthorized again
hyperion_error_logger.exception(
f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}, with and without payer {payer_user_name} infos",
)
raise

init_checkout_body.payer = None
try:
response = checkout_api.organizations_organization_slug_checkout_intents_post(
self._helloasso_slug,
init_checkout_body,
)
except UnauthorizedException:
# HelloAsso returned a 401 unauthorized again
hyperion_error_logger.exception(
f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}, with and without payer {payer_user_name} infos",
)

if response and response.id:
if response and response.id and response.redirect_url:
checkout_model = models_checkout.Checkout(
id=checkout_model_id,
module=module,
Expand All @@ -246,7 +249,7 @@ async def init_checkout(
payment_url=response.redirect_url or "",
)
hyperion_error_logger.error(
f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}. No checkout id returned",
f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}. No checkout id or redirect URL returned",
)
raise MissingHelloAssoCheckoutIdError() # noqa: TRY301

Expand Down
8 changes: 4 additions & 4 deletions app/core/google_api/google_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ def create_folder(self, folder_name: str, parent_folder_id: GoogleId) -> GoogleI
"mimeType": "application/vnd.google-apps.folder",
"parents": [parent_folder_id],
}
response = self._drive.files().create(body=file_metadata).execute()
response = self._drive.files().create(body=file_metadata).execute() # ty:ignore[unresolved-attribute]
folder_id: GoogleId = response.get("id")
return folder_id

Expand All @@ -270,7 +270,7 @@ async def upload_file(
mimetype=mimetype,
)
response = (
self._drive.files().create(body=file_metadata, media_body=media).execute()
self._drive.files().create(body=file_metadata, media_body=media).execute() # ty:ignore[unresolved-attribute]
)
uploaded_file_id: GoogleId = response.get("id")
return uploaded_file_id
Expand All @@ -288,12 +288,12 @@ def replace_file(
mimetype="application/pdf",
)
response = (
self._drive.files()
self._drive.files() # ty:ignore[unresolved-attribute]
.update(fileId=file_id, body=file_metadata, media_body=media)
.execute()
)
result: GoogleId = response.get("id")
return result

def delete_file(self, file_id: GoogleId) -> None:
self._drive.files().delete(fileId=file_id).execute()
self._drive.files().delete(fileId=file_id).execute() # ty:ignore[unresolved-attribute]
3 changes: 3 additions & 0 deletions app/core/mypayment/endpoints_mypayment.py
Original file line number Diff line number Diff line change
Expand Up @@ -1861,6 +1861,8 @@ async def get_user_wallet_history(
transaction_type = HistoryType.DIRECT_TRANSACTION
elif transaction.transaction_type == TransactionType.REQUEST:
transaction_type = HistoryType.REQUEST_TRANSACTION
else:
raise UnexpectedError("Unknown transaction type") # noqa: TRY003
if transaction.credited_wallet_id == user_payment.wallet_id:
# The user received the transaction
direction = HistoryDirection.CREDITED
Expand Down Expand Up @@ -2596,6 +2598,7 @@ async def refund_transaction(
)

if wallet_previously_credited.user is not None:
wallet_previously_debited_name: str = "Unknown"
if wallet_previously_debited.user is not None:
wallet_previously_debited_name = wallet_previously_debited.user.full_name
elif wallet_previously_debited.store is not None:
Expand Down
2 changes: 1 addition & 1 deletion app/core/utils/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pathlib import Path
from typing import Any

import uvicorn
import uvicorn.logging

from app.core.utils.config import Settings

Expand Down
Loading
Loading