"""MySQL 上验证新工作区权限、队列、乐观版本和正式表隔离。""" from uuid import uuid4 import pytest from sqlalchemy import delete, func, select from app.core.db import get_session_factory from app.models import ( MonthlySettlement, PerformanceLedger, Receipt, TPlusFact, TPlusSourceHead, TPlusSyncBatch, TPlusSyncRecord, TPlusWorkLine, TPlusWorkRun, ) from app.services.tplus import workbench, worker from app.services.tplus.mapping import get_mapping from app.services.tplus.source_heads import observe_rows from app.services.tplus.staging import stage_row BASE = "/api/v1/system/tplus" pytestmark = pytest.mark.integration @pytest.fixture def workspace(monkeypatch): from app.core.config import Settings source = "wb_test_" + uuid4().hex[:16] settings = Settings( _env_file=None, app_secret_key="x" * 32, t_plus_enabled=True, t_plus_database=source, t_plus_user="reader", t_plus_odbc_driver="unit", ) monkeypatch.setattr("app.services.tplus.task_service.get_settings", lambda: settings) monkeypatch.setattr(workbench, "_source_database", lambda: source) with get_session_factory()() as db: yield db, source db.rollback() numbers = list( db.scalars(select(TPlusWorkRun.run_no).where(TPlusWorkRun.source_database == source)) ) batches = list( db.scalars( select(TPlusSyncBatch.batch_no).where(TPlusSyncBatch.source_database == source) ) ) db.execute(delete(TPlusWorkLine).where(TPlusWorkLine.run_no.in_(numbers))) db.execute(delete(TPlusWorkRun).where(TPlusWorkRun.source_database == source)) db.execute(delete(TPlusFact).where(TPlusFact.source_database == source)) db.execute(delete(TPlusSourceHead).where(TPlusSourceHead.source_database == source)) db.execute(delete(TPlusSyncRecord).where(TPlusSyncRecord.batch_no.in_(batches))) db.execute(delete(TPlusSyncBatch).where(TPlusSyncBatch.batch_no.in_(batches))) db.commit() def test_工作区全部入口沿用管理员权限(client, normal): for path in ( "/facts", "/work-runs", "/source-records?entity=personnel", "/work-runs/example/report", ): assert client.get(BASE + path, headers=normal["headers"]).status_code == 403 assert ( client.post( BASE + "/work-runs", headers=normal["headers"], json={ "kind": "MASTER_PREVIEW", "entity_types": ["personnel"], "request_key": str(uuid4()), }, ).status_code == 403 ) def test_资料乐观锁和历史在MySQL生效(client, admin, workspace): db, _ = workspace payload = { "kind": "PRODUCT", "fact_key": "9", "expected_version": 0, "evidence": "隔离测试:资料版本", "data": {"category": "municipal_water", "unit": "件"}, } first = client.post(BASE + "/facts", headers=admin["headers"], json=payload) assert first.status_code == 200 assert client.post(BASE + "/facts", headers=admin["headers"], json=payload).status_code == 400 payload.update(expected_version=1, evidence="隔离测试:修改分类") payload["data"]["category"] = "rail_transit" second = client.post(BASE + "/facts", headers=admin["headers"], json=payload) assert second.json()["data"]["version"] == 2 result = client.get( BASE + f"/facts/{first.json()['data']['id']}/history", headers=admin["headers"] ) assert result.json()["data"]["total"] == 2 assert db.scalar(select(func.count()).select_from(TPlusFact)) >= 2 def test_MySQL队列预览和CSV不改正式工资(client, admin, workspace): db, source = workspace batch_no = "WB" + uuid4().hex[:20] source_id = str(100000000 + int(uuid4().hex[:8], 16)) batch = TPlusSyncBatch( batch_no=batch_no, source_database=source, sync_mode="FULL", status="SUCCEEDED", entity_types_json=["products"], audit_result_json={"observed_entities": ["products"], "source_head_version": 1}, ) db.add(batch) db.flush() row = { "id": int(source_id), "code": "=SOURCE", "name": "t_workbench_product", "unit_name": "件", } stage_row(db, batch_no=batch_no, entity_type="products", source_table="AA_Inventory", row=row) observe_rows(db, batch, get_mapping("products"), [row]) db.commit() before = { m.__tablename__: db.scalar(select(func.count()).select_from(m)) for m in (Receipt, PerformanceLedger, MonthlySettlement) } payload = {"kind": "MASTER_PREVIEW", "request_key": str(uuid4()), "entity_types": ["products"]} first = client.post(BASE + "/work-runs", headers=admin["headers"], json=payload).json()["data"] assert ( client.post(BASE + "/work-runs", headers=admin["headers"], json=payload).json()["data"][ "run_no" ] == first["run_no"] ) assert worker.run_once(db) == first["run_no"] db.expire_all() detail = client.get(BASE + f"/work-runs/{first['run_no']}", headers=admin["headers"]).json()[ "data" ] assert detail["summary"]["counts"] == {"PENDING": 1} assert detail["formal_payroll_written"] is False report = client.get(BASE + f"/work-runs/{first['run_no']}/report", headers=admin["headers"]) assert report.status_code == 200 and "未写入正式工资" in report.text response = client.get(BASE + "/source-records?entity=products", headers=admin["headers"]) assert response.status_code == 200, response.json() choices = response.json()["data"] assert choices["total"] == 1 and choices["list"][0]["source_id"] == source_id assert before == { m.__tablename__: db.scalar(select(func.count()).select_from(m)) for m in (Receipt, PerformanceLedger, MonthlySettlement) } def test_MySQL完整预演冻结重跑且正式表不变(client, admin, workspace): db, source = workspace n = 100000000 + int(uuid4().hex[:8], 16) ship_id, line_id, cash_id = map(str, (n, n + 1, n + 2)) entities = { "shipments": [ { "id": n, "code": "SH-TEST", "idcustomer": 8, "voucherdate": "2026-03-10", "voucherState": 189, "isCancel": 428, } ], "shipment_items": [ { "id": n + 1, "source_parent_id": n, "idinventory": 9, "taxAmount": "100", "quantity": "2", } ], "receipts": [ { "id": n + 2, "code": "RC-TEST", "idpartner": 8, "voucherDate": "2026-03-10", "isReceiveFlag": 1, "voucherstate": 189, "amount": "100", "accounting_voucher_count": 1, "valid_accounting_voucher_count": 1, "accounting_voucher_id": 123, "accounting_voucher_code": "GL-TEST-UNREVIEWED", }, { "id": n + 3, "code": "RC-NOT-GENERATED", "idpartner": 8, "voucherDate": "2026-03-10", "isReceiveFlag": 1, "voucherstate": 189, "amount": "45", "accounting_voucher_count": 0, "valid_accounting_voucher_count": 0, }, ], } for entity, rows in entities.items(): batch_no = "WB" + uuid4().hex[:20] batch = TPlusSyncBatch( batch_no=batch_no, source_database=source, sync_mode="FULL", status="SUCCEEDED", entity_types_json=[entity], audit_result_json={"observed_entities": [entity]}, ) db.add(batch) db.flush() for row in rows: stage_row( db, batch_no=batch_no, entity_type=entity, source_table=get_mapping(entity).source_table, row=row, ) observe_rows(db, batch, get_mapping(entity), rows) db.commit() facts = [ ( "PERSONNEL", "001", {"code": "001", "name": "测试人员", "role_code": "salesperson", "status": 1}, ), ("PRODUCT", "9", {"category": "municipal_water", "unit": "件"}), ("CUSTOMER", "8", {"customer_type": "personal", "owner_name": "测试负责人"}), ( "GUIDANCE", "price9", {"product_id": "9", "month": "2026-03", "basis": "UNIT", "amount": "25", "unit": "件"}, ), ( "SHIPMENT", line_id, { "sales_amount_excludes_freight": False, "freight_amount": "10", "business_key": "test", "personnel_id": "001", }, ), ( "GROUP", "g", { "customer_id": "8", "shipment_ids": [ship_id], "receipt_ids": [cash_id, str(n + 3)], "scope_confirmed": True, "opening_confirmed": True, "opening_balance": "0", "historical_settlement_confirmed": True, "guidance_month_basis": "SHIPMENT", }, ), ] for kind, key, data in facts: r = client.post( BASE + "/facts", headers=admin["headers"], json={ "kind": kind, "fact_key": key, "expected_version": 0, "evidence": "隔离测试资料,不是真实客户规则", "data": data, }, ) assert r.status_code == 200, r.json() before = { m.__tablename__: db.scalar(select(func.count()).select_from(m)) for m in (Receipt, PerformanceLedger, MonthlySettlement) } def execute(**values): r = client.post( BASE + "/work-runs", headers=admin["headers"], json={"kind": "CALC_PREVIEW", "request_key": str(uuid4()), **values}, ) assert r.status_code == 200, r.json() number = r.json()["data"]["run_no"] assert worker.run_once(db) == number return client.get(BASE + f"/work-runs/{number}", headers=admin["headers"]).json()["data"] first = execute(month="2026-03", group_keys=["g"]) assert first["status"] == "SUCCEEDED", first assert first["summary"]["groups"]["g"]["prepayment_remaining"] == "10.00" assert first["summary"]["groups"]["g"]["receipt_total"] == "100.00" assert first["summary"]["groups"]["g"]["allocated_total"] == "100.00" assert first["summary"]["groups"]["g"]["excluded_receipt_total"] == "45.00" lines = client.get( BASE + f"/work-runs/{first['run_no']}/lines", headers=admin["headers"] ).json()["data"]["list"] shipment_line = next(row for row in lines if row["data"].get("kind") == "SHIPMENT") assert shipment_line["data"]["accounting_voucher_code"] == "GL-TEST-UNREVIEWED" assert any(row["action"] == "EXCLUDED" for row in lines) r = client.post( BASE + "/facts", headers=admin["headers"], json={ "kind": "GUIDANCE", "fact_key": "price9", "expected_version": 1, "evidence": "后续价格变化不应修改原运行", "data": {**facts[3][2], "amount": "40"}, }, ) assert r.status_code == 200 repeated = execute(replay_no=first["run_no"]) assert repeated["snapshot_hash"] == first["snapshot_hash"] assert repeated["summary"] == first["summary"] assert before == { m.__tablename__: db.scalar(select(func.count()).select_from(m)) for m in (Receipt, PerformanceLedger, MonthlySettlement) }