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,510 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wiq
|
|
4
|
+
module Commands
|
|
5
|
+
class Reports < Base
|
|
6
|
+
# Curated allowlist surfaced in `wiq reports types`. Users can still pass
|
|
7
|
+
# any type string to `run`; this map governs documentation, arg hints,
|
|
8
|
+
# and agent-facing recommendations. Per-entry keys:
|
|
9
|
+
# args: permitted args.* keys for this type
|
|
10
|
+
# dates: :required | :optional — whether start_at/end_at are needed
|
|
11
|
+
# desc: one-line description
|
|
12
|
+
# recommended: true | false — agents should prefer/avoid this type
|
|
13
|
+
# (omit when neutral)
|
|
14
|
+
# prefer: array of better alternatives (only when recommended: false)
|
|
15
|
+
# notes: agent-facing guidance, surfaced in `wiq reports types --agent`
|
|
16
|
+
# example: concrete `wiq reports run ...` invocation
|
|
17
|
+
# admin_only: true for the 9 finance reports gated by Pundit's
|
|
18
|
+
# acceptable_finance_permissions? (= is_admin?). Non-finance
|
|
19
|
+
# reports require a CoachProfile PAT on the team. PATs bound
|
|
20
|
+
# to ParentProfile or WrestlerProfile 403 on any report POST.
|
|
21
|
+
#
|
|
22
|
+
# Note on auth: "elite" is a Team#account_type (gates UI tab rendering),
|
|
23
|
+
# not a per-user permission — the report POST doesn't check it. Same for
|
|
24
|
+
# "payments_enabled" — that's a team attribute, not a permission flag.
|
|
25
|
+
# The only API-level gate beyond CoachProfile is admin? for finance reports.
|
|
26
|
+
TYPES = {
|
|
27
|
+
# ── Roster tab ───────────────────────────────────────────────
|
|
28
|
+
"RosterReport" => {
|
|
29
|
+
args: %w[roster_id append_property_ids include_archived_roster_tags],
|
|
30
|
+
dates: :optional,
|
|
31
|
+
desc: "Roster snapshot — name, weight class, academic class, age",
|
|
32
|
+
recommended: true,
|
|
33
|
+
notes: "Supports custom columns via --append-properties <q_ids>: pass " \
|
|
34
|
+
"registration_question ids (discoverable via " \
|
|
35
|
+
"`wiq registrations questions --visibility public`, plus " \
|
|
36
|
+
"--visibility private if admin) to surface intake data as " \
|
|
37
|
+
"extra columns. Skip questions with a deleted_at — they're " \
|
|
38
|
+
"still in the index payload but won't render. roster_id=0 " \
|
|
39
|
+
"means \"all rosters\". Each appended question becomes a column.",
|
|
40
|
+
example: "wiq reports run RosterReport --roster 42 --append-properties 17 23"
|
|
41
|
+
},
|
|
42
|
+
"FullExportWrestlerReport" => {
|
|
43
|
+
args: %w[],
|
|
44
|
+
dates: :optional,
|
|
45
|
+
desc: "Full wrestler export — every known field for every wrestler",
|
|
46
|
+
notes: "Very slow on large teams. Prefer RosterReport with " \
|
|
47
|
+
"--append-properties unless you actually need the exhaustive dump."
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
# ── Invite tab ───────────────────────────────────────────────
|
|
51
|
+
"InviteStatusReport" => {
|
|
52
|
+
args: %w[],
|
|
53
|
+
dates: :optional,
|
|
54
|
+
desc: "Wrestlers / parents / coaches invited but not yet account-created",
|
|
55
|
+
recommended: true,
|
|
56
|
+
notes: "Mostly useful for high school teams or anywhere a coach is " \
|
|
57
|
+
"manually inviting accounts. Club teams should prefer the " \
|
|
58
|
+
"registration flow — parents register, then invite their own " \
|
|
59
|
+
"kids — so this report stays empty for them."
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
# ── USAW / AAU tab ───────────────────────────────────────────
|
|
63
|
+
"UsawReport" => {
|
|
64
|
+
args: %w[roster_id],
|
|
65
|
+
dates: :optional,
|
|
66
|
+
desc: "All USA Wrestling card info on file, one row per wrestler",
|
|
67
|
+
recommended: true,
|
|
68
|
+
notes: "UI hides this tab when USAW collection is off (team setting)."
|
|
69
|
+
},
|
|
70
|
+
"UsawExpiredReport" => {
|
|
71
|
+
args: %w[roster_id],
|
|
72
|
+
dates: :optional,
|
|
73
|
+
desc: "Wrestlers missing or with expired USAW memberships",
|
|
74
|
+
notes: "Output is also a bulk-purchase upload format for USAW's system."
|
|
75
|
+
},
|
|
76
|
+
"UsawExportReport" => {
|
|
77
|
+
args: %w[roster_id paid_session_id],
|
|
78
|
+
dates: :optional,
|
|
79
|
+
desc: "USAW bulk-purchase upload format",
|
|
80
|
+
notes: "Only report that accepts paid_session_id=0 to mean " \
|
|
81
|
+
"\"all sessions\"."
|
|
82
|
+
},
|
|
83
|
+
"AauReport" => {
|
|
84
|
+
args: %w[roster_id],
|
|
85
|
+
dates: :optional,
|
|
86
|
+
desc: "All AAU card info on file, one row per wrestler",
|
|
87
|
+
recommended: true,
|
|
88
|
+
notes: "UI hides this tab when AAU collection is off (team setting)."
|
|
89
|
+
},
|
|
90
|
+
"AauExpiredReport" => {
|
|
91
|
+
args: %w[roster_id],
|
|
92
|
+
dates: :optional,
|
|
93
|
+
desc: "Wrestlers missing or with expired AAU memberships"
|
|
94
|
+
},
|
|
95
|
+
"AauExportReport" => {
|
|
96
|
+
args: %w[roster_id],
|
|
97
|
+
dates: :optional,
|
|
98
|
+
desc: "AAU bulk-purchase upload format"
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
# ── Stats tab ────────────────────────────────────────────────
|
|
102
|
+
"WinLossReport" => {
|
|
103
|
+
args: %w[roster_id],
|
|
104
|
+
dates: :required,
|
|
105
|
+
desc: "Wins and losses per wrestler",
|
|
106
|
+
recommended: true
|
|
107
|
+
},
|
|
108
|
+
"RosterStatsReport" => {
|
|
109
|
+
args: %w[roster_id],
|
|
110
|
+
dates: :required,
|
|
111
|
+
desc: "Wrestling stats per wrestler (takedowns, nearfall, etc.)",
|
|
112
|
+
recommended: true
|
|
113
|
+
},
|
|
114
|
+
"EventStatsReport" => {
|
|
115
|
+
args: %w[event_id],
|
|
116
|
+
dates: :optional,
|
|
117
|
+
desc: "Per-wrestler stats for a single event",
|
|
118
|
+
notes: "Single-event drill-down. For a date range use RosterStatsReport. " \
|
|
119
|
+
"Discover event_id via `wiq events list --start ... --end ...`."
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
# ── Attendance tab ───────────────────────────────────────────
|
|
123
|
+
"CheckInSummaryReport" => {
|
|
124
|
+
args: %w[roster_id],
|
|
125
|
+
dates: :required,
|
|
126
|
+
desc: "Summarized check-ins for a date range — per-wrestler totals",
|
|
127
|
+
recommended: true,
|
|
128
|
+
notes: "One row per wrestler over the date range — totals across all " \
|
|
129
|
+
"their check-ins in the window. If you want one row per " \
|
|
130
|
+
"check-in event use CheckInFeedReport. The UI doesn't expose " \
|
|
131
|
+
"a roster picker; counts span the whole team.",
|
|
132
|
+
example: "wiq reports run CheckInSummaryReport --start 2026-05-01 --end 2026-05-31"
|
|
133
|
+
},
|
|
134
|
+
"CheckInFeedReport" => {
|
|
135
|
+
args: %w[roster_id],
|
|
136
|
+
dates: :required,
|
|
137
|
+
desc: "Raw check-in feed — one row per check-in, sorted by time",
|
|
138
|
+
recommended: true,
|
|
139
|
+
notes: "Useful for \"who came to the club today and what registrations " \
|
|
140
|
+
"did they have at check-in time.\" Larger payload than the " \
|
|
141
|
+
"summary.",
|
|
142
|
+
example: "wiq reports run CheckInFeedReport --start 2026-05-01 --end 2026-05-31"
|
|
143
|
+
},
|
|
144
|
+
"PracticeAttendanceReport" => {
|
|
145
|
+
args: %w[roster_id],
|
|
146
|
+
dates: :required,
|
|
147
|
+
desc: "Practice-event attendance roll-up across a date range",
|
|
148
|
+
recommended: false,
|
|
149
|
+
prefer: %w[CheckInSummaryReport CheckInFeedReport],
|
|
150
|
+
notes: "Primarily used by high school coaches who need a report for " \
|
|
151
|
+
"their athletic director about who was present vs absent. " \
|
|
152
|
+
"Only pulls practice events. Not recommended for club teams — " \
|
|
153
|
+
"use CheckInSummaryReport or CheckInFeedReport instead."
|
|
154
|
+
},
|
|
155
|
+
"LastPracticeAttendedReport" => {
|
|
156
|
+
args: %w[roster_id],
|
|
157
|
+
dates: :optional,
|
|
158
|
+
desc: "Days since last practice attended, per wrestler",
|
|
159
|
+
recommended: true,
|
|
160
|
+
notes: "Useful for following up with people who haven't shown up to " \
|
|
161
|
+
"practice in a while — particularly for seasonal clubs."
|
|
162
|
+
},
|
|
163
|
+
"ChurnRiskReport" => {
|
|
164
|
+
args: %w[roster_id days_threshold],
|
|
165
|
+
dates: :optional,
|
|
166
|
+
desc: "Active recurring subscribers who haven't checked in within a window",
|
|
167
|
+
recommended: true,
|
|
168
|
+
notes: "Useful for subscription-based clubs to identify who might " \
|
|
169
|
+
"cancel soon, based on no check-in within a chosen window. " \
|
|
170
|
+
"Pass --days-threshold (7|14|30|60|90); any other value " \
|
|
171
|
+
"falls back to the model's 30-day default.",
|
|
172
|
+
example: "wiq reports run ChurnRiskReport --roster 0 --days-threshold 30"
|
|
173
|
+
},
|
|
174
|
+
"CheckInReport" => {
|
|
175
|
+
args: %w[roster_id],
|
|
176
|
+
dates: :required,
|
|
177
|
+
desc: "Extended attendance — one row per check-in INCLUDING Q&A responses",
|
|
178
|
+
notes: "UI labels this \"Attendance Extended (with questions).\" Same " \
|
|
179
|
+
"shape as CheckInFeedReport PLUS the registration_answer " \
|
|
180
|
+
"values collected at check-in time. Use when Q&A capture " \
|
|
181
|
+
"matters; otherwise prefer CheckInFeedReport."
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
# ── Subscription tab (admin-gated UI; non-finance reports below
|
|
185
|
+
# only require coach access at the API level) ────────────────
|
|
186
|
+
"MembershipSummaryReport" => {
|
|
187
|
+
args: %w[],
|
|
188
|
+
dates: :optional,
|
|
189
|
+
desc: "Every subscription ever created, one row each",
|
|
190
|
+
recommended: true,
|
|
191
|
+
admin_only: true
|
|
192
|
+
},
|
|
193
|
+
"CancelledSubscriptionsReport" => {
|
|
194
|
+
args: %w[],
|
|
195
|
+
dates: :optional,
|
|
196
|
+
desc: "Canceled subscriptions, ordered by cancellation date"
|
|
197
|
+
},
|
|
198
|
+
"CurrentlyPausedSubscriptionsReport" => {
|
|
199
|
+
args: %w[],
|
|
200
|
+
dates: :optional,
|
|
201
|
+
desc: "Currently paused subscriptions, ordered by paused date"
|
|
202
|
+
},
|
|
203
|
+
"ExpiringSubscriptionsReport" => {
|
|
204
|
+
args: %w[],
|
|
205
|
+
dates: :required,
|
|
206
|
+
desc: "Subscriptions expiring in a date range (past or future)",
|
|
207
|
+
recommended: true,
|
|
208
|
+
notes: "Past dates audit churn; future dates plan retention outreach."
|
|
209
|
+
},
|
|
210
|
+
"DiscountedSubscriptionsReport" => {
|
|
211
|
+
args: %w[],
|
|
212
|
+
dates: :optional,
|
|
213
|
+
desc: "Subscriptions (active or canceled) with a scholarship applied"
|
|
214
|
+
},
|
|
215
|
+
"WrestlersWithoutSubscriptionsReport" => {
|
|
216
|
+
args: %w[],
|
|
217
|
+
dates: :optional,
|
|
218
|
+
desc: "Wrestlers without an active recurring subscription"
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
# ── Registration tab ────────────────────────────────────────
|
|
222
|
+
"SessionRegistrationAnswerReport" => {
|
|
223
|
+
args: %w[paid_session_id],
|
|
224
|
+
dates: :optional,
|
|
225
|
+
desc: "Full Q&A export of info submitted by parents at signup",
|
|
226
|
+
recommended: true,
|
|
227
|
+
notes: "Pass --paid-session <id> — required."
|
|
228
|
+
},
|
|
229
|
+
"PaidSessionAccountingReport" => {
|
|
230
|
+
args: %w[paid_session_id],
|
|
231
|
+
dates: :optional,
|
|
232
|
+
desc: "Line-item charges for a registration session",
|
|
233
|
+
recommended: true,
|
|
234
|
+
admin_only: true,
|
|
235
|
+
notes: "Pass --paid-session <id> — required."
|
|
236
|
+
},
|
|
237
|
+
"RegistrationFinanceSummaryReport" => {
|
|
238
|
+
args: %w[],
|
|
239
|
+
dates: :required,
|
|
240
|
+
desc: "Financial summary, one row per session"
|
|
241
|
+
},
|
|
242
|
+
"OverdueRegistrationReport" => {
|
|
243
|
+
args: %w[],
|
|
244
|
+
dates: :optional,
|
|
245
|
+
desc: "Overdue installment registrations",
|
|
246
|
+
recommended: true,
|
|
247
|
+
admin_only: true,
|
|
248
|
+
notes: "Standard AR follow-up tool."
|
|
249
|
+
},
|
|
250
|
+
"InProgressRegistrationReport" => {
|
|
251
|
+
args: %w[],
|
|
252
|
+
dates: :optional,
|
|
253
|
+
desc: "Carts that haven't finished signup (abandoned-cart audit)",
|
|
254
|
+
admin_only: true
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
# ── Scholarship tab ──────────────────────────────────────────
|
|
258
|
+
"ScholarshipAuditReport" => {
|
|
259
|
+
args: %w[],
|
|
260
|
+
dates: :required,
|
|
261
|
+
desc: "Scholarship code usage across registrations + subscriptions",
|
|
262
|
+
recommended: true,
|
|
263
|
+
admin_only: true
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
# ── Donor tab ────────────────────────────────────────────────
|
|
267
|
+
"RecurringDonorReport" => {
|
|
268
|
+
args: %w[],
|
|
269
|
+
dates: :optional,
|
|
270
|
+
desc: "Active recurring donors",
|
|
271
|
+
recommended: true,
|
|
272
|
+
admin_only: true
|
|
273
|
+
},
|
|
274
|
+
"DonationTransactionReport" => {
|
|
275
|
+
args: %w[],
|
|
276
|
+
dates: :required,
|
|
277
|
+
desc: "All payments that included a donation",
|
|
278
|
+
recommended: true,
|
|
279
|
+
admin_only: true
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
# ── Fundraiser tab ───────────────────────────────────────────
|
|
283
|
+
"FundraiserSummaryReport" => {
|
|
284
|
+
args: %w[fundraiser_id],
|
|
285
|
+
dates: :optional,
|
|
286
|
+
desc: "Fundraiser results, one row per contributor",
|
|
287
|
+
recommended: true,
|
|
288
|
+
admin_only: true,
|
|
289
|
+
notes: "Pass --fundraiser <id> — required."
|
|
290
|
+
},
|
|
291
|
+
"FundraiserAccountingReport" => {
|
|
292
|
+
args: %w[fundraiser_id],
|
|
293
|
+
dates: :optional,
|
|
294
|
+
desc: "Fundraiser line items, one row per item purchased",
|
|
295
|
+
admin_only: true,
|
|
296
|
+
notes: "Pass --fundraiser <id> — required. Drill-down of " \
|
|
297
|
+
"FundraiserSummaryReport."
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
# ── Online Store tab ─────────────────────────────────────────
|
|
301
|
+
"OnlineStoreSummaryReport" => {
|
|
302
|
+
args: %w[online_store_id],
|
|
303
|
+
dates: :optional,
|
|
304
|
+
desc: "Summary of store orders",
|
|
305
|
+
recommended: true,
|
|
306
|
+
notes: "Pass --online-store <id> — required."
|
|
307
|
+
},
|
|
308
|
+
"OnlineStoreDetailReport" => {
|
|
309
|
+
args: %w[online_store_id],
|
|
310
|
+
dates: :optional,
|
|
311
|
+
desc: "Detailed store orders, one row per line item",
|
|
312
|
+
notes: "Pass --online-store <id> — required. Useful to hand off to a " \
|
|
313
|
+
"gear or printing partner who needs sku-level data."
|
|
314
|
+
}
|
|
315
|
+
}.freeze
|
|
316
|
+
|
|
317
|
+
desc "run TYPE", "Create + (by default) poll a report"
|
|
318
|
+
long_desc <<~DESC
|
|
319
|
+
Submits a report to `POST /api/v1/reports` and (by default) polls
|
|
320
|
+
until status=ready. Returns the full report row including
|
|
321
|
+
`result` (jsonb), which is type-specific output.
|
|
322
|
+
|
|
323
|
+
TYPE can be any WIQ report class name. See `wiq reports types` for
|
|
324
|
+
the curated allowlist with recommendations, required args, and
|
|
325
|
+
per-type notes (admin_only flag, special arg semantics, etc.).
|
|
326
|
+
|
|
327
|
+
Common args (only those documented in TYPES are honored
|
|
328
|
+
server-side):
|
|
329
|
+
--roster <id> roster_id (0 = all rosters)
|
|
330
|
+
--paid-session <id> paid_session_id (0 = all, UsawExport only)
|
|
331
|
+
--event <id> event_id (EventStatsReport)
|
|
332
|
+
--fundraiser <id> fundraiser_id (Fundraiser* reports)
|
|
333
|
+
--online-store <id> online_store_id (OnlineStore* reports)
|
|
334
|
+
--append-properties <ids...> RosterReport custom columns
|
|
335
|
+
--include-archived-roster-tags RosterReport tag inclusion
|
|
336
|
+
--days-threshold <7|14|30|60|90> ChurnRiskReport window
|
|
337
|
+
--season <year> Resolves to a paid_session_id via the
|
|
338
|
+
overlap rule; errors if multiple match
|
|
339
|
+
|
|
340
|
+
Polling: 2-second initial interval, exponential backoff to 30s
|
|
341
|
+
cap, default 5-minute timeout. Pass --no-wait to return the
|
|
342
|
+
report row immediately after submission (status=queued or
|
|
343
|
+
processing) and poll later with `wiq reports show <id> --wait`.
|
|
344
|
+
DESC
|
|
345
|
+
method_option :start, type: :string, desc: "YYYY-MM-DD"
|
|
346
|
+
method_option :end, type: :string, desc: "YYYY-MM-DD"
|
|
347
|
+
method_option :roster, type: :numeric, desc: "args.roster_id (0 = all rosters)"
|
|
348
|
+
method_option :paid_session, type: :numeric, desc: "args.paid_session_id"
|
|
349
|
+
method_option :event, type: :numeric, desc: "args.event_id"
|
|
350
|
+
method_option :fundraiser, type: :numeric, desc: "args.fundraiser_id"
|
|
351
|
+
method_option :online_store, type: :numeric, desc: "args.online_store_id"
|
|
352
|
+
method_option :append_properties, type: :array,
|
|
353
|
+
desc: "Registration question ids to surface as columns (RosterReport)"
|
|
354
|
+
method_option :include_archived_roster_tags, type: :boolean, default: false,
|
|
355
|
+
desc: "Include archived roster tags (RosterReport)"
|
|
356
|
+
method_option :days_threshold, type: :numeric,
|
|
357
|
+
enum: [7, 14, 30, 60, 90],
|
|
358
|
+
desc: "args.days_threshold (ChurnRiskReport)"
|
|
359
|
+
method_option :name, type: :string, desc: "Display name (default: CLI <type> <range>)"
|
|
360
|
+
method_option :season, type: :numeric, desc: "Resolve to paid_session_id via overlap with calendar year"
|
|
361
|
+
method_option :wait, type: :boolean, default: true
|
|
362
|
+
method_option :timeout, type: :numeric, default: 300
|
|
363
|
+
map "run" => :run_report
|
|
364
|
+
def run_report(type)
|
|
365
|
+
args = build_args(type)
|
|
366
|
+
|
|
367
|
+
body = {
|
|
368
|
+
report: {
|
|
369
|
+
type: type,
|
|
370
|
+
version: "v1",
|
|
371
|
+
name: options[:name] || default_name(type),
|
|
372
|
+
start_at: options[:start],
|
|
373
|
+
end_at: options[:end],
|
|
374
|
+
args: args
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
report = client.post("/api/v1/reports", body)
|
|
379
|
+
report = self.class.poll(client, report["id"], timeout: options[:timeout]) if options[:wait]
|
|
380
|
+
|
|
381
|
+
render(report,
|
|
382
|
+
summary: "Report ##{report["id"]} status=#{report["status"]}.",
|
|
383
|
+
breadcrumbs: [
|
|
384
|
+
{ "cmd" => "wiq reports show #{report["id"]}", "description" => "Refetch the result later" }
|
|
385
|
+
])
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
desc "show ID", "Fetch a single report (optionally poll)"
|
|
389
|
+
long_desc <<~DESC
|
|
390
|
+
Fetches a report by id. Pass --wait to poll until status=ready,
|
|
391
|
+
useful for "I kicked this off earlier, is it done yet?" flows.
|
|
392
|
+
DESC
|
|
393
|
+
method_option :wait, type: :boolean, default: false
|
|
394
|
+
method_option :timeout, type: :numeric, default: 300
|
|
395
|
+
def show(id)
|
|
396
|
+
report = options[:wait] ? self.class.poll(client, id, timeout: options[:timeout])
|
|
397
|
+
: client.get("/api/v1/reports/#{id}")
|
|
398
|
+
render(report,
|
|
399
|
+
summary: "Report ##{report["id"]} status=#{report["status"]}.",
|
|
400
|
+
breadcrumbs: [
|
|
401
|
+
{ "cmd" => "wiq reports show #{id} --wait", "description" => "Poll until ready" }
|
|
402
|
+
])
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
desc "types", "Print the curated report-type allowlist with recommendations"
|
|
406
|
+
long_desc <<~DESC
|
|
407
|
+
Dumps the curated TYPES allowlist with per-entry metadata:
|
|
408
|
+
description, permitted args, whether dates are required, the
|
|
409
|
+
recommended/preferred status, and an example invocation where
|
|
410
|
+
applicable.
|
|
411
|
+
|
|
412
|
+
Recommended starting point for any agent trying to pick a report
|
|
413
|
+
for a user question — read this once, then call
|
|
414
|
+
`wiq reports run <TYPE>` with the documented args.
|
|
415
|
+
|
|
416
|
+
Entries with `admin_only: true` (the 9 finance reports) require
|
|
417
|
+
an admin CoachProfile PAT. Non-admin coach PATs 403 on those.
|
|
418
|
+
DESC
|
|
419
|
+
def types
|
|
420
|
+
rows = TYPES.map do |type, info|
|
|
421
|
+
row = {
|
|
422
|
+
"type" => type,
|
|
423
|
+
"description" => info[:desc],
|
|
424
|
+
"args" => info[:args],
|
|
425
|
+
"dates" => info[:dates].to_s
|
|
426
|
+
}
|
|
427
|
+
row["recommended"] = info[:recommended] if info.key?(:recommended)
|
|
428
|
+
row["prefer"] = info[:prefer] if info[:prefer]
|
|
429
|
+
row["admin_only"] = info[:admin_only] if info[:admin_only]
|
|
430
|
+
row["notes"] = info[:notes] if info[:notes]
|
|
431
|
+
row["example"] = info[:example] if info[:example]
|
|
432
|
+
row
|
|
433
|
+
end
|
|
434
|
+
render_index(
|
|
435
|
+
rows,
|
|
436
|
+
summary: "Documented report types. Auth: every report POST requires a " \
|
|
437
|
+
"CoachProfile-bound PAT on the team. The 9 entries with " \
|
|
438
|
+
"admin_only: true additionally require admin? — " \
|
|
439
|
+
"non-admin coach PATs 403 on those. recommended: true rows " \
|
|
440
|
+
"are WIQ-blessed picks; prefer: lists alternatives for " \
|
|
441
|
+
"de-emphasized types.",
|
|
442
|
+
breadcrumbs: [
|
|
443
|
+
{ "cmd" => "wiq reports run <TYPE>", "description" => "Submit a report" },
|
|
444
|
+
{ "cmd" => "wiq registrations questions",
|
|
445
|
+
"description" => "Discover question ids for RosterReport --append-properties" }
|
|
446
|
+
]
|
|
447
|
+
)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Class-level poller used by reports + check_ins summary.
|
|
451
|
+
def self.poll(client, id, timeout: 300, initial: 2, max_interval: 30)
|
|
452
|
+
deadline = Time.now + timeout
|
|
453
|
+
interval = initial
|
|
454
|
+
report = nil
|
|
455
|
+
loop do
|
|
456
|
+
report = client.get("/api/v1/reports/#{id}")
|
|
457
|
+
case report["status"]
|
|
458
|
+
when "ready"
|
|
459
|
+
return report
|
|
460
|
+
when "failed"
|
|
461
|
+
raise Wiq::ReportFailedError, report
|
|
462
|
+
end
|
|
463
|
+
if Time.now >= deadline
|
|
464
|
+
raise Wiq::ReportTimeoutError.new(report, timeout)
|
|
465
|
+
end
|
|
466
|
+
sleep(interval)
|
|
467
|
+
interval = [interval * 2, max_interval].min
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
no_commands do
|
|
472
|
+
def build_args(type)
|
|
473
|
+
if options[:season]
|
|
474
|
+
resolver = Wiq::SeasonResolver.new(client)
|
|
475
|
+
matches = resolver.paid_sessions_for(options[:season])
|
|
476
|
+
raise Wiq::SeasonNotFoundError, options[:season] if matches.empty?
|
|
477
|
+
|
|
478
|
+
spec = TYPES[type]
|
|
479
|
+
wants_paid_session = spec.nil? || spec[:args].include?("paid_session_id")
|
|
480
|
+
unless wants_paid_session
|
|
481
|
+
raise Wiq::SeasonUnsupportedError.new(type, matches)
|
|
482
|
+
end
|
|
483
|
+
if matches.size > 1 && !options[:paid_session]
|
|
484
|
+
raise Wiq::SeasonUnsupportedError.new(type, matches)
|
|
485
|
+
end
|
|
486
|
+
options[:paid_session] ||= matches.first["id"]
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
args = {}
|
|
490
|
+
args["roster_id"] = options[:roster] if options[:roster] || options[:roster] == 0
|
|
491
|
+
args["paid_session_id"] = options[:paid_session] if options[:paid_session] || options[:paid_session] == 0
|
|
492
|
+
args["event_id"] = options[:event] if options[:event]
|
|
493
|
+
args["fundraiser_id"] = options[:fundraiser] if options[:fundraiser]
|
|
494
|
+
args["online_store_id"] = options[:online_store] if options[:online_store]
|
|
495
|
+
if options[:append_properties] && !options[:append_properties].empty?
|
|
496
|
+
args["append_property_ids"] = options[:append_properties].map(&:to_i)
|
|
497
|
+
end
|
|
498
|
+
args["include_archived_roster_tags"] = true if options[:include_archived_roster_tags]
|
|
499
|
+
args["days_threshold"] = options[:days_threshold] if options[:days_threshold]
|
|
500
|
+
args
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def default_name(type)
|
|
504
|
+
range = [options[:start], options[:end]].compact.join("..")
|
|
505
|
+
range.empty? ? "CLI #{type}" : "CLI #{type} #{range}"
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wiq
|
|
4
|
+
module Commands
|
|
5
|
+
class Rosters < Base
|
|
6
|
+
desc "list", "List rosters"
|
|
7
|
+
long_desc <<~DESC
|
|
8
|
+
Returns every roster on the calling profile's team, paginated.
|
|
9
|
+
|
|
10
|
+
Season filtering is a CLI-side projection (WIQ has no first-class
|
|
11
|
+
Season entity):
|
|
12
|
+
--season <year> Resolves to paid_session ids whose date
|
|
13
|
+
window overlaps that calendar year, then
|
|
14
|
+
filters rosters whose roster_syncers point
|
|
15
|
+
at any of those paid sessions.
|
|
16
|
+
--season-tag <tag> Filters rosters carrying a tag with that
|
|
17
|
+
exact name. Some teams tag rosters with
|
|
18
|
+
conventions like "2025-26".
|
|
19
|
+
|
|
20
|
+
Each roster row embeds roster_syncers and taggings, which is what
|
|
21
|
+
--season uses to filter without needing extra calls.
|
|
22
|
+
DESC
|
|
23
|
+
method_option :season, type: :numeric,
|
|
24
|
+
desc: "Filter to rosters whose syncers point at paid sessions overlapping this year"
|
|
25
|
+
method_option :season_tag, type: :string, desc: "Filter to rosters carrying this tag"
|
|
26
|
+
method_option :archived, type: :boolean, desc: "Show only archived (true) or active (false)"
|
|
27
|
+
method_option :all, type: :boolean, default: false
|
|
28
|
+
def list
|
|
29
|
+
params = { "per_page" => 100 }
|
|
30
|
+
unless options[:archived].nil?
|
|
31
|
+
params["q[archived_eq]"] = options[:archived]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
records, total = fetch_index("/api/v1/rosters", params, key: "rosters")
|
|
35
|
+
|
|
36
|
+
if options[:season]
|
|
37
|
+
resolver = Wiq::SeasonResolver.new(client)
|
|
38
|
+
ids = resolver.paid_session_ids_for(options[:season])
|
|
39
|
+
raise Wiq::SeasonNotFoundError, options[:season] if ids.empty?
|
|
40
|
+
|
|
41
|
+
records = resolver.filter_rosters_by_ids(records, ids)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if options[:season_tag]
|
|
45
|
+
tag = options[:season_tag]
|
|
46
|
+
records = records.select do |r|
|
|
47
|
+
(r["taggings"] || []).any? { |t| t.dig("tag", "name") == tag }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
render_index(
|
|
52
|
+
records, total: total,
|
|
53
|
+
summary: "Listed #{records.size} rosters.",
|
|
54
|
+
breadcrumbs: [
|
|
55
|
+
{ "cmd" => "wiq rosters show <id>", "description" => "Inspect a single roster" },
|
|
56
|
+
{ "cmd" => "wiq reports run RosterReport --roster <id>",
|
|
57
|
+
"description" => "Export the roster as a report" }
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
desc "show ID", "Fetch a single roster"
|
|
63
|
+
long_desc <<~DESC
|
|
64
|
+
Full roster payload: name, archived flag, age_division_id, taggings
|
|
65
|
+
(with tag metadata), roster_syncers (linkage to paid sessions), and
|
|
66
|
+
an embedded stats.roster_memberships_count.
|
|
67
|
+
DESC
|
|
68
|
+
def show(id)
|
|
69
|
+
roster = client.get("/api/v1/rosters/#{id}")
|
|
70
|
+
render(roster,
|
|
71
|
+
summary: "Roster #{roster["id"]} — #{roster["name"]}",
|
|
72
|
+
breadcrumbs: [
|
|
73
|
+
{ "cmd" => "wiq reports run RosterStatsReport --roster #{roster["id"]} --start <date> --end <date>",
|
|
74
|
+
"description" => "Pull stats for this roster" },
|
|
75
|
+
{ "cmd" => "wiq check_ins summary --roster #{roster["id"]} --start <date> --end <date>",
|
|
76
|
+
"description" => "Attendance summary for this roster" }
|
|
77
|
+
])
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Wiq
|
|
6
|
+
module Commands
|
|
7
|
+
class Setup < Base
|
|
8
|
+
SKILL_FILENAME = "SKILL.md"
|
|
9
|
+
BUNDLED_SKILL_PATH = File.expand_path("../../../../share/skills/wiq/#{SKILL_FILENAME}", __FILE__)
|
|
10
|
+
|
|
11
|
+
desc "claude", "Install the wiq Claude Code skill"
|
|
12
|
+
long_desc <<~DESC
|
|
13
|
+
Copies the bundled SKILL.md into the right location for Claude Code
|
|
14
|
+
to auto-detect.
|
|
15
|
+
|
|
16
|
+
Default install (user-global):
|
|
17
|
+
~/.claude/skills/wiq/SKILL.md
|
|
18
|
+
|
|
19
|
+
Per-project install (--project):
|
|
20
|
+
./.claude/skills/wiq/SKILL.md
|
|
21
|
+
|
|
22
|
+
Claude Code watches both locations live during a session — no
|
|
23
|
+
restart needed. Re-run with --force to overwrite an existing
|
|
24
|
+
install (useful when upgrading the gem).
|
|
25
|
+
|
|
26
|
+
The skill is read-only on the WIQ side: it teaches Claude how to
|
|
27
|
+
drive `wiq`, but doesn't touch any WIQ data.
|
|
28
|
+
DESC
|
|
29
|
+
method_option :project, type: :boolean, default: false,
|
|
30
|
+
desc: "Install per-project (./.claude/skills/) instead of user-global"
|
|
31
|
+
method_option :force, type: :boolean, default: false,
|
|
32
|
+
desc: "Overwrite an existing SKILL.md at the target path"
|
|
33
|
+
method_option :print, type: :boolean, default: false,
|
|
34
|
+
desc: "Print SKILL.md to stdout instead of installing"
|
|
35
|
+
def claude
|
|
36
|
+
unless File.exist?(BUNDLED_SKILL_PATH)
|
|
37
|
+
raise Wiq::Error.new(
|
|
38
|
+
"Bundled SKILL.md not found at #{BUNDLED_SKILL_PATH}.",
|
|
39
|
+
code: "skill_missing",
|
|
40
|
+
hint: "Reinstall wiq-cli — the gem package may be incomplete."
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if options[:print]
|
|
45
|
+
puts File.read(BUNDLED_SKILL_PATH)
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
target_dir = options[:project] ? File.join(Dir.pwd, ".claude", "skills", "wiq")
|
|
50
|
+
: File.join(Dir.home, ".claude", "skills", "wiq")
|
|
51
|
+
target_path = File.join(target_dir, SKILL_FILENAME)
|
|
52
|
+
|
|
53
|
+
if File.exist?(target_path) && !options[:force]
|
|
54
|
+
raise Wiq::Error.new(
|
|
55
|
+
"#{target_path} already exists.",
|
|
56
|
+
code: "skill_exists",
|
|
57
|
+
hint: "Pass --force to overwrite, or --print to inspect the bundled version first."
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
FileUtils.mkdir_p(target_dir)
|
|
62
|
+
FileUtils.cp(BUNDLED_SKILL_PATH, target_path)
|
|
63
|
+
|
|
64
|
+
render(
|
|
65
|
+
{
|
|
66
|
+
"scope" => options[:project] ? "project" : "user-global",
|
|
67
|
+
"target_path" => target_path,
|
|
68
|
+
"size_bytes" => File.size(target_path),
|
|
69
|
+
"claude_will_auto_detect" => true
|
|
70
|
+
},
|
|
71
|
+
summary: "Installed Claude Code skill to #{target_path}.",
|
|
72
|
+
breadcrumbs: [
|
|
73
|
+
{ "cmd" => "wiq setup claude --print",
|
|
74
|
+
"description" => "Inspect the bundled skill content" },
|
|
75
|
+
{ "cmd" => "wiq setup claude --force",
|
|
76
|
+
"description" => "Re-install (e.g. after upgrading wiq-cli)" }
|
|
77
|
+
]
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|