wiq-cli 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cac3732fa4c286907bce03d6ac62765477d828507719a29074873a9f9b90bde2
4
- data.tar.gz: c686b4e1fb901e6554727c7030eb63d66cd70f659ca95345db2124b0ec6c9138
3
+ metadata.gz: bffdea54163c5b95a5cab8b219d3e5cb3a898a059d7edce13afa8ae02928b27a
4
+ data.tar.gz: 0f9e49529c6bc9fb37b75761e581d1de90a186107242a6c3092fcca55687f9ba
5
5
  SHA512:
6
- metadata.gz: 970cfd866b44f27e8be608f8ddb40b147521bc241557637e545b21e23ec490d6e5a6ca8b6252b9f1c5f5a0ece7d1ad4a12f6457b80e1131c0beef691a5e65a3d
7
- data.tar.gz: 22e94b802d4b4209c32131919733b68937775515a0b089ce764c1496758b281562e5532b708258351bac66c78f2db138eaefa9b55987dcb68671112ef2272ca5
6
+ metadata.gz: 57f4ccba1f18ca17207739d051e94fcfb1b52b712c2d87e97fc64951acd4041d37a69f6d89d262dfde73cc44d278549d6ac9af02b67a572e3dd549dd2c58cd95
7
+ data.tar.gz: a317f13e135ab5b7a20f24b7f0c8a6640cbf84afc6a0a3e8d38d9d6483a537c302289f479b1cac0d454ca9c10485ebbcf453804decf4cdf02dbd73b822b0b933
data/docs/deferred.md CHANGED
@@ -144,10 +144,14 @@ Last updated: 2026-05-12 (after L1 ships).
144
144
  - **Echo `request_id`** in the response body or `X-Request-ID` header,
145
145
  for support correlation. Today the CLI can't give a customer a request
146
146
  ID to ship to support.
147
- - **Real location/site filter on events.** `Event.location` is free-text
148
- and not in `ransackable_attributes`. WIQ team has flagged a structured
149
- location concept as roadmap. Until then the CLI deliberately exposes
150
- no `--site` flag.
147
+ - ~~**Real location/site filter on events.**~~ SHIPPED (June 2026,
148
+ wre-506). The backend grew a structured Location model. The CLI now
149
+ exposes `wiq locations list|show` plus `--location` flags on
150
+ `events list` (repeatable → `location_ids[]`), `rosters list`
151
+ (`q[location_id_eq]`), `paid_sessions list`, `wrestlers list`,
152
+ `metrics show`, and `reports run` (args.location_id, honored by 13
153
+ report types). The legacy free-text `Event.location` remains for old
154
+ rows; serialized `location` is the display form.
151
155
  - **Subdomain discovery flow for `wiq auth login`.** PAT settings URL
152
156
  lives at `<team-subdomain>.wrestlingiq.com/settings/personal_access_tokens`.
153
157
  If a customer doesn't know their subdomain, the CLI can't deep-link
@@ -246,6 +246,29 @@ file with the WIQ team.
246
246
 
247
247
  - `POST /api/v1/reports`
248
248
  - Body: `{ "report": { "type": "<ReportClass>", "version": "v1"|"vrow", "name": "...", "start_at": "YYYY-MM-DD", "end_at": "YYYY-MM-DD", "args": { ... } } }`
249
+ - **`version` — v1 vs vrow.** `:version` is in the controller permit
250
+ list (`reports_controller.rb#report_params`). Despite both being
251
+ accepted everywhere, version only changes output for three reports —
252
+ `RosterReport`, `UsawReport`, `PaidSessionAccountingReport` — which
253
+ branch in `generate_ver_result!` (`v1?` → structured JSON objects,
254
+ `vrow?` → row/CSV with a leading header row). All three implement
255
+ both; no report is v1-only. Every other report overrides
256
+ `generate_ver_result!` and emits rows regardless of version. vrow is
257
+ the de facto standard, so the CLI defaults to it (uniform row shape
258
+ across all reports) and exposes the legacy structured shape via
259
+ `wiq reports run … --v1`.
260
+ - **`RosterReport` "Added to roster at" lives only in vrow.** The vrow
261
+ path calls `get_wrestlers_and_roster(include_roster_memberships: true)`
262
+ (`report.rb`), which `.select`s
263
+ `roster_memberships.created_at as added_to_roster_at` and emits it as
264
+ the `"Added to roster at"` column. The v1 path renders wrestlers via
265
+ `_wrestler_profile.json.jbuilder`, which has no such field. The join
266
+ only happens for a specific roster (`roster_id > 0`); `roster_id: 0`
267
+ ("all wrestlers") takes the else branch and the value is nil — a
268
+ wrestler can be on multiple rosters, so the join date is per-roster.
269
+ The roster index/show jbuilder (`_roster.json.jbuilder`) exposes only
270
+ `stats.roster_memberships_count`, never per-membership timestamps, so
271
+ vrow RosterReport is the sole API path to this data.
249
272
  - Permitted `args` keys (`reports_controller.rb#report_params`):
250
273
  `paid_session_id, roster_id, fundraiser_id, online_store_id, event_id,
251
274
  include_archived_roster_tags, append_property_ids[]`.
@@ -445,13 +468,16 @@ Other registration surface:
445
468
  - `expand=event_invites,event_bookings,private_lessons` — comma-separated
446
469
  CSV; pulls in nested data on the events index/show.
447
470
  - `Event.ransackable_attributes` allows
448
- `id, name, start_at, end_at, event_type, paid_session_id`. **`location`
449
- is not ransackable today.** A real location filter is on the WIQ
450
- backend roadmap (it will replace the free-text `location` column with a
451
- structured concept); until that ships the CLI doesn't expose a
452
- location/site flag. Users who need it can fetch the date range, write
453
- the results to a file, and post-process — that's not the CLI's problem
454
- to solve in v1.
471
+ `id, name, start_at, end_at, event_type, paid_session_id`. The
472
+ free-text `location` column is still not ransackable, but the
473
+ structured location filter shipped (June 2026): pass `location_ids[]`
474
+ (repeatable, dedicated param handled in `apply_event_filters`, not
475
+ Ransack) to filter on the event's `location_id`. Events with no
476
+ `location_id` are excluded from a filtered listing. The serialized
477
+ `location` field is now `Event#display_location` — the structured
478
+ Location's name when set, else the legacy free text — and `location_id`
479
+ is serialized alongside it. The CLI exposes this as
480
+ `wiq events list --location <id> [<id>...]`.
455
481
  - Soft deletes: events use `acts_as_paranoid`; the index calls
456
482
  `.without_deleted`. No "include deleted" param exposed.
457
483
  - `POST /api/v1/events` — recurring practice creation:
@@ -619,9 +645,12 @@ PaidSession-overlap convention proves load-bearing for many customers.
619
645
  demand. Until then, jbuilders are the source of truth.
620
646
  2. `request_id` echoed in response body and/or `X-Request-ID` header for
621
647
  support correlation.
622
- 3. Real location/site filter on `GET /api/v1/events` (when the backend
623
- gets a structured location concept). Until then the CLI doesn't expose
624
- a flag.
648
+ 3. ~~Real location/site filter on `GET /api/v1/events`~~ SHIPPED
649
+ (June 2026, wre-506). Structured Location model + `location_ids[]` on
650
+ events, `location_id` on paid_sessions/wrestlers/metrics/reports,
651
+ `q[location_id_eq]` on rosters, and a `GET /api/v1/locations`
652
+ resource. All exposed in the CLI via `wiq locations` and `--location`
653
+ flags.
625
654
  4. Subdomain discovery for `wiq auth login`. The PAT settings URL lives at
626
655
  `<team-subdomain>.wrestlingiq.com/settings/personal_access_tokens`; if
627
656
  the customer doesn't know their subdomain, the CLI can't deep-link
data/lib/wiq/cli.rb CHANGED
@@ -65,6 +65,9 @@ module Wiq
65
65
  desc "rosters SUBCOMMAND", "Rosters (list, show)"
66
66
  subcommand "rosters", Wiq::Commands::Rosters
67
67
 
68
+ desc "locations SUBCOMMAND", "Team locations / sites — ID discovery for --location flags (list, show)"
69
+ subcommand "locations", Wiq::Commands::Locations
70
+
68
71
  desc "prospects SUBCOMMAND", "Prospect pipeline — individual kids (list, show, summary)"
69
72
  subcommand "prospects", Wiq::Commands::Prospects
70
73
 
@@ -25,18 +25,25 @@ module Wiq
25
25
  --event-type One of `wiq events types` (practice, dual_meet, ...)
26
26
  --roster One or more roster ids (the API joins through
27
27
  roster_events)
28
+ --location One or more location ids (structured Location
29
+ records; discover via `wiq locations list`)
28
30
  --expand CSV of event_invites,event_bookings,private_lessons.
29
31
  Drops nested data into each event row.
30
32
 
31
- There is no server-side location/site filter today — `Event.location`
32
- is free-text and not in `ransackable_attributes`. A location-aware
33
- backend is on the WIQ roadmap; until then post-process client-side.
33
+ Location caveat: --location matches the event's own structured
34
+ location_id. Events with NO location set are excluded from a
35
+ filtered listing they only appear when you don't filter. The
36
+ legacy free-text `location` field still exists on old events; the
37
+ serialized `location` value is the display form (structured record
38
+ when set, else the free text).
34
39
 
35
40
  Use --all to walk every page; default is page 1, per_page=100.
36
41
  DESC
37
42
  method_option :start, type: :string, required: true, desc: "YYYY-MM-DD (team timezone)"
38
43
  method_option :end, type: :string, required: true, desc: "YYYY-MM-DD (team timezone)"
39
44
  method_option :roster, type: :array, desc: "One or more roster ids"
45
+ method_option :location, type: :array,
46
+ desc: "One or more location ids (events with no location are excluded)"
40
47
  method_option :event_type, type: :string, enum: %w[practice dual_meet tournament scramble private_lesson other],
41
48
  desc: "Filter to one event_type (see `wiq events types`)"
42
49
  method_option :expand, type: :string,
@@ -53,6 +60,9 @@ module Wiq
53
60
  if options[:roster]
54
61
  options[:roster].each { |rid| (params["roster_ids[]"] ||= []) << rid }
55
62
  end
63
+ if options[:location]
64
+ options[:location].each { |lid| (params["location_ids[]"] ||= []) << lid }
65
+ end
56
66
 
57
67
  records, total = fetch_index("/api/v1/events", params, key: "events")
58
68
  render_index(
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wiq
4
+ module Commands
5
+ class Locations < Base
6
+ desc "list", "List the team's structured locations (sites/gyms)"
7
+ long_desc <<~DESC
8
+ Locations are WIQ's structured multi-site concept — a named place
9
+ (with optional street address) that rosters, events, paid sessions,
10
+ wrestlers, metrics, and reports can be scoped to. Single-site clubs
11
+ typically have zero or one; multi-site clubs use them to slice
12
+ everything per gym.
13
+
14
+ Archived locations are hidden by default; pass --include-archived
15
+ to see them. Ordered by name server-side.
16
+
17
+ This is the ID-discovery surface for every --location flag in the
18
+ CLI:
19
+ wiq rosters list --location <id>
20
+ wiq events list --location <id> [...more ids]
21
+ wiq wrestlers list --location <id>
22
+ wiq paid_sessions list --location <id>
23
+ wiq metrics show <name> --location <id>
24
+ wiq reports run <Type> --location <id>
25
+ DESC
26
+ method_option :include_archived, type: :boolean, default: false,
27
+ desc: "Include archived locations"
28
+ method_option :all, type: :boolean, default: false
29
+ def list
30
+ params = { "per_page" => 100 }
31
+ params["include_archived"] = true if options[:include_archived]
32
+
33
+ records, total = fetch_index("/api/v1/locations", params, key: "locations")
34
+ render_index(
35
+ records, total: total,
36
+ summary: "Listed #{records.size} locations#{options[:include_archived] ? " (including archived)" : ""}.",
37
+ breadcrumbs: [
38
+ { "cmd" => "wiq locations show <id>", "description" => "Inspect a single location" },
39
+ { "cmd" => "wiq rosters list --location <id>", "description" => "Rosters at a location" },
40
+ { "cmd" => "wiq metrics show mrr --location <id>",
41
+ "description" => "Finance metrics scoped to a location (admin only)" }
42
+ ]
43
+ )
44
+ end
45
+
46
+ desc "show ID", "Fetch a single location"
47
+ long_desc <<~DESC
48
+ Full location payload: id, name, address_line1, address_line2,
49
+ city, state, postal_code, country, archived.
50
+ DESC
51
+ def show(id)
52
+ location = client.get("/api/v1/locations/#{id}")
53
+ render(location,
54
+ summary: "Location #{location["id"]} — #{location["name"]}",
55
+ breadcrumbs: [
56
+ { "cmd" => "wiq rosters list --location #{location["id"]}",
57
+ "description" => "Rosters at this location" },
58
+ { "cmd" => "wiq wrestlers list --location #{location["id"]}",
59
+ "description" => "Wrestlers on any roster at this location" }
60
+ ])
61
+ end
62
+ end
63
+ end
64
+ end
@@ -62,6 +62,13 @@ module Wiq
62
62
  Intervals: hourly, daily, weekly, monthly. Invalid values are
63
63
  silently coerced to `daily` server-side.
64
64
 
65
+ --location <id> scopes every metric to one structured location
66
+ (discover ids via `wiq locations list`). CAUTION: the server
67
+ validates the id against the team's own locations and silently
68
+ falls back to ALL locations when it doesn't match — a typo'd or
69
+ foreign id returns team-wide numbers, not an error. The CLI echoes
70
+ the requested location in `meta` so you can sanity-check.
71
+
65
72
  Currency metrics return integer cents (divide by 100 for dollars).
66
73
  Non-currency metrics return raw counts.
67
74
  DESC
@@ -70,6 +77,8 @@ module Wiq
70
77
  enum: VALID_INTERVALS, desc: "interval_group"
71
78
  method_option :start_date, type: :string, desc: "Required if --range=custom"
72
79
  method_option :end_date, type: :string, desc: "Required if --range=custom"
80
+ method_option :location, type: :numeric,
81
+ desc: "Scope to one location id (unknown ids silently mean ALL locations)"
73
82
  method_option :no_comparison, type: :boolean, default: false,
74
83
  desc: "Drop comparison_series/comparison_total from output"
75
84
  def show(name)
@@ -91,6 +100,7 @@ module Wiq
91
100
  params["start_date"] = options[:start_date]
92
101
  params["end_date"] = options[:end_date]
93
102
  end
103
+ params["location_id"] = options[:location] if options[:location]
94
104
 
95
105
  payload = client.get("/api/v1/metrics/#{name}", params)
96
106
  metrics = payload["metrics"] || {}
@@ -106,9 +116,13 @@ module Wiq
106
116
  data["comparison_total"] = metrics["comparison_total"]
107
117
  end
108
118
 
119
+ meta = { "currency_unit" => CURRENCY_METRICS.include?(name) ? "cents" : "count" }
120
+ meta["location_id"] = options[:location] if options[:location]
121
+
109
122
  render(data,
110
- summary: "metric=#{name} range=#{options[:range]} interval=#{options[:interval]}",
111
- meta: { "currency_unit" => CURRENCY_METRICS.include?(name) ? "cents" : "count" },
123
+ summary: "metric=#{name} range=#{options[:range]} interval=#{options[:interval]}" \
124
+ "#{options[:location] ? " location=#{options[:location]}" : ""}",
125
+ meta: meta,
112
126
  breadcrumbs: [
113
127
  { "cmd" => "wiq metrics list", "description" => "See all supported metrics" }
114
128
  ])
@@ -20,6 +20,10 @@ module Wiq
20
20
  recurring_registerable, not_recurring, not_recurring_with_archived,
21
21
  not_archived, dropin, trial, trial_or_dropin
22
22
 
23
+ --location <id> filters server-side to sessions stamped with that
24
+ structured location (discover ids via `wiq locations list`).
25
+ Sessions with no location are excluded when the filter is on.
26
+
23
27
  --season filters CLI-side to sessions whose [start_at, end_at] window
24
28
  overlaps the given calendar year. Pair with --all to get an exhaustive
25
29
  list.
@@ -30,11 +34,13 @@ module Wiq
30
34
  you usually don't need a follow-up call.
31
35
  DESC
32
36
  method_option :type, type: :string, enum: PRESET_TYPES, desc: "Preset scope filter"
37
+ method_option :location, type: :numeric, desc: "Filter to sessions at one location id"
33
38
  method_option :season, type: :numeric, desc: "Filter to sessions overlapping calendar year"
34
39
  method_option :all, type: :boolean, default: false
35
40
  def list
36
41
  params = { "per_page" => 50 }
37
42
  params[:type] = options[:type] if options[:type]
43
+ params["location_id"] = options[:location] if options[:location]
38
44
 
39
45
  records, total = fetch_index("/api/v1/paid_sessions", params, key: "paid_sessions")
40
46
 
@@ -26,7 +26,7 @@ module Wiq
26
26
  TYPES = {
27
27
  # ── Roster tab ───────────────────────────────────────────────
28
28
  "RosterReport" => {
29
- args: %w[roster_id append_property_ids include_archived_roster_tags],
29
+ args: %w[roster_id location_id append_property_ids include_archived_roster_tags],
30
30
  dates: :optional,
31
31
  desc: "Roster snapshot — name, weight class, academic class, age",
32
32
  recommended: true,
@@ -36,8 +36,17 @@ module Wiq
36
36
  "--visibility private if admin) to surface intake data as " \
37
37
  "extra columns. Skip questions with a deleted_at — they're " \
38
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"
39
+ "means \"all rosters\". Each appended question becomes a column. " \
40
+ "The default row/CSV shape (web Download) carries the \"Added " \
41
+ "to roster at\" column (roster_memberships.created_at — when the " \
42
+ "wrestler landed on the roster, NOT their registration date), " \
43
+ "populated only for a specific --roster <id> (id > 0), not " \
44
+ "--roster 0. With --location <id> instead, the report scopes to " \
45
+ "wrestlers on any roster at that location (one row each) and " \
46
+ "\"Added to roster at\" becomes their EARLIEST membership across " \
47
+ "that location's rosters. --v1 returns fuller per-wrestler " \
48
+ "objects but drops that column.",
49
+ example: "wiq reports run RosterReport --roster 42"
41
50
  },
42
51
  "FullExportWrestlerReport" => {
43
52
  args: %w[],
@@ -61,52 +70,52 @@ module Wiq
61
70
 
62
71
  # ── USAW / AAU tab ───────────────────────────────────────────
63
72
  "UsawReport" => {
64
- args: %w[roster_id],
73
+ args: %w[roster_id location_id],
65
74
  dates: :optional,
66
75
  desc: "All USA Wrestling card info on file, one row per wrestler",
67
76
  recommended: true,
68
77
  notes: "UI hides this tab when USAW collection is off (team setting)."
69
78
  },
70
79
  "UsawExpiredReport" => {
71
- args: %w[roster_id],
80
+ args: %w[roster_id location_id],
72
81
  dates: :optional,
73
82
  desc: "Wrestlers missing or with expired USAW memberships",
74
83
  notes: "Output is also a bulk-purchase upload format for USAW's system."
75
84
  },
76
85
  "UsawExportReport" => {
77
- args: %w[roster_id paid_session_id],
86
+ args: %w[roster_id location_id paid_session_id],
78
87
  dates: :optional,
79
88
  desc: "USAW bulk-purchase upload format",
80
89
  notes: "Only report that accepts paid_session_id=0 to mean " \
81
90
  "\"all sessions\"."
82
91
  },
83
92
  "AauReport" => {
84
- args: %w[roster_id],
93
+ args: %w[roster_id location_id],
85
94
  dates: :optional,
86
95
  desc: "All AAU card info on file, one row per wrestler",
87
96
  recommended: true,
88
97
  notes: "UI hides this tab when AAU collection is off (team setting)."
89
98
  },
90
99
  "AauExpiredReport" => {
91
- args: %w[roster_id],
100
+ args: %w[roster_id location_id],
92
101
  dates: :optional,
93
102
  desc: "Wrestlers missing or with expired AAU memberships"
94
103
  },
95
104
  "AauExportReport" => {
96
- args: %w[roster_id],
105
+ args: %w[roster_id location_id],
97
106
  dates: :optional,
98
107
  desc: "AAU bulk-purchase upload format"
99
108
  },
100
109
 
101
110
  # ── Stats tab ────────────────────────────────────────────────
102
111
  "WinLossReport" => {
103
- args: %w[roster_id],
112
+ args: %w[roster_id location_id],
104
113
  dates: :required,
105
114
  desc: "Wins and losses per wrestler",
106
115
  recommended: true
107
116
  },
108
117
  "RosterStatsReport" => {
109
- args: %w[roster_id],
118
+ args: %w[roster_id location_id],
110
119
  dates: :required,
111
120
  desc: "Wrestling stats per wrestler (takedowns, nearfall, etc.)",
112
121
  recommended: true
@@ -127,8 +136,10 @@ module Wiq
127
136
  recommended: true,
128
137
  notes: "One row per wrestler over the date range — totals across all " \
129
138
  "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.",
139
+ "check-in event use CheckInFeedReport. Always team-wide: the " \
140
+ "model ignores roster_id AND location_id args. For a " \
141
+ "location-scoped attendance report use CheckInReport " \
142
+ "--location <id> instead.",
132
143
  example: "wiq reports run CheckInSummaryReport --start 2026-05-01 --end 2026-05-31"
133
144
  },
134
145
  "CheckInFeedReport" => {
@@ -138,11 +149,12 @@ module Wiq
138
149
  recommended: true,
139
150
  notes: "Useful for \"who came to the club today and what registrations " \
140
151
  "did they have at check-in time.\" Larger payload than the " \
141
- "summary.",
152
+ "summary. Always team-wide: the model ignores roster_id AND " \
153
+ "location_id args.",
142
154
  example: "wiq reports run CheckInFeedReport --start 2026-05-01 --end 2026-05-31"
143
155
  },
144
156
  "PracticeAttendanceReport" => {
145
- args: %w[roster_id],
157
+ args: %w[roster_id location_id],
146
158
  dates: :required,
147
159
  desc: "Practice-event attendance roll-up across a date range",
148
160
  recommended: false,
@@ -153,7 +165,7 @@ module Wiq
153
165
  "use CheckInSummaryReport or CheckInFeedReport instead."
154
166
  },
155
167
  "LastPracticeAttendedReport" => {
156
- args: %w[roster_id],
168
+ args: %w[roster_id location_id],
157
169
  dates: :optional,
158
170
  desc: "Days since last practice attended, per wrestler",
159
171
  recommended: true,
@@ -161,7 +173,7 @@ module Wiq
161
173
  "practice in a while — particularly for seasonal clubs."
162
174
  },
163
175
  "ChurnRiskReport" => {
164
- args: %w[roster_id days_threshold],
176
+ args: %w[roster_id location_id days_threshold],
165
177
  dates: :optional,
166
178
  desc: "Active recurring subscribers who haven't checked in within a window",
167
179
  recommended: true,
@@ -172,7 +184,7 @@ module Wiq
172
184
  example: "wiq reports run ChurnRiskReport --roster 0 --days-threshold 30"
173
185
  },
174
186
  "CheckInReport" => {
175
- args: %w[roster_id],
187
+ args: %w[roster_id location_id],
176
188
  dates: :required,
177
189
  desc: "Extended attendance — one row per check-in INCLUDING Q&A responses",
178
190
  notes: "UI labels this \"Attendance Extended (with questions).\" Same " \
@@ -327,6 +339,14 @@ module Wiq
327
339
  Common args (only those documented in TYPES are honored
328
340
  server-side):
329
341
  --roster <id> roster_id (0 = all rosters)
342
+ --location <id> location_id — scope the wrestler set to
343
+ one location (wrestlers on ANY roster at
344
+ that location, deduped to one row each).
345
+ Precedence: a specific --roster <id> (> 0)
346
+ WINS over --location; pass --location
347
+ alone (or with --roster 0) to get
348
+ location scoping. Discover ids via
349
+ `wiq locations list`.
330
350
  --paid-session <id> paid_session_id (0 = all, UsawExport only)
331
351
  --event <id> event_id (EventStatsReport)
332
352
  --fundraiser <id> fundraiser_id (Fundraiser* reports)
@@ -341,10 +361,31 @@ module Wiq
341
361
  cap, default 5-minute timeout. Pass --no-wait to return the
342
362
  report row immediately after submission (status=queued or
343
363
  processing) and poll later with `wiq reports show <id> --wait`.
364
+
365
+ Result shape (vrow default / --v1):
366
+ The CLI requests the row/CSV shape (version "vrow") by default —
367
+ `result.rows.objects` with the first row being the headers, the
368
+ exact layout behind the "Download" buttons in the WIQ web UI.
369
+ Most reports emit only this shape and ignore the version.
370
+
371
+ For RosterReport, vrow is the ONLY shape carrying the "Added to
372
+ roster at" column (roster_memberships.created_at — when a
373
+ wrestler landed on that roster, distinct from their registration
374
+ date). That column is populated only for a specific roster: pass
375
+ --roster <id> with id > 0, NOT --roster 0 ("all wrestlers"),
376
+ since a wrestler can sit on multiple rosters.
377
+
378
+ Pass --v1 for the legacy structured-JSON shape. Only
379
+ RosterReport, UsawReport, and PaidSessionAccountingReport differ
380
+ under it (fuller per-wrestler objects); for those reports v1
381
+ drops the "Added to roster at" column.
344
382
  DESC
345
383
  method_option :start, type: :string, desc: "YYYY-MM-DD"
346
384
  method_option :end, type: :string, desc: "YYYY-MM-DD"
347
385
  method_option :roster, type: :numeric, desc: "args.roster_id (0 = all rosters)"
386
+ method_option :location, type: :numeric,
387
+ desc: "args.location_id — scope wrestlers to one location " \
388
+ "(a specific --roster <id> > 0 takes precedence)"
348
389
  method_option :paid_session, type: :numeric, desc: "args.paid_session_id"
349
390
  method_option :event, type: :numeric, desc: "args.event_id"
350
391
  method_option :fundraiser, type: :numeric, desc: "args.fundraiser_id"
@@ -358,6 +399,11 @@ module Wiq
358
399
  desc: "args.days_threshold (ChurnRiskReport)"
359
400
  method_option :name, type: :string, desc: "Display name (default: CLI <type> <range>)"
360
401
  method_option :season, type: :numeric, desc: "Resolve to paid_session_id via overlap with calendar year"
402
+ method_option :v1, type: :boolean, default: false,
403
+ desc: "Request the legacy v1 structured-JSON shape instead of the default " \
404
+ "row/CSV (vrow). Only RosterReport/UsawReport/PaidSessionAccountingReport " \
405
+ "differ; v1 returns fuller per-wrestler objects but drops RosterReport's " \
406
+ "\"Added to roster at\" column."
361
407
  method_option :wait, type: :boolean, default: true
362
408
  method_option :timeout, type: :numeric, default: 300
363
409
  map "run" => :run_report
@@ -367,7 +413,7 @@ module Wiq
367
413
  body = {
368
414
  report: {
369
415
  type: type,
370
- version: "v1",
416
+ version: report_version,
371
417
  name: options[:name] || default_name(type),
372
418
  start_at: options[:start],
373
419
  end_at: options[:end],
@@ -469,6 +515,18 @@ module Wiq
469
515
  end
470
516
 
471
517
  no_commands do
518
+ # The API recognizes two report versions: "vrow" (the row/CSV shape
519
+ # behind the web Download buttons) and "v1" (legacy structured JSON).
520
+ # Only RosterReport/UsawReport/PaidSessionAccountingReport branch on
521
+ # it; every other report emits rows regardless. vrow is the de facto
522
+ # standard and is the only shape carrying RosterReport's "Added to
523
+ # roster at" column, so the CLI defaults to it for a uniform surface.
524
+ # --v1 is an escape hatch to the fuller structured objects on those
525
+ # three reports.
526
+ def report_version
527
+ options[:v1] ? "v1" : "vrow"
528
+ end
529
+
472
530
  def build_args(type)
473
531
  if options[:season]
474
532
  resolver = Wiq::SeasonResolver.new(client)
@@ -488,6 +546,7 @@ module Wiq
488
546
 
489
547
  args = {}
490
548
  args["roster_id"] = options[:roster] if options[:roster] || options[:roster] == 0
549
+ args["location_id"] = options[:location] if options[:location]
491
550
  args["paid_session_id"] = options[:paid_session] if options[:paid_session] || options[:paid_session] == 0
492
551
  args["event_id"] = options[:event] if options[:event]
493
552
  args["fundraiser_id"] = options[:fundraiser] if options[:fundraiser]
@@ -17,12 +17,19 @@ module Wiq
17
17
  exact name. Some teams tag rosters with
18
18
  conventions like "2025-26".
19
19
 
20
+ --location <id> filters server-side (Ransack q[location_id_eq]) to
21
+ rosters stamped with that structured location. Discover ids via
22
+ `wiq locations list`. Rosters with no location are excluded when
23
+ the filter is on. Each roster row embeds its location object (or
24
+ null) so you can also group client-side without the filter.
25
+
20
26
  Each roster row embeds roster_syncers and taggings, which is what
21
27
  --season uses to filter without needing extra calls.
22
28
  DESC
23
29
  method_option :season, type: :numeric,
24
30
  desc: "Filter to rosters whose syncers point at paid sessions overlapping this year"
25
31
  method_option :season_tag, type: :string, desc: "Filter to rosters carrying this tag"
32
+ method_option :location, type: :numeric, desc: "Filter to rosters at one location id"
26
33
  method_option :archived, type: :boolean, desc: "Show only archived (true) or active (false)"
27
34
  method_option :all, type: :boolean, default: false
28
35
  def list
@@ -30,6 +37,7 @@ module Wiq
30
37
  unless options[:archived].nil?
31
38
  params["q[archived_eq]"] = options[:archived]
32
39
  end
40
+ params["q[location_id_eq]"] = options[:location] if options[:location]
33
41
 
34
42
  records, total = fetch_index("/api/v1/rosters", params, key: "rosters")
35
43
 
@@ -20,6 +20,10 @@ module Wiq
20
20
  --first-name q[first_name_cont]
21
21
  --last-name q[last_name_cont]
22
22
  --roster <id> q[rosters_id_eq] — filter to one roster
23
+ --location <id> location_id (dedicated param, not Ransack) —
24
+ wrestlers on ANY roster at that location.
25
+ Composes with the other filters. Discover
26
+ ids via `wiq locations list`.
23
27
  --weight-class q[weight_class_numeric_eq] — exact numeric
24
28
  --academic-class q[academic_class_eq] (senior, junior, …)
25
29
  --age q[age_eq]
@@ -43,6 +47,8 @@ module Wiq
43
47
  method_option :first_name, type: :string, desc: "First name (contains)"
44
48
  method_option :last_name, type: :string, desc: "Last name (contains)"
45
49
  method_option :roster, type: :numeric, desc: "Filter to a single roster id"
50
+ method_option :location, type: :numeric,
51
+ desc: "Filter to wrestlers on any roster at this location id"
46
52
  method_option :weight_class, type: :string,
47
53
  desc: "Weight class (exact numeric, e.g. 132)"
48
54
  method_option :academic_class, type: :string,
@@ -107,6 +113,7 @@ module Wiq
107
113
  params["q[first_name_cont]"] = options[:first_name] if options[:first_name]
108
114
  params["q[last_name_cont]"] = options[:last_name] if options[:last_name]
109
115
  params["q[rosters_id_eq]"] = options[:roster] if options[:roster]
116
+ params["location_id"] = options[:location] if options[:location]
110
117
  params["q[weight_class_numeric_eq]"] = options[:weight_class] if options[:weight_class]
111
118
  params["q[academic_class_eq]"] = options[:academic_class] if options[:academic_class]
112
119
  params["q[age_eq]"] = options[:age] if options[:age]
@@ -130,6 +137,7 @@ module Wiq
130
137
  bits = []
131
138
  bits << "matching #{options[:query].inspect}" if options[:query]
132
139
  bits << "in roster #{options[:roster]}" if options[:roster]
140
+ bits << "at location #{options[:location]}" if options[:location]
133
141
  bits << "weight class #{options[:weight_class]}" if options[:weight_class]
134
142
  bits << "profile_type=#{options[:profile_type]}" if profile_type_filter? && options[:profile_type] != "teammate"
135
143
  bits.empty? ? "" : " (#{bits.join(", ")})"
data/lib/wiq/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wiq
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/wiq.rb CHANGED
@@ -20,6 +20,7 @@ require "wiq/commands/registrations"
20
20
  require "wiq/commands/metrics"
21
21
  require "wiq/commands/events"
22
22
  require "wiq/commands/rosters"
23
+ require "wiq/commands/locations"
23
24
  require "wiq/commands/prospects"
24
25
  require "wiq/commands/prospect_families"
25
26
  require "wiq/commands/workflows"
@@ -1,6 +1,6 @@
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, reports, USAW/AAU memberships, fundraising, or online store orders. The `wiq` CLI provides read-only access to /api/v1 via personal access tokens. 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?", or anything that maps to a wrestling club's admin workflows.
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, reports, USAW/AAU memberships, fundraising, online store orders, or per-location/site breakdowns for multi-gym clubs. The `wiq` CLI provides read-only access to /api/v1 via personal access tokens. 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
@@ -172,6 +172,11 @@ Key reports an agent should know by heart:
172
172
  within N days (7, 14, 30, 60, 90). Requires `--days-threshold`.
173
173
  - **`RosterReport`** — Roster snapshot with optional custom columns via
174
174
  `--append-properties <q_ids>`. Discover ids via `wiq registrations questions`.
175
+ The default (row/CSV) shape carries the **"Added to roster at"** column
176
+ (`roster_memberships.created_at` — when a wrestler landed on that roster,
177
+ NOT their registration date). Requires a specific `--roster <id>` (id > 0);
178
+ it's blank for `--roster 0`. `--v1` returns fuller per-wrestler objects but
179
+ drops that column.
175
180
  - **`LastPracticeAttendedReport`** — "Who hasn't been to practice in a
176
181
  while?"
177
182
  - **`PaidSessionAccountingReport`** (admin_only) — Line-item charges for
@@ -194,6 +199,18 @@ wiq reports run <Type> --start <date> --end <date> [args]
194
199
  processing → ready` (terminal) | `failed` (terminal). `result` is the
195
200
  type-specific jsonb payload.
196
201
 
202
+ **Result shape — vrow default (`--v1` escape hatch).** The CLI requests
203
+ the row/CSV shape (`version: "vrow"`) by default: `result.rows.objects`
204
+ with the first row being the header — identical to the web "Download"
205
+ buttons, and a uniform shape across every report type. Most reports emit
206
+ only this shape and ignore the version. Only `RosterReport`, `UsawReport`,
207
+ and `PaidSessionAccountingReport` also support a legacy v1 shape
208
+ (structured JSON objects) via `--v1`, which returns fuller per-wrestler
209
+ data but drops RosterReport's **"Added to roster at"** column. If a user
210
+ asks when a wrestler joined a roster (e.g. roster-join → first-practice
211
+ latency), `wiq reports run RosterReport --roster <id>` is the answer — the
212
+ join date is in the default output.
213
+
197
214
  ## Common gotchas
198
215
 
199
216
  - **`roster_id=0` = "all rosters"** in report args. UI convention; if
@@ -206,9 +223,14 @@ type-specific jsonb payload.
206
223
  resolves to paid_session ids whose date range overlaps the calendar
207
224
  year, then filters client-side. There's no first-class Season entity
208
225
  in WIQ.
209
- - **`Event.location` is free-text and not filterable.** No `--site` or
210
- `--location` flag exists yet. A structured location concept is on the
211
- WIQ roadmap.
226
+ - **Legacy free-text `Event.location` vs structured locations.** Old
227
+ events carry a free-text `location` string; the serialized `location`
228
+ field is the display form (structured Location name/address when
229
+ `location_id` is set, else the legacy text). `--location` filters only
230
+ match the structured `location_id` — events, rosters, and paid sessions
231
+ with NO location set are excluded from filtered listings, so an empty
232
+ filtered result doesn't mean "nothing happened", it may mean "nothing
233
+ is stamped with that location yet."
212
234
  - **Index responses are wrapped.** Every `/api/v1` index returns
213
235
  `{"<resource>": [...]}` — the CLI unwraps internally, but if you ever
214
236
  hit the API directly remember to unwrap.
@@ -321,13 +343,58 @@ For exhaustive exports go through `wiq reports run RosterReport`
321
343
  `profile_type=teammate` (matching the WIQ web UI default); pass
322
344
  `--profile-type alumnus|guest|all` to widen.
323
345
 
346
+ ## Locations (multi-site clubs)
347
+
348
+ WIQ has a structured Location model (name + street address, per team).
349
+ Multi-gym clubs stamp rosters, events, and paid sessions with a
350
+ location; single-site clubs usually have none, and every `--location`
351
+ flag is simply irrelevant for them.
352
+
353
+ Discover ids first — this is the anchor for everything below:
354
+
355
+ ```bash
356
+ wiq locations list # id, name, address, archived
357
+ wiq locations list --include-archived
358
+ ```
359
+
360
+ Then scope any of these surfaces:
361
+
362
+ | Command | Flag | Semantics |
363
+ | --- | --- | --- |
364
+ | `wiq rosters list` | `--location <id>` | Rosters stamped with that location (`q[location_id_eq]`) |
365
+ | `wiq events list` | `--location <id> [<id>...]` | Events at those locations (repeatable; unset-location events excluded) |
366
+ | `wiq paid_sessions list` | `--location <id>` | Sessions stamped with that location |
367
+ | `wiq wrestlers list` | `--location <id>` | Wrestlers on ANY roster at that location; composes with other filters |
368
+ | `wiq metrics show <name>` | `--location <id>` | Per-location finance metrics (the payment-dashboard filter) |
369
+ | `wiq reports run <Type>` | `--location <id>` | Scopes the wrestler set for the 13 location-aware report types |
370
+
371
+ Three gotchas an agent must know:
372
+
373
+ 1. **Reports precedence:** a specific `--roster <id>` (> 0) WINS over
374
+ `--location` in report args. Pass `--location` alone (or with
375
+ `--roster 0`) to get location scoping. A wrestler on multiple rosters
376
+ at the location collapses to one row; RosterReport's "Added to roster
377
+ at" becomes their EARLIEST membership across that location's rosters.
378
+ `CheckInSummaryReport` / `CheckInFeedReport` ignore both args
379
+ entirely (always team-wide) — use `CheckInReport --location <id>` for
380
+ location-scoped attendance.
381
+ 2. **Metrics fail silent, not loud:** an unknown or foreign `--location`
382
+ id on `wiq metrics show` silently falls back to ALL locations —
383
+ team-wide numbers, no error. Verify the id against
384
+ `wiq locations list` before quoting per-site revenue to the user.
385
+ 3. **Nothing is auto-stamped retroactively.** Filters only match records
386
+ whose `location_id` is set. Empty filtered results on a club that
387
+ just adopted locations usually mean unstamped data, not zero
388
+ activity. Rosters/events/paid_sessions embed their `location` object
389
+ (or null) in list payloads, so you can check coverage cheaply.
390
+
324
391
  ## What's NOT available (yet)
325
392
 
326
393
  The CLI is read-only by design except for report submission. You
327
394
  cannot via this CLI:
328
395
 
329
396
  - Create/edit prospects, families, notes, check-ins, events, paid
330
- sessions, rosters, or any other resource
397
+ sessions, rosters, locations, or any other resource
331
398
  - Mint, list, or revoke PATs (use the web UI at
332
399
  `<host>/settings/personal_access_tokens`)
333
400
  - Mark attendance, advance prospect stages, log contact notes
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wiq-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - WrestlingIQ
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-22 00:00:00.000000000 Z
11
+ date: 2026-07-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -90,6 +90,7 @@ files:
90
90
  - lib/wiq/commands/check_ins.rb
91
91
  - lib/wiq/commands/doctor.rb
92
92
  - lib/wiq/commands/events.rb
93
+ - lib/wiq/commands/locations.rb
93
94
  - lib/wiq/commands/metrics.rb
94
95
  - lib/wiq/commands/paid_sessions.rb
95
96
  - lib/wiq/commands/prospect_families.rb