Pull Tekion's DAY COLLECTION report (cashier collections / money actually collected per day, by payment mode + department + cashier) for any AMG store and any date(s) via the internal cashier API, then render a per-day branded PDF and email it to Joe. Replaces reading the Day Collection screen by hand.
Install
npx skillscat add joecastelino/jay-skill-pack/tekion-day-collection-report Install via the SkillsCat registry.
Tekion Day Collection Report (API + PDF)
What it answers: "What did we collect on ?" — total closing amount,
split by payment mode (card / cash / check / cashier's check / charge / other),
by department (Service / Parts / Vehicle Sales), by asset type (RO / Parts Sale /
Vehicle Sale), by card brand, by cashier ("Received by"), plus refunds/reversals
and full transaction detail.
The Tekion UI screen is DC Day Collection → /ro/invoices/day-collection
(App Grid, Services group). One day at a time, no export worth using — so pull it
via API.
THE ENDPOINTS (internal, cracked 2026-08-31 at SCT)
All POST, all in-page from an authenticated persistent browser (?allCashiers=true):
| Endpoint | Purpose |
|---|---|
/api/cashier/u/api/transactions?allCashiers=true |
the transaction rows (paginate pageInfo.start/rows) |
/api/cashier/u/api/summaryByType?allCashiers=true |
tender-type totals — use to reconcile |
/api/cashier/u/api/summaryByCard?allCashiers=true |
card-brand totals (visa/mc/amex/discover) |
/api/lookup/ids body {"TENANT_USER_MINIMALV2":{"ids":[...]}} |
cashierId → display name |
Request body shape (transactions):
{"sort":[{"key":"sortKey","field":"invoiceNumber","order":"DESC"}],
"pageInfo":{"start":0,"rows":50},
"filters":[{"field":"status","key":"status","operator":"NIN","values":[
"CREATED","IN_PROGRESS","FAILED","PENDING","VOIDED","CANCELLED",
"WAITING_FOR_SWIPE","EXPIRED","REQUESTED","PENDING_APPROVAL","REJECTED"]}],
"date": <epoch_ms local midnight>, "endDate": <epoch_ms 23:59:59.999>,
"cashierIds":[]}summaryByType / summaryByCard take the same date/endDate/filters (no sort/pageInfo).
⚠️ THE MONEY-FIELD TRAP (cost 3 debug rounds — read this)
Each row has THREE amount fields:
amount.amount✅ USE THIS — cents, already signed for refunds.effectiveTransactionAmount.amount❌ refunds come back positive here, and
partial-capture rows differ (e.g. 332878: amount 53.86 vs effective 9.79).dueAmountBeforeTransaction— informational only.
Only amount.amount reconciles exactly to Tekion's own summaryByType
totals. My first four sign-handling theories (refund flag, refundId,
neg-if-positive) all mismatched by $44–$2,100/day; plain amount.amount matched
to the cent on all 3 days. All amounts are CENTS.
refund:true rows already carry a negative amount — do NOT negate again.
RUN IT
# 1) pull (browser must be logged in AND already on the target dealer)
python3 /home/itadmin/tekion-reports/day_collection_pull.py \
--port 9225 --dealer 876 \
--dates 2026-08-28,2026-08-29,2026-08-30 \
--out /tmp/sct_daycoll.json --names /tmp/sct_names.json
# 2) render one PDF per day
cd /home/itadmin/tekion-reports && python3 render_day_collection.py ST 'Stevens Creek Toyota'
# -> out/ST-DayCollection-Fri_8-28.pdf etc.
# 3) email (jay_mail.send_report; SMTP + positively verified)day_collection_pull.py asserts currentActiveDealerId == --dealer before
pulling (guards against the silent wrong-store read) and printsRECONCILED / MISMATCH per day against summaryByType. Never ship the report
without a RECONCILED line.
Dealer IDs: AR=6195, BC=1251, BT=1249, ST/SCT=876, SV=826, TL=1092, VC=1891.
GETTING AN AUTHENTICATED PORT
The 1:16AM cron-tekion.sh Caliber scraper owns :9223 for ~12h and drifts the
dealer — pgrep -af 'cron-tekion.sh|run-scraper' first. If it's live, use :9225
and clone the live session over (fastest re-auth, no OTP):
ls = eval9223("JSON.stringify(Object.fromEntries(Object.entries(localStorage)))")
inject = {k:v for k,v in json.loads(ls).items() if not k.startswith("amplitude")} # amplitude_* is ~5MB -> 413
post9225("/navigate", {"url":"https://app.tekioncloud.com/login"}) # establish origin
for k,v in inject.items(): post9225("/eval", {"js": f"localStorage.setItem({json.dumps(k)},{json.dumps(v)});'ok'"})
post9225("/navigate", {"url":"https://app.tekioncloud.com/home"})Then UI-switch dealer: /mouse the dealer pill at ~(1130,32), wait 3s,scrollIntoView the [class*="root_dealerInfoItem_container"] row, re-read its
rect, /mouse the fresh center, wait ~10s, verify currentActiveDealerId flipped.
HOW THE ENDPOINTS WERE FOUND (reusable recon)
Bare in-page fetch('/api/...') normally 500s "Token doesn't exist" — but for
these cashier endpoints a hand-built header set works, because they only need
the standard header block:
{'tekion-api-token': localStorage.t_token, 'roleId': currentActiveRoleId,
'userId': __user_id, 'tenantname':'americanmotorscorporation',
'dealerId': currentActiveDealerId, 'tek-siteId': currentActiveSiteId,
'clientId':'web','locale':'en_US','program':'DEFAULT',
'applicationId':'ARC_NA','subApplicationId':'US','productIds':'ARC'}Discovery path: arm an XHR hook (also capture setRequestHeader intothis.__hdr — that's how you learn the required header set), then click the
page's own Reset button to refire the search, then read window.__H.
Fetch-only hooks MISS these — they're XHR.
/eval truncates ~20000 chars → stash the response in window.__R and pull it
back in 15000-char slices.
RE-SUMMARIZING FROM THE SAVED JSON (⚠️ shape trap)
Once day_collection_pull.py has written /tmp/<store>_daycoll.json, the savedbyType is a dict, NOT the list-of-buckets the live API returns. The
reconciliation comprehension in this skill only works on the live response:
# LIVE response only:
{b["key"].upper(): b["projections"]["sumByTenderType"]/100 for b in v["byType"]}
# From the SAVED file this raises: TypeError: string indices must be integersTo re-summarize from the saved file (e.g. Joe asks for the numbers again in chat),
sum the rows instead — same answer, no shape assumptions:
import json
from collections import defaultdict
d = json.load(open('/tmp/sct_daycoll.json'))
for k, v in d.items():
rows = v['rows']
tot = sum(r['amount']['amount'] for r in rows)/100
fo = sum(r['amount']['amount'] for r in rows
if r.get('departmentType') in ('SERVICE','PARTS'))/100
vs = sum(r['amount']['amount'] for r in rows
if r.get('departmentType') == 'VEHICLE_SALES')/100
bt = defaultdict(float)
for r in rows:
bt[r.get('tenderType') or r.get('paymentMode') or '?'] += r['amount']['amount']/100
print(k, len(rows), tot, fo, vs, dict(bt))THE ANSWER SHAPE JOE WANTS
PDFs go by email (Slack can't take PDFs); post page-1 PNGs to Slack withMEDIA: lines alongside a table. Give him, per day: Txns · Total Collected ·
Fixed Ops (Svc+Parts) · Vehicle Sales, a 3-day total row, then the tender
breakdown per day. Always call out that vehicle sales dominate (~90% at SCT) and
offer the fixed-ops-only rerun + a daily cron — don't wait to be asked.
PITFALLS
- 216 of 273 Fri rows had
assetNumber: null(counter parts sales) — fall back toinvoiceNumberfor the display invoice #. - Customer name:
searchDetail.customerName, fall back topayeeDetails.name. - Times are epoch ms; render in Pacific.
departmentTypeis the useful grouping (SERVICE/PARTS/VEHICLE_SALES);departmentIdis unresolvable (DEPARTMENTis not a valid/api/lookup/idskey,
and/api/dealer/u/departments404s).- Day Collection includes Vehicle Sales — it dwarfs fixed ops (SCT 8/28:
$344K of $404K). If Joe wants fixed-ops-only collections, filterdepartmentType IN (SERVICE, PARTS)and say so on the report. - Report logo: SCT is the only store with a real logo asset (
logo_st.png);
everything else gets a text wordmark.logo_0.pngis ALSO Stevens Creek Toyota
despite the generic name — never use it for another store. vision_analyze the
first page before emailing. - No
pdftoppm/pdf2imageon this box; QA a PDF viapython3 /home/itadmin/tekion-reports/_png.py <pdf>(PyMuPDF →/tmp/dcpN.png),
thenvision_analyze. Plainimport fitzinside a terminal heredoc can blow up
oninspect.signature— run it from a file, not a heredoc. _png.pyoverwrites/tmp/dcpN.pngon every run. If you want a page-1
thumbnail per day for Slack, copydcp0.pngto a distinct name
(/tmp/dc_fri.pngetc.) immediately after each render, before the next PDF.- Jay's
memorystore is at its char cap — none of this lives in memory.
This skill is the ONLY record of the Day Collection method. Don't assume a
memory entry exists; load this skill.
FILES
/home/itadmin/tekion-reports/day_collection_pull.py— pull + reconcile/home/itadmin/tekion-reports/render_day_collection.py— per-day PDF (landscape letter)/home/itadmin/tekion-reports/_png.py— PDF→PNG for vision QA
Related
tekion-standard-reports-performance, tekion-part-sales-ledger-report,persistent-browser-server, jay-gmail-draft-verification