Skip to content

Payroll 1.4.0 — Mark Paid Paid-from Bank Production Readiness Audit ​

FieldValue
Date2026-09-26 (remediation 2026-09-27)
StatusGo — all audit findings closed; migrate + SPA/Mobile before production Mark paid with Accounting
ScopeTenant Payroll module payroll 1.3.0 → 1.4.0 (Mark paid requires Paid from cash/bank when Accounting entitled; auto-post accrual + payment journals)
CompanionPayroll ops · Phase 7 HR readiness · Accounting · User · Developer · API · CHANGELOG

Additive MINOR on Payroll. No new permissions, queues, scheduler entries, or env vars. Workspaces without Accounting keep status-only Mark paid. Workspaces with Accounting gain a required Paid-from picker and cash/bank balance decrease on pay.

This audit covers Payroll 1.4.0 paid-from only. Prior Phase 7 HR readiness remains valid for profiles / approve / pay slips.


Executive summary ​

When Accounting is entitled, Mark paid on an approved pay run requires an active cash/bank Paid from account. The service:

  1. Ensures a posted accrual journal (Dr Salary Expense 6400 / Cr Salaries Payable 2200) — creates+posts if missing, or posts an existing draft from Post to journal.
  2. Creates+posts a payment journal (Dr Salaries Payable / Cr paid-from) via CashMovementJournalService.
  3. Persists paid_from_account_id, accrual account FKs, journal_entry_id, and payment_journal_entry_id.

Optional Post to journal still creates a draft accrual only. Locked accounting periods are enforced when journals are posted (JournalEntryService::post → assertDateNotLocked), covered by dedicated Pest.

GateResult
Catalog 1.4.0 migrate-only bumpPass
Schema additive + nullable account / payment JE columnsPass
FK on journal_entry_id + payment_journal_entry_id → journal_entriesPass
Starter CoA 2200 / 6400 (+ ensureMissingSystemAccounts)Pass (6100 remains Rent)
Accounting Mark paid requires cash/bankPass
Accrual ensure (missing / draft / posted)Pass
Payment JE auto-posted (not draft)Pass
Net ≤ 0 Mark paid rejected (Accounting)Pass (Pest)
Locked period blocks Mark paidPass (Pest)
Without Accounting: status-only payPass
Soft dep payroll → accounting (existing)Pass
SPA Paid-from dialog (list / peek / view)Pass
Mobile Paid-from pickerPass
Pest PayrollTest (19)Pass (local 2026-09-27)
Docs + changelog + this pagePass
CatalogSeeder payroll versionPass
Marketing payroll copy (Paid-from)Pass

Go / No-Go: Go after companion CI and php artisan migrate --force before SPA/Mobile that send paid_from_account_id.


Findings ​

IDSeverityStatusFindingAction
F1MediumFixedCatalogSeeder payroll row omitted version'version' => '1.4.0' on payroll in CatalogSeeder
F2LowFixedPlan text said Salary Expense 6100 (already Rent)Shipped 6400 Salary Expense; docs/constants aligned
F3LowFixedNo dedicated Pest for locked fiscal period on payroll payAdded blocks mark paid when the pay run period end falls in a locked accounting period
F4LowFixedNo Pest for net <= 0 Mark paid rejectAdded rejects mark paid when accounting is entitled and net total is zero
F5LowFixedpayment_journal_entry_id (and legacy journal_entry_id) lacked FKMigration 2026_09_26_190924_… adds FKs → journal_entries nullOnDelete
F6LowFixedMarketing payroll blurb omitted Paid-fromWebsite lib/constants.ts copy updated

Accepted / intentional (not defects) ​

ItemNotes
No new permissionsReuse payroll.pay / payroll.post
Draft accrual from PostCash movement rule: payment JE auto-posts; early accrual may stay draft until Mark paid
No WHT / unpay / void reverseExplicitly out of scope for 1.4.0
Catalog bump ≠ auto-installWorkspaces without Payroll entitlement unchanged
Post to journal after paid-with-AccountingHidden once journal_entry_id is set

Plan coverage matrix ​

Plan itemResult
paid_from / expense / liability / payment_journal_entry_id columnsPass
Starter 2200 + salary expense codePass (6400)
markPaid + CashMovementJournalServicePass
PayPayRunRequest + controllerPass
Resource nested journals / accountsPass
Catalog 1.3.0 → 1.4.0Pass
Pest: cash drop / missing paid_from / no accounting / draft→payPass
Pest: locked period + net ≤ 0Pass
Journal entry FKs on pay runPass
.ai/rules/tenant.md cash movementsPass
Frontend dialog list/peek/view + e2e helperPass
Mobile picker + payloadPass
User / dev / API / accounting docs + CHANGELOGPass

Test evidence ​

SuiteResultNotes
php artisan test --compact tests/Feature/Tenant/Payroll/PayrollTest.php19 passed (local 2026-09-27)Accounting pay cash drop, missing paid_from 422, no-accounting status-only, draft accrual then pay, net ≤ 0, locked period
tests/Feature/Tenant/Accounting/AccountTest.php7 passedStarter CoA; custom 6300 Marketing unaffected by 6400
npm run test:e2e:payroll (or module suite)Required green in CImarkPayRunPaidFromList opens Paid-from dialog when Accounting entitled

Deploy order ​

  1. Companion CI green (Backend / Frontend / Docs / Mobile as applicable)
  2. Backend — php artisan migrate --force
    • 2026_09_26_182640_add_payroll_paid_from_and_payment_journal_to_pay_runs_table
    • 2026_09_26_182643_bump_payroll_module_to_1_4_0
    • 2026_09_26_190924_add_foreign_keys_to_pay_run_journal_entry_columns
  3. Confirm catalog payroll version 1.4.0 (no db:seed)
  4. Confirm Accounts list backfills 2200 / 6400 via ensureMissingSystemAccounts (or first Mark paid / Post)
  5. SPA then Mobile — Paid-from UI must not ship before migrate
  6. Docs / Website (marketing copy)
  7. Staging smoke below before production traffic

Suggested merge: Backend → Frontend → Mobile → Docs (+ Website). Do not db:seed.


Pre-flight checklist ​

#CheckOwnerPass?
1Pest PayrollTest green in CI (19)Eng☐
2Playwright payroll suite green (Paid-from dialog path)QA☐
3Migrations 182640 + 182643 + 190924 appliedOps☐
4Catalog payroll version 1.4.0Ops☐
5SPA/Mobile deployed after migrateOps☐
6Staging smoke signed offQA / Ops☐

Staging smoke (human) ​

  1. Marketplace → install Employees, Payroll, Accounting (free).
  2. Open Accounts — confirm Cash 1000, Salaries Payable 2200, Salary Expense 6400 (or create cash/bank if needed). Seed cash via Set balance / opening journal so a credit is visible.
  3. Create payroll profile + pay run → Approve.
  4. Mark paid without selecting Paid from → expect validation (dialog requires selection / API 422).
  5. Mark paid with Cash → status Paid; accrual + payment journals posted; Cash balance decreases by net total; Salaries Payable nets to zero for that run.
  6. Optional path: on another approved run, Post to journal first (draft accrual) → Mark paid with bank → draft becomes posted + payment JE posted.
  7. Workspace without Accounting: Mark paid one-click, no journals / no Paid from.
  8. Locked fiscal period covering period_end: Mark paid fails with period lock validation; pay run stays approved.
  9. Approved run with net 0.00: Mark paid with Accounting fails validation; stays approved.

Rollback ​

LayerAction
Frontend / MobileRedeploy previous build (Paid-from UI disappears; columns remain)
Backend codeRedeploy previous release; leave columns (nullable)
CatalogOptional bumpVersion('payroll', '1.3.0') only if ops must show prior Marketplace version — prefer leave 1.4.0 if columns remain
DataDo not drop payment/accrual journals already posted without a finance-approved void plan

Official documentation for the EloSync SaaS Platform.