pike13 0.1.0.beta → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -117
  3. data/README.md +1222 -315
  4. data/lib/pike13/api/v2/account/business.rb +0 -4
  5. data/lib/pike13/api/v2/account.rb +19 -0
  6. data/lib/pike13/api/v2/desk/booking.rb +10 -0
  7. data/lib/pike13/api/v2/desk/business.rb +2 -1
  8. data/lib/pike13/api/v2/desk/event_occurrence.rb +2 -2
  9. data/lib/pike13/api/v2/desk/person.rb +5 -3
  10. data/lib/pike13/api/v2/desk/plan.rb +2 -2
  11. data/lib/pike13/api/v2/desk/plan_product.rb +2 -2
  12. data/lib/pike13/api/v2/desk/service.rb +2 -2
  13. data/lib/pike13/api/v2/desk/visit.rb +2 -2
  14. data/lib/pike13/api/v2/front/branding.rb +2 -1
  15. data/lib/pike13/api/v2/front/business.rb +2 -1
  16. data/lib/pike13/api/v2/front/event_occurrence.rb +2 -2
  17. data/lib/pike13/api/v2/front/plan_product.rb +2 -2
  18. data/lib/pike13/api/v2/front/service.rb +2 -2
  19. data/lib/pike13/api/v2/front/visit.rb +2 -2
  20. data/lib/pike13/api/v3/desk/base.rb +46 -0
  21. data/lib/pike13/api/v3/desk/clients.rb +180 -0
  22. data/lib/pike13/api/v3/desk/enrollments.rb +203 -0
  23. data/lib/pike13/api/v3/desk/event_occurrence_staff_members.rb +170 -0
  24. data/lib/pike13/api/v3/desk/event_occurrences.rb +154 -0
  25. data/lib/pike13/api/v3/desk/invoice_item_transactions.rb +189 -0
  26. data/lib/pike13/api/v3/desk/invoice_items.rb +193 -0
  27. data/lib/pike13/api/v3/desk/invoices.rb +167 -0
  28. data/lib/pike13/api/v3/desk/monthly_business_metrics.rb +151 -0
  29. data/lib/pike13/api/v3/desk/pays.rb +128 -0
  30. data/lib/pike13/api/v3/desk/person_plans.rb +265 -0
  31. data/lib/pike13/api/v3/desk/staff_members.rb +127 -0
  32. data/lib/pike13/api/v3/desk/transactions.rb +169 -0
  33. data/lib/pike13/http_client.rb +4 -1
  34. data/lib/pike13/http_client_v3.rb +101 -0
  35. data/lib/pike13/validators.rb +136 -0
  36. data/lib/pike13/version.rb +1 -1
  37. data/lib/pike13.rb +26 -7
  38. metadata +19 -4
  39. data/lib/pike13/api/v2/account/me.rb +0 -20
@@ -0,0 +1,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pike13
4
+ module API
5
+ module V3
6
+ module Desk
7
+ # Person Plans resource
8
+ # Comprehensive data about passes and plans that are available for use or on hold
9
+ #
10
+ # @example Basic query
11
+ # Pike13::Reporting::PersonPlans.query(
12
+ # fields: ['person_plan_id', 'full_name', 'plan_name', 'is_available', 'start_date']
13
+ # )
14
+ #
15
+ # @example Query available memberships
16
+ # Pike13::Reporting::PersonPlans.query(
17
+ # fields: ['full_name', 'plan_name', 'start_date', 'end_date', 'remaining_visit_count'],
18
+ # filter: [
19
+ # 'and',
20
+ # [
21
+ # ['eq', 'is_available', true],
22
+ # ['eq', 'grants_membership', true]
23
+ # ]
24
+ # ]
25
+ # )
26
+ #
27
+ # @example Group by plan type
28
+ # Pike13::Reporting::PersonPlans.query(
29
+ # fields: ['person_plan_count', 'is_available_count', 'is_on_hold_count'],
30
+ # group: 'plan_type'
31
+ # )
32
+ class PersonPlans < Base
33
+ class << self
34
+ # Execute a person plans query
35
+ #
36
+ # @param fields [Array<String>] Fields to return (detail or summary fields)
37
+ # @param filter [Array, nil] Filter criteria (optional)
38
+ # @param group [String, nil] Grouping field (optional)
39
+ # @param sort [Array<String>, nil] Sort order (optional)
40
+ # @param page [Hash, nil] Pagination options (optional)
41
+ # @param total_count [Boolean] Whether to return total count (optional)
42
+ # @return [Hash] Query result with rows, fields, and metadata
43
+ #
44
+ # @see https://developer.pike13.com/docs/api/v3/reports/person-plans
45
+ def query(fields:, filter: nil, group: nil, sort: nil, page: nil, total_count: nil)
46
+ query_params = { fields: fields }
47
+ query_params[:filter] = filter if filter
48
+ query_params[:group] = group if group
49
+ query_params[:sort] = sort if sort
50
+ query_params[:page] = page if page
51
+ query_params[:total_count] = total_count if total_count
52
+
53
+ super("person_plans", query_params)
54
+ end
55
+
56
+ # Available detail fields (when not grouping)
57
+ DETAIL_FIELDS = %w[
58
+ account_manager_emails
59
+ account_manager_names
60
+ account_manager_phones
61
+ allowed_visit_count
62
+ are_visits_shared
63
+ base_price
64
+ business_id
65
+ business_name
66
+ business_subdomain
67
+ canceled_at
68
+ canceled_by
69
+ canceled_by_account_id
70
+ cancellation_fee_amount
71
+ charged_cancellation_fee_amount
72
+ commitment_length
73
+ completed_visit_count
74
+ currency_code
75
+ deactivated_at
76
+ email
77
+ end_date
78
+ exhausted_at
79
+ first_name
80
+ first_visit_date
81
+ first_visit_day
82
+ first_visit_instructor_names
83
+ first_visit_time
84
+ first_visit_to_next_plan
85
+ franchise_id
86
+ full_name
87
+ grants_membership
88
+ has_cancellation_fee
89
+ home_location_name
90
+ invoice_interval_count
91
+ invoice_interval_unit
92
+ is_available
93
+ is_canceled
94
+ is_cancellation_fee_charged
95
+ is_complimentary_pass
96
+ is_deactivated
97
+ is_deleted
98
+ is_ended
99
+ is_exhausted
100
+ is_first_membership
101
+ is_first_plan
102
+ is_last_hold_indefinite
103
+ is_on_hold
104
+ is_primary_participant
105
+ key
106
+ last_hold_by
107
+ last_hold_by_account_id
108
+ last_hold_end_date
109
+ last_hold_start_date
110
+ last_name
111
+ last_usable_date
112
+ last_visit_date
113
+ last_visit_day
114
+ last_visit_time
115
+ last_visit_to_next_plan
116
+ latest_invoice_autobill
117
+ latest_invoice_due_date
118
+ latest_invoice_item_amount
119
+ latest_invoice_past_due
120
+ lifetime_used_visit_count
121
+ middle_name
122
+ next_plan_grants_membership
123
+ next_plan_id
124
+ next_plan_name
125
+ next_plan_revenue_category
126
+ next_plan_start_date
127
+ next_plan_type
128
+ person_id
129
+ person_plan_id
130
+ plan_id
131
+ plan_location_id
132
+ plan_location_name
133
+ plan_name
134
+ plan_product_id
135
+ plan_type
136
+ primary_staff_name
137
+ product_id
138
+ product_name
139
+ remaining_commitment
140
+ remaining_visit_count
141
+ revenue_category
142
+ rollover_count
143
+ start_date
144
+ start_date_to_first_visit
145
+ start_date_to_next_plan
146
+ stop_renewing_after_commitment
147
+ term_acceptance_required
148
+ term_accepted
149
+ used_for_initial_visit
150
+ used_visit_count
151
+ visit_refresh_interval_count
152
+ visit_refresh_interval_unit
153
+ ].freeze
154
+
155
+ # Available summary fields (when grouping)
156
+ SUMMARY_FIELDS = %w[
157
+ avg_first_visit_to_next_plan
158
+ avg_last_visit_to_next_plan
159
+ avg_start_date_to_first_visit
160
+ avg_start_date_to_next_plan
161
+ business_id_summary
162
+ business_subdomain_summary
163
+ grants_membership_count
164
+ has_cancellation_fee_count
165
+ is_available_count
166
+ is_canceled_count
167
+ is_complimentary_pass_count
168
+ is_first_membership_count
169
+ is_first_plan_count
170
+ is_on_hold_count
171
+ latest_invoice_past_due_count
172
+ next_plan_count
173
+ next_plan_grants_membership_percent
174
+ next_plan_on_visit_date_percent
175
+ next_plan_out_of_visited_percent
176
+ next_plan_percent
177
+ next_plan_within_week_percent
178
+ person_count
179
+ person_plan_count
180
+ plan_count
181
+ total_cancellation_fee_amount
182
+ total_charged_cancellation_fee_amount
183
+ total_count
184
+ total_latest_invoice_item_amount
185
+ total_lifetime_used_visit_count
186
+ total_used_for_initial_visit
187
+ total_used_visit_count
188
+ visited_count
189
+ visited_percent
190
+ ].freeze
191
+
192
+ # Available grouping fields
193
+ GROUPINGS = %w[
194
+ are_visits_shared
195
+ business_id
196
+ business_name
197
+ business_subdomain
198
+ canceled_by
199
+ canceled_by_account_id
200
+ end_date
201
+ first_visit_date
202
+ first_visit_instructor_names
203
+ first_visit_month_start_date
204
+ first_visit_quarter_start_date
205
+ first_visit_week_mon_start_date
206
+ first_visit_week_sun_start_date
207
+ first_visit_year_start_date
208
+ full_name
209
+ grants_membership
210
+ has_cancellation_fee
211
+ home_location_name
212
+ is_available
213
+ is_canceled
214
+ is_cancellation_fee_charged
215
+ is_complimentary_pass
216
+ is_deleted
217
+ is_first_membership
218
+ is_first_plan
219
+ is_last_hold_indefinite
220
+ is_on_hold
221
+ last_hold_by
222
+ last_hold_by_account_id
223
+ last_hold_end_date
224
+ last_hold_start_date
225
+ last_usable_date
226
+ last_usable_month_start_date
227
+ last_usable_quarter_start_date
228
+ last_usable_week_mon_start_date
229
+ last_usable_week_sun_start_date
230
+ last_usable_year_start_date
231
+ last_visit_date
232
+ last_visit_month_start_date
233
+ last_visit_quarter_start_date
234
+ last_visit_week_mon_start_date
235
+ last_visit_week_sun_start_date
236
+ last_visit_year_start_date
237
+ latest_invoice_autobill
238
+ latest_invoice_due_date
239
+ latest_invoice_past_due
240
+ person_id
241
+ plan_id
242
+ plan_location_id
243
+ plan_location_name
244
+ plan_name
245
+ plan_product_id
246
+ plan_type
247
+ primary_staff_name
248
+ product_id
249
+ product_name
250
+ revenue_category
251
+ start_date
252
+ start_month_start_date
253
+ start_quarter_start_date
254
+ start_week_mon_start_date
255
+ start_week_sun_start_date
256
+ start_year_start_date
257
+ stop_renewing_after_commitment
258
+ used_for_initial_visit
259
+ ].freeze
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pike13
4
+ module API
5
+ module V3
6
+ module Desk
7
+ # Staff Members resource
8
+ # All staff member data — from tenure and events to birthdays and custom fields
9
+ #
10
+ # @example Basic query
11
+ # Pike13::Reporting::StaffMembers.query(
12
+ # fields: ['person_id', 'full_name', 'email', 'role', 'person_state']
13
+ # )
14
+ #
15
+ # @example Query active staff members
16
+ # Pike13::Reporting::StaffMembers.query(
17
+ # fields: ['full_name', 'email', 'role', 'tenure', 'future_events'],
18
+ # filter: ['eq', 'person_state', 'active']
19
+ # )
20
+ #
21
+ # @example Group by role
22
+ # Pike13::Reporting::StaffMembers.query(
23
+ # fields: ['person_count', 'total_future_events', 'total_past_events'],
24
+ # group: 'role'
25
+ # )
26
+ class StaffMembers < Base
27
+ class << self
28
+ # Execute a staff members query
29
+ #
30
+ # @param fields [Array<String>] Fields to return (detail or summary fields)
31
+ # @param filter [Array, nil] Filter criteria (optional)
32
+ # @param group [String, nil] Grouping field (optional)
33
+ # @param sort [Array<String>, nil] Sort order (optional)
34
+ # @param page [Hash, nil] Pagination options (optional)
35
+ # @param total_count [Boolean] Whether to return total count (optional)
36
+ # @return [Hash] Query result with rows, fields, and metadata
37
+ #
38
+ # @see https://developer.pike13.com/docs/api/v3/reports/staff_members
39
+ def query(fields:, filter: nil, group: nil, sort: nil, page: nil, total_count: nil)
40
+ query_params = { fields: fields }
41
+ query_params[:filter] = filter if filter
42
+ query_params[:group] = group if group
43
+ query_params[:sort] = sort if sort
44
+ query_params[:page] = page if page
45
+ query_params[:total_count] = total_count if total_count
46
+
47
+ super("staff_members", query_params)
48
+ end
49
+
50
+ # Available detail fields (when not grouping)
51
+ DETAIL_FIELDS = %w[
52
+ address
53
+ age
54
+ also_client
55
+ attendance_not_completed_events
56
+ birthdate
57
+ business_id
58
+ business_name
59
+ business_subdomain
60
+ city
61
+ country_code
62
+ currency_code
63
+ custom_fields
64
+ days_until_birthday
65
+ email
66
+ first_name
67
+ franchise_id
68
+ full_name
69
+ future_events
70
+ home_location_id
71
+ home_location_name
72
+ key
73
+ last_name
74
+ middle_name
75
+ past_events
76
+ person_id
77
+ person_state
78
+ phone
79
+ postal_code
80
+ role
81
+ show_to_clients
82
+ staff_since_date
83
+ state_code
84
+ street_address
85
+ street_address2
86
+ tenure
87
+ tenure_group
88
+ ].freeze
89
+
90
+ # Available summary fields (when grouping)
91
+ SUMMARY_FIELDS = %w[
92
+ also_client_count
93
+ business_id_summary
94
+ business_subdomain_summary
95
+ demoted_staff_count
96
+ person_count
97
+ total_attendance_not_completed_events
98
+ total_count
99
+ total_future_events
100
+ total_past_events
101
+ ].freeze
102
+
103
+ # Available grouping fields
104
+ GROUPINGS = %w[
105
+ age
106
+ also_client
107
+ business_id
108
+ business_name
109
+ business_subdomain
110
+ home_location_name
111
+ person_state
112
+ role
113
+ show_to_clients
114
+ staff_since_date
115
+ staff_since_month_start_date
116
+ staff_since_quarter_start_date
117
+ staff_since_week_mon_start_date
118
+ staff_since_week_sun_start_date
119
+ staff_since_year_start_date
120
+ tenure_group
121
+ ].freeze
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pike13
4
+ module API
5
+ module V3
6
+ module Desk
7
+ # Transactions resource
8
+ # Data about the money moving through your business
9
+ #
10
+ # @example Basic query
11
+ # Pike13::Reporting::Transactions.query(
12
+ # fields: ['transaction_id', 'transaction_date', 'net_paid_amount', 'payment_method']
13
+ # )
14
+ #
15
+ # @example Query by date range
16
+ # Pike13::Reporting::Transactions.query(
17
+ # fields: ['transaction_date', 'net_paid_amount', 'invoice_payer_name', 'payment_method'],
18
+ # filter: ['btw', 'transaction_date', '2024-01-01', '2024-12-31']
19
+ # )
20
+ #
21
+ # @example Group by payment method
22
+ # Pike13::Reporting::Transactions.query(
23
+ # fields: ['total_net_paid_amount', 'transaction_count'],
24
+ # group: 'payment_method'
25
+ # )
26
+ class Transactions < Base
27
+ class << self
28
+ # Execute a transactions query
29
+ #
30
+ # @param fields [Array<String>] Fields to return (detail or summary fields)
31
+ # @param filter [Array, nil] Filter criteria (optional)
32
+ # @param group [String, nil] Grouping field (optional)
33
+ # @param sort [Array<String>, nil] Sort order (optional)
34
+ # @param page [Hash, nil] Pagination options (optional)
35
+ # @param total_count [Boolean] Whether to return total count (optional)
36
+ # @return [Hash] Query result with rows, fields, and metadata
37
+ #
38
+ # @see https://developer.pike13.com/docs/api/v3/reports/transactions
39
+ def query(fields:, filter: nil, group: nil, sort: nil, page: nil, total_count: nil)
40
+ query_params = { fields: fields }
41
+ query_params[:filter] = filter if filter
42
+ query_params[:group] = group if group
43
+ query_params[:sort] = sort if sort
44
+ query_params[:page] = page if page
45
+ query_params[:total_count] = total_count if total_count
46
+
47
+ super("transactions", query_params)
48
+ end
49
+
50
+ # Available detail fields (when not grouping)
51
+ DETAIL_FIELDS = %w[
52
+ business_id
53
+ business_name
54
+ business_subdomain
55
+ commission_recipient_name
56
+ created_by_name
57
+ credit_card_name
58
+ currency_code
59
+ error_message
60
+ external_payment_name
61
+ failed_at
62
+ failed_date
63
+ franchise_id
64
+ invoice_autobill
65
+ invoice_due_date
66
+ invoice_id
67
+ invoice_number
68
+ invoice_payer_email
69
+ invoice_payer_home_location
70
+ invoice_payer_id
71
+ invoice_payer_name
72
+ invoice_payer_phone
73
+ invoice_payer_primary_staff_name_at_sale
74
+ invoice_state
75
+ key
76
+ net_paid_amount
77
+ net_paid_revenue_amount
78
+ net_paid_tax_amount
79
+ payment_method
80
+ payment_method_detail
81
+ payment_transaction_id
82
+ payments_amount
83
+ processing_method
84
+ processor_transaction_id
85
+ refunds_amount
86
+ sale_location_name
87
+ transaction_amount
88
+ transaction_at
89
+ transaction_autopay
90
+ transaction_date
91
+ transaction_id
92
+ transaction_state
93
+ transaction_type
94
+ voided_at
95
+ ].freeze
96
+
97
+ # Available summary fields (when grouping)
98
+ SUMMARY_FIELDS = %w[
99
+ business_id_summary
100
+ business_subdomain_summary
101
+ failed_count
102
+ invoice_count
103
+ settled_count
104
+ total_count
105
+ total_net_ach_paid_amount
106
+ total_net_american_express_paid_amount
107
+ total_net_amex_processing_paid_amount
108
+ total_net_cash_paid_amount
109
+ total_net_check_paid_amount
110
+ total_net_credit_paid_amount
111
+ total_net_discover_paid_amount
112
+ total_net_external_paid_amount
113
+ total_net_global_pay_processing_paid_amount
114
+ total_net_mastercard_paid_amount
115
+ total_net_other_credit_card_paid_amount
116
+ total_net_other_processing_paid_amount
117
+ total_net_paid_amount
118
+ total_net_paid_revenue_amount
119
+ total_net_paid_tax_amount
120
+ total_net_visa_paid_amount
121
+ total_payments_amount
122
+ total_refunds_amount
123
+ transaction_autopay_count
124
+ transaction_count
125
+ ].freeze
126
+
127
+ # Available grouping fields
128
+ GROUPINGS = %w[
129
+ business_id
130
+ business_name
131
+ business_subdomain
132
+ commission_recipient_name
133
+ created_by_name
134
+ credit_card_name
135
+ external_payment_name
136
+ failed_date
137
+ failed_month_start_date
138
+ failed_quarter_start_date
139
+ failed_week_mon_start_date
140
+ failed_week_sun_start_date
141
+ failed_year_start_date
142
+ invoice_autobill
143
+ invoice_due_date
144
+ invoice_id
145
+ invoice_number
146
+ invoice_payer_home_location
147
+ invoice_payer_id
148
+ invoice_payer_name
149
+ invoice_payer_primary_staff_name_at_sale
150
+ invoice_state
151
+ payment_method
152
+ processing_method
153
+ sale_location_name
154
+ transaction_autopay
155
+ transaction_date
156
+ transaction_month_start_date
157
+ transaction_quarter_start_date
158
+ transaction_state
159
+ transaction_type
160
+ transaction_week_mon_start_date
161
+ transaction_week_sun_start_date
162
+ transaction_year_start_date
163
+ ].freeze
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "httparty"
4
+ require "json"
4
5
 
5
6
  module Pike13
6
7
  # HTTParty-based HTTP client for Pike13 API
@@ -162,9 +163,11 @@ module Pike13
162
163
  return nil if response.body.nil? || response.body.empty?
163
164
 
164
165
  parsed = response.parsed_response
166
+
167
+ # If parsed response is not a Hash (e.g., it's an Array), return as-is
165
168
  return parsed unless parsed.is_a?(Hash)
166
169
 
167
- # Handle different response formats
170
+ # Handle different response formats for Hash responses
168
171
  if parsed.key?("data")
169
172
  parsed["data"]
170
173
  else
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httparty"
4
+ require "json"
5
+
6
+ module Pike13
7
+ # HTTParty-based HTTP client for Pike13 v3 Reporting API
8
+ # V3 API uses JSON API 1.0 (application/vnd.api+json) and a different URL structure than v2
9
+ class HTTPClientV3
10
+ include HTTParty
11
+
12
+ attr_reader :base_url, :access_token
13
+
14
+ def initialize(base_url:, access_token:)
15
+ @base_url = base_url
16
+ @access_token = access_token
17
+ end
18
+
19
+ # POST request for v3 reporting queries
20
+ #
21
+ # @param path [String] The API endpoint path
22
+ # @param body [Hash] Request body
23
+ # @param params [Hash] Query parameters
24
+ # @return [Hash] Parsed response body
25
+ def post(path, body = {}, params = {})
26
+ handle_response do
27
+ self.class.post(
28
+ full_path(path),
29
+ body: body.to_json,
30
+ query: params,
31
+ headers: headers,
32
+ timeout: 30
33
+ )
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ # V3 API uses /desk/api/v3/reports/... structure instead of /api/v2/desk/...
40
+ def full_path(path)
41
+ "#{base_url}/#{path}"
42
+ end
43
+
44
+ def headers
45
+ {
46
+ "Authorization" => "Bearer #{access_token}",
47
+ "Content-Type" => "application/vnd.api+json",
48
+ "Accept" => "application/vnd.api+json"
49
+ }
50
+ end
51
+
52
+ def handle_response
53
+ response = yield
54
+ handle_status_code(response)
55
+ rescue HTTParty::Error => e
56
+ raise Pike13::ConnectionError, "Connection failed: #{e.message}"
57
+ end
58
+
59
+ def handle_status_code(response)
60
+ return parse_response_body(response) if response.code.between?(200, 299)
61
+
62
+ raise_error_for_status(response)
63
+ end
64
+
65
+ def raise_error_for_status(response)
66
+ case response.code
67
+ when 400 then raise Pike13::BadRequestError.new(response.body, http_status: response.code)
68
+ when 401 then raise Pike13::UnauthorizedError.new("Unauthorized", http_status: response.code)
69
+ when 404 then raise Pike13::NotFoundError.new("Resource not found", http_status: response.code)
70
+ when 422 then raise_validation_error(response)
71
+ when 429 then raise Pike13::RateLimitError.new("Rate limit exceeded", http_status: response.code)
72
+ when 500..599 then raise Pike13::ServerError.new("Server error", http_status: response.code)
73
+ else raise Pike13::APIError.new("Unexpected error", http_status: response.code)
74
+ end
75
+ end
76
+
77
+ def raise_validation_error(response)
78
+ parsed = parse_json_safely(response.body)
79
+ error_message = extract_error_message(parsed, response.body)
80
+ raise Pike13::ValidationError.new(error_message, http_status: response.code)
81
+ end
82
+
83
+ def parse_json_safely(body)
84
+ JSON.parse(body)
85
+ rescue StandardError
86
+ {}
87
+ end
88
+
89
+ def extract_error_message(parsed, fallback)
90
+ return fallback unless parsed.is_a?(Hash) && parsed["errors"]
91
+
92
+ parsed["errors"].is_a?(Array) ? parsed["errors"].first : parsed["errors"]
93
+ end
94
+
95
+ def parse_response_body(response)
96
+ return nil if response.body.nil? || response.body.empty?
97
+
98
+ response.parsed_response
99
+ end
100
+ end
101
+ end