kaui 4.0.24 → 4.0.25

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: 19115a4ca850dfd755598a560dc66e96b00694f00b305e057260fd6b13c57e3d
4
- data.tar.gz: 9780699cd5e97d8c6887b3e0c200d968a11e644afd5496c58829277d558a28bc
3
+ metadata.gz: 908a750ff22253e88a65a7330cd770b76335a4a334435c119730259ec0d34e92
4
+ data.tar.gz: b74d7a880dcf04a21a890b617c5584115eb03376d38659cb261125b80af51f29
5
5
  SHA512:
6
- metadata.gz: 8bcf2e6e322833277c4d305aca8621bf4495cc6443961af0a565bf7da0d6efdb5892d8b66ac31e820f08953d221ad54b309018fbd3d088032e211ea73d51abd9
7
- data.tar.gz: e1a44d558e7b996185be6a79cca68fb2eecb4e200f2affff7e5de3ccf0ca6a38f78d0ffdc814c67f3ab794585e6d7b4c4604dfe06ea77e7b7bc86360e229273e
6
+ metadata.gz: bda938be812b9b0902d96ca7524e35283b00cb50654bfaffeeea41bf8dc6a1eaa021ee01c81fd0e7fb4b76adbc9652e52bee2ae07422f9763fe955198238bd02
7
+ data.tar.gz: 3e004500ec5e7c98f216c9642612537daa77373599a1b0c987d64cb4d33867a19e5cb0eda628ecec26e5eb58c8da2170ca152cbf5b7473469ccc17be6f98e6b3
@@ -2,6 +2,15 @@
2
2
  min-height: 100vh;
3
3
  width: 100%;
4
4
  background-color: #fff;
5
+ overflow: hidden;
6
+ }
7
+
8
+ .auth-container .centered-absolute {
9
+ position: relative;
10
+ top: 0;
11
+ left: 0;
12
+ transform: none;
13
+ margin: 1.5rem auto 0;
5
14
  }
6
15
 
7
16
  .auth-form-container {
@@ -16,209 +16,3 @@
16
16
  image-url("glyphicons-halflings-regular.ttf") format("truetype"),
17
17
  image-url("glyphicons-halflings-regular.svg#glyphicons_halflingsregular") format("svg");
18
18
  }
19
-
20
- .tag-bar {
21
- border-bottom: 0.0625rem solid #ddd;
22
- margin-bottom: 0;
23
- position: relative;
24
- }
25
-
26
- .tag-bar .tag-select {
27
- display: inline-block;
28
- vertical-align: middle;
29
- }
30
-
31
- @media (max-width: 28.125rem) {
32
- .tag-bar .tag-select {
33
- width: 35%;
34
- }
35
- }
36
-
37
- .tag-bar .tag-select span {
38
- padding: 0.3125rem 1.25rem;
39
- text-align: center;
40
- display: block;
41
- border-right: 0.0625rem solid #ddd;
42
- }
43
-
44
- @media (max-width: 48rem) {
45
- .tag-bar .tag-select span {
46
- border: 0;
47
- }
48
- }
49
-
50
- .tag-bar .tag-select span i {
51
- margin: 0 0.3125rem;
52
- color: #515151;
53
- font-size: 1rem;
54
- }
55
-
56
- .tag-bar .tag-select span i.fa-caret-down {
57
- color: #999999;
58
- font-size: 0.75rem;
59
- }
60
-
61
- .tag-bar .tag-select span i.fa-tag {
62
- font-size: 1.125rem;
63
- }
64
-
65
- .tag-bar .tag-select span i.fa-money,
66
- .tag-bar .tag-select span i.fa-credit-card {
67
- font-size: 1rem;
68
- }
69
-
70
- .tag-bar .tag-select:hover .tag-select-box {
71
- max-height: 31.25rem;
72
- padding: 1.25rem 1.5625rem;
73
- border: 0.0625rem solid #ddd;
74
- box-shadow: 0.0625rem 0.1875rem 0.125rem rgba(0, 0, 0, 0.11);
75
- }
76
-
77
- .tag-bar .tag-select .tag-select-box {
78
- position: absolute;
79
- top: 100%;
80
- left: -0.0625rem;
81
- max-height: 0;
82
- background-color: #fff;
83
- z-index: 100;
84
- padding: 0 0.9375rem;
85
- font-size: 0.75rem;
86
- color: #999999;
87
- overflow: auto;
88
- width: auto;
89
- border-bottom: 0;
90
- display: inline-block;
91
- }
92
-
93
- @media (max-width: 48rem) {
94
- .tag-bar .tag-select .tag-select-box {
95
- border: 0;
96
- }
97
- }
98
-
99
- .tag-bar .tag-select .tag-select-box strong {
100
- display: block;
101
- font-weight: 400;
102
- margin: 0 0 0.625rem;
103
- }
104
-
105
- .tag-bar .tag-select .tag-select-box form {
106
- display: inline-block;
107
- width: 13.125rem;
108
- }
109
-
110
- .tag-bar .tag-select .tag-select-box input[type="checkbox"] {
111
- display: inline-block;
112
- width: 9%;
113
- margin: 0 1% 0 0;
114
- line-height: 1.25rem;
115
- vertical-align: middle;
116
- }
117
-
118
- .tag-bar .tag-select .tag-select-box label {
119
- font-size: 0.75rem;
120
- display: inline-block;
121
- width: 85%;
122
- line-height: 1.25rem;
123
- vertical-align: middle;
124
- margin: 0;
125
- font-weight: 400;
126
- }
127
-
128
- .tag-bar .tag-select .tag-select-box textarea {
129
- resize: none;
130
- height: 5rem;
131
- margin: 0 0 1.25rem;
132
- width: 100%;
133
- }
134
-
135
- .tag-bar .tag-select .tag-select-box a {
136
- display: block;
137
- font-size: 0.75rem;
138
- line-height: 1.25rem;
139
- }
140
-
141
- .tag-bar .tag-selected {
142
- display: inline-block;
143
- padding: 0.125rem 0.3125rem;
144
- vertical-align: middle;
145
- }
146
-
147
- @media (max-width: 48rem) {
148
- .tag-bar .tag-selected {
149
- width: 64%;
150
- padding: 0.125rem;
151
- overflow: hidden;
152
- border-left: 0.0625rem solid #ddd;
153
- }
154
- }
155
-
156
- .tag-bar .tag-selected span {
157
- display: inline-block;
158
- font-size: 0.75rem;
159
- color: #777777;
160
- background: #e5e5e5;
161
- padding: 0.1875rem 1.25rem;
162
- margin: 0 0.3125rem;
163
- transition: all 0.5s;
164
- cursor: pointer;
165
- }
166
-
167
- .tag-bar .tag-selected span:hover {
168
- background-color: #cce9eb;
169
- }
170
-
171
- @media (max-width: 48rem) {
172
- .tag-bar .tag-selected span {
173
- font-size: 0.625rem;
174
- padding: 0.1875rem 0.3125rem;
175
- display: block;
176
- margin: 0 0 0.125rem;
177
- }
178
- }
179
-
180
- .tag-bar-breathe {
181
- margin-bottom: 1.875rem;
182
- }
183
-
184
- .tag-bar-no-border {
185
- border-bottom: 0;
186
- }
187
-
188
- .tag-bar-no-border .tag-select span {
189
- border-right: 0;
190
- }
191
-
192
- #main-menu .tag-bar .tag-select {
193
- position: relative;
194
- }
195
-
196
- #main-menu .tag-bar .tag-select-box {
197
- word-break: keep-all;
198
- white-space: nowrap;
199
- }
200
-
201
- .inline-row-tag-bar {
202
- padding-left: 0.625rem !important;
203
- }
204
-
205
- .inline-row-tag-bar .tag-bar .tag-select span {
206
- padding: 0.3125rem;
207
- border-right: none;
208
- }
209
-
210
- .inline-row-tag-bar .tag-bar .tag-select span i {
211
- margin: 0 0.1875rem;
212
- font-size: 0.75rem;
213
- }
214
-
215
- .inline-row-tag-bar .tag-bar .tag-select span i.fa-tag {
216
- font-size: 0.75rem;
217
- }
218
-
219
- .inline-row-tag-bar .tag-bar .tag-select-box label {
220
- white-space: nowrap;
221
- }
222
-
223
-
224
- /*# sourceMappingURL=kaui2.css.map */
@@ -15,7 +15,7 @@
15
15
  }
16
16
  }
17
17
 
18
- .tag-bar .tag-select span {
18
+ .tag-bar .tag-select > span {
19
19
  padding: 0.3125rem 1.25rem;
20
20
  text-align: center;
21
21
  display: block;
@@ -23,7 +23,7 @@
23
23
  }
24
24
 
25
25
  @media (max-width: 48rem) {
26
- .tag-bar .tag-select span {
26
+ .tag-bar .tag-select > span {
27
27
  border: 0;
28
28
  }
29
29
  }
@@ -67,9 +67,8 @@
67
67
  font-size: 0.75rem;
68
68
  color: #999999;
69
69
  overflow: hidden;
70
- min-width: 15rem;
71
- width: max-content;
72
- max-width: min(25rem, 95vw);
70
+ width: 17rem;
71
+ max-width: min(17rem, 95vw);
73
72
  border-bottom: 0;
74
73
  display: inline-block;
75
74
  }
@@ -107,6 +106,7 @@
107
106
  vertical-align: middle;
108
107
  margin: 0;
109
108
  font-weight: 400;
109
+ overflow-wrap: anywhere;
110
110
  }
111
111
 
112
112
  .tag-bar .tag-select .tag-select-box textarea {
@@ -169,7 +169,7 @@
169
169
  border-bottom: 0;
170
170
  }
171
171
 
172
- .tag-bar-no-border .tag-select span {
172
+ .tag-bar-no-border .tag-select > span {
173
173
  border-right: 0;
174
174
  }
175
175
 
@@ -186,7 +186,7 @@
186
186
  padding-left: 0.625rem !important;
187
187
  }
188
188
 
189
- .inline-row-tag-bar .tag-bar .tag-select span {
189
+ .inline-row-tag-bar .tag-bar .tag-select > span {
190
190
  padding: 0.3125rem;
191
191
  border-right: none;
192
192
  }
@@ -156,7 +156,15 @@ module Kaui
156
156
  @children = wait(fetch_children)
157
157
  @account_parent = @account.parent_account_id.nil? ? nil : wait(fetch_parent)
158
158
  @email_notification_configuration = is_email_notifications_plugin_available ? wait(fetch_email_notification_configuration) : []
159
- @bundles = wait(fetch_bundles)
159
+ @bundles = begin
160
+ wait(fetch_bundles)
161
+ rescue KillBillClient::API::ResponseError => e
162
+ # e.g. a subscription references a plan that no longer exists in the currently loaded catalog.
163
+ # Don't fail the whole account page for this, just warn and show it without bundle-derived data.
164
+ Rails.logger.warn("Unable to fetch bundles for account #{@account.account_id}: #{as_string(e)}")
165
+ flash.now[:warning] = "Unable to load some subscription details: #{as_string(e)}"
166
+ []
167
+ end
160
168
 
161
169
  @last_transaction_by_payment_method_id = {}
162
170
  wait(fetch_payments).each do |payment|
@@ -24,6 +24,18 @@ module Kaui
24
24
  DateTime.parse(date_s).strftime('%FT%T')
25
25
  end
26
26
 
27
+ # Best-effort epoch (seconds) timestamp for a timeline entry's date/time
28
+ # value, used purely for client-side "Sort by Date" ordering on the
29
+ # account timeline page. Returns nil if the date is missing or cannot be
30
+ # parsed (entries without a usable timestamp are sorted last).
31
+ def timeline_sort_timestamp(date)
32
+ return nil if date.blank?
33
+
34
+ DateTime.parse(date.to_s).to_i
35
+ rescue ArgumentError, TypeError
36
+ nil
37
+ end
38
+
27
39
  # Retrieve current killbill server time based on a time zone.
28
40
  # if no time zone is provided it will return UTC.
29
41
  # if the killbill server is not reachable it will return the time of the Kaui server
@@ -21,10 +21,13 @@ module Dependencies
21
21
  # Any error (plugin not installed, missing/invalid JWT, network issue, etc.) is treated as
22
22
  # "not Aviate" so that callers can safely fall back to the existing XML-catalog behavior.
23
23
  def aviate_catalog?(account_id, options_for_klient)
24
- response = KillBillClient::API.get("#{KILLBILL_AVIATE_PREFIX}/catalog/info", catalog_params(account_id), request_options(options_for_klient))
25
- JSON.parse(response.body)['state'] == CATALOG_STATE_AVIATE
26
- rescue StandardError => e
27
- Rails.logger.warn("Failed to retrieve Aviate catalog info: #{e.class}: #{e.message}")
24
+ params_list = account_id.present? ? [{ accountId: account_id }, {}] : [{}]
25
+ params_list.each do |params|
26
+ response = KillBillClient::API.get("#{KILLBILL_AVIATE_PREFIX}/catalog/info", params, request_options(options_for_klient))
27
+ return true if JSON.parse(response.body)['state'] == CATALOG_STATE_AVIATE
28
+ rescue StandardError => e
29
+ Rails.logger.warn("Failed to retrieve Aviate catalog info (params=#{params}): #{e.class}: #{e.message}")
30
+ end
28
31
  false
29
32
  end
30
33
 
@@ -49,7 +49,14 @@
49
49
  </div>
50
50
  </form>
51
51
 
52
- <div>
52
+ <div class="form-horizontal d-flex gap-2 mb-4 mx-0">
53
+ <div class="form-group p-0 d-flex align-items-center gap-2">
54
+ <label for="group-by" class="control-label mb-0 text-nowrap">Group by</label>
55
+ <%= select_tag 'group-by', options_for_select([['Object Type', 'type'], ['Date', 'date']], 'type'), :class => 'form-control' %>
56
+ </div>
57
+ </div>
58
+
59
+ <div id="timeline-entries">
53
60
  <% if @account.present? %>
54
61
  <% @invoices.each do |invoice_stub| %>
55
62
  <% if invoice_stub.bundle_keys.present? %>
@@ -60,7 +67,7 @@
60
67
  <% else %>
61
68
  <% invoice = invoice_stub %>
62
69
  <% end %>
63
- <div title="<%= bundles %>" data-event-type="INVOICE">
70
+ <div title="<%= bundles %>" data-event-type="INVOICE" data-timestamp="<%= timeline_sort_timestamp(invoice.target_date) %>">
64
71
  <div class="datetime-container">
65
72
  <% if invoice.target_date.present? %>
66
73
  <%= invoice.target_date %>
@@ -148,7 +155,7 @@
148
155
  <% invoice = nil %>
149
156
  <% end %>
150
157
  <% payment.transactions.each do |transaction| %>
151
- <div title="<%= bundles %>" data-event-type="<%= transaction.transaction_type %>">
158
+ <div title="<%= bundles %>" data-event-type="<%= transaction.transaction_type %>" data-timestamp="<%= timeline_sort_timestamp(transaction.effective_date) %>">
152
159
  <div class="datetime-container">
153
160
  <% if transaction.effective_date.present? %>
154
161
  <%= format_date(transaction.effective_date, @account.time_zone).html_safe %>
@@ -229,7 +236,7 @@
229
236
  <% bundle.subscriptions.each do |sub| %>
230
237
  <% sub.events.each do |event| %>
231
238
  <% next if event.event_type == 'SERVICE_STATE_CHANGE' %>
232
- <div title="<%= @bundle_names[bundle.external_key] %>" data-event-type="<%= event.event_type %>">
239
+ <div title="<%= @bundle_names[bundle.external_key] %>" data-event-type="<%= event.event_type %>" data-timestamp="<%= timeline_sort_timestamp(event.effective_date) %>">
233
240
  <div class="datetime-container">
234
241
  <% if event.effective_date.present? %>
235
242
  <%= format_date(event.effective_date, @account.time_zone).html_safe %>
@@ -352,6 +359,39 @@
352
359
  });
353
360
  }
354
361
 
362
+ var originalTimelineEntriesOrder = null;
363
+
364
+ function sortTimelineEntries(sortByDate) {
365
+ var $container = $('#timeline-entries');
366
+ var $entries = $container.children('[data-event-type]');
367
+
368
+ if (originalTimelineEntriesOrder === null) {
369
+ // Cache the initial (grouped-by-object-type) DOM order the first time we need it
370
+ originalTimelineEntriesOrder = $entries.get();
371
+ }
372
+
373
+ var orderedEntries;
374
+ if (sortByDate) {
375
+ orderedEntries = $entries.get().sort(function (a, b) {
376
+ var x = parseInt($(a).attr('data-timestamp'), 10);
377
+ var y = parseInt($(b).attr('data-timestamp'), 10);
378
+ x = isNaN(x) ? -Infinity : x;
379
+ y = isNaN(y) ? -Infinity : y;
380
+ // Most recent first; entries without a usable timestamp sort last
381
+ return y - x;
382
+ });
383
+ } else {
384
+ orderedEntries = originalTimelineEntriesOrder;
385
+ }
386
+
387
+ $.each(orderedEntries, function (_, entry) {
388
+ $container.append(entry);
389
+ });
390
+
391
+ // Re-apply any active filters, since the DOM order just changed
392
+ filterTimeline();
393
+ }
394
+
355
395
  function filterTimeline() {
356
396
  var selectedBundle = $("#bundles").val();
357
397
  var selectedPayment = $('#payment_filter').val().toUpperCase();
@@ -424,6 +464,11 @@
424
464
  clearTimeout(searchTimeout);
425
465
  searchTimeout = setTimeout(filterTimeline, 300);
426
466
  });
467
+
468
+ $('#group-by').on('change', function() {
469
+ sortTimelineEntries($(this).val() === 'date');
470
+ });
471
+
427
472
 
428
473
 
429
474
 
@@ -6,6 +6,7 @@
6
6
  <%= link_to 'Account', kaui_engine.account_path(params[:account_id]), :class => (params[:controller].ends_with?('accounts') ? 'current' : 'non-current') %>
7
7
  <% if aviate_plugin_installed %>
8
8
  <%= link_to 'Wallets', aviate_engine.account_wallets_path(params[:account_id]), :class => (params[:controller].ends_with?('wallets') ? 'current' : 'non-current') %>
9
+ <%= link_to 'Usage', aviate_engine.account_usage_index_path(params[:account_id]), :class => (params[:controller].ends_with?('usage') ? 'current' : 'non-current') %>
9
10
  <% end %>
10
11
  <%= link_to 'Subscriptions', kaui_engine.account_bundles_path(params[:account_id]), :class => (params[:controller].ends_with?('bundles') ? 'current' : 'non-current') %>
11
12
  <%= link_to 'Invoices', kaui_engine.account_invoices_path(params[:account_id]), :class => (params[:controller].ends_with?('invoices') ? 'current' : 'non-current') %>
@@ -15,6 +15,7 @@
15
15
 
16
16
  <% if aviate_plugin_installed %>
17
17
  <% menu_items << { label: 'Wallets', path: aviate_engine.account_wallets_path(params[:account_id]), controller_suffix: 'wallets', icon_path: 'kaui/sidebar/invoices.svg' } %>
18
+ <% menu_items << { label: 'Usage', path: aviate_engine.account_usage_index_path(params[:account_id]), controller_suffix: 'usage', icon_path: 'kaui/sidebar/subscriptions.svg' } %>
18
19
  <% end %>
19
20
 
20
21
  <% if current_user.root? %>
@@ -215,6 +215,14 @@
215
215
  path: kaui_engine.record_usage_path(id: sub.subscription_id),
216
216
  icon: "kaui/subscription/change.svg"
217
217
  } %>
218
+
219
+ <% if aviate_plugin_installed %>
220
+ <% menu_items << {
221
+ label: "View Usage",
222
+ path: aviate_engine.usage_subscription_path(id: sub.subscription_id),
223
+ icon: "kaui/subscription/change.svg"
224
+ } %>
225
+ <% end %>
218
226
  <% end %>
219
227
 
220
228
  <% if can?(:transfer, Kaui::Bundle) && sub.product_category != 'ADD_ON' %>
data/lib/kaui/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kaui
4
- VERSION = '4.0.24'
4
+ VERSION = '4.0.25'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kaui
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.24
4
+ version: 4.0.25
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-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack