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