---
name: evolution-shopify-inbound-tax
description: "How Shopify order tax flows into Evo invoices, why tax-free orders got GST, and the fix"
metadata: 
  node_type: memory
  type: project
  originSessionId: e1866a39-14e2-4e9e-bfe3-72022392a4d2
---

Shopify inbound order → Evo invoice tax path (`plugins/shopify/plugin.php`):
- `orderCreateJobItems`/`orderCreateShippingItem` read `line_item->tax_lines`, sum `$totalTax`, **strip it from price** (`jobInventory.price` stored ex-GST), and save tax only to `customextrameta` as `order-item-{jobInventoryId}-taxAmount` / `-taxAmountRate`. **`jobInventory` has NO tax column.**
- `orderCreateInvoice` builds `invoiceitems` via `invoice->addLines()`.

Root cause of "Shopify order had no GST but Evo invoice does" (inv item 55984, ticket area):
1. The invoice header tax + GL are recomputed from each line's **`taxId` rate × subtotal** in `invoice.php` `saveInvoiceTotal`/`updateTotals`/commit — NOT from the stored per-line `taxAmount`. So **taxId is the authoritative lever.**
2. Plugin set `taxid` from `inventory.taxSell` (GST id 4), never from the order → taxable item + tax-free order = GST added.
3. `addLines` used `empty($vars->taxamount)` — `empty(0)===true` — so a passed 0 was discarded and GST recomputed.
4. Tax meta was only saved when `$totalTax > 0`, so tax-free orders recorded nothing (indistinguishable from missing).

GOTCHA — shopify plugin `get_meta` collapses '0' to '': wrapper does `if ($ret == false) $ret = ''` and PHP `'0' == false` is TRUE. So you CANNOT detect tax-free by reading back a 0 `-taxAmount`/`-taxAmountRate` — a genuine zero reads as "missing". Must use an affirmative flag string like `-taxfree = '1'` (survives the falsy check). This bit the first fix attempt (inv 59889: meta `-taxamount=0` written + "Tax Free" logged, but invoice still got taxId 4 because the 0 read back as '').

Fix applied (2026-07-14, forward-fix only, no backfill):
- Always save `-taxAmount`/`-taxAmountRate` incl. explicit 0, AND write affirmative `order-item-{id}-taxfree = '1'` when `$totalTax == 0` (job item + shipping item).
- `orderCreateInvoice`: `$orderTaxFree = (get_meta('...-taxfree') == '1')` → set line `taxid` to the 0-rate FREE code (dev id 7) instead of taxSell. Header tax + GL are rate-driven off taxId, so FREE code is what actually zeroes the GST.
- FREE tax id is NEVER hardcoded (tax table is user-editable): resolved at runtime by new memoised helper `shopifyPlugin::getFreeTaxId()` — prefers `name='FREE' AND value=0` (canonical name, same signal job.php uses), falls back to lowest-id `value=0` (accounting.php convention). Property `var $freeTaxId` declared for the memo cache.
- Hardcoded-value sweep of plugin.php: only the Shopify GraphQL API version `2026-01` (line ~618 URL) is a real hardcoded literal (intentionally pinned; Shopify rotates quarterly — revisit before deprecation). Everything else is standard status=1/siteid=0 filters or ticket-ref comments.
- `invoice.php` `addLines`: guard changed to `!isset || ===null || ===''` so a caller-supplied numeric 0 is respected (shared method; audited callers jobeditsave/invoice.php pass unset, dispatch passes "" → all still recompute).

Deploy note: shopify webhook runs on the dev LAMP php-fpm (web opcache ON) on the same host; edits need the endpoint's opcache refreshed / pulled before testing (early test invoices 59887/59889 ran stale bytecode).

Tax codes (dev tenant): 4=GST 10%, 6=ABNW -46%, 7=FREE 0%. See [[gst_precision_project]], [[evolution_importquote_tax_drift]] (taxSell override gotcha), [[evolution_two_accounting_files]].
