Files
Fulfillment-Backend/services/billing.py
2025-03-20 13:34:31 +03:00

351 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
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_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):
card_bill_request = CardBillRequest(
card_id=card.id,
created_at=datetime.datetime.now()
)
self.session.add(card_bill_request)
card.is_locked = True
await self.session.commit()
async def create_group_bill_request(self, group: CardGroup):
group_bill_request = GroupBillRequest(
group_id=group.id,
created_at=datetime.datetime.now()
)
self.session.add(group_bill_request)
for card in group.cards:
card.is_locked = True
await self.session.commit()
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] = []
for service in services.values():
# 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
)
)
card = basic_card
listener_transaction_id = card.id
if card.group:
listener_transaction_id = f"group-{basic_card.group.id}"
inn: str = card.client.details.inn
create_bill_request = CreateBillRequestSchema(
listener_transaction_id=listener_transaction_id,
payer_name=card.client.name,
payer_inn=inn.strip(),
payer_phone=card.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 CreateCardBillResponse(ok=create_bill_response.ok, message=create_bill_response.message or 'Неизвестная ошибка')
if basic_card.group:
await self.create_group_bill_request(basic_card.group)
else:
await self.create_card_bill_request(basic_card)
return CreateCardBillResponse(ok=create_bill_response.ok, message='Заявка успешно создана!')
except Exception as e:
return CreateCardBillResponse(ok=False, message=str(e))
async def _get_card_bill_by_id(self, card_id: int) -> Optional[CardBillRequest]:
return await self.session.scalar(select(CardBillRequest).where(CardBillRequest.card_id == card_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_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_card_billing(self, user, 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:
bill = await self._get_group_bill_by_id(card.group.id)
if not bill:
return CancelCardBillResponse(ok=False, message='Заявка не найдена')
billing_client = BillingClient(backend.config.BILLING_API_KEY)
response = await billing_client.delete(DeleteBillRequestSchema(listener_transaction_id=card.group.id))
else:
bill = await self._get_card_bill_by_id(request.card_id)
if not bill:
return CancelCardBillResponse(ok=False, message='Заявка не найдена')
billing_client = BillingClient(backend.config.BILLING_API_KEY)
response = await billing_client.delete(DeleteBillRequestSchema(listener_transaction_id=request.card_id))
if not response.ok:
return CancelCardBillResponse(ok=False, message='Ошибка')
await self.session.delete(bill)
await self.session.commit()
return CancelCardBillResponse(ok=True, message='Заявка успешно отозвана')
except Exception as e:
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