from io import BytesIO from pathlib import Path from typing import List from fastapi import HTTPException from jinja2 import Environment, FileSystemLoader from number_to_string import get_string_by_number from sqlalchemy import select from sqlalchemy.orm import selectinload from starlette import status from weasyprint import HTML import backend.config from constants import MONTHS from external.billing import BillingClient, CreateBillingRequestValue, CreateBillRequestSchema, CreateBillRequestItems, \ BillStatusUpdateRequest, NotificationChannel, NotifyReceivedBillRequestSchema, DeleteBillRequestSchema from models import DealBillRequest, Deal, DealProduct, DealService as DealServiceModel from schemas.billing import * from services.base import BaseService from services.deal import DealService from utils.list_utils import to_locale_number env = Environment(loader=FileSystemLoader(Path("templates") / Path("documents"))) class BillingService(BaseService): async def _process_update_details( self, request: BillStatusUpdateRequest ): billing_client = BillingClient(backend.config.BILLING_API_KEY) notify_received_request = NotifyReceivedBillRequestSchema( listener_transaction_id=request.listener_transaction_id, channel=NotificationChannel.PAYMENT_DETAILS, received=True ) response = await billing_client.notify_received(notify_received_request) deal_bill_request = await self._get_deal_bill_by_id(request.listener_transaction_id) if not response.ok: return deal_bill_request.pdf_url = request.info.pdf_url deal_bill_request.invoice_number = request.info.invoice_number await self.session.commit() async def _process_update_verification( self, request: BillStatusUpdateRequest ): billing_client = BillingClient(backend.config.BILLING_API_KEY) notify_received_request = NotifyReceivedBillRequestSchema( listener_transaction_id=request.listener_transaction_id, channel=NotificationChannel.PAYMENT_VERIFICATION, received=True ) response = await billing_client.notify_received(notify_received_request) if not response.ok: return deal_bill_request = await self._get_deal_bill_by_id(request.listener_transaction_id) if not deal_bill_request: return deal_bill_request.paid = request.info.payed await self.session.commit() async def process_update( self, request: BillStatusUpdateRequest ): if request.channel == NotificationChannel.PAYMENT_DETAILS: await self._process_update_details(request) elif request.channel == NotificationChannel.PAYMENT_VERIFICATION: await self._process_update_verification(request) async def create_deal_billing(self, user, request: CreateDealBillRequest) -> CreateDealBillResponse: try: deal_service = DealService(self.session) billing_client = BillingClient(backend.config.BILLING_API_KEY) deal: Deal = await deal_service.get_by_id(user, request.deal_id) billing_request_values: List[CreateBillingRequestValue] = [] for product in deal.products: for service in product.services: billing_request_values.append( CreateBillingRequestValue( name=f'[{product.product.name}] - {service.service.name}', price=service.price, amount=product.quantity ) ) for service in deal.services: billing_request_values.append( CreateBillingRequestValue( name=f'{service.service.name}', price=service.price, amount=service.quantity ) ) create_bill_request = CreateBillRequestSchema( listener_transaction_id=deal.id, payer_name=deal.client.name, payer_inn=deal.client.details.inn, payer_phone=deal.client.details.phone_number, items=CreateBillRequestItems( values=billing_request_values ) ) create_bill_response = await billing_client.create(create_bill_request) if not create_bill_response.ok: return CreateDealBillResponse(ok=create_bill_response.ok, message='Ошибка!') deal_bill_request = DealBillRequest( deal_id=request.deal_id, created_at=datetime.datetime.now() ) self.session.add(deal_bill_request) deal.is_locked = True await self.session.commit() return CreateDealBillResponse(ok=create_bill_response.ok, message='Заявка успешно создана!') except Exception as e: return CreateDealBillResponse(ok=False, message=str(e)) async def _get_deal_bill_by_id(self, deal_id: int) -> Optional[DealBillRequest]: return await self.session.scalar(select(DealBillRequest).where(DealBillRequest.deal_id == deal_id)) async def get_deal_bill_by_id(self, deal_id: int) -> GetDealBillById: deal_bill = await self._get_deal_bill_by_id(deal_id) if not deal_bill: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Deal bill was not found') return GetDealBillById(deal_bill=DealBillRequestSchema.model_validate(deal_bill)) async def cancel_deal_billing(self, user, request: CancelDealBillRequest) -> CancelDealBillResponse: try: deal_bill = await self._get_deal_bill_by_id(request.deal_id) if not deal_bill: return CancelDealBillResponse(ok=False, message='Заявка не найдена') billing_client = BillingClient(backend.config.BILLING_API_KEY) response = await billing_client.delete(DeleteBillRequestSchema(listener_transaction_id=request.deal_id)) if not response.ok: return CancelDealBillResponse(ok=False, message='Ошибка') await self.session.delete(deal_bill) await self.session.commit() return CancelDealBillResponse(ok=True, message='Заявка успешно отозвана') except Exception as e: return CancelDealBillResponse(ok=False, message=str(e)) async def _get_services_for_deal(self, deal: Deal) -> List[CreateBillingRequestValue]: services: List[CreateBillingRequestValue] = [] for product in deal.products: for service in product.services: services.append( CreateBillingRequestValue( name=f'[{product.product.name}] - {service.service.name}', price=service.price, amount=product.quantity ) ) for service in deal.services: services.append( CreateBillingRequestValue( name=f'{service.service.name}', price=service.price, amount=service.quantity ) ) return services async def _create_billing_document_html(self, deal_id: int): deal: Deal | None = await self.session.scalar( select(Deal) .where(Deal.id == deal_id) .options( selectinload(Deal.products).selectinload(DealProduct.services), selectinload(Deal.services).selectinload(DealServiceModel.service), ) ) if not deal: return "" services = await self._get_services_for_deal(deal) deal_price = sum((service.price * service.amount for service in services)) deal_price_words = get_string_by_number(deal_price)[0:-10] deal_price = to_locale_number(deal_price) template = env.get_template("bill-of-payment.html") now = datetime.datetime.now() curr_date = f"{now.day} {MONTHS[now.month - 1]} {now.year} г." return template.render({ "services": services, "deal_price": deal_price, "deal_price_words": deal_price_words, "deal": deal, "curr_date": curr_date }) async def create_billing_document_pdf(self, deal_id) -> BytesIO: doc = await self._create_billing_document_html(deal_id) pdf_file = BytesIO() HTML(string=doc).write_pdf(pdf_file) return pdf_file