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 +4 -4
- data/README.md +225 -0
- data/lib/sift.rb +2 -2
- data/lib/sift/client.rb +56 -13
- data/lib/sift/client/decision.rb +67 -0
- data/lib/sift/client/decision/apply_to.rb +104 -0
- data/lib/sift/error.rb +13 -0
- data/lib/sift/router.rb +41 -0
- data/lib/sift/utils/hash_getter.rb +15 -0
- data/lib/sift/validate/decision.rb +53 -0
- data/lib/sift/validate/primitive.rb +43 -0
- data/lib/sift/version.rb +1 -1
- data/sift.gemspec +2 -1
- data/spec/fixtures/fake_responses.rb +16 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/unit/client/decision/apply_to_spec.rb +183 -0
- data/spec/unit/client/decision_spec.rb +83 -0
- data/spec/unit/client_203_spec.rb +2 -1
- data/spec/unit/client_label_spec.rb +2 -3
- data/spec/unit/client_spec.rb +3 -3
- data/spec/unit/router_spec.rb +37 -0
- data/spec/unit/validate/decision_spec.rb +85 -0
- data/spec/unit/validate/primitive_spec.rb +73 -0
- metadata +39 -8
- data/README.rdoc +0 -110
- data/spec/unit/sift_spec.rb +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 48d2e914b4876ebfd0d5ec6d14c53149342e807c
|
4
|
+
data.tar.gz: d608b5808f89566020a2d115cc71f34232986f0f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6933162c8d8e2f9e61ddb84128f7cc69dd32c00c1dec5f1413c256778718cfda82f59d47ba5d9ddd8783629f7444bc36b10c19966652358bd7499a889ea38a32
|
7
|
+
data.tar.gz: 8ff5d3999392b1a5308344d198f02af3bd70df507021175fff59de52941ec6ae50a03dc5bce8a17cbe8f95ee6d65db57f60e4d443b6dcbc83590586b6d8b3f6d
|
data/README.md
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
# Sift Science Ruby bindings [](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
|
+
```
|
data/lib/sift.rb
CHANGED
data/lib/sift/client.rb
CHANGED
@@ -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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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"]
|
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
|