feat: billing for groups of deals
This commit is contained in:
		
							
								
								
									
										8
									
								
								external/billing/schemas.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								external/billing/schemas.py
									
									
									
									
										vendored
									
									
								
							@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user