Implement ops operational closure
This commit is contained in:
@@ -274,7 +274,8 @@ def grant_ops_subscription(
|
||||
days: int = 30,
|
||||
deduct_points: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
_require_ops(request)
|
||||
admin = _require_ops(request) or {}
|
||||
actor_email = str(admin.get("email") or "").strip().lower()
|
||||
from datetime import datetime
|
||||
|
||||
import web.routes as legacy_routes # lazy – avoid circular import
|
||||
@@ -344,11 +345,34 @@ def grant_ops_subscription(
|
||||
if safe_deduct > 0:
|
||||
db = _get_db()
|
||||
deduct_result = db.deduct_points_by_supabase_email(
|
||||
normalized_email, safe_deduct
|
||||
normalized_email,
|
||||
safe_deduct,
|
||||
source="ops_subscription_deduction",
|
||||
actor_email=actor_email,
|
||||
reference_type="subscription",
|
||||
reference_id=user_id,
|
||||
metadata={"plan_code": plan_code, "days": safe_days},
|
||||
)
|
||||
result["points_deducted"] = safe_deduct
|
||||
result["points_result"] = deduct_result
|
||||
|
||||
try:
|
||||
_get_db().append_ops_audit_event(
|
||||
action="subscription_manual_grant",
|
||||
actor_email=actor_email,
|
||||
target_user_id=user_id,
|
||||
target_email=normalized_email,
|
||||
target_type="subscription",
|
||||
payload={
|
||||
"plan_code": plan_code,
|
||||
"days": safe_days,
|
||||
"expires_at": expires_at,
|
||||
"deduct_points": safe_deduct,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -357,7 +381,8 @@ def extend_ops_subscription(
|
||||
email: str,
|
||||
additional_days: int = 30,
|
||||
) -> dict[str, Any]:
|
||||
_require_ops(request)
|
||||
admin = _require_ops(request) or {}
|
||||
actor_email = str(admin.get("email") or "").strip().lower()
|
||||
from datetime import datetime
|
||||
|
||||
import web.routes as legacy_routes # lazy – avoid circular import
|
||||
@@ -419,6 +444,22 @@ def extend_ops_subscription(
|
||||
)
|
||||
if patch_resp.ok:
|
||||
legacy_routes.SUPABASE_ENTITLEMENT.invalidate_subscription_cache(user_id)
|
||||
try:
|
||||
_get_db().append_ops_audit_event(
|
||||
action="subscription_manual_extend",
|
||||
actor_email=actor_email,
|
||||
target_user_id=user_id,
|
||||
target_email=normalized_email,
|
||||
target_type="subscription",
|
||||
target_id=str(sub.get("id") or ""),
|
||||
payload={
|
||||
"additional_days": safe_days,
|
||||
"previous_expires_at": current_expiry,
|
||||
"new_expires_at": new_expiry,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"ok": True,
|
||||
"email": normalized_email,
|
||||
|
||||
@@ -146,6 +146,8 @@ def _normalize_payment_incident(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
or confirm_failure.get("tx_hash")
|
||||
or ""
|
||||
).strip(),
|
||||
"refund_case_id": payload.get("refund_case_id"),
|
||||
"refund_status": str(payload.get("refund_status") or "").strip(),
|
||||
"resolved": bool(resolved_at),
|
||||
"resolved_at": resolved_at,
|
||||
"resolved_by": str(payload.get("resolved_by") or "").strip(),
|
||||
@@ -235,10 +237,47 @@ def list_ops_payment_incidents(
|
||||
_require_ops(request)
|
||||
db = _get_db()
|
||||
safe_limit = max(1, min(int(limit or 50), 200))
|
||||
incidents = db.list_payment_audit_events(
|
||||
limit=max(safe_limit, 500),
|
||||
event_type="payment_intent_failed",
|
||||
)
|
||||
incidents: List[Dict[str, Any]] = []
|
||||
for event_type in ("payment_intent_failed", "payment_refund_required"):
|
||||
try:
|
||||
rows = db.list_payment_audit_events(
|
||||
limit=max(safe_limit, 500),
|
||||
event_type=event_type,
|
||||
)
|
||||
incidents.extend(
|
||||
row for row in rows
|
||||
if str(row.get("event_type") or "").strip().lower() == event_type
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
terminal_refund_statuses = {"refunded", "rejected", "closed"}
|
||||
list_refund_cases = getattr(db, "list_refund_cases", None)
|
||||
if callable(list_refund_cases):
|
||||
try:
|
||||
refund_cases = list_refund_cases(limit=max(safe_limit, 500))
|
||||
except Exception:
|
||||
refund_cases = []
|
||||
for case in refund_cases:
|
||||
if not isinstance(case, dict):
|
||||
continue
|
||||
status = str(case.get("status") or "").strip().lower()
|
||||
if not include_resolved and status in terminal_refund_statuses:
|
||||
continue
|
||||
incidents.append(
|
||||
{
|
||||
"id": int(case.get("id") or 0),
|
||||
"event_type": "payment_refund_case",
|
||||
"payload": {
|
||||
"reason": str(case.get("reason") or "refund_required"),
|
||||
"intent_id": case.get("intent_id"),
|
||||
"user_id": case.get("user_id"),
|
||||
"tx_hash": case.get("tx_hash"),
|
||||
"refund_case_id": case.get("id"),
|
||||
"refund_status": status,
|
||||
},
|
||||
"created_at": case.get("created_at"),
|
||||
}
|
||||
)
|
||||
grouped = _group_payment_incidents(
|
||||
incidents,
|
||||
reason=reason,
|
||||
@@ -250,6 +289,106 @@ def list_ops_payment_incidents(
|
||||
}
|
||||
|
||||
|
||||
def list_ops_refund_cases(
|
||||
request: Request,
|
||||
limit: int = 50,
|
||||
status: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
_require_ops(request)
|
||||
db = _get_db()
|
||||
safe_limit = max(1, min(int(limit or 50), 200))
|
||||
return {
|
||||
"refunds": db.list_refund_cases(
|
||||
limit=safe_limit,
|
||||
status=str(status or "").strip().lower() or None,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def create_ops_refund_case(
|
||||
request: Request,
|
||||
*,
|
||||
reason: str,
|
||||
intent_id: str = "",
|
||||
tx_hash: str = "",
|
||||
user_id: str = "",
|
||||
amount_usdc: str = "",
|
||||
note: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
admin = _require_ops(request) or {}
|
||||
actor_email = str(admin.get("email") or "").strip().lower()
|
||||
db = _get_db()
|
||||
created = db.create_refund_case(
|
||||
reason=reason,
|
||||
intent_id=intent_id,
|
||||
tx_hash=tx_hash,
|
||||
user_id=user_id,
|
||||
amount_usdc=amount_usdc,
|
||||
created_by=actor_email,
|
||||
note=note,
|
||||
)
|
||||
if not created or created.get("ok") is False:
|
||||
raise HTTPException(status_code=400, detail=created or "refund_case_failed")
|
||||
db.append_ops_audit_event(
|
||||
action="refund_case_create",
|
||||
actor_email=actor_email,
|
||||
target_user_id=str(user_id or ""),
|
||||
target_type="refund_case",
|
||||
target_id=str(created.get("id") or ""),
|
||||
payload={
|
||||
"reason": reason,
|
||||
"intent_id": intent_id,
|
||||
"tx_hash": tx_hash,
|
||||
"amount_usdc": amount_usdc,
|
||||
},
|
||||
)
|
||||
db.append_payment_audit_event(
|
||||
"payment_refund_required",
|
||||
{
|
||||
"reason": str(reason or "refund_required").strip().lower(),
|
||||
"intent_id": intent_id,
|
||||
"user_id": user_id,
|
||||
"tx_hash": tx_hash,
|
||||
"refund_case_id": created.get("id"),
|
||||
},
|
||||
)
|
||||
return {"ok": True, "refund": created}
|
||||
|
||||
|
||||
def update_ops_refund_case(
|
||||
request: Request,
|
||||
*,
|
||||
case_id: int,
|
||||
status: str,
|
||||
note: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
admin = _require_ops(request) or {}
|
||||
actor_email = str(admin.get("email") or "").strip().lower()
|
||||
db = _get_db()
|
||||
updated = db.update_refund_case(
|
||||
case_id,
|
||||
status=status,
|
||||
handled_by=actor_email,
|
||||
note=note,
|
||||
)
|
||||
if not updated:
|
||||
raise HTTPException(status_code=404, detail="refund_case_not_found")
|
||||
db.append_ops_audit_event(
|
||||
action="refund_case_update",
|
||||
actor_email=actor_email,
|
||||
target_user_id=str(updated.get("user_id") or ""),
|
||||
target_type="refund_case",
|
||||
target_id=str(case_id),
|
||||
payload={
|
||||
"status": status,
|
||||
"note": note,
|
||||
"intent_id": updated.get("intent_id"),
|
||||
"tx_hash": updated.get("tx_hash"),
|
||||
},
|
||||
)
|
||||
return {"ok": True, "refund": updated}
|
||||
|
||||
|
||||
def resolve_ops_payment_incident(request: Request, event_id: int) -> Dict[str, Any]:
|
||||
admin = _require_ops(request) or {}
|
||||
db = _get_db()
|
||||
|
||||
@@ -63,6 +63,25 @@ def search_ops_users(request: Request, q: str = "", limit: int = 20) -> Dict[str
|
||||
return {"users": db.search_users(q, limit=limit)}
|
||||
|
||||
|
||||
def list_ops_audit_log(
|
||||
request: Request,
|
||||
*,
|
||||
limit: int = 100,
|
||||
action: str = "",
|
||||
actor_email: str = "",
|
||||
target_user_id: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
_require_ops(request)
|
||||
db = _get_db()
|
||||
rows = db.list_ops_audit_events(
|
||||
limit=limit,
|
||||
action=action,
|
||||
actor_email=actor_email,
|
||||
target_user_id=target_user_id,
|
||||
)
|
||||
return {"events": rows, "total": len(rows)}
|
||||
|
||||
|
||||
def get_ops_weekly_leaderboard(request: Request, limit: int = 20) -> Dict[str, Any]:
|
||||
_require_ops(request)
|
||||
db = _get_db()
|
||||
@@ -76,12 +95,34 @@ def get_ops_weekly_leaderboard(request: Request, limit: int = 20) -> Dict[str, A
|
||||
def grant_ops_points(request: Request, body: GrantPointsRequest) -> Dict[str, Any]:
|
||||
admin = _require_ops(request) or {}
|
||||
db = _get_db()
|
||||
result = db.grant_points_by_supabase_email(body.email, body.points)
|
||||
result["operator_email"] = admin.get("email")
|
||||
actor_email = str(admin.get("email") or "").strip().lower()
|
||||
result = db.grant_points_by_supabase_email(
|
||||
body.email,
|
||||
body.points,
|
||||
source="ops_manual_grant",
|
||||
actor_email=actor_email,
|
||||
reference_type="ops_action",
|
||||
metadata={"action": "manual_points_grant"},
|
||||
)
|
||||
result["operator_email"] = actor_email
|
||||
if not result.get("ok"):
|
||||
reason = str(result.get("reason") or "grant_points_failed")
|
||||
status_code = 404 if reason == "user_not_found" else 400
|
||||
raise HTTPException(status_code=status_code, detail=result)
|
||||
append_audit = getattr(db, "append_ops_audit_event", None)
|
||||
if callable(append_audit):
|
||||
audit = append_audit(
|
||||
action="manual_points_grant",
|
||||
actor_email=actor_email,
|
||||
target_user_id=str(result.get("supabase_user_id") or ""),
|
||||
target_email=str(result.get("supabase_email") or body.email),
|
||||
target_type="user",
|
||||
payload={
|
||||
"points_added": int(result.get("points_added") or body.points),
|
||||
"points_after": int(result.get("points_after") or 0),
|
||||
},
|
||||
)
|
||||
result["audit_event_id"] = audit.get("id")
|
||||
return result
|
||||
|
||||
|
||||
@@ -170,12 +211,23 @@ def grant_ops_feedback_reward(
|
||||
reason: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
admin = _require_ops(request) or {}
|
||||
actor_email = str(admin.get("email") or "").strip().lower()
|
||||
db = _get_db()
|
||||
result = db.grant_feedback_reward(
|
||||
feedback_id,
|
||||
points=points,
|
||||
reason=reason,
|
||||
)
|
||||
try:
|
||||
result = db.grant_feedback_reward(
|
||||
feedback_id,
|
||||
points=points,
|
||||
reason=reason,
|
||||
actor_email=actor_email,
|
||||
)
|
||||
except TypeError as exc:
|
||||
if "actor_email" not in str(exc):
|
||||
raise
|
||||
result = db.grant_feedback_reward(
|
||||
feedback_id,
|
||||
points=points,
|
||||
reason=reason,
|
||||
)
|
||||
if not result.get("ok") and str(result.get("reason") or "") == "user_not_found":
|
||||
feedback = result.get("feedback") if isinstance(result.get("feedback"), dict) else {}
|
||||
reward_status = str(feedback.get("reward_status") or "").strip().lower()
|
||||
@@ -203,11 +255,32 @@ def grant_ops_feedback_reward(
|
||||
"supabase_user_id": supabase_user_id,
|
||||
"feedback": updated_feedback,
|
||||
}
|
||||
result["operator_email"] = admin.get("email")
|
||||
result["operator_email"] = actor_email
|
||||
if not result.get("ok"):
|
||||
reason_code = str(result.get("reason") or "feedback_reward_failed")
|
||||
status_code = 404 if reason_code in {"feedback_not_found", "user_not_found"} else 400
|
||||
if reason_code == "already_rewarded":
|
||||
status_code = 409
|
||||
raise HTTPException(status_code=status_code, detail=result)
|
||||
feedback = result.get("feedback") if isinstance(result.get("feedback"), dict) else {}
|
||||
append_audit = getattr(db, "append_ops_audit_event", None)
|
||||
if callable(append_audit):
|
||||
audit = append_audit(
|
||||
action="feedback_reward_grant",
|
||||
actor_email=actor_email,
|
||||
target_user_id=str(
|
||||
result.get("supabase_user_id")
|
||||
or feedback.get("user_id")
|
||||
or ""
|
||||
),
|
||||
target_email=str(result.get("supabase_email") or feedback.get("user_email") or ""),
|
||||
target_type="feedback",
|
||||
target_id=str(feedback_id),
|
||||
payload={
|
||||
"points_added": int(points or 0),
|
||||
"reason": str(reason or ""),
|
||||
"points_after": int(result.get("points_after") or 0),
|
||||
},
|
||||
)
|
||||
result["audit_event_id"] = audit.get("id")
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user