diff --git a/lib/ioki/apis/operator_api.rb b/lib/ioki/apis/operator_api.rb index d748a9fc..e55bf411 100644 --- a/lib/ioki/apis/operator_api.rb +++ b/lib/ioki/apis/operator_api.rb @@ -464,13 +464,22 @@ class OperatorApi model_class: Ioki::Model::Operator::Reporting::ReportAggregation ), Endpoints::Create.new( - :reporting_aggregation_query, + :reporting_aggregation_series, base_path: [ API_BASE_PATH, 'reporting', 'report', 'scopes', :scope, 'reports', :name, 'aggregations', :aggregation_name ], - path: 'aggregate', - model_class: Ioki::Model::Operator::Reporting::ReportAggregationResult, - outgoing_model_class: Ioki::Model::Operator::Reporting::ReportAggregationQuery + path: 'series', + model_class: Ioki::Model::Operator::Reporting::ReportAggregationSeries, + outgoing_model_class: Ioki::Model::Operator::Reporting::ReportAggregationSeriesQuery + ), + Endpoints::Create.new( + :reporting_aggregation_totals, + base_path: [ + API_BASE_PATH, 'reporting', 'report', 'scopes', :scope, 'reports', :name, 'aggregations', :aggregation_name + ], + path: 'totals', + model_class: Ioki::Model::Operator::Reporting::ReportAggregationTotals, + outgoing_model_class: Ioki::Model::Operator::Reporting::ReportAggregationTotalsQuery ), Endpoints.crud_endpoints( :cancellation_statement, diff --git a/lib/ioki/model/operator/reporting/report_aggregation_measure.rb b/lib/ioki/model/operator/reporting/report_aggregation_measure.rb index 14a2ac2a..636e1d65 100644 --- a/lib/ioki/model/operator/reporting/report_aggregation_measure.rb +++ b/lib/ioki/model/operator/reporting/report_aggregation_measure.rb @@ -17,6 +17,10 @@ class ReportAggregationMeasure < Base on: :read, type: :string + attribute :percentile, + on: :read, + type: :float + attribute :localized_function, on: :read, type: :string diff --git a/lib/ioki/model/operator/reporting/report_aggregation_measure_total.rb b/lib/ioki/model/operator/reporting/report_aggregation_measure_total.rb new file mode 100644 index 00000000..c6028136 --- /dev/null +++ b/lib/ioki/model/operator/reporting/report_aggregation_measure_total.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Ioki + module Model + module Operator + module Reporting + class ReportAggregationMeasureTotal < Base + attribute :type, + on: :read, + type: :string + + attribute :key, + on: :read, + type: :string + + attribute :localized_label, + on: :read, + type: :string + + attribute :value, + on: :read, + type: :float + end + end + end + end +end diff --git a/lib/ioki/model/operator/reporting/report_aggregation_result.rb b/lib/ioki/model/operator/reporting/report_aggregation_series.rb similarity index 96% rename from lib/ioki/model/operator/reporting/report_aggregation_result.rb rename to lib/ioki/model/operator/reporting/report_aggregation_series.rb index eb9a4b11..a38f70ab 100644 --- a/lib/ioki/model/operator/reporting/report_aggregation_result.rb +++ b/lib/ioki/model/operator/reporting/report_aggregation_series.rb @@ -4,7 +4,7 @@ module Ioki module Model module Operator module Reporting - class ReportAggregationResult < Base + class ReportAggregationSeries < Base attribute :type, on: :read, type: :string diff --git a/lib/ioki/model/operator/reporting/report_aggregation_query.rb b/lib/ioki/model/operator/reporting/report_aggregation_series_query.rb similarity index 93% rename from lib/ioki/model/operator/reporting/report_aggregation_query.rb rename to lib/ioki/model/operator/reporting/report_aggregation_series_query.rb index 630ce3df..f08c154c 100644 --- a/lib/ioki/model/operator/reporting/report_aggregation_query.rb +++ b/lib/ioki/model/operator/reporting/report_aggregation_series_query.rb @@ -4,7 +4,7 @@ module Ioki module Model module Operator module Reporting - class ReportAggregationQuery < Base + class ReportAggregationSeriesQuery < Base attribute :start_time, on: :create, type: :date_time diff --git a/lib/ioki/model/operator/reporting/report_aggregation_totals.rb b/lib/ioki/model/operator/reporting/report_aggregation_totals.rb new file mode 100644 index 00000000..dc53cc2b --- /dev/null +++ b/lib/ioki/model/operator/reporting/report_aggregation_totals.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Ioki + module Model + module Operator + module Reporting + class ReportAggregationTotals < Base + attribute :type, + on: :read, + type: :string + + attribute :aggregation_name, + on: :read, + type: :string + + attribute :measures, + on: :read, + type: :array, + class_name: 'ReportAggregationMeasureTotal' + end + end + end + end +end diff --git a/lib/ioki/model/operator/reporting/report_aggregation_totals_query.rb b/lib/ioki/model/operator/reporting/report_aggregation_totals_query.rb new file mode 100644 index 00000000..ea99de44 --- /dev/null +++ b/lib/ioki/model/operator/reporting/report_aggregation_totals_query.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Ioki + module Model + module Operator + module Reporting + class ReportAggregationTotalsQuery < Base + attribute :start_time, + on: :create, + type: :date_time + + attribute :end_time, + on: :create, + type: :date_time + + attribute :filters, + on: :create, + type: :array, + class_name: 'ReportAggregationFilterParam', + omit_if_nil_on: :create + end + end + end + end +end diff --git a/spec/ioki/model/operator/reporting/report_aggregation_measure_spec.rb b/spec/ioki/model/operator/reporting/report_aggregation_measure_spec.rb index 761b07ed..660844dd 100644 --- a/spec/ioki/model/operator/reporting/report_aggregation_measure_spec.rb +++ b/spec/ioki/model/operator/reporting/report_aggregation_measure_spec.rb @@ -8,6 +8,7 @@ type: 'reporting/report_aggregation_measure', name: 'login_count', function: 'count_rows', + percentile: nil, localized_function: 'Count', localized_label: 'Logins', localized_type: 'Count', @@ -18,6 +19,7 @@ it { is_expected.to define_attribute(:type).as(:string) } it { is_expected.to define_attribute(:name).as(:string) } it { is_expected.to define_attribute(:function).as(:string) } + it { is_expected.to define_attribute(:percentile).as(:float) } it { is_expected.to define_attribute(:localized_function).as(:string) } it { is_expected.to define_attribute(:localized_label).as(:string) } it { is_expected.to define_attribute(:localized_type).as(:string) } @@ -26,9 +28,14 @@ it 'casts measure metadata' do expect(measure.name).to eq('login_count') expect(measure.function).to eq('count_rows') + expect(measure.percentile).to be_nil expect(measure.localized_function).to eq('Count') expect(measure.localized_label).to eq('Logins') expect(measure.localized_type).to eq('Count') expect(measure.value_type).to eq('number') end + + it 'casts percentile values for percentile measures' do + expect(described_class.new(attributes.merge(percentile: 0.95)).percentile).to eq(0.95) + end end diff --git a/spec/ioki/model/operator/reporting/report_aggregation_measure_total_spec.rb b/spec/ioki/model/operator/reporting/report_aggregation_measure_total_spec.rb new file mode 100644 index 00000000..9c508716 --- /dev/null +++ b/spec/ioki/model/operator/reporting/report_aggregation_measure_total_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.describe Ioki::Model::Operator::Reporting::ReportAggregationMeasureTotal do + subject(:measure_total) { described_class.new(attributes) } + + let(:attributes) do + { + type: 'reporting/report_aggregation_measure_total', + key: 'login_count', + localized_label: 'Logins', + value: 30.0 + } + end + + it { is_expected.to define_attribute(:type).as(:string) } + it { is_expected.to define_attribute(:key).as(:string) } + it { is_expected.to define_attribute(:localized_label).as(:string) } + it { is_expected.to define_attribute(:value).as(:float) } + + it 'casts total measure metadata' do + expect(measure_total.key).to eq('login_count') + expect(measure_total.localized_label).to eq('Logins') + expect(measure_total.value).to eq(30.0) + end + + it 'accepts nullable values' do + expect(described_class.new(attributes.merge(value: nil)).value).to be_nil + end +end diff --git a/spec/ioki/model/operator/reporting/report_aggregation_query_spec.rb b/spec/ioki/model/operator/reporting/report_aggregation_series_query_spec.rb similarity index 95% rename from spec/ioki/model/operator/reporting/report_aggregation_query_spec.rb rename to spec/ioki/model/operator/reporting/report_aggregation_series_query_spec.rb index e399007e..a1a4d71f 100644 --- a/spec/ioki/model/operator/reporting/report_aggregation_query_spec.rb +++ b/spec/ioki/model/operator/reporting/report_aggregation_series_query_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe Ioki::Model::Operator::Reporting::ReportAggregationQuery do +RSpec.describe Ioki::Model::Operator::Reporting::ReportAggregationSeriesQuery do let(:start_time) { DateTime.parse('2026-04-01T00:00:00Z') } let(:end_time) { DateTime.parse('2026-05-01T00:00:00Z') } @@ -9,7 +9,7 @@ it { is_expected.to define_attribute(:bucket).as(:string) } it { is_expected.to define_attribute(:filters).as(:array).with(class_name: 'ReportAggregationFilterParam') } - it 'serializes the raw aggregation query payload' do + it 'serializes the raw series query payload' do query = described_class.new( start_time: start_time, end_time: end_time, diff --git a/spec/ioki/model/operator/reporting/report_aggregation_result_spec.rb b/spec/ioki/model/operator/reporting/report_aggregation_series_spec.rb similarity index 68% rename from spec/ioki/model/operator/reporting/report_aggregation_result_spec.rb rename to spec/ioki/model/operator/reporting/report_aggregation_series_spec.rb index 017f9eaa..b174fd25 100644 --- a/spec/ioki/model/operator/reporting/report_aggregation_result_spec.rb +++ b/spec/ioki/model/operator/reporting/report_aggregation_series_spec.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -RSpec.describe Ioki::Model::Operator::Reporting::ReportAggregationResult do - subject(:result) { described_class.new(attributes) } +RSpec.describe Ioki::Model::Operator::Reporting::ReportAggregationSeries do + subject(:series) { described_class.new(attributes) } let(:attributes) do { - type: 'reporting/report_aggregation_result', + type: 'reporting/report_aggregation_series', aggregation_name: 'admin_logins', visualization: 'bar', timezone_identifier: 'Europe/Berlin', @@ -40,26 +40,26 @@ it { is_expected.to define_attribute(:definition_versions).as(:array) } it 'casts measure series into reporting models' do - expect(result.aggregation_name).to eq('admin_logins') - expect(result.visualization).to eq('bar') - expect(result.timezone_identifier).to eq('Europe/Berlin') - expect(result.buckets).to eq(%w[2026-04-01 2026-04-02]) - expect(result.bucket).to eq('day') - expect(result.measures.first).to be_a(Ioki::Model::Operator::Reporting::ReportAggregationMeasureSeries) - expect(result.measures.first.localized_label).to eq('Logins') - expect(result.partitions_considered).to eq(2) - expect(result.definition_versions).to eq([1]) + expect(series.aggregation_name).to eq('admin_logins') + expect(series.visualization).to eq('bar') + expect(series.timezone_identifier).to eq('Europe/Berlin') + expect(series.buckets).to eq(%w[2026-04-01 2026-04-02]) + expect(series.bucket).to eq('day') + expect(series.measures.first).to be_a(Ioki::Model::Operator::Reporting::ReportAggregationMeasureSeries) + expect(series.measures.first.localized_label).to eq('Logins') + expect(series.partitions_considered).to eq(2) + expect(series.definition_versions).to eq([1]) end it 'accepts nullable bucket metadata and measure trends' do - aggregation_result = described_class.new( + aggregation_series = described_class.new( attributes.merge( bucket: nil, measures: [attributes[:measures].first.merge(trend: 12.5)] ) ) - expect(aggregation_result.bucket).to be_nil - expect(aggregation_result.measures.first.trend).to eq(12.5) + expect(aggregation_series.bucket).to be_nil + expect(aggregation_series.measures.first.trend).to eq(12.5) end end diff --git a/spec/ioki/model/operator/reporting/report_aggregation_spec.rb b/spec/ioki/model/operator/reporting/report_aggregation_spec.rb index ff76e081..03411a6f 100644 --- a/spec/ioki/model/operator/reporting/report_aggregation_spec.rb +++ b/spec/ioki/model/operator/reporting/report_aggregation_spec.rb @@ -23,6 +23,7 @@ type: 'reporting/report_aggregation_measure', name: 'rides', function: 'count_rows', + percentile: nil, localized_function: 'Count', localized_label: 'Rides', localized_type: 'Count', @@ -72,6 +73,7 @@ Ioki::Model::Operator::Reporting::ReportAggregationMeasure ) expect(report_aggregation.measures.first.value_type).to eq('number') + expect(report_aggregation.measures.first.percentile).to be_nil expect(report_aggregation.measures.first.localized_label).to eq('Rides') expect(report_aggregation.dimensions.first).to be_a( Ioki::Model::Operator::Reporting::ReportAggregationDimension diff --git a/spec/ioki/model/operator/reporting/report_aggregation_totals_query_spec.rb b/spec/ioki/model/operator/reporting/report_aggregation_totals_query_spec.rb new file mode 100644 index 00000000..cf25fb40 --- /dev/null +++ b/spec/ioki/model/operator/reporting/report_aggregation_totals_query_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.describe Ioki::Model::Operator::Reporting::ReportAggregationTotalsQuery do + let(:start_time) { DateTime.parse('2026-04-01T00:00:00Z') } + let(:end_time) { DateTime.parse('2026-05-01T00:00:00Z') } + + it { is_expected.to define_attribute(:start_time).as(:date_time) } + it { is_expected.to define_attribute(:end_time).as(:date_time) } + it { is_expected.to define_attribute(:filters).as(:array).with(class_name: 'ReportAggregationFilterParam') } + + it 'serializes the raw totals query payload' do + query = described_class.new( + start_time: start_time, + end_time: end_time, + filters: [ + Ioki::Model::Operator::Reporting::ReportAggregationFilterParam.new( + name: 'booking_type', + values: %w[prebooked adhoc] + ) + ] + ) + + expect(query.serialize(:create)).to eq( + start_time: start_time, + end_time: end_time, + filters: [ + { + name: 'booking_type', + values: %w[prebooked adhoc] + } + ] + ) + end + + it 'omits optional attributes when they are not set' do + query = described_class.new(start_time: start_time, end_time: end_time) + + expect(query.serialize(:create)).to eq( + start_time: start_time, + end_time: end_time + ) + end +end diff --git a/spec/ioki/model/operator/reporting/report_aggregation_totals_spec.rb b/spec/ioki/model/operator/reporting/report_aggregation_totals_spec.rb new file mode 100644 index 00000000..94f70d47 --- /dev/null +++ b/spec/ioki/model/operator/reporting/report_aggregation_totals_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.describe Ioki::Model::Operator::Reporting::ReportAggregationTotals do + subject(:totals) { described_class.new(attributes) } + + let(:attributes) do + { + type: 'reporting/report_aggregation_totals', + aggregation_name: 'admin_logins', + measures: [ + { + type: 'reporting/report_aggregation_measure_total', + key: 'login_count', + localized_label: 'Logins', + value: 30.0 + } + ] + } + end + + it { is_expected.to define_attribute(:type).as(:string) } + it { is_expected.to define_attribute(:aggregation_name).as(:string) } + it { is_expected.to define_attribute(:measures).as(:array).with(class_name: 'ReportAggregationMeasureTotal') } + + it 'casts measure totals into reporting models' do + expect(totals.aggregation_name).to eq('admin_logins') + expect(totals.measures.first).to be_a(Ioki::Model::Operator::Reporting::ReportAggregationMeasureTotal) + expect(totals.measures.first.localized_label).to eq('Logins') + expect(totals.measures.first.value).to eq(30.0) + end + + it 'accepts nullable total values' do + totals_with_nil_value = described_class.new( + attributes.merge(measures: [attributes[:measures].first.merge(value: nil)]) + ) + + expect(totals_with_nil_value.measures.first.value).to be_nil + end +end diff --git a/spec/ioki/operator_api_spec.rb b/spec/ioki/operator_api_spec.rb index 66bd2737..a041c2d9 100644 --- a/spec/ioki/operator_api_spec.rb +++ b/spec/ioki/operator_api_spec.rb @@ -1913,6 +1913,7 @@ type: 'reporting/report_aggregation_measure', name: 'login_count', function: 'count_rows', + percentile: nil, localized_function: 'Count', localized_label: 'Logins', localized_type: 'Count', @@ -1941,12 +1942,13 @@ expect(aggregations.first.release_stage).to eq('stable') expect(aggregations.first.bucket.default_preset).to eq('last_7_days') expect(aggregations.first.measures.first.localized_label).to eq('Logins') + expect(aggregations.first.measures.first.percentile).to be_nil end end - describe '#create_reporting_aggregation_query(scope, name, aggregation_name, query)' do + describe '#create_reporting_aggregation_series(scope, name, aggregation_name, query)' do let(:query) do - Ioki::Model::Operator::Reporting::ReportAggregationQuery.new( + Ioki::Model::Operator::Reporting::ReportAggregationSeriesQuery.new( start_time: DateTime.parse('2026-04-01T00:00:00Z'), end_time: DateTime.parse('2026-05-01T00:00:00Z'), bucket: 'day', @@ -1962,7 +1964,7 @@ let(:result_with_reporting_aggregation) do { 'data' => { - type: 'reporting/report_aggregation_result', + type: 'reporting/report_aggregation_series', aggregation_name: 'admin_logins', visualization: 'bar', timezone_identifier: 'Europe/Berlin', @@ -1986,13 +1988,13 @@ it 'calls request on the client with expected params' do expect(operator_client).to receive(:request) do |params| expect(params[:url].to_s) - .to eq('operator/reporting/report/scopes/myscope/reports/myname/aggregations/ride_counts/aggregate') + .to eq('operator/reporting/report/scopes/myscope/reports/myname/aggregations/ride_counts/series') expect(params[:method]).to eq(:post) expect(params[:body]).to eq({ data: query.serialize(:create, format: :json) }) [result_with_reporting_aggregation, full_response] end - aggregation_result = operator_client.create_reporting_aggregation_query( + aggregation_result = operator_client.create_reporting_aggregation_series( 'myscope', 'myname', 'ride_counts', @@ -2000,7 +2002,7 @@ options ) - expect(aggregation_result).to be_a(Ioki::Model::Operator::Reporting::ReportAggregationResult) + expect(aggregation_result).to be_a(Ioki::Model::Operator::Reporting::ReportAggregationSeries) expect(aggregation_result.aggregation_name).to eq('admin_logins') expect(aggregation_result.timezone_identifier).to eq('Europe/Berlin') expect(aggregation_result.bucket).to eq('day') @@ -2010,6 +2012,62 @@ end end + describe '#create_reporting_aggregation_totals(scope, name, aggregation_name, query)' do + let(:query) do + Ioki::Model::Operator::Reporting::ReportAggregationTotalsQuery.new( + start_time: DateTime.parse('2026-04-01T00:00:00Z'), + end_time: DateTime.parse('2026-05-01T00:00:00Z'), + filters: [ + Ioki::Model::Operator::Reporting::ReportAggregationFilterParam.new( + name: 'booking_type', + values: %w[prebooked adhoc] + ) + ] + ) + end + + let(:result_with_reporting_aggregation) do + { + 'data' => { + type: 'reporting/report_aggregation_totals', + aggregation_name: 'admin_logins', + measures: [ + { + type: 'reporting/report_aggregation_measure_total', + key: 'login_count', + localized_label: 'Logins', + value: 30.0 + } + ] + } + } + end + + it 'calls request on the client with expected params' do + expect(operator_client).to receive(:request) do |params| + expect(params[:url].to_s) + .to eq('operator/reporting/report/scopes/myscope/reports/myname/aggregations/ride_counts/totals') + expect(params[:method]).to eq(:post) + expect(params[:body]).to eq({ data: query.serialize(:create, format: :json) }) + [result_with_reporting_aggregation, full_response] + end + + aggregation_totals = operator_client.create_reporting_aggregation_totals( + 'myscope', + 'myname', + 'ride_counts', + query, + options + ) + + expect(aggregation_totals).to be_a(Ioki::Model::Operator::Reporting::ReportAggregationTotals) + expect(aggregation_totals.aggregation_name).to eq('admin_logins') + expect(aggregation_totals.measures.first).to be_a(Ioki::Model::Operator::Reporting::ReportAggregationMeasureTotal) + expect(aggregation_totals.measures.first.localized_label).to eq('Logins') + expect(aggregation_totals.measures.first.value).to eq(30.0) + end + end + describe '#cancellation_statements(product_id)' do it 'calls request on the client with expected params' do expect(operator_client).to receive(:request) do |params|