from sqlalchemy import ( Column, Integer, String, DateTime, Boolean, Enum as SQLEnum, Float, Text, ForeignKey, Date, UniqueConstraint ) from sqlalchemy.orm import relationship import enum from ..core.database import Base from ..core.datetime import utc_now class UserRole(str, enum.Enum): MEMBER = "member" ADMIN = "admin" SUPER_ADMIN = "super_admin" class MembershipStatus(str, enum.Enum): ACTIVE = "active" EXPIRED = "expired" PENDING = "pending" CANCELLED = "cancelled" class PaymentStatus(str, enum.Enum): PENDING = "pending" COMPLETED = "completed" FAILED = "failed" REFUNDED = "refunded" class PaymentMethod(str, enum.Enum): SQUARE = "square" CASH = "cash" CHECK = "check" DUMMY = "dummy" class EventStatus(str, enum.Enum): DRAFT = "draft" PUBLISHED = "published" CANCELLED = "cancelled" COMPLETED = "completed" class RSVPStatus(str, enum.Enum): PENDING = "pending" ATTENDING = "attending" NOT_ATTENDING = "not_attending" MAYBE = "maybe" class EspReaderType(str, enum.Enum): CHECKIN_CHECKOUT = "checkin_checkout" class EspReaderProvisioningStatus(str, enum.Enum): PENDING = "pending" APPROVED = "approved" PROVISIONED = "provisioned" REJECTED = "rejected" class EspTapAction(str, enum.Enum): CHECK_IN = "check_in" CHECK_OUT = "check_out" DENIED = "denied" UNKNOWN = "unknown" class AttendanceCheckoutSource(str, enum.Enum): USER = "user" SYSTEM = "system" class RfidWriteJobStatus(str, enum.Enum): PENDING = "pending" CLAIMED = "claimed" COMPLETED = "completed" FAILED = "failed" CANCELLED = "cancelled" class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) email = Column(String(255), unique=True, index=True, nullable=False) hashed_password = Column(String(255), nullable=False) first_name = Column(String(100), nullable=False) last_name = Column(String(100), nullable=False) phone = Column(String(20), nullable=True) address = Column(Text, nullable=True) role = Column(SQLEnum(UserRole, values_callable=lambda x: [e.value for e in x]), default=UserRole.MEMBER, nullable=False) volunteer_level = Column(String(50), nullable=True) is_active = Column(Boolean, default=True, nullable=False) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) last_login = Column(DateTime, nullable=True) # Relationships memberships = relationship("Membership", back_populates="user", cascade="all, delete-orphan") payments = relationship("Payment", back_populates="user", cascade="all, delete-orphan") event_rsvps = relationship("EventRSVP", back_populates="user", cascade="all, delete-orphan") volunteer_assignments = relationship("VolunteerAssignment", back_populates="user", cascade="all, delete-orphan") certificates = relationship("Certificate", back_populates="user", cascade="all, delete-orphan") profile_answers = relationship( "UserProfileAnswer", back_populates="user", cascade="all, delete-orphan", foreign_keys="UserProfileAnswer.user_id" ) rfid_cards = relationship("RfidCard", back_populates="user") attendance_sessions = relationship("AttendanceSession", back_populates="user") class ProfileQuestion(Base): __tablename__ = "profile_questions" id = Column(Integer, primary_key=True, index=True) key = Column(String(100), unique=True, nullable=False, index=True) label = Column(String(255), nullable=False) help_text = Column(Text, nullable=True) input_type = Column(String(30), nullable=False) # text, number, boolean, date, select placeholder = Column(String(255), nullable=True) options_json = Column(Text, nullable=True) # JSON array: [{"label":"Yes","value":"true"}] is_required = Column(Boolean, default=False, nullable=False) is_active = Column(Boolean, default=True, nullable=False) admin_only_edit = Column(Boolean, default=False, nullable=False) display_order = Column(Integer, default=0, nullable=False) depends_on_question_id = Column(Integer, ForeignKey("profile_questions.id"), nullable=True) depends_on_value = Column(String(255), nullable=True) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) depends_on_question = relationship("ProfileQuestion", remote_side=[id], backref="dependent_questions") answers = relationship("UserProfileAnswer", back_populates="question", cascade="all, delete-orphan") class UserProfileAnswer(Base): __tablename__ = "user_profile_answers" __table_args__ = ( UniqueConstraint("user_id", "question_id", name="uq_user_profile_answer"), ) id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) question_id = Column(Integer, ForeignKey("profile_questions.id"), nullable=False, index=True) value_text = Column(Text, nullable=True) updated_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) user = relationship("User", foreign_keys=[user_id], back_populates="profile_answers") question = relationship("ProfileQuestion", back_populates="answers") updated_by_user = relationship("User", foreign_keys=[updated_by_user_id]) class MembershipTier(Base): __tablename__ = "membership_tiers" id = Column(Integer, primary_key=True, index=True) name = Column(String(100), unique=True, nullable=False) description = Column(Text, nullable=True) annual_fee = Column(Float, nullable=False) benefits = Column(Text, nullable=True) is_active = Column(Boolean, default=True, nullable=False) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) # Relationships memberships = relationship("Membership", back_populates="tier") class Membership(Base): __tablename__ = "memberships" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) tier_id = Column(Integer, ForeignKey("membership_tiers.id"), nullable=False) status = Column(SQLEnum(MembershipStatus, values_callable=lambda x: [e.value for e in x]), default=MembershipStatus.PENDING, nullable=False) start_date = Column(Date, nullable=False) end_date = Column(Date, nullable=False) auto_renew = Column(Boolean, default=False, nullable=False) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) # Relationships user = relationship("User", back_populates="memberships") tier = relationship("MembershipTier", back_populates="memberships") payments = relationship("Payment", back_populates="membership") class Payment(Base): __tablename__ = "payments" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) membership_id = Column(Integer, ForeignKey("memberships.id"), nullable=True) amount = Column(Float, nullable=False) payment_method = Column(SQLEnum(PaymentMethod, values_callable=lambda x: [e.value for e in x]), nullable=False) status = Column(SQLEnum(PaymentStatus, values_callable=lambda x: [e.value for e in x]), default=PaymentStatus.PENDING, nullable=False) transaction_id = Column(String(255), nullable=True) payment_date = Column(DateTime, nullable=True) notes = Column(Text, nullable=True) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) # Relationships user = relationship("User", back_populates="payments") membership = relationship("Membership", back_populates="payments") class Event(Base): __tablename__ = "events" id = Column(Integer, primary_key=True, index=True) title = Column(String(255), nullable=False) description = Column(Text, nullable=True) event_date = Column(DateTime, nullable=False) event_time = Column(String(10), nullable=True) # HH:MM format location = Column(String(255), nullable=True) max_attendees = Column(Integer, nullable=True) status = Column(SQLEnum(EventStatus, values_callable=lambda x: [e.value for e in x]), default=EventStatus.DRAFT, nullable=False) created_by = Column(Integer, ForeignKey("users.id"), nullable=False) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) # Relationships rsvps = relationship("EventRSVP", back_populates="event", cascade="all, delete-orphan") class EventRSVP(Base): __tablename__ = "event_rsvps" id = Column(Integer, primary_key=True, index=True) event_id = Column(Integer, ForeignKey("events.id"), nullable=False) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) status = Column(SQLEnum(RSVPStatus, values_callable=lambda x: [e.value for e in x]), default=RSVPStatus.PENDING, nullable=False) attended = Column(Boolean, default=False, nullable=False) notes = Column(Text, nullable=True) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) # Relationships event = relationship("Event", back_populates="rsvps") user = relationship("User", back_populates="event_rsvps") class EspReader(Base): __tablename__ = "esp_readers" id = Column(Integer, primary_key=True, index=True) device_id = Column(String(100), unique=True, index=True, nullable=False) name = Column(String(255), nullable=False) location = Column(String(255), nullable=True) reader_type = Column(SQLEnum(EspReaderType, values_callable=lambda x: [e.value for e in x]), default=EspReaderType.CHECKIN_CHECKOUT, nullable=False) provisioning_status = Column(SQLEnum(EspReaderProvisioningStatus, values_callable=lambda x: [e.value for e in x]), default=EspReaderProvisioningStatus.PENDING, nullable=False) api_key_hash = Column(String(255), nullable=True) pending_api_key = Column(String(255), nullable=True) registration_token_hash = Column(String(255), nullable=True) is_active = Column(Boolean, default=True, nullable=False) can_write_cards = Column(Boolean, default=False, nullable=False) firmware_version = Column(String(100), nullable=True) last_seen_at = Column(DateTime, nullable=True) approved_at = Column(DateTime, nullable=True) provisioned_at = Column(DateTime, nullable=True) notes = Column(Text, nullable=True) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) taps = relationship("RfidTap", back_populates="reader") attendance_sessions = relationship("AttendanceSession", back_populates="reader") write_jobs = relationship("RfidCardWriteJob", back_populates="reader") class RfidCard(Base): __tablename__ = "rfid_cards" id = Column(Integer, primary_key=True, index=True) uid = Column(String(100), unique=True, index=True, nullable=False) user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True) label = Column(String(255), nullable=True) is_active = Column(Boolean, default=True, nullable=False) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) user = relationship("User", back_populates="rfid_cards") taps = relationship("RfidTap", back_populates="card") class RfidCardWriteJob(Base): __tablename__ = "rfid_card_write_jobs" id = Column(Integer, primary_key=True, index=True) reader_id = Column(Integer, ForeignKey("esp_readers.id"), nullable=False, index=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) card_id = Column(Integer, ForeignKey("rfid_cards.id"), nullable=True, index=True) label = Column(String(255), nullable=False) status = Column(SQLEnum(RfidWriteJobStatus, values_callable=lambda x: [e.value for e in x]), default=RfidWriteJobStatus.PENDING, nullable=False, index=True) requested_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=False) card_uid = Column(String(100), nullable=True, index=True) write_payload = Column(Text, nullable=True) claimed_at = Column(DateTime, nullable=True) completed_at = Column(DateTime, nullable=True) error_message = Column(String(500), nullable=True) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) reader = relationship("EspReader", back_populates="write_jobs") user = relationship("User", foreign_keys=[user_id]) requested_by_user = relationship("User", foreign_keys=[requested_by_user_id]) card = relationship("RfidCard") class RfidTap(Base): __tablename__ = "rfid_taps" id = Column(Integer, primary_key=True, index=True) reader_id = Column(Integer, ForeignKey("esp_readers.id"), nullable=False, index=True) card_id = Column(Integer, ForeignKey("rfid_cards.id"), nullable=True, index=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True) card_uid = Column(String(100), nullable=False, index=True) action = Column(SQLEnum(EspTapAction, values_callable=lambda x: [e.value for e in x]), default=EspTapAction.UNKNOWN, nullable=False) accepted = Column(Boolean, default=False, nullable=False) message = Column(String(255), nullable=True) raw_payload = Column(Text, nullable=True) tapped_at = Column(DateTime, default=utc_now, nullable=False, index=True) created_at = Column(DateTime, default=utc_now, nullable=False) reader = relationship("EspReader", back_populates="taps") card = relationship("RfidCard", back_populates="taps") user = relationship("User") class AttendanceSession(Base): __tablename__ = "attendance_sessions" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) reader_id = Column(Integer, ForeignKey("esp_readers.id"), nullable=False, index=True) check_in_tap_id = Column(Integer, ForeignKey("rfid_taps.id"), nullable=False) check_out_tap_id = Column(Integer, ForeignKey("rfid_taps.id"), nullable=True) checked_in_at = Column(DateTime, nullable=False, index=True) checked_out_at = Column(DateTime, nullable=True, index=True) checkout_source = Column(SQLEnum(AttendanceCheckoutSource, values_callable=lambda x: [e.value for e in x]), nullable=True) system_flag_reason = Column(String(255), nullable=True) duration_seconds = Column(Integer, nullable=True) is_open = Column(Boolean, default=True, nullable=False, index=True) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) user = relationship("User", back_populates="attendance_sessions") reader = relationship("EspReader", back_populates="attendance_sessions") check_in_tap = relationship("RfidTap", foreign_keys=[check_in_tap_id]) check_out_tap = relationship("RfidTap", foreign_keys=[check_out_tap_id]) class VolunteerRole(Base): __tablename__ = "volunteer_roles" id = Column(Integer, primary_key=True, index=True) name = Column(String(100), nullable=False) description = Column(Text, nullable=True) is_active = Column(Boolean, default=True, nullable=False) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) # Relationships assignments = relationship("VolunteerAssignment", back_populates="role", cascade="all, delete-orphan") class VolunteerAssignment(Base): __tablename__ = "volunteer_assignments" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) role_id = Column(Integer, ForeignKey("volunteer_roles.id"), nullable=False) assigned_date = Column(Date, nullable=False) is_active = Column(Boolean, default=True, nullable=False) notes = Column(Text, nullable=True) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) # Relationships user = relationship("User", back_populates="volunteer_assignments") role = relationship("VolunteerRole", back_populates="assignments") schedules = relationship("VolunteerSchedule", back_populates="assignment", cascade="all, delete-orphan") class VolunteerSchedule(Base): __tablename__ = "volunteer_schedules" id = Column(Integer, primary_key=True, index=True) assignment_id = Column(Integer, ForeignKey("volunteer_assignments.id"), nullable=False) schedule_date = Column(Date, nullable=False) start_time = Column(DateTime, nullable=False) end_time = Column(DateTime, nullable=False) location = Column(String(255), nullable=True) notes = Column(Text, nullable=True) completed = Column(Boolean, default=False, nullable=False) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) # Relationships assignment = relationship("VolunteerAssignment", back_populates="schedules") class Certificate(Base): __tablename__ = "certificates" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) certificate_name = Column(String(255), nullable=False) issuing_organization = Column(String(255), nullable=True) issue_date = Column(Date, nullable=False) expiry_date = Column(Date, nullable=True) certificate_number = Column(String(100), nullable=True) file_path = Column(String(500), nullable=True) notes = Column(Text, nullable=True) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) # Relationships user = relationship("User", back_populates="certificates") class File(Base): __tablename__ = "files" id = Column(Integer, primary_key=True, index=True) filename = Column(String(255), nullable=False) original_filename = Column(String(255), nullable=False) file_path = Column(String(500), nullable=False) file_size = Column(Integer, nullable=False) mime_type = Column(String(100), nullable=False) min_tier_id = Column(Integer, ForeignKey("membership_tiers.id"), nullable=True) description = Column(Text, nullable=True) uploaded_by = Column(Integer, ForeignKey("users.id"), nullable=False) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class Notification(Base): __tablename__ = "notifications" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) subject = Column(String(255), nullable=False) message = Column(Text, nullable=False) email_sent = Column(Boolean, default=False, nullable=False) sent_at = Column(DateTime, nullable=True) error_message = Column(Text, nullable=True) created_at = Column(DateTime, default=utc_now, nullable=False) class PasswordResetToken(Base): __tablename__ = "password_reset_tokens" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) token = Column(String(255), unique=True, nullable=False, index=True) expires_at = Column(DateTime, nullable=False) used = Column(Boolean, default=False, nullable=False) created_at = Column(DateTime, default=utc_now, nullable=False) # Relationships user = relationship("User", backref="password_reset_tokens") class EmailTemplate(Base): __tablename__ = "email_templates" id = Column(Integer, primary_key=True, index=True) template_key = Column(String(100), unique=True, nullable=False, index=True) name = Column(String(255), nullable=False) subject = Column(String(255), nullable=False) html_body = Column(Text, nullable=False) text_body = Column(Text, nullable=True) variables = Column(Text, nullable=True) # JSON string of available variables is_active = Column(Boolean, default=True, nullable=False) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False) class BounceType(str, enum.Enum): HARD = "hard" SOFT = "soft" COMPLAINT = "complaint" UNSUBSCRIBE = "unsubscribe" class EmailBounce(Base): __tablename__ = "email_bounces" id = Column(Integer, primary_key=True, index=True) email = Column(String(255), nullable=False, index=True) bounce_type = Column(SQLEnum(BounceType, values_callable=lambda x: [e.value for e in x]), nullable=False) bounce_reason = Column(String(500), nullable=True) smtp2go_message_id = Column(String(255), nullable=True, index=True) bounce_date = Column(DateTime, nullable=False) is_active = Column(Boolean, default=True, nullable=False) created_at = Column(DateTime, default=utc_now, nullable=False) updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)