fix: restrict telegram pricing to private group
This commit is contained in:
@@ -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 : "";
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
),
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user