From 97f835ffdea933db4285f1b132d9b6e387a657db Mon Sep 17 00:00:00 2001 From: fakz9 Date: Thu, 8 Aug 2024 07:49:53 +0300 Subject: [PATCH] feat: billing guest access --- backend/config.py | 2 + backend/dependecies.py | 4 +- external/billing/__init__.py | 3 + external/billing/billing_client.py | 27 +++++ external/billing/enums.py | 6 + external/billing/schemas.py | 81 +++++++++++++ main.py | 1 + models/__init__.py | 1 + models/billing.py | 25 ++++ models/deal.py | 6 + routers/__init__.py | 1 + routers/barcode.py | 11 +- routers/billing.py | 51 ++++++++ routers/client.py | 4 +- routers/deal.py | 183 ++++++++++++++++++++--------- routers/marketplace.py | 6 +- routers/payroll.py | 6 +- routers/position.py | 6 +- routers/product.py | 10 +- routers/role.py | 6 +- routers/service.py | 36 ++++-- routers/shipping_warehouse.py | 4 +- routers/time_tracking.py | 6 +- routers/user.py | 6 +- schemas/billing.py | 24 ++++ schemas/deal.py | 11 ++ services/auth.py | 41 +++++-- services/billing.py | 118 +++++++++++++++++++ services/deal.py | 120 +++++++++++++++---- services/product.py | 16 +-- 30 files changed, 682 insertions(+), 140 deletions(-) create mode 100644 external/billing/__init__.py create mode 100644 external/billing/billing_client.py create mode 100644 external/billing/enums.py create mode 100644 external/billing/schemas.py create mode 100644 models/billing.py create mode 100644 routers/billing.py create mode 100644 schemas/billing.py create mode 100644 services/billing.py diff --git a/backend/config.py b/backend/config.py index 993c716..6b8051a 100644 --- a/backend/config.py +++ b/backend/config.py @@ -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') diff --git a/backend/dependecies.py b/backend/dependecies.py index f85b408..0e67579 100644 --- a/backend/dependecies.py +++ b/backend/dependecies.py @@ -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)] diff --git a/external/billing/__init__.py b/external/billing/__init__.py new file mode 100644 index 0000000..d305814 --- /dev/null +++ b/external/billing/__init__.py @@ -0,0 +1,3 @@ +from .schemas import * +from .enums import * +from .billing_client import BillingClient diff --git a/external/billing/billing_client.py b/external/billing/billing_client.py new file mode 100644 index 0000000..3dbf904 --- /dev/null +++ b/external/billing/billing_client.py @@ -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) diff --git a/external/billing/enums.py b/external/billing/enums.py new file mode 100644 index 0000000..8751c16 --- /dev/null +++ b/external/billing/enums.py @@ -0,0 +1,6 @@ +from enum import StrEnum + + +class NotificationChannel(StrEnum): + PAYMENT_DETAILS = 'PAYMENT_DETAILS' + PAYMENT_VERIFICATION = 'PAYMENT_VERIFICATION' diff --git a/external/billing/schemas.py b/external/billing/schemas.py new file mode 100644 index 0000000..5283ab9 --- /dev/null +++ b/external/billing/schemas.py @@ -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 diff --git a/main.py b/main.py index 22171ca..12b3bcb 100644 --- a/main.py +++ b/main.py @@ -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) diff --git a/models/__init__.py b/models/__init__.py index 2b913c8..0a59eeb 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -10,5 +10,6 @@ from .barcode import * from .shipping_warehouse import * from .marketplace import * from .payroll import * +from .billing import * configure_mappers() diff --git a/models/billing.py b/models/billing.py new file mode 100644 index 0000000..062e223 --- /dev/null +++ b/models/billing.py @@ -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) diff --git a/models/deal.py b/models/deal.py index 65bbd08..08d884f 100644 --- a/models/deal.py +++ b/models/deal.py @@ -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): diff --git a/routers/__init__.py b/routers/__init__.py index e2e6c99..f8bbea3 100644 --- a/routers/__init__.py +++ b/routers/__init__.py @@ -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 diff --git a/routers/barcode.py b/routers/barcode.py index 79d3e3e..0e9c918 100644 --- a/routers/barcode.py +++ b/routers/barcode.py @@ -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)] ) diff --git a/routers/billing.py b/routers/billing.py new file mode 100644 index 0000000..07e4830 --- /dev/null +++ b/routers/billing.py @@ -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) diff --git a/routers/client.py b/routers/client.py index 0f335b0..0f93140 100644 --- a/routers/client.py +++ b/routers/client.py @@ -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)] ) diff --git a/routers/deal.py b/routers/deal.py index bda12d8..42a8512 100644 --- a/routers/deal.py +++ b/routers/deal.py @@ -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,10 +68,12 @@ async def change_status( return await DealService(session).change_status_manual(request, user) -@deal_router.get('/summaries', - response_model=DealSummaryResponse, - operation_id='getDealSummaries' - ) +@deal_router.get( + '/summaries', + response_model=DealSummaryResponse, + 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 + 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 diff --git a/routers/marketplace.py b/routers/marketplace.py index 80f8480..66c5a81 100644 --- a/routers/marketplace.py +++ b/routers/marketplace.py @@ -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)] ) diff --git a/routers/payroll.py b/routers/payroll.py index 3d86b99..ab89bad 100644 --- a/routers/payroll.py +++ b/routers/payroll.py @@ -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)] ) diff --git a/routers/position.py b/routers/position.py index d8dba40..763e791 100644 --- a/routers/position.py +++ b/routers/position.py @@ -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)] ) diff --git a/routers/product.py b/routers/product.py index 0a7a414..268e330 100644 --- a/routers/product.py +++ b/routers/product.py @@ -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( diff --git a/routers/role.py b/routers/role.py index 88f87ca..b434035 100644 --- a/routers/role.py +++ b/routers/role.py @@ -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)] ) diff --git a/routers/service.py b/routers/service.py index 2ef74f4..123bac5 100644 --- a/routers/service.py +++ b/routers/service.py @@ -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, diff --git a/routers/shipping_warehouse.py b/routers/shipping_warehouse.py index 3efb6f2..7e0ba4e 100644 --- a/routers/shipping_warehouse.py +++ b/routers/shipping_warehouse.py @@ -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)] ) diff --git a/routers/time_tracking.py b/routers/time_tracking.py index 0e7239f..8bcf565 100644 --- a/routers/time_tracking.py +++ b/routers/time_tracking.py @@ -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)] ) diff --git a/routers/user.py b/routers/user.py index 670f9a6..452a65f 100644 --- a/routers/user.py +++ b/routers/user.py @@ -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)] ) diff --git a/schemas/billing.py b/schemas/billing.py new file mode 100644 index 0000000..ba42174 --- /dev/null +++ b/schemas/billing.py @@ -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 diff --git a/schemas/deal.py b/schemas/deal.py index ffb988b..6c79059 100644 --- a/schemas/deal.py +++ b/schemas/deal.py @@ -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 diff --git a/services/auth.py b/services/auth.py index 0273964..21e7632 100644 --- a/services/auth.py +++ b/services/auth.py @@ -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) diff --git a/services/billing.py b/services/billing.py new file mode 100644 index 0000000..9ec71d2 --- /dev/null +++ b/services/billing.py @@ -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)) diff --git a/services/deal.py b/services/deal.py index 047afb3..6f1e398 100644 --- a/services/deal.py +++ b/services/deal.py @@ -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: diff --git a/services/product.py b/services/product.py index db05bdd..1c3873f 100644 --- a/services/product.py +++ b/services/product.py @@ -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()