rogiq 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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/exe/rogiq +49 -0
  3. data/lib/rogiq/cli.rb +51 -0
  4. data/lib/rogiq/commands/accounts.rb +114 -0
  5. data/lib/rogiq/commands/ai.rb +71 -0
  6. data/lib/rogiq/commands/auth.rb +194 -0
  7. data/lib/rogiq/commands/base.rb +26 -0
  8. data/lib/rogiq/commands/billing.rb +90 -0
  9. data/lib/rogiq/commands/billing_stripe.rb +15 -0
  10. data/lib/rogiq/commands/clients.rb +117 -0
  11. data/lib/rogiq/commands/content.rb +71 -0
  12. data/lib/rogiq/commands/diagnose.rb +134 -0
  13. data/lib/rogiq/commands/jobs.rb +126 -0
  14. data/lib/rogiq/commands/queue.rb +115 -0
  15. data/lib/rogiq/commands/security.rb +33 -0
  16. data/lib/rogiq/commands/seo.rb +49 -0
  17. data/lib/rogiq/commands/seo_audit.rb +51 -0
  18. data/lib/rogiq/commands/status.rb +166 -0
  19. data/lib/rogiq/commands/sync.rb +60 -0
  20. data/lib/rogiq/config_store.rb +59 -0
  21. data/lib/rogiq/formatters.rb +103 -0
  22. data/lib/rogiq/helpers.rb +53 -0
  23. data/lib/rogiq/http_api.rb +81 -0
  24. data/lib/rogiq/rails_loader.rb +22 -0
  25. data/lib/rogiq/remote/accounts.rb +38 -0
  26. data/lib/rogiq/remote/ai.rb +50 -0
  27. data/lib/rogiq/remote/billing.rb +39 -0
  28. data/lib/rogiq/remote/clients.rb +44 -0
  29. data/lib/rogiq/remote/command_base.rb +41 -0
  30. data/lib/rogiq/remote/content.rb +37 -0
  31. data/lib/rogiq/remote/diagnose.rb +24 -0
  32. data/lib/rogiq/remote/jobs.rb +52 -0
  33. data/lib/rogiq/remote/queue.rb +72 -0
  34. data/lib/rogiq/remote/security.rb +26 -0
  35. data/lib/rogiq/remote/seo.rb +32 -0
  36. data/lib/rogiq/remote/status.rb +35 -0
  37. data/lib/rogiq/remote/sync.rb +49 -0
  38. data/lib/rogiq/remote_cli.rb +73 -0
  39. data/lib/rogiq/version.rb +5 -0
  40. metadata +96 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1e6164cbe6a3fe5aeec2b3602d8c2401c552cb4d08ee8160d044bdf0296e17a9
4
+ data.tar.gz: c80152ab3a4191dc0991f178bcc60a94f2aa359cc176e5d16989e47a078e410a
5
+ SHA512:
6
+ metadata.gz: 8320cbe572bbfc5e54405e1a4e9967bfe900d0902bdf2c417c7dbb65c2aabde043fb87ed425c7e2c79d0cf9cc1e377212e3a4bacaf8a47652822cfb8020f2597
7
+ data.tar.gz: 61c8fb1e700c98086f848086d33c1de7046181128f6857d4b05b30183dc152dc299766ab3abff5e73559a7d12cda5b3eb7e5e81ad9c226e761b6deaa57981e5e
data/exe/rogiq ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ lib = File.expand_path("../lib", __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+
7
+ require "rogiq/version"
8
+
9
+ argv = ARGV.dup
10
+ if %w[version -v --version].include?(argv.first)
11
+ puts RogIQ::VERSION
12
+ exit 0
13
+ end
14
+
15
+ force_local = ENV["ROGIQ_LOCAL"].to_s == "1" || argv.delete("--local")
16
+
17
+ require "rogiq/config_store"
18
+
19
+ first = argv.first
20
+ auth_cmds = %w[login logout whoami use]
21
+
22
+ if auth_cmds.include?(first)
23
+ require "rogiq/commands/auth"
24
+ RogIQ::Commands::Auth.start(argv)
25
+ elsif !force_local && RogIQ::ConfigStore.remote_available?
26
+ require "rogiq/remote_cli"
27
+ RogIQ::RemoteCLI.start(argv)
28
+ else
29
+ require "rogiq/rails_loader"
30
+ require "rogiq/formatters"
31
+ require "rogiq/helpers"
32
+ require "rogiq/commands/base"
33
+ require "rogiq/commands/status"
34
+ require "rogiq/commands/queue"
35
+ require "rogiq/commands/jobs"
36
+ require "rogiq/commands/clients"
37
+ require "rogiq/commands/accounts"
38
+ require "rogiq/commands/billing_stripe"
39
+ require "rogiq/commands/billing"
40
+ require "rogiq/commands/ai"
41
+ require "rogiq/commands/seo_audit"
42
+ require "rogiq/commands/seo"
43
+ require "rogiq/commands/content"
44
+ require "rogiq/commands/diagnose"
45
+ require "rogiq/commands/security"
46
+ require "rogiq/commands/sync"
47
+ require "rogiq/cli"
48
+ RogIQ::CLI.start(argv)
49
+ end
data/lib/rogiq/cli.rb ADDED
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module RogIQ
6
+ class CLI < Thor
7
+ package_name "RogIQ"
8
+
9
+ desc "version", "Print rogiq version"
10
+ map %w[--version -v] => :version
11
+ def version
12
+ puts RogIQ::VERSION
13
+ end
14
+
15
+ desc "status SUBCOMMAND ...", "System health / queues / DB / Redis / AI config"
16
+ subcommand "status", RogIQ::Commands::Status
17
+
18
+ desc "queue SUBCOMMAND ...", "Solid Queue operations"
19
+ subcommand "queue", RogIQ::Commands::Queue
20
+
21
+ desc "jobs SUBCOMMAND ...", "Inspect and manage failed Solid Queue jobs"
22
+ subcommand "jobs", RogIQ::Commands::Jobs
23
+
24
+ desc "clients SUBCOMMAND ...", "Client lookup and settings"
25
+ subcommand "clients", RogIQ::Commands::Clients
26
+
27
+ desc "accounts SUBCOMMAND ...", "Accounts, subscriptions, usage"
28
+ subcommand "accounts", RogIQ::Commands::Accounts
29
+
30
+ desc "ai SUBCOMMAND ...", "AI usage, models, jobs"
31
+ subcommand "ai", RogIQ::Commands::AI
32
+
33
+ desc "seo SUBCOMMAND ...", "SEO module, audits, keywords"
34
+ subcommand "seo", RogIQ::Commands::SEO
35
+
36
+ desc "billing SUBCOMMAND ...", "Coins, invoices, Stripe catalog, usage overages"
37
+ subcommand "billing", RogIQ::Commands::Billing
38
+
39
+ desc "content SUBCOMMAND ...", "Content pipeline & healing"
40
+ subcommand "content", RogIQ::Commands::Content
41
+
42
+ desc "diagnose SUBCOMMAND ...", "Targeted diagnostics"
43
+ subcommand "diagnose", RogIQ::Commands::Diagnose
44
+
45
+ desc "security SUBCOMMAND ...", "Brakeman, bundler-audit, security_policy rake"
46
+ subcommand "security", RogIQ::Commands::Security
47
+
48
+ desc "sync SUBCOMMAND ...", "Postman, Stripe catalog, WordPress, integrations"
49
+ subcommand "sync", RogIQ::Commands::Sync
50
+ end
51
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RogIQ
4
+ module Commands
5
+ class Accounts < RogIQ::Commands::Base
6
+ desc "list", "List accounts"
7
+ method_option :search, type: :string, desc: "Filter name or slug (ILIKE)"
8
+ method_option :limit, type: :numeric, default: 40, aliases: "-n"
9
+ def list
10
+ RogIQ.load_rails!
11
+ scope = ::Account.order(created_at: :desc)
12
+ if options[:search].present?
13
+ q = "%#{options[:search]}%"
14
+ scope = scope.where("name ILIKE ? OR slug ILIKE ?", q, q)
15
+ end
16
+ rows = scope.limit(options[:limit]).map do |a|
17
+ [a.id, a.name, a.slug, a.status, a.account_type]
18
+ end
19
+ fmt.output(headers: %w[id name slug status account_type], rows: rows)
20
+ end
21
+
22
+ desc "show IDENTIFIER", "Account detail by id, slug, or name"
23
+ def show(identifier)
24
+ RogIQ.load_rails!
25
+ a = RogIQ::Helpers.resolve_account(identifier)
26
+ unless a
27
+ fmt.error_msg("Account not found")
28
+ exit 1
29
+ end
30
+
31
+ fmt.output(
32
+ id: a.id,
33
+ name: a.name,
34
+ slug: a.slug,
35
+ status: a.status,
36
+ account_type: a.account_type,
37
+ billing_status: a.try(:billing_status),
38
+ created_at: a.created_at
39
+ )
40
+ end
41
+
42
+ desc "usage IDENTIFIER", "Usage meters, coin wallet, open overages"
43
+ def usage(identifier)
44
+ RogIQ.load_rails!
45
+ a = RogIQ::Helpers.resolve_account(identifier)
46
+ unless a
47
+ fmt.error_msg("Account not found")
48
+ exit 1
49
+ end
50
+
51
+ meters = ::UsageMeter.where(account_id: a.id).order(period_end: :desc).limit(20).map do |m|
52
+ {
53
+ metric_type: m.metric_type,
54
+ period_start: m.period_start,
55
+ period_end: m.period_end,
56
+ current_value: m.current_value
57
+ }
58
+ end
59
+ wallet = a.rogue_coin_wallet
60
+ overages = a.usage_overages.status_pending.limit(20)
61
+
62
+ fmt.output(
63
+ account_id: a.id,
64
+ rogue_coin_balance: wallet&.balance,
65
+ rogue_coin_lifetime_spent: wallet&.lifetime_spent,
66
+ usage_meters: meters,
67
+ open_overages: overages.map do |o|
68
+ {
69
+ id: o.id,
70
+ metric: o.metric_type,
71
+ overage_amount: o.overage_amount,
72
+ total_charge: o.total_charge.to_f,
73
+ status: o.status,
74
+ created_at: o.created_at
75
+ }
76
+ end
77
+ )
78
+ end
79
+
80
+ desc "subscription IDENTIFIER", "Subscription + plan summary"
81
+ def subscription(identifier)
82
+ RogIQ.load_rails!
83
+ a = RogIQ::Helpers.resolve_account(identifier)
84
+ unless a
85
+ fmt.error_msg("Account not found")
86
+ exit 1
87
+ end
88
+
89
+ sub = a.subscription
90
+ plan = sub&.subscription_plan
91
+ fmt.output(
92
+ account_id: a.id,
93
+ subscription: sub&.slice(:id, :status, :trial_ends_at, :current_period_start, :current_period_end, :cancel_at_period_end),
94
+ plan: plan&.slice(:id, :name, :slug)
95
+ )
96
+ end
97
+
98
+ desc "users IDENTIFIER", "Active account users + roles"
99
+ def users(identifier)
100
+ RogIQ.load_rails!
101
+ a = RogIQ::Helpers.resolve_account(identifier)
102
+ unless a
103
+ fmt.error_msg("Account not found")
104
+ exit 1
105
+ end
106
+
107
+ rows = a.account_users.includes(:user, :role).where(status: "active").map do |au|
108
+ [au.user&.email, au.role&.name, au.status]
109
+ end
110
+ fmt.output(headers: %w[email role status], rows: rows)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RogIQ
4
+ module Commands
5
+ class AI < RogIQ::Commands::Base
6
+ desc "status", "AI engine readiness + provider keys"
7
+ def status
8
+ RogIQ.load_rails!
9
+ payload = {
10
+ openai_key_present: ENV["OPENAI_API_KEY"].present?,
11
+ anthropic_key_present: ENV["ANTHROPIC_API_KEY"].present?,
12
+ ai_engine_ready: defined?(AIEngine) && AIEngine.ready?,
13
+ available_providers: (defined?(AIEngine) ? AIEngine.available_providers : [])
14
+ }
15
+ if defined?(Rails.configuration.ai_engine)
16
+ cfg = Rails.configuration.ai_engine
17
+ payload[:openai_enabled] = cfg.try(:openai_enabled)
18
+ payload[:anthropic_enabled] = cfg.try(:anthropic_enabled)
19
+ end
20
+ fmt.output(payload)
21
+ end
22
+
23
+ desc "cost", "Roll up AI usage cost from ai_usage_daily"
24
+ method_option :days, type: :numeric, default: 30, desc: "Lookback window (days)"
25
+ method_option :account, type: :string, desc: "Limit to account id or slug"
26
+ def cost
27
+ RogIQ.load_rails!
28
+ start_date = options[:days].to_i.days.ago.to_date
29
+ scope = ::AIUsageDaily.where("usage_date >= ?", start_date)
30
+ if options[:account].present?
31
+ acc = RogIQ::Helpers.resolve_account(options[:account])
32
+ unless acc
33
+ fmt.error_msg("Account not found")
34
+ exit 1
35
+ end
36
+ scope = scope.where(account_id: acc.id)
37
+ end
38
+
39
+ rows = scope.group(:provider, :model).sum(:total_cost).sort_by { |_, v| -v.to_f }.first(50)
40
+ fmt.output(headers: %w[provider model total_cost], rows: rows.map { |(p, m), t| [p, m, t.to_f.round(6)] })
41
+ total = scope.sum(:total_cost)
42
+ fmt.say("Total (period): #{total.to_f.round(6)}") unless options[:quiet]
43
+ end
44
+
45
+ desc "sync", "Enqueue AiModelSyncJob"
46
+ def sync
47
+ RogIQ.load_rails!
48
+ ::AiModelSyncJob.perform_later
49
+ fmt.success("AiModelSyncJob enqueued.")
50
+ end
51
+
52
+ desc "health_check", "Enqueue AiModelHealthCheckJob"
53
+ map "health-check" => :health_check
54
+ def health_check
55
+ RogIQ.load_rails!
56
+ ::AiModelHealthCheckJob.perform_later
57
+ fmt.success("AiModelHealthCheckJob enqueued.")
58
+ end
59
+
60
+ desc "models", "List AIModelConfig rows"
61
+ method_option :limit, type: :numeric, default: 100
62
+ def models
63
+ RogIQ.load_rails!
64
+ rows = ::AIModelConfig.order(:sort_order, :display_name).limit(options[:limit]).map do |m|
65
+ [m.model_key, m.provider, m.display_name, m.enabled, m.available?]
66
+ end
67
+ fmt.output(headers: %w[model_key provider display_name enabled available], rows: rows)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ require "thor"
8
+
9
+ require "rogiq/config_store"
10
+ require "rogiq/formatters"
11
+ require "rogiq/http_api"
12
+
13
+ module RogIQ
14
+ module Commands
15
+ class Auth < Thor
16
+ class_option :format,
17
+ aliases: "-f",
18
+ type: :string,
19
+ default: "table",
20
+ enum: %w[table json],
21
+ desc: "Output format"
22
+
23
+ desc "login", "Sign in and save a CLI API token to ~/.rogiq/config.yml"
24
+ method_option :api,
25
+ type: :string,
26
+ default: nil,
27
+ desc: "API base URL (default #{ENV.fetch('ROGIQ_API_URL', 'https://api.rogiq.ai')})"
28
+ def login
29
+ fmt = RogIQ::Formatters.new(format: options[:format])
30
+ base = options[:api].to_s.strip
31
+ base = ENV["ROGIQ_API_URL"].to_s.strip if base.empty?
32
+ base = ask("API base URL:", default: "https://api.rogiq.ai") if base.empty?
33
+ base = base.to_s.chomp("/")
34
+ email = ask("Email:")
35
+ password = ask("Password:", echo: false)
36
+
37
+ headers = { "Content-Type" => "application/json", "Accept" => "application/json" }
38
+ res, jwt = post_login(base, headers, email, password)
39
+ json = JSON.parse(res.body)
40
+
41
+ if json["mfa_required"]
42
+ mfa_token = json["mfa_token"]
43
+ code = ask("MFA code:")
44
+ remember = ask("Remember this device for 30 days? (y/N):").downcase.start_with?("y")
45
+ res, jwt = post_mfa_verify(base, headers, mfa_token, code, remember)
46
+ json = JSON.parse(res.body)
47
+ end
48
+
49
+ unless jwt
50
+ fmt.error_msg(error_message(res))
51
+ exit 1
52
+ end
53
+
54
+ accounts = json["accounts"] || []
55
+ account_id = pick_account_id(fmt, accounts)
56
+ token_payload = mint_cli_token(base, jwt, account_id, headers)
57
+ plain = token_payload["token"]
58
+ unless plain
59
+ err = token_payload["error"] || token_payload["errors"]
60
+ fmt.error_msg("Could not create API token: #{err || token_payload.inspect}")
61
+ exit 1
62
+ end
63
+
64
+ RogIQ::ConfigStore.save(
65
+ "api_base_url" => base,
66
+ "token" => plain,
67
+ "account_id" => account_id,
68
+ "email" => email
69
+ )
70
+
71
+ fmt.success("Saved credentials to #{RogIQ::ConfigStore::PATH}")
72
+ out = token_payload.reject { |k, _| k.to_s == "token" }
73
+ out["token_saved"] = true
74
+ fmt.output(out)
75
+ rescue RogIQ::HttpApi::Error => e
76
+ fmt.error_msg(e.message)
77
+ exit 1
78
+ end
79
+
80
+ desc "logout", "Remove saved CLI credentials"
81
+ def logout
82
+ RogIQ::ConfigStore.clear!
83
+ puts "Removed #{RogIQ::ConfigStore::PATH}"
84
+ end
85
+
86
+ desc "whoami", "Print current user from /api/v1/me"
87
+ def whoami
88
+ fmt = RogIQ::Formatters.new(format: options[:format])
89
+ api = RogIQ::HttpApi.from_config!
90
+ data = api.get("/api/v1/me")
91
+ fmt.output(data)
92
+ rescue RogIQ::HttpApi::Error => e
93
+ fmt.error_msg(e.message)
94
+ exit 1
95
+ end
96
+
97
+ desc "use ACCOUNT_ID", "Set default X-Account-ID header for CLI requests"
98
+ def use(account_id)
99
+ RogIQ::ConfigStore.merge!("account_id" => account_id.to_s)
100
+ puts "Default account_id=#{account_id}"
101
+ end
102
+
103
+ private
104
+
105
+ def pick_account_id(fmt, accounts)
106
+ accounts = Array(accounts)
107
+ if accounts.empty?
108
+ return ask("Account ID (required for API token):").to_s.strip
109
+ end
110
+ return accounts.first["id"].to_s if accounts.size == 1
111
+
112
+ fmt.say("Accounts:")
113
+ accounts.each_with_index do |a, i|
114
+ fmt.say(" [#{i + 1}] #{a['name']} (id=#{a['id']})")
115
+ end
116
+ loop do
117
+ n = ask("Choose account number (1-#{accounts.size}):")
118
+ i = n.to_i
119
+ if i >= 1 && i <= accounts.size
120
+ return accounts[i - 1]["id"].to_s
121
+ end
122
+
123
+ fmt.warn_msg("Enter a number between 1 and #{accounts.size}")
124
+ end
125
+ end
126
+
127
+ def post_login(base, headers, email, password)
128
+ uri = URI.parse("#{base}/api/v1/auth/login")
129
+ req = Net::HTTP::Post.new(uri)
130
+ headers.each { |k, v| req[k] = v }
131
+ req.body = JSON.generate(user: { email: email, password: password })
132
+ response_for(uri, req)
133
+ end
134
+
135
+ def post_mfa_verify(base, headers, mfa_token, code, remember)
136
+ uri = URI.parse("#{base}/api/v1/auth/mfa/verify")
137
+ req = Net::HTTP::Post.new(uri)
138
+ headers.each { |k, v| req[k] = v }
139
+ req.body = JSON.generate(mfa_token: mfa_token, code: code, remember_me: remember)
140
+ response_for(uri, req)
141
+ end
142
+
143
+ def mint_cli_token(base, jwt, account_id, headers)
144
+ uri = URI.parse("#{base}/api/v1/api_tokens")
145
+ req = Net::HTTP::Post.new(uri)
146
+ headers.each { |k, v| req[k] = v }
147
+ req["Authorization"] = "Bearer #{jwt}"
148
+ aid = account_id.to_s.strip
149
+ req["X-Account-ID"] = aid unless aid.empty?
150
+ req.body = JSON.generate(api_token: { name: "RogIQ CLI", purpose: "cli" })
151
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
152
+ http.open_timeout = 15
153
+ http.read_timeout = 60
154
+ http.request(req)
155
+ end
156
+ body = res.body.to_s
157
+ parsed =
158
+ begin
159
+ body.strip.empty? ? {} : JSON.parse(body)
160
+ rescue JSON::ParserError
161
+ { "error" => body[0, 300] }
162
+ end
163
+
164
+ return parsed.merge("error" => "HTTP #{res.code}") unless res.is_a?(Net::HTTPSuccess)
165
+
166
+ parsed
167
+ end
168
+
169
+ def response_for(uri, req)
170
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
171
+ http.open_timeout = 15
172
+ http.read_timeout = 60
173
+ http.request(req)
174
+ end
175
+ jwt = extract_jwt(res)
176
+ [ res, jwt ]
177
+ end
178
+
179
+ def extract_jwt(res)
180
+ raw = res["Authorization"] || res["authorization"]
181
+ return nil if raw.nil? || raw.to_s.strip.empty?
182
+
183
+ raw.split(" ", 2).last
184
+ end
185
+
186
+ def error_message(res)
187
+ json = JSON.parse(res.body)
188
+ [ json["error"], json["message"] ].map { |x| x.to_s.strip }.reject(&:empty?).first || "HTTP #{res.code}"
189
+ rescue JSON::ParserError
190
+ "HTTP #{res.code}: #{res.body.to_s[0, 200]}"
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module RogIQ
6
+ module Commands
7
+ class Base < Thor
8
+ class_option :format,
9
+ aliases: "-f",
10
+ type: :string,
11
+ default: "table",
12
+ enum: %w[table json],
13
+ desc: "Output format"
14
+ class_option :quiet,
15
+ type: :boolean,
16
+ default: false,
17
+ desc: "Suppress non-error output"
18
+
19
+ no_tasks do
20
+ def fmt
21
+ @fmt ||= RogIQ::Formatters.new(format: options[:format], quiet: options[:quiet])
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "billing_stripe"
4
+
5
+ module RogIQ
6
+ module Commands
7
+ class Billing < RogIQ::Commands::Base
8
+ desc "coins ACCOUNT", "Rogue coin wallet + recent transactions"
9
+ def coins(identifier)
10
+ RogIQ.load_rails!
11
+ account = RogIQ::Helpers.resolve_account(identifier)
12
+ unless account
13
+ fmt.error_msg("Account not found")
14
+ exit 1
15
+ end
16
+
17
+ w = account.rogue_coin_wallet
18
+ unless w
19
+ fmt.output(wallet: nil, message: "No RogueCoinWallet for account")
20
+ return
21
+ end
22
+
23
+ txs = w.transactions.order(created_at: :desc).limit(25).map do |t|
24
+ [t.created_at.iso8601, t.transaction_type, t.amount, t.description.to_s.truncate(60)]
25
+ end
26
+ fmt.output(
27
+ account_id: account.id,
28
+ balance: w.balance,
29
+ lifetime_purchased: w.lifetime_purchased,
30
+ lifetime_spent: w.lifetime_spent,
31
+ recent_transactions: txs
32
+ )
33
+ end
34
+
35
+ desc "overages", "Pending usage overages (all accounts)"
36
+ method_option :limit, type: :numeric, default: 50
37
+ def overages
38
+ RogIQ.load_rails!
39
+ rows = ::UsageOverage.status_pending.includes(:account).order(created_at: :desc).limit(options[:limit]).map do |o|
40
+ [
41
+ o.id,
42
+ o.account&.name,
43
+ o.metric_type,
44
+ o.total_charge.to_f,
45
+ o.created_at.iso8601
46
+ ]
47
+ end
48
+ fmt.output(headers: %w[id account metric total_charge created_at], rows: rows)
49
+ end
50
+
51
+ desc "stripe SUBCOMMAND", "Stripe catalog (sync)"
52
+ subcommand "stripe", RogIQ::Commands::BillingStripe
53
+
54
+ desc "summary", "Enqueue Billing::DailyBillingSummaryJob (Slack)"
55
+ method_option :date, type: :string, desc: "YYYY-MM-DD (default yesterday)"
56
+ def summary
57
+ RogIQ.load_rails!
58
+ date = options[:date].present? ? Date.parse(options[:date]) : nil
59
+ ::Billing::DailyBillingSummaryJob.perform_later(date: date)
60
+ fmt.success("Billing::DailyBillingSummaryJob enqueued.")
61
+ end
62
+
63
+ desc "invoices ACCOUNT", "Recent invoices"
64
+ method_option :limit, type: :numeric, default: 20
65
+ def invoices(identifier)
66
+ RogIQ.load_rails!
67
+ account = RogIQ::Helpers.resolve_account(identifier)
68
+ unless account
69
+ fmt.error_msg("Account not found")
70
+ exit 1
71
+ end
72
+
73
+ rows = account.invoices.order(created_at: :desc).limit(options[:limit]).map do |inv|
74
+ [
75
+ inv.id,
76
+ inv.invoice_number,
77
+ inv.status,
78
+ inv.total.to_f,
79
+ inv.striven_invoice_id,
80
+ inv.created_at.iso8601
81
+ ]
82
+ end
83
+ fmt.output(
84
+ headers: %w[id invoice_number status total striven_invoice_id created_at],
85
+ rows: rows
86
+ )
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RogIQ
4
+ module Commands
5
+ class BillingStripe < RogIQ::Commands::Base
6
+ desc "sync", "Run rake stripe:ensure_subscription_prices"
7
+ def sync
8
+ RogIQ.load_rails!
9
+ Rails.application.load_tasks
10
+ Rake::Task["stripe:ensure_subscription_prices"].invoke
11
+ fmt.success("stripe:ensure_subscription_prices complete.")
12
+ end
13
+ end
14
+ end
15
+ end