"""现金流模块测试 — 收付款计划 + 资金缺口预测 + 看板 覆盖 cash.py 核心端点: gap-forecast / balance GET+POST / plans CRUD / plans{id}/complete / upcoming / dashboard / check-alerts / alerts/status """ from datetime import datetime, timedelta import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from app.models import CashPlan from tests.conftest import create_test_user, get_token_for_user, auth_header BASE = "/api/cma/cash" def _future_date(days: int = 5) -> str: return (datetime.now() + timedelta(days=days)).strftime("%Y-%m-%d") def _past_date(days: int = 5) -> str: return (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") class TestBalance: def test_get_balance_default(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = client.get(f"{BASE}/balance", headers=auth_header(token)) assert resp.status_code == 200 assert resp.json()["entity_id"] == 1 def test_set_balance(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = client.post(f"{BASE}/balance", headers=auth_header(token), json={"current_cash": 88.5}) assert resp.status_code == 200 assert resp.json()["current_cash"] == 88.5 get_resp = client.get(f"{BASE}/balance", headers=auth_header(token)) assert get_resp.json()["current_cash"] == 88.5 def test_set_balance_negative(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = client.post(f"{BASE}/balance", headers=auth_header(token), json={"current_cash": -5}) assert resp.status_code == 400 class TestPlans: def test_create_plan(self, client: TestClient, db: Session): """创建收款计划""" create_test_user(db) token = get_token_for_user(client) resp = client.post(f"{BASE}/plans", headers=auth_header(token), json={ "plan_type": "receive", "amount": 50.0, "plan_date": _future_date(), "counterparty": "客户A", "owner": "张三", }) assert resp.status_code == 200 data = resp.json()["data"] assert data["plan_type"] == "receive" assert data["status"] == "pending" def test_create_plan_invalid_type(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = client.post(f"{BASE}/plans", headers=auth_header(token), json={"plan_type": "weird", "amount": 10, "plan_date": _future_date()}) assert resp.status_code == 400 def test_create_plan_zero_amount(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = client.post(f"{BASE}/plans", headers=auth_header(token), json={"plan_type": "pay", "amount": 0, "plan_date": _future_date()}) assert resp.status_code == 400 def test_create_plan_missing_date(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = client.post(f"{BASE}/plans", headers=auth_header(token), json={"plan_type": "pay", "amount": 10}) assert resp.status_code == 400 def test_create_plan_bad_date(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = client.post(f"{BASE}/plans", headers=auth_header(token), json={"plan_type": "pay", "amount": 10, "plan_date": "not-a-date"}) assert resp.status_code == 400 def test_list_plans_filters(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) client.post(f"{BASE}/plans", headers=auth_header(token), json={"plan_type": "receive", "amount": 10, "plan_date": _future_date()}) client.post(f"{BASE}/plans", headers=auth_header(token), json={"plan_type": "pay", "amount": 20, "plan_date": _future_date()}) resp = client.get(f"{BASE}/plans?plan_type=receive", headers=auth_header(token)) assert resp.status_code == 200 assert resp.json()["total"] == 1 assert resp.json()["data"][0]["plan_type"] == "receive" def test_list_plans_bad_month(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = client.get(f"{BASE}/plans?month=bad", headers=auth_header(token)) assert resp.status_code == 400 def test_update_plan(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) pid = client.post(f"{BASE}/plans", headers=auth_header(token), json={"plan_type": "receive", "amount": 10, "plan_date": _future_date()} ).json()["data"]["id"] resp = client.put(f"{BASE}/plans/{pid}", headers=auth_header(token), json={"amount": 30, "counterparty": "客户B"}) assert resp.status_code == 200 assert resp.json()["data"]["amount"] == 30.0 def test_update_plan_not_found(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = client.put(f"{BASE}/plans/99999", headers=auth_header(token), json={"amount": 10}) assert resp.status_code == 404 def test_delete_plan(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) pid = client.post(f"{BASE}/plans", headers=auth_header(token), json={"plan_type": "pay", "amount": 10, "plan_date": _future_date()} ).json()["data"]["id"] resp = client.delete(f"{BASE}/plans/{pid}", headers=auth_header(token)) assert resp.status_code == 200 assert resp.json()["message"] == "计划已删除" def test_complete_plan(self, client: TestClient, db: Session): """标记收款完成""" create_test_user(db) token = get_token_for_user(client) pid = client.post(f"{BASE}/plans", headers=auth_header(token), json={"plan_type": "receive", "amount": 10, "plan_date": _future_date()} ).json()["data"]["id"] resp = client.post(f"{BASE}/plans/{pid}/complete", headers=auth_header(token)) assert resp.status_code == 200 data = resp.json()["data"] assert data["status"] == "completed" assert data["paid_amount"] == 10.0 def test_complete_plan_not_found(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = client.post(f"{BASE}/plans/99999/complete", headers=auth_header(token)) assert resp.status_code == 404 class TestUpcomingDashboard: def test_upcoming(self, client: TestClient, db: Session): """到期提醒 + 逾期""" create_test_user(db) token = get_token_for_user(client) client.post(f"{BASE}/plans", headers=auth_header(token), json={"plan_type": "receive", "amount": 10, "plan_date": _future_date(3)}) client.post(f"{BASE}/plans", headers=auth_header(token), json={"plan_type": "pay", "amount": 20, "plan_date": _past_date(3)}) resp = client.get(f"{BASE}/upcoming?days=7", headers=auth_header(token)) assert resp.status_code == 200 data = resp.json() assert len(data["upcoming"]) == 1 assert len(data["overdue"]) == 1 assert data["overdue_receive_amount"] == 0.0 assert data["overdue_pay_amount"] == 20.0 def test_dashboard(self, client: TestClient, db: Session): """资金看板""" create_test_user(db) token = get_token_for_user(client) client.post(f"{BASE}/plans", headers=auth_header(token), json={"plan_type": "receive", "amount": 10, "plan_date": _future_date(3)}) resp = client.get(f"{BASE}/dashboard", headers=auth_header(token)) assert resp.status_code == 200 data = resp.json() assert "calendar" in data def test_dashboard_bad_month(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) resp = client.get(f"{BASE}/dashboard?month=bad", headers=auth_header(token)) assert resp.status_code == 400 def test_gap_forecast(self, client: TestClient, db: Session): """资金缺口预测""" create_test_user(db) token = get_token_for_user(client) resp = client.get(f"{BASE}/gap-forecast?days=10¤t_cash=100", headers=auth_header(token)) assert resp.status_code == 200 class TestAlerts: def test_check_alerts(self, client: TestClient, db: Session): """触发资金预警检查""" create_test_user(db) token = get_token_for_user(client) resp = client.post(f"{BASE}/check-alerts", headers=auth_header(token), json={}) assert resp.status_code == 200 def test_alerts_status(self, client: TestClient, db: Session): """预警状态查询""" create_test_user(db) token = get_token_for_user(client) resp = client.get(f"{BASE}/alerts/status", headers=auth_header(token)) assert resp.status_code == 200 data = resp.json() assert "entity_id" in data and "critical_line" in data