Skip to main content

Payroll Module Overview

Orientation doc for new developers. Payroll spans three repos — web (the brain), Cloud Functions backend (thin), and the mobile app (employee-facing). This is the map; the manual is PAYROLL_LOGIC.md — a 2K-line deep dive into business rules. Read this first; reach for PAYROLL_LOGIC.md when you need rule-level detail.

TL;DR — where things live

Critical insight: most calculation lives on the web frontend, not the backend. New devs frequently look on the backend for tip math and don't find it. Backend handles persistence + anomaly detection + the auto-close cron; the engine is in src/routes/PayrollNew/utils/.


End-to-end flow

The numbered walkthrough:

  1. POS sale arrives. Toast cron pulls orders → toast-sale.plugin.ts maps to a canonical sale (tips live in CheckTotals.tipTotal) → writes to RTDB. Legacy POS providers follow the older cron-driven pattern under functions/integrations/.
  2. Mobile clock-in/out. Employee declares cash tips in Attendance/ShiftSummary/index.tsx (mobile, separate repo: PIVOT-Mobile/src/routes/Attendance/ShiftSummary/index.tsx) when useIsCashTipsEnabled returns true. Writes to Attendance/{cid}/{date}/{eid}/{shiftKey}.
  3. Web matches sales to shifts. shiftMatching.ts + salesCalculations.ts use carrier ID + time-overlap. Business-day boundary (sales after midnight assigned to previous day) from businessDay.ts.
  4. Web computes tips/cuts/hours. PayrollContext.tsx (the central state hub) orchestrates attendanceEnhancement.ts + tipOutGroupHelpers.ts + cutsCalculations.ts.
  5. Manager edits in UI. Tips/, Cuts/, Hours/ tabs. Manual matches/splits/team-ups persist to TipsModifications in RTDB.
  6. Period closes. Manually by manager OR automatically by auto-close-payroll.ts (AUTO_CLOSE_DAYS = 5). Sets PayrollStatus/{cid}/{date} → mobile blocks further edits via useIsPayrollPeriodClosed.
  7. Manager exports. ExportModal/ → format generators in export-formats/ → file download.

Architecture across the 3 repos

RepoRoleVolume
Web (src/routes/PayrollNew/)The brain. UI + all business calculations + exports.~104K LOC, 17 utility files, 8 export formats
Backend (functions/modules/payroll/)Thin: settings persistence, tip-out distribution lookup, anomaly detection, auto-close cron.17 files, ~656 LOC
Mobile (PIVOT-Mobile/src/routes/Attendance/)Employee clock-in/out + cash tips declaration. Limited math: shift duration + snap-to-schedule rounding only. No tip/cut/export computation.4 hooks + 1 modal field

Key files & structure

Web — src/routes/PayrollNew/

FolderPurpose
Tips/Daily/weekly tip entry, modals, activity log UI
Cuts/Tip-pool ("cut") summaries and activity
Hours/Shift / hours management, conflict resolution
Onboarding/Multi-step setup wizard (Hours + Tips/Cuts)
modals/Settings, Export, ConflictShift, etc.
components/Shared UI inside the feature
utils/Calculation engines (matching, tips, cuts, sales, periods)
export-formats/Format generators (Acomba, Payevo, QBO, etc.)

⚠️ Files >1000 lines — handle with care. Refactor only with the relevant Jest tests green and a manual smoke pass.

Backend — functions/modules/payroll/

functions/modules/payroll/
├── contracts/ interface files
├── endpoints/apis/payroll.router.ts 4 endpoints
├── handlers/ issues / settings / tips
├── logic/ anomaly-detection · split-settings-patch
├── repositories/ RTDB-only (no PG yet)
├── container.ts wiring
└── index.ts exports
MethodPathHandlerPurpose
GET/payroll/settingsgetPayrollSettingsMerged global + period settings
PATCH/payroll/settingspatchPayrollSettingsRoutes patch to global vs period RTDB paths
GET/payroll/tip-outslistTipsTip-out distribution entries by date range
GET/payroll/issueslistIssuesAnomalies (ghost punches, OT, short shifts, labor variance)

Cron: functions/cron/auto-close-payroll.ts — closes periods 5 days post-end.

Mobile — PIVOT-Mobile/src/routes/Attendance/

The mobile payroll surface is four hooks in Attendance/hooks.tsusePeriodAttendanceSettings, useGetAttendanceRoundingTime, useIsCashTipsEnabled, useIsPayrollPeriodClosed — plus the cash-tips field in ShiftSummary/index.tsx. Shift-duration math and snap-to-schedule rounding live in Attendance/utils.ts (getShiftLengthWithoutBreaks, getSnappedStartTime, roundTime). No tip-out math, no callable Cloud Function endpoints — all reads/writes go to RTDB.

Mobile lives in a separate repo. Paths above are not clickable from this doc.


Tip / tip-out calculation logic

A summary; full rules in PAYROLL_LOGIC.md.

  • Two ingestion paths. POS-pulled tips (Toast/Veloce/etc embedded in sale records) vs manual entry via Tips/DailyTable. → PAYROLL_LOGIC.md §2.1, §7.
  • Salary type drives the flow. wage_tips employees receive tips and cuts; salary employees do not. → §3.1.
  • Cuts pool (tip pooling). Aggregate sales × tip-out % per service period; distributed to back-of-house by hours. Engine in cutsCalculations.ts. → §3.2.
  • Tip-out groups. Managers configure groups (role membership + percentage); each group claims a slice of the pool. Engine in tipOutGroupHelpers.ts (~723 lines, has its own test file). → §5.6.
  • Reconciliation & overrides. tipsOutReconciliation.ts compares saved vs computed tip-out and flags manual overrides. Audit trail lives in TipsOutActivity and TipsActivityLog. → §4.4, §5.2.

Six more mechanisms layer on top of the basic flow — non-trivial variance from "sales × percentage":

  • Position bonus — fixed amount per position/subposition added before hourly distribution. → §3.3.
  • Fixed tip-out — per-employee fixed cut, once per period, regardless of hours. → §3.4.
  • Takeout handling — 100% of takeout tips go to the pool without the position's tipOutPercentage discount. → §3.5.
  • Team-up & split — one sale split across multiple employees/shifts; mid-shift role changes handled. → §3.6.
  • Multi-period shifts — shift spanning periods is assigned to the period with most overlap (not duplicated). → §3.7.
  • Exclusions — sales can be tagged to exclude from auto-matching. → §3.8.

For split-modal rounding and edge cases — read PAYROLL_LOGIC.md §3–§5. Don't reinvent these rules; they're load-bearing.


Data model — RTDB paths

PathWritten byRead byNotes
Attendance/{cid}/{date}/{eid}/{shiftKey}Mobile + Web HoursWeb · Backend listIssuesPunches; cashTips field is the mobile contribution
AttendanceSettings/{cid}Web SettingsWeb · Backend · MobileGlobal payroll config
PayrollPeriodSettings/{cid}/{effectiveFromDate}Web SettingsWeb · Backend · MobilePeriod-specific (rounding, cash-tips toggle, position salary types)
PayrollStatus/{cid}/{date}Web (period close), auto-close-payroll cronMobile (locks edits)Boolean value at path (true = closed)
TipsOut/{cid}/{date}/{eid}/{shiftKey}WebBackend listTips · WebTip-out distribution (fixed / role_bonus / sales_tips_out)
TipsModifications/{cid}/{date}WebWebManual matches / splits / team-ups
WeeklySchedule/{cid}/{date}/{eid}/...Schedule moduleBackend listIssues (variance check)Cross-module read
AttendanceClaims/{cid}/{date}/{shiftKey}WebWebConflict tracking for overlapping shifts
TipsActivityLog/{cid}WebWebAudit trail for manual tip modifications
TipsMatchExceptions/{cid}WebWebExceptions for failed sale-to-shift matches
TipsOutActivities/{cid}WebWebTip-out distribution change log
ExtraTipsOut/{cid}/{date}WebWebCarry-forward of unallocated cuts to next period
EmployeeRates/{cid}Employees moduleWeb (read-only)Rate overrides; not consumed by backend or mobile

Tips arrive embedded in sales records (Toast plugin pulls net/tax/gross/tip in one shot). There is no standalone "tips" RTDB ingestion path — sales paths are owned by the sales/integrations modules, not payroll.


Settings: global vs period

Settings live in two RTDB paths and the backend's splitSettingsPatch (logic/split-settings-patch.ts) routes a single PATCH to the right place:

  • Global (AttendanceSettings) — 6 keys: payrollFrequency, payrollStartingDay, employeeOrder, customEmployeeOrderIds, exportWeekMode (weekly vs biweekly export grouping), dailySortOrder.
  • Period (PayrollPeriodSettings/{effectiveFromDate}) — 13 keys including roundingTime, snapToScheduleThreshold, declareCashTips, overtimeCalculationMode, tipOutGroups (group definitions live here, not in global), tippingStructure, tipsPaymentMethod, tipOutFrequency, rolesReceiveTipOuts, plus break/threshold tuning (breakIntervals, flagUnder3Hours, lateClockOutThreshold, additionalTimePaid). See PERIOD_SETTINGS_KEYS in split-settings-patch.ts for the full list.

Why it matters: a global patch affects all history immediately. A period patch updates the latest existing effective-from row in place — a new row is only created when no period exists yet. So changing tipOutGroups mid-period rewrites the current row; it doesn't fork into a new effective-from. Check GLOBAL_SETTINGS_KEYS / PERIOD_SETTINGS_KEYS before adding a setting.


Exports

8 generators in export-formats/, formatted via export-formats/formatters/ (formatAsExcel.ts / formatAsTxt.ts). Entry point: modals/ExportModal/.

GeneratorTarget
generateAcombaExport.tsAcomba ADX (semicolon-delimited .adx text)
generatePayevolutionExport.tsPayevolution (Excel)
generateQuickBooksOnlineExport.tsQuickBooks Online
generateQuickBooksTimeExport.tsQuickBooks Time
generate-powerpay-export.tsPowerPay (XML)
generateByShiftExport.tsGeneric by-shift
generateByWeekExport.tsGeneric by-week
generatePayrollExport.tsGeneric accounting

Dependencies

Payroll consumes:

  • POS sales (sales / integration modules) — read directly from RTDB.
  • Schedule (WeeklySchedule) — for variance detection in listIssues.
  • Employees (Employees/{cid}) — frontend-only read for tip-out group membership + integration IDs (Veloce/Toast/etc) for matching. Backend payroll repos do not touch Employees/{cid}.
  • Auth / company state — for permissions and companyId.

Payroll is consumed by:

  • Mobile — reads PayrollPeriodSettings and PayrollStatus, writes cashTips.
  • Export-formats — every formatter consumes data through PayrollContext.
  • HeadOfficeDashboardexport-payroll reads PayrollPeriodSettings for multi-location exports.
  • OwnerDashboard — feature flags gate payroll features (isPayrollTabsEnabled, hasAccessToAttendance).

Cross-module communication style: synchronous HTTP (frontend → backend) + direct RTDB reads (mobile, frontend). No Pub/Sub events — sales arrive via cron pull, period close is a silent RTDB write.


Known issues & fragile areas

  • 🚩 PayrollContext.tsx dual-write — TODO: "Remove dual-write once cloud functions + mobile read from PayrollPeriodSettings". Migration in flight; touching either side without the other causes drift.
  • 🚩 DailyTable.tsx (8K lines) — daily tip entry grid carries quiet invariants encoded across rules in PAYROLL_LOGIC.md §5. Refactor only with green tests + manual smoke.
  • 🚩 Tips/Cuts/Hours code duplication — same calculation appears in Cuts/components/SummaryTable.tsx and elsewhere; TODO logged, not yet unified.
  • 🚩 Calculation lives on the frontend — closing a period or running a re-export depends on client compute. Backend can't easily take over without a significant refactor.
  • 🚩 RTDB → Postgres migration — backend payroll repositories are still all Rtdb*. Frontend reads RTDB directly in many places. No agreed cut line yet for which module owns which path.
  • 🚩 No Pub/Sub events — period close, settings changes, cash-tips writes are silent. Cross-module consumers (notifications, etc.) must poll or piggyback on RTDB triggers.
  • 🚩 No backend tests under functions/modules/payroll/. Test safety net is frontend Jest (utils/__tests__/, export-formats/__tests__/) + Playwright e2e. The auto-close cron has 1 test.
  • 🚩 Heterogeneous POS legacy integrations — Toast uses the new plugin engine; Veloce / MaitreD / Lightspeed / Clover / MYR / Cluster / Givex / Libro / Nethris-EmployerD are cron-driven snowflakes under functions/integrations/. Tip-data shape varies subtly per provider.
  • 🚩 Auto-close cron lacks transaction isolationauto-close-payroll.ts does a bulk RTDB update() with no transaction wrapper. If a manager closes manually at the same instant the cron fires, RTDB last-write-wins (no data corruption, but ordering may surprise).
  • 🚩 Hard-coded America/Toronto timezone in auto-close-payroll.ts — period-end detection will drift for non-Toronto companies if Pivot ever supports other regions.

Testing

  • Frontend Jest — 17 test files total. Tests sit beside source files (not under __tests__/ for utils):
    • utils/*.test.ts (9): businessDay, shiftMatching, periodHelpers, getClosestPayPeriodStart, autoMatchPersistence, summaryCalculations, tipOutGroupHelpers, tipsConflicts, tipsOutReconciliation.
    • export-formats/__tests__/*.test.ts (3): PowerPay, ByShift, generic Payroll.
    • export-formats/formatters/__tests__/ (2) + export-formats/utils/__tests__/ (1).
    • Hours/utils/shiftValidation.test.ts (1) + Onboarding/TipsAndCut/utils/__tests__/ (1).
    • ⚠️ Acomba, Payevolution, QBO, QB Time generators have no tests.
    • Run the relevant test before refactoring tipOutGroupHelpers, cutsCalculations, or any export generator.
  • Backend — only functions/cron/tests/auto-close-payroll.test.ts. None for the 4 payroll endpoints (settings, tip-outs, issues).
  • Playwright e2e in e2e/ covers auth / schedule / navigation. No dedicated payroll specs today — only navigation guards touch payroll routes tangentially.
  • No mobile tests for the cash-tips path or any Attendance/ShiftSummary flow. (Mobile-wide untyped surface: ~107 \bany\b + 8 @ts-ignore sites in PIVOT-Mobile/src/.)

Where to start when…

Your ticket is aboutStart here
Tip entry UI bugTips/components/DailyTable.tsx + TipModal/
Tip-out mathPayrollContext.tsx (orchestrator — invokes helpers) → utils/tipOutGroupHelpers.ts (engine) + PAYROLL_LOGIC.md §3.2, §5.6
Sales not matchingutils/shiftMatching.ts + salesCalculations.ts + PAYROLL_LOGIC.md §2
New POS providerfunctions/integration-engine/domains/pos/providers/ (use Toast as template)
New export formatexport-formats/ + export-formats/formatters/ (no test fixtures; see generatePayrollExport.test.ts for mock setup pattern)
Settings missing on a periodUI bug → modals/SettingsModal/. Global-vs-period routing → logic/split-settings-patch.ts (check key in correct list).
Mobile cash tipsData/persistence bug → PIVOT-Mobile/src/routes/Attendance/hooks.ts (useIsCashTipsEnabled). Display/UI bug → PIVOT-Mobile/src/routes/Attendance/ShiftSummary/index.tsx.
Period not auto-closingfunctions/cron/auto-close-payroll.ts
Anomaly false positivelogic/anomaly-detection.ts
Period not showing in UIcomponents/HeaderCard/ + utils/periodHelpers.ts (period math) + businessDay.ts (boundary)
Tips from POS look wrongToast mapper in toast-sale.plugin.ts (tip extraction) → utils/salesCalculations.ts (aggregation)
Employee missing from payrollPayrollContext.tsx employee filtering + Employees/{cid} salary type (wage_tips vs salary)