Skip to content

Add support for Paddles new Sandbox environment #4

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 26, 2021

Conversation

Wolf-Byte
Copy link
Contributor

With the release of Paddles much awaited sandbox environment I have updated the package to add sandbox support.

  • When initialising PaddleClient you can now pass the parameter 'sandbox' (default=None)
  • If the parameter is not set (equals None) the sandbox variable is loaded from the new environment variable 'PADDLE_SANDBOX'. The variable must equal "True" to enable sandbox via the environment variable.

All tests have been updated to support the sandbox environment.

I found several issues with the tests so I included my fixes in the PR. Hopefully this will help any future contributors.

Notes:

  • The last commit (cba3b5f) includes the feature request and all prior commits include the test fixes.
  • All tests where run agains the sandbox environment which paddle state is identical to the production environment. If you could please run the tests on your production environment that would be great. Unfortunately I am unable to do this on our production environment. I have, however, contacted Paddle to request a new live account so I can run live tests in the future.

Read more about the sandbox environment here:
https://developer.paddle.com/getting-started/sandbox

Tests passed
68 passed, 4 skipped, 1 xpassed, 2 warnings in 53.15s

… not return a 'plan' according to the Schema, this resulted in a KeyError. Please see documentation for the Schema https://developer.paddle.com/api-reference/subscription-api/payments/listpayments
…e amount. The assert is then used to compare the expected amount (new_quantity * amount) with the next_payment amount returned from preview_update_subscription.
…as the passthrough value can be null which raises an AssertionError. You may want to remove the check entirely instead of having an OR statement.

https://developer.paddle.com/api-reference/product-api/transactions/listtransactions
…on checks on the passthrough value I found the list_transactions was not failing when a subscription was being passed (despite the passthrough returning null in postman). After investigating I found the plan_id (PADDLE_TEST_DEFAULT_PLAN_ID) was being passed as the subscription_id. In this instance the paddle API responds with {"success": true, "response": []} as the response is an empty list test never failed. Instead of passing a plan_id we should pass a subscription_id.
… avoid error "amount is less than allowed minimum transaction amount" (Paddle error 186). Also fix assertion check on the amount as paddle returns the value to three decimal places.
Add constructor parameter 'sandbox' to PaddleClient to enable sandbox mode
If the parameter 'sandbox' is not passed in the constructor the option is loaded form the Environment variable 'PADDLE_SANDBOX' (default=False)
Add get_environment_url(self, url) function to PaddleClient() which checks if sandbox has been enabled and if it had it prepends 'sandbox-' to the subdomain. Returns url
Update PaddleClient request() to get the environment url after building and validating the url
Update all tests to call paddle_client.get_environment_url()
@pyepye
Copy link
Member

pyepye commented Mar 15, 2021

Hi @Wolf-Byte

Sorry about the lack of reply, I had totally missed the GitHub notification.

I had also missed the fact Paddle had released a sandbox environment, that is great news.

Please give me a day or 2 to review the code and re-setup my production environment to ensure the tests run.

Thank you for for the PR and your hard work.

Copy link
Member

@pyepye pyepye left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all the work so far. I've requested a few changes based on me running the tests against my production environment.
Hopefully the changes requested mean the tests pass for you as well. The problem is the test setup is a pain making the tests a little flakey.

With that in mind...

Looking at how the Paddle sandbox environment works it looks like the best approach for the long term will be to update the tests to use the sandbox environment instead of requiring / assuming the person testing this package has set up their prod environment correctly (which is a pain right now).

I think the current plan should be to make the tests only run against the sandbox environment:

  1. Create a sandbox account for this package
  2. Add fixtures to create plans and subscriptions etc - basically remove the need for the environmental variables like PADDLE_TEST_DEFAULT_SUBSCRIPTION_ID. They should be idempotent and only create new things if old ones have been deleted for whatever reason
  3. Create some manual payments, subscriptions and one-off purchases that can be used in the future
  4. Remove the mock tests and make the none mock / skipped tests the default and run all the time
  5. Update the docs on how to easily make new payments, subscriptions and one-off purchases based on the plans and subscriptions created by the tests if the correct data does not exist

I understand the above is quite a bit of work and don't expect you to do it, I'm more stating my current intention / next steps to remove some of issues with making the tests run and pass for everyone that tries.

As I have requested some changes to this pull request but with the above in mind I will also be working on the tests and the code you have written, I wanted to give you a choice on next steps for this pull request on beyond:

  1. Leave everything how it is now with a big thanks from me. I'll fix up this pull request while I implement the above
  2. You can fix up the comments, we can get this PR merged for now and I'll make the changes above in my own time
  3. You can help with the above as well - this really isn't expected I just wanted to give you the option incase you are interested

Please let me know which one you would like to do. And again thank you very much for everything so far. Hopefully it's clear that I appreciate the effort and there is no pressure for you to continue you current contribution if you don't want to.

@@ -53,7 +54,11 @@ def __init__(self, vendor_id: int = None, api_key: str = None):
api_key = os.environ['PADDLE_API_KEY']
except KeyError:
raise ValueError('API key not set')
if sandbox is None:
# Load sandbox flag from environment if not set in the constructor
sandbox = os.getenv('PADDLE_SANDBOX', False) == 'True'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this please be updated to use distutils strtobool? It would just mean lowercase values would also be valid as the env var:

from distutils.util import strtobool

sandbox = bool(strtobool(os.getenv('PADDLE_SANDBOX', 'False')))

It also means is is True can be removed from the self.is_sandbox line.

@@ -40,7 +40,8 @@ class PaddleClient():
called ``PADDLE_VENDOR_ID`` and ``PADDLE_API_KEY``
"""

def __init__(self, vendor_id: int = None, api_key: str = None):
def __init__(self, vendor_id: int = None, api_key: str = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only small but could each kwarg be moved onto a separate line? Just to keep the styling the same as the rest of the code base

def __init__(
        self,
        vendor_id: int = None,
        api_key: str = None,
        sandbox: bool = None,
    ):

@@ -94,6 +99,7 @@ def request(
warnings.warn(warning_message, RuntimeWarning)
if 'paddle.com/api/' not in url:
raise ValueError('URL "{0}" does not appear to be a Paddle API URL') # NOQA: E501
url = self.get_environment_url(url)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be done on a single line

kwargs['url'] = self.get_environment_url(url)

@@ -60,9 +60,6 @@ def test_list_subscription_payments_with_plan_id(paddle_client): # NOQA: F811
skip_message = ('list_subscription_payments did not return any subscription payments') # NOQA: E501
pytest.skip(skip_message)

for payment in response:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like Paddle has stopped including the plan even when it's sent in the body of the request.
I think it's still a good idea to check the response so can we do:

for payment in response:
        assert 'id' in payment
        assert 'amount' in payment
        assert 'currency' in payment

I've just had a look at each payment in the response has the below keys:

[
...
{
  'amount': 0,
  'currency': 'USD',
  'id': 1234,
  'is_one_off_charge': False,
  'is_paid': 0,
  'payout_date': '2021-03-20',
  'subscription_id': 12345
}
...
]

But I think just using making sure id, amount and currency is safe as they are very unlikely to be removed.

@@ -338,9 +342,11 @@ def test_preview_subscription_update(mocker, paddle_client): # NOQA: F811
pytest.skip(skip_message)

subscription_id = subscription_data['subscription_id']
quantity = subscription_data['quantity']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my subscription this quantity key did not exist which made this test fail.

This is the contents of my subscription_data dict:

{'cancel_url': 'https://checkout.paddle.com/subscription/cancel?user=1234&subscription=1234&hash=aa123fakehash321aa',
 'last_payment': {'amount': 0, 'currency': 'USD', 'date': '2021-03-19'},
 'linked_subscriptions': [],
 'marketing_consent': False,
 'next_payment': {'amount': 0, 'currency': 'USD', 'date': '2021-03-20'},
 'payment_information': {'card_type': 'master',
                         'expiry_date': '12/2023',
                         'last_four_digits': '9999',
                         'payment_method': 'card'},
 'plan_id': 1234,
 'signup_date': '2021-03-19 19:54:48',
 'state': 'active',
 'subscription_id': 1234,
 'update_url': 'https://checkout.paddle.com/subscription/update?user=1234&subscription=1234&hash=aa123fakehash321aa',
 'user_email': '[email protected]',
 'user_id': 1234}

@@ -64,7 +65,7 @@ def test_create_one_off_charge_no_mock(mocker, paddle_client): # NOQA: F811
assert isinstance(response['currency'], str)
assert isinstance(response['receipt_url'], str)
assert response['subscription_id'] == subscription_id
assert response['amount'] == '%.2f' % round(amount, 2)
assert response['amount'] == '%.3f' % round(amount, 2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe this is correct:

>>> amount = 12.34
>>> '%.3f' % round(amount, 2)
'12.340'
>>> '%.2f' % round(amount, 2)
'12.34'

@@ -54,7 +55,7 @@ def test_create_one_off_charge(mocker, paddle_client): # NOQA: F811
@pytest.mark.skip()
def test_create_one_off_charge_no_mock(mocker, paddle_client): # NOQA: F811
subscription_id = int(os.environ['PADDLE_TEST_DEFAULT_SUBSCRIPTION_ID'])
amount = 0.01
amount = 1.00
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is set to the lowest amount possible as it actually creates a one off charge when it's not skipped.

amount = subscription_data['next_payment']['amount']
currency = subscription_data['next_payment']['currency']
new_quantity = amount + 1
new_quantity = quantity + 1
Copy link
Member

@pyepye pyepye Mar 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice spot on this test was incorrect, although as bill_immediately being set for preview_update_subscription I believe your fix is a little off as well. I think it should have been:

assert response['immediate_payment']['amount']) == expected_amount

Saying that

It turns out quantity for preview_update_subscription must be 1 and only passed as the amount for my subscription had luckily been set to 0.00...

If it's 0 you get Paddle error 116 - One or more required arguments are missing
If it's > 1 you get Paddle error 173 - The subscription does not allow quantities to be set.

That makes this endpoint and test pretty much pointless...

On that note I decided to check the docs for the endpoint and it turns out they have been removed.
So with that in mind I think we should probably remove the preview_update_subscription method and any test that uses it.

If you could revert these changes for now I'll delete them all separately so you don't have to bother.

@@ -157,6 +163,12 @@ def post(self, url, **kwargs):
kwargs['method'] = 'POST'
return self.request(**kwargs)

def get_environment_url(self, url):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a test for this function to ensure each of the urls are all updated as expected when passed through this method?

self.checkout_v1 = 'https://checkout.paddle.com/api/1.0/'
self.checkout_v2 = 'https://checkout.paddle.com/api/2.0/'
self.checkout_v2_1 = 'https://vendors.paddle.com/api/2.1/'
self.vendors_v2 = 'https://vendors.paddle.com/api/2.0/'

@pyepye pyepye merged commit cba3b5f into paddle-python:master Mar 26, 2021
@pyepye
Copy link
Member

pyepye commented Mar 26, 2021

Hi @Wolf-Byte

I've had a few hours spare over the last week so I decided to take a run at the above.

Your changes in this pull request have now been merged into master via another pull request with some additional changes - #6

We now only have 1 test which requires any kind of mocking and more importantly we have been able to really simplify the test setup for this package which is fantastic.

A new version of this package (1.0.0) has been released to Pypi which includes the support for the sandbox environment 🎉

Thanks again for your help

@Wolf-Byte
Copy link
Contributor Author

@pyepye Sorry for the delay in getting back to you surrounding the PR, we have had a product launch and I fell behind on my GitHub notifications.

Thank you for soldiering on with the changes and releasing an update to Pypi. I will take a proper look over your notes and adjustments in the week.

@pyepye
Copy link
Member

pyepye commented Mar 31, 2021

@Wolf-Byte No worries at all. I just had some free time so decided to plow on with the changes.

If you have any question just let me know on here.

And I hope the product launch went well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants