From 292b7eaf572a90c9435a86dcfc33b571d1eb74e3 Mon Sep 17 00:00:00 2001 From: Craig Christenson Date: Wed, 7 Jul 2021 13:19:28 -0400 Subject: [PATCH] Staging Release Candidate 2.0 --- README.md | 226 +----------- examples/ipn.py | 131 +++++++ examples/order.py | 100 ++++++ examples/subscription.py | 78 +++++ requirements.txt | 1 + setup.py | 17 +- test/config.py | 3 + test/test_twocheckout.py | 614 +++++++++++++++++++++------------ twocheckout/__init__.py | 17 +- twocheckout/api.py | 51 +++ twocheckout/api_request.py | 91 ----- twocheckout/charge.py | 61 ---- twocheckout/company.py | 16 - twocheckout/contact.py | 15 - twocheckout/cplus_signature.py | 31 ++ twocheckout/error.py | 5 +- twocheckout/ins.py | 40 --- twocheckout/ipn_helper.py | 66 ++++ twocheckout/order.py | 25 ++ twocheckout/passback.py | 40 --- twocheckout/payment.py | 23 -- twocheckout/product.py | 41 --- twocheckout/response.py | 24 ++ twocheckout/sale.py | 97 ------ twocheckout/subscription.py | 25 ++ twocheckout/twocheckout.py | 14 - twocheckout/util.py | 21 -- 27 files changed, 955 insertions(+), 918 deletions(-) create mode 100644 examples/ipn.py create mode 100644 examples/order.py create mode 100644 examples/subscription.py create mode 100644 requirements.txt mode change 100755 => 100644 setup.py create mode 100644 test/config.py mode change 100755 => 100644 test/test_twocheckout.py create mode 100644 twocheckout/api.py delete mode 100755 twocheckout/api_request.py delete mode 100755 twocheckout/charge.py delete mode 100644 twocheckout/company.py delete mode 100644 twocheckout/contact.py create mode 100644 twocheckout/cplus_signature.py delete mode 100644 twocheckout/ins.py create mode 100644 twocheckout/ipn_helper.py create mode 100644 twocheckout/order.py delete mode 100644 twocheckout/passback.py delete mode 100644 twocheckout/payment.py delete mode 100644 twocheckout/product.py create mode 100644 twocheckout/response.py delete mode 100644 twocheckout/sale.py create mode 100644 twocheckout/subscription.py delete mode 100644 twocheckout/twocheckout.py delete mode 100644 twocheckout/util.py diff --git a/README.md b/README.md index 2f006ad..0569f32 100644 --- a/README.md +++ b/README.md @@ -1,244 +1,42 @@ 2Checkout Python Library ===================== -This library provides developers with a simple set of bindings to the 2Checkout purchase routine, Instant Notification Service and Back Office API. +This library provides developers with a simple set of bindings to the 2Checkout API, Checkout and IPN. To use, download or clone the repository. ```shell -git clone https://github.com/2checkout/2checkout-python.git -``` -If your using python version 3.0 or higher, checkout the 3.x branch. +git clone https://github.com/2checkout/2checkout-python.git -```shell -git checkout 3.x ``` Install _(Or just import in your script.)_ ```shell -cd 2checkout-python -sudo python setup.py install -``` - -Import in your script - -```python -import twocheckout -``` - -Full documentation for each binding will be provided in the [Wiki](https://github.com/2checkout/2checkout-python/wiki). - -Example Purchase API Usage ------------------ - -*Example Usage:* - -```python -import twocheckout +cd 2checkout-python -twocheckout.Api.auth_credentials({ - 'private_key': '3508079E-5383-44D4-BF69-DC619C0D9811', - 'seller_id': '1817037', - 'mode': 'production' -}) - -params = { - 'merchantOrderId': '123', - 'token': 'ODAxZjUzMDEtOWU0MC00NzA3LWFmMDctYmY1NTQ3MDhmZDFh', - 'currency': 'USD', - 'total': '1.00', - 'billingAddr': { - 'name': 'Testing Tester', - 'addrLine1': '123 Test St', - 'city': 'Columbus', - 'state': 'OH', - 'zipCode': '43123', - 'country': 'USA', - 'email': 'cchristenson@2co.com', - 'phoneNumber': '555-555-5555' - } -} - -try: - result = twocheckout.Charge.authorize(params) - print result.responseCode -except twocheckout.TwocheckoutError as error: - print error.msg +sudo python setup.py install ``` -*Example Response:* +Import in your script ```python -{ - 'lineItems': [ - { - 'tangible': 'N', - 'name': '123', - 'price': '1.00', - 'description': '', - 'recurrence': None, - 'duration': None, - 'startupFee': None, - 'productId': '', - 'type': 'product', - 'options': [ - - ], - 'quantity': '1' - } - ], - 'responseMsg': 'Successfully authorized the provided creditcard', - 'recurrentInstallmentId': None, - 'shippingAddr': { - 'city': None, - 'phoneExtension': None, - 'country': None, - 'addrLine2': None, - 'zipCode': None, - 'addrLine1': None, - 'state': None, - 'phoneNumber': None, - 'email': None, - 'name': None - }, - 'orderNumber': '205180784763', - 'currencyCode': 'USD', - 'merchantOrderId': '123', - 'errors': None, - 'responseCode': 'APPROVED', - 'transactionId': '205180784772', - 'total': '1.00', - 'type': 'AuthResponse', - 'billingAddr': { - 'city': 'Columbus', - 'phoneExtension': None, - 'country': 'USA', - 'addrLine2': None, - 'zipCode': '43123', - 'addrLine1': '123 Test St', - 'state': 'OH', - 'phoneNumber': '555-555-5555', - 'email': 'cchristenson@2co.com', - 'name': 'Testing Tester' - } -} -``` -Example Admin API Usage ------------------ - -*Example Usage:* - -```python import twocheckout - -twocheckout.Api.credentials({'username':'APIuser1817037', 'password':'APIpass1817037'}) - -params = { - 'sale_id': 4774467596, - 'category': 1, - 'comment': "Refunding Sale" - } - -sale = twocheckout.Sale.find(params) -sale.refund(params); -``` - -*Example Response:* - -```python -{ - 'response_code': 'OK', - 'response_message': 'refund added to invoice' -} -``` - -Example Checkout Usage: ------------------------ - -*Example Usage:* - -```python -params = { - 'sid': 1817037, - 'cart_order_id': 'test1', - 'total': 1.00 -} - -form = twocheckout.Charge.submit(params) -``` -*Example Response:* - -```html -
- - - - - -
- -``` - -Example Return Usage: ---------------------- - -*Example Usage:* - -```python -params = web.input() # using web.py -params['secret'] = 'tango' -result = twocheckout.Passback.check(params) -``` - -*Example Response:* - -```python -{ - 'response_code': 'SUCCESS', - 'response_message': 'Hash Matched' -} ``` -Example INS Usage: ------------------- *Example Usage:* -```python -params = web.input() # using web.py -params['secret'] = 'tango' -result = twocheckout.Notification.check(params) -``` - -*Example Response:* +You can browse through the /examples folder to see how new order is placed, get an order from 2CO API side, create a +subscription, etc -```python -{ - 'response_code': 'SUCCESS', - 'response_message': 'Hash Matched' -} -``` - -Full documentation for each binding is provided in the [Wiki](https://github.com/craigchristenson/2checkout-python/wiki). - -Exceptions: ------------ -TwocheckoutError exceptions are thrown by if an error has returned. It is best to catch these exceptions so that they can be gracefully handled in your application. - -*Example Usage* - -```python -try: - sale = twocheckout.Sale.find(EXAMPLE_SALE) - invoice = sale.invoices[0] - lineitem = invoice.lineitems[0] - result = lineitem.refund(EXAMPLE_REFUND) -except TwocheckoutError as error: - error.message -``` +IPN must be validated before you deal with the request. We create a helper class for you to ease you process intro +validate the request. After you add your IPN url intro 2Checkout account (IPN Settings area), your will receive updates +from our API. Instantiate the IpnHelper class with your secret key, validate the request using the `is_valid` function, +then you can update your order, subscription etc. After you validate the request and do your thing, you must use +the `calculate_ipn_response` to response the 2Checkout API with our custom response date|hash diff --git a/examples/ipn.py b/examples/ipn.py new file mode 100644 index 0000000..c144fae --- /dev/null +++ b/examples/ipn.py @@ -0,0 +1,131 @@ +import twocheckout + +# fill you VENDOR_ID & SECRET_KEY from 2Checkout account page +auth_params = { + 'merchant_code': '', + 'secret_key': '' +} + +payload = { + 'GIFT_ORDER': '0', + 'SALEDATE': '2021-04-08 16:29:38', + 'PAYMENTDATE': '2021-04-08 16:29:42', + 'REFNO': '148998082', + 'REFNOEXT': 'REST_API_AVANGTE', + 'SHOPPER_REFERENCE_NUMBER': '', + 'ORDERNO': '8978', + 'ORDERSTATUS': 'COMPLETE', + 'PAYMETHOD': 'Visa/MasterCard', + 'PAYMETHOD_CODE': 'CCVISAMC', + 'FIRSTNAME': 'Customer', + 'LASTNAME': '2Checkout', + 'COMPANY': '', + 'REGISTRATIONNUMBER': '', + 'FISCALCODE': '', + 'TAX_OFFICE': '', + 'CBANKNAME': '', + 'CBANKACCOUNT': '', + 'ADDRESS1': 'Test Address', + 'ADDRESS2': '', + 'CITY': 'LA', + 'STATE': 'California', + 'ZIPCODE': '12345', + 'COUNTRY': 'United States of America', + 'COUNTRY_CODE': 'us', + 'PHONE': '', + 'FAX': '', + 'CUSTOMEREMAIL': 'testcustomer@2Checkout.com', + 'FIRSTNAME_D': 'Customer', + 'LASTNAME_D': '2Checkout', + 'COMPANY_D': '', + 'ADDRESS1_D': 'Test Address', + 'ADDRESS2_D': '', + 'CITY_D': 'LA', + 'STATE_D': 'California', + 'ZIPCODE_D': '12345', + 'COUNTRY_D': 'United States of America', + 'COUNTRY_D_CODE': 'us', + 'PHONE_D': '', + 'EMAIL_D': 'testcustomer@2Checkout.com', + 'IPADDRESS': '91.220.121.21', + 'IPCOUNTRY': 'Romania', + 'COMPLETE_DATE': '2021-04-08 16:29:48', + 'TIMEZONE_OFFSET': 'GMT+03:00', + 'CURRENCY': 'USD', + 'LANGUAGE': 'en', + 'ORDERFLOW': 'REGULAR', + 'IPN_PID[]': '35144095', + 'IPN_PNAME[]': 'Dynamic product', + 'IPN_PCODE[]': '', + 'IPN_EXTERNAL_REFERENCE[]': '', + 'IPN_INFO[]': '', + 'IPN_QTY[]': '1', + 'IPN_PRICE[]': '107.00', + 'IPN_VAT[]': '0.00', + 'IPN_VAT_RATE[]': '0.00', + 'IPN_VER[]': '1', + 'IPN_DISCOUNT[]': '0.00', + 'IPN_PROMOTION_CATEGORY[]': '', + 'IPN_PROMONAME[]': '', + 'IPN_PROMOCODE[]': '', + 'IPN_ORDER_COSTS[]': '0', + 'IPN_SKU[]': '', + 'IPN_PARTNER_CODE': '', + 'IPN_PGROUP[]': '0', + 'IPN_PGROUP_NAME[]': '', + 'MESSAGE_ID': '250833683479', + 'MESSAGE_TYPE': 'COMPLETE', + 'IPN_LICENSE_PROD[]': '35144095', + 'IPN_LICENSE_TYPE[]': 'REGULAR', + 'IPN_LICENSE_REF[]': '9WITYHQ6NF', + 'IPN_LICENSE_EXP[]': '2021-04-10 16:29:42', + 'IPN_LICENSE_START[]': '2021-04-08 16:29:42', + 'IPN_LICENSE_LIFETIME[]': 'NO', + 'IPN_LICENSE_ADDITIONAL_INFO[]': '', + 'IPN_DELIVEREDCODES[]': '', + 'IPN_DOWNLOAD_LINK': '', + 'IPN_TOTAL[]': '107.00', + 'IPN_TOTALGENERAL': '107.00', + 'IPN_SHIPPING': '0.00', + 'IPN_SHIPPING_TAX': '0.00', + 'AVANGATE_CUSTOMER_REFERENCE': '884855078', + 'EXTERNAL_CUSTOMER_REFERENCE': '', + 'IPN_PARTNER_MARGIN_PERCENT': '0.00', + 'IPN_PARTNER_MARGIN': '0.00', + 'IPN_EXTRA_MARGIN': '0.00', + 'IPN_EXTRA_DISCOUNT': '0.00', + 'IPN_COUPON_DISCOUNT': '0.00', + 'IPN_LINK_SOURCE': 'testAPI.com', + 'IPN_COMMISSION': '4.1015', + 'REFUND_TYPE': '', + 'IPN_PRODUCT_OPTIONS_35144095_TEXT[]': 'Name LR', + 'IPN_PRODUCT_OPTIONS_35144095_VALUE[]': 'f21a6009c31851ab5166190e353012bd', + 'IPN_PRODUCT_OPTIONS_35144095_OPTIONAL_VALUE[]': 'f21a6009c31851ab5166190e353012bd', + 'IPN_PRODUCT_OPTIONS_35144095_PRICE[]': '7.00', + 'IPN_PRODUCT_OPTIONS_35144095_OPERATOR[]': 'ADD', + 'IPN_PRODUCT_OPTIONS_35144095_USAGE[]': 'PREPAID', + 'CHARGEBACK_RESOLUTION': 'NONE', + 'CHARGEBACK_REASON_CODE': '', + 'TEST_ORDER': '1', + 'IPN_ORDER_ORIGIN': 'API', + 'FRAUD_STATUS': 'APPROVED', + 'CARD_TYPE': 'visa', + 'CARD_LAST_DIGITS': '1111', + 'CARD_EXPIRATION_DATE': '12/22', + 'GATEWAY_RESPONSE': 'Approved', + 'IPN_DATE': '20210408185911', + 'FX_RATE': '1', + 'FX_MARKUP': '0', + 'PAYABLE_AMOUNT': '102.90', + 'PAYOUT_CURRENCY': 'USD', + 'VENDOR_CODE': '250111206876', + 'PROPOSAL_ID': '', + 'HASH': '8d05499f0933c2e07c8599ff3a2e5338' +} + +# instantiate the object ( for auth) +ipn = twocheckout.ipn_helper.IpnHelper(auth_params['secret_key']) +ipn_valid = ipn.is_valid(payload) +ipn_response = ipn.calculate_ipn_response(payload) +print('ipn is valid: ', ipn_valid) +print('ipn response: ', ipn_response) diff --git a/examples/order.py b/examples/order.py new file mode 100644 index 0000000..909895e --- /dev/null +++ b/examples/order.py @@ -0,0 +1,100 @@ +import twocheckout + +# fill you MERCHANT_CODE & SECRET_KEY from 2Checkout account page +auth_params = { + 'merchant_code': '250111206876', + 'secret_key': '=B6gcTl(4t8@D3yUM!TP' +} + +# Transaction ID example +order_transaction_id = '147288494' + +# order params ( when creating new orders use this JSON format (some fields are optional) +# to view what are the required or optional fields please read the our docs +order_params = { + "Country": "us", + "Currency": "USD", + "CustomerIP": "91.220.121.21", + "ExternalReference": "REST_API_AVANGTE", + "Language": "en", + "Source": "testAPI.com", + "BillingDetails": { + "Address1": "Test Address", + "City": "LA", + "State": "California", + "CountryCode": "US", + "Email": "testcustomer@2Checkout.com", + "FirstName": "Customer", + "LastName": "2Checkout", + "Zip": "12345" + }, + "Items": [ + { + "Name": "Dynamic product", + "Description": "Test description", + "Quantity": 1, + "IsDynamic": True, + "Tangible": False, + "PurchaseType": "PRODUCT", + "CrossSell": { + "CampaignCode": "CAMPAIGN_CODE", + "ParentCode": "MASTER_PRODUCT_CODE" + }, + "Price": { + "Amount": 100, + "Type": "CUSTOM" + }, + "PriceOptions": [ + { + "Name": "OPT1", + "Options": [ + { + "Name": "Name LR", + "Value": "Value LR", + "Surcharge": 7 + } + ] + } + ], + "RecurringOptions": { + "CycleLength": 2, + "CycleUnit": "DAY", + "CycleAmount": 12.2, + "ContractLength": 3, + "ContractUnit": "DAY" + } + } + ], + "PaymentDetails": { + "Type": "CC", + "Currency": "USD", + "CustomerIP": "91.220.121.21", + "PaymentMethod": { + "CardNumber": "4111111111111111", + "CardType": "VISA", + "Vendor3DSReturnURL": "www.success.com", + "Vendor3DSCancelURL": "www.fail.com", + "ExpirationYear": "2044", + "ExpirationMonth": "12", + "CCID": "123", + "HolderName": "John Doe", + "RecurringEnabled": True, + "HolderNameTime": 1, + "CardNumberTime": 1 + } + } +} +# # instantiate the object ( for auth) +order = twocheckout.order.Order(auth_params) + +## creates a new order +new_order = order.create(order_params) + +## get full info for an order +get_order = order.get(order_transaction_id) + +print('new order') +print(new_order) +print('#########################') +print('get order ') +print(get_order) diff --git a/examples/subscription.py b/examples/subscription.py new file mode 100644 index 0000000..3cfc5dc --- /dev/null +++ b/examples/subscription.py @@ -0,0 +1,78 @@ +import twocheckout + +# fill you VENDOR_ID & SECRET_KEY from 2Checkout account page +auth_params = { + 'merchant_code': '250111206876', + 'secret_key': '=B6gcTl(4t8@D3yUM!TP' +} + +# Transaction ID example +subscription_id = '37A4678B13' + +subscription_params = { + "CustomPriceBillingCyclesLeft": 2, + "DeliveryInfo": { + "Codes": [ + { + "Code": "___TEST___CODE____" + } + ] + }, + "EndUser": { + "Address1": "Test Address", + "Address2": "", + "City": "LA", + "Company": "", + "CountryCode": "us", + "Email": "customer@2Checkout.com", + "Fax": "", + "FirstName": "Customer", + "Language": "en", + "LastName": "2Checkout", + "Phone": "", + "State": "CA", + "Zip": "12345" + }, + "ExpirationDate": "2015-12-16", + "ExternalSubscriptionReference": "ThisIsYourUniqueIdentifier123", + "NextRenewalPrice": 49.99, + "NextRenewalPriceCurrency": "usd", + "PartnerCode": "", + "Payment": { + "CCID": "123", + "CardNumber": "4111111111111111", + "CardType": "VISA", + "ExpirationMonth": "12", + "ExpirationYear": "2018", + "HolderName": "John Doe" + }, + "Product": { + "PriceOptionCodes": [ + "addon-1_1_annually" + ], + "ProductCode": "my_subscription_1", + "ProductId": "24584760", + "ProductName": "2Checkout Subscription", + "ProductQuantity": 1, + "ProductVersion": "" + }, + "StartDate": "2015-02-16", + "SubscriptionValue": 199, + "SubscriptionValueCurrency": "usd", + "Test": 1 +} + +# instantiate the object ( for auth) +subscription = twocheckout.subscription.Subscription(auth_params) + +# creates a new subscription +new_subscription = subscription.create(subscription_params) + +# get full info for an subscription +get_subscription = subscription.get(subscription_id) + +print('new subscription') +print(new_subscription) +print('#########################') +print('get subscription ') +print(get_subscription) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4970db3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests~=2.25.1 \ No newline at end of file diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 3af7780..c0dd91c --- a/setup.py +++ b/setup.py @@ -1,12 +1,15 @@ - from distutils.core import setup setup( name="twocheckout", - version='0.4.0', - description="2Checkout Python Library", - author="Craig Christenson", - author_email="christensoncraig@gmail.com", + version='2.0.0', + description="2Checkout Python Library using API 6.0", + author="2Checkout", + author_email="supportplus@2checkout.com", url="iframe.php?url=https%3A%2F%2Fwww.2checkout.com", - packages=["twocheckout"] -) \ No newline at end of file + packages=["twocheckout"], + python_requires=">=3.5", + install_requires=[ + 'requests >= 2.20; python_version >= "3.5"; pyjwt >= 19.2' + ] +) diff --git a/test/config.py b/test/config.py new file mode 100644 index 0000000..d514e41 --- /dev/null +++ b/test/config.py @@ -0,0 +1,3 @@ +TWOCHECKOUT_TEST_MERCHANT_ID = 'YOUR-MERCHANT-ID-HERE' +TWOCHECKOUT_TEST_BUYLINK_SECRET_WORD = 'YOUR-MERCHANT-BUY-LINK-SECRET-WORD-HERE' +TWOCHECKOUT_TEST_MERCHANT_SECRET_KEY = 'YOUR-MERCHANT-SECRET-KEY-HERE' diff --git a/test/test_twocheckout.py b/test/test_twocheckout.py old mode 100755 new mode 100644 index e14626d..daf1223 --- a/test/test_twocheckout.py +++ b/test/test_twocheckout.py @@ -3,254 +3,422 @@ import sys import datetime import unittest -from twocheckout import TwocheckoutError - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) import twocheckout +from test import config +import hmac +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) NOW = datetime.datetime.now() -EXAMPLE_PRODUCT = { - 'name': 'Python Example Product', - 'price': 2.00 -} - -EXAMPLE_SALE = { - 'sale_id': 250353589267 -} - -EXAMPLE_COMMENT = { - 'sale_comment': "python2 test" -} - -EXAMPLE_REFUND = { - 'comment': "Python Refund Sale", - 'category': 1 -} - -EXAMPLE_SHIP = { - 'tracking_number': "test" +auth_params = { + 'merchant_code': config.TWOCHECKOUT_TEST_MERCHANT_ID, + 'secret_key': config.TWOCHECKOUT_TEST_MERCHANT_SECRET_KEY, } - -EXAMPLE_PASSBACK = { - 'sid': '1817037', - 'key': '7AB926D469648F3305AE361D5BD2C3CB', - 'total': '0.01', - 'order_number': '4774380224', - 'secret': 'tango' -} - -EXAMPLE_NOTIFICATION = { - 'vendor_id': '1817037', - 'sale_id': '4774380224', - 'invoice_id': '4774380233', - 'md5_hash': '566C45D68B75357AD43F9010CFFE8CF5', - 'secret': 'tango' -} - -EXAMPLE_AUTH = { - "sellerId": "CREDENTIALS_HERE", - "privateKey": "CREDENTIALS_HERE", - "merchantOrderId": "123", - "token": "CUSTOMER-CLIENT-SIDE-TOKEN", - "currency": "USD", - "total": "2.00", - "demo": True, - "billingAddr": { - "name": "John Doe", - "addrLine1": "123 Test St", - "city": "Columbus", - "state": "Ohio", - "zipCode": "43123", - "country": "USA", - "email": "example@2co.com", - "phoneNumber": "5555555555" +order_transaction_id = '147288494' +order_get_test = {"meta": {"status": "success", "message": "ok"}, + "body": {'RefNo': '147288494', 'OrderNo': 0, 'ExternalReference': 'REST_API_AVANGTE', + 'ShopperRefNo': None, + 'Status': 'PENDING', 'ApproveStatus': 'WAITING', 'VendorApproveStatus': 'OK', + 'MerchantCode': config.TWOCHECKOUT_TEST_MERCHANT_ID, 'Language': 'en', + 'OrderDate': '2021-03-19 11:49:50', + 'FinishDate': None, + 'Source': 'testAPI.com', + 'Affiliate': {'AffiliateCode': None, 'AffiliateSource': None, 'AffiliateName': None, + 'AffiliateUrl': None}, + 'HasShipping': False, + 'BillingDetails': {'FiscalCode': None, 'TaxOffice': None, 'Phone': None, + 'FirstName': 'Customer', + 'LastName': '2Checkout', 'Company': None, + 'Email': 'testcustomer@2Checkout.com', + 'Address1': 'Test Address', 'Address2': None, 'City': 'LA', + 'Zip': '12345', + 'CountryCode': 'us', 'State': 'California'}, + 'DeliveryDetails': {'Phone': None, 'FirstName': 'Customer', 'LastName': '2Checkout', + 'Company': None, + 'Email': 'testcustomer@2Checkout.com', 'Address1': 'Test Address', + 'Address2': None, + 'City': 'LA', 'Zip': '12345', 'CountryCode': 'us', + 'State': 'California'}, + 'PaymentDetails': {'Type': 'CC', 'Currency': 'usd', + 'PaymentMethod': {'Authorize3DS': None, 'Vendor3DSReturnURL': None, + 'Vendor3DSCancelURL': None, 'FirstDigits': '4111', + 'LastDigits': '1111', 'CardType': 'Visa', + 'RecurringEnabled': True}, + 'CustomerIP': '91.220.121.21'}, 'DeliveryInformation': { + 'ShippingMethod': {'Code': None, 'TrackingUrl': None, 'TrackingNumber': None, + 'Comment': None}}, + 'CustomerDetails': None, 'Origin': 'API', 'AvangateCommission': 4.1, 'OrderFlow': 'REGULAR', + 'GiftDetails': None, 'PODetails': None, 'ExtraInformation': None, 'PartnerCode': None, + 'PartnerMargin': None, 'PartnerMarginPercent': None, 'ExtraMargin': None, + 'ExtraMarginPercent': None, + 'ExtraDiscount': None, 'ExtraDiscountPercent': None, 'LocalTime': None, 'TestOrder': False, + 'FxRate': 1, + 'FxMarkup': 0, 'PayoutCurrency': 'USD', 'DeliveryFinalized': False, 'Errors': None, + 'Items': [{ + 'ProductDetails': { + 'Name': 'Dynamic product', + 'ShortDescription': 'Test description', + 'Tangible': False, + 'IsDynamic': True, + 'ExtraInfo': None, + 'RenewalStatus': False, + 'Subscriptions': None, + 'DeliveryInformation': { + 'Delivery': 'NO_DELIVERY', + 'DownloadFile': None, + 'DeliveryDescription': '', + 'CodesDescription': '', + 'Codes': []}}, + 'PriceOptions': [ + { + 'Code': 'OPT1_292', + 'Name': 'OPT1', + 'Required': True, + 'Options': [ + { + 'Name': 'Name LR', + 'Value': 'f7f4d3d5546e4f25e8dcdaf8301c34d6', + 'Surcharge': '7.00'}]}], + 'Price': { + 'UnitNetPrice': 107, + 'UnitGrossPrice': 107, + 'UnitVAT': 0, + 'UnitDiscount': 0, + 'UnitNetDiscountedPrice': 107, + 'UnitGrossDiscountedPrice': 107, + 'UnitAffiliateCommission': 0, + 'ItemUnitNetPrice': 0, + 'ItemUnitGrossPrice': 0, + 'ItemNetPrice': 0, + 'ItemGrossPrice': 0, + 'VATPercent': 0, + 'HandlingFeeNetPrice': 0, + 'HandlingFeeGrossPrice': 0, + 'Currency': 'usd', + 'NetPrice': 107, + 'GrossPrice': 107, + 'NetDiscountedPrice': 107, + 'GrossDiscountedPrice': 107, + 'Discount': 0, + 'VAT': 0, + 'AffiliateCommission': 0}, + 'LineItemReference': '69057567c67d17d570523b4ea67fe8770fdbc5bd', + 'PurchaseType': 'PRODUCT', + 'ExternalReference': '', + 'Quantity': 1, + 'SKU': None, + 'CrossSell': None, + 'Trial': None, + 'AdditionalFields': None, + 'Promotion': None, + 'RecurringOptions': None, + 'SubscriptionStartDate': None, + 'SubscriptionCustomSettings': None}], + 'Promotions': [], 'AdditionalFields': None, 'Currency': 'usd', 'NetPrice': 107, + 'GrossPrice': 107, + 'NetDiscountedPrice': 107, 'GrossDiscountedPrice': 107, 'Discount': 0, 'VAT': 0, + 'AffiliateCommission': 0, + 'CustomParameters': None + }} +order_params_test = { + "Country": "us", + "Currency": "USD", + "CustomerIP": "91.220.121.21", + "ExternalReference": "REST_API_AVANGTE", + "Language": "en", + "Source": "testAPI.com", + "BillingDetails": { + "Address1": "Test Address", + "City": "LA", + "State": "California", + "CountryCode": "US", + "Email": "testcustomer@2Checkout.com", + "FirstName": "Customer", + "LastName": "2Checkout", + "Zip": "12345" + }, + "Items": [ + { + "Name": "Dynamic product", + "Description": "Test description", + "Quantity": 1, + "IsDynamic": True, + "Tangible": False, + "PurchaseType": "PRODUCT", + "CrossSell": { + "CampaignCode": "CAMPAIGN_CODE", + "ParentCode": "MASTER_PRODUCT_CODE" + }, + "Price": { + "Amount": 100, + "Type": "CUSTOM" + }, + "PriceOptions": [ + { + "Name": "OPT1", + "Options": [ + { + "Name": "Name LR", + "Value": "Value LR", + "Surcharge": 7 + } + ] + } + ], + "RecurringOptions": { + "CycleLength": 2, + "CycleUnit": "DAY", + "CycleAmount": 12.2, + "ContractLength": 3, + "ContractUnit": "DAY" + } + } + ], + "PaymentDetails": { + "Type": "CC", + "Currency": "USD", + "CustomerIP": "91.220.121.21", + "PaymentMethod": { + "CardNumber": "4111111111111111", + "CardType": "VISA", + "Vendor3DSReturnURL": "www.success.com", + "Vendor3DSCancelURL": "www.fail.com", + "ExpirationYear": "2044", + "ExpirationMonth": "12", + "CCID": "123", + "HolderName": "John Doe", + "RecurringEnabled": True, + "HolderNameTime": 1, + "CardNumberTime": 1 + } } } +json_encoded_convert_plus_parameters = '{"merchant":"' \ + + config.TWOCHECKOUT_TEST_MERCHANT_ID \ + + '","dynamic":1,"src":"DJANGO",' \ + '"return-url":"https:\/\/google.com",' \ + '"return-type":"redirect",' \ + '"expiration":1617189603,"order-ext-ref":292,' \ + '"customer-ext-ref":"example@example.com",' \ + '"currency":"GBP","test":"1","language":"en",' \ + '"prod":"test site","price":"71.03","qty":"1",' \ + '"type":"PRODUCT","tangible":"0",' \ + '"ship-name":"John Doe","ship-country":"US",' \ + '"ship-state":"AL",' \ + '"ship-email":"example@example.com",' \ + '"ship-address":"Example","ship-address2":"",' \ + '"ship-city":"Example","name":"John Doe",' \ + '"phone":"756852919","country":"US","state":"AL",' \ + '"email":"example@example.com",' \ + '"address":"Example","address2":"",' \ + '"city":"Example","zip":"35242","company-name":""} ' + +ipn_payload = { + 'GIFT_ORDER': '0', + 'SALEDATE': '2021-04-08 16:29:38', + 'PAYMENTDATE': '2021-04-08 16:29:42', + 'REFNO': '148998082', + 'REFNOEXT': 'REST_API_AVANGTE', + 'SHOPPER_REFERENCE_NUMBER': '', + 'ORDERNO': '8978', + 'ORDERSTATUS': 'COMPLETE', + 'PAYMETHOD': 'Visa/MasterCard', + 'PAYMETHOD_CODE': 'CCVISAMC', + 'FIRSTNAME': 'Customer', + 'LASTNAME': '2Checkout', + 'COMPANY': '', + 'REGISTRATIONNUMBER': '', + 'FISCALCODE': '', + 'TAX_OFFICE': '', + 'CBANKNAME': '', + 'CBANKACCOUNT': '', + 'ADDRESS1': 'Test Address', + 'ADDRESS2': '', + 'CITY': 'LA', + 'STATE': 'California', + 'ZIPCODE': '12345', + 'COUNTRY': 'United States of America', + 'COUNTRY_CODE': 'us', + 'PHONE': '', + 'FAX': '', + 'CUSTOMEREMAIL': 'testcustomer@2Checkout.com', + 'FIRSTNAME_D': 'Customer', + 'LASTNAME_D': '2Checkout', + 'COMPANY_D': '', + 'ADDRESS1_D': 'Test Address', + 'ADDRESS2_D': '', + 'CITY_D': 'LA', + 'STATE_D': 'California', + 'ZIPCODE_D': '12345', + 'COUNTRY_D': 'United States of America', + 'COUNTRY_D_CODE': 'us', + 'PHONE_D': '', + 'EMAIL_D': 'testcustomer@2Checkout.com', + 'IPADDRESS': '91.220.121.21', + 'IPCOUNTRY': 'Romania', + 'COMPLETE_DATE': '2021-04-08 16:29:48', + 'TIMEZONE_OFFSET': 'GMT+03:00', + 'CURRENCY': 'USD', + 'LANGUAGE': 'en', + 'ORDERFLOW': 'REGULAR', + 'IPN_PID[]': '35144095', + 'IPN_PNAME[]': 'Dynamic product', + 'IPN_PCODE[]': '', + 'IPN_EXTERNAL_REFERENCE[]': '', + 'IPN_INFO[]': '', + 'IPN_QTY[]': '1', + 'IPN_PRICE[]': '107.00', + 'IPN_VAT[]': '0.00', + 'IPN_VAT_RATE[]': '0.00', + 'IPN_VER[]': '1', + 'IPN_DISCOUNT[]': '0.00', + 'IPN_PROMOTION_CATEGORY[]': '', + 'IPN_PROMONAME[]': '', + 'IPN_PROMOCODE[]': '', + 'IPN_ORDER_COSTS[]': '0', + 'IPN_SKU[]': '', + 'IPN_PARTNER_CODE': '', + 'IPN_PGROUP[]': '0', + 'IPN_PGROUP_NAME[]': '', + 'MESSAGE_ID': '250833683479', + 'MESSAGE_TYPE': 'COMPLETE', + 'IPN_LICENSE_PROD[]': '35144095', + 'IPN_LICENSE_TYPE[]': 'REGULAR', + 'IPN_LICENSE_REF[]': '9WITYHQ6NF', + 'IPN_LICENSE_EXP[]': '2021-04-10 16:29:42', + 'IPN_LICENSE_START[]': '2021-04-08 16:29:42', + 'IPN_LICENSE_LIFETIME[]': 'NO', + 'IPN_LICENSE_ADDITIONAL_INFO[]': '', + 'IPN_DELIVEREDCODES[]': '', + 'IPN_DOWNLOAD_LINK': '', + 'IPN_TOTAL[]': '107.00', + 'IPN_TOTALGENERAL': '107.00', + 'IPN_SHIPPING': '0.00', + 'IPN_SHIPPING_TAX': '0.00', + 'AVANGATE_CUSTOMER_REFERENCE': '884855078', + 'EXTERNAL_CUSTOMER_REFERENCE': '', + 'IPN_PARTNER_MARGIN_PERCENT': '0.00', + 'IPN_PARTNER_MARGIN': '0.00', + 'IPN_EXTRA_MARGIN': '0.00', + 'IPN_EXTRA_DISCOUNT': '0.00', + 'IPN_COUPON_DISCOUNT': '0.00', + 'IPN_LINK_SOURCE': 'testAPI.com', + 'IPN_COMMISSION': '4.1015', + 'REFUND_TYPE': '', + 'IPN_PRODUCT_OPTIONS_35144095_TEXT[]': 'Name LR', + 'IPN_PRODUCT_OPTIONS_35144095_VALUE[]': 'f21a6009c31851ab5166190e353012bd', + 'IPN_PRODUCT_OPTIONS_35144095_OPTIONAL_VALUE[]': 'f21a6009c31851ab5166190e353012bd', + 'IPN_PRODUCT_OPTIONS_35144095_PRICE[]': '7.00', + 'IPN_PRODUCT_OPTIONS_35144095_OPERATOR[]': 'ADD', + 'IPN_PRODUCT_OPTIONS_35144095_USAGE[]': 'PREPAID', + 'CHARGEBACK_RESOLUTION': 'NONE', + 'CHARGEBACK_REASON_CODE': '', + 'TEST_ORDER': '1', + 'IPN_ORDER_ORIGIN': 'API', + 'FRAUD_STATUS': 'APPROVED', + 'CARD_TYPE': 'visa', + 'CARD_LAST_DIGITS': '1111', + 'CARD_EXPIRATION_DATE': '12/22', + 'GATEWAY_RESPONSE': 'Approved', + 'IPN_DATE': '20210408185911', + 'FX_RATE': '1', + 'FX_MARKUP': '0', + 'PAYABLE_AMOUNT': '102.90', + 'PAYOUT_CURRENCY': 'USD', + 'VENDOR_CODE': '250111206876', + 'PROPOSAL_ID': '', + 'HASH': '8d05499f0933c2e07c8599ff3a2e5338' +} -class TwocheckoutTestCase(unittest.TestCase): - def setUp(self): - super(TwocheckoutTestCase, self).setUp() - - twocheckout.Api.credentials({ - 'username': 'CREDENTIALS_HERE', - 'password': 'CREDENTIALS_HERE' - }) - - twocheckout.Api.auth_credentials({ - 'private_key': 'CREDENTIALS_HERE', - 'seller_id': 'CREDENTIALS_HERE' - }) - - -class AuthorizationTest(TwocheckoutTestCase): - def setUp(self): - super(AuthorizationTest, self).setUp() - -## Place order test - def test_1_auth(self): - params = EXAMPLE_AUTH - try: - result = twocheckout.Charge.authorize(params) - - ## use OrderNumber for sale_id for next tests - print("OrderNumber: ", result.orderNumber) - self.assertEqual(result.responseCode, "APPROVED") - except TwocheckoutError as error: - self.assertEqual(error.msg, "Unauthorized") - -class SaleTest(TwocheckoutTestCase): - def setUp(self): - super(SaleTest, self).setUp() - - def test_1_find_sale(self): - try: - sale = twocheckout.Sale.find(EXAMPLE_SALE) - self.assertEqual(int(sale.sale_id), 250353589267) - except TwocheckoutError as error: - self.assertEqual(error.message, "Unable to find record.") - - def test_2_list_sale(self): - params = {'pagesize': 2} - list = twocheckout.Sale.list(params) - self.assertEqual(len(list), 2) - - def test_3_refund_sale(self): - try: - sale = twocheckout.Sale.find(EXAMPLE_SALE) - result = sale.refund(EXAMPLE_REFUND) - self.assertEqual(result.message, "refund added to invoice") - except TwocheckoutError as error: - self.assertEqual(error.message, "Amount greater than remaining balance on invoice.") - -## If you have already run test_3 then test_4 will fail for the same sale_id. - def test_4_refund_invoice(self): - try: - sale = twocheckout.Sale.find(EXAMPLE_SALE) - invoice = sale.invoices[0] - result = invoice.refund(EXAMPLE_REFUND) - self.assertEqual(result.response_message, "refund added to invoice") - except TwocheckoutError as error: - self.assertEqual(error.message, "Amount greater than remaining balance on invoice.") - - def test_5_refund_lineitem(self): - try: - sale = twocheckout.Sale.find(EXAMPLE_SALE) - invoice = sale.invoices[0] - lineitem = invoice.lineitems[0] - result = lineitem.refund(EXAMPLE_REFUND) - self.assertEqual(result.response_message, "lineitem refunded") - except TwocheckoutError as error: - self.assertEqual(error.message, "Lineitem amount greater than remaining balance on invoice.") - - def test_6_stop_sale(self): - sale = twocheckout.Sale.find(EXAMPLE_SALE) - result = sale.stop() - self.assertEqual(result.response_message, "No active recurring lineitems") - - def test_7_stop_invoice(self): - sale = twocheckout.Sale.find(EXAMPLE_SALE) - invoice = sale.invoices[0] - result = invoice.stop() - self.assertEqual(result.response_message, "No active recurring lineitems") -## If you have already run "test_7_stop_invoice" then "test_8_stop_sale_lineitem" will fail for the same sale_id. - def test_8_stop_sale_lineitem(self): - sale = twocheckout.Sale.find(EXAMPLE_SALE) - invoice = sale.invoices[0] - try: - lineitem = invoice.lineitems[0] - result = lineitem.stop() - except TwocheckoutError as error: - self.assertEqual(error.message, "Lineitem is not scheduled to recur.") +class CplusTestCase(unittest.TestCase): + def setUp(self) -> None: + super(CplusTestCase, self).setUp() - def test_9_comment(self): - sale = twocheckout.Sale.find(EXAMPLE_SALE) - result = sale.comment(EXAMPLE_COMMENT) - self.assertEqual(result.response_message, "Created comment successfully.") - def test_10_ship(self): - try: - sale = twocheckout.Sale.find(EXAMPLE_SALE) - result = sale.ship(EXAMPLE_SHIP) - except TwocheckoutError as error: - self.assertEqual(error.message, "Sale already marked shipped.") +class CplusSignatureTest(CplusTestCase): + cplus = None -class ProductTest(TwocheckoutTestCase): def setUp(self): - super(ProductTest, self).setUp() - - def test_1_create(self): - result = twocheckout.Product.create(EXAMPLE_PRODUCT) - self.assertEqual(result.response_message, "Product successfully created.") - EXAMPLE_PRODUCT['product_id'] = result.product_id - - def test_2_find(self): - product = twocheckout.Product.find(EXAMPLE_PRODUCT) - self.assertEqual(product.name, "Python Example Product") - - def test_3_update(self): - product = twocheckout.Product.find(EXAMPLE_PRODUCT) - EXAMPLE_PRODUCT['name'] = "Updated Name" - product = product.update(EXAMPLE_PRODUCT) - self.assertEqual(product.name, "Updated Name") + super(CplusSignatureTest, self).setUp() + self.cplus = twocheckout.CplusSignature() - def test_4_delete(self): - product = twocheckout.Product.find(EXAMPLE_PRODUCT) - result = product.delete(EXAMPLE_PRODUCT) - self.assertEqual(result.response_message, "Product successfully deleted.") + def test_1_get_signature_without_token_expiration(self): + self.assertEqual(64, len(self.cplus.get_signature( + config.TWOCHECKOUT_TEST_MERCHANT_ID, + config.TWOCHECKOUT_TEST_BUYLINK_SECRET_WORD, + json_encoded_convert_plus_parameters))) - def test_5_list(self): - params = {'pagesize': 2} - list = twocheckout.Product.list(params) - self.assertEqual(len(list), 2) + def test_1_get_signature_with_token_expiration(self): + self.assertEqual(64, len(self.cplus.get_signature( + config.TWOCHECKOUT_TEST_MERCHANT_ID, + config.TWOCHECKOUT_TEST_BUYLINK_SECRET_WORD, + json_encoded_convert_plus_parameters, + 1000))) -class CompanyTest(TwocheckoutTestCase): - def setUp(self): - super(CompanyTest, self).setUp() - def test_1_retrieve(self): - company = twocheckout.Company.retrieve() - self.assertEqual(company.vendor_id, "250111206876") +class ApiTestCase(unittest.TestCase): + def setUp(self) -> None: + super(ApiTestCase, self).setUp() -class ContactTest(TwocheckoutTestCase): - def setUp(self): - super(ContactTest, self).setUp() - def test_1_create(self): - contact = twocheckout.Contact.retrieve() - self.assertEqual(contact.vendor_id, "250111206876") +class OrderTest(ApiTestCase): + order = None -class PaymentTest(TwocheckoutTestCase): + # setup auth headers def setUp(self): - super(PaymentTest, self).setUp() + super(OrderTest, self).setUp() + self.order = twocheckout.Order(auth_params) - def test_1_pending(self): - payment = twocheckout.Payment.pending() - self.assertEqual(payment.release_level, "300") + # Get order test + def test_1_order_get(self): + self.assertEqual(order_get_test, self.order.get(order_transaction_id)) - def test_2_list(self): - payments = twocheckout.Payment.list() - self.assertEqual(len(payments), 0) + # Create order test + def test_2_order_create(self): + self.assertEqual('REST_API_AVANGTE', self.order.create(order_params_test)['body']['ExternalReference']) -class PassbackTest(TwocheckoutTestCase): - def setUp(self): - super(PassbackTest, self).setUp() - def test_1_check(self): - params = EXAMPLE_PASSBACK - result = twocheckout.Passback.check(params) - self.assertEqual(result.response_code, "SUCCESS") +class IpnHelperTestCase(unittest.TestCase): + ipn = None -class NotificationTest(TwocheckoutTestCase): def setUp(self): - super(NotificationTest, self).setUp() - - def test_1_check(self): - params = EXAMPLE_NOTIFICATION - result = twocheckout.Notification.check(params) - self.assertEqual(result.response_code, "SUCCESS") - -if __name__ == '__main__': - unittest.main() + super(IpnHelperTestCase, self).setUp() + self.ipn = twocheckout.IpnHelper(config.TWOCHECKOUT_TEST_MERCHANT_SECRET_KEY) + + def test_1_ipn_hash(self): + + self.assertEqual(True, self.ipn.is_valid(ipn_payload)) + + def test_2_ipn_calculate_response(self): + expected = self.calculate_ipn_response(ipn_payload) + received = self.ipn.calculate_ipn_response(ipn_payload) + + self.assertEqual(expected, received) + + def calculate_ipn_response(self, params): + now = NOW + result = '' + ipn_response = {'IPN_PID': [params['IPN_PID[]']], + 'IPN_NAME': [params['IPN_PNAME[]']], + 'IPN_DATE': params['IPN_DATE'], + 'DATE': now.strftime('%Y%m%d%H%M%S')} + + for param in ipn_response: + if type(ipn_response[param]) is list: + result += self.expand(ipn_response[param]) + else: + size = len(ipn_response[param]) + result += str(size) + ipn_response[param] + + return '' + ipn_response['DATE'] + '|' + hmac.new( + config.TWOCHECKOUT_TEST_MERCHANT_SECRET_KEY.encode(), result.encode(), + 'md5').hexdigest() + '' + + def expand(self, val_list): + result = '' + for val in val_list: + size = len(val.lstrip()) + result += str(size) + str(val.lstrip()) + return result diff --git a/twocheckout/__init__.py b/twocheckout/__init__.py index 5fa2871..3195866 100644 --- a/twocheckout/__init__.py +++ b/twocheckout/__init__.py @@ -1,11 +1,6 @@ -from sale import Sale -from api_request import Api -from util import Util -from passback import Passback -from ins import Notification -from product import Product -from contact import Contact -from company import Company -from charge import Charge -from payment import Payment -from error import TwocheckoutError \ No newline at end of file +from .api import Api +from .order import Order +from .subscription import Subscription +from .error import TwocheckoutError +from .cplus_signature import CplusSignature +from .ipn_helper import IpnHelper diff --git a/twocheckout/api.py b/twocheckout/api.py new file mode 100644 index 0000000..c4ca640 --- /dev/null +++ b/twocheckout/api.py @@ -0,0 +1,51 @@ +import hashlib +import hmac +import codecs +import datetime +import requests +import json +from .error import TwocheckoutError + + +class Api: + api_url = 'https://api.2checkout.com/rest/6.0/' + resource_url = '' + merchant_code = None + secret_key = None + + # endpoint resource + def set_resource(self, resource): + self.resource_url = resource + '/' + + def get_resource(self): + return self.resource_url + + # constructor function used to set the merchant_code & SECRET_KEY + def __init__(self, params): + self.merchant_code = str(params['merchant_code']) + self.secret_key = str(params['secret_key']) + + # set the authentication headers using the VENDOR_ID & SECRET_KEY for creating the hash + def get_headers(self): + now = str(datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")) + string = str(len(self.merchant_code)) + self.merchant_code + str(len(now)) + now + string = codecs.encode(string) + secret_key = codecs.encode(self.secret_key) + string_hash = hmac.new(secret_key, string, hashlib.md5).hexdigest() + return { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-Avangate-Authentication': 'code="' + self.merchant_code + '" date="' + now + '" hash="' + string_hash + '"' + } + + # make request to 2Checkout API and returns the response + # or throws an Error + def call(self, endpoint, params=None, method='POST'): + method.upper() + data_json = json.dumps(params) + url = self.api_url + endpoint + try: + response = requests.request(method, url, data=data_json, headers=self.get_headers()) + return response.text + except Exception as e: + raise TwocheckoutError('REQUEST_FAILED', e.args) diff --git a/twocheckout/api_request.py b/twocheckout/api_request.py deleted file mode 100755 index 2a2082a..0000000 --- a/twocheckout/api_request.py +++ /dev/null @@ -1,91 +0,0 @@ -import urllib -import urllib2 -import json -from error import TwocheckoutError - - -class Api: - - username = None - password = None - private_key = None - seller_id = None - version = '1' - - @classmethod - def credentials(cls, credentials): - Api.username = credentials['username'] - Api.password = credentials['password'] - - @classmethod - def auth_credentials(cls, credentials): - Api.private_key = credentials['private_key'] - Api.seller_id = credentials['seller_id'] - - @classmethod - def call(cls, method, params=None): - data = cls.set_opts(method, params) - url = cls.build_url(method) - headers = cls.build_headers(method) - try: - req = urllib2.Request(url, data, headers) - result = urllib2.urlopen(req).read() - result_safe=None - try: - result_safe = unicode(result) - except UnicodeDecodeError: - result_safe = unicode( str(result).decode('utf-8', 'ignore') ) - return json.loads(result_safe) - except urllib2.HTTPError, e: - if not hasattr(e, 'read'): - raise TwocheckoutError(e.code, e.msg) - else: - exception = json.loads(e.read()) - if method == 'authService': - raise TwocheckoutError(exception['exception']['errorCode'], exception['exception']['errorMsg']) - else: - raise TwocheckoutError(exception['errors'][0]['code'], exception['errors'][0]['message']) - - @classmethod - def set_opts(cls, method, params=None): - if method == 'authService': - params['sellerId'] = cls.seller_id - params['privateKey'] = cls.private_key - data = json.dumps(params) - else: - username = cls.username - password = cls.password - passwd_url = 'https://www.2checkout.com' - data = urllib.urlencode(params) - password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - password_manager.add_password( - None, passwd_url, username, password - ) - auth_handler = urllib2.HTTPBasicAuthHandler(password_manager) - opener = urllib2.build_opener(auth_handler) - urllib2.install_opener(opener) - return data - - @classmethod - def build_headers(cls, method): - if method == 'authService': - headers = { - 'Accept': 'application/json', - 'User-Agent': '2Checkout Python/0.1.0/%s', - 'Content-Type': 'application/JSON' - } - else: - headers = { - 'Accept': 'application/json', - 'User-Agent': '2Checkout Python/0.1.0/%s' - } - return headers - - @classmethod - def build_url(cls, method): - url = 'https://www.2checkout.com' - if method == 'authService': - url += '/checkout/api/' + cls.version + '/' + cls.seller_id + '/rs/' + method - else: - url += '/api/' + method - return url diff --git a/twocheckout/charge.py b/twocheckout/charge.py deleted file mode 100755 index 6766561..0000000 --- a/twocheckout/charge.py +++ /dev/null @@ -1,61 +0,0 @@ -import urllib -from api_request import Api -from twocheckout import Twocheckout - - -class Charge(Twocheckout): - - checkout_url = "https://www.2checkout.com/checkout/purchase" - - def __init__(self, dict_): - super(self.__class__, self).__init__(dict_) - - @classmethod - def mode(cls, mode): - if mode == 'sandbox': - Charge.checkout_url = 'https://sandbox.2checkout.com/checkout/purchase' - else: - Charge.checkout_url = 'https://www.2checkout.com/checkout/purchase' - - @classmethod - def form(cls, params=None, text='Proceed to Checkout'): - if params is None: - params = dict() - form = "
" - for param in params: - form = form + "" - return form + "
" - - @classmethod - def submit(cls, params=None, text='Proceed to Checkout'): - if params is None: - params = dict() - form = "
" - for param in params: - form = form + "" - return form + "
" + \ - "" - - @classmethod - def direct(cls, params=None, text='Proceed to Checkout'): - if params is None: - params = dict() - form = "
" - for param in params: - form = form + "" - return form + "
" + \ - "" - - @classmethod - def link(cls, params=None): - url = Charge.checkout_url + '?' - if params is None: - params = dict() - param = urllib.urlencode(params) - url = url.endswith('?') and (url + param) - return url - - @classmethod - def authorize(cls, params=None): - response = Charge(Api.call('authService', params)) - return response.response \ No newline at end of file diff --git a/twocheckout/company.py b/twocheckout/company.py deleted file mode 100644 index a8adf12..0000000 --- a/twocheckout/company.py +++ /dev/null @@ -1,16 +0,0 @@ -from api_request import Api -from twocheckout import Twocheckout - - -class Company(Twocheckout): - def __init__(self, dict_): - super(self.__class__, self).__init__(dict_) - - @classmethod - def retrieve(cls, params=None): - if params is None: - params = dict() - url = 'acct/detail_company_info' - response = cls(Api.call(url, params)) - return response.vendor_company_info - diff --git a/twocheckout/contact.py b/twocheckout/contact.py deleted file mode 100644 index 15f70fd..0000000 --- a/twocheckout/contact.py +++ /dev/null @@ -1,15 +0,0 @@ -from api_request import Api -from twocheckout import Twocheckout - - -class Contact(Twocheckout): - def __init__(self, dict_): - super(self.__class__, self).__init__(dict_) - - @classmethod - def retrieve(cls, params=None): - if params is None: - params = dict() - url = 'acct/detail_contact_info' - response = cls(Api.call(url, params)) - return response.vendor_contact_info diff --git a/twocheckout/cplus_signature.py b/twocheckout/cplus_signature.py new file mode 100644 index 0000000..1df8da7 --- /dev/null +++ b/twocheckout/cplus_signature.py @@ -0,0 +1,31 @@ +import time +import requests +import jwt +import json +from .error import TwocheckoutError + + +class CplusSignature: + SIGNATURE_URL = 'https://secure.2checkout.com/checkout/api/encrypt/generate/signature' + + def get_signature(self, merchant_id, buylink_secret_word, json_encoded_convert_plus_parameters, token_expiration=None): + if not token_expiration: + token_expiration = 3600 + jwt_token = jwt.encode({ + 'sub': merchant_id, + 'iat': str(time.time()).split('.')[0], + 'exp': str(time.time() + token_expiration).split('.')[0] + }, buylink_secret_word, algorithm='HS512') + + headers = {'Content-Type': 'application/json', 'Accept': 'application/json', 'merchant-token': jwt_token} + try: + response = requests.request('POST', self.SIGNATURE_URL, data=json_encoded_convert_plus_parameters, + headers=headers) + except Exception as e: + raise TwocheckoutError('REQUEST_FAILED', e.args) + + try: + signature = json.loads(response.text) + return signature['signature'] + except Exception as e: + raise ValueError('Unable to decode server response') diff --git a/twocheckout/error.py b/twocheckout/error.py index 7e11312..12c4073 100644 --- a/twocheckout/error.py +++ b/twocheckout/error.py @@ -1,8 +1,5 @@ class TwocheckoutError(Exception): - def __init__(self, code=None, msg=None, product_id=None, option_id=None, coupon_code=None): + def __init__(self, code=None, msg=None): super(TwocheckoutError, self).__init__(msg) self.code = code self.msg = msg - self.product_id = product_id - self.option_id = option_id - self.coupon_code = coupon_code diff --git a/twocheckout/ins.py b/twocheckout/ins.py deleted file mode 100644 index 2c57084..0000000 --- a/twocheckout/ins.py +++ /dev/null @@ -1,40 +0,0 @@ -import hashlib -from twocheckout import Twocheckout - -class Notification(Twocheckout): - def __init__(self, dict_): - super(self.__class__, self).__init__(dict_) - - @classmethod - def check_hash(cls, params=None): - m = hashlib.md5() - m.update(params['sale_id']) - m.update(params['vendor_id']) - m.update(params['invoice_id']) - m.update(params['secret']) - check_hash = m.hexdigest() - check_hash = check_hash.upper() - if check_hash == params['md5_hash']: - return True - else: - return False - - @classmethod - def check(cls, params=None): - if params is None: - params = dict() - if 'sale_id' in params and 'invoice_id' in params: - check = Notification.check_hash(params) - if check: - response = { "response_code": "SUCCESS", - "response_message": "Hash Matched" - } - else: - response = { "response_code": "FAILED", - "response_message": "Hash Mismatch" - } - else: - response = { "response_code": "ERROR", - "response_message": "You must pass sale_id, vendor_id, invoice_id, secret word." - } - return cls(response) \ No newline at end of file diff --git a/twocheckout/ipn_helper.py b/twocheckout/ipn_helper.py new file mode 100644 index 0000000..1c8aa03 --- /dev/null +++ b/twocheckout/ipn_helper.py @@ -0,0 +1,66 @@ +import hmac +from .error import TwocheckoutError +from datetime import datetime + + +# more information on calculating the IPN HASH signature can be found here +# https://knowledgecenter.2checkout.com/API-Integration/Webhooks/06Instant_Payment_Notification_(IPN)/Calculate-the-IPN-HASH-signature#PHP_Hash_Example +class IpnHelper: + secret_key = None + + def __init__(self, secret): + self.secret_key = secret + + # check if the received request is a valid one + def is_valid(self, params): + if self.secret_key is None: + raise TwocheckoutError('SECRET KEY MISSING', 'You must pass the secret key to the constructor class') + + try: + result = '' + receivedHash = params['HASH'] + for param in params: + if param != 'HASH': + var_type = type(params[param]) + if var_type is list: + result += self.expand(params[param]) + else: + size = str(len(params[param].lstrip())) + result += size + params[param].lstrip() + try: + calcHash = hmac.new(self.secret_key.encode(), result.encode(), 'md5').hexdigest() + return receivedHash == calcHash + except Exception as e: + raise TwocheckoutError('Hash signatures do not match', e.args) + + except Exception as error: + raise TwocheckoutError('Exception validating ipn signature', error.args) + + def calculate_ipn_response(self, params): + try: + now = datetime.now() + result = '' + ipn_response = {'IPN_PID': [params['IPN_PID[]']], + 'IPN_NAME': [params['IPN_PNAME[]']], + 'IPN_DATE': params['IPN_DATE'], + 'DATE': now.strftime('%Y%m%d%H%M%S')} + + for param in ipn_response: + if type(ipn_response[param]) is list: + result += self.expand(ipn_response[param]) + else: + size = len(ipn_response[param]) + result += str(size) + ipn_response[param] + + return '' + ipn_response['DATE'] + '|' + hmac.new(self.secret_key.encode(), result.encode(), 'md5').hexdigest() + '' + + except Exception as e: + raise TwocheckoutError('Exception generating ipn response', e.args) + + @classmethod + def expand(cls, val_list): + result = '' + for val in val_list: + size = len(val.lstrip()) + result += str(size) + str(val.lstrip()) + return result diff --git a/twocheckout/order.py b/twocheckout/order.py new file mode 100644 index 0000000..8117bb2 --- /dev/null +++ b/twocheckout/order.py @@ -0,0 +1,25 @@ +from twocheckout import Api +from twocheckout import response + + +# full docs about order here: https://app.swaggerhub.com/apis-docs/2Checkout-API/api-rest_documentation/6.0#/Order +class Order(Api): + + def __init__(self, params): + Api.__init__(self, params) + self.set_resource('orders') + + # get an order full info bu 2Checkout transaction ID (RefNo) + # more info here: https://app.swaggerhub.com/apis-docs/2Checkout-API/api-rest_documentation/6.0#/Order/get_orders__OrderReference__ + def get(self, ref_no): + result = self.call(self.get_resource() + ref_no + '/', None, 'get') + + return response.parse(result) + + # place a new order using the params provided + # for more information about the params you can visit our docs + # https://app.swaggerhub.com/apis-docs/2Checkout-API/api-rest_documentation/6.0#/Order/post_orders_ + def create(self, data): + result = self.call(self.get_resource(), data) + + return response.parse(result) diff --git a/twocheckout/passback.py b/twocheckout/passback.py deleted file mode 100644 index a95c1df..0000000 --- a/twocheckout/passback.py +++ /dev/null @@ -1,40 +0,0 @@ -import hashlib -from twocheckout import Twocheckout - -class Passback(Twocheckout): - def __init__(self, dict_): - super(self.__class__, self).__init__(dict_) - - @classmethod - def check_hash(cls, params=None): - m = hashlib.md5() - m.update(params['secret']) - m.update(params['sid']) - m.update(params['order_number']) - m.update(params['total']) - check_hash = m.hexdigest() - check_hash = check_hash.upper() - if check_hash == params['key']: - return True - else: - return False - - @classmethod - def check(cls, params=None): - if params is None: - params = dict() - if 'order_number' in params and 'total' in params: - check = Passback.check_hash(params) - if check: - response = { "response_code": "SUCCESS", - "response_message":"Hash Matched" - } - else: - response = { "response_code": "FAILED", - "response_message": "Hash Mismatch" - } - else: - return { "response_code": "ERROR", - "response_message": "You must pass secret word, sid, order_number, total" - } - return cls(response) diff --git a/twocheckout/payment.py b/twocheckout/payment.py deleted file mode 100644 index 6b24802..0000000 --- a/twocheckout/payment.py +++ /dev/null @@ -1,23 +0,0 @@ -from .api_request import Api -from .twocheckout import Twocheckout - - -class Payment(Twocheckout): - def __init__(self, dict_): - super(self.__class__, self).__init__(dict_) - - @classmethod - def pending(cls, params=None): - if params is None: - params = dict() - url = 'acct/detail_pending_payment' - response = cls(Api.call(url, params)) - return response.payment - - @classmethod - def list(cls, params=None): - if params is None: - params = dict() - url = 'acct/list_payments' - response = cls(Api.call(url, params)) - return response.payments diff --git a/twocheckout/product.py b/twocheckout/product.py deleted file mode 100644 index 41f1ab4..0000000 --- a/twocheckout/product.py +++ /dev/null @@ -1,41 +0,0 @@ -from api_request import Api -from twocheckout import Twocheckout - - -class Product(Twocheckout): - def __init__(self, dict_): - super(self.__class__, self).__init__(dict_) - - @classmethod - def create(cls, params=None): - if params is None: - params = dict() - return cls(Api.call('products/create_product', params)) - - @classmethod - def find(cls, params=None): - if params is None: - params = dict() - result = cls(Api.call('products/detail_product', params)) - return result.product - - @classmethod - def list(cls, params=None): - if params is None: - params = dict() - result = cls(Api.call('products/list_products', params)) - return result.products - - def update(self, params=None): - if params is None: - params = dict() - params['product_id'] = self.product_id - Api.call('products/update_product', params) - product = Product(Api.call('products/detail_product', params)) - return product.product - - def delete(self, params=None): - if params is None: - params = dict() - params['product_id'] = self.product_id - return Product(Api.call('products/delete_product', params)) diff --git a/twocheckout/response.py b/twocheckout/response.py new file mode 100644 index 0000000..d288070 --- /dev/null +++ b/twocheckout/response.py @@ -0,0 +1,24 @@ +import json + + +# helper file for parsing response from 2Checkout API in order to return an standard response +def parse(data): + # if invalid JSON, exception is thrown + data_params = json.loads(data) + + if 'error_code' in data_params: + response = {'meta': {'status': 'fail', 'message': data_params['message']}, 'body': data_params} + elif 'Errors' in data_params and data_params['Errors'] is not None: + message = '' + for key in data_params['Errors']: + message += data_params['Errors'][key] + + response = {'meta': {'status': 'fail', 'message': message}, 'body': data_params} + else: + response = {'meta': {'status': 'success', 'message': 'ok'}, 'body': data_params} + + return response + + +def get(data): + return json.loads(data) diff --git a/twocheckout/sale.py b/twocheckout/sale.py deleted file mode 100644 index 7efe31c..0000000 --- a/twocheckout/sale.py +++ /dev/null @@ -1,97 +0,0 @@ -from api_request import Api -from util import Util -from twocheckout import Twocheckout - - -class Sale(Twocheckout): - def __init__(self, dict_): - super(self.__class__, self).__init__(dict_) - - @classmethod - def find(cls, params=None): - if params is None: - params = dict() - response = cls(Api.call('sales/detail_sale', params)) - return response.sale - - @classmethod - def list(cls, params=None): - if params is None: - params = dict() - response = cls(Api.call('sales/list_sales', params)) - return response.sale_summary - - def refund(self, params=None): - if params is None: - params = dict() - if hasattr(self, 'lineitem_id'): - params['lineitem_id'] = self.lineitem_id - url = 'sales/refund_lineitem' - elif hasattr(self, 'invoice_id'): - params['invoice_id'] = self.invoice_id - url = 'sales/refund_invoice' - else: - params['sale_id'] = self.sale_id - url = 'sales/refund_invoice' - return Sale(Api.call(url, params)) - - def stop(self, params=None): - if params is None: - params = dict() - if hasattr(self, 'lineitem_id'): - params['lineitem_id'] = self.lineitem_id - return Api.call('sales/stop_lineitem_recurring', params) - elif hasattr(self, 'sale_id'): - active_lineitems = Util.active(self) - if dict(active_lineitems): - result = dict() - i = 0 - for k, v in active_lineitems.items(): - lineitem_id = v - params = {'lineitem_id': lineitem_id} - result[i] = Api.call('sales/stop_lineitem_recurring', params) - i += 1 - response = { "response_code": "OK", - "response_message": str(len(result)) + " lineitems stopped successfully" - } - else: - response = { - "response_code": "NOTICE", - "response_message": "No active recurring lineitems" - } - else: - response = { "response_code": "NOTICE", - "response_message": "This method can only be called on a sale or lineitem" - } - return Sale(response) - - def active(self): - active_lineitems = Util.active(self) - if dict(active_lineitems): - result = dict() - i = 0 - for k, v in active_lineitems.items(): - lineitem_id = v - result[i] = lineitem_id - i += 1 - response = { "response_code": "ACTIVE", - "response_message": str(len(result)) + " active recurring lineitems" - } - else: - response = { - "response_code": "NOTICE","response_message": - "No active recurring lineitems" - } - return Sale(response) - - def comment(self, params=None): - if params is None: - params = dict() - params['sale_id'] = self.sale_id - return Sale(Api.call('sales/create_comment', params)) - - def ship(self, params=None): - if params is None: - params = dict() - params['sale_id'] = self.sale_id - return Sale(Api.call('sales/mark_shipped', params)) diff --git a/twocheckout/subscription.py b/twocheckout/subscription.py new file mode 100644 index 0000000..8993bb7 --- /dev/null +++ b/twocheckout/subscription.py @@ -0,0 +1,25 @@ +from twocheckout import Api +from twocheckout import response + + +# full docs about order here: https://app.swaggerhub.com/apis-docs/2Checkout-API/api-rest_documentation/6.0#/Order +class Subscription(Api): + + def __init__(self, params): + Api.__init__(self, params) + self.set_resource('subscriptions') + + # get an order full info bu 2Checkout transaction ID (RefNo) + # more info here: https://app.swaggerhub.com/apis-docs/2Checkout-API/api-rest_documentation/6.0#/Subscription/get_subscriptions__SubscriptionReference__ + def get(self, subscription_reference): + result = self.call(self.get_resource() + subscription_reference + '/', None, 'get') + + return response.parse(result) + + # place a new order using the params provided + # for more information about the params you can visit our docs + # https://app.swaggerhub.com/apis-docs/2Checkout-API/api-rest_documentation/6.0#/Subscription/post_subscriptions_ + def create(self, data): + result = self.call(self.get_resource(), data) + + return response.parse(result) diff --git a/twocheckout/twocheckout.py b/twocheckout/twocheckout.py deleted file mode 100644 index d57c850..0000000 --- a/twocheckout/twocheckout.py +++ /dev/null @@ -1,14 +0,0 @@ -class Twocheckout(dict): - def __init__(self, dict_): - super(Twocheckout, self).__init__(dict_) - for key in self: - item = self[key] - if isinstance(item, list): - for id, it in enumerate(item): - if isinstance(it, dict): - item[id] = self.__class__(it) - elif isinstance(item, dict): - self[key] = self.__class__(item) - - def __getattr__(self, key): - return self[key] diff --git a/twocheckout/util.py b/twocheckout/util.py deleted file mode 100644 index ec1c4e2..0000000 --- a/twocheckout/util.py +++ /dev/null @@ -1,21 +0,0 @@ -class Util: - - @classmethod - def active(cls, sale): - i = 0 - if hasattr(sale, 'recurring'): - invoice = sale - else: - invoices = dict() - sale = sale.invoices - for invoice in sale: - invoices[i] = invoice - i += 1 - invoice = max(invoices.values()) - i = 0 - lineitems = dict() - for lineitem_id in invoice.lineitems: - if lineitem_id.billing.recurring_status == 'active': - lineitems[i] = lineitem_id['lineitem_id'] - i += 1 - return lineitems \ No newline at end of file