Invoice→QB sync suppressed on Cancelled work orders even when the invoice carries earned revenue (Cancellation service type)

Objective

  • Allow Attik→QuickBooks invoice sync for cancelled work orders when the invoice still carries earned billable revenue (e.g. a Cancellation service-type fee line)
  • Stop conflating work order status Cancelled with billable Cancellation service lines — they are independent concepts
  • Prevent payments from syncing to QuickBooks without a matching invoice, which leaves unapplied customer credits and breaks revenue recognition for brands that charge cancellation fees

Background

  • Today, invoice sync is gated on work order status === confirmed only. When a job is cancelled, invoice sync is blocked and a QuickBooks invoice delete is queued — regardless of whether the invoice has charge lines or a non-zero total
  • Payment sync remains enabled for cancelled Attik jobs, so QuickBooks can receive a payment with no invoice to apply against (unapplied credit / phantom liability on the customer account)
  • Several brands use a billable service type named "Cancellation" (or similar) with an associated fee. That is not the same as setting the work order status to Cancelled
  • Reference case — FHI624: Work order status Cancelled (cancelled banner shown; top chip may still show Published because published report status takes precedence in the workorder header). Invoice FHI624 has a single Cancellation-type line "Same Day Cancelation - HI NOT Done" at $125.00, fully paid ($340.52 captured, $211.14 refunded, $0 balance). Invoice and payment did not sync to QuickBooks solely because the WO is cancelled — not because the invoice is empty or unsettled
  • Confirmed in code (investigation complete):
  • Invoice queue/worker skip when inspection.status !== 'confirmed': queueQuickbooksSync.ts (queueQuickbooksInvoiceSync, queueQuickbooksInvoiceSyncFromInspection) and upsertQuickbooksInvoice.ts (processQuickbooksInvoiceJob)
  • On status change to cancelled: inspectionStream.ts calls queueQuickbooksInvoiceDelete using invoiceNumber
  • Payment queue allows confirmed or cancelled: queueQuickbooksSync.ts (queueQuickbooksPaymentSync)
  • Refund path can queue invoice re-sync via resolveQbPaymentSyncActionsync_payment_and_invoice, but invoice workers still skip non-confirmed inspections
  • Workorder QuickBooks UI disables invoice manual sync when cancelled: Quickbooks.tsx (canSyncInvoice requires status === 'confirmed'; subtext: "Cancelled—invoice sync off; payments can sync")
  • Charge line → QB mapping uses service QB item ref on each charge: qbChargeLineItems.ts / buildQbInvoiceLineItems.ts (no special-case skip for Cancellation service type today)

Product Decisions

Locked

  1. Problem framing — The bug is status-based suppression of invoice sync on cancelled jobs, not missing Cancellation service configuration in general
  2. Naming collision — Billable Cancellation service type (fee line on invoice) must not be treated the same as work order status Cancelled
  3. Reference validation — FHI624 is the acceptance example: cancelled WO, paid $125 Cancellation fee, zero balance, should sync invoice + apply payment in QuickBooks
  4. Attik-created jobs only — QuickBooks invoice sync applies to jobCreationType === 'attik' (existing integration scope)

Open

  1. Net billable threshold — What condition should allow invoice sync on a cancelled WO: total > 0, balance >= 0 with at least one charge line, only when a Cancellation-type service is present, or any non-zero invoice regardless of line types?
  2. Cancel-time invoice delete — When a WO is cancelled but retains billable revenue, should queueQuickbooksInvoiceDelete still run (removing a previously synced invoice), or should delete be suppressed / replaced with upsert when net billable revenue exists?
  3. Partial refund pattern (FHI624) — After partial refund on a cancelled job, should the QuickBooks invoice total reflect the retained amount ($125), the original charge total, or follow existing refund-adjustment logic (sync_payment_and_invoice) once the status gate is fixed?
  4. Manual sync UX — Should the workorder Integrations → QuickBooks "Sync to QuickBooks" action be enabled for cancelled jobs that meet the net-billable criteria, or queue-only via automatic paths?
  5. Empty cancelled invoices — Confirm continued suppression when invoice total is zero / no chargeable lines (no change to current intent for truly empty cancelled jobs)

Scope

Backend (attik-backend)

  • src/util/functions/quickbooks/queueQuickbooksSync.ts — invoice sync enqueue requires status === 'confirmed' today; payment sync allows confirmed | cancelled
  • src/events/subscribers/quickbooksIntegration/upsertQuickbooksInvoice.tsprocessQuickbooksInvoiceJob hard-skips non-confirmed inspections before QB API calls
  • src/events/streamHandlers/inspectionStream.ts — on cancel: queueQuickbooksInvoiceDelete; on confirm/charge/reschedule: queueQuickbooksInvoiceSyncFromInspection
  • src/events/streamHandlers/chargeStream.ts — charge insert/update triggers invoice sync (still subject to confirmed gate in queue helper)
  • src/events/streamHandlers/paymentStream.ts — payment insert/update triggers payment sync; refund policy in qbPaymentSyncPolicy.ts may queue invoice sync that is then blocked
  • src/util/functions/quickbooks/buildQbInvoiceLineItems.ts / qbChargeLineItems.ts — charge lines map via _serviceId.quickbooks.id; verify Cancellation services have correct QB item mapping per brand (data/config, not code branch today)
  • src/routes/quickbooks.ts — manual POST /sync-invoice/:inspectionId endpoint (no status check at route level; worker skip applies)

Frontend (attik-frontend)

  • src/app/tools/inspections/[id]/components/integrations/Quickbooks.tsxcanSyncInvoice and cancelled subtext; expectedQbInvoiceTotal for balance comparison
  • src/app/tools/inspections/[id]/components/InspectionWorkorderShell.tsx — cancelled banner vs PropertyStatusChip showing published when inspection.published (orthogonal to QB sync; explains FHI624 header confusion but out of scope unless separately fixed)

Out of scope (unless expanded)

  • Changing published vs cancelled header chip priority (separate UX concern)
  • QuickBooks sync for Spectora-created jobs
  • HubSpot / other integrations that also branch on status === 'cancelled'

References

  • Reference case: invoice FHI624 — cancelled WO, Cancellation fee line, paid zero balance
  • QuickBooks integration entry points: queueQuickbooksSync.ts, upsertQuickbooksInvoice.ts, Quickbooks.tsx
  • Related cancellation-fee naming in ISN import: isn/historicalImportPayments.ts (isIsnSameDayCancellationCatalogFeeName)

Please authenticate to join the conversation.

Upvoters
Status

Planned

Board
🏠

Main App

Date

About 5 hours ago

Author

Linear

Subscribe to post

Get notified by email when there are changes.