From e2d35fb7c4dcb090b9a56937867bed0ccdafe38b Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Fri, 15 Nov 2024 20:27:16 +0400 Subject: [PATCH] feat: billing for groups of deals --- external/billing/schemas.py | 8 +-- models/billing.py | 19 +++++- models/deal_group.py | 5 ++ schemas/billing.py | 7 ++ schemas/deal.py | 4 +- services/billing.py | 133 +++++++++++++++++++++++++++++------- 6 files changed, 145 insertions(+), 31 deletions(-) diff --git a/external/billing/schemas.py b/external/billing/schemas.py index 96f8485..d8839b1 100644 --- a/external/billing/schemas.py +++ b/external/billing/schemas.py @@ -25,7 +25,7 @@ class CreateBillRequestItems(BaseSchema): class CreateBillRequestSchema(BaseSchema): - listener_transaction_id: int + listener_transaction_id: int | str payer_name: str payer_inn: str payer_phone: str | None @@ -46,7 +46,7 @@ class CreateBillRequestSchema(BaseSchema): class DeleteBillRequestSchema(BaseSchema): - listener_transaction_id: int + listener_transaction_id: int | str class DeleteBillResponseSchema(BaseSchema): @@ -54,7 +54,7 @@ class DeleteBillResponseSchema(BaseSchema): class NotifyReceivedBillRequestSchema(BaseSchema): - listener_transaction_id: int + listener_transaction_id: int | str channel: NotificationChannel received: bool @@ -84,7 +84,7 @@ class BillPaymentStatus(BaseSchema): class BillStatusUpdateRequest(BaseSchema): - listener_transaction_id: int + listener_transaction_id: int | str channel: NotificationChannel info: BillPaymentInfo | BillPaymentStatus diff --git a/models/billing.py b/models/billing.py index 062e223..dfb2736 100644 --- a/models/billing.py +++ b/models/billing.py @@ -5,8 +5,9 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship from models import BaseModel + if TYPE_CHECKING: - from models import Deal + from models import Deal, DealGroup class DealBillRequest(BaseModel): @@ -23,3 +24,19 @@ class DealBillRequest(BaseModel): pdf_url: Mapped[str] = mapped_column(nullable=True) invoice_number: Mapped[str] = mapped_column(nullable=True) + + +class GroupBillRequest(BaseModel): + __tablename__ = 'group_bill_requests' + + group_id: Mapped[int] = mapped_column(ForeignKey('deal_groups.id'), + nullable=False, + primary_key=True, + unique=True) + group: Mapped['DealGroup'] = relationship(back_populates='bill_request') + + created_at: Mapped[datetime.datetime] = mapped_column(nullable=False) + paid: Mapped[bool] = mapped_column(nullable=False, default=False) + + pdf_url: Mapped[str] = mapped_column(nullable=True) + invoice_number: Mapped[str] = mapped_column(nullable=True) diff --git a/models/deal_group.py b/models/deal_group.py index 6bbb26b..e3a6f7d 100644 --- a/models/deal_group.py +++ b/models/deal_group.py @@ -4,6 +4,7 @@ from sqlalchemy import ForeignKey, Table, Column from sqlalchemy.orm import mapped_column, Mapped, relationship from models import BaseModel +from models import GroupBillRequest if TYPE_CHECKING: from models import Deal @@ -24,6 +25,10 @@ class DealGroup(BaseModel): back_populates='group', secondary='deal_relations' ) + bill_request: Mapped[Optional['GroupBillRequest']] = relationship( + back_populates='group', + lazy='joined' + ) deal_relations = Table( diff --git a/schemas/billing.py b/schemas/billing.py index a63223b..21d6b6b 100644 --- a/schemas/billing.py +++ b/schemas/billing.py @@ -11,6 +11,13 @@ class DealBillRequestSchema(BaseSchema): pdf_url: Optional[str] invoice_number: Optional[str] +class GroupBillRequestSchema(BaseSchema): + group_id: int + created_at: datetime.datetime + paid: bool + pdf_url: Optional[str] + invoice_number: Optional[str] + # endregion # region Requests diff --git a/schemas/deal.py b/schemas/deal.py index 1ddc03f..5c8a86f 100644 --- a/schemas/deal.py +++ b/schemas/deal.py @@ -6,7 +6,7 @@ from pydantic import constr, field_validator from models import ServiceCategoryPrice, ServicePriceCategory, Deal, Product, DealProduct, DealStatusHistory from schemas.base import BaseSchema, OkMessageSchema -from schemas.billing import DealBillRequestSchema +from schemas.billing import DealBillRequestSchema, GroupBillRequestSchema from schemas.client import ClientSchema from schemas.marketplace import BaseMarketplaceSchema from schemas.product import ProductSchema @@ -27,6 +27,7 @@ class DealGroupSchema(BaseSchema): id: int name: Optional[str] = None lexorank: str + bill_request: Optional[GroupBillRequestSchema] = None class DealSummary(BaseSchema): @@ -99,6 +100,7 @@ class DealSchema(BaseSchema): shipping_warehouse: Optional[Union[ShippingWarehouseSchema, str]] = None bill_request: Optional[DealBillRequestSchema] = None category: Optional[ServicePriceCategorySchema] = None + group: Optional[DealGroupSchema] = None delivery_date: Optional[datetime.datetime] = None receiving_slot_date: Optional[datetime.datetime] = None diff --git a/services/billing.py b/services/billing.py index 7c652bf..9346190 100644 --- a/services/billing.py +++ b/services/billing.py @@ -1,4 +1,4 @@ -from collections import defaultdict +import logging from io import BytesIO from typing import List from uuid import uuid4 @@ -12,12 +12,12 @@ from weasyprint import HTML, CSS import backend.config import constants -import models from constants import MONTHS, ENV from external.billing import BillingClient, CreateBillingRequestValue, CreateBillRequestSchema, CreateBillRequestItems, \ BillStatusUpdateRequest, NotificationChannel, NotifyReceivedBillRequestSchema, DeleteBillRequestSchema, \ ProductBillingDocumentPdf, ServiceBillingDocumentPdf -from models import DealBillRequest, Deal, DealProduct, DealService as DealServiceModel, DealProductService, DealGroup +from models import DealBillRequest, Deal, DealProduct, DealService as DealServiceModel, DealProductService, DealGroup, \ + GroupBillRequest from schemas.billing import * from services.base import BaseService from services.deal import DealService @@ -25,6 +25,30 @@ from utils.list_utils import to_locale_number class BillingService(BaseService): + async def _process_deal_update_details( + self, + request: BillStatusUpdateRequest, + ): + deal_bill_request = await self._get_deal_bill_by_id(request.listener_transaction_id) + if not deal_bill_request: + return + deal_bill_request.pdf_url = request.info.pdf_url + deal_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 @@ -36,13 +60,36 @@ class BillingService(BaseService): received=True ) response = await billing_client.notify_received(notify_received_request) - deal_bill_request = await self._get_deal_bill_by_id(request.listener_transaction_id) if not response.ok: return - deal_bill_request.pdf_url = request.info.pdf_url - deal_bill_request.invoice_number = request.info.invoice_number + if type(request.listener_transaction_id) is int: + await self._process_deal_update_details(request) + else: + await self._process_group_update_details(request) await self.session.commit() + async def _process_deal_update_verification( + self, + request: BillStatusUpdateRequest + ): + deal_bill_request = await self._get_deal_bill_by_id(request.listener_transaction_id) + if not deal_bill_request: + return + deal_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 @@ -56,10 +103,10 @@ class BillingService(BaseService): response = await billing_client.notify_received(notify_received_request) if not response.ok: return - deal_bill_request = await self._get_deal_bill_by_id(request.listener_transaction_id) - if not deal_bill_request: - return - deal_bill_request.paid = request.info.payed + if type(request.listener_transaction_id) is int: + await self._process_deal_update_verification(request) + else: + await self._process_group_update_verification(request) await self.session.commit() async def process_update( @@ -71,6 +118,25 @@ class BillingService(BaseService): elif request.channel == NotificationChannel.PAYMENT_VERIFICATION: await self._process_update_verification(request) + async def create_deal_bill_request(self, deal: Deal): + deal_bill_request = DealBillRequest( + deal_id=deal.id, + created_at=datetime.datetime.now() + ) + self.session.add(deal_bill_request) + deal.is_locked = True + await self.session.commit() + + async def create_group_bill_request(self, group: DealGroup): + group_bill_request = GroupBillRequest( + group_id=group.id, + created_at=datetime.datetime.now() + ) + self.session.add(group_bill_request) + for deal in group.deals: + deal.is_locked = True + await self.session.commit() + async def create_deal_billing(self, user, request: CreateDealBillRequest) -> CreateDealBillResponse: try: deal_service = DealService(self.session) @@ -88,7 +154,6 @@ class BillingService(BaseService): is_size_needed: bool billing_request_values: List[CreateBillingRequestValue] = [] - for service in services.values(): billing_request_values.append( CreateBillingRequestValue( @@ -99,8 +164,11 @@ class BillingService(BaseService): ) deal = basic_deal + listener_transaction_id = deal.id + if deal.group: + listener_transaction_id = f"group-{basic_deal.group.id}" create_bill_request = CreateBillRequestSchema( - listener_transaction_id=deal.id, + listener_transaction_id=listener_transaction_id, payer_name=deal.client.name, payer_inn=deal.client.details.inn, payer_phone=deal.client.details.phone_number, @@ -111,13 +179,12 @@ class BillingService(BaseService): create_bill_response = await billing_client.create(create_bill_request) if not create_bill_response.ok: return CreateDealBillResponse(ok=create_bill_response.ok, message='Ошибка!') - deal_bill_request = DealBillRequest( - deal_id=request.deal_id, - created_at=datetime.datetime.now() - ) - self.session.add(deal_bill_request) - deal.is_locked = True - await self.session.commit() + + if basic_deal.group: + await self.create_group_bill_request(basic_deal.group) + else: + await self.create_deal_bill_request(basic_deal) + return CreateDealBillResponse(ok=create_bill_response.ok, message='Заявка успешно создана!') except Exception as e: return CreateDealBillResponse(ok=False, message=str(e)) @@ -125,6 +192,9 @@ class BillingService(BaseService): async def _get_deal_bill_by_id(self, deal_id: int) -> Optional[DealBillRequest]: return await self.session.scalar(select(DealBillRequest).where(DealBillRequest.deal_id == deal_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_deal_bill_by_id(self, deal_id: int) -> GetDealBillById: deal_bill = await self._get_deal_bill_by_id(deal_id) if not deal_bill: @@ -133,14 +203,27 @@ class BillingService(BaseService): async def cancel_deal_billing(self, user, request: CancelDealBillRequest) -> CancelDealBillResponse: try: - deal_bill = await self._get_deal_bill_by_id(request.deal_id) - if not deal_bill: - return CancelDealBillResponse(ok=False, message='Заявка не найдена') - billing_client = BillingClient(backend.config.BILLING_API_KEY) - response = await billing_client.delete(DeleteBillRequestSchema(listener_transaction_id=request.deal_id)) + deal = await self._get_deal_by_id(request.deal_id) + if not deal: + return CancelDealBillResponse(ok=False, message='Сделка не найдена') + + if deal.group: + bill = await self._get_group_bill_by_id(deal.group.id) + if not bill: + return CancelDealBillResponse(ok=False, message='Заявка не найдена') + billing_client = BillingClient(backend.config.BILLING_API_KEY) + response = await billing_client.delete(DeleteBillRequestSchema(listener_transaction_id=deal.group.id)) + else: + bill = await self._get_deal_bill_by_id(request.deal_id) + if not bill: + return CancelDealBillResponse(ok=False, message='Заявка не найдена') + billing_client = BillingClient(backend.config.BILLING_API_KEY) + response = await billing_client.delete(DeleteBillRequestSchema(listener_transaction_id=request.deal_id)) + if not response.ok: return CancelDealBillResponse(ok=False, message='Ошибка') - await self.session.delete(deal_bill) + + await self.session.delete(bill) await self.session.commit() return CancelDealBillResponse(ok=True, message='Заявка успешно отозвана') except Exception as e: