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 location_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. " \
           "The default row/CSV shape (web Download) carries the \"Added " \
           "to roster at\" column (roster_memberships.created_at — when the " \
           "wrestler landed on the roster, NOT their registration date), " \
           "populated only for a specific --roster <id> (id > 0), not " \
           "--roster 0. With --location <id> instead, the report scopes to " \
           "wrestlers on any roster at that location (one row each) and " \
           "\"Added to roster at\" becomes their EARLIEST membership across " \
           "that location's rosters. --v1 returns fuller per-wrestler " \
           "objects but drops that column.",
    example: "wiq reports run RosterReport --roster 42"
  },
  "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 location_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 location_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 location_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 location_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 location_id],
    dates: :optional,
    desc: "Wrestlers missing or with expired AAU memberships"
  },
  "AauExportReport" => {
    args: %w[roster_id location_id],
    dates: :optional,
    desc: "AAU bulk-purchase upload format"
  },

  # ── Stats tab ────────────────────────────────────────────────
  "WinLossReport" => {
    args: %w[roster_id location_id],
    dates: :required,
    desc: "Wins and losses per wrestler",
    recommended: true
  },
  "RosterStatsReport" => {
    args: %w[roster_id location_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. Always team-wide: the " \
           "model ignores roster_id AND location_id args. For a " \
           "location-scoped attendance report use CheckInReport " \
           "--location <id> instead.",
    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. Always team-wide: the model ignores roster_id AND " \
           "location_id args.",
    example: "wiq reports run CheckInFeedReport --start 2026-05-01 --end 2026-05-31"
  },
  "PracticeAttendanceReport" => {
    args: %w[roster_id location_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 location_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 location_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 location_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.



497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
# File 'lib/wiq/commands/reports.rb', line 497

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



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# File 'lib/wiq/commands/reports.rb', line 410

def run_report(type)
  args = build_args(type)

  body = {
    report: {
      type: type,
      version: report_version,
      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



441
442
443
444
445
446
447
448
449
# File 'lib/wiq/commands/reports.rb', line 441

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



465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# File 'lib/wiq/commands/reports.rb', line 465

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