gigafibre-fsm/scripts/migration/analyze_pricing_cleanup.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

264 lines
12 KiB
Python

"""
Analyze pricing cleanup: show catalog price for services, keep rebates as-is,
adjust biggest rebate to absorb the difference when hijack < catalog.
Rules:
1. Positive products: show MAX(hijack_price, base_price) — catalog or higher
2. Negative products (rebates): show hijack_price as-is
3. If hijack_price < base_price on a positive product, the difference
gets added to the biggest rebate at that delivery address
4. Total per delivery must stay the same
Run inside erpnext-backend-1:
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/analyze_pricing_cleanup.py
"""
import frappe
import pymysql
import os
from datetime import datetime
os.chdir("/home/frappe/frappe-bench/sites")
frappe.init(site="erp.gigafibre.ca", sites_path=".")
frappe.connect()
print("Connected:", frappe.local.site)
conn = pymysql.connect(
host="legacy-db",
user="facturation",
password="*******",
database="gestionclient",
cursorclass=pymysql.cursors.DictCursor
)
# ═══════════════════════════════════════════════════════════════
# LOAD LEGACY DATA
# ═══════════════════════════════════════════════════════════════
print("Loading legacy data...")
with conn.cursor() as cur:
# All active services with product info
cur.execute("""
SELECT s.id, s.delivery_id, s.product_id, s.hijack, s.hijack_price, s.hijack_desc,
p.sku, p.price as base_price, p.category,
d.account_id
FROM service s
JOIN product p ON p.id = s.product_id
JOIN delivery d ON d.id = s.delivery_id
WHERE s.status = 1
ORDER BY s.delivery_id, p.price DESC
""")
all_services = cur.fetchall()
conn.close()
print("Active services loaded: {}".format(len(all_services)))
# Group by delivery_id
deliveries = {}
for s in all_services:
did = s["delivery_id"]
if did not in deliveries:
deliveries[did] = []
base = float(s["base_price"] or 0)
actual = float(s["hijack_price"]) if s["hijack"] else base
is_rebate = base < 0 # Product is inherently a rebate (SKU like RAB24M, RAB2X)
deliveries[did].append({
"svc_id": s["id"],
"sku": s["sku"],
"base_price": base,
"actual_price": actual,
"is_rebate": is_rebate,
"hijack": s["hijack"],
"hijack_desc": s["hijack_desc"] or "",
"account_id": s["account_id"],
})
print("Deliveries with active services: {}".format(len(deliveries)))
# ═══════════════════════════════════════════════════════════════
# APPLY PRICING RULES
# ═══════════════════════════════════════════════════════════════
print("\n" + "=" * 70)
print("PRICING ANALYSIS")
print("=" * 70)
total_deliveries_affected = 0
total_services_adjusted = 0
adjustments = [] # [(delivery_id, account_id, old_total, new_total, services)]
for did, services in deliveries.items():
old_total = sum(s["actual_price"] for s in services)
# Step 1: Determine new display prices
discount_to_absorb = 0.0
for s in services:
if s["is_rebate"]:
# Rebate product: keep actual price
s["new_price"] = s["actual_price"]
else:
# Positive product
if s["actual_price"] < s["base_price"]:
# Hijack made it cheaper → show catalog, track discount
discount_to_absorb += (s["base_price"] - s["actual_price"])
s["new_price"] = s["base_price"]
s["adjusted"] = True
else:
# Hijack same or higher → keep actual (custom service)
s["new_price"] = s["actual_price"]
s["adjusted"] = False
# Step 2: Find biggest rebate and add discount_to_absorb to it
rebate_adjusted = False
if discount_to_absorb > 0.005:
# Find the rebate with the most negative price
rebates = [s for s in services if s["is_rebate"]]
if rebates:
biggest_rebate = min(rebates, key=lambda s: s["new_price"])
biggest_rebate["new_price"] -= discount_to_absorb
biggest_rebate["absorbed"] = discount_to_absorb
rebate_adjusted = True
total_deliveries_affected += 1
else:
# No rebate exists — need to create one? Or leave as-is
# For now, mark as needing attention
pass
new_total = sum(s["new_price"] for s in services)
# Verify totals match
if abs(old_total - new_total) > 0.02:
adjustments.append((did, services[0]["account_id"], old_total, new_total, services, "MISMATCH"))
elif rebate_adjusted:
adjustments.append((did, services[0]["account_id"], old_total, new_total, services, "OK"))
for s in services:
if s.get("adjusted"):
total_services_adjusted += 1
print("\nDeliveries affected (rebate adjusted): {}".format(total_deliveries_affected))
print("Individual services price-restored to catalog: {}".format(total_services_adjusted))
# Count mismatches
mismatches = [a for a in adjustments if a[5] == "MISMATCH"]
ok_adjustments = [a for a in adjustments if a[5] == "OK"]
print("Clean adjustments (total preserved): {}".format(len(ok_adjustments)))
print("MISMATCHES (total changed): {}".format(len(mismatches)))
# ═══════════════════════════════════════════════════════════════
# SHOW DETAILED EXAMPLES
# ═══════════════════════════════════════════════════════════════
# Find Expro Transit (account 3673) deliveries
expro = [a for a in adjustments if a[1] == 3673]
print("\n" + "=" * 70)
print("EXPRO TRANSIT (account 3673) — {} deliveries affected".format(len(expro)))
print("=" * 70)
for did, acct_id, old_total, new_total, services, status in expro:
print("\n Delivery {}{} [total: {:.2f}]".format(did, status, old_total))
print(" {:<14} {:>10} {:>10} {:>10} {}".format(
"SKU", "BASE", "ACTUAL", "NEW", "NOTE"))
print(" " + "-" * 66)
for s in sorted(services, key=lambda x: (x["is_rebate"], -x["new_price"])):
note = ""
if s.get("adjusted"):
note = "← restored to catalog (+{:.2f} to rebate)".format(s["base_price"] - s["actual_price"])
if s.get("absorbed"):
note = "← absorbed {:.2f} discount".format(s["absorbed"])
marker = " " if not s["is_rebate"] else " "
print(" {}{:<12} {:>10.2f} {:>10.2f} {:>10.2f} {}".format(
marker, s["sku"], s["base_price"], s["actual_price"], s["new_price"], note))
# ═══════════════════════════════════════════════════════════════
# SHOW OTHER SAMPLE CUSTOMERS
# ═══════════════════════════════════════════════════════════════
print("\n" + "=" * 70)
print("SAMPLE OTHER CUSTOMERS")
print("=" * 70)
# Get customer names for sample accounts
sample_accts = set()
for a in ok_adjustments[:20]:
sample_accts.add(a[1])
if sample_accts:
acct_list = list(sample_accts)[:5]
for acct_id in acct_list:
cust = frappe.db.sql("""
SELECT name, customer_name FROM "tabCustomer"
WHERE legacy_account_id = %s LIMIT 1
""", (acct_id,), as_dict=True)
cust_name = cust[0]["customer_name"] if cust else "account {}".format(acct_id)
acct_adjustments = [a for a in adjustments if a[1] == acct_id]
print("\n {} (account {}) — {} deliveries".format(cust_name, acct_id, len(acct_adjustments)))
for did, _, old_total, new_total, services, status in acct_adjustments[:2]:
print(" Delivery {} [total: {:.2f}] {}".format(did, old_total, status))
for s in sorted(services, key=lambda x: (x["is_rebate"], -x["new_price"])):
if s.get("adjusted") or s.get("absorbed"):
marker = " " if not s["is_rebate"] else " "
note = ""
if s.get("adjusted"):
note = "catalog:{:.2f} actual:{:.2f} → NEW:{:.2f}".format(
s["base_price"], s["actual_price"], s["new_price"])
if s.get("absorbed"):
note = "absorbed {:.2f} → NEW:{:.2f}".format(s["absorbed"], s["new_price"])
print(" {}{:<12} {}".format(marker, s["sku"], note))
# ═══════════════════════════════════════════════════════════════
# DELIVERIES WITHOUT REBATES (discount but no rebate to absorb)
# ═══════════════════════════════════════════════════════════════
print("\n" + "=" * 70)
print("PROBLEM CASES: discount but NO rebate to absorb it")
print("=" * 70)
no_rebate_discount = 0
for did, services in deliveries.items():
has_discount = False
has_rebate = False
for s in services:
if not s["is_rebate"] and s["actual_price"] < s["base_price"] - 0.01:
has_discount = True
if s["is_rebate"]:
has_rebate = True
if has_discount and not has_rebate:
no_rebate_discount += 1
if no_rebate_discount <= 5:
print(" Delivery {}: services with discount but no rebate product".format(did))
for s in services:
if s["actual_price"] < s["base_price"] - 0.01:
print(" {} base={:.2f} actual={:.2f} diff={:.2f}".format(
s["sku"], s["base_price"], s["actual_price"],
s["base_price"] - s["actual_price"]))
print("\nTotal deliveries with discount but no rebate: {}".format(no_rebate_discount))
# ═══════════════════════════════════════════════════════════════
# GLOBAL STATS
# ═══════════════════════════════════════════════════════════════
print("\n" + "=" * 70)
print("GLOBAL SUMMARY")
print("=" * 70)
total_svc = len(all_services)
hijacked = sum(1 for s in all_services if s["hijack"])
hijacked_lower = sum(1 for did, svcs in deliveries.items() for s in svcs
if not s["is_rebate"] and s["actual_price"] < s["base_price"] - 0.01)
hijacked_higher = sum(1 for did, svcs in deliveries.items() for s in svcs
if not s["is_rebate"] and s["actual_price"] > s["base_price"] + 0.01 and s["hijack"])
print("Total active services: {:,}".format(total_svc))
print("Hijacked (custom price): {:,}".format(hijacked))
print(" ↳ cheaper than catalog: {:,} (restore to catalog + absorb in rebate)".format(hijacked_lower))
print(" ↳ more expensive than catalog: {:,} (keep actual — custom service)".format(hijacked_higher))
print("Deliveries needing rebate adjustment: {:,}".format(total_deliveries_affected))
print("Deliveries with no rebate to absorb: {:,}".format(no_rebate_discount))
print("\n" + "=" * 70)
print("ANALYSIS COMPLETE — no changes made")
print("=" * 70)