Add trial and referral subscription program

This commit is contained in:
2569718930@qq.com
2026-05-29 19:24:46 +08:00
parent f8f5035225
commit 522e35de7f
20 changed files with 1629 additions and 54 deletions
+123 -11
View File
@@ -13,6 +13,7 @@ from typing import Any, Dict, List, Optional
import requests
from eth_account import Account
from eth_account.messages import encode_defunct
from loguru import logger
from web3 import Web3
from src.auth.supabase_entitlement import SUPABASE_ENTITLEMENT
@@ -90,9 +91,12 @@ ERC20_TRANSFER_EVENT_ABI = {
}
DEFAULT_PLAN_CATALOG: Dict[str, Dict[str, Any]] = {
"pro_monthly": {"plan_id": 101, "amount_usdc": "10", "duration_days": 30},
"pro_monthly": {"plan_id": 101, "amount_usdc": "29.9", "duration_days": 30},
"pro_quarterly": {"plan_id": 102, "amount_usdc": "79.9", "duration_days": 90},
}
REFERRAL_FIRST_MONTH_DISCOUNT_USDC = Decimal("3")
def _env_bool(name: str, default: bool = False) -> bool:
raw = os.getenv(name)
@@ -195,13 +199,13 @@ def _parse_plan_catalog(raw: str) -> Dict[str, Dict[str, Any]]:
def _parse_allowed_plan_codes(raw: str) -> List[str]:
text = str(raw or "").strip()
if not text:
return ["pro_monthly"]
return ["pro_monthly", "pro_quarterly"]
out: List[str] = []
for part in text.split(","):
code = str(part or "").strip().lower()
if code and code not in out:
out.append(code)
return out or ["pro_monthly"]
return out or ["pro_monthly", "pro_quarterly"]
@dataclass
@@ -363,6 +367,10 @@ class PaymentContractCheckoutService:
self.notify_telegram = _env_bool(
"POLYWEATHER_PAYMENT_TELEGRAM_NOTIFY_ENABLED", True
)
self.telegram_payment_pricing_enabled = _env_bool(
"POLYWEATHER_PAYMENT_TELEGRAM_PRICING_ENABLED",
False,
)
self.points_enabled = _env_bool("POLYWEATHER_PAYMENT_POINTS_ENABLED", True)
self.points_per_usdc = max(
1, _env_int("POLYWEATHER_PAYMENT_POINTS_PER_USDC", 500)
@@ -1650,6 +1658,55 @@ class PaymentContractCheckoutService:
out["telegram_pricing"] = price_payload
return out
def _get_pending_referral_attribution(self, user_id: str) -> Optional[Dict[str, Any]]:
try:
row = SUPABASE_ENTITLEMENT.get_pending_referral_attribution(user_id)
return dict(row) if isinstance(row, dict) else None
except Exception:
return None
def _has_prior_paid_subscription(self, user_id: str) -> bool:
try:
return bool(SUPABASE_ENTITLEMENT.has_paid_subscription(user_id))
except Exception:
return False
def _apply_referral_pricing(
self,
user_id: str,
plan: Dict[str, Any],
) -> Dict[str, Any]:
out = dict(plan)
attribution = self._get_pending_referral_attribution(user_id)
if not attribution or self._has_prior_paid_subscription(user_id):
return out
out["referral_attribution"] = {
"id": attribution.get("id"),
"code": str(attribution.get("code") or "").strip().upper(),
"referrer_user_id": str(attribution.get("referrer_user_id") or "").strip(),
"referred_user_id": user_id,
}
if str(out.get("plan_code") or "").strip().lower() != "pro_monthly":
return out
base_amount = _parse_decimal(out.get("amount_usdc_decimal"), Decimal("0"))
discount = min(REFERRAL_FIRST_MONTH_DISCOUNT_USDC, base_amount)
discounted = base_amount - discount
if discount <= 0 or discounted <= 0:
return out
out["amount_before_discount_usdc_decimal"] = base_amount
out["amount_usdc_decimal"] = discounted
out["amount_usdc"] = _format_decimal(discounted)
out["referral_discount"] = {
"discount_usdc": _format_decimal(discount),
"amount_before_discount_usdc": _format_decimal(base_amount),
"amount_after_discount_usdc": _format_decimal(discounted),
"reason": "first_month_referral",
}
return out
def _build_tx_payload(self, intent: PaymentIntentRecord) -> Dict[str, Any]:
contract = self._get_contract(intent.receiver_address, intent.chain_id)
tx_data = contract.encode_abi(
@@ -1687,10 +1744,10 @@ class PaymentContractCheckoutService:
points_to_consume: Optional[int] = None,
) -> Dict[str, Any]:
self._ensure_enabled()
plan = self._apply_telegram_group_pricing(
user_id,
self._select_plan(plan_code),
)
selected_plan = self._select_plan(plan_code)
if self.telegram_payment_pricing_enabled:
selected_plan = self._apply_telegram_group_pricing(user_id, selected_plan)
plan = self._apply_referral_pricing(user_id, selected_plan)
selected_token = self._resolve_supported_token(token_address, chain_id)
selected_chain_id = int(selected_token.chain_id)
mode = str(payment_mode or "strict").strip().lower()
@@ -1728,6 +1785,10 @@ class PaymentContractCheckoutService:
elif target_wallet:
self._require_user_wallet(user_id, target_wallet)
plan_amount_usdc = plan["amount_usdc_decimal"]
amount_before_discount_usdc = plan.get(
"amount_before_discount_usdc_decimal",
plan_amount_usdc,
)
redemption = self._build_points_redemption(
user_id=user_id,
plan_amount_usdc=plan_amount_usdc,
@@ -1748,13 +1809,17 @@ class PaymentContractCheckoutService:
combined_metadata["chain_name"] = selected_token.chain_name
if isinstance(plan.get("telegram_pricing"), dict):
combined_metadata["telegram_pricing"] = plan["telegram_pricing"]
if isinstance(plan.get("referral_attribution"), dict):
combined_metadata["referral_attribution"] = plan["referral_attribution"]
if isinstance(plan.get("referral_discount"), dict):
combined_metadata["referral_discount"] = plan["referral_discount"]
receiver_address = (
selected_token.direct_receiver_address
if mode == "direct"
else selected_token.receiver_contract
)
combined_metadata["amount_before_discount_usdc"] = _format_decimal(
plan_amount_usdc
amount_before_discount_usdc
)
combined_metadata["amount_after_discount_usdc"] = _format_decimal(
final_amount_usdc
@@ -1813,7 +1878,9 @@ class PaymentContractCheckoutService:
"plan_code": plan["plan_code"],
"plan_id": plan["plan_id"],
"duration_days": plan["duration_days"],
"amount_before_discount_usdc": _format_decimal(plan_amount_usdc),
"amount_before_discount_usdc": _format_decimal(
amount_before_discount_usdc
),
"amount_after_discount_usdc": _format_decimal(final_amount_usdc),
},
"token": {
@@ -2487,13 +2554,14 @@ class PaymentContractCheckoutService:
duration_days: int,
tx_hash: str,
payload: Dict[str, Any],
source: str = "payment",
) -> Dict[str, Any]:
now = _now_utc()
latest_rows = self._rest(
"GET",
"subscriptions",
params={
"select": "starts_at,expires_at",
"select": "starts_at,expires_at,plan_code,source",
"user_id": f"eq.{user_id}",
"status": "eq.active",
"order": "expires_at.desc",
@@ -2507,6 +2575,8 @@ class PaymentContractCheckoutService:
for row in latest_rows:
if not isinstance(row, dict):
continue
if self._subscription_row_is_trial(row):
continue
try:
starts_at = datetime.fromisoformat(
str(row.get("starts_at") or "").replace("Z", "+00:00")
@@ -2540,7 +2610,7 @@ class PaymentContractCheckoutService:
"status": "active",
"starts_at": _to_iso(starts),
"expires_at": _to_iso(expires),
"source": "payment_contract",
"source": str(source or "payment").strip() or "payment",
"created_at": _to_iso(now),
"updated_at": _to_iso(now),
}
@@ -2602,6 +2672,31 @@ class PaymentContractCheckoutService:
source = str(row.get("source") or "").strip().lower()
return "trial" in plan_code or "trial" in source
def _settle_referral_reward_for_intent(
self,
user_id: str,
intent: PaymentIntentRecord,
tx_hash: str,
) -> Dict[str, Any]:
metadata = dict(intent.metadata or {})
if not isinstance(metadata.get("referral_attribution"), dict):
return {}
try:
result = SUPABASE_ENTITLEMENT.settle_referral_reward(
referred_user_id=user_id,
payment_intent_id=intent.intent_id,
tx_hash=tx_hash,
)
return dict(result) if isinstance(result, dict) else {}
except Exception as exc:
logger.warning(
"referral reward settlement failed user_id={} intent_id={}: {}",
user_id,
intent.intent_id,
exc,
)
return {"awarded": False, "reason": "settlement_error"}
def _ensure_confirm_side_effects(
self,
user_id: str,
@@ -2624,9 +2719,15 @@ class PaymentContractCheckoutService:
},
)
subscription_row = self._ensure_confirmed_subscription(user_id, intent, tx_hash)
referral_reward = self._settle_referral_reward_for_intent(
user_id,
intent,
tx_hash,
)
return {
"payment": payment_row,
"subscription": subscription_row,
"referral_reward": referral_reward,
}
def _attempt_confirm_repair(
@@ -2779,6 +2880,7 @@ class PaymentContractCheckoutService:
"already_confirmed": True,
"payment": repaired.get("payment"),
"subscription": repaired.get("subscription"),
"referral_reward": repaired.get("referral_reward"),
}
if intent.status in {"cancelled", "expired"}:
raise PaymentCheckoutError(409, f"intent status is {intent.status}")
@@ -2964,6 +3066,7 @@ class PaymentContractCheckoutService:
"duplicate_tx_hash": tx_hash_text,
"payment": repaired.get("payment"),
"subscription": repaired.get("subscription"),
"referral_reward": repaired.get("referral_reward"),
}
raise PaymentCheckoutError(
409, f"intent status is {refreshed.status}, cannot confirm"
@@ -2999,6 +3102,7 @@ class PaymentContractCheckoutService:
plan = self._select_plan(intent.plan_code)
payment_row = {}
subscription_row = {}
referral_reward = {}
try:
payment_row = self._insert_payment_record(
user_id=user_id,
@@ -3015,6 +3119,12 @@ class PaymentContractCheckoutService:
tx_hash=tx_hash_text,
payload=payload,
)
intent.metadata = confirmed_metadata
referral_reward = self._settle_referral_reward_for_intent(
user_id,
intent,
tx_hash_text,
)
except PaymentCheckoutError as exc:
repaired = self._attempt_confirm_repair(
user_id=user_id,
@@ -3025,6 +3135,7 @@ class PaymentContractCheckoutService:
)
payment_row = repaired.get("payment") or payment_row
subscription_row = repaired.get("subscription") or subscription_row
referral_reward = repaired.get("referral_reward") or referral_reward
if not subscription_row:
raise
self._notify_telegram(
@@ -3046,6 +3157,7 @@ class PaymentContractCheckoutService:
"transaction": tx_payload,
"payment": payment_row,
"subscription": subscription_row,
"referral_reward": referral_reward,
"points_redemption": points_result,
"tx": payload,
}