# Payroll Loan Deduction — Implementation, Operations, and Deployment Handoff ## Status The payroll loan deduction workflow is implemented and validated in the local Solidmark environment. Payroll users now save loan deductions as period-scoped drafts. A draft affects the payroll preview only; it does not create a loan journal entry, reduce the real loan balance, or close a loan until payroll is finalized. The authenticated acceptance test for legacy unfinalized undo and finalized-batch reversal passed with a required audit reason. The formal release-gate acceptance for tenant isolation, RBAC, concurrency, idempotency, and audit logging is intentionally **deferred/skipped** for this execution pass. The implemented controls and focused regression checks are documented below, but this item must not be represented as formal acceptance. ## Business Problem Resolved Previously, entering a loan deduction in Payroll Management could immediately create a posted payroll journal and reduce the loan balance before the payroll batch was finalized. If the amount was wrong, users could not safely remove it because it already appeared as a payroll-origin journal entry. The replacement workflow separates a proposed payroll deduction from a posted repayment: 1. Payroll staff save, edit, or skip a deduction draft for a selected employee and cutoff. 2. Payroll preview and net pay use the draft amount and show projected loan balance. 3. Payroll finalization locks and rechecks the draft and loan, then posts the journal and changes the real balance atomically. 4. Errors after posting are corrected through a reasoned legacy undo or finalized-batch void/reversal; the original journal is preserved for audit history. ## Data Model ```text loans (authoritative loan balance and lifecycle) 1 ──< payroll_loan_deduction_drafts (one draft per tenant/live row/loan) 1 ──< loan_journal_entry (posted repayments and reversals) payroll_live_cache (payroll preview row) 1 ──< payroll_loan_deduction_drafts (source_live_id) payroll_loan_deduction_drafts 1 ──< payroll_loan_deduction_audit (create/edit/skip/post/undo/void history) payroll_finalized / payroll batch ──> posted draft and payroll-origin journal through payroll_batch_no ``` ### New tables | Table | Purpose | | --- | --- | | `payroll_loan_deduction_drafts` | Stores each editable payroll-period deduction, its state, amount, version, actor, source preview row, posting journal, and finalization batch. | | `payroll_loan_deduction_audit` | Stores the create, update, skip, post, undo, and void trail with actor, previous/new amount, reason, request ID, metadata, and timestamp. | ### States | State | Meaning | | --- | --- | | `draft` | Editable proposal; affects payroll preview only. | | `skipped` | User explicitly excluded the deduction for that cutoff; it will not silently return on recalculation. | | `posted` | Payroll was finalized and the corresponding repayment journal was created. | | `voided` | A legacy undo or finalized-batch correction invalidated the posted deduction while retaining audit history. | ## Backend Changes ### Draft, preview, and finalization | Area | Implemented behavior | | --- | --- | | Loan summary read | Returns actual balance, draft amount, projected balance, state, version, and editability for the selected payroll period. | | Save draft | `update_loan_summary.php` saves or updates a period-scoped draft. It does not reduce `loans.balance` or create a journal. | | Skip draft | `remove_loan_deduction_draft.php` marks a draft as `skipped` with a reason and optimistic version check. | | Deprecated legacy save | `save_loan_deduction.php` no longer writes a global loan deduction; it returns a retirement response so it cannot bypass drafts. | | Preview sync | `loan_live_cache_sync.php`, `sync_loan_deductions.php`, and compatibility routes read active drafts to calculate preview totals. | | Finalization | `finalize_payroll_from_live_cache.php` locks preview rows, active drafts, and loans; revalidates amounts; posts one journal per draft; updates the real loan balance/lifecycle; marks the draft posted; and writes audit records in one transaction. | | Journal creation | `create_journal_entry.php` protects payroll-origin posting with tenant-scoped idempotency keys. | ### Shared rules The reusable payroll-loan deduction service validates: - Company, brand, employee, loan, and selected payroll-period ownership. - Amount precision and positive amounts. - Fixed-term balance cap; a deduction cannot exceed the current balance. - Closed and cancelled loan rules. - Open-ended loans separately, because they do not have a fixed balance payoff. - Stale edits using draft `version` and row locks. - Duplicate posting with a scoped idempotency key. ### Corrections and reversals | Scenario | Supported action | Result | | --- | --- | --- | | Legacy payroll credit that was posted before finalization | `undo_unfinalized_payroll_deduction.php` | Requires a reason, verifies no matching finalized payroll, preserves the original journal as `reversed`, restores fixed-loan balance/lifecycle, voids any linked posted draft, writes audit, and refreshes preview cache. | | Finalized payroll with a loan-deduction error | `void_finalized_payroll_batch.php` | Requires the established payroll void workflow and reason. It creates reversal records, restores eligible loan state, marks linked drafts voided, records audit data, and refreshes cache atomically. | | Journal points to a deleted/missing loan | Finalized void is blocked with a documented `409` conflict | The system does not delete the journal or guess a balance. Restore/reconcile the loan first, then void through the authorized workflow. | ## Frontend Changes ### Payroll Loan Summary / Editor The editor now: - Loads loans for the selected employee and cutoff with actual and projected values kept separate. - Shows an explicit draft state rather than representing a draft as a posted journal entry. - Saves selected entries as drafts and allows an explicit skip for the selected cutoff. - Shows employee/loan identity, balance, suggested per-cutoff amount, editable amount, posted state, and draft state. - Prevents duplicate save/skip requests and handles loading, empty, and error states. - Refreshes the affected employee row only; it preserves the editor context and avoids rerendering the whole Payroll Management page. - Uses a responsive card layout for narrower screens. ### Payroll preview and finalization UI - Payroll details distinguish editable draft deductions from read-only posted journal history. - The finalization dialog includes a loan-review checkpoint showing employee, loan description/ID, draft amount, actual balance, projected balance, and payoff warnings. - Finalized payroll has read-only posted history; unfinalized payroll retains draft actions. ### Loan Management and journal UI - Loan cards display their saved description, inactive/paid/closed status, and ended date where applicable. - The journal dialog shows deduction history, collected repayments, balance, and last deduction/payoff date. - Opening or closing the journal preserves the Loan Management view and scroll position. - Eligible legacy payroll credits expose the guarded undo action with a mandatory reason; finalized entries remain audit-locked. ## Security, Data Integrity, and Error Handling The implementation contains the following controls. Formal end-to-end release acceptance for this group is deferred. | Control | Implementation | | --- | --- | | Authentication/RBAC | Mutating payroll routes use `require_admin_state()` and payroll request guard checks. The authenticated caller must be an admin-portal user with the relevant payroll/loan permission. | | Tenant isolation | Server derives company/brand context, validates payload company/brand values, and verifies employee, loan, live-payroll, branch, and batch scope. | | Optimistic concurrency | Draft save/skip calls require the expected draft version; mismatches are rejected and require refresh. Rows are also locked for write operations. | | Idempotency | Payroll and correction journal entries use company/brand-scoped idempotency keys; retries cannot create duplicate posted repayment journals. | | Atomicity | Finalization and correction paths use database transactions and row locks. Failed posting rolls back payroll, draft, journal, and loan changes together. | | Audit | Draft and correction actions record actor, old/new amounts, reason, request ID, metadata, and timestamp. Original journals are reversed, not deleted. | | Errors | Draft routes return structured JSON with appropriate `400`, `401`, `403`, `404`, `409`, and `500` responses. Frontend callers display errors and retain user context. | ## Deployment SQL Use the consolidated migration below for a target environment: - [2026_09_25_payroll_loan_deduction_final.sql](../backend/migrations/2026_09_25_payroll_loan_deduction_final.sql) It creates the draft/audit tables and adds journal idempotency/reversal fields and indexes. It is designed to be rerunnable and does not delete or rewrite historical payroll data. The deployment-account-friendly version does not query `information_schema`; it uses `ALTER ... IF NOT EXISTS` and `SHOW TABLES` / `SHOW INDEX` verification commands. ### Deployment procedure 1. Take a verified backup of the target database. 2. Ensure the correct database is selected in phpMyAdmin or the deployment client. 3. Run the full `2026_09_25_payroll_loan_deduction_final.sql` file. 4. Confirm both draft tables are returned by the final `SHOW TABLES` statements. 5. Confirm `loan_journal_entry` has `uniq_lje_scope_idempotency`, `uniq_lje_reversal_of`, and `idx_lje_payroll_batch_status` in `SHOW INDEX` output. 6. Confirm `accounting_journal_entries` has `uniq_aje_reversal_of`. 7. Run the application smoke test below before allowing payroll finalization. Do **not** deploy local repair scripts that reference a specific batch, employee, loan, or backup table. Those files are local recovery evidence, not general production migrations. ## Operational Workflow ### Before payroll finalization 1. Open Payroll Management for the intended brand and payroll cutoff. 2. Open **Review loan deductions**. 3. Review the suggested amount, actual balance, projected balance, and draft status for each loan. 4. Select the intended rows and save drafts. 5. If a deduction must not apply, use **Skip this cutoff** and provide the required reason. 6. Confirm the main payroll preview shows the draft loan amount and recalculated net pay. At this stage, no loan balance has been reduced and no payroll loan journal has been created. ### During finalization 1. Use the payroll finalization action. 2. Review the loan-review checkpoint, especially payoff warnings and projected zero balances. 3. Finalize the complete required payroll scope. 4. The server rechecks every selected loan and posts the deductions once. 5. Confirm the payroll batch, draft state (`posted`), loan balance, and loan journal history. ### Correcting a mistake - **Unfinalized legacy posted journal:** use the Loan Journal undo action, provide a reason, and let the application restore the balance and cache. - **Finalized payroll:** use the authorized payroll void/correction action with a reason. Do not delete payroll journals directly from the database. - **Missing original loan:** restore/reconcile the loan first. The system intentionally blocks a void that cannot safely restore the corresponding balance. ## Validation Performed | Test | Result | | --- | --- | | Draft save | Passed: preview changes while real balance and payoff date remain unchanged. | | Draft edit and skip | Passed: state remains correct after refresh/recalculation. | | Over-balance and closed-loan validation | Passed: invalid deduction is rejected without state change. | | Cancelled/open-ended loan handling | Passed in rollback-only QA fixture. | | Fixed-term payoff | Passed in rollback-only QA fixture: one journal, zero balance, and cutoff date stored as `date_end`. | | Failed multi-loan posting | Passed: cancelled-loan failure leaves no fixture, draft, or journal committed. | | Retry/idempotency | Passed: a repeat finalized cutoff was blocked with no duplicate journal, draft, or finalized rows. | | Legacy undo / finalized reversal | Passed through authenticated acceptance testing with audit reasons. | | PHP lint / frontend production build / diff validation | Passed. Existing local font warnings are non-blocking. | Focused regression scripts: - `php backend/tests/payroll_loan_draft_rules_test.php` - `php backend/tests/payroll_loan_draft_posting_transaction_test.php` - `php backend/tests/payroll_loan_failed_posting_transaction_test.php` - `php backend/tests/payroll/permissions.php` ## Deferred or Follow-up Work | Item | Status | | --- | --- | | Formal tenant isolation, RBAC, concurrency, idempotency, and audit acceptance | Deferred/skipped for this execution pass. Schedule as a separate release-gate test. | | Mobile acceptance for editor, review dialog, and correction actions | Pending manual QA on target device widths. | | Payroll Loan Summary visual polish | Deferred by request; functional behavior is implemented. | ## Main Files Changed ### Backend - `backend/loan_api/get_loan_summary.php` - `backend/loan_api/update_loan_summary.php` - `backend/loan_api/remove_loan_deduction_draft.php` - `backend/loan_journal_entry_api/create_journal_entry.php` - `backend/loan_journal_entry_api/undo_unfinalized_payroll_deduction.php` - `backend/payroll/payroll_loan_draft_service.php` - `backend/payroll/loan_live_cache_sync.php` - `backend/payroll/finalize_payroll_from_live_cache.php` - `backend/payroll/void_finalized_payroll_batch.php` - `backend/payroll/save_loan_deduction.php` - `backend/payroll/sync_loan_deductions.php` - `backend/payroll/update_loan_deduction_applied.php` - `backend/migrations/2026_09_25_payroll_loan_deduction_final.sql` ### Frontend - `frontend/src/payrollPage/payrollLoanSummary/PayrollLoanEditor.jsx` - `frontend/src/payrollPage/payrollApi/PayrollLaonEditorAPIs.js` - `frontend/src/payrollPage/payrollApi/payroll_loan_summaryAPI.js` - `frontend/src/payrollPage/payrollApi/payrollapi.jsx` - `frontend/src/payrollPage/payrollComponents/LoanDetailsMultiApply.jsx` - `frontend/src/payrollPage/payrollComponents/deductionTemplate.jsx` - `frontend/src/payrollPage/payrollComponents/payrollJournalEntry.jsx` - `frontend/src/payrollPage/payrollComponents/FinalizePayrollButton.jsx` - `frontend/src/payrollPage/payrollComponents/VoidCorrectPayrollButton.jsx` - `frontend/src/payrollPage/payrollpage/PayrollPage.jsx` - `frontend/src/components/loan/LoanJournalEntries/LoanJournalEntriesTable.jsx` ## Related Records - [Execution Checklist](PAYROLL-LOAN-DEDUCTION-EXECUTION-CHECKLIST.md) - [Draft and Correction Plan](PAYROLL-LOAN-DEDUCTION-DRAFT-PLAN.md) - [Loan Journal and Payoff-Date Documentation](PAYROLL-LOAN-JOURNAL-VOID-SECURITY-20260730.md) - QOL-020 progression report and Master Tracker were updated with implementation and authenticated correction-test status.