feat(workiz): add workiz#1441
Conversation
|
@greptileai review Auto-nudge from |
Greptile SummaryThis PR introduces the Workiz CLI integration for the printing-press library, covering all five Workiz resources (job, lead, customer, team, time-off) plus six novel analytics commands not present in any existing Workiz SDK:
Confidence Score: 4/5Safe to merge with one correction: the job audit command needs its sync-hint resources string updated from "job,lead" to "job,lead,customer" to avoid guiding first-time users toward an incomplete mirror that produces zero customer findings. The bulk of the code — auth patches, novel analytics commands, and the multi-round fixes to credential leaks, lead funnel filtering, and time-off conflict detection — is well-implemented and regression-tested. One concrete gap remains: job_audit.go advertises "job,lead" as the resources to sync but also reads the customer table, so users who follow the first-time sync hint will silently miss all customer-level billing-readiness issues. All other changed files reviewed cleanly. library/sales-and-crm/workiz/internal/cli/job_audit.go — the checkNovelMirror resources string needs to include "customer". Important Files Changed
|
- AuthHeader() no longer falls back to a secret-only credential: the secret is only ever a POST-body field, so a config with just WORKIZ_API_SECRET set now correctly reports unauthenticated instead of passing validateCachedRequestAuth while every real request 404s. - Fixed misleading auth setup/status placeholder text that told users to paste their token into the WORKIZ_API_SECRET slot. - lead_funnel's lead-to-job matching no longer silently disables its chronology guard when a lead's CreatedDate is unparseable; extracted matchLeadToJob() picks deterministically (earliest-created match) instead of depending on iteration order.
|
@greptileai review Auto-nudge from |
…in matchLeadToJob Greptile's second review pass caught a residual bug in the previous fix: when the first contact-matching job had no parseable CreatedDate, bestCreated stayed at the zero time.Time value and no later dated job could satisfy jobCreated.Before(zero) to displace it, so the undated match would win permanently. Selection now explicitly prefers any dated candidate over an undated one, then earliest-created among dated candidates, falling back to undated only when nothing dated exists.
|
@greptileai review Auto-nudge from |
- store.go: job/lead sync silently dropped every real record. Workiz returns a fully-uppercase "UUID" field, but the generic ID-fallback matcher only tries id/ID/uuid/Uuid case variants, never all-caps. Added an explicit resourceIDFieldOverrides entry so job/lead resolve their primary key directly. - novel_shared.go: wzJob.JobTotalPrice/JobAmountDue were declared as string, but live responses return JSON numbers (int or float) — json.Unmarshal fails the whole struct on a single field type mismatch, so every real job row was silently dropped from loadJobs. Introduced flexibleMoney to accept both wire shapes. Also: wzComments only handled empty-string/array-of-objects shapes; live Comments are plain non-empty strings, so real comments were silently lost. wzLead.LeadSource was tagged "LeadSource", but every live lead reports its source under "JobSource" instead — lead funnel's group-by-source always fell through to (unknown). - lead_funnel.go: the job-predates-lead chronology guard was too strict for real data. Workiz's own "AI Call" intake integration creates the job record 2-3 seconds before the lead record for the same contact, so a strict jobCreated >= leadCreated check rejected every real conversion from that source. Added a 10-minute grace window. Verified end-to-end against a real Workiz account: sync now populates all 4 resources cleanly (0 warnings, was previously dropping every job/lead), and job revenue/team bottleneck/lead funnel/job audit all produce correct output against real data (lead funnel now finds all 6 real conversions at 40% rate, was 0% for every source before this fix).
|
@greptileai review Auto-nudge from |
Greptile flagged two missing items per AGENTS.md's canonical PR shape contract: dogfood-results.json and a spec_source field. Both trace to the same root cause — dogfood-results.json (which itself carries spec_source: "bundled") was present in the local library copy but was dropped when the CLI was first copied into the publish repo. Restored from the local library.
|
Re: missing For clarity, |
|
@greptileai review Auto-nudge from |
Greptile finding: the condition ok && t.Before(cutoff) short-circuited to false for leads with an empty/unparseable CreatedDate, so undated leads always passed through regardless of --since, inflating per-source counts and skewing conversion rates for a scoped query. Extracted filterLeadsSince() and changed the guard to !ok || t.Before(cutoff) so both cases are excluded. Verified against live data: --since 7d now correctly returns 17/53 leads instead of leaking all 53.
|
@greptileai review Auto-nudge from |
Greptile finding: TimeOff.UserName is a login/username, not the display name job.Team[].Name reports (confirmed by the community PHP SDK, which calls the TimeOff lookup parameter "username" — a separate identifier space). A bare string comparison between the two never matched, so type:"time_off_conflict" entries were unreachable in practice despite the detection logic looking correct. Added buildUsernameToDisplayName()/resolveTimeOffOwner(), which resolve a TimeOff record's UserName to a display name via the synced team roster's email field (matching full email or its local-part, case-insensitive) — email is the only field shared between the timeoff and team resources. Falls back to the raw UserName when no roster match is found, so a time-off record is never silently dropped. Could not verify against live data (the test account has no recorded time-off), so this is a best-effort fix based on the SDK-documented semantics of the UserName field, not an empirically-confirmed match. Added regression tests for the email/local-part/no-match cases.
|
@greptileai review Auto-nudge from |
…aled set-token only ever persisted WorkizApiToken. WorkizApiSecret could only come from WORKIZ_API_SECRET, so a new shell without that var exported sent every write request to Workiz missing the required auth_secret body field, with no CLI-side indication of why. Adds Config.SaveSecret and `auth set-secret <secret>`, mirroring the existing set-token command, plus a regression test covering persistence and rotation. Greptile finding: PR mvanhorn#1441, internal/cli/auth.go:144
|
@greptileai review Auto-nudge from |
| } | ||
| ctx := cmd.Context() | ||
| var bail bool | ||
| if dbPath, bail = checkNovelMirror(cmd, flags, dbPath, "job,lead", []auditIssue{}); bail { |
There was a problem hiding this comment.
Sync hint omits
customer resource, silently producing incomplete audit results
checkNovelMirror is called with "job,lead" but job audit also loads the customer table. The migration always creates the customer table so there is no runtime error — but if a first-time user follows the displayed hint (workiz-pp-cli sync --resources job,lead), the customer table remains empty. Every subsequent job audit run will report zero customer-level issues regardless of actual data, making the customer section of the audit a silent false-negative. The resources string on this line should be "job,lead,customer".
workiz
Every Workiz dispatch and pipeline workflow, plus crew utilization, revenue, and conversion views no other Workiz tool has.
API: workiz | Category: sales-and-crm | Press version: 4.24.0
Spec: Not specified (hand-authored internal YAML spec; no official OpenAPI/Swagger spec is published by Workiz — see research brief for ground-truth sourcing from 4 independent SDKs)
Publication Path
New print
CLI Shape
Novel Commands
team bottleneck --weeklead funneljob revenuejob auditdigestjob searchWhat This CLI Does
Workiz has no CLI, MCP server, or agent-native tool today — every existing integration is a thin polling script or a hand-rolled SDK wrapper with zero cross-entity intelligence. This CLI absorbs every job/lead/client/team/time-off operation from the SDK ecosystem (18 endpoints across 5 resources, matching and beating 4 independent open-source SDKs plus Pipedream's official integration), then adds local joins across synced data to answer questions the live API simply can't: who's overbooked this week, which lead sources convert, and what changed since you last checked.
Workiz's auth is non-standard (API token embedded in the URL path, API secret in the POST body) — handled via two documented customizations (see
.printing-press-patches/) rather than the generator's standard header/query auth injection.Manuscripts
Validation Results
Publish Live Gate
Skipped with explicit reason:
auth-unavailable: no Workiz API credentials provided by the user for this session. Workiz requires a Developer API add-on token + secret pair that the operator did not have available. Everything else was verified via dry-run, mock-mode shipcheck, and hand-checked behavioral testing against a synthetic SQLite fixture standing in for real synced data (see Build Log for the exact assertions).Notable customizations
Two hand-authored patches (
.printing-press-patches/) work around Workiz's non-standard auth shape, which the generator has no built-in mode for:workiz-token-in-path-auth.json— API token folded into the base URL as a path segment at request-build time, not persisted, after an earlier version accidentally persisted and doubled the token acrossauth set-tokenrotations.workiz-auth-secret-body-injection.json— API secret auto-injected into every write request's JSON body, with--dry-runmasking to match the existing URL/header masking.A parallel 3-agent code review (correctness/security/maintainability) caught and fixed 2 credential-leak bugs and 1 critical token-persistence bug before this PR was opened; see the Phase 4.95 findings report linked above for full detail.