gigafibre-fsm/docs/design/prepaid-service-model.md
louispaulb 6628eaaa5b feat(ops+hub): sender picker, per-user notifs, editable rating emails, planif & tickets UX
- Comms: sélecteur d'expéditeur « De: » (défaut groupe Support TARGO) via resolveSendFrom + alias vérifiés
- Notifs: prefs de feeds PAR utilisateur (/conversations/notif-prefs) + cloche à bascules ; boot tooltip-ux (clic prioritaire + anti-empilement)
- Courriel: invitation à évaluer = modèle Unlayer éditable (transactional-rating-invite-*) ; test-send via Gmail + expansion {{rating}} ; logo TARGO auto-hébergé sur le magasin d'actifs du hub
- Planif: bloc « sans déplacement » (damier, début de quart, alerte si pas de quart), quart éditable dans l'éditeur de jour, icônes de compétence en vue jour (TV pour télé), clic cellule → éditeur, clic gauche lane → liste / clic droit → menu quart, lien ↗ ticket par job
- Tickets: défaut « Non fermés » + correction du filtre « Mes tickets » (owner)
- Inbox: poll Gmail 1 min + rafraîchir à la demande (poll-now)

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

9.3 KiB
Raw Permalink Blame History

Prepaid Service Model — Design (design-first, not yet built)

Status: DRAFT for review · 2026-06-16 · grounded in an infra audit of the hub/ERPNext/F/RADIUS/OLT stack.

Goal: offer a prepaid internet service. When the paid-through date lapses, give a 3-day grace (full speed), then reduce speed, then disable internet — all reversible instantly on top-up.


0. The critical reality (read this first)

Today there is ZERO automated network enforcement. Marking a service Suspendu/disabled in F or ERPNext only changes records and what staff see — no code cuts or restores a customer's internet. Verified across the whole hub + ops app + legacy bridge.

The actual speed/access lever is RADIUS:

  • A FreeRADIUS + MySQL server at 10.5.2.25 (db radiusdb). Subscriber auth = radcheck (Cleartext-Password) + radusergroup (policy class). The group name (e.g. residentiel) is the speed bucket — FreeRADIUS reply attributes for that group define the rate limit.
  • Per-service RADIUS creds live on the service record: legacy service.radius_user/radius_pwd, mirrored to ERPNext Service Subscription.radius_user/radius_pwd. ERPNext speed_down/speed_up are descriptive only (not enforced).
  • The hub has no RADIUS code at all — no radiusdb connection, no CoA/Disconnect.

Implication: the prepaid ledger + grace state machine is straightforward to build in the hub/ERPNext. The hard part — and the real prerequisite — is a RADIUS control plane: write radusergroup + push a live change to the subscriber session (CoA/Disconnect) on the BNG/NAS. That actuator must be built, and the BNG/NAS that terminates PPPoE/IPoE sessions (for CoA) is not identified anywhere in the repo — only the FreeRADIUS DB host is known.

Enforcement levers, ranked

Lever State today Use
RADIUS group reassign + CoA/Disconnect Not built; radiusdb@10.5.2.25 known, BNG unknown Primary — matches how speed already works
OLT ONU service-profile (profileid) swap via n8n dostuff PATCH Contract exists, not wired (hub only calls GET) Secondary GPON-layer throttle
OLT ONU enable/disable CLI strings generated (provision.js), not executed; SNMP read-only Disable fallback via n8n SSH
TR-069 setParameterValues (rate-limit/disable WAN) Raw /tasks proxy exists (devices.js:523); no caller; CPE param support unverified Tertiary, if CPE model supports it
Modem GUI (modem-bridge) Read-only Playwright scraper Not a control lever as built

1. Customer flow & state machine

                 top-up (any state) ─────────────────────────────┐
                                                                  ▼
ACTIVE ──paid_through passes──▶ GRACE (3 d, full speed, notify) ──▶ THROTTLED ──▶ SUSPENDED
  ▲          (full speed)              │ remind day 1/2/3              (slow group)   (walled-garden / reject)
  └──────────────────────── top-up restores group + CoA ◀───────────────────────────┘
  • ACTIVE: paid_through_date in the future. Normal RADIUS group.
  • GRACE (0 < days_past ≤ 3): full speed, escalating notifications (SMS + email + portal banner).
  • THROTTLED (3 < days_past ≤ disable_after): RADIUS group → residentiel-throttle (e.g. 1 Mbps) + CoA to apply live.
  • SUSPENDED (days_past > disable_after): RADIUS → walled-garden/reject + Disconnect-Message.
  • Top-up from any state: extend paid_through_date, restore full group, CoA → back to ACTIVE.

All transitions idempotent (act only on change) and audited (every customer-impacting action logged) — this is the hard lesson from the PPA double-charge incident.


2. Data model (the prepaid ledger — does not exist today)

New ERPNext doctype Prepaid Account (hub/OPS-owned; see §8 decision on F vs ERPNext):

  • customer (link), service (link to Service Subscription), radius_user (denormalized for the actuator)
  • daily_rate (derived from the plan price ÷ cycle, or a flat prepaid daily price)
  • paid_through_date (the anchor — everything derives from this)
  • state (Active / Grace / Throttled / Suspended)
  • grace_days (default 3), disable_after_days (e.g. 10), throttle_group, active_group
  • last_enforced_state + last_enforced_at (idempotency)

Top-ups recorded as ERPNext Payment Entry (existing, idempotent by reference_no) and extend paid_through_date by amount / daily_rate.


3. Enforcement actuator — new hub module lib/service-control.js

setGroup(radius_user, group)   // UPDATE radusergroup.groupname (radiusdb @ 10.5.2.25)
coa(radius_user, action)       // CoA (apply new policy) or Disconnect (drop session) → BNG/NAS
throttle(svc) / suspend(svc) / restore(svc)  // setGroup + coa, idempotent + audited
  • New env RADIUS_DB_URL (mysql @ 10.5.2.25/radiusdb) + BNG_HOST/BNG_COA_SECRET (OPEN — see §8).
  • If live CoA is infeasible at first: change the group anyway; it applies on next re-auth/lease renewal (slower, but functional). CoA is the upgrade for instant effect.
  • Fallbacks wired behind the same interface: OLT profileid swap (n8n dostuff PATCH) and TR-069 set-param.

4. Daily cron (gated, dry-run first)

Hub cron (once daily, low-traffic hour): for each Prepaid Account, compute days_past = today paid_through_date, derive target state, and only act if it differs from last_enforced_state. Emit notifications on entering GRACE (day 1/2/3), THROTTLED, SUSPENDED.

Safety (mandatory, per PPA-incident lessons):

  • PREPAID_ENFORCE env: unset/report = log intended actions, change nothing; enforce = act. (Same pattern as PAYMENTS_AUTH.)
  • Idempotency on last_enforced_state; never re-disable/re-charge.
  • Per-run cap + kill switch; structured audit log of every state change.

5. Top-up (reuse existing Stripe flow — already usable)

  • New POST /prepaid/topup (or reuse /payments/checkout-product): ad-hoc Stripe Checkout for $X. ensureStripeCustomer already reuses F's authoritative cus_…, so no double customer.
  • On payment success (webhook → recordPayment, idempotent): credit balance → extend paid_through_date → if state was Throttled/Suspended, restore() (group + CoA).
  • Saved-card off-session top-up possible via POST /payments/charge for auto-renew prepaid.

6. Read-side (extend what exists)

  • GET /collab/service-status → add a prepaid block: {state, balance_days, paid_through, daily_rate}.
  • Client /diag/<jwt> page → show "X days remaining / topped through DATE", state banner, Top-up now CTA.

7. Phasing (value early, risk late)

  • Phase 1 — ledger + top-up + reminders, NO enforcement (SAFE, no network actions): Prepaid Account doctype, Stripe top-up, balance/days on service-status + /diag, grace/expiry notifications. Delivers prepaid billing + dunning immediately with zero risk.
  • Phase 2 — actuator in report mode: lib/service-control.js connects to radiusdb, logs what it would change. Identify the BNG/NAS for CoA. No live changes.
  • Phase 3 — throttle (gated): define residentiel-throttle group attrs in FreeRADIUS; enable group reassign + CoA for the THROTTLED transition.
  • Phase 4 — disable (gated): walled-garden/reject group + Disconnect for SUSPENDED.

8. Open decisions (need your input before Phase 1)

  1. Balance home — ERPNext/hub-owned ledger (recommended) vs F. F is authoritative for postpaid billing but has no prepaid concept; bolting prepaid onto legacy F is heavy. Recommend the hub/ERPNext owns the prepaid ledger, with prepaid services flagged distinctly. (This is a deliberate divergence from "F authoritative" — for a model F doesn't have.)
  2. The BNG/NAS that terminates subscriber PPPoE/IPoE sessions — IP + CoA shared secret? Without it, enforcement relies on re-auth/lease expiry (minutes-to-hours lag) instead of instant CoA.
  3. Throttle target — a new FreeRADIUS group (residentiel-throttle, e.g. 1 Mbps)? Its reply attributes must be defined on the RADIUS server.
  4. Disable UX — hard reject (no internet) vs walled-garden (captive redirect to the top-up page). Walled-garden is far better UX but needs a captive portal.
  5. Windows — grace = 3 d (given). Gap before disable? (e.g. throttle at d3, disable at d10.)
  6. Who is prepaid — new signups only? Opt-in per service? A billing_mode = prepaid|postpaid flag on the service.

Key files (for whoever builds this)

  • services/targo-hub/lib/payments.js — Stripe (ensureStripeCustomer, checkout-product, charge, recordPayment); reuse for top-ups.
  • services/targo-hub/lib/ticket-collab.jsserviceStatus() (read-side), dostuffStatus() (n8n dostuff, GET only).
  • services/targo-hub/lib/devices.js — TR-069 /tasks passthrough (:523).
  • services/targo-hub/lib/olt-snmp.js (read-only), lib/provision.js (OLT CLI gen, not executed), lib/client-diag.js (/diag).
  • docs/reference/legacy-wizard/account_wizard.php:142-169 — how RADIUS users/groups are provisioned today (the template for the actuator).
  • RADIUS: FreeRADIUS + MySQL radiusdb @ 10.5.2.25 (creds in the legacy wizard). BNG/NAS for CoA: unknown — must be located.