Skip to content

Commit 140aee3

Browse files
authored
Add and Remove Authorized Users for a Customer (unit-finance#168)
* there is an infinite loop if max_retries set to 0 global _retries isn't passed properly to backoff_predicated, I've added a get function for this var * add and remove authorized users for customer added tests * max_retries must be greater than 0 * max_tries must be greater than 0 due to infinite loop of backoff library otherwise * typo
1 parent 0e727cc commit 140aee3

File tree

5 files changed

+158
-86
lines changed

5 files changed

+158
-86
lines changed

e2e_tests/customer_test.py

Lines changed: 77 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,48 @@
33
from unit import Unit
44
from unit.models.customer import *
55
from unit.models.codecs import DtoDecoder
6+
from e2e_tests.helpers.helpers import create_individual_customer
67

78
token = os.environ.get('TOKEN')
89
client = Unit("https://api.s.unit.sh", token)
910

11+
authorized_users = [
12+
{
13+
"fullName": {
14+
"first": "Jared",
15+
"last": "Dunn"
16+
},
17+
"email": "[email protected]",
18+
"phone": {
19+
"countryCode": "1",
20+
"number": "1555555590"
21+
}
22+
}
23+
]
24+
25+
1026
def get_customer_by_type(type: str):
11-
response = client.customers.list(ListCustomerParams(0,1000))
27+
response = client.customers.list(ListCustomerParams(0, 1000))
1228
for c in response.data:
1329
if c.type == type:
1430
return c
1531
return None
1632

33+
1734
def test_update_individual_customer():
1835
individual_customer_id = get_customer_by_type("individualCustomer").id
1936
request = PatchIndividualCustomerRequest(individual_customer_id, phone=Phone("1", "1115551111"))
2037
response = client.customers.update(request)
2138
assert response.data.type == "individualCustomer"
2239

40+
2341
def test_update_business_customer():
2442
business_customer_id = get_customer_by_type("businessCustomer").id
2543
request = PatchBusinessCustomerRequest(business_customer_id, phone=Phone("1", "1115551111"))
2644
response = client.customers.update(request)
2745
assert response.data.type == "businessCustomer"
2846

47+
2948
def test_get_customer():
3049
customer_ids = []
3150
response = client.customers.list()
@@ -36,6 +55,7 @@ def test_get_customer():
3655
response = client.customers.get(id)
3756
assert response.data.type == "individualCustomer" or response.data.type == "businessCustomer"
3857

58+
3959
def test_list_customers():
4060
response = client.customers.list()
4161
for customer in response.data:
@@ -44,91 +64,58 @@ def test_list_customers():
4464

4565
def test_business_customer():
4666
business_customer_api_response = {
47-
"type": "businessCustomer",
48-
"id": "1",
49-
"attributes": {
67+
"type": "businessCustomer",
68+
"id": "1",
69+
"attributes": {
5070
"createdAt": "2020-05-10T12:28:37.698Z",
5171
"name": "Pied Piper",
5272
"address": {
53-
"street": "5230 Newell Rd",
54-
"street2": None,
55-
"city": "Palo Alto",
56-
"state": "CA",
57-
"postalCode": "94303",
58-
"country": "US"
73+
"street": "5230 Newell Rd",
74+
"street2": None,
75+
"city": "Palo Alto",
76+
"state": "CA",
77+
"postalCode": "94303",
78+
"country": "US"
5979
},
6080
"phone": {
61-
"countryCode": "1",
62-
"number": "1555555578"
81+
"countryCode": "1",
82+
"number": "1555555578"
6383
},
6484
"stateOfIncorporation": "DE",
6585
"ein": "123456789",
6686
"entityType": "Corporation",
6787
"contact": {
68-
"fullName": {
69-
"first": "Richard",
70-
"last": "Hendricks"
71-
},
72-
"email": "[email protected]",
73-
"phone": {
74-
"countryCode": "1",
75-
"number": "1555555578"
76-
}
77-
},
78-
"authorizedUsers": [
79-
{
8088
"fullName": {
81-
"first": "Jared",
82-
"last": "Dunn"
89+
"first": "Richard",
90+
"last": "Hendricks"
8391
},
84-
"email": "jared@piedpiper.com",
92+
"email": "richard@piedpiper.com",
8593
"phone": {
86-
"countryCode": "1",
87-
"number": "1555555590"
94+
"countryCode": "1",
95+
"number": "1555555578"
8896
}
89-
},
90-
{
91-
"fullName": {
92-
"first": "Jared",
93-
"last": "Dunn"
94-
},
95-
"email": "[email protected]",
96-
"phone": {
97-
"countryCode": "1",
98-
"number": "1555555590"
99-
}
100-
},{
101-
"fullName": {
102-
"first": "Jared",
103-
"last": "Dunn"
104-
},
105-
"email": "[email protected]",
106-
"phone": {
107-
"countryCode": "1",
108-
"number": "1555555590"
109-
}
110-
}
111-
],
97+
},
98+
"authorizedUsers": authorized_users,
11299
"status": "Active",
113100
"tags": {
114-
"userId": "106a75e9-de77-4e25-9561-faffe59d7814"
101+
"userId": "106a75e9-de77-4e25-9561-faffe59d7814"
115102
}
116-
},
117-
"relationships": {
103+
},
104+
"relationships": {
118105
"org": {
119-
"data": {
120-
"type": "org",
121-
"id": "1"
122-
}
106+
"data": {
107+
"type": "org",
108+
"id": "1"
109+
}
123110
},
124111
"application": {
125-
"data": {
126-
"type": "businessApplication",
127-
"id": "1"
128-
}
112+
"data": {
113+
"type": "businessApplication",
114+
"id": "1"
115+
}
129116
}
130-
}
131117
}
118+
}
132119

133120
id = business_customer_api_response["id"]
134121
_type = business_customer_api_response["type"]
@@ -144,6 +131,7 @@ def test_business_customer():
144131
assert customer.attributes["authorizedUsers"][0].full_name.first == "Jared"
145132
assert customer.attributes["status"] == "Active"
146133

134+
147135
def test_individual_customer():
148136
individual_customer_api_response = {
149137
"type": "individualCustomer",
@@ -191,7 +179,6 @@ def test_individual_customer():
191179
}
192180
}
193181

194-
195182
id = individual_customer_api_response["id"]
196183
_type = individual_customer_api_response["type"]
197184

@@ -207,3 +194,28 @@ def test_individual_customer():
207194
assert customer.attributes["authorizedUsers"] == []
208195
assert customer.attributes["status"] == "Active"
209196

197+
198+
def add_authorized_users_to_individual_customer():
199+
individual_customer_id = create_individual_customer(client)
200+
request = AddAuthorizedUsersRequest(individual_customer_id, authorized_users)
201+
return client.customers.add_authorized_users(request)
202+
203+
204+
def test_add_authorized_users_to_individual_customer():
205+
response = add_authorized_users_to_individual_customer()
206+
assert response.data.type == "individualCustomer"
207+
assert len(response.data.attributes.get("authorizedUsers")) == 1
208+
assert response.data.attributes.get("authorizedUsers")[0].email == authorized_users[0].get("email")
209+
210+
211+
def test_remove_authorized_users_to_individual_customer():
212+
add_response = add_authorized_users_to_individual_customer()
213+
assert add_response.data.type == "individualCustomer"
214+
assert len(add_response.data.attributes.get("authorizedUsers")) == 1
215+
assert add_response.data.attributes.get("authorizedUsers")[0].email == authorized_users[0].get("email")
216+
authorized_users_emails = [authorized_users[0].get("email")]
217+
request = RemoveAuthorizedUsersRequest(add_response.data.id, authorized_users_emails)
218+
remove_response = client.customers.remove_authorized_users(request)
219+
assert remove_response.data.id == add_response.data.id
220+
assert remove_response.data.type == add_response.data.type
221+
assert len(remove_response.data.attributes.get("authorizedUsers")) == 0

unit/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929

3030
class Unit(object):
31-
def __init__(self, api_url, token, retries=0):
31+
def __init__(self, api_url, token, retries=1):
3232
self.applications = ApplicationResource(api_url, token, retries)
3333
self.customers = CustomerResource(api_url, token, retries)
3434
self.accounts = AccountResource(api_url, token, retries)

unit/api/base_resource.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
from typing import Optional, Dict
55
from unit.models.codecs import UnitEncoder
66

7-
retries = 0
7+
_retries = 1
8+
9+
10+
def get_max_retries():
11+
return _retries
812

913

1014
def backoff_idempotency_key_handler(e):
@@ -38,7 +42,7 @@ def idempotency_key_is_present(e):
3842

3943
class BaseResource(object):
4044
def __init__(self, api_url, token, retries_amount):
41-
global retries
45+
global _retries
4246

4347
self.api_url = api_url.rstrip("/")
4448
self.token = token
@@ -47,51 +51,51 @@ def __init__(self, api_url, token, retries_amount):
4751
"authorization": f"Bearer {self.token}",
4852
"user-agent": "unit-python-sdk"
4953
}
50-
retries = retries_amount
54+
# max_tries must be greater than 0 due to an infinite loop of backoff library otherwise
55+
_retries = retries_amount if retries_amount > 1 else 1
5156

5257
@backoff.on_predicate(backoff.expo,
5358
backoff_handler,
54-
max_tries=retries,
59+
max_tries=get_max_retries,
5560
jitter=backoff.random_jitter)
5661
def get(self, resource: str, params: Dict = None, headers: Optional[Dict[str, str]] = None):
5762
return requests.get(f"{self.api_url}/{resource}", params=params, headers=self.__merge_headers(headers))
5863

5964
@backoff.on_predicate(backoff.expo,
6065
backoff_handler,
61-
max_tries=retries,
66+
max_tries=get_max_retries,
6267
jitter=backoff.random_jitter)
6368
def post(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None):
6469
data = json.dumps(data, cls=UnitEncoder) if data is not None else None
65-
return requests.post(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers))\
66-
70+
return requests.post(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers))
6771

6872
@backoff.on_predicate(backoff.expo,
6973
backoff_idempotency_key_handler,
70-
max_tries=retries,
74+
max_tries=get_max_retries,
7175
jitter=backoff.random_jitter)
7276
def post_create(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None):
7377
data = json.dumps(data, cls=UnitEncoder) if data is not None else None
7478
return requests.post(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers))
7579

7680
@backoff.on_predicate(backoff.expo,
7781
backoff_handler,
78-
max_tries=retries,
82+
max_tries=get_max_retries,
7983
jitter=backoff.random_jitter)
8084
def patch(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None):
8185
data = json.dumps(data, cls=UnitEncoder) if data is not None else None
8286
return requests.patch(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers))
8387

8488
@backoff.on_predicate(backoff.expo,
8589
backoff_handler,
86-
max_tries=retries,
90+
max_tries=get_max_retries,
8791
jitter=backoff.random_jitter)
8892
def delete(self, resource: str, data: Dict = None, headers: Optional[Dict[str, str]] = None):
8993
data = json.dumps(data, cls=UnitEncoder) if data is not None else None
9094
return requests.delete(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers))
9195

9296
@backoff.on_predicate(backoff.expo,
9397
backoff_handler,
94-
max_tries=retries,
98+
max_tries=get_max_retries,
9599
jitter=backoff.random_jitter)
96100
def put(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None):
97101
return requests.put(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers))
@@ -106,4 +110,3 @@ def __merge_headers(self, headers: Optional[Dict[str, str]] = None):
106110

107111
def is_20x(self, status: int):
108112
return status == 200 or status == 201 or status == 204
109-

unit/api/customer_resource.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,19 @@ def __init__(self, api_url, token, retries):
88
super().__init__(api_url, token, retries)
99
self.resource = "customers"
1010

11-
def update(self, request: Union[PatchIndividualCustomerRequest, PatchBusinessCustomerRequest]) -> Union[UnitResponse[CustomerDTO], UnitError]:
11+
def update(self, request: PatchCustomerRequest) -> Union[UnitResponse[CustomerDTO], UnitError]:
1212
payload = request.to_json_api()
1313
response = super().patch(f"{self.resource}/{request.customer_id}", payload)
1414

15-
if response.ok:
15+
if super().is_20x(response.status_code):
1616
data = response.json().get("data")
17-
if data["type"] == "individualCustomer":
18-
return UnitResponse[IndividualCustomerDTO](DtoDecoder.decode(data), None)
19-
else:
20-
return UnitResponse[BusinessCustomerDTO](DtoDecoder.decode(data), None)
17+
return UnitResponse[CustomerDTO](DtoDecoder.decode(data), None)
2118
else:
2219
return UnitError.from_json_api(response.json())
2320

2421
def get(self, customer_id: str) -> Union[UnitResponse[CustomerDTO], UnitError]:
2522
response = super().get(f"{self.resource}/{customer_id}")
26-
if response.status_code == 200:
23+
if super().is_20x(response.status_code):
2724
data = response.json().get("data")
2825
return UnitResponse[CustomerDTO](DtoDecoder.decode(data), None)
2926
else:
@@ -32,7 +29,7 @@ def get(self, customer_id: str) -> Union[UnitResponse[CustomerDTO], UnitError]:
3229
def list(self, params: ListCustomerParams = None) -> Union[UnitResponse[List[CustomerDTO]], UnitError]:
3330
params = params or ListCustomerParams()
3431
response = super().get(self.resource, params.to_dict())
35-
if response.status_code == 200:
32+
if super().is_20x(response.status_code):
3633
data = response.json().get("data")
3734
return UnitResponse[CustomerDTO](DtoDecoder.decode(data), None)
3835
else:
@@ -46,3 +43,22 @@ def archive(self, request: ArchiveCustomerRequest) -> Union[UnitResponse[Custome
4643
return UnitResponse[CustomerDTO](DtoDecoder.decode(data), None)
4744
else:
4845
return UnitError.from_json_api(response.json())
46+
47+
def add_authorized_users(self, request: AddAuthorizedUsersRequest) -> Union[UnitResponse[CustomerDTO], UnitError]:
48+
payload = request.to_json_api()
49+
response = super().post(f"{self.resource}/{request.customer_id}/authorized-users", payload)
50+
if super().is_20x(response.status_code):
51+
data = response.json().get("data")
52+
return UnitResponse[CustomerDTO](DtoDecoder.decode(data), None)
53+
else:
54+
return UnitError.from_json_api(response.json())
55+
56+
def remove_authorized_users(self, request: RemoveAuthorizedUsersRequest) -> Union[UnitResponse[CustomerDTO], UnitError]:
57+
payload = request.to_json_api()
58+
response = super().delete(f"{self.resource}/{request.customer_id}/authorized-users", payload)
59+
if super().is_20x(response.status_code):
60+
data = response.json().get("data")
61+
return UnitResponse[CustomerDTO](DtoDecoder.decode(data), None)
62+
else:
63+
return UnitError.from_json_api(response.json())
64+

0 commit comments

Comments
 (0)