feat: client endpoints for clients page

This commit is contained in:
2025-10-04 18:12:13 +04:00
parent 66b50fb951
commit 986712d5b7
12 changed files with 268 additions and 0 deletions

View File

@ -23,6 +23,14 @@ class CreatedAtMixin:
)
class LastModifiedAtMixin:
last_modified_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
nullable=False,
)
class PriceMixin:
price: Mapped[float] = mapped_column(Numeric(12, 2), comment="Стоимость")

View File

View File

@ -0,0 +1 @@
from .client import Client as Client, ClientDetails as ClientDetails

View File

@ -0,0 +1,51 @@
from datetime import datetime
from typing import Optional, TYPE_CHECKING
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship, Mapped, mapped_column
from models.base import BaseModel
from models.mixins import IdMixin, CreatedAtMixin, SoftDeleteMixin, LastModifiedAtMixin
if TYPE_CHECKING:
from models import Product
class Client(BaseModel, IdMixin, CreatedAtMixin, SoftDeleteMixin):
__tablename__ = "clients"
name: Mapped[str] = mapped_column(unique=True, comment="Название клиента")
company_name: Mapped[str] = mapped_column(
nullable=False,
comment="Название компании",
)
products: Mapped[list["Product"]] = relationship(
back_populates="client", lazy="noload"
)
details: Mapped["ClientDetails"] = relationship(
uselist=False,
back_populates="client",
cascade="all, delete",
lazy="joined",
)
comment: Mapped[Optional[str]] = mapped_column(comment="Комментарий")
class ClientDetails(BaseModel, IdMixin, LastModifiedAtMixin):
__tablename__ = "client_details"
client_id: Mapped[int] = mapped_column(
ForeignKey("clients.id"), unique=True, comment="ID клиента"
)
client: Mapped[Client] = relationship(
back_populates="details", cascade="all, delete", uselist=False
)
telegram: Mapped[Optional[str]] = mapped_column()
phone_number: Mapped[Optional[str]] = mapped_column()
inn: Mapped[Optional[str]] = mapped_column()
email: Mapped[Optional[str]] = mapped_column()

View File

@ -0,0 +1 @@
from .client import ClientRepository as ClientRepository

View File

@ -0,0 +1,47 @@
from datetime import timezone, datetime
from sqlalchemy import update
from modules.clients.models import Client, ClientDetails
from modules.clients.schemas.client import UpdateClientSchema, CreateClientSchema
from repositories.mixins import *
class ClientRepository(
BaseRepository,
RepGetAllMixin[Client],
RepDeleteMixin[Client],
RepUpdateMixin[Client, UpdateClientSchema],
RepGetByIdMixin[Client],
):
entity_class = Client
entity_not_found_msg = "Клиент не найден"
def _process_get_all_stmt(self, stmt: Select) -> Select:
return stmt.where(Client.is_deleted.is_(False)).order_by(Client.created_at)
async def create(self, data: CreateClientSchema) -> int:
details = ClientDetails(**data.details.model_dump())
data_dict = data.model_dump()
data_dict["details"] = details
client = Client(**data_dict)
self.session.add(client)
await self.session.commit()
await self.session.refresh(client)
return client.id
async def update(self, client: Client, data: UpdateClientSchema) -> Client:
if data.details is not None:
stmt = (
update(ClientDetails)
.where(ClientDetails.client_id == client.id)
.values(
**data.details.model_dump(),
last_modified_at=datetime.now(timezone.utc),
)
)
await self.session.execute(stmt)
del data.details
return await self._apply_update_data_to_model(client, data, True)

View File

View File

@ -0,0 +1,55 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.clients.schemas.client import *
from modules.clients.services import ClientService
router = APIRouter(tags=["client"])
@router.get(
"/",
response_model=GetClientsResponse,
operation_id="get_clients",
)
async def get_clients(
session: SessionDependency,
):
return await ClientService(session).get_all()
@router.post(
"/",
response_model=CreateClientResponse,
operation_id="create_client",
)
async def create_client(
session: SessionDependency,
request: CreateClientRequest,
):
return await ClientService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateClientResponse,
operation_id="update_client",
)
async def update_client(
session: SessionDependency,
request: UpdateClientRequest,
pk: int = Path(),
):
return await ClientService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteClientResponse,
operation_id="delete_client",
)
async def delete_product(
session: SessionDependency,
pk: int = Path(),
):
return await ClientService(session).delete(pk)

View File

@ -0,0 +1,74 @@
from typing import Optional
from schemas.base import BaseSchema, BaseResponse
# region Entities
class ClientDetailsSchema(BaseSchema):
telegram: str
phone_number: str
inn: str
email: str
class CreateClientSchema(BaseSchema):
name: str
company_name: str
comment: Optional[str] = ""
details: ClientDetailsSchema
class ClientSchema(CreateClientSchema):
id: int
class UpdateClientSchema(BaseSchema):
name: Optional[str] = None
company_name: Optional[str] = None
comment: Optional[str] = None
details: Optional[ClientDetailsSchema] = None
# endregion
# region Requests
class ClientUpdateDetailsRequest(BaseSchema):
client_id: int
details: ClientDetailsSchema
class CreateClientRequest(BaseSchema):
entity: CreateClientSchema
class UpdateClientRequest(BaseSchema):
entity: UpdateClientSchema
# endregion
# region Responses
class GetClientsResponse(BaseSchema):
items: list[ClientSchema]
class UpdateClientResponse(BaseResponse):
pass
class CreateClientResponse(BaseResponse):
pass
class DeleteClientResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1 @@
from .client import ClientService as ClientService

View File

@ -0,0 +1,26 @@
from modules.clients.models import Client
from modules.clients.repositories import ClientRepository
from modules.clients.schemas.client import (
ClientSchema,
CreateClientRequest,
UpdateClientRequest,
)
from services.mixins import *
class ClientService(
ServiceGetAllMixin[Client, ClientSchema],
ServiceCreateMixin[Client, CreateClientRequest, ClientSchema],
ServiceUpdateMixin[Client, UpdateClientRequest],
ServiceDeleteMixin[Client],
):
schema_class = ClientSchema
entity_deleted_msg = "Клиент успешно удален"
entity_updated_msg = "Клиент успешно обновлен"
entity_created_msg = "Клиент успешно создан"
def __init__(self, session: AsyncSession):
self.repository = ClientRepository(session)
async def is_soft_delete(self, client: ClientSchema) -> bool:
return True

View File

@ -5,6 +5,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import IdMixin, SoftDeleteMixin
from modules.clients.models import Client
from modules.fulfillment_base.models import (
ProductBarcode,
BarcodeTemplate,
@ -28,6 +29,9 @@ class Product(BaseModel, IdMixin, SoftDeleteMixin):
default="", comment="Дополнительная информация"
)
client_id: Mapped[int] = mapped_column(ForeignKey("clients.id"))
client: Mapped["Client"] = relationship(back_populates="products")
images: Mapped[list["ProductImage"]] = relationship(
"ProductImage",
back_populates="product",