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