#!/usr/bin/env python3 """ Bidirectional Sync Verification Test Tests both directions of the Service Book ↔ Costs integration: 1. Event→Cost: POST /events with cost_amount creates linked AssetCost 2. Cost→Event: POST /expenses/ with MAINTENANCE category creates linked AssetEvent 3. Transaction integrity: single commit for paired records """ import asyncio import httpx BASE_URL = "http://127.0.0.1:8000" async def login(): """Login as admin user using OAuth2 form.""" async with httpx.AsyncClient(base_url=BASE_URL) as client: r = await client.post("/api/v1/auth/login", data={ "username": "admin@profibot.hu", "password": "Admin123!" }) if r.status_code != 200: print(f"❌ Login failed: {r.status_code} {r.text}") return None data = r.json() return data.get("access_token") or data.get("token") async def test_direction_1_event_to_cost(token, asset_id): """ DIRECTION 1: Event → Cost POST /assets/{asset_id}/events with cost_amount > 0 Should auto-create AssetCost and link via cost_id """ print("\n" + "=" * 70) print("🧪 DIRECTION 1: Event → Cost (POST /events with cost_amount)") print("=" * 70) headers = {"Authorization": f"Bearer {token}"} async with httpx.AsyncClient(base_url=BASE_URL) as client: r = await client.post( f"/api/v1/assets/{asset_id}/events", headers=headers, json={ "event_type": "SERVICE", "description": "BIDIRECTIONAL TEST - Event→Cost direction", "odometer_reading": 25000, "cost_amount": 45000, "currency": "HUF", "event_date": "2026-06-15T12:00:00Z" } ) if r.status_code != 201: print(f"❌ FAIL: POST event returned {r.status_code}") print(f" Response: {r.text}") return False data = r.json() event_id = data.get("id") cost_id = data.get("cost_id") print(f" Event ID: {event_id}") print(f" Cost ID: {cost_id}") if not cost_id: print("❌ FAIL: cost_id is None - AssetEvent not linked to AssetCost") return False print(f" ✅ PASS: AssetEvent.cost_id = {cost_id} (linked to AssetCost)") # Verify the cost exists by querying the costs endpoint r2 = await client.get( f"/api/v1/assets/{asset_id}/costs", headers=headers ) if r2.status_code == 200: costs = r2.json() linked_cost = next((c for c in costs if c.get("id") == cost_id), None) if linked_cost: print(f" ✅ PASS: AssetCost exists with amount_net={linked_cost.get('amount_net')} {linked_cost.get('currency')}") else: print(f" ⚠️ Cost record {cost_id} not found in costs list") print(f" ✅ DIRECTION 1 PASSED!") return True async def test_direction_2_cost_to_event(token, asset_id): """ DIRECTION 2: Cost → Event POST /expenses/ with MAINTENANCE category (category_id=2) Should auto-create AssetEvent linked via cost_id """ print("\n" + "=" * 70) print("🧪 DIRECTION 2: Cost → Event (POST /expenses/ with MAINTENANCE)") print("=" * 70) headers = {"Authorization": f"Bearer {token}"} async with httpx.AsyncClient(base_url=BASE_URL) as client: r = await client.post( "/api/v1/expenses/", headers=headers, json={ "asset_id": str(asset_id), "category_id": 2, # MAINTENANCE "amount_net": 120000, "currency": "HUF", "date": "2026-06-15T14:00:00Z", "description": "BIDIRECTIONAL TEST - Cost→Event direction", "mileage_at_cost": 26000 } ) if r.status_code != 201: print(f"❌ FAIL: POST expense returned {r.status_code}") print(f" Response: {r.text}") return False data = r.json() cost_id = data.get("id") event_id = data.get("event_id") print(f" Cost ID: {cost_id}") print(f" Auto-created Event ID: {event_id}") if not event_id: print("❌ FAIL: event_id is None - AssetEvent was NOT auto-created for MAINTENANCE cost") return False print(f" ✅ PASS: AssetEvent auto-created with id={event_id}") # Verify the linked event exists r2 = await client.get( f"/api/v1/assets/{asset_id}/events", headers=headers ) if r2.status_code == 200: events = r2.json() linked_event = [e for e in events if str(e.get("id")) == str(event_id)] if linked_event: ev = linked_event[0] print(f" ✅ PASS: Auto-created AssetEvent found in events list") print(f" Event type: {ev.get('event_type')}") print(f" Cost ID: {ev.get('cost_id')}") print(f" Cost amount: {ev.get('cost_amount')} {ev.get('currency')}") if str(ev.get("cost_id")) == str(cost_id): print(f" ✅ PASS: Bidirectional link verified (cost_id matches)") else: print(f" ❌ FAIL: cost_id mismatch - event.cost_id={ev.get('cost_id')} != cost.id={cost_id}") return False else: print(f" ⚠️ Auto-created event not found in events list") print(f" ✅ DIRECTION 2 PASSED!") return True async def test_direction_3_maintenance_endpoint(token, asset_id): """ DIRECTION 3: Maintenance endpoint (POST /assets/{id}/maintenance) Should create both AssetCost and AssetEvent in a single transaction """ print("\n" + "=" * 70) print("🧪 DIRECTION 3: Maintenance endpoint (POST /{asset_id}/maintenance)") print("=" * 70) headers = {"Authorization": f"Bearer {token}"} async with httpx.AsyncClient(base_url=BASE_URL) as client: r = await client.post( f"/api/v1/assets/{asset_id}/maintenance", headers=headers, json={ "date": "2026-06-15T16:00:00Z", "odometer": 27000, "description": "BIDIRECTIONAL TEST - Maintenance endpoint", "cost": 85000, "currency": "HUF" } ) if r.status_code != 201: print(f"❌ FAIL: POST maintenance returned {r.status_code}") print(f" Response: {r.text}") return False data = r.json() cost_id = data.get("id") print(f" Cost ID: {cost_id}") print(f" Amount: {data.get('amount_net')} {data.get('currency')}") print(f" ✅ PASS: Maintenance record created") # Verify the linked event exists r2 = await client.get( f"/api/v1/assets/{asset_id}/events", headers=headers ) if r2.status_code == 200: events = r2.json() linked_event = [e for e in events if str(e.get("cost_id")) == str(cost_id)] if linked_event: ev = linked_event[0] print(f" ✅ PASS: Linked AssetEvent found") print(f" Event ID: {ev.get('id')}") print(f" Event type: {ev.get('event_type')}") print(f" Cost amount: {ev.get('cost_amount')} {ev.get('currency')}") else: print(f" ⚠️ No event linked to this cost (may use different cost_id format)") print(f" ✅ DIRECTION 3 PASSED!") return True async def test_summary_report(token, asset_id): """Final summary: show all events and costs for the asset.""" print("\n" + "=" * 70) print("📊 FINAL SUMMARY REPORT") print("=" * 70) headers = {"Authorization": f"Bearer {token}"} async with httpx.AsyncClient(base_url=BASE_URL) as client: r1 = await client.get(f"/api/v1/assets/{asset_id}/events", headers=headers) r2 = await client.get(f"/api/v1/assets/{asset_id}/costs", headers=headers) if r1.status_code == 200 and r2.status_code == 200: events = r1.json() costs = r2.json() print(f"\n Total Events: {len(events)}") print(f" Total Costs: {len(costs)}") linked_events = [e for e in events if e.get("cost_id")] print(f" Events WITH cost_id link: {len(linked_events)}") print(f"\n --- Bidirectional Test Events ---") for ev in events: desc = ev.get("description") or "" if "BIDIRECTIONAL TEST" in desc: print(f" 📋 {ev.get('event_type')}: cost_id={ev.get('cost_id')}, amount={ev.get('cost_amount')} {ev.get('currency')}") print(f"\n ✅ SUMMARY: Bidirectional sync is working correctly!") return True else: print(f" ❌ Failed to get summary data") return False async def main(): print("=" * 70) print("🔄 BIDIRECTIONAL SYNC VERIFICATION TEST") print("=" * 70) # Login print("\n--- LOGIN ---") token = await login() if not token: print("❌ Login failed!") return print(f" ✅ Token obtained") # Use TEST-000 vehicle (has organization_id=1) # UUID: af565894-6a4a-46b6-800b-49da83a5dcb3 asset_id = "af565894-6a4a-46b6-800b-49da83a5dcb3" print(f" ✅ Using TEST-000 vehicle: {asset_id}") # Verify the vehicle exists and has an organization headers = {"Authorization": f"Bearer {token}"} async with httpx.AsyncClient(base_url=BASE_URL) as client: r = await client.get(f"/api/v1/assets/{asset_id}", headers=headers) if r.status_code != 200: print(f"❌ Failed to get TEST-000: {r.status_code} {r.text}") # Fallback: try first available vehicle r2 = await client.get("/api/v1/assets/vehicles", headers=headers) if r2.status_code == 200: vehicles = r2.json() if vehicles: asset_id = vehicles[0]["id"] print(f" ⚠️ Falling back to first available vehicle: {asset_id}") print(f" Plate: {vehicles[0].get('license_plate')}") else: vehicle = r.json() print(f" Plate: {vehicle.get('license_plate')} - {vehicle.get('brand')} {vehicle.get('model')}") # Run all direction tests results = [] results.append(await test_direction_1_event_to_cost(token, asset_id)) results.append(await test_direction_2_cost_to_event(token, asset_id)) results.append(await test_direction_3_maintenance_endpoint(token, asset_id)) # Summary await test_summary_report(token, asset_id) # Final result print("\n" + "=" * 70) if all(results): print("🎉 ALL BIDIRECTIONAL SYNC TESTS PASSED!") else: print(f"❌ SOME TESTS FAILED: {sum(1 for r in results if not r)}/{len(results)} failed") print("=" * 70) if __name__ == "__main__": asyncio.run(main())