fix: apply telegram group member monthly price
This commit is contained in:
@@ -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") &&
|
||||
|
||||
@@ -271,8 +271,8 @@ export function createAccountCopy(isEn: boolean): Record<string, string> {
|
||||
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 并确认绑定;完成后刷新本页再申请入群。",
|
||||
|
||||
@@ -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 ──────────────────────────────────────
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user