feat: billing guest access

This commit is contained in:
2024-08-08 07:49:53 +03:00
parent a7c4fabed0
commit 97f835ffde
30 changed files with 682 additions and 140 deletions

View File

@@ -16,3 +16,5 @@ 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_API_KEY = os.environ.get('BILLING_API_KEY')

View File

@@ -6,9 +6,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
from backend.session import get_session
from models import User
from schemas.base import PaginationSchema
from services.auth import get_current_user
from services.auth import get_current_user, authorized_user, guest_user
from utils.dependecies import pagination_parameters
SessionDependency = Annotated[AsyncSession, Depends(get_session)]
PaginationDependency = Annotated[PaginationSchema, Depends(pagination_parameters)]
CurrentUserDependency = Annotated[User, Depends(get_current_user)]
AuthorizedUserDependency = Annotated[User, Depends(authorized_user)]
GuestUserDependency = Annotated[User, Depends(guest_user)]

3
external/billing/__init__.py vendored Normal file
View File

@@ -0,0 +1,3 @@
from .schemas import *
from .enums import *
from .billing_client import BillingClient

27
external/billing/billing_client.py vendored Normal file
View File

@@ -0,0 +1,27 @@
import aiohttp
from .schemas import *
class BillingClient:
def __init__(self, api_key: str):
self.api_key = api_key
self.headers = {
'Authorization': f'Bearer {self.api_key}'
}
self.base_url = 'https://billing.denco.store'
async def _method(self, http_method, method, **kwargs):
async with aiohttp.ClientSession(headers=self.headers) as session:
async with session.request(http_method, self.base_url + method, **kwargs) as response:
return await response.json()
async def create(self, request: CreateBillRequestSchema) -> CreateBillingResponseSchema:
json_data = request.model_dump()
response = await self._method('POST', '/create', json=json_data)
return CreateBillingResponseSchema.model_validate(response)
async def notify_received(self, request: NotifyReceivedBillRequestSchema) -> NotifyReceivedBillResponseSchema:
json_data = request.model_dump()
response = await self._method('POST', '/notify-received', json=json_data)
return NotifyReceivedBillResponseSchema.model_validate(response)

6
external/billing/enums.py vendored Normal file
View File

@@ -0,0 +1,6 @@
from enum import StrEnum
class NotificationChannel(StrEnum):
PAYMENT_DETAILS = 'PAYMENT_DETAILS'
PAYMENT_VERIFICATION = 'PAYMENT_VERIFICATION'

81
external/billing/schemas.py vendored Normal file
View File

@@ -0,0 +1,81 @@
import re
from typing import List
from pydantic import field_validator
from schemas.base import BaseSchema
from .enums import NotificationChannel
from datetime import datetime as dt
from datetime import date
phone_regexp = re.compile(r'^((\+7)([0-9]){10})$')
class CreateBillingRequestValue(BaseSchema):
name: str = ""
unit: str = "Руб"
vat: str = "None"
price: int
amount: int = 1
class CreateBillRequestItems(BaseSchema):
values: List[CreateBillingRequestValue]
class CreateBillRequestSchema(BaseSchema):
listener_transaction_id: int
payer_name: str
payer_inn: int
payer_phone: str | None
items: CreateBillRequestItems
@field_validator("payer_phone", mode="before")
def payer_phone_validator(cls, phone: str) -> str:
if phone is None:
return None
if not phone.startswith("+"):
phone = f"+{phone}"
if phone_regexp.match(phone):
return phone
return None
class NotifyReceivedBillRequestSchema(BaseSchema):
listener_transaction_id: int
channel: NotificationChannel
received: bool
class CreateBillingResponseSchema(BaseSchema):
ok: bool
class NotifyReceivedBillResponseSchema(BaseSchema):
ok: bool
class BillPaymentInfo(BaseSchema):
pdf_url: str
due_date: date
payment_amount: int
invoice_number: str
@field_validator('due_date', mode='plain')
def serialize_due_date(value: str) -> date:
date = dt.strptime(value, '%Y-%m-%d').date()
return date
class BillPaymentStatus(BaseSchema):
payed: bool
class BillStatusUpdateRequest(BaseSchema):
listener_transaction_id: int
channel: NotificationChannel
info: BillPaymentInfo | BillPaymentStatus

View File

@@ -42,6 +42,7 @@ routers_list = [
routers.marketplace_router,
routers.payroll_router,
routers.time_tracking_router,
routers.billing_router
]
for router in routers_list:
app.include_router(router)

View File

@@ -10,5 +10,6 @@ from .barcode import *
from .shipping_warehouse import *
from .marketplace import *
from .payroll import *
from .billing import *
configure_mappers()

25
models/billing.py Normal file
View File

@@ -0,0 +1,25 @@
import datetime
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models import BaseModel
if TYPE_CHECKING:
from models import Deal
class DealBillRequest(BaseModel):
__tablename__ = 'deal_bill_requests'
deal_id: Mapped[int] = mapped_column(ForeignKey('deals.id'),
nullable=False,
primary_key=True,
unique=True)
deal: Mapped['Deal'] = 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

@@ -1,4 +1,5 @@
from enum import IntEnum, unique
from typing import Optional, TYPE_CHECKING
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean
from sqlalchemy.orm import relationship, backref, Mapped, mapped_column
@@ -7,6 +8,9 @@ from models.base import BaseModel
from .marketplace import BaseMarketplace
from .shipping_warehouse import ShippingWarehouse
if TYPE_CHECKING:
from . import DealBillRequest
@unique
class DealStatus(IntEnum):
@@ -33,6 +37,7 @@ class Deal(BaseModel):
is_deleted = Column(Boolean, nullable=False, server_default='0', default=False, comment='Удалена')
is_completed = Column(Boolean, nullable=False, server_default='0', default=False, comment='Завершена')
is_locked: Mapped[bool] = mapped_column(default=False, server_default='0')
shipping_warehouse_id: Mapped[int] = mapped_column(ForeignKey('shipping_warehouses.id'), nullable=True)
shipping_warehouse: Mapped["ShippingWarehouse"] = relationship()
@@ -58,6 +63,7 @@ class Deal(BaseModel):
lexorank = Column(String, nullable=False, comment='Lexorank', index=True)
comment = Column(String, nullable=False, server_default='', comment='Коментарий к заданию')
bill_request: Mapped[Optional['DealBillRequest']] = relationship(back_populates='deal', lazy='joined')
class DealStatusHistory(BaseModel):

View File

@@ -11,3 +11,4 @@ from .role import role_router
from .marketplace import marketplace_router
from .payroll import payroll_router
from .time_tracking import time_tracking_router
from .billing import billing_router

View File

@@ -4,19 +4,14 @@ from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from backend.session import get_session
from schemas.barcode import (GetBarcodeTemplateByIdResponse,
GetBarcodeTemplateByIdRequest,
BarcodeTemplateCreateResponse,
BarcodeTemplateCreateRequest, GetAllBarcodeTemplateAttributesResponse,
CreateBarcodeTemplateAttributeResponse, CreateBarcodeTemplateAttributeRequest,
BarcodeTemplateUpdateResponse, BarcodeTemplateUpdateRequest,
GetAllBarcodeTemplatesResponse, BarcodeTemplateDeleteRequest,
BarcodeTemplateDeleteResponse, GetAllBarcodeTemplateSizesResponse)
from schemas.barcode import *
from services.auth import authorized_user, guest_user
from services.barcode import BarcodeService
barcode_router = APIRouter(
prefix='/barcode',
tags=['barcode'],
dependencies=[Depends(guest_user)]
)

51
routers/billing.py Normal file
View File

@@ -0,0 +1,51 @@
from fastapi import APIRouter
from backend.dependecies import SessionDependency, CurrentUserDependency
from external.billing import BillStatusUpdateRequest
from schemas.billing import *
from services.billing import BillingService
billing_router = APIRouter(
prefix='/billing',
tags=['billing']
)
@billing_router.post(
'/webhook'
)
async def webhook(
request: BillStatusUpdateRequest,
session: SessionDependency
):
try:
await BillingService(session).process_update(request)
return {'ok': True}
except Exception as e:
print(e)
return {'ok': False}
@billing_router.post(
'/create-deal-bill',
operation_id='create_deal_bill',
response_model=CreateDealBillResponse
)
async def create_deal_bill(
session: SessionDependency,
request: CreateDealBillRequest,
user: CurrentUserDependency
):
return await BillingService(session).create_deal_billing(user, request)
@billing_router.get(
'/deal-bill-request/{deal_id}',
response_model=GetDealBillById,
operation_id='get_deal_bill_by_id'
)
async def get_deal_bill_by_id(
deal_id: int,
session: SessionDependency
):
return await BillingService(session).get_deal_bill_by_id(deal_id)

View File

@@ -6,13 +6,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
from backend.session import get_session
from models import User
from schemas.client import *
from services.auth import get_current_user
from services.auth import get_current_user, authorized_user
from services.client import ClientService
client_router = APIRouter(
prefix="/client",
tags=['client'],
dependencies=[Depends(get_current_user)]
dependencies=[Depends(authorized_user)]
)

View File

@@ -3,22 +3,24 @@ from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from backend.dependecies import SessionDependency
from backend.dependecies import SessionDependency, CurrentUserDependency
from backend.session import get_session
from models import User
from schemas.deal import *
from services.auth import get_current_user
from services.auth import get_current_user, authorized_user, guest_user
from services.deal import DealService
deal_router = APIRouter(
prefix='/deal',
tags=['deal'],
dependencies=[Depends(get_current_user)]
)
# region Deal
@deal_router.post('/create')
@deal_router.post(
'/create',
dependencies=[Depends(authorized_user)]
)
async def create(
request: DealCreateRequest,
session: Annotated[AsyncSession, Depends(get_session)],
@@ -30,7 +32,8 @@ async def create(
@deal_router.post(
'/delete',
response_model=DealDeleteResponse,
operation_id='deleteDeal'
operation_id='deleteDeal',
dependencies=[Depends(authorized_user)]
)
async def delete(
request: DealDeleteRequest,
@@ -39,7 +42,11 @@ async def delete(
return await DealService(session).delete(request)
@deal_router.post('/quickCreate', response_model=DealQuickCreateResponse)
@deal_router.post(
'/quickCreate',
response_model=DealQuickCreateResponse,
dependencies=[Depends(authorized_user)]
)
async def quick_create(
request: DealQuickCreateRequest,
session: Annotated[AsyncSession, Depends(get_session)],
@@ -48,7 +55,11 @@ async def quick_create(
return await DealService(session).quick_create(request, user)
@deal_router.post('/changeStatus', response_model=DealChangeStatusResponse)
@deal_router.post(
'/changeStatus',
response_model=DealChangeStatusResponse,
dependencies=[Depends(authorized_user)]
)
async def change_status(
request: DealChangeStatusRequest,
session: Annotated[AsyncSession, Depends(get_session)],
@@ -57,9 +68,11 @@ async def change_status(
return await DealService(session).change_status_manual(request, user)
@deal_router.get('/summaries',
@deal_router.get(
'/summaries',
response_model=DealSummaryResponse,
operation_id='getDealSummaries'
operation_id='getDealSummaries',
dependencies=[Depends(authorized_user)]
)
async def get_summary(
session: Annotated[AsyncSession, Depends(get_session)]
@@ -70,7 +83,8 @@ async def get_summary(
@deal_router.post(
'/summaries/reorder',
response_model=DealSummaryResponse,
operation_id='reorderDealSummaries'
operation_id='reorderDealSummaries',
dependencies=[Depends(authorized_user)]
)
async def reorder(
session: Annotated[AsyncSession, Depends(get_session)],
@@ -83,7 +97,8 @@ async def reorder(
@deal_router.get(
'/get-all',
response_model=DealGetAllResponse,
operation_id='getAllDeals'
operation_id='getAllDeals',
dependencies=[Depends(authorized_user)]
)
async def get_all(
session: Annotated[AsyncSession, Depends(get_session)]
@@ -95,19 +110,22 @@ async def get_all(
@deal_router.get(
'/get/{deal_id}',
response_model=DealSchema,
operation_id='getDealById'
operation_id='getDealById',
dependencies=[Depends(guest_user)]
)
async def get_deal_by_id(
deal_id: int,
user: CurrentUserDependency,
session: Annotated[AsyncSession, Depends(get_session)]
):
return await DealService(session).get_by_id(deal_id)
return await DealService(session).get_by_id(user, deal_id)
@deal_router.post(
'/update-general-info',
response_model=DealUpdateGeneralInfoResponse,
operation_id='updateDealGeneralInfo'
operation_id='updateDealGeneralInfo',
dependencies=[Depends(authorized_user)]
)
async def update_general_info(
request: DealUpdateGeneralInfoRequest,
@@ -115,16 +133,34 @@ async def update_general_info(
):
return await DealService(session).update_general_info(request)
@deal_router.post(
'/add-kit',
response_model=DealAddKitResponse,
operation_id='add_kit_to_deal'
)
async def add_kit_to_deal(
session: SessionDependency,
request: DealAddKitRequest
):
return await DealService(session).add_kit_to_deal(request)
@deal_router.post(
'/create-guest-url',
response_model=DealCreateGuestUrlResponse,
operation_id='create_deal_guest_url',
dependencies=[Depends(authorized_user)]
)
async def create_guest_url(
session: SessionDependency,
request: DealCreateGuestUrlRequest,
user: CurrentUserDependency
):
return DealService(session).create_guest_url(user, request)
# endregion
# region Deal services
@@ -132,11 +168,12 @@ async def add_kit_to_deal(
@deal_router.post(
'/services/add/multiple',
response_model=DealAddServicesResponse,
operation_id='addMultipleDealServices'
operation_id='addMultipleDealServices',
dependencies=[Depends(guest_user)]
)
async def services_add(
request: DealAddServicesRequest,
session: Annotated[AsyncSession, Depends(get_session)]
session: Annotated[AsyncSession, Depends(get_session)],
):
return await DealService(session).add_services(request)
@@ -144,73 +181,86 @@ async def services_add(
@deal_router.post(
'/services/add',
response_model=DealAddServiceResponse,
operation_id='addDealService'
operation_id='addDealService',
dependencies=[Depends(guest_user)]
)
async def services_add(
request: DealAddServiceRequest,
session: Annotated[AsyncSession, Depends(get_session)]
session: Annotated[AsyncSession, Depends(get_session)],
user: CurrentUserDependency
):
return await DealService(session).add_service(request)
return await DealService(session).add_service(user, request)
@deal_router.post(
'/services/update-quantity',
response_model=DealUpdateServiceQuantityResponse,
operation_id='updateDealServiceQuantity'
operation_id='updateDealServiceQuantity',
dependencies=[Depends(guest_user)]
)
async def services_update_quantity(
request: DealUpdateServiceQuantityRequest,
session: Annotated[AsyncSession, Depends(get_session)]
session: Annotated[AsyncSession, Depends(get_session)],
user: CurrentUserDependency
):
return await DealService(session).update_service_quantity(request)
return await DealService(session).update_service_quantity(user, request)
@deal_router.post(
'/services/update',
response_model=DealUpdateServiceResponse,
operation_id='updateDealService'
operation_id='updateDealService',
dependencies=[Depends(guest_user)]
)
async def services_update(
request: DealUpdateServiceRequest,
session: Annotated[AsyncSession, Depends(get_session)]
session: Annotated[AsyncSession, Depends(get_session)],
user: CurrentUserDependency
):
return await DealService(session).update_service(request)
return await DealService(session).update_service(user, request)
@deal_router.post(
'/services/delete',
response_model=DealDeleteServiceResponse,
operation_id='deleteDealService'
operation_id='deleteDealService',
dependencies=[Depends(guest_user)]
)
async def services_delete(
request: DealDeleteServiceRequest,
session: Annotated[AsyncSession, Depends(get_session)]
session: Annotated[AsyncSession, Depends(get_session)],
user: CurrentUserDependency
):
return await DealService(session).delete_service(request)
return await DealService(session).delete_service(user, request)
@deal_router.post(
'/services/delete/multiple',
response_model=DealDeleteServicesResponse,
operation_id='deleteMultipleDealServices'
operation_id='deleteMultipleDealServices',
dependencies=[Depends(guest_user)]
)
async def services_delete(
request: DealDeleteServicesRequest,
session: Annotated[AsyncSession, Depends(get_session)]
session: Annotated[AsyncSession, Depends(get_session)],
user: CurrentUserDependency
):
return await DealService(session).delete_services(request)
return await DealService(session).delete_services(user, request)
@deal_router.post(
'/services/copy',
response_model=DealServicesCopyResponse,
operation_id='copy_product_services'
operation_id='copy_product_services',
dependencies=[Depends(guest_user)]
)
async def services_copy(
session: SessionDependency,
request: DealServicesCopyRequest
request: DealServicesCopyRequest,
user: CurrentUserDependency
):
return await DealService(session).copy_services(request)
return await DealService(session).copy_services(user, request)
# endregion
@@ -219,63 +269,84 @@ async def services_copy(
@deal_router.post(
'/products/update-quantity',
response_model=DealUpdateProductQuantityResponse,
operation_id='updateDealProductQuantity')
operation_id='updateDealProductQuantity',
dependencies=[Depends(guest_user)]
)
async def products_update(
request: DealUpdateProductQuantityRequest,
session: Annotated[AsyncSession, Depends(get_session)]):
return await DealService(session).update_product_quantity(request)
session: Annotated[AsyncSession, Depends(get_session)],
user: CurrentUserDependency
):
return await DealService(session).update_product_quantity(user, request)
@deal_router.post(
'/products/add',
response_model=DealAddProductResponse,
operation_id='addDealProduct')
operation_id='addDealProduct',
dependencies=[Depends(guest_user)]
)
async def products_add(
request: DealAddProductRequest,
session: Annotated[AsyncSession, Depends(get_session)]):
return await DealService(session).add_product(request)
session: Annotated[AsyncSession, Depends(get_session)],
user: CurrentUserDependency
):
return await DealService(session).add_product(user, request)
@deal_router.post(
'/products/delete',
response_model=DealDeleteProductResponse,
operation_id='deleteDealProduct')
operation_id='deleteDealProduct',
dependencies=[Depends(guest_user)]
)
async def products_delete(
request: DealDeleteProductRequest,
session: Annotated[AsyncSession, Depends(get_session)]):
return await DealService(session).delete_product(request)
session: Annotated[AsyncSession, Depends(get_session)],
user: CurrentUserDependency
):
return await DealService(session).delete_product(user, request)
@deal_router.post(
'/products/delete/multiple',
response_model=DealDeleteProductsResponse,
operation_id='deleteMultipleDealProducts')
operation_id='deleteMultipleDealProducts',
dependencies=[Depends(guest_user)]
)
async def products_delete(
request: DealDeleteProductsRequest,
session: Annotated[AsyncSession, Depends(get_session)]):
return await DealService(session).delete_products(request)
session: Annotated[AsyncSession, Depends(get_session)],
user: CurrentUserDependency
):
return await DealService(session).delete_products(user, request)
@deal_router.post(
'/product/update',
response_model=DealUpdateProductResponse,
operation_id='updateDealProduct'
operation_id='updateDealProduct',
dependencies=[Depends(guest_user)]
)
async def products_update(
request: DealUpdateProductRequest,
session: Annotated[AsyncSession, Depends(get_session)]
session: Annotated[AsyncSession, Depends(get_session)],
user: CurrentUserDependency
):
return await DealService(session).update_product(request)
return await DealService(session).update_product(user, request)
@deal_router.post(
'/product/add-kit',
response_model=DealProductAddKitResponse,
operation_id='add_kit_to_deal_product'
operation_id='add_kit_to_deal_product',
dependencies=[Depends(guest_user)]
)
async def add_kit_to_deal_product(
session: SessionDependency,
request: DealProductAddKitRequest
request: DealProductAddKitRequest,
user: CurrentUserDependency
):
return await DealService(session).add_kit_to_deal_product(request)
return await DealService(session).add_kit_to_deal_product(user, request)
# endregion

View File

@@ -1,12 +1,14 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from backend.dependecies import SessionDependency
from schemas.marketplace import *
from services.auth import authorized_user
from services.marketplace import MarketplaceService
marketplace_router = APIRouter(
prefix="/marketplace",
tags=["marketplace"]
tags=["marketplace"],
dependencies=[Depends(authorized_user)]
)

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from backend.dependecies import SessionDependency, PaginationDependency, CurrentUserDependency
from schemas.finances import CreatePayRateRequest, UpdatePayRateRequest, DeletePayRateRequest, \
@@ -6,11 +6,13 @@ from schemas.finances import CreatePayRateRequest, UpdatePayRateRequest, DeleteP
DeletePayRateResponse
from schemas.payment_record import GetPaymentRecordsResponse, CreatePaymentRecordResponse, CreatePaymentRecordRequest, \
DeletePaymentRecordResponse, DeletePaymentRecordRequest
from services.auth import authorized_user
from services.payroll import PayrollService
payroll_router = APIRouter(
prefix="/payroll",
tags=["payroll"]
tags=["payroll"],
dependencies=[Depends(authorized_user)]
)

View File

@@ -1,12 +1,14 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from backend.dependecies import SessionDependency
from schemas.position import *
from services.auth import authorized_user
from services.position import PositionService
position_router = APIRouter(
prefix="/position",
tags=["position"]
tags=["position"],
dependencies=[Depends(authorized_user)]
)

View File

@@ -6,18 +6,20 @@ from fastapi import APIRouter, Depends, UploadFile
from sqlalchemy.ext.asyncio import AsyncSession
import utils.dependecies
from backend.dependecies import CurrentUserDependency
from backend.session import get_session
from schemas.barcode import GetProductBarcodeResponse, GetProductBarcodeRequest, GetProductBarcodePdfResponse, \
GetProductBarcodePdfRequest
from schemas.base import PaginationSchema
from schemas.product import *
from services.auth import guest_user
from services.barcode import BarcodeService
from services.product import ProductService
product_router = APIRouter(
prefix="/product",
tags=["product"],
# dependencies=[Depends(get_current_user)]
dependencies=[Depends(guest_user)]
)
@@ -28,7 +30,7 @@ product_router = APIRouter(
)
async def create_product(
request: ProductCreateRequest,
session: Annotated[AsyncSession, Depends(get_session)]
session: Annotated[AsyncSession, Depends(get_session)],
):
return await ProductService(session).create(request)
@@ -146,9 +148,7 @@ async def get_product_barcode_pdf(
filename=filename,
mime_type='application/pdf'
)
# return StreamingResponse(content=pdf_buffer,
# media_type='application/pdf',
# headers={"Content-Disposition": f"inline; filename={filename}"})
@product_router.post(

View File

@@ -1,12 +1,14 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from backend.dependecies import SessionDependency
from schemas.role import *
from services.auth import authorized_user
from services.role import RoleService
role_router = APIRouter(
prefix='/role',
tags=['role']
tags=['role'],
dependencies=[Depends(authorized_user)]
)

View File

@@ -6,22 +6,23 @@ from sqlalchemy.ext.asyncio import AsyncSession
import enums.service
from backend.dependecies import SessionDependency
from backend.session import get_session
from schemas.base import BaseEnumSchema, BaseEnumListSchema
from schemas.base import BaseEnumListSchema
from schemas.service import *
from services.auth import get_current_user
from services.auth import guest_user, authorized_user
from services.service import ServiceService
service_router = APIRouter(
prefix="/service",
tags=['service'],
dependencies=[Depends(get_current_user)]
)
@service_router.get(
'/get-all',
response_model=ServiceGetAllResponse,
operation_id="get_all_services"
operation_id="get_all_services",
dependencies=[Depends(guest_user)]
)
async def get_all(
session: Annotated[AsyncSession, Depends(get_session)]
@@ -32,7 +33,8 @@ async def get_all(
@service_router.post(
'/create',
response_model=ServiceCreateResponse,
operation_id="create_service"
operation_id="create_service",
dependencies=[Depends(authorized_user)]
)
async def create(
session: Annotated[AsyncSession, Depends(get_session)],
@@ -44,7 +46,8 @@ async def create(
@service_router.post(
'/update',
response_model=ServiceUpdateResponse,
operation_id="update_service"
operation_id="update_service",
dependencies=[Depends(authorized_user)]
)
async def update(
session: Annotated[AsyncSession, Depends(get_session)],
@@ -56,7 +59,8 @@ async def update(
@service_router.post(
'/delete',
response_model=ServiceDeleteResponse,
operation_id="delete_service"
operation_id="delete_service",
dependencies=[Depends(authorized_user)]
)
async def delete(
session: Annotated[AsyncSession, Depends(get_session)],
@@ -68,7 +72,8 @@ async def delete(
@service_router.get(
'/categories/get-all',
response_model=ServiceGetAllCategoriesResponse,
operation_id="get_all_service_categories"
operation_id="get_all_service_categories",
dependencies=[Depends(guest_user)]
)
async def get_all_categories(
session: Annotated[AsyncSession, Depends(get_session)]
@@ -79,7 +84,8 @@ async def get_all_categories(
@service_router.post(
'/categories/create',
response_model=ServiceCreateCategoryResponse,
operation_id="create_service_category"
operation_id="create_service_category",
dependencies=[Depends(authorized_user)]
)
async def create_category(
session: Annotated[AsyncSession, Depends(get_session)],
@@ -91,7 +97,8 @@ async def create_category(
@service_router.get(
'/types/get-all',
response_model=BaseEnumListSchema,
operation_id="get_all_service_types"
operation_id="get_all_service_types",
dependencies=[Depends(guest_user)]
)
async def get_all_service_types(
):
@@ -104,7 +111,8 @@ async def get_all_service_types(
@service_router.get(
'/kits/get-all',
response_model=GetAllServicesKitsResponse,
operation_id='get_all_services_kits'
operation_id='get_all_services_kits',
dependencies=[Depends(guest_user)]
)
async def get_all_services_kits(
session: SessionDependency
@@ -115,7 +123,8 @@ async def get_all_services_kits(
@service_router.post(
'/kits/create',
response_model=CreateServicesKitResponse,
operation_id='create_services_kit'
operation_id='create_services_kit',
dependencies=[Depends(authorized_user)]
)
async def create_services_kit(
session: SessionDependency,
@@ -127,7 +136,8 @@ async def create_services_kit(
@service_router.post(
'/kits/update',
response_model=UpdateServicesKitResponse,
operation_id='update_services_kit'
operation_id='update_services_kit',
dependencies=[Depends(authorized_user)]
)
async def update_services_kit(
session: SessionDependency,

View File

@@ -5,13 +5,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
from backend.session import get_session
from schemas.shipping_warehouse import GetAllShippingWarehousesResponse
from services.auth import get_current_user
from services.auth import authorized_user
from services.shipping_warehouse import ShippingWarehouseService
shipping_warehouse_router = APIRouter(
prefix="/shipping-warehouse",
tags=["shipping-warehouse"],
dependencies=[Depends(get_current_user)]
dependencies=[Depends(authorized_user)]
)

View File

@@ -1,12 +1,14 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from backend.dependecies import SessionDependency, CurrentUserDependency
from schemas.time_tracking import *
from services.auth import authorized_user
from services.time_tracking import TimeTrackingService
time_tracking_router = APIRouter(
prefix="/time-tracking",
tags=["time-tracking"]
tags=["time-tracking"],
dependencies=[Depends(authorized_user)]
)

View File

@@ -1,12 +1,14 @@
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from backend.dependecies import SessionDependency
from schemas.user import *
from services.auth import authorized_user
from services.user import UserService
user_router = APIRouter(
prefix="/user",
tags=["user"]
tags=["user"],
dependencies=[Depends(authorized_user)]
)

24
schemas/billing.py Normal file
View File

@@ -0,0 +1,24 @@
import datetime
from typing import Optional
from schemas.base import BaseSchema, OkMessageSchema
class DealBillRequestSchema(BaseSchema):
deal_id: int
created_at: datetime.datetime
paid: bool
pdf_url: Optional[str]
invoice_number: Optional[str]
class CreateDealBillRequest(BaseSchema):
deal_id: int
class CreateDealBillResponse(OkMessageSchema):
pass
class GetDealBillById(BaseSchema):
deal_bill: DealBillRequestSchema

View File

@@ -4,6 +4,7 @@ from typing import List, Optional, Union
from pydantic import constr, field_validator
from schemas.base import BaseSchema, OkMessageSchema
from schemas.billing import DealBillRequestSchema
from schemas.client import ClientSchema
from schemas.marketplace import BaseMarketplaceSchema
from schemas.product import ProductSchema
@@ -71,9 +72,11 @@ class DealSchema(BaseSchema):
status_history: List[DealStatusHistorySchema]
is_deleted: bool
is_completed: bool
is_locked: bool
client: ClientSchema
comment: str
shipping_warehouse: Optional[Union[ShippingWarehouseSchema, str]] = None
bill_request: Optional[DealBillRequestSchema] = None
class DealGeneralInfoSchema(BaseSchema):
@@ -202,6 +205,10 @@ class DealAddKitRequest(BaseSchema):
kit_id: int
class DealCreateGuestUrlRequest(BaseSchema):
deal_id: int
# endregion Requests
# region Responses
@@ -293,4 +300,8 @@ class DealProductAddKitResponse(OkMessageSchema):
class DealAddKitResponse(OkMessageSchema):
pass
class DealCreateGuestUrlResponse(OkMessageSchema):
url: str
# endregion Responses

View File

@@ -19,8 +19,10 @@ oauth2_schema = HTTPBearer()
algorithm = 'HS256'
async def get_current_user(session: Annotated[AsyncSession, Depends(get_session)],
token: Annotated[HTTPAuthorizationCredentials, Depends(oauth2_schema)]) -> User | None:
async def get_current_user(
session: Annotated[AsyncSession, Depends(get_session)],
token: Annotated[HTTPAuthorizationCredentials, Depends(oauth2_schema)]
) -> Union[User, None, dict]:
if not token.credentials:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token')
try:
@@ -28,6 +30,8 @@ async def get_current_user(session: Annotated[AsyncSession, Depends(get_session)
user_id = payload.get('sub')
if not user_id:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid credentials')
if user_id == 'guest':
return payload
user_id = int(user_id)
user = await session.get(User, user_id)
@@ -35,16 +39,26 @@ async def get_current_user(session: Annotated[AsyncSession, Depends(get_session)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid credentials')
return user
except JWTError as e:
print(e)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token')
async def authorized_user(
user: Annotated[User, Depends(get_current_user)]
):
if type(user) is User:
return user
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token')
async def guest_user(user: Annotated[User, Depends(get_current_user)]):
if (type(user) is User) or (type(user) is dict):
return user
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token')
class AuthService(BaseService):
@staticmethod
def _generate_jwt_token(user: User) -> str:
payload = {
'sub': str(user.id)
}
def _generate_jwt_token(payload: dict) -> str:
return jwt.encode(payload, backend.config.SECRET_KEY, algorithm=algorithm)
async def authenticate(self, request: AuthLoginRequest):
@@ -60,5 +74,16 @@ class AuthService(BaseService):
)
self.session.add(user)
await self.session.commit()
access_token = self._generate_jwt_token(user)
payload = {
'sub': str(user.id)
}
access_token = self._generate_jwt_token(payload)
return AuthLoginResponse(access_token=access_token)
def create_deal_guest_token(self, deal_id: int):
payload = {
'sub': 'guest',
'deal_id': deal_id
}
return self._generate_jwt_token(payload)

118
services/billing.py Normal file
View File

@@ -0,0 +1,118 @@
import datetime
from typing import List
from fastapi import HTTPException
from sqlalchemy import select
from starlette import status
import backend.config
from external.billing import BillingClient, CreateBillingRequestValue, CreateBillRequestSchema, CreateBillRequestItems, \
BillStatusUpdateRequest, NotificationChannel, NotifyReceivedBillRequestSchema, BillPaymentInfo
from models import DealBillRequest, Deal
from schemas.billing import *
from services.base import BaseService
from services.deal import DealService
class BillingService(BaseService):
async def _process_update_details(
self,
request: BillStatusUpdateRequest
):
billing_client = BillingClient(backend.config.BILLING_API_KEY)
notify_received_request = NotifyReceivedBillRequestSchema(
listener_transaction_id=request.listener_transaction_id,
channel=NotificationChannel.PAYMENT_DETAILS,
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
await self.session.commit()
async def _process_update_verification(
self,
request: BillStatusUpdateRequest
):
billing_client = BillingClient(backend.config.BILLING_API_KEY)
notify_received_request = NotifyReceivedBillRequestSchema(
listener_transaction_id=request.listener_transaction_id,
channel=NotificationChannel.PAYMENT_VERIFICATION,
received=True
)
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
await self.session.commit()
async def process_update(
self,
request: BillStatusUpdateRequest
):
if request.channel == NotificationChannel.PAYMENT_DETAILS:
await self._process_update_details(request)
elif request.channel == NotificationChannel.PAYMENT_VERIFICATION:
await self._process_update_verification(request)
async def create_deal_billing(self, user, request: CreateDealBillRequest) -> CreateDealBillResponse:
try:
deal_service = DealService(self.session)
billing_client = BillingClient(backend.config.BILLING_API_KEY)
deal: Deal = await deal_service.get_by_id(user, request.deal_id)
billing_request_values: List[CreateBillingRequestValue] = []
for product in deal.products:
for service in product.services:
billing_request_values.append(
CreateBillingRequestValue(
name=f'[{product.product.name}] - {service.service.name}',
price=service.price,
amount=product.quantity
)
)
for service in deal.services:
billing_request_values.append(
CreateBillingRequestValue(
name=f'{service.service.name}',
price=service.price,
amount=service.quantity
)
)
create_bill_request = CreateBillRequestSchema(
listener_transaction_id=deal.id,
payer_name=deal.client.name,
payer_inn=deal.client.details.inn,
payer_phone=deal.client.details.phone_number,
items=CreateBillRequestItems(
values=billing_request_values
)
)
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()
return CreateDealBillResponse(ok=create_bill_response.ok, message='Заявка успешно создана!')
except Exception as e:
return CreateDealBillResponse(ok=False, message=str(e))
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_deal_bill_by_id(self, deal_id: int) -> GetDealBillById:
deal_bill = await self._get_deal_bill_by_id(deal_id)
if not deal_bill:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Deal bill was not found')
return GetDealBillById(deal_bill=DealBillRequestSchema.model_validate(deal_bill))

View File

@@ -1,16 +1,16 @@
import lexorank
import models.secondary
from typing import Union
import models.deal
from fastapi import HTTPException
from sqlalchemy import select, func, update, delete, insert
from sqlalchemy.orm import joinedload, selectinload
from starlette import status
import models.deal
import models.secondary
from models import User, Service, Client
from models.deal import *
from schemas.client import ClientDetailsSchema
from schemas.deal import *
from services.auth import AuthService
from services.base import BaseService
from services.client import ClientService
from services.service import ServiceService
@@ -20,6 +20,15 @@ from services.shipping_warehouse import ShippingWarehouseService
class DealService(BaseService):
# region Deal
@staticmethod
def grant_access(user: Union[models.User, dict], deal_id):
if type(user) is models.User:
return
user_deal_id = user['deal_id']
if int(user_deal_id) != int(deal_id):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token')
async def _get_deal_by_id(self, deal_id) -> Union[Deal, None]:
return await self.session.get(Deal, deal_id)
@@ -211,7 +220,9 @@ class DealService(BaseService):
result.append(DealSchema.model_validate(deal))
return DealGetAllResponse(deals=result)
async def get_by_id(self, deal_id: int) -> DealSchema:
async def get_by_id(self, user: Union[models.User, dict], deal_id: int, return_raw=False) -> Union[
DealSchema, models.Deal]:
self.grant_access(user, deal_id)
deal = await self.session.scalar(
select(Deal)
@@ -244,7 +255,8 @@ class DealService(BaseService):
)
.where(Deal.id == deal_id)
)
if return_raw:
return deal
if not deal:
raise HTTPException(status_code=404, detail="Сделка не найдена")
return DealSchema.model_validate(deal)
@@ -360,11 +372,17 @@ class DealService(BaseService):
except Exception as e:
return DealAddKitResponse(ok=False, message=str(e))
def create_guest_url(self, user: models.User, request: DealCreateGuestUrlRequest) -> DealCreateGuestUrlResponse:
if not user.is_admin:
return DealCreateGuestUrlResponse(ok=False, message='Создать ссылку может только администратор', url="")
access_token = AuthService(self.session).create_deal_guest_token(request.deal_id)
url = f"deals/{request.deal_id}?accessToken={access_token}"
return DealCreateGuestUrlResponse(ok=True, message='Ссылка успешно создана!', url=url)
# endregion
# region Deal services
async def add_services(self, request: DealAddServicesRequest):
# TODO refactor
deal: Deal = await self.session.scalar(
select(Deal)
.options(selectinload(Deal.services))
@@ -404,9 +422,13 @@ class DealService(BaseService):
await self.session.commit()
return DealAddServicesResponse(ok=True, message='Услуги успешно добавлены')
async def update_service_quantity(self,
request: DealUpdateServiceQuantityRequest) -> DealUpdateServiceQuantityResponse:
async def update_service_quantity(
self,
user: Union[models.User, dict],
request: DealUpdateServiceQuantityRequest
) -> DealUpdateServiceQuantityResponse:
try:
self.grant_access(user, request.deal_id)
deal_service = await self.session.scalar(
select(models.secondary.DealService)
.where(models.secondary.DealService.deal_id == request.deal_id,
@@ -421,8 +443,12 @@ class DealService(BaseService):
await self.session.rollback()
return DealUpdateServiceQuantityResponse(ok=False, message=str(e))
async def add_service(self, request: DealAddServiceRequest) -> DealAddServiceResponse:
async def add_service(self,
user: Union[models.User, dict],
request: DealAddServiceRequest
) -> DealAddServiceResponse:
try:
self.grant_access(user, request.deal_id)
deal = await self.session.scalar(select(Deal).where(Deal.id == request.deal_id))
if not deal:
raise HTTPException(status_code=404, detail="Сделка не найдена")
@@ -450,8 +476,13 @@ class DealService(BaseService):
await self.session.rollback()
return DealAddServiceResponse(ok=False, message=str(e))
async def delete_service(self, request: DealDeleteServiceRequest) -> DealDeleteServiceResponse:
async def delete_service(
self,
user: Union[models.User, dict],
request: DealDeleteServiceRequest
) -> DealDeleteServiceResponse:
try:
self.grant_access(user, request.deal_id)
deal_service = await self.session.scalar(
select(models.secondary.DealService)
.where(models.secondary.DealService.deal_id == request.deal_id,
@@ -466,8 +497,13 @@ class DealService(BaseService):
await self.session.rollback()
return DealDeleteServiceResponse(ok=False, message=str(e))
async def delete_services(self, request: DealDeleteServicesRequest) -> DealDeleteServicesResponse:
async def delete_services(
self,
user: Union[models.User, dict],
request: DealDeleteServicesRequest
) -> DealDeleteServicesResponse:
try:
self.grant_access(user, request)
deal_services = await self.session.scalars(
select(models.secondary.DealService)
.where(models.secondary.DealService.deal_id == request.deal_id,
@@ -481,8 +517,13 @@ class DealService(BaseService):
await self.session.rollback()
return DealDeleteServicesResponse(ok=False, message=str(e))
async def update_service(self, request: DealUpdateServiceRequest) -> DealUpdateServiceResponse:
async def update_service(
self,
user: Union[models.User, dict],
request: DealUpdateServiceRequest
) -> DealUpdateServiceResponse:
try:
self.grant_access(user, request.deal_id)
deal_service = await self.session.scalar(
select(models.secondary.DealService)
.where(models.secondary.DealService.deal_id == request.deal_id,
@@ -535,9 +576,13 @@ class DealService(BaseService):
await self.session.rollback()
return DealUpdateServiceQuantityResponse(ok=False, message=str(e))
async def copy_services(self, request: DealServicesCopyRequest) -> DealServicesCopyResponse:
async def copy_services(
self,
user: Union[models.User, dict],
request: DealServicesCopyRequest
) -> DealServicesCopyResponse:
try:
self.grant_access(user, request.deal_id)
source_services_stmt = (
select(
models.DealProductService
@@ -614,9 +659,13 @@ class DealService(BaseService):
# endregion
# region Deal products
async def update_product_quantity(self,
request: DealUpdateProductQuantityRequest) -> DealUpdateProductQuantityResponse:
async def update_product_quantity(
self,
user: Union[models.User, dict],
request: DealUpdateProductQuantityRequest
) -> DealUpdateProductQuantityResponse:
try:
self.grant_access(user, request.deal_id)
# check if there is no deal or no product with different exceptions
deal_product = await self.session.scalar(
select(models.secondary.DealProduct)
@@ -632,8 +681,15 @@ class DealService(BaseService):
await self.session.rollback()
return DealUpdateProductQuantityResponse(ok=False, message=str(e))
async def add_product(self, request: DealAddProductRequest) -> DealAddProductResponse:
async def add_product(
self,
user: Union[models.User, dict],
request: DealAddProductRequest
) -> DealAddProductResponse:
try:
self.grant_access(user, request.deal_id)
deal = await self.session.scalar(select(Deal).where(Deal.id == request.deal_id))
if not deal:
raise HTTPException(status_code=404, detail="Сделка не найдена")
@@ -670,8 +726,13 @@ class DealService(BaseService):
await self.session.rollback()
return DealAddProductResponse(ok=False, message=str(e))
async def delete_product(self, request: DealDeleteProductRequest) -> DealDeleteProductResponse:
async def delete_product(
self,
user: Union[models.User, dict],
request: DealDeleteProductRequest
) -> DealDeleteProductResponse:
try:
self.grant_access(user, request.deal_id)
deal_product = await self.session.scalar(
select(models.secondary.DealProduct)
.where(models.secondary.DealProduct.deal_id == request.deal_id,
@@ -686,8 +747,13 @@ class DealService(BaseService):
await self.session.rollback()
return DealDeleteProductResponse(ok=False, message=str(e))
async def delete_products(self, request: DealDeleteProductsRequest) -> DealDeleteProductsResponse:
async def delete_products(
self,
user: Union[models.User, dict],
request: DealDeleteProductsRequest
) -> DealDeleteProductsResponse:
try:
self.grant_access(user, request.deal_id)
deal_products = await self.session.scalars(
select(models.secondary.DealProduct)
.where(models.secondary.DealProduct.deal_id == request.deal_id,
@@ -701,8 +767,13 @@ class DealService(BaseService):
await self.session.rollback()
return DealDeleteProductsResponse(ok=False, message=str(e))
async def update_product(self, request: DealUpdateProductRequest):
async def update_product(
self,
user: Union[models.User, dict],
request: DealUpdateProductRequest
):
try:
self.grant_access(user, request.deal_id)
deal_product: models.DealProduct = await self.session.scalar(
select(models.secondary.DealProduct)
.where(models.secondary.DealProduct.deal_id == request.deal_id,
@@ -781,8 +852,13 @@ class DealService(BaseService):
await self.session.rollback()
return DealUpdateProductResponse(ok=False, message=str(e))
async def add_kit_to_deal_product(self, request: DealProductAddKitRequest) -> DealProductAddKitResponse:
async def add_kit_to_deal_product(
self,
user: Union[models.User, dict],
request: DealProductAddKitRequest
) -> DealProductAddKitResponse:
try:
self.grant_access(user, request.deal_id)
service_service = ServiceService(self.session)
kit = await service_service.get_kit_by_id(request.kit_id)
if not kit:

View File

@@ -1,3 +1,5 @@
from typing import Union
from fastapi import HTTPException
from sqlalchemy import select, func, Integer, update
from sqlalchemy.orm import selectinload
@@ -5,25 +7,17 @@ from sqlalchemy.orm import selectinload
import utils.barcodes
from backend import config
from external.s3_uploader.uploader import S3Uploader
from models.product import Product, ProductBarcode, ProductImage
from models import User
from models.product import Product, ProductImage
from schemas.base import PaginationSchema
from services.base import BaseService
from schemas.product import *
from services.base import BaseService
from utils.dependecies import is_valid_pagination
class ProductService(BaseService):
async def create(self, request: ProductCreateRequest) -> ProductCreateResponse:
# Unique article validation
# existing_product_query = await self.session.execute(
# select(Product)
# .where(Product.client_id == request.client_id,
# Product.article == request.article)
# )
# existing_product = existing_product_query.first()
# if existing_product:
# return ProductCreateResponse(ok=False, message='Товар с таким артикулом уже существует у клиента')
# Creating product
product_dict = request.dict()