403Webshell
Server IP : 3.147.158.171  /  Your IP : 216.73.216.88
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/repo/api/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /tsai/repo/api/reconcile_credits.py
"""
Credit Reconciliation Sweep

Pushes each site's current metered spend to the store so the credit balance
stays accurate for *all* usage — including cheap, ungated operations (plain
chat, quizzes) that never pass through `credits_guard.ensure_allowed()`.

Why this exists
---------------
Enforcement gates only the expensive/automated operations (articles, image
studio, AMA research). Those call `credits_guard.check_credits()`, which reports
the live `used` level to the store and triggers auto-recharge / cap. A site that
only ever runs ungated chat would therefore never update its stored `used`, so
auto-recharge and the monthly cap would never fire for it. This sweep closes
that gap: it evaluates every site on a schedule, so ungated spend is reconciled
and billed just like gated spend.

For each site with a credit account it runs the same `check_credits()` path the
gates use (compute `used` since the period start → POST /scike-credits/check →
store updates `used`, recharges if allowed, or caps). Sites with no account, or
when the store is unreachable, fail open and cost one cheap status call.

Usage:
    python reconcile_credits.py [--slug <slug>] [--verbose]

    --slug <slug>   Reconcile only this site (default: every configured site).
    --verbose       Log a line per site (default: only summary + actions).

Intended to run on a schedule (e.g. hourly systemd timer), alongside the
article/comment crons.
"""

import argparse
import asyncio
import logging
import time

# IMPORTANT: Import cron_common FIRST to load .env before any app imports.
import cron_common

from app.services import credits_client, credits_guard
from app.services.site_config_service import SiteConfigService


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Reconcile metered credit usage to the store.")
    parser.add_argument('--slug', help="Reconcile only this site slug.")
    parser.add_argument('--verbose', action='store_true', help="Log a line per site.")
    return parser.parse_args()


async def reconcile() -> int:
    """Reconcile every (or one) site's usage to the store. Returns exit code."""
    args = parse_args()

    if not credits_client.is_configured():
        logging.info("Credits bridge not configured (STORE_CREDITS_URL / SCIKE_CREDITS_SECRET); nothing to do.")
        return 0

    site_config_service = SiteConfigService()
    sites = site_config_service.get_all_sites()
    if args.slug:
        sites = [s for s in sites if s.slug == args.slug]
        if not sites:
            logging.error(f"No configured site with slug '{args.slug}'.")
            return 1

    # Reconcile distinct slugs only (dev aliases can share a slug).
    seen: set[str] = set()
    checked = recharged = capped = 0

    for site in sites:
        slug = site.slug
        if not slug or slug in seen:
            continue
        seen.add(slug)

        # A fresh process has empty caches, so this always does real work:
        # compute used → ask the store → it updates used / recharges / caps.
        credits_guard.invalidate(slug)
        try:
            verdict = await credits_guard.check_credits(slug, operation='reconcile')
        except Exception as e:
            logging.warning(f"Reconcile failed for '{slug}': {e}")
            continue

        # No account or store unavailable → fail-open sentinel; skip quietly.
        if verdict.get('status') in (None, 'unknown') and verdict.get('reason') == 'unavailable':
            if args.verbose:
                logging.info(f"  {slug}: no account / unavailable — skipped")
            continue

        checked += 1
        if verdict.get('recharged'):
            recharged += 1
            logging.info(
                f"  {slug}: auto-recharged (remaining now {verdict.get('remaining')})"
            )
        elif not verdict.get('allowed', True):
            capped += 1
            logging.info(
                f"  {slug}: capped/paused (reason={verdict.get('reason')}, remaining={verdict.get('remaining')})"
            )
        elif args.verbose:
            logging.info(
                f"  {slug}: ok (remaining {verdict.get('remaining')})"
            )

    logging.info(
        f"Reconcile complete: {checked} account(s) checked, {recharged} recharged, {capped} capped."
    )
    return 0


if __name__ == '__main__':
    cron_common.setup_logging('reconcile_credits.log')
    start = time.time()
    try:
        code = asyncio.run(reconcile())
    except Exception as e:
        logging.error(f"Credit reconciliation failed: {e}", exc_info=True)
        raise SystemExit(1)
    logging.info(f"Script completed ({time.time() - start:.2f}s total)")
    raise SystemExit(code)

Youez - 2016 - github.com/yon3zu
LinuXploit