fix(reports/legacy): exclude expired credits; confirm monthly price model
Reviewed against docs/archive/LEGACY-ACCOUNTING-ANALYSIS.md (the migration audit) which surfaced two things to check in the overpriced-internet report: 1. service.payment_recurrence (0=annual, 2=monthly, 5=semestrial...) — checked whether per-cycle prices needed /N normalization. They do NOT: verified a semestrial FTTH1500I carries product.price=109.95, identical to the monthly one (billed 6×109.95 every 6 months). Per §6.1 "prix = quantité × prix_unitaire", product.price is already the monthly unit price. The original monthly logic was correct — no division. The SKU-LIKE-'%ANN' /12 special-case stays (true annual plans where price IS the yearly amount, e.g. FTTH_ANN @ 480$/yr). 2. Promo credits carry an actif_until end date (§10). A discount line whose actif_until is past no longer reduces today's bill, so counting it understates what the client actually pays. Now excluded. NULL-safety: the exclusion needs an explicit `actif_until IS NOT NULL` guard — without it, `NOT (price<0 AND actif_until>0 AND actif_until<now)` evaluates to NULL for permanent credits (actif_until NULL), which SQL treats as not-true and silently DROPS every permanent credit line. That briefly inflated the residential count to 3330; with the guard it's a correct 1000 (vs 983 before — +17 addresses whose only sub-90 reason was a now-expired credit). Net effect: the report reflects the *current* real monthly Internet bill. 🤖 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:
parent
7f06c254c8
commit
b631fabf91
|
|
@ -22,15 +22,26 @@
|
||||||
*
|
*
|
||||||
* 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
|
||||||
* // Annual plans (SKU ends in "ANN", e.g. FTTH_ANN @ 480$/yr) are
|
* // product.price is ALREADY the monthly unit price regardless of the
|
||||||
* // normalized to a monthly equivalent so the threshold compares apples
|
* // service's billing frequency — a semestrial FTTH1500I has price
|
||||||
* // to apples — otherwise a $480/yr (=$40/mo) plan falsely shows as $480.
|
* // 109.95 (same as the monthly one) and is billed 6×109.95 every 6
|
||||||
|
* // months. Verified against the legacy billing logic (LEGACY-
|
||||||
|
* // ACCOUNTING-ANALYSIS.md §6.1: "prix = quantité × prix_unitaire").
|
||||||
|
* // So we do NOT divide by service.payment_recurrence.
|
||||||
|
* // The one exception: true annual plans (SKU ends in "ANN", e.g.
|
||||||
|
* // FTTH_ANN @ 480$/yr where price IS the yearly amount) → normalize /12.
|
||||||
* monthly = base / (sku LIKE '%ANN' ? 12 : 1)
|
* monthly = base / (sku LIKE '%ANN' ? 12 : 1)
|
||||||
*
|
*
|
||||||
* Only recurring lines count: product.price_recurr_type = 1. Type 0 is
|
* Only recurring lines count: product.price_recurr_type = 1. Type 0 is
|
||||||
* one-time charges (equipment, installation) which don't belong on a
|
* one-time charges (equipment, installation) which don't belong on a
|
||||||
* recurring monthly bill.
|
* recurring monthly bill.
|
||||||
*
|
*
|
||||||
|
* Expired credits excluded: a discount line (price<0) whose service
|
||||||
|
* actif_until is in the past no longer reduces the real bill — counting
|
||||||
|
* it would understate what the client actually pays today. (Per
|
||||||
|
* LEGACY-ACCOUNTING-ANALYSIS.md §10: promo credits carry an actif_until
|
||||||
|
* end date.)
|
||||||
|
*
|
||||||
* Internet product categories:
|
* Internet product categories:
|
||||||
* 32 Mensualités fibre, 4 Mensualités sans-fil, 23 Internet camping
|
* 32 Mensualités fibre, 4 Mensualités sans-fil, 23 Internet camping
|
||||||
* (add-ons) 16 Téléch. supp, 17 IP fixe, 21 Location point-à-point
|
* (add-ons) 16 Téléch. supp, 17 IP fixe, 21 Location point-à-point
|
||||||
|
|
@ -106,6 +117,7 @@ function buildQuery (url) {
|
||||||
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 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())
|
||||||
${commercialClause}
|
${commercialClause}
|
||||||
GROUP BY d.id
|
GROUP BY d.id
|
||||||
HAVING net_internet > ?
|
HAVING net_internet > ?
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user