11import logging
22import uuid
3+ from datetime import UTC , datetime
4+ from typing import TYPE_CHECKING
35
4- from helloasso_api_wrapper import HelloAssoAPIWrapper
5- from helloasso_api_wrapper .exceptions import ApiV5BadRequest
6- from helloasso_api_wrapper .models .carts import (
7- CheckoutPayer ,
8- InitCheckoutBody ,
9- InitCheckoutResponse ,
6+ from authlib .integrations .requests_client import OAuth2Session
7+ from helloasso_python .api .checkout_api import CheckoutApi
8+ from helloasso_python .api .paiements_api import PaiementsApi
9+ from helloasso_python .api_client import ApiClient
10+ from helloasso_python .configuration import Configuration
11+ from helloasso_python .models .hello_asso_api_v5_models_carts_checkout_payer import (
12+ HelloAssoApiV5ModelsCartsCheckoutPayer ,
13+ )
14+ from helloasso_python .models .hello_asso_api_v5_models_carts_init_checkout_body import (
15+ HelloAssoApiV5ModelsCartsInitCheckoutBody ,
1016)
1117from sqlalchemy .ext .asyncio import AsyncSession
1218
1319from app .core .payment import cruds_payment , models_payment , schemas_payment
1420from app .core .users import schemas_users
1521from app .core .utils import security
1622from app .core .utils .config import Settings
17- from app .types .exceptions import PaymentToolCredentialsNotSetException
23+ from app .types .exceptions import (
24+ MissingHelloAssoCheckoutIdError ,
25+ PaymentToolCredentialsNotSetException ,
26+ )
27+
28+ if TYPE_CHECKING :
29+ from helloasso_python .models .hello_asso_api_v5_models_carts_init_checkout_response import (
30+ HelloAssoApiV5ModelsCartsInitCheckoutResponse ,
31+ )
1832
1933hyperion_error_logger = logging .getLogger ("hyperion.error" )
2034
2135
2236class PaymentTool :
23- hello_asso : HelloAssoAPIWrapper | None
37+ _access_token : str | None = None
38+ _refresh_token : str | None = None
39+ _access_token_expiry : int | None = None
40+
41+ _auth_client : OAuth2Session | None = None
42+
43+ _hello_asso_api_base : str | None = None
2444
2545 def __init__ (self , settings : Settings ):
2646 if (
2747 settings .HELLOASSO_API_BASE
2848 and settings .HELLOASSO_CLIENT_ID
2949 and settings .HELLOASSO_CLIENT_SECRET
3050 ):
31- self .hello_asso = HelloAssoAPIWrapper (
32- api_base = settings .HELLOASSO_API_BASE ,
33- client_id = settings .HELLOASSO_CLIENT_ID ,
34- client_secret = settings .HELLOASSO_CLIENT_SECRET ,
35- timeout = 60 ,
51+ self ._hello_asso_api_base = settings .HELLOASSO_API_BASE
52+ self ._auth_client = OAuth2Session (
53+ settings .HELLOASSO_CLIENT_ID ,
54+ settings .HELLOASSO_CLIENT_SECRET ,
55+ token_endpoint = "https://"
56+ + settings .HELLOASSO_API_BASE
57+ + "/oauth2/token" ,
3658 )
3759 else :
3860 hyperion_error_logger .warning (
3961 "HelloAsso API credentials are not set, payment won't be available" ,
4062 )
41- self .hello_asso = None
63+
64+ def get_access_token (self ) -> str :
65+ if not self ._auth_client :
66+ raise PaymentToolCredentialsNotSetException
67+ # If the access token is not set, we get one
68+ if self ._access_token is None :
69+ try :
70+ tokens = self ._auth_client .fetch_token (grant_type = "client_credentials" )
71+ except Exception :
72+ hyperion_error_logger .exception (
73+ "Payment: failed to get HelloAsso access token" ,
74+ )
75+ raise
76+ self ._access_token = tokens ["access_token" ]
77+ self ._refresh_token = tokens ["refresh_token" ]
78+ self ._access_token_expiry = tokens ["expires_at" ]
79+ # If we have a token but it's expired, we need to refresh it
80+ elif self ._access_token_expiry is None or self ._access_token_expiry < int (
81+ datetime .now (UTC ).timestamp (),
82+ ):
83+ # Token is expired, we need to refresh it
84+ try :
85+ tokens = self ._auth_client .refresh_token (
86+ refresh_token = self ._refresh_token ,
87+ )
88+ except Exception :
89+ hyperion_error_logger .exception (
90+ "Payment: failed to refresh HelloAsso access token, getting a new one" ,
91+ )
92+ self ._access_token = None
93+ self ._refresh_token = None
94+ self ._access_token_expiry = None
95+ return self .get_access_token ()
96+
97+ return self ._access_token
98+
99+ def get_hello_asso_configuration (self ) -> Configuration :
100+ """
101+ Get a valid access token and construct an HelloAsso API configuration object
102+ """
103+ access_token = self .get_access_token ()
104+ if self ._hello_asso_api_base is None :
105+ raise PaymentToolCredentialsNotSetException
106+ return Configuration (
107+ host = "https://" + self ._hello_asso_api_base + "/v5" ,
108+ access_token = access_token ,
109+ retries = 3 ,
110+ )
42111
43112 def is_payment_available (self ) -> bool :
44113 """
@@ -47,7 +116,13 @@ def is_payment_available(self) -> bool:
47116 You should always call this method before trying to init a checkout
48117 If payment is not available, you usually should raise an HTTP Exception explaining that payment is disabled because the API credentials are not configured in settings.
49118 """
50- return self .hello_asso is not None
119+ return (
120+ self ._auth_client is not None
121+ and self ._hello_asso_api_base is not None
122+ and self ._access_token is not None
123+ and self ._access_token_expiry is not None
124+ and self ._refresh_token is not None
125+ )
51126
52127 async def init_checkout (
53128 self ,
@@ -76,17 +151,16 @@ async def init_checkout(
76151 payment_url: you need to redirect the user to this payment page
77152
78153 This method use HelloAsso API. It may raise exceptions if HA checkout initialization fails.
79- Exceptions can be imported from `helloasso_api_wrapper.exceptions`
154+ Exceptions can be imported from `helloasso_python` package.
80155 """
81- if not self .hello_asso :
82- raise PaymentToolCredentialsNotSetException
156+ configuration = self .get_hello_asso_configuration ()
83157
84158 # We want to ensure that any error is logged, even if modules tries to try/except this method
85159 # Thus we catch any exception and log it, then reraise it
86160 try :
87- payer : CheckoutPayer | None = None
161+ payer : HelloAssoApiV5ModelsCartsCheckoutPayer | None = None
88162 if payer_user :
89- payer = CheckoutPayer (
163+ payer = HelloAssoApiV5ModelsCartsCheckoutPayer (
90164 firstName = payer_user .firstname ,
91165 lastName = payer_user .name ,
92166 email = payer_user .email ,
@@ -96,14 +170,14 @@ async def init_checkout(
96170 checkout_model_id = uuid .uuid4 ()
97171 secret = security .generate_token (nbytes = 12 )
98172
99- init_checkout_body = InitCheckoutBody (
100- totalAmount = checkout_amount ,
101- initialAmount = checkout_amount ,
102- itemName = checkout_name ,
103- backUrl = redirection_uri ,
104- errorUrl = redirection_uri ,
105- returnUrl = redirection_uri ,
106- containsDonation = False ,
173+ init_checkout_body = HelloAssoApiV5ModelsCartsInitCheckoutBody (
174+ total_amount = checkout_amount ,
175+ initial_amount = checkout_amount ,
176+ item_name = checkout_name ,
177+ back_url = redirection_uri ,
178+ error_url = redirection_uri ,
179+ return_url = redirection_uri ,
180+ contains_donation = False ,
107181 payer = payer ,
108182 metadata = schemas_payment .HelloAssoCheckoutMetadata (
109183 secret = secret ,
@@ -113,25 +187,33 @@ async def init_checkout(
113187
114188 # TODO: if payment fail, we can retry
115189 # then try without the payer infos
116- response : InitCheckoutResponse
117- try :
118- response = self .hello_asso .checkout_intents_management .init_a_checkout (
119- helloasso_slug ,
120- init_checkout_body ,
121- )
122- except ApiV5BadRequest :
123- # We know that HelloAsso may refuse some payer infos, like using the firstname "test"
124- # Even when prefilling the payer infos,the user will be able to edit them on the payment page,
125- # so we can safely retry without the payer infos
126- hyperion_error_logger .exception (
127- f"Payment: failed to init a checkout with HA for module { module } and name { checkout_name } . Retrying without payer infos" ,
128- )
190+ response : HelloAssoApiV5ModelsCartsInitCheckoutResponse
191+ with ApiClient (configuration ) as api_client :
192+ checkout_api = CheckoutApi (api_client )
193+ try :
194+ response = checkout_api .organizations_organization_slug_checkout_intents_post (
195+ helloasso_slug ,
196+ init_checkout_body ,
197+ )
198+ except Exception :
199+ # We know that HelloAsso may refuse some payer infos, like using the firstname "test"
200+ # Even when prefilling the payer infos,the user will be able to edit them on the payment page,
201+ # so we can safely retry without the payer infos
202+ hyperion_error_logger .exception (
203+ f"Payment: failed to init a checkout with HA for module { module } and name { checkout_name } . Retrying without payer infos" ,
204+ )
129205
130- init_checkout_body .payer = None
131- response = self .hello_asso .checkout_intents_management .init_a_checkout (
132- helloasso_slug ,
133- init_checkout_body ,
206+ init_checkout_body .payer = None
207+ response = checkout_api .organizations_organization_slug_checkout_intents_post (
208+ helloasso_slug ,
209+ init_checkout_body ,
210+ )
211+
212+ if response .id is None :
213+ hyperion_error_logger .error (
214+ f"Payment: failed to init a checkout with HA for module { module } and name { checkout_name } . No checkout id returned" ,
134215 )
216+ raise MissingHelloAssoCheckoutIdError () # noqa: TRY301
135217
136218 checkout_model = models_payment .Checkout (
137219 id = checkout_model_id ,
@@ -146,7 +228,7 @@ async def init_checkout(
146228
147229 return schemas_payment .Checkout (
148230 id = checkout_model_id ,
149- payment_url = response .redirectUrl ,
231+ payment_url = response .redirect_url ,
150232 )
151233 except Exception :
152234 hyperion_error_logger .exception (
@@ -172,3 +254,28 @@ async def get_checkout(
172254 for payment in checkout_dict ["payments" ]
173255 ]
174256 return schemas_payment .CheckoutComplete (** checkout_dict )
257+
258+ async def refund_payment (
259+ self ,
260+ checkout_id : uuid .UUID ,
261+ hello_asso_payment_id : int ,
262+ amount : int ,
263+ ) -> None :
264+ """
265+ Refund a payment
266+ """
267+ configuration = self .get_hello_asso_configuration ()
268+
269+ with ApiClient (configuration ) as api_client :
270+ paiements_api = PaiementsApi (api_client )
271+ try :
272+ paiements_api .payments_payment_id_refund_post (
273+ payment_id = hello_asso_payment_id ,
274+ send_refund_mail = True ,
275+ amount = amount ,
276+ )
277+ except Exception :
278+ hyperion_error_logger .exception (
279+ f"Payment: failed to refund payment { hello_asso_payment_id } for checkout { checkout_id } " ,
280+ )
281+ raise
0 commit comments