wiq-cli 0.1.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +144 -0
- data/bin/wiq +22 -0
- data/docs/deferred.md +155 -0
- data/docs/wiq_api_notes.md +629 -0
- data/lib/wiq/cli.rb +89 -0
- data/lib/wiq/client.rb +127 -0
- data/lib/wiq/commands/auth.rb +257 -0
- data/lib/wiq/commands/base.rb +70 -0
- data/lib/wiq/commands/billing_profiles.rb +52 -0
- data/lib/wiq/commands/charges.rb +104 -0
- data/lib/wiq/commands/check_ins.rb +124 -0
- data/lib/wiq/commands/doctor.rb +93 -0
- data/lib/wiq/commands/events.rb +109 -0
- data/lib/wiq/commands/metrics.rb +118 -0
- data/lib/wiq/commands/paid_sessions.rb +81 -0
- data/lib/wiq/commands/prospect_families.rb +129 -0
- data/lib/wiq/commands/prospects.rb +153 -0
- data/lib/wiq/commands/registrations.rb +78 -0
- data/lib/wiq/commands/reports.rb +510 -0
- data/lib/wiq/commands/rosters.rb +81 -0
- data/lib/wiq/commands/setup.rb +82 -0
- data/lib/wiq/commands/workflows.rb +84 -0
- data/lib/wiq/commands/wrestlers.rb +140 -0
- data/lib/wiq/config.rb +165 -0
- data/lib/wiq/credentials.rb +95 -0
- data/lib/wiq/errors.rb +172 -0
- data/lib/wiq/introspection.rb +105 -0
- data/lib/wiq/output.rb +67 -0
- data/lib/wiq/pagination.rb +41 -0
- data/lib/wiq/season_resolver.rb +46 -0
- data/lib/wiq/version.rb +5 -0
- data/lib/wiq/workflows.rb +314 -0
- data/lib/wiq.rb +30 -0
- data/share/skills/wiq/SKILL.md +348 -0
- metadata +141 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wiq
|
|
4
|
+
module Commands
|
|
5
|
+
class ProspectFamilies < Base
|
|
6
|
+
SORT_OPTIONS = %w[newest oldest_followup oldest_contact next_trial].freeze
|
|
7
|
+
|
|
8
|
+
desc "list", "List prospect families (one row per household)"
|
|
9
|
+
long_desc <<~DESC
|
|
10
|
+
Returns one row per family (household). Each row embeds:
|
|
11
|
+
- All the family's prospects (one per kid) inline
|
|
12
|
+
- The most recent `last_contact` note (activity_type, author,
|
|
13
|
+
occurred_at) preloaded as has_one
|
|
14
|
+
- The family's registration_answers, with question prompts
|
|
15
|
+
|
|
16
|
+
`--query` searches BOTH family contact (name, email, phone — with
|
|
17
|
+
digit-stripped phone matching) AND child first/last names via a
|
|
18
|
+
subquery on the prospects table. A "Johnny" search matches either
|
|
19
|
+
a parent named Johnny OR a child named Johnny; you'll see which
|
|
20
|
+
from the `child_first_name` field on the embedded prospects.
|
|
21
|
+
|
|
22
|
+
Filters mirror `wiq prospects list`, plus:
|
|
23
|
+
--question-id <id> + --answer-value <str> Filter by a specific
|
|
24
|
+
registration answer
|
|
25
|
+
(both flags required)
|
|
26
|
+
--sort newest (default for
|
|
27
|
+
"All Leads"),
|
|
28
|
+
oldest_followup
|
|
29
|
+
(default when
|
|
30
|
+
--attention=needs_attention),
|
|
31
|
+
oldest_contact (NULLs
|
|
32
|
+
first — never-contacted
|
|
33
|
+
families surface), or
|
|
34
|
+
next_trial
|
|
35
|
+
DESC
|
|
36
|
+
method_option :query, type: :string,
|
|
37
|
+
desc: "Free-text search (bypasses other filters)"
|
|
38
|
+
method_option :attention, type: :string, enum: Prospects::ATTENTION_MODES,
|
|
39
|
+
desc: "Pipeline cut: needs_attention or handled"
|
|
40
|
+
method_option :stage, type: :string, enum: Prospects::STAGES,
|
|
41
|
+
desc: "Families with at least one prospect in this stage"
|
|
42
|
+
method_option :assigned_to_me, type: :boolean, default: false,
|
|
43
|
+
desc: "Only families assigned to the calling coach"
|
|
44
|
+
method_option :assigned_coach, type: :numeric,
|
|
45
|
+
desc: "Only families assigned to this coach_profile id"
|
|
46
|
+
method_option :question_id, type: :numeric,
|
|
47
|
+
desc: "Registration question id to filter by (pair with --answer-value)"
|
|
48
|
+
method_option :answer_value, type: :string,
|
|
49
|
+
desc: "Registration answer value (pair with --question-id)"
|
|
50
|
+
method_option :sort, type: :string, enum: SORT_OPTIONS,
|
|
51
|
+
desc: "Override default sort. Default depends on --attention."
|
|
52
|
+
method_option :all, type: :boolean, default: false
|
|
53
|
+
def list
|
|
54
|
+
params = { "per_page" => 50 }
|
|
55
|
+
if options[:query]
|
|
56
|
+
params["query"] = options[:query]
|
|
57
|
+
else
|
|
58
|
+
params["attention_mode"] = options[:attention] if options[:attention]
|
|
59
|
+
params["stage"] = options[:stage] if options[:stage]
|
|
60
|
+
if options[:assigned_to_me]
|
|
61
|
+
params["assigned_to"] = "me"
|
|
62
|
+
elsif options[:assigned_coach]
|
|
63
|
+
params["assigned_coach_id"] = options[:assigned_coach]
|
|
64
|
+
end
|
|
65
|
+
if options[:question_id] && options[:answer_value]
|
|
66
|
+
params["question_id"] = options[:question_id]
|
|
67
|
+
params["answer_value"] = options[:answer_value]
|
|
68
|
+
elsif options[:question_id] || options[:answer_value]
|
|
69
|
+
raise Wiq::Error.new("--question-id and --answer-value must be passed together.",
|
|
70
|
+
code: "missing_question_pair")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
params["sort"] = options[:sort] if options[:sort]
|
|
74
|
+
|
|
75
|
+
records, total = fetch_index("/api/v1/prospect_families", params, key: "prospect_families")
|
|
76
|
+
render_index(
|
|
77
|
+
records, total: total,
|
|
78
|
+
summary: "Listed #{records.size} prospect families.",
|
|
79
|
+
breadcrumbs: [
|
|
80
|
+
{ "cmd" => "wiq prospect_families show <id>", "description" => "Drill into one family" },
|
|
81
|
+
{ "cmd" => "wiq prospect_families notes <id>", "description" => "See contact log" },
|
|
82
|
+
{ "cmd" => "wiq prospects summary", "description" => "Pipeline dashboard" }
|
|
83
|
+
]
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
desc "show ID", "Fetch a single prospect family"
|
|
88
|
+
long_desc <<~DESC
|
|
89
|
+
Full family payload: contact info (name/email/phone), source
|
|
90
|
+
("How did you hear about us?"), linked guardian (parent profile),
|
|
91
|
+
assigned coach, the most recent `last_contact` summary, every
|
|
92
|
+
prospect (kid) sorted by created_at, and every registration_answer
|
|
93
|
+
with its question prompt.
|
|
94
|
+
DESC
|
|
95
|
+
def show(id)
|
|
96
|
+
family = client.get("/api/v1/prospect_families/#{id}")
|
|
97
|
+
render(family,
|
|
98
|
+
summary: "Family #{family["id"]} — #{family["contact_name"]} (#{family["prospects"].size} prospects).",
|
|
99
|
+
breadcrumbs: [
|
|
100
|
+
{ "cmd" => "wiq prospect_families notes #{family["id"]}",
|
|
101
|
+
"description" => "See contact log for this family" },
|
|
102
|
+
{ "cmd" => "wiq prospects show <prospect_id>", "description" => "Drill into one kid" }
|
|
103
|
+
])
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
desc "notes FAMILY_ID", "List notes / contact log for a prospect family"
|
|
107
|
+
long_desc <<~DESC
|
|
108
|
+
Contact log for a family — every Note row with
|
|
109
|
+
noteable_type=ProspectFamily, sorted newest-first. Each row:
|
|
110
|
+
author (name + profile type), created_at, activity_type
|
|
111
|
+
(e.g. phone_call, email, text, in_person, dm), and the note
|
|
112
|
+
body in both rich content + plain_content.
|
|
113
|
+
DESC
|
|
114
|
+
method_option :all, type: :boolean, default: false
|
|
115
|
+
def notes(family_id)
|
|
116
|
+
records, total = fetch_index("/api/v1/prospect_families/#{family_id}/notes",
|
|
117
|
+
{ "per_page" => 50 },
|
|
118
|
+
key: "notes")
|
|
119
|
+
render_index(
|
|
120
|
+
records, total: total,
|
|
121
|
+
summary: "Listed #{records.size} notes for family #{family_id}.",
|
|
122
|
+
breadcrumbs: [
|
|
123
|
+
{ "cmd" => "wiq prospect_families show #{family_id}", "description" => "Back to the family" }
|
|
124
|
+
]
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wiq
|
|
4
|
+
module Commands
|
|
5
|
+
class Prospects < Base
|
|
6
|
+
STAGES = %w[inquiry trial_scheduled trialing trial_complete converted didnt_join archived].freeze
|
|
7
|
+
ATTENTION_MODES = %w[needs_attention handled].freeze
|
|
8
|
+
|
|
9
|
+
desc "list", "List individual prospects (one row per kid)"
|
|
10
|
+
long_desc <<~DESC
|
|
11
|
+
Returns one row per prospect (kid), sorted newest-first.
|
|
12
|
+
|
|
13
|
+
Funnel stages: inquiry → trial_scheduled → trialing →
|
|
14
|
+
trial_complete → converted (terminal) | didnt_join (terminal) |
|
|
15
|
+
archived (terminal).
|
|
16
|
+
|
|
17
|
+
Filters:
|
|
18
|
+
--query Free-text search across BOTH family contact
|
|
19
|
+
(name/email/phone) AND child first/last
|
|
20
|
+
name. Backed by the ProspectFamily.search
|
|
21
|
+
scope which unions both via a subquery.
|
|
22
|
+
When set, ALL other filters are bypassed
|
|
23
|
+
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
|
+
--attention needs_attention | handled
|
|
34
|
+
--stage One funnel stage
|
|
35
|
+
--assigned-to-me Only families assigned to the calling coach
|
|
36
|
+
--assigned-coach <id> Same, for any coach by id
|
|
37
|
+
|
|
38
|
+
Pair with `wiq prospect_families list` to see leads at the
|
|
39
|
+
household level instead.
|
|
40
|
+
DESC
|
|
41
|
+
method_option :query, type: :string,
|
|
42
|
+
desc: "Free-text search across family name/email/phone (bypasses other filters)"
|
|
43
|
+
method_option :attention, type: :string, enum: ATTENTION_MODES,
|
|
44
|
+
desc: "Pipeline cut: needs_attention or handled"
|
|
45
|
+
method_option :stage, type: :string, enum: STAGES,
|
|
46
|
+
desc: "Filter to one funnel stage"
|
|
47
|
+
method_option :assigned_to_me, type: :boolean, default: false,
|
|
48
|
+
desc: "Only prospects on a family assigned to the calling coach"
|
|
49
|
+
method_option :assigned_coach, type: :numeric,
|
|
50
|
+
desc: "Only prospects on a family assigned to this coach_profile id"
|
|
51
|
+
method_option :all, type: :boolean, default: false
|
|
52
|
+
def list
|
|
53
|
+
params = { "per_page" => 50 }
|
|
54
|
+
if options[:query]
|
|
55
|
+
params["query"] = options[:query]
|
|
56
|
+
else
|
|
57
|
+
params["attention_mode"] = options[:attention] if options[:attention]
|
|
58
|
+
params["stage"] = options[:stage] if options[:stage]
|
|
59
|
+
if options[:assigned_to_me]
|
|
60
|
+
params["assigned_to"] = "me"
|
|
61
|
+
elsif options[:assigned_coach]
|
|
62
|
+
params["assigned_coach_id"] = options[:assigned_coach]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
records, total = fetch_index("/api/v1/prospects", params, key: "prospects")
|
|
67
|
+
render_index(records, total: total,
|
|
68
|
+
summary: "Listed #{records.size} prospects.",
|
|
69
|
+
breadcrumbs: breadcrumbs)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
desc "show ID", "Fetch a single prospect"
|
|
73
|
+
long_desc <<~DESC
|
|
74
|
+
Full prospect payload: child name + DOB + academic class,
|
|
75
|
+
experience_level, current stage + stage_changed_at, all the
|
|
76
|
+
funnel timestamps (trial_scheduled_at, trial_completed_at,
|
|
77
|
+
converted_at, archived_at), needs_follow_up + follow_up_reason,
|
|
78
|
+
days_in_stage and days_since_follow_up, plus the linked
|
|
79
|
+
wrestler_profile (if converted), paid_session (if trialing),
|
|
80
|
+
and conversion_billing_subscription with plan name (if converted
|
|
81
|
+
on a recurring sub).
|
|
82
|
+
DESC
|
|
83
|
+
def show(id)
|
|
84
|
+
prospect = client.get("/api/v1/prospects/#{id}")
|
|
85
|
+
render(prospect,
|
|
86
|
+
summary: "Prospect #{prospect["id"]} — #{prospect["child_first_name"]} #{prospect["child_last_name"]} (stage=#{prospect["stage"]}).",
|
|
87
|
+
breadcrumbs: [
|
|
88
|
+
{ "cmd" => "wiq prospect_families show #{prospect["prospect_family_id"]}",
|
|
89
|
+
"description" => "Family this prospect belongs to" },
|
|
90
|
+
{ "cmd" => "wiq prospect_families notes #{prospect["prospect_family_id"]}",
|
|
91
|
+
"description" => "Contact log for the family" }
|
|
92
|
+
])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
desc "summary", "Pipeline dashboard: counts per stage + conversion rate"
|
|
96
|
+
long_desc <<~DESC
|
|
97
|
+
Single-call dashboard. Returns an unwrapped object (not paginated)
|
|
98
|
+
with stage-bucketed counts, needs-action totals, family totals,
|
|
99
|
+
and an aggregate conversion_rate.
|
|
100
|
+
|
|
101
|
+
Cohort options for the conversion calculation:
|
|
102
|
+
--start-date / --end-date Explicit window (must be paired)
|
|
103
|
+
--conversion-days N Look-back of 30, 60, 90, or 180
|
|
104
|
+
days (default 90)
|
|
105
|
+
|
|
106
|
+
Both numerator and denominator are pinned to the same cohort
|
|
107
|
+
(prospects created in the window). Without that pin, an old
|
|
108
|
+
prospect converting now would push the rate past 100%.
|
|
109
|
+
|
|
110
|
+
Best agent entry point for "how's our pipeline?" — single round
|
|
111
|
+
trip, structured numbers.
|
|
112
|
+
DESC
|
|
113
|
+
method_option :start_date, type: :string,
|
|
114
|
+
desc: "Cohort window start (YYYY-MM-DD). Requires --end-date."
|
|
115
|
+
method_option :end_date, type: :string,
|
|
116
|
+
desc: "Cohort window end (YYYY-MM-DD). Requires --start-date."
|
|
117
|
+
method_option :conversion_days, type: :numeric, enum: [30, 60, 90, 180],
|
|
118
|
+
desc: "Look-back window in days when no explicit dates (default 90)"
|
|
119
|
+
def summary
|
|
120
|
+
params = {}
|
|
121
|
+
if options[:start_date] && options[:end_date]
|
|
122
|
+
params["start_date"] = options[:start_date]
|
|
123
|
+
params["end_date"] = options[:end_date]
|
|
124
|
+
elsif options[:start_date] || options[:end_date]
|
|
125
|
+
raise Wiq::Error.new("--start-date and --end-date must be passed together.",
|
|
126
|
+
code: "missing_cohort_dates")
|
|
127
|
+
end
|
|
128
|
+
params["conversion_days"] = options[:conversion_days] if options[:conversion_days]
|
|
129
|
+
|
|
130
|
+
data = client.get("/api/v1/prospects/summary", params)
|
|
131
|
+
render(data,
|
|
132
|
+
summary: "Prospect pipeline: #{data["needs_action_count"]} need action, " \
|
|
133
|
+
"#{data["active_trials_count"]} active trials, " \
|
|
134
|
+
"conversion=#{data["conversion_rate"]}%.",
|
|
135
|
+
breadcrumbs: [
|
|
136
|
+
{ "cmd" => "wiq prospects list --attention needs_attention",
|
|
137
|
+
"description" => "Drill into leads needing action" },
|
|
138
|
+
{ "cmd" => "wiq prospect_families list --sort oldest_followup",
|
|
139
|
+
"description" => "Queue of families ordered by stalest follow-up" }
|
|
140
|
+
])
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
no_commands do
|
|
144
|
+
def breadcrumbs
|
|
145
|
+
[
|
|
146
|
+
{ "cmd" => "wiq prospects summary", "description" => "Pipeline dashboard" },
|
|
147
|
+
{ "cmd" => "wiq prospect_families list", "description" => "List by family instead of by kid" }
|
|
148
|
+
]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wiq
|
|
4
|
+
module Commands
|
|
5
|
+
class Registrations < Base
|
|
6
|
+
desc "questions", "List the team's registration questions"
|
|
7
|
+
long_desc <<~DESC
|
|
8
|
+
Every registration question the team has authored. Useful for:
|
|
9
|
+
|
|
10
|
+
- Discovering question ids to pass to
|
|
11
|
+
`wiq reports run RosterReport --append-properties ...`
|
|
12
|
+
- Auditing what the team is collecting at signup
|
|
13
|
+
- Finding the team's address question(s) so you can join zip
|
|
14
|
+
codes (the `RegAddressQuestion` type) to a wrestler
|
|
15
|
+
|
|
16
|
+
Each row exposes `prompt`, `type` (e.g. RegTextQuestion,
|
|
17
|
+
RegAddressQuestion, RegYesNoQuestion), `for_type` (who the
|
|
18
|
+
question applies to), `is_public`, `coach_visibility`, and
|
|
19
|
+
`display_order`.
|
|
20
|
+
|
|
21
|
+
Skip rows whose `deleted_at` is set — they're still in the index
|
|
22
|
+
payload but no longer asked.
|
|
23
|
+
DESC
|
|
24
|
+
method_option :all, type: :boolean, default: false
|
|
25
|
+
def questions
|
|
26
|
+
records, total = fetch_index("/api/v1/registration_questions", { "per_page" => 100 }, key: "registration_questions")
|
|
27
|
+
render_index(
|
|
28
|
+
records, total: total,
|
|
29
|
+
summary: "Listed #{records.size} registration questions.",
|
|
30
|
+
breadcrumbs: [
|
|
31
|
+
{ "cmd" => "wiq reports run RosterReport --append-properties <id1> <id2>",
|
|
32
|
+
"description" => "Surface these as columns on a roster report" }
|
|
33
|
+
]
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "answers", "List a profile's registration answers"
|
|
38
|
+
long_desc <<~DESC
|
|
39
|
+
Returns the registration answers for a specific profile (wrestler,
|
|
40
|
+
parent, or coach). Useful for spot-checking what a family submitted
|
|
41
|
+
at signup.
|
|
42
|
+
|
|
43
|
+
--profile-type WrestlerProfile | ParentProfile | CoachProfile
|
|
44
|
+
--profile Profile id
|
|
45
|
+
--session Restrict to a specific paid_session
|
|
46
|
+
--visibility public | private (admin only for private)
|
|
47
|
+
|
|
48
|
+
Privacy note: Pundit gates private answers — a non-admin coach PAT
|
|
49
|
+
will get the public subset only.
|
|
50
|
+
DESC
|
|
51
|
+
method_option :profile, type: :numeric, required: true, desc: "Profile id"
|
|
52
|
+
method_option :profile_type, type: :string, required: true,
|
|
53
|
+
enum: %w[WrestlerProfile ParentProfile CoachProfile],
|
|
54
|
+
desc: "Profile class"
|
|
55
|
+
method_option :session, type: :numeric, desc: "Restrict to a paid session id"
|
|
56
|
+
method_option :visibility, type: :string, enum: %w[public private], desc: "Visibility filter"
|
|
57
|
+
method_option :all, type: :boolean, default: false
|
|
58
|
+
def answers
|
|
59
|
+
params = {
|
|
60
|
+
"profile_id" => options[:profile],
|
|
61
|
+
"profile_type" => options[:profile_type],
|
|
62
|
+
"per_page" => 50
|
|
63
|
+
}
|
|
64
|
+
params["session_id"] = options[:session] if options[:session]
|
|
65
|
+
params["visibility"] = options[:visibility] if options[:visibility]
|
|
66
|
+
|
|
67
|
+
records, total = fetch_index("/api/v1/registration_answers", params, key: "registration_answers")
|
|
68
|
+
render_index(
|
|
69
|
+
records, total: total,
|
|
70
|
+
summary: "Listed #{records.size} answers for #{options[:profile_type]} #{options[:profile]}.",
|
|
71
|
+
breadcrumbs: [
|
|
72
|
+
{ "cmd" => "wiq registrations questions", "description" => "See the corresponding questions" }
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|