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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee99cfc6ad08abdf7e1318883bcfa3ecc45635d2d521de150ed1622d867b0ebc
4
- data.tar.gz: d9b6f3d18c731b4ddfe1060a03a40310e4d58b940d1f00f97dd3ef480f22dd74
3
+ metadata.gz: ec283a11b87c6ba8e2f8db5bf5a56f5e02fbf092aa3fb93fab54d08660553aa3
4
+ data.tar.gz: 1afd0ade5f5de6770c74acdd5d3fb0f2c68721f1537b0344a2ab6a401f32a400
5
5
  SHA512:
6
- metadata.gz: 89f8f34ae2184fab2069c8a0d51eaf53936dcb69e538e2fd7c0f659900e1be6ea3ee2f43101726ecb25bcd933a77beb20b2b954af4facbbfb248f4f0e40ae914
7
- data.tar.gz: b63afbb5f6755e1821f268d0a797fc1d1f12d97148f790aa340ad0ec55bdf86df91ee649d003caaabe33351e91a25f848217fd6e26da09c3cf6dcc136b173d15
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
- Read-only command-line interface for [WrestlingIQ](https://www.wrestlingiq.com).
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.029 commands across 16 groups, 117 RSpec smoke examples.
139
- Reads-only except for report submission (the API's canonical async
140
- pattern). Write commands deferred see `docs/deferred.md`.
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 on every resource. v1 is reads-only except
85
- report submission. Cherry-pick as customers ask.
86
- - **Prospect write commands.** API supports `POST /prospect_families`,
87
- `PATCH /prospect_families/:id` (incl. assigned_coach), `DELETE`,
88
- `POST /prospect_families/:id/prospects`, `PATCH /prospects/:id`
89
- (incl. stage transitions via `advance_to!`), and
90
- `POST /prospect_families/:id/notes` (with `clear_follow_up_for[]` /
91
- `add_follow_up_for[]` side-effect params). Reads-only in v1 — most
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
- - **`GET /api/v1/prospects?query=<str>` 500s on ambiguous ORDER BY.**
137
- Found during the Phase 5 agent telemetry pass. The controller does
138
- `Prospect.joins(:prospect_family).merge(ProspectFamily.search(query))
139
- .order(created_at: :desc)` both `prospects` and `prospect_families`
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`**~~ —
@@ -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?`). Parent/wrestler
295
- PATs return 403 with body `"Personal access tokens are read-only..."`
296
- via `enforce_pat_restrictions` reports is the one write currently on
297
- the PAT allowlist.
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
@@ -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. That metadata is shown after
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. If the probe fails (network,
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
- return "#{prefix}." unless profile
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? ? "#{prefix}." : "#{prefix} — #{tail}."
275
+ head = tail.empty? ? prefix : "#{prefix} — #{tail}"
276
+ [head, scope_note].compact.join("; ") + "."
253
277
  end
254
278
  end
255
279
  end
@@ -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