feat: billing for groups of deals

This commit is contained in:
2024-11-15 20:27:16 +04:00
parent bbe9832923
commit e2d35fb7c4
6 changed files with 145 additions and 31 deletions

View File

@@ -25,7 +25,7 @@ class CreateBillRequestItems(BaseSchema):
class CreateBillRequestSchema(BaseSchema): class CreateBillRequestSchema(BaseSchema):
listener_transaction_id: int listener_transaction_id: int | str
payer_name: str payer_name: str
payer_inn: str payer_inn: str
payer_phone: str | None payer_phone: str | None
@@ -46,7 +46,7 @@ class CreateBillRequestSchema(BaseSchema):
class DeleteBillRequestSchema(BaseSchema): class DeleteBillRequestSchema(BaseSchema):
listener_transaction_id: int listener_transaction_id: int | str
class DeleteBillResponseSchema(BaseSchema): class DeleteBillResponseSchema(BaseSchema):
@@ -54,7 +54,7 @@ class DeleteBillResponseSchema(BaseSchema):
class NotifyReceivedBillRequestSchema(BaseSchema): class NotifyReceivedBillRequestSchema(BaseSchema):
listener_transaction_id: int listener_transaction_id: int | str
channel: NotificationChannel channel: NotificationChannel
received: bool received: bool
@@ -84,7 +84,7 @@ class BillPaymentStatus(BaseSchema):
class BillStatusUpdateRequest(BaseSchema): class BillStatusUpdateRequest(BaseSchema):
listener_transaction_id: int listener_transaction_id: int | str
channel: NotificationChannel channel: NotificationChannel
info: BillPaymentInfo | BillPaymentStatus info: BillPaymentInfo | BillPaymentStatus

View File

@@ -5,8 +5,9 @@ from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from models import BaseModel from models import BaseModel
if TYPE_CHECKING: if TYPE_CHECKING:
from models import Deal from models import Deal, DealGroup
class DealBillRequest(BaseModel): class DealBillRequest(BaseModel):
@@ -23,3 +24,19 @@ class DealBillRequest(BaseModel):
pdf_url: Mapped[str] = mapped_column(nullable=True) pdf_url: Mapped[str] = mapped_column(nullable=True)
invoice_number: 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)

View File

@@ -4,6 +4,7 @@ from sqlalchemy import ForeignKey, Table, Column
from sqlalchemy.orm import mapped_column, Mapped, relationship from sqlalchemy.orm import mapped_column, Mapped, relationship
from models import BaseModel from models import BaseModel
from models import GroupBillRequest
if TYPE_CHECKING: if TYPE_CHECKING:
from models import Deal from models import Deal
@@ -24,6 +25,10 @@ class DealGroup(BaseModel):
back_populates='group', back_populates='group',
secondary='deal_relations' secondary='deal_relations'
) )
bill_request: Mapped[Optional['GroupBillRequest']] = relationship(
back_populates='group',
lazy='joined'
)
deal_relations = Table( deal_relations = Table(

View File

@@ -11,6 +11,13 @@ class DealBillRequestSchema(BaseSchema):
pdf_url: Optional[str] pdf_url: Optional[str]
invoice_number: 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 # endregion
# region Requests # region Requests

View File

@@ -6,7 +6,7 @@ from pydantic import constr, field_validator
from models import ServiceCategoryPrice, ServicePriceCategory, Deal, Product, DealProduct, DealStatusHistory from models import ServiceCategoryPrice, ServicePriceCategory, Deal, Product, DealProduct, DealStatusHistory
from schemas.base import BaseSchema, OkMessageSchema from schemas.base import BaseSchema, OkMessageSchema
from schemas.billing import DealBillRequestSchema from schemas.billing import DealBillRequestSchema, GroupBillRequestSchema
from schemas.client import ClientSchema from schemas.client import ClientSchema
from schemas.marketplace import BaseMarketplaceSchema from schemas.marketplace import BaseMarketplaceSchema
from schemas.product import ProductSchema from schemas.product import ProductSchema
@@ -27,6 +27,7 @@ class DealGroupSchema(BaseSchema):
id: int id: int
name: Optional[str] = None name: Optional[str] = None
lexorank: str lexorank: str
bill_request: Optional[GroupBillRequestSchema] = None
class DealSummary(BaseSchema): class DealSummary(BaseSchema):
@@ -99,6 +100,7 @@ class DealSchema(BaseSchema):
shipping_warehouse: Optional[Union[ShippingWarehouseSchema, str]] = None shipping_warehouse: Optional[Union[ShippingWarehouseSchema, str]] = None
bill_request: Optional[DealBillRequestSchema] = None bill_request: Optional[DealBillRequestSchema] = None
category: Optional[ServicePriceCategorySchema] = None category: Optional[ServicePriceCategorySchema] = None
group: Optional[DealGroupSchema] = None
delivery_date: Optional[datetime.datetime] = None delivery_date: Optional[datetime.datetime] = None
receiving_slot_date: Optional[datetime.datetime] = None receiving_slot_date: Optional[datetime.datetime] = None

View File

@@ -1,4 +1,4 @@
from collections import defaultdict import logging
from io import BytesIO from io import BytesIO
from typing import List from typing import List
from uuid import uuid4 from uuid import uuid4
@@ -12,12 +12,12 @@ from weasyprint import HTML, CSS
import backend.config import backend.config
import constants import constants
import models
from constants import MONTHS, ENV from constants import MONTHS, ENV
from external.billing import BillingClient, CreateBillingRequestValue, CreateBillRequestSchema, CreateBillRequestItems, \ from external.billing import BillingClient, CreateBillingRequestValue, CreateBillRequestSchema, CreateBillRequestItems, \
BillStatusUpdateRequest, NotificationChannel, NotifyReceivedBillRequestSchema, DeleteBillRequestSchema, \ BillStatusUpdateRequest, NotificationChannel, NotifyReceivedBillRequestSchema, DeleteBillRequestSchema, \
ProductBillingDocumentPdf, ServiceBillingDocumentPdf 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 schemas.billing import *
from services.base import BaseService from services.base import BaseService
from services.deal import DealService from services.deal import DealService
@@ -25,6 +25,30 @@ from utils.list_utils import to_locale_number
class BillingService(BaseService): 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( async def _process_update_details(
self, self,
request: BillStatusUpdateRequest request: BillStatusUpdateRequest
@@ -36,13 +60,36 @@ class BillingService(BaseService):
received=True received=True
) )
response = await billing_client.notify_received(notify_received_request) 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: if not response.ok:
return return
deal_bill_request.pdf_url = request.info.pdf_url if type(request.listener_transaction_id) is int:
deal_bill_request.invoice_number = request.info.invoice_number await self._process_deal_update_details(request)
else:
await self._process_group_update_details(request)
await self.session.commit() 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( async def _process_update_verification(
self, self,
request: BillStatusUpdateRequest request: BillStatusUpdateRequest
@@ -56,10 +103,10 @@ class BillingService(BaseService):
response = await billing_client.notify_received(notify_received_request) response = await billing_client.notify_received(notify_received_request)
if not response.ok: if not response.ok:
return return
deal_bill_request = await self._get_deal_bill_by_id(request.listener_transaction_id) if type(request.listener_transaction_id) is int:
if not deal_bill_request: await self._process_deal_update_verification(request)
return else:
deal_bill_request.paid = request.info.payed await self._process_group_update_verification(request)
await self.session.commit() await self.session.commit()
async def process_update( async def process_update(
@@ -71,6 +118,25 @@ 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_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: async def create_deal_billing(self, user, request: CreateDealBillRequest) -> CreateDealBillResponse:
try: try:
deal_service = DealService(self.session) deal_service = DealService(self.session)
@@ -88,7 +154,6 @@ class BillingService(BaseService):
is_size_needed: bool is_size_needed: bool
billing_request_values: List[CreateBillingRequestValue] = [] billing_request_values: List[CreateBillingRequestValue] = []
for service in services.values(): for service in services.values():
billing_request_values.append( billing_request_values.append(
CreateBillingRequestValue( CreateBillingRequestValue(
@@ -99,8 +164,11 @@ class BillingService(BaseService):
) )
deal = basic_deal deal = basic_deal
listener_transaction_id = deal.id
if deal.group:
listener_transaction_id = f"group-{basic_deal.group.id}"
create_bill_request = CreateBillRequestSchema( create_bill_request = CreateBillRequestSchema(
listener_transaction_id=deal.id, listener_transaction_id=listener_transaction_id,
payer_name=deal.client.name, payer_name=deal.client.name,
payer_inn=deal.client.details.inn, payer_inn=deal.client.details.inn,
payer_phone=deal.client.details.phone_number, payer_phone=deal.client.details.phone_number,
@@ -111,13 +179,12 @@ class BillingService(BaseService):
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: if not create_bill_response.ok:
return CreateDealBillResponse(ok=create_bill_response.ok, message='Ошибка!') return CreateDealBillResponse(ok=create_bill_response.ok, message='Ошибка!')
deal_bill_request = DealBillRequest(
deal_id=request.deal_id, if basic_deal.group:
created_at=datetime.datetime.now() await self.create_group_bill_request(basic_deal.group)
) else:
self.session.add(deal_bill_request) await self.create_deal_bill_request(basic_deal)
deal.is_locked = True
await self.session.commit()
return CreateDealBillResponse(ok=create_bill_response.ok, message='Заявка успешно создана!') return CreateDealBillResponse(ok=create_bill_response.ok, message='Заявка успешно создана!')
except Exception as e: except Exception as e:
return CreateDealBillResponse(ok=False, message=str(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]: 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)) 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: async def get_deal_bill_by_id(self, deal_id: int) -> GetDealBillById:
deal_bill = await self._get_deal_bill_by_id(deal_id) deal_bill = await self._get_deal_bill_by_id(deal_id)
if not deal_bill: if not deal_bill:
@@ -133,14 +203,27 @@ class BillingService(BaseService):
async def cancel_deal_billing(self, user, request: CancelDealBillRequest) -> CancelDealBillResponse: async def cancel_deal_billing(self, user, request: CancelDealBillRequest) -> CancelDealBillResponse:
try: try:
deal_bill = await self._get_deal_bill_by_id(request.deal_id) deal = await self._get_deal_by_id(request.deal_id)
if not deal_bill: if not deal:
return CancelDealBillResponse(ok=False, message='Заявка не найдена') 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 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: if not response.ok:
return CancelDealBillResponse(ok=False, message='Ошибка') return CancelDealBillResponse(ok=False, message='Ошибка')
await self.session.delete(deal_bill)
await self.session.delete(bill)
await self.session.commit() await self.session.commit()
return CancelDealBillResponse(ok=True, message='Заявка успешно отозвана') return CancelDealBillResponse(ok=True, message='Заявка успешно отозвана')
except Exception as e: except Exception as e: