Initial commit: SIBU 2.0 MISSION
This commit is contained in:
18
backend/app/models/__init__.py
Normal file
18
backend/app/models/__init__.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""Database models."""
|
||||
from app.models.route import Route
|
||||
from app.models.bus_stop import BusStop
|
||||
from app.models.route_stop import RouteStop
|
||||
from app.models.bus_schedule import BusSchedule
|
||||
from app.models.user import User, DriverProfile
|
||||
from app.models.taxi import Taxi
|
||||
from app.models.favorite import Favorite
|
||||
from app.models.telemetry import Telemetry
|
||||
from app.models.coupon import Coupon
|
||||
from app.models.business import Business
|
||||
from app.models.user_coupon import UserCoupon
|
||||
from app.models.analytics import AnalyticsEvent
|
||||
from app.models.report import Report
|
||||
from app.models.shuttle import Shuttle
|
||||
|
||||
__all__ = ["Route", "BusStop", "RouteStop", "BusSchedule", "User", "DriverProfile", "Taxi", "Favorite", "Telemetry", "Coupon", "Business", "UserCoupon", "AnalyticsEvent", "Report", "Shuttle"]
|
||||
|
||||
19
backend/app/models/analytics.py
Normal file
19
backend/app/models/analytics.py
Normal file
@ -0,0 +1,19 @@
|
||||
from sqlmodel import SQLModel, Field
|
||||
from typing import Optional, Dict
|
||||
from uuid import UUID, uuid4
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, DateTime, func, JSON
|
||||
|
||||
class AnalyticsEvent(SQLModel, table=True):
|
||||
__tablename__ = "analytics_events"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
event_name: str = Field(index=True)
|
||||
user_id: Optional[UUID] = Field(default=None, index=True, foreign_key="users.id")
|
||||
screen_name: Optional[str] = None
|
||||
item_id: Optional[str] = None # route_id, stop_id, promo_id, etc.
|
||||
properties: Optional[Dict] = Field(default_factory=dict, sa_column=Column(JSON))
|
||||
timestamp: datetime = Field(
|
||||
default_factory=datetime.utcnow,
|
||||
sa_column=Column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
)
|
||||
31
backend/app/models/bus_schedule.py
Normal file
31
backend/app/models/bus_schedule.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""Bus schedule model."""
|
||||
from sqlmodel import SQLModel, Field, Column
|
||||
from datetime import datetime, time
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
from uuid import UUID, uuid4
|
||||
from sqlalchemy import DateTime, func
|
||||
|
||||
|
||||
class BusScheduleType(str, Enum):
|
||||
"""Schedule type enumeration."""
|
||||
WEEKDAY = "weekday"
|
||||
WEEKEND = "weekend"
|
||||
HOLIDAY = "holiday"
|
||||
|
||||
|
||||
class BusSchedule(SQLModel, table=True):
|
||||
"""Bus schedule model."""
|
||||
|
||||
__tablename__ = "bus_schedules"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
route_id: UUID = Field(foreign_key="routes.id")
|
||||
departure_time: time
|
||||
frequency_minutes: Optional[int] = 30
|
||||
schedule_type: BusScheduleType = BusScheduleType.WEEKDAY
|
||||
is_active: bool = Field(default=True)
|
||||
is_published: bool = Field(default=False)
|
||||
notes: Optional[str] = None
|
||||
created_at: Optional[datetime] = Field(sa_column=Column(DateTime, server_default=func.now()))
|
||||
|
||||
35
backend/app/models/bus_stop.py
Normal file
35
backend/app/models/bus_stop.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""Bus stop model."""
|
||||
from sqlmodel import SQLModel, Field, Column
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
from uuid import UUID, uuid4
|
||||
from sqlalchemy import DateTime, func
|
||||
|
||||
|
||||
class StopType(str, Enum):
|
||||
"""Stop type enumeration."""
|
||||
TERMINAL = "terminal"
|
||||
REGULAR = "regular"
|
||||
EXPRESS_ONLY = "express_only"
|
||||
|
||||
|
||||
class BusStop(SQLModel, table=True):
|
||||
"""Bus stop model."""
|
||||
|
||||
__tablename__ = "bus_stops"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
name: str
|
||||
latitude: float
|
||||
longitude: float
|
||||
city: str
|
||||
address: Optional[str] = None
|
||||
stop_type: StopType = StopType.REGULAR
|
||||
has_shelter: bool = False
|
||||
has_seating: bool = False
|
||||
is_accessible: bool = False
|
||||
stop_order: Optional[int] = None
|
||||
created_at: Optional[datetime] = Field(sa_column=Column(DateTime, server_default=func.now()))
|
||||
updated_at: Optional[datetime] = Field(sa_column=Column(DateTime, server_default=func.now(), onupdate=func.now()))
|
||||
|
||||
58
backend/app/models/business.py
Normal file
58
backend/app/models/business.py
Normal file
@ -0,0 +1,58 @@
|
||||
from sqlmodel import SQLModel, Field, Column, Relationship
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, TYPE_CHECKING
|
||||
from uuid import UUID, uuid4
|
||||
from sqlalchemy import DateTime, func
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.coupon import Coupon
|
||||
|
||||
class Business(SQLModel, table=True):
|
||||
"""Business record for local partners."""
|
||||
|
||||
__tablename__ = "businesses"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
name: str
|
||||
address: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
image_url: Optional[str] = None
|
||||
social_media: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
latitude: Optional[float] = None
|
||||
longitude: Optional[float] = None
|
||||
area: Optional[str] = Field(default="Boquete")
|
||||
|
||||
# Relationship to coupons
|
||||
coupons: List["Coupon"] = Relationship(back_populates="business")
|
||||
|
||||
created_at: Optional[datetime] = Field(
|
||||
default=None,
|
||||
sa_column=Column(DateTime(timezone=True), server_default=func.now())
|
||||
)
|
||||
updated_at: Optional[datetime] = Field(
|
||||
default=None,
|
||||
sa_column=Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
)
|
||||
|
||||
class BusinessCreate(SQLModel):
|
||||
name: str
|
||||
address: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
image_url: Optional[str] = None
|
||||
social_media: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
latitude: Optional[float] = None
|
||||
longitude: Optional[float] = None
|
||||
area: Optional[str] = "Boquete"
|
||||
|
||||
class BusinessUpdate(SQLModel):
|
||||
name: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
image_url: Optional[str] = None
|
||||
social_media: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
latitude: Optional[float] = None
|
||||
longitude: Optional[float] = None
|
||||
area: Optional[str] = None
|
||||
79
backend/app/models/coupon.py
Normal file
79
backend/app/models/coupon.py
Normal file
@ -0,0 +1,79 @@
|
||||
from sqlmodel import SQLModel, Field, Column, Relationship
|
||||
from datetime import datetime
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from uuid import UUID, uuid4
|
||||
from sqlalchemy import DateTime, func
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.business import Business
|
||||
|
||||
class Coupon(SQLModel, table=True):
|
||||
"""Coupon record for promotions."""
|
||||
|
||||
__tablename__ = "coupons"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
business_id: Optional[UUID] = Field(default=None, foreign_key="businesses.id")
|
||||
title: str = Field(index=True)
|
||||
description: Optional[str] = None
|
||||
|
||||
# Relationship to business
|
||||
business: Optional["Business"] = Relationship(back_populates="coupons")
|
||||
business_name: Optional[str] = None
|
||||
business_address: Optional[str] = None
|
||||
business_phone: Optional[str] = None
|
||||
image_url: Optional[str] = None
|
||||
social_media: Optional[str] = None
|
||||
terms: Optional[str] = None
|
||||
discount_percentage: Optional[int] = None
|
||||
discount_amount: Optional[float] = None
|
||||
category: Optional[str] = None
|
||||
valid_from: Optional[datetime] = Field(
|
||||
sa_column=Column(DateTime(timezone=True), nullable=True)
|
||||
)
|
||||
valid_until: Optional[datetime] = Field(
|
||||
sa_column=Column(DateTime(timezone=True), nullable=True)
|
||||
)
|
||||
is_active: bool = Field(default=True)
|
||||
created_at: Optional[datetime] = Field(
|
||||
default=None,
|
||||
sa_column=Column(DateTime(timezone=True), server_default=func.now())
|
||||
)
|
||||
updated_at: Optional[datetime] = Field(
|
||||
default=None,
|
||||
sa_column=Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
)
|
||||
|
||||
class CouponCreate(SQLModel):
|
||||
title: str
|
||||
business_id: Optional[UUID] = None
|
||||
description: Optional[str] = None
|
||||
business_name: Optional[str] = None
|
||||
business_address: Optional[str] = None
|
||||
business_phone: Optional[str] = None
|
||||
image_url: Optional[str] = None
|
||||
social_media: Optional[str] = None
|
||||
terms: Optional[str] = None
|
||||
discount_percentage: Optional[int] = None
|
||||
discount_amount: Optional[float] = None
|
||||
category: Optional[str] = None
|
||||
valid_from: Optional[datetime] = None
|
||||
valid_until: Optional[datetime] = None
|
||||
is_active: Optional[bool] = True
|
||||
|
||||
class CouponUpdate(SQLModel):
|
||||
title: Optional[str] = None
|
||||
business_id: Optional[UUID] = None
|
||||
description: Optional[str] = None
|
||||
business_name: Optional[str] = None
|
||||
business_address: Optional[str] = None
|
||||
business_phone: Optional[str] = None
|
||||
image_url: Optional[str] = None
|
||||
social_media: Optional[str] = None
|
||||
terms: Optional[str] = None
|
||||
discount_percentage: Optional[int] = None
|
||||
discount_amount: Optional[float] = None
|
||||
category: Optional[str] = None
|
||||
valid_from: Optional[datetime] = None
|
||||
valid_until: Optional[datetime] = None
|
||||
is_active: Optional[bool] = None
|
||||
31
backend/app/models/favorite.py
Normal file
31
backend/app/models/favorite.py
Normal file
@ -0,0 +1,31 @@
|
||||
from sqlmodel import SQLModel, Field
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
class Favorite(SQLModel, table=True):
|
||||
__tablename__ = "favorites"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID = Field(foreign_key="users.id", index=True)
|
||||
|
||||
# Type of favorite: 'coupon', 'business', 'taxi', 'route'
|
||||
item_type: str = Field(index=True)
|
||||
item_id: str = Field(index=True)
|
||||
|
||||
# Optional metadata
|
||||
item_name: Optional[str] = None
|
||||
item_image: Optional[str] = None
|
||||
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"user_id": "user-123",
|
||||
"item_type": "coupon",
|
||||
"item_id": "coupon-456",
|
||||
"item_name": "50% descuento en restaurante",
|
||||
"item_image": "/uploads/coupon.jpg"
|
||||
}
|
||||
}
|
||||
27
backend/app/models/report.py
Normal file
27
backend/app/models/report.py
Normal file
@ -0,0 +1,27 @@
|
||||
from sqlmodel import SQLModel, Field, Relationship
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from app.models.user import User
|
||||
from uuid import UUID, uuid4
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, DateTime, func
|
||||
from enum import Enum
|
||||
|
||||
class ReportStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
RESOLVED = "resolved"
|
||||
ARCHIVED = "archived"
|
||||
|
||||
class Report(SQLModel, table=True):
|
||||
__tablename__ = "reports"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: Optional[UUID] = Field(default=None, foreign_key="users.id")
|
||||
message: str
|
||||
status: ReportStatus = Field(default=ReportStatus.PENDING)
|
||||
created_at: Optional[datetime] = Field(
|
||||
sa_column=Column(DateTime, server_default=func.now())
|
||||
)
|
||||
|
||||
# Relationships
|
||||
user: Optional["User"] = Relationship()
|
||||
33
backend/app/models/route.py
Normal file
33
backend/app/models/route.py
Normal file
@ -0,0 +1,33 @@
|
||||
"""Route model."""
|
||||
from sqlmodel import SQLModel, Field, Column
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
from uuid import UUID, uuid4
|
||||
from sqlalchemy import DateTime, func
|
||||
|
||||
|
||||
class RouteStatus(str, Enum):
|
||||
"""Route status enumeration."""
|
||||
ACTIVE = "active"
|
||||
INACTIVE = "inactive"
|
||||
MAINTENANCE = "maintenance"
|
||||
|
||||
|
||||
class Route(SQLModel, table=True):
|
||||
"""Route model representing a bus route."""
|
||||
|
||||
__tablename__ = "routes"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
name: str = Field(unique=True, index=True)
|
||||
description: Optional[str] = None
|
||||
origin_city: str
|
||||
destination_city: str
|
||||
distance_km: Optional[float] = None
|
||||
estimated_duration_minutes: Optional[int] = None
|
||||
average_speed_kmh: Optional[float] = None
|
||||
status: RouteStatus = RouteStatus.ACTIVE
|
||||
created_at: Optional[datetime] = Field(sa_column=Column(DateTime, server_default=func.now()))
|
||||
updated_at: Optional[datetime] = Field(sa_column=Column(DateTime, server_default=func.now(), onupdate=func.now()))
|
||||
|
||||
23
backend/app/models/route_stop.py
Normal file
23
backend/app/models/route_stop.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""Route stop junction model."""
|
||||
from sqlmodel import SQLModel, Field, Column
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from uuid import UUID, uuid4
|
||||
from sqlalchemy import DateTime, func
|
||||
|
||||
|
||||
class RouteStop(SQLModel, table=True):
|
||||
"""Route stop junction table connecting routes to their stops."""
|
||||
|
||||
__tablename__ = "route_stops"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
route_id: UUID = Field(foreign_key="routes.id")
|
||||
stop_id: UUID = Field(foreign_key="bus_stops.id")
|
||||
stop_order: int
|
||||
travel_time_minutes: Optional[int] = None
|
||||
stop_delay_minutes: int = 0
|
||||
is_pickup_point: bool = True
|
||||
is_dropoff_point: bool = True
|
||||
created_at: Optional[datetime] = Field(sa_column=Column(DateTime, server_default=func.now()))
|
||||
|
||||
32
backend/app/models/shuttle.py
Normal file
32
backend/app/models/shuttle.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""Shuttle model for intercity and tourist trips."""
|
||||
from sqlmodel import SQLModel, Field, Column
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from uuid import UUID, uuid4
|
||||
from sqlalchemy import DateTime, func
|
||||
|
||||
|
||||
class Shuttle(SQLModel, table=True):
|
||||
"""Model representing an intercity shuttle or tourist trip."""
|
||||
|
||||
__tablename__ = "shuttles"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
route_name: str = Field(index=True) # e.g. "Boquete - Santa Catalina"
|
||||
description: Optional[str] = None
|
||||
origin: str = Field(index=True)
|
||||
destination: str = Field(index=True)
|
||||
vehicle_type: str # Private Bus, Private Car, Van
|
||||
company_name: Optional[str] = None # e.g. "Chiriqui Transfers"
|
||||
trip_type: str = "one_way" # one_way, round_trip, both
|
||||
price_per_person: Optional[float] = None
|
||||
price_private_trip: Optional[float] = None
|
||||
estimated_duration: str # e.g. "4.5 hours"
|
||||
departure_times: Optional[str] = None # e.g. "Every Day at 8:00 AM"
|
||||
contact_whatsapp: str
|
||||
phone_number: Optional[str] = None
|
||||
english_speaking: bool = Field(default=False)
|
||||
image_url: Optional[str] = None
|
||||
is_active: bool = Field(default=True)
|
||||
created_at: Optional[datetime] = Field(sa_column=Column(DateTime, server_default=func.now()))
|
||||
updated_at: Optional[datetime] = Field(sa_column=Column(DateTime, server_default=func.now(), onupdate=func.now()))
|
||||
26
backend/app/models/taxi.py
Normal file
26
backend/app/models/taxi.py
Normal file
@ -0,0 +1,26 @@
|
||||
"""Taxi model."""
|
||||
from sqlmodel import SQLModel, Field, Column
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from uuid import UUID, uuid4
|
||||
from sqlalchemy import DateTime, func
|
||||
|
||||
|
||||
class Taxi(SQLModel, table=True):
|
||||
"""Taxi model representing an authorized taxi."""
|
||||
|
||||
__tablename__ = "taxis"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
owner_name: str
|
||||
phone_number: str
|
||||
license_plate: str = Field(unique=True, index=True)
|
||||
cooperative: Optional[str] = None
|
||||
corregimiento: str = Field(index=True)
|
||||
shift: str = "day" # day, night, 24h
|
||||
rating: float = Field(default=5.0)
|
||||
english_speaking: bool = Field(default=False)
|
||||
image_url: Optional[str] = None
|
||||
is_active: bool = Field(default=True)
|
||||
created_at: Optional[datetime] = Field(sa_column=Column(DateTime, server_default=func.now()))
|
||||
updated_at: Optional[datetime] = Field(sa_column=Column(DateTime, server_default=func.now(), onupdate=func.now()))
|
||||
35
backend/app/models/telemetry.py
Normal file
35
backend/app/models/telemetry.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""Telemetry model for real-time tracking."""
|
||||
from sqlmodel import SQLModel, Field, Column
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from uuid import UUID, uuid4
|
||||
from sqlalchemy import DateTime, func
|
||||
from enum import Enum
|
||||
|
||||
class VehicleStatus(str, Enum):
|
||||
ACTIVE = "active"
|
||||
OFFLINE = "offline"
|
||||
BREAK = "break"
|
||||
|
||||
class Telemetry(SQLModel, table=True):
|
||||
"""Telemetry record for a driver's vehicle."""
|
||||
|
||||
__tablename__ = "telemetry"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID = Field(foreign_key="users.id", index=True)
|
||||
latitude: float
|
||||
longitude: float
|
||||
speed: Optional[float] = None
|
||||
heading: Optional[float] = None
|
||||
status: VehicleStatus = Field(default=VehicleStatus.ACTIVE)
|
||||
timestamp: datetime = Field(
|
||||
sa_column=Column(DateTime, server_default=func.now(), index=True)
|
||||
)
|
||||
|
||||
class TelemetryCreate(SQLModel):
|
||||
latitude: float
|
||||
longitude: float
|
||||
speed: Optional[float] = None
|
||||
heading: Optional[float] = None
|
||||
status: VehicleStatus = VehicleStatus.ACTIVE
|
||||
59
backend/app/models/user.py
Normal file
59
backend/app/models/user.py
Normal file
@ -0,0 +1,59 @@
|
||||
"""User and DriverProfile models."""
|
||||
from sqlmodel import SQLModel, Field, Relationship
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
from uuid import UUID, uuid4
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, DateTime, func
|
||||
|
||||
|
||||
class UserRole(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
PASSENGER = "PASSENGER"
|
||||
DRIVER = "DRIVER"
|
||||
PROMOTER = "PROMOTER"
|
||||
|
||||
|
||||
class VehicleType(str, Enum):
|
||||
TAXI = "taxi"
|
||||
BUS = "bus"
|
||||
|
||||
|
||||
class User(SQLModel, table=True):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
email: str = Field(unique=True, index=True)
|
||||
hashed_password: str
|
||||
full_name: str = Field(index=True)
|
||||
role: UserRole = Field(default=UserRole.PASSENGER)
|
||||
is_active: bool = Field(default=True)
|
||||
is_verified: bool = Field(default=False) # For drivers/admins verification
|
||||
profile_photo_url: Optional[str] = None
|
||||
created_at: Optional[datetime] = Field(
|
||||
sa_column=Column(DateTime, server_default=func.now())
|
||||
)
|
||||
|
||||
# Relationships
|
||||
driver_profile: Optional["DriverProfile"] = Relationship(
|
||||
back_populates="user", sa_relationship_kwargs={"uselist": False}
|
||||
)
|
||||
|
||||
|
||||
class DriverProfile(SQLModel, table=True):
|
||||
__tablename__ = "driver_profiles"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID = Field(foreign_key="users.id")
|
||||
cedula: str
|
||||
vehicle_type: VehicleType
|
||||
license_plate: str
|
||||
photo_url: Optional[str] = None
|
||||
vehicle_photo_url: Optional[str] = None
|
||||
cooperative_name: Optional[str] = None # Specifically for Bus
|
||||
shift: Optional[str] = None # For Taxi schedules (e.g. "Dia,Noche")
|
||||
payment_methods: Optional[str] = None # e.g. "Efectivo,Yappi"
|
||||
speaks_english: bool = Field(default=False)
|
||||
|
||||
# Relationship
|
||||
user: User = Relationship(back_populates="driver_profile")
|
||||
49
backend/app/models/user_coupon.py
Normal file
49
backend/app/models/user_coupon.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""UserCoupon model for tracking claimed coupons."""
|
||||
from sqlmodel import SQLModel, Field, Relationship
|
||||
from datetime import datetime
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from uuid import UUID, uuid4
|
||||
from sqlalchemy import Column, DateTime, func, String
|
||||
from enum import Enum
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.user import User
|
||||
from app.models.coupon import Coupon
|
||||
|
||||
class UserCouponStatus(str, Enum):
|
||||
CLAIMED = "claimed"
|
||||
REDEEMED = "redeemed"
|
||||
EXPIRED = "expired"
|
||||
|
||||
class UserCoupon(SQLModel, table=True):
|
||||
__tablename__ = "user_coupons"
|
||||
|
||||
id: Optional[UUID] = Field(default_factory=uuid4, primary_key=True)
|
||||
user_id: UUID = Field(foreign_key="users.id")
|
||||
coupon_id: UUID = Field(foreign_key="coupons.id")
|
||||
|
||||
status: UserCouponStatus = Field(default=UserCouponStatus.CLAIMED)
|
||||
redemption_code: str = Field(
|
||||
sa_column=Column(String, unique=True, index=True)
|
||||
)
|
||||
|
||||
claimed_at: datetime = Field(
|
||||
sa_column=Column(DateTime(timezone=True), server_default=func.now())
|
||||
)
|
||||
redeemed_at: Optional[datetime] = Field(
|
||||
sa_column=Column(DateTime(timezone=True), nullable=True)
|
||||
)
|
||||
|
||||
# Relationships
|
||||
user: "User" = Relationship()
|
||||
coupon: "Coupon" = Relationship()
|
||||
|
||||
class UserCouponRead(SQLModel):
|
||||
id: UUID
|
||||
user_id: UUID
|
||||
coupon_id: UUID
|
||||
status: UserCouponStatus
|
||||
redemption_code: str
|
||||
claimed_at: datetime
|
||||
redeemed_at: Optional[datetime]
|
||||
coupon: Optional["Coupon"] = None
|
||||
Reference in New Issue
Block a user