feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
230
server/tests/test_commercial_billing_periods.py
Normal file
230
server/tests/test_commercial_billing_periods.py
Normal file
@@ -0,0 +1,230 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
import app.models # noqa: F401 - 注册完整 metadata
|
||||
from app.db.base_class import Base
|
||||
from app.models.commercial_billing import CommercialAdminEvent, CommercialBillingPeriod
|
||||
from app.schemas.commercial import CommercialPlanCreate, CommercialSubscriptionCreate
|
||||
from app.services.commercial_admin import CommercialAdminService
|
||||
from app.services.commercial_subscription_rollover import (
|
||||
CommercialSubscriptionRolloverService,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def db() -> Session:
|
||||
engine = create_engine(
|
||||
"sqlite+pysqlite:///:memory:",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
Base.metadata.create_all(engine)
|
||||
factory = sessionmaker(bind=engine, expire_on_commit=False)
|
||||
with factory() as session:
|
||||
yield session
|
||||
Base.metadata.drop_all(engine)
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_subscription_creation_issues_period_and_redacted_audit(db: Session) -> None:
|
||||
start = datetime(2026, 1, 1, tzinfo=UTC)
|
||||
end = datetime(2026, 2, 1, tzinfo=UTC)
|
||||
_, subscription = _seed_subscription(
|
||||
db,
|
||||
"tenant-a",
|
||||
start=start,
|
||||
end=end,
|
||||
request_prefix="initial",
|
||||
)
|
||||
|
||||
periods = list(
|
||||
db.scalars(
|
||||
select(CommercialBillingPeriod).where(CommercialBillingPeriod.tenant_id == "tenant-a")
|
||||
).all()
|
||||
)
|
||||
assert len(periods) == 1
|
||||
assert periods[0].subscription_id == subscription.id
|
||||
assert periods[0].period_start.replace(tzinfo=UTC) == start
|
||||
assert periods[0].period_end.replace(tzinfo=UTC) == end
|
||||
assert periods[0].base_fee_snapshot == Decimal("100.0000")
|
||||
|
||||
events = list(
|
||||
db.scalars(
|
||||
select(CommercialAdminEvent).where(CommercialAdminEvent.tenant_id == "tenant-a")
|
||||
).all()
|
||||
)
|
||||
assert {event.action for event in events} >= {
|
||||
"plan_created",
|
||||
"plan_activated",
|
||||
"subscription_created",
|
||||
"billing_period_created",
|
||||
}
|
||||
serialized = str([(event.before_json, event.after_json) for event in events]).casefold()
|
||||
for forbidden in (
|
||||
"contract_terms_json",
|
||||
"metadata_json",
|
||||
"external_subscription_id",
|
||||
"secret",
|
||||
"password",
|
||||
"token",
|
||||
):
|
||||
assert forbidden not in serialized
|
||||
|
||||
|
||||
def test_auto_renew_rollover_is_idempotent_and_keeps_month_end_anchor(db: Session) -> None:
|
||||
start = datetime(2027, 1, 31, tzinfo=UTC)
|
||||
due = datetime(2027, 2, 28, tzinfo=UTC)
|
||||
_, subscription = _seed_subscription(
|
||||
db,
|
||||
"tenant-rollover",
|
||||
start=start,
|
||||
end=due,
|
||||
auto_renew=True,
|
||||
request_prefix="rollover",
|
||||
)
|
||||
|
||||
first = CommercialSubscriptionRolloverService(db).rollover_due(
|
||||
"tenant-rollover",
|
||||
subscription.id,
|
||||
as_of=due,
|
||||
)
|
||||
replay = CommercialSubscriptionRolloverService(db).rollover_due(
|
||||
"tenant-rollover",
|
||||
subscription.id,
|
||||
as_of=due,
|
||||
)
|
||||
|
||||
assert first.status == "rolled_over"
|
||||
assert len(first.created_period_ids) == 1
|
||||
assert first.current_period_start == due
|
||||
assert first.current_period_end == datetime(2027, 3, 31, tzinfo=UTC)
|
||||
assert replay.status == "not_due"
|
||||
periods = list(
|
||||
db.scalars(
|
||||
select(CommercialBillingPeriod)
|
||||
.where(CommercialBillingPeriod.subscription_id == subscription.id)
|
||||
.order_by(CommercialBillingPeriod.period_sequence)
|
||||
).all()
|
||||
)
|
||||
assert [period.period_sequence for period in periods] == [1, 2]
|
||||
assert periods[0].period_start.replace(tzinfo=UTC) == start
|
||||
assert periods[0].period_end.replace(tzinfo=UTC) == due
|
||||
assert periods[1].source == "auto_renew"
|
||||
assert subscription.version == 2
|
||||
actions = set(
|
||||
db.scalars(
|
||||
select(CommercialAdminEvent.action).where(
|
||||
CommercialAdminEvent.tenant_id == "tenant-rollover"
|
||||
)
|
||||
).all()
|
||||
)
|
||||
assert {"billing_period_created", "subscription_rolled_over"} <= actions
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("interval", "contract_end", "reason_code"),
|
||||
[
|
||||
("contract", None, "contract_interval_requires_explicit_renewal"),
|
||||
(
|
||||
"monthly",
|
||||
datetime(2026, 2, 15, tzinfo=UTC),
|
||||
"contract_boundary_requires_renewal",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_rollover_fails_closed_at_unprovable_contract_boundary(
|
||||
db: Session,
|
||||
interval: str,
|
||||
contract_end: datetime | None,
|
||||
reason_code: str,
|
||||
) -> None:
|
||||
tenant_id = f"tenant-{interval}"
|
||||
_, subscription = _seed_subscription(
|
||||
db,
|
||||
tenant_id,
|
||||
start=datetime(2026, 1, 1, tzinfo=UTC),
|
||||
end=datetime(2026, 2, 1, tzinfo=UTC),
|
||||
billing_interval=interval,
|
||||
auto_renew=True,
|
||||
contract_end=contract_end,
|
||||
request_prefix=interval,
|
||||
)
|
||||
|
||||
result = CommercialSubscriptionRolloverService(db).rollover_due(
|
||||
tenant_id,
|
||||
subscription.id,
|
||||
as_of=datetime(2026, 2, 1, tzinfo=UTC),
|
||||
)
|
||||
|
||||
assert result.status == "ineligible"
|
||||
assert result.reason_code == reason_code
|
||||
assert (
|
||||
db.scalar(
|
||||
select(CommercialBillingPeriod.period_sequence).where(
|
||||
CommercialBillingPeriod.subscription_id == subscription.id
|
||||
)
|
||||
)
|
||||
== 1
|
||||
)
|
||||
|
||||
|
||||
def _seed_subscription(
|
||||
db: Session,
|
||||
tenant_id: str,
|
||||
*,
|
||||
start: datetime,
|
||||
end: datetime,
|
||||
billing_interval: str = "monthly",
|
||||
auto_renew: bool = False,
|
||||
contract_end: datetime | None = None,
|
||||
request_prefix: str,
|
||||
):
|
||||
admin = CommercialAdminService(db)
|
||||
plan = admin.create_plan(
|
||||
tenant_id,
|
||||
CommercialPlanCreate(
|
||||
plan_code="billing",
|
||||
name="不可变账期测试套餐",
|
||||
pricing_model="subscription",
|
||||
billing_interval=billing_interval,
|
||||
currency="CNY",
|
||||
base_fee=Decimal("100"),
|
||||
effective_from=start,
|
||||
contract_terms_json={"secret": "must-not-enter-audit"},
|
||||
),
|
||||
actor_id="platform-admin",
|
||||
request_id=f"{request_prefix}-plan-create",
|
||||
)
|
||||
admin.activate_plan(
|
||||
tenant_id,
|
||||
plan.id,
|
||||
expected_version=plan.version,
|
||||
actor_id="platform-admin",
|
||||
request_id=f"{request_prefix}-plan-activate",
|
||||
reason="测试激活套餐",
|
||||
)
|
||||
subscription = admin.create_subscription(
|
||||
tenant_id,
|
||||
CommercialSubscriptionCreate(
|
||||
subscription_key=f"{tenant_id}-subscription",
|
||||
plan_id=plan.id,
|
||||
starts_at=start,
|
||||
ends_at=contract_end,
|
||||
current_period_start=start,
|
||||
current_period_end=end,
|
||||
seats=3,
|
||||
auto_renew=auto_renew,
|
||||
metadata_json={"token": "must-not-enter-audit"},
|
||||
),
|
||||
actor_id="platform-admin",
|
||||
request_id=f"{request_prefix}-subscription-create",
|
||||
)
|
||||
db.flush()
|
||||
return plan, subscription
|
||||
Reference in New Issue
Block a user