"""Subscription state resolution + activation — the single place plan state is decided, so gating and metering agree. Key skill rule enforced here: plan activation only ever happens off a trusted, server-verified confirmation (grant_plan is called by the payment webhook handler after it verifies the processor event), NEVER off a client 'payment succeeded'. Activation is atomic and idempotent via the payment_events ledger in store.py. """ from __future__ import annotations import time from backend.billing import store from backend.billing.plans import (DEFAULT_PLAN_ID, OWNER_PLAN_ID, OWNER_USER_ID, Plan, get_plan) def effective_plan(user_id: str) -> Plan: """The plan whose entitlements currently apply to this user. A subscription in 'provisioning' still grants the plan's non-provisioned features immediately; the metering/AR layers separately check provisioning state for the parts that depend on real provisioned model access.""" # The operator is always on the hidden owner plan (unmetered, all features) even # with no subscription row — never gated off their own deployment. if user_id == OWNER_USER_ID: return get_plan(OWNER_PLAN_ID) sub = store.get_subscription(user_id) if not sub: return get_plan(DEFAULT_PLAN_ID) plan = get_plan(sub["plan_id"]) or get_plan(DEFAULT_PLAN_ID) if sub["status"] in ("active", "provisioning"): return plan return get_plan(DEFAULT_PLAN_ID) def is_provisioning(user_id: str) -> bool: sub = store.get_subscription(user_id) if not sub: return False return sub["status"] == "provisioning" and (sub["provisioning_until"] or 0) > time.time() def grant_plan_for_order(order_id: str, event_id: str) -> dict: """Atomically grant the plan for a CONFIRMED order. Idempotent: replaying the same processor event never double-grants. This is the ONLY path that activates a paid plan.""" if store.event_already_applied(event_id): return {"granted": False, "reason": "duplicate_event", "order_id": order_id} order = store.get_order(order_id) if not order: return {"granted": False, "reason": "unknown_order", "order_id": order_id} plan = get_plan(order["plan_id"]) if not plan: return {"granted": False, "reason": "unknown_plan", "order_id": order_id} store.set_order_state(order_id, "confirmed") # Provisioning delay applies only to plans that must provision real model # access; others activate immediately. if plan.provisioning_delay_hours > 0: until = time.time() + plan.provisioning_delay_hours * 3600 store.set_subscription(order["user_id"], plan.id, "provisioning", provisioning_until=until) status = "provisioning" else: store.set_subscription(order["user_id"], plan.id, "active") status = "active" store.mark_event_applied(event_id, order_id) # Privileged action -> queryable audit record (skill: never a silent grant). try: import json as _json store.record_audit(action="plan_grant", actor="payment_webhook", target=order["user_id"], detail=_json.dumps({"order_id": order_id, "plan_id": plan.id, "status": status, "event_id": event_id})) except Exception: pass return {"granted": True, "reason": "ok", "order_id": order_id, "plan_id": plan.id, "status": status} def reconcile(user_id: str) -> dict: """Catch mismatches in either direction (skill: build reconciliation, not just the happy path). Returns a report — a confirmed order with no active/ provisioning subscription, or an active subscription with no confirmed order.""" orders = store.find_orders(user_id) sub = store.get_subscription(user_id) confirmed = [o for o in orders if o["state"] == "confirmed"] problems = [] if confirmed and (not sub or sub["status"] not in ("active", "provisioning")): problems.append("paid_but_not_granted") if sub and sub["status"] == "active" and sub["plan_id"] != DEFAULT_PLAN_ID and not confirmed: problems.append("granted_but_unpaid") return {"user_id": user_id, "subscription": sub, "confirmed_orders": len(confirmed), "problems": problems}