From 5e66ef6563dfa513fda9cf4c2c20df8ed2dc6ebe Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 5 Jan 2026 11:34:39 +0100 Subject: [PATCH] Add: Customer linking verification endpoint med health score og anbefalinger --- app/customers/backend/router.py | 177 ++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/app/customers/backend/router.py b/app/customers/backend/router.py index f33ecb5..b85a1e0 100644 --- a/app/customers/backend/router.py +++ b/app/customers/backend/router.py @@ -170,6 +170,183 @@ async def list_customers( } +@router.get("/customers/verify-linking") +async def verify_customer_linking(): + """ + 🔍 Verificer kunde-linking på tværs af systemer. + + Krydstjek: + 1. tmodule_customers → Hub customers (via hub_customer_id) + 2. Hub customers → e-conomic (via economic_customer_number) + 3. tmodule_customers → e-conomic (via economic_customer_number match) + """ + try: + logger.info("🔍 Starting customer linking verification...") + + # 1. Tmodule customers overview + tmodule_stats = execute_query_single(""" + SELECT + COUNT(*) as total, + COUNT(hub_customer_id) as linked_to_hub, + COUNT(economic_customer_number) as has_economic_number + FROM tmodule_customers + """) + + # 2. Hub customers overview + hub_stats = execute_query_single(""" + SELECT + COUNT(*) as total, + COUNT(economic_customer_number) as has_economic_number, + COUNT(CASE WHEN economic_customer_number IS NOT NULL + AND economic_customer_number::text != '' + THEN 1 END) as valid_economic_number + FROM customers + """) + + # 3. Find tmodule customers UDEN Hub link + tmodule_unlinked = execute_query(""" + SELECT id, name, economic_customer_number, email + FROM tmodule_customers + WHERE hub_customer_id IS NULL + ORDER BY name + LIMIT 20 + """) + + # 4. Find tmodule customers med Hub link MEN Hub customer mangler economic number + tmodule_linked_but_no_economic = execute_query(""" + SELECT + tc.id as tmodule_id, + tc.name as tmodule_name, + tc.economic_customer_number as tmodule_economic, + c.id as hub_id, + c.name as hub_name, + c.economic_customer_number as hub_economic + FROM tmodule_customers tc + JOIN customers c ON tc.hub_customer_id = c.id + WHERE c.economic_customer_number IS NULL + OR c.economic_customer_number::text = '' + LIMIT 20 + """) + + # 5. Find economic number mismatches + economic_mismatches = execute_query(""" + SELECT + tc.id as tmodule_id, + tc.name as tmodule_name, + tc.economic_customer_number as tmodule_economic, + c.id as hub_id, + c.name as hub_name, + c.economic_customer_number as hub_economic + FROM tmodule_customers tc + JOIN customers c ON tc.hub_customer_id = c.id + WHERE tc.economic_customer_number IS NOT NULL + AND c.economic_customer_number IS NOT NULL + AND tc.economic_customer_number::text != c.economic_customer_number::text + LIMIT 20 + """) + + # 6. Find Hub customers der kunne matches på navn men ikke er linket + potential_name_matches = execute_query(""" + SELECT + tc.id as tmodule_id, + tc.name as tmodule_name, + tc.economic_customer_number as tmodule_economic, + c.id as hub_id, + c.name as hub_name, + c.economic_customer_number as hub_economic + FROM tmodule_customers tc + JOIN customers c ON LOWER(TRIM(tc.name)) = LOWER(TRIM(c.name)) + WHERE tc.hub_customer_id IS NULL + LIMIT 20 + """) + + # 7. Beregn health score + tmodule_link_pct = (tmodule_stats['linked_to_hub'] / tmodule_stats['total'] * 100) if tmodule_stats['total'] > 0 else 0 + hub_economic_pct = (hub_stats['valid_economic_number'] / hub_stats['total'] * 100) if hub_stats['total'] > 0 else 0 + + health_score = (tmodule_link_pct * 0.6) + (hub_economic_pct * 0.4) + + if health_score >= 90: + health_status = "excellent" + elif health_score >= 75: + health_status = "good" + elif health_score >= 50: + health_status = "fair" + else: + health_status = "poor" + + result = { + "status": "success", + "health": { + "score": round(health_score, 1), + "status": health_status, + "description": f"{round(tmodule_link_pct, 1)}% tmodule linked, {round(hub_economic_pct, 1)}% hub has economic numbers" + }, + "tmodule_customers": { + "total": tmodule_stats['total'], + "linked_to_hub": tmodule_stats['linked_to_hub'], + "has_economic_number": tmodule_stats['has_economic_number'], + "link_percentage": round(tmodule_link_pct, 1) + }, + "hub_customers": { + "total": hub_stats['total'], + "has_economic_number": hub_stats['valid_economic_number'], + "economic_percentage": round(hub_economic_pct, 1) + }, + "issues": { + "tmodule_unlinked_count": len(tmodule_unlinked), + "tmodule_unlinked_sample": tmodule_unlinked[:5], + "hub_missing_economic_count": len(tmodule_linked_but_no_economic), + "hub_missing_economic_sample": tmodule_linked_but_no_economic[:5], + "economic_mismatches_count": len(economic_mismatches), + "economic_mismatches_sample": economic_mismatches[:5], + "potential_name_matches_count": len(potential_name_matches), + "potential_name_matches_sample": potential_name_matches[:5] + }, + "recommendations": [] + } + + # Generer anbefalinger + if len(tmodule_unlinked) > 0: + result["recommendations"].append({ + "issue": "unlinked_tmodule_customers", + "count": len(tmodule_unlinked), + "action": "POST /api/v1/timetracking/sync/relink-customers", + "description": "Kør re-linking for at matche på economic_customer_number eller navn" + }) + + if len(tmodule_linked_but_no_economic) > 0: + result["recommendations"].append({ + "issue": "hub_customers_missing_economic_number", + "count": len(tmodule_linked_but_no_economic), + "action": "POST /api/v1/customers/sync-economic-from-simplycrm", + "description": "Sync e-conomic numre fra Simply-CRM" + }) + + if len(economic_mismatches) > 0: + result["recommendations"].append({ + "issue": "economic_number_mismatches", + "count": len(economic_mismatches), + "action": "Manual review required", + "description": "Forskellige e-conomic numre i tmodule vs hub - tjek data manuelt" + }) + + if len(potential_name_matches) > 0: + result["recommendations"].append({ + "issue": "potential_name_matches", + "count": len(potential_name_matches), + "action": "POST /api/v1/timetracking/sync/relink-customers", + "description": "Disse kunder kunne linkes på navn" + }) + + logger.info(f"✅ Verification complete - Health: {health_status} ({health_score:.1f}%)") + return result + + except Exception as e: + logger.error(f"❌ Verification failed: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @router.get("/customers/{customer_id}") async def get_customer(customer_id: int): """Get single customer by ID with contact count and vTiger BMC Låst status"""