Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Document API #46

Merged
merged 4 commits into from
Feb 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 105 additions & 2 deletions omise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def iteritems(d, **kw):
'Collection',
'Customer',
'Dispute',
'Document',
'Event',
'Forex',
'Link',
Expand Down Expand Up @@ -72,6 +73,7 @@ def _get_class_for(type):
'charge': Charge,
'customer': Customer,
'dispute': Dispute,
'document': Document,
'event': Event,
'forex': Forex,
'link': Link,
Expand Down Expand Up @@ -206,6 +208,9 @@ class _MainResource(Base):
def _request(cls, *args, **kwargs):
return Request(api_secret, api_main, api_version).send(*args, **kwargs)

def _upload(cls, *args, **kwargs):
return Request(api_secret, api_main, api_version).send_file(*args, **kwargs)

def _nested_object_path(self, association_cls):
return (
self.__class__._collection_path(),
Expand Down Expand Up @@ -1111,7 +1116,7 @@ def _fetch_objects(self, **kwargs):


class Dispute(_MainResource, Base):
"""API class representing a recipient in an account.
"""API class representing a dispute in an account.

This API class is used for retrieving and updating a dispute in an
account for charge back handling.
Expand All @@ -1121,7 +1126,7 @@ class Dispute(_MainResource, Base):
>>> import omise
>>> omise.api_secret = 'skey_test_4xs8breq3htbkj03d2x'
>>> dispute = omise.Dispute.retrieve('dspt_test_4zgf15h89w8t775kcm8')
<Recipient id='dspt_test_4zgf15h89w8t775kcm8' at 0x7fd06ce3d5d0>
<Dispute id='dspt_test_4zgf15h89w8t775kcm8' at 0x7fd06ce3d5d0>
>>> dispute.status
'open'
"""
Expand Down Expand Up @@ -1178,6 +1183,13 @@ def list_closed_disputes(cls):
"""
return LazyCollection(cls._collection_path("closed"))

def list_documents(self):
"""Returns all documents that belong to a given dispute.

:rtype: LazyCollection
"""
return LazyCollection(self._nested_object_path(Document))

def reload(self):
"""Reload the dispute details.

Expand Down Expand Up @@ -1234,6 +1246,97 @@ def accept(self):
path = self._instance_path(self._attributes['id']) + ('accept',)
return self._reload_data(self._request('patch', path))

def upload_document(self, document):
"""Add a dispute evidence document.

See the `create a document`_ section in the API documentation for list
of available arguments.

:rtype: Document

.. _create a document: https://www.omise.co/documents-api#create
"""
path = self._instance_path(self._attributes['id']) + ('documents',)
document = _as_object(self._upload('post', path, files=document))
self.reload()
return document


class Document(_MainResource, Base):
"""API class representing a dispute document in an account.

This API class is used for managing dispute document files. Documents are
used to help resolve disputes. Supported file types include PNG, JPG, and
PDF.

Basic usage::

>>> import omise
>>> omise.api_secret = 'skey_test_4xs8breq3htbkj03d2x'
>>> dispute = omise.Dispute.retrieve('dspt_test_5mr4ox8e818viqtaqs1')
>>> document = dispute.documents.retrieve("docu_test_5mr4oyqphijal1ps9u6")
<Document id='docu_test_5mr4oyqphijal1ps9u6' at 0x7ffdbb90d410>
>>> document.filename
'evidence.png'
"""

@classmethod
def _collection_path(cls):
return 'documents'

@classmethod
def _instance_path(cls, dispute_id, document_id):
return ('disputes', dispute_id, 'documents', document_id)

@classmethod
def retrieve(cls, dispute_id, document_id):
"""Retrieve the document details for the given :param:`document_id`.

:param dispute_id: a dispute id of a document.
:type dispute_id: str
:param document_id: a document id to retrieve.
:type document_id: str
:rtype: Document
"""
return _as_object(cls._request('get', cls._instance_path(dispute_id, document_id)))

def reload(self):
"""Reload the document details.

:rtype: Document
"""
return self._reload_data(
self._request('get',
self._attributes['location']))

def destroy(self):
"""Delete the document and unassociated it from the dispute.

Basic usage::

>>> import omise
>>> omise.api_secret = 'skey_test_4xs8breq3htbkj03d2x'
>>> dispute = omise.Dispute.retrieve('dspt_test_5mr4ox8e818viqtaqs1')
>>> document = dispute.documents.retrieve("docu_test_5mr4oyqphijal1ps9u6")
>>> document.destroy()
<Document id='docu_test_5mr4oyqphijal1ps9u6' at 0x7ffdbb90d410>
>>> document.destroyed
True

:rtype: Document
"""
return self._reload_data(
self._request('delete',
self._attributes['location']))

@property
def destroyed(self):
"""Returns ``True`` if document has been deleted.

:rtype: bool
"""
return self._attributes.get('deleted', False)


class Event(_MainResource, Base):
"""API class representing an event in an account.
Expand Down
37 changes: 37 additions & 0 deletions omise/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,40 @@ def send(self, method, path, payload=None, headers=None):
errors._raise_from_data(response)
return response

def send_file(self, method, path, files=None, headers=None):
request_path = self._build_path(path)
request_files = self._build_files(files)
request_headers = self._build_file_header(headers)

logger.info('Sending HTTP request: %s %s', method.upper(), request_path)
logger.debug('Authorization: %s', self.api_key)
logger.debug('Files: %s', request_files)
logger.debug('Headers: %s', request_headers)

response = getattr(requests, method)(
request_path,
files=request_files,
headers=request_headers,
auth=(self.api_key, '')
).json()

logger.info('Received HTTP response: %s', response)

if response.get('object') == 'error':
errors._raise_from_data(response)
return response

def _build_path(self, path):
if not hasattr(path, '__iter__') or isinstance(path, basestring):
path = (path,)
path = map(str, path)
return urlparse.urljoin(self.api_base, '/'.join(path))

def _build_files(self, files):
if files is None:
files = {}
return files

def _build_payload(self, payload):
if payload is None:
payload = {}
Expand All @@ -122,3 +150,12 @@ def _build_headers(self, headers):
headers['Omise-Version'] = self.api_version
headers['User-Agent'] = 'OmisePython/%s' % version.__VERSION__
return headers

def _build_file_header(self, headers):
if headers is None:
headers = {}
headers['Accept'] = 'application/json'
if self.api_version is not None:
headers['Omise-Version'] = self.api_version
headers['User-Agent'] = 'OmisePython/%s' % version.__VERSION__
return headers
11 changes: 11 additions & 0 deletions omise/test/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ def assertRequest(self, api_call, url, data=None, headers=None):
headers=headers,
auth=(mock.ANY, ''))

def assertUpload(self, api_call, url, files=None, headers=None):
if files is None:
files = {}
if headers is None:
headers = mock.ANY
api_call.assert_called_with(
url,
files=files,
headers=headers,
auth=(mock.ANY, ''))


class _ResourceMixin(_RequestAssertable):

Expand Down
86 changes: 85 additions & 1 deletion omise/test/test_dispute.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import mock
import unittest
import tempfile

from .helper import _ResourceMixin

Expand Down Expand Up @@ -228,4 +229,87 @@ def test_accept(self, api_call):
self.assertRequest(
api_call,
'https://api.omise.co/disputes/dspt_test/accept'
)
)

@mock.patch('requests.get')
@mock.patch('requests.post')
def test_upload_document(self, api_call, reload_call):
dispute = self._makeOne()
class_ = self._getTargetClass()
self.mockResponse(api_call, """{
"object": "document",
"livemode": false,
"id": "docu_test",
"deleted": false,
"filename": "evidence.png",
"location": "/disputes/dspt_test/documents/docu_test",
"download_uri": null,
"created_at": "2021-02-05T10:40:32Z"
}""")

self.mockResponse(reload_call, """{
"object": "dispute",
"id": "dspt_test",
"livemode": false,
"location": "/disputes/dspt_test",
"currency": "THB",
"amount": 1101000,
"funding_amount": 1101000,
"funding_currency": "THB",
"metadata": {
},
"charge": "chrg_test_5m7wj8yi1pa9vlk9bq8",
"documents": {
"object": "list",
"data": [
{
"object": "document",
"livemode": false,
"id": "docu_test",
"deleted": false,
"filename": "evidence.png",
"location": "/disputes/dspt_test/documents/docu_test",
"download_uri": null,
"created_at": "2021-02-05T10:40:32Z"
}
],
"limit": 20,
"offset": 0,
"total": 1,
"location": "/disputes/dspt_test/documents",
"order": "chronological",
"from": "1970-01-01T00:00:00Z",
"to": "2021-02-05T10:42:02Z"
},
"transactions": [
{
"object": "transaction",
"id": "trxn_test",
"livemode": false,
"currency": "THB",
"amount": 1101000,
"location": "/transactions/trxn_test",
"direction": "debit",
"key": "dispute.started.debit",
"origin": "dspt_test",
"transferable_at": "2021-02-04T12:08:04Z",
"created_at": "2021-02-04T12:08:04Z"
}
],
"admin_message": null,
"message": null,
"reason_code": "goods_or_services_not_provided",
"reason_message": "Services not provided or Merchandise not received",
"status": "open",
"closed_at": null,
"created_at": "2021-02-04T12:08:04Z"
}""")

self.assertTrue(isinstance(dispute, class_))

files = tempfile.TemporaryFile()
document = dispute.upload_document(files)
files.close()
self.assertEqual(dispute.id, 'dspt_test')
self.assertEqual(document.filename, 'evidence.png')
self.assertUpload(api_call, 'https://api.omise.co/disputes/dspt_test/documents', files)
Loading