# PRD — quickbooks_integration

Evidence date: 2026-10-05. Source: `git@bitbucket.org:overdrive/quickbooks_integration.git`, `master` at `4ff6e81` (2020-04-30).

There is no prior PRD in Silicon Ark. `README.md` documents the 2020 QuickBooks Online invoice API. `README.txt` documents reinstalling QuickBooks Desktop Web Connector. Requirements below recover both. Provenance is marked. Credential values are omitted.

## Product purpose

**Documented** (`README.md`): an API that performs accounting functions on QuickBooks Online; currently creates an invoice and returns an invoice number. Date stated: March 2020. Intuit developer app name: **eit**.

**Documented** (`README.txt`): a SOAP service for Intuit Web Connector so Quotes (and optionally Add Client) can import into QuickBooks Desktop. Host names in that file: `quickbooks.silicon.local`. Later `.qwc` files use `qb.overdrive.co.za`.

**Implemented:** three Desktop invoice pipelines (Quotes, EIT billing, Timesheet) plus a separate Online invoice pipeline.

A written statement of which pipeline is the live accounting system in 2026 was not found. **Unknown / Requires Confirmation.**

## Users and roles

There is no login UI for staff in this repository. Access is:

| Actor | Evidence | Provenance |
|---|---|---|
| Quotes application | `handler.php` GET params; redirect comments to `http://quote/quote.php` | **Implemented** |
| EIT billing application | `handlerEIT.php` `Action=Month End` / `Ad Hoc`; CORS origin `http://eit.overdrive.co.za` in `.htaccess` | **Implemented** |
| Timesheet application | `handlerTS.php` `Action=Ad Hoc`; reads `tmpQBImport` | **Implemented** |
| QuickBooks Desktop admin on a workstation | `README.txt`: Web Connector + company file in single-user mode; username `username` | **Documented** |
| Person who authenticates QBO (named **Tania W** in README) | Must complete Intuit OAuth and add the server under Keys & OAuth | **Documented**. Whether that person still owns the app is **Unknown** |
| Browser hitting `/create/` | Demo page loads jQuery and `create.js`, which GET `/invoice/` | **Implemented** |
| Named application-user table for HTTP callers | Not present | **Unknown** |

Web Connector authenticates with ConsoliBYTE user `username` / `password` in `config.php.bak` and `README.txt`. That is SOAP-to-QBWC, not staff SSO.

## Main workflows

### WF-001 — Quotes: queue a Desktop invoice

Trigger: HTTP GET `handler.php` with `strQuoteNumber`, `strPONumber`, `QuoteID`.

Behaviour:

1. Connect to `quickbook_webservice`.
2. Insert `invoice_table` with `reference` = PO + `-` + quote number, `refQuoteID`, `strApplication` = `Quotes`, `strQuoteNumber`.
3. Enqueue ConsoliBYTE action `QUICKBOOKS_ADD_INVOICE` with the new id.
4. Echo `true`.

A commented redirect would send Quotes to status `3a. To be Invoiced`. That redirect is not executed.

Provenance: **Implemented** (`handler.php`).

### WF-002 — Quotes: Web Connector adds the invoice

Trigger: Web Connector SOAP to `qbwc.php` (`.qwc` AppName **Import Quotes**, every 30 minutes in `serverconfigQuotes.qwc`).

Behaviour (`functions.php`):

1. Load `invoice_table` row. If `strApplication` is not `Quotes`, return `QUICKBOOKS_NOOP`.
2. Load `customer_table` id **1** (comment: live customer "A Import Quotes").
3. Load quote header and lines from `quoteslive` (`get_quote_details`, `get_quote_items`).
4. Build qbXML 2.0 `InvoiceAdd` with bill address = client name, lines = SKU / description / qty / rate, tax code `S`.
5. Sleep 2 seconds. Persist XML on `invoice_table.xmlQBRequest` and under `XMLOutput/`.
6. On response: store ListID, EditSequence, RefNumber; set `blnEmail = 0`; `insert_invoice_number` sets Quotes `strInvoiceNumber` and `strStatus = '4. Invoiced'`.

Provenance: **Implemented**.

### WF-003 — Quotes: mark paid from QuickBooks

Trigger: mapped `QUICKBOOKS_QUERY_INVOICE` on the same SOAP server.

Behaviour: query all `invoice_table` rows with `paid = 0` and `invoice_number != 0`; parse `IsPaid`; set `paid = 1`; call `paid_invoices()` which attempts `UPDATE tblQuote SET strStatus = '5. Paid' WHERE ...`. The SQL concatenates extra `WHERE` / `AND` clauses in a way that is invalid for more than one invoice and is fragile for one invoice. **Requires Confirmation** that paid-sync ever worked in production.

Provenance: **Implemented** as written.

### WF-004 — Quotes: credited

Trigger: GET `credited.php` with `strQuoteNumber`, `QuoteID`.

Behaviour: `DELETE FROM invoice_table WHERE strQuoteNumber = ... AND paid = 0` using `mysql_query` (not mysqli). Redirect Quotes to status `6. Credited`.

Provenance: **Implemented**. Mixed mysql API vs mysqli elsewhere.

### WF-005 — EIT: month-end queue

Trigger: GET `handlerEIT.php?Action=Month End&Year=&Month=`.

Behaviour: `get_monthend_client_details` from `eitlivedb` for billable, non–Fixed-SLA, not Silicon Overdrive, `LENGTH(strInvoice) < 2`, dates in that month. For each client insert `invoice_table` (`strApplication` = `EIT`, period `YYYYMM`) and enqueue `QUICKBOOKS_ADD_INVOICE`. JSON `["true", "N Records have been queued..."]`.

The file does **not** `require` `config.php` or `eit.connection.php`. Those functions would only exist if something else included them. **Requires Confirmation** how production invokes this script.

Provenance: **Implemented** (`handlerEIT.php`).

### WF-006 — EIT: ad-hoc queue

Trigger: GET `handlerEIT.php?Action=Ad Hoc&ClientID=&Year=&Month=`.

Behaviour: one client insert + enqueue. Uses `$dsn` for the queue without defining it in this file (Month End inlines a DSN). **Implemented** with that gap.

### WF-007 — EIT: Web Connector invoice add

Trigger: SOAP `qbwcEIT.php` (AppName **Import Billing**, every 10 minutes).

Behaviour (`functionsEIT.php`): skip unless `strApplication` = `EIT`. Customer `customer_table` id **2** ("A Import Billing"). Lines: non-SLA billing, SLA billing with rollover split, then transit fixed charges (`Transit Fixed` × count of on-site days × `dblBillableTransit`). Item names `software` or `support - {initials}`. Tax `S`. Sleep 4 seconds.

Response handler uses `mysql_query($con, ...)` which does not match mysqli. Whether ListID is stored is **Unknown** at runtime.

EIT `insert_invoice_number` in `eit.connection.php` updates `tblQuote` in `eitlivedb`, but the EIT add-response does **not** call it. Writing the QB number back onto EIT `tblBilling.strInvoice` was not found. **Unknown / Requires Confirmation.**

### WF-008 — Timesheet: queue and add

Trigger: GET `handlerTS.php?Action=Ad Hoc`.

Behaviour: read `tmpQBImport` from `quickbook_webservice`; insert `invoice_table` rows `strApplication` = `TS`; enqueue; `TRUNCATE tmpQBImport`; JSON success.

SOAP `qbwcTS.php`: customer id **5** ("A Import Timesheet"). Builds one invoice with item **Software**, memo from rows, qty/rate from table. Marks related queue rows `qb_status = 's'` so they are not duplicated. Sleep 2 seconds. Query-response handler is empty.

Provenance: **Implemented**.

### WF-009 — Hourly re-queue

Trigger: `cron.hourly.php` (how cron is scheduled is **Unknown**).

Behaviour: find `quickbooks_queue` joined to `invoice_table` where `qb_status = 'n'`; set those rows to `qb_status = 'q'` and clear `msg`. `$Action` is never set; the empty switch always hits `default`.

Comments mention emailing errors and EIT quote updates; those tasks are not coded.

Provenance: **Implemented**.

### WF-010 — Add customer (Desktop)

Trigger: Web Connector to `AddClientService/qbwc.php` (and EIT/TS variants). Map is `QUICKBOOKS_ADD_CUSTOMER`.

Behaviour: `CustomerAdd` from `customer_table` id **1** (Quotes variant hard-codes id 1). Response writes ListID back to id 1.

`AddClientService/handler.php` inserts a quotes invoice row but the enqueue of `QUICKBOOKS_ADD_INVOICE` is **commented out**; it dies with `Great, queued up a customer!` without enqueueing a customer either.

Provenance: **Implemented**. README.txt still describes Add Client as optional.

### WF-011 — QuickBooks Online: authenticate

Trigger: GET/POST `/auth/` (`auth.php`).

Behaviour:

- Uses Intuit OAuth2 (`auth_mode` oauth2, `baseUrl` **development**), scope `com.intuit.quickbooks.accounting`, redirect `https://qb.overdrive.co.za/auth/`.
- If `code` / `realmId` / `state` missing, echo JSON of the authorization URL.
- If present, exchange code for tokens, store access token and expiry in PHP session, load latest row from `quickbooks_request`, redirect to `/invoice/`.
- If `stream` is posted, truncate `quickbooks_request` and insert one serialized payload (app, URL, bill id).

README: new servers must be added on the Intuit app; Tania W authenticates. Access token "expires after 1 hour" (`index.php` comment). `index.php` re-runs `auth.php` after 3600 seconds; `invoice.php` does **not** refresh on expiry.

Provenance: **Documented** + **Implemented**.

### WF-012 — QuickBooks Online: create invoice

Trigger: `/invoice/` after session token exists.

**README / create.js:** GET JSON `{ stream: { Line: [{ItemName, Amount, Qty}], CustomerId } }`; success reads `data.Invoice.DocNumber`.

**Implemented in `invoice.php`:** stream from `$_REQUEST['stream']` or `$_SESSION['stream']`. Looks up Customer by `DisplayName = CustomerName`. Looks up Item by `Name = sku['name']`. POSTs SalesItemLineDetail with Qty, UnitPrice, Description, currency **ZAR**, minorversion 47, **sandbox** base URL. Then `Location` redirect to `$_SESSION['url']?BillingID=&QBAPI=`.

**Implemented in `index.php`:** no customer name lookup; uses `stream['CustomerId']`; Item lookup uses `$sku['name']`; echoes API JSON instead of redirect. Token refresh via `auth.php` if expired.

`.htaccess` maps `invoice` → `invoice.php`, not `index.php`. Demo `create.js` matches neither file fully (has `CustomerId` like `index.php` but hits `/invoice/`).

Provenance: **Documented** vs **Implemented** conflict — **Requires Confirmation** which contract callers use.

## Functional requirements

### FR-001 — Rewrite pretty URIs

**Documented / Implemented.** `.htaccess`: `auth`, `invoice`, `create`, `handlerEIT`. HTTPS redirect is commented out. CORS allow-origin `http://eit.overdrive.co.za`.

### FR-002 — Queue denied concurrent Web Connector logins is off

**Implemented.** All `qbwc*.php` set `deny_concurrent_logins` and `deny_reallyfast_logins` false. Log level `QUICKBOOKS_LOG_DEVELOP`. SOAP builtin server.

### FR-003 — VAT line tax code S

**Implemented.** Quotes, EIT, TS invoice lines set `SalesTaxCodeRef/FullName` to `S`. Meaning "Standard VAT" is a code comment, not a signed policy document.

### FR-004 — EIT excludes Silicon Overdrive and Fixed SLA from month-end client list

**Implemented** in `get_monthend_client_details`.

### FR-005 — EIT only bills codes with `blnBill = 1` or code `Labour NC`, and `LENGTH(strInvoice) < 2`

**Implemented** in EIT SQL.

### FR-006 — On-site work adds a transit line

**Implemented.** Count distinct dates where location is `ON-SITE` or `ON-SITE and OFF-SITE`; item `Transit Fixed`; rate `tblClient.dblBillableTransit`.

### FR-007 — SLA hours beyond remaining hours use rollover rate

**Implemented** as a split line when `$Multiplier > $dblSupportHoursLeft`. `$dblSupportHoursLeft` is not assigned before first use in the inspected function. Behaviour of the first SLA row is **Unknown / Requires Confirmation**.

### FR-008 — Online invoices are ZAR against the sandbox company

**Implemented.** `CurrencyRef` ZAR twice in the JSON. Company id and sandbox URL are constants in `auth.php` / `invoice.php` / `index.php`. Production QBO base URL was not found.

### FR-009 — Empty Online stream or missing token

**Implemented.** `index.php` JSON `Fatal.Error` "Data stream is empty" / "Access not authorised". `invoice.php` redirects those JSON blobs onto `$_SESSION['url']`.

## Integrations

See `technical-documentation.md`. Direction: outbound to QuickBooks Desktop (SOAP/qbXML) and QuickBooks Online (HTTPS REST); inbound HTTP from Quotes/EIT/Timesheet; MySQL read/write to those apps' databases.

## Constraints

- qbXML version **2.0** (comments: higher versions can fail).
- Web Connector AppSupport URL must return HTTP 200 (`README.txt`). `support.php` in git is **empty**.
- SSL certificate on the SOAP host; `README.txt` describes self-signed and date-rollback workarounds.
- `config.php` is gitignored; backups in git may not match production DSN.

## Assumptions (not confirmed)

- Desktop and Online may have been a migration path (EIT app name on Intuit). Whether Desktop was retired is **Unknown**.
- Host `qb.overdrive.co.za` still serves this tree.
