101:async def google_start_impl(user): 102- """Shared implementation: generate state, persist, return Google's authorization URL.""" 103- try: 104- client = gmail_service.resolve_client_config() 105- except gmail_service.GmailOauthError as e: 106- # Be honest: no client config -> no authorization URL (never fabricate one) 107- raise HTTPException(400, f"Google OAuth is not configured: {e}") 108- state = gmail_service.generate_state() 109- if _oauth_redis is not None: 110- _oauth_redis.setex(f"oauthstate:{state}", 600, json.dumps({"user_id": user.id})) 111- else: 112- _pending_oauth_states[state] = {"user_id": user.id} 113- auth_url = gmail_service.build_authorization_url(client, state) 114- return {"authorization_url": auth_url} 115- 116- 117-_pending_oauth_states: dict = {} 118- 119- 120-@router.get("/google/start") 121-async def google_start(request: Request, user=Depends(get_current_user)): 122: return await google_start_impl(user) 123- 124- 125-@router.get("/google/callback") 126-async def google_callback(request: Request, code: str = "", state: str = "", error: str = "", 127- db: AsyncSession = Depends(get_db)): 128- """Handle Google's response: verify state, exchange code for tokens, store encrypted.""" 129- from app.models import GmailAccount, OAuthClientConfig, OAuthToken 130- 131- if error: 132- return HTMLResponse(f"

Google authorization failed: {error}

" 133- f"

Back

") 134- if not code or not state: 135- raise HTTPException(400, "Missing code or state") 136- 137- # 1) verify state (CSRF protection of OAuth handshake) 138- user_id = None 139- if _oauth_redis is not None: 140- raw = _oauth_redis.get(f"oauthstate:{state}") === path '' === plan keys 201:async def materialize_campaign(db: AsyncSession, campaign: Campaign, recipient_rows: Sequence[Recipient], 202- preview: bool = False, preview_size: int = 0) -> dict: 203- """Create CampaignRecipient rows with generated values & assignments. 204- 205- This is the core deterministic plan: which subject/template/phone/account each recipient gets, 206- plus generated invoice numbers (unique per campaign), amounts, shipping. -- 275: plan.append({"seq": seq, "email": r.email, "invoice": inv_num, "amount": g.amount, 276- "subject": subj.subject if subj else "", "template": tpl.name if tpl else None, 277: "phone": phone, "account": account.email if account else None, 278- "account_id": account.id if account else None, 279- "recipient_id": r.id, "subject_id": subj.id if subj else None, 280- "template_id": tpl.id if tpl else None, 281- "invoice_number": inv_num, 282- "invoice_date": g.invoice_date, "due_date": g.due_date,