# Feature: QuickBooks

## Purpose

Per-team QuickBooks Online connection so sent invoices are created and emailed in QuickBooks, and QuickBooks payment webhooks mark the matching ServDiary invoice paid.

## Boundaries

- **Owns:** `quickbooks_connections`, `quickbooks_customers`, `quickbooks_invoices`, `quickbooks_payments`; OAuth connect/callback/disconnect; signed `POST /webhooks/quickbooks`; Inertia `Pages/QuickBooks/Settings.vue`; queued push and payment jobs
- **Does not own:** Invoice totals, statuses, PDF/email templates, customer CRM, QuickBooks Desktop, tax/VAT mapping, pushing ServDiary-recorded payments into QuickBooks, quotes
- **Depends on (platform only):** User / Team / auth; Laravel HTTP client; queue
- **Depends on (other domains):** none required — optional `class_exists` consumption of Invoices `ApplyExternalInvoicePayment`; Invoices optionally calls `QuickBooksInvoiceDelivery`
- **Platform UI touchpoint:** Invoice settings card and invoice show send UI (`Invoices/Settings.vue`, `Invoices/Show.vue`) when the domain is present. Revert those optional props/links if this domain is removed.

## Models

| Model | Table | Notes |
|-------|-------|-------|
| `QuickBooksConnection` | `quickbooks_connections` | One row per team. Encrypted `access_token` / `refresh_token`. `connected_at` null means disconnected; mappings are kept for the same `realm_id`. |
| `QuickBooksCustomer` | `quickbooks_customers` | Soft `customer_id` + `qbo_customer_id`. Find-or-create by email/name. |
| `QuickBooksInvoice` | `quickbooks_invoices` | Soft `invoice_id` + `qbo_invoice_id`; `email_status` pending/sent/failed; `last_error`; `pushed_at`. |
| `QuickBooksPayment` | `quickbooks_payments` | `qbo_payment_id` unique per connection — webhook idempotency. Soft `invoice_id`. |

Schema is normalised (3NF). No QuickBooks ids on `invoices`. Tokens use Laravel `encrypted` casts (ciphertext at rest; **intentional**: not business data duplication). Migration: `database/migrations/2026_08_28_090000_create_quickbooks_tables.php`. Factory: `database/factories/Domains/QuickBooks/`.

## Behaviour

- **Connect:** Owner/admin OAuth against one Intuit app (`QUICKBOOKS_*` env). Each team has its own QBO company (`realm_id`).
- **Send:** When connected, Invoices `MarkInvoiceSent` / `SendInvoiceEmail` skip Laravel Mail and queue `PushInvoiceToQuickBooksJob`. The job find-or-creates a QBO customer and a “ServDiary Services” item, creates the invoice, then `POST /invoice/{id}/send` with body `{}` and `Content-Type: application/octet-stream` (and optional `?sendTo=` query param). Idempotent: existing `qbo_invoice_id` is reused; `/send` is skipped once `email_status=sent`.
- **Queue:** Default `QUEUE_CONNECTION=database` — pushes run only when `php artisan queue:work` is running. On sandbox without a worker, set `QUICKBOOKS_QUEUE_SYNC=true` (or `QUEUE_CONNECTION=sync`) so pushes run immediately.
- **No customer email:** Mark-sent still succeeds; the job records `last_error` on the mapping. Staff can retry after adding an email (invoice email form while connected).
- **Payments:** Intuit POSTs Payment Create/Update to `/webhooks/quickbooks`. HMAC-SHA256 `intuit-signature` is verified against `QUICKBOOKS_WEBHOOK_VERIFIER`. Classic `eventNotifications` and CloudEvents payloads are parsed. Unknown realms and unmapped QBO invoices are ignored. Matching payments call Invoices `ApplyExternalInvoicePayment` with method `quickbooks` (releases held provider payouts when the invoice is fully paid).
- **Out of scope (v1):** quote sync, void/edit sync, tax codes, QuickBooks Payments onboarding (Pay Now only if that QBO company already has Payments).

Operators must create an Intuit Developer app, subscribe to **Payment** Create/Update webhooks, and point them at the public HTTPS URL `/webhooks/quickbooks`. Local environments need a tunnel.

### Intuit OAuth redirect URI

Intuit rejects the connect flow when the `redirect_uri` sent by ServDiary is not listed **exactly** on the app’s **Keys & OAuth → Redirect URIs** tab (scheme, host, path; no trailing slash unless registered).

ServDiary sends:

- `QUICKBOOKS_REDIRECT_URI` when set in `.env`, otherwise
- `{APP_URL}/quickbooks/callback` (Laravel `url('/quickbooks/callback')`)

For each deployment (local, sandbox, production), set `APP_URL` and/or `QUICKBOOKS_REDIRECT_URI` on **that server** and register the same value in Intuit. You can register multiple redirect URIs on one Intuit app (e.g. local tunnel + sandbox + production).

Example sandbox:

```env
APP_URL=https://sandbox.servdiary.com
QUICKBOOKS_REDIRECT_URI=https://sandbox.servdiary.com/quickbooks/callback
```

The QuickBooks settings page shows the effective redirect URI this instance will use.

## Routes

### Web

| Method | URI | Name | Controller |
|--------|-----|------|------------|
| GET | `/quickbooks` | `quickbooks.settings` | Web\QuickBooksSettingsController@edit |
| GET | `/quickbooks/connect` | `quickbooks.connect` | Web\QuickBooksSettingsController@connect |
| GET | `/quickbooks/callback` | `quickbooks.callback` | Web\QuickBooksSettingsController@callback |
| POST | `/quickbooks/disconnect` | `quickbooks.disconnect` | Web\QuickBooksSettingsController@disconnect |

### Webhook (no session / CSRF)

| Method | URI | Name | Controller |
|--------|-----|------|------------|
| POST | `/webhooks/quickbooks` | `quickbooks.webhooks` | QuickBooksWebhookController@handle |

### API

N/A — mobile clients do not connect QuickBooks.

## Permissions

- View settings: same billing-style restriction as invoices (not staff/customer/provider)
- Connect / disconnect: team owner, `admin` role, or super admin
- Webhook: signature only

## Tests

- Path: `laravel/tests/Feature/Domains/QuickBooks/`
- Cover: OAuth callback stores encrypted tokens; owner/admin vs editor/staff; mark-sent while connected queues push and does not Mail; push job creates customer + invoice + send and is idempotent; valid Payment webhook marks invoice paid (held payouts released); invalid signature 401; unknown realm ignored; duplicate `qbo_payment_id` no-ops

## Add / remove checklist

### Add

- [ ] Domain folder + `DomainServiceProvider`
- [ ] Provider registered in `bootstrap/providers.php`
- [ ] This wiki page linked from `docs/README.md`
- [ ] Feature tests passing
- [ ] Env keys in `.env.example` / Intuit app + Payment webhooks

### Remove

- [ ] Provider unregistered
- [ ] Domain folder, factories, migration, `config/quickbooks.php`, wiki, tests deleted
- [ ] Optional Invoices hooks (`class_exists(QuickBooksInvoiceDelivery::class)`), Settings/Show QuickBooks UI, and `METHOD_QUICKBOOKS` left harmless or reverted
- [ ] Remaining suite still green
