kaui 4.0.17 → 4.0.18

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: 98e5dfbab802d1505c3e5c5e85dcde9ee8c98724f35ad5a007acd2849f75eb6e
4
- data.tar.gz: 5cdcd79cdc9a95951c09bed2b2cbda90f3fe750e59d2adb93e1b83196f0a91a9
3
+ metadata.gz: b226cddd4e3508c415761943d804cab4ff71e506eca8d8ffbe592fde0cbed193
4
+ data.tar.gz: 6b29cc16b8e32acc0fceca6f1444e3073e8bbe8cd29fbd59158c7edb05cf4796
5
5
  SHA512:
6
- metadata.gz: fde50f9e9a89e44b0370a9c2b31713735fc3603accab084bede5bf03ca8586da3f12809be9cdf6a098d272ae02e5f7ee5a531d5cd056b5ed1820944f1fd72071
7
- data.tar.gz: 4539ef5e1f52389fb34d1905e31fd58d3c4d9941dc491b0d79fe31db83c1b431fdb5268b6bbe47a652c1f15edb69d0d6e4012898a06f650dc5af516b1470c530
6
+ metadata.gz: b562deca64d70f1fe3323cedd752ed20fe00202ea56e248873aeb11672a3128c45b809ff7138395dc04d301b2b35fc07b8191e0d6e653423eb18176131e3b7d2
7
+ data.tar.gz: 276b11b5364329227e3be9d0b121500350ced30662505dccea557c7084eaccb91508401b73c7bf1ae80e92cec06d630ee0fab7f9bbfc1c17cbead743d2b2efa1
@@ -392,69 +392,12 @@ function isBlank(value) {
392
392
  // data-id = content of the popover,object id; required
393
393
  // title = title of the popover; not required
394
394
  // id = (must be {{id}}-popover) used to close popover when the copy image is clicked; if present; if not present a timeout of 5s will apply; not required
395
+ //
396
+ // Deprecated: the underlying Bootstrap 4 popover('destroy') API was removed in
397
+ // Bootstrap 5 and this implementation has been superseded by setObjectIdTooltip().
398
+ // Kept as a thin shim so existing call sites continue to work.
395
399
  function setObjectIdPopover(){
396
- $(".object-id-popover").each(function(idx, e){
397
- $(this).popover('destroy');
398
- $(this).off("shown.bs.popover");
399
- $(this).data("index", idx);
400
-
401
- $(this).popover({
402
- html: true,
403
- content: function() {
404
- var template = '<div class="{{id}}-content" >' +
405
- '{{id}}&emsp;<i id="{{id}}-copy" class="fa fa-clipboard copy-icon" aria-hidden="true"></i> ' +
406
- '</div>';
407
-
408
- var popover_html = Mustache.render( template , { id: $(this).data("id") });
409
- return popover_html;
410
- },
411
- container: 'body',
412
- trigger: 'hover',
413
- delay: { "show": 100, "hide": 4000 }
414
- });
415
-
416
- $(this).on("show.bs.popover", function(e) {
417
- var currentPopoverIndex = $(this).data('index');
418
- $(".object-id-popover").each(function(idx, e){
419
- var index = $(this).data('index');
420
-
421
- if (currentPopoverIndex != index) {
422
- $(this).popover('hide');
423
- }
424
- });
425
- });
426
-
427
- $(this).on("shown.bs.popover", function(e) {
428
- var objectId = $(this).data('id');
429
- var copyIdImg = $("#" + objectId + "-copy");
430
-
431
- copyIdImg.data("popover",$(this).attr("id"));
432
- copyIdImg.click(function(e){
433
- var id = ($(this).attr("id")).replace('-copy','');
434
- navigator.clipboard.writeText(id);
435
- ajaxInfoAlert("Id [" + id + "] was copied into the clipboard!", 4000);
436
-
437
- if (!isBlank(popover)) {
438
- popover.popover('hide');
439
- }
440
-
441
- });
442
-
443
- });
444
-
445
- });
446
-
447
- // close all object id popover on modal show
448
- $(".modal").on('show.bs.modal',function(e){
449
- $(".object-id-popover").each(function(idx, e) {
450
- $(this).popover('destroy');
451
- });
452
- });
453
-
454
- // check if object id must be restored
455
- $(".modal").on('hide.bs.modal',function(e){
456
- setObjectIdPopover();
457
- });
400
+ setObjectIdTooltip();
458
401
  }
459
402
 
460
403
  // Custom tooltip function for object IDs
@@ -158,11 +158,114 @@ module Kaui
158
158
  redirect_to kaui_engine.account_bundles_path(input_subscription['account_id']), notice: 'Subscription BCD was successfully changed'
159
159
  end
160
160
 
161
+ def edit_quantity
162
+ @subscription = Kaui::Subscription.find_by_id(params.require(:id), 'NONE', options_for_klient)
163
+ end
164
+
165
+ def update_quantity
166
+ id = params.require(:id)
167
+ input_subscription = params.require(:subscription)
168
+
169
+ quantity_raw = input_subscription['quantity'].to_s.strip
170
+ quantity = Integer(quantity_raw, exception: false)
171
+ if quantity.nil? || quantity <= 0
172
+ flash.now[:error] = 'Quantity must be a positive integer'
173
+ @subscription = Kaui::Subscription.find_by_id(id, 'NONE', options_for_klient)
174
+ @subscription.quantity = quantity_raw
175
+ render :edit_quantity and return
176
+ end
177
+
178
+ subscription = Kaui::Subscription.new
179
+ subscription.subscription_id = id
180
+ subscription.quantity = quantity
181
+
182
+ effective_from_date = params['effective_from_date']
183
+
184
+ subscription.update_quantity(current_user.kb_username, params[:reason], params[:comment], effective_from_date, nil, options_for_klient)
185
+ redirect_to kaui_engine.account_bundles_path(input_subscription['account_id']), notice: 'Subscription quantity was successfully changed'
186
+ rescue ActionController::ParameterMissing
187
+ redirect_to kaui_engine.edit_quantity_path(params[:id]), flash: { error: 'Required parameter missing: subscription' }
188
+ end
189
+
190
+ def record_usage
191
+ @subscription = Kaui::Subscription.find_by_id(params.require(:id), 'NONE', options_for_klient)
192
+ end
193
+
194
+ def create_usage
195
+ subscription_id = params.require(:id)
196
+ unit_type = params[:unit_type].to_s.strip
197
+ amount_raw = params[:amount].to_s.strip
198
+ record_date = params[:record_date].to_s.strip
199
+
200
+ # Input validation
201
+ errors = []
202
+ errors << 'Unit type is required' if unit_type.blank?
203
+ errors << 'Amount is required' if amount_raw.blank?
204
+ amount = Integer(amount_raw, exception: false)
205
+ errors << 'Amount must be a positive integer' if amount.nil? || amount <= 0
206
+ errors << 'Date/time of usage is required' if record_date.blank?
207
+ parsed_date = begin
208
+ record_date.blank? ? nil : Time.iso8601(record_date)
209
+ rescue ArgumentError
210
+ nil
211
+ end
212
+ errors << 'Date/time of usage must be a valid ISO 8601 timestamp' if record_date.present? && parsed_date.nil?
213
+
214
+ if errors.any?
215
+ flash.now[:error] = errors.join('. ')
216
+ @subscription = Kaui::Subscription.find_by_id(subscription_id, 'NONE', options_for_klient)
217
+ @unit_type = unit_type
218
+ @amount = amount_raw
219
+ @record_date = record_date
220
+ @tracking_id = params[:tracking_id]
221
+ render :record_usage and return
222
+ end
223
+
224
+ begin
225
+ usage_record = KillBillClient::Model::UsageRecordAttributes.new
226
+ usage_record.record_date = parsed_date.utc.iso8601
227
+ usage_record.amount = amount
228
+
229
+ unit_usage_record = KillBillClient::Model::UnitUsageRecordAttributes.new
230
+ unit_usage_record.unit_type = unit_type
231
+ unit_usage_record.usage_records = [usage_record]
232
+
233
+ usage = Kaui::Usage.new
234
+ usage.subscription_id = subscription_id
235
+ usage.tracking_id = params[:tracking_id].presence
236
+ usage.unit_usage_records = [unit_usage_record]
237
+
238
+ usage.create(current_user.kb_username, params[:reason], params[:comment], options_for_klient)
239
+
240
+ subscription = Kaui::Subscription.find_by_id(subscription_id, 'NONE', options_for_klient)
241
+ redirect_to kaui_engine.account_bundles_path(subscription.account_id), notice: 'Usage was successfully recorded'
242
+ rescue StandardError => e
243
+ Rails.logger.error("Failed to record usage for subscription #{subscription_id}: #{e.class}: #{e.message}")
244
+ Rails.logger.error(e.backtrace.join("\n")) if e.backtrace
245
+ flash.now[:error] = "Error while recording usage: #{as_string(e)}"
246
+ @subscription = Kaui::Subscription.find_by_id(subscription_id, 'NONE', options_for_klient)
247
+ @unit_type = unit_type
248
+ @amount = amount_raw
249
+ @record_date = record_date
250
+ @tracking_id = params[:tracking_id]
251
+ render :record_usage
252
+ end
253
+ end
254
+
161
255
  def restful_show
162
256
  subscription = Kaui::Subscription.find_by_id(params.require(:id), 'NONE', options_for_klient)
163
257
  redirect_to kaui_engine.account_bundles_path(subscription.account_id)
164
258
  end
165
259
 
260
+ def show_json
261
+ raw_body = Kaui::Subscription.find_raw_by_id(params.require(:id), 'NONE', options_for_klient)
262
+ render body: raw_body, content_type: 'application/json'
263
+ rescue KillBillClient::API::ResponseError => e
264
+ render body: e.response.body, content_type: 'application/json', status: e.code
265
+ rescue StandardError => e
266
+ render json: { error: e.message }, status: :internal_server_error
267
+ end
268
+
166
269
  def validate_bundle_external_key
167
270
  json_response do
168
271
  external_key = params.require(:external_key)
@@ -189,6 +189,34 @@ module Kaui
189
189
  sub.present? and sub.billing_end_date.present?
190
190
  end
191
191
 
192
+ def subscription_has_usage?(sub, catalog = nil)
193
+ return false if sub.blank?
194
+
195
+ # First try the subscription's resolved per-phase prices
196
+ # (populated by GET /subscriptions/{id} but not by the bundles listing endpoint).
197
+ (sub.prices || []).each do |phase_price|
198
+ usage_prices = if phase_price.is_a?(Hash)
199
+ phase_price['usagePrices'] || phase_price[:usagePrices] ||
200
+ phase_price['usage_prices'] || phase_price[:usage_prices]
201
+ elsif phase_price.respond_to?(:usage_prices)
202
+ phase_price.usage_prices
203
+ end
204
+ return true if usage_prices.present?
205
+ end
206
+
207
+ # Fall back to the catalog plan definition
208
+ return false if catalog.blank? || sub.plan_name.blank?
209
+
210
+ product = catalog.products&.find { |p| p.name == sub.product_name }
211
+ plan = product&.plans&.find { |p| p.name == sub.plan_name }
212
+ return false if plan.nil?
213
+
214
+ (plan.phases || []).any? do |phase|
215
+ usages = phase.respond_to?(:usages) ? phase.usages : nil
216
+ usages.present?
217
+ end
218
+ end
219
+
192
220
  def paging_button_class(num, current_page)
193
221
  num == current_page ? 'btn btn-primary' : 'btn btn-custom'
194
222
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kaui
4
+ class Usage < KillBillClient::Model::UsageRecord
5
+ end
6
+ end
@@ -20,7 +20,7 @@
20
20
  style="min-width: 6.25rem; display: none;">
21
21
  <% menu_items&.each do |item| %>
22
22
  <li>
23
- <%= link_to item[:path], class: "dropdown-item d-flex align-items-center gap-2" do %>
23
+ <%= link_to item[:path], class: "dropdown-item d-flex align-items-center gap-2", data: (item[:data] || {}), method: item[:method] do %>
24
24
  <%= image_tag(item[:icon], alt: "#{item[:label]} Icon", size: "16x16") if item[:icon].present? %>
25
25
  <span class="text-black-700 fs-6 text-normal" style="font-weight: 500; font-size: 0.875rem !important; line-height: 1.25rem;"><%= item[:label] %></span>
26
26
  <% end %>
@@ -111,8 +111,26 @@
111
111
  </span>
112
112
  </td>
113
113
  <td><span class="phase-type"><%= humanized_subscription_phase_type(sub).downcase %></span></td>
114
- <td><%= humanized_subscription_start_date(sub, account) %></td>
115
- <td><%= humanized_subscription_cancelled_information(sub, account) %></td>
114
+ <td>
115
+ <% if sub.start_date.present? %>
116
+ <span class="object-id-popover" data-id="<%= sub.start_date %>" data-title="Start Date (ISO)">
117
+ <%= humanized_subscription_start_date(sub, account) %>
118
+ </span>
119
+ <% end %>
120
+ </td>
121
+ <td>
122
+ <% if sub.cancelled_date.present? %>
123
+ <span class="object-id-popover" data-id="<%= sub.cancelled_date %>" data-title="Entitlement Date (ISO)">
124
+ <%= humanized_subscription_cancelled_date(sub, account) %>
125
+ </span>
126
+ <% if sub.billing_end_date.present? %>
127
+ <br>
128
+ <span class="object-id-popover" data-id="<%= sub.billing_end_date %>" data-title="Billing End Date (ISO)">
129
+ <%= humanized_subscription_billing_end_date(sub, account) %>
130
+ </span>
131
+ <% end %>
132
+ <% end %>
133
+ </td>
116
134
  <td><%= humanized_subscription_charged_through_date(sub, account) %></td>
117
135
  <td><%= sub.quantity || 1 %></td>
118
136
  <td class="text-center">
@@ -141,9 +159,9 @@
141
159
  </div>
142
160
  <% end %>
143
161
 
144
- <% if can?(:create, Kaui::Subscription) || can?(:pause_resume, Kaui::Subscription) || can?(:change_plan, Kaui::Subscription) || can?(:transfer, Kaui::Bundle) %>
145
- <% menu_items = [] %>
162
+ <% menu_items = [] %>
146
163
 
164
+ <% if can?(:create, Kaui::Subscription) || can?(:pause_resume, Kaui::Subscription) || can?(:change_plan, Kaui::Subscription) || can?(:transfer, Kaui::Bundle) || (can?(:record, Kaui::Usage) && subscription_has_usage?(sub, catalog_for_subscription(sub, catalogs))) %>
147
165
  <% if can?(:change_plan, Kaui::Subscription) %>
148
166
  <% menu_items << {
149
167
  label: "Change Plan",
@@ -183,6 +201,22 @@
183
201
  } %>
184
202
  <% end %>
185
203
 
204
+ <% if can?(:create, Kaui::Subscription) %>
205
+ <% menu_items << {
206
+ label: "Update Quantity",
207
+ path: kaui_engine.edit_quantity_path(id: sub.subscription_id),
208
+ icon: "kaui/subscription/change.svg"
209
+ } %>
210
+ <% end %>
211
+
212
+ <% if can?(:record, Kaui::Usage) && subscription_has_usage?(sub, catalog_for_subscription(sub, catalogs)) %>
213
+ <% menu_items << {
214
+ label: "Record Usage",
215
+ path: kaui_engine.record_usage_path(id: sub.subscription_id),
216
+ icon: "kaui/subscription/change.svg"
217
+ } %>
218
+ <% end %>
219
+
186
220
  <% if can?(:transfer, Kaui::Bundle) && sub.product_category != 'ADD_ON' %>
187
221
  <% menu_items << {
188
222
  label: I18n.translate('transfer'),
@@ -190,7 +224,18 @@
190
224
  icon: "kaui/subscription/transfer.svg"
191
225
  } %>
192
226
  <% end %>
227
+ <% end %>
228
+
229
+ <% if can?(:read, Kaui::Subscription) %>
230
+ <% menu_items << {
231
+ label: "View JSON",
232
+ path: "#view_subscription_json_modal",
233
+ icon: "kaui/view-doc.svg",
234
+ data: { bs_toggle: 'modal', id: sub.subscription_id }
235
+ } %>
236
+ <% end %>
193
237
 
238
+ <% if menu_items.any? %>
194
239
  <%= render partial: "kaui/components/menu_dropdown/menu_dropdown", locals: {
195
240
  variant: "btn-light",
196
241
  label: "",
@@ -210,6 +255,7 @@
210
255
  </table>
211
256
 
212
257
  <%= render :partial => 'kaui/subscriptions/cancel_by_date_modal' %>
258
+ <%= render :partial => 'kaui/subscriptions/view_json_modal' %>
213
259
 
214
260
  <%= javascript_tag do %>
215
261
  $(document).ready(function() {
@@ -0,0 +1,100 @@
1
+ <div class="modal fade view_subscription_json_modal" id="view_subscription_json_modal" tabindex="-1" role="dialog" aria-labelledby="viewSubscriptionJsonLabel" aria-hidden="true">
2
+ <div class="modal-dialog modal-lg modal-dialog-scrollable" role="document">
3
+ <div class="modal-content">
4
+ <div class="modal-header">
5
+ <h5 class="modal-title d-flex align-items-center gap-3" id="viewSubscriptionJsonLabel">
6
+ <span class="icon-container">
7
+ <%= image_tag("kaui/view-doc.svg", width: 20, height: 20) %>
8
+ </span>
9
+ Subscription JSON
10
+ </h5>
11
+ <button type="button" class="close close-button custom-hover" data-bs-dismiss="modal" aria-label="Close">
12
+ <span aria-hidden="true">
13
+ <%= image_tag("kaui/modal/close.svg", width: 20, height: 20) %>
14
+ </span>
15
+ </button>
16
+ </div>
17
+ <div class="modal-body">
18
+ <div id="view_subscription_json_loading">Loading...</div>
19
+ <div id="view_subscription_json_error" class="text-danger" style="display: none;"></div>
20
+ <pre id="view_subscription_json_content" style="display: none; max-height: 60vh; overflow: auto; background-color: #f8f9fa; padding: 1rem; border-radius: 0.375rem; font-size: 0.8125rem; line-height: 1.4;"></pre>
21
+ </div>
22
+ <div class="modal-footer">
23
+ <%= render "kaui/components/button/button", {
24
+ label: 'Close',
25
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
26
+ type: "button",
27
+ html_class: "kaui-button custom-hover",
28
+ html_options: {
29
+ data: {
30
+ bs_dismiss: "modal"
31
+ }
32
+ }
33
+ } %>
34
+ <%= render "kaui/components/button/button", {
35
+ label: 'Copy JSON',
36
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
37
+ type: "button",
38
+ html_class: "kaui-dropdown custom-hover",
39
+ html_options: {
40
+ id: "copy_subscription_json_btn"
41
+ }
42
+ } %>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+
48
+ <%= javascript_tag do %>
49
+ $(document).ready(function() {
50
+ var $modal = $('#view_subscription_json_modal');
51
+ if (!$modal.length) return;
52
+
53
+ $modal.on('show.bs.modal', function (e) {
54
+ var subId = e.relatedTarget && e.relatedTarget.dataset ? e.relatedTarget.dataset['id'] : null;
55
+ var $loading = $('#view_subscription_json_loading');
56
+ var $error = $('#view_subscription_json_error');
57
+ var $content = $('#view_subscription_json_content');
58
+
59
+ $loading.show();
60
+ $error.hide().text('');
61
+ $content.hide().text('');
62
+
63
+ if (!subId) {
64
+ $loading.hide();
65
+ $error.text('Missing subscription id').show();
66
+ return;
67
+ }
68
+
69
+ $.ajax({
70
+ url: Routes.kaui_engine_show_subscription_json_path(subId),
71
+ type: 'GET',
72
+ dataType: 'json'
73
+ })
74
+ .done(function (data) {
75
+ $content.text(JSON.stringify(data, null, 2)).show();
76
+ $loading.hide();
77
+ })
78
+ .fail(function (xhr) {
79
+ $loading.hide();
80
+ var message = 'Failed to load subscription JSON';
81
+ if (xhr && xhr.responseText) {
82
+ message += ': ' + xhr.responseText;
83
+ }
84
+ $error.text(message).show();
85
+ });
86
+ });
87
+
88
+ $('#copy_subscription_json_btn').on('click', function () {
89
+ var text = $('#view_subscription_json_content').text();
90
+ if (!text) return;
91
+ if (navigator.clipboard && navigator.clipboard.writeText) {
92
+ navigator.clipboard.writeText(text).then(function () {
93
+ if (typeof ajaxInfoAlert === 'function') {
94
+ ajaxInfoAlert('JSON copied to clipboard!', 3000);
95
+ }
96
+ });
97
+ }
98
+ });
99
+ });
100
+ <% end %>
@@ -0,0 +1,59 @@
1
+ <div class="register kaui-subscription-new">
2
+ <div class="">
3
+ <div class="mx-auto" style="max-width: 37.5rem;">
4
+ <h5 class="add-account-title border-bottom pb-3 mb-3">
5
+ <span class="icon-container">
6
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
7
+ <path d="M9.16675 11.6665H13.3334M6.66675 11.6665H6.67423M10.8334 14.9998H6.66675M13.3334 14.9998H13.3259" stroke="#414651" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
8
+ <path d="M13.75 1.6665V4.99984M6.25 1.6665V4.99984" stroke="#414651" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
9
+ <path d="M15.8333 3.3335H4.16667C3.24619 3.3335 2.5 4.07969 2.5 5.00016V16.6668C2.5 17.5873 3.24619 18.3335 4.16667 18.3335H15.8333C16.7538 18.3335 17.5 17.5873 17.5 16.6668V5.00016C17.5 4.07969 16.7538 3.3335 15.8333 3.3335Z" stroke="#414651" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
10
+ <path d="M2.5 8.3335H17.5" stroke="#414651" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
11
+ </svg>
12
+ </span>
13
+ Update Subscription Quantity
14
+ </h5>
15
+ <%= form_for @subscription, :as => :subscription, :url => update_quantity_path(@subscription.subscription_id), :html => {:method => :put, :class => 'form-horizontal'} do |f| %>
16
+ <%= f.hidden_field :account_id %>
17
+
18
+ <div class="form-group d-flex pb-3">
19
+ <%= f.label :quantity, 'Quantity', :class => 'col-sm-3 control-label' %>
20
+ <div class="col-sm-9">
21
+ <%= f.number_field :quantity, { :min => 1, :required => true, :class => 'form-control'} %>
22
+ </div>
23
+ </div>
24
+ <div class="form-group d-flex pb-3 border-bottom mb-3" id="form_effective_from_date">
25
+ <%= label_tag :effective_from_date, 'Effective Date', :class => 'col-sm-3 control-label' %>
26
+ <div class="col-sm-9">
27
+ <div class="custom-date-input-wrapper kaui-button custom-hover">
28
+ <svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
29
+ <path d="M14.9167 12.5V18.3333M17.8333 15.4167H12" stroke="#A4A7AE" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
30
+ <path d="M14.0833 1.66675V5.00008M6.58325 1.66675V5.00008" stroke="#A4A7AE" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
31
+ <path d="M17.8333 10.8333V4.99992C17.8333 4.07944 17.0871 3.33325 16.1666 3.33325H4.49992C3.57944 3.33325 2.83325 4.07944 2.83325 4.99992V16.6666C2.83325 17.5871 3.57944 18.3333 4.49992 18.3333H10.3333" stroke="#A4A7AE" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
32
+ <path d="M2.83325 8.33325H17.8333" stroke="#A4A7AE" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
33
+ </svg>
34
+ <input class="form-control" value="<%= Time.zone.today.strftime('%Y-%m-%d') %>" id="effective_from_date" name="effective_from_date" type="text" data-provide="datepicker" data-date-format="yyyy-mm-dd" data-date-today-highlight="true">
35
+ </div>
36
+ </div>
37
+ </div>
38
+ <div class="form-group d-flex justify-content-end pb-3">
39
+ <%= render "kaui/components/button/button", {
40
+ label: 'Close',
41
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
42
+ type: "button",
43
+ html_class: "kaui-button custom-hover mx-2",
44
+ html_options: {
45
+ id: "closeButton",
46
+ onclick: "window.history.back();"
47
+ }
48
+ } %>
49
+ <%= render "kaui/components/button/button", {
50
+ label: 'Save Subscription',
51
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
52
+ type: "submit",
53
+ html_class: "kaui-dropdown custom-hover"
54
+ } %>
55
+ </div>
56
+ <% end %>
57
+ </div>
58
+ </div>
59
+ </div>
@@ -0,0 +1,126 @@
1
+ <div class="register kaui-subscription-new">
2
+ <div class="">
3
+ <div class="mx-auto" style="max-width: 37.5rem;">
4
+ <h5 class="add-account-title border-bottom pb-3 mb-3">
5
+ <span class="icon-container">
6
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
7
+ <path d="M9.16675 11.6665H13.3334M6.66675 11.6665H6.67423M10.8334 14.9998H6.66675M13.3334 14.9998H13.3259" stroke="#414651" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
8
+ <path d="M13.75 1.6665V4.99984M6.25 1.6665V4.99984" stroke="#414651" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
9
+ <path d="M15.8333 3.3335H4.16667C3.24619 3.3335 2.5 4.07969 2.5 5.00016V16.6668C2.5 17.5873 3.24619 18.3335 4.16667 18.3335H15.8333C16.7538 18.3335 17.5 17.5873 17.5 16.6668V5.00016C17.5 4.07969 16.7538 3.3335 15.8333 3.3335Z" stroke="#414651" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
10
+ <path d="M2.5 8.3335H17.5" stroke="#414651" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
11
+ </svg>
12
+ </span>
13
+ Record Usage
14
+ </h5>
15
+
16
+ <p class="text-muted small mb-3">
17
+ Record usage for subscription
18
+ <strong><%= @subscription.plan_name %></strong>
19
+ (<code><%= @subscription.subscription_id %></code>).
20
+ </p>
21
+
22
+ <%= form_tag create_usage_path(@subscription.subscription_id), method: :post, id: 'record_usage_form', class: 'form-horizontal' do %>
23
+
24
+ <div class="form-group d-flex pb-3">
25
+ <%= label_tag :unit_type, 'Unit Type', class: 'col-sm-3 control-label' %>
26
+ <div class="col-sm-9">
27
+ <%= text_field_tag :unit_type, @unit_type, required: true, maxlength: 255, class: 'form-control', placeholder: 'e.g. api-calls' %>
28
+ </div>
29
+ </div>
30
+
31
+ <div class="form-group d-flex pb-3">
32
+ <%= label_tag :amount, 'Amount', class: 'col-sm-3 control-label' %>
33
+ <div class="col-sm-9">
34
+ <%= number_field_tag :amount, @amount, required: true, min: 1, step: 1, class: 'form-control', placeholder: 'Positive integer' %>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="form-group d-flex pb-3">
39
+ <%= label_tag :record_date, 'Date/Time of Usage', class: 'col-sm-3 control-label' %>
40
+ <div class="col-sm-9">
41
+ <%= text_field_tag :record_date,
42
+ @record_date.presence || Time.now.utc.iso8601,
43
+ required: true,
44
+ class: 'form-control',
45
+ placeholder: 'YYYY-MM-DDTHH:MM:SSZ (ISO 8601)' %>
46
+ <small class="form-text text-muted">ISO 8601 format, e.g. <code>2026-06-13T15:30:00Z</code></small>
47
+ </div>
48
+ </div>
49
+
50
+ <div class="form-group d-flex pb-3">
51
+ <%= label_tag :tracking_id, 'Tracking ID', class: 'col-sm-3 control-label' %>
52
+ <div class="col-sm-9">
53
+ <%= text_field_tag :tracking_id, @tracking_id, maxlength: 255, class: 'form-control', placeholder: 'Optional, used for idempotency' %>
54
+ </div>
55
+ </div>
56
+
57
+ <div class="form-group d-flex pb-3 border-bottom mb-3">
58
+ <%= label_tag :reason, 'Reason', class: 'col-sm-3 control-label' %>
59
+ <div class="col-sm-9">
60
+ <%= text_field_tag :reason, params[:reason], class: 'form-control' %>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="form-group d-flex pb-3 border-bottom mb-3">
65
+ <%= label_tag :comment, 'Comment', class: 'col-sm-3 control-label' %>
66
+ <div class="col-sm-9">
67
+ <%= text_field_tag :comment, params[:comment], class: 'form-control' %>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="form-group d-flex justify-content-end pb-3">
72
+ <%= render "kaui/components/button/button", {
73
+ label: 'Close',
74
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
75
+ type: "button",
76
+ html_class: "kaui-button custom-hover mx-2",
77
+ html_options: {
78
+ id: "closeButton",
79
+ onclick: "window.history.back();"
80
+ }
81
+ } %>
82
+ <%= render "kaui/components/button/button", {
83
+ label: 'Record Usage',
84
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
85
+ type: "submit",
86
+ html_class: "kaui-dropdown custom-hover"
87
+ } %>
88
+ </div>
89
+ <% end %>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ <%= javascript_tag do %>
95
+ document.addEventListener('DOMContentLoaded', function () {
96
+ var form = document.getElementById('record_usage_form');
97
+ if (!form) { return; }
98
+ form.addEventListener('submit', function (e) {
99
+ var errors = [];
100
+
101
+ var unitType = (document.getElementById('unit_type').value || '').trim();
102
+ if (unitType.length === 0) {
103
+ errors.push('Unit type is required.');
104
+ }
105
+
106
+ var amountStr = (document.getElementById('amount').value || '').trim();
107
+ var amount = parseInt(amountStr, 10);
108
+ if (amountStr.length === 0 || isNaN(amount) || amount <= 0 || String(amount) !== amountStr) {
109
+ errors.push('Amount must be a positive integer.');
110
+ }
111
+
112
+ var dateStr = (document.getElementById('record_date').value || '').trim();
113
+ // Permissive ISO 8601 check
114
+ var isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/;
115
+ var parsed = Date.parse(dateStr);
116
+ if (dateStr.length === 0 || isNaN(parsed) || !isoRegex.test(dateStr)) {
117
+ errors.push('Date/time of usage must be a valid ISO 8601 timestamp (e.g. 2026-06-13T15:30:00Z).');
118
+ }
119
+
120
+ if (errors.length > 0) {
121
+ e.preventDefault();
122
+ alert(errors.join('\n'));
123
+ }
124
+ });
125
+ });
126
+ <% end %>
data/config/routes.rb CHANGED
@@ -141,7 +141,12 @@ Kaui::Engine.routes.draw do
141
141
  post '/:id/tags' => 'subscriptions#update_tags', :as => 'update_subscriptions_tags'
142
142
  get '/:id/edit_bcd' => 'subscriptions#edit_bcd', :as => 'edit_bcd'
143
143
  put '/:id/update_bcd' => 'subscriptions#update_bcd', :as => 'update_bcd'
144
+ get '/:id/edit_quantity' => 'subscriptions#edit_quantity', :as => 'edit_quantity'
145
+ put '/:id/update_quantity' => 'subscriptions#update_quantity', :as => 'update_quantity'
146
+ get '/:id/record_usage' => 'subscriptions#record_usage', :as => 'record_usage'
147
+ post '/:id/record_usage' => 'subscriptions#create_usage', :as => 'create_usage'
144
148
  put '/:id/reinstate' => 'subscriptions#reinstate', :as => 'reinstate'
149
+ get '/:id/show_json' => 'subscriptions#show_json', :as => 'show_subscription_json'
145
150
  get '/validate_external_key' => 'subscriptions#validate_external_key', :as => 'subscriptions_validate_external_key'
146
151
  get '/validate_bundle_external_key' => 'subscriptions#validate_bundle_external_key', :as => 'subscriptions_validate_bundle_external_key'
147
152
  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.17'
4
+ VERSION = '4.0.18'
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.17
4
+ version: 4.0.18
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-06-03 00:00:00.000000000 Z
11
+ date: 2026-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -507,6 +507,7 @@ files:
507
507
  - app/models/kaui/tag_definition.rb
508
508
  - app/models/kaui/tenant.rb
509
509
  - app/models/kaui/transaction.rb
510
+ - app/models/kaui/usage.rb
510
511
  - app/models/kaui/user.rb
511
512
  - app/models/kaui/user_role.rb
512
513
  - app/services/dependencies/kenui.rb
@@ -649,9 +650,12 @@ files:
649
650
  - app/views/kaui/subscriptions/_edit_form.html.erb
650
651
  - app/views/kaui/subscriptions/_form.html.erb
651
652
  - app/views/kaui/subscriptions/_subscriptions_table.html.erb
653
+ - app/views/kaui/subscriptions/_view_json_modal.html.erb
652
654
  - app/views/kaui/subscriptions/edit.html.erb
653
655
  - app/views/kaui/subscriptions/edit_bcd.erb
656
+ - app/views/kaui/subscriptions/edit_quantity.erb
654
657
  - app/views/kaui/subscriptions/new.html.erb
658
+ - app/views/kaui/subscriptions/record_usage.erb
655
659
  - app/views/kaui/tag_definitions/_form.html.erb
656
660
  - app/views/kaui/tag_definitions/index.html.erb
657
661
  - app/views/kaui/tag_definitions/new.html.erb