diff --git a/sofort/client.py b/sofort/client.py index 4b24c85..25e85c5 100644 --- a/sofort/client.py +++ b/sofort/client.py @@ -7,7 +7,7 @@ from sofort.internals import Config, as_list from sofort import model -from _version import __version__ +from sofort._version import __version__ API_URL = 'https://api.sofort.com/api/xml' TRANSACTION_ID = '-TRANSACTION-' @@ -71,6 +71,13 @@ def payment(self, amount, **kwargs): return self._request(sofort.xml.multipay(params), params) + def refunds(self, sender, refunds): + request_body = sofort.xml.refunds_by_params({ + 'sender': sender, + 'refunds': refunds + }) + return self._request(request_body) + def details(self, transaction_ids): request_body = sofort.xml.transaction_request_by_params({ 'transaction': as_list(transaction_ids) diff --git a/sofort/model.py b/sofort/model.py index d538d85..ace2745 100644 --- a/sofort/model.py +++ b/sofort/model.py @@ -14,11 +14,11 @@ def response(xmlstr): result = xmltodict.parse(xmlstr) # only one root element is allowed in XML - for root, value in result.iteritems(): + for root, value in result.items(): if value is None: return None factory = factories[root] - return factory(value) + return factory(value, strict=False) class ForcedListType(ListType): @@ -119,14 +119,33 @@ class TransactionDetailsModel(Model): ModelType(StatusHistoryItemModel)) -def transaction_list(transactions): - return [TransactionDetailsModel(transact) +class RefundModel(Model): + recipient = ModelType(BankAccountModel) + transaction = StringType() + amount = DecimalType() + comment = StringType() + reason_1 = StringType() + reason_2 = StringType() + time = Iso8601DateTimeType() + partial_refund_id = StringType() + status = StringType() + errors = SofortListType('error', ModelType(ErrorModel)) + +class RefundsModel(Model): + sender = ModelType(BankAccountModel) + title = StringType() + pain = StringType() + refund = ModelType(RefundModel) + + +def transaction_list(transactions, strict=False): + return [TransactionDetailsModel(transact, strict=strict) for transact in as_list(transactions['transaction_details'])] -def error_handler(data): - root = RootErrorsModel(data) +def error_handler(data, strict=False): + root = RootErrorsModel(data, strict=strict) errors = [RequestError(**error_item) for error_item in root.error] if root.su: errors.extend([RequestError(**error_item) @@ -139,4 +158,5 @@ def error_handler(data): 'errors': error_handler, 'transactions': transaction_list, 'new_transaction': NewTransactionModel, + 'refunds': RefundsModel, } diff --git a/sofort/xml.py b/sofort/xml.py index ff76d70..6820af4 100644 --- a/sofort/xml.py +++ b/sofort/xml.py @@ -59,7 +59,7 @@ def multipay(config): def transaction_request_by_params(params): root = etree.Element('transaction_request') root.set('version', '2') - for name, value in params.iteritems(): + for name, value in params.items(): if name == 'transaction': for transaction_id in value: etree.SubElement(root, 'transaction').text = transaction_id @@ -68,6 +68,27 @@ def transaction_request_by_params(params): return etree.tostring(root) +def refunds_by_params(params): + root = etree.Element('refunds') + root.set('version', '3') + + sender = params.pop('sender') + sender_xml = etree.SubElement(root, 'sender') + for name, value in sender.items(): + etree.SubElement(sender_xml, name).text = __serialize(value) + + refunds = params.pop('refunds') + for refund in refunds: + refund_xml = etree.SubElement(root, 'refund') + for name, value in refund.items(): + etree.SubElement(refund_xml, name).text = __serialize(value) + + for name, value in params.items(): + etree.SubElement(root, name).text = __serialize(value) + + return etree.tostring(root) + + def __compact_notification_addresses(addresses_): """ Input:: diff --git a/tests/test_sofort.py b/tests/test_sofort.py index 82d559b..2cc5a46 100644 --- a/tests/test_sofort.py +++ b/tests/test_sofort.py @@ -63,6 +63,23 @@ def test_pay(self): self.assertIsInstance(tran.payment_url, basestring) self.assertEqual('https://www.sofort.com/payment/go/136b2012718da0160fac20c2ec2f51100c90406e', tran.payment_url) + def test_refunds(self): + self.client._request_xml = MagicMock(return_value=REFUNDS_RESPONSE) + refunds_response = self.client.refunds(sender={ + 'holder': 'Max Samplemerchant', + 'iban': 'DE71700111109999999999', + 'bic': 'DEKTDE7GXXX' + }, refunds=[{ + 'transaction': '00000-00000-00000000-0000', + 'amount': '1.11', + 'comment': 'Order cancelled by user.', + 'reason_1': 'OrderID 123456', + 'reason_2': 'Refund' + }]) + self.assertEqual('Max Samplemerchant', refunds_response.sender.holder) + self.assertEqual('00000-00000-00000000-0000', refunds_response.refund.transaction) + self.assertEqual('accepted', refunds_response.refund.status) + def test_details_multiple_transaction_ids(self): self.client._request_xml = MagicMock(return_value=TRANSACTION_LIST_BY_IDS_RESPONSE) tran_id = [ @@ -479,6 +496,35 @@ def test_empty_response(self): """ +REFUNDS_RESPONSE = u""" + + + Max Samplemerchant + Demo Bank + DE71700111109999999999 + DEKTDE7GXXX + + Test Refund December 5, 2013 + [Base-64 encoded content for PAIN-file] + + + Max Mustermann + Demo Bank + DE06000000000023456789 + SFRTDE20XXX + + 00000-00000-00000000-0000 + 1.11 + Order cancelled by user. + OrderID 123456 + Refund + + fb1244caad + accepted + + +""" + NEST_ERRORS = """ diff --git a/tests/test_xml.py b/tests/test_xml.py index 586a806..44b1a8b 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -62,6 +62,28 @@ def test_transactions_request_by_params(self): sofort.xml.transaction_request_by_params(params) ) + def test_refunds_by_params(self): + from collections import OrderedDict + params = { + 'sender': { + 'holder': 'Max Samplemerchant', + 'iban': 'DE71700111109999999999', + 'bic': 'DEKTDE7GXXX' + }, + 'refunds': [{ + 'transaction': '00000-00000-00000000-0000', + 'amount': '1.11', + 'comment': 'Order cancelled by user.', + 'reason_1': 'OrderID 123456', + 'reason_2': 'Refund' + }] + } + + self.assertXmlEqual( + REFUNDS_SAMPLE, + sofort.xml.refunds_by_params(params) + ) + MULTIPAY_SAMPLE = """ @@ -96,3 +118,20 @@ def test_transactions_request_by_params(self): 10 """ + +REFUNDS_SAMPLE = """ + + + DE71700111109999999999 + Max Samplemerchant + DEKTDE7GXXX + + + Order cancelled by user. + OrderID 123456 + 1.11 + 00000-00000-00000000-0000 + Refund + + +"""