kount-ris 0.1.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.
@@ -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
+