Skip to content

International Tax & Withholding — Production Readiness Audit

FieldValue
Date2026-08-24
StatusGo — staging → production after CI, migrate, and smoke
ScopeAccounting 1.7.0 → 1.7.2 — tax types catalog, billing line tax types, payment/expense withholding
Branchfeature/international-tax-withholding
CompanionUser guide · Developer · API

Additive work on the existing Accounting module. No new permission slugs (reuses accounting.*). No new env vars or queues. Migrate-only deploy path.


Executive summary

Workspaces that already entitled Accounting pick up tax types and withholding after migrate. Catalog bumps do not auto-install for workspaces that never entitled the module. Invoices, Payments, and Expenses modules unlock the full billing and WHT surface.

GateResult
module:accounting + accounting.* (no new family)Pass
Tax types CRUD + tenant isolationPass
Invoice/credit accrual GL split (Dr AR / Cr Revenue / Cr Tax Payable)Pass
Payment WHT 3-line journal (Dr Deposit net / Dr WHT Receivable / Cr AR gross)Pass
Expense pay WHT 3-line journalPass
List API direction sort vs filter collision (P0)Fixed (1.7.2)
Sort column whitelist on tax types listPass
Payment & expense view pages show withholdingPass
Pest tax/WHT/accountingPass (14+)
Playwright test:e2e:accountingPass (8 runnable, 2 authz skips on entitled demo)
Docs + changelog + this runbookPass

Go / No-Go: Go after companion CI green, php artisan migrate --force, and staging smoke below.


Findings

IDSeverityStatusFindingAction
F1CriticalFixedTax types list returned empty when UI sent direction=asc|desc (treated as tax direction filter)Only filter when value is a tax direction enum; use sort + direction for ordering; Pest regression
F2MediumFixedPayment view did not show posted withholding amountRead-only WHT fields on payment view; API loads withholdingTaxType
F3LowFixedExpense view did not show withholding after paySame pattern on expense view
F4LowFixedorderBy($request->input('sort')) without whitelistAllowed sorts: name, code, kind, direction, rate, created_at, updated_at
F5LowFixedSelect controlled/uncontrolled warnings on tax type formStable watch() values; client-side zod for required GL accounts
F6InfoAcceptedAuthz e2e skips when demo workspace has AccountingRun negative authz on isolated tenant in CI if needed

Accepted / intentional

ItemNotes
No new permissionsReuse existing accounting.* for tax types
Payment amount = gross applied to ARWithholding reduces deposit only
Jurisdiction-agnostic catalogFBR-style values are example authority_reference only
DeferredCountry templates, WHT certificates, payroll statutory — out of scope

Test evidence

SuiteResultNotes
php artisan test --compact tests/Feature/Tenant/Accounting/TaxTypeTest.phpPassCRUD, list sort/filter, invalid sort fallback, authz, soft delete
PaymentWithholdingTest, ExpenseWithholdingTestPass3-line journals
CustomerInvoiceAccountingTest, CustomerCreditNoteAccountingTestPassTax split to 2100
npm run test:e2e:accounting8 passed, 2 skippedTax types 4/4 in one session; authz skips on entitled demo

Tax types e2e (single session): validation + CRUD; invoice tax split; payment WHT create/post + view; expense pay WHT + view.


Deploy order

  1. Backendcomposer install then php artisan migrate --force (tax_types, billing line FK, WHT columns, catalog 1.7.0 → 1.7.2)
  2. SPA — tax types pages, billing line picker, payment/expense WHT UI, view-page withholding
  3. Docs — user/dev/API/changelog + this page
  4. Staging smoke below before production traffic

Suggested merge: Backend → Frontend → Docs. Do not db:seed in production.

Starter GL accounts 1150 (WHT receivable) and 2150 (WHT payable) are created via the existing chart seeder when Accounting is enabled or CoA is seeded.


Pre-flight checklist

#CheckOwnerPass?
1Migrations applied (tax_types + WHT columns + catalog bumps through 1.7.2)Ops
2Catalog accounting version 1.7.2Ops
3Pest tax/WHT/accounting green in CIEng
4Playwright test:e2e:accounting greenQA
5Staging smoke signed offQA / Ops

Staging smoke (human)

  1. Marketplace → Accounting already installed (or install free)
  2. Tax types → list shows rows (not empty) → create sales tax + payment-in WHT
  3. Invoice → line with tax type → send → journal splits tax to 2100
  4. Payment → gross amount + WHT type → post → view shows Withheld and Deposit (net)
  5. Expense → approve → pay with WHT → view shows Withheld and Cash paid (net)
  6. Contact/vendor default withholding type pre-fills payment/expense pay dialogs

Rollback

LayerAction
FrontendRedeploy previous SPA (WHT UI disappears; API columns remain)
Backend codeRedeploy previous release; keep additive migrations
Module disableMarketplace uninstall Accounting (tax type rows retained)
SchemaDo not roll back tax_types / WHT columns in prod without a data plan

Monitoring

  • Spatie activity log names: tax_types, customer_payments, expenses
  • Nightwatch: validation errors on POST …/payments/{id}/post and POST …/expenses/{id}/pay with WHT fields
  • Support signal: empty tax types list after deploy → verify Backend 1.7.2 list fix is deployed

Sign-off

RoleNameDateDecision
EngineeringGo / No-Go
ProductF1–F5 fixed
OpsStaging migrate + smoke ☐

Recommendation: Merge companions after CI green; run staging smoke.

Official documentation for the EloSync SaaS Platform.