| Server IP : 3.147.158.171 / Your IP : 216.73.216.216 Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5 System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64 User : ec2-user ( 1000) PHP Version : 8.4.21 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /tsai/www/html/api.turmansolutions.ai/app/services/ |
Upload File : |
"""HTTP client for the store's internal Scike Credits API.
The FastAPI backend meters usage; the Drupal Commerce store owns money and
entitlement. This client is the server-to-server bridge between them, keyed by
site slug and authenticated with a shared secret.
Configuration (in /api/.env):
- STORE_CREDITS_URL Base URL of the store, e.g. https://scike.ai
- SCIKE_CREDITS_SECRET Shared secret matching the store's
$settings['scike_credits_shared_secret'].
Every call is best-effort and fails open (returns None / an "allowed" verdict on
transport error) so a store outage never hard-blocks generation.
"""
import logging
import os
from typing import Any, Dict, Optional
import httpx
_TIMEOUT = httpx.Timeout(6.0, connect=3.0)
def _base_url() -> Optional[str]:
url = os.getenv('STORE_CREDITS_URL')
return url.rstrip('/') if url else None
def _headers() -> Dict[str, str]:
return {
'X-Scike-Credits-Secret': os.getenv('SCIKE_CREDITS_SECRET', ''),
'Content-Type': 'application/json',
}
def is_configured() -> bool:
return bool(_base_url() and os.getenv('SCIKE_CREDITS_SECRET'))
async def check(slug: str, used_credits: float) -> Optional[Dict[str, Any]]:
"""Enforcement decision. Returns the store verdict, or None on error/unconfigured.
Verdict shape: {allowed, remaining, recharged, reason, status}.
"""
base = _base_url()
if not base or not is_configured():
return None
try:
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
resp = await client.post(
f'{base}/scike-credits/check',
headers=_headers(),
json={'slug': slug, 'used_credits': used_credits},
)
resp.raise_for_status()
return resp.json()
except Exception as e:
logging.warning(f"credits check failed for {slug}: {e}")
return None
async def report(slug: str, used_credits: float, period_start: Optional[int] = None) -> None:
"""Idempotent usage reconciliation (level, not delta). Best-effort."""
base = _base_url()
if not base or not is_configured():
return
try:
payload: Dict[str, Any] = {'slug': slug, 'used_credits': used_credits}
if period_start is not None:
payload['period_start'] = period_start
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
resp = await client.post(
f'{base}/scike-credits/report',
headers=_headers(),
json=payload,
)
resp.raise_for_status()
except Exception as e:
logging.warning(f"credits report failed for {slug}: {e}")
async def status(slug: str) -> Optional[Dict[str, Any]]:
"""Full account state for the dashboard, or None if unavailable/no account."""
base = _base_url()
if not base or not is_configured():
return None
try:
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
resp = await client.get(
f'{base}/scike-credits/status',
headers=_headers(),
params={'slug': slug},
)
if resp.status_code == 404:
return None
resp.raise_for_status()
return resp.json()
except Exception as e:
logging.warning(f"credits status failed for {slug}: {e}")
return None
async def update_settings(slug: str, settings: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Update auto-recharge / increment / cap. Returns the fresh status."""
base = _base_url()
if not base or not is_configured():
return None
try:
payload = {'slug': slug, **settings}
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
resp = await client.post(
f'{base}/scike-credits/settings',
headers=_headers(),
json=payload,
)
if resp.status_code == 404:
return None
resp.raise_for_status()
return resp.json()
except Exception as e:
logging.warning(f"credits settings update failed for {slug}: {e}")
return None
async def portal_link(slug: str, return_url: Optional[str] = None) -> Optional[str]:
"""Create a Stripe Customer Portal session URL, or None if unavailable."""
base = _base_url()
if not base or not is_configured():
return None
try:
params = {'slug': slug}
if return_url:
params['return_url'] = return_url
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
resp = await client.get(
f'{base}/scike-credits/portal-link',
headers=_headers(),
params=params,
)
if resp.status_code == 404:
return None
resp.raise_for_status()
return resp.json().get('url')
except Exception as e:
logging.warning(f"credits portal-link failed for {slug}: {e}")
return None