Spaces:
Runtime error
Runtime error
Commit ·
8e2a6eb
1
Parent(s): 811ee7a
LETS GOOOOOOOOOOOOOOOOOOOOOOOOO
Browse files
backend/app/config.py
CHANGED
|
@@ -106,9 +106,14 @@ class Settings(BaseSettings):
|
|
| 106 |
|
| 107 |
# Billing (Stripe). With BILLING_ENABLED=false everyone gets Studio limits and checkout is off.
|
| 108 |
billing_enabled: bool = False
|
| 109 |
-
stripe_secret_key: str = Field(
|
|
|
|
|
|
|
| 110 |
stripe_webhook_secret: str = Field(
|
| 111 |
-
"",
|
|
|
|
|
|
|
|
|
|
| 112 |
)
|
| 113 |
# Optional existing Price ids. Empty = Notestack finds or creates prices by lookup key
|
| 114 |
# (notestack_<plan>_<cycle>) from the plan table in services/plans.py.
|
|
|
|
| 106 |
|
| 107 |
# Billing (Stripe). With BILLING_ENABLED=false everyone gets Studio limits and checkout is off.
|
| 108 |
billing_enabled: bool = False
|
| 109 |
+
stripe_secret_key: str = Field(
|
| 110 |
+
"", validation_alias=AliasChoices("STRIPE_SECRET_KEY", "STRIPE_API_KEY", "STRIPE_SECRET")
|
| 111 |
+
)
|
| 112 |
stripe_webhook_secret: str = Field(
|
| 113 |
+
"",
|
| 114 |
+
validation_alias=AliasChoices(
|
| 115 |
+
"STRIPE_WEBHOOK_SECRET", "STRIPE_WEBHOOK_SIGNING_SECRET", "STRIPE_SIGNING_SECRET"
|
| 116 |
+
),
|
| 117 |
)
|
| 118 |
# Optional existing Price ids. Empty = Notestack finds or creates prices by lookup key
|
| 119 |
# (notestack_<plan>_<cycle>) from the plan table in services/plans.py.
|
backend/app/routers/billing.py
CHANGED
|
@@ -2,7 +2,6 @@ from typing import Literal
|
|
| 2 |
|
| 3 |
from fastapi import APIRouter, Depends, HTTPException, Request
|
| 4 |
from pydantic import BaseModel
|
| 5 |
-
|
| 6 |
from sqlalchemy.orm import Session
|
| 7 |
|
| 8 |
from app.auth import Ctx, get_ctx
|
|
|
|
| 2 |
|
| 3 |
from fastapi import APIRouter, Depends, HTTPException, Request
|
| 4 |
from pydantic import BaseModel
|
|
|
|
| 5 |
from sqlalchemy.orm import Session
|
| 6 |
|
| 7 |
from app.auth import Ctx, get_ctx
|
backend/app/services/billing.py
CHANGED
|
@@ -6,7 +6,6 @@ Stripe goes in create_checkout_url, create_portal_url and the webhook route; eve
|
|
| 6 |
|
| 7 |
from datetime import UTC, datetime
|
| 8 |
|
| 9 |
-
from fastapi import HTTPException
|
| 10 |
from sqlalchemy import func, select
|
| 11 |
from sqlalchemy.orm import Session
|
| 12 |
|
|
|
|
| 6 |
|
| 7 |
from datetime import UTC, datetime
|
| 8 |
|
|
|
|
| 9 |
from sqlalchemy import func, select
|
| 10 |
from sqlalchemy.orm import Session
|
| 11 |
|
backend/app/services/stripe_billing.py
CHANGED
|
@@ -226,11 +226,13 @@ def portal_url(db: Session, workspace: Workspace, return_url: str) -> str:
|
|
| 226 |
if not row or not row.provider_customer_id:
|
| 227 |
raise HTTPException(404, {"code": "no_billing_account", "message": "There is no billing account yet."})
|
| 228 |
try:
|
| 229 |
-
session = client().v1.billing_portal.sessions.create(
|
|
|
|
|
|
|
| 230 |
except stripe.InvalidRequestError as exc:
|
| 231 |
log.error("stripe portal failed: %s", exc)
|
| 232 |
raise HTTPException(503, {"code": "portal_unavailable",
|
| 233 |
-
"message": "Billing management is unavailable
|
| 234 |
return session.url
|
| 235 |
|
| 236 |
|
|
@@ -327,7 +329,8 @@ def handle_event(db: Session, event) -> str:
|
|
| 327 |
return "synced"
|
| 328 |
|
| 329 |
if kind.startswith("invoice."):
|
| 330 |
-
|
|
|
|
| 331 |
ws = _workspace_for(db, {"id": sub_id} if sub_id else None, customer_id=_get(obj, "customer"))
|
| 332 |
if ws and sub_id:
|
| 333 |
sync_subscription(db, ws, sc.v1.subscriptions.retrieve(sub_id))
|
|
@@ -337,7 +340,8 @@ def handle_event(db: Session, event) -> str:
|
|
| 337 |
to = _get(obj, "customer_email") or _owner_email(db, ws)
|
| 338 |
url = _get(obj, "hosted_invoice_url") or f"{settings.frontend_url.rstrip('/')}/app/settings"
|
| 339 |
if to:
|
| 340 |
-
|
|
|
|
| 341 |
return "synced" if ws else "unknown"
|
| 342 |
|
| 343 |
if kind == "charge.dispute.created" and first:
|
|
|
|
| 226 |
if not row or not row.provider_customer_id:
|
| 227 |
raise HTTPException(404, {"code": "no_billing_account", "message": "There is no billing account yet."})
|
| 228 |
try:
|
| 229 |
+
session = client().v1.billing_portal.sessions.create(
|
| 230 |
+
{"customer": row.provider_customer_id, "return_url": return_url}
|
| 231 |
+
)
|
| 232 |
except stripe.InvalidRequestError as exc:
|
| 233 |
log.error("stripe portal failed: %s", exc)
|
| 234 |
raise HTTPException(503, {"code": "portal_unavailable",
|
| 235 |
+
"message": "Billing management is unavailable. Please try again soon."}) from exc
|
| 236 |
return session.url
|
| 237 |
|
| 238 |
|
|
|
|
| 329 |
return "synced"
|
| 330 |
|
| 331 |
if kind.startswith("invoice."):
|
| 332 |
+
details = _get(_get(obj, "parent", {}), "subscription_details", {}) # newer API versions
|
| 333 |
+
sub_id = _get(obj, "subscription") or _get(details, "subscription")
|
| 334 |
ws = _workspace_for(db, {"id": sub_id} if sub_id else None, customer_id=_get(obj, "customer"))
|
| 335 |
if ws and sub_id:
|
| 336 |
sync_subscription(db, ws, sc.v1.subscriptions.retrieve(sub_id))
|
|
|
|
| 340 |
to = _get(obj, "customer_email") or _owner_email(db, ws)
|
| 341 |
url = _get(obj, "hosted_invoice_url") or f"{settings.frontend_url.rstrip('/')}/app/settings"
|
| 342 |
if to:
|
| 343 |
+
action = "confirm" if kind.endswith("action_required") else "failed"
|
| 344 |
+
email_service.send_payment_action(to, action, url)
|
| 345 |
return "synced" if ws else "unknown"
|
| 346 |
|
| 347 |
if kind == "charge.dispute.created" and first:
|
backend/requirements.txt
CHANGED
|
@@ -17,3 +17,4 @@ dspy==2.6.*
|
|
| 17 |
feedparser==6.*
|
| 18 |
beautifulsoup4==4.*
|
| 19 |
pypdf==5.*
|
|
|
|
|
|
| 17 |
feedparser==6.*
|
| 18 |
beautifulsoup4==4.*
|
| 19 |
pypdf==5.*
|
| 20 |
+
stripe==13.*
|
frontend/src/components/AppShell.tsx
CHANGED
|
@@ -72,11 +72,11 @@ export default function AppShell() {
|
|
| 72 |
<div className="sidebar-top">
|
| 73 |
{collapsed ? (
|
| 74 |
<button type="button" className="sidebar-brand sidebar-brand-btn" onClick={toggle} aria-label="Expand sidebar" title="Expand sidebar">
|
| 75 |
-
<Logo size={28} />
|
| 76 |
</button>
|
| 77 |
) : (
|
| 78 |
<NavLink to="/" className="sidebar-brand">
|
| 79 |
-
<Logo size={28} />
|
| 80 |
</NavLink>
|
| 81 |
)}
|
| 82 |
<button
|
|
|
|
| 72 |
<div className="sidebar-top">
|
| 73 |
{collapsed ? (
|
| 74 |
<button type="button" className="sidebar-brand sidebar-brand-btn" onClick={toggle} aria-label="Expand sidebar" title="Expand sidebar">
|
| 75 |
+
<Logo size={28} withWordmark={false} />
|
| 76 |
</button>
|
| 77 |
) : (
|
| 78 |
<NavLink to="/" className="sidebar-brand">
|
| 79 |
+
<Logo size={28} withWordmark={false} />
|
| 80 |
</NavLink>
|
| 81 |
)}
|
| 82 |
<button
|
frontend/src/styles/pages.css
CHANGED
|
@@ -1874,6 +1874,12 @@ select.input option {
|
|
| 1874 |
gap: 14px;
|
| 1875 |
}
|
| 1876 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1877 |
.side-link {
|
| 1878 |
padding: 7px 10px;
|
| 1879 |
font-size: 0.9rem;
|
|
|
|
| 1874 |
gap: 14px;
|
| 1875 |
}
|
| 1876 |
|
| 1877 |
+
/* Collapsed: just the logo (and the upgrade rocket), so nothing to scroll. */
|
| 1878 |
+
.shell.collapsed > .sidebar {
|
| 1879 |
+
overflow: hidden;
|
| 1880 |
+
padding: 14px 10px;
|
| 1881 |
+
}
|
| 1882 |
+
|
| 1883 |
.side-link {
|
| 1884 |
padding: 7px 10px;
|
| 1885 |
font-size: 0.9rem;
|