"""积分消耗规则单元测试 (#1895)""" from __future__ import annotations import math import pytest from packages.domain.points_rules import ( DAILY_FREE_CLIP_LIMIT, FREE_USER_MULTIPLIER, MEMBER_DISCOUNT, MEMBERSHIP_PRICES, POINTS_PACKAGES, POINTS_SCENES, calculate_points_cost, ) class TestPointsScenesConfig: """场景配置完整性""" def test_all_nine_scenes_defined(self): assert len(POINTS_SCENES) == 9 def test_required_keys_present(self): for key, scene in POINTS_SCENES.items(): assert "base_points" in scene, f"{key} missing base_points" assert "unit" in scene, f"{key} missing unit" assert "name" in scene, f"{key} missing name" def test_voice_clone_train_is_free(self): assert POINTS_SCENES["voice_clone_train"]["base_points"] == 0 def test_ai_video_has_extra_per_30s(self): assert POINTS_SCENES["ai_video"]["extra_per_30s"] == 1 class TestPointsPackages: def test_three_packages(self): assert len(POINTS_PACKAGES) == 3 assert POINTS_PACKAGES["starter_pack"]["points"] == 100 assert POINTS_PACKAGES["basic_pack"]["points"] == 500 assert POINTS_PACKAGES["pro_pack"]["points"] == 2000 class TestMembershipPrices: def test_three_plans(self): assert len(MEMBERSHIP_PRICES) == 3 assert MEMBERSHIP_PRICES["monthly"]["price_cents"] == 1990 assert MEMBERSHIP_PRICES["quarterly"]["duration_days"] == 90 assert MEMBERSHIP_PRICES["yearly"]["price_cents"] == 15900 class TestDailyFreeLimit: def test_limit_is_2(self): assert DAILY_FREE_CLIP_LIMIT == 2 class TestCalculatePointsCost: """核心计费逻辑""" # ── 按次计费 ── def test_per_time_base_cost(self): # ai_rewrite: 1积分/次,免费用户 ceil(1 * 1.15) = 2 cost = calculate_points_cost("ai_rewrite", is_member=False, quantity=1) assert cost == math.ceil(1 * FREE_USER_MULTIPLIER) def test_per_time_multiple(self): # ai_cover: 1积分/张,3张 → base=3, free: ceil(3*1.15)=4 cost = calculate_points_cost("ai_cover", is_member=False, quantity=3) assert cost == math.ceil(3 * FREE_USER_MULTIPLIER) # ── 按时长计费 ── def test_per_minute_base(self): # ai_voice: 1积分/分钟,3分钟 → base=3, free: ceil(3*1.15)=4 cost = calculate_points_cost("ai_voice", is_member=False, duration_minutes=3) assert cost == math.ceil(3 * FREE_USER_MULTIPLIER) def test_per_minute_rounds_up(self): # 2.3分钟 → ceil(2.3)=3分钟 → base=3 cost = calculate_points_cost("ai_voice", is_member=False, duration_minutes=2.3) assert cost == math.ceil(3 * FREE_USER_MULTIPLIER) def test_digital_human_expensive(self): # ai_digital_human: 15积分/分钟,1分钟 → base=15, free: ceil(15*1.15)=18 cost = calculate_points_cost("ai_digital_human", is_member=False, duration_minutes=1) assert cost == 18 # ── 免费场景 ── def test_voice_clone_train_free(self): cost = calculate_points_cost("voice_clone_train", is_member=False) assert cost == 0 def test_voice_clone_train_free_for_member(self): cost = calculate_points_cost("voice_clone_train", is_member=True) assert cost == 0 # ── 混剪额外逻辑 ── def test_ai_video_short_no_extra(self): # 20s (0.33min) ≤ 30s,不额外加积分,base=3, free: ceil(3*1.15)=4 cost = calculate_points_cost("ai_video", is_member=False, quantity=1, duration_minutes=0.33) assert cost == math.ceil(3 * FREE_USER_MULTIPLIER) def test_ai_video_long_extra_charge(self): # 80s → base=3 + extra ceil((80-30)/30)=2 → total_base=5, free: ceil(5*1.15)=6 cost = calculate_points_cost("ai_video", is_member=False, quantity=1, duration_minutes=80 / 60) assert cost == math.ceil(5 * FREE_USER_MULTIPLIER) # ── 会员折扣 ── def test_monthly_member_discount(self): # ai_voice 1分钟 base=1, 月卡0.9 → floor(1*0.9)=1 → max(1,1)=1 cost = calculate_points_cost("ai_voice", is_member=True, duration_minutes=1, member_type="monthly") assert cost == max(1, math.floor(1 * 0.9)) def test_yearly_member_deep_discount(self): # ai_digital_human 2分钟 base=30, 年卡0.8 → floor(30*0.8)=24 cost = calculate_points_cost( "ai_digital_human", is_member=True, duration_minutes=2, member_type="yearly", ) assert cost == max(1, math.floor(30 * 0.8)) def test_member_without_type_no_discount(self): # is_member=True 但没传 member_type → 不按会员折扣 cost = calculate_points_cost("ai_voice", is_member=True, duration_minutes=1) assert cost == 1 # base=1, no discount applied # ── 异常 ── def test_unknown_scene_raises(self): with pytest.raises(ValueError, match="Unknown points scene"): calculate_points_cost("nonexistent_scene", is_member=False)