Class: Wiq::Commands::Reports

Inherits:
Base
  • Object
show all
Defined in:
lib/wiq/commands/reports.rb

Constant Summary collapse

TYPES =

Curated allowlist surfaced in ‘wiq reports types`. Users can still pass any type string to `run`; this map governs documentation, arg hints, and agent-facing recommendations. Per-entry keys:

args:        permitted args.* keys for this type
dates:       :required | :optional — whether start_at/end_at are needed
desc:        one-line description
recommended: true | false — agents should prefer/avoid this type
             (omit when neutral)
prefer:      array of better alternatives (only when recommended: false)
notes:       agent-facing guidance, surfaced in `wiq reports types --agent`
example:     concrete `wiq reports run ...` invocation
admin_only:  true for the 9 finance reports gated by Pundit's
             acceptable_finance_permissions? (= is_admin?). Non-finance
             reports require a CoachProfile PAT on the team. PATs bound
             to ParentProfile or WrestlerProfile 403 on any report POST.

Note on auth: “elite” is a Team#account_type (gates UI tab rendering), not a per-user permission — the report POST doesn’t check it. Same for “payments_enabled” — that’s a team attribute, not a permission flag. The only API-level gate beyond CoachProfile is admin? for finance reports.

{
  # ── Roster tab ───────────────────────────────────────────────
  "RosterReport" => {
    args: %w[roster_id append_property_ids include_archived_roster_tags],
    dates: :optional,
    desc: "Roster snapshot — name, weight class, academic class, age",
    recommended: true,
    notes: "Supports custom columns via --append-properties <q_ids>: pass " \
           "registration_question ids (discoverable via " \
           "`wiq registrations questions --visibility public`, plus " \
           "--visibility private if admin) to surface intake data as " \
           "extra columns. Skip questions with a deleted_at — they're " \
           "still in the index payload but won't render. roster_id=0 " \
           "means \"all rosters\". Each appended question becomes a column.",
    example: "wiq reports run RosterReport --roster 42 --append-properties 17 23"
  },
  "FullExportWrestlerReport" => {
    args: %w[],
    dates: :optional,
    desc: "Full wrestler export — every known field for every wrestler",
    notes: "Very slow on large teams. Prefer RosterReport with " \
           "--append-properties unless you actually need the exhaustive dump."
  },

  # ── Invite tab ───────────────────────────────────────────────
  "InviteStatusReport" => {
    args: %w[],
    dates: :optional,
    desc: "Wrestlers / parents / coaches invited but not yet account-created",
    recommended: true,
    notes: "Mostly useful for high school teams or anywhere a coach is " \
           "manually inviting accounts. Club teams should prefer the " \
           "registration flow — parents register, then invite their own " \
           "kids — so this report stays empty for them."
  },

  # ── USAW / AAU tab ───────────────────────────────────────────
  "UsawReport" => {
    args: %w[roster_id],
    dates: :optional,
    desc: "All USA Wrestling card info on file, one row per wrestler",
    recommended: true,
    notes: "UI hides this tab when USAW collection is off (team setting)."
  },
  "UsawExpiredReport" => {
    args: %w[roster_id],
    dates: :optional,
    desc: "Wrestlers missing or with expired USAW memberships",
    notes: "Output is also a bulk-purchase upload format for USAW's system."
  },
  "UsawExportReport" => {
    args: %w[roster_id paid_session_id],
    dates: :optional,
    desc: "USAW bulk-purchase upload format",
    notes: "Only report that accepts paid_session_id=0 to mean " \
           "\"all sessions\"."
  },
  "AauReport" => {
    args: %w[roster_id],
    dates: :optional,
    desc: "All AAU card info on file, one row per wrestler",
    recommended: true,
    notes: "UI hides this tab when AAU collection is off (team setting)."
  },
  "AauExpiredReport" => {
    args: %w[roster_id],
    dates: :optional,
    desc: "Wrestlers missing or with expired AAU memberships"
  },
  "AauExportReport" => {
    args: %w[roster_id],
    dates: :optional,
    desc: "AAU bulk-purchase upload format"
  },

  # ── Stats tab ────────────────────────────────────────────────
  "WinLossReport" => {
    args: %w[roster_id],
    dates: :required,
    desc: "Wins and losses per wrestler",
    recommended: true
  },
  "RosterStatsReport" => {
    args: %w[roster_id],
    dates: :required,
    desc: "Wrestling stats per wrestler (takedowns, nearfall, etc.)",
    recommended: true
  },
  "EventStatsReport" => {
    args: %w[event_id],
    dates: :optional,
    desc: "Per-wrestler stats for a single event",
    notes: "Single-event drill-down. For a date range use RosterStatsReport. " \
           "Discover event_id via `wiq events list --start ... --end ...`."
  },

  # ── Attendance tab ───────────────────────────────────────────
  "CheckInSummaryReport" => {
    args: %w[roster_id],
    dates: :required,
    desc: "Summarized check-ins for a date range — per-wrestler totals",
    recommended: true,
    notes: "One row per wrestler over the date range — totals across all " \
           "their check-ins in the window. If you want one row per " \
           "check-in event use CheckInFeedReport. The UI doesn't expose " \
           "a roster picker; counts span the whole team.",
    example: "wiq reports run CheckInSummaryReport --start 2026-05-01 --end 2026-05-31"
  },
  "CheckInFeedReport" => {
    args: %w[roster_id],
    dates: :required,
    desc: "Raw check-in feed — one row per check-in, sorted by time",
    recommended: true,
    notes: "Useful for \"who came to the club today and what registrations " \
           "did they have at check-in time.\" Larger payload than the " \
           "summary.",
    example: "wiq reports run CheckInFeedReport --start 2026-05-01 --end 2026-05-31"
  },
  "PracticeAttendanceReport" => {
    args: %w[roster_id],
    dates: :required,
    desc: "Practice-event attendance roll-up across a date range",
    recommended: false,
    prefer: %w[CheckInSummaryReport CheckInFeedReport],
    notes: "Primarily used by high school coaches who need a report for " \
           "their athletic director about who was present vs absent. " \
           "Only pulls practice events. Not recommended for club teams — " \
           "use CheckInSummaryReport or CheckInFeedReport instead."
  },
  "LastPracticeAttendedReport" => {
    args: %w[roster_id],
    dates: :optional,
    desc: "Days since last practice attended, per wrestler",
    recommended: true,
    notes: "Useful for following up with people who haven't shown up to " \
           "practice in a while — particularly for seasonal clubs."
  },
  "ChurnRiskReport" => {
    args: %w[roster_id days_threshold],
    dates: :optional,
    desc: "Active recurring subscribers who haven't checked in within a window",
    recommended: true,
    notes: "Useful for subscription-based clubs to identify who might " \
           "cancel soon, based on no check-in within a chosen window. " \
           "Pass --days-threshold (7|14|30|60|90); any other value " \
           "falls back to the model's 30-day default.",
    example: "wiq reports run ChurnRiskReport --roster 0 --days-threshold 30"
  },
  "CheckInReport" => {
    args: %w[roster_id],
    dates: :required,
    desc: "Extended attendance — one row per check-in INCLUDING Q&A responses",
    notes: "UI labels this \"Attendance Extended (with questions).\" Same " \
           "shape as CheckInFeedReport PLUS the registration_answer " \
           "values collected at check-in time. Use when Q&A capture " \
           "matters; otherwise prefer CheckInFeedReport."
  },

  # ── Subscription tab (admin-gated UI; non-finance reports below
  # only require coach access at the API level) ────────────────
  "MembershipSummaryReport" => {
    args: %w[],
    dates: :optional,
    desc: "Every subscription ever created, one row each",
    recommended: true,
    admin_only: true
  },
  "CancelledSubscriptionsReport" => {
    args: %w[],
    dates: :optional,
    desc: "Canceled subscriptions, ordered by cancellation date"
  },
  "CurrentlyPausedSubscriptionsReport" => {
    args: %w[],
    dates: :optional,
    desc: "Currently paused subscriptions, ordered by paused date"
  },
  "ExpiringSubscriptionsReport" => {
    args: %w[],
    dates: :required,
    desc: "Subscriptions expiring in a date range (past or future)",
    recommended: true,
    notes: "Past dates audit churn; future dates plan retention outreach."
  },
  "DiscountedSubscriptionsReport" => {
    args: %w[],
    dates: :optional,
    desc: "Subscriptions (active or canceled) with a scholarship applied"
  },
  "WrestlersWithoutSubscriptionsReport" => {
    args: %w[],
    dates: :optional,
    desc: "Wrestlers without an active recurring subscription"
  },

  # ── Registration tab ────────────────────────────────────────
  "SessionRegistrationAnswerReport" => {
    args: %w[paid_session_id],
    dates: :optional,
    desc: "Full Q&A export of info submitted by parents at signup",
    recommended: true,
    notes: "Pass --paid-session <id> — required."
  },
  "PaidSessionAccountingReport" => {
    args: %w[paid_session_id],
    dates: :optional,
    desc: "Line-item charges for a registration session",
    recommended: true,
    admin_only: true,
    notes: "Pass --paid-session <id> — required."
  },
  "RegistrationFinanceSummaryReport" => {
    args: %w[],
    dates: :required,
    desc: "Financial summary, one row per session"
  },
  "OverdueRegistrationReport" => {
    args: %w[],
    dates: :optional,
    desc: "Overdue installment registrations",
    recommended: true,
    admin_only: true,
    notes: "Standard AR follow-up tool."
  },
  "InProgressRegistrationReport" => {
    args: %w[],
    dates: :optional,
    desc: "Carts that haven't finished signup (abandoned-cart audit)",
    admin_only: true
  },

  # ── Scholarship tab ──────────────────────────────────────────
  "ScholarshipAuditReport" => {
    args: %w[],
    dates: :required,
    desc: "Scholarship code usage across registrations + subscriptions",
    recommended: true,
    admin_only: true
  },

  # ── Donor tab ────────────────────────────────────────────────
  "RecurringDonorReport" => {
    args: %w[],
    dates: :optional,
    desc: "Active recurring donors",
    recommended: true,
    admin_only: true
  },
  "DonationTransactionReport" => {
    args: %w[],
    dates: :required,
    desc: "All payments that included a donation",
    recommended: true,
    admin_only: true
  },

  # ── Fundraiser tab ───────────────────────────────────────────
  "FundraiserSummaryReport" => {
    args: %w[fundraiser_id],
    dates: :optional,
    desc: "Fundraiser results, one row per contributor",
    recommended: true,
    admin_only: true,
    notes: "Pass --fundraiser <id> — required."
  },
  "FundraiserAccountingReport" => {
    args: %w[fundraiser_id],
    dates: :optional,
    desc: "Fundraiser line items, one row per item purchased",
    admin_only: true,
    notes: "Pass --fundraiser <id> — required. Drill-down of " \
           "FundraiserSummaryReport."
  },

  # ── Online Store tab ─────────────────────────────────────────
  "OnlineStoreSummaryReport" => {
    args: %w[online_store_id],
    dates: :optional,
    desc: "Summary of store orders",
    recommended: true,
    notes: "Pass --online-store <id> — required."
  },
  "OnlineStoreDetailReport" => {
    args: %w[online_store_id],
    dates: :optional,
    desc: "Detailed store orders, one row per line item",
    notes: "Pass --online-store <id> — required. Useful to hand off to a " \
           "gear or printing partner who needs sku-level data."
  }
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

exit_on_failure?

Class Method Details

.poll(client, id, timeout: 300, initial: 2, max_interval: 30) ⇒ Object

Class-level poller used by reports + check_ins summary.



451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
# File 'lib/wiq/commands/reports.rb', line 451

def self.poll(client, id, timeout: 300, initial: 2, max_interval: 30)
  deadline = Time.now + timeout
  interval = initial
  report = nil
  loop do
    report = client.get("/api/v1/reports/#{id}")
    case report["status"]
    when "ready"
      return report
    when "failed"
      raise Wiq::ReportFailedError, report
    end
    if Time.now >= deadline
      raise Wiq::ReportTimeoutError.new(report, timeout)
    end
    sleep(interval)
    interval = [interval * 2, max_interval].min
  end
end

Instance Method Details

#run_report(type) ⇒ Object



364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/wiq/commands/reports.rb', line 364

def run_report(type)
  args = build_args(type)

  body = {
    report: {
      type: type,
      version: "v1",
      name: options[:name] || default_name(type),
      start_at: options[:start],
      end_at: options[:end],
      args: args
    }
  }

  report = client.post("/api/v1/reports", body)
  report = self.class.poll(client, report["id"], timeout: options[:timeout]) if options[:wait]

  render(report,
         summary: "Report ##{report["id"]} status=#{report["status"]}.",
         breadcrumbs: [
           { "cmd" => "wiq reports show #{report["id"]}", "description" => "Refetch the result later" }
         ])
end

#show(id) ⇒ Object



395
396
397
398
399
400
401
402
403
# File 'lib/wiq/commands/reports.rb', line 395

def show(id)
  report = options[:wait] ? self.class.poll(client, id, timeout: options[:timeout])
                          : client.get("/api/v1/reports/#{id}")
  render(report,
         summary: "Report ##{report["id"]} status=#{report["status"]}.",
         breadcrumbs: [
           { "cmd" => "wiq reports show #{id} --wait", "description" => "Poll until ready" }
         ])
end

#typesObject



419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/wiq/commands/reports.rb', line 419

def types
  rows = TYPES.map do |type, info|
    row = {
      "type" => type,
      "description" => info[:desc],
      "args" => info[:args],
      "dates" => info[:dates].to_s
    }
    row["recommended"] = info[:recommended] if info.key?(:recommended)
    row["prefer"] = info[:prefer] if info[:prefer]
    row["admin_only"] = info[:admin_only] if info[:admin_only]
    row["notes"] = info[:notes] if info[:notes]
    row["example"] = info[:example] if info[:example]
    row
  end
  render_index(
    rows,
    summary: "Documented report types. Auth: every report POST requires a " \
             "CoachProfile-bound PAT on the team. The 9 entries with " \
             "admin_only: true additionally require admin? — " \
             "non-admin coach PATs 403 on those. recommended: true rows " \
             "are WIQ-blessed picks; prefer: lists alternatives for " \
             "de-emphasized types.",
    breadcrumbs: [
      { "cmd" => "wiq reports run <TYPE>", "description" => "Submit a report" },
      { "cmd" => "wiq registrations questions",
        "description" => "Discover question ids for RosterReport --append-properties" }
    ]
  )
end