QBOP&L-by-Job Workbench
Foley Ridge Audio · invented sample company · 2026-06-01 to 2026-06-30

The job-costing report QuickBooks Online won’t export, rebuilt from the line-item API.

A working reproduction of the whole pipeline: the Custom Report Builder failure mode where a multi-project bill repeats its full total against every project, the per-line truth the QBO API returns instead, the four business rules from the brief, and the formatted Excel output you can download. Every number on this page is computed live from QBO-API-shaped fixture data by the same engine that generates the spreadsheet.

running on bundled QBO-shaped fixtures; wire to production by swapping base URL + client credentials (Station 5)
SELFTEST RUNNING13 invariants asserted server-side against the live engine on every run: /api/selftestSee the checks
STATION 1

The report-builder bug, reproduced

One subcontractor bill, three line items, three projects: $100 + $200 + $100, a correctly entered $400.00 bill. The Custom Report Builder reads a header-level figure and repeats it against every tagged project. The API reads Line[] and returns each line’s own amount. Same bill, both readings:

Custom Report Builder (header-level read)

ProjectAmount shown
The Cartographer’s Daughter$400.00
Salt & Cedar$400.00
A Study in Feathers$400.00
Summed across projects$1,200.00
WRONG

A $400.00 bill lands as $1,200.00 of project cost: 3.0x overstated. On a bill spread across 70 projects, the same read overstates 70x.

QBO API (per-line read)

ProjectLine amount
The Cartographer’s Daughter$100.00
Salt & Cedar$200.00
A Study in Feathers$100.00
Summed across projects$400.00
CORRECT

Per-line amounts sum to $400.00, exactly the bill’s TotalAmt. Selftest check 1 asserts this identity on every run.

STATION 2

What the API actually returns

The same bill as a QBO API Bill object (schema per Intuit’s v3 reference, minorversion 75). The header carries the vendor and the A/P control account; the money truth lives in Line[], where every line has its own Amount and its own project ref in AccountBasedExpenseLineDetail.CustomerRef. The balance-sheet side of the entry never appears in Line[], which is why the double-entry duplicate-row problem from the report exports disappears on this path.

Bill bill_101, as returned by the API

{
  "Id": "bill_101",
  "TxnDate": "2026-06-05",
  "VendorRef": {
    "value": "v_whit",
    "name": "S. Whitcombe (1099 contractor)"
  },
  "APAccountRef": {
    "value": "acc_ap",
    "name": "Accounts Payable (A/P)"
  },
  "TotalAmt": 400,
  "Line": [
    {
      "Id": "1",
      "Amount": 100,
      "DetailType": "AccountBasedExpenseLineDetail",
      "AccountBasedExpenseLineDetail": {
        "AccountRef": {
          "value": "acc_exp_rec",
          "name": "Production Labor:Recording"
        },
        "CustomerRef": {
          "value": "p_cart",
          "name": "The Cartographer’s Daughter"
        }
      }
    },
    {
      "Id": "2",
      "Amount": 200,
      "DetailType": "AccountBasedExpenseLineDetail",
      "AccountBasedExpenseLineDetail": {
        "AccountRef": {
          "value": "acc_exp_rec",
          "name": "Production Labor:Recording"
        },
        "CustomerRef": {
          "value": "p_salt",
          "name": "Salt & Cedar"
        }
      }
    },
    {
      "Id": "3",
      "Amount": 100,
      "DetailType": "AccountBasedExpenseLineDetail",
      "AccountBasedExpenseLineDetail": {
        "AccountRef": {
          "value": "acc_exp_rec",
          "name": "Production Labor:Recording"
        },
        "CustomerRef": {
          "value": "p_feathers",
          "name": "A Study in Feathers"
        }
      }
    }
  ]
}
per-line project refper-line amount

Paged extraction, no silent truncation

Pages through Bill, Invoice and Expense queries with STARTPOSITION / MAXRESULTS until a page returns fewer rows than requested, then asserts the fetched count equals COUNT(*).

Honest scaling note: the real API caps MAXRESULTS at 1,000 (default 100). This workbench pages at 3 so the loop is visible on a small fixture set; the loop logic is identical at any page size.
STATION 3

The four business rules, live

The brief’s rules applied as visible passes over the 42 flattened P&L lines. Toggle any rule off to see what breaks. The raw pull had 8 bills, 10 invoices and 2 expenses, flattened to one row per line item.

Rule 1: consolidate W-2 + 1099 labor by task category
W-2 staff and 1099 subcontractors both bill hours through Bills; the report reads labor by task category, never by worker classification.
38 rows in, 36 rows out. 21 labor lines (W-2 + 1099) consolidated into 19 task-category lines
Rule 2: split narrator fees into pass-through lines
Talent and TalentReimb amounts are pass-through with a small handling markup; blending them into core figures distorts margins.
38 rows in, 38 rows out. 6 Talent / TalentReimb lines mapped to "Income Other - Narrator Fees" / "Expense Other - Narrator Fees"
Rule 3: drop line items with no project
Overhead (rent, consumables, admin card spend) is deliberately ignored, not allocated.
42 rows in, 39 rows out. 3 overhead lines dropped (no Customer/Project ref)
Rule 4: drop projects with costs but no income
A project with costs and no income simply has not been invoiced yet; analyzing it would show artificial losses.
39 rows in, 38 rows out. project excluded: Quiet Hours (costs, no income yet)
Net effect with all four rules on: 42 raw P&L lines become 36 report-ready lines across 10 income-bearing projects in 4 customer groups. The toggles above re-run the engine on every click; the Excel download always applies all four rules.
STATION 4

The formatted Excel, generated for real

The Phase 4 deliverable made visible: projects as columns, customer in row 1 and project in row 2, customers alphabetical, single-project customers consolidated under Indie Author, a trailing total column per customer, and paired dollar / percent-of-revenue columns. The preview below and the downloadable .xlsx come from the same engine call.

percent baseDownload the .xlsx
P&L by JobAster & Vale PublishingBramblewood PressCormorant HouseIndie Author
Glass Orchard%Winter Counting%TotalNight Harvest%Salt & Cedar%The Cartographer’s Daughter%TotalA Study in Feathers%The Last Lighthouse Keeper%TotalDriftwood Letters%Lagos Frequencies%Pocket Storms%Total
Income - Production Services$2,150.00100.0%$1,700.00100.0%$3,850.00$2,600.00100.0%$2,900.00100.0%$3,800.00100.0%$9,300.00$2,450.00100.0%$1,980.00100.0%$4,430.00$1,400.00100.0%$1,850.00100.0%$980.00100.0%$4,230.00
Income Other - Narrator Fees$0.000.0%$0.000.0%$0.00$0.000.0%$0.000.0%$1,236.0032.5%$1,236.00$0.000.0%$0.000.0%$0.00$670.0047.9%$824.0044.5%$0.000.0%$1,494.00
Total Income$2,150.00100.0%$1,700.00100.0%$3,850.00$2,600.00100.0%$2,900.00100.0%$5,036.00132.5%$10,536.00$2,450.00100.0%$1,980.00100.0%$4,430.00$2,070.00147.9%$2,674.00144.5%$980.00100.0%$5,724.00
Recording Labor$330.0015.3%$0.000.0%$330.00$410.0015.8%$510.0017.6%$320.008.4%$1,240.00$100.004.1%$300.0015.2%$400.00$0.000.0%$305.0016.5%$0.000.0%$305.00
Editing Labor$340.0015.8%$260.0015.3%$600.00$0.000.0%$390.0013.4%$280.007.4%$670.00$0.000.0%$360.0018.2%$360.00$280.0020.0%$0.000.0%$260.0026.5%$540.00
Mastering Labor$0.000.0%$0.000.0%$0.00$280.0010.8%$0.000.0%$240.006.3%$520.00$0.000.0%$0.000.0%$0.00$0.000.0%$0.000.0%$0.000.0%$0.00
Proofing & QC Labor$0.000.0%$190.0011.2%$190.00$0.000.0%$0.000.0%$0.000.0%$0.00$220.009.0%$0.000.0%$220.00$0.000.0%$0.000.0%$150.0015.3%$150.00
Expense Other - Narrator Fees$0.000.0%$0.000.0%$0.00$0.000.0%$0.000.0%$1,200.0031.6%$1,200.00$0.000.0%$0.000.0%$0.00$650.0046.4%$800.0043.2%$0.000.0%$1,450.00
Other Project Costs$0.000.0%$0.000.0%$0.00$0.000.0%$0.000.0%$0.000.0%$0.00$48.002.0%$0.000.0%$48.00$0.000.0%$0.000.0%$0.000.0%$0.00
Total Costs$670.0031.2%$450.0026.5%$1,120.00$690.0026.5%$900.0031.0%$2,040.0053.7%$3,630.00$368.0015.0%$660.0033.3%$1,028.00$930.0066.4%$1,105.0059.7%$410.0041.8%$2,445.00
Net Profit$1,480.0068.8%$1,250.0073.5%$2,730.00$1,910.0073.5%$2,000.0069.0%$2,996.0078.8%$6,906.00$2,082.0085.0%$1,320.0066.7%$3,402.00$1,140.0081.4%$1,569.0084.8%$570.0058.2%$3,279.00
The percent-base question is real: the brief books narrator fees as pass-through income, so “% of revenue” can divide by core service revenue only, or by core plus pass-through. Both are implemented; the toggle above switches the whole sheet. This is the first thing to confirm against the sample spreadsheet.
STATION 5

OAuth 2.0, documented honestly

This workbench does not fake a live Intuit connection. Below is the exact production flow the monthly script uses, per Intuit’s OAuth 2.0 docs, written so a non-developer can re-authorize without the original developer.

One-time authorization (browser, ~2 minutes)
The company admin approves the app once. Intuit redirects back with an authorization code and the company’s realmId.
https://appcenter.intuit.com/connect/oauth2
  ?client_id=<from Intuit developer portal>
  &response_type=code
  &scope=com.intuit.quickbooks.accounting
  &redirect_uri=<registered redirect>
  &state=<csrf token>
Code exchange: access + refresh token
The access token lives 3,600 seconds; the refresh token has a 100-day rolling window (the response reports x_refresh_token_expires_in: 8726400).
POST https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer
grant_type=authorization_code&code=<code>&redirect_uri=<registered redirect>
Every monthly run: refresh first, then query
The script refreshes before querying, then persists the newest token pair. Intuit rotates the refresh token value periodically (about every 24 hours a new value can be issued), so the script always stores the latest one. Running at least once every 100 days keeps the grant alive with zero manual steps.
POST https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer
grant_type=refresh_token&refresh_token=<latest stored refresh token>

then:  GET https://quickbooks.api.intuit.com/v3/company/<realmId>/query
       ?query=SELECT * FROM Bill WHERE TxnDate >= '<start>' AND TxnDate <= '<end>'
              STARTPOSITION 1 MAXRESULTS 1000
Credential runbook (part of the handoff docs)
Client ID / secret and the current token pair live in a local .env file next to the script, never in the code. The docs cover: where each value comes from, how to rotate the client secret in the Intuit portal, and the 5-minute re-authorization walkthrough for the one failure that ever needs a human (a refresh token idle past 100 days).
Wire to production: swap the sandbox base URL (sandbox-quickbooks.api.intuit.com) for quickbooks.api.intuit.com and drop in the production client credentials. Since August 2025 the API defaults to minorversion 75; the script pins it explicitly.
STATION 6

Selftest: the invariants that make it trustworthy

Correctness is the whole job, so the workbench asserts its own claims. Each check recomputes from the fixtures through the same pipeline that renders this page and generates the Excel. /api/selftest returns this as JSON.

Running selftest...

Raw JSON