mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-08 22:22:17 +02:00
Add per-item, per-platform billing for the platform-native connectors (Reddit, Google Search, Google Maps places/reviews, YouTube videos/comments) through the capability gate/charge seam. Rates are config-driven with a shared wallet-credit module (wallet_credit) and a dedicated PlatformScrapeCreditService; agent and REST capability runs now record cost_micros. Google Maps scrape dual-meters places and attached reviews. Remove the main-agent scrape_webpage tool now that the web.crawl capability covers single-page (maxCrawlDepth=0) and site crawling. The main agent now reaches crawling via task(web_crawler, ...). Update prompts, tool catalog, receipts, skills, proprietary docs, and tests; drop the obsolete chat-turn crawl fold path. Co-authored-by: Cursor <cursoragent@cursor.com>
99 lines
3.5 KiB
Python
99 lines
3.5 KiB
Python
"""Shared credit-wallet primitives for the flat-rate per-unit billers.
|
|
|
|
Both :class:`app.services.web_crawl_credit_service.WebCrawlCreditService` and
|
|
:class:`app.services.platform_scrape_credit_service.PlatformScrapeCreditService`
|
|
follow the same gate -> pre-check -> post-charge model against the unified
|
|
``User.credit_micros_*`` wallet. The wallet math lives here once instead of
|
|
being copied per service:
|
|
|
|
- :func:`spendable_micros` — ``balance - reserved`` (ungated by any flag)
|
|
- :func:`check_balance` — raise :class:`InsufficientCreditsError` if short
|
|
- :func:`apply_debit` — debit + commit + best-effort auto-reload
|
|
|
|
``InsufficientCreditsError`` is re-exported from ``etl_credit_service`` so every
|
|
per-unit biller (ETL, crawl, platform scrape) shares one "out of credit" type —
|
|
the capability doors already catch exactly that one.
|
|
"""
|
|
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.services.etl_credit_service import InsufficientCreditsError
|
|
|
|
__all__ = ["InsufficientCreditsError", "apply_debit", "check_balance", "spendable_micros"]
|
|
|
|
|
|
async def spendable_micros(session: AsyncSession, user_id: str | UUID) -> int:
|
|
"""Raw ``balance - reserved`` read, **ungated** by any billing flag."""
|
|
from app.db import User
|
|
|
|
result = await session.execute(
|
|
select(User.credit_micros_balance, User.credit_micros_reserved).where(
|
|
User.id == user_id
|
|
)
|
|
)
|
|
row = result.first()
|
|
if not row:
|
|
raise ValueError(f"User with ID {user_id} not found")
|
|
|
|
balance, reserved = row
|
|
return balance - reserved
|
|
|
|
|
|
async def check_balance(
|
|
session: AsyncSession, user_id: str | UUID, required_micros: int
|
|
) -> None:
|
|
"""Raise :class:`InsufficientCreditsError` if the wallet can't cover
|
|
``required_micros``. Generic and **ungated** — the caller decides when at
|
|
least one relevant biller is enabled. No-op for a non-positive requirement.
|
|
"""
|
|
if required_micros <= 0:
|
|
return
|
|
available = await spendable_micros(session, user_id)
|
|
if required_micros > available:
|
|
raise InsufficientCreditsError(
|
|
message=(
|
|
"This run would exceed your available credit. "
|
|
f"Available: ${available / 1_000_000:.2f}, "
|
|
f"estimated need: ${required_micros / 1_000_000:.2f}. "
|
|
"Add more credits to continue."
|
|
),
|
|
balance_micros=available,
|
|
required_micros=required_micros,
|
|
)
|
|
|
|
|
|
async def apply_debit(
|
|
session: AsyncSession, user_id: str | UUID, cost_micros: int
|
|
) -> int | None:
|
|
"""Debit ``cost_micros`` from the wallet and commit.
|
|
|
|
Flushes any audit row the caller staged before this, then fires a
|
|
best-effort auto-reload check. No-op for a non-positive cost; returns the
|
|
new balance in micros, or ``None`` when nothing was charged.
|
|
"""
|
|
if cost_micros <= 0:
|
|
return None
|
|
|
|
from app.db import User
|
|
|
|
result = await session.execute(select(User).where(User.id == user_id))
|
|
user = result.unique().scalar_one_or_none()
|
|
if not user:
|
|
raise ValueError(f"User with ID {user_id} not found")
|
|
|
|
user.credit_micros_balance -= cost_micros
|
|
await session.commit()
|
|
await session.refresh(user)
|
|
|
|
# Best-effort: fire an auto-reload check if the balance dropped low.
|
|
try:
|
|
from app.services.auto_reload_service import maybe_trigger_auto_reload
|
|
|
|
await maybe_trigger_auto_reload(user_id)
|
|
except Exception:
|
|
pass
|
|
|
|
return user.credit_micros_balance
|