diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c61cfec --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.coverage +tests/__pycache__/ +veryfi/__pycache__/ diff --git a/NEWS.md b/NEWS.md index b905c80..232b1e3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,10 @@ CHANGES ======= +3.1.0 +----- +* Add support for operations with line items + 3.0.0 ----- * Return proper 404 and other errors diff --git a/requirements.txt b/requirements.txt index e000c3a..8d0276d 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -requests>=2.22.0 \ No newline at end of file +requests>=2.22.0 +pydantic==1.9.0 diff --git a/tests/test_line_items.py b/tests/test_line_items.py new file mode 100644 index 0000000..f3b4743 --- /dev/null +++ b/tests/test_line_items.py @@ -0,0 +1,91 @@ +import pytest +import responses + +from veryfi import * + + +@pytest.mark.parametrize("client_secret", [None, "s"]) +@responses.activate +def test_line_items(client_secret): + mock_doc_id = 1 + mock_line_item_id = 1 + mock_resp = { + "line_items": [ + { + "date": "", + "description": "foo", + "discount": 0.0, + "id": mock_line_item_id, + "order": 1, + "price": 0.0, + "quantity": 1.0, + "reference": "", + "sku": "", + "tax": 0.0, + "tax_rate": 0.0, + "total": 1.0, + "type": "food", + "unit_of_measure": "", + } + ], + } + client = Client(client_id="v", client_secret=client_secret, username="o", api_key="c") + responses.add( + responses.GET, + f"{client.versioned_url}/partner/documents/{mock_doc_id}/line-items/", + json=mock_resp, + status=200, + ) + assert client.get_line_items(mock_doc_id) == mock_resp + + responses.add( + responses.GET, + f"{client.versioned_url}/partner/documents/{mock_doc_id}/line-items/{mock_line_item_id}", + json=mock_resp["line_items"][0], + status=200, + ) + assert client.get_line_item(mock_doc_id, mock_line_item_id) == mock_resp["line_items"][0] + + mock_resp["line_items"][0]["description"] = "bar" + responses.add( + responses.PUT, + f"{client.versioned_url}/partner/documents/{mock_doc_id}/line-items/{mock_line_item_id}", + json=mock_resp["line_items"][0], + status=200, + ) + assert ( + client.update_line_item(mock_doc_id, mock_line_item_id, {"description": "foo"}) + == mock_resp["line_items"][0] + ) + + responses.add( + responses.DELETE, + f"{client.versioned_url}/partner/documents/{mock_doc_id}/line-items/{mock_line_item_id}", + json={}, + status=200, + ) + assert client.delete_line_item(mock_doc_id, mock_line_item_id) is None + + responses.add( + responses.DELETE, + f"{client.versioned_url}/partner/documents/{mock_doc_id}/line-items/", + json={}, + status=200, + ) + assert client.delete_line_items(mock_doc_id) is None + + responses.add( + responses.POST, + f"{client.versioned_url}/partner/documents/{mock_doc_id}/line-items/", + json=mock_resp["line_items"][0], + status=200, + ) + with pytest.raises(Exception): + client.add_line_item(mock_doc_id, {"order": 1}) + with pytest.raises(Exception): + client.add_line_item(mock_doc_id, {"order": 1, "description": "foo"}) + + assert ( + client.add_line_item(mock_doc_id, {"order": 1, "description": "foo", "total": 1.0}) + == mock_resp["line_items"][0] + ) diff --git a/veryfi/client.py b/veryfi/client.py index e830904..eb32a29 100644 --- a/veryfi/client.py +++ b/veryfi/client.py @@ -8,6 +8,7 @@ import requests +from veryfi.model import AddLineItem, UpdateLineItem from veryfi.errors import VeryfiClientError @@ -237,3 +238,69 @@ def update_document(self, document_id: int, **kwargs) -> Dict: endpoint_name = f"/documents/{document_id}/" return self._request("PUT", endpoint_name, kwargs) + + def get_line_items(self, document_id): + """ + Retrieve all line items for a document. + :param document_id: ID of the document you'd like to retrieve + :return: List of line items extracted from the document + """ + endpoint_name = f"/documents/{document_id}/line-items/" + request_arguments = {} + line_items = self._request("GET", endpoint_name, request_arguments) + return line_items + + def get_line_item(self, document_id, line_item_id): + """ + Retrieve a line item for existing document by ID. + :param document_id: ID of the document you'd like to retrieve + :param line_item_id: ID of the line item you'd like to retrieve + :return: Line item extracted from the document + """ + endpoint_name = f"/documents/{document_id}/line-items/{line_item_id}" + request_arguments = {} + line_items = self._request("GET", endpoint_name, request_arguments) + return line_items + + def add_line_item(self, document_id: int, payload: Dict) -> Dict: + """ + Add a new line item on an existing document. + :param document_id: ID of the document you'd like to update + :param payload: line item object to add + :return: Added line item data + """ + endpoint_name = f"/documents/{document_id}/line-items/" + request_arguments = AddLineItem(**payload).dict(exclude_none=True) + return self._request("POST", endpoint_name, request_arguments) + + def update_line_item(self, document_id: int, line_item_id: int, payload: Dict) -> Dict: + """ + Update an existing line item on an existing document. + :param document_id: ID of the document you'd like to update + :param line_item_id: ID of the line item you'd like to update + :param payload: line item object to update + + :return: Line item data with updated fields, if fields are writable. Otherwise line item data with unchanged fields. + """ + endpoint_name = f"/documents/{document_id}/line-items/{line_item_id}" + request_arguments = UpdateLineItem(**payload).dict(exclude_none=True) + return self._request("PUT", endpoint_name, request_arguments) + + def delete_line_items(self, document_id): + """ + Delete all line items on an existing document. + :param document_id: ID of the document you'd like to delete + """ + endpoint_name = f"/documents/{document_id}/line-items/" + request_arguments = {} + self._request("DELETE", endpoint_name, request_arguments) + + def delete_line_item(self, document_id, line_item_id): + """ + Delete an existing line item on an existing document. + :param document_id: ID of the document you'd like to delete + :param line_item_id: ID of the line item you'd like to delete + """ + endpoint_name = f"/documents/{document_id}/line-items/{line_item_id}" + request_arguments = {} + self._request("DELETE", endpoint_name, request_arguments) diff --git a/veryfi/errors.py b/veryfi/errors.py index e1340db..96a3510 100644 --- a/veryfi/errors.py +++ b/veryfi/errors.py @@ -32,6 +32,10 @@ class BadRequest(VeryfiClientError): pass +class ResourceNotFound(VeryfiClientError): + pass + + class UnexpectedHTTPMethod(VeryfiClientError): pass @@ -46,6 +50,7 @@ class InternalError(VeryfiClientError): _error_map = { 400: BadRequest, + 404: ResourceNotFound, 401: UnauthorizedAccessToken, 405: UnexpectedHTTPMethod, 409: AccessLimitReached, diff --git a/veryfi/model.py b/veryfi/model.py new file mode 100644 index 0000000..1049537 --- /dev/null +++ b/veryfi/model.py @@ -0,0 +1,31 @@ +from typing import Optional +from pydantic import BaseModel + + +class SharedLineItem(BaseModel): + sku: Optional[str] + category: Optional[str] + tax: Optional[float] + price: Optional[float] + unit_of_measure: Optional[str] + quantity: Optional[float] + upc: Optional[str] + tax_rate: Optional[float] + discount_rate: Optional[float] + start_date: Optional[str] + end_date: Optional[str] + hsn: Optional[str] + section: Optional[str] + weight: Optional[str] + + +class AddLineItem(SharedLineItem): + order: int + description: str + total: float + + +class UpdateLineItem(SharedLineItem): + order: Optional[int] + description: Optional[str] + total: Optional[float]