From be13c77164b14f88fe8a0ea2b6d58b3a304fafe6 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Fri, 9 May 2025 13:32:30 +0400 Subject: [PATCH] feat: split bills for one deal --- backend/config.py | 2 + external/billing/billing_client.py | 3 +- models/billing.py | 12 +- models/card.py | 6 +- models/card_group.py | 5 +- routers/billing.py | 3 +- schemas/billing.py | 1 - schemas/card.py | 4 +- schemas/group.py | 2 +- services/billing.py | 184 ++++++++++++++++++----------- services/card.py | 13 +- services/client.py | 9 +- 12 files changed, 154 insertions(+), 90 deletions(-) diff --git a/backend/config.py b/backend/config.py index b1a7d93..c216682 100644 --- a/backend/config.py +++ b/backend/config.py @@ -24,7 +24,9 @@ TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN') SECRET_KEY = os.environ.get('SECRET_KEY') S3_API_KEY = os.environ.get('S3_API_KEY') +# Billing 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_CONNECTOR_API_KEY = os.environ.get('CHAT_CONNECTOR_API_KEY') diff --git a/external/billing/billing_client.py b/external/billing/billing_client.py index 24887bf..6aa4156 100644 --- a/external/billing/billing_client.py +++ b/external/billing/billing_client.py @@ -1,5 +1,6 @@ import aiohttp +from backend.config import BILLING_URL from .schemas import * @@ -9,7 +10,7 @@ class BillingClient: self.headers = { '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 with aiohttp.ClientSession(headers=self.headers) as session: diff --git a/models/billing.py b/models/billing.py index 5a2159e..19fcd8a 100644 --- a/models/billing.py +++ b/models/billing.py @@ -13,13 +13,13 @@ if TYPE_CHECKING: class CardBillRequest(BaseModel): __tablename__ = 'card_bill_requests' + id: Mapped[int] = mapped_column(primary_key=True) + card_id: Mapped[int] = mapped_column( ForeignKey('cards.id'), 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) paid: Mapped[bool] = mapped_column(nullable=False, default=False) @@ -31,13 +31,13 @@ class CardBillRequest(BaseModel): class GroupBillRequest(BaseModel): __tablename__ = 'group_bill_requests' + id: Mapped[int] = mapped_column(primary_key=True) + group_id: Mapped[int] = mapped_column( ForeignKey('card_groups.id'), 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) paid: Mapped[bool] = mapped_column(nullable=False, default=False) diff --git a/models/card.py b/models/card.py index 5a83446..a41e840 100644 --- a/models/card.py +++ b/models/card.py @@ -93,7 +93,11 @@ class Card(BaseModel): 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 client_id: Mapped[Optional[int]] = mapped_column( diff --git a/models/card_group.py b/models/card_group.py index 92bdb5b..e11f51e 100644 --- a/models/card_group.py +++ b/models/card_group.py @@ -25,9 +25,10 @@ class CardGroup(BaseModel): back_populates='group', secondary='card_relations' ) - bill_request: Mapped[Optional['GroupBillRequest']] = relationship( + bill_requests: Mapped[list['GroupBillRequest']] = relationship( back_populates='group', - lazy='joined' + lazy='joined', + uselist=True, ) diff --git a/routers/billing.py b/routers/billing.py index d1d2447..1e962e3 100644 --- a/routers/billing.py +++ b/routers/billing.py @@ -45,9 +45,8 @@ async def create_deal_bill( async def cancel_deal_billing( session: SessionDependency, request: CancelCardBillRequest, - user: CurrentUserDependency ): - return await BillingService(session).cancel_card_billing(user, request) + return await BillingService(session).cancel_card_billing(request) @billing_router.get( diff --git a/schemas/billing.py b/schemas/billing.py index d1adf8a..de8cafa 100644 --- a/schemas/billing.py +++ b/schemas/billing.py @@ -30,7 +30,6 @@ class CreateCardBillRequest(BaseSchema): class CancelCardBillRequest(BaseSchema): card_id: int - force: Optional[bool] = False # endregion diff --git a/schemas/card.py b/schemas/card.py index 35acd13..f69fe7a 100644 --- a/schemas/card.py +++ b/schemas/card.py @@ -49,7 +49,7 @@ class CardSummary(BaseSchema): shipment_warehouse_id: Optional[int] shipment_warehouse_name: Optional[str] - bill_request: Optional[CardBillRequestSchema] = None + bill_requests: list[CardBillRequestSchema] group: Optional[CardGroupSchema] = None @@ -98,7 +98,7 @@ class BaseCardSchema(BaseSchema): client_id: Optional[int] client: Optional[ClientSchema] shipping_warehouse: Optional[Union[ShippingWarehouseSchema, str]] = None - bill_request: Optional[CardBillRequestSchema] = None + bill_requests: list[CardBillRequestSchema] = [] group: Optional[CardGroupSchema] = None manager: Optional[UserSchema] = None pallets: List[PalletSchema] = [] diff --git a/schemas/group.py b/schemas/group.py index 56519db..fe83b2e 100644 --- a/schemas/group.py +++ b/schemas/group.py @@ -10,7 +10,7 @@ class CardGroupSchema(BaseSchema): id: int name: Optional[str] = None lexorank: str - bill_request: Optional[GroupBillRequestSchema] = None + bill_requests: list[GroupBillRequestSchema] = [] # endregion diff --git a/services/billing.py b/services/billing.py index 2cb555a..1d9c2d1 100644 --- a/services/billing.py +++ b/services/billing.py @@ -41,8 +41,8 @@ class BillingService(BaseService): 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) + 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 @@ -117,24 +117,43 @@ class BillingService(BaseService): elif request.channel == NotificationChannel.PAYMENT_VERIFICATION: 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_id=card.id, created_at=datetime.datetime.now() ) self.session.add(card_bill_request) - card.is_locked = True - await self.session.commit() + await self.session.flush() + 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_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() + + 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: @@ -151,56 +170,79 @@ class BillingService(BaseService): 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 - ) - ) + billing_request_values: List[CreateBillingRequestValue] = self._get_billing_values_from_services( + services.values() + ) 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 + 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_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 'Неизвестная ошибка') + create_bill_response = await billing_client.create(create_bill_request) - if basic_card.group: - await self.create_group_bill_request(basic_card.group) + 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: - 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: + await self.session.rollback() 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_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_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_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) @@ -208,34 +250,42 @@ class BillingService(BaseService): 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: + 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: - 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) - listener_transaction_id = f"group-{card.group.id}" - - response = await billing_client.delete(DeleteBillRequestSchema(listener_transaction_id=listener_transaction_id)) + bills = await self._get_group_bill_by_group_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)) + bills = await self._get_card_bill_by_card_id(request.card_id) + if not bills: + return CancelCardBillResponse(ok=False, message='Заявка не найдена') - if not response.ok: - return CancelCardBillResponse(ok=False, message='Ошибка') - if bill: - await self.session.delete(bill) - await self.session.commit() + 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: diff --git a/services/card.py b/services/card.py index 96edeec..689e795 100644 --- a/services/card.py +++ b/services/card.py @@ -4,7 +4,7 @@ import lexorank import math from fastapi import HTTPException 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 card_attributes import CardAttributesCommandHandler @@ -252,9 +252,10 @@ class CardsService(BaseService): selectinload(Card.status_history), joinedload(Card.client), joinedload(Card.shipping_warehouse), - joinedload(Card.bill_request), + selectinload(Card.bill_requests), joinedload(Card.status), joinedload(Card.board), + joinedload(Card.group).selectinload(CardGroup.bill_requests) ) .outerjoin( 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_name=shipment_warehouse_name, total_products=products_count, - bill_request=card.bill_request, + bill_requests=card.bill_requests, tags=card.tags, attributes=attributes, ) @@ -335,7 +336,7 @@ class CardsService(BaseService): .joinedload(CardStatusHistory.user), selectinload(Card.status_history) .noload(CardStatusHistory.card), - + selectinload(Card.bill_requests), ) ) ) @@ -1229,14 +1230,16 @@ class CardsService(BaseService): async def _recalculate_price_group(self, group: CardGroup): cards = await self.session.scalars( select(Card) + .join(card_relations, Card.id == card_relations.c.card_id) .options( selectinload(Card.services) .joinedload(CardService.service), selectinload(Card.products) .selectinload(CardProduct.services) .joinedload(CardProductService.service), + noload(Card.group) ) - .where(Card.group == group) + .where(card_relations.c.group_id == group.id) ) cards = list(cards.all()) services_quantity = await self.get_quantity_dict(cards) diff --git a/services/client.py b/services/client.py index 6214466..145cc5e 100644 --- a/services/client.py +++ b/services/client.py @@ -52,8 +52,13 @@ class ClientService(BaseService): async def get_all(self) -> ClientGetAllResponse: clients_query = await self.session.scalars( select(Client) - .options(joinedload(Client.details)) - .order_by(Client.id.desc()) + .options( + joinedload(Client.details), + noload(Client.products), + noload(Client.pallets), + noload(Client.boxes), + noload(Client.chat) + ) ) clients = clients_query.all() result = []