diff --git a/.gitignore b/.gitignore index e43b0f98..8c76fd2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .DS_Store +**/venv +**/VivaPython.egg-info \ No newline at end of file diff --git a/CreateOrder/Python/LICENSE.txt b/CreateOrder/Python/LICENSE.txt new file mode 100644 index 00000000..176139ad --- /dev/null +++ b/CreateOrder/Python/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Peter Perlepes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/CreateOrder/Python/README.md b/CreateOrder/Python/README.md new file mode 100644 index 00000000..bcb4f944 --- /dev/null +++ b/CreateOrder/Python/README.md @@ -0,0 +1,54 @@ +# VivaPayments Python API wrapper + +![N|Solid](https://www.vivawallet.com/App_Themes/VivaWallet/Resources/img/viva-logo.png) + +### Installation +```bash +python setup.py install +``` + +PyPI will be up soon :) + +### Usage +```sh +from VivaPayments import client +viva_client = client.Client('MERCHANT_ID', 'API_KEY') +#for production do client.Client('MERCHANT_ID', 'API_KEY', 'production') +``` +With viva_client you can call any model you require and the desired action. The tests folder can serve as an initial guideline for your API calls. +```sh +#example +order = viva_client.Order.Create(100) +order_code = order['result']['OrderCode'] +``` +### Models + +The Python wrapper currently supports the listed calls. + +* Card (CreateToken, CheckInstallments) +* Order(Create, Cancel, Get, Update) +* Source(Add) +* Transaction(Get, Create, CreateRecurring, Cance, OriginalCreditTransaction) +* Wallet(BalanceTransfer) + +For some of the calls you need special permissions from Viva so consult the wiki before using. + +### Testing + +```sh +python setup.py test +``` +For the tests to pass you need to set up the followiwng enviroment variables.: +* TEST_MERCHANT (Your demo merchant ID) +* TEST_KEY (Your demo API Key) +* WALLET_ID (Your Viva wallet ID) + +License +---- +MIT + + +### Documentation +Code is clearly documented inside the module at the moment but will be officially documented after the PyPI release. + +For more information about the API usage refer to [Viva wiki](https://github.com/VivaPayments/API/wiki). diff --git a/CreateOrder/Python/README.rst b/CreateOrder/Python/README.rst new file mode 100644 index 00000000..d8dc76ca --- /dev/null +++ b/CreateOrder/Python/README.rst @@ -0,0 +1,76 @@ +VivaPayments Python API wrapper +=============================== + +.. figure:: https://www.vivawallet.com/App_Themes/VivaWallet/Resources/img/viva-logo.png + :alt: N\|Solid + + N\|Solid + +Installation +~~~~~~~~~~~~ + +.. code:: bash + + python setup.py install + +PyPI will be up soon :) + +Usage +~~~~~ + +.. code:: sh + + from VivaPayments import client + viva_client = client.Client('MERCHANT_ID', 'API_KEY') + #for production do client.Client('MERCHANT_ID', 'API_KEY', 'production') + +With viva\_client you can call any model you require and the desired +action. The tests folder can serve as an initial guideline for your API +calls. + +.. code:: sh + + #example + order = viva_client.Order.Create(100) + order_code = order['result']['OrderCode'] + +Models +~~~~~~ + +The Python wrapper currently supports the listed calls. + +- Card (CreateToken, CheckInstallments) +- Order(Create, Cancel, Get, Update) +- Source(Add) +- Transaction(Get, Create, CreateRecurring, Cance, + OriginalCreditTransaction) +- Wallet(BalanceTransfer) + +For some of the calls you need special permissions from Viva so consult +the wiki before using. + +Testing +~~~~~~~ + +.. code:: sh + + python setup.py test + +For the tests to pass you need to set up the followiwng enviroment +variables.: \* TEST\_MERCHANT (Your demo merchant ID) \* TEST\_KEY (Your +demo API Key) \* WALLET\_ID (Your Viva wallet ID) + +License +------- + +MIT + +Documentation +~~~~~~~~~~~~~ + +Code is clearly documented inside the module at the moment but will be +officially documented after the PyPI release. + +For more information about the API usage refer to `Viva wiki`_. + +.. _Viva wiki: https://github.com/VivaPayments/API/wiki \ No newline at end of file diff --git a/CreateOrder/Python/VivaPayments/__init__.py b/CreateOrder/Python/VivaPayments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/CreateOrder/Python/VivaPayments/client.py b/CreateOrder/Python/VivaPayments/client.py new file mode 100644 index 00000000..de004b1a --- /dev/null +++ b/CreateOrder/Python/VivaPayments/client.py @@ -0,0 +1,144 @@ +import json +import logging +import urllib +from base64 import b64encode +from util.http import Urllib2Client +from util.optional import allowed_parameters +from models import * + +class Client(object): + """Client responsible for constructing the API calls. + + Example: + new_client = Client(merchant_id = 'TEST_ID', api_key = 'TEST_KEY') + new_client.Order.Create(100) + + """ + http_client = Urllib2Client() + + def __init__(self, merchant_id=None, api_key=None, enviroment=None): + """__init__ function for Client + + Args: + merchant_id (str): Defaults to None. The client's Merchant ID. + api_key (str): Defaults to None. The client's API Key. + enviroment (str): Defaults to None. The enviroment that + the client calls will be directed to. The production enviroment + will only be called if 'production' is passed. + + Attributes: + url (str): URL that will be used for the API calls. Depends on the + enviroment argument. + headers (str): Authentication header that is used in every request. + Transaction (object): Transaction class instance. + Order (object): Order class instance. + Source (object): Source class instance. + Card (object): Card class instance. + Wallet (object): Wallet class instance. + + Raises: + Exception: If the Client instance was initialized without Merchant ID or API Key. + """ + if not (api_key and merchant_id): + raise Exception('Client must be instantiated with Merchant ID and API Key.') + + self.api_key = api_key + self.merchant_id = merchant_id + self.url = 'https://www.vivapayments.com/api/' if enviroment in [ + 'production'] else 'http://demo.vivapayments.com/api/' + self.enviroment = 'demo' if 'demo' in self.url else 'production' + self.headers = 'Basic {}'.format( + b64encode(self.merchant_id + ':' + self.api_key)) + + self.Transaction = Transaction(self) + self.Order = Order(self) + self.Source = Source(self) + self.Card = Card(self) + self.Wallet = Wallet(self) + + def __repr__(self): + return 'Vivapayments {} Client object with api_key: {} , merchant_id: {} '.format(self.enviroment, self.api_key, self.merchant_id) + + def call_api(self, method, path, params=None, data=None, headers=None, optional_parameters = None): + """Function that formats the API request. + + The call_api function is used for every request done to API, it formats the request parameters, + request body and optional parameters if expected and optional headers passed. Finally uses the + http_client to fullfill the request. + + Args: + method (str): The HTTP method that will be used. + path (str): The path that the request is directed to. + params (dict): Defaults to None. URL parameters to be used in the API call. + data (dict): Defaults to None. Data to be used in the request's body if the + appropriate method is used. + headers (dict): Defaults to None. Optional headers that will be used along with + the Basic Auth for the API. + optional_parameters (dict): Defaults to None. Any optional parameter that could be + passed in the request's body. + + Raises: + Exception: HTTP method not supported by the API. + + """ + headers = headers if headers else {} + headers['Authorization'] = self.headers + + request_url = self.url + path + + if params: + request_url = request_url + '?' + urllib.urlencode(self._pack(params)) + + if data and method not in ['POST', 'PUT', 'PATCH', 'DELETE']: + raise Exception('Incorrect HTTP method for arguments: ' + data) + elif data: + headers['Content-type'] = 'application/json' + if optional_parameters: + self._check_allowed(optional_parameters) + data = json.dumps(self._pack(data, optional_parameters = optional_parameters)) + + return self.http_client.make_request(method, request_url, headers, post_data = data) + + + def _pack(self, data, optional_parameters = None): + """Function to merge the standard with optional params in the request. + + The _pack function is used to add the optional parameters along with the standard, in the + body of the request. It is also used to remove None values from body and URL params. + + Args: + data (dict): The standard data to be passed in the URL body. + optional_parameters (dict): Any optional parameters that the user added. + + Returns: + merged_data (dict): Dictionary stripped of the None valued keys, along with + any optional parameters merged. + + Example: + data = {'OrderCode':'1234', 'date': None} + optional_parameters = {'TransactionId': '5678'} + merged_data = _pack(data, optional_parameters) + '{'OrderCode':'1234', 'TransactionId': '5678'}' + + """ + data = dict((key,value) for key, value in data.iteritems() if value != None) + merged_data = data.copy() + if optional_parameters: merged_data.update(optional_parameters) + return merged_data + + def _check_allowed(self, optional_parameters): + """Function that checks the validity of the optional parameters passed. + + The _check_allowed function, checks if the optional parameteres that were + passed in the request, are valid based on the online documentation and warns + the user using the logging module. + + Args: + optional_parameters (dict): The optional parameters to be passed in the request. + + """ + for key in optional_parameters.keys(): + if key not in allowed_parameters.keys(): + logging.warn('Parameter {} is most likely not supported by the API.'.format(key)) + + diff --git a/CreateOrder/Python/VivaPayments/models/__init__.py b/CreateOrder/Python/VivaPayments/models/__init__.py new file mode 100644 index 00000000..70801906 --- /dev/null +++ b/CreateOrder/Python/VivaPayments/models/__init__.py @@ -0,0 +1,5 @@ +from order import Order +from source import Source +from transaction import Transaction +from card import Card +from wallet import Wallet \ No newline at end of file diff --git a/CreateOrder/Python/VivaPayments/models/card.py b/CreateOrder/Python/VivaPayments/models/card.py new file mode 100644 index 00000000..71e64f67 --- /dev/null +++ b/CreateOrder/Python/VivaPayments/models/card.py @@ -0,0 +1,50 @@ +from datetime import datetime + + +class Card(object): + + def __init__(self, config): + self.config = config + + def CreateToken(self, public_key, cc_id, cvc, expiration_date, cardholder_name): + """Function used to generate a credit card token. + + Args: + public_key (str): The user's public API key. + cc_id (int): The credit card number. + cvc (int): The credit card CVC/CVV. + expiration_date (str) or (datetime): The credit card's + expiration date. + cardholder_name: The cardholder name of the card. + + """ + if isinstance(expiration_date, datetime): + expiration_date = date.strftime('%Y-%m-%d') + elif expiration_date: + expiration_date = expiration_date[:10] + + params = { + 'key': public_key + } + headers = { + 'NativeCheckoutVersion': '230' + } + data = { + 'Number': cc_id, + 'CVC': cvc, + 'ExpirationDate': expiration_date, + 'CardHolderName': cardholder_name + } + return self.config.call_api('POST', 'cards', params=params, data=data) + + def CheckInstallments(self, cc_id): + """Function used to check the installments limit of a credit card. + + Args: + cc_id (int): The credit card number to be checked. + + """ + headers = { + 'CardNumber': cc_id + } + return self.config.call_api('GET', 'cards/installments', headers=headers) diff --git a/CreateOrder/Python/VivaPayments/models/order.py b/CreateOrder/Python/VivaPayments/models/order.py new file mode 100644 index 00000000..ea74707a --- /dev/null +++ b/CreateOrder/Python/VivaPayments/models/order.py @@ -0,0 +1,61 @@ + + +class Order(object): + """Class to handle order API calls""" + def __init__(self, config): + self.config = config + + def Create(self, amount, **kwargs): + """Function used to create a new order. + + Args: + amount (int): Credit amount for the order. + + """ + data = { + 'Amount': amount + } + return self.config.call_api('POST', 'orders', data = data, optional_parameters = kwargs) + + def Cancel(self, order_code, **kwargs): + """Function used to create a new order. + + Args: + order_code (str): Code of the order to be cancelled. + + """ + url = 'orders/{}'.format(order_code) + return self.config.call_api('DELETE', url, optional_parameters = kwargs) + + def Get(self, order_code): + """Function used to get a specific order and info. + + Args: + order_code (str): Code of the order to aquire info. + + """ + url = 'orders/{}'.format(order_code) + return self.config.call_api('GET', url) + + def Update(self, order_code, amount = None, is_canceled = None, disable_paid_state = None, expiration_date = None): + """Function used to update specific order. + + Args: + order_code (str): Code of the order to be updated. + amount (int): Defaults to None. The new amount of the order. + is_canceled (bool): Defaults to None. Changes + the canceled state of the order. + disabled_paid_state (bool): Defaults to None. Enables + multiple payments for an order + expiration_date (str): Defaults to None. If passed + changes the expiration date of the order. + + """ + url = 'orders/{}'.format(order_code) + data = { + 'Amount': amount, + 'IsCanceled': is_canceled, + 'DisablePaidState': disable_paid_state, + 'ExpirationDate': expiration_date + } + return self.config.call_api('PATCH', url, data = data) \ No newline at end of file diff --git a/CreateOrder/Python/VivaPayments/models/source.py b/CreateOrder/Python/VivaPayments/models/source.py new file mode 100644 index 00000000..1b0fee5c --- /dev/null +++ b/CreateOrder/Python/VivaPayments/models/source.py @@ -0,0 +1,30 @@ + + +class Source(object): + """Class to handle source API calls""" + def __init__(self, config): + self.config = config + + def Add(self, name, source_code, domain, is_secure, path_fail, path_sucess): + """Function used to add a new source for sales groups. + + Args: + name (str): The name to be used for the new source. + source_code (str): A unique code to be used for the new source. + domain (str): The primary domain for the new source. + is_secure (bool): Value indicating id the protocol is http or https. + path_fail (str): The relative path the client will end up after + a failed transaction. + path_success (str): The relative path the client will end up after + a successful transaction. + + """ + data = { + 'Name': name, + 'SourceCode': source_code, + 'Domain': domain, + 'IsSecure': is_secure, + 'PathFail': path_fail, + 'PathSuccess': path_sucess + } + return self.config.call_api('POST', 'Sources', data = data) diff --git a/CreateOrder/Python/VivaPayments/models/transaction.py b/CreateOrder/Python/VivaPayments/models/transaction.py new file mode 100644 index 00000000..ab043b1e --- /dev/null +++ b/CreateOrder/Python/VivaPayments/models/transaction.py @@ -0,0 +1,109 @@ +from datetime import datetime + + +class Transaction(object): + """Class to handle transaction API calls""" + + def __init__(self, config): + self.config = config + + def Get(self, transaction_id=None, date=None, order_code=None): + """Function to get transactions based on function arguments. + + Args: + transaction_id (str): Defaults to None. Passing transaction_id + will search for the specific transaction info. + date (str) or (datetime): Defaults to None. Passing date will + result in all the transactions made this date. Date can be + either string or datetime object. + order_code (str): Defaults to None. Passing order_code will + return all the transactions based on the order code. + + """ + if transaction_id: + url = 'transactions/{}'.format(transaction_id) + else: + url = 'transactions/' + + if isinstance(date, datetime): + date = date.strftime('%Y-%m-%d') + elif date: + date = date[:10] + + params = { + 'date': date, + 'ordercode': order_code + } + + return self.config.call_api('GET', url, params=params) + + def Create(self, amount, order_code = None, card_token = None, **kwargs): + """Function used to generate a transaction based on order code and token. + + Args: + amount (int): Transaction amount. + order_code (str): The order code that the transaction will be based on. + card_token (str): The credit card token created by passing the credit + card to the API. + + """ + url = 'transactions' + data = { + 'Amount': amount, + 'OrderCode': order_code, + 'CreditCard': { + 'Token': card_token + } + } + return self.config.call_api('POST', url, data = data, optional_parameters = kwargs) + + def CreateRecurring(self, transaction_id, amount, **kwargs): + """Function used to generate a recurring transaction based on specific transaction id. + + Args: + transaction_id (str): The first transaction_id that was marked as recurring. + amount (int): Transaction amount. + + """ + url = 'transactions/{}'.format(transaction_id) + data = { + 'Amount': amount + } + return self.config.call_api('POST', url, data = data, optional_parameters = kwargs) + + def Cancel(self, transaction_id, amount = None, action_user = None): + """Function used to cancel a transaction based on specific transaction id. + + Args: + transaction_id (str): The transaction_id that will be cancelled. + amount (int): Defaults to None. Transaction amount. + action_user (str): Defaults to None. The user that initiated this action, + could be used for logging purposes. + + """ + url = 'transactions/{}'.format(transaction_id) + params = { + 'amount': amount, + 'actionuser': action_user + } + return self.config.call_api('DELETE', url, params = params) + + def OriginalCreditTransaction(self, transaction_id, amount, **kwargs): + """Function used to directly pay an amount to a card(refer to the docs). + + Args: + transaction_id (str): The transaction that the original payment was done. + amount (int): Amount to be paid. + + """ + url = 'transactions/{}'.format(transaction_id) + params = { + 'Amount': amount, + 'ServiceId': 6 + } + return self.config.call_api('DELETE', url, params = params, optional_parameters = kwargs) + + + + + diff --git a/CreateOrder/Python/VivaPayments/models/wallet.py b/CreateOrder/Python/VivaPayments/models/wallet.py new file mode 100644 index 00000000..40f56a06 --- /dev/null +++ b/CreateOrder/Python/VivaPayments/models/wallet.py @@ -0,0 +1,33 @@ + + +class Wallet(object): + """Class to handle wallet API calls""" + + def __init__(self, config): + self.config = config + + def BalanceTransfer(self, wallet_id, amount, target_wallet=None, person_id=None, description=None): + """Function that is used to transfer wallet funds to another wallet or person. + + Args: + wallet_id (str): The wallet that the money originate from. + amount (int): The amount to be transfered. + target_wallet (str): Default to None. The target wallet that the money will be transfered to. + person_id (str): The person account Id that the money will be transfered to. + description (str): The reason for transfer. + + """ + if (target_wallet and person_id) or (not target_wallet and not person_id): + raise Exception( + 'Balance transfer requires either target wallet or target person ID.') + if person_id: + url = 'wallets/{}/balancetransfer?TargetPersonId={}'.format( + wallet_id, person_id) + elif target_wallet: + url = 'wallets/{}/balancetransfer/{}'.format( + wallet_id, target_wallet) + data = { + 'Amount': amount, + 'Description': description + } + return self.config.call_api('POST', url, data=data) diff --git a/CreateOrder/Python/VivaPayments/tests/__init__.py b/CreateOrder/Python/VivaPayments/tests/__init__.py new file mode 100644 index 00000000..ad5ecd9a --- /dev/null +++ b/CreateOrder/Python/VivaPayments/tests/__init__.py @@ -0,0 +1,6 @@ +import unittest + +def viva_test_suite(): + test_loader = unittest.TestLoader() + return test_loader.discover('.', pattern = 'test_*.py') + diff --git a/CreateOrder/Python/VivaPayments/tests/base.py b/CreateOrder/Python/VivaPayments/tests/base.py new file mode 100644 index 00000000..c7d55cae --- /dev/null +++ b/CreateOrder/Python/VivaPayments/tests/base.py @@ -0,0 +1,16 @@ +import os +import unittest +from VivaPayments import client + +TEST_MERCHANT = os.environ['TEST_MERCHANT'] +TEST_KEY = os.environ['TEST_KEY'] + +class VivaTestClient(unittest.TestCase): + """unittest.TestCase subclass used for the modules tests. + + This class provides the test client already initialized and all + the unittest.TestCase methods. + + """ + test_client = client.Client( TEST_MERCHANT, TEST_KEY) + \ No newline at end of file diff --git a/CreateOrder/Python/VivaPayments/tests/test_card.py b/CreateOrder/Python/VivaPayments/tests/test_card.py new file mode 100644 index 00000000..573585ff --- /dev/null +++ b/CreateOrder/Python/VivaPayments/tests/test_card.py @@ -0,0 +1,19 @@ +import os +import unittest +from base import * + + +class CardTestCase(VivaTestClient): + + @unittest.skip("Skipping CreateToken.") + def test_create_token(self): + ret = self.test_client.Card.CreateToken(os.environ['PUBLIC_KEY'], 4111111111111111, 123, '2019-12-12', 'Igneel64') + self.assertEqual(ret['status_code'], 200) + self.assertEqual(len(ret['result']['Token']), 352) + + @unittest.skip("Skipping check_installments.") + def test_check_installments(self): + ret = self.test_client.Card.CheckInstallments(4111111111111111) + result = ret['result'] + self.assertEqual(ret['status_code'], 200) + self.assertEqual(result.keys(), ['MaxInstallments']) \ No newline at end of file diff --git a/CreateOrder/Python/VivaPayments/tests/test_order.py b/CreateOrder/Python/VivaPayments/tests/test_order.py new file mode 100644 index 00000000..7e30ec1b --- /dev/null +++ b/CreateOrder/Python/VivaPayments/tests/test_order.py @@ -0,0 +1,46 @@ +import unittest +from base import * +from datetime import datetime + + +class OrderTestCase(VivaTestClient): + + @unittest.skip("Skipping create.") + def test_create(self): + ret = self.test_client.Order.Create(100) + result = ret['result'] + self.assertTrue(isinstance(result['TimeStamp'], datetime)) + self.assertTrue(result['OrderCode']) + self.assertEqual(ret['status_code'], 200) + self.assertEqual(result['ErrorCode'], 0) + self.assertIsNone(result['ErrorText']) + + @unittest.skip("Skipping get.") + def test_get(self): + order = self.test_client.Order.Create(100) + order_code = order['result']['OrderCode'] + ret = self.test_client.Order.Get(order_code) + result = ret['result'] + self.assertEqual(ret['status_code'], 200) + self.assertEqual(ret['result'].keys(), ['RequestAmount', 'MerchantTrns', 'OrderCode', + 'Tags', 'StateId', 'ExpirationDate', 'SourceCode', 'RequestLang', 'CustomerTrns']) + + @unittest.skip("Skipping cancel.") + def test_cancel(self): + order = self.test_client.Order.Create(100) + order_code = order['result']['OrderCode'] + ret = self.test_client.Order.Cancel(order_code) + result = ret['result'] + self.assertTrue(isinstance(result['TimeStamp'], datetime)) + self.assertEqual(result['OrderCode'], order_code) + self.assertEqual(ret['status_code'], 200) + self.assertEqual(result['ErrorCode'], 0) + self.assertIsNone(result['ErrorText']) + + @unittest.skip("Skipping update.") + def test_update(self): + order = self.test_client.Order.Create(100) + order_code = order['result']['OrderCode'] + ret = self.test_client.Order.Update(order_code, amount=40000) + self.assertEqual(ret['status_code'], 200) + self.assertIsNone(ret['result']) diff --git a/CreateOrder/Python/VivaPayments/tests/test_source.py b/CreateOrder/Python/VivaPayments/tests/test_source.py new file mode 100644 index 00000000..1a8cf9ba --- /dev/null +++ b/CreateOrder/Python/VivaPayments/tests/test_source.py @@ -0,0 +1,13 @@ +import unittest +from base import * + + +class SourceTestCase(VivaTestClient): + + @unittest.skip("Skipping add.") + def test_add(self): + ret = self.test_client.Source.Add( + 'Test Source', 'test1', 'mydomain.com', False, 'site/failure.py', 'site/success.py') + result = ret['result'] + self.assertEqual(ret['status_code'], 200) + self.assertIsNone(result['result']) diff --git a/CreateOrder/Python/VivaPayments/tests/test_transaction.py b/CreateOrder/Python/VivaPayments/tests/test_transaction.py new file mode 100644 index 00000000..eaea0eca --- /dev/null +++ b/CreateOrder/Python/VivaPayments/tests/test_transaction.py @@ -0,0 +1,137 @@ +import unittest +from base import * +from datetime import datetime + + +class TransactionTestCase(VivaTestClient): + + def setUp(self): + card = self.test_client.Card.CreateToken( + os.environ['PUBLIC_KEY'], 4111111111111111, 123, '2019-12-12', 'Igneel64') + self.card_token = card['result']['Token'] + + order = self.test_client.Order.Create(100) + self.order_code = order['result']['OrderCode'] + + def tearDown(self): + pass + + @unittest.skip("Skipping create.") + def test_create(self): + ret = self.test_client.Transaction.Create( + 1000, order_code=self.order_code) + result = ret['result'] + self.assertEqual(ret['status_code'], 200) + self.assertIsNone(result['Amount']) + self.assertEqual(result['ErrorCode'], 403) + self.assertIsNone(result['TransactionId']) + self.assertIsNone(result['AuthorizationId']) + self.assertTrue(isinstance(result['TimeStamp'], datetime)) + + ret = self.test_client.Transaction.Create( + 1000, order_code=self.order_code, card_token=self.card_token) + result = ret['result'] + self.assertEqual(ret['status_code'], 200) + self.assertEqual(result['Amount'], 10.0) + self.assertEqual(result['ErrorCode'], 0) + self.assertIsNotNone(result['TransactionId']) + self.assertIsNotNone(result['AuthorizationId']) + self.assertTrue(isinstance(result['TimeStamp'], datetime)) + + @unittest.skip("Skipping create_recurring.") + def test_create_recurring(self): + initial_transaction = self.test_client.Transaction.Create( + 1000, order_code=self.order_code, card_token=self.card_token, AllowsRecurring=True) + initial_transaction_id = initial_transaction['result']['TransactionId'] + + ret = self.test_client.Transaction.CreateRecurring( + initial_transaction_id, 1000) + result = ret['result'] + + self.assertEqual(ret['status_code'], 200) + self.assertEqual(result['Amount'], 10.0) + self.assertEqual(result['ErrorCode'], 0) + self.assertNotEqual(result['TransactionId'], initial_transaction_id) + self.assertIsNotNone(result['AuthorizationId']) + self.assertTrue(isinstance(result['TimeStamp'], datetime)) + + ret = self.test_client.Transaction.CreateRecurring( + initial_transaction_id, 2000) + result = ret['result'] + self.assertEqual(ret['status_code'], 200) + self.assertEqual(result['Amount'], 20.0) + self.assertEqual(result['ErrorCode'], 0) + self.assertNotEqual(result['TransactionId'], initial_transaction_id) + self.assertIsNotNone(result['AuthorizationId']) + self.assertTrue(isinstance(result['TimeStamp'], datetime)) + + @unittest.skip("Skipping get.") + def test_get(self): + ret = self.test_client.Transaction.Get(order_code=self.order_code) + result = ret['result'] + self.assertEqual(ret['status_code'], 200) + self.assertEqual(result['ErrorCode'], 0) + self.assertFalse(result['Transactions']) + self.assertIsNone(result['ErrorText']) + self.assertTrue(isinstance(result['TimeStamp'], datetime)) + + transaction = self.test_client.Transaction.Create( + 100, order_code=self.order_code, card_token=self.card_token) + transaction_id = transaction['result']['TransactionId'] + + ret = self.test_client.Transaction.Get(order_code=self.order_code) + result = ret['result'] + self.assertEqual(ret['status_code'], 200) + self.assertEqual(result['ErrorCode'], 0) + self.assertIsNotNone(result['Transactions']) + self.assertEqual(result['Transactions'][0]['Order'][ + 'OrderCode'], self.order_code) + + ret = self.test_client.Transaction.Get(date=datetime.now()) + self.assertEqual(ret['status_code'], 200) + self.assertEqual(result['ErrorCode'], 0) + self.assertIsNotNone(result['Transactions']) + + @unittest.skip("Skipping cancel.") + def test_cancel(self): + transaction = self.test_client.Transaction.Create( + 100, order_code=self.order_code, card_token=self.card_token) + transaction_id = transaction['result']['TransactionId'] + + ret = self.test_client.Transaction.Cancel(transaction_id) + result = ret['result'] + self.assertEqual(ret['status_code'], 200) + self.assertIsNotNone(result['ErrorText']) + self.assertEqual(result['ErrorCode'], 400) + self.assertIsNone(result['TransactionId']) + + ret = self.test_client.Transaction.Cancel(transaction_id, amount=10) + result = ret['result'] + self.assertEqual(ret['status_code'], 200) + self.assertIsNotNone(result['ErrorText']) + self.assertEqual(result['ErrorCode'], 400) + self.assertIsNone(result['TransactionId']) + + ret = self.test_client.Transaction.Cancel(transaction_id, amount=100) + result = ret['result'] + self.assertEqual(ret['status_code'], 200) + self.assertIsNone(result['ErrorText']) + self.assertEqual(result['ErrorCode'], 0) + + ret = self.test_client.Transaction.Cancel(transaction_id, amount=100) + result = ret['result'] + self.assertEqual(ret['status_code'], 200) + self.assertEqual(result['ErrorText'], 'Non reversible transaction') + self.assertEqual(result['ErrorCode'], 403) + + @unittest.skip("Skipping OCT.") + def test_original_credit_transaction(self): + transaction = self.test_client.Transaction.Create( + 100, order_code=self.order_code, card_token=self.card_token) + transaction_id = transaction['result']['TransactionId'] + ret = self.test_client.Transaction.OriginalCreditTransaction( + transaction_id, 10) + result = ret['result'] + self.assertEqual(ret['status_code'], 200) + self.assertTrue(isinstance(result['TimeStamp'], datetime)) + self.assertEqual(result['ErrorCode'], 403) diff --git a/CreateOrder/Python/VivaPayments/tests/test_wallet.py b/CreateOrder/Python/VivaPayments/tests/test_wallet.py new file mode 100644 index 00000000..26a979ba --- /dev/null +++ b/CreateOrder/Python/VivaPayments/tests/test_wallet.py @@ -0,0 +1,13 @@ +import os +import unittest +import urllib2 +from base import * + + +class TransactionTestCase(VivaTestClient): + + @unittest.skip("Skipping balance_transfer.") + def test_balance_transfer(self): + with self.assertRaises(urllib2.HTTPError): + self.test_client.Wallet.BalanceTransfer( + os.environ['WALLET_ID'], 1000, target_wallet='587200706057') \ No newline at end of file diff --git a/CreateOrder/Python/VivaPayments/util/__init__.py b/CreateOrder/Python/VivaPayments/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/CreateOrder/Python/VivaPayments/util/http.py b/CreateOrder/Python/VivaPayments/util/http.py new file mode 100644 index 00000000..384b19fd --- /dev/null +++ b/CreateOrder/Python/VivaPayments/util/http.py @@ -0,0 +1,80 @@ +import json +import urllib2 +import urllib +from datetime import datetime, timedelta + + +def _timestamp_toUTC(timestamp): + """Function to turn a timestamp to UTC datetime object. + + Arguments: + timestamp (str): Timestamp returned from the API response. + + Attributes: + offset (str): The last six characters representing the offset. + sign (str): The offset sign (+ or -). + hours (int): The hours that need to be converted. + extra (int): The second part of the offset to be converted. + transaction_date (datetime): The datetime object from the timestamp. + + Returns: + Unaware Datetime object, in UTC time. + """ + + offset = timestamp[-6:] + sign = offset[0] + hours = int(offset.split(':')[0][1:]) + extra = int(offset.split(':')[1]) % 60.0 + hours + transaction_date = datetime.strptime(timestamp[:-7], '%Y-%m-%dT%H:%M:%S.%f') + return transaction_date - timedelta(hours=extra) if sign == '+' else transaction_date + timedelta(hours=extra) + + +class Urllib2Client(object): + + def make_request(self, method, url, headers, post_data=None): + """Function that makes the API requests using the urllib2 module. + + Arguments: + method (str): Request method to be used. + url (str): The url the request will be sent to. + headers (str): The headers to be included in the request. + post_data (dict): The data to be passed in the URL's body. + + Returns: + { + 'result': result + 'status_code': status_code + } + result (dict): Dictionary with the API response data. + status_code (int): Integer with the status code returned from the API. + + Raises: + urllib2.HTTPError: Generic urllib2 error handler. + """ + + request = urllib2.Request(url, post_data, headers) + + if method not in ('GET', 'POST'): + request.get_method = lambda: method.upper() + + try: + response = urllib2.urlopen(request) + rbody = response.read() + if hasattr(rbody, 'decode'): + rbody = rbody.decode('utf-8') + rcode = response.code + except urllib2.HTTPError, e: + raise urllib2.HTTPError(e.url, e.code, e.msg, e.hdrs, e.fp) + + try: + result = json.loads(rbody) + except ValueError: + result = rbody + + if 'TimeStamp' in (result or {}): + result['TimeStamp'] = _timestamp_toUTC(result['TimeStamp']) + + return { + 'result': result, + 'status_code': rcode + } diff --git a/CreateOrder/Python/VivaPayments/util/optional.py b/CreateOrder/Python/VivaPayments/util/optional.py new file mode 100644 index 00000000..6796b3ce --- /dev/null +++ b/CreateOrder/Python/VivaPayments/util/optional.py @@ -0,0 +1,24 @@ +allowed_parameters = { + 'IsPreAuth': bool, + 'ServiceId': int, + 'RequestLang': str, + 'FullName': str, + 'Email': str, + 'Phone': str, + 'Installments': int, + 'MaxInstallments': int, + 'MerchantTrns': str, + 'CustomerTrns': str, + 'SourceCode': str, + 'PaymentTimeOut': str, + 'ExpirationDate': str, + 'AllowRecurring': bool, + 'AllowsRecurring': bool, + 'Tags': list, + 'ActionUser': str, + 'DisableIVR': bool, + 'DisableCash': bool, + 'DisableCard': bool, + 'DisablePayAtHome': bool, + 'DisableExactAmount': bool +} \ No newline at end of file diff --git a/CreateOrder/Python/dist/VivaPython-0.0.1.tar.gz b/CreateOrder/Python/dist/VivaPython-0.0.1.tar.gz new file mode 100644 index 00000000..037206cc Binary files /dev/null and b/CreateOrder/Python/dist/VivaPython-0.0.1.tar.gz differ diff --git a/CreateOrder/Python/setup.cfg b/CreateOrder/Python/setup.cfg new file mode 100644 index 00000000..11e9ec40 --- /dev/null +++ b/CreateOrder/Python/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.rst \ No newline at end of file diff --git a/CreateOrder/Python/setup.py b/CreateOrder/Python/setup.py new file mode 100644 index 00000000..df29d7d4 --- /dev/null +++ b/CreateOrder/Python/setup.py @@ -0,0 +1,23 @@ +import os +from setuptools import setup + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + +setup( + name = 'VivaPython', + version = '0.0.1', + description = 'Python wrapper for VivaPayments API', + author = 'igneel64', + author_email = 'p.perlepes@gmail.com', + long_description = read('README.rst'), + classifiers = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 2.7' + ], + license = 'LICENSE.txt', + keywords = 'Viva VivaPayments payment', + packages = ['VivaPayments', 'VivaPayments.util', 'VivaPayments.models', 'VivaPayments.tests'], + test_suite = 'VivaPayments.tests.viva_test_suite' +) \ No newline at end of file diff --git a/CreateOrder/Python/vivapayments.py b/CreateOrder/Python/vivapayments.py deleted file mode 100644 index 777645cb..00000000 --- a/CreateOrder/Python/vivapayments.py +++ /dev/null @@ -1,131 +0,0 @@ -import datetime -import urllib2 -import urllib -import base64 -import json -import math - -class VivaPayments(object): - """VivaPayments API Wrapper""" - - # Demo constants - DEMO_URL = 'http://demo.vivapayments.com/api/' - DEMO_REDIRECT_URL = 'http://demo.vivapayments.com/web/newtransaction.aspx?ref=' - - # Production constants - PRODUCTION_URL = 'https://www.vivapayments.com/api/' - PRODUCT_REDIRECT_URL = 'https://www.vivapayments.com/web/newtransaction.aspx?ref=' - - def __init__(self,merchant_id=None,api_key=None,production=False): - self.url = VivaPayments.DEMO_URL if production == False else PRODUCT_REDIRECT_URL - self.merchant_id=merchant_id - self.api_key=api_key - - def create_order(self,amount,**kwargs): - """Create a Payment Order.""" - data = self.pack_data('amount',amount,kwargs) - return self._request('POST','orders',data) - - def cancel_order(self,order_code,**kwargs): - """Cancel an existing Payment Order.""" - data = self.pack_data('order_code',order_code,kwargs) - return self._request('DELETE','orders/'+str(order_code),data) - - def get_transaction(self,transaction_id,**kwargs): - """Get all details for a specific transaction, or for all transactions of a given date.""" - data = self.pack_data('transaction_id',transaction_id,kwargs) - return self._request('GET','transactions/'+str(transaction_id),data) - - def create_recurring_transaction(self,transaction_id,**kwargs): - """Make a recurring transaction.""" - data = self.pack_data('transaction_id',transaction_id,kwargs) - return self._request('POST','transactions/'+str(transaction_id),data) - - def cancel_transaction(self,transaction_id,amount): - """Cancel or refund a payment.""" - return self._grequest('DELETE','transactions/'+str(transaction_id)+'?amount='+str(amount)) - - def get_redirect_url(self,order_code): - """Returns the order code appended on the REDIRECT_URL_PREFIX""" - redirect_url = VivaPayments.DEMO_REDIRECT_URL if self.url == VivaPayments.DEMO_URL else VivaPayments.PRODUCT_REDIRET_URL - return redirect_url+str(order_code) - - ### UTILITY FUNCTIONS ### - def pack_data(self,arg_name,arg_val,kwargs): - return dict({arg_name:arg_val}.items() + kwargs.items()) - - def _grequest(self,request_method,url_suffix): - # Construct request object - request_url = self.url + url_suffix - request = urllib2.Request(request_url) - - # Request basic access authentication - base64string = base64.encodestring('%s:%s' % (self.merchant_id,self.api_key)).replace('\n', '') - request.add_header("Authorization", "Basic %s" % base64string) - - # Set http request method - request.get_method = lambda: request_method - response = urllib2.urlopen(request) - return self._decode(response.read()) - - def _request(self,request_method,url_suffix,data): - # Construct request object - data = urllib.urlencode(data) - request_url = self.url + url_suffix - request = urllib2.Request(request_url,data=data) - - # Request basic access authentication - base64string = base64.encodestring('%s:%s' % (self.merchant_id,self.api_key)).replace('\n', '') - request.add_header("Authorization", "Basic %s" % base64string) - - # Set http request method - request.get_method = lambda: request_method - response = urllib2.urlopen(request) - return self._decode(response.read()) - - def _decode(self,json_response): - obj = json.loads(json_response) - - timestamp=obj['TimeStamp'] - - date_tokens =timestamp[0:10].split('-') - year = int(date_tokens[0]) - month = int(date_tokens[1]) - day = int(date_tokens[2]) - - time_tokens =timestamp[11:27].split(':') - hour = int(time_tokens[0]) - minute = int(time_tokens[1]) - second = int(math.floor(float(time_tokens[2]))) - - # Doesn't add timezone info - obj['TimeStamp'] = datetime.datetime(year,month,day,hour,minute,second) - - return obj - -# Examples -if __name__ == '__main__': - # Create vivapayments API Wraper - viva_payments = VivaPayments(merchant_id='1b2573e7-2f67-4443-8a2e-84cac16ec79f',api_key='09014933') - - # Example 1 - - # Create order - result = viva_payments.create_order(100,RequestLang='en-US') - - # Get order code - order_code = result['OrderCode'] - - # Get redirect url - redirect_url = viva_payments.get_redirect_url(order_code) - - # Get the redirect url and paste it at your browser - print redirect_url - - # Example 2 - # Cancel Transaction - - result = viva_payments.cancel_transaction('959A0471-2CC8-4E75-A422-97E318E48ACD', 10) - print result - -