fix finance balance delta recharges

This commit is contained in:
SmartUp Developer
2026-07-03 20:05:28 +08:00
parent bb7ed5a7e0
commit 1670503720
12 changed files with 745 additions and 24 deletions
+168 -1
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import datetime as _dt
import json
from datetime import date
from datetime import datetime, timezone
from unittest.mock import MagicMock
import httpx
@@ -13,6 +14,7 @@ from app.services.finance_service import (
date_to_shanghai_timestamps,
fetch_upstream_cost_sub2api,
fetch_upstream_cost_new_api,
fetch_upstream_cost_balance_delta,
fetch_website_revenue,
get_daily_summary,
)
@@ -31,6 +33,7 @@ class FakeUpstream:
self.auth_type = kw.get("auth_type", "bearer")
self.auth_config_json = kw.get("auth_config_json", json.dumps({"token": "tok"}))
self.timeout_seconds = kw.get("timeout_seconds", 30)
self.finance_cost_mode = kw.get("finance_cost_mode", "usage_stats")
self.enabled = True
@@ -407,6 +410,163 @@ def test_get_daily_summary_all_success(monkeypatch):
assert abs(result["margin_percent"] - 60.0) < 1e-6
def _add_balance_snapshot(db, upstream_id: int, balance: float, captured_at: datetime):
from app.models.snapshot import UpstreamBalanceSnapshot
row = UpstreamBalanceSnapshot(
upstream_id=upstream_id,
balance=balance,
captured_at=captured_at,
)
db.add(row)
db.commit()
return row
def _add_recharge_event(db, upstream_id: int, recharge_date: date, amount: float, note: str = ""):
from app.models.upstream_recharge_event import UpstreamRechargeEvent
row = UpstreamRechargeEvent(
upstream_id=upstream_id,
recharge_date=recharge_date,
amount=amount,
note=note,
)
db.add(row)
db.commit()
return row
def test_balance_delta_cost_uses_manual_recharge_and_ending_balance():
"""Balance-delta cost is baseline + recharge_total - ending."""
db = _make_inmemory_db()
u = FakeUpstream(id=1, finance_cost_mode="balance_delta")
_add_balance_snapshot(db, 1, 100.0, datetime(2026, 7, 1, 15, 55, tzinfo=timezone.utc))
_add_balance_snapshot(db, 1, 90.0, datetime(2026, 7, 1, 16, 30, tzinfo=timezone.utc))
_add_balance_snapshot(db, 1, 110.0, datetime(2026, 7, 1, 18, 0, tzinfo=timezone.utc))
_add_balance_snapshot(db, 1, 80.0, datetime(2026, 7, 2, 15, 59, tzinfo=timezone.utc))
_add_recharge_event(db, 1, date(2026, 7, 2), 20.0)
amount, err = fetch_upstream_cost_balance_delta(u, date(2026, 7, 2), db)
assert err is None
assert abs(amount - 40.0) < 1e-6
db.close()
def test_balance_delta_balance_rise_without_recharge_fails():
db = _make_inmemory_db()
u = FakeUpstream(id=1, finance_cost_mode="balance_delta")
_add_balance_snapshot(db, 1, 100.0, datetime(2026, 7, 1, 15, 55, tzinfo=timezone.utc))
_add_balance_snapshot(db, 1, 120.0, datetime(2026, 7, 1, 16, 30, tzinfo=timezone.utc))
amount, err = fetch_upstream_cost_balance_delta(u, date(2026, 7, 2), db)
assert amount == 0.0
assert "无充值记录" in err
db.close()
def test_balance_delta_recharge_less_than_observed_rise_fails():
db = _make_inmemory_db()
u = FakeUpstream(id=1, finance_cost_mode="balance_delta")
_add_balance_snapshot(db, 1, 100.0, datetime(2026, 7, 1, 15, 55, tzinfo=timezone.utc))
_add_balance_snapshot(db, 1, 130.0, datetime(2026, 7, 1, 16, 30, tzinfo=timezone.utc))
_add_recharge_event(db, 1, date(2026, 7, 2), 20.0)
amount, err = fetch_upstream_cost_balance_delta(u, date(2026, 7, 2), db)
assert amount == 0.0
assert "充值金额可能漏填" in err
db.close()
def test_balance_delta_missing_baseline_fails():
"""Balance-delta requires a pre-day baseline sample."""
db = _make_inmemory_db()
u = FakeUpstream(id=1, finance_cost_mode="balance_delta")
_add_balance_snapshot(db, 1, 90.0, datetime(2026, 7, 1, 16, 30, tzinfo=timezone.utc))
amount, err = fetch_upstream_cost_balance_delta(u, date(2026, 7, 2), db)
assert amount == 0.0
assert "基线样本" in err
db.close()
def test_get_daily_summary_balance_delta_failure_excluded_from_total():
"""Incomplete balance-delta data must not be counted as zero-cost success."""
db = _make_inmemory_db()
u = FakeUpstream(id=1, api_prefix="api/v1", auth_type="bearer", finance_cost_mode="balance_delta")
result = get_daily_summary([], [u], date(2026, 7, 2), db=db)
assert result["failed_count"] == 1
assert result["total_cost"] == 0.0
assert result["upstream_items"][0]["status"] == "failed"
assert "基线样本" in result["upstream_items"][0]["error"]
db.close()
def test_upstream_recharge_crud_api_validates_and_scopes_to_upstream():
from fastapi.testclient import TestClient
from app.database import get_db
from app.main import app
from app.models.upstream import Upstream
from app.utils.auth import get_current_user
db = _make_inmemory_db()
db.add_all([
Upstream(id=1, name="U1", base_url="http://u1", api_prefix="api/v1", auth_type="bearer", auth_config_json="{}"),
Upstream(id=2, name="U2", base_url="http://u2", api_prefix="api/v1", auth_type="bearer", auth_config_json="{}"),
])
db.commit()
app.dependency_overrides[get_db] = lambda: db
app.dependency_overrides[get_current_user] = lambda: None
try:
client = TestClient(app)
bad_amount = client.post("/api/upstreams/1/recharges", json={"date": "2026-07-02", "amount": 0})
assert bad_amount.status_code == 422
future = client.post("/api/upstreams/1/recharges", json={"date": "2999-01-01", "amount": 10})
assert future.status_code == 422
created = client.post(
"/api/upstreams/1/recharges",
json={"date": "2026-07-02", "amount": 50.5, "note": "manual"},
)
assert created.status_code == 201
body = created.json()
rid = body["id"]
assert body["date"] == "2026-07-02"
assert body["amount"] == 50.5
listed = client.get("/api/upstreams/1/recharges", params={"date": "2026-07-02"})
assert listed.status_code == 200
assert [row["id"] for row in listed.json()] == [rid]
wrong_upstream = client.put(f"/api/upstreams/2/recharges/{rid}", json={"amount": 60})
assert wrong_upstream.status_code == 404
updated = client.put(f"/api/upstreams/1/recharges/{rid}", json={"amount": 60, "note": "fixed"})
assert updated.status_code == 200
assert updated.json()["amount"] == 60
assert updated.json()["note"] == "fixed"
deleted = client.delete(f"/api/upstreams/1/recharges/{rid}")
assert deleted.status_code == 204
empty = client.get("/api/upstreams/1/recharges", params={"date": "2026-07-02"})
assert empty.json() == []
finally:
app.dependency_overrides.clear()
db.close()
# ─────────────────────────────────────────────
# DB-backed snapshot tests (FinanceDailySummary)
# ─────────────────────────────────────────────
@@ -415,14 +575,21 @@ def _make_inmemory_db():
"""Create an isolated in-memory SQLite session with all tables."""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from app.database import Base
# Import models so they register with Base.metadata
from app.models import finance_daily_summary # noqa: F401
from app.models import snapshot # noqa: F401
from app.models import upstream_recharge_event # noqa: F401
from app.models.website import Website, WebsiteGroupBinding, WebsiteSyncLog # noqa: F401
from app.models.upstream import Upstream # noqa: F401
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(bind=engine)
Session = sessionmaker(bind=engine)
return Session()