193 lines
8.9 KiB
Python
193 lines
8.9 KiB
Python
import uuid
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
from sqlalchemy import String, Boolean, DateTime, ForeignKey, Text, JSON, func, UniqueConstraint
|
|
from sqlalchemy import Uuid
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
from app.models.base import Base, UUIDMixin, TimestampMixin
|
|
|
|
|
|
class Member(Base, UUIDMixin, TimestampMixin):
|
|
__tablename__ = "members"
|
|
|
|
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
|
hashed_password: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
|
first_name: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
last_name: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
phone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
|
address: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
|
|
emergency_contact: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
|
is_claimed: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
notifications_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
member_status: Mapped[str] = mapped_column(String(32), default="invited", nullable=False, index=True)
|
|
claimed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
onboarding_completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
contract_signed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
contract_signer_name: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
|
contract_version: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
|
activated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
onboarding_data: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
|
service_pricing_overrides: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict)
|
|
force_two_factor: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True)
|
|
|
|
verification_codes: Mapped[list["MemberVerificationCode"]] = relationship(
|
|
"MemberVerificationCode", back_populates="member", cascade="all, delete-orphan"
|
|
)
|
|
refresh_tokens: Mapped[list["MemberRefreshToken"]] = relationship(
|
|
"MemberRefreshToken", back_populates="member", cascade="all, delete-orphan"
|
|
)
|
|
walks: Mapped[list["Walk"]] = relationship(
|
|
"Walk", back_populates="member", cascade="all, delete-orphan"
|
|
)
|
|
bookings: Mapped[list["Booking"]] = relationship(
|
|
"Booking", back_populates="member", cascade="all, delete-orphan"
|
|
)
|
|
messages: Mapped[list["AdminMessage"]] = relationship(
|
|
"AdminMessage", back_populates="member", cascade="all, delete-orphan"
|
|
)
|
|
notification_dispatches: Mapped[list["MemberNotificationDispatch"]] = relationship(
|
|
"MemberNotificationDispatch", back_populates="member", cascade="all, delete-orphan"
|
|
)
|
|
magic_link_tokens: Mapped[list["MagicLinkToken"]] = relationship(
|
|
"MagicLinkToken", back_populates="member", cascade="all, delete-orphan"
|
|
)
|
|
|
|
|
|
class MemberVerificationCode(Base, UUIDMixin):
|
|
__tablename__ = "member_verification_codes"
|
|
|
|
member_id: Mapped[uuid.UUID] = mapped_column(
|
|
Uuid(as_uuid=True),
|
|
ForeignKey("members.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
code_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
purpose: Mapped[str] = mapped_column(String(20), nullable=False) # "claim" | "login_2fa"
|
|
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
used_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
|
|
member: Mapped["Member"] = relationship("Member", back_populates="verification_codes")
|
|
|
|
|
|
class MemberRefreshToken(Base, UUIDMixin):
|
|
__tablename__ = "member_refresh_tokens"
|
|
|
|
member_id: Mapped[uuid.UUID] = mapped_column(
|
|
Uuid(as_uuid=True),
|
|
ForeignKey("members.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
token_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
revoked: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
|
|
member: Mapped["Member"] = relationship("Member", back_populates="refresh_tokens")
|
|
|
|
|
|
class Walk(Base, UUIDMixin, TimestampMixin):
|
|
__tablename__ = "walks"
|
|
|
|
member_id: Mapped[uuid.UUID] = mapped_column(
|
|
Uuid(as_uuid=True),
|
|
ForeignKey("members.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
walked_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
service_type: Mapped[str] = mapped_column(String(50), nullable=False) # pack_walk | 1_1_walk | puppy_visit
|
|
duration_minutes: Mapped[int] = mapped_column(nullable=False, default=60)
|
|
notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
recorded_by: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
|
|
|
member: Mapped["Member"] = relationship("Member", back_populates="walks")
|
|
|
|
|
|
class Booking(Base, UUIDMixin, TimestampMixin):
|
|
__tablename__ = "bookings"
|
|
|
|
member_id: Mapped[uuid.UUID] = mapped_column(
|
|
Uuid(as_uuid=True),
|
|
ForeignKey("members.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
service_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
requested_date: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending") # pending | confirmed | cancelled | completed
|
|
notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
admin_notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
|
|
member: Mapped["Member"] = relationship("Member", back_populates="bookings")
|
|
|
|
|
|
class AdminMessage(Base, UUIDMixin, TimestampMixin):
|
|
__tablename__ = "admin_messages"
|
|
|
|
member_id: Mapped[uuid.UUID] = mapped_column(
|
|
Uuid(as_uuid=True),
|
|
ForeignKey("members.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
subject: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
body: Mapped[str] = mapped_column(Text, nullable=False)
|
|
read_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
sent_by: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
|
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
# "inbound" = admin → member, "outbound" = member reply
|
|
direction: Mapped[str] = mapped_column(String(16), nullable=False, default="inbound")
|
|
reply_to_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
|
Uuid(as_uuid=True),
|
|
ForeignKey("admin_messages.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
)
|
|
|
|
member: Mapped["Member"] = relationship("Member", back_populates="messages")
|
|
|
|
|
|
class MagicLinkToken(Base, UUIDMixin):
|
|
__tablename__ = "member_magic_link_tokens"
|
|
|
|
member_id: Mapped[uuid.UUID] = mapped_column(
|
|
Uuid(as_uuid=True),
|
|
ForeignKey("members.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
token_hash: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True)
|
|
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
used_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
|
|
member: Mapped["Member"] = relationship("Member", back_populates="magic_link_tokens")
|
|
|
|
|
|
class MemberNotificationDispatch(Base, UUIDMixin, TimestampMixin):
|
|
__tablename__ = "member_notification_dispatches"
|
|
__table_args__ = (
|
|
UniqueConstraint("member_id", "dispatch_key", name="uq_member_notification_dispatches_member_key"),
|
|
)
|
|
|
|
member_id: Mapped[uuid.UUID] = mapped_column(
|
|
Uuid(as_uuid=True),
|
|
ForeignKey("members.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
notification_type: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
|
dispatch_key: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
metadata_json: Mapped[Optional[dict]] = mapped_column("metadata", JSON, nullable=True)
|
|
|
|
member: Mapped["Member"] = relationship("Member", back_populates="notification_dispatches")
|