Diagnose "advisor gross dropped / gross not posting to advisor" complaints on the Tekion Advisor Performance Report. Usually the report's Status filter (INVOICED vs CLOSED), not a data loss or advisor-reassignment bug.
Install
npx skillscat add joecastelino/jay-skill-pack/tekion-gross-not-posting-to-advisor Install via the SkillsCat registry.
"Gross Not Posting To Advisor" — Diagnosis
Symptom
A manager (e.g. Kevin Stapp / SCT) screenshots the Advisor Performance Report
twice a few minutes apart. Between the two, the advisor's RO Count and Total Gross
DROP. They name 1-2 suspect ROs and say gross "isn't posting."
ROOT CAUSE (confirmed 2026-08-27, SCT / Artist Battle)
The Advisor Performance Report has a Status filter, and the store runs it onStatus = INVOICED. INVOICED and CLOSED are mutually exclusive buckets.
When accounting closes an RO, it moves INVOICED → CLOSED and silently falls out
of the report. Nothing was lost, unassigned, or mis-posted — the view is just
filtered to a bucket that drains all day.
Case data:
- 6:10 PM report: 37 ROs, labor $8,442.40, parts $3,457.33, total $11,899.73
- 6:13 PM report: 36 ROs, labor $8,165.10, parts $3,252.89, total $11,417.99
- Delta: −1 RO, −$277.30 labor, −$204.44 parts, −$481.74 total
- RO 581311 closed at 18:13:31 and its true gross = labor $277.30 / parts
$204.44 / total $481.74 — an exact penny match to the delta.
Scale of the distortion at SCT for that one advisor, August MTD:
| View | ROs | Labor GP | Parts GP | Total GP |
|---|---|---|---|---|
INVOICED only (what they see) |
31 | $7,459.45 | $2,889.71 | $10,349.16 |
CLOSED (hidden) |
195 | $53,391.66 | $19,165.45 | $72,557.11 |
| TRUE MTD | 226 | $60,851.11 | $22,055.16 | $82,906.27 |
~88% of the advisor's real gross was invisible.
THIRD failure mode: 8/26 cohort near-totally absent (2026-08-27) ✅ RESOLVED — index lag, since FIXED
After fixing Status and Pay Type View, a "Pay Type Closed Date = yesterday" run
STILL undercounted. Joe confirmed off his own screen: 19 rows = all 18 ROs that
closed 8/25, plus exactly one from 8/26 (581311). Truth was 28 ROs / $14,631.70.
Killer pair: 581233 closed 18:10, 581311 closed 18:13 — three minutes apart,
both CLOSED, both 2 invoices fully closed, same advisor, same pay types. One shows,
one doesn't. Every API date field (invoicedTime, modifiedTime, invoice created,
invoice closed) is identical in shape. Eight ROs / $2,282.01 dropped that day.
Legitimate exclusions found while checking (rule these out first):
- 581255 — status still
INVOICED; its Internal invoice never closed. Correct. - 580281 — warranty-only, excluded by Pay Type View = Customer Pay. Correct.
Still not proven a defect. Untested: the report only refreshes every 4–6 hrs and
Joe's run was 6:35 AM. Next step is Refresh + re-run, then reproduce at BT before
filing with Tekion. Suspicious shape: missing ROs cluster 08:24–10:59 while the
LATEST close of the day (18:13) is present — backwards for simple sync lag.
✅ CAUSE #3 RESOLVED 2026-08-28 — it is INDEX LAG, not a filter defect
Reproduced head-to-head at SCT. The native report is a batch-generated ES index
("Last Generated On" in the header — 3:31 AM the morning of the test) and recent
close-days backfill in over ~3 days. Aging curve (API truth vs native, same exact
RO sets, roNo IN probe with a full-year date window so the date filter can't be the
variable):
| Close day | age | API ROs | Native | % found | Missing $ |
|---|---|---|---|---|---|
| 8/20 | 8d | 18 | 18 | 100% | $0 |
| 8/24 | 4d | 257 | 257 | 100% | $88 |
| 8/25 | 3d | 270 | 270 | 100% | $0 |
| 8/26 | 2d | 134 | 102 | 76% | $10,156 |
| 8/27 | 1d | 225 | 55 | 24% | $36,905 |
Yesterday's number on the native report is ~1/4 of reality. ≥3 days old is exact.
The missing ROs are NOT clustered by pay type (INTERNAL dominates both the present and
the missing buckets in equal proportion), which kills the pay-type-filter theory.Practical rule: never read the native Advisor Performance Report for a day newer ← RETIRED 2026-08-31, see the re-test section below. Tekion fixed it.
than T-3.
Residual — CLEARED. 577056 / 580281 / 581233 (closed 8/26) were absent at T-2 on
8/28; re-probed at T-5 on 8/31 they are all present (3 ROs / $1,598.82). They
backfilled on their own, so there is no index-drop defect and no ticket to file.
✅ RE-TEST 2026-08-31 — THE LAG IS GONE. Native is now same-day accurate.
Re-ran the exact aging comparison at SCT (876) after the weekend. NativelastUpdatedTime = 8/30 23:31 PT (previously the batch was a 3:31 AM job that left
recent days starved). Every close-day 8/20–8/30 now matches the API to ~100% on RO
count, including T-1:
| Close day | age | API RO | Native RO | % | $ delta |
|---|---|---|---|---|---|
| 8/24 | 7d | 258 | 259 | 100% | +$573 |
| 8/26 | 5d | 135 | 136 | 101% | +$121 |
| 8/27 | 4d | 225 | 225 | 100% | −$162 |
| 8/28 | 3d | 239 | 239 | 100% | −$253 |
| 8/29 | 2d | 26 | 26 | 100% | $0 |
| 8/30 | 1d | 46 | 46 | 100% | +$47 |
Compare to 8/28: T-1 was 24% and T-2 was 76%. Residual ROs 577056 / 580281 /
581233 all now return present (roNo IN probe, 3 ROs / $1,598.82) — they backfilled, so
no index-drop ticket is warranted. ±1 RO / small-$ deltas are definitional (a single
RO whose pay types straddle midnight counts on both days in the API report).
Cross-checked BT (1249): native is now ≥ the API report on 8/29–8/30, i.e. not
undercounting in the other store either.
Revised practical rule (supersedes "never read newer than T-3"): the native report
is usable again for recent days, BUT it is still a batch index — always read the
"Last Generated On" timestamp in the header before trusting a same-day figure, and
re-verify the aging curve if numbers ever look light again. The API report
(advisor_closed_gross.py) remains the ground-truth reference for any dispute.
Re-testing method: hit generate-summary-report per close-day with only thepayTypeFirstClosedTime BTW filter swapped, read the TOTAL row's Ro Count
(f802dcf4-…) and Total Gross (c46b0950-…) out of reportCellList, and diff againstout/advisor_closed_gross_<store>_*.json grouped by closed_days.
How to reproduce the comparison (the method that settled it)
Capture the report's own XHR and replay it — a bare fetch 500s ("Token doesn't exist").
Shortcut (2026-08-31, faster than XHR-hooking): you don't need to capture headers by
driving the UI. Build them from localStorage in-page — this is what the axios
interceptor does anyway, and it works for generate-summary-report:
var u=JSON.parse(localStorage.getItem('t_user')||'{}');
window.__H={'Accept':'application/json, text/plain, */*','Content-Type':'application/json',
'tekion-api-token':localStorage.getItem('t_token'),
'roleId':localStorage.getItem('currentActiveRoleId'),
'userId':u.id,'tenantname':u.tenantName,
'dealerId':localStorage.getItem('currentActiveDealerId'),
'tek-siteId':'-1_'+localStorage.getItem('currentActiveDealerId'),
'original-userid':u.id,'original-tenantid':u.tenantName,'clientId':'web','locale':'en_US',
'program':'DEFAULT','applicationId':'ARC_NA','subApplicationId':'US','productIds':'ARC'};Then fetch(...,{headers:window.__H}) in-page. Cross-store without switching dealers:
copy __H and override just dealerId + tek-siteId ('-1_<id>') — verified against BT
1249 while the browser sat on SCT 876. Saved request body template lives at/tmp/native_body.json (a 1-element list; only swap [0].filters).
Stale saved header files (/tmp/tekion_rec_headers.json) expire — a 401session.expired means rebuild from localStorage, not that the endpoint changed.
- Preflight
:9223(opcode_preflight.py --dealer <ID>;--restoreafter). - Hook
XMLHttpRequestopen/send/setRequestHeader, stash any call to/api/rosearchservice/u/visibility-dashboard/generate-summary-reportwith its
headers, then SPA-nav (history.pushState+PopStateEvent) to/core/reports
and/mouse-click the report name to fire it. - Replay in-page with mutated filters: the date filter is
field:"payTypeFirstClosedTime"operator:"BTW"[startMs,endMs](Pacific).
Add{key:"roNo",field:"roNo",values:[...],operator:"IN"}to scope to an exact set.
Do NOT changegroups(replacing the group tree →unexpected.error). - Advisor IDs come back raw — resolve via
POST /visibility-dashboard/lookup/resolve-by-id{lookupByIds:[{lookUpAsset:"PRIMARY_ADVISOR_ID",ids:[...]}]}. - To find WHICH ROs are missing, recursive-bisect the RO list on the returned
Ro Count(~5 min for 225 ROs).:9223 /evaltakes{"js": ...}, not{"expression": ...}(400 otherwise), and results must be sliced ≤15,000 chars.
THE FIX
On /core/reports → Advisor Performance Report, set the Status filter to include
BOTH INVOICED and CLOSED (or clear it). Save it as the default view so the
store stops re-running the INVOICED-only version. Then the number stops "dropping."
CAUSE #2 — CVSC (and warranty) hidden by the Pay Type View dropdown
Toyota service-contract work bills to payType=CUSTOMER_PAY withsubPayType=CVSC, on a SEPARATE invoice from the plain Customer Pay one — and
the plain CP invoice is then $0.00. With Pay Type View = Customer Pay the report
reads that $0 invoice and shows zero gross on a ticket that clearly has gross.
Verified SCT 2026-08-27: RO 577046 (true GP $575.40) and RO 577056 (true GP
$1,478.50) both displayed $0. Scale for ONE advisor in one month: 60 of 226 ROs /
$11,478.92 sat in CVSC. Fix = Pay Type View → All. Tekion books this
correctly; whether CVSC should count toward advisor gross/pay plan is a business
decision for the VP, not a Tekion bug — raise it, don't assume.
Warranty-only ROs disappear the same way under a CP-filtered view.
CAUSE #3 — "Pay Type Closed Date = yesterday" silently drops ROs ✅ RESOLVED & FIXED
Was index lag; Tekion fixed it 2026-08-31. Kept for history — do not re-diagnose this.
SCT 2026-08-27, user-confirmed on his own screen (not OCR): filtering closed-date to
8/25–8/26 returned 19 ROs — all 18 that closed 8/25, but only 1 of the 10 that
closed 8/26. RO 581233 (all pay types closed 8/26 18:10) was absent while RO 581311
(closed 8/26 18:13) was present — 3 minutes apart, both CLOSED, both 2 fully-closed
invoices, same advisor. No API field distinguishes them. $2,282 of gross missing that
day. NOT yet proven a platform defect: Advisor Performance refreshes only every 4-6h
and the report was pulled 6:35 AM, so always have the user hit Refresh and re-run
before escalating, then reproduce at a second store.
THE DURABLE ANSWER — stop fighting the report, build it from the API
Joe's response to all three causes was "can you build a report for me via the API?"
That is the expected deliverable once a native report is shown unreliable. See skilltekion-advisor-closed-gross-api-report — ~/tekion-reports/advisor_closed_gross.py
keys on true per-invoice pay-type close time and counts ALL pay types, so causes #1-#3
cannot bite. It found 9 ROs / $2,763.75 on 8/26 where Tekion showed 1 / $481.74.
Diagnostic procedure (do it in this order)
- Sweep the RO# across all 7 dealers first — RO numbers are NOT unique.
Beware typos in the complaint: "851311" was really 581311 (851311 matched
3 unrelated 2019/2020 ROs at BC/BT/ST and would have sent you down a rabbit hole). - Confirm the advisor is actually still assigned:
assignee.advisor.idonrepair-orders:search. If it still resolves to the complaining advisor viaGET /users/{id}, it is NOT a reassignment/placeholder problem — skip that path. - Check
statuson each suspect RO.CLOSED+ an INVOICED-filtered report
= you already have your answer. - Match the delta to the penny. Compute the suspect RO's true gross
(laborsaleAmount−costAmount+ partssaleAmount−costAmount, all CENTS ÷100)
and compare to the difference between the two screenshots. An exact match is
proof; anything else means keep digging. - Quantify the hidden bucket (MTD INVOICED vs CLOSED gross) so the manager
sees the real magnitude, not just the one RO.
⚠️ NEVER diagnose off a SCREENSHOT transcription (burned 2026-08-27)
vision_analyze reliably MISREADS Tekion report digits. Verified errors in one image:579513→"S79613", $5,173.99→"$517.99", $3,394.19→"$394.19", $231.03→"$217.03",-$36.14→"$36.14" (sign dropped). Three "refs" I read off the image turned out to
belong to OTHER advisors and to have closed a week earlier.
Rule: use the screenshot ONLY to learn which filters/date range the manager used.
Pull every RO number and dollar figure from the API. Cross-check by matching the
report's ROW COUNT to your API set, not by matching individual OCR'd refs.
Also: "N Result(s)" on Advisor Performance = TOTAL row + advisor row(s), NOT RO count.
The real RO count is the Ro Count column.
⚠️ Do NOT declare a "Tekion defect" without a cross-store repro
On 2026-08-27 I called the Pay Type Closed Date filter defective because RO 581233
(pay types closed 8/26 18:10) was absent while 581311 (closed 8/26 18:13) was present.
That conclusion was premature — it rested on an OCR'd ref list. Before filing any
platform-defect ticket: (1) rebuild the expected set from the API, (2) have the user
read the specific RO numbers off their own screen, (3) reproduce at a second store.
Fleet-comparison-before-blaming-the-vendor is a standing rule.
ESCALATION PATH — where this ends up
Once two of the three filters are shown wrong, Joe's move is "can you build a
report for me via the API?" Don't keep diagnosing the screen — pivot totekion-advisor-closed-gross-api-report, which is immune to all three failure
modes. Anticipate this and offer it early.
Ask the user to read RO numbers off their screen
The fastest disambiguation is not more API work — it's "read me these five RO
numbers, are they present?" Joe's answer ("I have 581311 too, nothing else, 19
tickets") instantly separated the complete 8/25 cohort from the broken 8/26 one,
which no amount of my own OCR could establish.
Pitfalls
- All Tekion $ are CENTS — divide by 100 or you report $1.5M on one RO.
closedTimefilters have a broken pagination token; useinvoicedTimeBTW with
plainpaginationTokenchaining (works fine) — seetekion-openapi-repair-orders.- Do NOT accept the screenshot's transcription of the RO number as gospel; verify
against the API (documentNumberIN [...]), and read the RO# off the RO detail
screenshot itself if one was attached. - The screenshot advisor name on the RO detail view may show the CUSTOMER-facing
writer, not theassignee.advisor— trust the API field. - A large MTD fan-out (226 ROs × jobs/ops/parts) takes ~3 min; run it once and
cache. Don't loop it.
Pulling the manager's email
Stacey (email-agent) owns Gmail. Via the bridge, subprocess argument-list form
(parens/quotes in the message break the top-level terminal tool):~/bin/ask-agent stacey "<self-contained read-only request>" — tell her explicitly
DO NOT SEND, save attachments to /home/itadmin/tekion-reports/inbox/, and return
absolute paths. Inline Gmail images all arrive named image.png; she renames themimage_1..N.png.