fix(reports/legacy): active clients only — exclude terminated + non-customer accounts

User flagged that several listed accounts are inactive (Or Viande Inc,
Denis Henderson). Root cause: I filtered service.status=1 but NOT the
account, so terminated accounts carrying an orphan active service line
slipped through. The legacy billing job (LEGACY-ACCOUNTING-ANALYSIS.md
§6.1) bills only when BOTH service.status=1 AND account.status=1.

Three account-level filters added:
- account.status = 1   → drops terminated accounts. Or Viande Inc is
  status=4, terminated 2014 (terminate_date set), but still had a
  service.status=1 row. 8602 accounts are status=4 vs 6537 status=1.
- account.group_id = 5 → "Client" per account_group. Drops 6 Prospect,
  7 Fournisseur, 8 Relais (network infra, e.g. Denis Henderson's
  REL_CHRY_CHARLES tower account), 10 Équipement motorisé.
- customer_id NOT LIKE 'PROPRIO%' → 59 landowner-hosts-our-gear accounts
  that live in group 5 but aren't paying customers (Denis Henderson's
  other account PROPRIOH_STCHARLES). A genuine same-name customer
  (Robert Henderson, ROBEH...) correctly stays.

Residential >90$/mo: 983 → 554 (was inflated ~44% by dead/non-customer
accounts). Commercial: 255 → 240.

Ops page note updated to state "comptes clients actifs uniquement" and
list what's excluded.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-06-01 19:21:24 -04:00
parent b631fabf91
commit 8a9df4b85e
2 changed files with 27 additions and 4 deletions

View File

@ -7,11 +7,13 @@
<q-btn flat dense icon="download" label="CSV" :href="csvUrl" :disable="!rows.length" /> <q-btn flat dense icon="download" label="CSV" :href="csvUrl" :disable="!rows.length" />
</div> </div>
<div class="text-caption text-grey-7 q-mb-md" style="max-width:780px"> <div class="text-caption text-grey-7 q-mb-md" style="max-width:820px">
Total mensuel net du service Internet (forfait + rabais récurrents, forfaits annuels Total mensuel net du service Internet (forfait + rabais récurrents, forfaits annuels
ramenés au mois) par adresse de service, depuis la base legacy. Exclut TV, téléphonie ramenés au mois) par adresse de service, depuis la base legacy.
et les frais ponctuels (équipement, installation). Sert à repérer les clients qui <strong>Comptes clients actifs uniquement</strong> exclut les comptes résiliés,
gagneraient à être déplacés vers un forfait mieux adapté. prospects, relais/infrastructure et propriétaires-hébergeurs. Exclut aussi TV,
téléphonie, frais ponctuels (équipement, installation) et crédits promo expirés.
Sert à repérer les clients qui gagneraient à être déplacés vers un forfait mieux adapté.
</div> </div>
<!-- Filters --> <!-- Filters -->

View File

@ -20,6 +20,24 @@
* service (delivery_id, status=1) active service line * service (delivery_id, status=1) active service line
* product (category, price) the plan; price<0 = recurring discount * product (category, price) the plan; price<0 = recurring discount
* *
* Account-level filters (mirror the legacy recurring-billing logic in
* LEGACY-ACCOUNTING-ANALYSIS.md §6.1, which bills only when BOTH
* service.status=1 AND account.status=1):
* account.status = 1 active account. status=4 is terminated
* (8602 accounts, most with a terminate_date,
* e.g. Or Viande Inc closed in 2014 but still
* carrying an orphan service.status=1 line).
* account.group_id = 5 "Client" per the account_group table. Excludes
* 6 Prospect, 7 Fournisseur, 8 Relais (network
* infrastructure accounts like a tower-host),
* 10 Équipement motorisé none are billable
* residential/commercial customers.
* customer_id NOT LIKE 59 "PROPRIO*" accounts sit inside group 5 but
* 'PROPRIO%' are landowners hosting our relay/antenna gear
* under a special arrangement (e.g. Denis
* Henderson "PROPRIOH_STCHARLES"), not regular
* paying customers. Excluded by their id prefix.
*
* Effective monthly price of a service line: * Effective monthly price of a service line:
* base = service.hijack=1 ? service.hijack_price : product.price * base = service.hijack=1 ? service.hijack_price : product.price
* // product.price is ALREADY the monthly unit price regardless of the * // product.price is ALREADY the monthly unit price regardless of the
@ -116,6 +134,9 @@ function buildQuery (url) {
JOIN service s ON s.delivery_id = d.id AND s.status = 1 JOIN service s ON s.delivery_id = d.id AND s.status = 1
JOIN product p ON p.id = s.product_id JOIN product p ON p.id = s.product_id
WHERE p.category IN (${catPlaceholders}) WHERE p.category IN (${catPlaceholders})
AND a.status = 1
AND a.group_id = 5
AND a.customer_id NOT LIKE 'PROPRIO%'
AND p.price_recurr_type = 1 AND p.price_recurr_type = 1
AND NOT (p.price < 0 AND s.actif_until IS NOT NULL AND s.actif_until > 0 AND s.actif_until < UNIX_TIMESTAMP()) AND NOT (p.price < 0 AND s.actif_until IS NOT NULL AND s.actif_until > 0 AND s.actif_until < UNIX_TIMESTAMP())
${commercialClause} ${commercialClause}