""" Demo routes — each /demo/step/N page shows one Tink API call with the live JSON response and a curl example. """ import json import uuid import secrets from typing import Optional from fastapi import APIRouter, Request, Form, HTTPException from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from src.tink.client import TinkClient from src.config import get_settings from src import demo_data router = APIRouter() templates = Jinja2Templates(directory="src/templates") def _client() -> TinkClient: s = get_settings() return TinkClient( client_id=s.tink_client_id, client_secret=s.tink_client_secret, redirect_uri=s.tink_redirect_uri, api_base=s.tink_api_base, link_base=s.tink_link_base, ) def _session(request: Request) -> dict: return request.session.setdefault("demo", {}) # --------------------------------------------------------------------------- # Landing # --------------------------------------------------------------------------- @router.get("/", response_class=HTMLResponse) async def index(request: Request): return templates.TemplateResponse("index.html", {"request": request}) @router.get("/demo/reset") async def reset_demo(request: Request): """Clear all demo session state and restart from Step 1.""" request.session.pop("demo", None) return RedirectResponse("/demo/step/1") # --------------------------------------------------------------------------- # Step 1 — Authenticate (client credentials) # --------------------------------------------------------------------------- @router.get("/demo/step/1", response_class=HTMLResponse) async def step1(request: Request): client = _client() s = get_settings() error = None result = None curl_example = ( f"curl -X POST https://api.tink.com/api/v1/oauth/token \\\n" f" -d 'client_id={s.tink_client_id}' \\\n" f" -d 'client_secret=***' \\\n" f" -d 'grant_type=client_credentials' \\\n" f" -d 'scope=user:create'" ) try: result = await client.get_app_token(scope="user:create") _session(request)["app_token"] = result["access_token"] except Exception as e: error = str(e) return templates.TemplateResponse("step.html", { "request": request, "step": 1, "title": "Authenticate", "subtitle": "Client Credentials Flow", "endpoint": "POST /api/v1/oauth/token", "api_version": "v1", "description": "Vi starter med at hente et app-level access token via Client Credentials flow. Dette token bruges til at oprette brugere.", "curl_example": curl_example, "result": result, "error": error, "next_step": 2, "prev_step": None, }) # --------------------------------------------------------------------------- # Step 2 — Create User # --------------------------------------------------------------------------- @router.get("/demo/step/2", response_class=HTMLResponse) async def step2_get(request: Request): sess = _session(request) s = get_settings() app_token = sess.get("app_token", "") external_user_id = f"moneycapp-demo-{secrets.token_hex(4)}" body = json.dumps({"external_user_id": external_user_id, "market": "DK", "locale": "da_DK"}, indent=2) curl_example = ( f"curl -X POST https://api.tink.com/api/v1/user/create \\\n" f" -H 'Authorization: Bearer $APP_TOKEN' \\\n" f" -H 'Content-Type: application/json' \\\n" f" -d '{body}'" ) error = None result = None if not app_token: error = "Mangler app token — gå tilbage til Step 1 og kør authentication først." else: try: client = _client() result = await client.create_user(app_token, external_user_id) sess["user_id"] = result.get("user_id", "") sess["external_user_id"] = external_user_id except Exception as e: error = str(e) return templates.TemplateResponse("step.html", { "request": request, "step": 2, "title": "Opret Bruger", "subtitle": "Create Test Customer", "endpoint": "POST /api/v1/user/create", "api_version": "v1", "description": "Vi opretter en ny Tink-bruger med et unikt external_user_id — det er dit interne kunde-ID. Tink returnerer et user_id som vi bruger i de næste kald.", "curl_example": curl_example, "result": result, "error": error, "next_step": 3, "prev_step": 1, }) # --------------------------------------------------------------------------- # Step 3 — Connect Bank (Tink Link redirect) # --------------------------------------------------------------------------- CONSOLE_CALLBACK = "https://console.tink.com/callback" @router.get("/demo/step/3", response_class=HTMLResponse) async def step3_get(request: Request): sess = _session(request) s = get_settings() client = _client() tink_link_url = client.get_tink_link_url(market="DK") dev_tink_link_url = client.get_tink_link_url( market="DK", redirect_uri_override=CONSOLE_CALLBACK ) error = None credentials = None cb_success = request.query_params.get("cb_success") cb_error = request.query_params.get("cb_error") # Demo mode: auto-mark as connected with mock data if s.demo_mode and not sess.get("user_token"): sess["user_token"] = "demo-mode-token" sess["demo_mode"] = True # Check if already connected (returning from callback) user_token = sess.get("user_token", "") if user_token: if sess.get("demo_mode"): credentials = demo_data.MOCK_CREDENTIALS cb_success = cb_success or "demo" else: try: credentials = await client.list_credentials(user_token) except Exception as e: error = str(e) curl_example = ( "# Simpelt Tink Link flow — ingen pre-oprettet bruger nødvendig\n" f"# Redirect brugeren direkte til:\n" f"https://link.tink.com/1.0/transactions/connect-accounts\n" f" ?client_id=$CLIENT_ID\n" f" &redirect_uri=$REDIRECT_URI\n" f" &market=DK\n" f" &scope=accounts:read,transactions:read,credentials:read\n\n" "# Tink Link redirecter tilbage med ?code=... som du exchangeer for user token:\n" "curl -X POST https://api.tink.com/api/v1/oauth/token \\\n" " -d 'grant_type=authorization_code' \\\n" " -d 'client_id=$CLIENT_ID' \\\n" " -d 'client_secret=$CLIENT_SECRET' \\\n" " -d 'code=$CODE'" ) return templates.TemplateResponse("step.html", { "request": request, "step": 3, "title": "Tilslut Bank", "subtitle": "Tink Link — Bank Connection", "endpoint": "Tink Link /1.0/transactions/connect-accounts", "api_version": "Link v1", "description": "Brugeren åbner Tink Link, vælger Tink Demo Bank og logger ind med test-credentials fra Console. Tink redirecter tilbage med en authorization_code som automatisk exchang'es til et user token.", "curl_example": curl_example, "result": credentials, "tink_link_url": tink_link_url, "dev_tink_link_url": dev_tink_link_url, "error": error or (f"Callback fejl: {cb_error}" if cb_error else None), "cb_success": cb_success, "next_step": 4, "prev_step": 2, }) @router.post("/demo/step/3", response_class=HTMLResponse) async def step3_post(request: Request, code: str = Form(...)): """Manual code entry — exchange a code obtained via console.tink.com/callback.""" sess = _session(request) try: client = _client() tokens = await client.exchange_code_for_token( code=code.strip(), redirect_uri=CONSOLE_CALLBACK, ) sess["user_token"] = tokens.get("access_token", "") return RedirectResponse("/demo/step/3?cb_success=1", status_code=303) except Exception as e: return RedirectResponse(f"/demo/step/3?cb_error={str(e)}", status_code=303) @router.get("/callback", response_class=HTMLResponse) async def tink_callback(request: Request, code: Optional[str] = None, error: Optional[str] = None, credentials_id: Optional[str] = None): """Tink Link OAuth callback — exchange code for user token.""" sess = _session(request) if error: return RedirectResponse(f"/demo/step/3?error={error}") if code: try: client = _client() tokens = await client.exchange_code_for_token(code) sess["user_token"] = tokens.get("access_token", "") except Exception as e: return RedirectResponse(f"/demo/step/3?cb_error={str(e)}") return RedirectResponse("/demo/step/3") # --------------------------------------------------------------------------- # Step 4 — Accounts (v2) # --------------------------------------------------------------------------- @router.get("/demo/step/4", response_class=HTMLResponse) async def step4(request: Request): sess = _session(request) s = get_settings() user_token = sess.get("user_token", "") error = None result = None is_demo = sess.get("demo_mode") or s.demo_mode curl_example = ( "curl https://api.tink.com/data/v2/accounts \\\n" " -H 'Authorization: Bearer $USER_TOKEN'" ) if not user_token and not is_demo: error = "Mangler user token — tilslut en bank i Step 3 først." elif is_demo: result = demo_data.MOCK_ACCOUNTS else: try: client = _client() result = await client.list_accounts(user_token) # Fall back to demo data if no accounts connected yet if not result.get("accounts"): result = demo_data.MOCK_ACCOUNTS sess["demo_mode"] = True except Exception as e: error = str(e) return templates.TemplateResponse("step.html", { "request": request, "step": 4, "title": "Konti", "subtitle": "Account List with Balances", "endpoint": "GET /data/v2/accounts", "api_version": "v2 ✦", "description": "Henter brugerens konti via det nye v2 data endpoint. Returnerer account type, balance, currency og IBAN.", "curl_example": curl_example, "result": result, "error": error, "is_demo": is_demo or (result == demo_data.MOCK_ACCOUNTS), "next_step": 5, "prev_step": 3, }) # --------------------------------------------------------------------------- # Step 5 — Transactions (v2) # --------------------------------------------------------------------------- @router.get("/demo/step/5", response_class=HTMLResponse) async def step5(request: Request, account_id: Optional[str] = None): sess = _session(request) s = get_settings() user_token = sess.get("user_token", "") is_demo = sess.get("demo_mode") or s.demo_mode error = None result = None curl_example = ( "curl 'https://api.tink.com/data/v2/transactions?pageSize=25' \\\n" " -H 'Authorization: Bearer $USER_TOKEN'" ) if not user_token and not is_demo: error = "Mangler user token — tilslut en bank i Step 3 først." elif is_demo: result = demo_data.MOCK_TRANSACTIONS else: try: client = _client() result = await client.list_transactions(user_token, account_id=account_id) if not result.get("transactions"): result = demo_data.MOCK_TRANSACTIONS sess["demo_mode"] = True except Exception as e: error = str(e) return templates.TemplateResponse("step.html", { "request": request, "step": 5, "title": "Transaktioner", "subtitle": "Transaction History", "endpoint": "GET /data/v2/transactions", "api_version": "v2 ✦", "description": "Henter transaktioner via v2 endpoint med paginering. Returnerer amount, description, status (BOOKED/PENDING), og kategorisering.", "curl_example": curl_example, "result": result, "error": error, "is_demo": is_demo or (result == demo_data.MOCK_TRANSACTIONS), "next_step": 6, "prev_step": 4, }) # --------------------------------------------------------------------------- # Step 6 — Events (v2) # --------------------------------------------------------------------------- @router.get("/demo/step/6", response_class=HTMLResponse) async def step6(request: Request): sess = _session(request) s = get_settings() user_token = sess.get("user_token", "") is_demo = sess.get("demo_mode") or s.demo_mode error = None result_booked = None result_all = None curl_booked = ( "curl 'https://api.tink.com/events/v2/account-booked-transactions?pageSize=10' \\\n" " -H 'Authorization: Bearer $USER_TOKEN'" ) curl_all = ( "curl 'https://api.tink.com/events/v2/account-transactions?pageSize=10' \\\n" " -H 'Authorization: Bearer $USER_TOKEN'" ) if not user_token and not is_demo: error = "Mangler user token — tilslut en bank i Step 3 først." elif is_demo: result_booked = demo_data.MOCK_EVENTS_BOOKED result_all = demo_data.MOCK_EVENTS_ALL else: try: client = _client() result_booked = await client.list_booked_transaction_events(user_token, page_size=10) except Exception as e: error = f"Booked events: {e}" try: client = _client() result_all = await client.list_account_transaction_events(user_token, page_size=10) except Exception as e: error = (error or "") + f" | All events: {e}" return templates.TemplateResponse("step6.html", { "request": request, "step": 6, "title": "Events", "subtitle": "Real-time Event Feed", "error": error, "result_booked": result_booked, "result_all": result_all, "curl_booked": curl_booked, "curl_all": curl_all, "is_demo": is_demo, "app_base_url": s.app_base_url, "next_step": None, "prev_step": 5, }) # --------------------------------------------------------------------------- # Webhook receiver # --------------------------------------------------------------------------- @router.post("/webhooks/tink") async def webhook_receiver(request: Request): """Receive Tink webhook events (configure URL in Tink Console).""" body = await request.json() # In production you'd verify the signature and store events print(f"[WEBHOOK] {json.dumps(body, indent=2)}") return {"status": "received"}