feat: split bills for one deal

This commit is contained in:
2025-05-09 13:32:30 +04:00
parent 42ce73dd6a
commit be13c77164
12 changed files with 154 additions and 90 deletions

View File

@@ -24,7 +24,9 @@ TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN')
SECRET_KEY = os.environ.get('SECRET_KEY') SECRET_KEY = os.environ.get('SECRET_KEY')
S3_API_KEY = os.environ.get('S3_API_KEY') S3_API_KEY = os.environ.get('S3_API_KEY')
# Billing
BILLING_API_KEY = os.environ.get('BILLING_API_KEY') BILLING_API_KEY = os.environ.get('BILLING_API_KEY')
BILLING_URL = os.environ.get('BILLING_URL')
CHAT_TELEGRAM_BOT_TOKEN = os.environ.get('CHAT_TELEGRAM_BOT_TOKEN') CHAT_TELEGRAM_BOT_TOKEN = os.environ.get('CHAT_TELEGRAM_BOT_TOKEN')
CHAT_CONNECTOR_API_KEY = os.environ.get('CHAT_CONNECTOR_API_KEY') CHAT_CONNECTOR_API_KEY = os.environ.get('CHAT_CONNECTOR_API_KEY')

View File

@@ -1,5 +1,6 @@
import aiohttp import aiohttp
from backend.config import BILLING_URL
from .schemas import * from .schemas import *
@@ -9,7 +10,7 @@ class BillingClient:
self.headers = { self.headers = {
'Authorization': f'Bearer {self.api_key}' 'Authorization': f'Bearer {self.api_key}'
} }
self.base_url = 'https://billing.denco.store' self.base_url = BILLING_URL
async def _method(self, http_method, method, **kwargs): async def _method(self, http_method, method, **kwargs):
async with aiohttp.ClientSession(headers=self.headers) as session: async with aiohttp.ClientSession(headers=self.headers) as session:

View File

@@ -13,13 +13,13 @@ if TYPE_CHECKING:
class CardBillRequest(BaseModel): class CardBillRequest(BaseModel):
__tablename__ = 'card_bill_requests' __tablename__ = 'card_bill_requests'
id: Mapped[int] = mapped_column(primary_key=True)
card_id: Mapped[int] = mapped_column( card_id: Mapped[int] = mapped_column(
ForeignKey('cards.id'), ForeignKey('cards.id'),
nullable=False, nullable=False,
primary_key=True,
unique=True,
) )
card: Mapped['Card'] = relationship(back_populates='bill_request') card: Mapped['Card'] = relationship(back_populates='bill_requests')
created_at: Mapped[datetime.datetime] = mapped_column(nullable=False) created_at: Mapped[datetime.datetime] = mapped_column(nullable=False)
paid: Mapped[bool] = mapped_column(nullable=False, default=False) paid: Mapped[bool] = mapped_column(nullable=False, default=False)
@@ -31,13 +31,13 @@ class CardBillRequest(BaseModel):
class GroupBillRequest(BaseModel): class GroupBillRequest(BaseModel):
__tablename__ = 'group_bill_requests' __tablename__ = 'group_bill_requests'
id: Mapped[int] = mapped_column(primary_key=True)
group_id: Mapped[int] = mapped_column( group_id: Mapped[int] = mapped_column(
ForeignKey('card_groups.id'), ForeignKey('card_groups.id'),
nullable=False, nullable=False,
primary_key=True,
unique=True,
) )
group: Mapped['CardGroup'] = relationship(back_populates='bill_request') group: Mapped['CardGroup'] = relationship(back_populates='bill_requests')
created_at: Mapped[datetime.datetime] = mapped_column(nullable=False) created_at: Mapped[datetime.datetime] = mapped_column(nullable=False)
paid: Mapped[bool] = mapped_column(nullable=False, default=False) paid: Mapped[bool] = mapped_column(nullable=False, default=False)

View File

@@ -93,7 +93,11 @@ class Card(BaseModel):
order_by="desc(CardProduct.product_id)" order_by="desc(CardProduct.product_id)"
) )
bill_request: Mapped[Optional['CardBillRequest']] = relationship(back_populates='card', lazy='joined') bill_requests: Mapped[list['CardBillRequest']] = relationship(
back_populates='card',
lazy='selectin',
uselist=True,
)
# module client # module client
client_id: Mapped[Optional[int]] = mapped_column( client_id: Mapped[Optional[int]] = mapped_column(

View File

@@ -25,9 +25,10 @@ class CardGroup(BaseModel):
back_populates='group', back_populates='group',
secondary='card_relations' secondary='card_relations'
) )
bill_request: Mapped[Optional['GroupBillRequest']] = relationship( bill_requests: Mapped[list['GroupBillRequest']] = relationship(
back_populates='group', back_populates='group',
lazy='joined' lazy='joined',
uselist=True,
) )

View File

@@ -45,9 +45,8 @@ async def create_deal_bill(
async def cancel_deal_billing( async def cancel_deal_billing(
session: SessionDependency, session: SessionDependency,
request: CancelCardBillRequest, request: CancelCardBillRequest,
user: CurrentUserDependency
): ):
return await BillingService(session).cancel_card_billing(user, request) return await BillingService(session).cancel_card_billing(request)
@billing_router.get( @billing_router.get(

View File

@@ -30,7 +30,6 @@ class CreateCardBillRequest(BaseSchema):
class CancelCardBillRequest(BaseSchema): class CancelCardBillRequest(BaseSchema):
card_id: int card_id: int
force: Optional[bool] = False
# endregion # endregion

View File

@@ -49,7 +49,7 @@ class CardSummary(BaseSchema):
shipment_warehouse_id: Optional[int] shipment_warehouse_id: Optional[int]
shipment_warehouse_name: Optional[str] shipment_warehouse_name: Optional[str]
bill_request: Optional[CardBillRequestSchema] = None bill_requests: list[CardBillRequestSchema]
group: Optional[CardGroupSchema] = None group: Optional[CardGroupSchema] = None
@@ -98,7 +98,7 @@ class BaseCardSchema(BaseSchema):
client_id: Optional[int] client_id: Optional[int]
client: Optional[ClientSchema] client: Optional[ClientSchema]
shipping_warehouse: Optional[Union[ShippingWarehouseSchema, str]] = None shipping_warehouse: Optional[Union[ShippingWarehouseSchema, str]] = None
bill_request: Optional[CardBillRequestSchema] = None bill_requests: list[CardBillRequestSchema] = []
group: Optional[CardGroupSchema] = None group: Optional[CardGroupSchema] = None
manager: Optional[UserSchema] = None manager: Optional[UserSchema] = None
pallets: List[PalletSchema] = [] pallets: List[PalletSchema] = []

View File

@@ -10,7 +10,7 @@ class CardGroupSchema(BaseSchema):
id: int id: int
name: Optional[str] = None name: Optional[str] = None
lexorank: str lexorank: str
bill_request: Optional[GroupBillRequestSchema] = None bill_requests: list[GroupBillRequestSchema] = []
# endregion # endregion

View File

@@ -41,8 +41,8 @@ class BillingService(BaseService):
prefix = "group-" prefix = "group-"
if not request.listener_transaction_id.startswith(prefix): if not request.listener_transaction_id.startswith(prefix):
return return
group_id = int(request.listener_transaction_id.removeprefix(prefix)) request_id = int(request.listener_transaction_id.removeprefix(prefix))
group_bill_request = await self._get_group_bill_by_id(group_id) group_bill_request = await self._get_group_bill_by_id(request_id)
if not group_bill_request: if not group_bill_request:
return return
group_bill_request.pdf_url = request.info.pdf_url group_bill_request.pdf_url = request.info.pdf_url
@@ -117,24 +117,43 @@ class BillingService(BaseService):
elif request.channel == NotificationChannel.PAYMENT_VERIFICATION: elif request.channel == NotificationChannel.PAYMENT_VERIFICATION:
await self._process_update_verification(request) await self._process_update_verification(request)
async def create_card_bill_request(self, card: Card): async def create_card_bill_request(self, card: Card) -> CardBillRequest:
card_bill_request = CardBillRequest( card_bill_request = CardBillRequest(
card_id=card.id, card_id=card.id,
created_at=datetime.datetime.now() created_at=datetime.datetime.now()
) )
self.session.add(card_bill_request) self.session.add(card_bill_request)
card.is_locked = True await self.session.flush()
await self.session.commit() return card_bill_request
async def create_group_bill_request(self, group: CardGroup): async def create_group_bill_request(self, group: CardGroup) -> GroupBillRequest:
group_bill_request = GroupBillRequest( group_bill_request = GroupBillRequest(
group_id=group.id, group_id=group.id,
created_at=datetime.datetime.now() created_at=datetime.datetime.now()
) )
self.session.add(group_bill_request) self.session.add(group_bill_request)
for card in group.cards:
card.is_locked = True await self.session.flush()
await self.session.commit() 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: async def create_card_billing(self, user, request: CreateCardBillRequest) -> CreateCardBillResponse:
try: try:
@@ -151,56 +170,79 @@ class BillingService(BaseService):
services: dict[str, ServiceBillingDocumentPdf] services: dict[str, ServiceBillingDocumentPdf]
products: dict[str, ProductBillingDocumentPdf] products: dict[str, ProductBillingDocumentPdf]
is_size_needed: bool is_size_needed: bool
billing_request_values: List[CreateBillingRequestValue] = [] billing_request_values: List[CreateBillingRequestValue] = self._get_billing_values_from_services(
services.values()
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 card = basic_card
listener_transaction_id = card.id transaction_prefix = "group-" if card.group else ""
if card.group: inn: str = card.client.details.inn.strip()
listener_transaction_id = f"group-{basic_card.group.id}"
inn: str = card.client.details.inn one_request_size = 100
create_bill_request = CreateBillRequestSchema( for i in range(0, len(billing_request_values), one_request_size):
listener_transaction_id=listener_transaction_id, values = billing_request_values[i:i + one_request_size]
payer_name=card.client.name,
payer_inn=inn.strip(), if basic_card.group:
payer_phone=card.client.details.phone_number, bill_request = await self.create_group_bill_request(basic_card.group)
items=CreateBillRequestItems( else:
values=billing_request_values 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)
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: if not create_bill_response.ok and 'Request already exists' in create_bill_response.message:
await self.create_group_bill_request(basic_card.group) 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: else:
await self.create_card_bill_request(basic_card) card.is_locked = True
return CreateCardBillResponse(ok=create_bill_response.ok, message='Заявка успешно создана!') await self.session.commit()
return CreateCardBillResponse(ok=True, message='Заявка успешно создана!')
except Exception as e: except Exception as e:
await self.session.rollback()
return CreateCardBillResponse(ok=False, message=str(e)) return CreateCardBillResponse(ok=False, message=str(e))
async def _get_card_bill_by_id(self, card_id: int) -> Optional[CardBillRequest]: async def _get_card_bill_by_id(self, request_id: int) -> Optional[CardBillRequest]:
return await self.session.scalar(select(CardBillRequest).where(CardBillRequest.card_id == card_id)) return await self.session.scalar(
select(CardBillRequest)
.where(CardBillRequest.id == request_id)
)
async def _get_group_bill_by_id(self, group_id: int) -> Optional[GroupBillRequest]: async def _get_card_bill_by_card_id(self, card_id: int) -> List[CardBillRequest]:
return await self.session.scalar(select(GroupBillRequest).where(GroupBillRequest.group_id == group_id)) 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: async def get_card_bill_by_id(self, card_id: int) -> GetCardBillById:
bill = await self._get_card_bill_by_id(card_id) bill = await self._get_card_bill_by_id(card_id)
@@ -208,34 +250,42 @@ class BillingService(BaseService):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Card bill was not found') raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Card bill was not found')
return GetCardBillById(card_bill=CardBillRequestSchema.model_validate(bill)) return GetCardBillById(card_bill=CardBillRequestSchema.model_validate(bill))
async def cancel_card_billing(self, user, request: CancelCardBillRequest) -> CancelCardBillResponse: 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: try:
card = await self._get_card_by_id(request.card_id) card = await self._get_card_by_id(request.card_id)
if not card: if not card:
return CancelCardBillResponse(ok=False, message='Сделка не найдена') return CancelCardBillResponse(ok=False, message='Сделка не найдена')
if card.group: if card.group:
bill = await self._get_group_bill_by_id(card.group.id) bills = await self._get_group_bill_by_group_id(card.group.id)
if not bill and not request.force:
return CancelCardBillResponse(ok=False, message='Заявка не найдена')
billing_client = BillingClient(backend.config.BILLING_API_KEY)
listener_transaction_id = f"group-{card.group.id}"
response = await billing_client.delete(DeleteBillRequestSchema(listener_transaction_id=listener_transaction_id))
else: else:
bill = await self._get_card_bill_by_id(request.card_id) bills = await self._get_card_bill_by_card_id(request.card_id)
if not bill and not request.force: if not bills:
return CancelCardBillResponse(ok=False, message='Заявка не найдена') 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: for bill in bills:
return CancelCardBillResponse(ok=False, message='Ошибка') if card.group:
if bill: listener_transaction_id = f"group-{bill.id}"
await self.session.delete(bill) else:
await self.session.commit() listener_transaction_id = str(bill.id)
await self._cancel_one_bill(bill, listener_transaction_id)
await self.session.commit()
return CancelCardBillResponse(ok=True, message='Заявка успешно отозвана') return CancelCardBillResponse(ok=True, message='Заявка успешно отозвана')
except Exception as e: except Exception as e:
await self.session.rollback()
return CancelCardBillResponse(ok=False, message=str(e)) return CancelCardBillResponse(ok=False, message=str(e))
def _gen_key_for_service(self, service: ServiceBillingDocumentPdf) -> str: def _gen_key_for_service(self, service: ServiceBillingDocumentPdf) -> str:

View File

@@ -4,7 +4,7 @@ import lexorank
import math import math
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy import select, func, update, delete, insert, and_, Select from sqlalchemy import select, func, update, delete, insert, and_, Select
from sqlalchemy.orm import joinedload, selectinload from sqlalchemy.orm import joinedload, selectinload, noload
from starlette import status from starlette import status
from card_attributes import CardAttributesCommandHandler from card_attributes import CardAttributesCommandHandler
@@ -252,9 +252,10 @@ class CardsService(BaseService):
selectinload(Card.status_history), selectinload(Card.status_history),
joinedload(Card.client), joinedload(Card.client),
joinedload(Card.shipping_warehouse), joinedload(Card.shipping_warehouse),
joinedload(Card.bill_request), selectinload(Card.bill_requests),
joinedload(Card.status), joinedload(Card.status),
joinedload(Card.board), joinedload(Card.board),
joinedload(Card.group).selectinload(CardGroup.bill_requests)
) )
.outerjoin( .outerjoin(
price_subquery, Card.id == price_subquery.c.card_id, price_subquery, Card.id == price_subquery.c.card_id,
@@ -299,7 +300,7 @@ class CardsService(BaseService):
shipment_warehouse_id=card.shipping_warehouse_id, shipment_warehouse_id=card.shipping_warehouse_id,
shipment_warehouse_name=shipment_warehouse_name, shipment_warehouse_name=shipment_warehouse_name,
total_products=products_count, total_products=products_count,
bill_request=card.bill_request, bill_requests=card.bill_requests,
tags=card.tags, tags=card.tags,
attributes=attributes, attributes=attributes,
) )
@@ -335,7 +336,7 @@ class CardsService(BaseService):
.joinedload(CardStatusHistory.user), .joinedload(CardStatusHistory.user),
selectinload(Card.status_history) selectinload(Card.status_history)
.noload(CardStatusHistory.card), .noload(CardStatusHistory.card),
selectinload(Card.bill_requests),
) )
) )
) )
@@ -1229,14 +1230,16 @@ class CardsService(BaseService):
async def _recalculate_price_group(self, group: CardGroup): async def _recalculate_price_group(self, group: CardGroup):
cards = await self.session.scalars( cards = await self.session.scalars(
select(Card) select(Card)
.join(card_relations, Card.id == card_relations.c.card_id)
.options( .options(
selectinload(Card.services) selectinload(Card.services)
.joinedload(CardService.service), .joinedload(CardService.service),
selectinload(Card.products) selectinload(Card.products)
.selectinload(CardProduct.services) .selectinload(CardProduct.services)
.joinedload(CardProductService.service), .joinedload(CardProductService.service),
noload(Card.group)
) )
.where(Card.group == group) .where(card_relations.c.group_id == group.id)
) )
cards = list(cards.all()) cards = list(cards.all())
services_quantity = await self.get_quantity_dict(cards) services_quantity = await self.get_quantity_dict(cards)

View File

@@ -52,8 +52,13 @@ class ClientService(BaseService):
async def get_all(self) -> ClientGetAllResponse: async def get_all(self) -> ClientGetAllResponse:
clients_query = await self.session.scalars( clients_query = await self.session.scalars(
select(Client) select(Client)
.options(joinedload(Client.details)) .options(
.order_by(Client.id.desc()) joinedload(Client.details),
noload(Client.products),
noload(Client.pallets),
noload(Client.boxes),
noload(Client.chat)
)
) )
clients = clients_query.all() clients = clients_query.all()
result = [] result = []