fix: restrict telegram pricing to private group

This commit is contained in:
2569718930@qq.com
2026-06-05 17:26:43 +08:00
parent 09979f3bb5
commit 2a6c826f50
10 changed files with 129 additions and 24 deletions
+22 -1
View File
@@ -62,6 +62,10 @@ import {
import { createAccountCopy } from "./account-copy";
import { resetWalletConnectProvider } from "./wallet";
import { useAccountPayment } from "./useAccountPayment";
import {
isTelegramPrivateGroupPriceEligible,
telegramPrivateGroupAmountUsdc,
} from "./telegram-pricing";
// --- Main Component ---
@@ -432,6 +436,12 @@ export function AccountCenter() {
{ plan_code: "pro_monthly", plan_id: 101, amount_usdc: "29.9", duration_days: 30 },
{ plan_code: "pro_quarterly", plan_id: 102, amount_usdc: "79.9", duration_days: 90 },
];
const privateTelegramGroupPriceActive = isTelegramPrivateGroupPriceEligible(
backend?.telegram_pricing,
);
const privateTelegramGroupAmount = telegramPrivateGroupAmountUsdc(
backend?.telegram_pricing,
);
const referral = backend?.referral;
const referralCode = String(referral?.code || "").trim();
const appliedReferralCode = String(referral?.applied_code || "").trim();
@@ -1094,6 +1104,13 @@ export function AccountCenter() {
const code = String(plan.plan_code || "");
const active = code === selectedPlanCode;
const isQuarterly = code === "pro_quarterly";
const isPrivateGroupMonthly = Boolean(
code === "pro_monthly" &&
privateTelegramGroupPriceActive &&
privateTelegramGroupAmount &&
Number(plan.amount_usdc) ===
Number(privateTelegramGroupAmount),
);
return (
<button
type="button"
@@ -1108,7 +1125,11 @@ export function AccountCenter() {
>
<div className="flex items-center justify-between gap-2 text-xs font-bold">
<span>
{isQuarterly ? copy.quarterlyPlan : copy.monthlyPlan}
{isQuarterly
? copy.quarterlyPlan
: isPrivateGroupMonthly
? copy.privateGroupMonthlyPlan
: copy.monthlyPlan}
</span>
<span>{plan.amount_usdc} USDC</span>
</div>
@@ -27,6 +27,10 @@ export function runTests() {
path.join(projectRoot, "components", "account", "useBilling.ts"),
"utf8",
);
const telegramPricing = fs.readFileSync(
path.join(projectRoot, "components", "account", "telegram-pricing.ts"),
"utf8",
);
const types = fs.readFileSync(
path.join(projectRoot, "components", "account", "types.ts"),
"utf8",
@@ -66,21 +70,33 @@ export function runTests() {
assert(
useAccountPayment.includes("applyTelegramGroupPricingToPlanList") &&
useAccountPayment.includes("backend?.telegram_pricing") &&
useAccountPayment.includes("isTelegramPrivateGroupPriceEligible") &&
telegramPricing.includes("is_private_group_member") &&
telegramPricing.includes("telegram_private_group_member") &&
!telegramPricing.includes("is_group_member") &&
useAccountPayment.includes('=== "pro_monthly"') &&
useAccountPayment.includes("amount_usdc: telegramAmountUsdc"),
"account payment plan cards must display the 5 USDC Telegram group member monthly price after /bind verification",
"account payment plan cards must only display the 5 USDC private Telegram group monthly price after private /bind verification",
);
assert(
useBilling.includes("telegramGroupPriceApplies") &&
useBilling.includes("backend?.telegram_pricing?.is_group_member") &&
useBilling.includes("isTelegramPrivateGroupPriceEligible") &&
useBilling.includes("backend?.telegram_pricing") &&
useBilling.includes("!telegramGroupPriceApplies"),
"billing must not let referral first-month pricing override the lower Telegram group member monthly price",
"billing must not let referral first-month pricing override the lower private Telegram group monthly price",
);
assert(
accountCenter.includes("privateGroupMonthlyPlan") &&
accountCopy.includes("Private group monthly") &&
accountCopy.includes("私密群月付"),
"account plan card must label the 5 USDC price as a private Telegram group monthly price",
);
assert(
types.includes("ReferralSummary") &&
types.includes("referral?: ReferralSummary | null") &&
types.includes("is_private_group_member?: boolean") &&
types.includes("duration_days: number") &&
types.includes("max_discount_usdc_by_plan"),
"account auth and payment types must include referral summary and plan durations",
"account auth and payment types must include referral summary, private Telegram pricing, and plan durations",
);
}
@@ -59,6 +59,7 @@ export function createAccountCopy(isEn: boolean): Record<string, string> {
paymentMgmt: isEn ? "Payment Management" : "支付管理",
proPlan: isEn ? "Pro Plan" : "Pro 套餐",
monthlyPlan: isEn ? "Monthly" : "月付",
privateGroupMonthlyPlan: isEn ? "Private group monthly" : "私密群月付",
quarterlyPlan: isEn ? "Quarterly" : "季度",
trialBadge: isEn ? "3-day trial" : "3天试用",
trialPaidGroupLocked: isEn
@@ -0,0 +1,19 @@
import type { TelegramPricing } from "./types";
export function isTelegramPrivateGroupPriceEligible(
pricing?: TelegramPricing | null,
) {
return Boolean(
pricing?.is_private_group_member ||
pricing?.pricing_source === "telegram_private_group_member",
);
}
export function telegramPrivateGroupAmountUsdc(
pricing?: TelegramPricing | null,
) {
if (!isTelegramPrivateGroupPriceEligible(pricing)) return "";
const amount = String(pricing?.amount_usdc || "").trim();
const numeric = Number(amount);
return Number.isFinite(numeric) && numeric > 0 ? amount : "";
}
+1
View File
@@ -46,6 +46,7 @@ export type TelegramPricing = {
telegram_id?: number | null;
telegram_status?: string | null;
is_group_member?: boolean;
is_private_group_member?: boolean;
amount_usdc?: string;
pricing_source?: string;
};
@@ -22,20 +22,17 @@ import { usePaymentState } from "./usePaymentState";
import { useWalletBind } from "./useWalletBind";
import { usePaymentFlow } from "./usePaymentFlow";
import { useBilling } from "./useBilling";
// ============================================================
function telegramMemberAmountUsdc(pricing?: TelegramPricing | null) {
if (!pricing?.is_group_member) return "";
const amount = String(pricing.amount_usdc || "").trim();
const numeric = Number(amount);
return Number.isFinite(numeric) && numeric > 0 ? amount : "";
}
import {
isTelegramPrivateGroupPriceEligible,
telegramPrivateGroupAmountUsdc,
} from "./telegram-pricing";
function applyTelegramGroupPricingToPlanList(
plans: PaymentPlan[],
pricing?: TelegramPricing | null,
): PaymentPlan[] {
const telegramAmountUsdc = telegramMemberAmountUsdc(pricing);
if (!isTelegramPrivateGroupPriceEligible(pricing)) return plans;
const telegramAmountUsdc = telegramPrivateGroupAmountUsdc(pricing);
if (!telegramAmountUsdc) return plans;
return plans.map((plan) =>
String(plan.plan_code || "").toLowerCase() === "pro_monthly"
+6 -4
View File
@@ -19,6 +19,7 @@ import {
} from "./constants";
import { clearStoredPaymentRecovery, shortAddress } from "./formatters";
import { normalizePaymentError } from "./payment-utils";
import { isTelegramPrivateGroupPriceEligible } from "./telegram-pricing";
import { trackAppEvent } from "@/lib/app-analytics";
// ============================================================
@@ -124,7 +125,7 @@ export function useBilling(params: UseBillingParams) {
const selectedPlanCode = String(selectedPlan?.plan_code || "").toLowerCase();
const telegramGroupPriceApplies = Boolean(
selectedPlanCode === "pro_monthly" &&
backend?.telegram_pricing?.is_group_member,
isTelegramPrivateGroupPriceEligible(backend?.telegram_pricing),
);
const referral = backend?.referral;
const referralPending = Boolean(
@@ -192,7 +193,7 @@ export function useBilling(params: UseBillingParams) {
paymentConfig?.points_redemption,
backend?.referral,
backend?.subscription_active,
backend?.telegram_pricing?.is_group_member,
backend?.telegram_pricing,
selectedPlan?.plan_code,
selectedPlan?.amount_usdc,
totalPoints,
@@ -405,8 +406,9 @@ export function useBilling(params: UseBillingParams) {
throw new Error(copy.bindFailed.replace("{raw}", raw));
}
const data = (await res.json()) as { telegram_pricing?: TelegramPricing | null };
if (data.telegram_pricing?.is_group_member) {
const amount = data.telegram_pricing.amount_usdc || "10";
const telegramPricing = data.telegram_pricing;
if (isTelegramPrivateGroupPriceEligible(telegramPricing)) {
const amount = telegramPricing?.amount_usdc || "10";
setPaymentInfo(copy.telegramVerifySuccess.replace("{amount}", amount));
}
await loadSnapshot();
+9 -2
View File
@@ -97,6 +97,7 @@ class TelegramGroupPricing:
os.getenv("TELEGRAM_CHAT_IDS"),
os.getenv("TELEGRAM_CHAT_ID"),
)
self.private_group_chat_ids = dedicated_group_chat_ids
self.group_chat_ids = dedicated_group_chat_ids or fallback_group_chat_ids
self.member_price = _decimal_env("POLYWEATHER_GROUP_MEMBER_PRICE_USDC", "5")
self.public_price = _decimal_env("POLYWEATHER_PUBLIC_PRICE_USDC", "10")
@@ -139,12 +140,18 @@ class TelegramGroupPricing:
def resolve_price_for_telegram_id(self, telegram_id: Optional[int]) -> Dict[str, Any]:
status = self.get_member_status(int(telegram_id or 0)) if telegram_id else None
is_member = bool(status in TELEGRAM_MEMBER_STATUSES)
amount = self.member_price if is_member else self.public_price
is_private_group_member = bool(self.private_group_chat_ids and is_member)
amount = self.member_price if is_private_group_member else self.public_price
return {
"configured": self.configured,
"telegram_id": int(telegram_id or 0) or None,
"telegram_status": status,
"is_group_member": is_member,
"is_private_group_member": is_private_group_member,
"amount_usdc": _format_decimal(amount),
"pricing_source": "telegram_group_member" if is_member else "telegram_public",
"pricing_source": (
"telegram_private_group_member"
if is_private_group_member
else "telegram_public"
),
}
+1 -1
View File
@@ -1696,7 +1696,7 @@ class PaymentContractCheckoutService:
except Exception:
telegram_id = None
price_payload = pricing.resolve_price_for_telegram_id(telegram_id)
if not bool(price_payload.get("is_group_member")):
if not bool(price_payload.get("is_private_group_member")):
return out
amount_dec = _parse_decimal(
price_payload.get("amount_usdc"), out["amount_usdc_decimal"]
+44 -3
View File
@@ -74,8 +74,10 @@ def test_group_pricing_treats_member_status_as_group_member(monkeypatch):
result = pricing.resolve_price_for_telegram_id(12345)
assert result["is_group_member"] is True
assert result["is_private_group_member"] is True
assert result["amount_usdc"] == "5"
assert result["telegram_status"] == "member"
assert result["pricing_source"] == "telegram_private_group_member"
def test_group_member_price_defaults_to_five_usdc(monkeypatch):
@@ -99,6 +101,7 @@ def test_group_member_price_defaults_to_five_usdc(monkeypatch):
result = pricing.resolve_price_for_telegram_id(12345)
assert result["is_group_member"] is True
assert result["is_private_group_member"] is True
assert result["amount_usdc"] == "5"
@@ -124,6 +127,7 @@ def test_group_pricing_treats_left_status_as_public_price(monkeypatch):
result = pricing.resolve_price_for_telegram_id(12345)
assert result["is_group_member"] is False
assert result["is_private_group_member"] is False
assert result["amount_usdc"] == "10"
assert result["telegram_status"] == "left"
@@ -155,6 +159,43 @@ def test_payment_plan_uses_linked_telegram_group_price(monkeypatch, tmp_path):
service = PaymentContractCheckoutService()
service._db.bind_supabase_identity(12345, "user-1", "u@example.com")
monkeypatch.setattr(
"src.auth.telegram_group_pricing.TelegramGroupPricing.resolve_price_for_telegram_id",
lambda self, telegram_id: {
"configured": True,
"telegram_id": telegram_id,
"is_group_member": True,
"is_private_group_member": True,
"telegram_status": "member",
"amount_usdc": "5",
"pricing_source": "telegram_private_group_member",
},
)
plan = service._select_plan("pro_monthly")
priced = service._apply_telegram_group_pricing("user-1", plan)
assert priced["amount_usdc"] == "5"
assert priced["amount_usdc_decimal"] == Decimal("5")
assert priced["telegram_pricing"]["pricing_source"] == "telegram_private_group_member"
def test_payment_plan_ignores_legacy_group_member_without_private_group_flag(monkeypatch, tmp_path):
monkeypatch.setenv("POLYWEATHER_PAYMENT_ENABLED", "true")
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
monkeypatch.setenv("POLYWEATHER_PAYMENT_RPC_URL", "https://rpc-1.example")
monkeypatch.setenv("POLYWEATHER_DB_PATH", str(tmp_path / "payments.db"))
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "bot-token")
monkeypatch.setenv("POLYWEATHER_TELEGRAM_GROUP_ID", "-100123")
monkeypatch.setenv("POLYWEATHER_GROUP_MEMBER_PRICE_USDC", "5")
monkeypatch.setenv(
"POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON",
'[{"code":"usdc_e","address":"0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174","decimals":6,"receiver_contract":"0xeD2f13Aa5fF033c58FB436E178451Cd07f693f32","is_default":true}]',
)
service = PaymentContractCheckoutService()
service._db.bind_supabase_identity(12345, "user-1", "u@example.com")
monkeypatch.setattr(
"src.auth.telegram_group_pricing.TelegramGroupPricing.resolve_price_for_telegram_id",
lambda self, telegram_id: {
@@ -170,9 +211,9 @@ def test_payment_plan_uses_linked_telegram_group_price(monkeypatch, tmp_path):
plan = service._select_plan("pro_monthly")
priced = service._apply_telegram_group_pricing("user-1", plan)
assert priced["amount_usdc"] == "5"
assert priced["amount_usdc_decimal"] == Decimal("5")
assert priced["telegram_pricing"]["pricing_source"] == "telegram_group_member"
assert priced["amount_usdc"] == "29.9"
assert priced["amount_usdc_decimal"] == Decimal("29.9")
assert "telegram_pricing" not in priced
def test_payment_telegram_pricing_enabled_by_default_when_configured(monkeypatch, tmp_path):