"size_bytes": att.size_bytes, "file_sha256": att.file_sha256, "description": att.description, "is_html_attachment": bool(att.is_html_attachment), "created_at": att.created_at.isoformat() if att.created_at else None, } async def _get_own(db: AsyncSession, user: User, att_id: int) -> Attachment: === logs list === async def list_logs(campaign_id: int = Query(0), outcome: str = Query("", description="submitted|accepted|temp_failed|perm_failed|skipped"), search: str = Query("", description="email or message id fragment"), page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=200), db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """Unified send log: SendAttempt rows joined with CampaignRecipient for context.""" q = (select(SendAttempt, CampaignRecipient) .join(CampaignRecipient, SendAttempt.campaign_recipient_id == CampaignRecipient.id) .join(Campaign, Campaign.id == SendAttempt.campaign_id) .where(Campaign.user_id == user.id)) if campaign_id: q = q.where(SendAttempt.campaign_id == campaign_id) if outcome: q = q.where(SendAttempt.outcome == outcome) if search: like = f"%{search}%" q = q.where(or_(CampaignRecipient.email.ilike(like), SendAttempt.message_id.ilike(like))) total = (await db.execute( select(func.count()).select_from(q.subquery()))).scalar() or 0 rows = (await db.execute( q.order_by(SendAttempt.id.desc()).offset((page - 1) * page_size).limit(page_size) )).all() out = [] for a, cr in rows: d = _ser_attempt(a) d["email"] = cr.email d["recipient_status"] = cr.status out.append(d) === settings keys === 32: "amount_min": {"type": "float", "min": 0, "max": 100000, "default": cfg.AMOUNT_MIN, 34: "amount_max": {"type": "float", "min": 0, "max": 100000, "default": cfg.AMOUNT_MAX, 36: "shipping_min": {"type": "float", "min": 0, "max": 10000, "default": cfg.SHIPPING_MIN, 38: "shipping_max": {"type": "float", "min": 0, "max": 10000, "default": cfg.SHIPPING_MAX, 40: "invoice_format": {"type": "str", "max_len": 80, "default": cfg.INVOICE_NUMBER_FORMAT, 43: "invoice_prefix": {"type": "str", "max_len": 12, "default": "INV", 45: "default_daily_limit": {"type": "int", "min": 1, "max": 2000, "default": 500, 47: "default_campaign_limit": {"type": "int", "min": 1, "max": 2000, "default": 200, 49: "default_max_per_account": {"type": "int", "min": 1, "max": 2000, "default": 200, 51: "default_send_interval": {"type": "float", "min": 0.5, "max": 3600, "default": 1.0, 53: "default_rate_per_minute": {"type": "int", "min": 1, "max": 600, "default": 60, 55: "attachment_kind": {"type": "str", "max_len": 20, "default": "invoice", 58: "unsubscribe_enabled": {"type": "bool", "default": True, 60: "unsubscribe_base_url": {"type": "str", "max_len": 300, "default": "", 63: "company_name": {"type": "str", "max_len": 200, "default": "", 65: "test_email_default": {"type": "str", "max_len": 320, "default": "",