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,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Wiq
|
|
6
|
+
# Walks Thor's command registry and produces a stable JSON shape for agents
|
|
7
|
+
# to introspect the CLI surface in one call.
|
|
8
|
+
#
|
|
9
|
+
# Schema (nested):
|
|
10
|
+
# {
|
|
11
|
+
# "name": "wiq",
|
|
12
|
+
# "version": "0.1.0",
|
|
13
|
+
# "global_options": [ ... ],
|
|
14
|
+
# "top_level_commands": [ ... ],
|
|
15
|
+
# "groups": [ { "name": "...", "description": "...", "commands": [ ... ] } ]
|
|
16
|
+
# }
|
|
17
|
+
module Introspection
|
|
18
|
+
# Thor auto-generates these on every Thor::Group subclass; they're not
|
|
19
|
+
# useful for agents and would just bloat the dump.
|
|
20
|
+
INTERNAL_COMMANDS = %w[help tree].freeze
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def dump_tree(root: Wiq::CLI)
|
|
25
|
+
{
|
|
26
|
+
"name" => "wiq",
|
|
27
|
+
"version" => Wiq::VERSION,
|
|
28
|
+
"global_options" => dump_options(Wiq::Commands::Base.class_options),
|
|
29
|
+
"top_level_commands" => dump_commands(root.commands, klass: root),
|
|
30
|
+
"groups" => root.subcommand_classes.sort.map do |name, klass|
|
|
31
|
+
dump_group(name, klass, root)
|
|
32
|
+
end
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def dump_group(name, klass, root)
|
|
37
|
+
{
|
|
38
|
+
"name" => name,
|
|
39
|
+
"description" => root.commands[name]&.description,
|
|
40
|
+
"commands" => dump_commands(klass.commands, klass: klass)
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def dump_commands(commands, klass:)
|
|
45
|
+
aliases = method_to_alias(klass)
|
|
46
|
+
commands
|
|
47
|
+
.reject { |k, _| INTERNAL_COMMANDS.include?(k) || subcommand_name?(klass, k) }
|
|
48
|
+
.map { |k, cmd| dump_command(cmd, display_name: aliases[k] || k) }
|
|
49
|
+
.sort_by { |row| row["name"] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def dump_command(cmd, display_name:)
|
|
53
|
+
{
|
|
54
|
+
"name" => display_name,
|
|
55
|
+
"description" => cmd.description,
|
|
56
|
+
"long_description" => cmd.long_description,
|
|
57
|
+
"usage" => cmd.usage,
|
|
58
|
+
"options" => dump_options(cmd.options)
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Thor's `map` declares user-facing command aliases. `map "run" => :run_report`
|
|
63
|
+
# means when a user types `wiq reports run`, Thor dispatches to method
|
|
64
|
+
# :run_report. Internally Thor stores the command under "run_report" — the
|
|
65
|
+
# method name — but agents need to see "run" (the name they type).
|
|
66
|
+
# Build a reverse map: method_name (String) => alias_name (String).
|
|
67
|
+
#
|
|
68
|
+
# Flag-style aliases (--version, -v) are also registered via `map` for
|
|
69
|
+
# convenience but they're not command names — skip them.
|
|
70
|
+
def method_to_alias(klass)
|
|
71
|
+
return {} unless klass.respond_to?(:map)
|
|
72
|
+
|
|
73
|
+
klass.map.each_with_object({}) do |(alias_name, method_name), h|
|
|
74
|
+
alias_str = alias_name.to_s
|
|
75
|
+
next if alias_str.start_with?("-")
|
|
76
|
+
|
|
77
|
+
h[method_name.to_s] = alias_str
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def dump_options(options)
|
|
82
|
+
options.sort.map do |name, opt|
|
|
83
|
+
row = {
|
|
84
|
+
"name" => name.to_s,
|
|
85
|
+
"type" => opt.type.to_s,
|
|
86
|
+
"required" => opt.required?,
|
|
87
|
+
"description" => opt.description
|
|
88
|
+
}
|
|
89
|
+
row["default"] = opt.default unless opt.default.nil?
|
|
90
|
+
row["enum"] = opt.enum if opt.enum
|
|
91
|
+
row
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# When a class registers a `subcommand "foo", FooClass`, Thor also adds
|
|
96
|
+
# a placeholder command named "foo" to the parent's commands hash. Skip
|
|
97
|
+
# those when listing the parent's own commands — they're rendered as
|
|
98
|
+
# groups, not commands.
|
|
99
|
+
def subcommand_name?(klass, command_name)
|
|
100
|
+
return false unless klass.respond_to?(:subcommand_classes)
|
|
101
|
+
|
|
102
|
+
klass.subcommand_classes.key?(command_name)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/wiq/output.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Wiq
|
|
6
|
+
# Three output modes:
|
|
7
|
+
# --json full envelope (ok/data/summary/meta) for agents that want structure
|
|
8
|
+
# --agent bare JSON of `data` for headless scripts
|
|
9
|
+
# default pretty JSON to a TTY, or --agent equivalent when piped
|
|
10
|
+
module Output
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def render(data, summary: nil, breadcrumbs: [], meta: {}, options: {})
|
|
14
|
+
mode = pick_mode(options)
|
|
15
|
+
case mode
|
|
16
|
+
when :json
|
|
17
|
+
puts JSON.pretty_generate(envelope(data, summary: summary, breadcrumbs: breadcrumbs, meta: meta))
|
|
18
|
+
when :agent
|
|
19
|
+
puts JSON.generate(data)
|
|
20
|
+
when :pretty
|
|
21
|
+
puts JSON.pretty_generate(data)
|
|
22
|
+
puts "" unless data.nil? || (data.respond_to?(:empty?) && data.empty?)
|
|
23
|
+
puts "→ #{summary}" if summary
|
|
24
|
+
if meta && !meta.empty?
|
|
25
|
+
meta_line = meta.map { |k, v| "#{k}=#{v}" }.join(" ")
|
|
26
|
+
puts " #{meta_line}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render_error(err, options: {})
|
|
32
|
+
mode = pick_mode(options)
|
|
33
|
+
payload = {
|
|
34
|
+
"ok" => false,
|
|
35
|
+
"error" => err.message,
|
|
36
|
+
"code" => err.respond_to?(:code) ? err.code : "error",
|
|
37
|
+
"hint" => (err.respond_to?(:hint) ? err.hint : nil),
|
|
38
|
+
"details" => (err.respond_to?(:details) ? err.details : nil)
|
|
39
|
+
}.compact
|
|
40
|
+
|
|
41
|
+
case mode
|
|
42
|
+
when :json, :agent
|
|
43
|
+
warn JSON.generate(payload)
|
|
44
|
+
else
|
|
45
|
+
warn "✖ #{payload["error"]} [#{payload["code"]}]"
|
|
46
|
+
warn " hint: #{payload["hint"]}" if payload["hint"]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def pick_mode(options)
|
|
51
|
+
options ||= {}
|
|
52
|
+
return :agent if options[:agent] || options["agent"]
|
|
53
|
+
return :json if options[:json] || options["json"]
|
|
54
|
+
$stdout.tty? ? :pretty : :agent
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def envelope(data, summary:, breadcrumbs:, meta:)
|
|
58
|
+
{
|
|
59
|
+
"ok" => true,
|
|
60
|
+
"data" => data,
|
|
61
|
+
"summary" => summary,
|
|
62
|
+
"breadcrumbs" => breadcrumbs,
|
|
63
|
+
"meta" => meta
|
|
64
|
+
}.compact
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Wiq
|
|
6
|
+
# Parses the WIQ pagination headers:
|
|
7
|
+
# Link: <https://host/api/v1/foo?page=2>; rel="next", <...>; rel="last"
|
|
8
|
+
# TotalCount: 123
|
|
9
|
+
module Pagination
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Returns { "next" => "...url...", "prev" => "...", "first" => "...", "last" => "..." }.
|
|
13
|
+
def parse_link(header)
|
|
14
|
+
return {} if header.nil? || header.empty?
|
|
15
|
+
|
|
16
|
+
header.split(",").each_with_object({}) do |part, acc|
|
|
17
|
+
url_part, *params = part.split(";").map(&:strip)
|
|
18
|
+
next unless url_part&.start_with?("<") && url_part.end_with?(">")
|
|
19
|
+
|
|
20
|
+
url = url_part[1..-2]
|
|
21
|
+
rel = params.find { |p| p.start_with?("rel=") }
|
|
22
|
+
next unless rel
|
|
23
|
+
|
|
24
|
+
rel_value = rel.sub(/\Arel="?/, "").sub(/"?\z/, "")
|
|
25
|
+
acc[rel_value] = url
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def next_url(link_header)
|
|
30
|
+
parse_link(link_header)["next"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def total_count(headers)
|
|
34
|
+
# Header is literally `TotalCount` (case-insensitive via Faraday).
|
|
35
|
+
v = headers["TotalCount"] || headers["totalcount"] || headers["Totalcount"]
|
|
36
|
+
v && Integer(v)
|
|
37
|
+
rescue ArgumentError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wiq
|
|
4
|
+
# Translates `--season <year>` into a set of PaidSession ids. No first-class
|
|
5
|
+
# Season exists in WIQ; we approximate by finding paid sessions whose
|
|
6
|
+
# [start_at, end_at] window overlaps the calendar year.
|
|
7
|
+
class SeasonResolver
|
|
8
|
+
def initialize(client)
|
|
9
|
+
@client = client
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Returns the array of paid_session hashes that overlap `year`.
|
|
13
|
+
def paid_sessions_for(year)
|
|
14
|
+
year = Integer(year)
|
|
15
|
+
start_of_year = "#{year}-01-01"
|
|
16
|
+
end_of_year = "#{year}-12-31"
|
|
17
|
+
|
|
18
|
+
sessions, = @client.collect_all(
|
|
19
|
+
"/api/v1/paid_sessions",
|
|
20
|
+
{
|
|
21
|
+
"q[start_at_lteq]" => end_of_year,
|
|
22
|
+
"q[end_at_gteq]" => start_of_year,
|
|
23
|
+
"per_page" => 100
|
|
24
|
+
},
|
|
25
|
+
key: "paid_sessions"
|
|
26
|
+
)
|
|
27
|
+
sessions
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def paid_session_ids_for(year)
|
|
31
|
+
paid_sessions_for(year).map { |ps| ps["id"] }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Filter an array of rosters (as returned by /api/v1/rosters) to those
|
|
35
|
+
# whose roster_syncers point at any paid session id in `ids`.
|
|
36
|
+
def filter_rosters_by_ids(rosters, ids)
|
|
37
|
+
id_set = ids.to_set
|
|
38
|
+
rosters.select do |roster|
|
|
39
|
+
syncers = roster["roster_syncers"] || []
|
|
40
|
+
syncers.any? { |rs| id_set.include?(rs["paid_session_id"]) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
require "set"
|
data/lib/wiq/version.rb
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wiq
|
|
4
|
+
# Named multi-step recipes mapping a human question to a CLI command
|
|
5
|
+
# sequence. Discovery via `wiq workflows list` / `wiq workflows show NAME`.
|
|
6
|
+
#
|
|
7
|
+
# Per-entry schema:
|
|
8
|
+
# name kebab-case slug (the lookup key — also keys the hash)
|
|
9
|
+
# category one of CATEGORIES (used to group `list` output)
|
|
10
|
+
# question natural-language framing — what an agent matches against
|
|
11
|
+
# parameters structured list of placeholders:
|
|
12
|
+
# [{ name:, type:, required:, default?:, enum?:, description?: }]
|
|
13
|
+
# type is one of "date", "integer", "string"
|
|
14
|
+
# recipe ordered list of command strings using two substitution
|
|
15
|
+
# conventions:
|
|
16
|
+
# <name> → required substitution (must match a
|
|
17
|
+
# declared required parameter)
|
|
18
|
+
# [--flag <name>] → optional bracket; drop the whole
|
|
19
|
+
# expression if the parameter is unset
|
|
20
|
+
# admin_only true when the workflow depends on at least one admin-only
|
|
21
|
+
# report or admin-only metric (non-admin coach PATs 403)
|
|
22
|
+
# notes agent-facing guidance (when relevant)
|
|
23
|
+
module Workflows
|
|
24
|
+
CATEGORIES = %w[attendance leads roster finance memberships fundraising store].freeze
|
|
25
|
+
|
|
26
|
+
ALL = {
|
|
27
|
+
# ── Attendance / check-ins ──────────────────────────────────
|
|
28
|
+
"attendance-last-month" => {
|
|
29
|
+
name: "attendance-last-month",
|
|
30
|
+
category: "attendance",
|
|
31
|
+
question: "How many practices did each wrestler attend last month?",
|
|
32
|
+
parameters: [
|
|
33
|
+
{ name: "start_date", type: "date", required: true },
|
|
34
|
+
{ name: "end_date", type: "date", required: true },
|
|
35
|
+
{ name: "roster_id", type: "integer", required: false, default: 0,
|
|
36
|
+
description: "0 = all rosters" }
|
|
37
|
+
],
|
|
38
|
+
recipe: [
|
|
39
|
+
"wiq reports run CheckInSummaryReport --start <start_date> --end <end_date> [--roster <roster_id>]"
|
|
40
|
+
],
|
|
41
|
+
notes: "Useful to build a leaderboard of check-ins at your wrestling club."
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
"who-came-today" => {
|
|
45
|
+
name: "who-came-today",
|
|
46
|
+
category: "attendance",
|
|
47
|
+
question: "Who showed up at the club today?",
|
|
48
|
+
parameters: [
|
|
49
|
+
{ name: "today_date", type: "date", required: true,
|
|
50
|
+
description: "Today's date in the team's timezone (YYYY-MM-DD)" }
|
|
51
|
+
],
|
|
52
|
+
recipe: [
|
|
53
|
+
"wiq reports run CheckInFeedReport --start <today_date> --end <today_date>"
|
|
54
|
+
],
|
|
55
|
+
notes: "The feed includes registration type per check-in, so the agent " \
|
|
56
|
+
"can filter the result client-side to answer follow-ups like " \
|
|
57
|
+
"'who used a trial pass today' or 'who came without an active " \
|
|
58
|
+
"subscription'."
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
"churn-risk" => {
|
|
62
|
+
name: "churn-risk",
|
|
63
|
+
category: "attendance",
|
|
64
|
+
question: "Which subscribers are at risk of canceling because they haven't checked in lately?",
|
|
65
|
+
parameters: [
|
|
66
|
+
{ name: "days_threshold", type: "integer", required: true,
|
|
67
|
+
enum: [7, 14, 30, 60, 90],
|
|
68
|
+
description: "Window of inactivity, in days. Any value outside the enum falls back to 30." }
|
|
69
|
+
],
|
|
70
|
+
recipe: [
|
|
71
|
+
"wiq reports run ChurnRiskReport --roster 0 --days-threshold <days_threshold>"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
# ── Leads pipeline (Prospect is the internal name) ──────────
|
|
76
|
+
"overdue-followups" => {
|
|
77
|
+
name: "overdue-followups",
|
|
78
|
+
category: "leads",
|
|
79
|
+
question: "Which lead families have a stale follow-up?",
|
|
80
|
+
parameters: [],
|
|
81
|
+
recipe: [
|
|
82
|
+
"wiq prospect_families list --attention needs_attention --sort oldest_followup"
|
|
83
|
+
],
|
|
84
|
+
notes: "WIQ calls these 'prospects' internally but 'leads' in the UI — " \
|
|
85
|
+
"both terms refer to the same thing."
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
# ── Roster ops ──────────────────────────────────────────────
|
|
89
|
+
"growth-across-seasons" => {
|
|
90
|
+
name: "growth-across-seasons",
|
|
91
|
+
category: "roster",
|
|
92
|
+
question: "How has roster size changed across seasons?",
|
|
93
|
+
parameters: [
|
|
94
|
+
{ name: "year_a", type: "integer", required: true,
|
|
95
|
+
description: "Earlier season year (e.g. 2025)" },
|
|
96
|
+
{ name: "year_b", type: "integer", required: true,
|
|
97
|
+
description: "Later season year (e.g. 2026)" }
|
|
98
|
+
],
|
|
99
|
+
recipe: [
|
|
100
|
+
"wiq paid_sessions list --season <year_a>",
|
|
101
|
+
"wiq rosters list --season <year_a>",
|
|
102
|
+
"wiq paid_sessions list --season <year_b>",
|
|
103
|
+
"wiq rosters list --season <year_b>"
|
|
104
|
+
],
|
|
105
|
+
notes: "Client-side compare — no first-class growth report exists. " \
|
|
106
|
+
"Clubs are typically seasonal, so the natural comparison is " \
|
|
107
|
+
"season-vs-season (e.g. 2025 season vs 2026 season). Before " \
|
|
108
|
+
"running, clarify whether the user wants a full calendar-year " \
|
|
109
|
+
"diff or a specific roster/registration vs another."
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
"roster-by-zip" => {
|
|
113
|
+
name: "roster-by-zip",
|
|
114
|
+
category: "roster",
|
|
115
|
+
question: "What zip codes does our roster cover?",
|
|
116
|
+
parameters: [
|
|
117
|
+
{ name: "address_question_id", type: "integer", required: true,
|
|
118
|
+
description: "RegAddressQuestion id, discoverable via " \
|
|
119
|
+
"`wiq registrations questions --visibility public`" },
|
|
120
|
+
{ name: "roster_id", type: "integer", required: false, default: 0,
|
|
121
|
+
description: "0 = all rosters; pass a specific id to scope" }
|
|
122
|
+
],
|
|
123
|
+
recipe: [
|
|
124
|
+
"wiq registrations questions --visibility public",
|
|
125
|
+
"wiq reports run RosterReport --roster <roster_id> --append-properties <address_question_id>"
|
|
126
|
+
],
|
|
127
|
+
notes: "Address (with zip) is captured via a registration question, " \
|
|
128
|
+
"not stored on the wrestler directly. RosterReport with " \
|
|
129
|
+
"--append-properties surfaces it as a column. Step 1 of the " \
|
|
130
|
+
"recipe discovers the question id if the agent doesn't know it."
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
# ── Finance (admin-only) ────────────────────────────────────
|
|
134
|
+
"revenue-snapshot" => {
|
|
135
|
+
name: "revenue-snapshot",
|
|
136
|
+
category: "finance",
|
|
137
|
+
question: "How are we doing financially over the last quarter?",
|
|
138
|
+
parameters: [],
|
|
139
|
+
recipe: [
|
|
140
|
+
"wiq metrics show net_volume --range 3m",
|
|
141
|
+
"wiq metrics show mrr --range 3m",
|
|
142
|
+
"wiq metrics show active_subscribers --range 3m"
|
|
143
|
+
],
|
|
144
|
+
admin_only: true,
|
|
145
|
+
notes: "Three calls because we deliberately didn't ship a `metrics " \
|
|
146
|
+
"dashboard` convenience command — the agent fans out and " \
|
|
147
|
+
"composes the answer. net_volume and mrr return integer cents."
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
"revenue-by-category" => {
|
|
151
|
+
name: "revenue-by-category",
|
|
152
|
+
category: "finance",
|
|
153
|
+
question: "How is revenue broken down across categories (e.g. Level 1 vs Level 2 subscriptions)?",
|
|
154
|
+
parameters: [
|
|
155
|
+
{ name: "range", type: "string", required: false, default: "3m",
|
|
156
|
+
enum: ["today", "7d", "4w", "3m", "12m", "year to date", "custom"],
|
|
157
|
+
description: "Time window for the breakdown." }
|
|
158
|
+
],
|
|
159
|
+
recipe: [
|
|
160
|
+
"wiq metrics show category_breakdown_net --range <range>"
|
|
161
|
+
],
|
|
162
|
+
admin_only: true,
|
|
163
|
+
notes: "Returns revenue grouped by category → subcategory → " \
|
|
164
|
+
"detail_category. Common asks: comparing subscription tiers " \
|
|
165
|
+
"(Level 1 vs Level 2), comparing registration revenue vs " \
|
|
166
|
+
"donation revenue, or seeing which revenue source is growing. " \
|
|
167
|
+
"Values are integer cents."
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
"paid-session-accounting" => {
|
|
171
|
+
name: "paid-session-accounting",
|
|
172
|
+
category: "finance",
|
|
173
|
+
question: "Give me a line-item breakdown for this registration session.",
|
|
174
|
+
parameters: [
|
|
175
|
+
{ name: "paid_session_id", type: "integer", required: true,
|
|
176
|
+
description: "Discoverable via `wiq paid_sessions list`" }
|
|
177
|
+
],
|
|
178
|
+
recipe: [
|
|
179
|
+
"wiq reports run PaidSessionAccountingReport --paid-session <paid_session_id>"
|
|
180
|
+
],
|
|
181
|
+
admin_only: true
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
"failed-payments-recent" => {
|
|
185
|
+
name: "failed-payments-recent",
|
|
186
|
+
category: "finance",
|
|
187
|
+
question: "Which families have outstanding failed payments (after filtering out failures that were later retried successfully)?",
|
|
188
|
+
parameters: [
|
|
189
|
+
{ name: "since_date", type: "date", required: true,
|
|
190
|
+
description: "Earliest created_at to scan from (YYYY-MM-DD)" }
|
|
191
|
+
],
|
|
192
|
+
recipe: [
|
|
193
|
+
"wiq charges list --status failed --since <since_date> --all"
|
|
194
|
+
],
|
|
195
|
+
admin_only: true,
|
|
196
|
+
notes: "CRITICAL: a failed charge alone is NOT actionable. " \
|
|
197
|
+
"Stripe/Justifi retry subscriptions automatically and " \
|
|
198
|
+
"customers retry registrations after card declines, so most " \
|
|
199
|
+
"failures resolve themselves. The cross-check is mandatory:\n\n" \
|
|
200
|
+
"After the step-1 list returns, for EACH failed charge:\n" \
|
|
201
|
+
" 1. Note its billing_profile_id, chargeable_id, " \
|
|
202
|
+
"chargeable_type, and created_at.\n" \
|
|
203
|
+
" 2. Run `wiq charges list --billing-profile <id> " \
|
|
204
|
+
"--status successful --since <failure_created_at> --all`.\n" \
|
|
205
|
+
" 3. Drop the failed charge if any returned successful " \
|
|
206
|
+
"charge has matching chargeable_id AND chargeable_type.\n\n" \
|
|
207
|
+
"What remains is the actionable set: failures with no later " \
|
|
208
|
+
"successful retry on the same item. Surface those to the " \
|
|
209
|
+
"user with family name + amount + chargeable description."
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
"family-payment-history" => {
|
|
213
|
+
name: "family-payment-history",
|
|
214
|
+
category: "finance",
|
|
215
|
+
question: "What has this family paid (or tried to pay) recently?",
|
|
216
|
+
parameters: [
|
|
217
|
+
{ name: "billing_profile_id", type: "integer", required: true,
|
|
218
|
+
description: "Discover via `wiq billing_profiles show <profile_id> --profile-type ParentProfile`" },
|
|
219
|
+
{ name: "since_date", type: "date", required: false, default: "90 days ago",
|
|
220
|
+
description: "Earliest created_at; default is roughly 90 days back when omitted client-side" }
|
|
221
|
+
],
|
|
222
|
+
recipe: [
|
|
223
|
+
"wiq charges list --billing-profile <billing_profile_id> [--since <since_date>] --all"
|
|
224
|
+
],
|
|
225
|
+
admin_only: true,
|
|
226
|
+
notes: "Returns the family's charge history — both successful and " \
|
|
227
|
+
"failed attempts, with the chargeable description (what was " \
|
|
228
|
+
"paid for) on each row. To go from a wrestler name to a " \
|
|
229
|
+
"billing_profile_id: `wiq wrestlers list --query \"Name\"`, " \
|
|
230
|
+
"then `wiq wrestlers show <id>` to find the parent profile " \
|
|
231
|
+
"id, then `wiq billing_profiles show <parent_id> " \
|
|
232
|
+
"--profile-type ParentProfile`."
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
"scholarship-audit" => {
|
|
236
|
+
name: "scholarship-audit",
|
|
237
|
+
category: "finance",
|
|
238
|
+
question: "Where is scholarship money going?",
|
|
239
|
+
parameters: [
|
|
240
|
+
{ name: "start_date", type: "date", required: true },
|
|
241
|
+
{ name: "end_date", type: "date", required: true }
|
|
242
|
+
],
|
|
243
|
+
recipe: [
|
|
244
|
+
"wiq reports run ScholarshipAuditReport --start <start_date> --end <end_date>"
|
|
245
|
+
],
|
|
246
|
+
admin_only: true,
|
|
247
|
+
notes: "Also requires team.payments_enabled — the UI hides the tab " \
|
|
248
|
+
"otherwise, but the API just returns empty data."
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
# ── Memberships (USAW / AAU) ────────────────────────────────
|
|
252
|
+
"usaw-renewal-batch" => {
|
|
253
|
+
name: "usaw-renewal-batch",
|
|
254
|
+
category: "memberships",
|
|
255
|
+
question: "Which wrestlers need their USAW renewed before our next event?",
|
|
256
|
+
parameters: [
|
|
257
|
+
{ name: "roster_id", type: "integer", required: false, default: 0,
|
|
258
|
+
description: "0 = all rosters" }
|
|
259
|
+
],
|
|
260
|
+
recipe: [
|
|
261
|
+
"wiq reports run UsawExpiredReport [--roster <roster_id>]"
|
|
262
|
+
],
|
|
263
|
+
notes: "Output is a bulk-purchase upload format usable directly in " \
|
|
264
|
+
"USAW's system."
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
"aau-renewal-batch" => {
|
|
268
|
+
name: "aau-renewal-batch",
|
|
269
|
+
category: "memberships",
|
|
270
|
+
question: "Which wrestlers need their AAU memberships renewed?",
|
|
271
|
+
parameters: [
|
|
272
|
+
{ name: "roster_id", type: "integer", required: false, default: 0,
|
|
273
|
+
description: "0 = all rosters" }
|
|
274
|
+
],
|
|
275
|
+
recipe: [
|
|
276
|
+
"wiq reports run AauExpiredReport [--roster <roster_id>]"
|
|
277
|
+
],
|
|
278
|
+
notes: "Output is a bulk-purchase upload format usable directly in " \
|
|
279
|
+
"AAU's system."
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
# ── Fundraising ─────────────────────────────────────────────
|
|
283
|
+
"fundraiser-status" => {
|
|
284
|
+
name: "fundraiser-status",
|
|
285
|
+
category: "fundraising",
|
|
286
|
+
question: "How is our active fundraiser doing?",
|
|
287
|
+
parameters: [
|
|
288
|
+
{ name: "fundraiser_id", type: "integer", required: true,
|
|
289
|
+
description: "Discoverable via `wiq fundraisers list` (not yet on the CLI surface — " \
|
|
290
|
+
"open the WIQ web UI for now)" }
|
|
291
|
+
],
|
|
292
|
+
recipe: [
|
|
293
|
+
"wiq reports run FundraiserSummaryReport --fundraiser <fundraiser_id>"
|
|
294
|
+
],
|
|
295
|
+
admin_only: true
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
# ── Online store ────────────────────────────────────────────
|
|
299
|
+
"store-orders" => {
|
|
300
|
+
name: "store-orders",
|
|
301
|
+
category: "store",
|
|
302
|
+
question: "What's been sold in our online store?",
|
|
303
|
+
parameters: [
|
|
304
|
+
{ name: "online_store_id", type: "integer", required: true,
|
|
305
|
+
description: "Discoverable via the WIQ web UI (not yet on the CLI surface)" }
|
|
306
|
+
],
|
|
307
|
+
recipe: [
|
|
308
|
+
"wiq reports run OnlineStoreSummaryReport --online-store <online_store_id>"
|
|
309
|
+
],
|
|
310
|
+
admin_only: true
|
|
311
|
+
}
|
|
312
|
+
}.freeze
|
|
313
|
+
end
|
|
314
|
+
end
|
data/lib/wiq.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "wiq/version"
|
|
4
|
+
require "wiq/errors"
|
|
5
|
+
require "wiq/credentials"
|
|
6
|
+
require "wiq/config"
|
|
7
|
+
require "wiq/output"
|
|
8
|
+
require "wiq/pagination"
|
|
9
|
+
require "wiq/client"
|
|
10
|
+
require "wiq/season_resolver"
|
|
11
|
+
require "wiq/introspection"
|
|
12
|
+
require "wiq/workflows"
|
|
13
|
+
require "wiq/commands/base"
|
|
14
|
+
require "wiq/commands/auth"
|
|
15
|
+
require "wiq/commands/doctor"
|
|
16
|
+
require "wiq/commands/check_ins"
|
|
17
|
+
require "wiq/commands/reports"
|
|
18
|
+
require "wiq/commands/paid_sessions"
|
|
19
|
+
require "wiq/commands/registrations"
|
|
20
|
+
require "wiq/commands/metrics"
|
|
21
|
+
require "wiq/commands/events"
|
|
22
|
+
require "wiq/commands/rosters"
|
|
23
|
+
require "wiq/commands/prospects"
|
|
24
|
+
require "wiq/commands/prospect_families"
|
|
25
|
+
require "wiq/commands/workflows"
|
|
26
|
+
require "wiq/commands/setup"
|
|
27
|
+
require "wiq/commands/wrestlers"
|
|
28
|
+
require "wiq/commands/charges"
|
|
29
|
+
require "wiq/commands/billing_profiles"
|
|
30
|
+
require "wiq/cli"
|