kount-ris 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,28 @@
1
+ module Kount
2
+ module RIS
3
+ class Location < Base
4
+ attr_accessor :ip_address, :country_code, :latitude, :longitude, :city, :region, :owner
5
+
6
+ validates :ip_address, length: { maximum: 16 }
7
+ validates :country_code, length: { maximum: 16 }
8
+ validates :latitude, length: { maximum: 16 }
9
+ validates :longitude, length: { maximum: 16 }
10
+ validates :city, length: { maximum: 255 }
11
+ validates :region, length: { maximum: 255 }
12
+ validates :owner, length: { maximum: 64 } # owner of IP address or address block
13
+
14
+ def to_h
15
+ {
16
+ ip_address: ip_address,
17
+ country_code: country_code,
18
+ latitude: latitude,
19
+ longitude: longitude,
20
+ city: city,
21
+ region: region,
22
+ owner: owner,
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,26 @@
1
+ module Kount
2
+ module RIS
3
+ class Persona < Base
4
+ attr_accessor :card_count, :device_count, :email_count, :country_code, :order_count, :active_order_count
5
+
6
+ validates :card_count, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true # number of cards kount has associated to persona
7
+ validates :device_count, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true # number of devices kount has associated to persona
8
+ validates :email_count, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true # number of emails associated to persona
9
+ validates :country_code, length: { maximum: 2 } # contry related to the persona
10
+ validates :order_count, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true # number of orders from persona
11
+ validates :active_order_count, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true # count orders from persona within the most active 6 hour window in last 14 days
12
+
13
+ def to_h
14
+ {
15
+ card_count: card_count,
16
+ device_count: device_count,
17
+ email_count: email_count,
18
+ country_code: country_code,
19
+ order_count: order_count,
20
+ active_order_count: active_order_count,
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,30 @@
1
+ module Kount
2
+ module RIS
3
+ class Product < Base
4
+ attr_accessor :description, :id, :price, :quantity, :category
5
+
6
+ validates :description, presence: true, length: { maximum: 256 } # product description
7
+ validates :id, presence: true, length: { maximum: 256 } # product identifier or SKU
8
+ validates :price, presence: true, numericality: { greater_than_or_equal_to: 0 } # product price
9
+ validates :quantity, presence: true, numericality: { greater_than: 0 } # quantity being purchased
10
+ validates :category, presence: true, length: { maximum: 256 } # the product category
11
+
12
+ def initialize(attrs)
13
+ attrs.symbolize_keys!
14
+ attrs.reverse_merge!({ quantity: 1 })
15
+ super(attrs)
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ description: description,
21
+ id: id,
22
+ price: price,
23
+ quantity: quantity,
24
+ category: category,
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
30
+
@@ -0,0 +1,389 @@
1
+ module Kount
2
+ module RIS
3
+ class Response < Base
4
+ attr_accessor :score, :reason_text, :merchant_id, :mode, :order_number, :session_id, :site_id,
5
+ :api_version, :kount_id, :counters, :rules, :warnings, :errors, :persona, :device, :proxy_location,
6
+ :pierced_location, :error_code
7
+
8
+ DECISIONS = {
9
+ 'a' => 'Approve',
10
+ 'd' => 'Decline',
11
+ 'r' => 'Review',
12
+ 'e' => 'Esclate',
13
+ }.freeze
14
+ CARD_TYPES = {
15
+ 'disc' => 'Discover',
16
+ 'visa' => 'Visa',
17
+ # # FILL
18
+ }.freeze
19
+ REASON_CODES = {
20
+ 'amnt' => 'Amount',
21
+ 'geox' => 'Persona related country with highest probability of fraud',
22
+ 'list' => 'VIP List (EMail, Card, Address, Gift Card, Device ID)',
23
+ 'netw' => 'Network Type',
24
+ 'scor' => 'Score',
25
+ 'velo' => 'Sales velocity in a fourteen day time frame',
26
+ 'vmax' => 'Sales velocity in a six hour time frame',
27
+ }.freeze
28
+ STATUSES = {
29
+ 'a' => 'Approve',
30
+ 'd' => 'Decline',
31
+ 'r' => 'Review',
32
+ 'e' => 'Esclate',
33
+ 'c' => 'Original transaction was approved but due to dynamic scoring the transaction now has an elevated score and may require reevaluation',
34
+ 'x' => 'Transaction was flagged for review but never acted upon and has timed out',
35
+ 'y' => 'Original transaction was approved but updated with AUTH=D and then timed out',
36
+ }.freeze
37
+
38
+ class << self
39
+ def decisions
40
+ DECISIONS
41
+ end
42
+
43
+ def card_types
44
+ CARD_TYPES
45
+ end
46
+
47
+ def reason_codes
48
+ REASON_CODES
49
+ end
50
+
51
+ def statuses
52
+ STATUSES
53
+ end
54
+ end
55
+
56
+ def decisions
57
+ self.class.decisions
58
+ end
59
+
60
+ def card_types
61
+ self.class.card_types
62
+ end
63
+
64
+ def reason_codes
65
+ self.class.reason_codes
66
+ end
67
+
68
+ def statuses
69
+ self.class.statuses
70
+ end
71
+
72
+ def decision=(value)
73
+ return @decision = nil if value.nil?
74
+
75
+
76
+ decision_value = decisions.detect { |k,v| k if v.downcase == value.to_s.downcase }&.first || value.to_s.downcase
77
+ raise ArgumentError, "'#{value}' is not a valid decision" unless decisions.include?(decision_value)
78
+ @decision = decision_value
79
+ end
80
+
81
+ def decision
82
+ decisions[@decision]
83
+ end
84
+
85
+ def card_type=(value)
86
+ return @card_type = nil if value.nil?
87
+
88
+ card_type_value = card_types.detect { |k,v| k if v.downcase == value.to_s.downcase }&.first || value.to_s.downcase
89
+ raise ArgumentError, "'#{value}' is not a valid card type" unless card_types.include?(card_type_value)
90
+ @card_type = card_type_value
91
+ end
92
+
93
+ def card_type
94
+ card_types[@card_type]
95
+ end
96
+
97
+ def reason_code=(value)
98
+ return @reason_code = nil if value.nil?
99
+
100
+ reason_code_value = reason_codes.detect { |k,v| k if v.downcase == value.to_s.downcase }&.first || value.to_s.downcase
101
+ raise ArgumentError, "'#{value}' is not a valid reason code" unless reason_codes.include?(reason_code_value)
102
+ @reason_code = reason_code_value
103
+ end
104
+
105
+ def reason_code
106
+ reason_codes[@reason_code]
107
+ end
108
+
109
+ def status=(value)
110
+ return @status = nil if value.nil?
111
+
112
+ status_value = statuses.detect { |k,v| k if v.downcase == value.to_s.downcase }&.first || value.to_s.downcase
113
+ raise ArgumentError, "'#{value}' is not a valid status" unless statuses.include?(status_value)
114
+ @status = status_value
115
+ end
116
+
117
+ def status
118
+ statuses[@status]
119
+ end
120
+
121
+ validates :score, numericality: { in: 0..100 } # score 0-99 ?
122
+ validates :reason_text, length: { maximum: 16 } # reason for score, custom text
123
+ validates :merchant_id, length: { maximum: 6 } # (matches inquiry)
124
+ validates :mode, length: { maximum: 1 } # (matches inquiry)
125
+ validates :order_number, length: { maximum: 32 } # transaction order number
126
+ validates :session_id, length: { maximum: 32 } # (matches inquiry)
127
+ validates :site_id, length: { maximum: 8 } # (matches inquiry)
128
+ validates :api_version, length: { maximum: 4 } # (matches inquiry)
129
+ validates :kount_id, length: { maximum: 12 } # Kount transaction ID number
130
+ validates :error_code, length: { is: 3 }, allow_nil: true # error code, also in errors
131
+
132
+ validates :decision, inclusion: { in: decisions.values }, allow_nil: true # decision from Count for transaction
133
+ validates :card_type, inclusion: { in: card_types.values }, allow_nil: true # credit card type
134
+ validates :reason_code, inclusion: { in: reason_codes.values }, allow_nil: true # reason for score, if 'None' use #reason_text
135
+ validates :status, inclusion: { in: statuses.values }, allow_nil: true # extended decision details -> STAT
136
+
137
+ validate :counters_are_valid # array of Trigger::Counter objects -> {count} COUNTERS_TRIGGERED, COUNTER_NAME_X, COUNTER_VALUE_X
138
+ validate :rules_are_valid # array of Trigger::Rule objects -> {count} RULES_TRIGGERED / RULE_DESCRIPTION_X / RULE_ID_X
139
+ validate :warnings_are_valid # array of Trigger::Warning objects -> {count} WARNING_COUNT / WARNING_N
140
+ validate :errors_are_valid # array of Trigger::Error objects -> {count} ERROR_COUNT / ERROR_N
141
+ validate :persona_is_valid # Persona object
142
+ validate :device_is_valid # Device object
143
+ validate :proxy_location_is_valid # Location object -> {ALSO} PROXY derived from #proxy_location
144
+ validate :pierced_location_is_valid # Location object
145
+
146
+ def initialize(attrs = {})
147
+ self.score = attrs['SCOR'].to_i if attrs.include?('SCOR')
148
+ self.reason_text = attrs['REASON_CODE']
149
+ self.merchant_id = attrs['MERC']
150
+ self.mode = attrs['MODE']
151
+ self.order_number = attrs['ORDER']
152
+ self.session_id = attrs['SESS']
153
+ self.site_id = attrs['SITE']
154
+ self.api_version = attrs['VERS']
155
+ self.kount_id = attrs['TRAN']
156
+ self.error_code = attrs['ERRO']
157
+
158
+ self.decision = attrs['AUTO']
159
+ self.card_type = attrs['BRND']
160
+ self.reason_code = attrs['REAS']
161
+ self.status = attrs['STAT']
162
+
163
+ device_attributes = {
164
+ browser: attrs['BROWSER'],
165
+ cookies_enabled: ((attrs['COOKIES'].downcase == 'y' ? true : false) if attrs.include?('COOKIES')),
166
+ first_seen_at: ((Date.strptime(attrs['DDFS'], '%Y-%m-%d') rescue nil) if attrs.include?('DDFS')),
167
+ layers: attrs['DEVICE_LAYERS'],
168
+ screen_resolution: attrs['DSR'],
169
+ fingerprint: attrs['FINGERPRINT'],
170
+ flash_enabled: ((attrs['FLASH'].downcase == 'y' ? true : false) if attrs.include?('FLASH')),
171
+ configured_country_code: attrs['HTTP_COUNTRY'],
172
+ javascript_enabled: ((attrs['JAVASCRIPT'].downcase == 'y' ? true : false) if attrs.include?('JAVASCRIPT')),
173
+ captured: ((attrs['KAPT'].downcase == 'y' ? true : false) if attrs.include?('KAPT')),
174
+ language_country_code: attrs['LANGUAGE'],
175
+ localtime: ((Date.strptime(attrs['LOCALTIME'], '%Y-%m-%d %H:%M:%S') rescue nil) if attrs.include?('LOCALTIME')),
176
+ using_forwarder: ((attrs['MOBILE_FORWARDER'].downcase == 'y' ? true : false) if attrs.include?('MOBILE_FORWARDER')),
177
+ mobile_type: attrs['MOBILE_TYPE'],
178
+ network_type: attrs['NETW'],
179
+ operating_system: attrs['OS'],
180
+ remote_access: ((attrs['PC_REMOTE'].downcase == 'y' ? true : false) if attrs.include?('PC_REMOTE')),
181
+ configured_region: attrs['REGN'],
182
+ timezone: attrs['TIMEZONE'],
183
+ user_agent: attrs['UAS'],
184
+ voice_enabled: ((attrs['VOICE_DEVICE'].downcase == 'y' ? true : false) if attrs.include?('VOICE_DEVICE')),
185
+ region: attrs['REGION'],
186
+ country_code: attrs['COUNTRY'],
187
+ }.compact
188
+ self.device = Device.new(device_attributes)
189
+
190
+ persona_attributes = {
191
+ card_count: attrs['CARDS'],
192
+ device_count: attrs['DEVICES'],
193
+ email_count: attrs['EMAILS'],
194
+ country_code: attrs['GEOX'],
195
+ order_count: attrs['VELO'],
196
+ active_order_count: attrs['VMAX'],
197
+ }.compact
198
+ self.persona = Persona.new(persona_attributes) if persona_attributes.any?
199
+
200
+ proxy_location_attributes = {
201
+ ip_address: attrs['IP_IPAD'],
202
+ country_code: attrs['IP_COUNTRY'],
203
+ latitude: attrs['IP_LAT'],
204
+ longitude: attrs['IP_LON'],
205
+ city: attrs['IP_CITY'],
206
+ region: attrs['IP_REGION'],
207
+ owner: attrs['IP_ORG'],
208
+ }.compact
209
+ self.proxy_location = Location.new(proxy_location_attributes) if proxy_location_attributes.any?
210
+
211
+ pierced_location_attributes = {
212
+ ip_address: attrs['PIP_IPAD'],
213
+ country_code: attrs['PIP_COUNTRY'],
214
+ latitude: attrs['PIP_LAT'],
215
+ longitude: attrs['PIP_LON'],
216
+ city: attrs['PIP_CITY'],
217
+ region: attrs['PIP_REGION'],
218
+ owner: attrs['PIP_ORG'],
219
+ }.compact
220
+ self.pierced_location = Location.new(pierced_location_attributes) if pierced_location_attributes.any?
221
+
222
+ self.counters = attrs['COUNTERS_TRIGGERED'].to_i.times.collect.with_index(1) do |_,i|
223
+ Trigger::Counter.new(
224
+ name: attrs["COUNTER_NAME_#{i}"],
225
+ count: attrs["COUNTER_VALUE_#{i}"].to_i,
226
+ )
227
+ end
228
+
229
+ self.errors = attrs['ERROR_COUNT'].to_i.times.collect.with_index(1) do |_,i|
230
+ Trigger::Error.new(
231
+ message: attrs["ERROR_#{i}"],
232
+ )
233
+ end
234
+
235
+ self.rules = attrs['RULES_TRIGGERED'].to_i.times.collect.with_index(1) do |_,i|
236
+ Trigger::Rule.new(
237
+ description: attrs["RULE_DESCRIPTION_#{i}"],
238
+ id: attrs["RULE_ID_#{i}"].to_i,
239
+ )
240
+ end
241
+
242
+ self.warnings = attrs['WARNING_COUNT'].to_i.times.collect.with_index(1) do |_,i|
243
+ Trigger::Warning.new(
244
+ message: attrs["WARNING_#{i}"],
245
+ )
246
+ end
247
+ end
248
+
249
+ def to_h
250
+ {
251
+ score: score,
252
+ reason_text: reason_text,
253
+ merchant_id: merchant_id,
254
+ mode: mode,
255
+ order_number: order_number,
256
+ session_id: session_id,
257
+ site_id: site_id
258
+ api_version: api_version,
259
+ kount_id: kount_id,
260
+ counters: counters.collect(&:to_h),
261
+ rules: rules.collect(&:to_h),
262
+ warnings: warnings.collect(&:to_h),
263
+ errors: errors.collect(&:to_h),
264
+ persona: persona.to_h,
265
+ device: device.to_h,
266
+ proxy_location: proxy_location.to_h,
267
+ pierced_location: pierced_location.to_h,
268
+ error_code: error_code,
269
+ }
270
+ end
271
+
272
+ private
273
+
274
+ def counters_are_valid
275
+ unless counters.is_a?(Array)
276
+ errors.add(:counters, 'must be an Array of Counter objects')
277
+ return false
278
+ end
279
+
280
+ counters.each.with_index(1) do |counter, i|
281
+ unless counter.valid?
282
+ counter.errors.full_messages.each do |error|
283
+ errors.add(:counters, "element #{i} #{error}")
284
+ end
285
+ end
286
+ end
287
+ end
288
+
289
+ def rules_are_valid
290
+ unless rules.is_a?(Array)
291
+ errors.add(:rules, 'must be an Array of Rule objects')
292
+ return false
293
+ end
294
+
295
+ rules.each.with_index(1) do |rule, i|
296
+ unless rule.valid?
297
+ rule.errors.full_messages.each do |error|
298
+ errors.add(:rules, "element #{i} #{error}")
299
+ end
300
+ end
301
+ end
302
+ end
303
+
304
+ def warnings_are_valid
305
+ unless warnings.is_a?(Array)
306
+ errors.add(:warnings, 'must be an Array of Warning objects')
307
+ return false
308
+ end
309
+
310
+ warnings.each.with_index(1) do |warning, i|
311
+ unless warning.valid?
312
+ warning.errors.full_messages.each do |error|
313
+ errors.add(:warnings, "element #{i} #{error}")
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ def errors_are_valid
320
+ unless errors.is_a?(Array)
321
+ errors.add(:errors, 'must be an Array of Error objects')
322
+ return false
323
+ end
324
+
325
+ errors.each.with_index(1) do |error, i|
326
+ unless error.valid?
327
+ error.errors.full_messages.each do |error|
328
+ errors.add(:errors, "element #{i} #{error}")
329
+ end
330
+ end
331
+ end
332
+ end
333
+
334
+ def persona_is_valid
335
+ unless persona.is_a?(Persona)
336
+ errors.add(:persona, 'must be an Persona object')
337
+ return false
338
+ end
339
+
340
+ unless persona.valid?
341
+ persona.errors.full_messages.each do |error|
342
+ errors.add(:persona, error)
343
+ end
344
+ end
345
+ end
346
+
347
+ def device_is_valid
348
+ unless device.is_a?(Device)
349
+ errors.add(:device, 'must be an Device object')
350
+ return false
351
+ end
352
+
353
+ unless device.valid?
354
+ device.errors.full_messages.each do |error|
355
+ errors.add(:device, error)
356
+ end
357
+ end
358
+ end
359
+
360
+ def proxy_location_is_valid
361
+ unless proxy_location.is_a?(Location)
362
+ errors.add(:proxy_location, 'must be an Location object')
363
+ return false
364
+ end
365
+
366
+ unless proxy_location.valid?
367
+ proxy_location.errors.full_messages.each do |error|
368
+ errors.add(:proxy_location, error)
369
+ end
370
+ end
371
+ end
372
+
373
+ def pierced_location_is_valid
374
+ unless pierced_location.is_a?(Location)
375
+ errors.add(:pierced_location, 'must be an Location object')
376
+ return false
377
+ end
378
+
379
+ unless pierced_location.valid?
380
+ pierced_location.errors.full_messages.each do |error|
381
+ errors.add(:pierced_location, error)
382
+ end
383
+ end
384
+ end
385
+
386
+ end
387
+ end
388
+ end
389
+