Harvest Time Tracking Integration
The ticket system integrates with Harvest for time tracking, project management, and budget monitoring. Time entries from Harvest are synced back to tickets and displayed in the timeline. Staff can also log time directly from the ticket page via per-staff OAuth2.
Overview
| Component | Details |
|---|---|
| API Client | jolicode/harvest-php-api (Composer — sync cron), direct REST (OAuth time entry) |
| Sync File | include/custom/class-harvest.php (Suma\Harvest) |
| OAuth File | include/class.harvest-oauth.php (HarvestOAuth) |
| Time Entry File | include/class.harvest-time-entry.php (HarvestTimeEntry) |
| AJAX Controller | include/ajax.harvest.php (HarvestAjaxAPI) |
| Sync Cron | tools/harvest-times-used-import.php |
| Panel Template | include/staff/harvest-time-panel.inc.php |
| Frontend JS | custom/src/js/HarvestTimeEntry.js |
| Budget Fields | Custom form fields on tickets |
Direct Time Entry (Per-Staff OAuth)
Architecture
Staff authenticate via OAuth2 to get their own Harvest tokens. Time is logged directly from the reply/note forms on the ticket page.
Staff opens ticket → checks harvest_project_id field
→ If project exists AND staff has OAuth connection → show harvest panel
→ Panel renders in both reply and note forms
Timer flow:
Start Timer → POST /v2/time_entries (no hours = running timer)
Stop Timer → PATCH /v2/time_entries/{id}/stop
Manual flow:
Staff enters hours → POST /v2/time_entries (with hours field)
On reply/note submit:
→ scp/tickets.php processes harvest POST data
→ Timer: stopEntry() → updateEntry() (push notes) → finalizeEntry()
→ Manual: submitCompletedEntry() → finalizeEntry()
OAuth2 Flow
Staff clicks "Connect to Harvest"
→ GET /scp/ajax.php/harvest/connect-url
→ Returns Harvest authorization URL with state token
→ Staff authenticates at Harvest → redirects to callback
→ scp/harvest-oauth-callback.php exchanges code for tokens
→ Tokens stored in ost_staff_harvest_oauth
Token refresh is automatic — getAccessToken() refreshes within 600s of expiry.
Configuration (namespace: harvest_api)
| Key | Default | Description |
|---|---|---|
enabled | false | Master on/off switch |
client_id | '' | Harvest OAuth app Client ID |
client_secret | '' | Harvest OAuth app Client Secret |
redirect_uri | '' | OAuth callback URL |
account_id | '593667' | Harvest Account ID |
default_task_id | '5611181' | Default task (Development) |
gemini_note_enabled | true | Enable AI note optimization |
Database Tables
ost_staff_harvest_oauth — Per-staff OAuth tokens
| Column | Type | Description |
|---|---|---|
staff_id | INT UNIQUE | Staff member |
access_token | TEXT | Harvest OAuth access token |
refresh_token | TEXT | Refresh token for renewal |
token_expires_at | DATETIME | When access token expires |
harvest_user_id | INT | User's Harvest user ID |
ost_staff_harvest_time_log — Local log of all time entries submitted
| Column | Type | Description |
|---|---|---|
ticket_id | INT | osTicket ticket ID |
staff_id | INT | Staff who logged time |
harvest_entry_id | BIGINT UNIQUE | Harvest time entry ID |
hours | DECIMAL(8,2) NULL | Hours logged (NULL while running) |
notes | TEXT NULL | Time entry note text |
ai_optimized | TINYINT(1) | 1 = note was AI-generated |
thread_entry_id | INT NULL | Associated thread entry (reply/note) |
is_running | TINYINT(1) | 1 = timer currently running |
AJAX Endpoints
| Endpoint | Method | Description |
|---|---|---|
GET /scp/ajax.php/harvest/status | oauthStatus() | Check OAuth connection status |
GET /scp/ajax.php/harvest/connect-url | connectUrl() | Get OAuth authorization URL |
POST /scp/ajax.php/harvest/disconnect | disconnect() | Remove OAuth connection |
GET /scp/ajax.php/harvest/tasks/{project_id} | getTaskAssignments() | List project tasks |
POST /scp/ajax.php/harvest/optimize-note | optimizeNote() | AI note optimization |
POST /scp/ajax.php/harvest/timer/start | startTimer() | Start running timer |
POST /scp/ajax.php/harvest/timer/{entry_id}/stop | stopTimer() | Stop running timer |
GET /scp/ajax.php/harvest/timer/{project_id} | getRunningTimer() | Check for running timer |
PATCH /scp/ajax.php/harvest/timer/{entry_id} | updateTimerEntry() | Update timer notes |
AI Note Optimization
Uses shared gemini_api_key from AI Assistant config. Summarizes reply/note text into a concise 2–8 word time entry description.
Migration
-- Run once to create tables and config
deploy/deploy-harvest-oauth.sql
Features
Project Creation
When a ticket is assigned, a Harvest project can be created:
- Project name derived from ticket subject
- Client association based on organization
- Budget hours set from custom field
budget_hours
Time Entry Sync
The import cron pulls time entries from Harvest and correlates them with tickets:
Harvest API → GET /time_entries (filtered by project)
→ Match entries to ticket via project ID
→ Store in ost_ticket_harvest_entries
→ Display on ticket timeline
→ Calculate total hours used vs budget
Budget Tracking
Each ticket can have budget-related custom fields:
- Budget Hours — Total allocated hours
- Quoted Hours — Original quote given to client
- Hours Used — Calculated from Harvest entries
Timeline Events
Harvest time entries appear as events in the ticket timeline alongside thread entries. Events are ordered by timestamp (not ID) to maintain chronological accuracy.
Configuration
Harvest API Credentials
Configured in include/ost-config.php or via the admin panel:
| Setting | Purpose |
|---|---|
| Account ID | Harvest account identifier |
| Access Token | Personal access token for API |
| User Agent | Required by Harvest API (app-name (email)) |
Custom Form Fields
| Field | Purpose |
|---|---|
harvest_project_id | Links ticket to Harvest project |
budget_hours | Allocated budget hours |
quoted_hours | Hours quoted to client |
Sync Process
Import Cron (tools/harvest-times-used-import.php)
Runs periodically to sync time data:
- Fetches active Harvest projects
- For each project with a linked ticket:
- Pulls all time entries
- Calculates total hours used
- Updates ticket budget fields
- Inserts new entries into
ost_ticket_harvest_entries
- Triggers over-budget check (see Over-Budget Locking)
Database Table
-- ost_ticket_harvest_entries
CREATE TABLE ost_ticket_harvest_entries (
id INT AUTO_INCREMENT PRIMARY KEY,
ticket_id INT NOT NULL,
harvest_entry_id BIGINT NOT NULL,
user_id INT,
hours DECIMAL(8,2),
notes TEXT,
spent_date DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY (harvest_entry_id)
);
Staff Panel Integration
Timeline View (scp/timeline/)
The timeline page shows a combined view of:
- Thread entries (messages, responses, notes)
- Harvest time entries (hours logged, who, when, notes)
- Status changes and assignments
Entries are interleaved chronologically.
Harvester UI (scp/harvester/)
Staff can:
- View project details for a ticket
- See time entries logged against the project
- Check budget utilization percentage
- Create new Harvest projects from tickets
Over-Budget Integration
When synced hours exceed the budget threshold, the over-budget lock system activates:
Hours Used > Budget Hours × Threshold Multiplier
→ Ticket locked for further work
→ PM notification sent
→ PM must unlock to continue
See Over-Budget Locking for the tiered threshold system.
Troubleshooting
| Issue | Solution |
|---|---|
| Time entries not syncing | Run php tools/harvest-times-used-import.php manually |
| Project not linked to ticket | Check harvest_project_id custom field is populated |
| Budget showing 0 | Verify budget_hours field is set on the ticket |
| API rate limiting | Harvest allows 100 requests/15 seconds — cron respects this |
| Duplicate entries | UNIQUE key on harvest_entry_id prevents duplicates |
Key Files
| File | Purpose |
|---|---|
include/custom/class-harvest.php | Harvest API wrapper, project creation, rename, budget checks |
include/class.harvest-oauth.php | Per-staff OAuth2 token management |
include/class.harvest-time-entry.php | Direct time entry API calls, logging, Gemini note optimization |
include/ajax.harvest.php | AJAX controller (9 endpoints: OAuth, timer, tasks, notes) |
include/staff/harvest-time-panel.inc.php | Shared panel partial for reply + note forms |
scp/harvest-oauth-callback.php | OAuth callback handler |
include/staff/settings-harvest-api.inc.php | Admin settings page |
custom/src/js/HarvestTimeEntry.js | Frontend JS component (timer, tasks, AI notes, OAuth popup) |
tools/harvest-times-used-import.php | Cron: imports time entries, updates budgets, syncs timeline events |
tools/harvest-time-entries-backfill.php | One-time historical backfill script |
deploy/deploy-harvest-oauth.sql | Migration: OAuth + time log tables |
deploy/deploy-harvest-time-entries.sql | Migration: timeline event tracking table |