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') supplier_name = ' '.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 supplier_name: raise ApplicationException(status_code=400, message='User full name missing') supplier_inn = str(user.inn or '').strip() if not supplier_inn: raise ApplicationException(status_code=400, message='User inn missing') phone = str(user.phone or '').strip() or None 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, phone=phone, supplier_name=supplier_name, supplier_inn=supplier_inn, 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, )