sift 2.0.0.0 → 2.1.0.0

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
  SHA1:
3
- metadata.gz: 5116181929ffbab6a17941eb341a852a7c4c75c6
4
- data.tar.gz: 4699c62cdadc3fd2f9123b929925abcf45cfb19b
3
+ metadata.gz: 48d2e914b4876ebfd0d5ec6d14c53149342e807c
4
+ data.tar.gz: d608b5808f89566020a2d115cc71f34232986f0f
5
5
  SHA512:
6
- metadata.gz: 5d228bd45520217fe3e06a0e2673e4a176b497ff83fc5f73940a6e656006b7725521597fb263d64cc133020162400562380b30aee1f29c9a3bde91ce080e03a3
7
- data.tar.gz: bce89304cbf0eb1c963e05c9ec76248ba5e06d9c0829df983735dfaf5828288faf8e73fd3c81a53876ee39081cf3695364a929775ffdb5665caa99fc4dc6f957
6
+ metadata.gz: 6933162c8d8e2f9e61ddb84128f7cc69dd32c00c1dec5f1413c256778718cfda82f59d47ba5d9ddd8783629f7444bc36b10c19966652358bd7499a889ea38a32
7
+ data.tar.gz: 8ff5d3999392b1a5308344d198f02af3bd70df507021175fff59de52941ec6ae50a03dc5bce8a17cbe8f95ee6d65db57f60e4d443b6dcbc83590586b6d8b3f6d
@@ -0,0 +1,225 @@
1
+ # Sift Science Ruby bindings [![Build Status](https://travis-ci.org/SiftScience/sift-ruby.png?branch=master)](https://travis-ci.org/SiftScience/sift-ruby)
2
+
3
+ ## Requirements
4
+
5
+ * Ruby 1.9.3 or above.
6
+
7
+
8
+ ## Installation
9
+
10
+ If you want to build the gem from source:
11
+
12
+ ```
13
+ $ gem build sift.gemspec
14
+ ```
15
+
16
+ Alternatively, you can install the gem from rubygems.org:
17
+
18
+ ```
19
+ gem install sift
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ### Creating a Client:
25
+
26
+ ```ruby
27
+ require "sift"
28
+
29
+ Sift.api_key = '<your_api_key_here>'
30
+ Sift.account_id = '<your_account_id_here>'
31
+ client = Sift::Client.new()
32
+
33
+ ##### OR
34
+
35
+ client = Sift::Cient.new(api_key: '<your_api_key_here>', account_id: '<your_account_id_here>'
36
+
37
+ ```
38
+
39
+ ### Sending a transaction event
40
+
41
+ ```ruby
42
+ event = "$transaction"
43
+
44
+ user_id = "23056" # User ID's may only contain a-z, A-Z, 0-9, =, ., -, _, +, @, :, &, ^, %, !, $
45
+
46
+ properties = {
47
+ "$user_id" => user_id,
48
+ "$user_email" => "buyer@gmail.com",
49
+ "$seller_user_id" => "2371",
50
+ "seller_user_email" => "seller@gmail.com",
51
+ "$transaction_id" => "573050",
52
+ "$payment_method" => {
53
+ "$payment_type" => "$credit_card",
54
+ "$payment_gateway" => "$braintree",
55
+ "$card_bin" => "542486",
56
+ "$card_last4" => "4444"
57
+ },
58
+ "$currency_code" => "USD",
59
+ "$amount" => 15230000,
60
+ }
61
+
62
+ response = client.track(event, properties)
63
+
64
+ response.ok? # returns true or false
65
+ response.body # API response body
66
+ response.http_status_code # HTTP response code, 200 is ok.
67
+ response.api_status # status field in the return body, Link to Error Codes
68
+ response.api_error_message # Error message associated with status Error Code
69
+
70
+ # Request a score for the user with user_id 23056
71
+ response = client.score(user_id)
72
+ ```
73
+
74
+ ## Decisions
75
+
76
+ To learn more about the decisions endpoint visit our [developer docs](https://siftscience.com/developers/docs/ruby/decisions-api/get-decisions).
77
+
78
+ ### List of Configured Decisions
79
+
80
+ Get a list of your decisions.
81
+
82
+ **Optional Params**
83
+ - `entity_type`: `:user` or `:order`
84
+ - `abuse_types`: `["payment_abuse", "content_abuse", "content_abuse",
85
+ "account_abuse", "legacy"]`
86
+
87
+ **Returns**
88
+
89
+ A `Response` object
90
+
91
+ **Example:**
92
+ ```ruby
93
+ # fetch a list of all your decisions
94
+ response = client.decisions({
95
+ entity_type: :user,
96
+ abuse_types: ["payment_abuse", "content_abuse"]
97
+ })
98
+
99
+ # Check that response is okay.
100
+ unless response.ok?
101
+ raise "Unable to fetch decisions #{response.api_error_message} " +
102
+ "#{response.api_error_description}"
103
+ end
104
+
105
+ # find a decisions with the id "block_bad_user"
106
+ user_decision = response.body["data"].find do |decision|
107
+ decision["id"] == "block_bad_user"
108
+ end
109
+
110
+ # Get the next page
111
+
112
+ if response.body["has_more"]
113
+ client.decisions(response.body)
114
+ end
115
+ ```
116
+
117
+
118
+ ### Apply a decision
119
+
120
+ Applies a decision to an entity. Visit our [developer docs](http://siftscience.com/developers/docs/ruby/decisions-api/apply-decision) for more information.
121
+
122
+ **Required Params:**
123
+ - `decision_id`, `source`, `user_id`
124
+
125
+ **Other Params**
126
+ - `order_id`: when applying a decision to an order, you must pass in the `order_id`
127
+ - `analyst`: when `source` is set to `manual_review`, this field *is required*
128
+
129
+ **Returns**
130
+ `Response` object.
131
+
132
+ **Examples:**
133
+ ```ruby
134
+ # apply decision to a user
135
+ response = client.apply_decision_to({
136
+ decision_id: "block_bad_user",
137
+ source: "manual_review",
138
+ analyst: "bob@your_company.com",
139
+ user_id: "john@example.com"
140
+ })
141
+
142
+ # apply decision to "bob@example.com"'s order
143
+ response = client.apply_decision_to({
144
+ decision_id: "block_bad_order",
145
+ source: "manual_review",
146
+ analyst: "bob@your_company.com",
147
+ user_id: "john@example.com",
148
+ order_id: "ORDER_1234"
149
+ })
150
+
151
+ # Make sure you handle the response after applying a decision:
152
+
153
+ if response.ok?
154
+ # do stuff
155
+ else
156
+ # Error message
157
+ response.api_error_message
158
+
159
+ # Summary of the error
160
+ response.api_error_description
161
+
162
+ # hash of errors:
163
+ # key: field in question
164
+ # value: description of the issue
165
+ response.api_error_issues
166
+ end
167
+ ```
168
+
169
+ ## Sending a Label
170
+
171
+ ```ruby
172
+ # Label the user with user_id 23056 as Bad with all optional fields
173
+ response = client.label(user_id, {
174
+ "$is_bad" => true,
175
+ "$abuse_type" => "payment_abuse",
176
+ "$description" => "Chargeback issued",
177
+ "$source" => "Manual Review",
178
+ "$analyst" => "analyst.name@your_domain.com"
179
+ })
180
+
181
+ # Get the status of a workflow run
182
+ response = client.get_workflow_status('my_run_id')
183
+
184
+ # Get the latest decisions for a user
185
+ response = client.get_user_decisions('example_user_id')
186
+
187
+ # Get the latest decisions for an order
188
+ response = client.get_order_decisions('example_order_id')
189
+ ```
190
+
191
+ ## Response Object
192
+
193
+ All requests to our apis will return a `Response` instance.
194
+
195
+ ### Public Methods:
196
+ - `ok?` returns `true` when the response is a `200`-`299`, `false` if it isn't
197
+ - `body` returns a hash representation of the json body returned.
198
+ - `api_error_message` returns a string describing the api error code.
199
+ - `api_error_description` a summary of the error that occured.
200
+ - `api_error_issues` a hash describing the items the error occured. The `key` is the item and the `value` is the description of the error.
201
+
202
+
203
+ ## Building
204
+
205
+ Building and publishing the gem is captured by the following steps:
206
+
207
+ ```ruby
208
+ $ gem build sift.gemspec
209
+ $ gem push sift-<current version>.gem
210
+
211
+ $ bundle
212
+ $ rake -T
213
+ $ rake build
214
+ $ rake install
215
+ $ rake release
216
+ ```
217
+
218
+
219
+ ## Testing
220
+
221
+ To run the various tests use the rake command as follows:
222
+
223
+ ```ruby
224
+ $ rake spec
225
+ ```
@@ -1,5 +1,5 @@
1
- require "sift/version"
2
- require "sift/client"
1
+ require_relative "./sift/version"
2
+ require_relative "./sift/client"
3
3
 
4
4
  module Sift
5
5
 
@@ -1,17 +1,22 @@
1
1
  require 'httparty'
2
2
  require 'multi_json'
3
3
 
4
+ require_relative "./client/decision"
5
+ require_relative "./error"
6
+
4
7
  module Sift
5
8
 
6
9
  # Represents the payload returned from a call through the track API
7
10
  #
8
11
  class Response
9
- attr_reader :body
10
- attr_reader :http_class
11
- attr_reader :http_status_code
12
- attr_reader :api_status
13
- attr_reader :api_error_message
14
- attr_reader :request
12
+ attr_reader :body,
13
+ :http_class,
14
+ :http_status_code,
15
+ :api_status,
16
+ :api_error_message,
17
+ :request,
18
+ :api_error_description,
19
+ :api_error_issues
15
20
 
16
21
  # Constructor
17
22
  #
@@ -31,7 +36,13 @@ module Sift
31
36
  @body = MultiJson.load(http_response) unless http_response.nil?
32
37
  @request = MultiJson.load(@body["request"].to_s) if @body["request"]
33
38
  @api_status = @body["status"].to_i if @body["status"]
34
- @api_error_message = @body["error_message"].to_s if @body["error_message"]
39
+ @api_error_message = @body["error_message"]
40
+
41
+ if @body["error"]
42
+ @api_error_message = @body["error"]
43
+ @api_error_description = @body["description"]
44
+ @api_error_issues = @body["issues"] || {}
45
+ end
35
46
  end
36
47
  end
37
48
 
@@ -69,12 +80,21 @@ module Sift
69
80
  # This class wraps accesses through the API
70
81
  #
71
82
  class Client
72
- API_ENDPOINT = 'https://api.siftscience.com'
73
- API3_ENDPOINT = 'https://api3.siftscience.com'
83
+ API_ENDPOINT = ENV["SIFT_RUBY_API_URL"] || 'https://api.siftscience.com'
84
+ API3_ENDPOINT = ENV["SIFT_RUBY_API3_URL"] || 'https://api3.siftscience.com'
74
85
 
75
86
  include HTTParty
76
87
  base_uri API_ENDPOINT
77
88
 
89
+ attr_reader :api_key, :account_id
90
+
91
+ def self.build_auth_header(api_key)
92
+ { "Authorization" => "Basic #{Base64.encode64(api_key)}" }
93
+ end
94
+
95
+ def self.user_agent
96
+ "sift-ruby/#{VERSION}"
97
+ end
78
98
 
79
99
  # Constructor
80
100
  #
@@ -115,10 +135,6 @@ module Sift
115
135
  raise("path must be a non-empty string") if !@path.is_a?(String) || @path.empty?
116
136
  end
117
137
 
118
- def api_key
119
- @api_key
120
- end
121
-
122
138
  def user_agent
123
139
  "SiftScience/v#{@version} sift-ruby/#{VERSION}"
124
140
  end
@@ -487,9 +503,36 @@ module Sift
487
503
  Response.new(response.body, response.code, response.response)
488
504
  end
489
505
 
506
+ def decisions(opts = {})
507
+ decision_instance.list(opts)
508
+ end
509
+
510
+ def decisions!(opts = {})
511
+ handle_response(decisions(opts))
512
+ end
513
+
514
+ def apply_decision(configs = {})
515
+ decision_instance.apply_to(configs)
516
+ end
517
+
518
+ def apply_decision!(configs = {})
519
+ handle_response(apply_decision(configs))
520
+ end
490
521
 
491
522
  private
492
523
 
524
+ def handle_response(response)
525
+ if response.ok?
526
+ response.body
527
+ else
528
+ raise ApiError.new(response.api_error_message, response)
529
+ end
530
+ end
531
+
532
+ def decision_instance
533
+ @decision_instance ||= Decision.new(api_key, account_id)
534
+ end
535
+
493
536
  def delete_nils(properties)
494
537
  properties.delete_if do |k, v|
495
538
  case v
@@ -0,0 +1,67 @@
1
+ require "cgi"
2
+ require "Base64"
3
+
4
+ require_relative "../router"
5
+ require_relative "../validate/decision"
6
+ require_relative "../utils/hash_getter"
7
+ require_relative "./decision/apply_to"
8
+
9
+ module Sift
10
+ class Client
11
+ class Decision
12
+ FILTER_PARAMS = %w{ limit entity_type abuse_types from }
13
+
14
+ attr_reader :account_id, :api_key
15
+
16
+ def initialize(api_key, account_id)
17
+ @account_id = account_id
18
+ @api_key = api_key
19
+ end
20
+
21
+ def list(options = {})
22
+ getter = Utils::HashGetter.new(options)
23
+
24
+ if path = getter.get(:next_ref)
25
+ request_next_page(path)
26
+ else
27
+ Router.get(index_path, {
28
+ query: build_query(getter),
29
+ headers: auth_header
30
+ })
31
+ end
32
+ end
33
+
34
+ def build_query(getter)
35
+ FILTER_PARAMS.inject({}) do |result, filter|
36
+ if value = getter.get(filter)
37
+ result[filter] = value.is_a?(Array) ? value.join(",") : value
38
+ end
39
+
40
+ result
41
+ end
42
+ end
43
+
44
+ def apply_to(configs = {})
45
+ getter = Utils::HashGetter.new(configs)
46
+ configs[:account_id] = account_id
47
+
48
+ ApplyTo.new(api_key, getter.get(:decision_id), configs).run
49
+ end
50
+
51
+ def index_path
52
+ "#{Client::API3_ENDPOINT}/v3/accounts/#{account_id}/decisions"
53
+ end
54
+
55
+ private
56
+
57
+ def request_next_page(path)
58
+ Router.get(path, headers: auth_header)
59
+ end
60
+
61
+ def auth_header
62
+ Client.build_auth_header(api_key)
63
+ end
64
+ end
65
+ end
66
+ end
67
+
@@ -0,0 +1,104 @@
1
+ require "multi_json"
2
+
3
+ require_relative "../../validate/decision"
4
+ require_relative "../../client"
5
+ require_relative "../../router"
6
+ require_relative "../../utils/hash_getter"
7
+
8
+ module Sift
9
+ class Client
10
+ class Decision
11
+ class ApplyTo
12
+ PROPERTIES = %w{ source analyst description order_id user_id account_id }
13
+
14
+ attr_reader :decision_id, :configs, :getter, :api_key
15
+
16
+ PROPERTIES.each do |attribute|
17
+ class_eval %{
18
+ def #{attribute}
19
+ getter.get(:#{attribute})
20
+ end
21
+ }
22
+ end
23
+
24
+ def initialize(api_key, decision_id, configs)
25
+ @api_key = api_key
26
+ @decision_id = decision_id
27
+ @configs = configs
28
+ @getter = Utils::HashGetter.new(configs)
29
+ end
30
+
31
+ def run
32
+ if errors.empty?
33
+ send_request
34
+ else
35
+ Response.new(
36
+ MultiJson.dump({
37
+ status: 55,
38
+ error_message: errors.join(", ")
39
+ }),
40
+ 400,
41
+ nil
42
+ )
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def send_request
49
+ Router.post(path, {
50
+ body: request_body,
51
+ headers: headers
52
+ })
53
+ end
54
+
55
+ def request_body
56
+ {
57
+ source: source,
58
+ description: description,
59
+ analyst: analyst,
60
+ decision_id: decision_id
61
+ }
62
+ end
63
+
64
+ def errors
65
+ validator = Validate::Decision.new(configs)
66
+
67
+ if applying_to_order?
68
+ validator.valid_order?
69
+ else
70
+ validator.valid_user?
71
+ end
72
+
73
+ validator.error_messages
74
+ end
75
+
76
+ def applying_to_order?
77
+ configs.has_key?("order_id") || configs.has_key?(:order_id)
78
+ end
79
+
80
+ def path
81
+ if applying_to_order?
82
+ "#{user_path}/orders/#{CGI.escape(order_id)}/decisions"
83
+ else
84
+ "#{user_path}/decisions"
85
+ end
86
+ end
87
+
88
+ def user_path
89
+ "#{base_path}/users/#{CGI.escape(user_id)}"
90
+ end
91
+
92
+ def base_path
93
+ "#{Client::API3_ENDPOINT}/v3/accounts/#{account_id}"
94
+ end
95
+
96
+ def headers
97
+ {
98
+ "Content-type" => "application/json"
99
+ }.merge(Client.build_auth_header(api_key))
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end