Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from app.database import engine, Base, AsyncSessionLocal
from app.routers import auth_router, ask, crud, users_router, settings_router, bigquery_router, firebase_router, sql_router, summary_router, logs_router, audio_overview_router, telegram_router
from app.routers import api_keys_router, public_api_router, whatsapp_router, audit_router, webhook_router, automl_router, report_router
from app.routers import dbt_router, github_router, slack_router, mongodb_router, snowflake_router, notion_router, excel_online_router, s3_router, rest_api_router, jira_router
from app.routers import dbt_router, github_router, slack_router, mongodb_router, snowflake_router, notion_router, excel_online_router, s3_router, rest_api_router, jira_router, stripe_router
from app.routers import template_router
from app.routers import hubspot_router
from app.models import User
Expand Down Expand Up @@ -242,6 +242,7 @@ async def httpx_connect_error_handler(_request: Request, exc: httpx.ConnectError
app.include_router(s3_router.router, prefix=prefix)
app.include_router(rest_api_router.router, prefix=prefix)
app.include_router(jira_router.router, prefix=prefix)
app.include_router(stripe_router.router, prefix=prefix)
app.include_router(template_router.router, prefix=prefix)
app.include_router(hubspot_router.router, prefix=prefix)

Expand Down
17 changes: 17 additions & 0 deletions backend/app/routers/ask.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from app.scripts.ask_jira import ask_jira
from app.scripts.ask_notion import ask_notion
from app.scripts.ask_snowflake import ask_snowflake
from app.scripts.ask_stripe import ask_stripe
from app.scripts.ask_sql import ask_sql
from app.scripts.ask_sql_multi import ask_sql_multi_source
from app.scripts.sql_utils import validate_source_relationships
Expand Down Expand Up @@ -474,6 +475,22 @@ async def dispatch_question(
history=history,
channel=channel,
)
elif source.type == "stripe":
meta = source.metadata_ or {}
stripe_api_key = meta.get("apiKey", "")
if not stripe_api_key:
raise HTTPException(400, "Stripe source missing apiKey in metadata")
result = await ask_stripe(
api_key=stripe_api_key,
tables=meta.get("tables", []),
question=question,
agent_description=agent.description or "",
source_name=source.name,
table_infos=meta.get("table_infos"),
llm_overrides=llm_overrides,
history=history,
channel=channel,
)
else:
raise HTTPException(400, f"Unsupported source type: {source.type}")

Expand Down
2 changes: 1 addition & 1 deletion backend/app/routers/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ async def create_source(
user: User = Depends(require_user),
):
"""Create a non-file source (BigQuery, Google Sheets, SQL). Credentials stored locally in metadata."""
valid_types = ("bigquery", "google_sheets", "sql_database", "firebase", "mongodb", "snowflake", "notion", "excel_online", "s3", "rest_api", "jira", "hubspot")
valid_types = ("bigquery", "google_sheets", "sql_database", "firebase", "mongodb", "snowflake", "notion", "excel_online", "s3", "rest_api", "jira", "hubspot", "stripe")
if body.type not in valid_types:
raise HTTPException(400, f"type must be one of: {', '.join(valid_types)}")
source_id = str(uuid.uuid4())
Expand Down
99 changes: 99 additions & 0 deletions backend/app/routers/stripe_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
Stripe discovery and source metadata refresh.
- Test connection (GET /v1/balance)
- Discover resources (fetch sample from each table)
- Refresh source metadata
"""
import asyncio
from fastapi import APIRouter, Depends, HTTPException
from app.auth import require_user
from app.models import User, Source
from app.database import get_db
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.routers.crud import _sanitize_for_json

router = APIRouter(prefix="/stripe", tags=["stripe"])


@router.post("/test-connection")
async def test_connection(body: dict, user: User = Depends(require_user)):
"""Test Stripe API connection. Body: { "apiKey": "sk_..." }."""
api_key = body.get("apiKey") or body.get("api_key") or ""
if not api_key:
raise HTTPException(400, "apiKey is required")

from app.scripts.ask_stripe import _test_stripe_connection_sync

loop = asyncio.get_event_loop()
try:
balance = await loop.run_in_executor(None, lambda: _test_stripe_connection_sync(api_key))
except Exception as e:
raise HTTPException(400, f"Connection failed: {e}")

return {"ok": True, "balance": _sanitize_for_json(balance)}


@router.post("/discover")
async def discover_resources(body: dict, user: User = Depends(require_user)):
"""Discover Stripe resources. Body: { "apiKey": "sk_...", "tables": ["customers", ...] }."""
api_key = body.get("apiKey") or body.get("api_key") or ""
if not api_key:
raise HTTPException(400, "apiKey is required")

tables = body.get("tables") or list(
["customers", "subscriptions", "invoices", "charges", "products", "prices", "refunds", "payouts", "disputes"]
)

from app.scripts.ask_stripe import _discover_stripe_resources_sync

loop = asyncio.get_event_loop()
try:
resources = await loop.run_in_executor(
None, lambda: _discover_stripe_resources_sync(api_key, tables)
)
except Exception as e:
raise HTTPException(400, str(e))

return {"resources": _sanitize_for_json(resources)}


@router.post("/sources/{source_id}/refresh-metadata")
async def refresh_source_metadata(
source_id: str,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_user),
):
"""Refresh Stripe source metadata (re-discover resources and update sample data)."""
r = await db.execute(select(Source).where(Source.id == source_id, Source.user_id == user.id))
source = r.scalar_one_or_none()
if not source:
raise HTTPException(404, "Source not found")
if source.type != "stripe":
raise HTTPException(400, "Source is not Stripe")

meta = dict(source.metadata_ or {})
api_key = meta.get("apiKey") or meta.get("api_key") or ""
if not api_key:
raise HTTPException(400, "Source has no API key")

tables = meta.get("tables") or [
"customers", "subscriptions", "invoices", "charges",
"products", "prices", "refunds", "payouts", "disputes",
]

from app.scripts.ask_stripe import _discover_stripe_resources_sync

loop = asyncio.get_event_loop()
try:
resources = await loop.run_in_executor(
None, lambda: _discover_stripe_resources_sync(api_key, tables)
)
except Exception as e:
raise HTTPException(400, str(e))

meta["table_infos"] = resources
source.metadata_ = _sanitize_for_json(meta)
await db.commit()
await db.refresh(source)
return {"metaJSON": source.metadata_}
Loading
Loading