gigafibre-fsm/docs/features/billing-payments.md
louispaulb beb6ddc5e5 docs: reorganize into architecture/features/reference/archive folders
All docs moved with git mv so --follow preserves history. Flattens the
single-folder layout into goal-oriented folders and adds a README.md index
at every level.

- docs/README.md — new landing page with "I want to…" intent table
- docs/architecture/ — overview, data-model, app-design
- docs/features/ — billing-payments, cpe-management, vision-ocr, flow-editor
- docs/reference/ — erpnext-item-diff, legacy-wizard/
- docs/archive/ — HANDOFF-2026-04-18, MIGRATION, status-snapshots/
- docs/assets/ — pptx sources, build scripts (fixed hardcoded path)
- roadmap.md gains a "Modules in production" section with clickable
  URLs for every ops/tech/portal route and admin surface
- Phase 4 (Customer Portal) flipped to "Largely Shipped" based on
  audit of services/targo-hub/lib/payments.js (16 endpoints, webhook,
  PPA cron, Klarna BNPL all live)
- Archive files get an "ARCHIVED" banner so stale links inside them
  don't mislead readers

Code comments + nginx configs rewritten to use new doc paths. Root
README.md documentation table replaced with intent-oriented index.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 11:51:33 -04:00

19 KiB

Facturation & Paiements — Handoff dev

Référence unique pour toutes les fonctionnalités facture / paiement construites sur la stack erp.gigafibre.ca + client.gigafibre.ca + ops. Lisez ../architecture/overview.md d'abord pour le contexte réseau/services.

Dernière MàJ : 2026-04-17


Table des matières

  1. Importation du système legacy vers ERPNext
  2. App Frappe custom — gigafibre_utils
  3. Print Format « Facture TARGO »
  4. Flux de paiement public (sans Authentik)
  5. Infrastructure Docker
  6. UI Ops — aperçu client
  7. Configuration & secrets
  8. Points connus / TODO

1. Importation du système legacy vers ERPNext

Particularités

  • Source : MariaDB legacy gestionclient (conteneur legacy-db, 10.100.80.100).
  • Cible : PostgreSQL ERPNext v16 (patches GROUP BY/HAVING/quotes appliqués — cf. feedback_erpnext_postgres.md).
  • Correspondance des clés :
    • customer.id legacy → Customer.legacy_id (custom field).
    • service.delivery_id legacy → Service Location.legacy_delivery_id (custom field).
    • invoice_item.service_id legacy → chaîné via service.delivery_id pour rattacher un item à son Service Location ERPNext.
  • Prix : conservés tels quels (pas de re-calcul des taxes); TPS/TVQ sont imports inclus dans taxes[] de chaque Sales Invoice.
  • Volumes finaux : 6 667 customers · 21 K subscriptions · 115 K invoices · 1,6 M invoice items · 242 K tickets.

Fonctionnalités — scripts scripts/migration/*

Script Rôle
import_invoices.py Import complet des factures legacy + JOIN sur service pour récupérer delivery_id et renseigner service_location sur chaque item.
backfill_service_location.py Script one-shot pour remplir service_location sur les 1,6 M items déjà importés (batches execute_values de 20 K lignes, 227 s).
import_payments.py / reimport_payments.py / import_expro_payments.py Import des paiements legacy en Payment Entry réconciliés.
import_payment_methods.py Carte + ACH + chèque — créés en tant que Mode of Payment.
fix_invoice_outstanding.py Réconciliation après import — recalcule outstanding_amount via Frappe.
fix_invoice_customer_names.py Renomme customers pour refléter le nom légal courant (Subscription.bill_to_name).
import_payment_arrangements.py Ententes de paiement (échelonnement) → Payment Request + scheduler.
geocode_locations.py Géocode toutes les Service Location avec Mapbox.

Restauration Item ↔ Service Location (particularité majeure)

Sans ce lien le PDF ne peut pas grouper les lignes par adresse.

legacy.invoice_item.service_id
    └─→ legacy.service.delivery_id
            └─→ ERPNext Service Location.legacy_delivery_id  (custom field)
  1. Custom field Sales Invoice Item.service_location (Link → Service Location) créé via add_missing_custom_fields.py.
  2. import_invoices.py mis à jour : JOIN sur service + lookup Service Location.legacy_delivery_id → écrit service_location lors de l'INSERT.
  3. Backfill ponctuel : backfill_service_location.py (UPDATE ... FROM (VALUES %s) via psycopg2.extras.execute_values).

Points d'attention

  • Mots de passe legacy = MD5 non-salé → forcer un reset via OTP email/SMS (project_portal_auth.md).
  • Devices rattachés aux addresses (Service Location), pas aux customers (feedback_device_hierarchy.md).
  • Serials TPLG ERPNext ≠ serials réels — matching via MAC (feedback_device_serial_mapping.md).

2. App Frappe custom — gigafibre_utils

Particularités

  • Chemin source (serveur ERP) : /opt/erpnext/custom/gigafibre_utils/
  • Cuite dans l'image Docker/opt/erpnext/custom/Dockerfile copie l'app dans apps/gigafibre_utils et la pip-installe en editable.
  • Raison d'exister : contourner le sandbox Frappe safe_exec (qui bloque __import__ et certaines opérations) en exposant des @frappe.whitelist() Python natifs appelables depuis Jinja et côté client.

Arborescence

gigafibre_utils/
├── Dockerfile                          # FROM frappe/erpnext:v16.10.1 + chromium + cairosvg
└── gigafibre_utils/
    ├── __init__.py
    ├── hooks.py
    ├── modules.txt
    ├── patches.txt
    ├── api.py                          # 823 lignes, toutes les méthodes whitelisted
    └── www/
        └── pay-public.html             # Page publique /pay-public (sans Authentik)

Fonctionnalités — api.py

2.1 Rendu visuel (QR, logo, descriptions)

Méthode allow_guest Rôle
invoice_qr(invoice) PNG binaire du QR code (payload = URL /pay-public signée, TTL 60 j).
invoice_qr_base64(invoice) Base64 brut (pour embed direct dans Jinja).
invoice_qr_datauri(invoice) data:image/png;base64,… prêt à injecter en <img src>.
logo_base64(height) Logo TARGO rasterisé via cairosvg (SVG → PNG 96 px).
short_item_name(name, max_len) Nettoie/tronque les noms d'item (retire caractères parasites, limite longueur).

Particularité QR : la fonction _sign_pay_token génère un token HMAC-SHA256 (clé gigafibre_pay_secret) → {invoice, expires_at} encodé base64-url. Le QR pointe vers https://client.gigafibre.ca/pay-public?inv=…&t=….

Particularité logo : wkhtmltopdf (QtWebKit 2013) ignore <defs><style> SVG. Solution retenue : rendu serveur-side via cairosvg → PNG base64 injecté dans le template. Aucune dépendance côté client.

2.2 Génération PDF (invoice_pdf)

  • Contourne complètement la pipeline PDF Frappe (qui ajoute 15 mm de marge top forcée si pas de #header-html, et post-process avec pypdf).
  • Lance chromium --headless=new --no-pdf-header-footer --print-to-pdf directement dans un tempfile.TemporaryDirectory.
  • Réponse HTTP : Content-Type: application/pdf + Content-Disposition: inline (⚠️ ne pas utiliser "binary" — empêche l'affichage iframe et Acrobat échoue à ouvrir le fichier téléchargé).
  • Producer : Skia/PDF m147 (identique à Antigravity/Gemini → fidélité pixel-parfait).

Usage :

GET /api/method/gigafibre_utils.api.invoice_pdf?name=SINV-2026-700010
GET /api/method/gigafibre_utils.api.invoice_pdf?name=SINV-xxx&print_format=Facture+TARGO

2.3 Code de parrainage (referral_code)

  • Déterministe par Customer — HMAC-SHA256 (clé gigafibre_pay_secret) sur customer.name → 30 bits → 6 caractères Crockford base32 (pas de 0/O/1/I/L/U ambigus).
  • Stable dans le temps (pas stocké en DB).
  • ~1,07 milliard de codes possibles → collisions négligeables pour <10 M customers.

3. Print Format « Facture TARGO »

Particularités

  • Install : scripts/migration/setup_invoice_print_format.py (idempotent).
  • Doctype : Sales Invoice.
  • Générateur : pdf_generator="chrome" — Chromium --print-to-pdf headless. C'est la meilleure méthode dans ERPNext v16 ; wkhtmltopdf (QtWebKit 2013) est obsolète et donne un rendu dégradé (SVG <defs><style> ignorés, flexbox cassé, polices Unicode QC incomplètes). Avantages :
    • CSS moderne : flexbox/grid, @page avec counter(page) / counter(pages) pour la pagination (p. ex. « Page 1 de 2 » en @top-right), break-inside: avoid.
    • Polices système : les accents/caractères spéciaux (é, à, œ, ℃) passent sans mapping manuel.
    • Rendu pixel-perfect : même moteur que le navigateur de preview — le preview HTML et le PDF sont identiques.
    • Performances : ~1 s pour une facture typique (vs ~3 s wkhtmltopdf).
    • Prérequis : Chromium installé dans le container (voir Dockerfile FROM frappe/erpnext:v16.10.1 + apt-get install chromium), clé chromium_path: /usr/bin/chromium dans common_site_config.json.
  • Marges : forcées par le Print Format margin_top=5, bottom=5, left=15, right=15 (mm) — Chrome PDF ignore @page { margin: … } dans le CSS (preferCSSPageSize=false), il faut passer par les options ERPNext.
  • Template Jinja : ~25 Ko, inliné dans le script setup_invoice_print_format.py (constante html_template = r"""…""") — source de vérité unique, versionnée.

Fonctionnalités clés du template

  1. Prélude de résolution de données — récupère customer, service_locations, prev_invoice, recent_payments, remaining_balance, account_number, etc. depuis le doc Sales Invoice.
  2. Position de la fenêtre d'enveloppe — bloc adresse client à 50 mm du haut (2″) pour s'aligner sur enveloppe #10 à fenêtre (ouverture 2″-3½″).
    • margin-top du .cl-block varie selon la présence d'un prev_invoice (10 mm si bloc SOMMAIRE DU COMPTE présent en amont, 33 mm sinon).
  3. Regroupement par Service Location — la section « FRAIS COURANTS » itère current_charges_locations[], chaque location a ses items (pas de sous-total par location pour réduire la hauteur).
  4. Période de service inline — calculée par item :
    • Utilise service_start_date / service_end_date si présents (ERPNext deferred).
    • Sinon déduit la période courante sauf si l'item est one-time (mots-clés : install, activation, rabais, frais unique, remise, ou montant négatif).
    • Format : (1er avril au 30 avril) plein mois, (16 avril au 30 avril) prorata.
  5. Colonne de droite (meta + QR + parrainage + conditions) :
    • Meta-band : N° compte · Date · N° facture
    • Montant dû (boîte verte)
    • QR code (base64 inline, 60 j TTL)
    • Boîte parrainage (fond gris, accent vert) — code 6 car. via gigafibre_utils.api.referral_code.
    • Conditions légales (« sera assujettie à des frais de retard… »)
    • Contactez-nous : 1867 chemin de la Rivière, Ste-Clotilde, J0L 1W0 · 855 888-2746
  6. Mini-footer CPRST en bas de page.

Particularités Jinja

  • Variables svc_start / svc_end doivent être définies avant la boucle items (sinon UndefinedError).
  • Les appels frappe.call(...) dans le sandbox Jinja requièrent un frappe.local.request vivant → le rendu hors contexte web (ex. bench execute) échoue : toujours tester via HTTP (l'endpoint invoice_pdf fait ça correctement).

Fichiers connexes

  • scripts/migration/invoice_preview.jinja — copie de travail lisible (pas utilisée en prod — seul le Print Format installé compte).
  • scripts/migration/test_jinja_render.py — rend le template localement via Chrome macOS pour comparaison pixel (référence).

4. Flux de paiement public (sans Authentik)

Vue d'ensemble

    QR (facture) / SMS / Email                 Authentik-less
            ↓                                         ↑
    https://client.gigafibre.ca/pay-public?inv=X&t=HMAC
            ↓
    validate_pay_token  → affiche résumé facture
            ↓
    [Payer avec Stripe]
            ↓
    create_checkout_session  → redirect Stripe
            ↓
    Stripe Checkout (hors ERP)
            ↓
    stripe_webhook  → Payment Entry reconciled

Trois TTL de tokens (signés HMAC-SHA256)

Token TTL Usage
QR code 60 jours Lien permanent sur facture imprimée.
Magic link 15 minutes SMS/email en cas de token expiré.
pay_redirect 1 heure Bridge depuis portail authentifié (customer connecté).

Fonctionnalités — api.py

Méthode allow_guest Rôle
pay_token(invoice, ttl_days) (admin) Génère une URL signée (debug/test).
validate_pay_token(invoice, token) Vérifie signature+TTL, retourne résumé facture.
request_magic_link(invoice, channel, last4) Envoie SMS/email (gated par last4 du téléphone).
create_checkout_session(invoice, token) Crée Stripe Checkout Session.
stripe_webhook() Vérifie signature Stripe, crée Payment Entry.
pay_redirect(invoice) ⚠️ sous Authentik Bridge portal → pay-public (bypass staff @targo.ca/@gigafibre.ca).

Landing page /pay-public

  • Servie par Frappe depuis gigafibre_utils/www/pay-public.html.
  • Vanilla JS — pas de build step, pas de framework.
  • Lit inv + t depuis frappe.form_dict.
  • Appelle validate_pay_token → affiche facture OU formulaire magic-link.
  • Bouton « Payer avec Stripe » → create_checkout_session → redirect.

Carve-out Traefik (contournement Authentik)

Fichier : /opt/traefik/dynamic/pay-public.yml.

Routeurs avec priority ≥ 250 (> priorité 150 des règles /api Authentik) :

  • client-pay-publicPathPrefix(/pay-public) (landing)
  • client-api-validate-pay-token
  • client-api-magic-link
  • client-api-create-checkout-session
  • client-api-stripe-webhook

Tous servis sur client-portal-svc, aucun middleware (pas de ForwardAuth).

Stripe

  • Checkout Session API : créée côté serveur (_stripe_post), redirect 303 vers session.url.
  • Webhook : vérifie Stripe-Signature (_stripe_verify_signature) avec gigafibre_stripe_webhook_secret → crée Payment Entry rattaché à la facture.
  • Clé réutilisée depuis targo-hub (sk_live_51QCMOI…).
  • Auth basic (twilio_account_sid:twilio_auth_token).
  • From twilio_from_number=+14382313838.
  • Anti-abuse : request_magic_link exige last4 matchant les 4 derniers chiffres du téléphone customer avant d'envoyer.

5. Infrastructure Docker

Image custom ERPNext

/opt/erpnext/custom/Dockerfile :

FROM frappe/erpnext:v16.10.1
USER root
RUN apt-get install -y libcairo2 libpango-1.0-0 libpangocairo-1.0-0 \
      libgdk-pixbuf-2.0-0 shared-mime-info \
      chromium fonts-liberation fonts-noto-color-emoji
USER frappe
COPY --chown=frappe:frappe gigafibre_utils apps/gigafibre_utils
RUN env/bin/pip install -e apps/gigafibre_utils cairosvg
RUN grep -qx gigafibre_utils apps/apps.txt || echo gigafibre_utils >> apps/apps.txt

Particularités

  • Chromium installé à /usr/bin/chromium (config key chromium_path).
  • cairosvg (Python) pour rasterisation SVG → PNG.
  • App gigafibre_utils copiée dans l'image (pas un volume) → toute modif requiert docker compose build erpnext-backend && docker compose up -d.
  • ⚠️ Piège : sites/apps.txt peut se retrouver avec une ligne parasite apps.txt qui casse bench (ModuleNotFoundError: No module named 'apps') — nettoyer si ça arrive.

Build & déploiement

cd /opt/erpnext/custom
docker compose build erpnext-backend
docker compose up -d --force-recreate erpnext-backend

6. UI Ops — aperçu client

Fonctionnalité

Onglet « Aperçu client » dans le volet de détail facture (apps/ops/src/components/shared/detail-sections/InvoiceDetail.vue).

  • q-tabs : Détails (champs grid) ↔ Aperçu client (iframe PDF).
  • Iframe pointe sur gigafibre_utils.api.invoice_pdf?name=… → rendu Chrome pixel-parfait identique au PDF client.

Particularité Vue scoped

Les styles .modal-field-grid / .mf / .mf-label définis dans DetailModal.vue avec scoped ne cascadent pas vers les composants enfants montés via <component :is>. Il faut les dupliquer dans le <style scoped> du composant enfant (contre-intuitif — documenter pour les nouveaux devs).


7. Configuration & secrets

Toutes les clés vivent dans /opt/erpnext/custom/erpnext/sites/common_site_config.json (montage bind du volume sites sur le conteneur erpnext-backend).

Clés requises

Clé Valeur / format Utilisation
gigafibre_pay_secret 64 hex HMAC signing tokens + referral codes
gigafibre_pay_host https://client.gigafibre.ca/pay-public Base URL QR/magic-link
gigafibre_stripe_secret_key sk_live_… Stripe Checkout API
gigafibre_stripe_webhook_secret whsec_… TODO — webhook signature verify
twilio_account_sid AC… SMS magic-link
twilio_auth_token (hex) SMS auth
twilio_from_number +14382313838 SMS from
chromium_path /usr/bin/chromium PDF generator

Commande pour poser une clé

docker exec erpnext-backend-1 bench set-config -g <key> "<value>"

8. Points connus / TODO

⚠️ TODO bloquants

  • Webhook Stripe : enregistrer un second endpoint pointant vers https://client.gigafibre.ca/api/method/gigafibre_utils.api.stripe_webhook dans le dashboard Stripe, récupérer le whsec_…, poser via bench set-config -g gigafibre_stripe_webhook_secret.

Améliorations suggérées

  • Inverse lookup parrainage : customer_from_referral_code(code) pour appliquer le crédit 50 $ automatiquement à la souscription du parrain.
  • Rate-limit sur request_magic_link (actuellement protégé par last4 seulement — OK pour l'abus téléphone, pas pour énumération d'invoices).
  • Refresh token côté Traccar : le token généré expire le 2031-04-17 ; prévoir rotation automatique (script cron + API /api/session/token).
  • Archivage local des PDFs : stocker dans File Doctype pour historique et éviter de re-rendre via Chromium à chaque affichage.

Pièges connus

  • frappe.response.type = "binary" ⇒ Acrobat échoue à ouvrir le fichier téléchargé → toujours utiliser "pdf" pour les PDFs.
  • Postgres v16 + ERPNext : bugs GROUP BY/HAVING/double-quotes — patches dans feedback_erpnext_postgres.md.
  • Script bench execute échoue sur gigafibre_utils.api.invoice_pdf hors contexte web (Jinja appelle frappe.call qui requiert frappe.local.request). Tester via HTTP.
  • Traccar token ≠ FCM token — les APA91b… sont pour Firebase push, pas pour l'API Traccar (cf. conversation 2026-04-17).

Fichiers à connaître (index rapide)

Fichier Rôle
/opt/erpnext/custom/Dockerfile Image ERPNext custom
/opt/erpnext/custom/gigafibre_utils/gigafibre_utils/api.py 823 lignes — toutes les méthodes whitelisted
/opt/erpnext/custom/gigafibre_utils/gigafibre_utils/www/pay-public.html Landing publique
/opt/traefik/dynamic/pay-public.yml Carve-out Authentik
scripts/migration/setup_invoice_print_format.py Install/update du Print Format
scripts/migration/import_invoices.py Import legacy → ERPNext
scripts/migration/backfill_service_location.py Backfill item↔location 1,6 M lignes
scripts/migration/test_jinja_render.py Rendu local pour comparaison pixel
apps/ops/src/components/shared/detail-sections/InvoiceDetail.vue Onglet Aperçu client
services/targo-hub/lib/traccar.js Proxy Traccar (Bearer token)