gigafibre-fsm/patches/add_churn_fields.py
louispaulb 607ea54b5c refactor: reduce token count, DRY code, consolidate docs
Backend services:
- targo-hub: extract deepGetValue to helpers.js, DRY disconnect reasons
  lookup map, compact CAPABILITIES, consolidate vision.js prompts/schemas,
  extract dispatch scoring weights, trim section dividers across 9 files
- modem-bridge: extract getSession() helper (6 occurrences), resetIdleTimer(),
  consolidate DM query factory, fix duplicate username fill bug, trim headers
  (server.js -36%, tplink-session.js -47%, docker-compose.yml -57%)

Frontend:
- useWifiDiagnostic: extract THRESHOLDS const, split processDiagnostic into
  6 focused helpers (processOnlineStatus, processWanIPs, processRadios,
  processMeshNodes, processClients, checkRadioIssues)
- EquipmentDetail: merge duplicate ROLE_LABELS, remove verbose comments

Documentation (17 → 13 files, -1,400 lines):
- New consolidated README.md (architecture, services, dependencies, auth)
- Merge ECOSYSTEM-OVERVIEW into ARCHITECTURE.md
- Merge MIGRATION-PLAN + ARCHITECTURE-COMPARE + FIELD-GAP + CHANGELOG → MIGRATION.md
- Merge COMPETITIVE-ANALYSIS into PLATFORM-STRATEGY.md
- Update ROADMAP.md with current phase status
- Delete CONTEXT.md (absorbed into README)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 08:39:58 -04:00

163 lines
6.4 KiB
Python

"""
Add churn/cancel reason tracking fields to Customer and Issue doctypes.
Enables AI-powered retention intelligence and win-back campaigns.
Run: docker cp add_churn_fields.py erpnext-backend-1:/tmp/
docker exec erpnext-backend-1 bench --site erp.gigafibre.ca execute "exec(open('/tmp/add_churn_fields.py').read())"
"""
import frappe
def add_custom_fields(doctype, fields):
count = 0
for f in fields:
fieldname = f["fieldname"]
exists = frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": fieldname})
if exists:
print(f" EXISTS: {doctype}.{fieldname}")
continue
doc = frappe.get_doc({"doctype": "Custom Field", "dt": doctype, **f})
doc.insert(ignore_permissions=True)
count += 1
print(f" ADDED: {doctype}.{fieldname}")
return count
# ── Customer: Churn/Retention fields ─────────────────────────────────────────
customer_fields = [
# Section break for churn tracking
{"fieldname": "churn_section", "fieldtype": "Section Break",
"label": "Rétention / Désabonnement", "insert_after": "notes_internal",
"collapsible": 1},
# Cancel status
{"fieldname": "churn_status", "fieldtype": "Select",
"label": "Statut rétention",
"options": "\nActif\nÀ risque\nDésabonné\nRécupéré",
"insert_after": "churn_section"},
{"fieldname": "churn_cb1", "fieldtype": "Column Break",
"insert_after": "churn_status"},
# Cancel date
{"fieldname": "cancel_date", "fieldtype": "Date",
"label": "Date désabonnement",
"insert_after": "churn_cb1"},
# Cancel reason
{"fieldname": "cancel_reason", "fieldtype": "Select",
"label": "Raison désabonnement",
"options": "\nCompétiteur - Promotion\nCompétiteur - Prix\nCompétiteur - Service\nPrix trop élevé\nQualité WiFi\nPannes fréquentes\nService client\nDéménagement hors zone\nDéménagement dans zone\nDécès\nAutre",
"insert_after": "cancel_date"},
{"fieldname": "churn_cb2", "fieldtype": "Column Break",
"insert_after": "cancel_reason"},
# Competitor info
{"fieldname": "cancel_competitor", "fieldtype": "Data",
"label": "Compétiteur",
"description": "Bell, Vidéotron, Fizz, etc.",
"insert_after": "churn_cb2",
"depends_on": "eval:['Compétiteur - Promotion','Compétiteur - Prix','Compétiteur - Service'].includes(doc.cancel_reason)"},
{"fieldname": "cancel_competitor_offer", "fieldtype": "Small Text",
"label": "Offre compétiteur",
"description": "Ex: 6 mois gratuit, 49.99$/mois fibre 1Gbps",
"insert_after": "cancel_competitor",
"depends_on": "eval:['Compétiteur - Promotion','Compétiteur - Prix','Compétiteur - Service'].includes(doc.cancel_reason)"},
# Notes
{"fieldname": "cancel_notes", "fieldtype": "Small Text",
"label": "Notes rétention",
"description": "Contexte: ce que le client a dit, offre proposée, etc.",
"insert_after": "cancel_competitor_offer"},
# Win-back tracking
{"fieldname": "winback_section", "fieldtype": "Section Break",
"label": "Récupération", "insert_after": "cancel_notes",
"collapsible": 1,
"depends_on": "eval:doc.churn_status=='Désabonné'"},
{"fieldname": "winback_attempts", "fieldtype": "Int",
"label": "Tentatives récupération", "default": "0",
"insert_after": "winback_section"},
{"fieldname": "winback_last_date", "fieldtype": "Date",
"label": "Dernière tentative",
"insert_after": "winback_attempts"},
{"fieldname": "winback_cb", "fieldtype": "Column Break",
"insert_after": "winback_last_date"},
{"fieldname": "winback_date", "fieldtype": "Date",
"label": "Date récupération",
"insert_after": "winback_cb",
"depends_on": "eval:doc.churn_status=='Récupéré'"},
{"fieldname": "winback_offer", "fieldtype": "Small Text",
"label": "Offre accordée",
"description": "Offre qui a convaincu le client de revenir",
"insert_after": "winback_date",
"depends_on": "eval:doc.churn_status=='Récupéré'"},
# Risk score (AI-populated)
{"fieldname": "churn_risk_score", "fieldtype": "Int",
"label": "Score risque désabonnement",
"description": "0-100, calculé par l'IA basé sur le comportement",
"insert_after": "winback_offer", "read_only": 1},
]
# ── Issue: Enhanced categorization ───────────────────────────────────────────
issue_fields = [
# Customer link (if not already present)
{"fieldname": "customer", "fieldtype": "Link",
"label": "Client", "options": "Customer",
"insert_after": "naming_series"},
# Outage tracking
{"fieldname": "outage_section", "fieldtype": "Section Break",
"label": "Panne / Outage", "insert_after": "resolution_details",
"collapsible": 1},
{"fieldname": "outage_type", "fieldtype": "Select",
"label": "Type de panne",
"options": "\nPanne isolée\nPanne secteur\nCoupure fibre\nPanne OLT\nPanne backbone\nPanne courant",
"insert_after": "outage_section"},
{"fieldname": "outage_cb", "fieldtype": "Column Break",
"insert_after": "outage_type"},
{"fieldname": "affected_count", "fieldtype": "Int",
"label": "Clients affectés",
"insert_after": "outage_cb"},
{"fieldname": "olt_name", "fieldtype": "Data",
"label": "OLT",
"insert_after": "affected_count"},
{"fieldname": "olt_port", "fieldtype": "Data",
"label": "Port OLT",
"insert_after": "olt_name"},
# AI diagnosis
{"fieldname": "ai_diagnosis", "fieldtype": "Small Text",
"label": "Diagnostic IA",
"insert_after": "olt_port", "read_only": 1},
# Cancel reason (for support tickets about cancellation)
{"fieldname": "cancel_intent", "fieldtype": "Check",
"label": "Intention de désabonnement",
"insert_after": "ai_diagnosis"},
]
# ── Apply ────────────────────────────────────────────────────────────────────
print("Adding churn/retention fields to Customer...")
c1 = add_custom_fields("Customer", customer_fields)
print("\nAdding outage/categorization fields to Issue...")
c2 = add_custom_fields("Issue", issue_fields)
frappe.db.commit()
print(f"\nDone: {c1} Customer fields + {c2} Issue fields added")