-
Notifications
You must be signed in to change notification settings - Fork 4
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
Conversation
… 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 is_one_off=False. Documentation shows is_one_off can be True or False https://developer.paddle.com/api-reference/product-api/transactions/listtransactions
…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()
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. |
There was a problem hiding this 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:
- Create a sandbox account for this package
- 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 - Create some manual payments, subscriptions and one-off purchases that can be used in the future
- Remove the mock tests and make the none mock / skipped tests the default and run all the time
- 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:
- Leave everything how it is now with a big thanks from me. I'll fix up this pull request while I implement the above
- 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
- 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' |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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'] |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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/'
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 |
@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. |
@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. |
With the release of Paddles much awaited sandbox environment I have updated the package to add sandbox support.
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:
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