Repository: liberapay/liberapay.com

Issue:
Implement second KYC level
We may need this soon, it's required for "transactions worth more than €2,500 for cash-in and/or €1,000 for cash-out cumulated per legal year and per user". https://docs.mangopay.com/api-references/kyc-rules/



Discussion:
At the current pace we have less than 2 months before this becomes a real problem.

We now have a user whose balance is greater than €1000.

The new [MangoPay KYC documentation](https://docs.mangopay.com/guide/kyc) states that:

> A user will never be blocked due to going over their allowed KYC volumes when doing a payin - it is a transfer or payout that will be blocked

This is bad news for us, because we don't handle transfer failures (#238).

This would come in handy when a user is suspected of fraud, like https://github.com/liberapay/liberapay.org/issues/18, we could simply ask them to fill the extended identity form to unlock their account.


Resolving patch:
diff --git a/liberapay/constants.py b/liberapay/constants.py
--- a/liberapay/constants.py
+++ b/liberapay/constants.py
@@ -129,6 +129,18 @@ def check_bits(bits):
 )
 
 # https://docs.mangopay.com/api-references/kyc-rules/
+KYC_DOC_MAX_SIZE = 7000000
+KYC_DOC_MAX_SIZE_MB = int(KYC_DOC_MAX_SIZE / 1000000)
+KYC_DOCS_EXTS = ['pdf', 'jpeg', 'jpg', 'gif', 'png']
+KYC_DOCS_EXTS_STR = ', '.join(KYC_DOCS_EXTS)
+KYC_INCOME_THRESHOLDS = (
+    (1, 18000),
+    (2, 30000),
+    (3, 50000),
+    (4, 80000),
+    (5, 120000),
+    (6, 120000),
+)
 KYC_PAYIN_YEARLY_THRESHOLD = Decimal('2500')
 KYC_PAYOUT_YEARLY_THRESHOLD = Decimal('1000')
 
diff --git a/liberapay/utils/__init__.py b/liberapay/utils/__init__.py
--- a/liberapay/utils/__init__.py
+++ b/liberapay/utils/__init__.py
@@ -9,6 +9,7 @@
 import fnmatch
 from hashlib import sha256
 import hmac
+from operator import getitem
 import os
 import pickle
 import re
@@ -267,12 +268,13 @@ def include_svg(svg, height, width, x=None, y=None):
     return Markup(svg[:4] + attrs + svg[i:])
 
 
-def group_by(iterable, key):
+def group_by(iterable, key, attr=False, ignored_exceptions=KeyError):
+    f = getattr if attr else getitem
     r = {}
     for obj in iterable:
         try:
-            k = obj[key]
-        except KeyError:
+            k = f(obj, key)
+        except ignored_exceptions:
             continue
         r.setdefault(k, []).append(obj)
     return r


Test patch:
diff --git a/tests/py/fixtures/TestPages.yml b/tests/py/fixtures/TestPages.yml
--- a/tests/py/fixtures/TestPages.yml
+++ b/tests/py/fixtures/TestPages.yml
@@ -13,6 +13,23 @@ interactions:
       expires: ['-1']
       pragma: [no-cache]
     status: {code: 200, message: OK}
+- request:
+    body: null
+    headers: {}
+    method: GET
+    uri: https://api.sandbox.mangopay.com:443/v2.01/liberapay-dev/users/20717489/KYC/documents
+  response:
+    body: {string: !!python/unicode '[]'}
+    headers:
+      cache-control: [no-cache]
+      content-length: ['2']
+      content-type: [application/json; charset=utf-8]
+      expires: ['-1']
+      link: ['<https://api.sandbox.mangopay.com/v2.01/liberapay-dev/users/20717489/KYC/documents?page=1&per_page=10>;
+          rel="first",<https://api.sandbox.mangopay.com/v2.01/liberapay-dev/users/20717489/KYC/documents?page=1&per_page=10>;
+          rel="last"']
+      pragma: [no-cache]
+    status: {code: 200, message: OK}
 - request:
     body: null
     headers: {}
@@ -211,6 +228,23 @@ interactions:
       expires: ['-1']
       pragma: [no-cache]
     status: {code: 200, message: OK}
+- request:
+    body: null
+    headers: {}
+    method: GET
+    uri: https://api.sandbox.mangopay.com:443/v2.01/liberapay-dev/users/20717522/KYC/documents
+  response:
+    body: {string: !!python/unicode '[]'}
+    headers:
+      cache-control: [no-cache]
+      content-length: ['2']
+      content-type: [application/json; charset=utf-8]
+      expires: ['-1']
+      link: ['<https://api.sandbox.mangopay.com/v2.01/liberapay-dev/users/20717522/KYC/documents?page=1&per_page=10>;
+          rel="first",<https://api.sandbox.mangopay.com/v2.01/liberapay-dev/users/20717522/KYC/documents?page=1&per_page=10>;
+          rel="last"']
+      pragma: [no-cache]
+    status: {code: 200, message: OK}
 - request:
     body: null
     headers: {}
@@ -225,6 +259,23 @@ interactions:
       expires: ['-1']
       pragma: [no-cache]
     status: {code: 200, message: OK}
+- request:
+    body: null
+    headers: {}
+    method: GET
+    uri: https://api.sandbox.mangopay.com:443/v2.01/liberapay-dev/users/20717522/KYC/documents
+  response:
+    body: {string: !!python/unicode '[]'}
+    headers:
+      cache-control: [no-cache]
+      content-length: ['2']
+      content-type: [application/json; charset=utf-8]
+      expires: ['-1']
+      link: ['<https://api.sandbox.mangopay.com/v2.01/liberapay-dev/users/20717522/KYC/documents?page=1&per_page=10>;
+          rel="first",<https://api.sandbox.mangopay.com/v2.01/liberapay-dev/users/20717522/KYC/documents?page=1&per_page=10>;
+          rel="last"']
+      pragma: [no-cache]
+    status: {code: 200, message: OK}
 - request:
     body: null
     headers: {}
@@ -255,6 +306,23 @@ interactions:
       expires: ['-1']
       pragma: [no-cache]
     status: {code: 200, message: OK}
+- request:
+    body: null
+    headers: {}
+    method: GET
+    uri: https://api.sandbox.mangopay.com:443/v2.01/liberapay-dev/users/20717522/KYC/documents
+  response:
+    body: {string: !!python/unicode '[]'}
+    headers:
+      cache-control: [no-cache]
+      content-length: ['2']
+      content-type: [application/json; charset=utf-8]
+      expires: ['-1']
+      link: ['<https://api.sandbox.mangopay.com/v2.01/liberapay-dev/users/20717522/KYC/documents?page=1&per_page=10>;
+          rel="first",<https://api.sandbox.mangopay.com/v2.01/liberapay-dev/users/20717522/KYC/documents?page=1&per_page=10>;
+          rel="last"']
+      pragma: [no-cache]
+    status: {code: 200, message: OK}
 - request:
     body: '{"LegalRepresentativeBirthday": 790214400, "Tag": "/~4/", "PersonType":
       "NATURAL", "KYCLevel": "LIGHT", "LegalRepresentativeNationality": "IS", "LegalRepresentativeCountryOfResidence":
@@ -273,6 +341,23 @@ interactions:
       expires: ['-1']
       pragma: [no-cache]
     status: {code: 200, message: OK}
+- request:
+    body: null
+    headers: {}
+    method: GET
+    uri: https://api.sandbox.mangopay.com:443/v2.01/liberapay-dev/users/20717524/KYC/documents
+  response:
+    body: {string: !!python/unicode '[]'}
+    headers:
+      cache-control: [no-cache]
+      content-length: ['2']
+      content-type: [application/json; charset=utf-8]
+      expires: ['-1']
+      link: ['<https://api.sandbox.mangopay.com/v2.01/liberapay-dev/users/20717524/KYC/documents?page=1&per_page=10>;
+          rel="first",<https://api.sandbox.mangopay.com/v2.01/liberapay-dev/users/20717524/KYC/documents?page=1&per_page=10>;
+          rel="last"']
+      pragma: [no-cache]
+    status: {code: 200, message: OK}
 - request:
     body: null
     headers: {}
@@ -305,6 +390,23 @@ interactions:
       expires: ['-1']
       pragma: [no-cache]
     status: {code: 200, message: OK}
+- request:
+    body: null
+    headers: {}
+    method: GET
+    uri: https://api.sandbox.mangopay.com:443/v2.01/liberapay-dev/users/20717524/KYC/documents
+  response:
+    body: {string: !!python/unicode '[]'}
+    headers:
+      cache-control: [no-cache]
+      content-length: ['2']
+      content-type: [application/json; charset=utf-8]
+      expires: ['-1']
+      link: ['<https://api.sandbox.mangopay.com/v2.01/liberapay-dev/users/20717524/KYC/documents?page=1&per_page=10>;
+          rel="first",<https://api.sandbox.mangopay.com/v2.01/liberapay-dev/users/20717524/KYC/documents?page=1&per_page=10>;
+          rel="last"']
+      pragma: [no-cache]
+    status: {code: 200, message: OK}
 - request:
     body: null
     headers: {}
@@ -319,6 +421,23 @@ interactions:
       expires: ['-1']
       pragma: [no-cache]
     status: {code: 200, message: OK}
+- request:
+    body: null
+    headers: {}
+    method: GET
+    uri: https://api.sandbox.mangopay.com:443/v2.01/liberapay-dev/users/20717489/KYC/documents
+  response:
+    body: {string: !!python/unicode '[]'}
+    headers:
+      cache-control: [no-cache]
+      content-length: ['2']
+      content-type: [application/json; charset=utf-8]
+      expires: ['-1']
+      link: ['<https://api.sandbox.mangopay.com/v2.01/liberapay-dev/users/20717489/KYC/documents?page=1&per_page=10>;
+          rel="first",<https://api.sandbox.mangopay.com/v2.01/liberapay-dev/users/20717489/KYC/documents?page=1&per_page=10>;
+          rel="last"']
+      pragma: [no-cache]
+    status: {code: 200, message: OK}
 - request:
     body: null
     headers: {}