wiq-cli 0.4.0 → 0.6.1
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 +4 -4
- data/README.md +16 -5
- data/docs/deferred.md +12 -21
- data/docs/wiq_api_notes.md +41 -5
- data/lib/wiq/cli.rb +5 -2
- data/lib/wiq/client.rb +6 -1
- data/lib/wiq/commands/auth.rb +35 -11
- data/lib/wiq/commands/doctor.rb +9 -0
- data/lib/wiq/commands/payouts.rb +124 -0
- data/lib/wiq/commands/prospect_families.rb +226 -0
- data/lib/wiq/commands/prospects.rb +172 -9
- data/lib/wiq/commands/reports.rb +22 -2
- data/lib/wiq/commands/rosters.rb +6 -1
- data/lib/wiq/commands/setup.rb +3 -2
- data/lib/wiq/credentials.rb +5 -2
- data/lib/wiq/errors.rb +50 -4
- data/lib/wiq/version.rb +1 -1
- data/lib/wiq.rb +1 -0
- data/share/skills/wiq/SKILL.md +104 -17
- metadata +4 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec283a11b87c6ba8e2f8db5bf5a56f5e02fbf092aa3fb93fab54d08660553aa3
|
|
4
|
+
data.tar.gz: 1afd0ade5f5de6770c74acdd5d3fb0f2c68721f1537b0344a2ab6a401f32a400
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 93172776f6f73aac7093aa64b04bade0de00cc633f6b8e17b6a4c3a7898c3b8148fc98b9ccb108ab22df639b2e56c26b167e6f85ed22013cadd444f4d1fac574
|
|
7
|
+
data.tar.gz: 075e0ab62fb1098c04a0a7714825eef7ea0b8ad450e1d21703cbf2fe6c60b8d58f8ca2d2616ba6b8ead65ec9237db681737408476692aab9b0443847637d2ebc
|
data/README.md
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
> recommended for production-critical automation. Pin your gem version
|
|
8
8
|
> if you build anything load-bearing on top of it.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Command-line interface for [WrestlingIQ](https://www.wrestlingiq.com).
|
|
11
11
|
Designed to be driven by both humans and AI agents (Claude Code, Cursor,
|
|
12
|
-
etc.) via per-user personal access tokens (PATs).
|
|
12
|
+
etc.) via per-user personal access tokens (PATs). Every token can read;
|
|
13
|
+
tokens minted with a write scope can also work the leads pipeline.
|
|
13
14
|
|
|
14
15
|
## Getting started
|
|
15
16
|
|
|
@@ -39,6 +40,13 @@ coach, your CLI will be able to run admin reports; if you're not, it
|
|
|
39
40
|
won't. Either way, the token can be revoked anytime from the same
|
|
40
41
|
page.
|
|
41
42
|
|
|
43
|
+
Tokens are read-only unless you tick a write permission when minting
|
|
44
|
+
one. Today that's **Create and edit leads** (`prospects:write`) and
|
|
45
|
+
**Create reports** (`reports:write`). A team admin has to enable each
|
|
46
|
+
capability for the club first under **Settings → API Access**; the
|
|
47
|
+
token picker only offers what the team allows. Scopes are fixed for the
|
|
48
|
+
life of a token — to change them, revoke it and mint a new one.
|
|
49
|
+
|
|
42
50
|
### 3. Log in
|
|
43
51
|
|
|
44
52
|
```bash
|
|
@@ -81,6 +89,7 @@ Now you can ask Claude things like:
|
|
|
81
89
|
```bash
|
|
82
90
|
wiq rosters list # Browse rosters
|
|
83
91
|
wiq prospects summary # Pipeline dashboard
|
|
92
|
+
wiq prospects advance 123 trial_scheduled # Move a lead forward (needs prospects:write)
|
|
84
93
|
wiq events list --start 2026-05-01 --end 2026-05-31 # Calendar
|
|
85
94
|
wiq workflows list # Curated multi-step recipes
|
|
86
95
|
wiq reports types # 36 report types w/ recommendations
|
|
@@ -135,9 +144,11 @@ output (the CLI never logs your token — paste the full JSON safely).
|
|
|
135
144
|
|
|
136
145
|
## Status
|
|
137
146
|
|
|
138
|
-
v0.1
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
v0.6.1 — reads across every group, plus report submission and a
|
|
148
|
+
scoped write surface for the prospects pipeline (`prospect_families
|
|
149
|
+
create/update/note`, `prospects create/update/advance`) gated by the
|
|
150
|
+
`prospects:write` token scope. Other write commands deferred — see
|
|
151
|
+
`docs/deferred.md`.
|
|
141
152
|
|
|
142
153
|
## License
|
|
143
154
|
|
data/docs/deferred.md
CHANGED
|
@@ -81,17 +81,14 @@ Last updated: 2026-05-12 (after L1 ships).
|
|
|
81
81
|
- **`wiq url parse <url>`** — extract team/roster/event IDs from WIQ web
|
|
82
82
|
URLs. Initial-plan item; useful for agents pasted URLs by humans.
|
|
83
83
|
- **`wiq paid_sessions create/update`**, **`wiq rosters create/update`**,
|
|
84
|
-
etc. — every write path
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
`
|
|
91
|
-
|
|
92
|
-
pipeline edits happen in the drawer UI today, and exposing
|
|
93
|
-
stage-transition writes without first watching agents use them is
|
|
94
|
-
asking for trouble.
|
|
84
|
+
etc. — every other write path. Each needs a server-side capability in
|
|
85
|
+
`ApiCapability::REGISTRY` first (writes fail closed without one), then
|
|
86
|
+
a CLI command. Cherry-pick as customers ask.
|
|
87
|
+
- **Prospect deletes.** Shipped in v0.6.0: `prospect_families
|
|
88
|
+
create/update/note` and `prospects create/update/advance` behind the
|
|
89
|
+
`prospects:write` scope. `DELETE /prospect_families/:id` and
|
|
90
|
+
`DELETE /prospects/:id` remain out of reach — destroy actions are in
|
|
91
|
+
no API capability server-side, by design.
|
|
95
92
|
|
|
96
93
|
## Packaging / distribution
|
|
97
94
|
|
|
@@ -133,16 +130,10 @@ Last updated: 2026-05-12 (after L1 ships).
|
|
|
133
130
|
|
|
134
131
|
## Backend asks (push back to WIQ app team)
|
|
135
132
|
|
|
136
|
-
-
|
|
137
|
-
|
|
138
|
-
`
|
|
139
|
-
|
|
140
|
-
have `created_at`, so the join makes the order ambiguous. One-line
|
|
141
|
-
fix on the WIQ-app side: change to
|
|
142
|
-
`.order("prospects.created_at DESC")` (or
|
|
143
|
-
`.order(Prospect.arel_table[:created_at].desc)`). The CLI documents
|
|
144
|
-
the workaround (use `wiq prospect_families list --query` — same
|
|
145
|
-
matches, returns families with prospects embedded) until this ships.
|
|
133
|
+
- ~~**`GET /api/v1/prospects?query=<str>` 500s on ambiguous ORDER BY.**~~ —
|
|
134
|
+
SHIPPED (WIQ-app commit 7ac9d90a5, 2026-05-21, qualifies the ORDER BY
|
|
135
|
+
as `prospects.created_at`). `wiq prospects list --query` works; the
|
|
136
|
+
KNOWN BUG note was removed from its long_desc in v0.6.0.
|
|
146
137
|
|
|
147
138
|
|
|
148
139
|
- ~~**`days_threshold` permit fix on `Api::V1::ReportsController`**~~ —
|
data/docs/wiq_api_notes.md
CHANGED
|
@@ -288,13 +288,49 @@ file with the WIQ team.
|
|
|
288
288
|
permit list as of the May 2026 WIQ-app fix. The CLI's
|
|
289
289
|
`--days-threshold` flag lands unchanged on the model.
|
|
290
290
|
|
|
291
|
+
**PAT write capabilities (`Api::V1::BaseController#enforce_pat_restrictions`):**
|
|
292
|
+
|
|
293
|
+
PAT traffic is read-only by default. A non-GET request succeeds only when
|
|
294
|
+
`ApiCapability.for(controller, action)` resolves to a capability that
|
|
295
|
+
(a) the team has enabled (`Team#api_write_capabilities`, editable by an
|
|
296
|
+
admin at `/settings/team/api_access`) and (b) the token carries
|
|
297
|
+
(`PersonalAccessToken#scopes`, immutable after mint, coach tokens only).
|
|
298
|
+
Registry today:
|
|
299
|
+
|
|
300
|
+
| Capability | Actions |
|
|
301
|
+
| --- | --- |
|
|
302
|
+
| `prospects:write` | `prospect_families#create/#update`, `prospects#create/#update`, `prospect_family_notes#create` |
|
|
303
|
+
| `reports:write` | `reports#create` |
|
|
304
|
+
|
|
305
|
+
Unregistered writes (every `destroy`, everything else) fail closed with
|
|
306
|
+
`"This endpoint is not writable with a personal access token."` The other
|
|
307
|
+
two denials name the capability verbatim — the CLI regexes on them:
|
|
308
|
+
|
|
309
|
+
| Body | CLI `code` |
|
|
310
|
+
| --- | --- |
|
|
311
|
+
| `This endpoint is not writable with a personal access token.` | `pat_write_unsupported` |
|
|
312
|
+
| `This team has not enabled <cap> for API access. …` | `capability_disabled_for_team` |
|
|
313
|
+
| `This token lacks the <cap> scope. Revoke it and mint a new token …` | `token_missing_scope` |
|
|
314
|
+
|
|
315
|
+
Team policy is re-checked per request, so an admin turning a capability
|
|
316
|
+
off disables every token holding it immediately. Every PAT call writes an
|
|
317
|
+
audit row (`PersonalAccessTokenAuditJob`) carrying the capability + scopes.
|
|
318
|
+
`GET /api/v1/personal_access_tokens` now includes `scopes: [...]` per
|
|
319
|
+
token; the CLI stores it at login and shows it in `auth status` / `doctor`.
|
|
320
|
+
|
|
321
|
+
Prospect-specific PAT rule (`ProspectsController#update`): stage changes
|
|
322
|
+
are not forced for token callers, so `advance_to!` refuses downgrades and
|
|
323
|
+
moves out of a terminal stage. The controller turns that silent no-op
|
|
324
|
+
into a 422 `"Stage cannot move from <a> to <b> with a personal access
|
|
325
|
+
token …"` → CLI code `stage_transition_refused`.
|
|
326
|
+
|
|
291
327
|
**Auth model (POST /api/v1/reports):**
|
|
292
328
|
|
|
293
329
|
- Every report POST requires a `CoachProfile`-bound PAT on the same team
|
|
294
|
-
(`coach_belongs_to_team?` in `ReportPolicy#create?`)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
330
|
+
(`coach_belongs_to_team?` in `ReportPolicy#create?`) AND the
|
|
331
|
+
`reports:write` capability (backfilled onto every existing team and
|
|
332
|
+
token when per-token scopes shipped, so nothing changed for existing
|
|
333
|
+
scripts). See "PAT write capabilities" below.
|
|
298
334
|
- A subset of report types — the 9 listed in `Report.finance_types`
|
|
299
335
|
(Membership, PaidSessionAccounting, RecurringDonor, DonationTransaction,
|
|
300
336
|
FundraiserSummary, FundraiserAccounting, InProgressRegistration,
|
|
@@ -343,7 +379,7 @@ DiscountedSubscriptionsReport, **DonationTransactionReport**,
|
|
|
343
379
|
PaidSessionAddOnDetailReport, PaidSessionAddOnSummaryReport,
|
|
344
380
|
**PracticeAttendanceReport**, **RecurringDonorReport**, RegistrationAnswerReport,
|
|
345
381
|
**RegistrationFinanceSummaryReport**, **RosterReport**, **RosterStatsReport**,
|
|
346
|
-
**ScholarshipAuditReport**, SessionRegistrationAnswerReport
|
|
382
|
+
**ScholarshipAuditReport**, **SessionRegistrationAnswerReport**,
|
|
347
383
|
TeamRegistrationRosterReport, UsawExpiredReport, UsawExportReport, UsawReport,
|
|
348
384
|
**WinLossReport**, **WrestlersWithoutSubscriptionsReport**`
|
|
349
385
|
|
data/lib/wiq/cli.rb
CHANGED
|
@@ -68,10 +68,10 @@ module Wiq
|
|
|
68
68
|
desc "locations SUBCOMMAND", "Team locations / sites — ID discovery for --location flags (list, show)"
|
|
69
69
|
subcommand "locations", Wiq::Commands::Locations
|
|
70
70
|
|
|
71
|
-
desc "prospects SUBCOMMAND", "Prospect pipeline — individual kids (list, show, summary)"
|
|
71
|
+
desc "prospects SUBCOMMAND", "Prospect pipeline — individual kids (list, show, summary; create, update, advance need prospects:write)"
|
|
72
72
|
subcommand "prospects", Wiq::Commands::Prospects
|
|
73
73
|
|
|
74
|
-
desc "prospect_families SUBCOMMAND", "Prospect pipeline — households (list, show, notes)"
|
|
74
|
+
desc "prospect_families SUBCOMMAND", "Prospect pipeline — households (list, show, notes, stage_changes, linked_answers; create, update, note need prospects:write)"
|
|
75
75
|
subcommand "prospect_families", Wiq::Commands::ProspectFamilies
|
|
76
76
|
|
|
77
77
|
desc "workflows SUBCOMMAND", "Named multi-step recipes for common club-admin questions (list, show)"
|
|
@@ -91,5 +91,8 @@ module Wiq
|
|
|
91
91
|
|
|
92
92
|
desc "billing_profiles SUBCOMMAND", "Look up a parent or coach's billing profile (admin only)"
|
|
93
93
|
subcommand "billing_profiles", Wiq::Commands::BillingProfiles
|
|
94
|
+
|
|
95
|
+
desc "payouts SUBCOMMAND", "Bank payouts / deposits — list and show (admin only)"
|
|
96
|
+
subcommand "payouts", Wiq::Commands::Payouts
|
|
94
97
|
end
|
|
95
98
|
end
|
data/lib/wiq/client.rb
CHANGED
|
@@ -9,7 +9,8 @@ module Wiq
|
|
|
9
9
|
# Thin wrapper around Faraday that:
|
|
10
10
|
# - Adds Authorization: Bearer <pat>
|
|
11
11
|
# - Parses JSON requests and responses
|
|
12
|
-
# - Retries on 429 and 5xx (Retry-After honored)
|
|
12
|
+
# - Retries on 429 and 5xx (Retry-After honored) — GET/HEAD only, so a
|
|
13
|
+
# write is never replayed
|
|
13
14
|
# - Translates non-2xx responses into Wiq::APIError
|
|
14
15
|
# - Exposes paginate(path, params) that walks the Link: rel=next chain
|
|
15
16
|
class Client
|
|
@@ -36,6 +37,10 @@ module Wiq
|
|
|
36
37
|
request(:put, path, body: body)
|
|
37
38
|
end
|
|
38
39
|
|
|
40
|
+
def patch(path, body = {})
|
|
41
|
+
request(:patch, path, body: body)
|
|
42
|
+
end
|
|
43
|
+
|
|
39
44
|
def delete(path, params = {})
|
|
40
45
|
request(:delete, path, params: params)
|
|
41
46
|
end
|
data/lib/wiq/commands/auth.rb
CHANGED
|
@@ -21,8 +21,14 @@ module Wiq
|
|
|
21
21
|
|
|
22
22
|
Verification: the CLI hits `GET /api/v1/personal_access_tokens` to
|
|
23
23
|
confirm the token works AND fetch the bound profile (display_name,
|
|
24
|
-
team_name, type) in one round trip.
|
|
25
|
-
login and stored alongside the token.
|
|
24
|
+
team_name, type) plus the token's write `scopes` in one round trip.
|
|
25
|
+
That metadata is shown after login and stored alongside the token.
|
|
26
|
+
|
|
27
|
+
Scopes: every PAT can read. Writes need a capability (currently
|
|
28
|
+
prospects:write, reports:write) that the team admin has enabled
|
|
29
|
+
under Settings → API Access AND that the token was minted with.
|
|
30
|
+
Scopes are immutable — to change them, revoke and mint a new
|
|
31
|
+
token, then re-run this command with --force.
|
|
26
32
|
DESC
|
|
27
33
|
method_option :token, type: :string, desc: "PAT value (otherwise prompted interactively)"
|
|
28
34
|
method_option :force, type: :boolean, default: false,
|
|
@@ -64,7 +70,8 @@ module Wiq
|
|
|
64
70
|
token: token,
|
|
65
71
|
token_prefix: metadata[:token_prefix],
|
|
66
72
|
name: metadata[:name],
|
|
67
|
-
profile: metadata[:profile]
|
|
73
|
+
profile: metadata[:profile],
|
|
74
|
+
scopes: metadata[:scopes]
|
|
68
75
|
)
|
|
69
76
|
|
|
70
77
|
render(
|
|
@@ -74,10 +81,11 @@ module Wiq
|
|
|
74
81
|
"token_prefix" => metadata[:token_prefix],
|
|
75
82
|
"name" => metadata[:name],
|
|
76
83
|
"profile" => metadata[:profile],
|
|
84
|
+
"scopes" => metadata[:scopes],
|
|
77
85
|
"stored_at" => Wiq::Credentials.for_host(host, alias_name)["stored_at"]
|
|
78
86
|
}.compact,
|
|
79
87
|
summary: profile_summary("Stored PAT for #{host} as #{alias_name.inspect}",
|
|
80
|
-
metadata[:profile]),
|
|
88
|
+
metadata[:profile], metadata[:scopes]),
|
|
81
89
|
breadcrumbs: [
|
|
82
90
|
{ "cmd" => "wiq auth status --as #{alias_name}",
|
|
83
91
|
"description" => "Verify the stored token works" },
|
|
@@ -93,7 +101,9 @@ module Wiq
|
|
|
93
101
|
|
|
94
102
|
Performs a best-effort live probe against
|
|
95
103
|
`/api/v1/personal_access_tokens` to confirm the token still works
|
|
96
|
-
and surface the live last_used_at
|
|
104
|
+
and surface the live last_used_at and write `scopes` (empty =
|
|
105
|
+
read-only). Check `live_scopes` before attempting a write command
|
|
106
|
+
such as `wiq prospects advance`. If the probe fails (network,
|
|
97
107
|
revoked token, host unreachable), the error appears in `live_error`
|
|
98
108
|
rather than aborting the command — local state is always shown.
|
|
99
109
|
|
|
@@ -126,9 +136,11 @@ module Wiq
|
|
|
126
136
|
"token_prefix" => store_entry["token_prefix"],
|
|
127
137
|
"stored_name" => store_entry["name"],
|
|
128
138
|
"stored_profile" => store_entry["profile"],
|
|
139
|
+
"stored_scopes" => store_entry["scopes"],
|
|
129
140
|
"live_name" => live_metadata&.dig(:name),
|
|
130
141
|
"live_last_used_at" => live_metadata&.dig(:last_used_at),
|
|
131
142
|
"live_profile" => live_metadata&.dig(:profile),
|
|
143
|
+
"live_scopes" => live_metadata&.dig(:scopes),
|
|
132
144
|
"live_error" => live_error
|
|
133
145
|
}.compact
|
|
134
146
|
|
|
@@ -138,7 +150,8 @@ module Wiq
|
|
|
138
150
|
: "No alias resolvable for this host."
|
|
139
151
|
else
|
|
140
152
|
profile_summary("Token reachable (alias=#{cfg.alias_name})",
|
|
141
|
-
live_metadata&.dig(:profile) || store_entry["profile"]
|
|
153
|
+
live_metadata&.dig(:profile) || store_entry["profile"],
|
|
154
|
+
live_metadata ? live_metadata[:scopes] : store_entry["scopes"])
|
|
142
155
|
end
|
|
143
156
|
|
|
144
157
|
render(data, summary: summary)
|
|
@@ -231,25 +244,36 @@ module Wiq
|
|
|
231
244
|
key: "personal_access_tokens"
|
|
232
245
|
)
|
|
233
246
|
match = rows.find { |r| r["token_prefix"] == prefix }
|
|
234
|
-
return { token_prefix: prefix, name: nil, last_used_at: nil, profile: nil } unless match
|
|
247
|
+
return { token_prefix: prefix, name: nil, last_used_at: nil, profile: nil, scopes: nil } unless match
|
|
235
248
|
|
|
236
249
|
{
|
|
237
250
|
token_prefix: match["token_prefix"],
|
|
238
251
|
name: match["name"],
|
|
239
252
|
last_used_at: match["last_used_at"],
|
|
240
|
-
profile: match["profile"]
|
|
253
|
+
profile: match["profile"],
|
|
254
|
+
# Servers predating per-token scopes omit the key; treat as read-only.
|
|
255
|
+
scopes: Array(match["scopes"])
|
|
241
256
|
}
|
|
242
257
|
end
|
|
243
258
|
|
|
244
|
-
def profile_summary(prefix, profile)
|
|
245
|
-
|
|
259
|
+
def profile_summary(prefix, profile, scopes = nil)
|
|
260
|
+
scope_note =
|
|
261
|
+
if scopes.nil?
|
|
262
|
+
nil
|
|
263
|
+
elsif scopes.empty?
|
|
264
|
+
"scopes: read-only"
|
|
265
|
+
else
|
|
266
|
+
"scopes: #{scopes.join(", ")}"
|
|
267
|
+
end
|
|
268
|
+
return [prefix, scope_note].compact.join("; ") + "." unless profile
|
|
246
269
|
|
|
247
270
|
who = profile["display_name"]
|
|
248
271
|
where = profile["team_name"]
|
|
249
272
|
kind = profile["type"]
|
|
250
273
|
tail = [who, where].compact.reject(&:empty?).join(" @ ")
|
|
251
274
|
tail += " (#{kind})" if kind && !kind.empty?
|
|
252
|
-
tail.empty? ?
|
|
275
|
+
head = tail.empty? ? prefix : "#{prefix} — #{tail}"
|
|
276
|
+
[head, scope_note].compact.join("; ") + "."
|
|
253
277
|
end
|
|
254
278
|
end
|
|
255
279
|
end
|
data/lib/wiq/commands/doctor.rb
CHANGED
|
@@ -15,6 +15,9 @@ module Wiq
|
|
|
15
15
|
6. Live reachability + auth probe via
|
|
16
16
|
`GET /api/v1/personal_access_tokens`
|
|
17
17
|
7. Bound profile (display_name, type, team) for the calling token
|
|
18
|
+
8. Write scopes on the token (empty = read-only; writes such as
|
|
19
|
+
`wiq prospects advance` need the matching capability enabled
|
|
20
|
+
by the team AND present on the token)
|
|
18
21
|
|
|
19
22
|
Exits non-zero if any check fails. Agents should run this first
|
|
20
23
|
when handed an unfamiliar shell to confirm they can actually call
|
|
@@ -59,6 +62,12 @@ module Wiq
|
|
|
59
62
|
checks << check("Bound profile",
|
|
60
63
|
"#{profile["display_name"]} (#{profile["type"]}) @ #{profile["team_name"]}",
|
|
61
64
|
ok: !profile["display_name"].nil?)
|
|
65
|
+
scopes = Array(match["scopes"])
|
|
66
|
+
checks << check("Write scopes",
|
|
67
|
+
scopes.empty? ? "none (read-only)" : scopes.join(", "),
|
|
68
|
+
ok: true,
|
|
69
|
+
hint: scopes.empty? ? "Reads only. Mint a token with prospects:write to use " \
|
|
70
|
+
"prospect write commands." : nil)
|
|
62
71
|
else
|
|
63
72
|
checks << check("Reachability + auth", "200 OK", ok: true)
|
|
64
73
|
checks << check("Bound profile", "(token not in own user's PAT list?)",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wiq
|
|
4
|
+
module Commands
|
|
5
|
+
class Payouts < Base
|
|
6
|
+
# Payout.status is a plain text column mirroring the billing
|
|
7
|
+
# partner's normalized status — no integer-enum translation needed
|
|
8
|
+
# (unlike Charge.status). Common values below; the column is not
|
|
9
|
+
# constrained server-side, so --status stays free-form.
|
|
10
|
+
COMMON_STATUSES = %w[paid in_transit scheduled].freeze
|
|
11
|
+
BILLING_PARTNERS = %w[stripe justifi].freeze
|
|
12
|
+
DEFAULT_PER_PAGE = 25
|
|
13
|
+
|
|
14
|
+
desc "list", "List bank payouts / deposits (finance: admin coach only)"
|
|
15
|
+
long_desc <<~DESC
|
|
16
|
+
Returns payouts — the batched deposits Stripe or Justifi (WIQ
|
|
17
|
+
Payments) sends to the club's bank account. Useful for "when does
|
|
18
|
+
our money arrive?", "what landed in the bank last week?", and
|
|
19
|
+
reconciling a bank statement line against WIQ charges.
|
|
20
|
+
|
|
21
|
+
Auth: admin-only (Pundit scope filters non-admin coach PATs to
|
|
22
|
+
empty results; parent/wrestler PATs return 403).
|
|
23
|
+
|
|
24
|
+
Filters translate to Ransack server-side (allowlist: deposits_at,
|
|
25
|
+
status, billing_partner, amount, created_at):
|
|
26
|
+
--status <str> q[status_eq]. Common values: paid,
|
|
27
|
+
in_transit, scheduled. Free-form —
|
|
28
|
+
the column mirrors the billing
|
|
29
|
+
partner's status strings.
|
|
30
|
+
--billing-partner <str> q[billing_partner_eq] — stripe | justifi
|
|
31
|
+
(justifi is branded "WIQ Payments").
|
|
32
|
+
--since <YYYY-MM-DD> q[deposits_at_gteq]
|
|
33
|
+
--until <YYYY-MM-DD> q[deposits_at_lteq]
|
|
34
|
+
|
|
35
|
+
--since/--until filter on deposits_at (when the money reaches the
|
|
36
|
+
bank), not created_at — that's the date a treasurer reconciling a
|
|
37
|
+
bank statement cares about. Rows come back newest-deposit first.
|
|
38
|
+
|
|
39
|
+
Money fields are integer cents. Each row: amount (net deposit),
|
|
40
|
+
payments_count/payments_total, refunds_count/refunds_total,
|
|
41
|
+
fees_total, status, deposits_at, billing_partner, description,
|
|
42
|
+
and destination (bank name + last4). Payouts accrue slowly (a
|
|
43
|
+
few per week), so default page size is #{DEFAULT_PER_PAGE}; pass
|
|
44
|
+
--all with --since to bound an exhaustive scan.
|
|
45
|
+
|
|
46
|
+
To see which individual charges landed in a payout, go through
|
|
47
|
+
the charges side: each `wiq charges list` row embeds its payout,
|
|
48
|
+
so filter charges by date window and group on payout.id.
|
|
49
|
+
DESC
|
|
50
|
+
method_option :status, type: :string,
|
|
51
|
+
desc: "Filter by status (common: #{COMMON_STATUSES.join(", ")})"
|
|
52
|
+
method_option :billing_partner, type: :string, enum: BILLING_PARTNERS,
|
|
53
|
+
desc: "Filter by billing partner (stripe | justifi)"
|
|
54
|
+
method_option :since, type: :string, desc: "Earliest deposits_at (YYYY-MM-DD)"
|
|
55
|
+
method_option :until, type: :string, desc: "Latest deposits_at (YYYY-MM-DD)"
|
|
56
|
+
method_option :per_page, type: :numeric, default: DEFAULT_PER_PAGE,
|
|
57
|
+
desc: "Page size (default #{DEFAULT_PER_PAGE})"
|
|
58
|
+
method_option :all, type: :boolean, default: false,
|
|
59
|
+
desc: "Follow pagination until exhausted"
|
|
60
|
+
def list
|
|
61
|
+
params = build_list_params
|
|
62
|
+
records, total = fetch_index("/api/v1/payouts", params, key: "payouts")
|
|
63
|
+
render_index(
|
|
64
|
+
records, total: total,
|
|
65
|
+
summary: "Listed #{records.size} payouts#{summary_filters_suffix}.",
|
|
66
|
+
breadcrumbs: [
|
|
67
|
+
{ "cmd" => "wiq payouts show <id>",
|
|
68
|
+
"description" => "Inspect a single payout" },
|
|
69
|
+
{ "cmd" => "wiq charges list --since <date> --until <date> --all",
|
|
70
|
+
"description" => "Charges in a window — each row embeds its payout for reconciliation" }
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
desc "show ID", "Fetch a single payout"
|
|
76
|
+
long_desc <<~DESC
|
|
77
|
+
Full payout payload: amount, currency, status, deposits_at,
|
|
78
|
+
payments_count/payments_total, refunds_count/refunds_total,
|
|
79
|
+
fees_total, billing_partner (+ billing_partner_id, the Stripe/
|
|
80
|
+
Justifi payout id like po_...), delivery_method, description, and
|
|
81
|
+
destination bank account (holder name, bank name, last4,
|
|
82
|
+
account type). Money fields are integer cents.
|
|
83
|
+
|
|
84
|
+
Auth: admin-only; a payout outside the PAT's team returns 403.
|
|
85
|
+
DESC
|
|
86
|
+
def show(id)
|
|
87
|
+
payout = client.get("/api/v1/payouts/#{id}")
|
|
88
|
+
render(payout,
|
|
89
|
+
summary: "Payout #{payout["id"]} — #{payout["status"]}, " \
|
|
90
|
+
"#{format_cents(payout["amount"])} deposited #{payout["deposits_at"]}",
|
|
91
|
+
breadcrumbs: [
|
|
92
|
+
{ "cmd" => "wiq charges list --since <date> --until <date> --all",
|
|
93
|
+
"description" => "Find this payout's charges (rows embed payout.id)" }
|
|
94
|
+
])
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
no_commands do
|
|
98
|
+
def build_list_params
|
|
99
|
+
params = { "per_page" => options[:per_page] || DEFAULT_PER_PAGE }
|
|
100
|
+
params["q[status_eq]"] = options[:status] if options[:status]
|
|
101
|
+
params["q[billing_partner_eq]"] = options[:billing_partner] if options[:billing_partner]
|
|
102
|
+
params["q[deposits_at_gteq]"] = options[:since] if options[:since]
|
|
103
|
+
params["q[deposits_at_lteq]"] = options[:until] if options[:until]
|
|
104
|
+
params
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def summary_filters_suffix
|
|
108
|
+
bits = []
|
|
109
|
+
bits << "status=#{options[:status]}" if options[:status]
|
|
110
|
+
bits << "billing_partner=#{options[:billing_partner]}" if options[:billing_partner]
|
|
111
|
+
bits << "since=#{options[:since]}" if options[:since]
|
|
112
|
+
bits << "until=#{options[:until]}" if options[:until]
|
|
113
|
+
bits.empty? ? "" : " (#{bits.join(", ")})"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def format_cents(cents)
|
|
117
|
+
return "?" unless cents.is_a?(Numeric)
|
|
118
|
+
|
|
119
|
+
format("$%.2f", cents / 100.0)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|