Files
membership-be/models.py

274 lines
13 KiB
Python

from sqlalchemy import Column, String, Boolean, DateTime, Enum as SQLEnum, Text, Integer, BigInteger, ForeignKey, JSON
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from datetime import datetime, timezone
import uuid
import enum
from database import Base
class UserStatus(enum.Enum):
pending_email = "pending_email"
pending_approval = "pending_approval"
pre_approved = "pre_approved"
payment_pending = "payment_pending"
active = "active"
inactive = "inactive"
class UserRole(enum.Enum):
guest = "guest"
member = "member"
admin = "admin"
class RSVPStatus(enum.Enum):
yes = "yes"
no = "no"
maybe = "maybe"
class SubscriptionStatus(enum.Enum):
active = "active"
expired = "expired"
cancelled = "cancelled"
class User(Base):
__tablename__ = "users"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email = Column(String, unique=True, nullable=False, index=True)
password_hash = Column(String, nullable=False)
first_name = Column(String, nullable=False)
last_name = Column(String, nullable=False)
phone = Column(String, nullable=False)
address = Column(String, nullable=False)
city = Column(String, nullable=False)
state = Column(String, nullable=False)
zipcode = Column(String, nullable=False)
date_of_birth = Column(DateTime, nullable=False)
lead_sources = Column(JSON, default=list)
partner_first_name = Column(String, nullable=True)
partner_last_name = Column(String, nullable=True)
partner_is_member = Column(Boolean, default=False)
partner_plan_to_become_member = Column(Boolean, default=False)
referred_by_member_name = Column(String, nullable=True)
status = Column(SQLEnum(UserStatus), default=UserStatus.pending_email, nullable=False)
role = Column(SQLEnum(UserRole), default=UserRole.guest, nullable=False)
email_verified = Column(Boolean, default=False)
email_verification_token = Column(String, nullable=True)
newsletter_subscribed = Column(Boolean, default=False)
# Newsletter Publication Preferences (Step 2)
newsletter_publish_name = Column(Boolean, default=False, nullable=False)
newsletter_publish_photo = Column(Boolean, default=False, nullable=False)
newsletter_publish_birthday = Column(Boolean, default=False, nullable=False)
newsletter_publish_none = Column(Boolean, default=False, nullable=False)
# Volunteer Interests (Step 2)
volunteer_interests = Column(JSON, default=list)
# Scholarship Request (Step 2)
scholarship_requested = Column(Boolean, default=False, nullable=False)
scholarship_reason = Column(Text, nullable=True)
# Directory Settings (Step 3)
show_in_directory = Column(Boolean, default=False, nullable=False)
directory_email = Column(String, nullable=True)
directory_bio = Column(Text, nullable=True)
directory_address = Column(String, nullable=True)
directory_phone = Column(String, nullable=True)
directory_dob = Column(DateTime, nullable=True)
directory_partner_name = Column(String, nullable=True)
# Password Reset Fields
password_reset_token = Column(String, nullable=True)
password_reset_expires = Column(DateTime, nullable=True)
force_password_change = Column(Boolean, default=False, nullable=False)
# Members Only - Profile Photo & Social Media
profile_photo_url = Column(String, nullable=True) # Cloudflare R2 URL
social_media_facebook = Column(String, nullable=True)
social_media_instagram = Column(String, nullable=True)
social_media_twitter = Column(String, nullable=True)
social_media_linkedin = Column(String, nullable=True)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# Relationships
events_created = relationship("Event", back_populates="creator")
rsvps = relationship("EventRSVP", back_populates="user")
subscriptions = relationship("Subscription", back_populates="user", foreign_keys="Subscription.user_id")
class Event(Base):
__tablename__ = "events"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
title = Column(String, nullable=False)
description = Column(Text, nullable=True)
start_at = Column(DateTime, nullable=False)
end_at = Column(DateTime, nullable=False)
location = Column(String, nullable=False)
capacity = Column(Integer, nullable=True)
created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
published = Column(Boolean, default=False)
# Members Only - Universal Calendar Export
calendar_uid = Column(String, nullable=True) # Unique iCalendar UID (UUID4-based)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# Relationships
creator = relationship("User", back_populates="events_created")
rsvps = relationship("EventRSVP", back_populates="event", cascade="all, delete-orphan")
gallery_images = relationship("EventGallery", back_populates="event", cascade="all, delete-orphan")
class EventRSVP(Base):
__tablename__ = "event_rsvps"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
event_id = Column(UUID(as_uuid=True), ForeignKey("events.id"), nullable=False)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
rsvp_status = Column(SQLEnum(RSVPStatus), default=RSVPStatus.maybe, nullable=False)
attended = Column(Boolean, default=False)
attended_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# Relationships
event = relationship("Event", back_populates="rsvps")
user = relationship("User", back_populates="rsvps")
class SubscriptionPlan(Base):
__tablename__ = "subscription_plans"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, nullable=False)
description = Column(Text, nullable=True)
price_cents = Column(Integer, nullable=False) # Price in cents (legacy, kept for backward compatibility)
billing_cycle = Column(String, default="yearly", nullable=False) # yearly, monthly, quarterly, lifetime, custom
stripe_price_id = Column(String, nullable=True) # Stripe Price ID (legacy, deprecated)
active = Column(Boolean, default=True)
# Custom billing cycle fields (for recurring date ranges like Jan 1 - Dec 31)
custom_cycle_enabled = Column(Boolean, default=False, nullable=False)
custom_cycle_start_month = Column(Integer, nullable=True) # 1-12
custom_cycle_start_day = Column(Integer, nullable=True) # 1-31
custom_cycle_end_month = Column(Integer, nullable=True) # 1-12
custom_cycle_end_day = Column(Integer, nullable=True) # 1-31
# Dynamic pricing fields
minimum_price_cents = Column(Integer, default=3000, nullable=False) # $30 minimum
suggested_price_cents = Column(Integer, nullable=True) # Suggested price (can be higher than minimum)
allow_donation = Column(Boolean, default=True, nullable=False) # Allow members to add donations
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# Relationships
subscriptions = relationship("Subscription", back_populates="plan")
class Subscription(Base):
__tablename__ = "subscriptions"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
plan_id = Column(UUID(as_uuid=True), ForeignKey("subscription_plans.id"), nullable=False)
stripe_subscription_id = Column(String, nullable=True) # Stripe Subscription ID
stripe_customer_id = Column(String, nullable=True) # Stripe Customer ID
status = Column(SQLEnum(SubscriptionStatus), default=SubscriptionStatus.active, nullable=False)
start_date = Column(DateTime, nullable=False)
end_date = Column(DateTime, nullable=True)
amount_paid_cents = Column(Integer, nullable=True) # Total amount paid in cents (base + donation)
# Donation tracking fields (for transparency and tax reporting)
base_subscription_cents = Column(Integer, nullable=False) # Plan base price (minimum)
donation_cents = Column(Integer, default=0, nullable=False) # Additional donation amount
# Note: amount_paid_cents = base_subscription_cents + donation_cents
# Manual payment fields
manual_payment = Column(Boolean, default=False, nullable=False) # Whether this was a manual offline payment
manual_payment_notes = Column(Text, nullable=True) # Admin notes about the payment
manual_payment_admin_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) # Admin who processed the payment
manual_payment_date = Column(DateTime, nullable=True) # Date payment was received
payment_method = Column(String, nullable=True) # Payment method: stripe, cash, bank_transfer, check, other
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# Relationships
user = relationship("User", back_populates="subscriptions", foreign_keys=[user_id])
plan = relationship("SubscriptionPlan", back_populates="subscriptions")
class EventGallery(Base):
__tablename__ = "event_galleries"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
event_id = Column(UUID(as_uuid=True), ForeignKey("events.id"), nullable=False)
image_url = Column(String, nullable=False) # Cloudflare R2 URL
image_key = Column(String, nullable=False) # R2 object key for deletion
caption = Column(Text, nullable=True)
uploaded_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
file_size_bytes = Column(Integer, nullable=False)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
# Relationships
event = relationship("Event", back_populates="gallery_images")
uploader = relationship("User")
class NewsletterArchive(Base):
__tablename__ = "newsletter_archives"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
title = Column(String, nullable=False)
description = Column(Text, nullable=True)
published_date = Column(DateTime, nullable=False)
document_url = Column(String, nullable=False) # Google Docs URL or R2 URL
document_type = Column(String, default="google_docs") # google_docs, pdf, upload
file_size_bytes = Column(Integer, nullable=True) # For uploaded files
created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# Relationships
creator = relationship("User")
class FinancialReport(Base):
__tablename__ = "financial_reports"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
year = Column(Integer, nullable=False)
title = Column(String, nullable=False) # e.g., "2024 Annual Report"
document_url = Column(String, nullable=False) # Google Drive URL or R2 URL
document_type = Column(String, default="google_drive") # google_drive, pdf, upload
file_size_bytes = Column(Integer, nullable=True) # For uploaded files
created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# Relationships
creator = relationship("User")
class BylawsDocument(Base):
__tablename__ = "bylaws_documents"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
title = Column(String, nullable=False)
version = Column(String, nullable=False) # e.g., "v1.0", "v2.0"
effective_date = Column(DateTime, nullable=False)
document_url = Column(String, nullable=False) # Google Drive URL or R2 URL
document_type = Column(String, default="google_drive") # google_drive, pdf, upload
file_size_bytes = Column(Integer, nullable=True) # For uploaded files
is_current = Column(Boolean, default=True) # Only one should be current
created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
# Relationships
creator = relationship("User")
class StorageUsage(Base):
__tablename__ = "storage_usage"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
total_bytes_used = Column(BigInteger, default=0)
max_bytes_allowed = Column(BigInteger, nullable=False) # From .env
last_updated = Column(DateTime, default=lambda: datetime.now(timezone.utc))