killbill-aviate 2.4.0 → 2.4.1.pre.3

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: 5adcf4012c2b8055a67f0724c1b537e51c20f41456375f35d5670d2b232abbf5
4
- data.tar.gz: 190802f0514736ee87c8740ede305175254fe51d09bf1263e7f004e7f848b191
3
+ metadata.gz: 668e8f43e5ab2c7c9c617b13e6e741b355613950f64e850682174001f93ac03c
4
+ data.tar.gz: b8eaa6d33bd8029125ca1ecb6e78d6c29902730c15a75ecf74498814b2f78ef3
5
5
  SHA512:
6
- metadata.gz: 79e74b08be7e317e4d4f96a7dbae76019b1420f906da3cff97b39bfc4cad3a87e10ec36b0d961336eec9bbb20eb6bd73b8684998cf7245da5578693c1b8702a1
7
- data.tar.gz: e778000ade2913989a6e2b3e9038d129bbd6e5364d7fd9e97a4273b0536968246d26f939e1abc1b9261284e62d0a7af61878fd373dd5838478e64d27dbfb8655
6
+ metadata.gz: 57a7d1d3458d82f0891c67a3a243bc9a45b679a67e53c17703059d3a2e49454f5dd77a12fc4211f63a6492d94f2080eb5fd92e79e229b2facf68fabb83f6b29a
7
+ data.tar.gz: d4ecb9bad5c487cab51f82912a6daaabd424ad996d2a4d6fdd52ed2857371523c32ffa7dd9247a7676902056826a1d6ad71c016afa33362d0840b50100531e49
@@ -1,3 +1,2 @@
1
- //= link_tree ../images
2
1
  //= link_directory ../javascripts .js
3
2
  //= link_directory ../stylesheets .css
File without changes
@@ -47,15 +47,24 @@
47
47
  return result.concat(element.values);
48
48
  }, []);
49
49
 
50
- var x_domain = d3.extent(allValues, function (d) {
51
- return d.x;
52
- });
53
- self.x.domain(x_domain);
54
-
55
- var y_domain = d3.extent(allValues, function (d) {
56
- return d.y;
57
- });
58
- self.y.domain(y_domain);
50
+ // X domain: union of billing period bounds and actual data so that data
51
+ // points recorded just before the period start (e.g. on the charge date)
52
+ // remain visible.
53
+ var period_start = json.period_start ? helper.parseDate(json.period_start) : null;
54
+ var period_end = json.period_end ? helper.parseDate(json.period_end) : null;
55
+ var x_values = allValues.map(function (d) { return d.x; });
56
+ if (period_start) x_values.push(period_start);
57
+ if (period_end) x_values.push(period_end);
58
+ self.x.domain(d3.extent(x_values));
59
+
60
+ // Y domain: always start at 0 and use nice() for clean tick values.
61
+ var y_max = d3.max(allValues, function (d) { return d.y; }) || 1;
62
+ self.y.domain([0, y_max]).nice();
63
+
64
+ // Cap y-axis tick count to the domain max so integer formatting never
65
+ // produces duplicate labels (e.g. [0,3] with 10 ticks → "0,0,1,1,2,2,3").
66
+ var y_domain_max = self.y.domain()[1];
67
+ axes.y.ticks(Math.min(y_domain_max, 10));
59
68
 
60
69
  // Render axes
61
70
  axes.render(svg);
@@ -112,6 +121,17 @@
112
121
  yOffset += 22;
113
122
  });
114
123
 
124
+ // Extend each series to the right edge of the x domain so the cumulative
125
+ // step line is always visible — d3.curveStepAfter needs at least two points
126
+ // to produce a visible line segment; a single point produces only a move.
127
+ var x_domain_end = self.x.domain()[1];
128
+ datasets.forEach(function (dataset) {
129
+ var vals = dataset.values;
130
+ if (vals.length > 0 && vals[vals.length - 1].x < x_domain_end) {
131
+ vals.push({ x: x_domain_end, y: vals[vals.length - 1].y });
132
+ }
133
+ });
134
+
115
135
  // Render data lines (stepped)
116
136
  datasets.forEach(function (dataset) {
117
137
  var data = dataset.values,
@@ -49,7 +49,7 @@ module Aviate
49
49
  @wallet_balance = fetch_wallet_balance(@account_id, cached_options)
50
50
 
51
51
  # Build chart JSON for the view
52
- @chart_json = build_chart_json(@chart_data)
52
+ @chart_json = build_chart_json(@chart_data, start_date, end_date)
53
53
  end
54
54
 
55
55
  private
@@ -89,6 +89,23 @@ module Aviate
89
89
  end
90
90
 
91
91
  def fetch_unit_types(subscription, start_date, cached_options)
92
+ if Killbill::Aviate::AviateClient.aviate_catalog?(subscription.account_id, cached_options)
93
+ fetch_billing_meter_codes(subscription, cached_options)
94
+ else
95
+ fetch_catalog_phase_unit_types(subscription, start_date, cached_options)
96
+ end
97
+ end
98
+
99
+ # Aviate catalogs record usage against a billing meter code, resolved via the Aviate plugin's
100
+ # own (JWT-authenticated) Catalog API, rather than a standard catalog unit type.
101
+ def fetch_billing_meter_codes(subscription, cached_options)
102
+ Killbill::Aviate::AviateClient.billing_meter_codes_for_plan(subscription.plan_name, subscription.account_id, cached_options)
103
+ rescue StandardError => e
104
+ Rails.logger.warn("Failed to fetch Aviate billing meter codes: #{e.message}")
105
+ []
106
+ end
107
+
108
+ def fetch_catalog_phase_unit_types(subscription, start_date, cached_options)
92
109
  # NOTE: KillBillClient::Model::Catalog.get_catalog_phase parses the response as a
93
110
  # Catalog object by default, but the API actually returns a Phase-shaped payload
94
111
  # (it has no `usages` accessor otherwise) - request it as a Phase explicitly.
@@ -173,14 +190,17 @@ module Aviate
173
190
  chart_data
174
191
  end
175
192
 
176
- def build_chart_json(chart_data)
193
+ def build_chart_json(chart_data, period_start = nil, period_end = nil)
177
194
  return nil if chart_data.empty?
178
195
 
179
196
  series = chart_data.map do |unit_type, values|
180
197
  { 'name' => unit_type, 'values' => values }
181
198
  end
182
199
 
183
- { 'type' => 'TIMELINE', 'data' => series }.to_json
200
+ json = { 'type' => 'TIMELINE', 'data' => series }
201
+ json['period_start'] = period_start.to_s if period_start
202
+ json['period_end'] = period_end.to_s if period_end
203
+ json.to_json
184
204
  end
185
205
 
186
206
  def fetch_accrued_cost(account_id, cached_options)
@@ -1,5 +1,10 @@
1
1
  <div class="kaui-container usage-show">
2
- <%= render "kaui/components/breadcrumb/breadcrumb" %>
2
+ <%= render "kaui/components/breadcrumb/breadcrumb", breadcrumbs: [
3
+ { label: 'Accounts', href: '/accounts' },
4
+ { label: @account.name.presence || "Account #{@account_id}", href: "/accounts/#{@account_id}" },
5
+ { label: 'Usage', href: "/aviate/accounts/#{@account_id}/usage" },
6
+ { label: 'Usage Detail' }
7
+ ] %>
3
8
  <div class="d-flex" style="gap: 4rem;">
4
9
  <%= render :template => 'kaui/layouts/kaui_account_sidebar' %>
5
10
  <div class="usage-content" style="max-width: 67.5rem; width: 100%;">
data/lib/aviate/client.rb CHANGED
@@ -5,6 +5,8 @@ module Killbill
5
5
  class AviateClient < KillBillClient::Model::Resource
6
6
  KILLBILL_AVIATE_PREFIX = '/plugins/aviate-plugin/v1'
7
7
 
8
+ CATALOG_STATE_AVIATE = 'CATALOG_STATE_AVIATE'
9
+
8
10
  class << self
9
11
  def aviate_plugin_available?(options = nil)
10
12
  path = "#{KILLBILL_AVIATE_PREFIX}/health/data"
@@ -17,6 +19,51 @@ module Killbill
17
19
  [false, e.message.to_s]
18
20
  end
19
21
 
22
+ # Returns true if the account/subscription is billed via an Aviate catalog (as opposed to a
23
+ # standard XML one). Any error (plugin not installed, missing/invalid JWT, network issue, etc.)
24
+ # is treated as "not Aviate" so callers can safely fall back to the standard catalog behavior.
25
+ #
26
+ # JWT authentication is handled by build_request_options, which sets the Authorization header
27
+ # from options[:jwt_token] when present (populated from the session cookie by EngineController).
28
+ def aviate_catalog?(account_id, options = nil)
29
+ path = "#{KILLBILL_AVIATE_PREFIX}/catalog/info"
30
+ request_options = build_request_options(options)
31
+
32
+ response = KillBillClient::API.get path, catalog_params(account_id), request_options
33
+ JSON.parse(response.body)['state'] == CATALOG_STATE_AVIATE
34
+ rescue StandardError
35
+ false
36
+ end
37
+
38
+ # Returns the list of billing meter codes configured on the given plan's usage phases.
39
+ # Aviate catalogs record usage against a billing meter code rather than a standard
40
+ # catalog unit type, so this must be used instead of the plain KillBillClient catalog/phase
41
+ # lookup whenever `aviate_catalog?` is true.
42
+ # JWT authentication is handled by build_request_options (see aviate_catalog? above).
43
+ def billing_meter_codes_for_plan(plan_name, account_id, options = nil)
44
+ return [] if plan_name.blank?
45
+
46
+ path = "#{KILLBILL_AVIATE_PREFIX}/catalog/#{plan_name}/plan"
47
+ request_options = build_request_options(options)
48
+
49
+ response = KillBillClient::API.get path, catalog_params(account_id), request_options
50
+ plan = JSON.parse(response.body)
51
+
52
+ codes = []
53
+ Array(plan['phases']).each do |phase|
54
+ Array(phase['usages']).each do |usage|
55
+ Array(usage['tiers']).each do |tier|
56
+ Array(tier['blocks']).each do |block|
57
+ codes << block['billingMeterCode'] if block['billingMeterCode'].present?
58
+ end
59
+ end
60
+ end
61
+ end
62
+ codes.uniq
63
+ rescue StandardError
64
+ []
65
+ end
66
+
20
67
  def aviate_plugin_installed(options = nil)
21
68
  nodes_info = KillBillClient::Model::NodesInfo.nodes_info(options) || []
22
69
  plugins_info = nodes_info.empty? ? [] : (nodes_info.first.plugins_info || [])
@@ -117,18 +164,39 @@ module Killbill
117
164
 
118
165
  private
119
166
 
167
+ def catalog_params(account_id)
168
+ account_id.present? ? { accountId: account_id } : {}
169
+ end
170
+
120
171
  def parse_csv_response(body)
121
172
  return [] if body.nil? || body.strip.empty?
122
173
 
123
- require 'csv'
124
- rows = CSV.parse(body, headers: true)
125
- rows.map do |row|
126
- {
127
- date: row['timestamp']&.strip,
128
- value: row['value']&.strip&.to_f
129
- }
174
+ # The host_samples endpoint returns a JSON array. Each element has a
175
+ # "samples" field with usage data serialized as flat comma-separated
176
+ # timestamp/value pairs: "ts1,val1,ts2,val2,..." (observed format).
177
+ # Newlines, if ever present, are normalized to commas before parsing.
178
+ parsed = JSON.parse(body)
179
+ return [] unless parsed.is_a?(Array)
180
+
181
+ result = []
182
+ parsed.each do |entry|
183
+ samples_str = entry['samples']
184
+ next if samples_str.nil? || samples_str.strip.empty?
185
+
186
+ tokens = samples_str.tr("\n", ',').split(',').map(&:strip).reject(&:empty?)
187
+ tokens.each_slice(2) do |ts_str, val_str|
188
+ next if ts_str.nil? || val_str.nil?
189
+
190
+ ts = ts_str.to_i
191
+ next if ts.zero?
192
+
193
+ date = Time.at(ts).utc.strftime('%Y-%m-%dT%H:%M:%S')
194
+ result << { date: date, value: val_str.to_f }
195
+ end
130
196
  end
131
- rescue CSV::MalformedCSVError
197
+
198
+ result
199
+ rescue StandardError
132
200
  []
133
201
  end
134
202
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aviate
4
- VERSION = '2.4.0'
4
+ VERSION = '2.4.1.pre.3'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: killbill-aviate
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.4.1.pre.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kill Bill core team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-05 00:00:00.000000000 Z
11
+ date: 2026-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: killbill-assets-ui
@@ -76,6 +76,7 @@ files:
76
76
  - README.md
77
77
  - Rakefile
78
78
  - app/assets/config/aviate_manifest.js
79
+ - app/assets/images/aviate/.keep
79
80
  - app/assets/javascripts/aviate/aviate.js
80
81
  - app/assets/javascripts/aviate/kiddo/axes.js
81
82
  - app/assets/javascripts/aviate/kiddo/charts/line_chart.js
@@ -124,9 +125,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
124
125
  version: '0'
125
126
  required_rubygems_version: !ruby/object:Gem::Requirement
126
127
  requirements:
127
- - - ">="
128
+ - - ">"
128
129
  - !ruby/object:Gem::Version
129
- version: '0'
130
+ version: 1.3.1
130
131
  requirements: []
131
132
  rubygems_version: 3.4.10
132
133
  signing_key: