Skip to content
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

This is one step in fixing issue #88 #148

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

meatherly
Copy link

It implements the GET '/v1/customers/{id}/sources' endpoint for a customer as described here: https://stripe.com/docs/api/cards/list

Also I've never written python before 😬

Things I still need to implement is supporting the object param but I'm not really sure where to start with that one. Please feel free to give me some feedback on this one.

It implements the /sources endpoint for a customer
Copy link
Owner

@adrienverge adrienverge left a comment

Choose a reason for hiding this comment

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

Good job for a first Python contribution 👍

About the object parameter, it's OK to implement it in a future PR, if you want. For example you could take inspiration from source in:

def _api_add_source(cls, id, source=None, **kwargs):
if kwargs:
raise UserError(400, 'Unexpected ' + ', '.join(kwargs.keys()))
try:
if type(source) is str:
assert source[:4] in ('src_', 'tok_')
else:
assert type(source) is dict
except AssertionError:
raise UserError(400, 'Bad request')

localstripe/resources.py Outdated Show resolved Hide resolved
test.sh Show resolved Hide resolved
@meatherly
Copy link
Author

What would you suggest for filtering out sources on their type for the object type param? Would I just initialize a new List then filter out the obj.sources based on their prefix?

something like: (pseudo code)

returnList = List('/v1/customers/' + self.id + '/sources')
returnList._list = obj.sources.filter source.startswith(object)

@meatherly
Copy link
Author

Looking at their docs it looks like object param can only be card or bank_account

Copy link
Owner

@adrienverge adrienverge left a comment

Choose a reason for hiding this comment

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

Hey @meatherly,

To save time, I suggest looking at other functions (inside the Customer class, but also other listing methods from other classes). Then you could take inspiration from other parts of the code that list + filter, for example

if customer:
li._list = [c for c in li._list if c.customer == customer]
if created and created.get('gt'):
li._list = [c for c in li._list
if c.created > try_convert_to_int(created['gt'])]
or
if customer is not None:
Customer._api_retrieve(customer) # to return 404 if not existant
li._list = [i for i in li._list if i.customer == customer]
if subscription is not None:
# to return 404 if not existant
Subscription._api_retrieve(subscription)
li._list = [i for i in li._list if i.subscription == subscription]
li._list.sort(key=lambda i: i.date, reverse=True)

And inside Customer._api_list_sources, maybe something simple can be done, e.g.

return Refund._api_list_all('/v1/charges/' + self.id + '/refunds',
charge=self.id)
? (I haven't tested)

Looking at their docs it looks like object param can only be card or bank_account

Right, so you need to validate the input. E.g.:

        try:
            if object is not None:
                assert object in ('card', 'bank_account')
        except AssertionError:
            raise UserError(400, 'Bad request')

(again, you can look and copy existing code)

@@ -678,6 +678,11 @@ def _api_delete(cls, id):
schedule_webhook(Event('customer.deleted', obj))
return super()._api_delete(id)

@classmethod
def _api_retrieve_sources(cls, id):
obj = cls._api_retrieve(id) # return 404 if does not exist
Copy link
Owner

Choose a reason for hiding this comment

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

Does this code really retrieve a list of sources that belong to a customer?

Copy link
Author

Choose a reason for hiding this comment

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

I believe so because the sources are set here:

self.sources = List('/v1/customers/' + self.id + '/sources')

And then updated here:

obj.sources._list.append(source_obj)

and:
obj.sources._list.remove(source_obj)

So I'm just returning that value from the customer object

localstripe/resources.py Outdated Show resolved Hide resolved
@meatherly
Copy link
Author

Man can I just say you're awesome for creating this and for being patient with me as I'm working on this!!

overriding Card._api_list_all to filter only customer cards if param is supplied
@meatherly
Copy link
Author

meatherly commented Aug 13, 2020

Alright went off the beaten path did an override on Card._api_list_all to allow filtering of customer cards. If you're not a fan of this I can move that logic into the Customer._api_list_sources.

Let me know what you think

Also not sure why the build keeps failing 😬

Copy link
Owner

@adrienverge adrienverge left a comment

Choose a reason for hiding this comment

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

Alright went off the beaten path did an override on Card._api_list_all to allow filtering of customer cards. If you're not a fan of this I can move that logic into the Customer._api_list_sources.

Let me know what you think

It's interesting, but it would provide a new API route to list all cards, for every customer (GET /v1/cards). But this API doesn't exist for real (for security reasons maybe?). So it would be better in Customer._api_list_sources 👍

Also not sure why the build keeps failing

You can see errors by clicking on the Travis link -- I think some of your code doesn't pass linting.

if customer:
Customer._api_retrieve(customer) # to return 404 if not existant

li = super(Card, cls)._api_list_all(url, limit=limit)
Copy link
Owner

Choose a reason for hiding this comment

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

I think super() is enough.

Anyway, if you move this code inside the Customer class, you can simply call Card._api_list_all() which is even clearer 😉

@@ -678,6 +694,28 @@ def _api_delete(cls, id):
schedule_webhook(Event('customer.deleted', obj))
return super()._api_delete(id)

@classmethod
def _api_list_sources(cls, id, object=None, limit=10):
print(object)
Copy link
Owner

Choose a reason for hiding this comment

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

Please don't include this in final code.

Copy link
Author

Choose a reason for hiding this comment

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

Good catch!

Comment on lines +708 to +715
url = '/v1/customers/' + obj.id + '/sources'

if object is 'card':
return Card._api_list_all(url, customer=obj.id)

#TODO: implement bank accounts
if object is 'bank_account':
return List(url, limit=limit)
Copy link
Owner

Choose a reason for hiding this comment

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

I propose to simplify all this and don't use the object argument. Just check it, but don't use it, and return all sources. What do you think?

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