kaui 4.0.21 → 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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/kaui/multi_functions_bar_utils.js +18 -9
- data/app/assets/stylesheets/kaui/tenants.css +53 -0
- data/app/controllers/kaui/admin_tenants_controller.rb +12 -0
- data/app/controllers/kaui/subscriptions_controller.rb +58 -23
- data/app/models/kaui/admin_tenant.rb +10 -0
- data/app/services/dependencies/aviate.rb +89 -0
- data/app/views/kaui/account_tags/index.html.erb +1 -1
- data/app/views/kaui/accounts/index.html.erb +2 -3
- data/app/views/kaui/admin_tenants/_form_invoice_template.erb +0 -1
- data/app/views/kaui/admin_tenants/_form_invoice_translation.erb +47 -1
- data/app/views/kaui/audit_logs/index.html.erb +1 -1
- data/app/views/kaui/components/button/_button.html.erb +1 -1
- data/app/views/kaui/custom_fields/index.html.erb +1 -1
- data/app/views/kaui/invoices/index.html.erb +5 -6
- data/app/views/kaui/payments/index.html.erb +5 -6
- data/app/views/kaui/shared/_advanced_search_filterbar.html.erb +6 -6
- data/app/views/kaui/subscriptions/record_usage.erb +32 -8
- data/app/views/kaui/tags/index.html.erb +1 -1
- data/lib/kaui/version.rb +1 -1
- data/lib/kaui.rb +2 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19115a4ca850dfd755598a560dc66e96b00694f00b305e057260fd6b13c57e3d
|
|
4
|
+
data.tar.gz: 9780699cd5e97d8c6887b3e0c200d968a11e644afd5496c58829277d558a28bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
33
|
-
queryString =
|
|
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.
|
|
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 (
|
|
250
|
-
var
|
|
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 {
|
|
@@ -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
|
|
@@ -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
|
-
@
|
|
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
|
-
@
|
|
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 =
|
|
219
|
+
@tracking_id = tracking_id
|
|
211
220
|
render :record_usage and return
|
|
212
221
|
end
|
|
213
222
|
|
|
214
223
|
begin
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
243
|
+
usage.create(current_user.kb_username, nil, nil, options_for_klient)
|
|
244
|
+
end
|
|
229
245
|
|
|
230
|
-
|
|
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
|
-
@
|
|
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 =
|
|
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|
|
|
@@ -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
|
|
@@ -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'
|
|
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
|
|
|
@@ -132,11 +132,10 @@ $(document).ready(function() {
|
|
|
132
132
|
// If the page loaded with advanced search params in the URL (e.g. restored from
|
|
133
133
|
// localStorage) but @advance_search_query was not set server-side, re-fire the
|
|
134
134
|
// DataTable request with the correct filters.
|
|
135
|
-
if (window.location.search.includes('
|
|
136
|
-
var urlSearch = window.location.search.
|
|
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') || '';
|
|
137
137
|
var ajaxUrl = "<%= accounts_pagination_path(:ordering => @ordering, :format => :json) %>";
|
|
138
138
|
table.on('preXhr.dt.filter', function(e, settings, data) {
|
|
139
|
-
data.search.value = urlSearch;
|
|
140
139
|
data.advance_search_query = urlSearch;
|
|
141
140
|
});
|
|
142
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
|
-
<%=
|
|
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'
|
|
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}
|
|
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'
|
|
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 -->
|
|
@@ -100,18 +100,17 @@ $(document).ready(function() {
|
|
|
100
100
|
});
|
|
101
101
|
return reorderedData;
|
|
102
102
|
}
|
|
103
|
-
}
|
|
104
|
-
"columns": <%= raw @visible_columns.to_json %>
|
|
103
|
+
}
|
|
105
104
|
});
|
|
106
105
|
|
|
107
106
|
// If the page loaded with advanced search params in the URL (e.g. restored from
|
|
108
107
|
// localStorage) but @advance_search_query was not set server-side, re-fire the
|
|
109
108
|
// DataTable request with the correct filters.
|
|
110
|
-
if (window.location.search.includes('
|
|
111
|
-
var urlSearch = window.location.search.
|
|
112
|
-
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);
|
|
113
112
|
table.on('preXhr.dt.filter', function(e, settings, data) {
|
|
114
|
-
data.
|
|
113
|
+
data.advance_search_query = urlSearch;
|
|
115
114
|
});
|
|
116
115
|
table.ajax.url(ajaxUrl).load();
|
|
117
116
|
}
|
|
@@ -115,18 +115,17 @@ $(document).ready(function() {
|
|
|
115
115
|
<% end %>
|
|
116
116
|
"processing": true,
|
|
117
117
|
"serverSide": true,
|
|
118
|
-
"search": {"search": "<%= @search_query %>"}
|
|
119
|
-
"columns": <%= raw @visible_columns.to_json %>
|
|
118
|
+
"search": {"search": "<%= @search_query %>"}
|
|
120
119
|
});
|
|
121
120
|
|
|
122
121
|
// If the page loaded with advanced search params in the URL (e.g. restored from
|
|
123
122
|
// localStorage) but @advance_search_query was not set server-side, re-fire the
|
|
124
123
|
// DataTable request with the correct filters.
|
|
125
|
-
if (window.location.search.includes('
|
|
126
|
-
var urlSearch = window.location.search.
|
|
127
|
-
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);
|
|
128
127
|
table.on('preXhr.dt.filter', function(e, settings, data) {
|
|
129
|
-
data.
|
|
128
|
+
data.advance_search_query = urlSearch;
|
|
130
129
|
});
|
|
131
130
|
table.ajax.url(ajaxUrl).load();
|
|
132
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.
|
|
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 (
|
|
200
|
-
ajaxUrl += (ajaxUrl.includes('?') ? '&' : '?') +
|
|
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 (
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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'
|
|
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
data/lib/kaui.rb
CHANGED
|
@@ -133,6 +133,8 @@ module Kaui
|
|
|
133
133
|
view_context.humanized_money_with_symbol(invoice.amount_to_money)
|
|
134
134
|
when 'balance'
|
|
135
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))
|
|
136
138
|
when 'invoice_id'
|
|
137
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))
|
|
138
140
|
when 'status'
|
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.
|
|
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-
|
|
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
|