Spaces:
Running
Running
| """ | |
| Database models for the Producer Intake application. | |
| Two categories of tables: | |
| 1. AUTH tables β users, roles, organisations, sessions | |
| 2. CONFIG tables β commission rules, bell ringers, dropdowns, constants | |
| (Admin-editable via the Admin Panel) | |
| 3. DATA tables β producers, clients (refreshed daily from BigQuery) | |
| 4. FORM tables β submitted forms, approvals | |
| """ | |
| from datetime import datetime, timezone | |
| from sqlalchemy import ( | |
| Column, Integer, String, Float, Boolean, Text, DateTime, | |
| ForeignKey, UniqueConstraint, JSON, create_engine | |
| ) | |
| from sqlalchemy.orm import declarative_base, relationship, sessionmaker | |
| Base = declarative_base() | |
| # ===================================================================== | |
| # 1. AUTH TABLES | |
| # ===================================================================== | |
| class Organisation(Base): | |
| __tablename__ = "organisations" | |
| id = Column(Integer, primary_key=True, autoincrement=True) | |
| name = Column(String(200), unique=True, nullable=False) | |
| is_active = Column(Boolean, default=True) | |
| created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) | |
| users = relationship("User", back_populates="organisation") | |
| class User(Base): | |
| __tablename__ = "users" | |
| id = Column(Integer, primary_key=True, autoincrement=True) | |
| email = Column(String(255), unique=True, nullable=False, index=True) | |
| hashed_password = Column(String(255), nullable=False) | |
| full_name = Column(String(200), nullable=False) | |
| role = Column(String(20), nullable=False, default="user") # user | admin | superadmin | |
| org_id = Column(Integer, ForeignKey("organisations.id"), nullable=True) | |
| is_active = Column(Boolean, default=True) | |
| created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) | |
| reset_token = Column(String(255), nullable=True) | |
| reset_token_exp = Column(DateTime, nullable=True) | |
| organisation = relationship("Organisation", back_populates="users") | |
| # ===================================================================== | |
| # 2. CONFIG TABLES (Admin-editable) | |
| # ===================================================================== | |
| class CommissionAgreement(Base): | |
| """ | |
| Each row is one agreement (e.g. 'Standard Agreement'). | |
| The year-tier details live in CommissionTier. | |
| """ | |
| __tablename__ = "commission_agreements" | |
| id = Column(Integer, primary_key=True, autoincrement=True) | |
| name = Column(String(200), unique=True, nullable=False) | |
| p1_suffix = Column(Integer, nullable=True) # 1 or 2 or NULL | |
| p2_suffix = Column(Integer, nullable=True) # 1 or 2 or NULL | |
| ce_as_producer = Column(Boolean, default=False) | |
| display_order = Column(Integer, default=0) # controls dropdown order | |
| is_active = Column(Boolean, default=True) | |
| tiers = relationship("CommissionTier", back_populates="agreement", | |
| cascade="all, delete-orphan", order_by="CommissionTier.sort_order") | |
| class CommissionTier(Base): | |
| """ | |
| Year-tier rows for each agreement. | |
| producer_role is 'P1' or 'P2'. | |
| """ | |
| __tablename__ = "commission_tiers" | |
| id = Column(Integer, primary_key=True, autoincrement=True) | |
| agreement_id = Column(Integer, ForeignKey("commission_agreements.id"), nullable=False) | |
| producer_role = Column(String(2), nullable=False) # 'P1' or 'P2' | |
| years_label = Column(String(50), nullable=False) # 'Year 1', 'All Years', etc. | |
| commission = Column(Float, nullable=True) # NULL = ended | |
| credit = Column(Float, nullable=True) # NULL = ended | |
| agreement_label = Column(String(200), nullable=False) # display name for this tier | |
| note = Column(Text, nullable=True) | |
| flag = Column(Text, nullable=True) # warning text | |
| sort_order = Column(Integer, default=0) | |
| agreement = relationship("CommissionAgreement", back_populates="tiers") | |
| class FixedConstant(Base): | |
| """ | |
| Key-value store for fixed percentages that rarely change. | |
| e.g. CE_COMM=2, CE_CREDIT=0, CC_COMM=5, CE_PROD_COMM=10, etc. | |
| """ | |
| __tablename__ = "fixed_constants" | |
| id = Column(Integer, primary_key=True, autoincrement=True) | |
| key = Column(String(50), unique=True, nullable=False) | |
| value = Column(Float, nullable=False) | |
| description = Column(String(200), nullable=True) | |
| class BellRingerThreshold(Base): | |
| """Each row is one market_segment + dept_code combination.""" | |
| __tablename__ = "bell_ringer_thresholds" | |
| id = Column(Integer, primary_key=True, autoincrement=True) | |
| market_segment = Column(String(50), nullable=False) # 'Middle Market', 'Emerging Markets' | |
| dept_code = Column(String(10), nullable=False) # 'CL', 'EB', 'PL' | |
| threshold = Column(Float, nullable=False) | |
| __table_args__ = ( | |
| UniqueConstraint("market_segment", "dept_code", name="uq_bell_seg_dept"), | |
| ) | |
| class DropdownOption(Base): | |
| """ | |
| Generic table for admin-managed dropdown values. | |
| category = 'association' | 'department' | 'market_segment' | 'employee' | |
| """ | |
| __tablename__ = "dropdown_options" | |
| id = Column(Integer, primary_key=True, autoincrement=True) | |
| category = Column(String(50), nullable=False, index=True) | |
| label = Column(String(200), nullable=False) # display text | |
| value = Column(String(100), nullable=False) # internal value | |
| display_order = Column(Integer, default=0) | |
| is_active = Column(Boolean, default=True) | |
| __table_args__ = ( | |
| UniqueConstraint("category", "value", name="uq_dropdown_cat_val"), | |
| ) | |
| class EmailRoutingRule(Base): | |
| """ | |
| Configurable email routing for completed forms. | |
| condition: 'always' | 'bell_ringer' | 'dept_cl' | 'dept_bd' | 'submitter' | |
| """ | |
| __tablename__ = "email_routing_rules" | |
| id = Column(Integer, primary_key=True, autoincrement=True) | |
| condition = Column(String(50), nullable=False) | |
| email = Column(String(255), nullable=False) | |
| description = Column(String(200), nullable=True) | |
| is_active = Column(Boolean, default=True) | |
| # ===================================================================== | |
| # 3. DATA TABLES (Refreshed daily from BigQuery) | |
| # ===================================================================== | |
| class Producer(Base): | |
| """Loaded from Team Mapping BigQuery table.""" | |
| __tablename__ = "producers" | |
| id = Column(Integer, primary_key=True, autoincrement=True) | |
| code = Column(String(20), nullable=False) # full code e.g. RENCH1 | |
| prefix = Column(String(20), nullable=False, index=True) # e.g. RENCH | |
| suffix = Column(Integer, nullable=False) # 1 or 2 | |
| name = Column(String(200), nullable=False) # display name | |
| is_active = Column(Boolean, default=True) | |
| email = Column(String(255), nullable=True) | |
| class Client(Base): | |
| """Loaded from ClientExtract BigQuery table.""" | |
| __tablename__ = "clients" | |
| id = Column(Integer, primary_key=True, autoincrement=True) | |
| lookup_code = Column(String(50), nullable=False, index=True) | |
| name = Column(String(300), nullable=False) | |
| is_active = Column(Boolean, default=True) | |
| # ===================================================================== | |
| # 4. FORM TABLES (Submissions & Approvals β Phase 4) | |
| # ===================================================================== | |
| class IntakeSubmission(Base): | |
| """A form submission β can be a draft or a submitted form.""" | |
| __tablename__ = "intake_submissions" | |
| id = Column(Integer, primary_key=True, autoincrement=True) | |
| submitted_by = Column(Integer, ForeignKey("users.id"), nullable=False) | |
| submitted_at = Column(DateTime, nullable=True) # NULL for drafts | |
| updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), | |
| onupdate=lambda: datetime.now(timezone.utc)) | |
| status = Column(String(20), default="draft") # draft | pending_approval | approved | rejected | |
| title = Column(String(300), nullable=True) # auto-generated label for dashboard | |
| # Form data stored as JSON blob for flexibility | |
| form_data = Column(JSON, nullable=False) | |
| # Computed summary stored for the PDF | |
| summary_data = Column(JSON, nullable=True) | |
| bell_ringer_triggered = Column(Boolean, default=False) | |
| dept_code = Column(String(10), nullable=True) | |
| approvals = relationship("Approval", back_populates="submission", | |
| cascade="all, delete-orphan") | |
| submitter = relationship("User") | |
| class Approval(Base): | |
| """Each person on the account must approve.""" | |
| __tablename__ = "approvals" | |
| id = Column(Integer, primary_key=True, autoincrement=True) | |
| submission_id = Column(Integer, ForeignKey("intake_submissions.id"), nullable=False) | |
| approver_name = Column(String(200), nullable=False) | |
| approver_email = Column(String(255), nullable=True) | |
| producer_code = Column(String(20), nullable=True) | |
| role_label = Column(String(50), nullable=False) # 'Producer 1', 'CE', etc. | |
| status = Column(String(20), default="pending") # pending | approved | rejected | |
| responded_at = Column(DateTime, nullable=True) | |
| reminder_sent = Column(DateTime, nullable=True) | |
| approval_token = Column(String(64), unique=True, nullable=True, index=True) | |
| submission = relationship("IntakeSubmission", back_populates="approvals") | |
| # ===================================================================== | |
| # DB ENGINE & SESSION FACTORY | |
| # ===================================================================== | |
| def get_engine(db_url: str = "sqlite:///./data/app.db"): | |
| return create_engine(db_url, connect_args={"check_same_thread": False}) | |
| def create_tables(engine): | |
| Base.metadata.create_all(bind=engine) | |
| def get_session_factory(engine): | |
| return sessionmaker(autocommit=False, autoflush=False, bind=engine) | |