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.
@@ -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