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):
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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='Сделка не найдена')
|
||||||
|
|
||||||
|
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='Заявка не найдена')
|
return CancelDealBillResponse(ok=False, message='Заявка не найдена')
|
||||||
billing_client = BillingClient(backend.config.BILLING_API_KEY)
|
billing_client = BillingClient(backend.config.BILLING_API_KEY)
|
||||||
response = await billing_client.delete(DeleteBillRequestSchema(listener_transaction_id=request.deal_id))
|
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:
|
||||||
|
|||||||
Reference in New Issue
Block a user