407 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			407 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
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 CardBillRequest, Card, CardProduct, CardService as CardServiceModel, CardGroup, \
 | 
						||
    GroupBillRequest
 | 
						||
from schemas.billing import *
 | 
						||
from services.base import BaseService
 | 
						||
from services.card import CardsService
 | 
						||
from utils.list_utils import to_locale_number
 | 
						||
 | 
						||
 | 
						||
class BillingService(BaseService):
 | 
						||
    async def _process_card_update_details(
 | 
						||
            self,
 | 
						||
            request: BillStatusUpdateRequest,
 | 
						||
    ):
 | 
						||
        bill_request = await self._get_card_bill_by_id(request.listener_transaction_id)
 | 
						||
        if not bill_request:
 | 
						||
            return
 | 
						||
        bill_request.pdf_url = request.info.pdf_url
 | 
						||
        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
 | 
						||
        request_id = int(request.listener_transaction_id.removeprefix(prefix))
 | 
						||
        group_bill_request = await self._get_group_bill_by_id(request_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_card_update_details(request)
 | 
						||
        else:
 | 
						||
            await self._process_group_update_details(request)
 | 
						||
        await self.session.commit()
 | 
						||
 | 
						||
    async def _process_card_update_verification(
 | 
						||
            self,
 | 
						||
            request: BillStatusUpdateRequest
 | 
						||
    ):
 | 
						||
        card_bill_request = await self._get_card_bill_by_id(request.listener_transaction_id)
 | 
						||
        if not card_bill_request:
 | 
						||
            return
 | 
						||
        card_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_card_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_card_bill_request(self, card: Card) -> CardBillRequest:
 | 
						||
        card_bill_request = CardBillRequest(
 | 
						||
            card_id=card.id,
 | 
						||
            created_at=datetime.datetime.now()
 | 
						||
        )
 | 
						||
        self.session.add(card_bill_request)
 | 
						||
        await self.session.flush()
 | 
						||
        return card_bill_request
 | 
						||
 | 
						||
    async def create_group_bill_request(self, group: CardGroup) -> GroupBillRequest:
 | 
						||
        group_bill_request = GroupBillRequest(
 | 
						||
            group_id=group.id,
 | 
						||
            created_at=datetime.datetime.now()
 | 
						||
        )
 | 
						||
        self.session.add(group_bill_request)
 | 
						||
 | 
						||
        await self.session.flush()
 | 
						||
        return group_bill_request
 | 
						||
 | 
						||
    @staticmethod
 | 
						||
    def _get_billing_values_from_services(services) -> List[
 | 
						||
        CreateBillingRequestValue
 | 
						||
    ]:
 | 
						||
        billing_request_values: List[CreateBillingRequestValue] = []
 | 
						||
 | 
						||
        for service in services:
 | 
						||
            # Omit services that have no price
 | 
						||
            if not service.price:
 | 
						||
                continue
 | 
						||
            billing_request_values.append(
 | 
						||
                CreateBillingRequestValue(
 | 
						||
                    name=service.name,
 | 
						||
                    price=service.price,
 | 
						||
                    amount=service.quantity
 | 
						||
                )
 | 
						||
            )
 | 
						||
        return billing_request_values
 | 
						||
 | 
						||
    async def create_card_billing(self, user, request: CreateCardBillRequest) -> CreateCardBillResponse:
 | 
						||
        try:
 | 
						||
            card_service = CardsService(self.session)
 | 
						||
            billing_client = BillingClient(backend.config.BILLING_API_KEY)
 | 
						||
 | 
						||
            basic_card: Card = await card_service.get_by_id(user, request.card_id, return_raw=True)
 | 
						||
            if basic_card.group:
 | 
						||
                cards = await self._get_cards_by_group_id(basic_card.group.id)
 | 
						||
            else:
 | 
						||
                cards = [basic_card]
 | 
						||
 | 
						||
            (services, products, is_size_needed) = await self._get_products_for_card(cards)
 | 
						||
            services: dict[str, ServiceBillingDocumentPdf]
 | 
						||
            products: dict[str, ProductBillingDocumentPdf]
 | 
						||
            is_size_needed: bool
 | 
						||
            billing_request_values: List[CreateBillingRequestValue] = self._get_billing_values_from_services(
 | 
						||
                services.values()
 | 
						||
            )
 | 
						||
 | 
						||
            card = basic_card
 | 
						||
            transaction_prefix = "group-" if card.group else ""
 | 
						||
            inn: str = card.client.details.inn.strip()
 | 
						||
 | 
						||
            one_request_size = 100
 | 
						||
            for i in range(0, len(billing_request_values), one_request_size):
 | 
						||
                values = billing_request_values[i:i + one_request_size]
 | 
						||
 | 
						||
                if basic_card.group:
 | 
						||
                    bill_request = await self.create_group_bill_request(basic_card.group)
 | 
						||
                else:
 | 
						||
                    bill_request = await self.create_card_bill_request(basic_card)
 | 
						||
 | 
						||
                listener_transaction_id = transaction_prefix + str(bill_request.id)
 | 
						||
                create_bill_request = CreateBillRequestSchema(
 | 
						||
                    listener_transaction_id=listener_transaction_id,
 | 
						||
                    payer_name=card.client.name,
 | 
						||
                    payer_inn=inn,
 | 
						||
                    payer_phone=card.client.details.phone_number,
 | 
						||
                    items=CreateBillRequestItems(
 | 
						||
                        values=values,
 | 
						||
                    )
 | 
						||
                )
 | 
						||
                create_bill_response = await billing_client.create(create_bill_request)
 | 
						||
 | 
						||
                if not create_bill_response.ok and 'Request already exists' in create_bill_response.message:
 | 
						||
                    await self._cancel_one_bill(bill_request, listener_transaction_id)
 | 
						||
                    create_bill_response = await billing_client.create(create_bill_request)
 | 
						||
 | 
						||
                if not create_bill_response.ok:
 | 
						||
                    raise Exception(create_bill_response.message or 'Неизвестная ошибка')
 | 
						||
 | 
						||
            if card.group:
 | 
						||
                for card in card.group.cards:
 | 
						||
                    card.is_locked = True
 | 
						||
            else:
 | 
						||
                card.is_locked = True
 | 
						||
 | 
						||
            await self.session.commit()
 | 
						||
            return CreateCardBillResponse(ok=True, message='Заявка успешно создана!')
 | 
						||
        except Exception as e:
 | 
						||
            await self.session.rollback()
 | 
						||
            return CreateCardBillResponse(ok=False, message=str(e))
 | 
						||
 | 
						||
    async def _get_card_bill_by_id(self, request_id: int) -> Optional[CardBillRequest]:
 | 
						||
        return await self.session.scalar(
 | 
						||
            select(CardBillRequest)
 | 
						||
            .where(CardBillRequest.id == request_id)
 | 
						||
        )
 | 
						||
 | 
						||
    async def _get_card_bill_by_card_id(self, card_id: int) -> List[CardBillRequest]:
 | 
						||
        result = await self.session.scalars(
 | 
						||
            select(CardBillRequest)
 | 
						||
            .where(CardBillRequest.card_id == card_id)
 | 
						||
        )
 | 
						||
        return [*result.all()]
 | 
						||
 | 
						||
    async def _get_group_bill_by_id(self, request_id: int) -> Optional[GroupBillRequest]:
 | 
						||
        return await self.session.scalar(
 | 
						||
            select(GroupBillRequest)
 | 
						||
            .where(GroupBillRequest.id == request_id)
 | 
						||
        )
 | 
						||
 | 
						||
    async def _get_group_bill_by_group_id(self, group_id: int) -> List[GroupBillRequest]:
 | 
						||
        result = await self.session.scalars(
 | 
						||
            select(GroupBillRequest)
 | 
						||
            .where(GroupBillRequest.group_id == group_id)
 | 
						||
        )
 | 
						||
        return [*result.all()]
 | 
						||
 | 
						||
    async def get_card_bill_by_id(self, card_id: int) -> GetCardBillById:
 | 
						||
        bill = await self._get_card_bill_by_id(card_id)
 | 
						||
        if not bill:
 | 
						||
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Card bill was not found')
 | 
						||
        return GetCardBillById(card_bill=CardBillRequestSchema.model_validate(bill))
 | 
						||
 | 
						||
    async def _cancel_one_bill(self, bill: CardBillRequest | GroupBillRequest, listener_transaction_id: str) -> None:
 | 
						||
        billing_client = BillingClient(backend.config.BILLING_API_KEY)
 | 
						||
        response = await billing_client.delete(
 | 
						||
            DeleteBillRequestSchema(listener_transaction_id=listener_transaction_id)
 | 
						||
        )
 | 
						||
 | 
						||
        if not response.ok:
 | 
						||
            raise Exception('Ошибка удаления счета со стороны Billing сервиса')
 | 
						||
 | 
						||
        await self.session.delete(bill)
 | 
						||
 | 
						||
    async def cancel_card_billing(self, request: CancelCardBillRequest) -> CancelCardBillResponse:
 | 
						||
        try:
 | 
						||
            card = await self._get_card_by_id(request.card_id)
 | 
						||
            if not card:
 | 
						||
                return CancelCardBillResponse(ok=False, message='Сделка не найдена')
 | 
						||
 | 
						||
            if card.group:
 | 
						||
                bills = await self._get_group_bill_by_group_id(card.group.id)
 | 
						||
            else:
 | 
						||
                bills = await self._get_card_bill_by_card_id(request.card_id)
 | 
						||
            if not bills:
 | 
						||
                return CancelCardBillResponse(ok=False, message='Заявка не найдена')
 | 
						||
 | 
						||
            for bill in bills:
 | 
						||
                if card.group:
 | 
						||
                    listener_transaction_id = f"group-{bill.id}"
 | 
						||
                else:
 | 
						||
                    listener_transaction_id = str(bill.id)
 | 
						||
 | 
						||
                await self._cancel_one_bill(bill, listener_transaction_id)
 | 
						||
 | 
						||
            await self.session.commit()
 | 
						||
            return CancelCardBillResponse(ok=True, message='Заявка успешно отозвана')
 | 
						||
        except Exception as e:
 | 
						||
            await self.session.rollback()
 | 
						||
            return CancelCardBillResponse(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_card(self, cards: list[Card]) -> tuple[dict, dict, bool]:
 | 
						||
        services: dict[str, ServiceBillingDocumentPdf] = {}
 | 
						||
        products: dict[str, ProductBillingDocumentPdf] = {}
 | 
						||
        is_size_needed: bool = False
 | 
						||
 | 
						||
        for card in cards:
 | 
						||
            for product in card.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 card.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_card_by_id(self, card_id: int) -> Optional[Card]:
 | 
						||
        card: Card | None = await self.session.scalar(
 | 
						||
            select(Card)
 | 
						||
            .where(Card.id == card_id)
 | 
						||
            .options(
 | 
						||
                selectinload(Card.products).selectinload(CardProduct.services),
 | 
						||
                selectinload(Card.services).selectinload(CardServiceModel.service),
 | 
						||
                joinedload(Card.shipping_warehouse),
 | 
						||
                joinedload(Card.client),
 | 
						||
                selectinload(Card.group).selectinload(CardGroup.cards),
 | 
						||
            )
 | 
						||
        )
 | 
						||
        return card
 | 
						||
 | 
						||
    async def _get_cards_by_group_id(self, group_id: int) -> List[Card]:
 | 
						||
        group: CardGroup | None = await self.session.scalar(
 | 
						||
            select(CardGroup)
 | 
						||
            .where(CardGroup.id == group_id)
 | 
						||
            .options(
 | 
						||
                selectinload(CardGroup.cards).selectinload(Card.products).selectinload(CardProduct.services),
 | 
						||
                selectinload(CardGroup.cards).selectinload(Card.services).selectinload(CardServiceModel.service),
 | 
						||
                selectinload(CardGroup.cards).joinedload(Card.shipping_warehouse),
 | 
						||
                selectinload(CardGroup.cards).joinedload(Card.client),
 | 
						||
                selectinload(CardGroup.cards).selectinload(Card.group).selectinload(CardGroup.cards),
 | 
						||
            )
 | 
						||
        )
 | 
						||
        return group.cards if group else []
 | 
						||
 | 
						||
    async def _create_billing_document_html(self, card_id: int):
 | 
						||
        card = await self._get_card_by_id(card_id)
 | 
						||
        if not card:
 | 
						||
            return ""
 | 
						||
 | 
						||
        if card.group:
 | 
						||
            cards = await self._get_cards_by_group_id(card.group.id)
 | 
						||
        else:
 | 
						||
            cards = [card]
 | 
						||
 | 
						||
        (services, products, is_size_needed) = await self._get_products_for_card(cards)
 | 
						||
 | 
						||
        price = sum((service.price * service.quantity for service in services.values()))
 | 
						||
        price_words = get_string_by_number(price)[0:-10]
 | 
						||
        price = to_locale_number(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": price,
 | 
						||
            "deal_price_words": price_words,
 | 
						||
            "deal": card,
 | 
						||
            "curr_date": curr_date
 | 
						||
        })
 | 
						||
 | 
						||
    async def create_billing_document_pdf(self, card_id) -> BytesIO:
 | 
						||
        doc = await self._create_billing_document_html(card_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
 |