feat: update full path payment

This commit is contained in:
2026-05-11 13:48:02 +03:00
parent ad51f1220f
commit d7ccddc72c
11 changed files with 133 additions and 148 deletions

View File

@@ -1,3 +1,3 @@
from src.application.commands.create_order_command import CreateOrderCommand
from src.application.commands.create_payment_command import CreatePaymentCommand
from src.application.commands.create_payment_cloudkassir_command import CreatePaymentCloudkassirCommand
from src.application.commands.create_crypto_transfer_completed_command import CreateCryptoTransferCompletedCommand

View File

@@ -0,0 +1,84 @@
from __future__ import annotations
from decimal import Decimal
from ulid import ULID
from src.application.abstractions import IUnitOfWork
from src.application.contracts import IReceipt
from src.application.domain.exceptions import ApplicationException
from src.infrastructure.database.decorators import transactional
class CreateCryptoTransferCompletedCommand:
def __init__(self, *, unit_of_work: IUnitOfWork, receipt: IReceipt):
self._unit_of_work = unit_of_work
self._receipt = receipt
@transactional
async def __call__(self, *, order_id: str, user_id: str, web3_transaction_hash: str | None = None) -> None:
if not order_id:
raise ApplicationException(status_code=400, message='Crypto transfer completed message missing order_id')
if not user_id:
raise ApplicationException(status_code=400, message='Crypto transfer completed message missing user_id')
await self._unit_of_work.payment_repository.update_crypto_transfer_completed(
order_id=order_id,
web3_transaction_hash=web3_transaction_hash,
)
user = await self._unit_of_work.user_repository.get(user_id)
if user is None:
raise ApplicationException(status_code=404, message='User not found')
email = str(user.email or '').strip()
if not email:
raise ApplicationException(status_code=400, message='User email missing')
customer_info = ' '.join(
part
for part in (
str(user.last_name or '').strip(),
str(user.first_name or '').strip(),
str(user.middle_name or '').strip(),
)
if part
)
if not customer_info:
raise ApplicationException(status_code=400, message='User full name missing')
customer_inn = str(user.inn or '').strip()
if not customer_inn:
raise ApplicationException(status_code=400, message='User inn missing')
if user.birth_date is None:
raise ApplicationException(status_code=400, message='User birth date missing')
customer_birthday = f'{user.birth_date.isoformat()}T12:00:00.000Z'
order = await self._unit_of_work.order_repository.get_by_id(order_id)
if order is None:
raise ApplicationException(status_code=404, message='Order not found')
if order.total_price is None:
raise ApplicationException(status_code=400, message='Order total price missing for receipt')
if order.service_fee is None:
raise ApplicationException(status_code=400, message='Order service fee missing for receipt')
total_amount = Decimal(str(order.total_price)).quantize(Decimal('0.01'))
service_fee = Decimal(str(order.service_fee)).quantize(Decimal('0.01'))
principal_amount = (total_amount - service_fee).quantize(Decimal('0.01'))
if principal_amount < 0:
raise ApplicationException(status_code=400, message='Invalid receipt amounts: principal negative')
receipt_response = await self._receipt.create_receipt(
order_id=order_id,
user_id=user_id,
email=email,
total_amount=total_amount,
principal_amount=principal_amount,
service_fee=service_fee,
customer_info=customer_info,
customer_inn=customer_inn,
customer_birthday=customer_birthday,
request_id=str(ULID()),
)
receipt_model = receipt_response.get('Model')
if not isinstance(receipt_model, dict):
receipt_model = {}
await self._unit_of_work.payment_repository.update_receipt(
order_id=order_id,
receipt_cloudekassir_id=str(receipt_model.get('Id') or '') or None,
receipt_cloudekassir_link=str(receipt_model.get('ReceiptLocalUrl') or '') or None,
)

View File

@@ -1,132 +0,0 @@
from __future__ import annotations
from decimal import Decimal
from ulid import ULID
from src.application.abstractions import IUnitOfWork
from src.application.contracts import IReceipt
from src.application.domain.exceptions import ApplicationException
from src.infrastructure.database.decorators import transactional
from src.presentation.schemas.itpay_payment_models import ItpayPaymentData
def _parse_money(val: object | None) -> Decimal | None:
if val is None:
return None
s = str(val).strip()
if not s:
return None
return Decimal(s).quantize(Decimal('0.01'))
class CreatePaymentCloudkassirCommand:
def __init__(self, *, unit_of_work: IUnitOfWork, receipt: IReceipt):
self._unit_of_work = unit_of_work
self._receipt = receipt
@transactional
async def __call__(self, payment: ItpayPaymentData) -> None:
if str(payment.status).strip().lower() != 'completed':
return
metadata = payment.metadata or {}
order_id = str(metadata.get('order_id') or '')
user_id = str(metadata.get('user_id') or '')
if not order_id:
raise ApplicationException(status_code=400, message='Itpay webhook metadata missing order_id')
if not user_id:
raise ApplicationException(status_code=400, message='Itpay webhook metadata missing user_id')
await self._unit_of_work.payment_repository.create_completed(
user_id=user_id,
order_id=order_id,
itpay_payment_id=str(payment.id),
itpay_paid_amount=str(payment.amount) if payment.amount is not None else None,
transaction_id=str(payment.transaction_id) if payment.transaction_id is not None else None,
paid_at=str(payment.paid) if payment.paid is not None else None,
expired_date=str(payment.expired_date) if payment.expired_date is not None else None,
)
user = await self._unit_of_work.user_repository.get(user_id)
if user is None:
raise ApplicationException(status_code=404, message='User not found')
email = str(user.email or '').strip()
if not email:
raise ApplicationException(status_code=400, message='User email missing')
customer_info = ' '.join(
part
for part in (
str(user.last_name or '').strip(),
str(user.first_name or '').strip(),
str(user.middle_name or '').strip(),
)
if part
)
if not customer_info:
raise ApplicationException(status_code=400, message='User full name missing')
customer_inn = str(user.inn or '').strip()
if not customer_inn:
raise ApplicationException(status_code=400, message='User inn missing')
if user.birth_date is None:
raise ApplicationException(status_code=400, message='User birth date missing')
customer_birthday = f'{user.birth_date.isoformat()}T12:00:00.000Z'
paid_total = _parse_money(payment.amount)
if paid_total is None:
paid_total = _parse_money(metadata.get('amount'))
if paid_total is None:
paid_total = _parse_money(metadata.get('total_amount'))
meta_principal = _parse_money(metadata.get('principal_amount'))
meta_agent = _parse_money(metadata.get('agent_fee'))
if meta_agent is None:
meta_agent = _parse_money(metadata.get('service_fee'))
principal_amount: Decimal
service_fee: Decimal
total_amount: Decimal
if meta_principal is not None and meta_agent is not None:
principal_amount = meta_principal
service_fee = meta_agent
total_amount = (principal_amount + service_fee).quantize(Decimal('0.01'))
else:
order = await self._unit_of_work.order_repository.get_by_id(order_id)
if order is not None and order.total_price is not None and order.service_fee is not None:
total_amount = Decimal(str(order.total_price)).quantize(Decimal('0.01'))
service_fee = Decimal(str(order.service_fee)).quantize(Decimal('0.01'))
principal_amount = (total_amount - service_fee).quantize(Decimal('0.01'))
else:
if paid_total is None:
raise ApplicationException(status_code=400, message='Payment amount missing for receipt')
raw_sf = metadata.get('service_fee')
if raw_sf is None:
raise ApplicationException(
status_code=400,
message='Receipt amounts: need principal_amount+agent_fee in metadata, order in DB, or service_fee with paid amount',
)
service_fee = Decimal(str(raw_sf)).quantize(Decimal('0.01'))
total_amount = paid_total
principal_amount = (total_amount - service_fee).quantize(Decimal('0.01'))
if principal_amount < 0:
raise ApplicationException(status_code=400, message='Invalid receipt amounts: principal negative')
if paid_total is not None and abs(total_amount - paid_total) > Decimal('0.02'):
raise ApplicationException(status_code=400, message='Receipt total does not match paid amount')
receipt_response = await self._receipt.create_receipt(
order_id=order_id,
user_id=user_id,
email=email,
total_amount=total_amount,
principal_amount=principal_amount,
service_fee=service_fee,
customer_info=customer_info,
customer_inn=customer_inn,
customer_birthday=customer_birthday,
request_id=str(ULID()),
)
receipt_model = receipt_response.get('Model')
if not isinstance(receipt_model, dict):
receipt_model = {}
await self._unit_of_work.payment_repository.update_receipt(
order_id=order_id,
receipt_cloudekassir_id=str(receipt_model.get('Id') or '') or None,
receipt_cloudekassir_link=str(receipt_model.get('ReceiptLocalUrl') or '') or None,
)

View File

@@ -25,7 +25,7 @@ class CreatePaymentCommand:
raise ApplicationException(status_code=400, message='Itpay webhook metadata missing order_id')
if not user_id:
raise ApplicationException(status_code=400, message='Itpay webhook metadata missing user_id')
await self._unit_of_work.payment_repository.create_completed(
payment_created = await self._unit_of_work.payment_repository.create_completed(
user_id=user_id,
order_id=order_id,
itpay_payment_id=str(payment.id),
@@ -34,6 +34,8 @@ class CreatePaymentCommand:
paid_at=str(payment.paid) if payment.paid is not None else None,
expired_date=str(payment.expired_date) if payment.expired_date is not None else None,
)
if not payment_created:
return
message_id = str(ULID())
message: dict[str,str] = {
'order_id': order_id,