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

@@ -11,6 +11,7 @@ from starlette import status
import backend.config
import constants
from backend.session import get_session
from constants import DEALS_VIEWER, DEAL_EDITOR
from enums.user import UserRole
from models import User, InviteCode
from schemas.auth import *
@@ -23,7 +24,7 @@ algorithm = 'HS256'
async def get_current_user(
session: Annotated[AsyncSession, Depends(get_session)],
token: Annotated[HTTPAuthorizationCredentials, Depends(oauth2_schema)]
) -> Union[User, None, dict]:
) -> Optional[UserUnion]:
if not token.credentials:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token')
try:
@@ -31,11 +32,11 @@ async def get_current_user(
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':
if user_id == DEAL_EDITOR or user_id == DEALS_VIEWER:
return payload
user_id = int(user_id)
user = await session.get(User, user_id)
user: Optional[User] = await session.get(User, user_id)
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid credentials')
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')
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):
return user
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):
@staticmethod
def _generate_jwt_token(payload: dict) -> str:
@@ -101,9 +114,17 @@ class AuthService(BaseService):
return AuthLoginResponse(access_token=access_token)
def create_deal_guest_token(self, deal_id: int):
payload = {
'sub': 'guest',
payload: UserDealEditor = {
'sub': constants.DEAL_EDITOR,
'deal_id': deal_id
}
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
from models import CardBillRequest, Card, CardProduct, CardService as CardServiceModel, CardGroup, \
GroupBillRequest
from schemas.auth import UserUnion
from schemas.billing import *
from services.base import BaseService
from services.card import CardsService
from utils.auth import verify_user_deal_editor
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:
try:
verify_user_deal_editor(user)
card_service = CardsService(self.session)
billing_client = BillingClient(backend.config.BILLING_API_KEY)
@@ -261,8 +265,10 @@ class BillingService(BaseService):
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:
verify_user_deal_editor(user)
card = await self._get_card_by_id(request.card_id)
if not card:
return CancelCardBillResponse(ok=False, message='Сделка не найдена')

View File

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

View File

@@ -68,19 +68,17 @@ class CardGroupService(BaseService):
)
if not group:
return CardGroupUpdateResponse(ok=False, message="Группа не найдена")
# update by dictionary
request_dict = request.data.model_dump()
request_dict.pop("bill_request", None)
update_stmt = (
update(CardGroup)
.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.commit()
return CardGroupUpdateResponse(ok=True, message="Группа успешно обновлена")
except Exception as e:
print(e)
await self.session.rollback()
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.kafka.services.producer_service import ProducerService
from models import Message, Chat, MessageStatus, TgGroup, Client, Card, MessageFile, User
from schemas.auth import UserUnion
from schemas.chat import *
from services.base import BaseService
from utils.auth import verify_user_deal_editor
class ChatService(BaseService):
@@ -163,7 +165,9 @@ class ChatService(BaseService):
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)
if not client:
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 schemas.client import *
from services.auth import AuthService
from services.base import BaseService
@@ -165,3 +166,8 @@ class ClientService(BaseService):
return ClientDeleteResponse(ok=True, message='Клиент удален')
except Exception as 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 typing import Optional
import starlette.status
from fastapi import HTTPException, UploadFile
from sqlalchemy import select, func, Integer, update, or_
from sqlalchemy.orm import selectinload, Query
@@ -10,16 +11,20 @@ from backend import config
from barcodes.images_uploader import BarcodeImagesUploader
from barcodes.pdf.pdf_maker import PdfMaker
from external.s3_uploader.uploader import S3Uploader
from models import User
from models.product import Product, ProductImage, ProductBarcodeImage
from schemas.auth import UserUnion
from schemas.base import PaginationSchema
from schemas.product import *
from services.base import BaseService
from utils.auth import verify_user_deal_editor
from utils.dependecies import is_valid_pagination
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
product_dict = request.dict()

View File

@@ -1,8 +1,8 @@
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 services.base import BaseService
@@ -106,6 +106,18 @@ class StatusService(BaseService):
statuses[-2].is_finishing = True
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:
status = await self._get_status_by_id(status_id)
if not status:
@@ -119,7 +131,8 @@ class StatusService(BaseService):
await self._set_finishing_flag_to_prev_status(status)
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)
else:
status.is_deleted = True