feat: deals viewer mode and links for viewers

This commit is contained in:
2025-07-04 15:12:21 +04:00
parent bd7d9a19d3
commit 0caf1f3f42
19 changed files with 246 additions and 83 deletions

View File

@@ -60,3 +60,7 @@ DEAL_STATUS_STR = [
# 30 minutes in seconds # 30 minutes in seconds
INVITE_CODE_EXPIRY = 30 * 60 INVITE_CODE_EXPIRY = 30 * 60
DEAL_EDITOR = 'deal_editor'
DEALS_VIEWER = 'deals_viewer'

View File

@@ -45,8 +45,9 @@ async def create_deal_bill(
async def cancel_deal_billing( async def cancel_deal_billing(
session: SessionDependency, session: SessionDependency,
request: CancelCardBillRequest, request: CancelCardBillRequest,
user: CurrentUserDependency,
): ):
return await BillingService(session).cancel_card_billing(request) return await BillingService(session).cancel_card_billing(user, request)
@billing_router.get( @billing_router.get(

View File

@@ -12,7 +12,7 @@ from models import User
from parsers import DealParser from parsers import DealParser
from schemas.barcode import GetCardProductsBarcodesPdfRequest, GetCardProductsBarcodesPdfResponse from schemas.barcode import GetCardProductsBarcodesPdfRequest, GetCardProductsBarcodesPdfResponse
from schemas.card import * from schemas.card import *
from services.auth import get_current_user, authorized_user, guest_user from services.auth import get_current_user, authorized_user, guest_user, user_deals_viewer, user_deal_editor
from services.barcode import BarcodeService from services.barcode import BarcodeService
from services.billing import BillingService from services.billing import BillingService
from services.card import CardsService from services.card import CardsService
@@ -82,14 +82,14 @@ async def change_status(
'/summaries', '/summaries',
response_model=CardSummaryResponse, response_model=CardSummaryResponse,
operation_id='getCardSummaries', operation_id='getCardSummaries',
dependencies=[Depends(authorized_user)]
) )
async def get_summary( async def get_summary(
session: Annotated[AsyncSession, Depends(get_session)], session: Annotated[AsyncSession, Depends(get_session)],
request: GetCardSummariesRequest, request: GetCardSummariesRequest,
pagination: PaginationDependency, pagination: PaginationDependency,
user: CurrentUserDependency,
): ):
return await CardsService(session).get_summary(request, pagination) return await CardsService(session).get_summary(request, pagination, user)
@card_router.post( @card_router.post(
@@ -151,13 +151,13 @@ async def update_general_info(
'/update-products-and-services-general-info', '/update-products-and-services-general-info',
response_model=ProductsAndServicesGeneralInfoResponse, response_model=ProductsAndServicesGeneralInfoResponse,
operation_id='update_products_and_services_general_info', operation_id='update_products_and_services_general_info',
dependencies=[Depends(authorized_user)]
) )
async def update_products_and_services_general_info( async def update_products_and_services_general_info(
request: ProductsAndServicesGeneralInfoRequest, request: ProductsAndServicesGeneralInfoRequest,
session: Annotated[AsyncSession, Depends(get_session)], session: Annotated[AsyncSession, Depends(get_session)],
user: CurrentUserDependency,
): ):
return await CardsService(session).update_products_and_services_general_info(request) return await CardsService(session).update_products_and_services_general_info(request, user)
@card_router.post( @card_router.post(
@@ -185,6 +185,7 @@ async def update_card_client(
): ):
return await CardsService(session).update_card_client(request) return await CardsService(session).update_card_client(request)
@card_router.post( @card_router.post(
'/add-kit', '/add-kit',
response_model=CardAddKitResponse, response_model=CardAddKitResponse,
@@ -206,9 +207,8 @@ async def add_kit_to_card(
async def create_guest_url( async def create_guest_url(
session: SessionDependency, session: SessionDependency,
request: CardCreateGuestUrlRequest, request: CardCreateGuestUrlRequest,
user: CurrentUserDependency
): ):
return CardsService(session).create_guest_url(user, request) return await CardsService(session).create_guest_url(request)
@card_router.get( @card_router.get(
@@ -267,6 +267,7 @@ async def recalculate_card_price(
'/employee', '/employee',
response_model=ManageEmployeeResponse, response_model=ManageEmployeeResponse,
operation_id='manage_employee', operation_id='manage_employee',
dependencies=[Depends(user_deal_editor)],
) )
async def manage_employee( async def manage_employee(
session: SessionDependency, session: SessionDependency,
@@ -279,6 +280,7 @@ async def manage_employee(
'/employee/available/{card_id}', '/employee/available/{card_id}',
response_model=GetAvailableEmployeesToAssignResponse, response_model=GetAvailableEmployeesToAssignResponse,
operation_id='get_available_employees_to_assign', operation_id='get_available_employees_to_assign',
dependencies=[Depends(user_deal_editor)],
) )
async def get_available_employees_to_assign( async def get_available_employees_to_assign(
session: Annotated[AsyncSession, Depends(get_session)], session: Annotated[AsyncSession, Depends(get_session)],
@@ -291,6 +293,7 @@ async def get_available_employees_to_assign(
'/prefill/excel/parse', '/prefill/excel/parse',
response_model=ParseCardsExcelResponse, response_model=ParseCardsExcelResponse,
operation_id='parse_deals_excel', operation_id='parse_deals_excel',
dependencies=[Depends(authorized_user)]
) )
async def parse_deals_excel( async def parse_deals_excel(
session: Annotated[AsyncSession, Depends(get_session)], session: Annotated[AsyncSession, Depends(get_session)],
@@ -304,6 +307,7 @@ async def parse_deals_excel(
'/prefill/excel/create', '/prefill/excel/create',
response_model=CreateCardsFromExcelResponse, response_model=CreateCardsFromExcelResponse,
operation_id='create_deals_excel', operation_id='create_deals_excel',
dependencies=[Depends(authorized_user)],
) )
async def create_deals_from_excel( async def create_deals_from_excel(
session: Annotated[AsyncSession, Depends(get_session)], session: Annotated[AsyncSession, Depends(get_session)],
@@ -312,6 +316,7 @@ async def create_deals_from_excel(
): ):
return await CardsService(session).create_cards_from_excel(request, user) return await CardsService(session).create_cards_from_excel(request, user)
# endregion # endregion
# region Card services # region Card services
@@ -519,5 +524,4 @@ async def get_card_products_barcodes_pdf(
mime_type='application/pdf' mime_type='application/pdf'
) )
# endregion # endregion

View File

@@ -3,6 +3,7 @@ from typing import Annotated
from fastapi import APIRouter, Depends, UploadFile from fastapi import APIRouter, Depends, UploadFile
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from backend.dependecies import CurrentUserDependency
from backend.session import get_session from backend.session import get_session
from models import User from models import User
from schemas.chat import * from schemas.chat import *
@@ -111,8 +112,9 @@ async def get_client_chats_list(
async def create_chat( async def create_chat(
session: Annotated[AsyncSession, Depends(get_session)], session: Annotated[AsyncSession, Depends(get_session)],
request: CreateChatRequest, request: CreateChatRequest,
user: CurrentUserDependency,
): ):
return await ChatService(session).create_chat(request) return await ChatService(session).create_chat(request, user)
@chat_router.post( @chat_router.post(

View File

@@ -98,3 +98,15 @@ async def delete_client(
session: Annotated[AsyncSession, Depends(get_session)] session: Annotated[AsyncSession, Depends(get_session)]
): ):
return await ClientService(session).delete(request) return await ClientService(session).delete(request)
@client_router.post(
'/link/{client_id}',
operation_id='client_link',
response_model=CreateGuestUrlResponse,
)
async def client_link(
session: Annotated[AsyncSession, Depends(get_session)],
client_id: int,
):
return await ClientService(session).create_guest_url(client_id)

View File

@@ -8,17 +8,16 @@ from services.marketplace import MarketplaceService
marketplace_router = APIRouter( marketplace_router = APIRouter(
prefix="/marketplace", prefix="/marketplace",
tags=["marketplace"], tags=["marketplace"],
dependencies=[Depends(authorized_user)]
) )
@marketplace_router.get( @marketplace_router.get(
'/base/get-all', '/base/get-all',
operation_id='get_all_base_marketplaces', operation_id='get_all_base_marketplaces',
response_model=GetAllBaseMarketplacesResponse response_model=GetAllBaseMarketplacesResponse,
) )
async def get_all( async def get_all(
session: SessionDependency session: SessionDependency,
): ):
return await MarketplaceService(session).get_all_base_marketplaces() return await MarketplaceService(session).get_all_base_marketplaces()
@@ -26,7 +25,8 @@ async def get_all(
@marketplace_router.post( @marketplace_router.post(
'/get', '/get',
operation_id='get_client_marketplaces', operation_id='get_client_marketplaces',
response_model=GetClientMarketplacesResponse response_model=GetClientMarketplacesResponse,
dependencies=[Depends(authorized_user)]
) )
async def get( async def get(
session: SessionDependency, session: SessionDependency,
@@ -38,7 +38,8 @@ async def get(
@marketplace_router.post( @marketplace_router.post(
'/create', '/create',
operation_id='create_marketplace', operation_id='create_marketplace',
response_model=CreateMarketplaceResponse response_model=CreateMarketplaceResponse,
dependencies=[Depends(authorized_user)],
) )
async def create( async def create(
session: SessionDependency, session: SessionDependency,
@@ -50,7 +51,8 @@ async def create(
@marketplace_router.post( @marketplace_router.post(
'/delete', '/delete',
operation_id='delete_marketplace', operation_id='delete_marketplace',
response_model=DeleteMarketplaceResponse response_model=DeleteMarketplaceResponse,
dependencies=[Depends(authorized_user)],
) )
async def delete( async def delete(
session: SessionDependency, session: SessionDependency,
@@ -62,7 +64,8 @@ async def delete(
@marketplace_router.post( @marketplace_router.post(
'/update', '/update',
operation_id='update_marketplace', operation_id='update_marketplace',
response_model=UpdateMarketplaceResponse response_model=UpdateMarketplaceResponse,
dependencies=[Depends(authorized_user)],
) )
async def update( async def update(
session: SessionDependency, session: SessionDependency,
@@ -70,10 +73,12 @@ async def update(
): ):
return await MarketplaceService(session).update_marketplace(request) return await MarketplaceService(session).update_marketplace(request)
@marketplace_router.post( @marketplace_router.post(
'/yandex-market/get-campaigns', '/yandex-market/get-campaigns',
operation_id='get_yandex_market_campaigns', operation_id='get_yandex_market_campaigns',
response_model=GetYandexMarketCampaignsResponse response_model=GetYandexMarketCampaignsResponse,
dependencies=[Depends(authorized_user)],
) )
async def get_yandex_market_campaigns( async def get_yandex_market_campaigns(
session: SessionDependency, session: SessionDependency,

View File

@@ -31,8 +31,9 @@ product_router = APIRouter(
async def create_product( async def create_product(
request: ProductCreateRequest, request: ProductCreateRequest,
session: Annotated[AsyncSession, Depends(get_session)], session: Annotated[AsyncSession, Depends(get_session)],
user: CurrentUserDependency,
): ):
return await ProductService(session).create(request) return await ProductService(session).create(request, user)
@product_router.post( @product_router.post(

View File

@@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from backend.session import get_session from backend.session import get_session
from schemas.project import * from schemas.project import *
from services.auth import guest_user, authorized_user from services.auth import guest_user, authorized_user, user_deals_viewer
from services.project import ProjectService from services.project import ProjectService
project_router = APIRouter( project_router = APIRouter(
@@ -18,6 +18,7 @@ project_router = APIRouter(
"/", "/",
response_model=GetProjectsResponse, response_model=GetProjectsResponse,
operation_id="get_projects", operation_id="get_projects",
dependencies=[Depends(user_deals_viewer)],
) )
async def get_projects( async def get_projects(
session: Annotated[AsyncSession, Depends(get_session)], session: Annotated[AsyncSession, Depends(get_session)],

View File

@@ -1,5 +1,6 @@
from typing import Optional from typing import Optional, TypedDict, Union
from models import User
from schemas.base import BaseSchema, CustomModelSnake from schemas.base import BaseSchema, CustomModelSnake
@@ -14,3 +15,16 @@ class AuthLoginRequest(CustomModelSnake):
class AuthLoginResponse(BaseSchema): class AuthLoginResponse(BaseSchema):
access_token: str access_token: str
class UserDealEditor(TypedDict):
deal_id: int
sub: str
class UserViewer(TypedDict):
client_id: int
sub: str
UserUnion = Union[User, UserViewer, UserDealEditor]

View File

@@ -87,4 +87,9 @@ class ClientUpdateResponse(OkMessageSchema):
class ClientDeleteResponse(OkMessageSchema): class ClientDeleteResponse(OkMessageSchema):
pass pass
class CreateGuestUrlResponse(OkMessageSchema):
url: str
# endregion # endregion

View File

@@ -11,6 +11,7 @@ from starlette import status
import backend.config import backend.config
import constants import constants
from backend.session import get_session from backend.session import get_session
from constants import DEALS_VIEWER, DEAL_EDITOR
from enums.user import UserRole from enums.user import UserRole
from models import User, InviteCode from models import User, InviteCode
from schemas.auth import * from schemas.auth import *
@@ -23,7 +24,7 @@ algorithm = 'HS256'
async def get_current_user( async def get_current_user(
session: Annotated[AsyncSession, Depends(get_session)], session: Annotated[AsyncSession, Depends(get_session)],
token: Annotated[HTTPAuthorizationCredentials, Depends(oauth2_schema)] token: Annotated[HTTPAuthorizationCredentials, Depends(oauth2_schema)]
) -> Union[User, None, dict]: ) -> Optional[UserUnion]:
if not token.credentials: if not token.credentials:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token') raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token')
try: try:
@@ -31,11 +32,11 @@ async def get_current_user(
user_id = payload.get('sub') user_id = payload.get('sub')
if not user_id: if not user_id:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid credentials') raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid credentials')
if user_id == 'guest': if user_id == DEAL_EDITOR or user_id == DEALS_VIEWER:
return payload return payload
user_id = int(user_id) user_id = int(user_id)
user = await session.get(User, user_id) user: Optional[User] = await session.get(User, user_id)
if not user: if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid credentials') raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid credentials')
if user.is_deleted or user.is_blocked: if user.is_deleted or user.is_blocked:
@@ -53,12 +54,24 @@ async def authorized_user(
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token') raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token')
async def guest_user(user: Annotated[User, Depends(get_current_user)]): async def guest_user(user: Annotated[UserUnion, Depends(get_current_user)]):
if (type(user) is User) or (type(user) is dict): if (type(user) is User) or (type(user) is dict):
return user return user
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token') raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token')
async def user_deals_viewer(user: Annotated[UserUnion, Depends(get_current_user)]):
if (type(user) is User) or (type(user) is dict and user['sub'] == DEALS_VIEWER):
return user
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token')
async def user_deal_editor(user: Annotated[UserUnion, Depends(get_current_user)]):
if (type(user) is User) or (type(user) is dict and user['sub'] == DEAL_EDITOR):
return user
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token')
class AuthService(BaseService): class AuthService(BaseService):
@staticmethod @staticmethod
def _generate_jwt_token(payload: dict) -> str: def _generate_jwt_token(payload: dict) -> str:
@@ -101,9 +114,17 @@ class AuthService(BaseService):
return AuthLoginResponse(access_token=access_token) return AuthLoginResponse(access_token=access_token)
def create_deal_guest_token(self, deal_id: int): def create_deal_guest_token(self, deal_id: int):
payload = { payload: UserDealEditor = {
'sub': 'guest', 'sub': constants.DEAL_EDITOR,
'deal_id': deal_id 'deal_id': deal_id
} }
return self._generate_jwt_token(payload) return self._generate_jwt_token(payload)
def create_client_guest_token(self, client_id: int):
payload: UserViewer = {
'sub': constants.DEALS_VIEWER,
'client_id': client_id
}
return self._generate_jwt_token(payload)

View File

@@ -17,9 +17,11 @@ from external.billing import BillingClient, CreateBillingRequestValue, CreateBil
ProductBillingDocumentPdf, ServiceBillingDocumentPdf ProductBillingDocumentPdf, ServiceBillingDocumentPdf
from models import CardBillRequest, Card, CardProduct, CardService as CardServiceModel, CardGroup, \ from models import CardBillRequest, Card, CardProduct, CardService as CardServiceModel, CardGroup, \
GroupBillRequest GroupBillRequest
from schemas.auth import UserUnion
from schemas.billing import * from schemas.billing import *
from services.base import BaseService from services.base import BaseService
from services.card import CardsService from services.card import CardsService
from utils.auth import verify_user_deal_editor
from utils.list_utils import to_locale_number from utils.list_utils import to_locale_number
@@ -157,6 +159,8 @@ class BillingService(BaseService):
async def create_card_billing(self, user, request: CreateCardBillRequest) -> CreateCardBillResponse: async def create_card_billing(self, user, request: CreateCardBillRequest) -> CreateCardBillResponse:
try: try:
verify_user_deal_editor(user)
card_service = CardsService(self.session) card_service = CardsService(self.session)
billing_client = BillingClient(backend.config.BILLING_API_KEY) billing_client = BillingClient(backend.config.BILLING_API_KEY)
@@ -261,8 +265,10 @@ class BillingService(BaseService):
await self.session.delete(bill) await self.session.delete(bill)
async def cancel_card_billing(self, request: CancelCardBillRequest) -> CancelCardBillResponse: async def cancel_card_billing(self, user: UserUnion, request: CancelCardBillRequest) -> CancelCardBillResponse:
try: try:
verify_user_deal_editor(user)
card = await self._get_card_by_id(request.card_id) card = await self._get_card_by_id(request.card_id)
if not card: if not card:
return CancelCardBillResponse(ok=False, message='Сделка не найдена') return CancelCardBillResponse(ok=False, message='Сделка не найдена')

View File

@@ -2,14 +2,15 @@ from collections import defaultdict
import lexorank import lexorank
import math import math
import starlette.status
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy import select, func, update, delete, insert, and_, Select from sqlalchemy import select, func, update, delete, insert, and_, Select
from sqlalchemy.orm import joinedload, selectinload, noload from sqlalchemy.orm import joinedload, selectinload, noload
from starlette import status
from card_attributes import CardAttributesCommandHandler from card_attributes import CardAttributesCommandHandler
from card_attributes.exceptions import CardAttributeException from card_attributes.exceptions import CardAttributeException
from models import * from models import *
from schemas.auth import UserUnion
from schemas.base import PaginationSchema from schemas.base import PaginationSchema
from schemas.card import * from schemas.card import *
from schemas.client import ClientDetailsSchema from schemas.client import ClientDetailsSchema
@@ -20,20 +21,13 @@ from services.card_tag import CardTagService
from services.client import ClientService from services.client import ClientService
from services.service import ServiceService from services.service import ServiceService
from services.shipping_warehouse import ShippingWarehouseService from services.shipping_warehouse import ShippingWarehouseService
from utils.auth import verify_user_deal_editor, verify_user_viewer
class CardsService(BaseService): class CardsService(BaseService):
# region Card # region Card
@staticmethod
def grant_access(user: Union[User, dict], card_id: int):
if type(user) is User:
return
user_card_id = user['deal_id']
if int(user_card_id) != int(card_id):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token')
async def _get_card_by_id(self, card_id) -> Union[Card, None]: async def _get_card_by_id(self, card_id) -> Union[Card, None]:
return await self.session.get(Card, card_id) return await self.session.get(Card, card_id)
@@ -235,7 +229,12 @@ class CardsService(BaseService):
total_items=total_items, total_items=total_items,
) )
async def get_summary(self, request: GetCardSummariesRequest, pagination: PaginationSchema) -> CardSummaryResponse: async def get_summary(
self,
request: GetCardSummariesRequest,
pagination: PaginationSchema,
user: UserUnion,
) -> CardSummaryResponse:
price_subquery = self._get_price_subquery() price_subquery = self._get_price_subquery()
products_quantity_subquery = self._get_products_quantity_subquery() products_quantity_subquery = self._get_products_quantity_subquery()
q = ( q = (
@@ -268,6 +267,9 @@ class CardsService(BaseService):
) )
) )
if isinstance(user, dict) and "client_id" in user:
q = q.where(Card.client_id == user["client_id"])
q = self._apply_summary_filters(q, request) q = self._apply_summary_filters(q, request)
pagination_info = await self._summaries_pagination_info(q, pagination) pagination_info = await self._summaries_pagination_info(q, pagination)
@@ -346,10 +348,9 @@ class CardsService(BaseService):
result.append(CardSchema.model_validate(card)) result.append(CardSchema.model_validate(card))
return CardGetAllResponse(cards=result) return CardGetAllResponse(cards=result)
async def get_by_id(self, user: Union[User, dict], card_id: int, return_raw=False) -> Union[ async def get_by_id(self, user: UserUnion, card_id: int, return_raw=False) -> Union[
CardSchema, Card]: CardSchema, Card
self.grant_access(user, card_id) ]:
card: Optional[Card] = await self.session.scalar( card: Optional[Card] = await self.session.scalar(
select(Card) select(Card)
.options( .options(
@@ -393,11 +394,17 @@ class CardsService(BaseService):
) )
.where(Card.id == card_id) .where(Card.id == card_id)
) )
if return_raw:
return card
if not card: if not card:
raise HTTPException(status_code=404, detail="Карточка не найдена") raise HTTPException(status_code=404, detail="Карточка не найдена")
if not verify_user_deal_editor(user, card_id, False) and \
not verify_user_viewer(user, card.client_id, False):
raise HTTPException(status_code=starlette.status.HTTP_403_FORBIDDEN, detail='Forbidden')
if return_raw:
return card
return CardSchema.model_validate(card) return CardSchema.model_validate(card)
async def update_general_info( async def update_general_info(
@@ -450,12 +457,15 @@ class CardsService(BaseService):
await self.session.rollback() await self.session.rollback()
return CardUpdateGeneralInfoResponse(ok=False, message=str(e)) return CardUpdateGeneralInfoResponse(ok=False, message=str(e))
async def update_products_and_services_general_info(self, request: ProductsAndServicesGeneralInfoRequest) -> ( async def update_products_and_services_general_info(self, request: ProductsAndServicesGeneralInfoRequest,
user: UserUnion) -> (
ProductsAndServicesGeneralInfoResponse ProductsAndServicesGeneralInfoResponse
): ):
verify_user_deal_editor(user, request.card_id)
card: Optional[Card] = await self.session.get(Card, request.card_id) card: Optional[Card] = await self.session.get(Card, request.card_id)
if not card: if not card:
return ProductsAndServicesGeneralInfoRequest(ok=False, message='Карточка не найдена') return ProductsAndServicesGeneralInfoResponse(ok=False, message='Карточка не найдена')
# Updating shipping warehouse # Updating shipping warehouse
shipping_warehouse_service = ShippingWarehouseService(self.session) shipping_warehouse_service = ShippingWarehouseService(self.session)
@@ -464,8 +474,10 @@ class CardsService(BaseService):
shipping_warehouse = await shipping_warehouse_service.create_by_name(request.data.shipping_warehouse) shipping_warehouse = await shipping_warehouse_service.create_by_name(request.data.shipping_warehouse)
if card.group: if card.group:
for card in card.group.cards: stmt_group = select(CardGroup).where(CardGroup.id == card.group.id).options(selectinload(CardGroup.cards))
card.is_services_profit_accounted = request.data.is_services_profit_accounted group = (await self.session.scalars(stmt_group)).one_or_none()
for c in group.cards:
c.is_services_profit_accounted = request.data.is_services_profit_accounted
else: else:
card.is_services_profit_accounted = request.data.is_services_profit_accounted card.is_services_profit_accounted = request.data.is_services_profit_accounted
@@ -579,7 +591,7 @@ class CardsService(BaseService):
except Exception as e: except Exception as e:
return CardAddKitResponse(ok=False, message=str(e)) return CardAddKitResponse(ok=False, message=str(e))
def create_guest_url(self, user: User, request: CardCreateGuestUrlRequest) -> CardCreateGuestUrlResponse: async def create_guest_url(self, request: CardCreateGuestUrlRequest) -> CardCreateGuestUrlResponse:
access_token = AuthService(self.session).create_deal_guest_token(request.card_id) access_token = AuthService(self.session).create_deal_guest_token(request.card_id)
url = f"deals/{request.card_id}?accessToken={access_token}" url = f"deals/{request.card_id}?accessToken={access_token}"
return CardCreateGuestUrlResponse(ok=True, message='Ссылка успешно создана!', url=url) return CardCreateGuestUrlResponse(ok=True, message='Ссылка успешно создана!', url=url)
@@ -668,11 +680,11 @@ class CardsService(BaseService):
async def update_service_quantity( async def update_service_quantity(
self, self,
user: Union[User, dict], user: UserUnion,
request: CardUpdateServiceQuantityRequest request: CardUpdateServiceQuantityRequest
) -> CardUpdateServiceQuantityResponse: ) -> CardUpdateServiceQuantityResponse:
try: try:
self.grant_access(user, request.card_id) verify_user_deal_editor(user, request.card_id)
card_service = await self.session.scalar( card_service = await self.session.scalar(
select(CardService) select(CardService)
.where(CardService.card_id == request.card_id, .where(CardService.card_id == request.card_id,
@@ -689,11 +701,11 @@ class CardsService(BaseService):
async def add_service( async def add_service(
self, self,
user: Union[User, dict], user: UserUnion,
request: CardAddServiceRequest request: CardAddServiceRequest
) -> CardAddServiceResponse: ) -> CardAddServiceResponse:
try: try:
self.grant_access(user, request.card_id) verify_user_deal_editor(user, request.card_id)
card = await self.session.scalar(select(Card).where(Card.id == request.card_id)) card = await self.session.scalar(select(Card).where(Card.id == request.card_id))
if not card: if not card:
raise HTTPException(status_code=404, detail="Карточка не найдена") raise HTTPException(status_code=404, detail="Карточка не найдена")
@@ -723,11 +735,12 @@ class CardsService(BaseService):
async def delete_service( async def delete_service(
self, self,
user: Union[User, dict], user: UserUnion,
request: CardDeleteServiceRequest request: CardDeleteServiceRequest
) -> CardDeleteServiceResponse: ) -> CardDeleteServiceResponse:
try: try:
self.grant_access(user, request.card_id) verify_user_deal_editor(user, request.card_id)
card_service = await self.session.scalar( card_service = await self.session.scalar(
select(CardService) select(CardService)
.where(CardService.card_id == request.card_id, .where(CardService.card_id == request.card_id,
@@ -744,11 +757,12 @@ class CardsService(BaseService):
async def delete_services( async def delete_services(
self, self,
user: Union[User, dict], user: UserUnion,
request: CardDeleteServicesRequest request: CardDeleteServicesRequest
) -> CardDeleteServicesResponse: ) -> CardDeleteServicesResponse:
try: try:
self.grant_access(user, request) verify_user_deal_editor(user, request.card_id)
card_services = await self.session.scalars( card_services = await self.session.scalars(
select(CardService) select(CardService)
.where(CardService.card_id == request.card_id, .where(CardService.card_id == request.card_id,
@@ -764,11 +778,11 @@ class CardsService(BaseService):
async def update_service( async def update_service(
self, self,
user: Union[User, dict], user: UserUnion,
request: CardUpdateServiceRequest request: CardUpdateServiceRequest
) -> CardUpdateServiceResponse: ) -> CardUpdateServiceResponse:
try: try:
self.grant_access(user, request.card_id) verify_user_deal_editor(user, request.card_id)
card_service = await self.session.scalar( card_service = await self.session.scalar(
select(CardService) select(CardService)
.where(CardService.card_id == request.card_id, .where(CardService.card_id == request.card_id,
@@ -823,11 +837,11 @@ class CardsService(BaseService):
async def copy_services( async def copy_services(
self, self,
user: Union[User, dict], user: UserUnion,
request: CardServicesCopyRequest request: CardServicesCopyRequest
) -> CardServicesCopyResponse: ) -> CardServicesCopyResponse:
try: try:
self.grant_access(user, request.card_id) verify_user_deal_editor(user, request.card_id)
source_services_stmt = ( source_services_stmt = (
select( select(
CardProductService CardProductService
@@ -911,11 +925,11 @@ class CardsService(BaseService):
# region Card products # region Card products
async def update_product_quantity( async def update_product_quantity(
self, self,
user: Union[User, dict], user: UserUnion,
request: CardUpdateProductQuantityRequest request: CardUpdateProductQuantityRequest
) -> CardUpdateProductQuantityResponse: ) -> CardUpdateProductQuantityResponse:
try: try:
self.grant_access(user, request.card_id) verify_user_deal_editor(user, request.card_id)
# check if there is no card or no product with different exceptions # check if there is no card or no product with different exceptions
card_product = await self.session.scalar( card_product = await self.session.scalar(
select(CardProduct) select(CardProduct)
@@ -933,12 +947,11 @@ class CardsService(BaseService):
async def add_product( async def add_product(
self, self,
user: Union[User, dict], user: UserUnion,
request: CardAddProductRequest request: CardAddProductRequest
) -> CardAddProductResponse: ) -> CardAddProductResponse:
try: try:
self.grant_access(user, request.card_id) verify_user_deal_editor(user, request.card_id)
card = await self.session.scalar(select(Card).where(Card.id == request.card_id)) card = await self.session.scalar(select(Card).where(Card.id == request.card_id))
if not card: if not card:
@@ -978,11 +991,12 @@ class CardsService(BaseService):
async def delete_product( async def delete_product(
self, self,
user: Union[User, dict], user: UserUnion,
request: CardDeleteProductRequest request: CardDeleteProductRequest
) -> CardDeleteProductResponse: ) -> CardDeleteProductResponse:
try: try:
self.grant_access(user, request.card_id) verify_user_deal_editor(user, request.card_id)
card_product = await self.session.scalar( card_product = await self.session.scalar(
select(CardProduct) select(CardProduct)
.where(CardProduct.card_id == request.card_id, .where(CardProduct.card_id == request.card_id,
@@ -999,11 +1013,11 @@ class CardsService(BaseService):
async def delete_products( async def delete_products(
self, self,
user: Union[User, dict], user: UserUnion,
request: CardDeleteProductsRequest request: CardDeleteProductsRequest
) -> CardDeleteProductsResponse: ) -> CardDeleteProductsResponse:
try: try:
self.grant_access(user, request.card_id) verify_user_deal_editor(user, request.card_id)
card_products = await self.session.scalars( card_products = await self.session.scalars(
select(CardProduct) select(CardProduct)
.where(CardProduct.card_id == request.card_id, .where(CardProduct.card_id == request.card_id,
@@ -1019,11 +1033,11 @@ class CardsService(BaseService):
async def update_product( async def update_product(
self, self,
user: Union[User, dict], user: UserUnion,
request: CardUpdateProductRequest request: CardUpdateProductRequest
): ):
try: try:
self.grant_access(user, request.card_id) verify_user_deal_editor(user, request.card_id)
card_product: CardProduct = await self.session.scalar( card_product: CardProduct = await self.session.scalar(
select(CardProduct) select(CardProduct)
.where(CardProduct.card_id == request.card_id, .where(CardProduct.card_id == request.card_id,
@@ -1106,11 +1120,12 @@ class CardsService(BaseService):
async def add_kit_to_card_product( async def add_kit_to_card_product(
self, self,
user: Union[User, dict], user: UserUnion,
request: CardProductAddKitRequest request: CardProductAddKitRequest
) -> CardProductAddKitResponse: ) -> CardProductAddKitResponse:
try: try:
self.grant_access(user, request.card_id) verify_user_deal_editor(user, request.card_id)
service_service = ServiceService(self.session) service_service = ServiceService(self.session)
kit = await service_service.get_kit_by_id(request.kit_id) kit = await service_service.get_kit_by_id(request.kit_id)
if not kit: if not kit:

View File

@@ -68,19 +68,17 @@ class CardGroupService(BaseService):
) )
if not group: if not group:
return CardGroupUpdateResponse(ok=False, message="Группа не найдена") return CardGroupUpdateResponse(ok=False, message="Группа не найдена")
# update by dictionary
request_dict = request.data.model_dump()
request_dict.pop("bill_request", None)
update_stmt = ( update_stmt = (
update(CardGroup) update(CardGroup)
.where(CardGroup.id == request.data.id) .where(CardGroup.id == request.data.id)
.values(**request_dict) .values(name=request.data.name, lexorank=request.data.lexorank)
) )
await self.session.execute(update_stmt) await self.session.execute(update_stmt)
await self.session.commit() await self.session.commit()
return CardGroupUpdateResponse(ok=True, message="Группа успешно обновлена") return CardGroupUpdateResponse(ok=True, message="Группа успешно обновлена")
except Exception as e: except Exception as e:
print(e)
await self.session.rollback() await self.session.rollback()
return CardGroupUpdateResponse(ok=False, message=str(e)) return CardGroupUpdateResponse(ok=False, message=str(e))

View File

@@ -10,8 +10,10 @@ from external.chat.chat_client import ChatClient
from external.chat.schemas import * from external.chat.schemas import *
from external.kafka.services.producer_service import ProducerService from external.kafka.services.producer_service import ProducerService
from models import Message, Chat, MessageStatus, TgGroup, Client, Card, MessageFile, User from models import Message, Chat, MessageStatus, TgGroup, Client, Card, MessageFile, User
from schemas.auth import UserUnion
from schemas.chat import * from schemas.chat import *
from services.base import BaseService from services.base import BaseService
from utils.auth import verify_user_deal_editor
class ChatService(BaseService): class ChatService(BaseService):
@@ -163,7 +165,9 @@ class ChatService(BaseService):
return CreateChatResponse(ok=True, message='Чат успешно создан') return CreateChatResponse(ok=True, message='Чат успешно создан')
async def create_chat(self, request: CreateChatRequest) -> CreateChatResponse: async def create_chat(self, request: CreateChatRequest, user: UserUnion) -> CreateChatResponse:
verify_user_deal_editor(user, request.card_id)
client: Optional[Client] = await self.session.get(Client, request.client_id) client: Optional[Client] = await self.session.get(Client, request.client_id)
if not client: if not client:
return CreateChatResponse(ok=False, message=f'Клиент с ID {request.client_id} не найден') return CreateChatResponse(ok=False, message=f'Клиент с ID {request.client_id} не найден')

View File

@@ -7,6 +7,7 @@ from sqlalchemy.orm import joinedload, selectinload, noload
from models import Client, ClientDetails, User, ResidualPallet, ResidualBox, ResidualProduct, Product from models import Client, ClientDetails, User, ResidualPallet, ResidualBox, ResidualProduct, Product
from schemas.client import * from schemas.client import *
from services.auth import AuthService
from services.base import BaseService from services.base import BaseService
@@ -165,3 +166,8 @@ class ClientService(BaseService):
return ClientDeleteResponse(ok=True, message='Клиент удален') return ClientDeleteResponse(ok=True, message='Клиент удален')
except Exception as e: except Exception as e:
return ClientDeleteResponse(ok=False, message=str(e)) return ClientDeleteResponse(ok=False, message=str(e))
async def create_guest_url(self, client_id: int) -> CreateGuestUrlResponse:
access_token = AuthService(self.session).create_client_guest_token(client_id)
url = f"leads?accessToken={access_token}"
return CreateGuestUrlResponse(ok=True, message='Ссылка успешно создана!', url=url)

View File

@@ -1,6 +1,7 @@
from io import BytesIO from io import BytesIO
from typing import Optional from typing import Optional
import starlette.status
from fastapi import HTTPException, UploadFile from fastapi import HTTPException, UploadFile
from sqlalchemy import select, func, Integer, update, or_ from sqlalchemy import select, func, Integer, update, or_
from sqlalchemy.orm import selectinload, Query from sqlalchemy.orm import selectinload, Query
@@ -10,16 +11,20 @@ from backend import config
from barcodes.images_uploader import BarcodeImagesUploader from barcodes.images_uploader import BarcodeImagesUploader
from barcodes.pdf.pdf_maker import PdfMaker from barcodes.pdf.pdf_maker import PdfMaker
from external.s3_uploader.uploader import S3Uploader from external.s3_uploader.uploader import S3Uploader
from models import User
from models.product import Product, ProductImage, ProductBarcodeImage from models.product import Product, ProductImage, ProductBarcodeImage
from schemas.auth import UserUnion
from schemas.base import PaginationSchema from schemas.base import PaginationSchema
from schemas.product import * from schemas.product import *
from services.base import BaseService from services.base import BaseService
from utils.auth import verify_user_deal_editor
from utils.dependecies import is_valid_pagination from utils.dependecies import is_valid_pagination
class ProductService(BaseService): class ProductService(BaseService):
async def create(self, request: ProductCreateRequest) -> ProductCreateResponse: async def create(self, request: ProductCreateRequest, user: UserUnion) -> ProductCreateResponse:
verify_user_deal_editor(user)
# Creating product # Creating product
product_dict = request.dict() product_dict = request.dict()

View File

@@ -1,8 +1,8 @@
from typing import Optional from typing import Optional
from sqlalchemy import select, and_, func from sqlalchemy import select, and_, func, exists, or_
from models import CardStatus, Card from models import CardStatus, Card, CardStatusHistory
from schemas.status import * from schemas.status import *
from services.base import BaseService from services.base import BaseService
@@ -106,6 +106,18 @@ class StatusService(BaseService):
statuses[-2].is_finishing = True statuses[-2].is_finishing = True
statuses[-1].is_finishing = False statuses[-1].is_finishing = False
async def _status_has_history(self, status_id: int) -> bool:
stmt = (
select(exists(CardStatusHistory))
.where(
or_(
CardStatusHistory.to_status_id == status_id,
CardStatusHistory.from_status_id == status_id,
)
)
)
return (await self.session.scalars(stmt)).first()
async def delete_status(self, status_id: int) -> DeleteStatusResponse: async def delete_status(self, status_id: int) -> DeleteStatusResponse:
status = await self._get_status_by_id(status_id) status = await self._get_status_by_id(status_id)
if not status: if not status:
@@ -119,7 +131,8 @@ class StatusService(BaseService):
await self._set_finishing_flag_to_prev_status(status) await self._set_finishing_flag_to_prev_status(status)
count_deals = await self._count_deals(status_id) count_deals = await self._count_deals(status_id)
if count_deals == 0: exist_in_history = await self._status_has_history(status_id)
if count_deals == 0 and not exist_in_history:
await self.session.delete(status) await self.session.delete(status)
else: else:
status.is_deleted = True status.is_deleted = True

46
utils/auth.py Normal file
View File

@@ -0,0 +1,46 @@
from typing import Optional
import starlette.status
from fastapi import HTTPException
from constants import DEALS_VIEWER, DEAL_EDITOR
from models import User
from schemas.auth import UserUnion
def raise_403():
raise HTTPException(status_code=starlette.status.HTTP_403_FORBIDDEN, detail='Forbidden')
def verify_user(user: UserUnion, raising: bool = True) -> bool:
if type(user) is User:
return True
if raising:
raise_403()
return False
def verify_user_viewer(user: UserUnion, client_id: Optional[int] = None, raising: bool = True) -> bool:
is_regular_user: bool = type(user) is User
is_viewer: bool = isinstance(user, dict) and user['sub'] == DEALS_VIEWER
if client_id is not None:
is_viewer = is_viewer and int(user['client_id']) == client_id
if is_regular_user or is_viewer:
return True
if raising:
raise_403()
return False
def verify_user_deal_editor(user: UserUnion, deal_id: Optional[int] = None, raising: bool = True) -> bool:
is_regular_user: bool = type(user) is User
is_deal_editor: bool = isinstance(user, dict) and user['sub'] == DEAL_EDITOR
if deal_id is not None:
is_deal_editor = is_deal_editor and int(user['deal_id']) == deal_id
if is_regular_user or is_deal_editor:
return True
if raising:
raise_403()
return False