Add automatic paypal charging, add ticket page mock

This commit is contained in:
David Rodenkirchen
2026-05-21 11:32:08 +02:00
parent 695b5ae741
commit 6c8c0c7a4f
12 changed files with 321 additions and 29 deletions
+81 -14
View File
@@ -7,6 +7,7 @@ import httpx
import qrcode
from elm.types import Transaction, User, PayPalConfiguration
from elm.services import MailingService, ConfigurationService
logger = logging.getLogger(__name__.split(".")[-1])
@@ -16,16 +17,29 @@ class InsufficientFundsError(Exception):
class AccountingService:
def __init__(self, paypal_config: PayPalConfiguration) -> None:
self._paypal_config = paypal_config
PAYPAL_SANDBOX_URL = "https://api-m.sandbox.paypal.com"
PAYPAL_PROD_URL = "https://api-m.paypal.com"
def __init__(self, configuration_service: ConfigurationService, mailing_service: MailingService) -> None:
self._configuration_service = configuration_service
self._paypal_config: PayPalConfiguration = configuration_service.get_paypal_configuration()
self._pending_paypal_orders: dict[str, tuple[str, Decimal]] = {}
self._mailing_service = mailing_service
async def has_user_open_orders(self, user_name: str) -> bool:
for pending_paypal_order in self._pending_paypal_orders.values():
if pending_paypal_order[0] == user_name:
return True
return False
async def get_paypal_access_token(self) -> str:
url = self.PAYPAL_SANDBOX_URL if self._configuration_service.DEV_MODE_ACTIVE else self.PAYPAL_PROD_URL
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api-m.sandbox.paypal.com/v1/oauth2/token",
f"{url}/v1/oauth2/token",
auth=(
self._paypal_config.client_id_sandbox,
self._paypal_config.secret_sandbox,
self._paypal_config.client_id_sandbox if self._configuration_service.DEV_MODE_ACTIVE else self._paypal_config.client_id,
self._paypal_config.secret_sandbox if self._configuration_service.DEV_MODE_ACTIVE else self._paypal_config.secret,
),
headers={
"Content-Type": "application/x-www-form-urlencoded",
@@ -41,10 +55,13 @@ class AccountingService:
return data["access_token"]
async def start_paypal_process(self, user_name: str, amount: Decimal) -> str:
url = self.PAYPAL_SANDBOX_URL if self._configuration_service.DEV_MODE_ACTIVE else self.PAYPAL_PROD_URL
return_domain = "http://localhost:8000" if self._configuration_service.DEV_MODE_ACTIVE else self._configuration_service.BASE_URL
amount = amount.quantize(Decimal(".01"))
async with httpx.AsyncClient() as client:
access_token = await self.get_paypal_access_token()
response = await client.post(
url="https://api-m.sandbox.paypal.com/v2/checkout/orders/",
url=f"{url}/v2/checkout/orders/",
headers={
"Authorization": f"Bearer {access_token}"
},
@@ -58,16 +75,60 @@ class AccountingService:
"value": str(amount)
}
}
]
],
"payment_source": {
"paypal": {
"experience_context": {
"return_url": f"{return_domain}/return-paypal",
"cancel_url": f"{return_domain}/cancel-paypal",
"user_action": "PAY_NOW",
"shipping_preference": "NO_SHIPPING"
}
}
}
}
)
approval_url = next(
link["href"]
for link in response.json()["links"]
if link["rel"] == "approve"
)
return approval_url
try:
payer_action_url = next(
link["href"]
for link in response.json()["links"]
if link["rel"] == "payer-action"
)
except StopIteration:
logger.error("No payer action url found: %s", response.text)
return "#"
self._pending_paypal_orders[response.json()["id"]] = (user_name, amount)
return payer_action_url
async def finalize_paypal_process(self, order_id: str) -> bool:
url = self.PAYPAL_SANDBOX_URL if self._configuration_service.DEV_MODE_ACTIVE else self.PAYPAL_PROD_URL
async with httpx.AsyncClient() as client:
access_token = await self.get_paypal_access_token()
response = await client.get(
url=f"{url}/v2/checkout/orders/{order_id}",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
)
is_approved = response.json()["status"] == "APPROVED"
if is_approved:
response = await client.post(
f"{url}/v2/checkout/orders/{order_id}/capture",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
)
is_completed = response.json()["status"] == "COMPLETED"
if is_completed:
await self.add_balance(self._pending_paypal_orders[order_id][0], self._pending_paypal_orders[order_id][1], "PayPal Aufladung")
self._pending_paypal_orders.pop(order_id)
return True
return False
async def add_balance(self, user_name: str, balance_to_add: Decimal, title: str) -> Decimal:
user = await User.find_one(User.user_name == user_name)
@@ -80,7 +141,13 @@ class AccountingService:
title=title
).save()
logger.debug(f"Added balance of {self.make_euro_string_from_decimal(balance_to_add)} to user '{user_name}'")
return await self.get_balance(user_name)
new_balance = await self.get_balance(user_name)
await self._mailing_service.send_email(
"Dein Guthaben wurde aufgeladen",
self._mailing_service.generate_account_balance_added_mail_body(user, balance_to_add, new_balance),
user.user_mail
)
return new_balance
async def remove_balance(self, user_name: str, balance_to_remove: Decimal, title: str) -> Decimal:
current_balance = await self.get_balance(user_name)