kaui 4.0.19 → 4.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f667be5cb34e03c3d1b4da18ef422934d9794643606c998cf1ac84f2e6d99f60
4
- data.tar.gz: e202ccb38314d766896d3b72235de05c2818b74b985cb8b2717ff4850ab0b4ae
3
+ metadata.gz: ef67e5aac2cf29cee71a7a52b5b87452afc410ed711d1d55c08225dc39b1d044
4
+ data.tar.gz: 6232e9397218afea690a6d45422623b16fb346489a380eaaa6fdba3ed6e98930
5
5
  SHA512:
6
- metadata.gz: 18c8742fd7498cab0431aa9758b7f7ebda0e225d7fba76c9dd8a1fa97bd52a8678812b3a6e0397e1d15fc8128e59f451ee455ac7ebdfe8cb13de888a86101501
7
- data.tar.gz: ee27c185159e1b564e0a0d16322178ddf3fbbce4c78929a0dfd3747c4c198084f1269f3e7d261047b0a2589b5f08d70be3ab07f8d727dfcc969e372929f7395a
6
+ metadata.gz: d2cd48b2725369b2a466e49cabbf656ce64b6bea4cdcb272f97fb7d34c909bdb56a6876c13af14328a1dcdd0d341e9d04757318d9504aeece87d55ad2ba8a6fa
7
+ data.tar.gz: 8abadbb98b2d800acb54b5dd7fd9bdcf20a8a49186c1b3b9b2357eb3a717ff6a79a45094aebcfaa252a11a8ca562ac5ab2812080c6abb3774ea711b57d98099b
@@ -47,6 +47,12 @@ module Kaui
47
47
  @overdue_xml = nil
48
48
  end
49
49
 
50
+ fetch_invoice_template = promise do
51
+ Kaui::AdminTenant.get_invoice_template(false, options)
52
+ rescue StandardError
53
+ @invoice_template = nil
54
+ end
55
+
50
56
  fetch_tenant_plugin_config = promise { Kaui::AdminTenant.get_tenant_plugin_config(options) }
51
57
 
52
58
  @catalog_versions = []
@@ -64,6 +70,12 @@ module Kaui
64
70
  @overdue = flash[:overdue_deleted] ? nil : wait(fetch_overdue)
65
71
  @overdue_xml = flash[:overdue_deleted] ? nil : wait(fetch_overdue_xml)
66
72
  @overdue_config_exists = @overdue_xml.present? || (@overdue&.overdue_states.present? && !@overdue.has_states)
73
+ @invoice_template = begin
74
+ wait(fetch_invoice_template)
75
+ rescue StandardError
76
+ nil
77
+ end
78
+
67
79
  @tenant_plugin_config = begin
68
80
  wait(fetch_tenant_plugin_config)
69
81
  rescue StandardError
@@ -367,6 +379,22 @@ module Kaui
367
379
  redirect_to admin_tenant_path(current_tenant.id, active_tab: 'OverdueShow'), notice: I18n.t('flashes.notices.overdue_deleted_successfully')
368
380
  end
369
381
 
382
+ def invoice_template
383
+ current_tenant = safely_find_tenant_by_id(params[:id])
384
+
385
+ options = tenant_options_for_client
386
+ options[:api_key] = current_tenant.api_key
387
+ options[:api_secret] = current_tenant.api_secret
388
+
389
+ template = Kaui::AdminTenant.get_invoice_template(false, options)
390
+
391
+ if template.present?
392
+ render body: template, content_type: 'text/html'
393
+ else
394
+ render plain: 'No invoice template found', status: :not_found
395
+ end
396
+ end
397
+
370
398
  def upload_invoice_template
371
399
  current_tenant = safely_find_tenant_by_id(params[:id])
372
400
 
@@ -380,7 +408,7 @@ module Kaui
380
408
 
381
409
  Kaui::AdminTenant.upload_invoice_template(invoice_template, is_manual_pay, true, options[:username], nil, comment, options)
382
410
 
383
- redirect_to admin_tenant_path(current_tenant.id), notice: I18n.t('flashes.notices.invoice_template_uploaded_successfully')
411
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'InvoiceTemplate'), notice: I18n.t('flashes.notices.invoice_template_uploaded_successfully')
384
412
  end
385
413
 
386
414
  def upload_invoice_translation
@@ -394,9 +422,14 @@ module Kaui
394
422
  uploaded_invoice_translation = params.require(:invoice_translation)
395
423
  invoice_translation = uploaded_invoice_translation.read
396
424
 
425
+ if locale.blank?
426
+ flash[:error] = I18n.t('errors.messages.locale_required')
427
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'InvoiceTranslation') and return
428
+ end
429
+
397
430
  Kaui::AdminTenant.upload_invoice_translation(invoice_translation, locale, true, options[:username], nil, comment, options)
398
431
 
399
- redirect_to admin_tenant_path(current_tenant.id), notice: I18n.t('flashes.notices.invoice_translation_uploaded_successfully')
432
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'InvoiceTranslation'), notice: I18n.t('flashes.notices.invoice_translation_uploaded_successfully')
400
433
  end
401
434
 
402
435
  def upload_catalog_translation
@@ -410,9 +443,14 @@ module Kaui
410
443
  uploaded_catalog_translation = params.require(:catalog_translation)
411
444
  catalog_translation = uploaded_catalog_translation.read
412
445
 
446
+ if locale.blank?
447
+ flash[:error] = I18n.t('errors.messages.locale_required')
448
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'CatalogTranslation') and return
449
+ end
450
+
413
451
  Kaui::AdminTenant.upload_catalog_translation(catalog_translation, locale, true, options[:username], nil, comment, options)
414
452
 
415
- redirect_to admin_tenant_path(current_tenant.id), notice: I18n.t('flashes.notices.catalog_translation_uploaded_successfully')
453
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'CatalogTranslation'), notice: I18n.t('flashes.notices.catalog_translation_uploaded_successfully')
416
454
  end
417
455
 
418
456
  def upload_plugin_config
@@ -353,7 +353,7 @@ module Kaui
353
353
  return nil if raw.blank?
354
354
 
355
355
  entries = raw.respond_to?(:values) ? raw.values : Array(raw)
356
- phase_meta = (plan_details.phases || []).index_by(&:type)
356
+ phase_meta = plan_details.respond_to?(:phases) ? (plan_details.phases || []).index_by(&:type) : {}
357
357
 
358
358
  overrides = entries.filter_map do |entry|
359
359
  entry = entry.to_unsafe_h if entry.respond_to?(:to_unsafe_h)
@@ -384,18 +384,25 @@ module Kaui
384
384
 
385
385
  def build_plan_phases_map(plans_details)
386
386
  (plans_details || []).to_h do |pd|
387
- phases = (pd.phases || []).map do |ph|
388
- fixed = phase_uses_fixed_price?(ph)
389
- prices = ph.prices || []
390
- price_label = if fixed
391
- '$0.00'
392
- elsif prices.any?
393
- format('$%.2f', prices.first.value.to_f)
394
- else
395
- ''
396
- end
397
- { type: ph.type, fixed: fixed, priceLabel: price_label }
398
- end
387
+ # PlanDetail objects returned by available_base_plans/available_addons only expose the
388
+ # final phase (product/plan/priceList/finalPhaseBillingPeriod/finalPhaseRecurringPrice)
389
+ # and have no phases method, unlike full catalog Plan objects. Skip phase overrides for those.
390
+ phases = if pd.respond_to?(:phases)
391
+ (pd.phases || []).map do |ph|
392
+ fixed = phase_uses_fixed_price?(ph)
393
+ prices = ph.prices || []
394
+ price_label = if fixed
395
+ '$0.00'
396
+ elsif prices.any?
397
+ format('$%.2f', prices.first.value.to_f)
398
+ else
399
+ ''
400
+ end
401
+ { type: ph.type, fixed: fixed, priceLabel: price_label }
402
+ end
403
+ else
404
+ []
405
+ end
399
406
  [pd.plan, phases]
400
407
  end
401
408
  end
@@ -15,6 +15,10 @@ module Kaui
15
15
  KillBillClient::Model::Invoice.upload_invoice_template(invoice_template, is_manual_pay, delete_if_exists, user, reason, comment, options)
16
16
  end
17
17
 
18
+ def get_invoice_template(is_manual_pay, options = {})
19
+ KillBillClient::Model::Invoice.get_invoice_template(is_manual_pay, nil, options)
20
+ end
21
+
18
22
  def upload_invoice_translation(invoice_translation, locale, delete_if_exists, user = nil, reason = nil, comment = nil, options = {})
19
23
  KillBillClient::Model::Invoice.upload_invoice_translation(invoice_translation, locale, delete_if_exists, user, reason, comment, options)
20
24
  end
@@ -21,7 +21,7 @@
21
21
  <div class="form-group d-flex pb-3 border-bottom mb-3">
22
22
  <%= label_tag :translation_locale, 'Locale', :class => 'col-sm-2 control-label' %>
23
23
  <div class="col-sm-10">
24
- <%= text_field_tag :translation_locale, nil, :class => 'form-control' %>
24
+ <%= text_field_tag :translation_locale, nil, :class => 'form-control', :required => true, :placeholder => 'e.g. en_US' %>
25
25
  </div>
26
26
  </div>
27
27
  <div class="d-flex justify-content-end">
@@ -1,5 +1,28 @@
1
1
  <% if can? :config_upload, Kaui::AdminTenant %>
2
2
  <div class="tab-pane fade invoice-template form-invoice-template" id="InvoiceTemplate">
3
+ <% if @invoice_template.present? %>
4
+ <div class="invoice-template-display mb-4">
5
+ <h5>Current Invoice Template</h5>
6
+ <div class="card">
7
+ <div class="card-body p-0">
8
+ <iframe src="<%= admin_tenant_invoice_template_path(@tenant.id) %>" style="width: 100%; height: 500px; border: none;" title="Invoice Template Preview"></iframe>
9
+ </div>
10
+ </div>
11
+ <div class="mt-2">
12
+ <%= render "kaui/components/button/button", {
13
+ label: 'View Source',
14
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
15
+ type: "button",
16
+ html_class: "kaui-dropdown custom-hover",
17
+ html_options: {
18
+ onclick: '$(this).toggleClass("active"); $(this).next("pre").toggle(); return false;'
19
+ }
20
+ } %>
21
+ <pre class="mb-0 mt-2 p-2 bg-light" style="display: none; max-height: 400px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; font-family: monospace; font-size: 12px;"><%= html_escape(@invoice_template) %></pre>
22
+ </div>
23
+ </div>
24
+ <% end %>
25
+
3
26
  <%= form_tag({:action => :upload_invoice_template}, :method => 'post', :multipart => true, :class => 'form-horizontal') do %>
4
27
  <%= hidden_field_tag(:id, @tenant.id) %>
5
28
 
@@ -19,8 +42,6 @@
19
42
  %>
20
43
  </div>
21
44
 
22
-
23
-
24
45
  <div class="form-group pb-3 border-bottom mb-3">
25
46
  <div class="col-sm-10">
26
47
  <div class="checkbox">
@@ -21,7 +21,7 @@
21
21
  <div class="form-group d-flex pb-3 border-bottom mb-3">
22
22
  <%= label_tag :translation_locale, 'Locale', :class => 'col-sm-2 control-label' %>
23
23
  <div class="col-sm-10">
24
- <%= text_field_tag :translation_locale, nil, :class => 'form-control' %>
24
+ <%= text_field_tag :translation_locale, nil, :class => 'form-control', :required => true, :placeholder => 'e.g. en_US' %>
25
25
  </div>
26
26
  </div>
27
27
  <div class="d-flex justify-content-end">
@@ -27,7 +27,7 @@
27
27
  <div style="width: 40px;"></div>
28
28
  </div>
29
29
  <div id="phase_overrides_rows"></div>
30
- <button type="button" id="add_phase_override" class="btn w-100 mt-2 d-flex align-items-center justify-content-center gap-1"
30
+ <button type="button" id="add_phase_override" class="btn w-100 mt-2 align-items-center justify-content-center gap-1"
31
31
  style="border: 1px dashed #d0d5dd; background: white; color: #2563eb; font-size: 14px;">
32
32
  <span style="font-size: 16px; line-height: 1;">+</span> Add another phase
33
33
  </button>
@@ -161,7 +161,13 @@ function initPhaseOverrides() {
161
161
  var $planSelect = $('select[name="plan_name"]');
162
162
 
163
163
  function currentPhases() {
164
- return planPhases[$planSelect.val()] || [];
164
+ var phases = planPhases[$planSelect.val()] || [];
165
+ var seen = {};
166
+ return phases.filter(function(p) {
167
+ if (!p || !p.type || seen[p.type]) return false;
168
+ seen[p.type] = true;
169
+ return true;
170
+ });
165
171
  }
166
172
 
167
173
  function phaseLabel(p) {
@@ -177,9 +183,13 @@ function initPhaseOverrides() {
177
183
 
178
184
  function refreshAddBtn() {
179
185
  var phases = currentPhases();
180
- var used = selectedTypes();
186
+ var used = selectedTypes().filter(function(type) { return type; });
181
187
  var hasMore = phases.length > 0 && used.length < phases.length;
182
- $addBtn.toggle(hasMore);
188
+ if (hasMore) {
189
+ $addBtn.css('display', 'flex');
190
+ } else {
191
+ $addBtn.hide();
192
+ }
183
193
  $headers.css('display', $rows.find('.phase-override-row').length > 0 ? 'flex' : 'none');
184
194
  }
185
195
 
@@ -205,7 +215,7 @@ function initPhaseOverrides() {
205
215
  function addRow(preferredType) {
206
216
  var phases = currentPhases();
207
217
  if (phases.length === 0) return;
208
- var used = selectedTypes();
218
+ var used = selectedTypes().filter(function(type) { return type; });
209
219
  var next = preferredType && phases.some(function(p) { return p.type === preferredType; }) && used.indexOf(preferredType) === -1
210
220
  ? preferredType
211
221
  : (phases.find(function(p) { return used.indexOf(p.type) === -1; }) || {}).type;
@@ -37,11 +37,11 @@
37
37
  <div class="form-group d-flex pb-3">
38
38
  <div class="col-sm-3 control-label" style="padding-top: 2px;">
39
39
  <div id="toggle_phase_overrides" style="cursor: pointer; user-select: none;">
40
- <span id="toggle_phase_overrides_caret">&#9662;</span>
41
- <strong>Phase Overrides</strong>
40
+ <span id="toggle_phase_overrides_caret">&#9656;</span>
41
+ <strong>Price Override</strong>
42
42
  </div>
43
43
  </div>
44
- <div class="col-sm-9" id="phase_overrides_section">
44
+ <div class="col-sm-9" id="phase_overrides_section" style="display: none;">
45
45
  <p class="text-muted mb-2" style="font-size: 13px;">Override prices for specific phases. All other phases will use catalog pricing.</p>
46
46
  <div id="phase_overrides_headers" class="d-flex mb-1 gap-2" style="display: none;">
47
47
  <div style="flex: 1; padding-left: 2px;"><small><strong>Phase</strong></small></div>
@@ -55,7 +55,7 @@
55
55
  <div style="width: 40px;"></div>
56
56
  </div>
57
57
  <div id="phase_overrides_rows"></div>
58
- <button type="button" id="add_phase_override" class="btn w-100 mt-2 d-flex align-items-center justify-content-center gap-1"
58
+ <button type="button" id="add_phase_override" class="btn w-100 mt-2 align-items-center justify-content-center gap-1"
59
59
  style="border: 1px dashed #d0d5dd; background: white; color: #2563eb; font-size: 14px;">
60
60
  <span style="font-size: 16px; line-height: 1;">+</span> Add another phase
61
61
  </button>
@@ -178,7 +178,13 @@ function initPhaseOverrides() {
178
178
  var $planSelect = $('select[name="plan_name"]');
179
179
 
180
180
  function currentPhases() {
181
- return planPhases[$planSelect.val()] || [];
181
+ var phases = planPhases[$planSelect.val()] || [];
182
+ var seen = {};
183
+ return phases.filter(function(p) {
184
+ if (!p || !p.type || seen[p.type]) return false;
185
+ seen[p.type] = true;
186
+ return true;
187
+ });
182
188
  }
183
189
 
184
190
  function phaseLabel(p) {
@@ -194,9 +200,13 @@ function initPhaseOverrides() {
194
200
 
195
201
  function refreshAddBtn() {
196
202
  var phases = currentPhases();
197
- var used = selectedTypes();
203
+ var used = selectedTypes().filter(function(type) { return type; });
198
204
  var hasMore = phases.length > 0 && used.length < phases.length;
199
- $addBtn.toggle(hasMore);
205
+ if (hasMore) {
206
+ $addBtn.css('display', 'flex');
207
+ } else {
208
+ $addBtn.hide();
209
+ }
200
210
  $headers.css('display', $rows.find('.phase-override-row').length > 0 ? 'flex' : 'none');
201
211
  }
202
212
 
@@ -222,7 +232,7 @@ function initPhaseOverrides() {
222
232
  function addRow(preferredType) {
223
233
  var phases = currentPhases();
224
234
  if (phases.length === 0) return;
225
- var used = selectedTypes();
235
+ var used = selectedTypes().filter(function(type) { return type; });
226
236
  var next = preferredType && phases.some(function(p) { return p.type === preferredType; }) && used.indexOf(preferredType) === -1
227
237
  ? preferredType
228
238
  : (phases.find(function(p) { return used.indexOf(p.type) === -1; }) || {}).type;
@@ -27,7 +27,7 @@
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
- <%= select_tag :unit_type, options_for_select(@unit_types.map { |u| [u, u] }, @unit_type.presence || @unit_types.first), required: true, class: 'form-select' %>
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
32
  <%= text_field_tag :unit_type, @unit_type, required: true, maxlength: 255, class: 'form-control', placeholder: 'e.g. api-calls' %>
33
33
  <% end %>
@@ -44,11 +44,40 @@
44
44
  <div class="form-group d-flex pb-3">
45
45
  <%= label_tag :record_date, 'Date/Time of Usage', class: 'col-sm-3 control-label' %>
46
46
  <div class="col-sm-9">
47
- <%= datetime_field_tag :record_date,
48
- @record_date.presence || Time.now.utc.strftime('%Y-%m-%dT%H:%M'),
49
- required: true,
50
- class: 'form-control' %>
51
- <small class="form-text text-muted">Date and time treated as UTC</small>
47
+ <%
48
+ record_date_value = @record_date.to_s.strip
49
+ record_date_date_value = Time.now.utc.strftime('%Y-%m-%d')
50
+ record_date_time_value = '00:00'
51
+
52
+ begin
53
+ if record_date_value.match?(/^\d{4}-\d{2}-\d{2}$/)
54
+ record_date_date_value = record_date_value
55
+ elsif record_date_value.match?(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)
56
+ record_date_date_value = record_date_value[0, 10]
57
+ record_date_time_value = record_date_value[11, 5]
58
+ elsif !record_date_value.empty?
59
+ parsed_record_date = Time.iso8601(record_date_value).utc
60
+ record_date_date_value = parsed_record_date.strftime('%Y-%m-%d')
61
+ record_date_time_value = parsed_record_date.strftime('%H:%M')
62
+ end
63
+ rescue ArgumentError
64
+ record_date_date_value = Time.now.utc.strftime('%Y-%m-%d')
65
+ record_date_time_value = '00:00'
66
+ end
67
+ %>
68
+
69
+ <div class="d-flex gap-2 align-items-center">
70
+ <%= date_field_tag :record_date_date,
71
+ record_date_date_value,
72
+ required: true,
73
+ class: 'form-control' %>
74
+ <%= time_field_tag :record_date_time,
75
+ record_date_time_value,
76
+ step: 60,
77
+ class: 'form-control' %>
78
+ </div>
79
+ <%= hidden_field_tag :record_date, "#{record_date_date_value}T#{record_date_time_value}" %>
80
+ <small class="form-text text-muted">Pick a date. Time is optional and defaults to 00:00 (UTC) if unchanged.</small>
52
81
  </div>
53
82
  </div>
54
83
 
@@ -86,11 +115,19 @@
86
115
  document.addEventListener('DOMContentLoaded', function () {
87
116
  var form = document.getElementById('record_usage_form');
88
117
  if (!form) { return; }
118
+ var unitTypeSelect = document.getElementById('unit_type');
119
+ if (unitTypeSelect && unitTypeSelect.tagName === 'SELECT') {
120
+ unitTypeSelect.title = unitTypeSelect.value || '';
121
+ unitTypeSelect.addEventListener('change', function () {
122
+ unitTypeSelect.title = unitTypeSelect.value || '';
123
+ });
124
+ }
125
+
89
126
  form.addEventListener('submit', function (e) {
90
127
  var errors = [];
91
128
 
92
- var unitTypeEl = document.getElementById('unit_type');
93
- var unitType = unitTypeEl ? (unitTypeEl.value || '').trim() : '';
129
+ var unitTypeEl = document.getElementById('unit_type');
130
+ var unitType = unitTypeEl ? (unitTypeEl.value || '').trim() : '';
94
131
  if (unitType.length === 0) {
95
132
  errors.push('Unit type is required.');
96
133
  }
@@ -101,6 +138,31 @@
101
138
  errors.push('Amount must be a positive integer.');
102
139
  }
103
140
 
141
+ var dateEl = document.getElementById('record_date_date');
142
+ var timeEl = document.getElementById('record_date_time');
143
+ var hiddenDateEl = document.getElementById('record_date');
144
+
145
+ var datePart = dateEl ? (dateEl.value || '').trim() : '';
146
+ var timePart = timeEl ? (timeEl.value || '').trim() : '';
147
+ if (timePart.length === 0) {
148
+ timePart = '00:00';
149
+ }
150
+
151
+ var dateRegex = /^\d{4}-\d{2}-\d{2}$/;
152
+ var timeRegex = /^\d{2}:\d{2}$/;
153
+ if (datePart.length === 0) {
154
+ errors.push('Date/time of usage is required.');
155
+ } else if (!dateRegex.test(datePart)) {
156
+ errors.push('Date of usage must be in YYYY-MM-DD format.');
157
+ } else if (!timeRegex.test(timePart)) {
158
+ errors.push('Time must be in HH:MM format.');
159
+ } else {
160
+ var composedDateTime = datePart + 'T' + timePart;
161
+ if (hiddenDateEl) {
162
+ hiddenDateEl.value = composedDateTime;
163
+ }
164
+ }
165
+
104
166
  if (errors.length > 0) {
105
167
  e.preventDefault();
106
168
  alert(errors.join('\n'));
@@ -98,6 +98,7 @@ en:
98
98
  unable_to_connect_database: "Unable to connect to the database."
99
99
  unable_to_connect_killbill: "Unable to connect to the Kill Bill server."
100
100
  error_communicating_killbill: "Error while communicating with the Kill Bill server."
101
+ locale_required: "Please specify a Locale (e.g. en_US) for the translation file."
101
102
  views:
102
103
  subscriptions:
103
104
  requested_date_for_billing_notice: <ul>
data/config/routes.rb CHANGED
@@ -202,6 +202,7 @@ Kaui::Engine.routes.draw do
202
202
  post '/modify_overdue_config' => 'admin_tenants#modify_overdue_config', :as => 'admin_tenant_modify_overdue_config'
203
203
  post '/upload_overdue_config' => 'admin_tenants#upload_overdue_config', :as => 'admin_tenant_upload_overdue_config'
204
204
  post '/upload_invoice_template' => 'admin_tenants#upload_invoice_template', :as => 'admin_tenant_upload_invoice_template'
205
+ get '/:id/invoice_template' => 'admin_tenants#invoice_template', :as => 'admin_tenant_invoice_template'
205
206
  post '/upload_invoice_translation' => 'admin_tenants#upload_invoice_translation', :as => 'admin_tenant_upload_invoice_translation'
206
207
  post '/upload_catalog_translation' => 'admin_tenants#upload_catalog_translation', :as => 'admin_tenant_upload_catalog_translation'
207
208
  post '/upload_plugin_config' => 'admin_tenants#upload_plugin_config', :as => 'admin_tenant_upload_plugin_config'
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.19'
4
+ VERSION = '4.0.20'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kaui
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.19
4
+ version: 4.0.20
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-01 00:00:00.000000000 Z
11
+ date: 2026-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack