pif / app /models.py
pramodmisra's picture
Add draft saving, user dashboard, and rename to Commission Agreement Intake Form
c5de9ee
"""
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)