Skip to content

Commit fc87e9c

Browse files
committed
Merge pull request #5 from maxmind/rafl/new-outputs
Add new minFraud outputs
2 parents 8dc1b2e + 7522011 commit fc87e9c

File tree

8 files changed

+176
-58
lines changed

8 files changed

+176
-58
lines changed

HISTORY.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
History
44
-------
55

6+
* Added support for new minFraud Insights outputs. These are:
7+
* ``/credit_card/brand``
8+
* ``/credit_card/type``
9+
* ``/device/id``
10+
* ``/email/is_free``
11+
* ``/email/is_high_risk``
12+
* ``input`` on the ``Warning`` response model has been replaced with
13+
``input_pointer``. The latter is a JSON pointer to the input that
14+
caused the warning.
15+
616
0.2.0 (2015-08-10)
717
++++++++++++++++++
818

minfraud/models.py

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,56 @@ class Issuer(object):
247247
}
248248

249249

250+
@_inflate_to_namedtuple
251+
class Device(object):
252+
"""
253+
Information about the device associated with the IP address
254+
255+
.. attribute:: id
256+
257+
A UUID that MaxMind uses for the device associated with this IP address.
258+
Note that many devices cannot be uniquely identified because they are too
259+
common (for example, all iPhones of a given model and OS release). In
260+
these cases, the minFraud service will simply not return a UUID for that
261+
device.
262+
263+
:type: str
264+
265+
"""
266+
__slots__ = ()
267+
_fields = {
268+
'id': None
269+
}
270+
271+
272+
@_inflate_to_namedtuple
273+
class Email(object):
274+
"""
275+
Information about the email address passed in the request
276+
277+
.. attribute:: is_free
278+
279+
This field is true if MaxMind believes that this email is hosted by a
280+
free email provider such as Gmail or Yahoo! Mail.
281+
282+
:type: bool | None
283+
284+
.. attribute:: is_high_risk
285+
286+
This field is true if MaxMind believes that this email is likely to be
287+
used for fraud. Note that this is also factored into the overall
288+
`risk_score` in the response as well.
289+
290+
:type: bool | None
291+
292+
"""
293+
__slots__ = ()
294+
_fields = {
295+
'is_free': None,
296+
'is_high_risk': None
297+
}
298+
299+
250300
@_inflate_to_namedtuple
251301
class CreditCard(object):
252302
"""
@@ -263,6 +313,12 @@ class CreditCard(object):
263313
264314
:type: str | None
265315
316+
.. attribute:: brand
317+
318+
The card brand, such as "Visa", "Discover", "American Express", etc.
319+
320+
:type: str | None
321+
266322
.. attribute:: is_issued_in_billing_address_country
267323
268324
This property is true if the country of the billing address matches the
@@ -278,6 +334,14 @@ class CreditCard(object):
278334
279335
:type: bool | None
280336
337+
.. attribute:: type
338+
339+
The card's type. The valid values are "charge", "credit", and "debit".
340+
See Wikipedia for an explanation of the difference between charge and
341+
credit cards.
342+
343+
:type: str | None
344+
281345
.. attribute:: issuer
282346
283347
An object containing information about the credit card issuer.
@@ -289,8 +353,10 @@ class CreditCard(object):
289353
_fields = {
290354
'issuer': Issuer,
291355
'country': None,
356+
'brand': None,
292357
'is_issued_in_billing_address_country': None,
293-
'is_prepaid': None
358+
'is_prepaid': None,
359+
'type': None
294360
}
295361

296362

@@ -441,21 +507,20 @@ class ServiceWarning(object):
441507
442508
:type: str
443509
444-
.. attribute:: input
510+
.. attribute:: input_pointer
445511
446-
This is a tuple of keys representing the path to the input that
512+
This is a string representing the path to the input that
447513
the warning is associated with. For instance, if the warning was about
448-
the billing city, the tuple would be ``("billing", "city")``. The key is
449-
used for an object and the index number for an array.
514+
the billing city, the string would be ``"/billing/city"``.
450515
451-
:type: tuple[str|int]
516+
:type: str
452517
453518
"""
454519
__slots__ = ()
455520
_fields = {
456521
'code': None,
457522
'warning': None,
458-
'input': lambda x: tuple(x) if x else ()
523+
'input_pointer': None
459524
}
460525

461526

@@ -505,6 +570,20 @@ class Insights(object):
505570
506571
:type: CreditCard
507572
573+
.. attribute:: device
574+
575+
A :class:`.Device` object containing information about the device that
576+
MaxMind believes is associated with the IP address passed in the request.
577+
578+
:type: Device
579+
580+
.. attribute:: email
581+
582+
A :class:`.Email` object containing information about the email address
583+
passed in the request.
584+
585+
:type: Email
586+
508587
.. attribute:: ip_address
509588
510589
A :class:`.IPAddress` object containing GeoIP2 and
@@ -532,6 +611,8 @@ class Insights(object):
532611
'credits_remaining': None,
533612
'ip_address': IPAddress,
534613
'credit_card': CreditCard,
614+
'device': Device,
615+
'email': Email,
535616
'shipping_address': ShippingAddress,
536617
'billing_address': BillingAddress
537618
}

minfraud/webservice.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,8 @@ def _response_for(self, path, model_class, request, validate):
105105
uri,
106106
json=cleaned_request,
107107
auth=(self._user_id, self._license_key),
108-
headers=
109-
{'Accept': 'application/json',
110-
'User-Agent': self._user_agent()},
108+
headers={'Accept': 'application/json',
109+
'User-Agent': self._user_agent()},
111110
timeout=self._timeout)
112111
if response.status_code == 200:
113112
return self._handle_success(response, uri, model_class)

tests/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-

tests/data/insights-response.json

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,18 @@
103103
"phone_number": "8003421232",
104104
"matches_provided_phone_number": true
105105
},
106+
"brand": "Visa",
106107
"country": "US",
107108
"is_issued_in_billing_address_country": true,
108-
"is_prepaid": true
109+
"is_prepaid": true,
110+
"type": "credit"
111+
},
112+
"device": {
113+
"id": "7835b099-d385-4e5b-969e-7df26181d73b"
114+
},
115+
"email": {
116+
"is_free": true,
117+
"is_high_risk": true
109118
},
110119
"shipping_address": {
111120
"distance_to_billing_address": 2227,
@@ -119,19 +128,13 @@
119128
"warnings": [
120129
{
121130
"code": "INPUT_INVALID",
122-
"input": [
123-
"account",
124-
"user_id"
125-
],
131+
"input_pointer": "/account/user_id",
126132
"warning": "Encountered value at \/account\/user_id that does meet the required constraints"
127133
},
128134
{
129135
"code": "INPUT_INVALID",
130-
"input": [
131-
"account",
132-
"username_md5"
133-
],
136+
"input_pointer": "/account/username_md5",
134137
"warning": "Encountered value at \/account\/username_md5 that does meet the required constraints"
135138
}
136139
]
137-
}
140+
}

tests/data/score-response.json

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,13 @@
55
"warnings": [
66
{
77
"code": "INPUT_INVALID",
8-
"input": [
9-
"account",
10-
"user_id"
11-
],
8+
"input_pointer": "/account/user_id",
129
"warning": "Encountered value at \/account\/user_id that does meet the required constraints"
1310
},
1411
{
1512
"code": "INPUT_INVALID",
16-
"input": [
17-
"account",
18-
"username_md5"
19-
],
13+
"input_pointer": "/account/username_md5",
2014
"warning": "Encountered value at \/account\/username_md5 that does meet the required constraints"
2115
}
2216
]
23-
}
17+
}

tests/test_models.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@ def test_model_immutability(self):
1818
T = namedtuple('T', ['obj', 'attr'], {})
1919
models = [T(GeoIP2Country(), 'iso_code'),
2020
T(GeoIP2Location(), 'latitude'), T(Issuer(), 'name'),
21-
T(CreditCard(), 'country'), T(BillingAddress(), 'latitude'),
22-
T(ShippingAddress(), 'latitude'), T(
23-
ServiceWarning(), 'code'), T(Insights(), 'id'),
21+
T(CreditCard(), 'country'), T(Device(), 'id'),
22+
T(Email(), 'is_free'), T(BillingAddress(), 'latitude'),
23+
T(ShippingAddress(), 'latitude'),
24+
T(ServiceWarning(), 'code'), T(Insights(), 'id'),
2425
T(Score(), 'id'), T(IPAddress({}), 'city')]
2526
for model in models:
2627
for attr in (model.attr, 'does_not_exist'):
2728
with self.assertRaises(
28-
AttributeError,
29-
msg='{0!s} - {0}'.format(model.obj, attr)):
29+
AttributeError,
30+
msg='{0!s} - {0}'.format(model.obj, attr)):
3031
setattr(model.obj, attr, 5)
3132

3233
def test_billing_address(self):
@@ -63,15 +64,34 @@ def check_address(self, address):
6364
def test_credit_card(self):
6465
cc = CreditCard({
6566
'issuer': {'name': 'Bank'},
67+
'brand': 'Visa',
6668
'country': 'US',
6769
'is_issued_in_billing_address_country': True,
68-
'is_prepaid': True
70+
'is_prepaid': True,
71+
'type': 'credit'
6972
})
7073

7174
self.assertEqual('Bank', cc.issuer.name)
75+
self.assertEqual('Visa', cc.brand)
7276
self.assertEqual('US', cc.country)
7377
self.assertEqual(True, cc.is_prepaid)
7478
self.assertEqual(True, cc.is_issued_in_billing_address_country)
79+
self.assertEqual('credit', cc.type)
80+
81+
def test_device(self):
82+
id = 'b643d445-18b2-4b9d-bad4-c9c4366e402a'
83+
device = Device({'id': id})
84+
85+
self.assertEqual(id, device.id)
86+
87+
def test_email(self):
88+
email = Email({
89+
'is_free': True,
90+
'is_high_risk': False
91+
})
92+
93+
self.assertEqual(True, email.is_free)
94+
self.assertEqual(False, email.is_high_risk)
7595

7696
def test_geoip2_country(self):
7797
country = GeoIP2Country(is_high_risk=True, iso_code='US')
@@ -112,7 +132,13 @@ def test_insights(self):
112132
insights = Insights({
113133
'id': id,
114134
'ip_address': {'country': {'iso_code': 'US'}},
115-
'credit_card': {'is_prepaid': True},
135+
'credit_card': {
136+
'is_prepaid': True,
137+
'brand': 'Visa',
138+
'type': 'debit'
139+
},
140+
'device': {'id': id},
141+
'email': {'is_free': True},
116142
'shipping_address': {'is_in_ip_country': True},
117143
'billing_address': {'is_in_ip_country': True},
118144
'credits_remaining': 123,
@@ -122,6 +148,10 @@ def test_insights(self):
122148

123149
self.assertEqual('US', insights.ip_address.country.iso_code)
124150
self.assertEqual(True, insights.credit_card.is_prepaid)
151+
self.assertEqual('Visa', insights.credit_card.brand)
152+
self.assertEqual('debit', insights.credit_card.type)
153+
self.assertEqual(id, insights.device.id)
154+
self.assertEqual(True, insights.email.is_free)
125155
self.assertEqual(True, insights.shipping_address.is_in_ip_country)
126156
self.assertEqual(True, insights.billing_address.is_in_ip_country)
127157
self.assertEqual(id, insights.id)
@@ -167,8 +197,8 @@ def test_warning(self):
167197
warning = ServiceWarning(
168198
{'code': code,
169199
'warning': msg,
170-
'input': ["first", "second"]})
200+
'input_pointer': "/first/second"})
171201

172202
self.assertEqual(code, warning.code)
173203
self.assertEqual(msg, warning.warning)
174-
self.assertEqual(('first', 'second'), warning.input)
204+
self.assertEqual('/first/second', warning.input_pointer)

0 commit comments

Comments
 (0)