Imported from jonchoi17/cork-and-barrel-claude (
skills/daily-sales/SKILL.md). Install upstream withnpx skills add jonchoi17/cork-and-barrel-claude --skill daily-sales. Copyright stays with the author.
Daily Sales — Cork and Barrel NYC
⛔ ABSOLUTE RULES
Read these every run. They override everything else.
-
NO FREESTYLING. Same steps, same order, every run. If the rule isn't here, ASK before deviating. Do not invent shortcuts, alternate paths, or "close enough" approximations.
-
PARALLEL OR NOTHING. Every independent step runs in parallel. Open all platform tabs in one Chrome batch (one assistant message, multiple
navigate/tabs_create_mcpcalls — not one tab at a time), scrape ALL pages in one batch ofjavascript_tool/get_page_textcalls (one message, N tool calls), write all Excel files in parallel. Combined with an invoices session: daily-sales scrape and invoice PDF reads can also fire in the same parallel batch (different tools, both independent). Never serialize what can be parallelized. A scrape-and-run cycle for a single day should be ≤ 5 minutes of wall time. -
SCRAPE FIRST, SCREENSHOT NEVER (in chat). Use
javascript_tool/get_page_text/read_pagefor data. Screenshots may be used INTERNALLY only when scraping fails — but never share screenshot images in chat output. The user explicitly does not want to see them. If you cannot extract data, say so in text. Do not paste page renders. -
OUTPUT = FINAL TABLE ONLY. No interim narration ("now scraping UberEats…"), no screenshot dumps, no progress chatter, no "✓ done" between steps. The deliverable is the verification table. Anything that doesn't make the answer better gets cut.
-
FLAG = LS-CORRECT vs PLATFORM diff > $1.00. Always show the diff. Always write the platform value to Excel (platform = ground truth per [feedback_dd_platform_truth.md]). Label that row FLAG ⚠️, not CORRECTED. CORRECTED is reserved for user-confirmed employee fixes only.
-
DOUBLE-CHECK MANDATORY. Before reporting done: re-scrape every platform fresh (paginate fully), re-read every Excel value, confirm match. If anything differs, fix and re-check.
-
DISCREPANCIES ARE YOUR FAULT. Until a fresh re-scrape proves otherwise. Never blame "employee error" or "LS changed" without proof.
-
WHEN USER PREDICTS A DISCREPANCY THAT ISN'T THERE: tell them, don't FLAG. If they said "DD might be off by ~$X" and your scrape shows it isn't, just report "checked, no discrepancy on DD" — don't manufacture a flag.
-
⛔ DD EXCEL COL H = SUBTOTAL, SUM ALWAYS MATCHES LS. Col H of
Door Dash Transactions.xlsxis per-order subtotal (pre-fee). The SUM of col H across a day's rows MUST equal the LS DoorDash value for that day. There is no legitimate reason for divergence — LS rings up DD subtotals and that's the only source for the LS DD field. If your scrape only gave you net amounts (DD UI now hides per-row subtotal in the 7-day list view), distribute the day's commission proportionally so the col-H SUM hits LS exactly:subtotal_i = net_i × (LS_subtotal_sum / sum_of_nets). Recompute col L (commission) fromH − Tafter distribution. NEVER write col H values that don't sum to LS. (User directive 2026-05-18 after $3.67 drift on 5/17.) -
⛔ ADOPT THE EXISTING TAB GROUP — REUSE ACROSS DAYS, NEVER SPAWN FRESH (updated 2026-07-05). The MCP tab group PERSISTS across conversations/days as long as its tabs stay open, and
tabs_context_mcpreturns that existing group. Do NOT blindly create 6 new tabs each run. Procedure at the START of every run:- (a) Inspect first. Call
tabs_context_mcpwith no args (orcreateIfEmpty:false). If it returns a group that already has tabs, THAT is your workspace — do not open a new window/group. Only passcreateIfEmpty:truewhen there is genuinely no group at all. - (b) Build a domain→tabId map from the returned tabs. Match each platform by domain substring:
ubereats.com→UB,doordash.com→DD,grubhub.com→GH,minibardelivery.com→MB,dashboard.instacart.com→IC,cityhive.net→CH,lightspeedapp.com/merchantos→LS. - (c) Reuse, then fill gaps. For each platform: if a matching tab exists, REUSE it via
navigateto the dated URL. If not, first consume any spare blank tab (chrome://newtab/"New Tab"); only calltabs_create_mcpwhen no blank tab is left. NEVER create a second tab for a domain that already has one. - (d) Do NOT close tabs at end of run. No
tabs_close_mcpon platform tabs — leaving them open is exactly what lets the NEXT day's run reuse them. Close only genuine duplicates. - (e) Wrong-browser fallback. If the MCP group is empty/fresh but the user says their platform tabs are already open in Chrome, those tabs are in a DIFFERENT connected browser instance. Call
list_connected_browsers→select_browser(orswitch_browserto let the user pick) to attach to the browser that has them, then redo (a). Do not just spawn new tabs.
This supersedes the old "within a conversation" scope. Goal: ONE durable set of platform tabs, reused every day, minimal Chrome clutter.
- (a) Inspect first. Call
-
⛔ CH ROLLOVER INCLUDES THE TIP (2026-05-21 incident). When a CityHive post-9PM order from the previous day rolls to today's sales day, its tip rolls with it. Do NOT compute D3 as "calendar-date only" tips and do NOT subtract the rollover tip from D3. The correct D3 = sum of (prev_day_post9 + target_day_pre9) tips, non-OOB, non-Uber. The script's
sales_day_orders = prev_post9 + target_pre9aggregation is correct — never "fix" it to calendar-only. K3 (LS Tips diff) is the truth check: with rollover-included D3, K3 matches LS within ~$0.01. Without rollover, K3 diverges by ~$50+ — that divergence is the signal that D3 is wrong, not that LS is wrong. See [feedback_ch_rollover_includes_tip.md]. -
⛔ RE-FETCH LS FRESH BEFORE FINAL TABLE (2026-05-21 incident). The script's LS snapshot can be stale by 5–30 minutes. Voids, refunds, and returns happen between script run and final report. Before presenting the final verification table, re-run the LS pull (CC, payments, tips, COGS, sale count) and update Excel if anything drifted. If sale count dropped → a sale was voided → adjust B2 (CC) and B11 (COGS) at minimum. NEVER trust the script's snapshot as the final answer. See [feedback_ls_refetch_before_final.md].
-
⛔ UNFULFILLED PLATFORM ORDERS = OPERATIONAL FLAG (2026-05-21 incident). If a UB/DD/GH order shows status "Unfulfilled" or "Cancelled" on the platform AND its amount equals the LS-vs-portal-gross difference, do NOT mark it ✅ EXACT just because LS-completed-only matches. Mark it ⚠️ FLAG (unfulfilled $X.XX) with the order ID. Unfulfilled = lost revenue + operational follow-up with the platform. Always surface to the user, never silently dismiss. See [feedback_unfulfilled_orders_flag.md].
-
⛔ GH CANCEL/RE-ORDER TIP DRIFT → USE PORTAL VALUE (2026-05-21 incident). When a GrubHub order is cancelled and re-placed at a different tip (e.g., 7:17 PM $5.99 tip cancelled → 7:23 PM $8.98 tip re-order), LS often captures the original tip but misses the update. Trust the GH PORTAL: G2 = scraped (subtotal + tip) of the valid orders (i.e., net the cancellation/replacement pair). Mark 🔧 CORRECTED with note "LS missed tip update on cancel/re-order pair." Do NOT leave G2 = LS value. See [feedback_gh_portal_overrides_ls.md].
-
⛔ K3 = LS TIPS, EXACTLY. B3 NEVER FLOORS (2026-05-26 incident). K3 (SUM of row 3 tips) must equal LS Tips total to the penny. After all platform tips are written (D3 CH, E3 UB, G3 GH, H3 MB), compute B3 = LS_Tips − D3 − E3 − G3 − H3 and write it AS-IS (decimal allowed, round to 2dp to dodge float artifacts). NEVER floor B3. If K3 ≠ LS Tips, B3 is wrong. The script handles this — do not "improve" B3 manually after the script runs. See [feedback_k3_b3_no_floor.md].
-
⛔ C6 EXCLUDES B3 — READ THE ACTUAL CELL, DO NOT RE-DERIVE (2026-05-26 incident). The C6 cell formula in
Daily Sales Calculations.xlsxis=(D3+G3+H3)*0.9— B3 (CC tips) is NOT in C6. CC tips don't pay out in cash to employees; only online tips × 0.9 do. B17 = floor(C6). Tip Report online tips = floor(C6) (same value, not a separate calc). Always read C6's actual cell formula before plugging anything anywhere. Never re-derive from documentation or memory — the cell is the source of truth. I2 = B16 − B17 − B19. See [feedback_c6_excludes_b3.md]. -
⛔ CASH PROVIDED LATER — NEVER BLOCK ON CASH (2026-05-28 user directive). When the user kicks off a daily-sales run WITHOUT a cash amount, do NOT pause, do NOT ask, do NOT wait. Run the script without
--cash, complete every other step (LS pull, platform scrapes, Excel writes, Tip Report, verification table), and finish the run as if cash will be provided later. Surface a single one-line reminder near the end of the run ("Cash for {date} still needed — drop the amount and I'll re-run with --cash"). When the user later provides cash, re-run with--cash AMOUNTto fill B16/B17/B19/I2. This rule supersedes any tendency to "be helpful" by asking up front. The user's workflow: kick off the run while still counting cash; cash arrives minutes later. -
⛔ VERIFY THE B17 SCRIPT OUTPUT MATCHES
floor(C6)(2026-05-28 incident). Older copies ofdaily_sales.pycomputedc6 = (b3+d3+g3+h3)*0.9instead of(d3+g3+h3)*0.9— producing a B17 that's too high (and a wrong I2 cash calc). Canonical script is correct. If you find B17 ≠ floor(actual C6 cell value) after a script run, the running copy is stale → fix the running script, then patch B17 and recompute I2 = B16 − B17 − B19. Then verify Daily Sales Calculations.xlsx matches. See [feedback_b17_floor_c6.md]. -
⛔ DOORDASH ROWS NEED FULL PER-ORDER DATA — Col A (Transaction ID) + Col L (commission) ALWAYS (2026-06-02 incident, recurring). Every DD row written to
Door Dash Transactions.xlsxMUST have: Col A = Transaction ID (long numeric, e.g.17329704539), Col L = commission (negative, shows red), plus Col R/S for any customer discount / marketing credit. Two failure modes that recurred across days: (a) Col A left blank — the script used to hard-codeNonethere (FIXED 2026-06-02; now writestx_id); (b) Col L = 0 with no red — caused by a subtotal-only scrape that never captured per-order commission (5/29 & 5/30 hadL=0, net was right soH+L≠Tsilently). Never use the old subtotal-only DD scraper. Scrape full per-order columns and paginate all pages (§1C). After the script runs it now prints a "DoorDash row validation FAILED" warning if any row has a blank tx_id or a dropped commission (net≠subtotalbutcommission=0) — if you see that warning, re-scrape and refill A/L/R/S before reporting done. Verify visually: every DD day must show red in the L column. See [feedback_dd_full_per_order_columns.md]. -
⛔ KEEP CANONICAL AND SESSION-SKILLS COPIES IN SYNC (2026-05-28 incident). Skill scripts and
SKILL.mdlive in TWO places: canonical iCloud (~/Library/Mobile Documents/com~apple~CloudDocs/Shared invoices/claude code skills/skills/{name}/) and session-skills cache (~/Library/Application Support/Claude/local-agent-mode-sessions/skills-plugin/{uuid}/{uuid}/skills/{name}/). The session-skills copy is what the skill harness LOADS each run; the canonical iCloud copy is what the user EDITS. They can drift. After ANY edit to canonical, immediatelycpthe same file to the session-skills location AND verify md5 match. Same for any script fix. Do this before reporting "skill updated." -
⛔ NEVER DELETE A VALID CITYHIVE ORDER — FLAG AND ASK (2026-06-11 user directive, after 6/9 incident). A CH order that is non-OOB and fulfilled must NEVER be deleted from the CityHive Transactions log, no matter what discrepancy it explains. On 6/9 the last order of the day (8:59 PM, $47.53) wasn't rung in LS because the store closed at 8:59 — the prior session DELETED it to force reconciliation. Wrong. Correct handling: FLAG it, ask the user; they will say roll it to the NEXT sales day (it gets rung next morning). A store-close rollover behaves exactly like a post-9PM rollover: row stays with its real timestamp at the end of its calendar day's block, EXCLUDED from that day's SUM, INCLUDED in the next day's SUM range, and its tip rolls with it into next day's D3 (rule 11). Only "declined" status orders get the OOB timestamp-only treatment.
-
TAX/TIP RING RULES (user 2026-06-11): GH, DD, UB, and MB sales are rung in LS WITHOUT tax — tax is handled separately by each platform. Only direct LS sales and CityHive orders include the TIP in the rung amount. An LS-vs-portal overage of exactly subtotal×1.08875 means an employee rang the tax-included amount (e.g. DD 8EDC822E on 6/10: $75.09 rung vs $68.97 subtotal).
-
⛔ NEVER HALT THE RUN MID-PROCESS FOR A DISCREPANCY — BATCH QUESTIONS TO THE END (2026-06-16 user directive). When a flag/ambiguity comes up (CH mismatch, scheduled/voided order, platform gap, etc.), do NOT stop and ask immediately. Diagnose it, park it, and KEEP GOING — finish every other independent step (all platforms, every Excel write, the tip split, the full verification table) using best-effort/placeholder values where needed. Then surface ALL open questions together in ONE consolidated "needs your input" list AFTER the final table, each with the diagnosis already done and a note on what changes per answer. On 2026-06-16 I halted the whole 6/15 run for a single CityHive scheduled-order question before finishing the rest — the user does not want that. Extends rule 17 (don't block on cash). When the user answers, apply and proceed (don't re-gate).
-
CITYHIVE "scheduled" STATE = FUTURE ORDER, NOT IN LS YET (2026-06-16 incident). CityHive orders can have
multiple_product_order.state == "scheduled"(future delivery).daily_sales.pyexcludes onlyis_oob(manual://) andis_declined— it does NOT handle "scheduled", so it counts the scheduled order's total + tip in the CH platform sum, producing: CH platform > LS by exactly that order's amount, B3 negative, K3 ≠ LS by the scheduled tip. When a CH FLAG's gap matches a single order, query the CH API and checkstate. If "scheduled" it is NOT an error or missed ring — the order will ring in LS when fulfilled (watch for a double-count that day). Per rule 23, park it and ask at the end (default recommendation: exclude until fulfilled; it's the user's call). On 6/15 the user chose include all: D2 = CH platform value (not LS), the scheduled tip parked in E3 (UB) so it stays OUT of the employee payout C6=(D3+G3+H3)×0.9, B3 = true CC tip, so K3 exceeds LS by the scheduled tip by design. -
⛔ LOGGED-OUT IS NEVER A BARRIER — AUTO-RE-LOGIN THE LS WEB UI (2026-07-12 user directive). A logged-out automation browser must NEVER become an OPEN ITEM or a reason a value is skipped. If the LS web UI (
corkandbarrelnyc.retail.lightspeed.app) reads as logged-out when you go for the Sales Reports → Customer → Instacart cross-check, log back in automatically and retry — do not defer it, do not surface it as "needs your input." Credentials live inscripts/.envasLS_WEB_USERNAME/LS_WEB_PASSWORD(this is the WEB login, separate fromLS_PERSONAL_TOKEN). Login flow: navigate tohttps://corkandbarrelnyc.retail.lightspeed.app/; if it shows the login screen, read the two env vars (python3 -c "import os; from dotenv import load_dotenv; load_dotenv('scripts/.env'); print(os.environ['LS_WEB_USERNAME']); print(os.environ['LS_WEB_PASSWORD'])"— one simple command, no compound&&), then fill the email + password fields via Chromeform_inputand submit; wait for the dashboard, then proceed to Reports → Sales. ⛔ Usecorkandbarrelnyc.retail.lightspeed.app, NEVERmerchantos.com(that host reads as logged-out even when the session is fine — see [reference_ls_login_url.md]). This cross-check is SECONDARY: every written value already comes from the LS API token, and Instacart col D comes from the GMV tlog — so if login still can't complete after a genuine attempt, finish the run clean and note it as a one-line minor footnote (NOT an OPEN ITEM). See [feedback_ls_web_autologin.md]. -
⛔ INSTACART TLOG IS AUTO-POSTED ~T+1 — DOWNLOAD IT WHEN PRESENT, ELSE DEFER COL D (corrected 2026-07-13 after 7/12 incident; supersedes the 2026-07-12 "generate on demand" directive). The
tlogs.v2.{DATE}transaction-log report is auto-generated by Instacart on a ~T+1 schedule (e.g. the 7/12 tlog file was created 7/13). It CANNOT be generated on demand — verified 2026-07-13: the IPP transaction-logs page has no Create/New-report control, and the only "Create report" builder is the separate Insights/analytics dataset (the lagging GMV that returns 0/partial for the run day), NOT the raw tlog. Correct handling: at the reports list (https://dashboard.instacart.com/partners/3761/warehouses/1798/reports/transaction-logs), if therpt.4096*tlogs*{DATE}*.csvrow is present, download it to~/Downloadsand run the script so col D fills this run. If it is not present yet (the run fired before the tlog posted), you cannot force it — do NOT fabricate col D from LS or the lagging Insights GMV. Leave col D blank, note it as an OPEN ITEM, and it fills automatically on any later re-run once the tlog posts (the script auto-globs~/Downloads). The nightly routine now fires at 6 AM ET specifically so the prior day's tlog is normally already posted. Fallback only if col D is needed before the tlog posts AND the In-store transactions report (rpt.4094*in_store_transactions*{DATE}) IS present: sum itstransaction_amt(= GMV/1.1 = col D directly). (History: the 2026-07-12 directive assumed on-demand tlog generation was possible; the 7/12 unattended run at 1 AM ET proved it is not — no generator exists and the tlog had not yet posted — so this rule was corrected and the run moved to 6 AM.) -
⛔ CITYHIVE LOGGED-OUT IS NEVER A BARRIER — AUTO-RE-LOGIN, AND NEVER SHIP D3 = $0 (2026-07-22 user directive, after the 7/21 incident). CityHive credentials now live in
scripts/.envasCITYHIVE_USERNAME/CITYHIVE_PASSWORD.daily_sales.pyre-authenticates itself viaPOST https://api.cityhive.net/api/v1/users/sign_inwith body{"user":{"email":…,"password":…}}, which returnsresult: 0and sets a fresh_HiveNet_sessioncookie (_try_ch_login()→_ch_reauth(), verified 2026-07-22). This is the CityHive analogue of ABSOLUTE RULE 25 — a logged-out CityHive must never be an OPEN ITEM.- The silent-failure shape to watch for: an expired session does NOT return 401. The orders API answers HTTP 200 with
{"result":2,"description":"You must be logged in to perform this action."}and nodatakey, so a dead session looks exactly like a day with zero CityHive orders. The script then writes D3 = $0.00 and dumps the entire CityHive tip into B3. K3 still equals LS Tips, so the usual truth check does NOT catch it — butC6 = (D3+G3+H3)*0.9collapses and the Tip Report underpays badly (7/21: $22 instead of $89). - Therefore: if LS CityHive (D2) is non-zero but CH tips (D3) come back $0.00, treat it as a failed fetch, not a tipless day. Re-auth and re-query before writing anything.
- Do not stop at the first re-auth strategy that "succeeds."
_try_refresh_ch_cookie()(pycookiecheat from Chrome) returns True whenever it finds any cookie — including the same dead one. The retry loop must fall through to a real credential login and re-query after each strategy. That was the exact 7/21 failure mode. - Last-resort derivation if CityHive is genuinely unreachable: split LS tips by payment type — page
/api/register_sales?page_size=200&status=CLOSED, convertsale_dateUTC→ET, filter to the date, and sum each sale's "tip" products keyed byregister_sale_payments[].name. That yields D3 (CityHive), G3 (GrubHub) and B3 (Lightspeed Credit Card) directly, summing to LS Tips exactly. Cross-check G3 against the GrubHub portal scrape. This recovers tips only — not CityHive order rows, soCItyHive Transactions.xlsxstays unwritten and D2 stays UNVERIFIED. Prefer a real login every time.
- The silent-failure shape to watch for: an expired session does NOT return 401. The orders API answers HTTP 200 with
STEP 0 — INPUTS (extract from user message)
| User said | Argument |
|---|---|
| Specific date(s) | --date YYYY-MM-DD |
| Multiple days | run once per day |
| Cash $ amount | --cash AMOUNT |
| Cash tips | apply only to Tip Report column D, never B16 |
| Nothing about cash | omit --cash; proceed with everything else; ONE-line reminder near end (rule 17) |
| Employees worked | DERIVE from schedule below — do NOT ask |
Standard employee schedule (do NOT ask, only respect overrides):
| Day | Employees |
|---|---|
| Monday | Carlos, Wendy |
| Tuesday | Carlos, Sonam |
| Wednesday | Sonam, Wendy |
| Thursday | Sonam, Wendy |
| Friday | Carlos, Sonam |
| Saturday | Carlos, Sonam |
| Sunday | Carlos (alone) |
STEP 1 — PARALLEL SCRAPE PHASE
One assistant message. All these steps run in parallel. Do not serialize.
1A. Setup check (only if first run of the day)
ls scripts/.env >/dev/null 2>&1 && python3 -c "import requests, openpyxl, dotenv, pycookiecheat" 2>&1 && echo "OK"
If missing → tell user, abort.
1B. Open all browser tabs in parallel
In ONE browser_batch, navigate every platform. Use the literal URLs below — date params get substituted.
| Platform | URL |
|---|---|
| UberEats | https://merchants.ubereats.com/manager/payments/payouts-by-order?restaurantUUID=fce5efc1-37f6-47a6-b898-3dfeab4e0499&start={DATE}&end={DATE}&rangeType=1 |
| DoorDash | https://www.doordash.com/merchant/financials/transactions?store_id=1898885&start_date={DATE}&end_date={DATE} (URL params don't always stick — be ready to set custom range via calendar) |
| GrubHub | https://restaurant.grubhub.com/financials/transactions/2886031 (filter rows in JS by date) |
| Minibar | https://minibardelivery.com/services/supplier/orders/completed (no orders → MB=$0) |
| Instacart GMV (PRIMARY) | Transaction Logs report — https://dashboard.instacart.com/partners/3761/warehouses/1798/reports/transaction-logs → find the rpt.4096...tlogs.v2.{DATE}...csv row → Download it yourself to ~/Downloads (fresh, from the portal — never reuse a stale Downloads file, never rely on the user). ⛔ The tlog is AUTO-POSTED by Instacart ~T+1 and CANNOT be generated on demand — the IPP has no tlog generator (ABSOLUTE RULE 26). If the rpt.4096...tlogs.v2.{DATE}...csv row is present, download it. If it is not posted yet, leave col D blank as an OPEN ITEM — do NOT fabricate it — and it fills on any later re-run once the tlog posts (the 6 AM ET run timing is chosen so the prior day's tlog is normally already posted). The script then auto-consumes it: daily_sales.py's get_instacart_data globs rpt.4096*tlogs*{DATE}*.csv, sums the GMV column, writes col D = GMV ÷ 1.1, and cross-checks vs LS — so have the tlog on disk for each day BEFORE running the script for that day and you never hand-patch col D. (CSV can't be fetched in-page — signed-URL CORS — so a real download is the only way; that's the documented routine.) ⛔ Do NOT use the Insights portal as the primary source — it lags 2-3 days (delivered-only, Pacific date) and returns 0/partial for recent days, which is exactly the day this skill runs. See [feedback_instacart_dashboard_lag.md]. |
| Instacart GMV (fallback only) | Only if generating the tlogs.v2.{DATE} report genuinely fails, use the In-store transactions report (rpt.4094...in_store_transactions...{DATE}) — sum each order's transaction_amt = store revenue = GMV/1.1 = col D directly. Last resort: Insights portal ...insights-portal/store-performance/completed-orders?from={DATE}&to={DATE}&dateRangePreset=custom&globalMetric=SUM_GMV (only reliable for days 3+ old). Leaving col D blank is only acceptable when NO report can be produced at all — and then it's an OPEN ITEM. |
| LS Sales Reports | LS web UI = https://corkandbarrelnyc.retail.lightspeed.app/ (X-Series). ⛔ NOT merchantos.com — that redirects to an OIDC login and reads as logged-out (false "LS session expired"). If it shows the login screen, auto-re-login from LS_WEB_USERNAME/LS_WEB_PASSWORD in scripts/.env and retry — never treat logged-out as a barrier (ABSOLUTE RULE 25). Navigate to Reports → Sales, set Report type: Customer, Measure: Revenue incl tax, set date range, read the Instacart customer row's Revenue column. This is the LS-correct Instacart cross-check — never compute it from the API. Secondary check only: every written value already comes from the API token / GMV tlog, so if login still can't complete it's a one-line footnote, not an OPEN ITEM. (See [feedback_instacart_ls_lookup.md], [reference_ls_login_url.md], [feedback_ls_web_autologin.md].) |
For multiple tabs, follow ABSOLUTE RULE 10: tabs_context_mcp first, reuse existing platform tabs by domain match, consume spare blank tabs, and only tabs_create_mcp for whatever domain has no tab yet. Hold the IDs, then navigate all in one batch. Do not close them at the end — they carry over to tomorrow's run.
1C. Extract data (parallel javascript_tool calls)
For each tab, run javascript_tool to extract the totals. Use these patterns — they're proven:
UberEats: Read "Sales (excl. tax)" from summary row. (NOT "Total Payout".)
DoorDash: Extract the FULL per-order row, not just the subtotal. Each DD row in --scraped MUST carry tx_id, order_id, subtotal, commission, cust_disc, mkt_credit, net. ⛔ Do NOT use a subtotal-only scraper — dropping per-order commission writes L=0 (no red) and a blank Col A (no Transaction ID). This silently broke 5/29–5/30 (see ABSOLUTE RULE 20).
Column indices in the DD transactions table: 0=Transaction ID, 1=Date, 2=Time, 3=Order ID, 7=Subtotal, 11=Commission, 14=Error charges, 15=Adjustments, 17=Customer discounts, 18=Marketing credit, 19=Net total.
(()=>{
window.__dd=window.__dd||{}; // accumulate across pages, keyed by tx
const rows=[...document.querySelectorAll('table tr')].slice(1);
const parse=v=>parseFloat(String(v).replace(/[$,]/g,''))||0;
for(const r of rows){
const c=[...r.querySelectorAll('td')].map(x=>x.innerText.trim());
if(c.length<20) continue;
window.__dd[c[0]]={tx_id:c[0],date:c[1],time:c[2],order_id:c[3],
subtotal:parse(c[7]),commission:parse(c[11]),cust_disc:parse(c[17]),
mkt_credit:parse(c[18]),net:parse(c[19])};
}
const t=document.body.innerText;
return JSON.stringify({pagination:t.match(/\d+[-–]\d+ of \d+/g),total:Object.keys(window.__dd).length});
})()
PAGINATE FULLY. The portal shows ~20 rows/page over a rolling 7-day window (URL date params don't stick). Click each page button (aria-label="go to page N"), re-run the snippet to accumulate into window.__dd, then filter to the target date(s). If pagination shows "1-20 of 44", you have 3 pages — visit ALL of them. Then build doordash_orders from Object.values(window.__dd) filtered to the target date. Verify per-order identity subtotal + commission ≈ net (within ~$0.15 for DD's own rounding); cust_disc and mkt_credit normally offset to ~0 net effect (DoorDash-funded promos).
GrubHub: Match (date) (time) $sub — — $tax $tip regex on document.body.innerText, filter to target dates.
Minibar: Count completed orders for target date; if none, $0.
Instacart GMV (PRIMARY = Transaction Logs report, NOT the Insights portal): Go to Reports → Transaction logs, download the day's tlogs.v2.{DATE} CSV yourself, sum the GMV column → divide by 1.1, round to 2 decimals. If that day's row isn't listed yet, it has not been auto-posted yet — it CANNOT be generated on demand (ABSOLUTE RULE 26); leave col D blank as an OPEN ITEM and it fills on a later re-run once posted. The Insights "GMV (USD)" cell lags 2-3 days and shows 0/partial for yesterday — do not rely on it. Sanity check: report order COUNT must equal the LS Instacart customer order count; if it's short, the report is still settling.
LS Sales Reports (Instacart customer): If the tab reads as logged-out, auto-re-login from scripts/.env (LS_WEB_USERNAME/LS_WEB_PASSWORD) and retry — never skip on account of a logged-out UI (ABSOLUTE RULE 25). Locate the row for Customer = "Instacart" or similar, read the "Revenue" cell. This is your ls_instacart_correct value (secondary cross-check).
1D. Pre-populate scraped JSON
Build the --scraped JSON for the script:
{
"ubereats_sales": <number>,
"doordash_orders": [{"tx_id","date","time","order_id","subtotal","commission","cust_disc","mkt_credit","net"},…],
"grubhub_orders": [{"subtotal","tip"},…],
"minibar_total": <number>
}
Hold these values in working memory: ls_instacart_correct, scraped_ic_value (=GMV/1.1).
STEP 2 — RUN SCRIPT
python3 scripts/daily_sales.py --date YYYY-MM-DD --cash AMOUNT --scraped '{...}'
The script pulls LS API + CityHive API, reconciles against --scraped, writes Excel, prints discrepancies. Stream the output but do NOT report it back to the user — synthesize into the final table.
The script writes Daily Sales Calculations.xlsx TWICE every run — to the day's weekday slot {Weekday} M-D AND to SQ (automatic, see Step 3). Multi-day handling happens in Step 3.
The script's "Instacart Rev" output is INFORMATIONAL ONLY. Do not trust it as the LS-correct value. It sums total_price + total_tax and drifts from LS Sales Reports. Always use the scraped LS Sales Reports value as ls_instacart_correct.
STEP 3 — SHEET ROUTING
⛔ WEEKDAY RULE (automatic, user directive 2026-07-27 — supersedes the 2026-07-24 Thursday/Friday-only rule): EVERY day is written to BOTH its weekday slot {Weekday} M-D AND SQ. write_all_excel does this itself from TARGET_DATE.weekday(); the identical set of values goes to both sheets in one loop.
- Seven weekday slots = a rolling one-week view.
Monday…Sunday, one sheet each. Each run reuses the ONE existing sheet for that weekday (matched by name prefix, so a bareMondayor a staleMonday 7-20both resolve) and re-stamps its tab with the current date. A weekday is therefore only ever overwritten by the NEXT same-weekday run — seven days later (e.g.Monday 7-27→Monday 8-3). This is what preserves a full week of daily calculations. SQstill always holds the MOST RECENT day reconciled. So after the 7/27 run you see 7/27 in bothMonday 7-27andSQ.- (
/is illegal in Excel tab names, so the date is writtenM-D.) A first-ever run for a weekday with no slot clonesSQto create it, preserving formulas/styling. - ⛔ No
--cash⇒ the cash cells are CLEARED, not left stale.B16/B17/B19/I2are set toNoneon a cash-less run.I2sits insideK2 = SUM(B2:J2), so a leftover prior-dayI2silently inflates the day's total (found 2026-07-27: SQ held 7/25's $642/$36/$500/$106 next to 7/26's data). They fill correctly on the later--cashre-run. - Legacy dated sheets are gone (2026-07-27). The old ad-hoc
M-Dsheets (4-8…7-17, plusThursday 7-23) were deleted at the user's request; onlySQ,DEFAULT,Sheet1and the seven weekday slots remain. Do NOT recreate bareM-Dsheets. A backup of the pre-cleanup workbook is atDaily Sales Calculations BACKUP 2026-07-27.xlsx.
Multi-day rule: nothing extra to do — the weekday slots ARE the multi-day storage. Each day of a batch lands in its own weekday sheet automatically, so no manual M-D cloning is needed any more (that was only ever a workaround for SQ being a single scratch slot).
If user requested only one day → done; the weekday slot and SQ both have it. Skip to Step 4.
If user requested multiple days, process oldest first, newest last — run the script once per day in date order. Each run writes that day to its weekday slot and overwrites SQ, so when the batch finishes every day sits in its own weekday sheet and SQ holds the newest. ⛔ Batches spanning more than 7 days will overwrite a weekday slot twice; if that happens, say which day got clobbered rather than silently losing it. Also heed [feedback: backfill degrades after ~1 day] — for any date older than yesterday, --dry-run first, since platform scrapes expire and a blind re-run zeroes DD/UB/tips.
STEP 4 — APPLY PLATFORM-TRUTH CORRECTIONS
After script runs, for each platform compare scraped vs LS:
| Diff | Action | Status label |
|---|---|---|
$0.00 |
none | ✅ EXACT |
≤ $1.00 |
none | ✅ PASS |
> $1.00 |
Write scraped value to Excel cell (overwrite LS). Always show diff. | ⚠️ FLAG |
| User explicitly said "employee error fix" | Apply user's fix | 🔧 CORRECTED |
| No platform source available | LS value as-is | UNVERIFIED |
| Only one source exists (CC, COGS) | LS value | SOURCE |
Cells to potentially overwrite (SQ or M-D sheet):
- D2 (CityHive) — if CH platform total ≠ LS
- E2 (UberEats) — if UB scrape ≠ LS
- F2 (DoorDash) — if DD scrape ≠ LS
- G2 (GrubHub) — if GH scrape ≠ LS
- Instacart D column → ALWAYS scraped GMV / 1.1, never LS
Do NOT touch B2 (CC), B11 (COGS), B3/D3/E3/G3/H3 (tips) unless user reports a specific employee error.
4A. DIAGNOSING A FLAG — one targeted query, don't re-scrape everything
When a platform FLAGs (LS ≠ portal), pull the LS per-order line items directly from the API (/register_sales?page_size=200&status=CLOSED, paginate, UTC→ET, filter to the date, print each register_sale_payments[].name + amount for the flagged platform) and diff that against the scrape. One query identifies which known cause it is:
- Extra/missing order — count mismatch (e.g. a late order rung in LS but not yet on the portal).
- Tax-included mis-ring — gap = one order's subtotal × 0.08875 (rule 22).
- Cross-day DoorDash void — an order is +$X on day N and −$X on day N+1, never on the DD portal. Match LS each day (F2 = LS, Door Dash SUM = LS): include the +order on day N and a −void row on day N+1; their tx_id (Col A) is legitimately blank (the script's blank-tx warning is EXPECTED), and the two net to zero across the span — say so in the summary.
- Declined CityHive order — CH FLAG + B3 negative + K3≠LS by the same amount → a
state=="declined"order; the script now auto-excludes these (rule 21 / declined-orders memory). - Stale SPA read — a $0/empty scrape when LS is non-zero → reload the page and re-read before writing anything (don't write $0).
Don't re-investigate whether a documented source "works" each run — trust the established source, run ONE diff to identify the flag, fix it, move on.
STEP 5 — VERIFICATION TABLE (intermediate)
Render this table for each day. Format is non-negotiable.
📊 Daily Sales — YYYY-MM-DD (Day) — sheet `<SQ or M-D>`
| Cell | Source | Excel | Platform | LS-correct | Diff | Status |
|------|--------------|------------:|---------------:|------------:|-------:|------------|
| B2 | Credit Card | $X,XXX.XX | — | $X,XXX.XX | — | SOURCE |
| D2 | CityHive | $X,XXX.XX | $X,XXX.XX (API)| $X,XXX.XX | $X.XX | ✅/⚠️/🔧 |
| E2 | UberEats | $X,XXX.XX | $X,XXX.XX | $X,XXX.XX | $X.XX | ✅/⚠️/🔧 |
| F2 | DoorDash | $X,XXX.XX | $X,XXX.XX | $X,XXX.XX | $X.XX | ✅/⚠️/🔧 |
| G2 | GrubHub | $XXX.XX | $XXX.XX | $XXX.XX | $X.XX | ✅/⚠️/🔧 |
| H2 | Minibar | $0.00 | $0.00 | $0.00 | $0.00 | ✅ EXACT |
| IC | Instacart | $XXX.XX | GMV $XXX.XX | $XXX.XX | $X.XX | ✅/⚠️ |
| B11 | COGS | $X,XXX.XX | — | — | — | SOURCE |
| K3 | Tips Total | $XXX.XX | — | LS $XXX.XX | $0.00 | ✅ |
| B16 | Cash | $XXX | — | — | — | (input) |
| I2 | Cash Calc | $XXX | — | — | — | (calc) |
Status definitions:
- ✅ EXACT — diff = $0.00
- ✅ PASS — diff ≤ $1.00 (show actual diff)
- ⚠️ FLAG — diff > $1.00 (show diff; you wrote scraped value to Excel; user must investigate)
- 🔧 CORRECTED — user-confirmed employee error fix only
- UNVERIFIED — no independent source; LS value used; tell user clearly
- SOURCE — only one source exists (CC, COGS, MB when no orders)
STEP 6 — TIP REPORT
File: ~/Library/CloudStorage/OneDrive-Personal/Documents/Tip Report.xlsx
Each sheet has its own row alignment — do not assume the year sheet's row numbers match employee sheets. Always look up the row by date.
Algorithm:
- Compute
online_tips = floor((D3 + G3 + H3) * 0.9)— same as Daily Sales C6 (B3 EXCLUDED — see ABSOLUTE RULE 16). Best: read C6's actual computed value fromDaily Sales Calculations.xlsx(data_only=True) and floor it. total = online_tips + cash_tips(cash_tips defaults to 0 if not given).- Determine workers from schedule (or user override).
- Split total whole-dollar evenly. Remainder $1 → Carlos > Sonam > Wendy.
- Year sheet (e.g. "2026"): find row where col A = sales date. Write C = online_tips, D = cash_tips (or None if no cash tip — never 0).
- Each working employee's sheet: find row where col A = sales date. Write C = their share of total. Never write D on employee sheets.
- For employees who did NOT work that day: explicitly clear their cell on that date if anything is in it (
cell.value = None). - Read back values to verify before reporting.
Examples:
- $81 between Sonam, Wendy → Sonam $41, Wendy $40
- $100 between Carlos, Sonam, Wendy → Carlos $34, Sonam $33, Wendy $33
- $119 between Carlos, Sonam → Carlos $60, Sonam $59
STEP 7 — END-OF-RUN DOUBLE-CHECK
Before final output:
- Re-scrape DoorDash + UberEats + GrubHub + Instacart GMV + LS Sales Reports IC fresh in one parallel batch.
- Read back Daily Sales Calculations sheet (SQ + any M-D), Door Dash Transactions, CityHive Transactions, Instacart, Tip Report.
- Compare each Excel value against fresh scrape.
- If anything differs → fix and re-check.
- Only then proceed to Step 8.
STEP 8 — FINAL OUTPUT
The verification table from Step 5 (all days, both intermediate-table format) is the LAST thing the user sees. No "Done!" message after, no summary, no chatter. The table = the deliverable.
Files written list goes ABOVE the tables. Single line each.
If invoices are also being processed in this session (combined run), the invoice pricing table replaces the daily-sales table as the final output — see invoices/SKILL.md.
EXCEL LAYOUTS (reference)
Daily Sales Calculations.xlsx, sheet SQ or M-D:
- Row 2: B2=CC, D2=CH, E2=UB, F2=DD, G2=GH, H2=MB, I2=Cash(calc), J2=Check(0), K2=SUM
- Row 3: B3=CC tips, D3=CH tips, E3=UB tips (from CH), G3=GH tips, H3=MB tips, K3=SUM (matches LS Tips total)
- B11=COGS, B13=RSV (500 Fri/Sat else 250)
- B3=
LS_Tips − D3 − E3 − G3 − H3(decimal, no floor — K3 must equal LS_Tips exactly). - B16=Cash (user input), C6=
=(D3+G3+H3)*0.9(B3 excluded), B17=floor(C6), B19=Reserve (500 Fri/Sat else 300), I2=B16-B17-B19 - Weekend = Friday OR Saturday only (
dow in (4,5)). Sunday is NOT weekend.
Door Dash Transactions.xlsx, Sheet1:
- Reverse chronological, latest first
- Col A = Transaction ID (the long numeric ID, e.g.
17329704539). MUST be populated on every row — the established format puts the Transaction ID here. (Col D = the short 8-char order code, blue.) Leaving A blank = the "missing Order ID" bug. - Col H = subtotal · Col L = commission (negative) · Col R = customer discount (negative) · Col S = marketing credit (positive) · Col T = net. Identity:
H + L ≈ T(within ~$0.15 for DD rounding; R+S offset to ~0). - "Red" = the number format
"$"#,##0.00_);[Red]("$"#,##0.00)auto-colors NEGATIVE values red. Commission (L) is always negative → shows red. Discounts (R) are negative → red. If a day's L column shows NO red, commission was entered as 0 → it was dropped in the scrape → re-scrape with the full per-order extractor and refill A/L/R/S. Do not hand-set the font color; just write the correct negative value and the format colors it. - Col U = SUM(H:H) for that day on the day's LAST row only. This SUM MUST equal the LS DoorDash value for the day, exact to the penny.
- If the DD UI list view only exposes net per row (current new UI behavior), distribute proportionally:
subtotal_i = round(net_i × LS_subtotal_sum / sum_of_nets, 2)- Adjust the largest row by
±0.01to absorb rounding so SUM hits LS exactly - Recompute
L_i = T_i − H_i(so commission is consistent per row)
- NEVER write H values that don't sum to LS. If your SUM ≠ LS, your row values are wrong — fix before writing.
CItyHive Transactions.xlsx, active sheet:
- 9PM ET cutoff: post-9PM orders roll to NEXT sales day
- SUM range includes prev-day rollovers
- SUM placement: row of LAST current-day order (incl. OOB rows)
- Uber orders highlighted yellow
Instacart.xlsx:
- Sheet
M-YYYY(e.g. "5-2026"). If sheet missing for current month, copy previous month, clear column B AND D. - ⛔ Col D row = the row whose DATE column (A or C) equals the SALES date — match the date, NEVER use a positional
day±Nindex. Sheets are inconsistent (some have a header at row 1, some put day 1 at row 1), soday+1silently lands on the WRONG (next-day) row — e.g. for the6-2026sheet (row 1 = 6/1, no header),day+1wrote 6/2's value onto the 6/3 row. User-reported recurring bug, fixed indaily_sales.py2026-06-03 (now date-matches; errors out instead of guessing a row). When reconciling yesterday's sales, the value belongs on YESTERDAY's date row. Format:"$"#,##0.00 - Only touch column D. Column B belongs to user.
- Value = GMV ÷ 1.1, where GMV = the SUM of the GMV column in the Transaction Logs report (
tlogs.v2.{DATE}) you download yourself. NOT the Insights portal (lags 2-3 days). NEVER fall back to LS. If the tlog report isn't listed yet, it has not auto-posted yet — it CANNOT be generated on demand (ABSOLUTE RULE 26); leave col D blank as an OPEN ITEM (it fills on a later re-run once posted). If col D is needed sooner and the In-store transactions report IS present, use itstransaction_amttotal (= GMV/1.1 directly).
Tip Report.xlsx:
- Year sheet rows ≠ employee sheet rows — look up by date column A.
- Never write decimals.
- Never write 0 in column D — leave blank if no cash tip.
KNOWN ENVIRONMENT QUIRKS
- LS API:
/api/register_sales,page_size=200&page=N.sale_dateis UTC — convert to ET before grouping. - LS payment names: "Lightspeed Credit Card", "CityHive", "UberEats", "DoorDash", "GrubHub", "Cash", "Minibar".
- Instacart in LS: customer name (NOT a payment type) — never use API math; use Sales Reports per Step 1B.
- CityHive cookie: auto-refreshes via pycookiecheat from Chrome
_HiveNet_sessioncookie atapi.cityhive.net. - GrubHub LS payment = subtotal + tip (tax goes to GrubHub).
- UberEats: scrape "Sales (excl. tax)", NOT "Total Payout".
- MCP Chrome doesn't share LS session with user's Chrome — if the LS UI (
corkandbarrelnyc.retail.lightspeed.app) reads as logged-out in the MCP tab, do NOT ask the user and do NOT skip: auto-re-login fromLS_WEB_USERNAME/LS_WEB_PASSWORDinscripts/.env(fill the email/password fields viaform_input, submit, wait for dashboard), then continue (ABSOLUTE RULE 25). Logged-out is never a barrier and never an OPEN ITEM.
WHAT NOT TO DO
- ❌ Do NOT serialize platform scrapes. Always parallel batch.
- ❌ Do NOT take screenshots when JS extraction works.
- ❌ Do NOT post screenshots to chat as part of normal flow.
- ❌ Do NOT narrate "now scraping X" / "now writing Y". Final table only.
- ❌ Do NOT use 🔧 CORRECTED unless the user explicitly told you what to fix.
- ❌ Do NOT trust the script's "Instacart Rev" line as LS truth. Always cross-check via Sales Reports → Customer.
- ❌ Do NOT report "all PASS" when any platform was UNVERIFIED — call it out.
- ❌ Do NOT call the workflow done before the end-of-run double-check (Step 7).
- ❌ Do NOT proceed to invoices, tip splits, or anything else before the verification table is presented.