Files
ponzischeme89 6d44e05de4 v1
2026-04-18 07:23:55 +12:00

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