Skip to content

Commit

Permalink
Merge pull request #256 from orbcorp/kgrover/request-id-idempotence
Browse files Browse the repository at this point in the history
Thread through a `request_id` for create, update, and delete requests
  • Loading branch information
ej2 authored Nov 30, 2021
2 parents 1eb04e7 + 53a1460 commit 2582c5b
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 38 deletions.
17 changes: 10 additions & 7 deletions quickbooks/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,15 @@ def change_data_capture(self, entity_string, changed_since):
return result

def make_request(self, request_type, url, request_body=None, content_type='application/json',
params=None, file_path=None):
params=None, file_path=None, request_id=None):
if not params:
params = {}

if self.minorversion:
params['minorversion'] = self.minorversion

if request_id:
params['requestid'] = request_id

if self.invoice_link:
params['include'] = 'invoiceLink'
Expand Down Expand Up @@ -297,11 +300,11 @@ def handle_exceptions(results):
else:
raise exceptions.QuickbooksException(message, code, detail)

def create_object(self, qbbo, request_body, _file_path=None):
def create_object(self, qbbo, request_body, _file_path=None, request_id=None):
self.isvalid_object_name(qbbo)

url = "{0}/company/{1}/{2}".format(self.api_url, self.company_id, qbbo.lower())
results = self.post(url, request_body, file_path=_file_path)
results = self.post(url, request_body, file_path=_file_path, request_id=request_id)

return results

Expand All @@ -317,15 +320,15 @@ def isvalid_object_name(self, object_name):

return True

def update_object(self, qbbo, request_body, _file_path=None):
def update_object(self, qbbo, request_body, _file_path=None, request_id=None):
url = "{0}/company/{1}/{2}".format(self.api_url, self.company_id, qbbo.lower())
result = self.post(url, request_body, file_path=_file_path)
result = self.post(url, request_body, file_path=_file_path, request_id=request_id)

return result

def delete_object(self, qbbo, request_body, _file_path=None):
def delete_object(self, qbbo, request_body, _file_path=None, request_id=None):
url = "{0}/company/{1}/{2}".format(self.api_url, self.company_id, qbbo.lower())
result = self.post(url, request_body, params={'operation': 'delete'}, file_path=_file_path)
result = self.post(url, request_body, params={'operation': 'delete'}, file_path=_file_path, request_id=request_id)

return result

Expand Down
14 changes: 7 additions & 7 deletions quickbooks/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,14 @@ class UpdateMixin(object):
qbo_object_name = ""
qbo_json_object_name = ""

def save(self, qb=None):
def save(self, qb=None, request_id=None):
if not qb:
qb = QuickBooks()

if self.Id and int(self.Id) > 0:
json_data = qb.update_object(self.qbo_object_name, self.to_json())
json_data = qb.update_object(self.qbo_object_name, self.to_json(), request_id=request_id)
else:
json_data = qb.create_object(self.qbo_object_name, self.to_json())
json_data = qb.create_object(self.qbo_object_name, self.to_json(), request_id=request_id)

if self.qbo_json_object_name != '':
obj = type(self).from_json(json_data[self.qbo_json_object_name])
Expand All @@ -170,19 +170,19 @@ class UpdateNoIdMixin(object):
qbo_object_name = ""
qbo_json_object_name = ""

def save(self, qb=None):
def save(self, qb=None, request_id=None):
if not qb:
qb = QuickBooks()

json_data = qb.update_object(self.qbo_object_name, self.to_json())
json_data = qb.update_object(self.qbo_object_name, self.to_json(), request_id=request_id)
obj = type(self).from_json(json_data[self.qbo_object_name])
return obj


class DeleteMixin(object):
qbo_object_name = ""

def delete(self, qb=None):
def delete(self, qb=None, request_id=None):
if not qb:
qb = QuickBooks()

Expand All @@ -193,7 +193,7 @@ def delete(self, qb=None):
'Id': self.Id,
'SyncToken': self.SyncToken,
}
return qb.delete_object(self.qbo_object_name, json.dumps(data))
return qb.delete_object(self.qbo_object_name, json.dumps(data), request_id=request_id)


class ListMixin(object):
Expand Down
41 changes: 19 additions & 22 deletions tests/integration/test_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
from quickbooks.objects.invoice import Invoice
from quickbooks.objects.item import Item
from tests.integration.test_base import QuickbooksTestCase

import uuid

class InvoiceTest(QuickbooksTestCase):
def test_create(self):

def create_invoice(self, customer, request_id=None):
invoice = Invoice()

line = SalesItemLine()
Expand All @@ -20,40 +21,36 @@ def test_create(self):
line.SalesItemLineDetail.ItemRef = item.to_ref()
invoice.Line.append(line)

customer = Customer.all(max_results=1, qb=self.qb_client)[0]
invoice.CustomerRef = customer.to_ref()

invoice.CustomerMemo = CustomerMemo()
invoice.CustomerMemo.value = "Customer Memo"
invoice.save(qb=self.qb_client)
invoice.save(qb=self.qb_client, request_id=request_id)
return invoice

def test_create(self):
customer = Customer.all(max_results=1, qb=self.qb_client)[0]
invoice = self.create_invoice(customer)
query_invoice = Invoice.get(invoice.Id, qb=self.qb_client)

self.assertEquals(query_invoice.CustomerRef.name, customer.DisplayName)
self.assertEquals(query_invoice.CustomerMemo.value, "Customer Memo")
self.assertEquals(query_invoice.Line[0].Description, "description")
self.assertEquals(query_invoice.Line[0].Amount, 100.0)

def test_create_idempotence(self):
customer = Customer.all(max_results=1, qb=self.qb_client)[0]
sample_request_id = str(uuid.uuid4())
invoice = self.create_invoice(customer, request_id=sample_request_id)
duplicate_invoice = self.create_invoice(customer, request_id=sample_request_id)

def test_delete(self):
# First create an invoice
invoice = Invoice()

line = SalesItemLine()
line.LineNum = 1
line.Description = "description"
line.Amount = 100
line.SalesItemLineDetail = SalesItemLineDetail()
item = Item.all(max_results=1, qb=self.qb_client)[0]

line.SalesItemLineDetail.ItemRef = item.to_ref()
invoice.Line.append(line)
# Assert that both returned invoices have the same id
self.assertEquals(invoice.Id, duplicate_invoice.Id)

def test_delete(self):
customer = Customer.all(max_results=1, qb=self.qb_client)[0]
invoice.CustomerRef = customer.to_ref()

invoice.CustomerMemo = CustomerMemo()
invoice.CustomerMemo.value = "Customer Memo"
invoice.save(qb=self.qb_client)
# First create an invoice
invoice = self.create_invoice(customer)

# Then delete
invoice_id = invoice.Id
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ def test_update_object(self, post):

self.assertTrue(post.called)

@patch('quickbooks.client.QuickBooks.make_request')
def test_update_object_with_request_id(self, make_req):
qb_client = client.QuickBooks()
qb_client.company_id = "1234"
qb_client.update_object("Customer", "request_body", request_id="123")

url = "https://sandbox-quickbooks.api.intuit.com/v3/company/1234/customer"
make_req.assert_called_with("POST", url, "request_body", file_path = None, request_id="123")

@patch('quickbooks.client.QuickBooks.get')
def test_get_current_user(self, get):
qb_client = client.QuickBooks()
Expand Down Expand Up @@ -163,6 +172,7 @@ def test_make_request(self, process_request):
"GET", url, data={},
headers={'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'python-quickbooks V3 library'}, params={})


def test_handle_exceptions(self):
qb_client = client.QuickBooks()
error_data = {
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ class UpdateMixinTest(QuickbooksUnitTestCase):
def test_save_create(self, create_object):
department = Department()
department.save(qb=self.qb_client)
create_object.assert_called_once_with("Department", department.to_json())
create_object.assert_called_once_with("Department", department.to_json(), request_id=None)

def test_save_create_with_qb(self):
with patch.object(self.qb_client, 'create_object') as create_object:
Expand All @@ -242,7 +242,7 @@ def test_save_update(self, update_object):
json = department.to_json()

department.save(qb=self.qb_client)
update_object.assert_called_once_with("Department", json)
update_object.assert_called_once_with("Department", json, request_id=None)

def test_save_update_with_qb(self):
with patch.object(self.qb_client, 'update_object') as update_object:
Expand Down

0 comments on commit 2582c5b

Please sign in to comment.