Sample output from the Customer Revenue Concentration prompt in the Sonar AI Prompt Library, run against a NetSuite test account. Every name and number here is test data. Back to the post · The library

Sonar AI · Analytics Brief · Account TD3016323

Customer Revenue Concentration
FY2026 Year-to-Date vs FY2025

How dependent is the business on its largest customers, and is that dependence rising or falling? Herfindahl-Hirschman Index, Gini coefficient, Lorenz & Pareto curves, top-N shares and the customers driving the year-over-year shift — computed in a sandboxed reducer from posting invoices and cash sales, journals excluded.

Read-only · no records touched
Generated 2026-09-04 · Sonar v1.15.0
Current period Jan 1 – Sep 4, 2026 (247 days)
Comparison FY2025 (full year) + like-for-like Jan 1 – Sep 4, 2025
Basis Subsidiaries 1, 2, 3 · USD · xElim excluded

01Executive summary

Concentration fell sharply and the customer base broadened. HHI dropped from 683.9 (FY2025) to 474.0 (FY2026 YTD), a −30.7% move that raises the effective number of customers from 14.6 to 21.1. Both readings sit in the unconcentrated band (<1,500), so this is a shift from "healthy" to "healthier", not a rescue from a danger zone.

The largest single dependency halved. The #1 customer's share fell from 14.31% (Jones Manufacturing, FY2025) to 7.40% (Design Excellence Ltd., FY2026 YTD). The top-5 share fell 12.9 points to 34.8%; the top-10 fell 12.5 points to 62.5%. It now takes 8 customers (not 6) to reach half of revenue and 16 (not 12) to reach 80%.

Revenue is up, not just spread thinner. FY2026 YTD revenue of $1,329,784 already exceeds all of FY2025 ($1,249,050, +6.5%) with four months remaining; against the like-for-like Jan–Sep 4 2025 window ($728,254) it is up 82.6%. The annualised run-rate is ≈ $1.97M.

The nuance: inequality within the base barely moved. The Gini coefficient eased only from 0.774 to 0.759. The distribution is still a steep power law — the bottom 50% of customers produce 4.4% of revenue and 19 customers under $1,000 contribute 0.85% combined. Concentration fell because more mid-sized accounts arrived at the top (29 new customers contributed 40.2% of YTD revenue), not because the long tail grew in weight.

The risk pattern to watch: one-invoice whales. Three of the FY2026 top-10 (Red Rivers Consulting, Magna Tech Limited, Falcon Systems — 19.7% of revenue combined) are single-invoice customers. In FY2025 the four single-invoice customers in the top-12 (Global Information, Mercury Co., Gotter inc., Haskell Associates — 22.5% of that year) all produced zero revenue in FY2026. If the pattern repeats, ~20% of this year's revenue is non-recurring by construction.

02KPI dashboard — FY2026 YTD, with FY2025 comparison

Green deltas indicate lower concentration risk (the direction most finance teams treat as favourable). Amber indicates the opposite. Grey is context, not a risk signal.

Revenue (FY26 YTD)
$1,329,784
FY25 full year $1,249,050+6.5%
Like-for-like FY25 $728,254 +82.6%
Revenue-generating customers
98
FY25 77+21 · LFL 72
654 transactions (286 invoices, 368 cash sales)
HHI (0–10,000)
474.0
FY25 683.9−209.9 · LFL 827.4
Band: Unconcentrated (<1,500)
Effective # customers (1/HHI)
21.1
FY25 14.6+6.5 · LFL 12.1
Revenue behaves as if 21 equal-sized accounts
Gini coefficient
0.759
FY25 0.774−0.015 · LFL 0.792
Still a steep power-law distribution
Top-1 share
7.40%
FY25 14.31%−6.9 pts · LFL 16.04%
Design Excellence Ltd. (was Jones Manufacturing)
Top-5 / Top-10 share
34.8% / 62.5%
FY25 47.7% / 75.0%−12.9 / −12.5
Top-20: 87.4% (FY25 91.7%)
Customers to reach 50% / 80%
8 / 16
FY25 6 / 12+2 / +4 · LFL 5 / 10
More accounts needed to cover the same revenue
Customers under $1,000
19
Combined $11,336 · 0.85% of revenue
FY25: 8 customers, $5,421, 0.43%
Median customer revenue
$2,132
Mean $13,569 (6.4× median)
FY25 median $2,075 · mean $16,221
P90 / Max customer revenue
$54,824
Max $98,452
FY25 P90 $68,058 · max $178,754
Channel mix
95.4% invoiced
Cash sales $61,142 (4.6%) across 368 tickets
FY25: 94.2% invoiced, 530 cash tickets

HHI band position

01,500 · Unconcentrated →2,500 · Moderate →Highly concentrated → 5,000

Scale shown 0–5,000 (half the theoretical maximum of 10,000, which represents a single customer). Standard DOJ/FTC bands: <1,500 unconcentrated · 1,500–2,500 moderately concentrated · >2,500 highly concentrated. Applied to a customer book, a useful rule of thumb is that HHI above ~1,000 (effective N below 10) warrants explicit key-account risk management.

03Lorenz & Pareto curves

Left: the Lorenz curve (customers sorted ascending; the shaded area against the 45° equality line is the Gini). Right: the Pareto view (customers sorted descending) — the classic "what share of customers gives what share of revenue" reading. 20 points each at 5% intervals; both periods overlaid.

Lorenz curve — cumulative % customers (ascending) → cumulative % revenue

0%20%40%60%80%100% 0%20%40%60%80%100% Cumulative % of customers (smallest → largest) Cumulative % of revenue Line of equality Bottom 80% of customers= 12.6% of revenue (FY26) Gini: FY26 YTD 0.759 · FY25 0.774
FY2026 YTD (Jan 1 – Sep 4)FY2025 (full year)Perfect equality

Pareto curve — cumulative % customers (descending) → cumulative % revenue

0%20%40%60%80%100% 0%20%40%60%80%100% Cumulative % of customers (largest → smallest) Cumulative % of revenue 80/20 reference Top 20% of customers → 87.4% (FY26) · 90.0% (FY25) Top 5% → 34.8% (FY26) · 39.5% (FY25)
FY2026 YTD (Jan 1 – Sep 4)FY2025 (full year)Perfect equality
Curve data — 20 points per period (cumulative % customers → cumulative % revenue)
% customersLorenz FY26 (asc.)Lorenz FY25 (asc.)Pareto FY26 (desc.)Pareto FY25 (desc.)# cust. FY26 / FY25
5%0.06%0.16%34.80%39.52%5 / 4
10%0.30%0.43%62.50%65.81%10 / 8
15%0.59%0.79%78.90%81.68%15 / 12
20%0.93%1.09%87.44%90.02%20 / 15
25%1.34%1.57%90.44%91.46%25 / 19
30%1.70%2.09%91.56%92.55%29 / 23
35%2.27%2.64%92.73%93.44%34 / 27
40%2.93%3.21%93.76%94.26%39 / 31
45%3.67%3.79%94.71%95.03%44 / 35
50%4.44%4.42%95.56%95.74%49 / 39
55%5.29%4.97%96.33%96.21%54 / 42
60%6.24%5.74%97.07%96.79%59 / 46
65%7.27%6.56%97.73%97.36%64 / 50
70%8.44%7.45%98.30%97.91%69 / 54
75%9.87%8.54%98.74%98.43%74 / 58
80%12.56%9.98%99.07%98.91%78 / 62
85%21.10%18.32%99.41%99.21%83 / 65
90%37.50%34.19%99.70%99.57%88 / 69
95%65.20%60.48%99.94%99.84%93 / 73
100%100.00%100.00%100.00%100.00%98 / 77

Point k at percentile j is the first ⌊n·j/20 + 0.5⌉ customers in sort order; with n = 98 and 77 the customer counts are not perfectly even multiples, hence the "# customers" column.

04Top-15 customers — FY2026 YTD

Roll-up key is the top-level parent (see Method). No customer in either period had a parent with revenue, so every row is a single entity. 1 invoice flags customers whose entire period revenue came from a single transaction — a recurrence risk.

#Customer (internal id)RevenueShareShare %CumulativeTxnsFY25 rankNote
1Design Excellence Ltd. (257)$98,451.57
7.40%7.40%152Retained · was 8.92%
2Red Rivers Consulting (402)$94,370.00
7.10%14.50%1—New in FY26 1 invoice
3Jones Manufacturing (276)$91,439.53
6.88%21.38%101Retained · was 14.31%
4Magna Tech Limited (284)$89,343.00
6.72%28.10%1—New in FY26 1 invoice
5Pineapple Republic (398)$89,154.90
6.70%34.80%86Retained · was 6.15%
6Panaderia Co. (396)$85,596.53
6.44%41.24%84Retained · was 8.15%
7Falcon Systems (259)$78,456.00
5.90%47.14%1—New in FY26 1 invoice
8Recreational Outfitters (401)$74,345.66
5.59%52.73%910Retained · was 4.10% · crosses 50%
9Marshall Industries (287)$71,379.90
5.37%58.09%515Retained · was 2.28% · entered top-10
10Davis Supplies (253)$58,639.75
4.41%62.50%75Retained · was 8.13%
11Realpoint inc. (400)$53,188.16
4.00%66.50%77Retained · was 6.08% · left top-10
12Blockster Inc. (280)$52,770.00
3.97%70.47%3—New in FY26
13Macgruber Incorporated (281)$50,652.00
3.81%74.28%3—New in FY26
14Hugo Limited (270)$31,446.48
2.36%76.65%1011Retained · was 3.33%
15John G. Roche Opticians (275)$29,939.00
2.25%78.90%1—New in FY26 1 invoice
Top-15 subtotal$1,049,172.4878.90%89HHI contribution 455.2
Remaining 83 customers$280,611.7921.10%565HHI contribution 18.8 · includes all 368 cash-sale tickets
Total (98 customers)$1,329,784.27100.00%654HHI 474.0
Top-15 customers — FY2025 (full year), for reference
#Customer (id)RevenueShareShare %CumulativeTxnsFY26 status
1Jones Manufacturing (276)$178,754.46
14.31%14.31%12Rank 3 · 6.88%
2Design Excellence Ltd. (257)$111,358.56
8.92%23.23%12Rank 1 · 7.40%
3Global Information (263)$101,799.00
8.15%31.38%1No FY26 revenue 1 invoice
4Panaderia Co. (396)$101,737.39
8.15%39.52%11Rank 6 · 6.44%
5Davis Supplies (253)$101,590.06
8.13%47.66%12Rank 10 · 4.41%
6Pineapple Republic (398)$76,837.46
6.15%53.81%9Rank 5 · 6.70%
7Realpoint inc. (400)$75,943.79
6.08%59.89%11Rank 11 · 4.00%
8Mercury Co. (292)$73,976.00
5.92%65.81%1No FY26 revenue 1 invoice
9Gotter inc. (265)$64,112.00
5.13%70.94%1No FY26 revenue 1 invoice
10Recreational Outfitters (401)$51,216.69
4.10%75.04%11Rank 8 · 5.59%
11Hugo Limited (270)$41,542.26
3.33%78.37%12Rank 14 · 2.36%
12Haskell Associates (268)$41,356.00
3.31%81.68%1No FY26 revenue 1 invoice
13Karmabit (278)$39,753.34
3.18%84.86%12Rank >15 · 1.21%
14Entenmanns LLC (258)$35,875.36
2.87%87.73%8Rank >15
15Marshall Industries (287)$28,520.65
2.28%90.02%7Rank 9 · 5.37%
Top-15 subtotal$1,124,373.0290.02%121HHI contribution 681.9
Remaining 62 customers$124,676.579.98%776HHI contribution 2.0
Total (77 customers)$1,249,049.59100.00%897HHI 683.9

05Year-over-year metric changes

Two comparisons are shown. FY2025 full year is what you asked for; the like-for-like column (Jan 1 – Sep 4, 2025) removes the seasonal/partial-year bias — and it turns out the direction and magnitude of every concentration metric are the same on both bases, so the conclusion is robust.

MetricFY2026 YTDFY2025 (full)Δ vs FY25FY2025 like-for-likeΔ vs LFLReading
Total revenue$1,329,784$1,249,050+$80,735 · +6.5%$728,254+82.6%YTD already exceeds all of FY25
Revenue-generating customers9877+21 · +27.3%72+26Base broadened
HHI (0–10,000)474.0683.9−209.9 · −30.7%827.4−353.4 · −42.7%Less concentrated; both unconcentrated band
Effective # customers (1/HHI)21.114.6+6.5 · +44.5%12.1+9.0Diversification up
Gini coefficient0.7590.774−0.015 · −2.0%0.792−0.033Marginally more equal; still power-law
Top-1 share7.40%14.31%−6.91 pts16.04%−8.64 ptsSingle-customer dependency halved
Top-5 share34.80%47.66%−12.86 pts55.38%−20.58 ptsLargest absolute improvement
Top-10 share62.50%75.04%−12.54 pts81.84%−19.34 pts
Top-20 share87.44%91.74%−4.30 pts91.69%−4.25 ptsTail still thin below rank 20
Customers to reach 50% of revenue86+25+3
Customers to reach 80% of revenue1612+410+6
Customers under $1,00019 (0.85%)8 (0.43%)+11 · +0.42 pts21 (1.52%)−2Store retail tail; immaterial to revenue
Median / mean customer revenue$2,132 / $13,569$2,075 / $16,221+2.8% / −16.3%—Mean fell as whales shrank; median stable

06Who drove the change

Share deltas are FY2026 YTD share minus FY2025 full-year share, in percentage points. The HHI decomposition attributes the −209.9 move to individual customers via Δ(share²) × 10,000.

Entered the top-10

CustomerFY25 rank / shareFY26 rank / shareStatus
Red Rivers Consulting— / 0%#2 / 7.10%New 1 invoice
Magna Tech Limited— / 0%#4 / 6.72%New 1 invoice
Falcon Systems— / 0%#7 / 5.90%New 1 invoice
Marshall Industries#15 / 2.28%#9 / 5.37%Grew +150%

Left the top-10

CustomerFY25 rank / shareFY26 rank / shareStatus
Global Information#3 / 8.15%— / 0%No FY26 revenue
Mercury Co.#8 / 5.92%— / 0%No FY26 revenue
Gotter inc.#9 / 5.13%— / 0%No FY26 revenue
Realpoint inc.#7 / 6.08%#11 / 4.00%Slipped one place out

Biggest share gains (pts)

CustomerFY25 → FY26 shareΔ ptsFY26 revenue
Red Rivers Consulting0.00% → 7.10%+7.10$94,370
Magna Tech Limited0.00% → 6.72%+6.72$89,343
Falcon Systems0.00% → 5.90%+5.90$78,456
Blockster Inc.0.00% → 3.97%+3.97$52,770
Macgruber Incorporated0.00% → 3.81%+3.81$50,652
Marshall Industries2.28% → 5.37%+3.08$71,380
John G. Roche Opticians0.00% → 2.25%+2.25$29,939
Informics International0.00% → 2.01%+2.01$26,672

Biggest share losses (pts)

CustomerFY25 → FY26 shareΔ ptsFY25 → FY26 revenue
Global Information8.15% → 0.00%−8.15$101,799 → $0
Jones Manufacturing14.31% → 6.88%−7.43$178,754 → $91,440
Mercury Co.5.92% → 0.00%−5.92$73,976 → $0
Gotter inc.5.13% → 0.00%−5.13$64,112 → $0
Davis Supplies8.13% → 4.41%−3.72$101,590 → $58,640
Haskell Associates3.31% → 0.00%−3.31$41,356 → $0
Realpoint inc.6.08% → 4.00%−2.08$75,944 → $53,188
Karmabit3.18% → 1.21%−1.97$39,753 → $16,076

HHI decomposition — top contributors to the −209.9 move

CustomerΔ HHI pointsWhy
Jones Manufacturing−157.5Share 14.31% → 6.88%; the single biggest de-concentration effect (75% of the total move)
Global Information−66.48.15% → 0; one-invoice customer did not return
Red Rivers Consulting+50.4New at 7.10%; partially offsets
Davis Supplies−46.78.13% → 4.41%
Magna Tech Limited+45.1New at 6.72%
Mercury Co.−35.15.92% → 0; one-invoice customer did not return

Customer cohort bridge — FY2025 → FY2026 YTD

CohortCustomersFY25 revenueFY26 YTD revenueShare of FY26
Retained (revenue in both years)69$963,034$795,78559.8%
New in FY26 (no FY25 revenue)29—$533,99940.2%
Lost (FY25 revenue, none in FY26)8$286,016——
Total98 / 77$1,249,050$1,329,784100%

Retained customers are running at 82.6% of their FY25 full-year total with 4 months left — on pace to roughly match. Growth to date is almost entirely new-logo revenue. The 8 lost customers took 22.9% of FY25 revenue with them; 4 of them were single-invoice accounts.

07Risk read & suggested actions

🟢 What improved

  • No customer exceeds 7.5% of revenue (FY25: one at 14.3%).
  • Losing the top customer outright would now cost 7.4% of revenue vs 14.3% a year ago.
  • Effective customer count up 45%; 16 accounts now needed to cover 80%.
  • Base grew 27% (77 → 98 revenue-generating customers) while revenue grew too.

🟠 What to watch

  • Single-invoice whales: Red Rivers, Magna Tech, Falcon Systems, John G. Roche = $292,108 (22.0%) from four invoices. Last year's four equivalents all churned to zero.
  • Retained-cohort softness: Jones −49%, Davis −42%, Realpoint −30%, Karmabit −60% YoY. Check whether this is timing or genuine share loss.
  • Tail growth without weight: under-$1,000 customers rose 8 → 19 but still only 0.85% of revenue — long-tail acquisition isn't yet moving the needle.

🔵 Suggested next steps

  • Tag the four one-invoice top accounts for renewal/expansion outreach before Q4; a second order from any one of them changes the recurring-revenue picture materially.
  • Re-run this analysis at FY close; if HHI stays < 600 and top-5 < 40%, the diversification is structural rather than a timing artefact.
  • Track "effective # customers" as a standing KPI alongside top-10 share — it is the single number that captures both metrics.
  • Optionally re-cut by class (product category) or subsidiary to see whether concentration is uneven within the book.

08Method, definitions & assumptions

Revenue definition (as specified)

  • Transaction types CustInvc and CashSale with transaction.posting = 'T'.
  • Lines with transactionline.mainline = 'F' and taxline = 'F'; measure SUM(ABS(tl.netamount)).
  • transactionline.subsidiary IN (1, 2, 3) — subsidiary 4 (xElim, elimination) excluded. transaction.subsidiary is NOT_EXPOSED in this account, hence the line-level filter.
  • Journals excluded entirely — the monthly "Beg Balance Entries" JEs (JE102–JE149) are synthetic demo data (~86% of GL revenue). This analysis therefore measures transactional customer revenue, and will not tie to the Income Statement.
  • Period boundary on trandate (not posting period): FY2026 YTD = 2026-01-01..2026-09-04; FY2025 = 2025-01-01..2025-12-31; like-for-like = 2025-01-01..2025-09-04.

Parent roll-up rule

Key = COALESCE(customer.parent, transaction.entity) — one level. Verified before running: of 274 customers, exactly 1 has a parent and the hierarchy depth is 1 (no grandparents), so one level is exhaustive. In both periods the roll-up affected 0 rows — the sole child customer had no revenue — so every reported "customer" is a single entity. The reducer still applies the rule and reports member counts, so it will work correctly if hierarchies are added later.

Metric formulas

  • Share sᵢ = revenueᵢ / Σ revenue.
  • HHI = 10,000 × Σ sᵢ². Bands: <1,500 unconcentrated; 1,500–2,500 moderate; >2,500 high.
  • Effective # customers = 1 / Σ sᵢ² (the number of equal-sized customers that would produce the same HHI).
  • Gini = 2·Σ(i·xᵢ) / (n·Σxᵢ) − (n+1)/n, with xᵢ sorted ascending and i = 1..n.
  • Top-N share = Σ of the N largest sᵢ. Customers to 50%/80% = smallest k with cumulative share ≥ threshold.
  • Lorenz = cumulative revenue share of the first ⌊n·j/20 + ½⌉ customers sorted ascending, j = 1..20; Pareto = same on descending sort.
  • Δ HHI attribution per customer = 10,000 × (s₂₆² − s₂₅²).

Assumptions & caveats

  • No credit memos or returns netted. Only the two types you specified were included; CustCred would reduce gross revenue for some customers. Add it (with sign) if you want net revenue.
  • ABS() is safe here: a sign audit found every included line negative (GL credit convention) and zero positive lines in either period — so ABS flips sign only and does not inflate discounts or negative lines.
  • Shipping, handling and discount lines that are non-tax, non-mainline lines are included in revenue.
  • FY2026 is a partial year (247 of 365 days). The like-for-like column controls for this; the full-year FY2025 comparison overstates FY25 in absolute terms but does not change the direction of any concentration metric.
  • Customer count = entities with at least one qualifying line in the window; the account has 274 customer records in total.
  • Branding: no branding attachment was received with the request; this document uses the "Precision Slate" default palette. All colours are CSS variables at the top of the file for a one-line re-skin.

09Hand-checks

Check
FY2026 YTD
FY2025
Σ shares (all customers)
100.000000% ✓
100.000000% ✓
Top-15 revenue + residual = total
1,049,172.48 + 280,611.79 = 1,329,784.27 ✓ (Δ 0.00)
1,124,373.02 + 124,676.57 = 1,249,049.59 ✓ (Δ 0.00)
HHI(top-15) + HHI(residual) = HHI
455.2 + 18.8 = 474.0 ✓
681.9 + 2.0 = 683.9 ✓
1 / (HHI/10,000) = effective N
10,000 / 474.0 = 21.1 ✓
10,000 / 683.9 = 14.6 ✓
Top-1 share = max / total
98,451.57 / 1,329,784.27 = 7.40% ✓
178,754.46 / 1,249,049.59 = 14.31% ✓
Pareto(10%) ≈ top-10 share (n=98 → k=10; n=77 → k=8)
62.50% = 62.50% ✓
65.81% (k=8) vs top-10 75.04% — expected, k≠10
Invoice + cash sale = total
1,268,642.60 + 61,141.67 = 1,329,784.27 ✓
1,176,828.91 + 72,220.68 = 1,249,049.59 ✓
Sign audit: positive lines in scope
0 of 1,800 lines ✓
0 of 2,493 lines ✓
Cohort bridge: retained + new = FY26 total
795,785.33 + 533,998.94 = 1,329,784.27 ✓
retained + lost = 963,033.59 + 286,016.00 = 1,249,049.59 ✓

Manual spot-check of HHI from the published top-15 table (FY26): 7.40² + 7.10² + 6.88² + 6.72² + 6.70² + 6.44² + 5.90² + 5.59² + 5.37² + 4.41² + 4.00² + 3.97² + 3.81² + 2.36² + 2.25² = 455.4 (rounding of displayed shares; unrounded reducer value 455.2) + residual 18.8 ≈ 474. The residual's small size (83 customers, 21.1% of revenue, only 18.8 HHI points) confirms that concentration is entirely a top-15 phenomenon.

10Appendix — verbatim SuiteQL and reducer

Everything below was executed exactly as shown via sqlReduce (rows never entered the model context — only the reducer's summary did). Queries follow the house SuiteQL style guide (uppercase keywords, standard aliases, tl.subsidiary filtering, ROUND(…,2) on money, deterministic ORDER BY).

Q1 · Revenue by customer × transaction type — FY2026 YTD (the FY2025 query differs only in the two date literals)
SELECT
    t.entity                         AS customer_id,
    c.entityid                       AS customer,
    c.companyname                    AS company,
    c.parent                         AS parent_id,
    p.entityid                       AS parent_name,
    t.type                           AS tran_type,
    COUNT(DISTINCT t.id)             AS txn_count,
    ROUND(SUM(ABS(tl.netamount)), 2) AS revenue
FROM transaction t
JOIN transactionline tl ON tl.transaction = t.id
LEFT JOIN customer c    ON c.id = t.entity
LEFT JOIN customer p    ON p.id = c.parent
WHERE t.type IN ('CustInvc', 'CashSale')
  AND t.posting = 'T'
  AND tl.mainline = 'F'
  AND tl.taxline = 'F'
  AND tl.subsidiary IN (1, 2, 3)
  AND t.trandate >= TO_DATE('2026-01-01', 'YYYY-MM-DD')
  AND t.trandate <= TO_DATE('2026-09-04', 'YYYY-MM-DD')
GROUP BY t.entity, c.entityid, c.companyname, c.parent, p.entityid, t.type
ORDER BY t.entity, t.type
-- FY2025 variant: trandate BETWEEN TO_DATE('2025-01-01') AND TO_DATE('2025-12-31')  → 79 rows
-- Like-for-like variant: trandate BETWEEN TO_DATE('2025-01-01') AND TO_DATE('2025-09-04') → 72 rows
-- FY2026 YTD result: 100 rows (98 customers × up to 2 transaction types)
Q2 · Pre-flight: customer hierarchy depth (roll-up rule validation)
SELECT
    COUNT(*)                                                    AS total_customers,
    SUM(CASE WHEN c.parent IS NOT NULL THEN 1 ELSE 0 END)       AS with_parent,
    SUM(CASE WHEN c.parent IS NOT NULL AND p.parent IS NOT NULL THEN 1 ELSE 0 END) AS depth2,
    SUM(CASE WHEN c.parent IS NOT NULL AND p.parent IS NOT NULL AND gp.parent IS NOT NULL THEN 1 ELSE 0 END) AS depth3
FROM customer c
LEFT JOIN customer p  ON p.id = c.parent
LEFT JOIN customer gp ON gp.id = p.parent
-- Result: total_customers 274, with_parent 1, depth2 0, depth3 0
Q3 · Sign audit: are there positive (discount/return) lines that ABS() would distort?
SELECT
    CASE WHEN t.trandate >= TO_DATE('2026-01-01','YYYY-MM-DD') THEN 'FY26 YTD' ELSE 'FY25' END AS period,
    SUM(CASE WHEN tl.netamount < 0 THEN 1 ELSE 0 END)                     AS negative_lines,
    ROUND(SUM(CASE WHEN tl.netamount < 0 THEN tl.netamount ELSE 0 END), 2) AS negative_amount,
    SUM(CASE WHEN tl.netamount > 0 THEN 1 ELSE 0 END)                     AS positive_lines,
    ROUND(SUM(tl.netamount), 2)                                            AS signed_net,
    ROUND(SUM(ABS(tl.netamount)), 2)                                       AS abs_net
FROM transaction t
JOIN transactionline tl ON tl.transaction = t.id
WHERE t.type IN ('CustInvc', 'CashSale')
  AND t.posting = 'T'
  AND tl.mainline = 'F'
  AND tl.taxline = 'F'
  AND tl.subsidiary IN (1, 2, 3)
  AND t.trandate >= TO_DATE('2025-01-01', 'YYYY-MM-DD')
  AND t.trandate <= TO_DATE('2026-09-04', 'YYYY-MM-DD')
GROUP BY CASE WHEN t.trandate >= TO_DATE('2026-01-01','YYYY-MM-DD') THEN 'FY26 YTD' ELSE 'FY25' END
-- FY26 YTD: 1,800 negative lines, 0 positive, signed −1,329,784.27, abs 1,329,784.27
-- FY25:     2,493 negative lines, 0 positive, signed −1,249,049.59, abs 1,249,049.59
Reducer · sqlReduce JavaScript (contract "whole"; rows.fy26 / rows.fy25 from Q1 and its FY2025 variant)
function build(raw){
  // Roll up to top-level parent: key = parent_id || customer_id (hierarchy depth verified = 1)
  const map = {};
  let rolled = 0, unknownEntity = 0;
  const byType = {CustInvc:{rev:0,txns:0}, CashSale:{rev:0,txns:0}};
  for (const r of raw){
    const rev = H.num(r.revenue), txns = H.num(r.txn_count);
    const key = r.parent_id ? String(r.parent_id) : String(r.customer_id);
    if (r.parent_id) rolled++;
    if (!r.customer) unknownEntity++;
    const name = r.parent_id ? (r.parent_name || ('#'+r.parent_id)) : (r.customer || r.company || ('entity #'+r.customer_id));
    if (!map[key]) map[key] = {key, name, rev:0, txns:0, inv:0, cash:0, children:new Set()};
    map[key].rev += rev; map[key].txns += txns;
    if (r.tran_type==='CustInvc') map[key].inv += rev; else map[key].cash += rev;
    map[key].children.add(String(r.customer_id));
    byType[r.tran_type].rev += rev; byType[r.tran_type].txns += txns;
  }
  const custs = Object.values(map).map(c=>({key:c.key,name:c.name,rev:+c.rev.toFixed(2),txns:c.txns,inv:+c.inv.toFixed(2),cash:+c.cash.toFixed(2),members:c.children.size}));
  custs.sort((a,b)=>b.rev-a.rev);
  const n = custs.length, total = custs.reduce((s,c)=>s+c.rev,0);
  let cum = 0, hhi = 0, shareSum = 0, c50=null, c80=null;
  custs.forEach((c,i)=>{ c.share = c.rev/total; shareSum += c.share; hhi += c.share*c.share; cum += c.share; c.cumShare = cum; c.rank=i+1;
    if(c50===null && cum>=0.5) c50=i+1; if(c80===null && cum>=0.8) c80=i+1; });
  const topShare = k => custs.slice(0,k).reduce((s,c)=>s+c.share,0);
  // Gini on ascending sort: G = 2Σ(i·x_i)/(n·Σx) − (n+1)/n
  const asc = custs.slice().sort((a,b)=>a.rev-b.rev);
  let num=0; asc.forEach((c,i)=>{ num += (i+1)*c.rev; });
  const gini = (2*num)/(n*total) - (n+1)/n;
  // Lorenz (ascending) & Pareto (descending), 20 points at 5% steps
  const lorenz=[], pareto=[];
  for (let j=1;j<=20;j++){
    const k = Math.round(n*j/20);
    lorenz.push({pctCust:j*5, pctRev:+(asc.slice(0,k).reduce((s,c)=>s+c.rev,0)/total*100).toFixed(2), custs:k});
    pareto.push({pctCust:j*5, pctRev:+(custs.slice(0,k).reduce((s,c)=>s+c.rev,0)/total*100).toFixed(2), custs:k});
  }
  const small = custs.filter(c=>c.rev<1000);
  const hhi10k = hhi*10000;
  const band = hhi10k<1500?'Unconcentrated (<1,500)':hhi10k<2500?'Moderately concentrated (1,500-2,500)':'Highly concentrated (>2,500)';
  // Hand-check: HHI from top-15 + residual
  const top15 = custs.slice(0,15);
  const hhiTop15 = top15.reduce((s,c)=>s+c.share*c.share,0)*10000;
  const hhiResidual = custs.slice(15).reduce((s,c)=>s+c.share*c.share,0)*10000;
  const stats = H.stats(custs, c=>c.rev);
  return {
    n, total:+total.toFixed(2), rolledRows:rolled, unknownEntity,
    byType:{CustInvc:{rev:+byType.CustInvc.rev.toFixed(2),txns:byType.CustInvc.txns}, CashSale:{rev:+byType.CashSale.rev.toFixed(2),txns:byType.CashSale.txns}},
    hhi:+hhi10k.toFixed(1), band, effectiveN:+(1/hhi).toFixed(1), gini:+gini.toFixed(4),
    top1:+(topShare(1)*100).toFixed(2), top5:+(topShare(5)*100).toFixed(2), top10:+(topShare(10)*100).toFixed(2), top20:+(topShare(20)*100).toFixed(2),
    custTo50:c50, custTo80:c80, lorenz, pareto,
    top15: top15.map(c=>({rank:c.rank,key:c.key,name:c.name,rev:c.rev,txns:c.txns,inv:c.inv,cash:c.cash,members:c.members,share:+(c.share*100).toFixed(2),cumShare:+(c.cumShare*100).toFixed(2)})),
    under1k:{count:small.length, rev:+small.reduce((s,c)=>s+c.rev,0).toFixed(2), share:+(small.reduce((s,c)=>s+c.share,0)*100).toFixed(2)},
    checks:{shareSumPct:+(shareSum*100).toFixed(6), hhiTop15:+hhiTop15.toFixed(1), hhiResidual:+hhiResidual.toFixed(1), hhiRecomputed:+(hhiTop15+hhiResidual).toFixed(1),
            sumTop15Rev:+top15.reduce((s,c)=>s+c.rev,0).toFixed(2), sumRestRev:+custs.slice(15).reduce((s,c)=>s+c.rev,0).toFixed(2)},
    dist:{min:+stats.min.toFixed(2),median:+stats.median.toFixed(2),mean:+stats.mean.toFixed(2),p90:+stats.p90.toFixed(2),max:+stats.max.toFixed(2)},
    _all: custs.map(c=>({key:c.key,name:c.name,rev:c.rev,share:c.share,rank:c.rank}))
  };
}
const A = build(rows.fy26), B = build(rows.fy25);
// ---- YoY drivers ----
const idx26 = H.indexBy(A._all,'key'), idx25 = H.indexBy(B._all,'key');
const top10_26 = A._all.slice(0,10).map(c=>c.key), top10_25 = B._all.slice(0,10).map(c=>c.key);
const entered = top10_26.filter(k=>!top10_25.includes(k)).map(k=>({name:idx26[k].name, rank26:idx26[k].rank, share26:+(idx26[k].share*100).toFixed(2), rank25: idx25[k]?idx25[k].rank:null, share25: idx25[k]?+(idx25[k].share*100).toFixed(2):0}));
const left = top10_25.filter(k=>!top10_26.includes(k)).map(k=>({name:idx25[k].name, rank25:idx25[k].rank, share25:+(idx25[k].share*100).toFixed(2), rank26: idx26[k]?idx26[k].rank:null, share26: idx26[k]?+(idx26[k].share*100).toFixed(2):0}));
const keys = H.uniq([...Object.keys(idx26),...Object.keys(idx25)]);
const deltas = keys.map(k=>{const a=idx26[k],b=idx25[k];return {key:k,name:(a||b).name, share25:b?b.share*100:0, share26:a?a.share*100:0, rev25:b?b.rev:0, rev26:a?a.rev:0, d:(a?a.share:0)-(b?b.share:0)};});
const fmt = d=>({name:d.name, share25:+d.share25.toFixed(2), share26:+d.share26.toFixed(2), deltaPts:+(d.d*100).toFixed(2), rev25:d.rev25, rev26:d.rev26,
                  status: !idx25[d.key]?'NEW in FY26':!idx26[d.key]?'No FY26 revenue':'Both'});
const gains = H.sortBy(deltas, d=>-d.d).slice(0,8).map(fmt);
const losses = H.sortBy(deltas, d=>d.d).slice(0,8).map(fmt);
const newCust = deltas.filter(d=>!idx25[d.key]), lostCust = deltas.filter(d=>!idx26[d.key]), both = deltas.filter(d=>idx25[d.key]&&idx26[d.key]);
const retention = {newCount:newCust.length, newRev26:+newCust.reduce((s,d)=>s+d.rev26,0).toFixed(2), lostCount:lostCust.length, lostRev25:+lostCust.reduce((s,d)=>s+d.rev25,0).toFixed(2),
                   retainedCount:both.length, retainedRev25:+both.reduce((s,d)=>s+d.rev25,0).toFixed(2), retainedRev26:+both.reduce((s,d)=>s+d.rev26,0).toFixed(2)};
// HHI decomposition: Δ(share²) × 10,000 per customer
const hhiDrivers = H.sortBy(deltas.map(d=>({name:d.name, dHHI: (Math.pow(d.share26/100,2)-Math.pow(d.share25/100,2))*10000})), d=>-Math.abs(d.dHHI)).slice(0,6).map(d=>({name:d.name,dHHI:+d.dHHI.toFixed(1)}));
delete A._all; delete B._all;
return {meta:meta.rowCounts, fy26:A, fy25:B, yoy:{entered, left, gains, losses, retention, hhiDrivers}};

Execution: 100 + 79 rows fetched (28 KB), worker time 17 ms, total 1.3 s. The like-for-like FY2025 window was computed with a trimmed variant of build() against the 72-row Jan 1 – Sep 4, 2025 result set. SVG path coordinates were generated in evalJs from the reducer's 20-point arrays (x = 60 + pct × 4.6, y = 400 − pct × 3.8) so no coordinate was hand-derived.

Sonar AI v1.15.0 · account TD3016323 (production, OneWorld, USD) · run 2026-09-04 by Tim Dietrich (Administrator) · tools used: skillRead (house-suiteql-style, suiteql-essentials), runSql ×3 (pre-flight/audits), sqlReduce ×3, evalJs ×1, artifactCreate ×1 · no records created, modified or deleted · source tables: transaction, transactionline, customer.