kaui 4.0.12 → 4.0.14

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/images/kaui/save.svg +5 -0
  3. data/app/assets/javascripts/kaui/kaui_override.js +21 -0
  4. data/app/assets/javascripts/kaui/multi_functions_bar_utils.js +183 -7
  5. data/app/assets/stylesheets/kaui/common.css +4 -1
  6. data/app/assets/stylesheets/kaui/subscription.css +75 -0
  7. data/app/assets/stylesheets/kaui/tags.css +6 -3
  8. data/app/controllers/kaui/accounts_controller.rb +1 -1
  9. data/app/controllers/kaui/admin_tenants_controller.rb +22 -8
  10. data/app/controllers/kaui/bundles_controller.rb +53 -5
  11. data/app/controllers/kaui/engine_controller_util.rb +2 -2
  12. data/app/controllers/kaui/invoices_controller.rb +1 -1
  13. data/app/helpers/kaui/plugin_helper.rb +3 -5
  14. data/app/helpers/kaui/subscription_helper.rb +18 -0
  15. data/app/models/kaui/bundle.rb +6 -0
  16. data/app/models/kaui/overdue.rb +5 -2
  17. data/app/views/kaui/accounts/_account_details.html.erb +1 -1
  18. data/app/views/kaui/accounts/_account_filterbar.html.erb +34 -1
  19. data/app/views/kaui/accounts/_form_account.html.erb +1 -1
  20. data/app/views/kaui/accounts/_functions_bar.html.erb +3 -1
  21. data/app/views/kaui/accounts/edit.html.erb +9 -0
  22. data/app/views/kaui/accounts/index.html.erb +20 -0
  23. data/app/views/kaui/accounts/new.html.erb +14 -0
  24. data/app/views/kaui/admin_tenants/_show_overdue.erb +1 -1
  25. data/app/views/kaui/admin_tenants/new_overdue_config.html.erb +25 -1
  26. data/app/views/kaui/bundles/_bundle_filterbar.html.erb +133 -0
  27. data/app/views/kaui/bundles/index.html.erb +35 -3
  28. data/app/views/kaui/components/menu_dropdown/_menu_dropdown.html.erb +2 -2
  29. data/app/views/kaui/invoices/_invoice_filterbar.html.erb +42 -4
  30. data/app/views/kaui/invoices/_multi_functions_bar.html.erb +9 -10
  31. data/app/views/kaui/invoices/index.html.erb +27 -11
  32. data/app/views/kaui/layouts/kaui_application.html.erb +10 -10
  33. data/app/views/kaui/layouts/kaui_flash.html.erb +5 -26
  34. data/app/views/kaui/payments/_multi_functions_bar.html.erb +9 -9
  35. data/app/views/kaui/payments/_payment_filterbar.html.erb +42 -4
  36. data/app/views/kaui/payments/index.html.erb +12 -0
  37. data/app/views/kaui/refunds/_form.html.erb +3 -11
  38. data/app/views/kaui/subscriptions/_subscriptions_table.html.erb +1 -1
  39. data/config/locales/en.yml +1 -0
  40. data/lib/kaui/version.rb +1 -1
  41. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7cae94ec25bd96299f58f2bd783dabf34441f203d38fb2846e0f92b59b97e2a
4
- data.tar.gz: 04f9d02da92a77eb81160518cad11a5c88af312d20b1c65cfc0d3ad141814801
3
+ metadata.gz: e5d83c894b4705f72b548117b270bd7ea3c0d1fb962be8a49198dd81771c8c79
4
+ data.tar.gz: f95c3ff23e3b8729036252c2f45b2b2294bdb14dc474bf8666c188714bb29eff
5
5
  SHA512:
6
- metadata.gz: b99fa2fe8751882f22e754e79a1b1eacc1a7933a039e572bbfa22cf880d1967b7e8c8156d04d98cc6511c9b2b6ae634f9ba3ff24102bc4341072b2422dd41e68
7
- data.tar.gz: f494903ee370dd0a6bb10808bde638eedd769619cb7e0c70b321c74c9b246ea09ebc00f4d9976cfac8765c53a43fd724daace311ea59b15288d7adb070fb20cb
6
+ metadata.gz: f0ea477030153cfad26f64b1e8840a72613362a6a0655cf34e5c91c53439d1b67953b2b2c76b4cc1622c171389bb1676b7aa742fc3b90ec3e21b4d897c343a66
7
+ data.tar.gz: 5f0a1131795c3072024624fc7c55aace8111e8ec65cbf2dc4878b200bf4e6019ef6bf826e4d54364145b2a904fec983fa5f77fdaa809f2dbef43e2cd73bf6895
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#A4A7AE" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
2
+ <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
3
+ <polyline points="17 21 17 13 7 13 7 21"/>
4
+ <polyline points="7 3 7 8 15 8"/>
5
+ </svg>
@@ -302,6 +302,27 @@ jQuery(document).ready(function ($) {
302
302
 
303
303
  setObjectIdPopover();
304
304
  setObjectIdTooltip();
305
+
306
+ /*
307
+ * Tag dropdown overflow prevention
308
+ * Flips each .tag-select-box to open leftward when it would overflow the right edge of the viewport.
309
+ */
310
+ function repositionTagDropdowns() {
311
+ $('.tag-select').each(function() {
312
+ var $box = $(this).find('.tag-select-box');
313
+ var triggerRight = $(this).offset().left + $(this).outerWidth();
314
+ var boxWidth = Math.max($box[0].scrollWidth, 240);
315
+ var viewportWidth = $(window).width();
316
+ if (triggerRight + boxWidth > viewportWidth) {
317
+ $box.css({ left: 'auto', right: '0' });
318
+ } else {
319
+ $box.css({ left: '0', right: 'auto' });
320
+ }
321
+ });
322
+ }
323
+
324
+ repositionTagDropdowns();
325
+ $(window).on('resize', repositionTagDropdowns);
305
326
  });
306
327
 
307
328
 
@@ -84,11 +84,6 @@ function populateSearchLabelsFromUrl() {
84
84
 
85
85
  function searchQuery(account_id){
86
86
  var searchFields = $('.search-field');
87
- var searchLabelsContainer = $('#search-labels-container');
88
-
89
- if (searchFields.length > 0) {
90
- searchLabelsContainer.empty();
91
- }
92
87
  var searchLabels = '';
93
88
  if (searchFields.length > 0) {
94
89
  searchLabels = searchFields.map(function() {
@@ -135,17 +130,25 @@ function searchQuery(account_id){
135
130
  };
136
131
 
137
132
  function clearAdvanceSearch() {
133
+ var hasActiveSearch = $('#search-labels-container .label').length > 0 ||
134
+ window.location.search.includes('_q=1');
135
+
138
136
  // Clear all search fields
139
137
  $('#search-fields-container').empty();
140
138
 
141
139
  // Remove all search labels
142
140
  $('#search-labels-container').empty();
143
141
 
144
- // Reload the page with the original URL (no parameters)
145
- window.location.href = window.location.pathname;
142
+ // Clear Persisted Search for this page
143
+ localStorage.removeItem('kaui_adv_search_' + window.location.pathname);
146
144
 
147
145
  // Hide the modal
148
146
  $('#advanceSearchModal').modal('hide');
147
+
148
+ // Only reload if there was an active search to clear
149
+ if (hasActiveSearch) {
150
+ window.location.href = window.location.pathname;
151
+ }
149
152
  }
150
153
 
151
154
  function showAdvanceSearchModal() {
@@ -230,4 +233,177 @@ $(document).on('click', '.filter-close-icon', function() {
230
233
  var newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
231
234
  window.history.pushState({ path: newUrl }, '', newUrl);
232
235
  }
236
+ });
237
+
238
+ // --- Saved Searches Logic ---
239
+ function getSavedSearches() {
240
+ var key = 'kaui_saved_searches_' + window.location.pathname;
241
+ var data = localStorage.getItem(key);
242
+ if (!data) return {};
243
+ try {
244
+ var parsed = JSON.parse(data);
245
+ if (typeof parsed === 'object' && parsed !== null) {
246
+ return parsed;
247
+ }
248
+ } catch (e) {
249
+ // Legacy string format or invalid JSON
250
+ var legacy = {};
251
+ legacy["Default"] = data;
252
+ localStorage.setItem(key, JSON.stringify(legacy));
253
+ return legacy;
254
+ }
255
+ return {};
256
+ }
257
+
258
+ function saveSavedSearches(searches) {
259
+ var key = 'kaui_saved_searches_' + window.location.pathname;
260
+ localStorage.setItem(key, JSON.stringify(searches));
261
+ }
262
+
263
+ function populateSavedSearchesDropdown() {
264
+ var menu = $('#savedSearchesDropdown_menu');
265
+ if (!menu.length) return;
266
+
267
+ menu.empty();
268
+ var searches = getSavedSearches();
269
+ var names = Object.keys(searches);
270
+
271
+ var dropdownButton = $('#savedSearchesDropdown_button');
272
+ if (names.length === 0) {
273
+ menu.append('<li><span class="dropdown-item text-muted">No saved searches</span></li>');
274
+ return;
275
+ }
276
+
277
+ names.forEach(function(name) {
278
+ var li = $('<li>');
279
+ var a = $('<a>', {
280
+ class: 'dropdown-item d-flex align-items-center justify-content-between apply-saved-search',
281
+ href: 'javascript:void(0);',
282
+ 'data-name': name,
283
+ style: 'cursor: pointer;'
284
+ });
285
+
286
+ var nameSpan = $('<span>', {
287
+ class: 'text-black-700 fs-6 text-normal',
288
+ style: 'font-weight: 500; font-size: 0.875rem !important; line-height: 1.25rem;',
289
+ text: name
290
+ });
291
+
292
+ var deleteIcon = $('<span>', {
293
+ class: 'delete-saved-search text-danger',
294
+ 'data-name': name,
295
+ style: 'cursor: pointer; padding: 0 5px; font-weight: bold;',
296
+ html: '&times;',
297
+ title: 'Delete saved search'
298
+ });
299
+
300
+ a.append(nameSpan).append(deleteIcon);
301
+ li.append(a);
302
+ menu.append(li);
303
+ });
304
+ }
305
+
306
+ $(document).ready(function() {
307
+ populateSavedSearchesDropdown();
308
+
309
+ $(document).on('submit', '#advanceSearchForm', function(e) {
310
+ e.preventDefault();
311
+ return false;
312
+ });
313
+
314
+ $(document).on('input', '#savedSearchName', function() {
315
+ var val = $(this).val().trim();
316
+ if (val === '') {
317
+ $('#saveAdvanceSearch').text('Save Search As...');
318
+ } else {
319
+ $('#saveAdvanceSearch').text('Save');
320
+ }
321
+ });
322
+
323
+ $(document).on('click', '#saveAdvanceSearch', function(e) {
324
+ e.preventDefault();
325
+ var container = $('#save-search-container');
326
+ var input = $('#savedSearchName');
327
+
328
+ if (!container.is(':visible')) {
329
+ container.show();
330
+ input.focus();
331
+ return;
332
+ }
333
+
334
+ var name = input.val().trim();
335
+ if (name === '') {
336
+ container.hide();
337
+ return;
338
+ }
339
+
340
+ var searchParams = '';
341
+
342
+ // For bundles, it's search_by and q. For everything else, it's searchQuery()
343
+ if (window.location.pathname.includes('/bundles')) {
344
+ var searchBy = $('#searchFieldSelect').val();
345
+ var q = $('#bundleSearchValue').val();
346
+ if (q) q = q.trim();
347
+ if (!q) {
348
+ alert("Please enter a value to search.");
349
+ return;
350
+ }
351
+ searchParams = 'search_by=' + encodeURIComponent(searchBy) + '&q=' + encodeURIComponent(q);
352
+ } else {
353
+ // Check if there are any search fields before calling searchQuery(),
354
+ // because searchQuery() calls clearAdvanceSearch() (which reloads the page)
355
+ // when no fields exist.
356
+ if ($('.search-field').length === 0 && $('#search-labels-container .label').length === 0) {
357
+ alert("Please enter at least one search field.");
358
+ return;
359
+ }
360
+ searchParams = searchQuery();
361
+ if (!searchParams) {
362
+ alert("Please enter at least one search field.");
363
+ return;
364
+ }
365
+ searchParams = searchParams.replace(/account_id/g, 'ac_id');
366
+ }
367
+
368
+ var searches = getSavedSearches();
369
+ searches[name] = searchParams;
370
+ saveSavedSearches(searches);
371
+
372
+ input.val('');
373
+ $('#saveAdvanceSearch').text('Save Search As...');
374
+ container.hide();
375
+
376
+ populateSavedSearchesDropdown();
377
+ $('#advanceSearchModal').modal('hide');
378
+ });
379
+
380
+ $(document).on('hidden.bs.modal', '#advanceSearchModal', function () {
381
+ $('#save-search-container').hide();
382
+ $('#savedSearchName').val('');
383
+ $('#saveAdvanceSearch').text('Save Search As...');
384
+ });
385
+
386
+ $(document).on('click', '.apply-saved-search', function(e) {
387
+ // If they clicked the delete icon, don't apply the search
388
+ if ($(e.target).hasClass('delete-saved-search')) return;
389
+
390
+ var name = $(this).data('name');
391
+ var searches = getSavedSearches();
392
+ var params = searches[name];
393
+ if (params) {
394
+ var newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + params;
395
+ window.location.href = newUrl;
396
+ }
397
+ });
398
+
399
+ $(document).on('click', '.delete-saved-search', function(e) {
400
+ e.stopPropagation(); // prevent dropdown from closing and prevent applying search
401
+ var name = $(this).data('name');
402
+ if (confirm("Are you sure you want to delete the saved search '" + name + "'?")) {
403
+ var searches = getSavedSearches();
404
+ delete searches[name];
405
+ saveSavedSearches(searches);
406
+ populateSavedSearchesDropdown();
407
+ }
408
+ });
233
409
  });
@@ -1293,7 +1293,6 @@ table thead th:last-child {
1293
1293
  align-items: center;
1294
1294
  gap: 0.625rem;
1295
1295
  padding: 0.875rem 1.125rem;
1296
- margin: 0.625rem 0;
1297
1296
  border-radius: 0.375rem;
1298
1297
  font-size: 0.9375rem;
1299
1298
  font-weight: 500;
@@ -1467,6 +1466,10 @@ table thead th:last-child {
1467
1466
  width: 100%;
1468
1467
  }
1469
1468
 
1469
+ .field_with_errors {
1470
+ display: contents;
1471
+ }
1472
+
1470
1473
  /* Navbar color changes - Higher specificity to override existing rules */
1471
1474
  header .header-icon {
1472
1475
  color: #374151 !important;
@@ -689,4 +689,79 @@ table tr.expired td {
689
689
  line-height: 1.25rem;
690
690
  color: #414651;
691
691
  margin-bottom: 0.25rem;
692
+ }
693
+
694
+ /* Advanced Search modal – Subscription Bundles page */
695
+ .subscription-bundl-index .close-button {
696
+ background: transparent;
697
+ padding: 0;
698
+ margin-top: -1.25rem;
699
+ }
700
+
701
+ .subscription-bundl-index .close-button:hover {
702
+ background: transparent;
703
+ padding: 0;
704
+ }
705
+
706
+ .subscription-bundl-index .border-button {
707
+ background: transparent;
708
+ display: inline-flex;
709
+ justify-content: center;
710
+ align-items: center;
711
+ border: 0.0625rem solid #D5D7DA;
712
+ border-radius: 0.375rem;
713
+ width: 2.5rem;
714
+ height: 2.5rem;
715
+ padding: 0.625rem;
716
+ }
717
+
718
+ .subscription-bundl-index .border-button:hover {
719
+ background: transparent;
720
+ }
721
+
722
+ .subscription-bundl-index .button {
723
+ background: transparent;
724
+ display: inline-flex;
725
+ justify-content: center;
726
+ align-items: center;
727
+ width: 2.5rem;
728
+ height: 2.5rem;
729
+ padding: 0.625rem;
730
+ }
731
+
732
+ .subscription-bundl-index .button:hover {
733
+ background: transparent;
734
+ }
735
+
736
+ .subscription-bundl-index .field-label {
737
+ font-weight: 500;
738
+ font-size: 0.875rem;
739
+ line-height: 1.25rem;
740
+ color: #414651;
741
+ margin-top: -1.25rem;
742
+ min-width: 9.59375rem;
743
+ }
744
+
745
+ .subscription-bundl-index .search-field-label {
746
+ margin-top: -1.25rem;
747
+ }
748
+
749
+ .subscription-bundl-index .form-group.row.align-items-center {
750
+ display: flex;
751
+ align-items: center;
752
+ }
753
+
754
+ .subscription-bundl-index .form-group.row.align-items-center label {
755
+ margin-bottom: 0;
756
+ }
757
+
758
+ .subscription-bundl-index .form-group.row.align-items-center .form-control {
759
+ margin-right: 0.25rem;
760
+ height: 2.5rem;
761
+ border-radius: 0.375rem;
762
+ }
763
+
764
+ .subscription-bundl-index .form-group.row.align-items-center.search-field div {
765
+ padding: 0;
766
+ margin-right: 0.25rem;
692
767
  }
@@ -58,15 +58,18 @@
58
58
  .tag-bar .tag-select .tag-select-box {
59
59
  position: absolute;
60
60
  top: 100%;
61
- left: -0.0625rem;
61
+ left: 0;
62
+ right: auto;
62
63
  max-height: 0;
63
64
  background-color: #fff;
64
65
  z-index: 100;
65
66
  padding: 0 0.9375rem;
66
67
  font-size: 0.75rem;
67
68
  color: #999999;
68
- overflow: auto;
69
- width: auto;
69
+ overflow: hidden;
70
+ min-width: 15rem;
71
+ width: max-content;
72
+ max-width: min(25rem, 95vw);
70
73
  border-bottom: 0;
71
74
  display: inline-block;
72
75
  }
@@ -5,7 +5,7 @@ module Kaui
5
5
  class AccountsController < Kaui::EngineController
6
6
  def index
7
7
  @search_query = params[:q]
8
- @advance_search_query = @search_query || request.query_string
8
+ @advance_search_query = @search_query.presence || params[:advance_search_query].presence
9
9
  if @search_query.present?
10
10
  account = Kaui::Account.list_or_search(@search_query, -1, 1, options_for_klient).first
11
11
  if account.nil?
@@ -61,8 +61,8 @@ module Kaui
61
61
  nil
62
62
  end
63
63
 
64
- @overdue = wait(fetch_overdue)
65
- @overdue_xml = wait(fetch_overdue_xml)
64
+ @overdue = flash[:overdue_deleted] ? nil : wait(fetch_overdue)
65
+ @overdue_xml = flash[:overdue_deleted] ? nil : wait(fetch_overdue_xml)
66
66
  @tenant_plugin_config = begin
67
67
  wait(fetch_tenant_plugin_config)
68
68
  rescue StandardError
@@ -288,7 +288,15 @@ module Kaui
288
288
  options = tenant_options_for_client
289
289
  options[:api_key] = @tenant.api_key
290
290
  options[:api_secret] = @tenant.api_secret
291
- @overdue = Kaui::Overdue.get_overdue_json(options)
291
+ begin
292
+ @overdue = Kaui::Overdue.get_overdue_json(options)
293
+ @overdue_corrupted = false
294
+ rescue StandardError => e
295
+ Rails.logger.warn("Failed to load overdue configuration for tenant #{@tenant.id}: #{e.class}: #{e.message}")
296
+ @overdue = KillBillClient::Model::Overdue.new.tap { |o| o.overdue_states = [] }
297
+ @overdue_corrupted = true
298
+ flash.now[:warning] = 'The existing overdue configuration is corrupted and cannot be loaded. Use the XML upload below to replace it with a valid configuration.'
299
+ end
292
300
  end
293
301
 
294
302
  def modify_overdue_config
@@ -298,12 +306,18 @@ module Kaui
298
306
  options[:api_key] = current_tenant.api_key
299
307
  options[:api_secret] = current_tenant.api_secret
300
308
 
301
- view_form_model = params.require(:kill_bill_client_model_overdue).permit!.to_h.compact_blank
309
+ overdue_params = params[:kill_bill_client_model_overdue]&.permit!
310
+ view_form_model = overdue_params&.to_h&.compact_blank || {}
302
311
  view_form_model['states'] = view_form_model['states'].values if view_form_model['states'].present?
303
-
304
- overdue = Kaui::Overdue.from_overdue_form_model(view_form_model)
305
- Kaui::Overdue.upload_tenant_overdue_config_json(overdue.to_json, options[:username], nil, comment, options)
306
- redirect_to admin_tenant_path(current_tenant.id, active_tab: 'OverdueShow'), notice: I18n.t('flashes.notices.overdue_added_successfully')
312
+ if view_form_model['states'].blank?
313
+ Kaui::AdminTenant.delete_tenant_user_key_value('OVERDUE_CONFIG', options[:username], nil, comment, options)
314
+ flash[:overdue_deleted] = true
315
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'OverdueShow'), notice: I18n.t('flashes.notices.overdue_updated_successfully')
316
+ else
317
+ overdue = Kaui::Overdue.from_overdue_form_model(view_form_model)
318
+ Kaui::Overdue.upload_tenant_overdue_config_json(overdue.to_json, options[:username], nil, comment, options)
319
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'OverdueShow'), notice: I18n.t('flashes.notices.overdue_added_successfully')
320
+ end
307
321
  end
308
322
 
309
323
  def upload_overdue_config
@@ -5,10 +5,11 @@ module Kaui
5
5
  # rubocop:disable Lint/HashCompareByIdentity
6
6
  def index
7
7
  cached_options_for_klient = options_for_klient
8
+ @search_query = params[:q].presence
9
+ @search_by = params[:search_by] || 'bundle_id'
8
10
  @per_page = (params[:per_page] || 10).to_i
9
11
  @page = (params[:page] || 1).to_i
10
12
 
11
- fetch_bundles = promise { Kaui::Account.paginated_bundles(@account.account_id, (@page - 1) * @per_page, @per_page, 'NONE', cached_options_for_klient) }
12
13
  fetch_bundle_tags = promise do
13
14
  all_bundle_tags = @account.all_tags(:BUNDLE, false, 'NONE', cached_options_for_klient)
14
15
  all_bundle_tags.each_with_object({}) do |entry, hsh|
@@ -36,8 +37,15 @@ module Kaui
36
37
  fetch_available_tags = promise { Kaui::TagDefinition.all_for_bundle(cached_options_for_klient) }
37
38
  fetch_available_subscription_tags = promise { Kaui::TagDefinition.all_for_subscription(cached_options_for_klient) }
38
39
 
39
- @bundles = wait(fetch_bundles)
40
- @total_pages = (@bundles.pagination_max_nb_records.to_f / @per_page).ceil
40
+ if @search_query.present?
41
+ @bundles = search_bundles(@search_query, @search_by, cached_options_for_klient)
42
+ @total_pages = 1
43
+ @page = 1
44
+ else
45
+ fetched = Kaui::Account.paginated_bundles(@account.account_id, (@page - 1) * @per_page, @per_page, 'NONE', cached_options_for_klient)
46
+ @bundles = fetched
47
+ @total_pages = (fetched.pagination_max_nb_records.to_f / @per_page).ceil
48
+ end
41
49
 
42
50
  @tags_per_bundle = wait(fetch_bundle_tags)
43
51
  @tags_per_subscription = wait(fetch_subscription_tags)
@@ -46,8 +54,15 @@ module Kaui
46
54
  @available_tags = wait(fetch_available_tags)
47
55
  @available_subscription_tags = wait(fetch_available_subscription_tags)
48
56
 
49
- # Don't load the full catalog to avoid memory issues
50
- @catalog = nil
57
+ # Collect the distinct start dates from subscriptions on this page, then fetch
58
+ # only the catalog versions needed — one per unique date — to avoid loading all
59
+ # historical versions into memory.
60
+ start_dates = @bundles.flat_map(&:subscriptions).filter_map(&:start_date).uniq
61
+ @catalogs = start_dates.filter_map do |date|
62
+ Kaui::Catalog.get_account_catalog_json(@account.account_id, date, cached_options_for_klient)&.last
63
+ rescue StandardError
64
+ nil
65
+ end.uniq(&:effective_date)
51
66
 
52
67
  @subscription = {}
53
68
  @bundles.each do |bundle|
@@ -112,5 +127,38 @@ module Kaui
112
127
  end
113
128
  redirect_to kaui_engine.account_bundles_path(@account.account_id), notice: msg
114
129
  end
130
+
131
+ private
132
+
133
+ def search_bundles(query, search_by, options)
134
+ case search_by
135
+ when 'bundle_id'
136
+ bundle = Kaui::Bundle.find_by_id(query, options)
137
+ bundle ? [bundle] : []
138
+ when 'bundle_external_key'
139
+ bundle = Kaui::Bundle.find_by_external_key(query, false, options)
140
+ bundle ? [bundle] : []
141
+ when 'subscription_id'
142
+ subscription = KillBillClient::Model::Subscription.find_by_id(query, 'NONE', options)
143
+ if subscription
144
+ bundle = Kaui::Bundle.find_by_id(subscription.bundle_id, options)
145
+ bundle ? [bundle] : []
146
+ else
147
+ []
148
+ end
149
+ when 'subscription_external_key'
150
+ subscription = KillBillClient::Model::Subscription.find_by_external_key(query, 'NONE', options)
151
+ if subscription
152
+ bundle = Kaui::Bundle.find_by_id(subscription.bundle_id, options)
153
+ bundle ? [bundle] : []
154
+ else
155
+ []
156
+ end
157
+ else
158
+ []
159
+ end
160
+ rescue KillBillClient::API::NotFound
161
+ []
162
+ end
115
163
  end
116
164
  end
@@ -127,7 +127,7 @@ module Kaui
127
127
  as_string(exception.cause)
128
128
  else
129
129
  log_rescue_error(exception)
130
- exception.message
130
+ exception.message.to_s[0..200]
131
131
  end
132
132
  end
133
133
 
@@ -150,7 +150,7 @@ module Kaui
150
150
  error_message += " (code=#{error_message['code']})" if error_message['code'].present?
151
151
  end
152
152
  # Limit the error size to avoid ActionDispatch::Cookies::CookieOverflow
153
- error_message[0..1000]
153
+ error_message.to_s[0..200]
154
154
  end
155
155
 
156
156
  def nested_hash_value(obj, key)
@@ -6,7 +6,7 @@ module Kaui
6
6
  def index
7
7
  @search_query = params[:account_id]
8
8
  @advance_search_query = params[:q] || request.query_string
9
- @ordering = params[:ordering] || (@search_query.blank? ? 'desc' : 'asc')
9
+ @ordering = params[:ordering] || 'desc'
10
10
  @offset = params[:offset] || 0
11
11
  @limit = params[:limit] || 50
12
12
  @search_fields = Kaui::Invoice::ADVANCED_SEARCH_COLUMNS.map { |attr| [attr, attr.split('_').join(' ').capitalize] }
@@ -11,10 +11,8 @@ module Kaui
11
11
  def installed_plugin_names
12
12
  plugins = []
13
13
  nodes_info = KillBillClient::Model::NodesInfo.nodes_info(Kaui.current_tenant_user_options(current_user, session)) || []
14
- plugins_info = nodes_info.empty? ? [] : (nodes_info.first.plugins_info || [])
15
- plugins_info.each do |plugin|
16
- next unless plugin.state == 'RUNNING'
17
-
14
+ plugins_info = nodes_info.flat_map { |node| node.plugins_info || [] }
15
+ plugins_info.select { |p| p.state == 'RUNNING' }.uniq(&:plugin_name).each do |plugin|
18
16
  plugin_name = plugin.plugin_name
19
17
  plugin_key = plugin_name.gsub('-plugin', '')
20
18
 
@@ -40,7 +38,7 @@ module Kaui
40
38
  def installed_plugins
41
39
  installed_plugins = []
42
40
  nodes_info = KillBillClient::Model::NodesInfo.nodes_info(Kaui.current_tenant_user_options(current_user, session)) || []
43
- plugins_info = nodes_info.empty? ? [] : (nodes_info.first.plugins_info || [])
41
+ plugins_info = nodes_info.flat_map { |node| node.plugins_info || [] }
44
42
 
45
43
  plugins_info.each do |plugin|
46
44
  next if plugin.version.nil?
@@ -107,6 +107,24 @@ module Kaui
107
107
  end
108
108
  end
109
109
 
110
+ def catalog_for_subscription(sub, catalogs)
111
+ return nil if catalogs.blank? || sub&.start_date.blank?
112
+
113
+ start_date = Date.parse(sub.start_date)
114
+
115
+ # Find the latest catalog version whose effective_date <= subscription start_date
116
+ best = catalogs.select do |c|
117
+ Date.parse(c.effective_date) <= start_date
118
+ rescue StandardError
119
+ false
120
+ end.max_by(&:effective_date)
121
+
122
+ # Fall back to the oldest version if none precedes the start_date
123
+ best || catalogs.first
124
+ rescue StandardError
125
+ catalogs.last
126
+ end
127
+
110
128
  def humanized_subscription_phase_type(sub)
111
129
  sub.phase_type
112
130
  end
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Kaui
4
4
  class Bundle < KillBillClient::Model::Bundle
5
+ SEARCH_FIELDS = [
6
+ ['bundle_id', 'Bundle ID'],
7
+ ['bundle_external_key', 'Bundle External Key'],
8
+ ['subscription_id', 'Subscription ID'],
9
+ ['subscription_external_key', 'Subscription External Key']
10
+ ].freeze
5
11
  def self.find_by_id_or_key(bundle_id_or_key, options = {})
6
12
  if /[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}/.match?(bundle_id_or_key)
7
13
  bundle = begin
@@ -37,8 +37,11 @@ module Kaui
37
37
 
38
38
  result.overdue_states << state
39
39
  end
40
- # We reversed them to display on the form , so we have to reverse them back before uploading new config
41
- result.overdue_states.reverse!
40
+ # Sort by days descending (most severe first) so Kill Bill receives them in the correct order
41
+ # regardless of the order the user added rows in the form.
42
+ result.overdue_states.sort_by! do |s|
43
+ -s.condition&.time_since_earliest_unpaid_invoice_equals_or_exceeds&.number.to_i
44
+ end
42
45
 
43
46
  result
44
47
  end
@@ -45,7 +45,7 @@
45
45
  <% end %>
46
46
  <span class="tag-bar tag-bar-breathe">
47
47
  <% unless @available_tags.blank? %>
48
- <div class="tag-select" onclick="void(0)">
48
+ <div class="tag-select position-relative" onclick="void(0)">
49
49
  <%= render "kaui/components/button/button", {
50
50
  label: "Tag As",
51
51
  trailing_icon: "kaui/account/down-arrow.svg",