πŸ“‹ Changelog

All updates and improvements to SimProSync

v1.9.9

🧩 Pipeline Status Pagination + Projects Custom Object Create Fix

Latest
26 Jun 2026
  • βœ“ Pipeline Stage Mappings β€” the same pagination fix as v1.9.8, applied to the pipeline page. Its SimPro status fetch was also reading only the first 30 records (SimPro's default page size) before caching them, so on accounts with more than 30 status codes the later-sorting codes (typically the quote statuses) were missing from the status dropdown and could not be mapped. The fetch now requests the maximum page size and pages through the full list before caching. Note: this page reads from a cached status list, so after deploying, the status cache must be refreshed once for the complete list to appear.
  • βœ“ Projects custom object create failure (SimPro to GHL). The configurable project-type field that tags a synced record as lead/quote/job was being auto-populated with a capitalised value ("Job"/"Quote"/"Lead"). GHL single-option dropdown fields validate against their option keys, which are lowercase by convention, so GHL rejected the value with HTTP 400 ("couldn't validate the mapped field") and β€” critically β€” failed the entire Projects record create. The visible effect was that Projects custom object records silently stopped being created for affected accounts, while contacts, sites and opportunities continued to sync normally. The auto-populate now sends the lowercase entity type to match GHL's option keys. The project stage and status dropdown fields were never affected because they already pass through the status-mapping layer. Records that previously failed will be created the next time their entity fires a webhook.
  • βœ“ Both changes are multi-tenant safe with no per-customer hardcoding.
v1.9.8

πŸ“Š Status Mappings β€” Full Status Retrieval (Pagination Fix)

26 Jun 2026
  • βœ“ Fixes quote (and other) SimPro statuses being missing from the Stage & Status Mappings page on accounts with large status-code lists. SimPro list endpoints return only 30 records per page by default, and the page was reading just that first page. On builds with more than 30 status codes, the codes that sort later β€” frequently the quote statuses β€” fell onto subsequent pages that were never requested, so they never reached the mapping UI. Accounts with 30 or fewer codes were unaffected, which is why this only surfaced on larger configurations.
  • βœ“ The fix: the SimPro status fetch now requests the maximum page size and pages through until a short page is returned, retrieving the complete status list regardless of how many codes an account has. Results are de-duplicated by status ID.
  • βœ“ Supporting hardening made at the same time: the page now queries the SimPro status views resiliently (the base view plus the projects/quotes/jobs/leads filtered views), tolerating builds where some of those views return 404 by merging whatever is available. The built-in debug view now reports a per-view HTTP code, returned count, and pages fetched, making status-retrieval issues diagnosable at a glance. The earlier company_id = 0 handling for single-company SimPro accounts is retained.
  • βœ“ Multi-tenant safe with no per-customer hardcoding.
v1.9.7

⏭️ GHL β†’ SimPro Customer PATCH Echo-Break Gate

8 Jun 2026
  • βœ“ Companion fix to v1.9.6. Together they make the GHL ↔ SimPro customer sync no-op-safe in both directions β€” every webhook is verified against the actual state of the target record before any update is sent, and the round-trip is skipped if nothing has really changed.
  • βœ“ The problem: every inbound GHL contact webhook was triggering a SimPro customer PATCH, even when no field had actually changed in GHL. SimPro then fired its own customer.updated webhook back, racking up wasted API calls and burning through GoHighLevel workflow executions. v1.9.6 caught the return leg, but the wasted PATCH had already happened, and any momentary timing or cache mismatch could still spawn a fresh cycle.
  • βœ“ Part 1 β€” Echo-loop skip gate in updateSimproCustomer(). Before firing the PATCH, the handler now fetches the current SimPro record and runs a diff against the outbound payload. If a reliable baseline was retrieved AND the diff shows no real changes, the PATCH is skipped entirely and the event is tagged unified.no_action_needed. If the baseline fetch failed, the PATCH still fires defensively so a genuine change is never missed. The skip is visible in the auto_actions log as "⏭️ SimPro customer PATCH SKIPPED - no real changes detected".
  • βœ“ Part 2 β€” Companion diff logger fix (diff_logger.php v1.0.2). Without this, v1.9.7 could never fire in production. SimPro accepts dropdown and lookup fields (Profile.CustomerGroup, Banking.PaymentMethod, Banking.PaymentTermID, Tags, etc.) as scalar IDs on PATCH but returns them as {ID, Name} objects on GET. The previous flat-path comparison saw these as phantom changes on every sync, permanently disabling the skip gate. A new normaliseShapesForDiff() pre-pass walks the payload and existing trees in lockstep before flattening, reduces {ID, Name} objects to their scalar IDs where the payload sends a scalar, and normalises tag-style arrays to sorted ID lists for order-independent comparison. Recursive β€” handles arbitrary nesting. Applied only in the ghl_to_simpro direction; simpro_to_ghl is untouched.
  • βœ“ Verified in production across multiple integrations and both customer types (individuals and companies), including configurations that use company_id = 0. Multi-tenant safe with no per-customer hardcoding.
v1.9.6

πŸ›‘οΈ SimPro β†’ GHL Contact Upsert Echo-Break Gate

6 Jun 2026
  • βœ“ Fixes a severe production echo loop on the customer sync that was generating unnecessary API calls per cycle and racking up significant unexpected GoHighLevel workflow execution charges. v1.9.6 closes the SimPro β†’ GHL leg of the loop; v1.9.7 (released the same day) closes the symmetric GHL β†’ SimPro leg.
  • βœ“ The problem: every SimPro customer webhook was driving a GHL Contact upsert even when nothing had genuinely changed. Two phantom-diff drivers were responsible. (1) Tags came back from SimPro in Title Case but GHL stores tags lowercased, so a case-sensitive diff perpetually reported "Tags changed" on every sync. (2) Mappings sometimes targeted object-scope custom fields (business.*, opportunity.*, quote.*, job.*, site.*, lead.*, invoice.*, project.*) on the Contact. GHL silently drops these on Contact upsert because they belong on custom objects, not Contacts β€” but the diff would perpetually see them as "(empty) β†’ value".
  • βœ“ The fix β€” payload-build scrub in webhook_processor.php. When building the GHL Contact upsert payload, object-scope custom fields are now stripped before sending. Tags are lowercased and deduplicated before they hit the diff comparison. The Contact upsert is skipped entirely when the cleaned diff is empty, completing the upstream cycle break.
  • βœ“ Defensive guards in the diff logger (v1.0.1) so that even if a malformed payload reaches the diff stage, object-scope fields and case-mismatched tags will not be reported as phantom changes.
  • βœ“ Impact: the runaway customer cascade stopped at deploy. Loop-prevented events (unified.loop_prevented) became visible across integrations, confirming the defensive layer is active. Note for integrations using business custom objects: business.simpro_customer_id is no longer written to the Contact object β€” it belongs on the Business custom object.
v1.9.5

πŸ” Duplicate Jobs Loop Fix (SimPro ↔ GHL Echo-Break)

28 May 2026
  • βœ“ Fixes the duplicate SimPro jobs that were appearing roughly once per
  • βœ“ hour in production. This is a deeper fix than the v1.9.4
  • βœ“ duplicate-prevention work β€” it closes the actual echo LOOP, not just
  • βœ“ the symptoms.
  • βœ“
  • βœ“ THE BUG: SIMPRO β†’ GHL β†’ SIMPRO ECHO LOOP
  • βœ“ SimProSync creates a GHL opportunity FROM a SimPro job. That
  • βœ“ opportunity lands in a pipeline stage that is mapped (sync_direction
  • βœ“ = 'both') to CREATE a SimPro job. The GHLβ†’SimPro side then creates a
  • βœ“ brand-new SimPro job, which fires job.created, which creates another
  • βœ“ opportunity... one new duplicate job per cycle (~1/hour).
  • βœ“ The v1.9.3 creation lock only stops SIMULTANEOUS webhooks β€” it never
  • βœ“ caught this slow echo, because each loop produces a genuinely new job
  • βœ“ with a new ID a few minutes apart.
  • βœ“
  • βœ“ PART 1: ECHO-BREAK TRACKER (the core fix)
  • βœ“ When the SimProβ†’GHL processor creates a GHL opportunity FROM a SimPro
  • βœ“ job/quote, it now records that opportunity in a short-lived tracker
  • βœ“ (new table ghl_opportunity_origin_tracker, 5-minute TTL, auto-created
  • βœ“ and auto-pruned). Before the GHLβ†’SimPro side creates a SimPro job back,
  • βœ“ it checks this tracker: if SimProSync itself created the opportunity
  • βœ“ from a SimPro entity within the window, job creation is SKIPPED.
  • βœ“ Logs show "πŸ” Loop prevented: opportunity originated from SimPro job #X
  • βœ“ β€” not creating a job back."
  • βœ“
  • βœ“ PART 2: DURABLE LINK ON EVERY JOB/QUOTE WEBHOOK
  • βœ“ Recent jobs were missing their opportunity_*_links rows, so dedup
  • βœ“ checks could not rely on them. When a job/quote webhook arrives and an
  • βœ“ opportunity already exists, the link row is now upserted (backfilled)
  • βœ“ so the system has a durable "we have seen this" record.
  • βœ“
  • βœ“ PART 3: BELT-AND-BRACES PRE-CREATE CHECK
  • βœ“ Immediately before creating a SimPro job, the GHLβ†’SimPro handler now
  • βœ“ re-checks opportunity_job_links AND sync_links for an already-linked
  • βœ“ job. If one exists, creation is skipped (logs show "πŸ›‘οΈ PRE-CREATE
  • βœ“ GUARD"). Hardens the existing $isLinked check against rows written
  • βœ“ between detection and creation.
  • βœ“
  • βœ“ Multi-tenant safe: all logic is keyed by integration_id, with no
  • βœ“ per-customer hardcoding. The new tracker table is created
  • βœ“ automatically on first run β€” no manual SQL required.
v1.9.4

πŸ› Live Bug Fixes: Attachments, Duplicate Jobs, Quote Conversion & Log Cleanup

27 May 2026
  • βœ“ Three bugs reported from 3+ months of live production use β€” all fixed.
  • βœ“
  • βœ“ FIX #1: JOB ATTACHMENTS NOT DOWNLOADING
  • βœ“ Attachment metadata was syncing but no files were ever downloaded
  • βœ“ Root cause: the attachment download cron crashed on every run with
  • βœ“ SQLSTATE[HY093] β€” MySQL does not allow a bound parameter inside an
  • βœ“ INTERVAL expression (INTERVAL ? DAY). The crash was triggered once a
  • βœ“ user set a local-file retention period.
  • βœ“ Fixed in auto_cleanup_attachments.php β€” the (int)-cast retention value
  • βœ“ is now inlined safely. The download cron runs cleanly again.
  • βœ“
  • βœ“ FIX #2: DUPLICATE JOBS CREATED IN SIMPRO (Dwell / Places)
  • βœ“ Old jobs were being re-created in SimPro and Prospect Pilot
  • βœ“ Root cause: the "is this opportunity already linked?" check trusted
  • βœ“ ONLY the opportunity_*_links tables. Those tables are empty for any
  • βœ“ job/quote created directly in SimPro (then synced up to GHL), or whose
  • βœ“ previous creation attempt failed β€” so the opportunity looked unlinked
  • βœ“ and a DUPLICATE entity was created on every subsequent webhook.
  • βœ“ Now also checks sync_links, and reads the simpro_job_id / simpro_quote_id
  • βœ“ custom field written onto the opportunity itself β€” the authoritative
  • βœ“ proof the entity exists. When found, the missing link row is BACKFILLED
  • βœ“ so the system self-heals (logs show "πŸ›‘οΈ DUPLICATE PREVENTED").
  • βœ“ simpro_customer_id is now also stored in the link tables (was always
  • βœ“ NULL) for a customer-scoped dedup safety net.
  • βœ“
  • βœ“ FIX #3: QUOTEβ†’JOB CONVERSION NOT UPDATING PIPELINE
  • βœ“ Converting a quote to a job in SimPro left the quote opportunity
  • βœ“ stranded in its original pipeline stage, never marked won.
  • βœ“ Root cause: SimPro does not fire a quote webhook on conversion.
  • βœ“ Now, when a job webhook arrives for a job converted from a quote
  • βœ“ (ConvertedFromQuote / ConvertedFrom in the SimPro job payload), the
  • βœ“ originating quote's linked opportunity is moved to the mapped
  • βœ“ 'Converted' stage (falls back to the 'Complete' quote mapping).
  • βœ“ NOTE: requires a quote pipeline mapping for SimPro stage 'Converted'
  • βœ“ or 'Complete' to be configured in Pipeline Mappings.
  • βœ“
  • βœ“ ENHANCEMENT: STALE PIPELINE DETECTION
  • βœ“ When GHL rejects opportunity creation with "pipeline id is invalid"
  • βœ“ (caused by a pipeline being renamed/rebuilt in GHL), the failure is
  • βœ“ now recorded clearly in the event log with remediation instructions,
  • βœ“ instead of failing silently and being retried on every webhook.
  • βœ“
  • βœ“ FIX #4: EVENT_LOGS TABLE GROWING UNBOUNDED (reached 4.4GB)
  • βœ“ The daily log cleanup cron was launching but never deleting anything.
  • βœ“ Root cause: cleanup_logs.php used "INTERVAL ? DAY" with a bound
  • βœ“ parameter β€” the same MySQL limitation as FIX #1 β€” so it crashed with
  • βœ“ SQLSTATE[HY093] on every run for months. Plesk showed a green tick
  • βœ“ because the task LAUNCHED, not because it succeeded.
  • βœ“ cleanup_logs.php is now fixed: the (int)-cast retention value is
  • βœ“ inlined safely, deletion runs in batches of 2,000 rows to avoid
  • βœ“ locking the table, and the default retention is now 14 days.
  • βœ“
  • βœ“ ENHANCEMENT: NO-ACTION LOG PAYLOAD TRIMMING
  • βœ“ A single event type β€” unified.no_action_needed β€” accounted for
  • βœ“ ~1.8GB (40%) of the event_logs table: 20,000+ rows of full ~87KB
  • βœ“ forensic payloads logging webhooks where nothing happened.
  • βœ“ webhook_ghl_unified.php now stores a SLIM payload (under 1KB) for
  • βœ“ no-action events (no_action_needed, loop_prevented, throttled) β€”
  • βœ“ the heavy enrichment and API-call blobs are stripped, the summary
  • βœ“ fields are kept. Events that did real work keep their full payload.
  • βœ“ This cuts the largest source of log bloat at the source.
v1.9.3

πŸ›‘οΈ Duplicate Creation Prevention & Multiple Webhook Fixes

11 Apr 2026
  • βœ“ FIX #1: LOOP PREVENTION (v1.9.0 webhook)
  • βœ“ Job status was flip-flopping (Pending ↔ Progress) causing multiple notifications
  • βœ“ Root cause: GHLβ†’SimProβ†’GHL echo loop when both sides fired webhooks
  • βœ“ Now fetches current job/quote state BEFORE updating
  • βœ“ Skips redundant updates if Stage/Status already match (logs show "πŸ›‘ LOOP PREVENTED")
  • βœ“ Also removed AutoAdjustStatus=false which was permanently disabling SimPro's Auto Adjust
  • βœ“
  • βœ“ FIX #2: ENTITY TYPE ROUTING (v1.9.1 webhook)
  • βœ“ Creating opportunity in "Ongoing Jobs Pipeline" was creating a Quote instead of Job
  • βœ“ Root cause: ACTION code always called createSimproQuote() regardless of entity_type
  • βœ“ Now checks entity_type from pipeline_stage_mappings and routes correctly
  • βœ“ Added new createSimproJob() function for direct job creation from opportunities
  • βœ“ Logs now show "⚑ ACTION: Creating Job for Opportunity..." when entity_type=job
  • βœ“
  • βœ“ FIX #3: ARCHIVING BUG (v1.9.2 webhook)
  • βœ“ Quotes/Jobs weren't archiving when moved to "Ghosted" or "Abandoned" stages
  • βœ“ Root cause: archive_reason_id was extracted from mapping but never added to API payload
  • βœ“ SimPro requires ArchiveReason array when Stage="Archived"
  • βœ“ Now includes ArchiveReason in update payload (logs show "πŸ“‹ Adding ArchiveReason ID X")
  • βœ“
  • βœ“ FIX #4: DUPLICATE CREATION (v1.9.3 webhook)
  • βœ“ Multiple GHL workflows firing simultaneously could create duplicate jobs/quotes
  • βœ“ Root cause: No deduplication lock for job/quote creation (only sites had this)
  • βœ“ Added lock-based deduplication using acquireCreationLock() pattern
  • βœ“ First webhook creates entity; subsequent webhooks detect and skip
  • βœ“ Logs show "πŸ›‘ DEDUP: job #X already created for opportunity by another webhook"
  • βœ“
  • βœ“ ENHANCEMENT: GOOGLE DRIVE FILE NAMING
  • βœ“ Files uploaded to Google Drive now include customer name in filename
  • βœ“ Original: "photo.jpg" β†’ New: "photo - John Smith.jpg"
  • βœ“ Makes it easier to identify files in Drive without opening folders
  • βœ“ Customer name is sanitized to remove invalid filename characters
v1.9.0

πŸ—‘οΈ Deletion Manager

6 Apr 2026
  • βœ“ NEW FEATURE: Admin-only Deletion Manager for safely removing entities from both SimPro and GHL
  • βœ“
  • βœ“ πŸ” SEARCH & PREVIEW:
  • βœ“ Search for Jobs, Quotes, or Sites by ID or name
  • βœ“ Preview shows EVERYTHING that will be deleted (cascade view)
  • βœ“ Shows linked GHL Opportunities, Custom Objects, and Associations
  • βœ“ Site deletion shows all child Jobs and Quotes that will also be removed
  • βœ“
  • βœ“ πŸ›‘οΈ SAFETY FEATURES:
  • βœ“ Admin-only access - regular users cannot access deletion manager
  • βœ“ Soft-delete by default - items can be restored within configurable window (7-90 days)
  • βœ“ Must type "DELETE" to confirm soft-delete, "PERMANENT" for permanent delete
  • βœ“ Invoiced jobs are BLOCKED from deletion - must archive in SimPro instead
  • βœ“ Full audit trail of all deletion actions with user and timestamp
  • βœ“ Data snapshots stored for potential restore
  • βœ“
  • βœ“ ♻️ RESTORE & PERMANENT DELETE:
  • βœ“ Restore items from deletion queue within the expiry window
  • βœ“ Restored items get new IDs (SimPro limitation) with updated sync_links
  • βœ“ Permanent delete clears all snapshots - cannot be undone
  • βœ“ Auto-expiry cron job permanently deletes items past their restore window
  • βœ“
  • βœ“ πŸ“‹ NEW DATABASE TABLES:
  • βœ“ deletion_queue - tracks all deletion requests, status, and snapshots
  • βœ“ deletion_audit_log - full audit trail of all actions
  • βœ“ Tables auto-created on first access
v1.8.9

🏷️ Fix Duplicate Tags, Zombie Records & Attachment Display

6 Apr 2026
  • βœ“ FIX #1: Tags syncing from GHL to SimPro were creating massive duplicates
  • βœ“ Example: Dozens of identical "schools" tags with different IDs
  • βœ“ Root cause: findOrCreateSimproTagIds() was auto-creating tags when lookup failed
  • βœ“ Function now ONLY matches existing SimPro tags - never creates new ones
  • βœ“ Added whitespace trimming to tag name normalization for more robust matching
  • βœ“ To sync new tags, first create them manually in SimPro (Setup β†’ Tags)
  • βœ“
  • βœ“ FIX #2: Zombie record prevention for deleted SimPro entities
  • βœ“ When updating a job/quote returns 404 (deleted in SimPro), the stale link is now cleared
  • βœ“ Removes orphaned references from opportunity_job_links and opportunity_quote_links
  • βœ“ Prevents the system from holding references to deleted SimPro entities
  • βœ“
  • βœ“ FIX #3: Gallery showing attachments that have been removed from server
  • βœ“ Files cleaned up after Drive sync were still appearing in the gallery
  • βœ“ Added WHERE clause filter: only show attachments where file_path is not empty
  • βœ“ Removed attachments no longer clutter the gallery view
v1.8.8

🏒 Company Customer Contact Fallback

30 Mar 2026
  • βœ“ FIX: Jobs/Quotes for company customers were not creating GHL contacts or opportunities
  • βœ“ Company customers in SimPro often have empty Email/Phone fields on the company record itself
  • βœ“ Contact details are stored on the company's Contacts (people like "Jackie" or "Joe")
  • βœ“ SimProSync was checking customer.Email which was empty, so no GHL contact could be upserted
  • βœ“ Without a GHL contact, no opportunity could be created or linked
  • βœ“ NEW: Fallback logic for company customer email/phone
  • βœ“ When customer.Email is empty, now checks customer.Contacts array
  • βœ“ Prioritises the Primary Invoice Contact (PrimaryInvoiceContact: true)
  • βœ“ Falls back to first contact with an email if no primary is set
  • βœ“ Also auto-populates firstName/lastName from the selected contact
  • βœ“ Example: "Company name Ltd" with empty Email now uses "contact@companyname.com" from contact
  • βœ“ Logging shows: "πŸ“§ Auto-populated email from company contact (contact): contact@companyname.com"
v1.8.7

🧾 Invoice Status β†’ Job Opportunity Sync

30 Mar 2026
  • βœ“ FIX: When marking an invoice as paid, linked job opportunities were not updating in GHL
  • βœ“ SimPro automatically changes job status to "Job : Invoice Fully Paid" when invoice is paid
  • βœ“ But SimProSync was not syncing this status change to the GHL opportunity
  • βœ“ Root cause: invoice.status event type was not triggering opportunity stage sync
  • βœ“ NEW: invoice.status now triggers opportunity stage updates for associated jobs
  • βœ“ When invoice.status or invoice.updated fires, fetches all associated jobs from invoice.Jobs array
  • βœ“ For each job, fetches current job data to get latest status (e.g., "Job : Invoice Fully Paid")
  • βœ“ Looks up linked GHL opportunity for each job
  • βœ“ Updates opportunity stage based on job's current status using existing pipeline mappings
  • βœ“ Supports invoices linked to multiple jobs (processes each one)
  • βœ“ New logging section: invoice_job_sync shows each job processed and result
  • βœ“ Example: Invoice paid β†’ Job #8221 status becomes "Job : Invoice Fully Paid" β†’ GHL opportunity moves to mapped stage
v1.8.6

πŸ”§ company_id=0 Bug Fixes & Settings Test Buttons

25 Mar 2026
  • βœ“ FIX: Multiple files failed for single-company SimPro accounts (company_id = 0)
  • βœ“ PHP treats 0 as "falsy", so checks like !$companyId or empty($companyId) incorrectly failed
  • βœ“ Fixed in: mappings_pipeline.php, fetch_simpro_attachments.php, force_sync.php, dropdown_resolver.php
  • βœ“ All single-company SimPro setups now work correctly
  • βœ“ NEW: Test API Connection buttons on Settings page
  • βœ“ Each integration now has "Test SimPro Connection" and "Test GHL Connection" buttons
  • βœ“ SimPro test calls /setup/statusCodes/projects/ to verify credentials
  • βœ“ GHL test calls /locations/{locationId} to verify API access
  • βœ“ Shows success (green) with status count or location name
  • βœ“ Shows failure (red) with specific error message and fix instructions
  • βœ“ Helps diagnose 401 (wrong API key) vs 404 (wrong company ID) errors
  • βœ“ IMPROVED: Snapshot system now captures source status cache
  • βœ“ create_snapshot.php now stores source_status_cache when snapshotting pipeline mappings
  • βœ“ Contains status_id β†’ status_name mapping from source SimPro
  • βœ“ Enables future status ID remapping when applying to different SimPro instances
  • βœ“ Note: Status remapping only works when SimPro instances have matching status NAMES
  • βœ“ Different SimPro setups with custom statuses will need manual pipeline/status mapping
  • βœ“ Snapshot version bumped to 2.3
v1.8.4

πŸ“Έ Comprehensive Snapshots & Multiple Fixes

25 Mar 2026
  • βœ“ FIX: apply_snapshot.php was using wrong column names for association labels
  • βœ“ Changed first_object_key/second_object_key β†’ input_schema/output_schema to match actual DB schema
  • βœ“ FIX: Association mappings were looking for "is_enabled" but snapshot contains "enabled"
  • βœ“ FIX: Status mappings INSERT was completely wrong schema β€” fixed to match ghl_status_mappings table
  • βœ“ Correct columns: integration_id, custom_object_key, field_type, simpro_value, simpro_status_id, ghl_value, display_order
  • βœ“ FIX: Pipeline mappings now includes all columns from snapshot (simpro_status_id, simpro_status_ids, archive_reason_id, sync_direction, is_primary, is_default_for_new)
  • βœ“ Snapshots can now be successfully applied to new integrations
  • βœ“ NEW: Snapshots now include ALL mapping types for complete account cloning
  • βœ“ Added: Association Mappings (Contact↔Site, Site↔Project, etc.)
  • βœ“ Added: Association Labels (GHL custom object association definitions)
  • βœ“ Added: Pipeline Stage Mappings (GHL stages ↔ SimPro stages)
  • βœ“ Added: Status Mappings (SimPro status values ↔ GHL dropdown values)
  • βœ“ Snapshot version bumped to 2.2 to reflect new comprehensive structure
  • βœ“ NEW: Events are now ENABLED by default when applying snapshots
  • βœ“ Previously, if source account had disabled events, applied accounts inherited disabled state
  • βœ“ New accounts created from snapshots now start with all events active immediately
  • βœ“ FIX: Status Mappings page failed for single-company SimPro accounts
  • βœ“ company_id = 0 is valid for single-company setups, but PHP treated it as "falsy"
  • βœ“ Changed check from if($companyId) to if($companyId !== null && $companyId !== '')
  • βœ“ SimPro statuses now load correctly for ALL accounts regardless of company_id value
  • βœ“ FIX: Settings page now respects user permissions
  • βœ“ Cloud Storage section (Google Drive, Dropbox) hidden when user lacks permission
  • βœ“ Auto-Sync settings hidden when google_drive permission is disabled
  • βœ“ Permissions loaded from user_permissions table and checked before rendering sections
  • βœ“ NEW: Opportunity name automatically syncs to Quote/Job name in SimPro
  • βœ“ Name changes are detected independently β€” no stage change required
  • βœ“ When you rename an opportunity in GHL, the linked quote/job name updates in SimPro automatically
  • βœ“ New syncOpportunityNameToSimpro() function handles name-only updates
  • βœ“ Last known name stored in ghl_opportunity_name column for change detection
  • βœ“ Works for both quotes and jobs
  • βœ“ DB: Added ghl_opportunity_name column to opportunity_quote_links and opportunity_job_links tables
  • βœ“ UI: New "Additional Mappings" section in Create Snapshot modal
  • βœ“ UI: Info boxes explain snapshot behavior and event enabling
  • βœ“ UI: Association, Pipeline, and Status mappings checked by default when creating snapshots
v1.8.3

⚑ SimPro Webhook Timeout Prevention

24 Mar 2026
  • βœ“ FIX: SimPro webhooks failing with HTTP 408 (Request Timeout) during bulk operations
  • βœ“ SimPro was timing out waiting for SimProSync to respond while processing was still running
  • βœ“ NEW: Implemented fastcgi_finish_request() pattern in webhook_simpro.php
  • βœ“ Webhook endpoint now sends 200 OK immediately upon receiving valid JSON
  • βœ“ Processing continues in background after SimPro connection is closed
  • βœ“ Eliminates 408 errors even when enrichment requires 20-30 API calls
  • βœ“ Database logging still captures full processing results via shutdown function
  • βœ“ FIX: PHP warning "Undefined variable $ghlContactId" in webhook_processor.php
  • βœ“ Variable was referenced in diff logging section before being initialized
  • βœ“ Added proper initialization: $ghlContactId = null before the logging block
  • βœ“ Server timeout configuration also increased (max_execution_time: 300s, nginx proxy timeouts: 300s)
v1.8.2

πŸ”§ Server Timeout Configuration

24 Mar 2026
  • βœ“ INFRASTRUCTURE: Increased PHP max_execution_time from 30s to 300s
  • βœ“ INFRASTRUCTURE: Added nginx proxy timeout directives (300s) for webhook endpoints
  • βœ“ Prevents 504 Gateway Timeout errors during bulk SimPro operations
  • βœ“ Each webhook with full enrichment can require 10-15 seconds of API calls
  • βœ“ Bulk updates (e.g., updating many jobs at once) previously caused server to become unresponsive
  • βœ“ Note: This changelog entry documents server-side Plesk configuration changes
v1.8.1

🎯 Auto-Create Opportunity Fix

17 Mar 2026
  • βœ“ FIX: Auto-create opportunity logic was incorrectly blocking opportunity creation
  • βœ“ The "auto_create_opportunity" setting was being checked for ALL jobs/quotes, not just new contacts
  • βœ“ This caused opportunities to NOT be created when jobs/quotes came in for existing GHL contacts
  • βœ“ REMOVED: The setting check wrapper that was blocking createGhlOpportunity() calls
  • βœ“ Now opportunities are created for ALL jobs/quotes that have a valid GHL contact linked
  • βœ“ The original intent was to control opportunity creation for brand new contacts only
  • βœ“ Existing contact workflows now work correctly: Job created in SimPro β†’ Opportunity created in GHL
  • βœ“ No changes to association logic, custom object sync, or field mappings
v1.8.0

πŸ“Š Opportunity Custom Fields & Company Customer Fixes

16 Mar 2026
  • βœ“ FIX: Opportunity custom fields now sync from SimPro to GHL
  • βœ“ Previously, mappings for opportunity.* fields (SimPro Job ID, Opportunity Value, Site ID, etc.) were ignored
  • βœ“ Code was only handling contact.*, custom_objects.*, and contact.phone.* fields
  • βœ“ NEW: Mapping loop now collects opportunity.* fields into $opportunityFieldMappings array
  • βœ“ Opportunity fields are written when creating new opportunities (if auto-create enabled)
  • βœ“ Opportunity fields are written when updating existing linked opportunities
  • βœ“ Field ID lookup tries both "opportunity.fieldname" and "fieldname" formats (GHL API compatibility)
  • βœ“ FIX: Company customer sites URL now includes customer type (GHL β†’ SimPro direction)
  • βœ“ Previously, creating quotes/jobs from GHL failed for Company customers with 404 errors
  • βœ“ Sites endpoint was using /customers/{id}/sites/ instead of /customers/companies/{id}/sites/
  • βœ“ Now correctly detects customer type and uses appropriate endpoint
  • βœ“ Applies to: createSimproQuote(), createSimproEntityFromProject(), and related functions
  • βœ“ NEW: "Opportunity Custom Fields" section in SimPro Logs page
  • βœ“ Shows which opportunity fields were collected and their values
  • βœ“ Helps debug opportunity field sync issues
  • βœ“ Processing Steps now show: "πŸ“‹ Collected opportunity field: opportunity.simpro_job_id = 12345"
v1.7.9

🏒 Company Contact Fallback & Mapping-Driven Opportunity Value

5 Mar 2026
  • βœ“ NEW: Company customer contact fallback for SimProβ†’GHL direction
  • βœ“ Previously, jobs linked to Company customers with no direct email/phone failed with "No email or phone available"
  • βœ“ System now detects when customer is a Company with blank contact details
  • βœ“ Falls back to the job's CustomerContact (the specific person assigned to the job)
  • βœ“ Fetches full contact details from SimPro: /customers/{id}/contacts/{contactId}
  • βœ“ Extracts email, phone (CellPhone or WorkPhone), firstName, lastName from the contact record
  • βœ“ Enables contact upsert, opportunity sync, and project/site associations for Company customer jobs
  • βœ“ Fallback only activates when email AND phone are both empty β€” no impact on Individual customers
  • βœ“ Clear logging: "πŸ“§ Using CustomerContact email (company fallback): ..."
  • βœ“ NEW: Mapping-driven opportunity value sync
  • βœ“ Opportunity value (monetaryValue) is now read from ghl_field_mappings for the lead_value field
  • βœ“ Customers can map any SimPro value field to GHL opportunity value via the Mappings UI
  • βœ“ Falls back to Total.ExTax β†’ Total.IncTax if no mapping configured
  • βœ“ New helper function: getOpportunityValueFromMappings() used in both createGhlOpportunity() and stage sync
  • βœ“ FIX: Dropdown resolver now removes unresolvable dropdown values from payload entirely
  • βœ“ Previously, invalid dropdown values were left in the payload causing SimPro API errors
  • βœ“ Acts as a safety net for customer misconfiguration of dropdown field mappings
v1.7.8

🏒 Company Customer Support & Customer Type Storage

3 Mar 2026
  • βœ“ FIX: Customer updates now work for BOTH Individual and Company customers
  • βœ“ Previously, all customers were assumed to be Individuals, causing 404 errors when updating Company customers
  • βœ“ NEW: simpro_customer_type column in sync_links table stores customer type (individuals/companies)
  • βœ“ Customer type is now stored when a customer is created via GHLβ†’SimPro sync
  • βœ“ When updating, system checks sync_links first for stored type (avoids extra API call)
  • βœ“ If type not stored, detects from SimPro API and stores for future use
  • βœ“ NEW: Helper functions: getStoredCustomerType(), storeCustomerType(), detectSimproCustomerType()
  • βœ“ Detection logic improved: checks SimPro Type field AND CompanyName presence
  • βœ“ Company customers now correctly use /customers/companies/ endpoint
  • βœ“ Individual customers continue using /customers/individuals/ endpoint
  • βœ“ Backwards compatible: Existing sync_links records without type will auto-detect on next update
  • βœ“ FIX: PHP parse error in webhook_processor.php (duplicate empty function declaration)
  • βœ“ FIX: Undefined variable $ghlContactId warning in webhook_processor.php diff logging section
v1.7.7

πŸ” Site Deduplication - Prevent Duplicate GHL Sites

3 Mar 2026
  • βœ“ FIX: Duplicate Sites created when manually adding Sites in GHL before SimPro sync
  • βœ“ NEW: Multi-level Site deduplication in SimProβ†’GHL direction
  • βœ“ Level 1: Check site_sync_links table for existing link by SimPro Site ID
  • βœ“ Level 2: Search GHL by site_id field (catches Sites created by SimProSync)
  • βœ“ Level 3: Search GHL by site_name field (catches manually created Sites)
  • βœ“ Level 4: Search GHL by site_address + site_postal_code (catches Sites with different names but same location)
  • βœ“ When match found: Updates existing Site and writes SimPro Site ID to link them
  • βœ“ Only creates new Site if ALL dedup checks fail
  • βœ“ NEW: getGhlObjectRecord() helper function for fetching GHL custom object records
  • βœ“ Logs clearly show which dedup method matched: "Found existing Site by name/address+postcode"
v1.7.6

🏷️ Tags Sync Fix & Webhook 500 Error Resolution

3 Mar 2026
  • βœ“ FIX: Tags now sync correctly to SimPro (was sending tag names, now sends tag IDs)
  • βœ“ SimPro API requires Tags as array of integer IDs, not string names
  • βœ“ NEW: findOrCreateSimproTagIds() function resolves tag names to SimPro Tag IDs
  • βœ“ Auto-creates tags in SimPro if they don't exist (via POST /setup/tags/)
  • βœ“ Tags from GHL (comma-separated string) are parsed, resolved to IDs, and sent correctly
  • βœ“ FIX: 500 error on SimPro β†’ GHL webhooks caused by function redeclaration
  • βœ“ Wrapped duplicate functions with function_exists() checks to prevent conflicts
  • βœ“ Affects: webhook_processor.php and functions.php sharing common functions
  • βœ“ Loop prevention confirmed working: 60-second throttle prevents infinite sync loops
v1.7.5

πŸŽ›οΈ Dynamic Custom Object Mapping & Flexible Entity Types

2 Mar 2026
  • βœ“ NEW: Dynamic GHL Custom Object Mapping in Settings page
  • βœ“ Users can now map ANY GHL custom object to ANY SimPro entity type (Sites, Projects, Quotes, Jobs, Leads, Invoices)
  • βœ“ No longer limited to hardcoded "sites" and "projects" object names
  • βœ“ Settings UI shows a table of all discovered custom objects with dropdown to select SimPro entity type
  • βœ“ Auto-detection: Object names containing "site/location/property" default to Sites, "project/job/work" default to Projects, etc.
  • βœ“ Mappings stored in new ghl_custom_object_mappings database table
  • βœ“ NEW: SimPro entity type inference from GHL custom object key
  • βœ“ When custom object webhook arrives, system looks up configured entity type
  • βœ“ Falls back to smart detection based on object name if no mapping configured
  • βœ“ Unified webhook handler now routes to correct SimPro entity creation based on mapping
  • βœ“ FIX: Custom object field mappings now load correctly in GHL Mappings UI
  • βœ“ Field dropdowns now populate with custom object properties
v1.7.4

πŸ”— Enhanced Association System & Object-to-Object Links

1 Mar 2026
  • βœ“ NEW: Object-to-Object associations (Sites ↔ Projects) now fully supported
  • βœ“ Previously only Contact ↔ Custom Object associations were created
  • βœ“ System now creates associations between custom objects when both exist
  • βœ“ Example: When a Quote syncs with Site, the Quote's Project is linked to the Site
  • βœ“ NEW: Association processing split into two phases
  • βœ“ Phase 1: Contact ↔ Custom Object associations (immediate)
  • βœ“ Phase 2: Object ↔ Object associations (after all objects processed)
  • βœ“ Ensures both objects exist before attempting to link them
  • βœ“ FIX: Association errors no longer block the main sync process
  • βœ“ Errors are logged but processing continues
  • βœ“ Clear logging shows which associations succeeded, failed, or were skipped
v1.7.3

πŸ“ Site Sync Improvements & Address Handling

28 Feb 2026
  • βœ“ FIX: Site address fields now sync correctly to GHL custom objects
  • βœ“ Site address, city, state, postal code, and country now map to corresponding GHL fields
  • βœ“ NEW: Automatic site sync when job/quote has associated site
  • βœ“ When a job or quote syncs, its linked Site is automatically synced to GHL
  • βœ“ Site ↔ Contact association created automatically
  • βœ“ Site ↔ Project association created if project exists
  • βœ“ FIX: Site sync no longer fails when Site has no address data
  • βœ“ Empty address fields are handled gracefully
  • βœ“ NEW: site_sync_links table tracks SimPro Site ID to GHL Site custom object ID
  • βœ“ Prevents duplicate Site creation on subsequent syncs
v1.7.2

πŸ”„ Loop Prevention Enhancements

25 Feb 2026
  • βœ“ FIX: Loop prevention now correctly handles rapid sequential webhooks
  • βœ“ Previous 60-second throttle was too aggressive for legitimate batch updates
  • βœ“ NEW: Smart entity comparison - only blocks if Stage AND Status unchanged
  • βœ“ Allows legitimate updates while still preventing infinite loops
  • βœ“ NEW: GHL echo detection via sync_links table
  • βœ“ When GHL webhook arrives, checks if we just sent that update
  • βœ“ Prevents "we changed it β†’ GHL echoed it β†’ we changed it again" loops
  • βœ“ Improved logging shows exactly why a webhook was blocked or allowed
v1.7.1

πŸ“Š Enrichment Optimization & Hash-Based Skip

20 Feb 2026
  • βœ“ NEW: Hash-based enrichment optimization for SimProβ†’GHL direction
  • βœ“ Hashes webhook payload + enrichment data to detect duplicate content
  • βœ“ Skips processing if hash matches recent webhook (within 24 hours)
  • βœ“ Significantly reduces unnecessary GHL API calls
  • βœ“ NEW: webhook_data_hashes table stores recent hashes with auto-cleanup
  • βœ“ Hashes auto-expire after 24 hours to prevent table bloat
  • βœ“ Clear logging: "⏭️ Skipping - identical data recently processed (hash: abc123)"
  • βœ“ FIX: Enrichment no longer fetches data that won't be used
  • βœ“ Only fetches related entities that have active field mappings
v1.7.0

πŸš€ Unified GHL Webhook Handler

15 Feb 2026
  • βœ“ NEW: Single unified webhook endpoint for ALL GHL events
  • βœ“ Replaces multiple individual webhook handlers
  • βœ“ All GHL webhooks now route through actions/webhook_ghl_unified.php
  • βœ“ Simplified webhook configuration - one URL for all events
  • βœ“ NEW: Modular architecture with separate helper files
  • βœ“ core/ghl/discovery.php - GHL API discovery and custom fields
  • βœ“ core/ghl/simpro_api.php - SimPro API calls
  • βœ“ core/ghl/ghl_api.php - GHL API calls
  • βœ“ core/ghl/enrichment.php - Data enrichment logic
  • βœ“ core/ghl/pipeline.php - Pipeline stage handling
  • βœ“ core/ghl/payload_builder.php - SimPro payload construction
  • βœ“ core/ghl/helpers.php - Utility functions
  • βœ“ core/ghl/simpro_dedup.php - SimPro deduplication
  • βœ“ Improved error handling and logging throughout
v1.6.0

SimPro β†’ GHL Webhook Processor

10 Jan 2026
  • βœ“ NEW: SimPro β†’ GHL webhook processor with full enrichment
  • βœ“ Enriches SimPro webhooks with customer, site, job history, quotes, invoices, notes
  • βœ“ Smart field mapping from SimPro fields to GHL contact and custom fields
  • βœ“ Pipeline stage sync from SimPro job/quote stages to GHL opportunity stages
  • βœ“ Opportunity creation and linking with jobs/quotes
  • βœ“ Contact ↔ Site ↔ Project associations in GHL
v1.5.8

Company vs Individual Customer Payload Fix

26 Dec 2025
  • βœ“ Fixed HTTP 422 errors when creating Company customers
  • βœ“ Company customers no longer send GivenName/FamilyName at top level
  • βœ“ Individual customers no longer send nested "contact" object
  • βœ“ Fixed cascade customer creation in site.create and job.create
  • βœ“ Fixed fallback CREATE scenario payload cleanup
v1.5.7

GHL β†’ SimPro Custom Object Create Fix

25 Dec 2025
  • βœ“ Fixed site.create_object was not looking up Customer ID from associated contact
  • βœ“ Fixed job.create_object was not looking up Customer ID from associated contact
  • βœ“ Fixed quote.create_object was not looking up Customer ID from associated contact
  • βœ“ Fixed lead.create_object was not looking up Customer ID from associated contact
  • βœ“ All *.create_object handlers now properly call getSimproCustomerIdFromAssociatedContact()
  • βœ“ Sites, Jobs, Quotes, and Leads can now be created from GHL custom objects when linked to a contact
v1.5.6

Object-to-Object Associations Fix

25 Dec 2025
  • βœ“ Fixed Sites ↔ Projects associations not being called
  • βœ“ Association functions are now properly invoked after successful API calls
  • βœ“ Improved logging for association creation
v1.5.0

Enrichment Engine & Smart Mapping Analysis

20 Dec 2025
  • βœ“ Added GHL Enrichment Engine - fetches full contact, business, opportunities, and custom objects
  • βœ“ Smart Mapping Analysis now shows all available fields from enriched data
  • βœ“ Improved field lookup with unified GHL data
  • βœ“ Added support for custom object properties in mappings
v1.4.0

Quote Creation, ID Writebacks & Safety Checks

13 Dec 2025
  • βœ“ Fixed quote creation - Customer now sent as integer, auto-fetches Site/Type/Name
  • βœ“ Fixed enrichment to use correct contact_id instead of opportunity ID
  • βœ“ Added Company vs Individual customer auto-detection
  • βœ“ Improved Contact events handling (separate from Customer events)
  • βœ“ Added safety check to prevent accidental mapping deletion
  • βœ“ Fixed Site ID writeback API URL bug
  • βœ“ Added Job ID writeback for custom objects
  • βœ“ Added Quote ID writeback for custom objects
  • βœ“ Added smart fallbacks for site_id, job_id, quote_id field names
  • βœ“ Removed debug logging code
v1.3.0

Site Update Fixes & Log Improvements

10 Dec 2025
  • βœ“ Fixed site update failures - removed trailing slashes from PATCH URLs
  • βœ“ Fixed site.create_object payload structure
  • βœ“ Improved log display with loop prevention detection
  • βœ“ Added "No Action" status display
  • βœ“ Added detailed SimPro API Calls section in logs
v1.2.0

Site ID Writeback & Dynamic Field Mapping

8 Dec 2025
  • βœ“ Made Site ID writeback fully dynamic using ghl_field_mappings table
  • βœ“ Supports any custom object name and field name
  • βœ“ Improved association handling between custom objects and contacts
v1.1.0

Pipeline Stage Mapping

1 Dec 2025
  • βœ“ Added bi-directional pipeline stage sync
  • βœ“ SimPro job stages now sync to GHL opportunity stages
  • βœ“ GHL opportunity stage changes sync back to SimPro
  • βœ“ Added pipeline stage mapping configuration page
v1.0.0

Initial Release

15 Nov 2025
  • βœ“ Bi-directional sync between SimPro and GoHighLevel
  • βœ“ Customer, Site, Job, Quote, Lead, Invoice sync
  • βœ“ Custom object support
  • βœ“ Field mapping configuration
  • βœ“ Event toggle controls

SimProSync by My Local Trades