Spaces:
Running
Running
File size: 10,154 Bytes
996fcf9 b2253b1 996fcf9 c5de9ee 996fcf9 c5de9ee 996fcf9 b2253b1 996fcf9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | """
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)
|