feat: modules, products, services, services kits

This commit is contained in:
2025-09-16 10:54:10 +04:00
parent be8052848c
commit 276626d6f7
55 changed files with 1791 additions and 34 deletions

View File

@ -1,18 +1,19 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import ORJSONResponse from fastapi.responses import ORJSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.middleware.gzip import GZipMiddleware
from fastapi_endpoints import auto_include_routers
import modules
import routers import routers
from utils.auto_include_routers import auto_include_routers
origins = ["http://localhost:3000"] origins = ["http://localhost:3000"]
app = FastAPI( app = FastAPI(
separate_input_output_schemas=True, separate_input_output_schemas=True,
default_response_class=ORJSONResponse, default_response_class=ORJSONResponse,
root_path="/api" root_path="/api",
) )
app.add_middleware( app.add_middleware(
@ -28,5 +29,6 @@ app.add_middleware(
) )
auto_include_routers(app, routers) auto_include_routers(app, routers)
auto_include_routers(app, modules, True)
app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="static"), name="static")

View File

@ -1,7 +1,13 @@
from sqlalchemy.orm import configure_mappers from sqlalchemy.orm import configure_mappers
from modules.fulfillment_base.models import * # noqa: F401
from .base import BaseModel as BaseModel from .base import BaseModel as BaseModel
from .board import Board as Board from .board import Board as Board
from .built_in_module import ( # noqa: F401
BuiltInModule as BuiltInModule,
project_built_in_module as project_built_in_module,
built_in_module_dependencies as built_in_module_dependencies,
)
from .deal import Deal as Deal from .deal import Deal as Deal
from .project import Project as Project from .project import Project as Project
from .status import Status as Status, CardStatusHistory as CardStatusHistory from .status import Status as Status, CardStatusHistory as CardStatusHistory

58
models/built_in_module.py Normal file
View File

@ -0,0 +1,58 @@
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Table, Column, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
if TYPE_CHECKING:
from models import Project
project_built_in_module = Table(
"project_built_in_module",
BaseModel.metadata,
Column("project_id", ForeignKey("projects.id"), primary_key=True),
Column("module_id", ForeignKey("built_in_modules.id"), primary_key=True),
)
built_in_module_dependencies = Table(
"built_in_module_dependencies",
BaseModel.metadata,
Column("module_id", ForeignKey("built_in_modules.id"), primary_key=True),
Column("depends_on_id", ForeignKey("built_in_modules.id"), primary_key=True),
)
class BuiltInModule(BaseModel):
__tablename__ = "built_in_modules"
id: Mapped[int] = mapped_column(primary_key=True)
key: Mapped[str] = mapped_column(unique=True)
label: Mapped[str] = mapped_column()
description: Mapped[str] = mapped_column()
icon_name: Mapped[Optional[str]] = mapped_column(unique=True)
is_deleted: Mapped[bool] = mapped_column(default=False)
depends_on: Mapped[list["BuiltInModule"]] = relationship(
secondary=built_in_module_dependencies,
primaryjoin="BuiltInModule.id == built_in_module_dependencies.c.module_id",
secondaryjoin="BuiltInModule.id == built_in_module_dependencies.c.depends_on_id",
back_populates="depended_on_by",
lazy="immediate",
)
depended_on_by: Mapped[list["BuiltInModule"]] = relationship(
secondary="built_in_module_dependencies",
primaryjoin="BuiltInModule.id == built_in_module_dependencies.c.depends_on_id",
secondaryjoin="BuiltInModule.id == built_in_module_dependencies.c.module_id",
back_populates="depends_on",
lazy="noload",
)
projects: Mapped[list["Project"]] = relationship(
uselist=True,
secondary="project_built_in_module",
back_populates="built_in_modules",
lazy="noload",
)

View File

@ -1,6 +1,6 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from sqlalchemy import DateTime from sqlalchemy import DateTime, Numeric
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
@ -21,3 +21,11 @@ class CreatedAtMixin:
default=lambda: datetime.now(timezone.utc), default=lambda: datetime.now(timezone.utc),
nullable=False, nullable=False,
) )
class PriceMixin:
price: Mapped[float] = mapped_column(Numeric(12, 2), comment="Стоимость")
class CostMixin:
cost: Mapped[float] = mapped_column(Numeric(12, 2), comment="Себестоимость")

View File

@ -6,7 +6,7 @@ from models.base import BaseModel
from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
if TYPE_CHECKING: if TYPE_CHECKING:
from models import Board from models import Board, BuiltInModule
class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin): class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
@ -18,3 +18,10 @@ class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
back_populates="project", back_populates="project",
lazy="noload", lazy="noload",
) )
built_in_modules: Mapped[list["BuiltInModule"]] = relationship(
secondary="project_built_in_module",
back_populates="projects",
lazy="selectin",
order_by="asc(BuiltInModule.id)",
)

0
modules/__init__.py Normal file
View File

View File

View File

@ -0,0 +1 @@
from .service import *

View File

@ -0,0 +1,7 @@
from enum import IntEnum, unique
@unique
class ServiceType(IntEnum):
DEAL_SERVICE = 0
PRODUCT_SERVICE = 1

View File

@ -0,0 +1,6 @@
from .deal_product import DealProduct as DealProduct, DealProductService as DealProductService
from .deal_service import (
DealService as DealService,
)
from .product import Product as Product
from .service import Service as Service

View File

@ -0,0 +1,60 @@
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, ForeignKeyConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import PriceMixin
if TYPE_CHECKING:
from models import Deal, Service, Product
class DealProduct(BaseModel):
__tablename__ = "fulfillment_base_deal_products"
deal_id: Mapped[int] = mapped_column(ForeignKey("deals.id"), primary_key=True)
product_id: Mapped[int] = mapped_column(
ForeignKey("fulfillment_base_products.id"), primary_key=True
)
quantity: Mapped[int] = mapped_column(default=1)
comment: Mapped[str] = mapped_column(comment="Комментарий к товару")
deal: Mapped["Deal"] = relationship(backref="deal_products")
product: Mapped["Product"] = relationship()
product_services: Mapped[list["DealProductService"]] = relationship(
back_populates="deal_product",
primaryjoin="and_(DealProduct.deal_id==DealProductService.deal_id, DealProduct.product_id==DealProductService.product_id)",
)
class DealProductService(BaseModel, PriceMixin):
__tablename__ = "fulfillment_base_deal_products_services"
deal_id: Mapped[int] = mapped_column(primary_key=True)
product_id: Mapped[int] = mapped_column(primary_key=True)
service_id: Mapped[int] = mapped_column(
ForeignKey("fulfillment_base_services.id"), primary_key=True
)
is_fixed_price: Mapped[bool] = mapped_column(
default=False, server_default="0", comment="Фиксированная цена"
)
deal_product: Mapped["DealProduct"] = relationship(
back_populates="product_services",
primaryjoin="and_(DealProductService.deal_id==DealProduct.deal_id, DealProductService.product_id==DealProduct.product_id)",
)
service: Mapped["Service"] = relationship()
__table_args__ = (
ForeignKeyConstraint(
["deal_id", "product_id"],
[
"fulfillment_base_deal_products.deal_id",
"fulfillment_base_deal_products.product_id",
],
),
)

View File

@ -0,0 +1,28 @@
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import PriceMixin
if TYPE_CHECKING:
from models import Deal
from modules.fulfillment_base.models import Service
class DealService(BaseModel, PriceMixin):
__tablename__ = "fulfillment_base_deal_services"
deal_id: Mapped[int] = mapped_column(ForeignKey("deals.id"), primary_key=True)
deal: Mapped["Deal"] = relationship(backref="deal_services")
service_id: Mapped[int] = mapped_column(
ForeignKey("fulfillment_base_services.id"), primary_key=True
)
service: Mapped["Service"] = relationship()
quantity: Mapped[int] = mapped_column(default=1)
is_fixed_price: Mapped[bool] = mapped_column(
default=False, server_default="0", comment="Фиксированная цена"
)

View File

@ -0,0 +1,40 @@
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import IdMixin, SoftDeleteMixin
class Product(BaseModel, IdMixin, SoftDeleteMixin):
__tablename__ = "fulfillment_base_products"
name: Mapped[str] = mapped_column()
article: Mapped[str] = mapped_column(index=True)
factory_article: Mapped[str] = mapped_column(
index=True, default="", server_default=""
)
brand: Mapped[Optional[str]] = mapped_column(comment="Бренд")
color: Mapped[Optional[str]] = mapped_column(comment="Цвет")
composition: Mapped[Optional[str]] = mapped_column(comment="Состав")
size: Mapped[Optional[str]] = mapped_column(comment="Размер")
additional_info: Mapped[Optional[str]] = mapped_column(
comment="Дополнительная информация"
)
images: Mapped[list["ProductImage"]] = relationship(
"ProductImage",
back_populates="product",
lazy="selectin",
cascade="all, delete-orphan",
)
class ProductImage(BaseModel, IdMixin):
__tablename__ = "fulfillment_base_product_images"
product_id: Mapped[int] = mapped_column(ForeignKey("fulfillment_base_products.id"))
product: Mapped["Product"] = relationship(back_populates="images")
image_url: Mapped[str] = mapped_column(nullable=False)

View File

@ -0,0 +1,73 @@
from sqlalchemy import ForeignKey, Table, Column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import IdMixin, SoftDeleteMixin, CostMixin, PriceMixin
from modules.fulfillment_base import enums
services_kit_services = Table(
"fulfillment_base_services_kit_services",
BaseModel.metadata,
Column("services_kit_id", ForeignKey("fulfillment_base_services_kits.id")),
Column("service_id", ForeignKey("fulfillment_base_services.id")),
)
class Service(BaseModel, IdMixin, SoftDeleteMixin, PriceMixin, CostMixin):
__tablename__ = "fulfillment_base_services"
name: Mapped[str] = mapped_column(index=True)
price_ranges: Mapped[list["ServicePriceRange"]] = relationship(
back_populates="service",
lazy="selectin",
order_by="asc(ServicePriceRange.from_quantity)",
cascade="all, delete-orphan",
)
category_id: Mapped[int] = mapped_column(
ForeignKey("fulfillment_base_service_categories.id"),
comment="ID категории услуги",
)
category: Mapped["ServiceCategory"] = relationship("ServiceCategory", lazy="joined")
service_type: Mapped[int] = mapped_column(
server_default=f"{enums.service.ServiceType.DEAL_SERVICE}",
comment="Тип услуги",
)
lexorank: Mapped[str] = mapped_column(comment="Ранг услуги")
class ServiceCategory(BaseModel, IdMixin):
__tablename__ = "fulfillment_base_service_categories"
name: Mapped[str] = mapped_column()
is_deleted: Mapped[bool] = mapped_column(
default=False, comment="Удалена ли категория"
)
deal_service_rank: Mapped[str] = mapped_column(comment="Ранг услуги для сделки")
product_service_rank: Mapped[str] = mapped_column(comment="Ранг услуги для товара")
class ServicesKit(BaseModel, IdMixin):
__tablename__ = "fulfillment_base_services_kits"
name: Mapped[str] = mapped_column()
service_type: Mapped[int] = mapped_column(
server_default=f"{enums.ServiceType.DEAL_SERVICE}",
comment="Тип услуги",
)
services: Mapped[list["Service"]] = relationship(
secondary=services_kit_services, lazy="selectin"
)
class ServicePriceRange(BaseModel, IdMixin, PriceMixin):
__tablename__ = "fulfillment_base_service_price_ranges"
service_id: Mapped[int] = mapped_column(ForeignKey("fulfillment_base_services.id"))
service: Mapped[Service] = relationship(back_populates="price_ranges")
from_quantity: Mapped[int] = mapped_column(comment="От количества")
to_quantity: Mapped[int] = mapped_column(comment="До количества")

View File

@ -0,0 +1,6 @@
from .deal_product import DealProductRepository as DealProductRepository
from .product_service import ProductServiceRepository as ProductServiceRepository
from .deal_service import DealServiceRepository as DealServiceRepository
from .product import ProductRepository as ProductRepository
from .service import ServiceRepository as ServiceRepository
from .services_kit import ServicesKitRepository as ServicesKitRepository

View File

@ -0,0 +1,71 @@
from typing import Optional
from sqlalchemy import Select, select
from sqlalchemy.orm import joinedload, selectinload
from modules.fulfillment_base.models import DealProductService
from modules.fulfillment_base.models.deal_product import DealProduct
from modules.fulfillment_base.models.service import ServicesKit
from modules.fulfillment_base.schemas.deal_product import (
UpdateDealProductSchema,
CreateDealProductSchema,
)
from repositories.base import BaseRepository
from repositories.mixins import RepGetAllMixin, RepUpdateMixin
class DealProductRepository(
BaseRepository,
RepGetAllMixin[DealProduct],
RepUpdateMixin[DealProduct, UpdateDealProductSchema],
):
entity_class = DealProduct
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
deal_id = args[0]
return (
stmt.options(
joinedload(DealProduct.product),
selectinload(DealProduct.product_services).joinedload(
DealProductService.service
),
)
.where(DealProduct.deal_id == deal_id)
.order_by(DealProduct.product_id)
)
async def get_by_id(self, deal_id: int, product_id: int) -> Optional[DealProduct]:
stmt = (
select(DealProduct)
.options(
joinedload(DealProduct.product),
selectinload(DealProduct.product_services).joinedload(
DealProductService.service
),
)
.where(DealProduct.deal_id == deal_id, DealProduct.product_id == product_id)
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def create(self, data: CreateDealProductSchema):
deal_product = DealProduct(**data.model_dump())
self.session.add(deal_product)
await self.session.commit()
async def delete(self, obj: DealProduct):
await self.session.delete(obj)
await self.session.commit()
async def add_services_kit(
self, deal_product: DealProduct, services_kit: ServicesKit
):
for service in services_kit.services:
deal_product_service = DealProductService(
deal_id=deal_product.deal_id,
product_id=deal_product.product_id,
service_id=service.id,
price=service.price,
)
self.session.add(deal_product_service)
await self.session.commit()

View File

@ -0,0 +1,48 @@
from typing import Optional
from sqlalchemy import Select, select
from sqlalchemy.orm import joinedload
from modules.fulfillment_base.models import DealService
from modules.fulfillment_base.schemas.deal_service import (
UpdateDealServiceSchema,
CreateDealServiceSchema,
)
from repositories.base import BaseRepository
from repositories.mixins import RepGetAllMixin, RepUpdateMixin
class DealServiceRepository(
BaseRepository,
RepGetAllMixin[DealService],
RepUpdateMixin[DealService, UpdateDealServiceSchema],
):
entity_class = DealService
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
deal_id = args[0]
return (
stmt.options(
joinedload(DealService.service),
)
.where(DealService.deal_id == deal_id)
.order_by(DealService.service_id)
)
async def get_by_id(self, deal_id: int, service_id: int) -> Optional[DealService]:
stmt = (
select(DealService)
.options(joinedload(DealService.service))
.where(DealService.deal_id == deal_id, DealService.service_id == service_id)
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def create(self, data: CreateDealServiceSchema):
deal_service = DealService(**data.model_dump())
self.session.add(deal_service)
await self.session.commit()
async def delete(self, obj: DealService):
await self.session.delete(obj)
await self.session.commit()

View File

@ -0,0 +1,33 @@
from modules.fulfillment_base.models import Product
from modules.fulfillment_base.schemas.product import (
CreateProductSchema,
UpdateProductSchema,
)
from repositories.mixins import *
from schemas.base import PaginationSchema
class ProductRepository(
BaseRepository,
RepGetAllMixin[Product],
RepDeleteMixin[Product],
RepCreateMixin[Product, CreateProductSchema],
RepUpdateMixin[Product, UpdateProductSchema],
RepGetByIdMixin[Product],
):
entity_class = Product
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
search_input = args[0]
pagination: PaginationSchema = args[1]
if search_input:
stmt = stmt.where(Product.name.ilike(f"%{search_input}%"))
if pagination.items_per_page and pagination.page:
stmt = self._apply_pagination(
stmt, pagination.page, pagination.items_per_page
)
return stmt
async def update(self, product: Product, data: UpdateProductSchema) -> Product:
return await self._apply_update_data_to_model(product, data, True)

View File

@ -0,0 +1,84 @@
from typing import Optional
from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from modules.fulfillment_base.models import DealProductService
from modules.fulfillment_base.schemas.product_service import *
from repositories.base import BaseRepository
from repositories.mixins import RepUpdateMixin
class ProductServiceRepository(
BaseRepository,
RepUpdateMixin[DealProductService, UpdateProductServiceSchema],
):
entity_class = DealProductService
session: AsyncSession
async def get_by_id(
self, deal_id: int, product_id: int, service_id: int
) -> Optional[DealProductService]:
stmt = (
select(DealProductService)
.options(
joinedload(DealProductService.service),
)
.where(
DealProductService.deal_id == deal_id,
DealProductService.product_id == product_id,
DealProductService.service_id == service_id,
)
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def create(self, data: CreateProductServiceSchema):
deal_product_service = DealProductService(**data.model_dump())
self.session.add(deal_product_service)
await self.session.commit()
async def delete(self, obj: DealProductService):
await self.session.delete(obj)
await self.session.commit()
async def get_product_services(
self, deal_id: int, product_id: int
) -> list[DealProductService]:
stmt = (
select(DealProductService)
.options(
joinedload(DealProductService.service),
)
.where(
DealProductService.deal_id == deal_id,
DealProductService.product_id == product_id,
)
)
return list(await self.session.scalars(stmt))
async def delete_product_services(self, deal_id: int, product_ids: list[int]):
stmt = delete(DealProductService).where(
DealProductService.deal_id == deal_id,
DealProductService.product_id.in_(product_ids),
)
await self.session.execute(stmt)
await self.session.flush()
async def duplicate_services(
self, deal_id: int, product_ids: list[int], services: list[DealProductService]
):
await self.delete_product_services(deal_id, product_ids)
for product_id in product_ids:
for prod_service in services:
product_service = DealProductService(
deal_id=deal_id,
product_id=product_id,
service_id=prod_service.service.id,
price=prod_service.price,
is_fixed_price=prod_service.is_fixed_price,
)
self.session.add(product_service)
await self.session.commit()

View File

@ -0,0 +1,20 @@
from modules.fulfillment_base.models import Service
from modules.fulfillment_base.schemas.service import (
CreateServiceSchema,
UpdateServiceSchema,
)
from repositories.mixins import *
class ServiceRepository(
BaseRepository,
RepGetAllMixin[Service],
RepDeleteMixin[Service],
RepCreateMixin[Service, CreateServiceSchema],
RepUpdateMixin[Service, UpdateServiceSchema],
RepGetByIdMixin[Service],
):
entity_class = Service
async def update(self, service: Service, data: UpdateServiceSchema) -> Service:
return await self._apply_update_data_to_model(service, data, True)

View File

@ -0,0 +1,22 @@
from sqlalchemy.orm import selectinload
from modules.fulfillment_base.models.service import ServicesKit
from modules.fulfillment_base.schemas.services_kit import (
CreateServicesKitSchema,
UpdateServicesKitSchema,
)
from repositories.mixins import *
class ServicesKitRepository(
RepCrudMixin[ServicesKit, CreateServicesKitSchema, UpdateServicesKitSchema],
):
entity_class = ServicesKit
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt.options(selectinload(ServicesKit.services))
async def update(
self, service: ServicesKit, data: UpdateServicesKitSchema
) -> ServicesKit:
return await self._apply_update_data_to_model(service, data, True)

View File

@ -0,0 +1,137 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.deal_product import *
from modules.fulfillment_base.schemas.product_service import *
from modules.fulfillment_base.services import DealProductService, ProductServiceService
router = APIRouter(tags=["deal-product"])
# region DealProduct
@router.get(
"/{dealId}",
response_model=GetDealProductsResponse,
operation_id="get_deal_products",
)
async def get_deal_products(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
):
return await DealProductService(session).get_all(deal_id)
@router.post(
"/",
response_model=CreateDealProductResponse,
operation_id="create_deal_product",
)
async def create_deal_product(
session: SessionDependency,
request: CreateDealProductRequest,
):
return await DealProductService(session).create(request)
@router.patch(
"/{dealId}/product/{productId}",
response_model=UpdateDealProductResponse,
operation_id="update_deal_product",
)
async def update_deal_product(
session: SessionDependency,
request: UpdateDealProductRequest,
deal_id: int = Path(alias="dealId"),
product_id: int = Path(alias="productId"),
):
return await DealProductService(session).update(deal_id, product_id, request)
@router.delete(
"/{dealId}/product/{productId}",
response_model=DeleteDealProductResponse,
operation_id="delete_deal_product",
)
async def delete_deal_product(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
product_id: int = Path(alias="productId"),
):
return await DealProductService(session).delete(deal_id, product_id)
@router.post(
"/add-services-kit",
response_model=DealProductAddKitResponse,
operation_id="add_kit_to_deal_product",
)
async def add_kit_to_deal_product(
session: SessionDependency,
request: DealProductAddKitRequest,
):
return await DealProductService(session).add_services_kit(request)
# endregion
# region DealProductService
@router.post(
"/service",
response_model=CreateProductServiceResponse,
operation_id="create_deal_product_service",
)
async def create_deal_product_service(
session: SessionDependency,
request: CreateProductServiceRequest,
):
return await ProductServiceService(session).create(request)
@router.patch(
"/{dealId}/product/{productId}/service/{serviceId}",
response_model=UpdateProductServiceResponse,
operation_id="update_deal_product_service",
)
async def update_deal_product_service(
session: SessionDependency,
request: UpdateProductServiceRequest,
deal_id: int = Path(alias="dealId"),
product_id: int = Path(alias="productId"),
service_id: int = Path(alias="serviceId"),
):
return await ProductServiceService(session).update(
deal_id, product_id, service_id, request
)
@router.delete(
"/{dealId}/product/{productId}/service/{serviceId}",
response_model=DeleteProductServiceResponse,
operation_id="delete_deal_product_service",
)
async def delete_deal_product_service(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
product_id: int = Path(alias="productId"),
service_id: int = Path(alias="serviceId"),
):
return await ProductServiceService(session).delete(deal_id, product_id, service_id)
@router.post(
"/services/duplicate",
response_model=ProductServicesDuplicateResponse,
operation_id="duplicate_product_services",
)
async def copy_product_services(
session: SessionDependency,
request: ProductServicesDuplicateRequest,
):
return await ProductServiceService(session).duplicate_product_services(request)
# endregion

View File

@ -0,0 +1,58 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.deal_service import *
from modules.fulfillment_base.services import DealServiceService
router = APIRouter(tags=["deal-service"])
@router.get(
"/{dealId}",
response_model=GetDealServicesResponse,
operation_id="get_deal_services",
)
async def get_deal_services(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
):
return await DealServiceService(session).get_all(deal_id)
@router.post(
"/",
response_model=CreateDealServiceResponse,
operation_id="create_deal_service",
)
async def create_deal_service(
session: SessionDependency,
request: CreateDealServiceRequest,
):
return await DealServiceService(session).create(request)
@router.patch(
"/{dealId}/service/{serviceId}",
response_model=UpdateDealServiceResponse,
operation_id="update_deal_service",
)
async def update_deal_service(
session: SessionDependency,
request: UpdateDealServiceRequest,
deal_id: int = Path(alias="dealId"),
service_id: int = Path(alias="serviceId"),
):
return await DealServiceService(session).update(deal_id, service_id, request)
@router.delete(
"/{dealId}/service/{serviceId}",
response_model=DeleteDealServiceResponse,
operation_id="delete_deal_service",
)
async def delete_deal_service(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
service_id: int = Path(alias="serviceId"),
):
return await DealServiceService(session).delete(deal_id, service_id)

View File

@ -0,0 +1,57 @@
from fastapi import APIRouter, Path, Query
from backend.dependecies import SessionDependency, PaginationDependency
from modules.fulfillment_base.schemas.product import *
from modules.fulfillment_base.services import ProductService
router = APIRouter(tags=["product"])
@router.get(
"/",
response_model=GetProductsResponse,
operation_id="get_products",
)
async def get_products(
session: SessionDependency,
pagination: PaginationDependency,
search_input: Optional[str] = Query(alias="searchInput", default=None),
):
return await ProductService(session).get_all(search_input, pagination)
@router.post(
"/",
response_model=CreateProductResponse,
operation_id="create_product",
)
async def create_product(
session: SessionDependency,
request: CreateProductRequest,
):
return await ProductService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateProductResponse,
operation_id="update_product",
)
async def update_product(
session: SessionDependency,
request: UpdateProductRequest,
pk: int = Path(),
):
return await ProductService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteProductResponse,
operation_id="delete_product",
)
async def delete_product(
session: SessionDependency,
pk: int = Path(),
):
return await ProductService(session).delete(pk)

View File

@ -0,0 +1,55 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.service import *
from modules.fulfillment_base.services import ServiceModelService
router = APIRouter(tags=["service"])
@router.get(
"/",
response_model=GetServicesResponse,
operation_id="get_services",
)
async def get_services(
session: SessionDependency,
):
return await ServiceModelService(session).get_all()
@router.post(
"/",
response_model=CreateServiceResponse,
operation_id="create_service",
)
async def create_service(
session: SessionDependency,
request: CreateServiceRequest,
):
return await ServiceModelService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateServiceResponse,
operation_id="update_service",
)
async def update_service(
session: SessionDependency,
request: UpdateServiceRequest,
pk: int = Path(),
):
return await ServiceModelService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteServiceResponse,
operation_id="delete_service",
)
async def delete_service(
session: SessionDependency,
pk: int = Path(),
):
return await ServiceModelService(session).delete(pk)

View File

@ -0,0 +1,55 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.services_kit import *
from modules.fulfillment_base.services import ServicesKitService
router = APIRouter(tags=["services-kit"])
@router.get(
"/",
response_model=GetServicesKitResponse,
operation_id="get_services_kits",
)
async def get_services_kits(
session: SessionDependency,
):
return await ServicesKitService(session).get_all()
@router.post(
"/",
response_model=CreateServicesKitResponse,
operation_id="create_services_kit",
)
async def create_services_kit(
session: SessionDependency,
request: CreateServicesKitRequest,
):
return await ServicesKitService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateServicesKitResponse,
operation_id="update_services_kit",
)
async def update_services_kit(
session: SessionDependency,
request: UpdateServicesKitRequest,
pk: int = Path(),
):
return await ServicesKitService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteServicesKitResponse,
operation_id="delete_services_kit",
)
async def delete_services_kit(
session: SessionDependency,
pk: int = Path(),
):
return await ServicesKitService(session).delete(pk)

View File

@ -0,0 +1,74 @@
from modules.fulfillment_base.schemas.product import ProductSchema
from modules.fulfillment_base.schemas.product_service import ProductServiceSchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
class DealProductSchema(BaseSchema):
deal_id: int
product_id: int
product: ProductSchema
quantity: int
comment: str
product_services: list[ProductServiceSchema]
class CreateDealProductSchema(BaseSchema):
deal_id: int
product_id: int
quantity: int
comment: str
class UpdateDealProductSchema(BaseSchema):
quantity: int
comment: str
# endregion
# region Request
class CreateDealProductRequest(BaseSchema):
entity: CreateDealProductSchema
class UpdateDealProductRequest(BaseSchema):
entity: UpdateDealProductSchema
class DealProductAddKitRequest(BaseSchema):
deal_id: int
product_id: int
kit_id: int
# endregion
# region Response
class GetDealProductsResponse(BaseSchema):
items: list[DealProductSchema]
class CreateDealProductResponse(BaseResponse):
entity: DealProductSchema
class UpdateDealProductResponse(BaseResponse):
pass
class DeleteDealProductResponse(BaseResponse):
pass
class DealProductAddKitResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,64 @@
from modules.fulfillment_base.schemas.service import ServiceSchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
class DealServiceSchema(BaseSchema):
deal_id: int
service_id: int
service: ServiceSchema
quantity: int
price: float
is_fixed_price: bool
class CreateDealServiceSchema(BaseSchema):
deal_id: int
service_id: int
quantity: int
price: float
class UpdateDealServiceSchema(BaseSchema):
quantity: int
price: float
is_fixed_price: bool
# endregion
# region Request
class CreateDealServiceRequest(BaseSchema):
entity: CreateDealServiceSchema
class UpdateDealServiceRequest(BaseSchema):
entity: UpdateDealServiceSchema
# endregion
# region Response
class GetDealServicesResponse(BaseSchema):
items: list[DealServiceSchema]
class CreateDealServiceResponse(BaseResponse):
entity: DealServiceSchema
class UpdateDealServiceResponse(BaseResponse):
pass
class DeleteDealServiceResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,77 @@
from typing import Optional
from schemas.base import BaseSchema, BaseResponse
# region Entity
class ProductImageSchema(BaseSchema):
id: int
product_id: int
image_url: str
class CreateProductSchema(BaseSchema):
name: str
article: str
factory_article: str
brand: Optional[str]
color: Optional[str]
composition: Optional[str]
size: Optional[str]
additional_info: Optional[str]
class ProductSchema(CreateProductSchema):
id: int
class UpdateProductSchema(BaseSchema):
name: Optional[str] = None
article: Optional[str] = None
factory_article: Optional[str] = None
brand: Optional[str] = None
color: Optional[str] = None
composition: Optional[str] = None
size: Optional[str] = None
additional_info: Optional[str] = None
images: list[ProductImageSchema] | None = []
# endregion
# region Request
class CreateProductRequest(BaseSchema):
entity: CreateProductSchema
class UpdateProductRequest(BaseSchema):
entity: UpdateProductSchema
# endregion
# region Response
class GetProductsResponse(BaseSchema):
items: list[ProductSchema]
class CreateProductResponse(BaseResponse):
entity: ProductSchema
class UpdateProductResponse(BaseResponse):
pass
class DeleteProductResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,69 @@
from modules.fulfillment_base.schemas.service import ServiceSchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
class ProductServiceSchema(BaseSchema):
deal_id: int
product_id: int
service_id: int
service: ServiceSchema
price: float
is_fixed_price: bool
class CreateProductServiceSchema(BaseSchema):
deal_id: int
product_id: int
service_id: int
price: float
class UpdateProductServiceSchema(BaseSchema):
price: float
is_fixed_price: bool
# endregion
# region Request
class CreateProductServiceRequest(BaseSchema):
entity: CreateProductServiceSchema
class UpdateProductServiceRequest(BaseSchema):
entity: UpdateProductServiceSchema
class ProductServicesDuplicateRequest(BaseSchema):
deal_id: int
source_deal_product_id: int
target_deal_product_ids: list[int]
# endregion
# region Response
class CreateProductServiceResponse(BaseResponse):
entity: ProductServiceSchema
class UpdateProductServiceResponse(BaseResponse):
pass
class DeleteProductServiceResponse(BaseResponse):
pass
class ProductServicesDuplicateResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,76 @@
from typing import Optional
from schemas.base import BaseSchema, BaseResponse
# region Entity
class ServicePriceRangeSchema(BaseSchema):
id: int | None
from_quantity: int
to_quantity: int
price: float
class ServiceCategorySchema(BaseSchema):
id: int
name: str
deal_service_rank: str
product_service_rank: str
class ServiceSchema(BaseSchema):
id: int
name: str
category: ServiceCategorySchema
price: float
service_type: int
price_ranges: list[ServicePriceRangeSchema]
cost: Optional[float]
lexorank: str
class UpdateServiceSchema(ServiceSchema):
pass
class CreateServiceSchema(ServiceSchema):
pass
# endregion
# region Request
class CreateServiceRequest(BaseSchema):
entity: CreateServiceSchema
class UpdateServiceRequest(BaseSchema):
entity: UpdateServiceSchema
# endregion
# region Response
class GetServicesResponse(BaseSchema):
items: list[ServiceSchema]
class CreateServiceResponse(BaseResponse):
entity: ServiceSchema
class UpdateServiceResponse(BaseResponse):
pass
class DeleteServiceResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,60 @@
from modules.fulfillment_base.schemas.service import ServiceSchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
class BaseServicesKitSchema(BaseSchema):
name: str
service_type: int
class ServicesKitSchema(BaseServicesKitSchema):
id: int
services: list[ServiceSchema]
class CreateServicesKitSchema(BaseServicesKitSchema):
services_ids: list[int]
class UpdateServicesKitSchema(BaseServicesKitSchema):
services_ids: list[int]
# endregion
# region Request
class CreateServicesKitRequest(BaseSchema):
entity: CreateServicesKitSchema
class UpdateServicesKitRequest(BaseSchema):
entity: UpdateServicesKitSchema
# endregion
# region Response
class GetServicesKitResponse(BaseSchema):
items: list[ServicesKitSchema]
class CreateServicesKitResponse(BaseResponse):
entity: ServicesKitSchema
class UpdateServicesKitResponse(BaseResponse):
pass
class DeleteServicesKitResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,6 @@
from .deal_product import DealProductService as DealProductService
from .deal_service import DealServiceService as DealServiceService
from .product import ProductService as ProductService
from .product_service import ProductServiceService as ProductServiceService
from .service import ServiceModelService as ServiceModelService
from .services_kit import ServicesKitService as ServicesKitService

View File

@ -0,0 +1,72 @@
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from modules.fulfillment_base.models import DealProduct
from modules.fulfillment_base.repositories import (
DealProductRepository,
ServicesKitRepository,
ProductServiceRepository,
)
from modules.fulfillment_base.schemas.deal_product import *
from services.mixins import ServiceGetAllMixin
class DealProductService(ServiceGetAllMixin[DealProduct, DealProductSchema]):
schema_class = DealProductSchema
entity_not_found_msg = "Связь товара со сделкой не найдена"
def __init__(self, session: AsyncSession):
self.repository = DealProductRepository(session)
async def create(
self, request: CreateDealProductRequest
) -> CreateDealProductResponse:
await self.repository.create(request.entity)
deal_product = await self.repository.get_by_id(
request.entity.deal_id, request.entity.product_id
)
return CreateDealProductResponse(
entity=DealProductSchema.model_validate(deal_product),
message="Товар добавлен в сделку",
)
async def update(
self, deal_id: int, product_id: int, data: UpdateDealProductRequest
) -> UpdateDealProductResponse:
entity = await self.repository.get_by_id(deal_id, product_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.update(entity, data.entity)
return UpdateDealProductResponse(message="Товар сделки обновлен")
async def delete(self, deal_id: int, product_id: int) -> DeleteDealProductResponse:
entity = await self.repository.get_by_id(deal_id, product_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.delete(entity)
return DeleteDealProductResponse(message="Товар удален из сделки")
async def add_services_kit(
self, request: DealProductAddKitRequest
) -> DealProductAddKitResponse:
services_kit_repo = ServicesKitRepository(self.repository.session)
services_kit = await services_kit_repo.get_by_id(request.kit_id)
if not services_kit:
raise HTTPException(status_code=404, detail="Набор услуг не найден")
deal_product = await self.repository.get_by_id(
request.deal_id, request.product_id
)
if not deal_product:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
product_service_repo = ProductServiceRepository(self.repository.session)
await product_service_repo.delete_product_services(
request.deal_id, [request.product_id]
)
await self.repository.add_services_kit(deal_product, services_kit)
return DealProductAddKitResponse(message="Комплект добавлен в товар")

View File

@ -0,0 +1,45 @@
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from modules.fulfillment_base.models import DealService
from modules.fulfillment_base.repositories import DealServiceRepository
from modules.fulfillment_base.schemas.deal_service import *
from services.mixins import ServiceGetAllMixin
class DealServiceService(ServiceGetAllMixin[DealService, DealServiceSchema]):
schema_class = DealServiceSchema
entity_not_found_msg = "Связь услуги со сделкой не найдена"
def __init__(self, session: AsyncSession):
self.repository = DealServiceRepository(session)
async def create(
self, request: CreateDealServiceRequest
) -> CreateDealServiceResponse:
await self.repository.create(request.entity)
deal_service = await self.repository.get_by_id(
request.entity.deal_id, request.entity.service_id
)
return CreateDealServiceResponse(
entity=DealServiceSchema.model_validate(deal_service),
message="Услуга добавлена в сделку",
)
async def update(
self, deal_id: int, service_id: int, data: UpdateDealServiceRequest
) -> UpdateDealServiceResponse:
entity = await self.repository.get_by_id(deal_id, service_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.update(entity, data.entity)
return UpdateDealServiceResponse(message="Услуга сделки обновлена")
async def delete(self, deal_id: int, service_id: int) -> DeleteDealServiceResponse:
entity = await self.repository.get_by_id(deal_id, service_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.delete(entity)
return DeleteDealServiceResponse(message="Услуга удалена из сделки")

View File

@ -0,0 +1,27 @@
from modules.fulfillment_base.models import Product
from modules.fulfillment_base.repositories import ProductRepository
from modules.fulfillment_base.schemas.product import (
CreateProductRequest,
ProductSchema,
UpdateProductRequest,
)
from services.mixins import *
class ProductService(
ServiceGetAllMixin[Product, ProductSchema],
ServiceCreateMixin[Product, CreateProductRequest, ProductSchema],
ServiceUpdateMixin[Product, UpdateProductRequest],
ServiceDeleteMixin[Product],
):
schema_class = ProductSchema
entity_not_found_msg = "Товар не найден"
entity_deleted_msg = "Товар успешно удален"
entity_updated_msg = "Товар успешно обновлен"
entity_created_msg = "Товар успешно создан"
def __init__(self, session: AsyncSession):
self.repository = ProductRepository(session)
async def is_soft_delete(self, product: ProductSchema) -> bool:
return True

View File

@ -0,0 +1,66 @@
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from modules.fulfillment_base.models import DealProductService
from modules.fulfillment_base.repositories import ProductServiceRepository
from modules.fulfillment_base.schemas.product_service import *
class ProductServiceService:
schema_class = ProductServiceSchema
entity_not_found_msg = "Связь услуги с товаром не найдена"
def __init__(self, session: AsyncSession):
self.repository = ProductServiceRepository(session)
async def create(
self, request: CreateProductServiceRequest
) -> CreateProductServiceResponse:
await self.repository.create(request.entity)
deal_product = await self.repository.get_by_id(
request.entity.deal_id,
request.entity.product_id,
request.entity.service_id,
)
return CreateProductServiceResponse(
entity=ProductServiceSchema.model_validate(deal_product),
message="Услуга добавлена к товару",
)
async def update(
self,
deal_id: int,
product_id: int,
service_id: int,
data: UpdateProductServiceRequest,
) -> UpdateProductServiceResponse:
entity = await self.repository.get_by_id(deal_id, product_id, service_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.update(entity, data.entity)
return UpdateProductServiceResponse(message="Услуга обновлена")
async def delete(
self, deal_id: int, product_id: int, service_id: int
) -> DeleteProductServiceResponse:
entity = await self.repository.get_by_id(deal_id, product_id, service_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.delete(entity)
return DeleteProductServiceResponse(message="Товар удален из сделки")
async def duplicate_product_services(
self, request: ProductServicesDuplicateRequest
) -> ProductServicesDuplicateResponse:
services_to_copy: list[
DealProductService
] = await self.repository.get_product_services(
request.deal_id, request.source_deal_product_id
)
await self.repository.duplicate_services(
request.deal_id, request.target_deal_product_ids, services_to_copy
)
return ProductServicesDuplicateResponse(message="Услуги продублированы")

View File

@ -0,0 +1,27 @@
from modules.fulfillment_base.models import Service
from modules.fulfillment_base.repositories import ServiceRepository
from modules.fulfillment_base.schemas.service import (
ServiceSchema,
CreateServiceRequest,
UpdateServiceRequest,
)
from services.mixins import *
class ServiceModelService(
ServiceGetAllMixin[Service, ServiceSchema],
ServiceCreateMixin[Service, CreateServiceRequest, ServiceSchema],
ServiceUpdateMixin[Service, UpdateServiceRequest],
ServiceDeleteMixin[Service],
):
schema_class = ServiceSchema
entity_not_found_msg = "Услуга не найдена"
entity_deleted_msg = "Услуга успешно удалена"
entity_updated_msg = "Услуга успешно обновлена"
entity_created_msg = "Услуга успешно создана"
def __init__(self, session: AsyncSession):
self.repository = ServiceRepository(session)
async def is_soft_delete(self, service: ServiceSchema) -> bool:
return True

View File

@ -0,0 +1,24 @@
from modules.fulfillment_base.models.service import ServicesKit
from modules.fulfillment_base.repositories import ServicesKitRepository
from modules.fulfillment_base.schemas.services_kit import (
ServicesKitSchema,
CreateServicesKitRequest,
UpdateServicesKitRequest,
)
from services.mixins import *
class ServicesKitService(
ServiceCrudMixin[ServicesKit, ServicesKitSchema, CreateServicesKitRequest, UpdateServicesKitRequest]
):
schema_class = ServicesKitSchema
entity_not_found_msg = "Набор услуг не найден"
entity_deleted_msg = "Набор услуг успешно удален"
entity_updated_msg = "Набор услуг успешно обновлен"
entity_created_msg = "Набор услуг успешно создан"
def __init__(self, session: AsyncSession):
self.repository = ServicesKitRepository(session)
async def is_soft_delete(self, service: ServicesKitSchema) -> bool:
return False

View File

@ -2,3 +2,4 @@ from .board import BoardRepository as BoardRepository
from .deal import DealRepository as DealRepository from .deal import DealRepository as DealRepository
from .project import ProjectRepository as ProjectRepository from .project import ProjectRepository as ProjectRepository
from .status import StatusRepository as StatusRepository from .status import StatusRepository as StatusRepository
from .built_in_module import BuiltInModuleRepository as BuiltInModuleRepository

View File

@ -3,6 +3,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
class BaseRepository: class BaseRepository:
session: AsyncSession
def __init__(self, session: AsyncSession): def __init__(self, session: AsyncSession):
self.session = session self.session = session

View File

@ -0,0 +1,18 @@
from models import Board, BuiltInModule
from repositories.mixins import *
class BuiltInModuleRepository(
BaseRepository,
RepGetAllMixin[BuiltInModule],
):
entity_class = BuiltInModule
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
project_id = args[0]
return stmt.where(Board.project_id == project_id).order_by(Board.lexorank)
async def get_by_ids(self, ids: list[int]) -> list[BuiltInModule]:
stmt = select(BuiltInModule).where(BuiltInModule.id.in_(ids))
built_in_modules = await self.session.scalars(stmt)
return built_in_modules.all()

View File

@ -65,7 +65,7 @@ class RepUpdateMixin(Generic[EntityType, UpdateSchemaType], RepBaseMixin[EntityT
return model return model
async def update(self, entity: EntityType, data: UpdateSchemaType) -> EntityType: async def update(self, entity: EntityType, data: UpdateSchemaType) -> EntityType:
pass return await self._apply_update_data_to_model(entity, data, True)
class RepGetByIdMixin(Generic[EntityType], RepBaseMixin[EntityType]): class RepGetByIdMixin(Generic[EntityType], RepBaseMixin[EntityType]):

View File

@ -1,6 +1,7 @@
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from models.project import Project from models.project import Project
from repositories.built_in_module import BuiltInModuleRepository
from repositories.mixins import * from repositories.mixins import *
from schemas.project import CreateProjectSchema, UpdateProjectSchema from schemas.project import CreateProjectSchema, UpdateProjectSchema
@ -17,4 +18,10 @@ class ProjectRepository(
return stmt.options(selectinload(Project.boards)) return stmt.options(selectinload(Project.boards))
async def update(self, project: Project, data: UpdateProjectSchema) -> Project: async def update(self, project: Project, data: UpdateProjectSchema) -> Project:
if data.built_in_modules is not None:
built_in_modules = data.built_in_modules
data.built_in_modules = await BuiltInModuleRepository(self.session).get_by_ids(
[module.id for module in built_in_modules]
)
return await self._apply_update_data_to_model(project, data, True) return await self._apply_update_data_to_model(project, data, True)

View File

@ -4,12 +4,10 @@ from backend.dependecies import SessionDependency
from schemas.board import * from schemas.board import *
from services import BoardService from services import BoardService
board_router = APIRouter( router = APIRouter(tags=["board"])
tags=["board"],
)
@board_router.get( @router.get(
"/{projectId}", "/{projectId}",
response_model=GetBoardsResponse, response_model=GetBoardsResponse,
operation_id="get_boards", operation_id="get_boards",
@ -21,7 +19,7 @@ async def get_boards(
return await BoardService(session).get_all(project_id) return await BoardService(session).get_all(project_id)
@board_router.post( @router.post(
"/", "/",
response_model=CreateBoardResponse, response_model=CreateBoardResponse,
operation_id="create_board", operation_id="create_board",
@ -33,7 +31,7 @@ async def create_board(
return await BoardService(session).create(request) return await BoardService(session).create(request)
@board_router.patch( @router.patch(
"/{pk}", "/{pk}",
response_model=UpdateBoardResponse, response_model=UpdateBoardResponse,
operation_id="update_board", operation_id="update_board",
@ -46,7 +44,7 @@ async def update_board(
return await BoardService(session).update(pk, request) return await BoardService(session).update(pk, request)
@board_router.delete( @router.delete(
"/{pk}", "/{pk}",
response_model=DeleteBoardResponse, response_model=DeleteBoardResponse,
operation_id="delete_board", operation_id="delete_board",

View File

@ -8,12 +8,10 @@ from backend.dependecies import (
from schemas.deal import * from schemas.deal import *
from services import DealService from services import DealService
deal_router = APIRouter( router = APIRouter(tags=["deal"])
tags=["deal"],
)
@deal_router.get( @router.get(
"/", "/",
response_model=GetDealsResponse, response_model=GetDealsResponse,
operation_id="get_deals", operation_id="get_deals",
@ -39,7 +37,7 @@ async def get_deals(
) )
@deal_router.post( @router.post(
"/", "/",
response_model=CreateDealResponse, response_model=CreateDealResponse,
operation_id="create_deal", operation_id="create_deal",
@ -51,7 +49,7 @@ async def create_deal(
return await DealService(session).create(request) return await DealService(session).create(request)
@deal_router.patch( @router.patch(
"/{pk}", "/{pk}",
response_model=UpdateDealResponse, response_model=UpdateDealResponse,
operation_id="update_deal", operation_id="update_deal",
@ -64,7 +62,7 @@ async def update_deal(
return await DealService(session).update(pk, request) return await DealService(session).update(pk, request)
@deal_router.delete( @router.delete(
"/{pk}", "/{pk}",
response_model=DeleteDealResponse, response_model=DeleteDealResponse,
operation_id="delete_deal", operation_id="delete_deal",

18
routers/module.py Normal file
View File

@ -0,0 +1,18 @@
from fastapi import APIRouter
from backend.dependecies import SessionDependency
from schemas.module import GetAllBuiltInModulesResponse
from services.built_in_module import BuiltInModuleService
router = APIRouter(tags=["modules"])
@router.get(
"/built-in/",
response_model=GetAllBuiltInModulesResponse,
operation_id="get_built_in_modules",
)
async def get_built_in_modules(
session: SessionDependency,
):
return await BuiltInModuleService(session).get_all()

View File

@ -4,12 +4,10 @@ from backend.dependecies import SessionDependency
from schemas.project import * from schemas.project import *
from services import ProjectService from services import ProjectService
project_router = APIRouter( router = APIRouter(tags=["project"])
tags=["project"],
)
@project_router.get( @router.get(
"/", "/",
response_model=GetProjectsResponse, response_model=GetProjectsResponse,
operation_id="get_projects", operation_id="get_projects",
@ -20,7 +18,7 @@ async def get_projects(
return await ProjectService(session).get_all() return await ProjectService(session).get_all()
@project_router.post( @router.post(
"/", "/",
response_model=CreateProjectResponse, response_model=CreateProjectResponse,
operation_id="create_project", operation_id="create_project",
@ -32,7 +30,7 @@ async def create_project(
return await ProjectService(session).create(request) return await ProjectService(session).create(request)
@project_router.patch( @router.patch(
"/{pk}", "/{pk}",
response_model=UpdateProjectResponse, response_model=UpdateProjectResponse,
operation_id="update_project", operation_id="update_project",
@ -45,7 +43,7 @@ async def update_project(
return await ProjectService(session).update(pk, request) return await ProjectService(session).update(pk, request)
@project_router.delete( @router.delete(
"/{pk}", "/{pk}",
response_model=DeleteProjectResponse, response_model=DeleteProjectResponse,
operation_id="delete_project", operation_id="delete_project",

View File

@ -4,12 +4,10 @@ from backend.dependecies import SessionDependency
from schemas.status import * from schemas.status import *
from services import StatusService from services import StatusService
status_router = APIRouter( router = APIRouter(tags=["status"])
tags=["status"],
)
@status_router.get( @router.get(
"/{boardId}", "/{boardId}",
response_model=GetStatusesResponse, response_model=GetStatusesResponse,
operation_id="get_statuses", operation_id="get_statuses",
@ -21,7 +19,7 @@ async def get_statuses(
return await StatusService(session).get_all(board_id) return await StatusService(session).get_all(board_id)
@status_router.post( @router.post(
"/", "/",
response_model=CreateStatusResponse, response_model=CreateStatusResponse,
operation_id="create_status", operation_id="create_status",
@ -33,7 +31,7 @@ async def create_status(
return await StatusService(session).create(request) return await StatusService(session).create(request)
@status_router.patch( @router.patch(
"/{pk}", "/{pk}",
response_model=UpdateStatusResponse, response_model=UpdateStatusResponse,
operation_id="update_status", operation_id="update_status",
@ -46,7 +44,7 @@ async def update_status(
return await StatusService(session).update(pk, request) return await StatusService(session).update(pk, request)
@status_router.delete( @router.delete(
"/{pk}", "/{pk}",
response_model=DeleteStatusResponse, response_model=DeleteStatusResponse,
operation_id="delete_status", operation_id="delete_status",

23
schemas/module.py Normal file
View File

@ -0,0 +1,23 @@
from schemas.base import BaseSchema
# region Entity
class BuiltInModuleSchema(BaseSchema):
id: int
key: str
label: str
icon_name: str
description: str
# endregion
# region Response
class GetAllBuiltInModulesResponse(BaseSchema):
items: list[BuiltInModuleSchema]
# endregion

View File

@ -1,6 +1,7 @@
from typing import Optional from typing import Optional
from schemas.base import BaseSchema, BaseResponse from schemas.base import BaseSchema, BaseResponse
from schemas.module import BuiltInModuleSchema
# region Entity # region Entity
@ -9,6 +10,7 @@ from schemas.base import BaseSchema, BaseResponse
class ProjectSchema(BaseSchema): class ProjectSchema(BaseSchema):
id: int id: int
name: str name: str
built_in_modules: list[BuiltInModuleSchema]
class CreateProjectSchema(BaseSchema): class CreateProjectSchema(BaseSchema):
@ -17,6 +19,7 @@ class CreateProjectSchema(BaseSchema):
class UpdateProjectSchema(BaseSchema): class UpdateProjectSchema(BaseSchema):
name: Optional[str] = None name: Optional[str] = None
built_in_modules: list[BuiltInModuleSchema] = None
# endregion # endregion

View File

@ -0,0 +1,11 @@
from models import BuiltInModule
from repositories import BuiltInModuleRepository
from schemas.module import BuiltInModuleSchema
from services.mixins import *
class BuiltInModuleService(ServiceGetAllMixin[BuiltInModule, BuiltInModuleSchema]):
schema_class = BuiltInModuleSchema
def __init__(self, session: AsyncSession):
self.repository = BuiltInModuleRepository(session)

View File

@ -0,0 +1,51 @@
import importlib
import pkgutil
from pathlib import Path
from types import ModuleType
from fastapi import FastAPI, APIRouter
def auto_include_routers(app: FastAPI, package: ModuleType, full_path: bool = False):
"""
Automatically discover and include FastAPI routers from a given package.
:param FastAPI app: FastAPI application.
:param ModuleType package: Imported package.
:param bool full_path: If True, prefix is built from full path. If False, prefix is only the last filename.
"""
package_path = Path(package.__file__).parent
base_pkg_name = package.__name__.replace("_", "-")
for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + "."):
module = importlib.import_module(name)
# Try to get `router` from the module
router = getattr(module, "router", None)
if not isinstance(router, APIRouter):
continue
# Build API prefix
file_path = Path(module.__file__)
relative_path = file_path.relative_to(package_path)
parts: list[str] = list(relative_path.parts)
# Remove "routers" folder(s) from prefix parts
parts = [p for p in parts if p != "routers"]
# Drop extension from filename
parts[-1] = parts[-1].replace(".py", "")
if full_path:
# Use full path
prefix_parts = [p.replace("_", "-") for p in parts if p != "__init__"]
prefix_parts.insert(0, base_pkg_name)
else:
# Only use last file name
prefix_parts = [parts[-1].replace("_", "-")]
prefix = "/" + "/".join(prefix_parts)
app.include_router(router, prefix=prefix)

2
utils/exceptions.py Normal file
View File

@ -0,0 +1,2 @@
class ObjectNotFoundException(Exception):
pass