wiq-cli 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +16 -5
- data/docs/deferred.md +12 -21
- data/docs/wiq_api_notes.md +41 -5
- data/lib/wiq/cli.rb +2 -2
- data/lib/wiq/client.rb +6 -1
- data/lib/wiq/commands/auth.rb +35 -11
- data/lib/wiq/commands/doctor.rb +9 -0
- data/lib/wiq/commands/prospect_families.rb +226 -0
- data/lib/wiq/commands/prospects.rb +172 -9
- data/lib/wiq/commands/reports.rb +22 -2
- data/lib/wiq/commands/rosters.rb +6 -1
- data/lib/wiq/commands/setup.rb +3 -2
- data/lib/wiq/credentials.rb +5 -2
- data/lib/wiq/errors.rb +50 -4
- data/lib/wiq/version.rb +1 -1
- data/share/skills/wiq/SKILL.md +85 -17
- metadata +3 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec283a11b87c6ba8e2f8db5bf5a56f5e02fbf092aa3fb93fab54d08660553aa3
|
|
4
|
+
data.tar.gz: 1afd0ade5f5de6770c74acdd5d3fb0f2c68721f1537b0344a2ab6a401f32a400
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 93172776f6f73aac7093aa64b04bade0de00cc633f6b8e17b6a4c3a7898c3b8148fc98b9ccb108ab22df639b2e56c26b167e6f85ed22013cadd444f4d1fac574
|
|
7
|
+
data.tar.gz: 075e0ab62fb1098c04a0a7714825eef7ea0b8ad450e1d21703cbf2fe6c60b8d58f8ca2d2616ba6b8ead65ec9237db681737408476692aab9b0443847637d2ebc
|
data/README.md
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
> recommended for production-critical automation. Pin your gem version
|
|
8
8
|
> if you build anything load-bearing on top of it.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Command-line interface for [WrestlingIQ](https://www.wrestlingiq.com).
|
|
11
11
|
Designed to be driven by both humans and AI agents (Claude Code, Cursor,
|
|
12
|
-
etc.) via per-user personal access tokens (PATs).
|
|
12
|
+
etc.) via per-user personal access tokens (PATs). Every token can read;
|
|
13
|
+
tokens minted with a write scope can also work the leads pipeline.
|
|
13
14
|
|
|
14
15
|
## Getting started
|
|
15
16
|
|
|
@@ -39,6 +40,13 @@ coach, your CLI will be able to run admin reports; if you're not, it
|
|
|
39
40
|
won't. Either way, the token can be revoked anytime from the same
|
|
40
41
|
page.
|
|
41
42
|
|
|
43
|
+
Tokens are read-only unless you tick a write permission when minting
|
|
44
|
+
one. Today that's **Create and edit leads** (`prospects:write`) and
|
|
45
|
+
**Create reports** (`reports:write`). A team admin has to enable each
|
|
46
|
+
capability for the club first under **Settings → API Access**; the
|
|
47
|
+
token picker only offers what the team allows. Scopes are fixed for the
|
|
48
|
+
life of a token — to change them, revoke it and mint a new one.
|
|
49
|
+
|
|
42
50
|
### 3. Log in
|
|
43
51
|
|
|
44
52
|
```bash
|
|
@@ -81,6 +89,7 @@ Now you can ask Claude things like:
|
|
|
81
89
|
```bash
|
|
82
90
|
wiq rosters list # Browse rosters
|
|
83
91
|
wiq prospects summary # Pipeline dashboard
|
|
92
|
+
wiq prospects advance 123 trial_scheduled # Move a lead forward (needs prospects:write)
|
|
84
93
|
wiq events list --start 2026-05-01 --end 2026-05-31 # Calendar
|
|
85
94
|
wiq workflows list # Curated multi-step recipes
|
|
86
95
|
wiq reports types # 36 report types w/ recommendations
|
|
@@ -135,9 +144,11 @@ output (the CLI never logs your token — paste the full JSON safely).
|
|
|
135
144
|
|
|
136
145
|
## Status
|
|
137
146
|
|
|
138
|
-
v0.1
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
v0.6.1 — reads across every group, plus report submission and a
|
|
148
|
+
scoped write surface for the prospects pipeline (`prospect_families
|
|
149
|
+
create/update/note`, `prospects create/update/advance`) gated by the
|
|
150
|
+
`prospects:write` token scope. Other write commands deferred — see
|
|
151
|
+
`docs/deferred.md`.
|
|
141
152
|
|
|
142
153
|
## License
|
|
143
154
|
|
data/docs/deferred.md
CHANGED
|
@@ -81,17 +81,14 @@ Last updated: 2026-05-12 (after L1 ships).
|
|
|
81
81
|
- **`wiq url parse <url>`** — extract team/roster/event IDs from WIQ web
|
|
82
82
|
URLs. Initial-plan item; useful for agents pasted URLs by humans.
|
|
83
83
|
- **`wiq paid_sessions create/update`**, **`wiq rosters create/update`**,
|
|
84
|
-
etc. — every write path
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
`
|
|
91
|
-
|
|
92
|
-
pipeline edits happen in the drawer UI today, and exposing
|
|
93
|
-
stage-transition writes without first watching agents use them is
|
|
94
|
-
asking for trouble.
|
|
84
|
+
etc. — every other write path. Each needs a server-side capability in
|
|
85
|
+
`ApiCapability::REGISTRY` first (writes fail closed without one), then
|
|
86
|
+
a CLI command. Cherry-pick as customers ask.
|
|
87
|
+
- **Prospect deletes.** Shipped in v0.6.0: `prospect_families
|
|
88
|
+
create/update/note` and `prospects create/update/advance` behind the
|
|
89
|
+
`prospects:write` scope. `DELETE /prospect_families/:id` and
|
|
90
|
+
`DELETE /prospects/:id` remain out of reach — destroy actions are in
|
|
91
|
+
no API capability server-side, by design.
|
|
95
92
|
|
|
96
93
|
## Packaging / distribution
|
|
97
94
|
|
|
@@ -133,16 +130,10 @@ Last updated: 2026-05-12 (after L1 ships).
|
|
|
133
130
|
|
|
134
131
|
## Backend asks (push back to WIQ app team)
|
|
135
132
|
|
|
136
|
-
-
|
|
137
|
-
|
|
138
|
-
`
|
|
139
|
-
|
|
140
|
-
have `created_at`, so the join makes the order ambiguous. One-line
|
|
141
|
-
fix on the WIQ-app side: change to
|
|
142
|
-
`.order("prospects.created_at DESC")` (or
|
|
143
|
-
`.order(Prospect.arel_table[:created_at].desc)`). The CLI documents
|
|
144
|
-
the workaround (use `wiq prospect_families list --query` — same
|
|
145
|
-
matches, returns families with prospects embedded) until this ships.
|
|
133
|
+
- ~~**`GET /api/v1/prospects?query=<str>` 500s on ambiguous ORDER BY.**~~ —
|
|
134
|
+
SHIPPED (WIQ-app commit 7ac9d90a5, 2026-05-21, qualifies the ORDER BY
|
|
135
|
+
as `prospects.created_at`). `wiq prospects list --query` works; the
|
|
136
|
+
KNOWN BUG note was removed from its long_desc in v0.6.0.
|
|
146
137
|
|
|
147
138
|
|
|
148
139
|
- ~~**`days_threshold` permit fix on `Api::V1::ReportsController`**~~ —
|
data/docs/wiq_api_notes.md
CHANGED
|
@@ -288,13 +288,49 @@ file with the WIQ team.
|
|
|
288
288
|
permit list as of the May 2026 WIQ-app fix. The CLI's
|
|
289
289
|
`--days-threshold` flag lands unchanged on the model.
|
|
290
290
|
|
|
291
|
+
**PAT write capabilities (`Api::V1::BaseController#enforce_pat_restrictions`):**
|
|
292
|
+
|
|
293
|
+
PAT traffic is read-only by default. A non-GET request succeeds only when
|
|
294
|
+
`ApiCapability.for(controller, action)` resolves to a capability that
|
|
295
|
+
(a) the team has enabled (`Team#api_write_capabilities`, editable by an
|
|
296
|
+
admin at `/settings/team/api_access`) and (b) the token carries
|
|
297
|
+
(`PersonalAccessToken#scopes`, immutable after mint, coach tokens only).
|
|
298
|
+
Registry today:
|
|
299
|
+
|
|
300
|
+
| Capability | Actions |
|
|
301
|
+
| --- | --- |
|
|
302
|
+
| `prospects:write` | `prospect_families#create/#update`, `prospects#create/#update`, `prospect_family_notes#create` |
|
|
303
|
+
| `reports:write` | `reports#create` |
|
|
304
|
+
|
|
305
|
+
Unregistered writes (every `destroy`, everything else) fail closed with
|
|
306
|
+
`"This endpoint is not writable with a personal access token."` The other
|
|
307
|
+
two denials name the capability verbatim — the CLI regexes on them:
|
|
308
|
+
|
|
309
|
+
| Body | CLI `code` |
|
|
310
|
+
| --- | --- |
|
|
311
|
+
| `This endpoint is not writable with a personal access token.` | `pat_write_unsupported` |
|
|
312
|
+
| `This team has not enabled <cap> for API access. …` | `capability_disabled_for_team` |
|
|
313
|
+
| `This token lacks the <cap> scope. Revoke it and mint a new token …` | `token_missing_scope` |
|
|
314
|
+
|
|
315
|
+
Team policy is re-checked per request, so an admin turning a capability
|
|
316
|
+
off disables every token holding it immediately. Every PAT call writes an
|
|
317
|
+
audit row (`PersonalAccessTokenAuditJob`) carrying the capability + scopes.
|
|
318
|
+
`GET /api/v1/personal_access_tokens` now includes `scopes: [...]` per
|
|
319
|
+
token; the CLI stores it at login and shows it in `auth status` / `doctor`.
|
|
320
|
+
|
|
321
|
+
Prospect-specific PAT rule (`ProspectsController#update`): stage changes
|
|
322
|
+
are not forced for token callers, so `advance_to!` refuses downgrades and
|
|
323
|
+
moves out of a terminal stage. The controller turns that silent no-op
|
|
324
|
+
into a 422 `"Stage cannot move from <a> to <b> with a personal access
|
|
325
|
+
token …"` → CLI code `stage_transition_refused`.
|
|
326
|
+
|
|
291
327
|
**Auth model (POST /api/v1/reports):**
|
|
292
328
|
|
|
293
329
|
- Every report POST requires a `CoachProfile`-bound PAT on the same team
|
|
294
|
-
(`coach_belongs_to_team?` in `ReportPolicy#create?`)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
330
|
+
(`coach_belongs_to_team?` in `ReportPolicy#create?`) AND the
|
|
331
|
+
`reports:write` capability (backfilled onto every existing team and
|
|
332
|
+
token when per-token scopes shipped, so nothing changed for existing
|
|
333
|
+
scripts). See "PAT write capabilities" below.
|
|
298
334
|
- A subset of report types — the 9 listed in `Report.finance_types`
|
|
299
335
|
(Membership, PaidSessionAccounting, RecurringDonor, DonationTransaction,
|
|
300
336
|
FundraiserSummary, FundraiserAccounting, InProgressRegistration,
|
|
@@ -343,7 +379,7 @@ DiscountedSubscriptionsReport, **DonationTransactionReport**,
|
|
|
343
379
|
PaidSessionAddOnDetailReport, PaidSessionAddOnSummaryReport,
|
|
344
380
|
**PracticeAttendanceReport**, **RecurringDonorReport**, RegistrationAnswerReport,
|
|
345
381
|
**RegistrationFinanceSummaryReport**, **RosterReport**, **RosterStatsReport**,
|
|
346
|
-
**ScholarshipAuditReport**, SessionRegistrationAnswerReport
|
|
382
|
+
**ScholarshipAuditReport**, **SessionRegistrationAnswerReport**,
|
|
347
383
|
TeamRegistrationRosterReport, UsawExpiredReport, UsawExportReport, UsawReport,
|
|
348
384
|
**WinLossReport**, **WrestlersWithoutSubscriptionsReport**`
|
|
349
385
|
|
data/lib/wiq/cli.rb
CHANGED
|
@@ -68,10 +68,10 @@ module Wiq
|
|
|
68
68
|
desc "locations SUBCOMMAND", "Team locations / sites — ID discovery for --location flags (list, show)"
|
|
69
69
|
subcommand "locations", Wiq::Commands::Locations
|
|
70
70
|
|
|
71
|
-
desc "prospects SUBCOMMAND", "Prospect pipeline — individual kids (list, show, summary)"
|
|
71
|
+
desc "prospects SUBCOMMAND", "Prospect pipeline — individual kids (list, show, summary; create, update, advance need prospects:write)"
|
|
72
72
|
subcommand "prospects", Wiq::Commands::Prospects
|
|
73
73
|
|
|
74
|
-
desc "prospect_families SUBCOMMAND", "Prospect pipeline — households (list, show, notes)"
|
|
74
|
+
desc "prospect_families SUBCOMMAND", "Prospect pipeline — households (list, show, notes, stage_changes, linked_answers; create, update, note need prospects:write)"
|
|
75
75
|
subcommand "prospect_families", Wiq::Commands::ProspectFamilies
|
|
76
76
|
|
|
77
77
|
desc "workflows SUBCOMMAND", "Named multi-step recipes for common club-admin questions (list, show)"
|
data/lib/wiq/client.rb
CHANGED
|
@@ -9,7 +9,8 @@ module Wiq
|
|
|
9
9
|
# Thin wrapper around Faraday that:
|
|
10
10
|
# - Adds Authorization: Bearer <pat>
|
|
11
11
|
# - Parses JSON requests and responses
|
|
12
|
-
# - Retries on 429 and 5xx (Retry-After honored)
|
|
12
|
+
# - Retries on 429 and 5xx (Retry-After honored) — GET/HEAD only, so a
|
|
13
|
+
# write is never replayed
|
|
13
14
|
# - Translates non-2xx responses into Wiq::APIError
|
|
14
15
|
# - Exposes paginate(path, params) that walks the Link: rel=next chain
|
|
15
16
|
class Client
|
|
@@ -36,6 +37,10 @@ module Wiq
|
|
|
36
37
|
request(:put, path, body: body)
|
|
37
38
|
end
|
|
38
39
|
|
|
40
|
+
def patch(path, body = {})
|
|
41
|
+
request(:patch, path, body: body)
|
|
42
|
+
end
|
|
43
|
+
|
|
39
44
|
def delete(path, params = {})
|
|
40
45
|
request(:delete, path, params: params)
|
|
41
46
|
end
|
data/lib/wiq/commands/auth.rb
CHANGED
|
@@ -21,8 +21,14 @@ module Wiq
|
|
|
21
21
|
|
|
22
22
|
Verification: the CLI hits `GET /api/v1/personal_access_tokens` to
|
|
23
23
|
confirm the token works AND fetch the bound profile (display_name,
|
|
24
|
-
team_name, type) in one round trip.
|
|
25
|
-
login and stored alongside the token.
|
|
24
|
+
team_name, type) plus the token's write `scopes` in one round trip.
|
|
25
|
+
That metadata is shown after login and stored alongside the token.
|
|
26
|
+
|
|
27
|
+
Scopes: every PAT can read. Writes need a capability (currently
|
|
28
|
+
prospects:write, reports:write) that the team admin has enabled
|
|
29
|
+
under Settings → API Access AND that the token was minted with.
|
|
30
|
+
Scopes are immutable — to change them, revoke and mint a new
|
|
31
|
+
token, then re-run this command with --force.
|
|
26
32
|
DESC
|
|
27
33
|
method_option :token, type: :string, desc: "PAT value (otherwise prompted interactively)"
|
|
28
34
|
method_option :force, type: :boolean, default: false,
|
|
@@ -64,7 +70,8 @@ module Wiq
|
|
|
64
70
|
token: token,
|
|
65
71
|
token_prefix: metadata[:token_prefix],
|
|
66
72
|
name: metadata[:name],
|
|
67
|
-
profile: metadata[:profile]
|
|
73
|
+
profile: metadata[:profile],
|
|
74
|
+
scopes: metadata[:scopes]
|
|
68
75
|
)
|
|
69
76
|
|
|
70
77
|
render(
|
|
@@ -74,10 +81,11 @@ module Wiq
|
|
|
74
81
|
"token_prefix" => metadata[:token_prefix],
|
|
75
82
|
"name" => metadata[:name],
|
|
76
83
|
"profile" => metadata[:profile],
|
|
84
|
+
"scopes" => metadata[:scopes],
|
|
77
85
|
"stored_at" => Wiq::Credentials.for_host(host, alias_name)["stored_at"]
|
|
78
86
|
}.compact,
|
|
79
87
|
summary: profile_summary("Stored PAT for #{host} as #{alias_name.inspect}",
|
|
80
|
-
metadata[:profile]),
|
|
88
|
+
metadata[:profile], metadata[:scopes]),
|
|
81
89
|
breadcrumbs: [
|
|
82
90
|
{ "cmd" => "wiq auth status --as #{alias_name}",
|
|
83
91
|
"description" => "Verify the stored token works" },
|
|
@@ -93,7 +101,9 @@ module Wiq
|
|
|
93
101
|
|
|
94
102
|
Performs a best-effort live probe against
|
|
95
103
|
`/api/v1/personal_access_tokens` to confirm the token still works
|
|
96
|
-
and surface the live last_used_at
|
|
104
|
+
and surface the live last_used_at and write `scopes` (empty =
|
|
105
|
+
read-only). Check `live_scopes` before attempting a write command
|
|
106
|
+
such as `wiq prospects advance`. If the probe fails (network,
|
|
97
107
|
revoked token, host unreachable), the error appears in `live_error`
|
|
98
108
|
rather than aborting the command — local state is always shown.
|
|
99
109
|
|
|
@@ -126,9 +136,11 @@ module Wiq
|
|
|
126
136
|
"token_prefix" => store_entry["token_prefix"],
|
|
127
137
|
"stored_name" => store_entry["name"],
|
|
128
138
|
"stored_profile" => store_entry["profile"],
|
|
139
|
+
"stored_scopes" => store_entry["scopes"],
|
|
129
140
|
"live_name" => live_metadata&.dig(:name),
|
|
130
141
|
"live_last_used_at" => live_metadata&.dig(:last_used_at),
|
|
131
142
|
"live_profile" => live_metadata&.dig(:profile),
|
|
143
|
+
"live_scopes" => live_metadata&.dig(:scopes),
|
|
132
144
|
"live_error" => live_error
|
|
133
145
|
}.compact
|
|
134
146
|
|
|
@@ -138,7 +150,8 @@ module Wiq
|
|
|
138
150
|
: "No alias resolvable for this host."
|
|
139
151
|
else
|
|
140
152
|
profile_summary("Token reachable (alias=#{cfg.alias_name})",
|
|
141
|
-
live_metadata&.dig(:profile) || store_entry["profile"]
|
|
153
|
+
live_metadata&.dig(:profile) || store_entry["profile"],
|
|
154
|
+
live_metadata ? live_metadata[:scopes] : store_entry["scopes"])
|
|
142
155
|
end
|
|
143
156
|
|
|
144
157
|
render(data, summary: summary)
|
|
@@ -231,25 +244,36 @@ module Wiq
|
|
|
231
244
|
key: "personal_access_tokens"
|
|
232
245
|
)
|
|
233
246
|
match = rows.find { |r| r["token_prefix"] == prefix }
|
|
234
|
-
return { token_prefix: prefix, name: nil, last_used_at: nil, profile: nil } unless match
|
|
247
|
+
return { token_prefix: prefix, name: nil, last_used_at: nil, profile: nil, scopes: nil } unless match
|
|
235
248
|
|
|
236
249
|
{
|
|
237
250
|
token_prefix: match["token_prefix"],
|
|
238
251
|
name: match["name"],
|
|
239
252
|
last_used_at: match["last_used_at"],
|
|
240
|
-
profile: match["profile"]
|
|
253
|
+
profile: match["profile"],
|
|
254
|
+
# Servers predating per-token scopes omit the key; treat as read-only.
|
|
255
|
+
scopes: Array(match["scopes"])
|
|
241
256
|
}
|
|
242
257
|
end
|
|
243
258
|
|
|
244
|
-
def profile_summary(prefix, profile)
|
|
245
|
-
|
|
259
|
+
def profile_summary(prefix, profile, scopes = nil)
|
|
260
|
+
scope_note =
|
|
261
|
+
if scopes.nil?
|
|
262
|
+
nil
|
|
263
|
+
elsif scopes.empty?
|
|
264
|
+
"scopes: read-only"
|
|
265
|
+
else
|
|
266
|
+
"scopes: #{scopes.join(", ")}"
|
|
267
|
+
end
|
|
268
|
+
return [prefix, scope_note].compact.join("; ") + "." unless profile
|
|
246
269
|
|
|
247
270
|
who = profile["display_name"]
|
|
248
271
|
where = profile["team_name"]
|
|
249
272
|
kind = profile["type"]
|
|
250
273
|
tail = [who, where].compact.reject(&:empty?).join(" @ ")
|
|
251
274
|
tail += " (#{kind})" if kind && !kind.empty?
|
|
252
|
-
tail.empty? ?
|
|
275
|
+
head = tail.empty? ? prefix : "#{prefix} — #{tail}"
|
|
276
|
+
[head, scope_note].compact.join("; ") + "."
|
|
253
277
|
end
|
|
254
278
|
end
|
|
255
279
|
end
|
data/lib/wiq/commands/doctor.rb
CHANGED
|
@@ -15,6 +15,9 @@ module Wiq
|
|
|
15
15
|
6. Live reachability + auth probe via
|
|
16
16
|
`GET /api/v1/personal_access_tokens`
|
|
17
17
|
7. Bound profile (display_name, type, team) for the calling token
|
|
18
|
+
8. Write scopes on the token (empty = read-only; writes such as
|
|
19
|
+
`wiq prospects advance` need the matching capability enabled
|
|
20
|
+
by the team AND present on the token)
|
|
18
21
|
|
|
19
22
|
Exits non-zero if any check fails. Agents should run this first
|
|
20
23
|
when handed an unfamiliar shell to confirm they can actually call
|
|
@@ -59,6 +62,12 @@ module Wiq
|
|
|
59
62
|
checks << check("Bound profile",
|
|
60
63
|
"#{profile["display_name"]} (#{profile["type"]}) @ #{profile["team_name"]}",
|
|
61
64
|
ok: !profile["display_name"].nil?)
|
|
65
|
+
scopes = Array(match["scopes"])
|
|
66
|
+
checks << check("Write scopes",
|
|
67
|
+
scopes.empty? ? "none (read-only)" : scopes.join(", "),
|
|
68
|
+
ok: true,
|
|
69
|
+
hint: scopes.empty? ? "Reads only. Mint a token with prospects:write to use " \
|
|
70
|
+
"prospect write commands." : nil)
|
|
62
71
|
else
|
|
63
72
|
checks << check("Reachability + auth", "200 OK", ok: true)
|
|
64
73
|
checks << check("Bound profile", "(token not in own user's PAT list?)",
|
|
@@ -4,6 +4,7 @@ module Wiq
|
|
|
4
4
|
module Commands
|
|
5
5
|
class ProspectFamilies < Base
|
|
6
6
|
SORT_OPTIONS = %w[newest oldest_followup oldest_contact next_trial].freeze
|
|
7
|
+
ACTIVITY_TYPES = %w[phone_call sms email in_person other].freeze
|
|
7
8
|
|
|
8
9
|
desc "list", "List prospect families (one row per household)"
|
|
9
10
|
long_desc <<~DESC
|
|
@@ -124,6 +125,231 @@ module Wiq
|
|
|
124
125
|
]
|
|
125
126
|
)
|
|
126
127
|
end
|
|
128
|
+
|
|
129
|
+
desc "stage_changes FAMILY_ID", "Stage-transition audit log for every prospect in a family"
|
|
130
|
+
long_desc <<~DESC
|
|
131
|
+
Append-only history of every stage move for every prospect (kid)
|
|
132
|
+
in the family, newest first. Each row: prospect_id + child_name,
|
|
133
|
+
from_stage (null on the initial create), to_stage, changed_at,
|
|
134
|
+
changed_via, and changed_by (profile, or null for system moves).
|
|
135
|
+
|
|
136
|
+
changed_via values:
|
|
137
|
+
manual A person moved it (drawer, note shortcut, or a
|
|
138
|
+
PAT write — changed_by names the coach)
|
|
139
|
+
trial_registration Family bought a trial session
|
|
140
|
+
check_in Kid checked in to a trial practice
|
|
141
|
+
trial_expired Trial passes ran out
|
|
142
|
+
paid_registration Registered for a paid session (→ converted)
|
|
143
|
+
subscription Started a recurring membership (→ converted)
|
|
144
|
+
backfill Historical import
|
|
145
|
+
bulk_archive The stale-lead archive task
|
|
146
|
+
|
|
147
|
+
Use this to answer "did this lead actually trial or skip straight
|
|
148
|
+
to converted?" — the prospect row only carries its CURRENT stage —
|
|
149
|
+
and to review what an agent or coach did before attempting another
|
|
150
|
+
`wiq prospects advance` (moves are forward-only via PAT).
|
|
151
|
+
DESC
|
|
152
|
+
method_option :all, type: :boolean, default: false
|
|
153
|
+
def stage_changes(family_id)
|
|
154
|
+
records, total = fetch_index("/api/v1/prospect_families/#{family_id}/stage_changes",
|
|
155
|
+
{ "per_page" => 50 },
|
|
156
|
+
key: "stage_changes")
|
|
157
|
+
render_index(
|
|
158
|
+
records, total: total,
|
|
159
|
+
summary: "Listed #{records.size} stage changes for family #{family_id}.",
|
|
160
|
+
breadcrumbs: [
|
|
161
|
+
{ "cmd" => "wiq prospect_families show #{family_id}", "description" => "Back to the family" },
|
|
162
|
+
{ "cmd" => "wiq prospect_families notes #{family_id}", "description" => "Contact log alongside the stage history" }
|
|
163
|
+
]
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
desc "linked_answers FAMILY_ID", "Registration answers on the member profiles linked to a family"
|
|
168
|
+
long_desc <<~DESC
|
|
169
|
+
Trial-purchase leads skip the interest form, so their phone number
|
|
170
|
+
and other intake details usually exist only as registration answers
|
|
171
|
+
on the profiles the family is linked to: the guardian (parent or
|
|
172
|
+
coach) and each prospect's wrestler_profile. This returns one entry
|
|
173
|
+
per linked profile (profile_id, profile_type, full_name, relation =
|
|
174
|
+
guardian | wrestler) with its registration_answers, deduped to the
|
|
175
|
+
most recent answer per question and ordered by the question's
|
|
176
|
+
display order.
|
|
177
|
+
|
|
178
|
+
Not paginated. Coach visibility is enforced server-side, so
|
|
179
|
+
admin-only questions are omitted for non-admin tokens. A family
|
|
180
|
+
with no linked profiles returns an empty list.
|
|
181
|
+
|
|
182
|
+
Cheaper first stop: `wiq prospect_families show` already surfaces
|
|
183
|
+
`phone_suggestions` for blank-phone families. Use this when you
|
|
184
|
+
need everything known about the household before a call.
|
|
185
|
+
DESC
|
|
186
|
+
def linked_answers(family_id)
|
|
187
|
+
data = client.get("/api/v1/prospect_families/#{family_id}/linked_answers")
|
|
188
|
+
profiles = Array(data["linked_profiles"])
|
|
189
|
+
answer_count = profiles.sum { |p| Array(p["registration_answers"]).size }
|
|
190
|
+
render_index(
|
|
191
|
+
profiles,
|
|
192
|
+
summary: "#{profiles.size} linked profiles with #{answer_count} visible registration answers for family #{family_id}.",
|
|
193
|
+
breadcrumbs: [
|
|
194
|
+
{ "cmd" => "wiq prospect_families show #{family_id}", "description" => "Back to the family" },
|
|
195
|
+
{ "cmd" => "wiq prospect_families update #{family_id} --phone <number>",
|
|
196
|
+
"description" => "Copy a found phone onto the family record" }
|
|
197
|
+
]
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
desc "create", "Create a prospect family (household) [prospects:write]"
|
|
202
|
+
long_desc <<~DESC
|
|
203
|
+
POSTs to /api/v1/prospect_families. Requires a coach PAT minted
|
|
204
|
+
with the prospects:write scope on a team that has enabled it
|
|
205
|
+
(Settings → API Access). Only --first-name is required; --email
|
|
206
|
+
must be a valid address when given.
|
|
207
|
+
|
|
208
|
+
A family is the household record — add the kid(s) afterwards with
|
|
209
|
+
`wiq prospects create <family_id> --first-name ...`. Check for an
|
|
210
|
+
existing household first with `wiq prospect_families list --query
|
|
211
|
+
<name or email or phone>` to avoid duplicates.
|
|
212
|
+
|
|
213
|
+
--source is the machine-readable origin (e.g. web_form, walk_in,
|
|
214
|
+
referral); --hear-about-us is the family's free-text answer to
|
|
215
|
+
"How did you hear about us?". --assigned-coach takes a
|
|
216
|
+
coach_profile id. --guardian-id/--guardian-type link an existing
|
|
217
|
+
ParentProfile or CoachProfile as the guardian.
|
|
218
|
+
DESC
|
|
219
|
+
method_option :first_name, type: :string, required: true, desc: "contact_first_name (required)"
|
|
220
|
+
method_option :last_name, type: :string, desc: "contact_last_name"
|
|
221
|
+
method_option :email, type: :string, desc: "contact_email"
|
|
222
|
+
method_option :phone, type: :string, desc: "contact_phone"
|
|
223
|
+
method_option :hear_about_us, type: :string, desc: "Free-text 'How did you hear about us?'"
|
|
224
|
+
method_option :source, type: :string, desc: "Lead source tag (free text)"
|
|
225
|
+
method_option :assigned_coach, type: :numeric, desc: "assigned_coach_id (coach_profile id)"
|
|
226
|
+
method_option :guardian_id, type: :numeric, desc: "Existing profile id to link as guardian"
|
|
227
|
+
method_option :guardian_type, type: :string, enum: %w[ParentProfile CoachProfile],
|
|
228
|
+
desc: "Profile type for --guardian-id"
|
|
229
|
+
def create
|
|
230
|
+
family = client.post("/api/v1/prospect_families", { "prospect_family" => build_family_attrs })
|
|
231
|
+
render(family,
|
|
232
|
+
summary: "Created prospect family #{family["id"]} — #{family["contact_name"]}.",
|
|
233
|
+
breadcrumbs: [
|
|
234
|
+
{ "cmd" => "wiq prospects create #{family["id"]} --first-name <child>",
|
|
235
|
+
"description" => "Add the kid(s) to this household" },
|
|
236
|
+
{ "cmd" => "wiq prospect_families note #{family["id"]} --activity-type phone_call --content \"...\"",
|
|
237
|
+
"description" => "Log the first contact" }
|
|
238
|
+
])
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
desc "update ID", "Edit a prospect family's contact info or assignment [prospects:write]"
|
|
242
|
+
long_desc <<~DESC
|
|
243
|
+
PATCHes /api/v1/prospect_families/:id with only the flags you
|
|
244
|
+
pass. Requires the prospects:write scope (team-enabled + on the
|
|
245
|
+
token). Typical uses: fill in a missing phone (see
|
|
246
|
+
`phone_suggestions` on `wiq prospect_families show`), reassign a
|
|
247
|
+
coach, or correct a misspelled name.
|
|
248
|
+
DESC
|
|
249
|
+
method_option :first_name, type: :string, desc: "contact_first_name"
|
|
250
|
+
method_option :last_name, type: :string, desc: "contact_last_name"
|
|
251
|
+
method_option :email, type: :string, desc: "contact_email"
|
|
252
|
+
method_option :phone, type: :string, desc: "contact_phone"
|
|
253
|
+
method_option :hear_about_us, type: :string, desc: "Free-text 'How did you hear about us?'"
|
|
254
|
+
method_option :source, type: :string, desc: "Lead source tag (free text)"
|
|
255
|
+
method_option :assigned_coach, type: :numeric, desc: "assigned_coach_id (coach_profile id)"
|
|
256
|
+
method_option :guardian_id, type: :numeric, desc: "Existing profile id to link as guardian"
|
|
257
|
+
method_option :guardian_type, type: :string, enum: %w[ParentProfile CoachProfile],
|
|
258
|
+
desc: "Profile type for --guardian-id"
|
|
259
|
+
def update(id)
|
|
260
|
+
attrs = build_family_attrs
|
|
261
|
+
if attrs.empty?
|
|
262
|
+
raise Wiq::Error.new("Nothing to update — pass at least one field flag.",
|
|
263
|
+
code: "no_fields",
|
|
264
|
+
hint: "See `wiq prospect_families update --help` for the editable fields.")
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
family = client.patch("/api/v1/prospect_families/#{id}", { "prospect_family" => attrs })
|
|
268
|
+
render(family,
|
|
269
|
+
summary: "Updated prospect family #{family["id"]} — #{family["contact_name"]}.",
|
|
270
|
+
breadcrumbs: [
|
|
271
|
+
{ "cmd" => "wiq prospect_families show #{family["id"]}", "description" => "Refetch the family" }
|
|
272
|
+
])
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
desc "note FAMILY_ID", "Log a contact / add a note to a prospect family [prospects:write]"
|
|
276
|
+
long_desc <<~DESC
|
|
277
|
+
POSTs to /api/v1/prospect_families/:family_id/notes. Requires the
|
|
278
|
+
prospects:write scope (team-enabled + on the token). The note is
|
|
279
|
+
authored by the coach who minted the token.
|
|
280
|
+
|
|
281
|
+
--activity-type marks the note as a logged contact: the server
|
|
282
|
+
bumps last_contacted_at on every ACTIVE prospect in the family,
|
|
283
|
+
which is what clears "stale contact" follow-up flags. Omit it for
|
|
284
|
+
an internal note that shouldn't count as contact.
|
|
285
|
+
|
|
286
|
+
Side effects in the same call (prospect ids, comma-separated):
|
|
287
|
+
--clear-follow-up 12,34 Clear the needs_follow_up flag
|
|
288
|
+
--add-follow-up 56 Flag for follow-up (reason=manual)
|
|
289
|
+
Ids outside this family are ignored server-side.
|
|
290
|
+
|
|
291
|
+
Content is sent as plain text; @mentions are processed server-side.
|
|
292
|
+
DESC
|
|
293
|
+
method_option :content, type: :string, required: true, desc: "Note body (plain text)"
|
|
294
|
+
method_option :activity_type, type: :string, enum: ACTIVITY_TYPES,
|
|
295
|
+
desc: "phone_call | sms | email | in_person | other (omit for a plain note)"
|
|
296
|
+
method_option :clear_follow_up, type: :string,
|
|
297
|
+
desc: "Comma-separated prospect ids to un-flag for follow-up"
|
|
298
|
+
method_option :add_follow_up, type: :string,
|
|
299
|
+
desc: "Comma-separated prospect ids to flag for follow-up"
|
|
300
|
+
def note(family_id)
|
|
301
|
+
body = {
|
|
302
|
+
"note" => {
|
|
303
|
+
"content" => options[:content],
|
|
304
|
+
"plain_content" => options[:content]
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
body["note"]["activity_type"] = options[:activity_type] if options[:activity_type]
|
|
308
|
+
clear_ids = parse_id_list(options[:clear_follow_up])
|
|
309
|
+
add_ids = parse_id_list(options[:add_follow_up])
|
|
310
|
+
body["clear_follow_up_for"] = clear_ids unless clear_ids.empty?
|
|
311
|
+
body["add_follow_up_for"] = add_ids unless add_ids.empty?
|
|
312
|
+
|
|
313
|
+
note = client.post("/api/v1/prospect_families/#{family_id}/notes", body)
|
|
314
|
+
kind = options[:activity_type] ? "Logged #{options[:activity_type]} contact" : "Added note"
|
|
315
|
+
render(note,
|
|
316
|
+
summary: "#{kind} ##{note["id"]} on family #{family_id}.",
|
|
317
|
+
breadcrumbs: [
|
|
318
|
+
{ "cmd" => "wiq prospect_families notes #{family_id}", "description" => "Full contact log" },
|
|
319
|
+
{ "cmd" => "wiq prospect_families show #{family_id}", "description" => "Back to the family" }
|
|
320
|
+
])
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
no_commands do
|
|
324
|
+
# Maps CLI flags → the `prospect_family` permit list on
|
|
325
|
+
# Api::V1::ProspectFamiliesController.
|
|
326
|
+
FAMILY_FIELD_MAP = {
|
|
327
|
+
first_name: "contact_first_name",
|
|
328
|
+
last_name: "contact_last_name",
|
|
329
|
+
email: "contact_email",
|
|
330
|
+
phone: "contact_phone",
|
|
331
|
+
hear_about_us: "hear_about_us",
|
|
332
|
+
source: "source",
|
|
333
|
+
assigned_coach: "assigned_coach_id",
|
|
334
|
+
guardian_id: "guardian_id",
|
|
335
|
+
guardian_type: "guardian_type"
|
|
336
|
+
}.freeze
|
|
337
|
+
|
|
338
|
+
def build_family_attrs
|
|
339
|
+
attrs = {}
|
|
340
|
+
FAMILY_FIELD_MAP.each do |flag, param|
|
|
341
|
+
value = options[flag]
|
|
342
|
+
attrs[param] = value unless value.nil?
|
|
343
|
+
end
|
|
344
|
+
attrs
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def parse_id_list(raw)
|
|
348
|
+
return [] if raw.nil? || raw.to_s.strip.empty?
|
|
349
|
+
|
|
350
|
+
raw.to_s.split(",").map(&:strip).reject(&:empty?).map(&:to_i).reject(&:zero?)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
127
353
|
end
|
|
128
354
|
end
|
|
129
355
|
end
|
|
@@ -4,8 +4,14 @@ module Wiq
|
|
|
4
4
|
module Commands
|
|
5
5
|
class Prospects < Base
|
|
6
6
|
STAGES = %w[inquiry trial_scheduled trialing trial_complete converted didnt_join archived].freeze
|
|
7
|
+
TERMINAL_STAGES = %w[converted didnt_join archived].freeze
|
|
7
8
|
ATTENTION_MODES = %w[needs_attention handled].freeze
|
|
8
9
|
|
|
10
|
+
# Write capability every create/update/advance below needs. The server
|
|
11
|
+
# checks it per request: the team must have it enabled AND the token
|
|
12
|
+
# must have been minted with it. See `wiq auth status` for scopes.
|
|
13
|
+
WRITE_CAPABILITY = "prospects:write"
|
|
14
|
+
|
|
9
15
|
desc "list", "List individual prospects (one row per kid)"
|
|
10
16
|
long_desc <<~DESC
|
|
11
17
|
Returns one row per prospect (kid), sorted newest-first.
|
|
@@ -21,15 +27,6 @@ module Wiq
|
|
|
21
27
|
scope which unions both via a subquery.
|
|
22
28
|
When set, ALL other filters are bypassed
|
|
23
29
|
server-side.
|
|
24
|
-
|
|
25
|
-
KNOWN BUG: `wiq prospects list --query …`
|
|
26
|
-
currently returns HTTP 500 due to an
|
|
27
|
-
ambiguous-column ORDER BY on the
|
|
28
|
-
prospect↔prospect_family join. Workaround:
|
|
29
|
-
use `wiq prospect_families list --query …`
|
|
30
|
-
instead (same search scope, same matches,
|
|
31
|
-
returns the family with its prospects
|
|
32
|
-
nested inline).
|
|
33
30
|
--attention needs_attention | handled
|
|
34
31
|
--stage One funnel stage
|
|
35
32
|
--assigned-to-me Only families assigned to the calling coach
|
|
@@ -92,6 +89,138 @@ module Wiq
|
|
|
92
89
|
])
|
|
93
90
|
end
|
|
94
91
|
|
|
92
|
+
desc "create FAMILY_ID", "Add a prospect (kid) to an existing prospect family [prospects:write]"
|
|
93
|
+
long_desc <<~DESC
|
|
94
|
+
POSTs to /api/v1/prospect_families/:family_id/prospects. Requires a
|
|
95
|
+
coach PAT minted with the prospects:write scope on a team that has
|
|
96
|
+
enabled it (Settings → API Access). Only --first-name is required.
|
|
97
|
+
|
|
98
|
+
New prospects start at stage `inquiry` unless --stage is given.
|
|
99
|
+
The stage audit row is attributed to the coach who minted the token.
|
|
100
|
+
|
|
101
|
+
Create the household first with `wiq prospect_families create` if
|
|
102
|
+
the family doesn't exist yet; use `wiq prospect_families list
|
|
103
|
+
--query <name>` to check.
|
|
104
|
+
|
|
105
|
+
Errors you may see:
|
|
106
|
+
capability_disabled_for_team Team hasn't enabled prospects:write
|
|
107
|
+
token_missing_scope Token wasn't minted with it — mint a new one
|
|
108
|
+
validation_failed Server rejected a field (see details)
|
|
109
|
+
DESC
|
|
110
|
+
method_option :first_name, type: :string, required: true, desc: "child_first_name (required)"
|
|
111
|
+
method_option :last_name, type: :string, desc: "child_last_name"
|
|
112
|
+
method_option :dob, type: :string, desc: "child_date_of_birth (YYYY-MM-DD)"
|
|
113
|
+
method_option :academic_class, type: :string, desc: "child_academic_class (free text, e.g. 5th)"
|
|
114
|
+
method_option :experience_level, type: :string, desc: "experience_level (free text, e.g. none / 1 season)"
|
|
115
|
+
method_option :stage, type: :string, enum: STAGES, desc: "Initial stage (default: inquiry)"
|
|
116
|
+
method_option :paid_session, type: :numeric, desc: "paid_session_id of the trial session"
|
|
117
|
+
method_option :trial_event, type: :numeric, desc: "trial_event_id — the practice/event they'll try"
|
|
118
|
+
method_option :trial_scheduled_at, type: :string, desc: "ISO-8601 timestamp"
|
|
119
|
+
method_option :needs_follow_up, type: :boolean, desc: "Flag (or --no-needs-follow-up to clear) for follow-up"
|
|
120
|
+
def create(family_id)
|
|
121
|
+
body = { "prospect" => build_prospect_attrs }
|
|
122
|
+
prospect = client.post("/api/v1/prospect_families/#{family_id}/prospects", body)
|
|
123
|
+
render(prospect,
|
|
124
|
+
summary: "Created prospect #{prospect["id"]} — #{prospect["child_first_name"]} " \
|
|
125
|
+
"#{prospect["child_last_name"]} (stage=#{prospect["stage"]}) on family #{family_id}.",
|
|
126
|
+
breadcrumbs: [
|
|
127
|
+
{ "cmd" => "wiq prospects show #{prospect["id"]}", "description" => "Refetch the prospect" },
|
|
128
|
+
{ "cmd" => "wiq prospect_families note #{family_id} --activity-type phone_call --content \"...\"",
|
|
129
|
+
"description" => "Log the first contact" }
|
|
130
|
+
])
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
desc "update ID", "Edit a prospect's details and/or move its stage [prospects:write]"
|
|
134
|
+
long_desc <<~DESC
|
|
135
|
+
PATCHes /api/v1/prospects/:id with only the flags you pass. Requires
|
|
136
|
+
the prospects:write scope (team-enabled + on the token).
|
|
137
|
+
|
|
138
|
+
Stage changes via a PAT are FORWARD-ONLY and cannot leave a terminal
|
|
139
|
+
stage (converted, didnt_join, archived). The server answers 422
|
|
140
|
+
(`stage_transition_refused`) rather than silently no-op'ing; a coach
|
|
141
|
+
can force the move in the web app. `wiq prospects advance` is the
|
|
142
|
+
same call with a clearer signature for stage-only moves.
|
|
143
|
+
|
|
144
|
+
--needs-follow-up / --no-needs-follow-up also syncs follow_up_set_at
|
|
145
|
+
and follow_up_reason (=manual) server-side so the flag stays
|
|
146
|
+
internally consistent.
|
|
147
|
+
|
|
148
|
+
Funnel timestamps (--trial-scheduled-at, --trial-completed-at,
|
|
149
|
+
--converted-at, --archived-at, --last-contacted-at) accept ISO-8601
|
|
150
|
+
and are normally stamped by the stage change itself — pass them
|
|
151
|
+
only to backfill history.
|
|
152
|
+
DESC
|
|
153
|
+
method_option :first_name, type: :string, desc: "child_first_name"
|
|
154
|
+
method_option :last_name, type: :string, desc: "child_last_name"
|
|
155
|
+
method_option :dob, type: :string, desc: "child_date_of_birth (YYYY-MM-DD)"
|
|
156
|
+
method_option :academic_class, type: :string, desc: "child_academic_class"
|
|
157
|
+
method_option :experience_level, type: :string, desc: "experience_level"
|
|
158
|
+
method_option :stage, type: :string, enum: STAGES, desc: "Move to this stage (forward-only via PAT)"
|
|
159
|
+
method_option :lost_reason, type: :string, desc: "Why they didn't join (pairs with --stage didnt_join)"
|
|
160
|
+
method_option :paid_session, type: :numeric, desc: "paid_session_id of the trial session"
|
|
161
|
+
method_option :trial_event, type: :numeric, desc: "trial_event_id"
|
|
162
|
+
method_option :wrestler_profile, type: :numeric, desc: "wrestler_profile_id to link (on conversion)"
|
|
163
|
+
method_option :needs_follow_up, type: :boolean, desc: "Flag (or --no-needs-follow-up to clear) for follow-up"
|
|
164
|
+
method_option :trial_scheduled_at, type: :string, desc: "ISO-8601 timestamp"
|
|
165
|
+
method_option :trial_completed_at, type: :string, desc: "ISO-8601 timestamp"
|
|
166
|
+
method_option :converted_at, type: :string, desc: "ISO-8601 timestamp"
|
|
167
|
+
method_option :archived_at, type: :string, desc: "ISO-8601 timestamp"
|
|
168
|
+
method_option :last_contacted_at, type: :string, desc: "ISO-8601 timestamp"
|
|
169
|
+
def update(id)
|
|
170
|
+
attrs = build_prospect_attrs
|
|
171
|
+
if attrs.empty?
|
|
172
|
+
raise Wiq::Error.new("Nothing to update — pass at least one field flag.",
|
|
173
|
+
code: "no_fields",
|
|
174
|
+
hint: "See `wiq prospects update --help` for the editable fields.")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
prospect = client.patch("/api/v1/prospects/#{id}", { "prospect" => attrs })
|
|
178
|
+
render(prospect,
|
|
179
|
+
summary: "Updated prospect #{prospect["id"]} — #{prospect["child_first_name"]} " \
|
|
180
|
+
"#{prospect["child_last_name"]} (stage=#{prospect["stage"]}).",
|
|
181
|
+
breadcrumbs: [
|
|
182
|
+
{ "cmd" => "wiq prospects show #{prospect["id"]}", "description" => "Refetch the prospect" },
|
|
183
|
+
{ "cmd" => "wiq prospect_families show #{prospect["prospect_family_id"]}",
|
|
184
|
+
"description" => "Family this prospect belongs to" }
|
|
185
|
+
])
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
desc "advance ID STAGE", "Move a prospect forward to STAGE [prospects:write]"
|
|
189
|
+
long_desc <<~DESC
|
|
190
|
+
Shorthand for `wiq prospects update ID --stage STAGE`. Stage order:
|
|
191
|
+
|
|
192
|
+
inquiry → trial_scheduled → trialing → trial_complete
|
|
193
|
+
→ converted | didnt_join | archived (terminal)
|
|
194
|
+
|
|
195
|
+
Via a personal access token the move must go FORWARD in that order
|
|
196
|
+
and cannot start from a terminal stage. Skipping ahead is fine
|
|
197
|
+
(inquiry → converted). Anything else returns 422 with code
|
|
198
|
+
`stage_transition_refused`; a coach can override in the web app.
|
|
199
|
+
|
|
200
|
+
--lost-reason is stored alongside a move to didnt_join.
|
|
201
|
+
DESC
|
|
202
|
+
method_option :lost_reason, type: :string, desc: "Why they didn't join (with didnt_join)"
|
|
203
|
+
def advance(id, stage)
|
|
204
|
+
unless STAGES.include?(stage)
|
|
205
|
+
raise Wiq::Error.new("Unknown stage #{stage.inspect}.",
|
|
206
|
+
code: "invalid_stage",
|
|
207
|
+
hint: "Valid stages: #{STAGES.join(", ")}")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
attrs = { "stage" => stage }
|
|
211
|
+
attrs["lost_reason"] = options[:lost_reason] if options[:lost_reason]
|
|
212
|
+
prospect = client.patch("/api/v1/prospects/#{id}", { "prospect" => attrs })
|
|
213
|
+
render(prospect,
|
|
214
|
+
summary: "Prospect #{prospect["id"]} — #{prospect["child_first_name"]} " \
|
|
215
|
+
"#{prospect["child_last_name"]} is now #{prospect["stage"]}.",
|
|
216
|
+
breadcrumbs: [
|
|
217
|
+
{ "cmd" => "wiq prospect_families note #{prospect["prospect_family_id"]} " \
|
|
218
|
+
"--activity-type other --content \"...\"",
|
|
219
|
+
"description" => "Log why, so the contact history matches the stage" },
|
|
220
|
+
{ "cmd" => "wiq prospects show #{prospect["id"]}", "description" => "Refetch the prospect" }
|
|
221
|
+
])
|
|
222
|
+
end
|
|
223
|
+
|
|
95
224
|
desc "summary", "Pipeline dashboard: counts per stage + conversion rate"
|
|
96
225
|
long_desc <<~DESC
|
|
97
226
|
Single-call dashboard. Returns an unwrapped object (not paginated)
|
|
@@ -147,6 +276,40 @@ module Wiq
|
|
|
147
276
|
{ "cmd" => "wiq prospect_families list", "description" => "List by family instead of by kid" }
|
|
148
277
|
]
|
|
149
278
|
end
|
|
279
|
+
|
|
280
|
+
# Maps CLI flags → the `prospect` param permit list on
|
|
281
|
+
# Api::V1::ProspectsController. Only flags actually passed land in
|
|
282
|
+
# the body so PATCH stays a partial update.
|
|
283
|
+
FIELD_MAP = {
|
|
284
|
+
first_name: "child_first_name",
|
|
285
|
+
last_name: "child_last_name",
|
|
286
|
+
dob: "child_date_of_birth",
|
|
287
|
+
academic_class: "child_academic_class",
|
|
288
|
+
experience_level: "experience_level",
|
|
289
|
+
stage: "stage",
|
|
290
|
+
lost_reason: "lost_reason",
|
|
291
|
+
paid_session: "paid_session_id",
|
|
292
|
+
trial_event: "trial_event_id",
|
|
293
|
+
wrestler_profile: "wrestler_profile_id",
|
|
294
|
+
trial_scheduled_at: "trial_scheduled_at",
|
|
295
|
+
trial_completed_at: "trial_completed_at",
|
|
296
|
+
converted_at: "converted_at",
|
|
297
|
+
archived_at: "archived_at",
|
|
298
|
+
last_contacted_at: "last_contacted_at"
|
|
299
|
+
}.freeze
|
|
300
|
+
|
|
301
|
+
def build_prospect_attrs
|
|
302
|
+
attrs = {}
|
|
303
|
+
FIELD_MAP.each do |flag, param|
|
|
304
|
+
value = options[flag]
|
|
305
|
+
attrs[param] = value unless value.nil?
|
|
306
|
+
end
|
|
307
|
+
# Boolean: Thor sets nil when the flag isn't passed, true/false otherwise.
|
|
308
|
+
unless options[:needs_follow_up].nil?
|
|
309
|
+
attrs["needs_follow_up"] = options[:needs_follow_up]
|
|
310
|
+
end
|
|
311
|
+
attrs
|
|
312
|
+
end
|
|
150
313
|
end
|
|
151
314
|
end
|
|
152
315
|
end
|
data/lib/wiq/commands/reports.rb
CHANGED
|
@@ -234,9 +234,29 @@ module Wiq
|
|
|
234
234
|
"SessionRegistrationAnswerReport" => {
|
|
235
235
|
args: %w[paid_session_id],
|
|
236
236
|
dates: :optional,
|
|
237
|
-
desc: "Full Q&A export of info submitted by parents at signup"
|
|
237
|
+
desc: "Full Q&A export of info submitted by parents at signup, one row per wrestler " \
|
|
238
|
+
"with stable ids + registration status",
|
|
238
239
|
recommended: true,
|
|
239
|
-
notes: "Pass --paid-session <id> — required."
|
|
240
|
+
notes: "Pass --paid-session <id> — required. The default (vrow) row shape " \
|
|
241
|
+
"leads with seven registration columns: \"WIQ ID #\" (wrestler_profile " \
|
|
242
|
+
"id — same header/value as RosterReport column A, so the two reports " \
|
|
243
|
+
"join on it), \"Registration ID\" (the stable key for external syncs), " \
|
|
244
|
+
"\"Registration status\" (paid | partially_paid | pending | overdue | " \
|
|
245
|
+
"canceled | awaiting_approval), \"Good standing\" (true/false: status in " \
|
|
246
|
+
"paid/partially_paid/pending), \"Registered at\", \"Registration updated " \
|
|
247
|
+
"at\", \"Registration canceled at\" (timestamps in the team's zone; blank " \
|
|
248
|
+
"unless canceled). Then Last/First name, Email, Account type, Academic " \
|
|
249
|
+
"class, Weight class, DOB, Age, created-at, USAW/AAU membership columns " \
|
|
250
|
+
"(unless disabled for the team), each wrestler registration question, " \
|
|
251
|
+
"and per-guardian name/email/type + guardian questions. One row per " \
|
|
252
|
+
"wrestler: when a wrestler has several registrations for the session " \
|
|
253
|
+
"(canceled then re-registered) the latest non-canceled one is " \
|
|
254
|
+
"reported, else the latest canceled one; blank registration columns " \
|
|
255
|
+
"mean no registration row exists. Anonymous sessions emit only " \
|
|
256
|
+
"Last name / First name / Email. --v1 returns structured JSON " \
|
|
257
|
+
"(wrestler_profiles + questions + answers) without the " \
|
|
258
|
+
"registration columns.",
|
|
259
|
+
example: "wiq reports run SessionRegistrationAnswerReport --paid-session 42"
|
|
240
260
|
},
|
|
241
261
|
"PaidSessionAccountingReport" => {
|
|
242
262
|
args: %w[paid_session_id],
|
data/lib/wiq/commands/rosters.rb
CHANGED
|
@@ -5,7 +5,9 @@ module Wiq
|
|
|
5
5
|
class Rosters < Base
|
|
6
6
|
desc "list", "List rosters"
|
|
7
7
|
long_desc <<~DESC
|
|
8
|
-
Returns
|
|
8
|
+
Returns the team's active rosters by default, paginated. Archived
|
|
9
|
+
rosters are hidden unless you pass --include-archived (returns both)
|
|
10
|
+
or --archived true (returns only archived).
|
|
9
11
|
|
|
10
12
|
Season filtering is a CLI-side projection (WIQ has no first-class
|
|
11
13
|
Season entity):
|
|
@@ -31,9 +33,12 @@ module Wiq
|
|
|
31
33
|
method_option :season_tag, type: :string, desc: "Filter to rosters carrying this tag"
|
|
32
34
|
method_option :location, type: :numeric, desc: "Filter to rosters at one location id"
|
|
33
35
|
method_option :archived, type: :boolean, desc: "Show only archived (true) or active (false)"
|
|
36
|
+
method_option :include_archived, type: :boolean, default: false,
|
|
37
|
+
desc: "Include archived rosters alongside active ones"
|
|
34
38
|
method_option :all, type: :boolean, default: false
|
|
35
39
|
def list
|
|
36
40
|
params = { "per_page" => 100 }
|
|
41
|
+
params["include_archived"] = true if options[:include_archived]
|
|
37
42
|
unless options[:archived].nil?
|
|
38
43
|
params["q[archived_eq]"] = options[:archived]
|
|
39
44
|
end
|
data/lib/wiq/commands/setup.rb
CHANGED
|
@@ -23,8 +23,9 @@ module Wiq
|
|
|
23
23
|
restart needed. Re-run with --force to overwrite an existing
|
|
24
24
|
install (useful when upgrading the gem).
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
drive `wiq
|
|
26
|
+
Installing the skill doesn't touch any WIQ data: it only teaches
|
|
27
|
+
Claude how to drive `wiq`. What Claude can then change is bounded
|
|
28
|
+
by the token's write scopes (see `wiq auth status`).
|
|
28
29
|
DESC
|
|
29
30
|
method_option :project, type: :boolean, default: false,
|
|
30
31
|
desc: "Install per-project (./.claude/skills/) instead of user-global"
|
data/lib/wiq/credentials.rb
CHANGED
|
@@ -10,7 +10,8 @@ module Wiq
|
|
|
10
10
|
# {
|
|
11
11
|
# "<host>": {
|
|
12
12
|
# "<alias>": { "token": "...", "token_prefix": "...", "name": "...",
|
|
13
|
-
# "profile": {...}, "
|
|
13
|
+
# "profile": {...}, "scopes": ["prospects:write"],
|
|
14
|
+
# "stored_at": "..." },
|
|
14
15
|
# "<alias>": { ... }
|
|
15
16
|
# }
|
|
16
17
|
# }
|
|
@@ -55,13 +56,14 @@ module Wiq
|
|
|
55
56
|
"token_prefix" => entry["token_prefix"],
|
|
56
57
|
"name" => entry["name"],
|
|
57
58
|
"profile" => entry["profile"],
|
|
59
|
+
"scopes" => entry["scopes"],
|
|
58
60
|
"stored_at" => entry["stored_at"]
|
|
59
61
|
}
|
|
60
62
|
end
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
|
|
64
|
-
def store(host:, alias_name:, token:, token_prefix: nil, name: nil, profile: nil)
|
|
66
|
+
def store(host:, alias_name:, token:, token_prefix: nil, name: nil, profile: nil, scopes: nil)
|
|
65
67
|
data = load_all
|
|
66
68
|
data[host] ||= {}
|
|
67
69
|
data[host][alias_name] = {
|
|
@@ -69,6 +71,7 @@ module Wiq
|
|
|
69
71
|
"token_prefix" => token_prefix,
|
|
70
72
|
"name" => name,
|
|
71
73
|
"profile" => profile,
|
|
74
|
+
"scopes" => scopes,
|
|
72
75
|
"stored_at" => Time.now.utc.iso8601
|
|
73
76
|
}.compact
|
|
74
77
|
write(data)
|
data/lib/wiq/errors.rb
CHANGED
|
@@ -76,13 +76,28 @@ module Wiq
|
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
# Mirror of the HTTP error envelope from /api/v1.
|
|
79
|
+
#
|
|
80
|
+
# PAT write policy (Api::V1::BaseController#enforce_pat_restrictions):
|
|
81
|
+
# a write goes through only when it maps to a capability in the server's
|
|
82
|
+
# ApiCapability registry that the team has enabled AND the token carries.
|
|
83
|
+
# The server emits three distinct 403 bodies for the three failure modes,
|
|
84
|
+
# naming the capability verbatim; we key off that wording to give each
|
|
85
|
+
# its own `code` + fix-it hint. Keep the regexes in sync with
|
|
86
|
+
# `pat_denial_message` in the Rails app.
|
|
79
87
|
class APIError < Error
|
|
80
|
-
attr_reader :status, :response_body, :request_id
|
|
88
|
+
attr_reader :status, :response_body, :request_id, :capability
|
|
89
|
+
|
|
90
|
+
PAT_NOT_WRITABLE = /not writable with a personal access token/i
|
|
91
|
+
PAT_TEAM_DISABLED = /team has not enabled (\S+) for API access/i
|
|
92
|
+
PAT_TOKEN_MISSING_SCOPE = /token lacks the (\S+) scope/i
|
|
93
|
+
PAT_LEGACY_READ_ONLY = /personal access tokens are read-only/i
|
|
94
|
+
PAT_STAGE_REFUSED = /stage cannot move from (\S+) to (\S+) with a personal access token/i
|
|
81
95
|
|
|
82
96
|
def initialize(status:, body:, request_id: nil)
|
|
83
97
|
@status = status
|
|
84
98
|
@response_body = body
|
|
85
99
|
@request_id = request_id
|
|
100
|
+
@capability = nil
|
|
86
101
|
|
|
87
102
|
code, message, hint = derive(status, body)
|
|
88
103
|
super(message, code: code, hint: hint, exit_code: 1, details: body)
|
|
@@ -97,12 +112,11 @@ module Wiq
|
|
|
97
112
|
["unauthorized", "Token rejected by server (401).",
|
|
98
113
|
"Run `wiq auth status` to inspect the active token; `wiq auth login` to replace it."]
|
|
99
114
|
when 403
|
|
100
|
-
|
|
101
|
-
"PATs inherit the user's permissions. Confirm the minting user can see this resource in the web app."]
|
|
115
|
+
derive_forbidden(msgs)
|
|
102
116
|
when 404
|
|
103
117
|
["not_found", "Resource not found (404).", nil]
|
|
104
118
|
when 422
|
|
105
|
-
|
|
119
|
+
derive_unprocessable(msgs)
|
|
106
120
|
when 429
|
|
107
121
|
["rate_limited", "Rate limited (429): #{msgs}",
|
|
108
122
|
"WIQ enforces 100 req/3s per IP. Back off and retry."]
|
|
@@ -111,6 +125,38 @@ module Wiq
|
|
|
111
125
|
end
|
|
112
126
|
end
|
|
113
127
|
|
|
128
|
+
def derive_forbidden(msgs)
|
|
129
|
+
if msgs =~ PAT_NOT_WRITABLE || msgs =~ PAT_LEGACY_READ_ONLY
|
|
130
|
+
["pat_write_unsupported", "Server denied access (403): #{msgs}",
|
|
131
|
+
"This endpoint has no API write capability, so no personal access token can call it. " \
|
|
132
|
+
"Make the change in the WIQ web app."]
|
|
133
|
+
elsif (m = msgs.match(PAT_TEAM_DISABLED))
|
|
134
|
+
@capability = m[1]
|
|
135
|
+
["capability_disabled_for_team", "Server denied access (403): #{msgs}",
|
|
136
|
+
"A team admin must enable #{@capability} under Settings → API Access " \
|
|
137
|
+
"(<host>/settings/team/api_access). Existing tokens minted with that scope start working immediately."]
|
|
138
|
+
elsif (m = msgs.match(PAT_TOKEN_MISSING_SCOPE))
|
|
139
|
+
@capability = m[1]
|
|
140
|
+
["token_missing_scope", "Server denied access (403): #{msgs}",
|
|
141
|
+
"Token scopes are immutable. Mint a new token that includes #{@capability} at " \
|
|
142
|
+
"<host>/settings/personal_access_tokens, then `wiq auth login --force` to replace the stored one. " \
|
|
143
|
+
"Run `wiq auth status` to see the scopes on the current token."]
|
|
144
|
+
else
|
|
145
|
+
["forbidden", "Server denied access (403): #{msgs}",
|
|
146
|
+
"PATs inherit the user's permissions. Confirm the minting user can see this resource in the web app."]
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def derive_unprocessable(msgs)
|
|
151
|
+
if msgs =~ PAT_STAGE_REFUSED
|
|
152
|
+
["stage_transition_refused", "Validation error (422): #{msgs}",
|
|
153
|
+
"Stage changes via a personal access token are forward-only and cannot leave a terminal stage " \
|
|
154
|
+
"(converted, didnt_join, archived). A coach can force the move in the WIQ web app."]
|
|
155
|
+
else
|
|
156
|
+
["validation_failed", "Validation error (422): #{msgs}", nil]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
114
160
|
# /api/v1 always returns { "errors": <array|hash> }. Flatten to a human string.
|
|
115
161
|
def extract_messages(body)
|
|
116
162
|
return body.to_s unless body.is_a?(Hash)
|
data/lib/wiq/version.rb
CHANGED
data/share/skills/wiq/SKILL.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wiq
|
|
3
|
-
description: Use this skill when the user asks about their WrestlingIQ data — rosters, attendance, check-ins, paid sessions and registrations, the prospects/leads pipeline, financial metrics, payouts/bank deposits, reports, USAW/AAU memberships, fundraising, online store orders, or per-location/site breakdowns for multi-gym clubs. The `wiq` CLI
|
|
3
|
+
description: Use this skill when the user asks about their WrestlingIQ data — rosters, attendance, check-ins, paid sessions and registrations, the prospects/leads pipeline, financial metrics, payouts/bank deposits, reports, USAW/AAU memberships, fundraising, online store orders, or per-location/site breakdowns for multi-gym clubs. The `wiq` CLI reads /api/v1 via personal access tokens and, when the token carries the prospects:write scope, can also create/edit leads, log contact notes, and advance prospects through the pipeline. Recognize phrasings like "how is our pipeline?", "who came to practice this week?", "show me the roster", "what's our MRR?", "which kids need USAW renewal?", "how is the Eastside gym doing?", or anything that maps to a wrestling club's admin workflows.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# WrestlingIQ CLI Skill
|
|
7
7
|
|
|
8
|
-
`wiq` is a
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
`wiq` is a Ruby CLI for WrestlingIQ data, designed to be driven by both
|
|
9
|
+
humans and AI agents. It hits `/api/v1` using per-user personal access
|
|
10
|
+
tokens (PATs). Every token can read; a token additionally minted with a
|
|
11
|
+
write scope (see "Writes" below) can create and edit leads. This skill
|
|
12
|
+
orients you to its shape; the CLI itself is the source of truth for
|
|
13
|
+
what's available right now.
|
|
12
14
|
|
|
13
15
|
## Bootstrap (always do this first)
|
|
14
16
|
|
|
@@ -49,9 +51,17 @@ per situation.
|
|
|
49
51
|
CoachProfile + ParentProfile on different teams mints a separate token
|
|
50
52
|
per role. The CLI does not switch profiles at runtime.
|
|
51
53
|
- **Everything is gated by the bound profile's permissions.** Parent or
|
|
52
|
-
wrestler PATs cannot run reports (
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
wrestler PATs cannot run reports or touch prospects (Pundit requires a
|
|
55
|
+
CoachProfile on the team).
|
|
56
|
+
- **Writes need a capability on BOTH the team and the token.** Reads are
|
|
57
|
+
implicit for every token. A write (`POST`/`PATCH`) succeeds only when it
|
|
58
|
+
maps to a capability in the server's registry (`prospects:write`,
|
|
59
|
+
`reports:write`) that a team admin has enabled under Settings → API
|
|
60
|
+
Access AND that the token was minted with. Token scopes are immutable:
|
|
61
|
+
to add one, revoke + mint a new token, then `wiq auth login --force`.
|
|
62
|
+
`wiq auth status` / `wiq doctor` show the live scopes — check them
|
|
63
|
+
before attempting a write. `reports:write` was backfilled onto every
|
|
64
|
+
existing team and token, so report submission keeps working.
|
|
55
65
|
- **The 9 finance reports require `admin?`.** They're flagged
|
|
56
66
|
`admin_only: true` in `wiq reports types`. Workflows that submit them
|
|
57
67
|
carry the same flag. Non-admin coach PATs 403 on these.
|
|
@@ -106,8 +116,12 @@ Common codes and the right response:
|
|
|
106
116
|
| `alias_not_found` | `--as` named a slot that doesn't exist | Check `wiq auth list` |
|
|
107
117
|
| `unauthorized` (401) | Token rejected by server | Re-login; could be revoked |
|
|
108
118
|
| `forbidden` (403) | Pundit denial (often admin gate on a finance report) | Check the report's `admin_only:` flag in `wiq reports types` |
|
|
119
|
+
| `capability_disabled_for_team` (403) | Team hasn't enabled the write capability named in the message | Ask a team admin to enable it at `<host>/settings/team/api_access`; nothing to change on the token |
|
|
120
|
+
| `token_missing_scope` (403) | Team allows the write, but this token was minted without the scope | Mint a new token WITH the scope, `wiq auth login --force`; scopes can't be edited in place |
|
|
121
|
+
| `pat_write_unsupported` (403) | Endpoint has no API write capability at all | Use the WIQ web app; not a permissions problem |
|
|
109
122
|
| `not_found` (404) | Resource doesn't exist for this profile | Confirm the id; cross-team enumeration returns 404 to avoid leaking existence |
|
|
110
123
|
| `validation_failed` (422) | Server-side input rejection | Read `details` for per-field messages |
|
|
124
|
+
| `stage_transition_refused` (422) | PAT tried to move a prospect backwards or out of a terminal stage | Don't retry; tell the user a coach must make that move in the web app |
|
|
111
125
|
| `rate_limited` (429) | 100 req / 3 sec per IP exceeded | Back off |
|
|
112
126
|
| `season_not_found` | `--season <year>` matched zero paid sessions | `wiq paid_sessions list` to see configured periods |
|
|
113
127
|
| `report_failed` | The report ran but errored server-side | Inspect `details` (the report's result jsonb) |
|
|
@@ -181,6 +195,14 @@ Key reports an agent should know by heart:
|
|
|
181
195
|
while?"
|
|
182
196
|
- **`PaidSessionAccountingReport`** (admin_only) — Line-item charges for
|
|
183
197
|
a paid session.
|
|
198
|
+
- **`SessionRegistrationAnswerReport`** — Signup Q&A for one
|
|
199
|
+
`--paid-session <id>`, one row per wrestler. Rows lead with **"WIQ ID
|
|
200
|
+
#"** (wrestler_profile id, identical to RosterReport's first column, so
|
|
201
|
+
the two join on it), **"Registration ID"** (the stable key to hand an
|
|
202
|
+
external sync), "Registration status", "Good standing", "Registered
|
|
203
|
+
at", "Registration updated at", "Registration canceled at" — then the
|
|
204
|
+
name/DOB/USAW/AAU columns and every registration question. Use this,
|
|
205
|
+
not name+DOB matching, when a customer syncs registrations elsewhere.
|
|
184
206
|
|
|
185
207
|
For anything else, `wiq reports types` is faster than guessing.
|
|
186
208
|
|
|
@@ -426,20 +448,66 @@ Three gotchas an agent must know:
|
|
|
426
448
|
activity. Rosters/events/paid_sessions embed their `location` object
|
|
427
449
|
(or null) in list payloads, so you can check coverage cheaply.
|
|
428
450
|
|
|
429
|
-
##
|
|
451
|
+
## Writes — the prospects pipeline (`prospects:write`)
|
|
430
452
|
|
|
431
|
-
The
|
|
432
|
-
|
|
453
|
+
The only writable surface besides report submission is the leads
|
|
454
|
+
pipeline. Six commands, all requiring the `prospects:write` scope:
|
|
433
455
|
|
|
434
|
-
|
|
435
|
-
|
|
456
|
+
```bash
|
|
457
|
+
wiq prospect_families create --first-name Dana --last-name Lee --email dana@x.com --phone 5551234
|
|
458
|
+
wiq prospect_families update <family_id> --phone 5551234 --assigned-coach <coach_id>
|
|
459
|
+
wiq prospect_families note <family_id> --activity-type phone_call --content "Left voicemail" \
|
|
460
|
+
[--clear-follow-up <prospect_ids>] [--add-follow-up <prospect_ids>]
|
|
461
|
+
wiq prospects create <family_id> --first-name Sam --dob 2016-03-04 --academic-class 4th
|
|
462
|
+
wiq prospects update <prospect_id> --stage trial_scheduled --trial-event <event_id>
|
|
463
|
+
wiq prospects advance <prospect_id> trialing
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Rules the server enforces on PAT callers (and you should respect up
|
|
467
|
+
front rather than discover via errors):
|
|
468
|
+
|
|
469
|
+
1. **Check scopes first.** `wiq auth status` → `live_scopes` must include
|
|
470
|
+
`prospects:write`. If it doesn't, stop and tell the user: either the
|
|
471
|
+
team admin needs to enable it (Settings → API Access) or they need a
|
|
472
|
+
new token minted with it. Don't loop on 403s.
|
|
473
|
+
2. **Stage moves are forward-only and never leave a terminal stage.**
|
|
474
|
+
Order: inquiry → trial_scheduled → trialing → trial_complete →
|
|
475
|
+
converted | didnt_join | archived. Skipping ahead is fine; going back
|
|
476
|
+
or moving a converted/archived lead returns `stage_transition_refused`
|
|
477
|
+
(422). A coach can force it in the web app. Deletes are never allowed
|
|
478
|
+
through the API.
|
|
479
|
+
3. **Search before you create.** `wiq prospect_families list --query
|
|
480
|
+
<name|email|phone>` finds existing households so you don't duplicate
|
|
481
|
+
a family that came in through the web interest form.
|
|
482
|
+
4. **Log contact when you act.** A note WITH `--activity-type` counts as
|
|
483
|
+
contact: it stamps `last_contacted_at` on every active prospect in
|
|
484
|
+
the family and is what clears stale-contact follow-up flags. A note
|
|
485
|
+
without it is an internal comment only.
|
|
486
|
+
5. **Attribution.** Every write is audited against the token; stage
|
|
487
|
+
changes and notes are attributed to the coach who minted it.
|
|
488
|
+
`wiq prospect_families stage_changes <family_id>` is the audit log
|
|
489
|
+
(from/to stage, changed_at, changed_via, changed_by) — use it to
|
|
490
|
+
answer "who moved this lead?" and to check history before advancing.
|
|
491
|
+
6. **Trial-purchase leads have thin family records.** They skipped the
|
|
492
|
+
interest form, so contact details often live only as registration
|
|
493
|
+
answers on the linked wrestler/guardian profiles. `wiq
|
|
494
|
+
prospect_families linked_answers <family_id>` returns them; copy a
|
|
495
|
+
phone back with `wiq prospect_families update --phone`.
|
|
496
|
+
|
|
497
|
+
## What's NOT available
|
|
498
|
+
|
|
499
|
+
You cannot via this CLI:
|
|
500
|
+
|
|
501
|
+
- Create/edit check-ins, events, paid sessions, rosters, locations, or
|
|
502
|
+
any resource other than prospects/families/notes and reports
|
|
503
|
+
- Delete anything (destroy actions are outside every capability)
|
|
436
504
|
- Mint, list, or revoke PATs (use the web UI at
|
|
437
505
|
`<host>/settings/personal_access_tokens`)
|
|
438
|
-
- Mark attendance
|
|
439
|
-
|
|
506
|
+
- Mark attendance or trigger event-change notifications / roster
|
|
507
|
+
re-sync jobs
|
|
440
508
|
|
|
441
|
-
If the user asks for
|
|
442
|
-
|
|
509
|
+
If the user asks for one of these, say it's not exposed and point them
|
|
510
|
+
at the WIQ web UI.
|
|
443
511
|
|
|
444
512
|
## When to compose vs run a workflow
|
|
445
513
|
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wiq-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- WrestlingIQ
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: thor
|
|
@@ -122,7 +121,6 @@ metadata:
|
|
|
122
121
|
bug_tracker_uri: https://github.com/wrestlingiq/wiq-cli/issues
|
|
123
122
|
documentation_uri: https://github.com/wrestlingiq/wiq-cli#readme
|
|
124
123
|
rubygems_mfa_required: 'true'
|
|
125
|
-
post_install_message:
|
|
126
124
|
rdoc_options: []
|
|
127
125
|
require_paths:
|
|
128
126
|
- lib
|
|
@@ -137,8 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
137
135
|
- !ruby/object:Gem::Version
|
|
138
136
|
version: '0'
|
|
139
137
|
requirements: []
|
|
140
|
-
rubygems_version:
|
|
141
|
-
signing_key:
|
|
138
|
+
rubygems_version: 4.0.16
|
|
142
139
|
specification_version: 4
|
|
143
140
|
summary: Command-line interface for the WrestlingIQ API
|
|
144
141
|
test_files: []
|