From 137b0d8b3a4ac4053e67f263d596d2411b618dc7 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Tue, 2 Jun 2026 21:35:35 +0800 Subject: [PATCH] fix: apply telegram group member monthly price --- .../__tests__/paymentReferralPricing.test.ts | 17 +++++++ frontend/components/account/account-copy.ts | 4 +- .../components/account/useAccountPayment.ts | 28 ++++++++++- frontend/components/account/useBilling.ts | 6 +++ src/auth/telegram_group_pricing.py | 2 +- src/payments/contract_checkout.py | 4 +- tests/test_telegram_group_pricing.py | 49 +++++++++++++++++-- 7 files changed, 101 insertions(+), 9 deletions(-) diff --git a/frontend/components/account/__tests__/paymentReferralPricing.test.ts b/frontend/components/account/__tests__/paymentReferralPricing.test.ts index c348a15c..aa0674ba 100644 --- a/frontend/components/account/__tests__/paymentReferralPricing.test.ts +++ b/frontend/components/account/__tests__/paymentReferralPricing.test.ts @@ -23,6 +23,10 @@ export function runTests() { path.join(projectRoot, "components", "account", "usePaymentFlow.ts"), "utf8", ); + const useBilling = fs.readFileSync( + path.join(projectRoot, "components", "account", "useBilling.ts"), + "utf8", + ); const types = fs.readFileSync( path.join(projectRoot, "components", "account", "types.ts"), "utf8", @@ -59,6 +63,19 @@ export function runTests() { !usePaymentFlow.includes("monthlyPlanList"), "payment hooks must not filter checkout plans down to monthly only", ); + assert( + useAccountPayment.includes("applyTelegramGroupPricingToPlanList") && + useAccountPayment.includes("backend?.telegram_pricing") && + 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", + ); + assert( + useBilling.includes("telegramGroupPriceApplies") && + useBilling.includes("backend?.telegram_pricing?.is_group_member") && + useBilling.includes("!telegramGroupPriceApplies"), + "billing must not let referral first-month pricing override the lower Telegram group member monthly price", + ); assert( types.includes("ReferralSummary") && types.includes("referral?: ReferralSummary | null") && diff --git a/frontend/components/account/account-copy.ts b/frontend/components/account/account-copy.ts index ff5a9dd5..2d47fb85 100644 --- a/frontend/components/account/account-copy.ts +++ b/frontend/components/account/account-copy.ts @@ -271,8 +271,8 @@ export function createAccountCopy(isEn: boolean): Record { verifyUnknown: isEn ? "Unknown error" : "未知错误", // ── Telegram bind messages ──────────────────────────────────────── telegramVerifySuccess: isEn - ? "Telegram group membership verified. Checkout follows the selected Pro plan." - : "Telegram 群成员验证成功,结算金额以当前选择的 Pro 套餐为准。", + ? "Telegram group membership verified. Member monthly price {amount} USDC is active." + : "Telegram 群成员验证成功,群友月付价 {amount} USDC 已生效。", telegramBindClickHint: isEn ? "Open the Telegram Bot, click Start, and confirm binding. Then refresh this page to request group entry." : "已打开 Telegram Bot,请在 Bot 内点击 Start 并确认绑定;完成后刷新本页再申请入群。", diff --git a/frontend/components/account/useAccountPayment.ts b/frontend/components/account/useAccountPayment.ts index 5a935628..3afbd467 100644 --- a/frontend/components/account/useAccountPayment.ts +++ b/frontend/components/account/useAccountPayment.ts @@ -13,14 +13,37 @@ import type { AuthMeResponse, BoundWallet, PaymentConfig, + PaymentPlan, ProviderMode, InjectedProviderOption, + TelegramPricing, } from "./types"; 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 : ""; +} + +function applyTelegramGroupPricingToPlanList( + plans: PaymentPlan[], + pricing?: TelegramPricing | null, +): PaymentPlan[] { + const telegramAmountUsdc = telegramMemberAmountUsdc(pricing); + if (!telegramAmountUsdc) return plans; + return plans.map((plan) => + String(plan.plan_code || "").toLowerCase() === "pro_monthly" + ? { ...plan, amount_usdc: telegramAmountUsdc } + : plan, + ); +} + // ============================================================ export interface UseAccountPaymentParams { isEn: boolean; @@ -387,7 +410,10 @@ export function useAccountPayment(params: UseAccountPaymentParams) { ]); // ── Selected plan (derived, shared across sub-hooks) ─── - const effectivePlanList = paymentConfig?.plans || []; + const effectivePlanList = applyTelegramGroupPricingToPlanList( + paymentConfig?.plans || [], + backend?.telegram_pricing, + ); const selectedPlan = effectivePlanList.find((p) => p.plan_code === selectedPlanCode) || effectivePlanList[0]; // ── useWalletBind ────────────────────────────────────── diff --git a/frontend/components/account/useBilling.ts b/frontend/components/account/useBilling.ts index cc756af1..ea440c45 100644 --- a/frontend/components/account/useBilling.ts +++ b/frontend/components/account/useBilling.ts @@ -122,6 +122,10 @@ export function useBilling(params: UseBillingParams) { const listAmount = Number.isFinite(listAmountRaw) && listAmountRaw > 0 ? listAmountRaw : 29.9; const selectedPlanCode = String(selectedPlan?.plan_code || "").toLowerCase(); + const telegramGroupPriceApplies = Boolean( + selectedPlanCode === "pro_monthly" && + backend?.telegram_pricing?.is_group_member, + ); const referral = backend?.referral; const referralPending = Boolean( referral?.applied_code || @@ -137,6 +141,7 @@ export function useBilling(params: UseBillingParams) { const referralApplies = selectedPlanCode === "pro_monthly" && referralPending && + !telegramGroupPriceApplies && backend?.subscription_active !== true; const planAmount = referralApplies ? Number.isFinite(discountedMonthlyRaw) && discountedMonthlyRaw > 0 @@ -187,6 +192,7 @@ export function useBilling(params: UseBillingParams) { paymentConfig?.points_redemption, backend?.referral, backend?.subscription_active, + backend?.telegram_pricing?.is_group_member, selectedPlan?.plan_code, selectedPlan?.amount_usdc, totalPoints, diff --git a/src/auth/telegram_group_pricing.py b/src/auth/telegram_group_pricing.py index 7ac0de1e..37c1cdfe 100644 --- a/src/auth/telegram_group_pricing.py +++ b/src/auth/telegram_group_pricing.py @@ -98,7 +98,7 @@ class TelegramGroupPricing: os.getenv("TELEGRAM_CHAT_ID"), ) self.group_chat_ids = dedicated_group_chat_ids or fallback_group_chat_ids - self.member_price = _decimal_env("POLYWEATHER_GROUP_MEMBER_PRICE_USDC", "10") + self.member_price = _decimal_env("POLYWEATHER_GROUP_MEMBER_PRICE_USDC", "5") self.public_price = _decimal_env("POLYWEATHER_PUBLIC_PRICE_USDC", "10") self.timeout_sec = max( 2, diff --git a/src/payments/contract_checkout.py b/src/payments/contract_checkout.py index d3ec5520..e12b0726 100644 --- a/src/payments/contract_checkout.py +++ b/src/payments/contract_checkout.py @@ -396,7 +396,7 @@ class PaymentContractCheckoutService: ) self.telegram_payment_pricing_enabled = _env_bool( "POLYWEATHER_PAYMENT_TELEGRAM_PRICING_ENABLED", - False, + True, ) self.points_enabled = _env_bool("POLYWEATHER_PAYMENT_POINTS_ENABLED", True) self.points_per_usdc = max( @@ -1696,6 +1696,8 @@ 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")): + return out amount_dec = _parse_decimal( price_payload.get("amount_usdc"), out["amount_usdc_decimal"] ) diff --git a/tests/test_telegram_group_pricing.py b/tests/test_telegram_group_pricing.py index 7249c91f..59309374 100644 --- a/tests/test_telegram_group_pricing.py +++ b/tests/test_telegram_group_pricing.py @@ -78,6 +78,30 @@ def test_group_pricing_treats_member_status_as_group_member(monkeypatch): assert result["telegram_status"] == "member" +def test_group_member_price_defaults_to_five_usdc(monkeypatch): + monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "bot-token") + monkeypatch.setenv("POLYWEATHER_TELEGRAM_GROUP_ID", "-100123") + monkeypatch.delenv("POLYWEATHER_GROUP_MEMBER_PRICE_USDC", raising=False) + + def fake_get(url, params, timeout): + class Response: + status_code = 200 + + @staticmethod + def json(): + return {"ok": True, "result": {"status": "member"}} + + return Response() + + monkeypatch.setattr("src.auth.telegram_group_pricing.requests.get", fake_get) + + pricing = TelegramGroupPricing() + result = pricing.resolve_price_for_telegram_id(12345) + + assert result["is_group_member"] is True + assert result["amount_usdc"] == "5" + + def test_group_pricing_treats_left_status_as_public_price(monkeypatch): monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "bot-token") monkeypatch.setenv("POLYWEATHER_TELEGRAM_GROUP_ID", "-100123") @@ -151,7 +175,24 @@ def test_payment_plan_uses_linked_telegram_group_price(monkeypatch, tmp_path): assert priced["telegram_pricing"]["pricing_source"] == "telegram_group_member" -def test_payment_plan_uses_public_price_without_telegram_link(monkeypatch, tmp_path): +def test_payment_telegram_pricing_enabled_by_default_when_configured(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.delenv("POLYWEATHER_PAYMENT_TELEGRAM_PRICING_ENABLED", raising=False) + monkeypatch.setenv( + "POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON", + '[{"code":"usdc_e","address":"0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174","decimals":6,"receiver_contract":"0xeD2f13Aa5fF033c58FB436E178451Cd07f693f32","is_default":true}]', + ) + + service = PaymentContractCheckoutService() + + assert service.telegram_payment_pricing_enabled is True + + +def test_payment_plan_keeps_catalog_price_without_telegram_link(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") @@ -170,6 +211,6 @@ def test_payment_plan_uses_public_price_without_telegram_link(monkeypatch, tmp_p plan = service._select_plan("pro_monthly") priced = service._apply_telegram_group_pricing("user-1", plan) - assert priced["amount_usdc"] == "10" - assert priced["amount_usdc_decimal"] == Decimal("10") - assert priced["telegram_pricing"]["pricing_source"] == "telegram_public" + assert priced["amount_usdc"] == "29.9" + assert priced["amount_usdc_decimal"] == Decimal("29.9") + assert "telegram_pricing" not in priced