256:class CampaignRecipient(Base, TimestampMixin): 257- """Materialized campaign×recipient rows with generated values & audit info.""" 258- __tablename__ = "campaign_recipients" 259- id = Column(Integer, primary_key=True) 260- campaign_id = Column(Integer, ForeignKey("campaigns.id", ondelete="CASCADE"), nullable=False, index=True) 261- recipient_id = Column(Integer, ForeignKey("recipients.id", ondelete="CASCADE"), nullable=False) 262- email = Column(String(320), nullable=False, index=True) 263- status = Column(String(30), default="pending") # pending|queued|processing|sent|temp_failed|perm_failed|suppressed|skipped 264- generated_invoice_number = Column(String(100), default="") 265- generated_amount = Column(Float) 266- generated_shipping = Column(Float) 267- generated_subject_id = Column(Integer, ForeignKey("subject_lines.id", ondelete="SET NULL")) 268- generated_template_id = Column(Integer, ForeignKey("html_templates.id", ondelete="SET NULL")) 269- generated_phone = Column(String(60), default="") 270- assigned_account_id = Column(Integer, ForeignKey("gmail_accounts.id", ondelete="SET NULL")) 271- attempt_count = Column(Integer, default=0) 272- last_error = Column(Text, default="") 273- msg_id = Column(String(200), default="") # Gmail message id 274- thread_id = Column(String(200), default="") # Gmail thread id 275- sent_at = Column(DateTime(timezone=True)) 276- seq = Column(Integer, default=0) # ordering within campaign 277- 278- campaign = relationship("Campaign") 279- recipient = relationship("Recipient") 280- subject = relationship("SubjectLine") 281- template = relationship("EmailTemplate") 282- account = relationship("GmailAccount") 283- 284- __table_args__ = (UniqueConstraint("campaign_id", "recipient_id", name="uq_campaign_recipient"), 285- Index("ix_campaign_recipient_status", "campaign_id", "status"),) 286- 287- 288-class Attachment(Base, TimestampMixin): 64:class GmailAccount(Base, TimestampMixin): 65- __tablename__ = "gmail_accounts" 66- id = Column(Integer, primary_key=True) 67- user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) 68- email = Column(String(320), nullable=False) 69- display_name = Column(String(200), default="") 70- status = Column(String(30), default="pending") # pending|authorized|expired|revoked|error 71- sending_method = Column(String(20), default="api") # api|smtp 72- auth_provider = Column(String(60), default="google") 73- google_user_id = Column(String(120), default="") 74- # Per-account config (administrator-configurable) 75- daily_limit = Column(Integer, default=200) # provider/configured daily cap 76- campaign_limit = Column(Integer, default=200) # per-account per-campaign cap 77- sends_today = Column(Integer, default=0) 78- last_send_date = Column(DateTime(timezone=True)) 79- cooldown_until = Column(DateTime(timezone=True)) 80- last_success_at = Column(DateTime(timezone=True)) 81- last_error = Column(Text, default="") 82- error_count = Column(Integer, default=0) 83- token_expiry = Column(DateTime(timezone=True)) 84- # Imported client config reference (optional; may use server-level config) 85- oauth_config_id = Column(Integer, ForeignKey("oauth_client_configs.id", ondelete="SET NULL")) 86- # Encrypted SMTP OAuth2 credentials mirror (kept in oauth_tokens; this is metadata only) 87- is_active = Column(Boolean, default=True, nullable=False) 88- revoked_by_user = Column(Boolean, default=False) 89- revoked_at = Column(DateTime(timezone=True)) 90- 91- user = relationship("User", back_populates="gmail_accounts") 92- oauth_token = relationship("OAuthToken", back_populates="account", uselist=False, 93- cascade="all, delete-orphan") 94- oauth_config = relationship("OAuthClientConfig") 95- 96- __table_args__ = (UniqueConstraint("user_id", "email", name="uq_gmail_account_user_email"), 97- Index("ix_gmail_status", "status"),) 98- 99- def usage_summary(self): 100- return {"daily_limit": self.daily_limit, "sends_today": self.sends_today, 101- "campaign_limit": self.campaign_limit, 102- "last_success_at": self.last_success_at.isoformat() if self.last_success_at else None, 103- "errors": self.error_count, "status": self.status} 104-