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

131 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.js``serviceStatus()` (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.**