v1
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
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")
|
||||
Reference in New Issue
Block a user