This commit is contained in:
ponzischeme89
2026-04-18 07:23:55 +12:00
parent f210020772
commit 6d44e05de4
396 changed files with 75296 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
from app.models.base import Base
from app.models.page import Page
from app.models.post import BlogPost
from app.models.settings import SiteSettings
from app.models.user import User, RefreshToken
from app.models.section import ContentSection
from app.models.analytics import AnalyticsEvent
from app.models.experiment import Experiment, ExperimentVariant, ExperimentEvent
from app.models.member import (
Member,
MemberVerificationCode,
MemberRefreshToken,
MagicLinkToken,
Walk,
Booking,
AdminMessage,
MemberNotificationDispatch,
)
from app.models.contact_lead import ContactLead
from app.models.audit import AuditLog
__all__ = [
"Base",
"Page",
"BlogPost",
"SiteSettings",
"User",
"RefreshToken",
"ContentSection",
"AnalyticsEvent",
"Experiment",
"ExperimentVariant",
"ExperimentEvent",
"Member",
"MemberVerificationCode",
"MemberRefreshToken",
"MagicLinkToken",
"Walk",
"Booking",
"AdminMessage",
"MemberNotificationDispatch",
"ContactLead",
"AuditLog",
]
+29
View File
@@ -0,0 +1,29 @@
from datetime import datetime
from sqlalchemy import String, DateTime, func, JSON
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, UUIDMixin
class AnalyticsEvent(Base, UUIDMixin):
__tablename__ = "analytics_events"
event_type: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
page: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
element: Mapped[str | None] = mapped_column(String(255), nullable=True)
metadata_: Mapped[dict | None] = mapped_column("metadata", JSON, nullable=True)
session_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
ip_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
ip_partial: Mapped[str | None] = mapped_column(String(24), nullable=True)
user_agent: Mapped[str | None] = mapped_column(String(512), nullable=True)
browser: Mapped[str | None] = mapped_column(String(100), nullable=True)
os_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
country: Mapped[str | None] = mapped_column(String(100), nullable=True)
city: Mapped[str | None] = mapped_column(String(100), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
index=True,
)
+69
View File
@@ -0,0 +1,69 @@
import uuid
from datetime import datetime
from typing import Optional
from sqlalchemy import DateTime, ForeignKey, Index, String, Text, JSON, func
from sqlalchemy import Uuid
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, UUIDMixin
class AuditLog(Base, UUIDMixin):
"""Immutable record of member activity and application errors."""
__tablename__ = "audit_logs"
__table_args__ = (
Index("ix_audit_logs_timestamp", "timestamp"),
Index("ix_audit_logs_member_id", "member_id"),
Index("ix_audit_logs_action_type", "action_type"),
Index("ix_audit_logs_status", "status"),
)
timestamp: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
# Nullable FK — SET NULL if member is deleted so the log is preserved.
member_id: Mapped[Optional[uuid.UUID]] = mapped_column(
Uuid(as_uuid=True),
ForeignKey("members.id", ondelete="SET NULL"),
nullable=True,
)
# Denormalised for readability after member deletion.
member_email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
# One of: login, logout, page_visit, booking_created, booking_cancelled,
# profile_updated, onboarding_updated, contract_signed,
# account_claimed, message_read, error
action_type: Mapped[str] = mapped_column(String(64), nullable=False)
# Identifies the page / feature area, e.g. "members/dashboard"
area: Mapped[str] = mapped_column(String(255), nullable=False)
# Human-readable one-liner
description: Mapped[str] = mapped_column(String(500), nullable=False)
# success | warning | error
status: Mapped[str] = mapped_column(String(16), nullable=False, default="success")
# Optional related booking — SET NULL if booking is deleted.
booking_id: Mapped[Optional[uuid.UUID]] = mapped_column(
Uuid(as_uuid=True),
ForeignKey("bookings.id", ondelete="SET NULL"),
nullable=True,
)
# Error detail — populated for action_type='error' records.
error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
error_detail: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
# Request metadata
ip_address: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
user_agent: Mapped[Optional[str]] = mapped_column(String(512), nullable=True)
# Catch-all JSON for any extra context (e.g. booking service_type)
extra: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
member: Mapped[Optional["Member"]] = relationship( # type: ignore[name-defined]
"Member", foreign_keys=[member_id]
)
+30
View File
@@ -0,0 +1,30 @@
import uuid
from datetime import datetime, timezone
from sqlalchemy import DateTime, func, Uuid
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
class UUIDMixin:
id: Mapped[uuid.UUID] = mapped_column(
Uuid(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
)
+34
View File
@@ -0,0 +1,34 @@
import uuid
from datetime import datetime
from typing import Optional
from sqlalchemy import DateTime, ForeignKey, JSON, String, Text, func
from sqlalchemy import Uuid
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, UUIDMixin, TimestampMixin
class ContactLead(Base, UUIDMixin, TimestampMixin):
__tablename__ = "contact_leads"
full_name: Mapped[str] = mapped_column(String(255), nullable=False)
email: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
phone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
requested_services: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
pet_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
pet_breed: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
suburb: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
service_area_status: Mapped[Optional[str]] = mapped_column(String(32), nullable=True)
message: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
source: Mapped[str] = mapped_column(String(50), nullable=False, default="contact_form")
status: Mapped[str] = mapped_column(String(32), nullable=False, default="invite", index=True)
notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
metadata_json: Mapped[Optional[dict]] = mapped_column("metadata", JSON, nullable=True)
invited_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
invited_member_id: Mapped[Optional[uuid.UUID]] = mapped_column(
Uuid(as_uuid=True),
ForeignKey("members.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
+68
View File
@@ -0,0 +1,68 @@
from datetime import datetime
from decimal import Decimal
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, JSON, Numeric, String, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDMixin
class Experiment(Base, UUIDMixin, TimestampMixin):
__tablename__ = "experiments"
experiment_key: Mapped[str] = mapped_column(String(64), nullable=False, unique=True, index=True)
cookie_name: Mapped[str] = mapped_column(String(96), nullable=False, unique=True)
name: Mapped[str] = mapped_column(String(120), nullable=False)
description: Mapped[str | None] = mapped_column(String(512), nullable=True)
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, index=True)
eligible_routes: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list)
variants: Mapped[list["ExperimentVariant"]] = relationship(
back_populates="experiment",
cascade="all, delete-orphan",
passive_deletes=True,
order_by="ExperimentVariant.variant_key",
)
class ExperimentVariant(Base, UUIDMixin, TimestampMixin):
__tablename__ = "experiment_variants"
__table_args__ = (
UniqueConstraint("experiment_id", "variant_key", name="uq_experiment_variants_experiment_variant"),
)
experiment_id: Mapped[str] = mapped_column(
ForeignKey("experiments.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
variant_key: Mapped[str] = mapped_column(String(64), nullable=False)
label: Mapped[str] = mapped_column(String(120), nullable=False)
allocation: Mapped[int] = mapped_column(Integer, nullable=False)
is_control: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
experiment: Mapped[Experiment] = relationship(back_populates="variants")
class ExperimentEvent(Base, UUIDMixin):
__tablename__ = "experiment_events"
__table_args__ = (
Index("ix_experiment_events_experiment_variant_created_at", "experiment_key", "variant_key", "created_at"),
Index("ix_experiment_events_session_created_at", "session_id", "created_at"),
)
experiment_key: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
variant_key: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
session_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
user_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
path: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
event_type: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
conversion_value: Mapped[Decimal | None] = mapped_column(Numeric(12, 2), nullable=True)
metadata_: Mapped[dict | None] = mapped_column("metadata", JSON, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
index=True,
default=datetime.utcnow,
server_default=func.now(),
)
+192
View File
@@ -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")
+19
View File
@@ -0,0 +1,19 @@
from sqlalchemy import String, Text, Boolean, Index
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, UUIDMixin, TimestampMixin
class Page(Base, UUIDMixin, TimestampMixin):
__tablename__ = "pages"
title: Mapped[str] = mapped_column(String(255), nullable=False)
slug: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
body: Mapped[str] = mapped_column(Text, nullable=False, default="")
meta_title: Mapped[str | None] = mapped_column(String(255), nullable=True)
meta_description: Mapped[str | None] = mapped_column(String(500), nullable=True)
og_image_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
published: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
__table_args__ = (
Index("ix_pages_slug", "slug"),
)
+24
View File
@@ -0,0 +1,24 @@
from typing import List
from sqlalchemy import String, Text, Boolean, Index, JSON
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy import text
from app.models.base import Base, UUIDMixin, TimestampMixin
class BlogPost(Base, UUIDMixin, TimestampMixin):
__tablename__ = "blog_posts"
title: Mapped[str] = mapped_column(String(255), nullable=False)
slug: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
excerpt: Mapped[str | None] = mapped_column(Text, nullable=True)
body: Mapped[str] = mapped_column(Text, nullable=False, default="")
author: Mapped[str | None] = mapped_column(String(255), nullable=True)
featured_image_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
# Use JSON for broader DB compatibility; PostgreSQL ARRAY is handled via type override in migration
tags: Mapped[list] = mapped_column(JSON, nullable=False, default=list)
published: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
__table_args__ = (
Index("ix_blog_posts_slug", "slug"),
)
+20
View File
@@ -0,0 +1,20 @@
from datetime import datetime, timezone
from sqlalchemy import Text, DateTime, func
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy import JSON
from app.models.base import Base
class ContentSection(Base):
__tablename__ = "content_sections"
key: Mapped[str] = mapped_column(Text, primary_key=True)
data: Mapped[dict] = mapped_column(JSON().with_variant(JSONB, "postgresql"), nullable=False)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
+27
View File
@@ -0,0 +1,27 @@
from datetime import datetime
from sqlalchemy import String, Text, JSON, Boolean, Integer, DateTime
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, UUIDMixin, TimestampMixin
class SiteSettings(Base, UUIDMixin, TimestampMixin):
__tablename__ = "site_settings"
site_name: Mapped[str] = mapped_column(String(255), nullable=False, default="")
tagline: Mapped[str | None] = mapped_column(String(500), nullable=True)
logo_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
footer_text: Mapped[str | None] = mapped_column(Text, nullable=True)
social_links: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict)
automatic_member_notifications_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
nz_public_holiday_notifications_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
invoice_reminder_notifications_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
invoice_day_of_week: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
admin_notifications_cleared_before: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
bookings_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
walks_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
messages_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
two_factor_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
audit_history_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
experiments_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
service_pricing: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict)
+39
View File
@@ -0,0 +1,39 @@
import uuid
from datetime import datetime
from sqlalchemy import String, Boolean, DateTime, ForeignKey, func
from sqlalchemy import Uuid
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, UUIDMixin, TimestampMixin
class User(Base, UUIDMixin, TimestampMixin):
__tablename__ = "users"
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
refresh_tokens: Mapped[list["RefreshToken"]] = relationship(
"RefreshToken", back_populates="user", cascade="all, delete-orphan"
)
class RefreshToken(Base, UUIDMixin):
__tablename__ = "refresh_tokens"
user_id: Mapped[uuid.UUID] = mapped_column(
Uuid(as_uuid=True),
ForeignKey("users.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,
)
user: Mapped["User"] = relationship("User", back_populates="refresh_tokens")