Initial commit: SIBU 2.0 MISSION

This commit is contained in:
2026-02-21 09:53:31 -05:00
commit 0c7aa53c8b
400 changed files with 67708 additions and 0 deletions

View 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"]

View 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)
)

View 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()))

View 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()))

View 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

View 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

View 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"
}
}

View 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()

View 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()))

View 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()))

View 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()))

View 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()))

View 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

View 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")

View 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