Diagnose "the store can't see / isn't getting charged fee X" in Tekion — e.g. "SCT isn't seeing fees 1TIRE 2TIRE 3TIRE 4TIRE". Covers the Fee Management v3 API (list + per-fee detail), the ADD_TO_RO ("Allow to Add at RO Level") flag that controls whether a fee appears in the RO fee dropdown, opcode→fee attachment, and how to prove whether a fee is actually landing on real ROs. Use for any Tekion fee visibility, fee-not-auto-applying, or fee-attach-rate question.
Install
npx skillscat add joecastelino/jay-skill-pack/tekion-fee-not-showing-diagnosis Install via the SkillsCat registry.
Tekion "fee not showing / not charging" diagnosis
When to use
A store says a fee code is missing, can't be selected on an RO, or isn't being
billed. Distinct from tekion-ro-fee-waiver-investigation (that one is
"someone is deliberately zeroing a fee"). This one is config + attach-rate.
Wrong skill? If the complaint is "fee X is charging tax / shouldn't be
taxed", use tekion-fee-charging-tax-diagnosis instead — that's the new
Parts Tax Code Setup leaving pricingSetup.active[].taxConfigs empty, and it has
a one-call sweep that answers it. It also documents thepartTrade/u/sale/order/search pagination trap (page.from is IGNORED — every
page returns the same 20 rows, so multi-hundred-order scans silently lie).
Also wrong skill? If someone wants a NEW fee code created for a service-
contract deductible or hardware overage (CVSC / Fidelity / extended warranty),
STOP — use tekion-vsc-deductible-vs-fee-code. The deductible is a native
field in the job's Pay Type Split grid, bound to the contract record; a fee code
breaks claim reconciliation. It is usually already being collected on the covered
job while a $0.00 "CUSTOMER TO PAY DEDUCTABLE" placeholder job makes it look
like it isn't.
⚠️ THE #1 TRAP — /ro-fees is NOT the whole story
GET /repair-orders/{roId}/ro-fees returns only RO-LEVEL fees.
Fees attached to an opcode land at JOB level:
GET /repair-orders/{roId}/jobs/{jobId}/job-fees → data.roFees[] ← same key name!Note the response key is roFees on BOTH endpoints — easy to think you already
checked. In this session, scanning 6,000 SCT ROs with only /ro-fees produced
the confident-but-WRONG conclusion "zero CATIR tire fees have ever posted to
any RO."* Re-running the identical scan at job level showed CATIR2 on 45/48,
CATIR3 5/5, CATIR4 87/96 — the fees were working fine all along.
Rule: any "is fee X actually billing?" question requires a JOB-level scan.
Only manually-added / RO-wide fees (LYFTCONC, RENTAL, StorageFee) show on/ro-fees.
The three independent things that must all be true
| Layer | What it controls | How to read it |
|---|---|---|
1. Fee master active |
Fee exists at all | fee/v3/search → active, dealerFeeStatus |
2. ADD_TO_RO override flag |
Whether the fee appears in the RO Add/Edit Fee dropdown (manual add) | fee/v3/details → pricingSetup.active[].overrideFlags |
| 3. Opcode attachment | Whether the fee auto-applies when an opcode is billed | opcode search → fees[] / feecodes[] |
A fee can be ACTIVE and correctly priced and still be invisible to advisors
(ADD_TO_RO off) and still auto-apply fine on opcodes. Diagnose all three —
the store's complaint usually maps to exactly one.
ADD_TO_RO = KB's "Allow to Add at RO Level." Reference stores that have it
ON at SCT: CABATT, SS, StorageFee, LYFTCONC. OFF: all CATIR*,LOFDISP, TDF* (these are opcode-driven only).
Fee Management API (internal, verified SCT 876, 2026-08-20)
Page URL is /core/fees. /service/settings/fees, /core/fee-management,/fee-management, /fees all render a BLANK page or bounce to /home.
// 1) full fee list (66 fees at SCT)
POST /api/service-module/u/fee/v3/search?withNoDealers=false&locale=en_US
{pageInfo:{start:0,rows:50}, sort:[{order:'DESC',field:'modifiedTime'}],
searchText:null, filters:[]}
→ data.hits[] // paginate by bumping start by 50
// 2) per-fee full detail (REQUIRED for real overrideFlags)
POST /api/service-module/u/fee/v3/details?locale=en_US
{fees:[{feeCode:'CATIR4', id:'<id from search>'}]}
→ data[0].pricingSetup.active[].{overrideFlags, feeConfigs[].salePricingDetail, excludeOpcodes}CRITICAL: the overrideFlags field on the search result is a legacy/empty
array — it reports ADD_TO_RO=false for every fee including ones that
demonstrably have it on. You MUST call fee/v3/details and readpricingSetup.active[].overrideFlags for the truth. Same for the olderconfigs[] block — pricingSetup is the live one, configs is legacy.
salePricingDetail.flatPrice here is in DOLLARS×100? No — it is the literal
displayed amount in cents: CATIR4 flatPrice: 700 = $7.00, LOFDISP 137 =
$1.37. Cross-check against a real job-fees saleAmount (also cents) before
quoting dollars to Joe.
Dead ends (all 404): /fee/search, /fees/search, /feemanagement/*,/fee-management/search, /fee/all, /fee/list, /dealer-fee/search./fee/bootstrap returns {status, errorDetails} — useless.
Opcode→fee attachment
POST /api/service-module/u/opcode/search
{pageInfo:{start:0,rows:20}, searchText:'<OPCODE>',
sort:[{order:'DESC',field:'createdTime'}], filters:[],
nextPageToken:null, searchFields:['OPCODE']}
→ data.hits[].{fees:[{feeCode,id}], feecodes:[...]}Filter hits by exact x.opcode === CODE — search is fuzzy and returns cousins.GET /api/service-module/u/opcode/{ID}_{dealerId} and /opcode/details/* both
500 — search is the only read path.
Auth (window.__H) — rebuild after EVERY hard nav
The XHR-hook approach to capture headers does NOT survive /navigate. Build
headers straight from localStorage instead; re-run this after each navigation:
(()=>{const g=k=>localStorage.getItem(k);window.__H={
"Accept":"application/json, text/plain, */*","applicationId":"ARC_NA","clientId":"web",
"dealerId":g('currentActiveDealerId'),"locale":"en_US",
"original-tenantid":"americanmotorscorporation","original-userid":g('__user_id'),
"productIds":"ARC","program":"DEFAULT","roleId":g('currentActiveRoleId'),
"subApplicationId":"US","tek-siteId":g('currentActiveSiteId'),
"tekion-api-token":g('t_token'),"tenantname":"americanmotorscorporation",
"userId":g('__user_id'),"Content-Type":"application/json"};return 'ok'})()Then call the APIs with in-page fetch(..., {headers:window.__H}). Verifywindow.__H.dealerId matches the target store before trusting any result.
Method
- Pull the full fee list → find every fee whose code/description matches the
complaint. Expect legacy duplicates: at SCT the codes literally named1TIRE/2TIRE/4TIRE("CALIFORNIA TIRE FEE") are INACTIVE (killed 9/16/2022 at
CDK conversion) and were replaced byCATIR1–4("CA Tire Tax"). Same pattern:TIRDIS1-4→TDF1-4,TD→TDF*. The store often complains using the
dead code's name. Always list active + inactive and show the mapping. - Pull
fee/v3/detailson the live codes → readADD_TO_RO, price,excludeOpcodes, pay-type eligibility filters. - Read opcode attachment for every plausible opcode (don't assume — at SCT
1TIRE=mount&balance-1 andTIRE1=replace-1-tire are DIFFERENT opcodes and
only one had the fee). - Prove it against real ROs with a JOB-LEVEL scan (see trap above).
ThreadPoolExecutor(12-14), ~65s for 262 candidate ROs. Report an attach
rate per opcode, not a yes/no. - Bucket misses by RO number / date to distinguish "always broken" from
"broke recently."
Worked example — SCT tire fees, 2026-08-20
Complaint: "SCT isn't seeing fees 1tire, 2tire, 3tire, 4tire."
- Codes
1TIRE/2TIRE/4TIRE= INACTIVE legacy; no3TIREfee ever existed.
Live =CATIR1$1.75 /CATIR2$3.50 /CATIR3$5.25 /CATIR4$7.00, all ACTIVE. - Root cause of "can't see them": ADD_TO_RO is OFF on all four CATIR fees →
they never appear in the RO Add/Edit Fee dropdown. (CABATT/SS/StorageFee have
it ON, which is why those are visible.) - Bonus finding from the job-level scan: opcode
1TIREhas no fee attached
(fees:[]) while 2TIRE→CATIR2, 3TIRE→CATIR3, 4TIRE→CATIR4, TIRE1→CATIR1 all
are. Attach rates: 1TIRE 5/112, 2TIRE 45/48, 3TIRE 5/5, 4TIRE 87/96,
TIRE1 3/4. ~107 uncollected CA tire fees — state-mandated pass-through, so an
audit exposure, not just revenue. - Tire disposal is billed as a part line (
TDF, $20 on the operation's
parts), NOT as a fee — don't confuse it with theTDF*fee codes. - Proposed fix: (1) turn ON "Allow to Add at RO Level" on CATIR1-4;
(2) attach CATIR1 to opcode1TIRE.
Misc gotchas
/screenshoton the:9223server returns JSON{"screenshot": "<base64>"},
not raw bytes — decode before writing, orvision_analyzerejects the file
with "Only real image files are supported."/evalpayload key isjs, notexpression. A syntax error or a
bare-return-at-top-level returns HTTP 500 from the server (not a JS
error) — always wrap in(()=>{...})()or(async()=>{...})().document.body.innerTexton many Tekion settings pages returns only the
nav chrome (~350 chars) even when the page is fully rendered — the content is
in canvas/shadow subtrees. Don't conclude "page is blank" from innerText;
screenshot +vision_analyzeto confirm.- Recovering a lost internal endpoint:
grep -ohE "api/[A-Za-z0-9/_.:-]*fee[A-Za-z0-9/_.:-]*"
over~/.hermes/profiles/jay/sessions/session_*.json— past XHR-hook captures
are stored verbatim in the transcripts. This is howfee/v3/searchandfee/v3/detailswere recovered after a dozen guessed 404s. Do this FIRST
next time instead of guessing endpoint names.