From 2547bcfcba1b96d08836424e3de166788d069b3f Mon Sep 17 00:00:00 2001 From: hexdev Date: Sat, 25 Jul 2026 15:31:44 +0700 Subject: [PATCH] feat: introduced plans pricing --- ...b542353b092_edited_pricing_added_addons.py | 41 +++++++++++++++++++ db/models/__init__.py | 3 +- db/models/addons.py | 13 ++++++ db/models/pricing.py | 12 +++--- repositories/addons.py | 15 +++++++ repositories/pricing.py | 15 +++++++ routes/__init__.py | 3 +- routes/plans.py | 19 +++++++++ schemas/plans.py | 13 ++++++ services/plans.py | 17 ++++++++ 10 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 alembic/versions/bb542353b092_edited_pricing_added_addons.py create mode 100644 db/models/addons.py create mode 100644 repositories/addons.py create mode 100644 repositories/pricing.py create mode 100644 routes/plans.py create mode 100644 schemas/plans.py create mode 100644 services/plans.py diff --git a/alembic/versions/bb542353b092_edited_pricing_added_addons.py b/alembic/versions/bb542353b092_edited_pricing_added_addons.py new file mode 100644 index 0000000..f54ddd6 --- /dev/null +++ b/alembic/versions/bb542353b092_edited_pricing_added_addons.py @@ -0,0 +1,41 @@ +"""edited pricing + added addons + +Revision ID: bb542353b092 +Revises: 74bd9cedebdd +Create Date: 2026-07-25 00:35:38.927479 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'bb542353b092' +down_revision: Union[str, Sequence[str], None] = '74bd9cedebdd' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('addons', + sa.Column('id', sa.TEXT(), nullable=False), + sa.Column('name', sa.TEXT(), nullable=False), + sa.Column('price', sa.FLOAT(), nullable=False), + sa.Column('free_threshold', sa.INTEGER(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('id') + ) + op.create_unique_constraint(None, 'pricing_configs', ['id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'pricing_configs', type_='unique') + op.drop_table('addons') + # ### end Alembic commands ### diff --git a/db/models/__init__.py b/db/models/__init__.py index 1bf849c..35b4bb8 100644 --- a/db/models/__init__.py +++ b/db/models/__init__.py @@ -1,5 +1,6 @@ +from .addons import Addon from .pricing import PricingConfig from .sessions import Session from .users import User -__all__ = ["PricingConfig", "Session", "User"] +__all__ = ["Addon", "PricingConfig", "Session", "User"] diff --git a/db/models/addons.py b/db/models/addons.py new file mode 100644 index 0000000..7710536 --- /dev/null +++ b/db/models/addons.py @@ -0,0 +1,13 @@ +from sqlalchemy import FLOAT, INTEGER, TEXT +from sqlalchemy.orm import Mapped, mapped_column + +from db.base import Base + + +class Addon(Base): + __tablename__ = "addons" + + id: Mapped[str] = mapped_column(TEXT, nullable=False, unique=True, primary_key=True) + name: Mapped[str] = mapped_column(TEXT, nullable=False, unique=False) + price: Mapped[float] = mapped_column(FLOAT, nullable=False) + free_threshold: Mapped[int] = mapped_column(INTEGER, default=-1) diff --git a/db/models/pricing.py b/db/models/pricing.py index 1122d01..81525f0 100644 --- a/db/models/pricing.py +++ b/db/models/pricing.py @@ -9,11 +9,13 @@ from db.base import Base class PricingConfig(Base): __tablename__ = "pricing_configs" - __table_args__ = ( - CheckConstraint("id = 1", name="single_row_check"), - ) + __table_args__ = (CheckConstraint("id = 1", name="single_row_check"),) - id: Mapped[int] = mapped_column(INTEGER, autoincrement=True, unique=True, nullable=False, primary_key=True) + id: Mapped[int] = mapped_column( + INTEGER, autoincrement=True, unique=True, nullable=False, primary_key=True + ) device_price: Mapped[float] = mapped_column(FLOAT, nullable=False) - updated_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now(), onupdate=func.now()) + updated_at: Mapped[datetime] = mapped_column( + TIMESTAMP, server_default=func.now(), onupdate=func.now() + ) diff --git a/repositories/addons.py b/repositories/addons.py new file mode 100644 index 0000000..df452eb --- /dev/null +++ b/repositories/addons.py @@ -0,0 +1,15 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from db.models import Addon + + +class AddonsRepository: + def __init__(self, session: AsyncSession) -> None: + self.session = session + + async def get_all(self) -> list[Addon]: + stmt = select(Addon) + r = await self.session.execute(stmt) + + return list(r.scalars().all()) diff --git a/repositories/pricing.py b/repositories/pricing.py new file mode 100644 index 0000000..f6a01d8 --- /dev/null +++ b/repositories/pricing.py @@ -0,0 +1,15 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from db.models.pricing import PricingConfig + + +class PricingRepository: + def __init__(self, session: AsyncSession) -> None: + self.session = session + + async def get(self) -> PricingConfig | None: + stmt = select(PricingConfig).where(PricingConfig.id == 1) + r = await self.session.execute(stmt) + + return r.scalar_one_or_none() diff --git a/routes/__init__.py b/routes/__init__.py index 32ea286..dcdb7e2 100644 --- a/routes/__init__.py +++ b/routes/__init__.py @@ -1,5 +1,6 @@ from fastapi import APIRouter from .auth import router as auth_router +from .plans import router as plans_router -routers: list[APIRouter] = [auth_router] +routers: list[APIRouter] = [auth_router, plans_router] diff --git a/routes/plans.py b/routes/plans.py new file mode 100644 index 0000000..fbe7471 --- /dev/null +++ b/routes/plans.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from db.session import get_db +from repositories.addons import AddonsRepository +from repositories.pricing import PricingRepository +from schemas.plans import PricingPlans +from services.plans import get_pricing_model + +router = APIRouter(prefix="/plans") + + +@router.get("/", response_model=PricingPlans) +async def get_plans(session: AsyncSession = Depends(get_db)): + addons_repo = AddonsRepository(session) + pricing_repo = PricingRepository(session) + + res = await get_pricing_model(addons_repo, pricing_repo) + return res diff --git a/schemas/plans.py b/schemas/plans.py new file mode 100644 index 0000000..37b2566 --- /dev/null +++ b/schemas/plans.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel + + +class AddonData(BaseModel): + id: str + name: str + price: float + free_threshold: int + + +class PricingPlans(BaseModel): + device_price: int + addons: list[AddonData] diff --git a/services/plans.py b/services/plans.py new file mode 100644 index 0000000..271235d --- /dev/null +++ b/services/plans.py @@ -0,0 +1,17 @@ +from repositories.addons import AddonsRepository +from repositories.pricing import PricingRepository +from schemas.plans import AddonData, PricingPlans + + +async def get_pricing_model(addons_repo: AddonsRepository, pricing_repo: PricingRepository): + pricing = await pricing_repo.get() + if not pricing: + return None + + addons = await addons_repo.get_all() + addons_data = [ + AddonData(id=a.id, name=a.name, price=a.price, free_threshold=a.free_threshold) + for a in addons + ] + + return PricingPlans(device_price=pricing.device_price, addons=addons_data)