feat: 完成权益到期与续期闭环

This commit is contained in:
2026-08-03 14:32:38 +08:00
parent 20ed4875b8
commit b2b56f43bb
18 changed files with 792 additions and 20 deletions

View File

@@ -1,16 +1,16 @@
from __future__ import annotations
from datetime import UTC, datetime
from datetime import UTC, datetime, timedelta
import pytest
from fastapi import HTTPException
from sqlalchemy import create_engine, event
from sqlalchemy import create_engine, event, func, select
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
from app.models import Base
from app.models.chat import ChatMessage, ChatSession, TopicSession
from app.models.entitlement import EntitlementPlan
from app.models.entitlement import EntitlementPlan, UserEntitlement, UserEntitlementLog
from app.models.user import User
from app.services.chat_service import ChatService
from app.services.entitlement_service import EntitlementService, entitlement_dict
@@ -113,6 +113,112 @@ def test_assign_user_plan_replaces_previous_active_plan():
assert view.monthly_topic_remaining == 86
def test_expired_entitlement_falls_back_and_keeps_previous_plan_context():
with _db() as db:
user, _session = _seed_user_session(db)
db.add_all(
[
EntitlementPlan(id=10, name="基础版", plan_type="basic", monthly_topic_limit=30, status=1, sort_order=10),
EntitlementPlan(id=20, name="深度陪伴版", plan_type="deep", monthly_topic_limit=90, status=1, sort_order=20),
UserEntitlement(
id=100,
user_id=user.id,
plan_id=20,
status="active",
effective_at=_now() - timedelta(days=40),
expired_at=_now() - timedelta(days=1),
),
]
)
db.commit()
view = EntitlementService.active_entitlement(db, user)
expired_count = EntitlementService.expire_due_entitlements(db)
db.commit()
assert view.plan_id == 10
assert view.lifecycle_status == "expired_fallback"
assert view.previous_plan_name == "深度陪伴版"
assert view.previous_expired_at is not None
assert expired_count == 1
assert db.get(UserEntitlement, 100).status == "expired"
assert db.scalar(select(UserEntitlementLog).where(UserEntitlementLog.action == "expire")) is not None
def test_renew_user_plan_extends_from_current_expiry_and_is_idempotent():
with _db() as db:
user, _session = _seed_user_session(db)
current_expiry = _now() + timedelta(days=5)
db.add(EntitlementPlan(id=20, name="深度陪伴版", plan_type="deep", monthly_topic_limit=90, status=1))
db.add(
UserEntitlement(
id=100,
user_id=user.id,
plan_id=20,
status="active",
effective_at=_now() - timedelta(days=20),
expired_at=current_expiry,
)
)
db.commit()
renewed = EntitlementService.renew_user_plan(
db,
user=user,
extension_days=30,
request_key="renew-test-key:1",
operated_by=99,
remark="测试续期",
)
db.commit()
duplicate = EntitlementService.renew_user_plan(
db,
user=user,
extension_days=30,
request_key="renew-test-key:1",
operated_by=99,
remark="重复请求",
)
db.commit()
assert renewed.id == duplicate.id
assert renewed.expired_at == current_expiry + timedelta(days=30)
assert db.get(UserEntitlement, 100).status == "replaced"
assert db.scalar(select(func.count(UserEntitlementLog.id)).where(UserEntitlementLog.action == "renew")) == 1
def test_renew_expired_user_plan_restores_same_plan_from_now():
with _db() as db:
user, _session = _seed_user_session(db)
db.add(EntitlementPlan(id=20, name="深度陪伴版", plan_type="deep", monthly_topic_limit=90, status=1))
db.add(
UserEntitlement(
id=100,
user_id=user.id,
plan_id=20,
status="expired",
effective_at=_now() - timedelta(days=40),
expired_at=_now() - timedelta(days=1),
)
)
db.commit()
renewed = EntitlementService.renew_user_plan(
db,
user=user,
extension_days=30,
request_key="restore-test-key:1",
operated_by=99,
)
db.commit()
view = EntitlementService.active_entitlement(db, user)
assert renewed.expired_at is not None
assert timedelta(days=29) < renewed.expired_at - _now() <= timedelta(days=30)
assert view.plan_id == 20
assert view.source == "assigned"
def test_monthly_topic_quota_blocks_new_topic_but_allows_existing_topic():
with _db() as db:
user, session = _seed_user_session(db)