kaui 4.0.20 → 4.0.24

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/kaui/multi_functions_bar_utils.js +18 -9
  3. data/app/assets/stylesheets/kaui/tenants.css +53 -0
  4. data/app/controllers/kaui/accounts_controller.rb +2 -2
  5. data/app/controllers/kaui/admin_tenants_controller.rb +12 -0
  6. data/app/controllers/kaui/engine_controller_util.rb +2 -2
  7. data/app/controllers/kaui/invoices_controller.rb +2 -0
  8. data/app/controllers/kaui/payments_controller.rb +2 -0
  9. data/app/controllers/kaui/subscriptions_controller.rb +58 -23
  10. data/app/models/kaui/account.rb +3 -1
  11. data/app/models/kaui/admin_tenant.rb +10 -0
  12. data/app/models/kaui/invoice.rb +3 -0
  13. data/app/models/kaui/payment.rb +3 -0
  14. data/app/services/dependencies/aviate.rb +89 -0
  15. data/app/views/kaui/account_tags/index.html.erb +1 -1
  16. data/app/views/kaui/accounts/_functions_bar.html.erb +2 -2
  17. data/app/views/kaui/accounts/_multi_functions_bar.html.erb +2 -2
  18. data/app/views/kaui/accounts/index.html.erb +5 -5
  19. data/app/views/kaui/admin_tenants/_form_invoice_template.erb +0 -1
  20. data/app/views/kaui/admin_tenants/_form_invoice_translation.erb +47 -1
  21. data/app/views/kaui/audit_logs/index.html.erb +1 -1
  22. data/app/views/kaui/components/button/_button.html.erb +1 -1
  23. data/app/views/kaui/custom_fields/index.html.erb +1 -1
  24. data/app/views/kaui/invoices/_multi_functions_bar.html.erb +2 -2
  25. data/app/views/kaui/invoices/index.html.erb +6 -5
  26. data/app/views/kaui/payments/_multi_functions_bar.html.erb +2 -2
  27. data/app/views/kaui/payments/index.html.erb +9 -8
  28. data/app/views/kaui/shared/_advanced_search_filterbar.html.erb +6 -6
  29. data/app/views/kaui/subscriptions/record_usage.erb +32 -8
  30. data/app/views/kaui/tags/index.html.erb +1 -1
  31. data/lib/kaui/version.rb +1 -1
  32. data/lib/kaui.rb +13 -9
  33. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef67e5aac2cf29cee71a7a52b5b87452afc410ed711d1d55c08225dc39b1d044
4
- data.tar.gz: 6232e9397218afea690a6d45422623b16fb346489a380eaaa6fdba3ed6e98930
3
+ metadata.gz: 19115a4ca850dfd755598a560dc66e96b00694f00b305e057260fd6b13c57e3d
4
+ data.tar.gz: 9780699cd5e97d8c6887b3e0c200d968a11e644afd5496c58829277d558a28bc
5
5
  SHA512:
6
- metadata.gz: d2cd48b2725369b2a466e49cabbf656ce64b6bea4cdcb272f97fb7d34c909bdb56a6876c13af14328a1dcdd0d341e9d04757318d9504aeece87d55ad2ba8a6fa
7
- data.tar.gz: 8abadbb98b2d800acb54b5dd7fd9bdcf20a8a49186c1b3b9b2357eb3a717ff6a79a45094aebcfaa252a11a8ca562ac5ab2812080c6abb3774ea711b57d98099b
6
+ metadata.gz: 8bcf2e6e322833277c4d305aca8621bf4495cc6443961af0a565bf7da0d6efdb5892d8b66ac31e820f08953d221ad54b309018fbd3d088032e211ea73d51abd9
7
+ data.tar.gz: e1a44d558e7b996185be6a79cca68fb2eecb4e200f2affff7e5de3ccf0ca6a38f78d0ffdc814c67f3ab794585e6d7b4c4604dfe06ea77e7b7bc86360e229273e
@@ -29,8 +29,8 @@ function searchParseOperatorText(text) {
29
29
  // Function to parse URL parameters
30
30
  function getUrlParams() {
31
31
  var params = {};
32
- var queryString = window.location.search.substring(1);
33
- queryString = queryString.replace(/ac_id/g, 'account_id');
32
+ var advanceSearchQuery = new URLSearchParams(window.location.search).get('advance_search_query');
33
+ var queryString = advanceSearchQuery !== null ? advanceSearchQuery : window.location.search.substring(1).replace(/ac_id/g, 'account_id');
34
34
  var regex = /([^&=]+)=([^&]*)/g;
35
35
  var m;
36
36
  while (m = regex.exec(queryString)) {
@@ -55,15 +55,17 @@ function populateSearchLabelsFromUrl() {
55
55
  if (match) {
56
56
  var columnName = match[1].replace(/_/g, ' ').replace(/^\w/, function(l) { return l.toUpperCase(); });
57
57
  var filter = searchFormatOperator(match[2].trim());
58
+ var isHiddenAccountFilter = match[1].trim().toLowerCase() === 'account_id' && window.location.pathname.includes('/accounts/');
58
59
  var label = $('<span>', {
59
- class: 'label label-info d-inline-flex align-items-center gap-2',
60
+ class: 'label label-info d-inline-flex align-items-center gap-2' + (isHiddenAccountFilter ? ' d-none' : ''),
60
61
  'data-field': columnName.trim(),
61
62
  'data-filter': filter.trim(),
62
- 'data-value': value.trim()
63
+ 'data-value': value.trim(),
64
+ 'data-hidden-filter': isHiddenAccountFilter
63
65
  });
64
66
 
65
67
  if (hasBalanceFilter && columnName.toLowerCase().trim() !== 'balance') {
66
- label.attr('class', 'label label-default d-inline-flex align-items-center gap-2');
68
+ label.attr('class', 'label label-default d-inline-flex align-items-center gap-2' + (isHiddenAccountFilter ? ' d-none' : ''));
67
69
  }
68
70
 
69
71
  var labelText = $('<span>', {
@@ -168,6 +170,10 @@ function showAdvanceSearchModal() {
168
170
 
169
171
  // Populate the search fields with the current filters
170
172
  searchLabelsContainer.find('.label').each(function() {
173
+ if ($(this).data('hidden-filter')) {
174
+ return;
175
+ }
176
+
171
177
  var labelText = $(this).text().trim();
172
178
  var parts = labelText.split(' [');
173
179
  var columnName = parts[0].trim();
@@ -226,6 +232,10 @@ $(document).on('click', '.filter-close-icon', function() {
226
232
  }
227
233
  });
228
234
  var searchParams = searchQuery();
235
+ // searchQuery() returns an already percent-encoded field[op]=value fragment; decode it
236
+ // back to raw text so it can be sent as the (single-encoded) value of the dedicated
237
+ // advance_search_query param instead of as top-level bracketed keys.
238
+ var rawSearchParams = searchParams ? decodeURIComponent(searchParams) : searchParams;
229
239
  // Reapply search without the removed filter
230
240
  var tableSelectors = ['#invoices-table', '#accounts-table', '#payments-table', '#subscriptions-table'];
231
241
  var table = null;
@@ -240,15 +250,14 @@ $(document).on('click', '.filter-close-icon', function() {
240
250
 
241
251
  if (table && table.ajax) {
242
252
  table.on('preXhr.dt', function(e, settings, data) {
243
- data.search.value = searchParams;
253
+ data.advance_search_query = rawSearchParams;
244
254
  });
245
255
  table.ajax.reload(null, false);
246
256
  }
247
257
 
248
258
  // Update URL
249
- if (searchParams) {
250
- var pushParams = (searchParams || '').replace(/account_id/g, 'ac_id');
251
- var newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + pushParams;
259
+ if (rawSearchParams) {
260
+ var newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?advance_search_query=' + encodeURIComponent(rawSearchParams);
252
261
  window.history.pushState({ path: newUrl }, '', newUrl);
253
262
  } else {
254
263
  var newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
@@ -641,6 +641,59 @@
641
641
  color: #414651;
642
642
  }
643
643
 
644
+ /* app/views/kaui/admin_tenants/_form_invoice_translation.erb */
645
+
646
+ .existing-invoice-translations-container {
647
+ max-width: 80rem;
648
+ width: 100%;
649
+ }
650
+
651
+ .existing-invoice-translations-container .existing-invoice-translations-header {
652
+ display: flex;
653
+ align-items: start;
654
+ justify-content: space-between;
655
+ padding: 0;
656
+ border-bottom: 0.0625rem solid #E9EAEB;
657
+ }
658
+
659
+ .existing-invoice-translations-container .existing-invoice-translations-header h2 {
660
+ font-weight: 600;
661
+ font-size: 1.125rem;
662
+ line-height: 1.75rem;
663
+ color: #414651;
664
+ margin-bottom: 1.25rem;
665
+ }
666
+
667
+ .existing-invoice-translations-container table.existing-invoice-translations-table {
668
+ overflow-x: auto;
669
+ min-width: 100%;
670
+ border: none !important;
671
+ }
672
+
673
+ .existing-invoice-translations-container table.existing-invoice-translations-table tbody tr {
674
+ border-bottom: 0.0625rem solid #E9EAEB !important;
675
+ }
676
+
677
+ .existing-invoice-translations-container table.existing-invoice-translations-table th {
678
+ padding: 0.375rem 0.75rem !important;
679
+ font-weight: 500 !important;
680
+ font-size: 0.75rem !important;
681
+ line-height: 1.125rem;
682
+ letter-spacing: 0;
683
+ color: #717680;
684
+ text-transform: capitalize;
685
+ background-color: #FAFAFA !important;
686
+ }
687
+
688
+ .existing-invoice-translations-container table.existing-invoice-translations-table td {
689
+ font-weight: 500 !important;
690
+ font-size: 0.875rem !important;
691
+ line-height: 1.125rem !important;
692
+ text-align: start;
693
+ padding: 0.3125rem 0.75rem !important;
694
+ color: #535862 !important;
695
+ }
696
+
644
697
  /* app/views/kaui/admin_tenants/_show_catalog_simple.erb */
645
698
 
646
699
  #catalog_simple, #catalog_xml {
@@ -17,7 +17,7 @@ module Kaui
17
17
  return
18
18
  end
19
19
  @search_fields = Kaui::Account::ADVANCED_SEARCH_COLUMNS.map { |attr| [attr, attr.split('_').join(' ').capitalize] }
20
- @dropdown_default = default_columns(Kaui.account_search_columns.call[2], Kaui::Account::SENSIVITE_DATA_FIELDS)
20
+ @dropdown_default = default_columns(Kaui.account_search_columns.call[2], Kaui::Account::DEFAULT_VISIBLE_COLUMNS)
21
21
  @visible_columns = @dropdown_default
22
22
 
23
23
  @ordering = params[:ordering] || (@search_query.blank? ? 'desc' : 'asc')
@@ -47,7 +47,7 @@ module Kaui
47
47
  Kaui.account_search_columns.call(account, view_context)[1]
48
48
  end
49
49
 
50
- paginate searcher, data_extractor, formatter, default_columns(Kaui.account_search_columns.call[2], Kaui::Account::SENSIVITE_DATA_FIELDS)
50
+ paginate searcher, data_extractor, formatter, default_columns(Kaui.account_search_columns.call[2], Kaui::Account::DEFAULT_VISIBLE_COLUMNS)
51
51
  end
52
52
 
53
53
  def download
@@ -53,6 +53,12 @@ module Kaui
53
53
  @invoice_template = nil
54
54
  end
55
55
 
56
+ fetch_invoice_translations = promise do
57
+ Kaui::AdminTenant.get_invoice_translations(options)
58
+ rescue StandardError
59
+ @invoice_translations = {}
60
+ end
61
+
56
62
  fetch_tenant_plugin_config = promise { Kaui::AdminTenant.get_tenant_plugin_config(options) }
57
63
 
58
64
  @catalog_versions = []
@@ -76,6 +82,12 @@ module Kaui
76
82
  nil
77
83
  end
78
84
 
85
+ @invoice_translations = begin
86
+ wait(fetch_invoice_translations)
87
+ rescue StandardError
88
+ {}
89
+ end
90
+
79
91
  @tenant_plugin_config = begin
80
92
  wait(fetch_tenant_plugin_config)
81
93
  rescue StandardError
@@ -203,8 +203,8 @@ module Kaui
203
203
  render json: response, status: response_status
204
204
  end
205
205
 
206
- def default_columns(fields, sensivite_fields)
207
- fields.map { |field| { data: fields.index(field), visible: !(sensivite_fields.include? field) } }
206
+ def default_columns(fields, visible_fields)
207
+ fields.each_with_index.map { |field, index| { data: index, visible: visible_fields.include?(field) } }
208
208
  end
209
209
  end
210
210
  end
@@ -10,6 +10,8 @@ module Kaui
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] }
13
+ @dropdown_default = default_columns(Kaui.account_invoices_columns.call[3], Kaui::Invoice::DEFAULT_VISIBLE_COLUMNS)
14
+ @visible_columns = @dropdown_default
13
15
 
14
16
  @max_nb_records = @search_query.blank? ? Kaui::Invoice.list_or_search(nil, 0, 0, options_for_klient).pagination_max_nb_records : 0
15
17
  end
@@ -11,6 +11,8 @@ module Kaui
11
11
  @offset = params[:offset] || 0
12
12
  @limit = params[:limit] || 50
13
13
  @search_fields = Kaui::Payment::ADVANCED_SEARCH_COLUMNS.map { |attr| [attr, attr.split('_').join(' ').capitalize] }
14
+ @dropdown_default = default_columns(Kaui.account_payments_columns.call[3], Kaui::Payment::DEFAULT_VISIBLE_COLUMNS)
15
+ @visible_columns = @dropdown_default
14
16
 
15
17
  @max_nb_records = @search_query.blank? ? Kaui::Payment.list_or_search(nil, 0, 0, options_for_klient).pagination_max_nb_records : 0
16
18
  end
@@ -181,7 +181,12 @@ module Kaui
181
181
 
182
182
  def record_usage
183
183
  @subscription = Kaui::Subscription.find_by_id(params.require(:id), 'NONE', options_for_klient)
184
- @unit_types = fetch_unit_types_from_subscription(@subscription)
184
+ @is_aviate_catalog = Dependencies::Aviate::Metering.aviate_catalog?(@subscription.account_id, options_for_aviate_klient)
185
+ @unit_types = if @is_aviate_catalog
186
+ Dependencies::Aviate::Metering.billing_meter_codes_for_plan(@subscription.plan_name, @subscription.account_id, options_for_aviate_klient)
187
+ else
188
+ fetch_unit_types_from_subscription(@subscription)
189
+ end
185
190
  end
186
191
 
187
192
  def create_usage
@@ -189,56 +194,65 @@ module Kaui
189
194
  unit_type = params[:unit_type].to_s.strip
190
195
  amount_raw = params[:amount].to_s.strip
191
196
  record_date = params[:record_date].to_s.strip
197
+ tracking_id = params[:tracking_id].to_s.strip
198
+
199
+ @subscription = Kaui::Subscription.find_by_id(subscription_id, 'NONE', options_for_klient)
200
+ @is_aviate_catalog = Dependencies::Aviate::Metering.aviate_catalog?(@subscription.account_id, options_for_aviate_klient)
192
201
 
193
202
  # Input validation
194
203
  errors = []
195
- errors << 'Unit type is required' if unit_type.blank?
204
+ errors << (@is_aviate_catalog ? 'Billing meter code is required' : 'Unit type is required') if unit_type.blank?
196
205
  errors << 'Amount is required' if amount_raw.blank?
197
- amount = Integer(amount_raw, exception: false)
198
- errors << 'Amount must be a positive integer' if amount.nil? || amount <= 0
206
+ amount = @is_aviate_catalog ? parse_usage_amount(amount_raw) : Integer(amount_raw, exception: false)
207
+ errors << (@is_aviate_catalog ? 'Amount must be a positive number' : 'Amount must be a positive integer') if amount.nil? || amount <= 0
199
208
  errors << 'Date/time of usage is required' if record_date.blank?
200
209
  parsed_date = parse_usage_date(record_date) if record_date.present?
201
210
  errors << 'Date/time of usage must be a valid date or datetime' if record_date.present? && parsed_date.nil?
211
+ errors << 'Tracking ID must be a valid UUID' if @is_aviate_catalog && tracking_id.present? && !tracking_id.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
202
212
 
203
213
  if errors.any?
204
214
  flash.now[:error] = errors.join('. ')
205
- @subscription = Kaui::Subscription.find_by_id(subscription_id, 'NONE', options_for_klient)
206
- @unit_types = fetch_unit_types_from_subscription(@subscription)
215
+ @unit_types = fetch_unit_types_for_record_usage_form(@subscription, @is_aviate_catalog)
207
216
  @unit_type = unit_type
208
217
  @amount = amount_raw
209
218
  @record_date = record_date
210
- @tracking_id = params[:tracking_id]
219
+ @tracking_id = tracking_id
211
220
  render :record_usage and return
212
221
  end
213
222
 
214
223
  begin
215
- usage_record = KillBillClient::Model::UsageRecordAttributes.new
216
- usage_record.record_date = parsed_date.utc.iso8601
217
- usage_record.amount = amount
224
+ if @is_aviate_catalog
225
+ # trackingId is required by the Aviate metering API; auto-generate one if the user left it blank
226
+ tracking_id = SecureRandom.uuid if tracking_id.blank?
227
+ Dependencies::Aviate::Metering.submit_usage_event(@subscription.account_id, unit_type, subscription_id, tracking_id,
228
+ parsed_date.utc.strftime('%Y-%m-%dT%H:%M:%S'), amount.to_f, options_for_aviate_klient)
229
+ else
230
+ usage_record = KillBillClient::Model::UsageRecordAttributes.new
231
+ usage_record.record_date = parsed_date.utc.iso8601
232
+ usage_record.amount = amount.to_i
218
233
 
219
- unit_usage_record = KillBillClient::Model::UnitUsageRecordAttributes.new
220
- unit_usage_record.unit_type = unit_type
221
- unit_usage_record.usage_records = [usage_record]
234
+ unit_usage_record = KillBillClient::Model::UnitUsageRecordAttributes.new
235
+ unit_usage_record.unit_type = unit_type
236
+ unit_usage_record.usage_records = [usage_record]
222
237
 
223
- usage = Kaui::Usage.new
224
- usage.subscription_id = subscription_id
225
- usage.tracking_id = params[:tracking_id].presence
226
- usage.unit_usage_records = [unit_usage_record]
238
+ usage = Kaui::Usage.new
239
+ usage.subscription_id = subscription_id
240
+ usage.tracking_id = tracking_id.presence
241
+ usage.unit_usage_records = [unit_usage_record]
227
242
 
228
- usage.create(current_user.kb_username, nil, nil, options_for_klient)
243
+ usage.create(current_user.kb_username, nil, nil, options_for_klient)
244
+ end
229
245
 
230
- subscription = Kaui::Subscription.find_by_id(subscription_id, 'NONE', options_for_klient)
231
- redirect_to kaui_engine.account_bundles_path(subscription.account_id), notice: 'Usage was successfully recorded'
246
+ redirect_to kaui_engine.account_bundles_path(@subscription.account_id), notice: 'Usage was successfully recorded'
232
247
  rescue StandardError => e
233
248
  Rails.logger.error("Failed to record usage for subscription #{subscription_id}: #{e.class}: #{e.message}")
234
249
  Rails.logger.error(e.backtrace.join("\n")) if e.backtrace
235
250
  flash.now[:error] = "Error while recording usage: #{as_string(e)}"
236
- @subscription = Kaui::Subscription.find_by_id(subscription_id, 'NONE', options_for_klient)
237
- @unit_types = fetch_unit_types_from_subscription(@subscription)
251
+ @unit_types = fetch_unit_types_for_record_usage_form(@subscription, @is_aviate_catalog)
238
252
  @unit_type = unit_type
239
253
  @amount = amount_raw
240
254
  @record_date = record_date
241
- @tracking_id = params[:tracking_id]
255
+ @tracking_id = tracking_id
242
256
  render :record_usage
243
257
  end
244
258
  end
@@ -411,6 +425,27 @@ module Kaui
411
425
  (phase.prices || []).empty?
412
426
  end
413
427
 
428
+ def fetch_unit_types_for_record_usage_form(subscription, is_aviate_catalog)
429
+ if is_aviate_catalog
430
+ Dependencies::Aviate::Metering.billing_meter_codes_for_plan(subscription.plan_name, subscription.account_id, options_for_aviate_klient)
431
+ else
432
+ fetch_unit_types_from_subscription(subscription)
433
+ end
434
+ end
435
+
436
+ # The Aviate plugin's catalog/metering endpoints require a JWT (obtained by logging in via the
437
+ # Aviate Configuration page in killbill-aviate-ui), stored in a jwt_token cookie - same convention
438
+ # as Aviate::EngineController#options_for_klient in that engine.
439
+ def options_for_aviate_klient
440
+ options_for_klient(jwt_token: cookies[:jwt_token])
441
+ end
442
+
443
+ def parse_usage_amount(amount_raw)
444
+ BigDecimal(amount_raw, exception: false)
445
+ rescue ArgumentError
446
+ nil
447
+ end
448
+
414
449
  def fetch_unit_types_from_subscription(subscription)
415
450
  unit_types = []
416
451
  (subscription.prices || []).each do |phase_price|
@@ -4,7 +4,9 @@ module Kaui
4
4
  class Account < KillBillClient::Model::Account
5
5
  attr_accessor :phone, :bill_cycle_day_local
6
6
 
7
- SENSIVITE_DATA_FIELDS = %w[name email].freeze
7
+ # Columns shown by default on the Accounts list screen (demo-friendly defaults); the rest remain
8
+ # available but hidden until the user opts in via "Edit Columns".
9
+ DEFAULT_VISIBLE_COLUMNS = %w[name account_id external_key currency time_zone locale account_balance account_cba].freeze
8
10
  REMAPPING_FIELDS = {
9
11
  'is_payment_delegated_to_parent' => 'pay_via_parent',
10
12
  'account_balance' => 'balance',
@@ -23,6 +23,16 @@ module Kaui
23
23
  KillBillClient::Model::Invoice.upload_invoice_translation(invoice_translation, locale, delete_if_exists, user, reason, comment, options)
24
24
  end
25
25
 
26
+ # Return a map of locale => invoice translation content, for every invoice translation uploaded for the tenant
27
+ def get_invoice_translations(options = {})
28
+ raw_translations = KillBillClient::Model::Tenant.search_tenant_config('INVOICE_TRANSLATION_', options)
29
+
30
+ raw_translations.each_with_object({}) do |e, hsh|
31
+ locale = e.key.gsub('INVOICE_TRANSLATION_', '')
32
+ hsh[locale] = e.values[0]
33
+ end
34
+ end
35
+
26
36
  def upload_catalog_translation(catalog_translation, locale, delete_if_exists, user = nil, reason = nil, comment = nil, options = {})
27
37
  KillBillClient::Model::Invoice.upload_catalog_translation(catalog_translation, locale, delete_if_exists, user, reason, comment, options)
28
38
  end
@@ -3,6 +3,9 @@
3
3
  module Kaui
4
4
  class Invoice < KillBillClient::Model::Invoice
5
5
  TABLE_IGNORE_COLUMNS = %w[amount balance credit_adj refund_adj items is_parent_invoice parent_invoice_id parent_account_id audit_logs bundle_keys].freeze
6
+ # Columns shown by default on the Invoices list screen (demo-friendly defaults); the rest remain
7
+ # available but hidden until the user opts in via "Edit Columns".
8
+ DEFAULT_VISIBLE_COLUMNS = %w[invoice_number invoice_id status invoice_date target_date currency account_id].freeze
6
9
  ADVANCED_SEARCH_COLUMNS = %w[id account_id invoice_date target_date currency status balance].freeze
7
10
  ADVANCED_SEARCH_NAME_CHANGES = [%w[ac_id account_id]].freeze
8
11
 
@@ -7,6 +7,9 @@ module Kaui
7
7
  attr_accessor :payment_date, :target_invoice_id
8
8
 
9
9
  TRANSACTION_STATUSES = %w[SUCCESS PENDING PAYMENT_FAILURE PLUGIN_FAILURE UNKNOWN].freeze
10
+ # Columns shown by default on the Payments list screen (demo-friendly defaults); the rest remain
11
+ # available but hidden until the user opts in via "Edit Columns".
12
+ DEFAULT_VISIBLE_COLUMNS = %w[payment_date payment_number status account_id currency purchased_amount refunded_amount credited_amount].freeze
10
13
  REMAPPING_FIELDS = {
11
14
  'auth_amount' => 'auth',
12
15
  'captured_amount' => 'capture',
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dependencies
4
+ module Aviate
5
+ # Thin wrapper around the Aviate plugin's Catalog and Metering REST APIs
6
+ # (https://apidocs.killbill.io/aviate-catalog.html, https://apidocs.killbill.io/aviate-metering.html).
7
+ # These are not (yet) exposed by the killbill-client gem, so we call them directly.
8
+ #
9
+ # Unlike most Kill Bill APIs, these endpoints do NOT accept tenant RBAC (username/password) or
10
+ # api_key/api_secret alone - they require a JWT obtained via the Aviate Auth API
11
+ # (https://apidocs.killbill.io/aviate-auth.html), passed as a `jwt_token` entry in options_for_klient
12
+ # (see Aviate::EngineController#options_for_klient in killbill-aviate-ui, which stores it in a
13
+ # `jwt_token` cookie after logging in via the Aviate Configuration page). If no jwt_token is present,
14
+ # calls will fail authentication and callers should treat that as "unknown/not Aviate".
15
+ class Metering
16
+ KILLBILL_AVIATE_PREFIX = '/plugins/aviate-plugin/v1'
17
+ CATALOG_STATE_AVIATE = 'CATALOG_STATE_AVIATE'
18
+
19
+ class << self
20
+ # Returns true if the tenant/account is using an Aviate catalog (as opposed to an XML one).
21
+ # Any error (plugin not installed, missing/invalid JWT, network issue, etc.) is treated as
22
+ # "not Aviate" so that callers can safely fall back to the existing XML-catalog behavior.
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}")
28
+ false
29
+ end
30
+
31
+ # Returns the list of billing meter codes configured on the given plan's usage phases.
32
+ def billing_meter_codes_for_plan(plan_name, account_id, options_for_klient)
33
+ return [] if plan_name.blank?
34
+
35
+ response = KillBillClient::API.get("#{KILLBILL_AVIATE_PREFIX}/catalog/#{plan_name}/plan", catalog_params(account_id), request_options(options_for_klient))
36
+ plan = JSON.parse(response.body)
37
+
38
+ codes = []
39
+ Array(plan['phases']).each do |phase|
40
+ Array(phase['usages']).each do |usage|
41
+ Array(usage['tiers']).each do |tier|
42
+ Array(tier['blocks']).each do |block|
43
+ codes << block['billingMeterCode'] if block['billingMeterCode'].present?
44
+ end
45
+ end
46
+ end
47
+ end
48
+ codes.uniq
49
+ rescue StandardError => e
50
+ Rails.logger.warn("Failed to retrieve Aviate billing meter codes for plan #{plan_name}: #{e.class}: #{e.message}")
51
+ []
52
+ end
53
+
54
+ # Submits a single usage event via the Aviate metering plugin.
55
+ def submit_usage_event(account_id, billing_meter_code, subscription_id, tracking_id, timestamp, value, options_for_klient)
56
+ body = [{
57
+ billingMeterCode: billing_meter_code,
58
+ subscriptionId: subscription_id,
59
+ trackingId: tracking_id,
60
+ timestamp: timestamp,
61
+ value: value
62
+ }].to_json
63
+
64
+ KillBillClient::API.post("#{KILLBILL_AVIATE_PREFIX}/metering/billing/#{account_id}", body, {}, request_options(options_for_klient))
65
+ end
66
+
67
+ private
68
+
69
+ def catalog_params(account_id)
70
+ account_id.present? ? { accountId: account_id } : {}
71
+ end
72
+
73
+ # Builds request options scoped to just api_key/api_secret + the Aviate JWT bearer header.
74
+ # Deliberately excludes username/password/session_id from options_for_klient: KillBillClient's
75
+ # net_http_adapter calls request.basic_auth(username, password) when they're present, which
76
+ # would overwrite (clobber) the Authorization header we set below for the Bearer token.
77
+ def request_options(options_for_klient)
78
+ request_options = {
79
+ api_key: options_for_klient[:api_key],
80
+ api_secret: options_for_klient[:api_secret]
81
+ }
82
+ jwt_token = options_for_klient[:jwt_token]
83
+ request_options[:head] = { 'Authorization' => "Bearer #{jwt_token}" } if jwt_token.present?
84
+ request_options
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -63,7 +63,7 @@
63
63
  $(document).ready(function() {
64
64
  var tags = JSON.parse($("#tags").val());
65
65
  var table = $('#tags-table').DataTable({
66
- "dom": "<'row'<'col-md-6'l><'col-md-6'f>r>t<'row'<'col-md-6'i><'col-md-6'p>>",
66
+ "dom": "<'row'<'col-md-6'f>r>t<'row'<'col-md-6'i><'col-md-6'p>>",
67
67
  data: tags
68
68
  });
69
69
 
@@ -378,7 +378,7 @@ $(document).ready(function() {
378
378
  updateDropdownOrder();
379
379
 
380
380
  function updateDropdownOrder() {
381
- var state = JSON.parse(localStorage.getItem('DataTables_accounts-table_' + window.location.pathname));
381
+ var state = JSON.parse(localStorage.getItem('DataTables_accounts-table-v2_' + window.location.pathname));
382
382
  if (state === null) {
383
383
  // On initial load, ensure the existing checkboxes work
384
384
  setupColumnToggleEvents();
@@ -393,7 +393,7 @@ $(document).ready(function() {
393
393
 
394
394
  if (columnOrder !== undefined) {
395
395
  $list.empty();
396
- var state = JSON.parse(localStorage.getItem('DataTables_accounts-table_' + window.location.pathname));
396
+ var state = JSON.parse(localStorage.getItem('DataTables_accounts-table-v2_' + window.location.pathname));
397
397
  if (state !== null) {
398
398
  var colsOrder = state.ColReorder;
399
399
  }
@@ -319,7 +319,7 @@ $(document).ready(function() {
319
319
  updateDropdownOrder();
320
320
 
321
321
  function updateDropdownOrder() {
322
- var state = JSON.parse(localStorage.getItem('DataTables_accounts-table_' + window.location.pathname));
322
+ var state = JSON.parse(localStorage.getItem('DataTables_accounts-table-v2_' + window.location.pathname));
323
323
  if (state === null) {
324
324
  return;
325
325
  }
@@ -332,7 +332,7 @@ $(document).ready(function() {
332
332
 
333
333
  if (columnOrder !== undefined) {
334
334
  $list.empty();
335
- var state = JSON.parse(localStorage.getItem('DataTables_accounts-table_' + window.location.pathname));
335
+ var state = JSON.parse(localStorage.getItem('DataTables_accounts-table-v2_' + window.location.pathname));
336
336
  if (state !== null) {
337
337
  var colsOrder = state.ColReorder;
338
338
  }
@@ -79,11 +79,12 @@ $(document).ready(function() {
79
79
  "enable": false
80
80
  },
81
81
  "stateSave": true,
82
+ "stateDuration": -1,
82
83
  "stateSaveCallback": function(settings, data) {
83
- localStorage.setItem('DataTables_accounts-table_' + window.location.pathname, JSON.stringify(data));
84
+ localStorage.setItem('DataTables_accounts-table-v2_' + window.location.pathname, JSON.stringify(data));
84
85
  },
85
86
  "stateLoadCallback": function(settings) {
86
- return JSON.parse(localStorage.getItem('DataTables_accounts-table_' + window.location.pathname));
87
+ return JSON.parse(localStorage.getItem('DataTables_accounts-table-v2_' + window.location.pathname));
87
88
  },
88
89
  "scrollX": true,
89
90
  "dom": "<'row'r>t<'row'<'col-md-6'i><'col-md-6'p>>",
@@ -131,11 +132,10 @@ $(document).ready(function() {
131
132
  // If the page loaded with advanced search params in the URL (e.g. restored from
132
133
  // localStorage) but @advance_search_query was not set server-side, re-fire the
133
134
  // DataTable request with the correct filters.
134
- if (window.location.search.includes('_q=1') && '<%= j(@advance_search_query.to_s.strip) %>' === '') {
135
- var urlSearch = window.location.search.substring(1).replace(/ac_id/g, 'account_id');
135
+ if (window.location.search.includes('advance_search_query=') && '<%= j(@advance_search_query.to_s.strip) %>' === '') {
136
+ var urlSearch = new URLSearchParams(window.location.search).get('advance_search_query') || '';
136
137
  var ajaxUrl = "<%= accounts_pagination_path(:ordering => @ordering, :format => :json) %>";
137
138
  table.on('preXhr.dt.filter', function(e, settings, data) {
138
- data.search.value = urlSearch;
139
139
  data.advance_search_query = urlSearch;
140
140
  });
141
141
  table.ajax.url(ajaxUrl).load();
@@ -2,7 +2,6 @@
2
2
  <div class="tab-pane fade invoice-template form-invoice-template" id="InvoiceTemplate">
3
3
  <% if @invoice_template.present? %>
4
4
  <div class="invoice-template-display mb-4">
5
- <h5>Current Invoice Template</h5>
6
5
  <div class="card">
7
6
  <div class="card-body p-0">
8
7
  <iframe src="<%= admin_tenant_invoice_template_path(@tenant.id) %>" style="width: 100%; height: 500px; border: none;" title="Invoice Template Preview"></iframe>
@@ -1,5 +1,51 @@
1
1
  <% if can? :config_upload, Kaui::AdminTenant %>
2
2
  <div class="tab-pane fade" id="InvoiceTranslation">
3
+ <div class="existing-invoice-translations-container mb-4">
4
+ <div class="d-flex flex-column">
5
+ <div class="existing-invoice-translations-header mb-3">
6
+ <div class="d-flex align-items-start flex-column">
7
+ </div>
8
+ </div>
9
+ <div>
10
+ <table id="existing-invoice-translations-for-tenants" class="existing-invoice-translations-table">
11
+ <% if @invoice_translations.blank? %>
12
+ <tbody><tr><td>No invoice translation defined for tenant</td></tr></tbody>
13
+ <% else %>
14
+ <thead>
15
+ <tr>
16
+ <th>Locale</th>
17
+ <th></th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ <% @invoice_translations.each do |locale, content| %>
22
+ <tr>
23
+ <td><%= locale %></td>
24
+ <td class="text-end">
25
+ <%= render "kaui/components/button/button", {
26
+ label: 'View Source',
27
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
28
+ type: "button",
29
+ html_class: "kaui-button custom-hover",
30
+ html_options: {
31
+ onclick: '$(this).blur(); var $row = $(this).closest("tr").next(".invoice-translation-source-row"); $row.toggle(); $(this).find(".kaui-button-label").text($row.is(":visible") ? "Hide Source" : "View Source"); return false;'
32
+ }
33
+ } %>
34
+ </td>
35
+ </tr>
36
+ <tr class="invoice-translation-source-row" style="display: none;">
37
+ <td colspan="2">
38
+ <pre class="mb-0 p-2 bg-light" style="max-height: 400px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; font-family: monospace; font-size: 12px;"><%= html_escape(content) %></pre>
39
+ </td>
40
+ </tr>
41
+ <% end %>
42
+ </tbody>
43
+ <% end %>
44
+ </table>
45
+ </div>
46
+ </div>
47
+ </div>
48
+
3
49
  <%= form_tag({:action => :upload_invoice_translation}, :method => 'post', :multipart => true, :class => 'form-horizontal') do %>
4
50
  <%= hidden_field_tag(:id, @tenant.id) %>
5
51
 
@@ -21,7 +67,7 @@
21
67
  <div class="form-group d-flex pb-3 border-bottom mb-3">
22
68
  <%= label_tag :translation_locale, 'Locale', :class => 'col-sm-2 control-label' %>
23
69
  <div class="col-sm-10">
24
- <%= text_field_tag :translation_locale, nil, :class => 'form-control', :required => true, :placeholder => 'e.g. en_US' %>
70
+ <%= select_tag :translation_locale, options_for_select([['-- Select a locale --', '']] + all_available_locales, { disabled: ['---------------'] }), class: 'form-control', required: true %>
25
71
  </div>
26
72
  </div>
27
73
  <div class="d-flex justify-content-end">
@@ -103,7 +103,7 @@
103
103
  $(document).ready(function() {
104
104
  var auditLogs = JSON.parse($("#audit-logs").val());
105
105
  var table = $('#audit-logs-table').DataTable({
106
- dom: "<'row'<'col-md-6'l><'col-md-6'f>r>t<'row'<'col-md-6'i><'col-md-6'p>>",
106
+ "dom": "<'row'<'col-md-6'f>r>t<'row'<'col-md-6'i><'col-md-6'p>>",
107
107
  pagingType: "full_numbers",
108
108
  data: auditLogs,
109
109
  "scrollX": true,
@@ -9,7 +9,7 @@
9
9
  <% icon_html = icon.present? ? image_tag(icon, class: "me-1", size: "16x16") : "" %>
10
10
  <% trailing_icon_html = trailing_icon.present? ? image_tag(trailing_icon, class: "ms-1", size: "16x16") : "" %>
11
11
 
12
- <%= tag.button raw("#{icon_html}#{label}#{trailing_icon_html}"),
12
+ <%= tag.button raw("#{icon_html}<span class=\"kaui-button-label\">#{label}</span>#{trailing_icon_html}"),
13
13
  type: type,
14
14
  class: "btn #{variant} #{html_class}",
15
15
  **html_options
@@ -82,7 +82,7 @@
82
82
  <%= javascript_tag do %>
83
83
  $(document).ready(function() {
84
84
  var table = $('#custom_fields-table').DataTable({
85
- "dom": "<'row'<'col-md-6'l><'col-md-6'f>r>t<'row'<'col-md-6'i><'col-md-6'p>>",
85
+ "dom": "<'row'<'col-md-6'f>r>t<'row'<'col-md-6'i><'col-md-6'p>>",
86
86
  "pagingType": <% if @max_nb_records.nil? -%>"simple"<% else -%>"full_numbers"<% end -%>,
87
87
  "language": {
88
88
  <!-- See DefaultPaginationSqlDaoHelper.java -->
@@ -52,7 +52,7 @@
52
52
  <% Kaui.account_invoices_columns.call[0].each_with_index do |title, index| %>
53
53
  <li class="list-group-item-manual" data-id="<%= index %>">
54
54
  <label class="label-group-item-manual">
55
- <input type="checkbox" class="column-toggle" draggable="true" data-column="<%= index %>" checked>
55
+ <input type="checkbox" class="column-toggle" draggable="true" data-column="<%= index %>" <%= 'checked' if @dropdown_default[index][:visible] %>>
56
56
  <%= title %>
57
57
  <span class="icon-drag" aria-hidden="true"></span>
58
58
  </label>
@@ -337,7 +337,7 @@ $(document).ready(function() {
337
337
  updateDropdownOrder();
338
338
 
339
339
  function loadState() {
340
- var state = JSON.parse(localStorage.getItem('DataTables_invoices-table'));
340
+ var state = JSON.parse(localStorage.getItem('DataTables_invoices-table-v2'));
341
341
  return state || { columns: [], columnOrder: [] };
342
342
  }
343
343
 
@@ -50,7 +50,7 @@ $(document).ready(function() {
50
50
  }
51
51
  });
52
52
 
53
- var stateKey = 'DataTables_invoices-table';
53
+ var stateKey = 'DataTables_invoices-table-v2';
54
54
  var state = JSON.parse(localStorage.getItem(stateKey));
55
55
  if (state) {
56
56
  state.start = <%= @offset %>;
@@ -63,6 +63,7 @@ $(document).ready(function() {
63
63
  "enable": false
64
64
  },
65
65
  "stateSave": true,
66
+ "stateDuration": -1,
66
67
  "stateSaveCallback": function(settings, data) {
67
68
  localStorage.setItem(stateKey, JSON.stringify(data));
68
69
  },
@@ -105,11 +106,11 @@ $(document).ready(function() {
105
106
  // If the page loaded with advanced search params in the URL (e.g. restored from
106
107
  // localStorage) but @advance_search_query was not set server-side, re-fire the
107
108
  // DataTable request with the correct filters.
108
- if (window.location.search.includes('_q=1') && '<%= j(@advance_search_query.to_s.strip) %>' === '' && !window.location.pathname.includes('/accounts/')) {
109
- var urlSearch = window.location.search.substring(1).replace(/ac_id/g, 'account_id');
110
- var ajaxUrl = "<%= invoices_pagination_path(:ordering => @ordering, :format => :json) %>" + '?' + urlSearch;
109
+ if (window.location.search.includes('advance_search_query=') && '<%= j(@advance_search_query.to_s.strip) %>' === '' && !window.location.pathname.includes('/accounts/')) {
110
+ var urlSearch = new URLSearchParams(window.location.search).get('advance_search_query') || '';
111
+ var ajaxUrl = "<%= invoices_pagination_path(:ordering => @ordering, :format => :json) %>" + '?advance_search_query=' + encodeURIComponent(urlSearch);
111
112
  table.on('preXhr.dt.filter', function(e, settings, data) {
112
- data.search.value = urlSearch;
113
+ data.advance_search_query = urlSearch;
113
114
  });
114
115
  table.ajax.url(ajaxUrl).load();
115
116
  }
@@ -52,7 +52,7 @@
52
52
  <% Kaui.account_payments_columns.call()[0].each_with_index do |title, index| %>
53
53
  <li class="list-group-item-manual" data-id="<%= index %>">
54
54
  <label class="label-group-item-manual">
55
- <input type="checkbox" class="column-toggle" draggable="true" data-column="<%= index %>" checked>
55
+ <input type="checkbox" class="column-toggle" draggable="true" data-column="<%= index %>" <%= 'checked' if @dropdown_default[index][:visible] %>>
56
56
  <%= title %>
57
57
  <span class="icon-drag" aria-hidden="true"></span>
58
58
  </label>
@@ -338,7 +338,7 @@ $(document).ready(function() {
338
338
  updateDropdownOrder();
339
339
 
340
340
  function loadState() {
341
- var state = JSON.parse(localStorage.getItem('DataTables_payments-table'));
341
+ var state = JSON.parse(localStorage.getItem('DataTables_payments-table-v2'));
342
342
  return state || { columns: [], columnOrder: [] };
343
343
  }
344
344
 
@@ -41,7 +41,7 @@
41
41
 
42
42
  <%= javascript_tag do %>
43
43
  $(document).ready(function() {
44
- var stateKey = 'DataTables_payments-table';
44
+ var stateKey = 'DataTables_payments-table-v2';
45
45
  var state = JSON.parse(localStorage.getItem(stateKey));
46
46
  if (state) {
47
47
  state.start = <%= @offset %>;
@@ -54,11 +54,12 @@ $(document).ready(function() {
54
54
  "enable": false
55
55
  },
56
56
  "stateSave": true,
57
+ "stateDuration": -1,
57
58
  "stateSaveCallback": function(settings, data) {
58
- localStorage.setItem('DataTables_payments-table', JSON.stringify(data));
59
+ localStorage.setItem(stateKey, JSON.stringify(data));
59
60
  },
60
61
  "stateLoadCallback": function(settings) {
61
- return JSON.parse(localStorage.getItem('DataTables_payments-table'));
62
+ return JSON.parse(localStorage.getItem(stateKey));
62
63
  },
63
64
  "scrollX": true,
64
65
  <% if @account.account_id.blank? %>
@@ -114,17 +115,17 @@ $(document).ready(function() {
114
115
  <% end %>
115
116
  "processing": true,
116
117
  "serverSide": true,
117
- "search": {"search": "<%= @search_query %>"},
118
+ "search": {"search": "<%= @search_query %>"}
118
119
  });
119
120
 
120
121
  // If the page loaded with advanced search params in the URL (e.g. restored from
121
122
  // localStorage) but @advance_search_query was not set server-side, re-fire the
122
123
  // DataTable request with the correct filters.
123
- if (window.location.search.includes('_q=1') && '<%= j(@advance_search_query.to_s.strip) %>' === '' && !window.location.pathname.includes('/accounts/')) {
124
- var urlSearch = window.location.search.substring(1).replace(/ac_id/g, 'account_id');
125
- var ajaxUrl = "<%= payments_pagination_path(:ordering => @ordering, :format => :json) %>" + '?' + urlSearch;
124
+ if (window.location.search.includes('advance_search_query=') && '<%= j(@advance_search_query.to_s.strip) %>' === '' && !window.location.pathname.includes('/accounts/')) {
125
+ var urlSearch = new URLSearchParams(window.location.search).get('advance_search_query') || '';
126
+ var ajaxUrl = "<%= payments_pagination_path(:ordering => @ordering, :format => :json) %>" + '?advance_search_query=' + encodeURIComponent(urlSearch);
126
127
  table.on('preXhr.dt.filter', function(e, settings, data) {
127
- data.search.value = urlSearch;
128
+ data.advance_search_query = urlSearch;
128
129
  });
129
130
  table.ajax.url(ajaxUrl).load();
130
131
  }
@@ -191,19 +191,19 @@ $(document).ready(function() {
191
191
  var table = $('<%= table_selector %>').DataTable();
192
192
  table.off('preXhr.dt.filter');
193
193
  table.on('preXhr.dt.filter', function(e, settings, data) {
194
- data.search.value = searchQuery('<%= j search_query.to_s %>');
194
+ data.advance_search_query = decodeURIComponent(searchQuery('<%= j search_query.to_s %>'));
195
195
  });
196
196
 
197
197
  var searchParams = searchQuery('<%= j search_query.to_s %>');
198
+ var rawSearchParams = searchParams ? decodeURIComponent(searchParams) : searchParams;
198
199
  var ajaxUrl = "<%= pagination_path %>";
199
- if (searchParams) {
200
- ajaxUrl += (ajaxUrl.includes('?') ? '&' : '?') + searchParams;
200
+ if (rawSearchParams) {
201
+ ajaxUrl += (ajaxUrl.includes('?') ? '&' : '?') + 'advance_search_query=' + encodeURIComponent(rawSearchParams);
201
202
  }
202
203
  table.ajax.url(ajaxUrl).load();
203
204
 
204
- if (searchParams) {
205
- searchParams = searchParams.replace(/account_id/g, 'ac_id');
206
- var newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + searchParams;
205
+ if (rawSearchParams) {
206
+ var newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?advance_search_query=' + encodeURIComponent(rawSearchParams);
207
207
  window.history.pushState({ path: newUrl }, '', newUrl);
208
208
  }
209
209
 
@@ -22,22 +22,34 @@
22
22
  <%= form_tag create_usage_path(@subscription.subscription_id), method: :post, id: 'record_usage_form', class: 'form-horizontal' do %>
23
23
 
24
24
  <div class="form-group d-flex pb-3">
25
- <%= label_tag :unit_type, 'Unit Type', class: 'col-sm-3 control-label' %>
25
+ <%= label_tag :unit_type, (@is_aviate_catalog ? 'Billing Meter Code' : 'Unit Type'), class: 'col-sm-3 control-label' %>
26
26
  <div class="col-sm-9">
27
27
  <% if @unit_types.length == 1 %>
28
28
  <%= text_field_tag :unit_type, @unit_types.first, readonly: true, required: true, class: 'form-control bg-light' %>
29
29
  <% elsif @unit_types.length > 1 %>
30
30
  <%= select_tag :unit_type, options_for_select(@unit_types.map { |u| [u, u] }, @unit_type.presence || @unit_types.first), required: true, class: 'form-control', title: (@unit_type.presence || @unit_types.first) %>
31
31
  <% else %>
32
- <%= text_field_tag :unit_type, @unit_type, required: true, maxlength: 255, class: 'form-control', placeholder: 'e.g. api-calls' %>
32
+ <%= text_field_tag :unit_type, @unit_type, required: true, maxlength: 255, class: 'form-control', placeholder: (@is_aviate_catalog ? 'e.g. api-calls-meter' : 'e.g. api-calls') %>
33
33
  <% end %>
34
+ <small class="form-text text-muted">
35
+ <% if @is_aviate_catalog %>
36
+ This subscription's plan is billed via an Aviate catalog: usage is recorded against a <strong>billing meter code</strong> instead of a unit type.
37
+ <% else %>
38
+ This subscription's plan is billed via a standard (XML) catalog: usage is recorded against a <strong>unit type</strong>.
39
+ <% end %>
40
+ <a href="#" data-toggle="tooltip" class="kb-tooltip" title="Kaui automatically detects the catalog type for this subscription's account and submits usage through the matching Kill Bill API: the Record Usage API (unit type) for standard catalogs, or the Aviate Submit Usage Events API (billing meter code) for Aviate catalogs."></a>
41
+ </small>
34
42
  </div>
35
43
  </div>
36
44
 
37
45
  <div class="form-group d-flex pb-3">
38
46
  <%= label_tag :amount, 'Amount', class: 'col-sm-3 control-label' %>
39
47
  <div class="col-sm-9">
40
- <%= number_field_tag :amount, @amount, required: true, min: 1, step: 1, class: 'form-control', placeholder: 'Positive integer' %>
48
+ <% if @is_aviate_catalog %>
49
+ <%= number_field_tag :amount, @amount, required: true, min: 0, step: 'any', class: 'form-control', placeholder: 'Positive number' %>
50
+ <% else %>
51
+ <%= number_field_tag :amount, @amount, required: true, min: 1, step: 1, class: 'form-control', placeholder: 'Positive integer' %>
52
+ <% end %>
41
53
  </div>
42
54
  </div>
43
55
 
@@ -84,10 +96,12 @@
84
96
  <div class="form-group d-flex pb-3">
85
97
  <%= label_tag :tracking_id, 'Tracking ID', class: 'col-sm-3 control-label' %>
86
98
  <div class="col-sm-9">
87
- <%= text_field_tag :tracking_id, @tracking_id, maxlength: 255, class: 'form-control', placeholder: 'Optional, used for idempotency' %>
99
+ <%= text_field_tag :tracking_id, @tracking_id, maxlength: 255, class: 'form-control', placeholder: (@is_aviate_catalog ? 'Optional UUID, auto-generated if left blank' : 'Optional, used for idempotency') %>
88
100
  </div>
89
101
  </div>
90
102
 
103
+ <%= hidden_field_tag :is_aviate_catalog, @is_aviate_catalog.to_s %>
104
+
91
105
  <div class="form-group d-flex justify-content-end pb-3">
92
106
  <%= render "kaui/components/button/button", {
93
107
  label: 'Close',
@@ -113,8 +127,11 @@
113
127
 
114
128
  <%= javascript_tag do %>
115
129
  document.addEventListener('DOMContentLoaded', function () {
130
+ $('[data-toggle="tooltip"]').tooltip();
131
+
116
132
  var form = document.getElementById('record_usage_form');
117
133
  if (!form) { return; }
134
+ var isAviateCatalog = document.getElementById('is_aviate_catalog') && document.getElementById('is_aviate_catalog').value === 'true';
118
135
  var unitTypeSelect = document.getElementById('unit_type');
119
136
  if (unitTypeSelect && unitTypeSelect.tagName === 'SELECT') {
120
137
  unitTypeSelect.title = unitTypeSelect.value || '';
@@ -129,13 +146,20 @@
129
146
  var unitTypeEl = document.getElementById('unit_type');
130
147
  var unitType = unitTypeEl ? (unitTypeEl.value || '').trim() : '';
131
148
  if (unitType.length === 0) {
132
- errors.push('Unit type is required.');
149
+ errors.push(isAviateCatalog ? 'Billing meter code is required.' : 'Unit type is required.');
133
150
  }
134
151
 
135
152
  var amountStr = (document.getElementById('amount').value || '').trim();
136
- var amount = parseInt(amountStr, 10);
137
- if (amountStr.length === 0 || isNaN(amount) || amount <= 0 || String(amount) !== amountStr) {
138
- errors.push('Amount must be a positive integer.');
153
+ if (isAviateCatalog) {
154
+ var floatAmount = parseFloat(amountStr);
155
+ if (amountStr.length === 0 || isNaN(floatAmount) || floatAmount <= 0) {
156
+ errors.push('Amount must be a positive number.');
157
+ }
158
+ } else {
159
+ var amount = parseInt(amountStr, 10);
160
+ if (amountStr.length === 0 || isNaN(amount) || amount <= 0 || String(amount) !== amountStr) {
161
+ errors.push('Amount must be a positive integer.');
162
+ }
139
163
  }
140
164
 
141
165
  var dateEl = document.getElementById('record_date_date');
@@ -71,7 +71,7 @@
71
71
  <%= javascript_tag do %>
72
72
  $(document).ready(function() {
73
73
  var table = $('#tags-table').DataTable({
74
- "dom": "<'row'<'col-md-6'l><'col-md-6'f>r>t<'row'<'col-md-6'i><'col-md-6'p>>",
74
+ "dom": "<'row'<'col-md-6'f>r>t<'row'<'col-md-6'i><'col-md-6'p>>",
75
75
  "pagingType": <% if @max_nb_records.nil? -%>"simple"<% else -%>"full_numbers"<% end -%>,
76
76
  "language": {
77
77
  "info": <% if @max_nb_records.nil? -%>"Showing _START_ to _END_ of <%= number_with_delimiter(Kaui::EngineControllerUtil::SIMPLE_PAGINATION_THRESHOLD) -%>+ entries"<% else -%>"Showing _START_ to _END_ of _TOTAL_ entries"<% end -%>
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.20'
4
+ VERSION = '4.0.24'
5
5
  end
data/lib/kaui.rb CHANGED
@@ -66,6 +66,8 @@ module Kaui
66
66
  # Add additional fields if needed
67
67
  fields = original_fields.dup
68
68
  fields -= %w[audit_logs first_name_length]
69
+ # Demo-friendly default column order (see Kaui::Account::DEFAULT_VISIBLE_COLUMNS for the ones shown by default)
70
+ fields = Kaui::Account::DEFAULT_VISIBLE_COLUMNS + (fields - Kaui::Account::DEFAULT_VISIBLE_COLUMNS)
69
71
  headers = fields.dup
70
72
  Kaui::Account::REMAPPING_FIELDS.each do |k, v|
71
73
  headers[fields.index(k)] = v
@@ -115,8 +117,8 @@ module Kaui
115
117
  fields = KillBillClient::Model::InvoiceAttributes.instance_variable_get(:@json_attributes)
116
118
  # Change the order if needed
117
119
  fields -= Kaui::Invoice::TABLE_IGNORE_COLUMNS
118
- fields.delete('invoice_id')
119
- fields.unshift('invoice_id')
120
+ # Demo-friendly default column order (see Kaui::Invoice::DEFAULT_VISIBLE_COLUMNS for the ones shown by default)
121
+ fields = Kaui::Invoice::DEFAULT_VISIBLE_COLUMNS + (fields - Kaui::Invoice::DEFAULT_VISIBLE_COLUMNS)
120
122
 
121
123
  headers = fields.map { |attr| attr.split('_').join(' ').capitalize }
122
124
  # Add additional headers if needed
@@ -131,6 +133,8 @@ module Kaui
131
133
  view_context.humanized_money_with_symbol(invoice.amount_to_money)
132
134
  when 'balance'
133
135
  view_context.humanized_money_with_symbol(invoice.balance_to_money)
136
+ when 'invoice_number'
137
+ view_context.link_to(invoice.invoice_number, view_context.url_for(controller: :invoices, action: :show, account_id: invoice.account_id, id: invoice.invoice_id))
134
138
  when 'invoice_id'
135
139
  view_context.link_to(invoice.invoice_id, view_context.url_for(controller: :invoices, action: :show, account_id: invoice.account_id, id: invoice.invoice_id))
136
140
  when 'status'
@@ -146,16 +150,16 @@ module Kaui
146
150
 
147
151
  raw_data = fields.map { |attr| invoice&.send(attr.downcase) }
148
152
 
149
- [headers, values, raw_data]
153
+ [headers, values, raw_data, fields]
150
154
  end
151
155
 
152
156
  self.account_payments_columns = lambda do |account = nil, payment = nil, view_context = nil|
153
157
  fields = KillBillClient::Model::PaymentAttributes.instance_variable_get(:@json_attributes)
154
158
  # Change the order if needed
155
- fields = %w[payment_date] + fields
156
- fields -= %w[payment_number transactions audit_logs]
157
- fields.unshift('status')
158
- fields.unshift('payment_number')
159
+ fields = %w[payment_date status] + fields
160
+ fields -= %w[transactions audit_logs]
161
+ # Demo-friendly default column order (see Kaui::Payment::DEFAULT_VISIBLE_COLUMNS for the ones shown by default)
162
+ fields = Kaui::Payment::DEFAULT_VISIBLE_COLUMNS + (fields - Kaui::Payment::DEFAULT_VISIBLE_COLUMNS)
159
163
 
160
164
  headers = fields.dup
161
165
  Kaui::Payment::REMAPPING_FIELDS.each do |k, v|
@@ -163,7 +167,7 @@ module Kaui
163
167
  end
164
168
  headers.map! { |attr| attr.split('_').join(' ').capitalize }
165
169
 
166
- return [headers, []] if payment.nil?
170
+ return [headers, [], [], fields] if payment.nil?
167
171
 
168
172
  values = fields.map do |attr|
169
173
  case attr
@@ -190,7 +194,7 @@ module Kaui
190
194
  end
191
195
 
192
196
  # Add additional values if needed
193
- [headers, values, raw_data]
197
+ [headers, values, raw_data, fields]
194
198
  end
195
199
 
196
200
  self.account_audit_logs_columns = lambda do
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.20
4
+ version: 4.0.24
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-07-15 00:00:00.000000000 Z
11
+ date: 2026-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -510,6 +510,7 @@ files:
510
510
  - app/models/kaui/usage.rb
511
511
  - app/models/kaui/user.rb
512
512
  - app/models/kaui/user_role.rb
513
+ - app/services/dependencies/aviate.rb
513
514
  - app/services/dependencies/kenui.rb
514
515
  - app/views/kaui/account_children/index.html.erb
515
516
  - app/views/kaui/account_custom_fields/index.html.erb