import logging from io import BytesIO from typing import List from uuid import uuid4 from fastapi import HTTPException from number_to_string import get_string_by_number from sqlalchemy import select from sqlalchemy.orm import selectinload, joinedload from starlette import status from weasyprint import HTML, CSS import backend.config import constants from constants import MONTHS, ENV from external.billing import BillingClient, CreateBillingRequestValue, CreateBillRequestSchema, CreateBillRequestItems, \ BillStatusUpdateRequest, NotificationChannel, NotifyReceivedBillRequestSchema, DeleteBillRequestSchema, \ ProductBillingDocumentPdf, ServiceBillingDocumentPdf from models import DealBillRequest, Deal, DealProduct, DealService as DealServiceModel, DealProductService, DealGroup, \ GroupBillRequest from schemas.billing import * from services.base import BaseService from services.deal import DealService from utils.list_utils import to_locale_number class BillingService(BaseService): async def _process_deal_update_details( self, request: BillStatusUpdateRequest, ): deal_bill_request = await self._get_deal_bill_by_id(request.listener_transaction_id) if not deal_bill_request: return deal_bill_request.pdf_url = request.info.pdf_url deal_bill_request.invoice_number = request.info.invoice_number async def _process_group_update_details( self, request: BillStatusUpdateRequest, ): prefix = "group-" if not request.listener_transaction_id.startswith(prefix): return group_id = int(request.listener_transaction_id.removeprefix(prefix)) group_bill_request = await self._get_group_bill_by_id(group_id) if not group_bill_request: return group_bill_request.pdf_url = request.info.pdf_url group_bill_request.invoice_number = request.info.invoice_number 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) if not response.ok: return if type(request.listener_transaction_id) is int: await self._process_deal_update_details(request) else: await self._process_group_update_details(request) await self.session.commit() async def _process_deal_update_verification( self, request: BillStatusUpdateRequest ): 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 async def _process_group_update_verification( self, request: BillStatusUpdateRequest ): prefix = "group-" if not request.listener_transaction_id.startswith(prefix): return group_id = int(request.listener_transaction_id.removeprefix(prefix)) group_bill_request = await self._get_group_bill_by_id(group_id) if not group_bill_request: return group_bill_request.paid = request.info.payed 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 if type(request.listener_transaction_id) is int: await self._process_deal_update_verification(request) else: await self._process_group_update_verification(request) 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_bill_request(self, deal: Deal): deal_bill_request = DealBillRequest( deal_id=deal.id, created_at=datetime.datetime.now() ) self.session.add(deal_bill_request) deal.is_locked = True await self.session.commit() async def create_group_bill_request(self, group: DealGroup): group_bill_request = GroupBillRequest( group_id=group.id, created_at=datetime.datetime.now() ) self.session.add(group_bill_request) for deal in group.deals: deal.is_locked = True await self.session.commit() async def create_deal_billing(self, user, request: CreateDealBillRequest) -> CreateDealBillResponse: try: deal_service = DealService(self.session) billing_client = BillingClient(backend.config.BILLING_API_KEY) basic_deal: Deal = await deal_service.get_by_id(user, request.deal_id, return_raw=True) if basic_deal.group: deals = await self._get_deals_by_group_id(basic_deal.group.id) else: deals = [basic_deal] (services, products, is_size_needed) = await self._get_products_for_deal(deals) services: dict[str, ServiceBillingDocumentPdf] products: dict[str, ProductBillingDocumentPdf] is_size_needed: bool billing_request_values: List[CreateBillingRequestValue] = [] for service in services.values(): billing_request_values.append( CreateBillingRequestValue( name=service.name, price=service.price, amount=service.quantity ) ) deal = basic_deal listener_transaction_id = deal.id if deal.group: listener_transaction_id = f"group-{basic_deal.group.id}" inn: str = deal.client.details.inn create_bill_request = CreateBillRequestSchema( listener_transaction_id=listener_transaction_id, payer_name=deal.client.name, payer_inn=inn.strip(), 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='Ошибка!') if basic_deal.group: await self.create_group_bill_request(basic_deal.group) else: await self.create_deal_bill_request(basic_deal) 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_group_bill_by_id(self, group_id: int) -> Optional[GroupBillRequest]: return await self.session.scalar(select(GroupBillRequest).where(GroupBillRequest.group_id == group_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 = await self._get_deal_by_id(request.deal_id) if not deal: return CancelDealBillResponse(ok=False, message='Сделка не найдена') if deal.group: bill = await self._get_group_bill_by_id(deal.group.id) if not bill: return CancelDealBillResponse(ok=False, message='Заявка не найдена') billing_client = BillingClient(backend.config.BILLING_API_KEY) response = await billing_client.delete(DeleteBillRequestSchema(listener_transaction_id=deal.group.id)) else: bill = await self._get_deal_bill_by_id(request.deal_id) if not 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(bill) await self.session.commit() return CancelDealBillResponse(ok=True, message='Заявка успешно отозвана') except Exception as e: return CancelDealBillResponse(ok=False, message=str(e)) def _gen_key_for_service(self, service: ServiceBillingDocumentPdf) -> str: return f"{service.name}-{service.price}" def _gen_key_for_product(self, product: ProductBillingDocumentPdf) -> str: article = product.article if product.article else uuid4() return f"{article}-{product.size}-{product.price}" async def _get_products_for_deal(self, deals: list[Deal]) -> tuple[dict, dict, bool]: services: dict[str, ServiceBillingDocumentPdf] = {} products: dict[str, ProductBillingDocumentPdf] = {} is_size_needed: bool = False for deal in deals: for product in deal.products: product_price = 0 for service in product.services: service_data = ServiceBillingDocumentPdf( name=f'[{product.product.name}] - {service.service.name}', price=service.price, quantity=product.quantity ) key = self._gen_key_for_service(service_data) if key in services: services[key].quantity += product.quantity else: services[key] = service_data product_price += service_data.price is_size_needed = is_size_needed | bool(product.product.size) product_data = ProductBillingDocumentPdf( article=product.product.article, size=product.product.size if product.product.size else "", price=product_price, quantity=product.quantity, ) product_key = self._gen_key_for_product(product_data) if product_key in products: products[product_key].quantity += product_data.quantity else: products[product_key] = product_data for service in deal.services: service_data = ServiceBillingDocumentPdf( name=service.service.name, price=service.price, quantity=service.quantity ) key = self._gen_key_for_service(service_data) if key in services: services[key].quantity += service_data.quantity else: services[key] = service_data return services, products, is_size_needed async def _get_deal_by_id(self, deal_id: int) -> Optional[Deal]: 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), joinedload(Deal.shipping_warehouse), joinedload(Deal.client), selectinload(Deal.group).selectinload(DealGroup.deals), ) ) return deal async def _get_deals_by_group_id(self, group_id: int) -> List[Deal]: group: DealGroup | None = await self.session.scalar( select(DealGroup) .where(DealGroup.id == group_id) .options( selectinload(DealGroup.deals).selectinload(Deal.products).selectinload(DealProduct.services), selectinload(DealGroup.deals).selectinload(Deal.services).selectinload(DealServiceModel.service), selectinload(DealGroup.deals).joinedload(Deal.shipping_warehouse), selectinload(DealGroup.deals).joinedload(Deal.client), selectinload(DealGroup.deals).selectinload(Deal.group).selectinload(DealGroup.deals), ) ) return group.deals if group else [] async def _create_billing_document_html(self, deal_id: int): deal = await self._get_deal_by_id(deal_id) if not deal: return "" if deal.group: deals = await self._get_deals_by_group_id(deal.group.id) else: deals = [deal] (services, products, is_size_needed) = await self._get_products_for_deal(deals) deal_price = sum((service.price * service.quantity for service in services.values())) 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({ "products": products, "services": services, "is_size_needed": is_size_needed, "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, stylesheets=[CSS(constants.APP_PATH + '/static/css/bill-of-payment.css')]) return pdf_file