# Frappe HR Integration Plan

Replace Gusto with Frappe HR for HR + payroll, reusing the existing Frappe finance/GL integration pattern.

- **Branch:** `csdecatur`
- **Sync direction:** Missio → Frappe (one-way, same as finance)
- **Scope:** Both one-time import of existing records AND ongoing sync on write
- **Feature flag:** `payroll_provider` on company (`gusto` | `frappe` | `both`)

---

## Existing integration to reuse

- **Client:** `app/Services/Frappe/FrappeClient.php` — token auth (`Authorization: token {key}:{secret}`), full DocType CRUD, submit/cancel, RPC
- **Config:** `config/frappe.php` + `CompanyFrappeSetting` (encrypted `api_secret`)
- **Pattern:** Observer → `SyncToFrappe` job → Poster → FrappeClient (retry 3×, backoff 10/30/90s)
- **Already live:** Invoices, Vendors, Expenses, Bank Entries, Payments, Journal Entries, Budgets
- **Payroll GL bridge (keep):** `app/Services/Gl/PayrollGlBridge.php` posts approved `PayrollRun` → Frappe `Journal Entry`

## Critical gap (flagged, do not block)

Frappe HR does NOT do US federal/state tax filings, W-2s, 1095s, 941s, garnishment remittance, or ACH file generation. Gusto does. Strategy:

- **Phases 0–6 below** cover HR masters, leave/attendance, salary structure, payroll runs, salary slips, onboarding, time management.
- **Tax/compliance + ACH + check printing stay local** (see "Local-only" list).
- **Final tax-engine decision** (Symmetry / Vertex / ADP SmartCompliance / keep Gusto for tax-only) is a separate workstream — surface to stakeholder before cutover.

---

## Page → Frappe DocType map

🟢 Full sync · 🟡 Transform · 🔵 Derived · ⚫ Local-only

### Human Resources

| Page | URL | Frappe DocType | Sync |
|---|---|---|---|
| Employees | `/account/employees` | `Employee` | 🟢 |
| Appreciations | `/account/appreciations` | `Energy Point Log` / custom | 🟡 |
| Leave | `/account/leaves` | `Leave Application` + `Leave Allocation` | 🟢 |
| Shifts | `/account/shifts` | `Shift Type` + `Shift Assignment` | 🟢 |
| Attendance | `/account/attendances` | `Attendance` | 🟢 |
| Holidays | `/account/holidays` | `Holiday List` | 🟢 |
| Positions | `/account/designations` | `Designation` | 🟢 |
| Org Chart | `/account/org-chart` | — (auto from `Employee.reports_to`) | 🔵 |
| Departments | `/account/departments` | `Department` | 🟢 |
| New Hire Onboarding | `/account/new-hire-onboarding` | `Employee Onboarding` | 🟢 |
| Position Control | `/account/positions` | `Staffing Plan` | 🟡 |
| Employee Benefits | `/account/benefits` | `Employee Benefit Application` | 🟡 |
| Background Check | `/account/background-checks` | — | ⚫ |

### Time Management

| Page | URL | Frappe DocType | Sync |
|---|---|---|---|
| Projects | `/account/projects` | `Project` | 🟢 |
| Tasks | `/account/tasks` | `Task` | 🟢 |
| Timesheet | `/account/timelogs` | `Timesheet` | 🟢 |
| PTO Accrual Policies | `/account/pto-accruals` | `Leave Policy` + `Leave Type` | 🟢 |
| Accrual Entries | `/account/pto-accruals/entries` | `Leave Allocation` | 🟢 |

### Payroll

**Processing**

| Page | URL | Frappe DocType | Sync |
|---|---|---|---|
| Dashboard & Runs | `/account/payroll/dashboard` | `Payroll Entry` | 🟢 (replaces Gusto) |
| Pay Calendars | `/account/pay-calendars` | `Payroll Period` | 🟡 |
| Import Data | `/account/payroll-imports` | — (staging only) | ⚫ |
| Substitute Payroll | `/account/substitute-payroll` | `Additional Salary` | 🟢 |
| Fund Allocations | `/account/fund-allocations` | custom field on `Employee` | 🟡 |

**Compensation**

| Page | URL | Frappe DocType | Sync |
|---|---|---|---|
| Salary Schedules | `/account/salary-schedules` | `Salary Structure` | 🟡 transform step-bands |
| Salary Changes | `/account/salary-changes` | `Salary Structure Assignment` | 🟢 on approval |
| Additional Payments | `/account/additional-payments` | `Additional Salary` | 🟢 |
| Gross-Up Calculator | `/account/gross-up` | `Additional Salary` | 🟡 calc local, push result |

**Deductions**

| Page | URL | Frappe DocType | Sync |
|---|---|---|---|
| Deduction Codes | `/account/deduction-codes` | `Salary Component` (type=Deduction) | 🟢 |
| Garnishments | `/account/garnishments` | `Additional Salary` (negative) | 🟡 |

**Adjustments**

| Page | URL | Frappe DocType | Sync |
|---|---|---|---|
| Payroll Adjustments | `/account/payroll-adjustments` | `Additional Salary` | 🟢 |
| Retro Pay | `/account/retro-pay` | `Additional Salary` | 🟢 |

**Disbursements**

| Page | URL | Frappe DocType | Sync |
|---|---|---|---|
| ACH & Check Printing | `/account/payroll-disbursements` | — | ⚫ |
| Direct Deposit | `/account/direct-deposit` | custom on `Employee` | 🟡 |
| Pay Stubs | `/account/paychecks` | `Salary Slip` | 🟢 |

**GL & Accruals** (already synced via `PayrollGlBridge`)

| Page | URL | DocType |
|---|---|---|
| Journal Entries | `/account/payroll-journal-entries` | `Journal Entry` ✅ |
| Payroll Accruals | `/account/payroll-accruals` | `Journal Entry` ✅ |
| PTO Liability | `/account/payroll-accruals/pto-liability` | `Journal Entry` ✅ |

**Audit / Controls / Documents / Settings** — all ⚫ local-only except Payroll Settings (🟡 partial mirror to Frappe `Payroll Settings` singleton).

### Local-only (never touches Frappe)

Appreciations · Background Checks · Import Data staging · ACH & Check Printing · Payroll Audit · Trial Runs · Blackout Periods · Audit Logs · Documents & Compliance (W-2 / 1095 / 941)

---

## Gusto URLs being replaced

| Current | New |
|---|---|
| `payroll/{id}/submit-to-gusto` | `payroll/{id}/submit-to-frappe` |
| `new-hire-onboarding/{userId}/sync-gusto` | `new-hire-onboarding/{userId}/sync-frappe` |
| `gusto-settings` | `frappe-hr-settings` |
| `gusto-settings/connect` / `callback` / `manual-connect` | — (removed; Frappe reuses finance API key) |
| `gusto-settings/disconnect` | `frappe-hr-settings/disconnect` |
| `gusto-settings/sync` | `frappe-hr-settings/sync` |
| `gusto-settings/employees` | `frappe-hr-settings/employees` |
| `gusto-settings/onboard-all` | `frappe-hr-settings/bulk-import/employees` |
| `gusto-settings/import-employees` | — (removed; one-way) |
| `gusto-settings/remap-employees` | `frappe-hr-settings/remap-employees` |
| `gusto-settings/webhook` | — (removed; one-way) |
| `benefits/sync-from-gusto` | — (removed; one-way) |

`gusto_*` DB columns: keep as historical reference, stop writing.

---

## Phased implementation (~4 weeks)

### Phase 0 — Settings + infrastructure (2 days)

- [ ] Extend `CompanyFrappeSetting` with `hr_enabled`, `hr_company_abbr`, `default_payroll_frequency`, `default_holiday_list` (OR new `CompanyFrappeHrSetting` model)
- [ ] New settings page at `/account/payroll/frappe-hr-settings`
  - Controller: `app/Http/Controllers/FrappeHrSettingsController.php` (model on `GustoSettingsController`)
  - View: `resources/views/payroll/frappe-hr-settings/index.blade.php`
- [ ] Add `payroll_provider` enum column on `companies` (gusto|frappe|both)
- [ ] Migration: add sync columns to each table synced (see "DB schema" below)
- [ ] Menu: `resources/views/sections/menus/payroll.blade.php` — add Frappe HR Settings entry

### Phase 1 — Foundational masters (3–5 days)

Order enforces dependencies: **Department → Designation → Holiday List → Employee → Shift Type**.

For each: Poster + Observer + registration in `AppServiceProvider::boot()`. Bulk-import route per type.

- [ ] `FrappeDepartmentPoster` (`Team` model → `Department`)
- [ ] `FrappeDesignationPoster` (`Designation` → `Designation`)
- [ ] `FrappeHolidayListPoster` (`Holiday` grouped by company/year → `Holiday List`)
- [ ] `FrappeEmployeePoster` (`EmployeeDetails` → `Employee`)
- [ ] `FrappeShiftTypePoster` (`EmployeeShift` shift definitions → `Shift Type`)

### Phase 2 — Leave + Attendance (3 days)

- [ ] `FrappeLeaveTypePoster` (`LeaveType` → `Leave Type`)
- [ ] `FrappeLeavePolicyPoster` (PTO accrual policies → `Leave Policy`)
- [ ] `FrappeLeaveAllocationPoster` (`EmployeeLeaveQuota` → `Leave Allocation`)
- [ ] `FrappeLeaveApplicationPoster` (`Leave` → `Leave Application`)
- [ ] `FrappeShiftAssignmentPoster` (`EmployeeShiftSchedule` → `Shift Assignment`)
- [ ] `FrappeAttendancePoster` (`Attendance` → `Attendance`)

### Phase 3 — Compensation setup (3–5 days)

- [ ] `FrappeSalaryComponentPoster` (`DeductionCode` + earning codes → `Salary Component`)
- [ ] `FrappeSalaryStructurePoster` — transforms Missio step-based `SalarySchedule` into flat Frappe `Salary Structure` (one structure per schedule/step combo, or one per schedule with step as base amount)
- [ ] `FrappeSalaryStructureAssignmentPoster` (`EmployeeSalaryPlacement` + `SalaryChangeRequest` on approval → `Salary Structure Assignment`)

### Phase 4 — Payroll transactions (5 days)

- [ ] `FrappeAdditionalSalaryPoster` — covers Additional Payments, Substitute Payroll, Retro Pay, Gross-Up results, Payroll Adjustments, Garnishments (negative amount)
- [ ] `FrappePayrollEntryPoster`
  - Replaces `submitToGusto()` → new `submitToFrappe()` on `PayrollController`
  - Creates `Payroll Entry` header, then RPC `erpnext.payroll.doctype.payroll_entry.payroll_entry.create_salary_slips` to fan out slips
  - Reconciles returned Salary Slip names back to `Paycheck.frappe_salary_slip_name`
- [ ] `FrappeSalarySlipPoster` — fallback for manual paycheck edits
- [ ] Route change: `payroll/{id}/submit-to-gusto` → `payroll/{id}/submit-to-frappe`
- [ ] View: update `resources/views/payroll/*/show.blade.php` submit button label + sync status badge

### Phase 5 — Onboarding + Time Management (3 days)

- [ ] `FrappeEmployeeOnboardingPoster` (`NewHireOnboarding` → `Employee Onboarding`)
- [ ] `FrappeProjectPoster` (`Project` → `Project`)
- [ ] `FrappeTaskPoster` (`Task` → `Task`)
- [ ] `FrappeTimesheetPoster` (`ProjectTimeLog` → `Timesheet`)
- [ ] Route change: `new-hire-onboarding/{id}/sync-gusto` → `sync-frappe`

### Phase 6 — Cutover (2 days)

- [ ] Disable Gusto webhook route
- [ ] 410-gone the `submit-to-gusto` route (or alias to Frappe)
- [ ] Run full reconciliation: every active `EmployeeDetails` has `frappe_employee_name`; every pending `PayrollRun` is Frappe-ready
- [ ] Docs: update `resources/views/payroll/*` submit-flow help text
- [ ] Flip `payroll_provider = frappe` per company
- [ ] Leave `gusto_*` columns in place (read-only historical reference)

---

## DB schema changes

New columns (one migration per table, or one combined migration):

| Table | New columns |
|---|---|
| `employee_details` | `frappe_employee_name`, `frappe_sync_status`, `frappe_last_synced_at`, `frappe_sync_error` |
| `teams` (departments) | `frappe_department_name`, `frappe_sync_status` |
| `designations` | `frappe_designation_name`, `frappe_sync_status` |
| `holidays` | `frappe_holiday_list`, `frappe_sync_status` (grouped per year/company) |
| `leaves` | `frappe_leave_name`, `frappe_sync_status` |
| `leave_types` | `frappe_leave_type_name`, `frappe_sync_status` |
| `employee_leave_quotas` | `frappe_leave_allocation_name`, `frappe_sync_status` |
| `attendances` | `frappe_attendance_name`, `frappe_sync_status` |
| `employee_shifts` | `frappe_shift_type_name`, `frappe_sync_status` |
| `employee_shift_schedules` | `frappe_shift_assignment_name`, `frappe_sync_status` |
| `deduction_codes` | `frappe_salary_component_name`, `frappe_sync_status` |
| `salary_schedules` | `frappe_salary_structure_name`, `frappe_sync_status` |
| `employee_salary_placements` | `frappe_assignment_name`, `frappe_sync_status` |
| `additional_payments` | `frappe_additional_salary_name`, `frappe_sync_status` |
| `garnishments` | `frappe_additional_salary_name`, `frappe_sync_status` |
| `payroll_adjustments` | `frappe_additional_salary_name`, `frappe_sync_status` |
| `retro_pays` | `frappe_additional_salary_name`, `frappe_sync_status` |
| `payroll_runs` | `frappe_payroll_entry_name` (note: `frappe_voucher_name` already exists for JE) |
| `paychecks` | `frappe_salary_slip_name`, `frappe_sync_status` |
| `new_hire_onboardings` | `frappe_onboarding_name`, `frappe_sync_status` |
| `projects` | `frappe_project_name`, `frappe_sync_status` |
| `tasks` | `frappe_task_name`, `frappe_sync_status` |
| `project_time_logs` | `frappe_timesheet_name`, `frappe_sync_status` |

New/extended settings table: extend `company_frappe_settings` with `hr_enabled` (bool), `hr_company_abbr`, `default_payroll_frequency`, `default_holiday_list`.

New column on `companies`: `payroll_provider` ENUM(`gusto`,`frappe`,`both`) DEFAULT `gusto`.

---

## API reference — Frappe endpoints

Base: `{FRAPPE_BASE_URL}`. Auth: `Authorization: token {api_key}:{api_secret}`.

### Standard REST per DocType

```
GET    /api/resource/{DocType}                    # list (supports filters, fields, limit_page_length, order_by)
GET    /api/resource/{DocType}/{name}             # read
POST   /api/resource/{DocType}                    # create
PUT    /api/resource/{DocType}/{name}             # update
DELETE /api/resource/{DocType}/{name}             # delete
POST   /api/method/frappe.client.submit           # submit (body: {"doc": {...}})
POST   /api/method/frappe.client.cancel           # cancel (body: {"doctype":"...","name":"..."})
```

### DocType payloads

**Department** — `POST /api/resource/Department`
```json
{ "department_name": "Instruction", "parent_department": "All Departments",
  "company": "CSD", "is_group": 0 }
```

**Designation** — `POST /api/resource/Designation`
```json
{ "designation_name": "Teacher" }
```

**Holiday List** — `POST /api/resource/Holiday List`
```json
{ "holiday_list_name": "CSD 2026",
  "from_date": "2026-01-01", "to_date": "2026-12-31",
  "holidays": [{ "holiday_date": "2026-07-04", "description": "Independence Day", "weekly_off": 0 }] }
```

**Employee** — `POST /api/resource/Employee`
```json
{
  "employee_name": "Jane Doe", "first_name": "Jane", "last_name": "Doe",
  "gender": "Female", "date_of_birth": "1985-03-15",
  "date_of_joining": "2023-08-01",
  "company": "CSD", "department": "Instruction", "designation": "Teacher",
  "reports_to": "HR-EMP-0005", "employment_type": "Full-time", "status": "Active",
  "personal_email": "jane@example.com", "holiday_list": "CSD 2026"
}
```

**Shift Type** / **Shift Assignment**
```json
{ "name": "Morning", "start_time": "08:00:00", "end_time": "17:00:00" }
{ "employee": "HR-EMP-0012", "shift_type": "Morning", "start_date": "2026-04-17" }
```

**Leave Type** / **Leave Allocation** / **Leave Application**
```json
{ "leave_type_name": "Sick Leave", "max_leaves_allowed": 10, "is_paid_leave": 1 }

{ "employee": "HR-EMP-0012", "leave_type": "Annual",
  "from_date": "2026-01-01", "to_date": "2026-12-31", "new_leaves_allocated": 20 }

{ "employee": "HR-EMP-0012", "leave_type": "Sick Leave",
  "from_date": "2026-04-20", "to_date": "2026-04-22",
  "total_leave_days": 3, "status": "Approved" }
```

**Attendance** — submit after create
```json
{ "employee": "HR-EMP-0012", "attendance_date": "2026-04-17",
  "status": "Present", "in_time": "08:00:00", "out_time": "17:00:00",
  "working_hours": 8, "shift": "Morning" }
```

**Salary Component** (deduction codes + earnings)
```json
{ "salary_component": "Federal Tax", "salary_component_abbr": "FT",
  "type": "Deduction", "is_tax_applicable": 1 }
```

**Salary Structure**
```json
{ "name": "Teacher Step 1", "company": "CSD", "payroll_frequency": "Monthly",
  "earnings":   [{ "salary_component": "Basic", "amount": 4500 }],
  "deductions": [{ "salary_component": "Federal Tax", "amount": 500 }],
  "is_active": "Yes" }
```

**Salary Structure Assignment**
```json
{ "employee": "HR-EMP-0012", "salary_structure": "Teacher Step 1",
  "from_date": "2026-05-01", "base": 54000 }
```

**Additional Salary** (payments, substitute, retro, gross-up, garnishments, adjustments)
```json
{ "employee": "HR-EMP-0012", "salary_component": "Bonus",
  "amount": 500, "payroll_date": "2026-05-15",
  "overwrite_salary_structure_amount": 0 }
```

**Payroll Entry** + fan-out salary slips (replaces Gusto submit)
```json
POST /api/resource/Payroll Entry
{ "company": "CSD", "posting_date": "2026-04-30",
  "start_date": "2026-04-01", "end_date": "2026-04-30",
  "payroll_frequency": "Monthly", "payment_account": "Cash - CSD" }

POST /api/method/erpnext.payroll.doctype.payroll_entry.payroll_entry.create_salary_slips
{ "name": "HR-PRL-0042" }

POST /api/method/erpnext.payroll.doctype.payroll_entry.payroll_entry.submit_salary_slips
{ "name": "HR-PRL-0042" }
```

**Salary Slip** (manual)
```json
{ "employee": "HR-EMP-0012", "payroll_entry": "HR-PRL-0042",
  "start_date": "2026-04-01", "end_date": "2026-04-30",
  "earnings": [...], "deductions": [...] }
```

**Employee Onboarding**
```json
{ "employee_name": "Jane Doe", "job_applicant": "...",
  "date_of_joining": "2026-05-01", "company": "CSD",
  "designation": "Teacher", "department": "Instruction",
  "activities": [{ "activity_name": "I-9 verification", "role": "HR User",
                    "required_for_employee_creation": 1 }] }
```

**Project / Task / Timesheet**
```json
{ "project_name": "Math Curriculum Review", "company": "CSD", "expected_start_date": "2026-04-20" }

{ "subject": "Write assessment items", "project": "PROJ-0001", "status": "Open" }

{ "employee": "HR-EMP-0012", "company": "CSD",
  "time_logs": [{ "activity_type": "Teaching", "from_time": "2026-04-17 08:00:00",
                   "to_time": "2026-04-17 12:00:00", "hours": 4,
                   "project": "PROJ-0001", "task": "TASK-0001" }] }
```

---

## Missio-side new routes

```php
// routes/payroll.php — add under the existing payroll group

// Frappe HR settings
Route::get('frappe-hr-settings',                     [FrappeHrSettingsController::class, 'index'])->name('frappe-hr-settings.index');
Route::post('frappe-hr-settings/save',               [FrappeHrSettingsController::class, 'save'])->name('frappe-hr-settings.save');
Route::post('frappe-hr-settings/disconnect',         [FrappeHrSettingsController::class, 'disconnect'])->name('frappe-hr-settings.disconnect');
Route::post('frappe-hr-settings/sync',               [FrappeHrSettingsController::class, 'syncNow'])->name('frappe-hr-settings.sync');
Route::get('frappe-hr-settings/employees',           [FrappeHrSettingsController::class, 'employees'])->name('frappe-hr-settings.employees');
Route::post('frappe-hr-settings/remap-employees',    [FrappeHrSettingsController::class, 'remapEmployees'])->name('frappe-hr-settings.remap-employees');
Route::post('frappe-hr-settings/bulk-import/{type}', [FrappeHrSettingsController::class, 'bulkImport'])->name('frappe-hr-settings.bulk-import');
Route::post('frappe-hr-settings/retry-failed/{type}',[FrappeHrSettingsController::class, 'retryFailed'])->name('frappe-hr-settings.retry-failed');
Route::get('frappe-hr-settings/sync-log',            [FrappeHrSettingsController::class, 'syncLog'])->name('frappe-hr-settings.sync-log');

// Replacements for Gusto routes
Route::post('payroll/{payrollRun}/submit-to-frappe', [PayrollController::class, 'submitToFrappe'])->name('payroll.submit_to_frappe');
Route::post('new-hire-onboarding/{userId}/sync-frappe', [NewHireOnboardingController::class, 'syncToFrappe'])->name('new-hire-onboarding.sync-frappe');
```

Bulk-import `{type}` values: `departments` | `designations` | `holidays` | `employees` | `shifts` | `leave-types` | `leave-allocations` | `salary-components` | `salary-structures` | `projects` | `tasks`

---

## Poster class inventory (new files)

```
app/Services/Frappe/Posters/
├── FrappeDepartmentPoster.php
├── FrappeDesignationPoster.php
├── FrappeHolidayListPoster.php
├── FrappeEmployeePoster.php
├── FrappeShiftTypePoster.php
├── FrappeShiftAssignmentPoster.php
├── FrappeLeaveTypePoster.php
├── FrappeLeavePolicyPoster.php
├── FrappeLeaveAllocationPoster.php
├── FrappeLeaveApplicationPoster.php
├── FrappeAttendancePoster.php
├── FrappeSalaryComponentPoster.php
├── FrappeSalaryStructurePoster.php
├── FrappeSalaryStructureAssignmentPoster.php
├── FrappeAdditionalSalaryPoster.php
├── FrappePayrollEntryPoster.php
├── FrappeSalarySlipPoster.php
├── FrappeEmployeeOnboardingPoster.php
├── FrappeProjectPoster.php
├── FrappeTaskPoster.php
└── FrappeTimesheetPoster.php
```

Observers (pair one to one with model):

```
app/Observers/
├── FrappeEmployeeObserver.php          → EmployeeDetails
├── FrappeDepartmentObserver.php        → Team
├── FrappeDesignationObserver.php       → Designation
├── FrappeHolidayObserver.php           → Holiday
├── FrappeLeaveObserver.php             → Leave
├── FrappeLeaveTypeObserver.php         → LeaveType
├── FrappeAttendanceObserver.php        → Attendance
├── FrappeEmployeeShiftObserver.php     → EmployeeShift / EmployeeShiftSchedule
├── FrappeDeductionCodeObserver.php     → DeductionCode
├── FrappeSalaryScheduleObserver.php    → SalarySchedule
├── FrappeSalaryPlacementObserver.php   → EmployeeSalaryPlacement
├── FrappeAdditionalPaymentObserver.php → AdditionalPayment / PayrollAdjustment / Garnishment / RetroPay
├── FrappePayrollRunObserver.php        → PayrollRun (augment existing)
├── FrappePaycheckObserver.php          → Paycheck
├── FrappeOnboardingObserver.php        → NewHireOnboarding
├── FrappeProjectObserver.php           → Project
├── FrappeTaskObserver.php              → Task
└── FrappeTimesheetObserver.php         → ProjectTimeLog
```

Register in `app/Providers/AppServiceProvider.php::boot()`.

---

## One-time bulk import approach

For each DocType, controller method chunks the source table and dispatches `SyncToFrappe` jobs in batches of 50:

```php
EmployeeDetails::where('company_id', $companyId)
    ->whereNull('frappe_employee_name')
    ->chunkById(50, function ($chunk) {
        foreach ($chunk as $emp) {
            SyncToFrappe::dispatch('employee', $emp->id, 'create');
        }
    });
```

Dependency order for first-time import of a company:

1. `departments` → `designations` → `holidays`
2. `employees` (requires 1)
3. `leave-types` → `leave-allocations`
4. `shifts` → shift assignments
5. `salary-components` → `salary-structures` → salary-structure-assignments
6. `projects` → `tasks`
7. historical: `attendances`, `leave-applications`, `additional-salaries`, `salary-slips` (optional backfill)

UI shows per-type counts: total / synced / failed / pending, with "Retry failed" and "Resync all" buttons.

---

## UI changes

| File | Change |
|---|---|
| `resources/views/payroll/gusto-settings/index.blade.php` | Rename/clone to `frappe-hr-settings/index.blade.php`; same tabs/layout |
| `resources/views/payroll/*/show.blade.php` (payroll run) | "Submit to Gusto" button → "Submit to Frappe"; show `frappe_payroll_entry_name` + status badge |
| `resources/views/employees/show.blade.php` | Frappe sync status badge (pattern exists on invoices) |
| `resources/views/sections/menus/payroll.blade.php` | "Gusto Settings" → "Frappe HR Settings" |
| New Hire Onboarding wizard (final step view) | "Sync to Gusto" → "Sync to Frappe" |

---

## Open decisions

1. **Tax/compliance path** — which option? (Symmetry / Vertex / ADP / keep Gusto tax-only)
2. **Salary Structure transform** — one structure per schedule-step combo, OR one structure per schedule with step as assignment `base`? (Recommend the second: fewer structures, cleaner.)
3. **Historical backfill** — sync old `Attendance` / `Leave` / `Paycheck` records, or only go-forward? (Suggest go-forward for transactions, full backfill for masters.)
4. **Frappe company naming** — one Frappe company per Missio company, or shared? (Reuse finance setting.)

---

## Start here tomorrow

1. Read [app/Services/Frappe/FrappeClient.php](app/Services/Frappe/FrappeClient.php), [app/Services/Frappe/Posters/FrappeSupplierPoster.php](app/Services/Frappe/Posters/FrappeSupplierPoster.php), and [app/Services/Gl/PayrollGlBridge.php](app/Services/Gl/PayrollGlBridge.php) for the existing pattern.
2. Build Phase 0: settings controller, view, migration for sync columns (do all tables in one migration).
3. Build Phase 1 in dependency order: Department → Designation → Holiday → Employee → Shift Type. Each = Poster + Observer + bulk-import handler.
4. Smoke-test against local Frappe: create one department, verify it appears in Frappe UI, then scale up.
