355 lines
15 KiB
Python
355 lines
15 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
|
||
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 and 'Request already exists' in create_bill_response.message:
|
||
await self.cancel_card_billing(user, CancelCardBillRequest(card_id=request.card_id, force=True))
|
||
return await self.create_card_billing(user, 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 and not request.force:
|
||
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 and not request.force:
|
||
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='Ошибка')
|
||
if bill:
|
||
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
|