twitter-ads 5.1.0 → 7.0.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -2
  3. data/README.md +1 -1
  4. data/lib/twitter-ads.rb +8 -4
  5. data/lib/twitter-ads/account.rb +6 -25
  6. data/lib/twitter-ads/audiences/tailored_audience.rb +98 -3
  7. data/lib/twitter-ads/{targeting_criteria/behavior_taxonomy.rb → campaign/advertiser_business_categories.rb} +6 -6
  8. data/lib/twitter-ads/campaign/campaign.rb +1 -2
  9. data/lib/twitter-ads/campaign/content_categories.rb +23 -0
  10. data/lib/twitter-ads/campaign/funding_instrument.rb +1 -2
  11. data/lib/twitter-ads/campaign/line_item.rb +4 -4
  12. data/lib/twitter-ads/campaign/organic_tweet.rb +1 -3
  13. data/lib/twitter-ads/campaign/targeting_criteria.rb +0 -1
  14. data/lib/twitter-ads/campaign/tweet.rb +4 -49
  15. data/lib/twitter-ads/client.rb +2 -2
  16. data/lib/twitter-ads/creative/account_media.rb +4 -6
  17. data/lib/twitter-ads/creative/draft_tweet.rb +40 -0
  18. data/lib/twitter-ads/creative/image_app_download_card.rb +2 -2
  19. data/lib/twitter-ads/creative/image_conversation_card.rb +3 -2
  20. data/lib/twitter-ads/creative/media_creative.rb +2 -3
  21. data/lib/twitter-ads/creative/media_library.rb +12 -13
  22. data/lib/twitter-ads/creative/promoted_account.rb +1 -2
  23. data/lib/twitter-ads/creative/promoted_tweet.rb +1 -2
  24. data/lib/twitter-ads/creative/scheduled_tweet.rb +1 -12
  25. data/lib/twitter-ads/creative/tweets.rb +52 -0
  26. data/lib/twitter-ads/creative/video_app_download_card.rb +4 -6
  27. data/lib/twitter-ads/creative/video_conversation_card.rb +6 -6
  28. data/lib/twitter-ads/creative/video_website_card.rb +3 -5
  29. data/lib/twitter-ads/creative/website_card.rb +2 -2
  30. data/lib/twitter-ads/cursor.rb +6 -0
  31. data/lib/twitter-ads/enum.rb +19 -9
  32. data/lib/twitter-ads/error.rb +5 -15
  33. data/lib/twitter-ads/http/request.rb +37 -2
  34. data/lib/twitter-ads/http/response.rb +1 -13
  35. data/lib/twitter-ads/resources/analytics.rb +99 -47
  36. data/lib/twitter-ads/resources/dsl.rb +8 -1
  37. data/lib/twitter-ads/restapi.rb +29 -0
  38. data/lib/twitter-ads/settings/tax.rb +13 -1
  39. data/lib/twitter-ads/targeting/audience_summary.rb +47 -0
  40. data/lib/twitter-ads/targeting_criteria/{behavior.rb → conversation.rb} +3 -7
  41. data/lib/twitter-ads/utils.rb +23 -0
  42. data/lib/twitter-ads/version.rb +1 -1
  43. data/spec/fixtures/audience_summary.json +14 -0
  44. data/spec/fixtures/line_items_all.json +2 -10
  45. data/spec/fixtures/line_items_load.json +0 -1
  46. data/spec/fixtures/tweet_previews.json +23 -0
  47. data/spec/spec_helper.rb +1 -4
  48. data/spec/twitter-ads/campaign/line_item_spec.rb +0 -1
  49. data/spec/twitter-ads/campaign/targeting_criteria_spec.rb +0 -1
  50. data/spec/twitter-ads/campaign/tweet_spec.rb +0 -59
  51. data/spec/twitter-ads/client_spec.rb +17 -1
  52. data/spec/twitter-ads/creative/media_creative_spec.rb +1 -1
  53. data/spec/twitter-ads/creative/tweet_previews_spec.rb +41 -0
  54. data/spec/twitter-ads/rate_limit_spec.rb +247 -0
  55. data/spec/twitter-ads/retry_count_spec.rb +61 -0
  56. data/spec/twitter-ads/{creative/image_app_download_card_spec.rb → targeting/audience_summary_spec.rb} +16 -18
  57. metadata +46 -47
  58. data/lib/twitter-ads/audiences/audience_intelligence.rb +0 -68
  59. data/lib/twitter-ads/targeting/reach_estimate.rb +0 -78
  60. data/spec/fixtures/tweet_preview.json +0 -24
  61. data/spec/twitter-ads/campaign/reach_estimate_spec.rb +0 -103
  62. data/spec/twitter-ads/creative/account_media_spec.rb +0 -32
  63. data/spec/twitter-ads/creative/image_conversation_card_spec.rb +0 -40
  64. data/spec/twitter-ads/creative/video_app_download_card_spec.rb +0 -42
  65. data/spec/twitter-ads/creative/video_conversation_card_spec.rb +0 -51
  66. data/spec/twitter-ads/creative/website_card_spec.rb +0 -42
@@ -59,18 +59,7 @@ module TwitterAds
59
59
 
60
60
  # Server Errors (5XX)
61
61
  class ServerError < Error; end
62
-
63
- class ServiceUnavailable < ServerError
64
- attr_reader :retry_after
65
-
66
- def initialize(object)
67
- super object
68
- if object.headers['retry-after']
69
- @retry_after = object.headers['retry-after']
70
- end
71
- self
72
- end
73
- end
62
+ class ServiceUnavailable < ServerError; end
74
63
 
75
64
  # Client Errors (4XX)
76
65
  class ClientError < Error; end
@@ -80,12 +69,13 @@ module TwitterAds
80
69
  class BadRequest < ClientError; end
81
70
 
82
71
  class RateLimit < ClientError
83
- attr_reader :reset_at, :retry_after
72
+ attr_reader :reset_at
84
73
 
85
74
  def initialize(object)
86
75
  super object
87
- @retry_after = object.headers['retry-after']
88
- @reset_at = object.headers['rate_limit_reset']
76
+ header = object.headers.fetch('x-account-rate-limit-reset', nil) ||
77
+ object.headers.fetch('x-rate-limit-reset', nil)
78
+ @reset_at = header.first.to_i
89
79
  self
90
80
  end
91
81
  end
@@ -71,18 +71,53 @@ module TwitterAds
71
71
  token = OAuth::AccessToken.new(consumer, @client.access_token, @client.access_token_secret)
72
72
  request.oauth!(consumer.http, consumer, token)
73
73
 
74
+ handle_rate_limit = @client.options.fetch(:handle_rate_limit, false)
75
+ retry_max = @client.options.fetch(:retry_max, 0)
76
+ retry_delay = @client.options.fetch(:retry_delay, 1500)
77
+ retry_on_status = @client.options.fetch(:retry_on_status, [500, 503])
78
+ retry_count = 0
79
+ retry_after = nil
80
+
74
81
  write_log(request) if @client.options[:trace]
75
- response = consumer.http.request(request)
82
+ while retry_count <= retry_max
83
+ response = consumer.http.request(request)
84
+ status_code = response.code.to_i
85
+ break if status_code >= 200 && status_code < 300
86
+
87
+ if handle_rate_limit && retry_after.nil?
88
+ rate_limit_reset = response.fetch('x-account-rate-limit-reset', nil) ||
89
+ response.fetch('x-rate-limit-reset', nil)
90
+ if status_code == 429
91
+ retry_after = rate_limit_reset.to_i - Time.now.to_i
92
+ @client.logger.warn('Request reached Rate Limit: resume in %d seconds' % retry_after)
93
+ sleep(retry_after + 5)
94
+ next
95
+ end
96
+ end
97
+
98
+ if retry_max.positive?
99
+ break unless retry_on_status.include?(status_code)
100
+ sleep(retry_delay / 1000)
101
+ end
102
+
103
+ retry_count += 1
104
+ end
76
105
  write_log(response) if @client.options[:trace]
77
106
 
78
107
  Response.new(response.code, response.each {}, response.body)
79
108
  end
80
109
 
110
+ def escape_params(input)
111
+ input.map do |key, value|
112
+ "#{CGI.escape key.to_s}=#{CGI.escape value.to_s}"
113
+ end.join('&')
114
+ end
115
+
81
116
  def http_request
82
117
  request_url = @resource
83
118
 
84
119
  if @options[:params] && !@options[:params].empty?
85
- request_url += "?#{URI.encode_www_form(@options[:params])}"
120
+ request_url += "?#{escape_params(@options[:params])}"
86
121
  end
87
122
 
88
123
  request = HTTP_METHOD[@method].new(request_url)
@@ -9,9 +9,7 @@ module TwitterAds
9
9
  attr_reader :code,
10
10
  :headers,
11
11
  :raw_body,
12
- :body,
13
- :rate_limit_remaining,
14
- :rate_limit_reset
12
+ :body
15
13
 
16
14
  # Creates a new Response object instance.
17
15
  #
@@ -37,16 +35,6 @@ module TwitterAds
37
35
  @body = raw_body
38
36
  end
39
37
 
40
- if headers.key?('x-rate-limit-reset')
41
- @rate_limit = headers['x-rate-limit-limit'].first
42
- @rate_limit_remaining = headers['x-rate-limit-remaining'].first
43
- @rate_limit_reset = headers['x-rate-limit-reset'].first.to_i
44
- elsif headers.key?('x-cost-rate-limit-reset')
45
- @rate_limit = headers['x-cost-rate-limit-limit'].first
46
- @rate_limit_remaining = headers['x-cost-rate-limit-remaining'].first
47
- @rate_limit_reset = Time.at(headers['x-cost-rate-limit-reset'].first.to_i)
48
- end
49
-
50
38
  self
51
39
  end
52
40
 
@@ -5,15 +5,35 @@ require 'zlib'
5
5
  require 'open-uri'
6
6
 
7
7
  module TwitterAds
8
- module Analytics
8
+ class Analytics
9
9
 
10
+ include TwitterAds::DSL
11
+ include TwitterAds::Resource
10
12
  include TwitterAds::Enum
11
13
 
14
+ attr_reader :account
15
+
16
+ property :id, read_only: true
17
+ property :id_str, read_only: true
18
+ property :status, read_only: true
19
+ property :url, read_only: true
20
+ property :created_at, type: :time, read_only: true
21
+ property :expires_at, type: :time, read_only: true
22
+ property :updated_at, type: :time, read_only: true
23
+ property :start_time, type: :time, read_only: true
24
+ property :end_time, type: :time, read_only: true
25
+
26
+ property :entity, read_only: true
27
+ property :entity_ids, read_only: true
28
+ property :placement, read_only: true
29
+ property :granularity, read_only: true
30
+ property :metric_groups, read_only: true
31
+
12
32
  ANALYTICS_MAP = {
13
33
  'TwitterAds::Campaign' => Entity::CAMPAIGN,
14
34
  'TwitterAds::LineItem' => Entity::LINE_ITEM,
15
35
  'TwitterAds::OrganicTweet' => Entity::ORGANIC_TWEET,
16
- 'TwitterAds::Creative::PromotedAccount' => Entity::ACCOUNT,
36
+ 'TwitterAds::Creative::PromotedAccount' => Entity::PROMOTED_ACCOUNT,
17
37
  'TwitterAds::Creative::PromotedTweet' => Entity::PROMOTED_TWEET,
18
38
  'TwitterAds::Creative::MediaCreative' => Entity::MEDIA_CREATIVE
19
39
  }.freeze
@@ -25,42 +45,38 @@ module TwitterAds
25
45
  RESOURCE_ACTIVE_ENTITIES = "/#{TwitterAds::API_VERSION}/" +
26
46
  'stats/accounts/%{account_id}/active_entities' # @api private
27
47
 
28
- def self.included(klass)
29
- klass.send :include, InstanceMethods
30
- klass.extend ClassMethods
48
+ def initialize(account)
49
+ @account = account
50
+ self
31
51
  end
32
52
 
33
- module InstanceMethods
34
-
35
- # Pulls a list of metrics for the current object instance.
36
- #
37
- # @example
38
- # metric_groups = [:promoted_tweet_timeline_clicks, :promoted_tweet_search_clicks]
39
- # object.stats(metrics)
40
- #
41
- # @param metric_groups [Array] A collection of metric groups to fetch.
42
- # @param opts [Hash] An optional Hash of extended options.
43
- # @option opts [Time] :start_time The starting time to use (default: 7 days ago).
44
- # @option opts [Time] :end_time The end time to use (default: now).
45
- # @option opts [Symbol] :granularity The granularity to use (default: :hour).
46
- #
47
- # @return [Array] The collection of stats requested.
48
- #
49
- # @see https://dev.twitter.com/ads/analytics/metrics-and-segmentation
50
- # @since 1.0.0
51
- def stats(metric_groups, opts = {})
52
- self.class.stats(account, [id], metric_groups, opts)
53
- end
54
-
53
+ # Pulls a list of metrics for the current object instance.
54
+ #
55
+ # @example
56
+ # metric_groups = [MetricGroup::MOBILE_CONVERSION, MetricGroup::ENGAGEMENT]
57
+ # object.stats(metrics)
58
+ #
59
+ # @param metric_groups [Array] A collection of metric groups to fetch.
60
+ # @param opts [Hash] An optional Hash of extended options.
61
+ # @option opts [Time] :start_time The starting time to use (default: 7 days ago).
62
+ # @option opts [Time] :end_time The end time to use (default: now).
63
+ # @option opts [Symbol] :granularity The granularity to use (default: :hour).
64
+ #
65
+ # @return [Array] The collection of stats requested.
66
+ #
67
+ # @see https://dev.twitter.com/ads/analytics/metrics-and-segmentation
68
+ # @since 1.0.0
69
+ def stats(metric_groups, opts = {})
70
+ self.class.stats(account, [id], metric_groups, opts)
55
71
  end
56
72
 
57
- module ClassMethods
73
+ class << self
58
74
 
59
75
  # Pulls a list of metrics for a specified set of object IDs.
60
76
  #
61
77
  # @example
62
78
  # ids = ['7o4em', 'oc9ce', '1c5lji']
63
- # metric_groups = [MetricGroups.MOBILE_CONVERSION, MetricGroups.ENGAGEMENT]
79
+ # metric_groups = [MetricGroup::MOBILE_CONVERSION, MetricGroup::ENGAGEMENT]
64
80
  # object.stats(account, ids, metric_groups)
65
81
  #
66
82
  # @param account [Account] The Account object instance.
@@ -70,7 +86,7 @@ module TwitterAds
70
86
  # @option opts [Time] :start_time The starting time to use (default: 7 days ago).
71
87
  # @option opts [Time] :end_time The end time to use (default: now).
72
88
  # @option opts [Symbol] :granularity The granularity to use (default: :hour).
73
- # @option opts [Symbol] :placement The placement of entity (default: ALL_ON_TWITTER).
89
+ # @option opts [String] :placement The placement of entity (default: ALL_ON_TWITTER).
74
90
  #
75
91
  # @return [Array] The collection of stats requested.
76
92
  #
@@ -108,7 +124,7 @@ module TwitterAds
108
124
  #
109
125
  # @example
110
126
  # ids = ['7o4em', 'oc9ce', '1c5lji']
111
- # metric_groups = [MetricGroups.MOBILE_CONVERSION, MetricGroups.ENGAGEMENT]
127
+ # metric_groups = [MetricGroup::MOBILE_CONVERSION, MetricGroup::ENGAGEMENT]
112
128
  # object.create_async_job(account, ids, metric_groups)
113
129
  #
114
130
  # @param account [Account] The Account object instance.
@@ -118,16 +134,17 @@ module TwitterAds
118
134
  # @option opts [Time] :start_time The starting time to use (default: 7 days ago).
119
135
  # @option opts [Time] :end_time The end time to use (default: now).
120
136
  # @option opts [Symbol] :granularity The granularity to use (default: :hour).
121
- # @option opts [Symbol] :placement The placement of entity (default: ALL_ON_TWITTER).
122
- # @option opts [Symbol] :segmentation_type The segmentation type to use (default: none).
137
+ # @option opts [String] :placement The placement of entity (default: ALL_ON_TWITTER).
138
+ # @option opts [String] :segmentation_type The segmentation type to use (default: none).
123
139
  #
124
140
  # @return The response of creating job
125
141
  #
126
142
  # @see https://dev.twitter.com/ads/analytics/metrics-and-segmentation
127
- # @sync 1.0.0
143
+ # @since 1.0.0
128
144
 
129
145
  def create_async_job(account, ids, metric_groups, opts = {})
130
146
  # set default metric values
147
+ entity = opts.fetch(:entity, name)
131
148
  end_time = opts.fetch(:end_time, (Time.now - Time.now.sec - (60 * Time.now.min)))
132
149
  start_time = opts.fetch(:start_time, end_time - 604_800) # 7 days ago
133
150
  granularity = opts.fetch(:granularity, :hour)
@@ -143,7 +160,7 @@ module TwitterAds
143
160
  start_time: TwitterAds::Utils.to_time(start_time, granularity, start_utc_offset),
144
161
  end_time: TwitterAds::Utils.to_time(end_time, granularity, end_utc_offset),
145
162
  granularity: granularity.to_s.upcase,
146
- entity: ANALYTICS_MAP[name],
163
+ entity: ANALYTICS_MAP[entity],
147
164
  placement: placement,
148
165
  country: country,
149
166
  platform: platform
@@ -153,19 +170,18 @@ module TwitterAds
153
170
  params['entity_ids'] = ids.join(',')
154
171
 
155
172
  resource = self::RESOURCE_ASYNC_STATS % { account_id: account.id }
156
- puts 'my resource is ' + resource
157
173
  response = Request.new(account.client, :post, resource, params: params).perform
158
- response.body[:data]
174
+ TwitterAds::Analytics.new(account).from_response(response.body[:data], response.headers)
159
175
  end
160
176
 
161
177
  # Check async job status.
162
178
  # GET /#{TwitterAds::API_VERSION}/stats/jobs/accounts/:account_id
163
179
  #
164
180
  # @example
165
- # TwitterAds::LineItem.check_async_job_status(account, job_id: '1357343438724431305')
181
+ # TwitterAds::LineItem.check_async_job_status(account, job_ids: ['1357343438724431305'])
166
182
  #
167
183
  # @param account [Account] The Account object instance.
168
- # @option opts [String] :job_id The starting time to use (default: 7 days ago).
184
+ # @option opts [Array] :job_ids A collection of job IDs to fetch.
169
185
  #
170
186
  # @return A cursor of job statuses
171
187
 
@@ -173,18 +189,18 @@ module TwitterAds
173
189
  # set default values
174
190
  job_ids = opts.fetch(:job_ids, nil)
175
191
  params = {}
176
- params[:job_ids] = Array.wrap(job_ids).join(',') if job_ids
192
+ params[:job_ids] = job_ids.join(',') if job_ids
177
193
 
178
194
  resource = self::RESOURCE_ASYNC_STATS % { account_id: account.id }
179
195
  request = Request.new(account.client, :get, resource, params: params)
180
- Cursor.new(nil, request, init_with: [account])
196
+ Cursor.new(TwitterAds::Analytics, request, init_with: [account])
181
197
  end
182
198
 
183
199
  # Fetch async job data for a completed job.
184
200
  # Raises HTTP 404 exception, otherwise retries up to 5 times with exponential backoff.
185
201
  #
186
202
  # @example
187
- # response_data = TwitterAds::LineItem.fetch_async_job_data(account, file_url)
203
+ # response_data = TwitterAds::LineItem.fetch_async_job_data(account, data_url)
188
204
  #
189
205
  # @param data_url [String] The URL from the successful completion of an async job.
190
206
  #
@@ -209,21 +225,57 @@ module TwitterAds
209
225
  end
210
226
  end
211
227
 
228
+ # Retrieve details about which entities' analytics metrics
229
+ # have changed in a given time period.
230
+ #
231
+ # @example
232
+ # time = Time.now
233
+ # utc_offset = '+09:00'
234
+ # start_time = time - (60 * 60 * 24) # -1 day
235
+ # end_time = time
236
+ # active_entities = TwitterAds::LineItem.active_entities(
237
+ # account,
238
+ # line_item_ids: %w(exrfs),
239
+ # start_time: start_time,
240
+ # end_time: end_time,
241
+ # utc_offset: utc_offset,
242
+ # granularity: :day)
243
+ #
244
+ # @param account [Account] The Account object instance.
245
+ # @param entity [String] The entity type to retrieve data for.
246
+ # @param start_time [Time] Scopes the retrieved data to the specified start time.
247
+ # @param end_time [Time] Scopes the retrieved data to the specified end time.
248
+ # @option opts [Array] :campaign_ids A collection of IDs to be fetched.
249
+ # @option opts [Array] :funding_instrument_ids A collection of IDs to be fetched.
250
+ # @option opts [Array] :line_item_ids A collection of IDs to be fetched.
251
+ #
252
+ # @return A list of entity details.
253
+ #
254
+ # @see https://developer.twitter.com/en/docs/ads/analytics/api-reference/active-entities
255
+
212
256
  def active_entities(account, start_time:, end_time:, **opts)
213
- entity_type = name
257
+ entity = opts.fetch(:entity, name)
214
258
  granularity = opts.fetch(:granularity, nil)
215
259
  start_utc_offset = opts[:start_utc_offset] || opts[:utc_offset]
216
260
  end_utc_offset = opts[:end_utc_offset] || opts[:utc_offset]
217
261
 
218
- if entity_type == 'OrganicTweet'
219
- raise "'OrganicTweet' not support with 'active_entities'"
262
+ if entity == 'OrganicTweet'
263
+ raise "'OrganicTweet' is not supported with 'active_entities'"
220
264
  end
221
265
 
222
266
  params = {
223
- entity: ANALYTICS_MAP[entity_type],
267
+ entity: ANALYTICS_MAP[entity],
224
268
  start_time: TwitterAds::Utils.to_time(start_time, granularity, start_utc_offset),
225
269
  end_time: TwitterAds::Utils.to_time(end_time, granularity, end_utc_offset)
226
- }.merge!(opts)
270
+ }
271
+
272
+ opts.each { |k, v|
273
+ params[k] = if v.instance_of?(Array)
274
+ v.join(',')
275
+ else
276
+ v
277
+ end
278
+ }
227
279
 
228
280
  resource = self::RESOURCE_ACTIVE_ENTITIES % { account_id: account.id }
229
281
  response = Request.new(account.client, :get, resource, params: params).perform
@@ -20,7 +20,14 @@ module TwitterAds
20
20
  # @return [self] A fully hydrated instance of the current class.
21
21
  #
22
22
  # @since 0.1.0
23
- def from_response(object)
23
+ def from_response(object, headers = nil)
24
+ if !headers.nil?
25
+ TwitterAds::Utils.extract_response_headers(headers).each { |key, value|
26
+ singleton_class.class_eval { attr_accessor key }
27
+ instance_variable_set("@#{key}", value)
28
+ }
29
+ end
30
+
24
31
  self.class.properties.each do |name, type|
25
32
  value = nil
26
33
  if type == :time && object[name] && !object[name].empty?
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ # Copyright (C) 2019 Twitter, Inc.
3
+
4
+ module TwitterRestApi
5
+ class UserIdLookup
6
+ include TwitterAds::DSL
7
+ include TwitterAds::Resource
8
+
9
+ attr_reader :account
10
+
11
+ property :id, read_only: true
12
+ property :id_str, read_only: true
13
+ property :screen_name, read_only: true
14
+
15
+ DOMAIN = 'https://api.twitter.com'
16
+ RESOURCE = '/1.1/users/show.json'
17
+
18
+ def self.load(account, opts = {})
19
+ response = TwitterAds::Request.new(
20
+ account.client,
21
+ :get,
22
+ RESOURCE,
23
+ params: opts,
24
+ domain: DOMAIN
25
+ ).perform
26
+ new.from_response(response.body, response.headers)
27
+ end
28
+ end
29
+ end
@@ -41,12 +41,24 @@ module TwitterAds
41
41
  property :to_delete, type: :bool
42
42
 
43
43
  RESOURCE = "/#{TwitterAds::API_VERSION}/" \
44
- 'accounts/%{account_id}/user_settings/%{id}' # @api private
44
+ 'accounts/%{account_id}/tax_settings' # @api private
45
45
 
46
46
  def initialize(account)
47
47
  @account = account
48
48
  self
49
49
  end
50
50
 
51
+ def self.load(account)
52
+ resource = RESOURCE % { account_id: account.id }
53
+ response = Request.new(account.client, :get, resource).perform
54
+ new(account).from_response(response.body[:data])
55
+ end
56
+
57
+ def save
58
+ resource = RESOURCE % { account_id: account.id }
59
+ params = to_params
60
+ response = Request.new(account.client, :put, resource, params: params).perform
61
+ from_response(response.body[:data])
62
+ end
51
63
  end
52
64
  end