gigafibre-fsm/scripts/migration/import_items.py
louispaulb 041df48ac3 security: harden payments/hub auth + remove leaked ERPNext token from source
Auth hardening:
- payments: per-customer JWT authorization on dual-use /payments routes
  (balance/methods/invoice/checkout/setup/portal/toggle-ppa) via authorizeCustomer
  + PAYMENTS_AUTH=enforce; portal retains+sends magic-link JWT (sessionStorage)
- hub: fail-closed Stripe webhook, /accept/doc-pdf IDOR gate, telephony field-name
  guard (SQLi), modem-bridge private-IP guard (SSRF), ALWAYS_ENFORCE expansion

Leaked-credential cleanup (token already rotated in ERPNext):
- de-hardcode ERPNext API token -> env in bulk_submit.py, import_items.py,
  apps/client/deploy.sh; placeholder in apps/ops/infra/nginx.conf (nginx injects)
- ops prod build no longer bakes VITE_ERP_TOKEN (.env.production empty)
- de-hardcode legacy DB password -> env in import_items.py
- gitignore legacy migration PII exports (tsv/json)

Conversations/UI:
- floating (un-docked) conversation panel; full-width mailbox
- per-message real sender from email headers; unified scroll; header spacing
- campaign pre-send review (subject/from/channel), Gmail send channel, clone-as-draft

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 06:17:17 -04:00

138 lines
4.1 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Migration Script: Legacy product -> ERPNext Item
Python 3.5 compatible (no f-strings)
"""
import json
import os
import sys
import requests
import pymysql
try:
from html import unescape
except ImportError:
try:
from HTMLParser import HTMLParser
unescape = HTMLParser().unescape
except:
unescape = lambda x: x
LEGACY = {
"host": "legacy-db",
"user": "facturation",
"password": os.environ.get("LEGACY_DB_PASSWORD", ""),
"database": "gestionclient",
}
ERP_URL = "https://erp.gigafibre.ca"
ERP_TOKEN = "token " + os.environ.get("ERP_TOKEN", "") # set via env, never commit
CAT_MAP = {
1: u"Garantie prolongée",
2: u"Intérêts et frais divers",
3: u"Impression",
4: u"Mensualités sans fil",
5: u"Technicien",
6: u"Frais d'activation",
7: u"Equipement internet sans fil",
8: u"Installation et équipement internet sans fil",
9: u"Téléphonie",
10: u"Site internet",
11: u"Nom de domaine",
12: u"Services informatique",
13: u"Location d'espace",
14: u"Pièces informatique",
15: u"Hébergement",
16: u"Téléchargement supplémentaire",
17: u"Adresse IP Fixe",
18: u"Infographie",
19: u"Revenus - Frais de recouvrement",
20: u"Créances irrécouvrables",
21: u"Location point à point",
22: u"Frais pour irrécouvrables",
23: u"Internet camping",
24: u"Transport",
25: u"Frais divers taxables",
26: u"Installation et équipement fibre",
27: u"SPECIAL",
28: u"Quincaillerie",
29: u"Equipement internet fibre",
30: u"Location espace cloud",
31: u"Honoraires",
32: u"Mensualités fibre",
33: u"Mensualités télévision",
34: u"Installation et équipement télé",
}
SERVICE_CATS = {1,2,4,6,9,11,12,13,15,16,17,18,19,20,21,22,23,24,25,27,30,31,32,33}
def erp_post(doctype, data):
url = "{}/api/resource/{}".format(ERP_URL, doctype)
headers = {"Authorization": ERP_TOKEN, "Content-Type": "application/json"}
r = requests.post(url, headers=headers, json={"data": json.dumps(data)}, timeout=30, verify=True)
try:
return r.status_code, r.json()
except:
return r.status_code, {"error": r.text[:200]}
def main():
conn = pymysql.connect(**LEGACY)
cur = conn.cursor(pymysql.cursors.DictCursor)
cur.execute("""
SELECT p.*,
(SELECT COUNT(*) FROM service s WHERE s.product_id = p.id AND s.status = 1) as active_services
FROM product p
ORDER BY p.id
""")
products = cur.fetchall()
created = 0
skipped = 0
errors = 0
for p in products:
sku = p["sku"] or "LEGACY-{}".format(p["id"])
cat_id = p["category"] or 4
item_group = CAT_MAP.get(cat_id, "Products")
sku = unescape(sku).strip()
is_service = cat_id in SERVICE_CATS
disabled = 1 if (not p["active"] and p["active_services"] == 0) else 0
item_data = {
"doctype": "Item",
"item_code": sku,
"item_name": sku,
"item_group": item_group,
"stock_uom": "Nos",
"is_stock_item": 0 if is_service else 1,
"disabled": disabled,
"description": "Legacy product ID: {}".format(p["id"]),
"standard_rate": float(p["price"] or 0),
}
status, resp = erp_post("Item", item_data)
resp_str = str(resp)
if status == 200 and resp.get("data"):
created += 1
print(" OK {:<30s} ${:>8.2f} grp={:<25s} svc={}".format(
sku, float(p["price"] or 0), item_group[:25], p["active_services"]))
elif "DuplicateEntryError" in resp_str or "already exists" in resp_str.lower():
skipped += 1
print(" SKIP {:<30s} (already exists)".format(sku))
else:
errors += 1
err_msg = str(resp.get("exc_type", resp.get("message", resp_str)))[:80]
print(" ERR {:<30s} -> {}".format(sku, err_msg))
conn.close()
print("\n=== DONE: {} created, {} skipped, {} errors (total {}) ===".format(
created, skipped, errors, len(products)))
if __name__ == "__main__":
main()