From 855397198111602e1a1d2ae5b1f1ab0c3393e73d Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 17 Jul 2026 21:24:55 +0700 Subject: [PATCH] Add models and schemas for users, tenants, subscriptions, plans, and business types --- app/models/__init__.py | 15 +++++++++++++++ app/models/business_type.py | 21 +++++++++++++++++++++ app/models/plan.py | 23 +++++++++++++++++++++++ app/models/subscription.py | 25 +++++++++++++++++++++++++ app/models/tenant.py | 27 +++++++++++++++++++++++++++ app/models/user.py | 25 +++++++++++++++++++++++++ app/models/user_profile.py | 23 +++++++++++++++++++++++ app/schemas/__init__.py | 17 +++++++++++++++++ app/schemas/business_type.py | 21 +++++++++++++++++++++ app/schemas/plan.py | 25 +++++++++++++++++++++++++ app/schemas/subscription.py | 16 ++++++++++++++++ app/schemas/tenant.py | 29 +++++++++++++++++++++++++++++ app/schemas/user.py | 24 ++++++++++++++++++++++++ main.py | 12 +++++++----- 14 files changed, 298 insertions(+), 5 deletions(-) create mode 100644 app/models/__init__.py create mode 100644 app/models/business_type.py create mode 100644 app/models/plan.py create mode 100644 app/models/subscription.py create mode 100644 app/models/tenant.py create mode 100644 app/models/user.py create mode 100644 app/models/user_profile.py create mode 100644 app/schemas/__init__.py create mode 100644 app/schemas/business_type.py create mode 100644 app/schemas/plan.py create mode 100644 app/schemas/subscription.py create mode 100644 app/schemas/tenant.py create mode 100644 app/schemas/user.py diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..24909e3 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,15 @@ +from app.models.user import User +from app.models.user_profile import UserProfile +from app.models.business_type import BusinessType +from app.models.plan import Plan +from app.models.tenant import Tenant +from app.models.subscription import Subscription + +__all__ = [ + "User", + "UserProfile", + "BusinessType", + "Plan", + "Tenant", + "Subscription", +] diff --git a/app/models/business_type.py b/app/models/business_type.py new file mode 100644 index 0000000..810a2a4 --- /dev/null +++ b/app/models/business_type.py @@ -0,0 +1,21 @@ +import uuid +from datetime import datetime + +from sqlalchemy import Boolean, DateTime, String, Text +from sqlalchemy.dialects.mysql import CHAR +from sqlalchemy.orm import Mapped, mapped_column + +from app.database import Base + + +class BusinessType(Base): + __tablename__ = "business_types" + + id: Mapped[str] = mapped_column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4())) + code: Mapped[str] = mapped_column(String(30), unique=True) + name: Mapped[str] = mapped_column(String(100)) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) + updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, onupdate=datetime.now) + deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) diff --git a/app/models/plan.py b/app/models/plan.py new file mode 100644 index 0000000..f0e76c3 --- /dev/null +++ b/app/models/plan.py @@ -0,0 +1,23 @@ +import uuid +from datetime import datetime + +from sqlalchemy import Boolean, DateTime, Integer, String, Text +from sqlalchemy.dialects.mysql import CHAR, JSON +from sqlalchemy.orm import Mapped, mapped_column + +from app.database import Base + + +class Plan(Base): + __tablename__ = "plans" + + id: Mapped[str] = mapped_column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4())) + code: Mapped[str] = mapped_column(String(20), unique=True) + name: Mapped[str] = mapped_column(String(100)) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + price: Mapped[int] = mapped_column(Integer) + limits: Mapped[dict | None] = mapped_column(JSON, nullable=True) + is_active: Mapped[bool] = mapped_column(Boolean, default=True) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) + updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, onupdate=datetime.now) + deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) diff --git a/app/models/subscription.py b/app/models/subscription.py new file mode 100644 index 0000000..c80199a --- /dev/null +++ b/app/models/subscription.py @@ -0,0 +1,25 @@ +import uuid +from datetime import datetime + +from sqlalchemy import DateTime, ForeignKey, String +from sqlalchemy.dialects.mysql import CHAR, JSON +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class Subscription(Base): + __tablename__ = "subscriptions" + + id: Mapped[str] = mapped_column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4())) + tenant_id: Mapped[str] = mapped_column(CHAR(36), ForeignKey("tenants.id"), unique=True) + plan_id: Mapped[str] = mapped_column(CHAR(36), ForeignKey("plans.id")) + limits: Mapped[dict | None] = mapped_column(JSON, nullable=True) + status: Mapped[str] = mapped_column(String(20), default="TRIAL") + started_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) + ended_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) + updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, onupdate=datetime.now) + + tenant: Mapped["Tenant"] = relationship(back_populates="subscription") + plan: Mapped["Plan"] = relationship() diff --git a/app/models/tenant.py b/app/models/tenant.py new file mode 100644 index 0000000..d9a53fb --- /dev/null +++ b/app/models/tenant.py @@ -0,0 +1,27 @@ +import uuid +from datetime import datetime + +from sqlalchemy import DateTime, ForeignKey, String, Text +from sqlalchemy.dialects.mysql import CHAR +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class Tenant(Base): + __tablename__ = "tenants" + + id: Mapped[str] = mapped_column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4())) + name: Mapped[str] = mapped_column(String(200)) + slug: Mapped[str] = mapped_column(String(100), unique=True) + business_type_id: Mapped[str] = mapped_column(CHAR(36), ForeignKey("business_types.id")) + phone: Mapped[str | None] = mapped_column(String(20), nullable=True) + address: Mapped[str | None] = mapped_column(Text, nullable=True) + email: Mapped[str | None] = mapped_column(String(100), nullable=True) + status: Mapped[str] = mapped_column(String(20), default="ACTIVE") + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) + updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, onupdate=datetime.now) + deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + + business_type: Mapped["BusinessType"] = relationship() + subscription: Mapped["Subscription"] = relationship(back_populates="tenant", uselist=False) diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..2c6b92c --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,25 @@ +import uuid +from datetime import datetime + +from sqlalchemy import DateTime, String +from sqlalchemy.dialects.mysql import CHAR +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class User(Base): + __tablename__ = "users" + + id: Mapped[str] = mapped_column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4())) + email: Mapped[str] = mapped_column(String(100), unique=True) + username: Mapped[str] = mapped_column(String(20), unique=True) + password: Mapped[str] = mapped_column(String(255)) + email_verified_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + tenant_id: Mapped[str | None] = mapped_column(CHAR(36), nullable=True) + status: Mapped[str] = mapped_column(String(20), default="ONBOARDING") + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) + updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, onupdate=datetime.now) + deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + + profile: Mapped["UserProfile"] = relationship(back_populates="user", uselist=False) diff --git a/app/models/user_profile.py b/app/models/user_profile.py new file mode 100644 index 0000000..4c9f246 --- /dev/null +++ b/app/models/user_profile.py @@ -0,0 +1,23 @@ +import uuid +from datetime import datetime + +from sqlalchemy import DateTime, ForeignKey, String +from sqlalchemy.dialects.mysql import CHAR +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class UserProfile(Base): + __tablename__ = "user_profiles" + + id: Mapped[str] = mapped_column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4())) + user_id: Mapped[str] = mapped_column(CHAR(36), ForeignKey("users.id"), unique=True) + full_name: Mapped[str] = mapped_column(String(200)) + phone: Mapped[str | None] = mapped_column(String(20), nullable=True) + timezone: Mapped[str | None] = mapped_column(String(50), nullable=True, default="Asia/Jakarta") + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) + updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, onupdate=datetime.now) + deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) + + user: Mapped["User"] = relationship(back_populates="profile") diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..515d72c --- /dev/null +++ b/app/schemas/__init__.py @@ -0,0 +1,17 @@ +from app.schemas.user import UserCreate, UserResponse +from app.schemas.business_type import BusinessTypeCreate, BusinessTypeResponse +from app.schemas.plan import PlanCreate, PlanResponse +from app.schemas.tenant import TenantCreate, TenantResponse +from app.schemas.subscription import SubscriptionResponse + +__all__ = [ + "UserCreate", + "UserResponse", + "BusinessTypeCreate", + "BusinessTypeResponse", + "PlanCreate", + "PlanResponse", + "TenantCreate", + "TenantResponse", + "SubscriptionResponse", +] diff --git a/app/schemas/business_type.py b/app/schemas/business_type.py new file mode 100644 index 0000000..6e44694 --- /dev/null +++ b/app/schemas/business_type.py @@ -0,0 +1,21 @@ +from datetime import datetime + +from pydantic import BaseModel + + +class BusinessTypeCreate(BaseModel): + code: str + name: str + description: str | None = None + + +class BusinessTypeResponse(BaseModel): + id: str + code: str + name: str + description: str | None + is_active: bool + created_at: datetime + + class Config: + from_attributes = True diff --git a/app/schemas/plan.py b/app/schemas/plan.py new file mode 100644 index 0000000..59a691d --- /dev/null +++ b/app/schemas/plan.py @@ -0,0 +1,25 @@ +from datetime import datetime + +from pydantic import BaseModel + + +class PlanCreate(BaseModel): + code: str + name: str + description: str | None = None + price: int + limits: dict | None = None + + +class PlanResponse(BaseModel): + id: str + code: str + name: str + description: str | None + price: int + limits: dict | None + is_active: bool + created_at: datetime + + class Config: + from_attributes = True diff --git a/app/schemas/subscription.py b/app/schemas/subscription.py new file mode 100644 index 0000000..124678b --- /dev/null +++ b/app/schemas/subscription.py @@ -0,0 +1,16 @@ +from datetime import datetime + +from pydantic import BaseModel + + +class SubscriptionResponse(BaseModel): + id: str + tenant_id: str + plan_id: str + limits: dict | None + status: str + started_at: datetime + ended_at: datetime | None + + class Config: + from_attributes = True diff --git a/app/schemas/tenant.py b/app/schemas/tenant.py new file mode 100644 index 0000000..683bed0 --- /dev/null +++ b/app/schemas/tenant.py @@ -0,0 +1,29 @@ +from datetime import datetime + +from pydantic import BaseModel + + +class TenantCreate(BaseModel): + name: str + slug: str + business_type_id: str + phone: str | None = None + address: str | None = None + email: str | None = None + plan_id: str + limits: dict | None = None + + +class TenantResponse(BaseModel): + id: str + name: str + slug: str + business_type_id: str + phone: str | None + address: str | None + email: str | None + status: str + created_at: datetime + + class Config: + from_attributes = True diff --git a/app/schemas/user.py b/app/schemas/user.py new file mode 100644 index 0000000..d6224de --- /dev/null +++ b/app/schemas/user.py @@ -0,0 +1,24 @@ +from datetime import datetime + +from pydantic import BaseModel, EmailStr + + +class UserCreate(BaseModel): + email: EmailStr + username: str + password: str + full_name: str + phone: str | None = None + timezone: str = "Asia/Jakarta" + + +class UserResponse(BaseModel): + id: str + email: str + username: str + tenant_id: str | None + status: str + created_at: datetime + + class Config: + from_attributes = True diff --git a/main.py b/main.py index 2579b66..ed9c38e 100644 --- a/main.py +++ b/main.py @@ -19,11 +19,13 @@ app.add_middleware( allow_headers=["*"], ) -app.include_router(auth_router) -app.include_router(business_types_router) -app.include_router(plans_router) -app.include_router(tenants_router) -app.include_router(subscriptions_router) +API_PREFIX = "/v1" + +app.include_router(auth_router, prefix=API_PREFIX) +app.include_router(business_types_router, prefix=API_PREFIX) +app.include_router(plans_router, prefix=API_PREFIX) +app.include_router(tenants_router, prefix=API_PREFIX) +app.include_router(subscriptions_router, prefix=API_PREFIX) @app.get("/")