Add trial and referral subscription program
This commit is contained in:
@@ -1,15 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import secrets
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import requests
|
||||
from loguru import logger
|
||||
|
||||
from src.database.db_manager import DBManager
|
||||
|
||||
SIGNUP_TRIAL_PLAN_CODE = "signup_trial_3d"
|
||||
SIGNUP_TRIAL_SOURCE = "signup_trial"
|
||||
SIGNUP_TRIAL_DAYS = 3
|
||||
|
||||
REFERRAL_REWARD_DAYS = 3
|
||||
REFERRAL_MONTHLY_REWARD_LIMIT = 10
|
||||
REFERRAL_MONTHLY_DAY_LIMIT = 30
|
||||
REFERRAL_DISCOUNT_USDC = "3"
|
||||
REFERRAL_MONTHLY_DISCOUNTED_AMOUNT_USDC = "26.9"
|
||||
|
||||
|
||||
def _env_bool(name: str, default: bool = False) -> bool:
|
||||
raw = os.getenv(name)
|
||||
@@ -121,9 +134,595 @@ class SupabaseEntitlementService:
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
def _service_rest_headers(self, prefer: Optional[str] = None) -> Dict[str, str]:
|
||||
headers = self._request_headers_for_service_role()
|
||||
headers["Content-Type"] = "application/json"
|
||||
if prefer:
|
||||
headers["Prefer"] = prefer
|
||||
return headers
|
||||
|
||||
def _rest(
|
||||
self,
|
||||
method: str,
|
||||
table: str,
|
||||
*,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
payload: Optional[Any] = None,
|
||||
prefer: Optional[str] = None,
|
||||
allowed_status: Optional[List[int]] = None,
|
||||
) -> Any:
|
||||
if not self.supabase_url or not self.service_role_key:
|
||||
raise RuntimeError("supabase service role is not configured")
|
||||
status_ok = allowed_status or [200, 201, 204]
|
||||
response = requests.request(
|
||||
method=method.upper(),
|
||||
url=f"{self.supabase_url}/rest/v1/{table}",
|
||||
headers=self._service_rest_headers(prefer=prefer),
|
||||
params=params,
|
||||
json=payload,
|
||||
timeout=self.timeout_sec,
|
||||
)
|
||||
if response.status_code not in status_ok:
|
||||
detail = response.text[:350] if response.text else response.reason
|
||||
raise RuntimeError(
|
||||
f"supabase {method.upper()} {table} failed: "
|
||||
f"{response.status_code} {detail}"
|
||||
)
|
||||
if not response.content:
|
||||
return None
|
||||
try:
|
||||
return response.json()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _admin_user_endpoint(self, user_id: str) -> str:
|
||||
return f"{self.supabase_url}/auth/v1/admin/users/{user_id}"
|
||||
|
||||
@staticmethod
|
||||
def _to_iso(dt: datetime) -> str:
|
||||
return dt.astimezone(timezone.utc).isoformat()
|
||||
|
||||
@staticmethod
|
||||
def _normalize_email(value: Optional[str]) -> str:
|
||||
return str(value or "").strip().lower()
|
||||
|
||||
@staticmethod
|
||||
def _is_trial_subscription_row(row: Optional[Dict[str, object]]) -> bool:
|
||||
if not isinstance(row, dict):
|
||||
return False
|
||||
plan_code = str(row.get("plan_code") or "").strip().lower()
|
||||
source = str(row.get("source") or "").strip().lower()
|
||||
return "trial" in plan_code or "trial" in source
|
||||
|
||||
@staticmethod
|
||||
def _is_paid_subscription_row(row: Optional[Dict[str, object]]) -> bool:
|
||||
if not isinstance(row, dict):
|
||||
return False
|
||||
if SupabaseEntitlementService._is_trial_subscription_row(row):
|
||||
return False
|
||||
source = str(row.get("source") or "").strip().lower()
|
||||
if "referral_reward" in source:
|
||||
return False
|
||||
return "payment" in source or source in {"payment_contract", "payment_manual"}
|
||||
|
||||
def _telegram_user_id_for(self, user_id: str) -> Optional[int]:
|
||||
try:
|
||||
linked = DBManager().get_user_by_supabase_user_id(user_id)
|
||||
if not isinstance(linked, dict):
|
||||
return None
|
||||
telegram_id = int(linked.get("telegram_id") or 0)
|
||||
return telegram_id or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _active_wallet_addresses_for(self, user_id: str) -> List[str]:
|
||||
try:
|
||||
rows = self._rest(
|
||||
"GET",
|
||||
"user_wallets",
|
||||
params={
|
||||
"select": "address",
|
||||
"user_id": f"eq.{user_id}",
|
||||
"status": "eq.active",
|
||||
"limit": "50",
|
||||
},
|
||||
allowed_status=[200],
|
||||
)
|
||||
except Exception:
|
||||
return []
|
||||
out: List[str] = []
|
||||
if isinstance(rows, list):
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
address = str(row.get("address") or "").strip().lower()
|
||||
if address and address not in out:
|
||||
out.append(address)
|
||||
return out
|
||||
|
||||
def _trial_claim_exists(
|
||||
self,
|
||||
*,
|
||||
user_id: str,
|
||||
email: str,
|
||||
telegram_user_id: Optional[int],
|
||||
wallet_addresses: List[str],
|
||||
) -> bool:
|
||||
checks = [f"user_id.eq.{user_id}"]
|
||||
if email:
|
||||
checks.append(f"email.eq.{email}")
|
||||
if telegram_user_id:
|
||||
checks.append(f"telegram_user_id.eq.{telegram_user_id}")
|
||||
try:
|
||||
rows = self._rest(
|
||||
"GET",
|
||||
"trial_claims",
|
||||
params={
|
||||
"select": "id",
|
||||
"or": f"({','.join(checks)})",
|
||||
"limit": "1",
|
||||
},
|
||||
allowed_status=[200],
|
||||
)
|
||||
if isinstance(rows, list) and rows:
|
||||
return True
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
if not wallet_addresses:
|
||||
return False
|
||||
try:
|
||||
wallet_rows = self._rest(
|
||||
"GET",
|
||||
"trial_claim_wallets",
|
||||
params={
|
||||
"select": "id",
|
||||
"wallet_address": f"in.({','.join(wallet_addresses)})",
|
||||
"limit": "1",
|
||||
},
|
||||
allowed_status=[200],
|
||||
)
|
||||
return bool(isinstance(wallet_rows, list) and wallet_rows)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def ensure_signup_trial(self, user_id: str, email: Optional[str] = None) -> Dict[str, object]:
|
||||
user_key = str(user_id or "").strip()
|
||||
if not user_key:
|
||||
return {"created": False, "reason": "missing_user_id"}
|
||||
if not _env_bool("POLYWEATHER_SIGNUP_TRIAL_ENABLED", True):
|
||||
return {"created": False, "reason": "disabled"}
|
||||
if not self.supabase_url or not self.service_role_key:
|
||||
return {"created": False, "reason": "supabase_not_configured"}
|
||||
|
||||
normalized_email = self._normalize_email(email)
|
||||
try:
|
||||
telegram_user_id = self._telegram_user_id_for(user_key)
|
||||
wallet_addresses = self._active_wallet_addresses_for(user_key)
|
||||
if self._trial_claim_exists(
|
||||
user_id=user_key,
|
||||
email=normalized_email,
|
||||
telegram_user_id=telegram_user_id,
|
||||
wallet_addresses=wallet_addresses,
|
||||
):
|
||||
return {"created": False, "reason": "already_claimed"}
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
expires = now + timedelta(days=SIGNUP_TRIAL_DAYS)
|
||||
claim_payload = {
|
||||
"user_id": user_key,
|
||||
"email": normalized_email,
|
||||
"telegram_user_id": telegram_user_id,
|
||||
"primary_wallet_address": wallet_addresses[0] if wallet_addresses else None,
|
||||
"claimed_at": self._to_iso(now),
|
||||
"metadata": {"wallet_addresses": wallet_addresses},
|
||||
}
|
||||
claim_rows = self._rest(
|
||||
"POST",
|
||||
"trial_claims",
|
||||
payload=claim_payload,
|
||||
prefer="return=representation",
|
||||
allowed_status=[200, 201],
|
||||
)
|
||||
claim_id = None
|
||||
if isinstance(claim_rows, list) and claim_rows and isinstance(claim_rows[0], dict):
|
||||
claim_id = claim_rows[0].get("id")
|
||||
if wallet_addresses and claim_id is not None:
|
||||
self._rest(
|
||||
"POST",
|
||||
"trial_claim_wallets",
|
||||
payload=[
|
||||
{
|
||||
"trial_claim_id": claim_id,
|
||||
"wallet_address": address,
|
||||
"created_at": self._to_iso(now),
|
||||
}
|
||||
for address in wallet_addresses
|
||||
],
|
||||
prefer="return=minimal",
|
||||
allowed_status=[201],
|
||||
)
|
||||
|
||||
subscription_payload = {
|
||||
"user_id": user_key,
|
||||
"plan_code": SIGNUP_TRIAL_PLAN_CODE,
|
||||
"status": "active",
|
||||
"starts_at": self._to_iso(now),
|
||||
"expires_at": self._to_iso(expires),
|
||||
"source": SIGNUP_TRIAL_SOURCE,
|
||||
"created_at": self._to_iso(now),
|
||||
"updated_at": self._to_iso(now),
|
||||
}
|
||||
self._rest(
|
||||
"POST",
|
||||
"subscriptions",
|
||||
payload=subscription_payload,
|
||||
prefer="return=minimal",
|
||||
allowed_status=[201],
|
||||
)
|
||||
self._rest(
|
||||
"POST",
|
||||
"entitlement_events",
|
||||
payload={
|
||||
"user_id": user_key,
|
||||
"action": "signup_trial_granted",
|
||||
"reason": "first_auth",
|
||||
"actor": "supabase_auth",
|
||||
"payload": {
|
||||
"plan_code": SIGNUP_TRIAL_PLAN_CODE,
|
||||
"expires_at": self._to_iso(expires),
|
||||
},
|
||||
"created_at": self._to_iso(now),
|
||||
},
|
||||
prefer="return=minimal",
|
||||
allowed_status=[201],
|
||||
)
|
||||
self.invalidate_subscription_cache(user_key)
|
||||
return {
|
||||
"created": True,
|
||||
"plan_code": SIGNUP_TRIAL_PLAN_CODE,
|
||||
"expires_at": self._to_iso(expires),
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.warning("signup trial grant failed user_id={}: {}", user_key, exc)
|
||||
return {"created": False, "reason": "error"}
|
||||
|
||||
def has_paid_subscription(self, user_id: str) -> bool:
|
||||
user_key = str(user_id or "").strip()
|
||||
if not user_key:
|
||||
return False
|
||||
try:
|
||||
rows = self._rest(
|
||||
"GET",
|
||||
"subscriptions",
|
||||
params={
|
||||
"select": "plan_code,source,status,starts_at,expires_at",
|
||||
"user_id": f"eq.{user_key}",
|
||||
"limit": "100",
|
||||
},
|
||||
allowed_status=[200],
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
if not isinstance(rows, list):
|
||||
return False
|
||||
return any(self._is_paid_subscription_row(row) for row in rows if isinstance(row, dict))
|
||||
|
||||
@staticmethod
|
||||
def _normalize_referral_code(value: Optional[str]) -> str:
|
||||
return "".join(str(value or "").strip().upper().split())
|
||||
|
||||
def ensure_referral_code(self, user_id: str) -> Optional[Dict[str, object]]:
|
||||
user_key = str(user_id or "").strip()
|
||||
if not user_key or not self.service_role_key:
|
||||
return None
|
||||
try:
|
||||
rows = self._rest(
|
||||
"GET",
|
||||
"referral_codes",
|
||||
params={
|
||||
"select": "code,status,created_at",
|
||||
"user_id": f"eq.{user_key}",
|
||||
"status": "eq.active",
|
||||
"limit": "1",
|
||||
},
|
||||
allowed_status=[200],
|
||||
)
|
||||
if isinstance(rows, list) and rows and isinstance(rows[0], dict):
|
||||
return rows[0]
|
||||
now = datetime.now(timezone.utc)
|
||||
for _ in range(5):
|
||||
code = f"PW{secrets.token_hex(4).upper()}"
|
||||
try:
|
||||
created = self._rest(
|
||||
"POST",
|
||||
"referral_codes",
|
||||
payload={
|
||||
"user_id": user_key,
|
||||
"code": code,
|
||||
"status": "active",
|
||||
"created_at": self._to_iso(now),
|
||||
"updated_at": self._to_iso(now),
|
||||
},
|
||||
prefer="return=representation",
|
||||
allowed_status=[200, 201],
|
||||
)
|
||||
if isinstance(created, list) and created and isinstance(created[0], dict):
|
||||
return created[0]
|
||||
return {"code": code, "status": "active"}
|
||||
except Exception:
|
||||
continue
|
||||
except Exception as exc:
|
||||
logger.warning("referral code ensure failed user_id={}: {}", user_key, exc)
|
||||
return None
|
||||
|
||||
def get_pending_referral_attribution(self, user_id: str) -> Optional[Dict[str, object]]:
|
||||
user_key = str(user_id or "").strip()
|
||||
if not user_key:
|
||||
return None
|
||||
try:
|
||||
rows = self._rest(
|
||||
"GET",
|
||||
"referral_attributions",
|
||||
params={
|
||||
"select": "id,code,referrer_user_id,referred_user_id,status,created_at",
|
||||
"referred_user_id": f"eq.{user_key}",
|
||||
"status": "eq.pending",
|
||||
"order": "created_at.desc",
|
||||
"limit": "1",
|
||||
},
|
||||
allowed_status=[200],
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
if isinstance(rows, list) and rows and isinstance(rows[0], dict):
|
||||
return rows[0]
|
||||
return None
|
||||
|
||||
def _current_month_reward_rows(self, referrer_user_id: str) -> List[Dict[str, object]]:
|
||||
month_start = datetime.now(timezone.utc).replace(
|
||||
day=1,
|
||||
hour=0,
|
||||
minute=0,
|
||||
second=0,
|
||||
microsecond=0,
|
||||
)
|
||||
try:
|
||||
rows = self._rest(
|
||||
"GET",
|
||||
"referral_rewards",
|
||||
params={
|
||||
"select": "id,reward_days,created_at",
|
||||
"referrer_user_id": f"eq.{referrer_user_id}",
|
||||
"created_at": f"gte.{self._to_iso(month_start)}",
|
||||
"limit": "100",
|
||||
},
|
||||
allowed_status=[200],
|
||||
)
|
||||
except Exception:
|
||||
return []
|
||||
return [row for row in rows if isinstance(row, dict)] if isinstance(rows, list) else []
|
||||
|
||||
def get_referral_summary(self, user_id: str) -> Optional[Dict[str, object]]:
|
||||
user_key = str(user_id or "").strip()
|
||||
if not user_key or not self.service_role_key:
|
||||
return None
|
||||
try:
|
||||
code_row = self.ensure_referral_code(user_key) or {}
|
||||
pending = self.get_pending_referral_attribution(user_key)
|
||||
rewards = self._current_month_reward_rows(user_key)
|
||||
reward_count = len(rewards)
|
||||
reward_days = sum(int(row.get("reward_days") or 0) for row in rewards)
|
||||
return {
|
||||
"code": str(code_row.get("code") or ""),
|
||||
"discount_usdc": REFERRAL_DISCOUNT_USDC,
|
||||
"discounted_monthly_amount_usdc": REFERRAL_MONTHLY_DISCOUNTED_AMOUNT_USDC,
|
||||
"reward_days": REFERRAL_REWARD_DAYS,
|
||||
"monthly_reward_limit": REFERRAL_MONTHLY_REWARD_LIMIT,
|
||||
"monthly_reward_days_limit": REFERRAL_MONTHLY_DAY_LIMIT,
|
||||
"monthly_reward_count": reward_count,
|
||||
"monthly_reward_days": min(reward_days, REFERRAL_MONTHLY_DAY_LIMIT),
|
||||
"applied_code": str(pending.get("code") or "") if isinstance(pending, dict) else "",
|
||||
"attribution_status": str(pending.get("status") or "") if isinstance(pending, dict) else "",
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.warning("referral summary failed user_id={}: {}", user_key, exc)
|
||||
return None
|
||||
|
||||
def apply_referral_code(self, user_id: str, code: str) -> Dict[str, object]:
|
||||
user_key = str(user_id or "").strip()
|
||||
normalized_code = self._normalize_referral_code(code)
|
||||
if not user_key:
|
||||
raise ValueError("user_id required")
|
||||
if len(normalized_code) < 3:
|
||||
raise ValueError("invalid referral code")
|
||||
if self.has_paid_subscription(user_key):
|
||||
raise ValueError("referral code can only be used before first paid subscription")
|
||||
|
||||
rows = self._rest(
|
||||
"GET",
|
||||
"referral_codes",
|
||||
params={
|
||||
"select": "user_id,code,status",
|
||||
"code": f"eq.{normalized_code}",
|
||||
"status": "eq.active",
|
||||
"limit": "1",
|
||||
},
|
||||
allowed_status=[200],
|
||||
)
|
||||
if not isinstance(rows, list) or not rows or not isinstance(rows[0], dict):
|
||||
raise ValueError("referral code not found")
|
||||
referrer_user_id = str(rows[0].get("user_id") or "").strip()
|
||||
if not referrer_user_id or referrer_user_id == user_key:
|
||||
raise ValueError("cannot use your own referral code")
|
||||
|
||||
existing = self.get_pending_referral_attribution(user_key)
|
||||
if isinstance(existing, dict):
|
||||
return {
|
||||
"ok": True,
|
||||
"already_applied": True,
|
||||
"referral": self.get_referral_summary(user_key),
|
||||
}
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
self._rest(
|
||||
"POST",
|
||||
"referral_attributions",
|
||||
payload={
|
||||
"referrer_user_id": referrer_user_id,
|
||||
"referred_user_id": user_key,
|
||||
"code": normalized_code,
|
||||
"status": "pending",
|
||||
"created_at": self._to_iso(now),
|
||||
"updated_at": self._to_iso(now),
|
||||
},
|
||||
prefer="return=minimal",
|
||||
allowed_status=[201],
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"already_applied": False,
|
||||
"referral": self.get_referral_summary(user_key),
|
||||
}
|
||||
|
||||
def _subscription_extension_start(self, user_id: str) -> datetime:
|
||||
now = datetime.now(timezone.utc)
|
||||
try:
|
||||
rows = self._rest(
|
||||
"GET",
|
||||
"subscriptions",
|
||||
params={
|
||||
"select": "starts_at,expires_at,plan_code,source",
|
||||
"user_id": f"eq.{user_id}",
|
||||
"status": "eq.active",
|
||||
"expires_at": f"gt.{self._to_iso(now)}",
|
||||
"order": "expires_at.desc",
|
||||
"limit": "20",
|
||||
},
|
||||
allowed_status=[200],
|
||||
)
|
||||
except Exception:
|
||||
return now
|
||||
starts = now
|
||||
if isinstance(rows, list):
|
||||
for row in rows:
|
||||
if not isinstance(row, dict) or self._is_trial_subscription_row(row):
|
||||
continue
|
||||
exp = self._parse_iso_datetime(str(row.get("expires_at") or ""))
|
||||
starts_at = self._parse_iso_datetime(str(row.get("starts_at") or ""))
|
||||
if exp and (starts_at is None or starts_at <= now) and exp > starts:
|
||||
starts = exp
|
||||
break
|
||||
return starts
|
||||
|
||||
def settle_referral_reward(
|
||||
self,
|
||||
*,
|
||||
referred_user_id: str,
|
||||
payment_intent_id: str,
|
||||
tx_hash: str,
|
||||
) -> Dict[str, object]:
|
||||
referred_key = str(referred_user_id or "").strip()
|
||||
attribution = self.get_pending_referral_attribution(referred_key)
|
||||
if not isinstance(attribution, dict):
|
||||
return {"awarded": False, "reason": "no_pending_referral"}
|
||||
referrer_key = str(attribution.get("referrer_user_id") or "").strip()
|
||||
if not referrer_key or referrer_key == referred_key:
|
||||
return {"awarded": False, "reason": "invalid_referrer"}
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
monthly_rewards = self._current_month_reward_rows(referrer_key)
|
||||
if len(monthly_rewards) >= REFERRAL_MONTHLY_REWARD_LIMIT:
|
||||
self._rest(
|
||||
"PATCH",
|
||||
"referral_attributions",
|
||||
params={"id": f"eq.{attribution.get('id')}"},
|
||||
payload={
|
||||
"status": "capped",
|
||||
"updated_at": self._to_iso(now),
|
||||
"converted_payment_intent_id": payment_intent_id,
|
||||
"converted_tx_hash": tx_hash,
|
||||
},
|
||||
prefer="return=minimal",
|
||||
allowed_status=[204],
|
||||
)
|
||||
return {"awarded": False, "reason": "monthly_cap_reached"}
|
||||
|
||||
starts = self._subscription_extension_start(referrer_key)
|
||||
expires = starts + timedelta(days=REFERRAL_REWARD_DAYS)
|
||||
subscription_payload = {
|
||||
"user_id": referrer_key,
|
||||
"plan_code": "pro_monthly",
|
||||
"status": "active",
|
||||
"starts_at": self._to_iso(starts),
|
||||
"expires_at": self._to_iso(expires),
|
||||
"source": "referral_reward",
|
||||
"created_at": self._to_iso(now),
|
||||
"updated_at": self._to_iso(now),
|
||||
}
|
||||
self._rest(
|
||||
"POST",
|
||||
"subscriptions",
|
||||
payload=subscription_payload,
|
||||
prefer="return=minimal",
|
||||
allowed_status=[201],
|
||||
)
|
||||
self._rest(
|
||||
"POST",
|
||||
"referral_rewards",
|
||||
payload={
|
||||
"referral_attribution_id": attribution.get("id"),
|
||||
"referrer_user_id": referrer_key,
|
||||
"referred_user_id": referred_key,
|
||||
"payment_intent_id": payment_intent_id,
|
||||
"tx_hash": tx_hash,
|
||||
"reward_days": REFERRAL_REWARD_DAYS,
|
||||
"created_at": self._to_iso(now),
|
||||
},
|
||||
prefer="return=minimal",
|
||||
allowed_status=[201],
|
||||
)
|
||||
self._rest(
|
||||
"PATCH",
|
||||
"referral_attributions",
|
||||
params={"id": f"eq.{attribution.get('id')}"},
|
||||
payload={
|
||||
"status": "converted",
|
||||
"converted_payment_intent_id": payment_intent_id,
|
||||
"converted_tx_hash": tx_hash,
|
||||
"converted_at": self._to_iso(now),
|
||||
"updated_at": self._to_iso(now),
|
||||
},
|
||||
prefer="return=minimal",
|
||||
allowed_status=[204],
|
||||
)
|
||||
self._rest(
|
||||
"POST",
|
||||
"entitlement_events",
|
||||
payload={
|
||||
"user_id": referrer_key,
|
||||
"action": "referral_reward_granted",
|
||||
"reason": "referred_user_paid",
|
||||
"actor": "payment_contract_checkout",
|
||||
"payload": {
|
||||
"referred_user_id": referred_key,
|
||||
"payment_intent_id": payment_intent_id,
|
||||
"tx_hash": tx_hash,
|
||||
"reward_days": REFERRAL_REWARD_DAYS,
|
||||
},
|
||||
"created_at": self._to_iso(now),
|
||||
},
|
||||
prefer="return=minimal",
|
||||
allowed_status=[201],
|
||||
)
|
||||
self.invalidate_subscription_cache(referrer_key)
|
||||
return {
|
||||
"awarded": True,
|
||||
"reward_days": REFERRAL_REWARD_DAYS,
|
||||
"referrer_user_id": referrer_key,
|
||||
"subscription": subscription_payload,
|
||||
}
|
||||
|
||||
def get_identity(self, access_token: str) -> Optional[SupabaseIdentity]:
|
||||
if not access_token:
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user