PHONE_RE = re.compile(r"^\+?[0-9][0-9 ()\-.]{4,30}$") class PhoneIn(BaseModel): phone: str label: str = "" @field_validator("phone") @classmethod def valid_phone(cls, v): v = v.strip() if not PHONE_RE.match(v): raise ValueError("Invalid phone number format") return v class PhonePatch(BaseModel): label: Optional[str] = None is_active: Optional[bool] = None def _serialize(p: CompanyPhoneNumber) -> dict: return { "id": p.id, "label": p.label, "phone": p.phone, "is_active": bool(p.is_active), "use_count": p.use_count, "created_at": p.created_at.isoformat() if p.created_at else None, } async def _get_own(db: AsyncSession, user: User, pid: int) -> CompanyPhoneNumber: p = await db.get(CompanyPhoneNumber, pid) if not p or p.user_id != user.id: raise HTTPException(status_code=404, detail="Phone number not found") return p @router.get("/list") async def list_phones(active_only: bool = Query(False), search: str = Query(""), ===SUPP=== def _norm_reason(r: str) -> str: r = (r or "").strip().lower() if r == "complained": r = "complaint" return r if r in REASON_CHOICES else "manual" class SuppressionIn(BaseModel): email: EmailStr reason: str = "manual" source: str = "" class BulkEmails(BaseModel): emails: list[EmailStr] reason: str = "manual" class CheckEmails(BaseModel): emails: list[EmailStr] def _serialize(s: SuppressionEntry) -> dict: return { "id": s.id, "email": s.email, "reason": s.reason, "source": s.source, "campaign_id": s.campaign_id, "created_at": s.created_at.isoformat() if s.created_at else None, } @router.get("/list") async def list_suppression(search: str = Query(""), ===SET=== class SettingIn(BaseModel): key: str = Field(min_length=1, max_length=150) value: object = None category: str = "general" description: str = "" class BatchSettingsIn(BaseModel): values: dict # Known settings keys with validation constraints SETTING_DEFS = { "amount_min": {"type": "float", "min": 0, "max": 100000, "default": cfg.AMOUNT_MIN, "label": "Random amount minimum ($)", "category": "financial"}, "amount_max": {"type": "float", "min": 0, "max": 100000, "default": cfg.AMOUNT_MAX, "label": "Random amount maximum ($)", "category": "financial"}, "shipping_min": {"type": "float", "min": 0, "max": 10000, "default": cfg.SHIPPING_MIN, "label": "Random shipping minimum ($)", "category": "financial"}, "shipping_max": {"type": "float", "min": 0, "max": 10000, "default": cfg.SHIPPING_MAX, "label": "Random shipping maximum ($)", "category": "financial"}, "invoice_format": {"type": "str", "max_len": 80, "default": cfg.INVOICE_NUMBER_FORMAT, "label": "Invoice number format (tokens: {prefix},{R#},{r#},{d#},{seq4},{date},{hash})", "category": "financial"}, "invoice_prefix": {"type": "str", "max_len": 12, "default": "INV", "label": "Invoice prefix", "category": "financial"}, "default_daily_limit": {"type": "int", "min": 1, "max": 2000, "default": 500, "label": "Default per-account daily send limit", "category": "limits"}, "default_campaign_limit": {"type": "int", "min": 1, "max": 2000, "default": 200, "label": "Default per-account per-campaign limit", "category": "limits"}, "default_max_per_account": {"type": "int", "min": 1, "max": 2000, "default": 200, "label": "Default max sends per account per campaign", "category": "limits"}, "default_send_interval": {"type": "float", "min": 0.5, "max": 3600, "default": 1.0, "label": "Default seconds between sends per account", "category": "limits"}, "default_rate_per_minute": {"type": "int", "min": 1, "max": 600, "default": 60, "label": "Default max sends per minute (per account)", "category": "limits"}, "attachment_kind": {"type": "str", "max_len": 20, "default": "invoice", "label": "Attachment filename prefix (invoice|order|receipt)", "category": "attachments"}, "unsubscribe_enabled": {"type": "bool", "default": True, "label": "Enable unsubscribe links globally", "category": "compliance"}, "unsubscribe_base_url": {"type": "str", "max_len": 300, "default": "", "label": "Base URL for unsubscribe links (e.g. https://your-domain.com)", "category": "compliance"}, "company_name": {"type": "str", "max_len": 200, "default": "", "label": "Company name for %CompanyName% default", "category": "general"}, "test_email_default": {"type": "str", "max_len": 320, "default": "", "label": "Default test email recipient", "category": "general"}, } def _validate_value(key: str, value) -> tuple: