From facf13c6422f4d92cb976f3ec51ff5a354ee4e24 Mon Sep 17 00:00:00 2001 From: Adrian Lungu Date: Mon, 18 May 2020 18:18:11 +0300 Subject: [PATCH 01/17] Support trialing subscriptions --- localstripe/resources.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/localstripe/resources.py b/localstripe/resources.py index c3553da1..c5615760 100644 --- a/localstripe/resources.py +++ b/localstripe/resources.py @@ -932,6 +932,9 @@ def __init__(self, customer=None, subscription=None, metadata=None, else: self.currency = 'eur' # arbitrary default + if subscription is not None and subscription_obj.status == "trialing": + self.lines = List() + self._draft = True self._voided = False @@ -2382,10 +2385,15 @@ def __init__(self, customer=None, metadata=None, items=None, quantity=items[0]['quantity'], tax_rates=items[0]['tax_rates'])) - create_an_invoice = \ - self.trial_end is None and self.trial_period_days is None - if create_an_invoice: - self._create_invoice() + is_trial = \ + self.trial_end is not None and self.trial_end >= int(time.time()) + + if is_trial: + self.trial_start = int(time.time()) + self.status = 'trialing' + + # if subscription is in trial, a 0 € should still be created + self._create_invoice() schedule_webhook(Event('customer.subscription.created', self)) @@ -2424,6 +2432,9 @@ def _create_invoice(self): if invoice.status == 'paid': self.status = 'active' + + if self.trial_end is not None and self.trial_end >= int(time.time()): + self.status = 'trialing' elif invoice.charge: if invoice.charge.status == 'failed': if self.status != 'incomplete': @@ -2592,6 +2603,8 @@ def _update(self, metadata=None, items=None, trial_end=None, if trial_end is not None: self.trial_end = trial_end + if trial_end > int(time.time()): + self.status = "trialing" if cancel_at_period_end is not None: self.cancel_at_period_end = cancel_at_period_end From e1a9580cb0981f7428a56fd4ae4d7f7e39449367 Mon Sep 17 00:00:00 2001 From: Hoel IRIS Date: Fri, 29 May 2020 16:46:17 +0200 Subject: [PATCH 02/17] refactor: Rename a variable to fix lint rule E741 Log of the CI before this commit: ``` $ if [[ $TRAVIS_PYTHON_VERSION != nightly ]]; then flake8 .; fi ./localstripe/resources.py:594:47: E741 ambiguous variable name 'l' The command "if [[ $TRAVIS_PYTHON_VERSION != nightly ]]; then flake8 .; fi" exited with 1. ``` --- localstripe/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localstripe/resources.py b/localstripe/resources.py index c3553da1..95efb196 100644 --- a/localstripe/resources.py +++ b/localstripe/resources.py @@ -564,7 +564,7 @@ def __init__(self, name=None, description=None, email=None, assert type(business_vat_id) is str if preferred_locales is not None: assert type(preferred_locales) is list - assert all(type(l) is str for l in preferred_locales) + assert all(type(lo) is str for lo in preferred_locales) if tax_id_data is None: tax_id_data = [] assert type(tax_id_data) is list From 54831f9ec5c7e6377f584042ba23dd8da092dc65 Mon Sep 17 00:00:00 2001 From: Hoel IRIS Date: Fri, 29 May 2020 16:33:20 +0200 Subject: [PATCH 03/17] Charge: Add `customer` and `created` to the list API Made from Stripe documentation: https://stripe.com/docs/api/charges/list#list_charges-customer --- localstripe/resources.py | 28 ++++++++++++++++++++++++++++ test.sh | 6 ++++++ 2 files changed, 34 insertions(+) diff --git a/localstripe/resources.py b/localstripe/resources.py index 95efb196..3f412555 100644 --- a/localstripe/resources.py +++ b/localstripe/resources.py @@ -475,6 +475,34 @@ def amount_refunded(self): def refunded(self): return self.amount <= self.amount_refunded + @classmethod + def _api_list_all(cls, url, customer=None, created=None, limit=10): + try: + assert _type(customer) is str and customer.startswith('cus_') + assert type(created) in (dict, str) + if type(created) is dict: + assert len(created.keys()) == 1 and \ + list(created.keys())[0] in ('gt', 'gte', 'lt', 'lte') + date = try_convert_to_int(list(created.values())[0]) + elif type(created) is str: + date = try_convert_to_int(created) + assert type(date) is int and date > 1500000000 + except AssertionError: + raise UserError(400, 'Bad request') + + Customer._api_retrieve(customer) # to return 404 if not existant + + if created: + if type(created) is str or not created.get('gt'): + raise UserError(500, 'Not implemented') + + li = super(Charge, cls)._api_list_all(url, limit=limit) + li._list = [c for c in li._list if c.customer == customer] + if created.get('gt'): + li._list = [c for c in li._list + if c.created > try_convert_to_int(created['gt'])] + return li + extra_apis.append(( ('POST', '/v1/charges/{id}/capture', Charge._api_capture))) diff --git a/test.sh b/test.sh index c8457d54..1a80363a 100755 --- a/test.sh +++ b/test.sh @@ -754,3 +754,9 @@ status=$( curl -sSf -u $SK: $HOST/v1/charges/$charge \ | grep -oE '"status": "failed"') [ -n "$status" ] + +# list charges +total_count=$( + curl -sSf -u $SK: $HOST/v1/charges?customer=$cus\&created%5Bgt%5D=1588166306 \ + | grep -oE '"total_count": 6') +[ -n "$total_count" ] From 597e4dde7a6cef31eded345c9040d2b62e927e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Wed, 3 Jun 2020 17:36:45 +0200 Subject: [PATCH 04/17] localstripe version 1.12.5 --- localstripe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localstripe/__init__.py b/localstripe/__init__.py index 62fc2575..f34ef386 100644 --- a/localstripe/__init__.py +++ b/localstripe/__init__.py @@ -21,4 +21,4 @@ raise RuntimeError('Please run with Python >= 3.5') __author__ = 'Adrien Vergé' -__version__ = '1.12.4' +__version__ = '1.12.5' From 69dc63d4187a1538c90dd70ce6114a9fa9548cc0 Mon Sep 17 00:00:00 2001 From: Sam Smith Date: Wed, 1 Jul 2020 15:48:17 +0100 Subject: [PATCH 05/17] Coupon: use float value for percent_off --- localstripe/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localstripe/resources.py b/localstripe/resources.py index 3f412555..e9c96d3b 100644 --- a/localstripe/resources.py +++ b/localstripe/resources.py @@ -518,7 +518,7 @@ def __init__(self, id=None, duration=None, amount_off=None, raise UserError(400, 'Unexpected ' + ', '.join(kwargs.keys())) amount_off = try_convert_to_int(amount_off) - percent_off = try_convert_to_int(percent_off) + percent_off = try_convert_to_float(percent_off) duration_in_months = try_convert_to_int(duration_in_months) try: assert type(id) is str and id From 5fa05205550b02fb6120b7247c3d1d406f1af4e5 Mon Sep 17 00:00:00 2001 From: Hoel IRIS Date: Tue, 7 Jul 2020 10:10:55 +0200 Subject: [PATCH 06/17] fix(resources): Check that coupon `percent_off` is float not int Since commit 4810d4b _coupon: use float value for percent_off_ the following unit test is failing: ``` ... + curl -sSf -u sk_test_12345: http://localhost:8420/v1/coupons -d id=PARRAIN -d percent_off=30 -d duration=once curl: (22) The requested URL returned error: 400 Bad Request ``` It's because, in the code just after 4810d4b change, the expected type for `percent_off` is still `int`. --- localstripe/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localstripe/resources.py b/localstripe/resources.py index e9c96d3b..72774e3b 100644 --- a/localstripe/resources.py +++ b/localstripe/resources.py @@ -526,7 +526,7 @@ def __init__(self, id=None, duration=None, amount_off=None, if amount_off is not None: assert type(amount_off) is int and amount_off >= 0 if percent_off is not None: - assert type(percent_off) is int + assert type(percent_off) is float assert percent_off >= 0 and percent_off <= 100 assert duration in ('forever', 'once', 'repeating') if amount_off is not None: From e181fdb1e44c1eaaa7acfa1fed8db313c9afc2f5 Mon Sep 17 00:00:00 2001 From: Hoel IRIS Date: Tue, 7 Jul 2020 10:30:02 +0200 Subject: [PATCH 07/17] fix(resources): Make charges list API parameters optional In Stripe [documentation of the Charges list API], all parameters are optional. Since commit 54831f9 _Charge: Add `customer` and `created` to the list API_, localstripe support parameters `customer` and `created` but both were made mandatory. Let's fix them. [documentation of the Charges list API]: https://stripe.com/docs/api/charges/list --- localstripe/resources.py | 28 ++++++++++++++++------------ test.sh | 9 +++++++++ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/localstripe/resources.py b/localstripe/resources.py index 72774e3b..41743c5e 100644 --- a/localstripe/resources.py +++ b/localstripe/resources.py @@ -478,27 +478,31 @@ def refunded(self): @classmethod def _api_list_all(cls, url, customer=None, created=None, limit=10): try: - assert _type(customer) is str and customer.startswith('cus_') - assert type(created) in (dict, str) - if type(created) is dict: - assert len(created.keys()) == 1 and \ - list(created.keys())[0] in ('gt', 'gte', 'lt', 'lte') - date = try_convert_to_int(list(created.values())[0]) - elif type(created) is str: - date = try_convert_to_int(created) - assert type(date) is int and date > 1500000000 + if customer is not None: + assert type(customer) is str and customer.startswith('cus_') + if created is not None: + assert type(created) in (dict, str) + if type(created) is dict: + assert len(created.keys()) == 1 and \ + list(created.keys())[0] in ('gt', 'gte', 'lt', 'lte') + date = try_convert_to_int(list(created.values())[0]) + elif type(created) is str: + date = try_convert_to_int(created) + assert type(date) is int and date > 1500000000 except AssertionError: raise UserError(400, 'Bad request') - Customer._api_retrieve(customer) # to return 404 if not existant + if customer: + Customer._api_retrieve(customer) # to return 404 if not existant if created: if type(created) is str or not created.get('gt'): raise UserError(500, 'Not implemented') li = super(Charge, cls)._api_list_all(url, limit=limit) - li._list = [c for c in li._list if c.customer == customer] - if created.get('gt'): + 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'])] return li diff --git a/test.sh b/test.sh index 1a80363a..ac5d2813 100755 --- a/test.sh +++ b/test.sh @@ -756,6 +756,15 @@ status=$( [ -n "$status" ] # list charges +total_count=$( + curl -sSf -u $SK: $HOST/v1/charges | grep -oE '"total_count": 15') +[ -n "$total_count" ] + +total_count=$( + curl -sSf -u $SK: $HOST/v1/charges?customer=$cus \ + | grep -oE '"total_count": 6') +[ -n "$total_count" ] + total_count=$( curl -sSf -u $SK: $HOST/v1/charges?customer=$cus\&created%5Bgt%5D=1588166306 \ | grep -oE '"total_count": 6') From f0a9400bd8be32edf03f6823ece872e295e033bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Tue, 7 Jul 2020 14:01:27 +0200 Subject: [PATCH 08/17] localstripe version 1.12.6 --- localstripe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localstripe/__init__.py b/localstripe/__init__.py index f34ef386..d9f1d549 100644 --- a/localstripe/__init__.py +++ b/localstripe/__init__.py @@ -21,4 +21,4 @@ raise RuntimeError('Please run with Python >= 3.5') __author__ = 'Adrien Vergé' -__version__ = '1.12.5' +__version__ = '1.12.6' From ad61eb9fe6d914448afbead2bd0743afd46fc99c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Wed, 9 Sep 2020 09:29:48 +0200 Subject: [PATCH 09/17] Invoice: Add 'billing_reason' property Let's add the property `billing_reason` that was missing. For the moment, it's not implemented so it's always `null`. https://stripe.com/docs/api/invoices/object#invoice_object-billing_reason --- localstripe/resources.py | 1 + 1 file changed, 1 insertion(+) diff --git a/localstripe/resources.py b/localstripe/resources.py index 41743c5e..60b75021 100644 --- a/localstripe/resources.py +++ b/localstripe/resources.py @@ -927,6 +927,7 @@ def __init__(self, customer=None, subscription=None, metadata=None, self.application_fee = None self.attempt_count = 1 self.attempted = True + self.billing_reason = None self.description = description self.discount = None self.ending_balance = 0 From 38add1cecbd447d777c7cedc803f79302113c1bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Wed, 9 Sep 2020 09:56:55 +0200 Subject: [PATCH 10/17] localstripe version 1.12.7 --- localstripe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localstripe/__init__.py b/localstripe/__init__.py index d9f1d549..3501fb72 100644 --- a/localstripe/__init__.py +++ b/localstripe/__init__.py @@ -21,4 +21,4 @@ raise RuntimeError('Please run with Python >= 3.5') __author__ = 'Adrien Vergé' -__version__ = '1.12.6' +__version__ = '1.12.7' From c7b5782a22a2b6b8ef0cf956e823ad504d86ff90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Thu, 24 Sep 2020 08:13:21 +0200 Subject: [PATCH 11/17] tests: Use 'curl --globoff' and [] instead of %5B%5D Problem: very recently, Travis started to fail on commit that were previously fine. Probably a change on their side? It appears that on some Python version (e.g. 3.7 but not 3.8) they double-escape `%5B` and `%5D` caracters, so they are passed as `%255B` and `%255D`. To avoid that, I propose to use `[` and `]` directly, and enable curl's `-g` option (`--globoff`) so that it doesn't interpret them. In summary now the commands are a bit clearer: curl -sSfg -u $SK: $HOST/v1/plans?expand[]=data.product --- test.sh | 272 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 136 insertions(+), 136 deletions(-) diff --git a/test.sh b/test.sh index ac5d2813..265307bf 100755 --- a/test.sh +++ b/test.sh @@ -6,19 +6,19 @@ set -eux HOST=http://localhost:8420 SK=sk_test_12345 -cus=$(curl -sSf -u $SK: $HOST/v1/customers \ +cus=$(curl -sSfg -u $SK: $HOST/v1/customers \ -d email=james.robinson@example.com \ | grep -oE 'cus_\w+' | head -n 1) -curl -sSf -u $SK: $HOST/v1/customers/$cus \ +curl -sSfg -u $SK: $HOST/v1/customers/$cus \ -d description='Adding a description...' -curl -sSf -u $SK: $HOST/v1/customers/$cus \ +curl -sSfg -u $SK: $HOST/v1/customers/$cus \ -d preferred_locales[]='fr-FR' -d preferred_locales[]='es-ES' -curl -sSf -u $SK: -X DELETE $HOST/v1/customers/$cus +curl -sSfg -u $SK: -X DELETE $HOST/v1/customers/$cus -cus=$(curl -sSf -u $SK: $HOST/v1/customers \ +cus=$(curl -sSfg -u $SK: $HOST/v1/customers \ -d description='This customer is a company' \ -d email=foo@bar.com \ -d phone=0102030405 \ @@ -27,19 +27,19 @@ cus=$(curl -sSf -u $SK: $HOST/v1/customers \ -d tax_id_data[0][type]=eu_vat -d tax_id_data[0][value]=FR12345678901 \ | grep -oE 'cus_\w+' | head -n 1) -curl -sSf -u $SK: $HOST/v1/customers/$cus/tax_ids \ +curl -sSfg -u $SK: $HOST/v1/customers/$cus/tax_ids \ -d type=eu_vat -d value=DE123456789 \ -d expand[]=customer -curl -sSf -u $SK: $HOST/v1/customers/$cus?expand%5B%5D=tax_ids.data.customer +curl -sSfg -u $SK: $HOST/v1/customers/$cus?expand[]=tax_ids.data.customer -curl -sSf -u $SK: $HOST/v1/customers/$cus?expand%5B%5D=subscriptions.data.items.data +curl -sSfg -u $SK: $HOST/v1/customers/$cus?expand[]=subscriptions.data.items.data -code=$(curl -so /dev/null -w '%{http_code}' -u $SK: \ - $HOST/v1/customers/$cus?expand%5B%5D=subscriptions.data.items.data.tax_ids) +code=$(curl -sg -o /dev/null -w '%{http_code}' -u $SK: \ + $HOST/v1/customers/$cus?expand[]=subscriptions.data.items.data.tax_ids) [ "$code" -eq 400 ] -txr1=$(curl -sSf -u $SK: $HOST/v1/tax_rates \ +txr1=$(curl -sSfg -u $SK: $HOST/v1/tax_rates \ -d display_name=VAT \ -d description='TVA France taux normal' \ -d jurisdiction=FR \ @@ -47,7 +47,7 @@ txr1=$(curl -sSf -u $SK: $HOST/v1/tax_rates \ -d inclusive=false \ | grep -oE 'txr_\w+' | head -n 1) -txr2=$(curl -sSf -u $SK: $HOST/v1/tax_rates \ +txr2=$(curl -sSfg -u $SK: $HOST/v1/tax_rates \ -d display_name=VAT \ -d description='TVA France taux réduit' \ -d jurisdiction=FR \ @@ -55,21 +55,21 @@ txr2=$(curl -sSf -u $SK: $HOST/v1/tax_rates \ -d inclusive=false \ | grep -oE 'txr_\w+' | head -n 1) -curl -sSf -u $SK: $HOST/v1/plans \ +curl -sSfg -u $SK: $HOST/v1/plans \ -d id=basique-mensuel \ -d product[name]='Abonnement basique (mensuel)' \ -d amount=2500 \ -d currency=eur \ -d interval=month -curl -sSf -u $SK: $HOST/v1/plans \ +curl -sSfg -u $SK: $HOST/v1/plans \ -d id=basique-annuel \ -d name='Abonnement basique (annuel)' \ -d amount=20000 \ -d currency=eur \ -d interval=year -curl -sSf -u $SK: $HOST/v1/plans \ +curl -sSfg -u $SK: $HOST/v1/plans \ -d id=annual-tiered-volume \ -d name='Annual tiered volume' \ -d currency=eur \ @@ -85,7 +85,7 @@ curl -sSf -u $SK: $HOST/v1/plans \ -d tiers[1][unit_amount]=1000 \ -d tiers[1][flat_amount]=1200 -curl -sSf -u $SK: $HOST/v1/plans \ +curl -sSfg -u $SK: $HOST/v1/plans \ -d id=monthly-tiered-graduated \ -d name='Monthly tiered graduated' \ -d currency=eur \ @@ -101,7 +101,7 @@ curl -sSf -u $SK: $HOST/v1/plans \ -d tiers[1][unit_amount]=1000 \ -d tiers[1][flat_amount]=1200 -curl -sSf -u $SK: $HOST/v1/plans \ +curl -sSfg -u $SK: $HOST/v1/plans \ -d id=pro-annuel \ -d product[name]='Abonnement PRO (annuel)' \ -d product[statement_descriptor]='abonnement pro' \ @@ -109,57 +109,57 @@ curl -sSf -u $SK: $HOST/v1/plans \ -d currency=eur \ -d interval=year -curl -sSf -u $SK: $HOST/v1/plans \ +curl -sSfg -u $SK: $HOST/v1/plans \ -d product[name]='Without id' \ -d product[statement_descriptor]='Without id' \ -d amount=30000 \ -d currency=eur \ -d interval=year -curl -sSf -u $SK: $HOST/v1/plans \ +curl -sSfg -u $SK: $HOST/v1/plans \ -d id=delete-me \ -d product[name]='Delete me' \ -d amount=30000 \ -d currency=eur \ -d interval=year -curl -sSf -u $SK: -X DELETE $HOST/v1/plans/delete-me +curl -sSfg -u $SK: -X DELETE $HOST/v1/plans/delete-me -code=$(curl -so /dev/null -w '%{http_code}' -u $SK: $HOST/v1/plans \ +code=$(curl -sg -o /dev/null -w '%{http_code}' -u $SK: $HOST/v1/plans \ -d doesnotexist=1) [ "$code" -eq 400 ] -code=$(curl -so /dev/null -w '%{http_code}' -u $SK: \ +code=$(curl -sg -o /dev/null -w '%{http_code}' -u $SK: \ $HOST/v1/plans?doesnotexist=1) [ "$code" -eq 400 ] -curl -sSf -u $SK: $HOST/v1/products \ +curl -sSfg -u $SK: $HOST/v1/products \ -d name=T-shirt \ -d type=good \ -d description='Comfortable cotton t-shirt' \ -d attributes[]=size \ -d attributes[]=gender -curl -sSf -u $SK: $HOST/v1/products \ +curl -sSfg -u $SK: $HOST/v1/products \ -d id=PRODUCT1234 \ -d name='Product 1234' \ -d type=service -curl -sSf -u $SK: $HOST/v1/products/PRODUCT1234 +curl -sSfg -u $SK: $HOST/v1/products/PRODUCT1234 -curl -sSf -u $SK: $HOST/v1/plans?expand%5B%5D=data.product +curl -sSfg -u $SK: $HOST/v1/plans?expand[]=data.product -code=$(curl -so /dev/null -w '%{http_code}' -u $SK: \ - $HOST/v1/plans?expand%5B%5D=data.doesnotexist) +code=$(curl -sg -o /dev/null -w '%{http_code}' -u $SK: \ + $HOST/v1/plans?expand[]=data.doesnotexist) [ "$code" -eq 400 ] -curl -sSf -u $SK: $HOST/v1/coupons \ +curl -sSfg -u $SK: $HOST/v1/coupons \ -d id=PARRAIN \ -d percent_off=30 \ -d duration=once # This is what a Stripe.js request does: -tok=$(curl -sSf $HOST/v1/tokens \ +tok=$(curl -sSfg $HOST/v1/tokens \ -d key=pk_test_sldkjflaksdfj \ -d card[number]=4242424242424242 \ -d card[exp_month]=12 \ @@ -167,23 +167,23 @@ tok=$(curl -sSf $HOST/v1/tokens \ -d card[cvc]=123 \ | grep -oE 'tok_\w+') -curl -sSf -u $SK: $HOST/v1/customers/$cus/sources \ +curl -sSfg -u $SK: $HOST/v1/customers/$cus/sources \ -d source=$tok # This is what a request from back-end does: -tok=$(curl -sSf -u $SK: $HOST/v1/tokens \ +tok=$(curl -sSfg -u $SK: $HOST/v1/tokens \ -d card[number]=4242424242424242 \ -d card[exp_month]=12 \ -d card[exp_year]=2019 \ -d card[cvc]=123 \ | grep -oE 'tok_\w+') -curl -sSf -u $SK: $HOST/v1/customers/$cus/sources \ +curl -sSfg -u $SK: $HOST/v1/customers/$cus/sources \ -d source=$tok # add a new card card=$( - curl -sSf -u $SK: $HOST/v1/customers/$cus/cards \ + curl -sSfg -u $SK: $HOST/v1/customers/$cus/cards \ -d source[object]=card \ -d source[number]=4242424242424242 \ -d source[exp_month]=12 \ @@ -193,23 +193,23 @@ card=$( # observe new card in customer response res=$( - curl -sSf -u $SK: $HOST/v1/customers/$cus \ + curl -sSfg -u $SK: $HOST/v1/customers/$cus \ | grep -oE $card) [ -n "$res" ] # delete the card -curl -sSf -u $SK: $HOST/v1/customers/$cus/sources/$card \ +curl -sSfg -u $SK: $HOST/v1/customers/$cus/sources/$card \ -X DELETE # observe card no longer in customer response res=$( - curl -sSf -u $SK: $HOST/v1/customers/$cus \ + curl -sSfg -u $SK: $HOST/v1/customers/$cus \ | grep -oE $card || true) [ -z "$res" ] # add a new card card=$( - curl -sSf -u $SK: $HOST/v1/customers/$cus/cards \ + curl -sSfg -u $SK: $HOST/v1/customers/$cus/cards \ -d source[object]=card \ -d source[number]=4242424242424242 \ -d source[exp_month]=12 \ @@ -220,21 +220,21 @@ card=$( # observe name on card name=$( - curl -sSf -u $SK: $HOST/v1/customers/$cus/sources/$card \ + curl -sSfg -u $SK: $HOST/v1/customers/$cus/sources/$card \ | grep -oE '"name": "John Smith",') [ -n "$name" ] # update name on card -curl -sSf -u $SK: $HOST/v1/customers/$cus/sources/$card \ +curl -sSfg -u $SK: $HOST/v1/customers/$cus/sources/$card \ -d name=Jane\ Doe # observe name on card name=$( - curl -sSf -u $SK: $HOST/v1/customers/$cus/sources/$card \ + curl -sSfg -u $SK: $HOST/v1/customers/$cus/sources/$card \ | grep -oE '"name": "Jane Doe",') [ -n "$name" ] -card=$(curl -sSf -u $SK: $HOST/v1/customers/$cus/cards \ +card=$(curl -sSfg -u $SK: $HOST/v1/customers/$cus/cards \ -d source[object]=card \ -d source[number]=4242424242424242 \ -d source[exp_month]=12 \ @@ -242,7 +242,7 @@ card=$(curl -sSf -u $SK: $HOST/v1/customers/$cus/cards \ -d source[cvc]=123 \ | grep -oE 'card_\w+') -code=$(curl -s -o /dev/null -w "%{http_code}" -u $SK: \ +code=$(curl -sg -o /dev/null -w "%{http_code}" -u $SK: \ $HOST/v1/customers/$cus/cards \ -d source[object]=card \ -d source[number]=4000000000000002 \ @@ -253,7 +253,7 @@ code=$(curl -s -o /dev/null -w "%{http_code}" -u $SK: \ # new charges are captured by default captured=$( - curl -sSf -u $SK: $HOST/v1/charges \ + curl -sSfg -u $SK: $HOST/v1/charges \ -d customer=$cus \ -d source=$card \ -d amount=1000 \ @@ -263,7 +263,7 @@ captured=$( # create a pre-auth charge charge=$( - curl -sSf -u $SK: $HOST/v1/charges \ + curl -sSfg -u $SK: $HOST/v1/charges \ -d customer=$cus \ -d source=$card \ -d amount=1000 \ @@ -273,33 +273,33 @@ charge=$( # charge was not captured captured=$( - curl -sSf -u $SK: $HOST/v1/charges/$charge \ + curl -sSfg -u $SK: $HOST/v1/charges/$charge \ | grep -oE '"captured": false,') [ -n "$captured" ] # cannot capture more than pre-authed amount code=$( - curl -s -o /dev/null -w "%{http_code}" \ + curl -sg -o /dev/null -w "%{http_code}" \ -u $SK: $HOST/v1/charges/$charge/capture \ -d amount=2000) [ "$code" = 400 ] # can capture less than the pre-auth amount captured=$( - curl -sSf -u $SK: $HOST/v1/charges/$charge/capture \ + curl -sSfg -u $SK: $HOST/v1/charges/$charge/capture \ -d amount=800 \ | grep -oE '"captured": true,') [ -n "$captured" ] # difference between pre-auth and capture is refunded refunded=$( - curl -sSf -u $SK: $HOST/v1/charges/$charge \ + curl -sSfg -u $SK: $HOST/v1/charges/$charge \ | grep -oE '"amount_refunded": 200,') [ -n "$captured" ] # create a pre-auth charge charge=$( - curl -sSf -u $SK: $HOST/v1/charges \ + curl -sSfg -u $SK: $HOST/v1/charges \ -d customer=$cus \ -d source=$card \ -d amount=1000 \ @@ -309,195 +309,195 @@ charge=$( # capture the full amount (default) captured=$( - curl -sSf -u $SK: $HOST/v1/charges/$charge/capture \ + curl -sSfg -u $SK: $HOST/v1/charges/$charge/capture \ -X POST \ | grep -oE '"captured": true,') [ -n "$captured" ] # none is refunded refunded=$( - curl -sSf -u $SK: $HOST/v1/charges/$charge \ + curl -sSfg -u $SK: $HOST/v1/charges/$charge \ | grep -oE '"amount_refunded": 0,') [ -n "$captured" ] # cannot capture an already captured charge code=$( - curl -s -o /dev/null -w "%{http_code}" \ + curl -sg -o /dev/null -w "%{http_code}" \ -u $SK: $HOST/v1/charges/$charge/capture \ -X POST) [ "$code" = 400 ] sepa_cus=$( - curl -sSf -u $SK: $HOST/v1/customers \ + curl -sSfg -u $SK: $HOST/v1/customers \ -d description='I pay with SEPA debit' \ -d email=sepa@euro.fr \ | grep -oE 'cus_\w+' | head -n 1) -src=$(curl -sSf -u $SK: $HOST/v1/sources \ +src=$(curl -sSfg -u $SK: $HOST/v1/sources \ -d type=ach_credit_transfer \ -d currency=usd \ -d owner[email]='jenny.rosen@example.com' \ | grep -oE 'src_\w+') -curl -sSf -u $SK: $HOST/v1/customers/$sepa_cus/sources \ +curl -sSfg -u $SK: $HOST/v1/customers/$sepa_cus/sources \ -d source=$src # This is what a Stripe.js request does: -src=$(curl -sSf -u $SK: $HOST/v1/sources \ +src=$(curl -sSfg -u $SK: $HOST/v1/sources \ -d type=sepa_debit \ -d sepa_debit[iban]=DE89370400440532013000 \ -d currency=eur \ -d owner[name]='Jenny Rosen' \ | grep -oE 'src_\w+') -curl -sSf -u $SK: $HOST/v1/customers/$sepa_cus/sources \ +curl -sSfg -u $SK: $HOST/v1/customers/$sepa_cus/sources \ -d source=$src # Get a customer source directly: -curl -sSf -u $SK: $HOST/v1/customers/$sepa_cus/sources/$src -code=$(curl -s -o /dev/null -w "%{http_code}" -u $SK: \ +curl -sSfg -u $SK: $HOST/v1/customers/$sepa_cus/sources/$src +code=$(curl -sg -o /dev/null -w "%{http_code}" -u $SK: \ $HOST/v1/customers/cus_doesnotexist/sources/$src) [ "$code" = 404 ] -code=$(curl -s -o /dev/null -w "%{http_code}" -u $SK: \ +code=$(curl -sg -o /dev/null -w "%{http_code}" -u $SK: \ $HOST/v1/customers/$sepa_cus/sources/src_doesnotexist) [ "$code" = 404 ] -tok=$(curl -sSf -u $SK: $HOST/v1/tokens \ +tok=$(curl -sSfg -u $SK: $HOST/v1/tokens \ -d card[number]=4242424242424242 \ -d card[exp_month]=12 \ -d card[exp_year]=2020 \ -d card[cvc]=123 \ | grep -oE 'tok_\w+') -curl -sSf -u $SK: $HOST/v1/customers \ +curl -sSfg -u $SK: $HOST/v1/customers \ -d description='Customer with already existing source' \ -d source=$tok # For a customer with no source, `default_source` should be `null`: -cus=$(curl -sSf -u $SK: $HOST/v1/customers -d email=joe.malvic@example.com \ +cus=$(curl -sSfg -u $SK: $HOST/v1/customers -d email=joe.malvic@example.com \ | grep -oE 'cus_\w+' | head -n 1) -ds=$(curl -sSf -u $SK: $HOST/v1/customers/$cus?expand%5B%5D=default_source \ +ds=$(curl -sSfg -u $SK: $HOST/v1/customers/$cus?expand[]=default_source \ | grep -oE '"default_source": \w+,') [ "$ds" = '"default_source": null,' ] -curl -sSf -u $SK: $HOST/v1/customers/$cus/cards \ +curl -sSfg -u $SK: $HOST/v1/customers/$cus/cards \ -d source[object]=card \ -d source[number]=4242424242424242 \ -d source[exp_month]=12 \ -d source[exp_year]=2020 \ -d source[cvc]=123 -ds=$(curl -sSf -u $SK: $HOST/v1/customers/$cus?expand%5B%5D=default_source \ +ds=$(curl -sSfg -u $SK: $HOST/v1/customers/$cus?expand[]=default_source \ | grep -oE '"default_source": null",' || true) [ -z "$ds" ] # we can charge a customer without specifying the source -curl -sSf -u $SK: $HOST/v1/charges \ +curl -sSfg -u $SK: $HOST/v1/charges \ -d customer=$cus \ -d amount=1000 \ -d currency=usd -curl -sSf -u $SK: $HOST/v1/invoices?customer=$cus +curl -sSfg -u $SK: $HOST/v1/invoices?customer=$cus -code=$(curl -s -o /dev/null -w "%{http_code}" -u $SK: \ +code=$(curl -sg -o /dev/null -w "%{http_code}" -u $SK: \ $HOST/v1/invoices/upcoming?customer=$cus) [ "$code" = 404 ] -curl -sSf -u $SK: $HOST/v1/subscriptions \ +curl -sSfg -u $SK: $HOST/v1/subscriptions \ -d customer=$cus \ -d items[0][plan]=basique-mensuel \ -d expand[]=latest_invoice.payment_intent -res=$(curl -sSf -u $SK: $HOST/v1/subscriptions \ +res=$(curl -sSfg -u $SK: $HOST/v1/subscriptions \ -d customer=$cus \ -d items[0][plan]=basique-mensuel \ -d items[0][tax_rates][0]=$txr1) sub=$(echo "$res" | grep -oE 'sub_\w+' | head -n 1) in=$(echo "$res" | grep -oE 'in_\w+' | head -n 1) -curl -sSf -u $SK: $HOST/v1/invoices?customer=$cus +curl -sSfg -u $SK: $HOST/v1/invoices?customer=$cus -curl -sSf -u $SK: $HOST/v1/invoices/upcoming?customer=$cus +curl -sSfg -u $SK: $HOST/v1/invoices/upcoming?customer=$cus -curl -sSf -u $SK: $HOST/v1/invoices/upcoming?customer=$cus\&subscription_items%5B0%5D%5Bplan%5D=pro-annuel\&subscription_tax_percent=20 +curl -sSfg -u $SK: $HOST/v1/invoices/upcoming?customer=$cus\&subscription_items[0][plan]=pro-annuel\&subscription_tax_percent=20 -curl -sSf -u $SK: $HOST/v1/invoices/upcoming?customer=$cus\&subscription=$sub\&subscription_items%5B0%5D%5Bid%5D=si_RBrVStcKDimMnp\&subscription_items%5B0%5D%5Bplan%5D=basique-annuel\&subscription_proration_date=1504182686\&subscription_tax_percent=20 +curl -sSfg -u $SK: $HOST/v1/invoices/upcoming?customer=$cus\&subscription=$sub\&subscription_items[0][id]=si_RBrVStcKDimMnp\&subscription_items[0][plan]=basique-annuel\&subscription_proration_date=1504182686\&subscription_tax_percent=20 -curl -sSf -u $SK: $HOST/v1/invoices/$in/lines +curl -sSfg -u $SK: $HOST/v1/invoices/$in/lines -cus=$(curl -sSf -u $SK: $HOST/v1/customers \ +cus=$(curl -sSfg -u $SK: $HOST/v1/customers \ -d description='This customer will have a subscription with volume tiered pricing' \ -d email=tiered@bar.com \ | grep -oE 'cus_\w+' | head -n 1) -curl -sSf -u $SK: $HOST/v1/customers/$cus/sources \ +curl -sSfg -u $SK: $HOST/v1/customers/$cus/sources \ -d source=$tok -curl -sSf -u $SK: $HOST/v1/subscriptions \ +curl -sSfg -u $SK: $HOST/v1/subscriptions \ -d customer=$cus \ -d items[0][plan]=annual-tiered-volume \ -d items[0][quantity]=5 -curl -sSf -u $SK: $HOST/v1/invoices?customer=$cus +curl -sSfg -u $SK: $HOST/v1/invoices?customer=$cus -curl -sSf -u $SK: $HOST/v1/subscriptions?customer=$cus +curl -sSfg -u $SK: $HOST/v1/subscriptions?customer=$cus -curl -sSf -u $SK: $HOST/v1/customers/$cus/subscriptions +curl -sSfg -u $SK: $HOST/v1/customers/$cus/subscriptions -cus=$(curl -sSf -u $SK: $HOST/v1/customers \ +cus=$(curl -sSfg -u $SK: $HOST/v1/customers \ -d description='This customer will have a subscription with graduated tiered pricing' \ -d email=tiered@bar.com \ | grep -oE 'cus_\w+' | head -n 1) -curl -sSf -u $SK: $HOST/v1/customers/$cus/sources \ +curl -sSfg -u $SK: $HOST/v1/customers/$cus/sources \ -d source=$tok -sub=$(curl -sSf -u $SK: $HOST/v1/subscriptions \ +sub=$(curl -sSfg -u $SK: $HOST/v1/subscriptions \ -d customer=$cus \ -d items[0][plan]=monthly-tiered-graduated \ -d items[0][quantity]=5 \ | grep -oE 'sub_\w+' | head -n 1) -data=$(curl -sSf -u $SK: $HOST/v1/subscriptions/$sub \ +data=$(curl -sSfg -u $SK: $HOST/v1/subscriptions/$sub \ -d items[0][plan]=annual-tiered-volume) -same_data=$(curl -sSf -u $SK: $HOST/v1/subscriptions/$sub \ +same_data=$(curl -sSfg -u $SK: $HOST/v1/subscriptions/$sub \ -d items[0][plan]=annual-tiered-volume) diff <(echo "$data") <(echo "$same_data") -curl -sSf -u $SK: $HOST/v1/subscriptions/$sub \ +curl -sSfg -u $SK: $HOST/v1/subscriptions/$sub \ -d metadata[toto]=toto -curl -sSf -u $SK: $HOST/v1/invoices?customer=$cus +curl -sSfg -u $SK: $HOST/v1/invoices?customer=$cus -cus=$(curl -sSf -u $SK: $HOST/v1/customers \ +cus=$(curl -sSfg -u $SK: $HOST/v1/customers \ -d description='This customer will switch from a yearly to another yearly plan' \ -d email=switch@bar.com \ | grep -oE 'cus_\w+' | head -n 1) -curl -sSf -u $SK: $HOST/v1/customers/$cus/sources \ +curl -sSfg -u $SK: $HOST/v1/customers/$cus/sources \ -d source=$tok -sub=$(curl -sSf -u $SK: $HOST/v1/subscriptions \ +sub=$(curl -sSfg -u $SK: $HOST/v1/subscriptions \ -d customer=$cus \ -d items[0][plan]=basique-annuel) sub_id=$(echo "$sub" | grep -oE 'sub_\w+' | head -n 1) sub_item_id=$(echo "$sub" | grep -oE 'si_\w+' | head -n 1) -sub=$(curl -sSf -u $SK: $HOST/v1/subscriptions/$sub_id \ +sub=$(curl -sSfg -u $SK: $HOST/v1/subscriptions/$sub_id \ -d items[0][plan]=pro-annuel \ -d items[0][id]=$sub_item_id) -in=$(curl -sSf -u $SK: $HOST/v1/invoices \ +in=$(curl -sSfg -u $SK: $HOST/v1/invoices \ -d customer=$cus) grep -q "Abonnement PRO (annuel)" <<<"$in" grep -q "Abonnement basique (annuel)" <<<"$in" -cus=$(curl -sSf -u $SK: $HOST/v1/customers \ +cus=$(curl -sSfg -u $SK: $HOST/v1/customers \ -d email=john.malkovich@example.com \ | grep -oE 'cus_\w+' | head -n 1) -pm=$(curl -sSf -u $SK: $HOST/v1/payment_methods \ +pm=$(curl -sSfg -u $SK: $HOST/v1/payment_methods \ -d type=card \ -d card[number]=4242424242424242 \ -d card[exp_month]=12 \ @@ -505,46 +505,46 @@ pm=$(curl -sSf -u $SK: $HOST/v1/payment_methods \ -d card[cvc]=123 \ | grep -oE 'pm_\w+' | head -n 1) -curl -sSf -u $SK: $HOST/v1/payment_methods/$pm/attach \ +curl -sSfg -u $SK: $HOST/v1/payment_methods/$pm/attach \ -d customer=$cus -curl -sSf -u $SK: $HOST/v1/customers/$cus \ +curl -sSfg -u $SK: $HOST/v1/customers/$cus \ -d invoice_settings[default_payment_method]=$pm -curl -sSf -u $SK: $HOST/v1/customers/$cus?expand%5B%5D=invoice_settings.default_payment_method +curl -sSfg -u $SK: $HOST/v1/customers/$cus?expand[]=invoice_settings.default_payment_method -curl -sSf -u $SK: $HOST/v1/payment_methods?customer=$cus\&type=card +curl -sSfg -u $SK: $HOST/v1/payment_methods?customer=$cus\&type=card -curl -sSf -u $SK: $HOST/v1/payment_methods/$pm/detach -X POST +curl -sSfg -u $SK: $HOST/v1/payment_methods/$pm/detach -X POST -pm=$(curl -sSf -u $SK: $HOST/v1/payment_methods \ +pm=$(curl -sSfg -u $SK: $HOST/v1/payment_methods \ -d type=card \ -d card[number]=4000000000000002 \ -d card[exp_month]=4 \ -d card[exp_year]=2042 \ -d card[cvc]=123 \ | grep -oE 'pm_\w+' | head -n 1) -code=$(curl -s -o /dev/null -w "%{http_code}" -u $SK: \ +code=$(curl -sg -o /dev/null -w "%{http_code}" -u $SK: \ $HOST/v1/payment_methods/$pm/attach \ -d customer=$cus) [ "$code" = 402 ] -curl -sSf -u $SK: $HOST/v1/payment_methods?customer=$cus\&type=card +curl -sSfg -u $SK: $HOST/v1/payment_methods?customer=$cus\&type=card -res=$(curl -sSf -u $SK: $HOST/v1/setup_intents -X POST) +res=$(curl -sSfg -u $SK: $HOST/v1/setup_intents -X POST) seti=$(echo "$res" | grep '"id"' | grep -oE 'seti_\w+' | head -n 1) seti_secret=$(echo $res | grep -oE 'seti_\w+_secret_\w+' | head -n 1) -curl -sSf -u $SK: $HOST/v1/setup_intents/$seti/confirm -X POST +curl -sSfg -u $SK: $HOST/v1/setup_intents/$seti/confirm -X POST -curl -sSf -u $SK: $HOST/v1/setup_intents/$seti/cancel -X POST +curl -sSfg -u $SK: $HOST/v1/setup_intents/$seti/cancel -X POST -res=$(curl -sSf -u $SK: $HOST/v1/setup_intents -X POST) +res=$(curl -sSfg -u $SK: $HOST/v1/setup_intents -X POST) seti=$(echo "$res" | grep '"id"' | grep -oE 'seti_\w+' | head -n 1) seti_secret=$(echo $res | grep -oE 'seti_\w+_secret_\w+' | head -n 1) # This is what a Stripe.js request does: -curl -sSf $HOST/v1/setup_intents/$seti/confirm \ +curl -sSfg $HOST/v1/setup_intents/$seti/confirm \ -d key=pk_test_sldkjflaksdfj \ -d use_stripe_sdk=true \ -d client_secret=$seti_secret \ @@ -557,7 +557,7 @@ curl -sSf $HOST/v1/setup_intents/$seti/confirm \ # off_session cannot be used when confirm is false code=$( - curl -s -o /dev/null -w "%{http_code}" \ + curl -sg -o /dev/null -w "%{http_code}" \ -u $SK: $HOST/v1/payment_intents \ -d amount=1000 \ -d currency=usd \ @@ -567,7 +567,7 @@ code=$( # card fingerprint fingerprint=$( - curl -sSf -u $SK: $HOST/v1/customers/$cus/cards \ + curl -sSfg -u $SK: $HOST/v1/customers/$cus/cards \ -d source[object]=card \ -d source[number]=4242424242424242 \ -d source[exp_month]=12 \ @@ -577,7 +577,7 @@ fingerprint=$( [ -n "$fingerprint" ] fingerprint=$( - curl -sSf -u $SK: $HOST/v1/customers/$cus/cards \ + curl -sSfg -u $SK: $HOST/v1/customers/$cus/cards \ -d source[object]=card \ -d source[number]=4000056655665556 \ -d source[exp_month]=12 \ @@ -587,7 +587,7 @@ fingerprint=$( [ -n "$fingerprint" ] fingerprint=$( - curl -sSf -u $SK: $HOST/v1/customers/$cus/cards \ + curl -sSfg -u $SK: $HOST/v1/customers/$cus/cards \ -d source[object]=card \ -d source[number]=5555555555554444 \ -d source[exp_month]=12 \ @@ -598,7 +598,7 @@ fingerprint=$( # sepa debit fingerprint fingerprint=$( - curl -sSf -u $SK: $HOST/v1/sources \ + curl -sSfg -u $SK: $HOST/v1/sources \ -d type=sepa_debit \ -d sepa_debit[iban]=DE89370400440532013000 \ -d currency=eur \ @@ -606,7 +606,7 @@ fingerprint=$( [ -n "$fingerprint" ] fingerprint=$( - curl -sSf -u $SK: $HOST/v1/sources \ + curl -sSfg -u $SK: $HOST/v1/sources \ -d type=sepa_debit \ -d sepa_debit[iban]=FR1420041010050500013M02606 \ -d currency=eur \ @@ -614,7 +614,7 @@ fingerprint=$( [ -n "$fingerprint" ] fingerprint=$( - curl -sSf -u $SK: $HOST/v1/sources \ + curl -sSfg -u $SK: $HOST/v1/sources \ -d type=sepa_debit \ -d sepa_debit[iban]=IT40S0542811101000000123456 \ -d currency=eur \ @@ -623,7 +623,7 @@ fingerprint=$( # payment method fingerprint fingerprint=$( - curl -sSf -u $SK: $HOST/v1/payment_methods \ + curl -sSfg -u $SK: $HOST/v1/payment_methods \ -d type=card \ -d card[number]=4242424242424242 \ -d card[exp_month]=12 \ @@ -633,7 +633,7 @@ fingerprint=$( [ -n "$fingerprint" ] fingerprint=$( - curl -sSf -u $SK: $HOST/v1/payment_methods \ + curl -sSfg -u $SK: $HOST/v1/payment_methods \ -d type=card \ -d card[number]=4000056655665556 \ -d card[exp_month]=12 \ @@ -643,7 +643,7 @@ fingerprint=$( [ -n "$fingerprint" ] fingerprint=$( - curl -sSf -u $SK: $HOST/v1/payment_methods \ + curl -sSfg -u $SK: $HOST/v1/payment_methods \ -d type=card \ -d card[number]=5555555555554444 \ -d card[exp_month]=12 \ @@ -654,7 +654,7 @@ fingerprint=$( # create a chargeable source card=$( - curl -sSf -u $SK: $HOST/v1/customers/$cus/cards \ + curl -sSfg -u $SK: $HOST/v1/customers/$cus/cards \ -d source[object]=card \ -d source[number]=4242424242424242 \ -d source[exp_month]=12 \ @@ -664,7 +664,7 @@ card=$( # create a normal charge, verify charge status succeeded status=$( - curl -sSf -u $SK: $HOST/v1/charges \ + curl -sSfg -u $SK: $HOST/v1/charges \ -d source=$card \ -d amount=1000 \ -d currency=usd \ @@ -673,7 +673,7 @@ status=$( # create a pre-auth charge charge=$( - curl -sSf -u $SK: $HOST/v1/charges \ + curl -sSfg -u $SK: $HOST/v1/charges \ -d source=$card \ -d amount=1000 \ -d currency=usd \ @@ -682,23 +682,23 @@ charge=$( # verify charge status pending status=$( - curl -sSf -u $SK: $HOST/v1/charges/$charge \ + curl -sSfg -u $SK: $HOST/v1/charges/$charge \ | grep -oE '"status": "pending"') [ -n "$status" ] # capture the charge -curl -sSf -u $SK: $HOST/v1/charges/$charge/capture \ +curl -sSfg -u $SK: $HOST/v1/charges/$charge/capture \ -X POST # verify charge status succeeded status=$( - curl -sSf -u $SK: $HOST/v1/charges/$charge \ + curl -sSfg -u $SK: $HOST/v1/charges/$charge \ | grep -oE '"status": "succeeded"') [ -n "$status" ] # create a non-chargeable source card=$( - curl -sSf -u $SK: $HOST/v1/customers/$cus/cards \ + curl -sSfg -u $SK: $HOST/v1/customers/$cus/cards \ -d source[object]=card \ -d source[number]=4000000000000341 \ -d source[exp_month]=12 \ @@ -708,7 +708,7 @@ card=$( # create a normal charge, observe 402 response code=$( - curl -s -o /dev/null -w "%{http_code}" \ + curl -sg -o /dev/null -w "%{http_code}" \ -u $SK: $HOST/v1/charges \ -d source=$card \ -d amount=1000 \ @@ -717,7 +717,7 @@ code=$( # create a normal charge charge=$( - curl -s -u $SK: $HOST/v1/charges \ + curl -sg -u $SK: $HOST/v1/charges \ -d source=$card \ -d amount=1000 \ -d currency=usd \ @@ -725,14 +725,14 @@ charge=$( # verify charge status failed status=$( - curl -sSf -u $SK: $HOST/v1/charges/$charge \ + curl -sSfg -u $SK: $HOST/v1/charges/$charge \ | grep -oE '"status": "failed"') [ -n "$status" ] # create a pre-auth charge, observe 402 response code=$( - curl -s -o /dev/null -w "%{http_code}" \ + curl -sg -o /dev/null -w "%{http_code}" \ -u $SK: $HOST/v1/charges \ -d source=$card \ -d amount=1000 \ @@ -742,7 +742,7 @@ code=$( # create a pre-auth charge charge=$( - curl -s -u $SK: $HOST/v1/charges \ + curl -sg -u $SK: $HOST/v1/charges \ -d source=$card \ -d amount=1000 \ -d currency=usd \ @@ -751,21 +751,21 @@ charge=$( # verify charge status failed status=$( - curl -sSf -u $SK: $HOST/v1/charges/$charge \ + curl -sSfg -u $SK: $HOST/v1/charges/$charge \ | grep -oE '"status": "failed"') [ -n "$status" ] # list charges total_count=$( - curl -sSf -u $SK: $HOST/v1/charges | grep -oE '"total_count": 15') + curl -sSfg -u $SK: $HOST/v1/charges | grep -oE '"total_count": 15') [ -n "$total_count" ] total_count=$( - curl -sSf -u $SK: $HOST/v1/charges?customer=$cus \ + curl -sSfg -u $SK: $HOST/v1/charges?customer=$cus \ | grep -oE '"total_count": 6') [ -n "$total_count" ] total_count=$( - curl -sSf -u $SK: $HOST/v1/charges?customer=$cus\&created%5Bgt%5D=1588166306 \ + curl -sSfg -u $SK: $HOST/v1/charges?customer=$cus\&created[gt]=1588166306 \ | grep -oE '"total_count": 6') [ -n "$total_count" ] From e6c5886427cfdb1d762088ca612347dbf73230da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Thu, 24 Sep 2020 08:19:20 +0200 Subject: [PATCH 12/17] CI: Support Python 3.8 and 3.9 And drop support on Python 3.5 and 3.6. --- .travis.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0abd842e..0e4fd9d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,10 @@ --- language: python -dist: xenial # required for Python >= 3.7 (travis-ci/travis-ci#9069) python: - - 3.5 - - 3.6 - 3.7 - # Python nightly currently disabled because of: - # curl: (56) Recv failure: Connection reset by peer + - 3.8 + - 3.9-dev + # Python nightly currently disabled because of compilation issues # - nightly install: - pip install flake8 flake8-import-order doc8 Pygments From 591f3b5a62eccfbe86ac24a2b937dc047a46095a Mon Sep 17 00:00:00 2001 From: Daniel Nowak <13685818+lowlyocean@users.noreply.github.com> Date: Fri, 25 Sep 2020 02:09:19 -0400 Subject: [PATCH 13/17] Product: Default 'type' as 'service' Resolves https://github.com/adrienverge/localstripe/issues/150. --- localstripe/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localstripe/resources.py b/localstripe/resources.py index 60b75021..6cf9a01b 100644 --- a/localstripe/resources.py +++ b/localstripe/resources.py @@ -2023,7 +2023,7 @@ class Product(StripeObject): object = 'product' _id_prefix = 'prod_' - def __init__(self, id=None, name=None, type=None, active=True, + def __init__(self, id=None, name=None, type='service', active=True, caption=None, description=None, attributes=None, shippable=True, url=None, statement_descriptor=None, metadata=None, **kwargs): From 4b4576d0dd82b424b937453ab8a3ad39c9ed9fbe Mon Sep 17 00:00:00 2001 From: vados Date: Tue, 5 Jan 2021 13:15:15 +0900 Subject: [PATCH 14/17] Subscription: Add missing metadata to SubscriptionItems --- localstripe/resources.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/localstripe/resources.py b/localstripe/resources.py index 6cf9a01b..8a44e08c 100644 --- a/localstripe/resources.py +++ b/localstripe/resources.py @@ -2362,6 +2362,9 @@ def __init__(self, customer=None, metadata=None, items=None, if item['tax_rates'] is not None: assert type(item['tax_rates']) is list assert all(type(tr) is str for tr in item['tax_rates']) + item['metadata'] = item.get('metadata', None) + if item['metadata'] is not None: + assert type(item['metadata']) is dict assert type(enable_incomplete_payments) is bool assert payment_behavior in ('allow_incomplete', 'error_if_incomplete') @@ -2413,6 +2416,7 @@ def __init__(self, customer=None, metadata=None, items=None, subscription=self.id, plan=items[0]['plan'], quantity=items[0]['quantity'], + metadata=items[0]['metadata'], tax_rates=items[0]['tax_rates'])) create_an_invoice = \ @@ -2557,6 +2561,9 @@ def _update(self, metadata=None, items=None, trial_end=None, if item['tax_rates'] is not None: assert type(item['tax_rates']) is list assert all(type(tr) is str for tr in item['tax_rates']) + item['metadata'] = item.get('metadata', None) + if item['metadata'] is not None: + assert type(item['metadata']) is dict except AssertionError: raise UserError(400, 'Bad request') @@ -2586,6 +2593,7 @@ def _update(self, metadata=None, items=None, trial_end=None, item = SubscriptionItem(subscription=self.id, plan=items[0]['plan'], quantity=items[0]['quantity'], + metadata=items[0]['metadata'], tax_rates=items[0]['tax_rates']) self.items._list.append(item) From 6028cfdd28f336b596e7c0fb172eb24c9bf332df Mon Sep 17 00:00:00 2001 From: 2pac Date: Tue, 19 Jan 2021 11:26:46 +0100 Subject: [PATCH 15/17] List: Implement pagination with starting_after parameter --- localstripe/resources.py | 69 ++++++++++++++++++++++++++++++---------- test.sh | 12 +++++++ 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/localstripe/resources.py b/localstripe/resources.py index 8a44e08c..58b64e93 100644 --- a/localstripe/resources.py +++ b/localstripe/resources.py @@ -161,11 +161,11 @@ def _api_delete(cls, id): return {"deleted": True, "id": id} @classmethod - def _api_list_all(cls, url, limit=None, **kwargs): + def _api_list_all(cls, url, limit=None, starting_after=None, **kwargs): if kwargs: raise UserError(400, 'Unexpected ' + ', '.join(kwargs.keys())) - li = List(url, limit=limit) + li = List(url, limit=limit, starting_after=starting_after) li._list = [value for key, value in store.items() if key.startswith(cls.object + ':')] return li @@ -476,7 +476,8 @@ def refunded(self): return self.amount <= self.amount_refunded @classmethod - def _api_list_all(cls, url, customer=None, created=None, limit=10): + def _api_list_all(cls, url, customer=None, created=None, limit=10, + starting_after=None): try: if customer is not None: assert type(customer) is str and customer.startswith('cus_') @@ -499,7 +500,8 @@ def _api_list_all(cls, url, customer=None, created=None, limit=10): if type(created) is str or not created.get('gt'): raise UserError(500, 'Not implemented') - li = super(Charge, cls)._api_list_all(url, limit=limit) + li = super(Charge, cls)._api_list_all(url, limit=limit, + starting_after=starting_after) if customer: li._list = [c for c in li._list if c.customer == customer] if created and created.get('gt'): @@ -1254,7 +1256,8 @@ def _api_delete(cls, id): return super()._api_delete(id) @classmethod - def _api_list_all(cls, url, customer=None, subscription=None, limit=None): + def _api_list_all(cls, url, customer=None, subscription=None, limit=None, + starting_after=None): try: if customer is not None: assert type(customer) is str and customer.startswith('cus_') @@ -1264,7 +1267,8 @@ def _api_list_all(cls, url, customer=None, subscription=None, limit=None): except AssertionError: raise UserError(400, 'Bad request') - li = super(Invoice, cls)._api_list_all(url, limit=limit) + li = super(Invoice, cls)._api_list_all(url, limit=limit, + starting_after=starting_after) 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] @@ -1435,14 +1439,17 @@ def __init__(self, invoice=None, subscription=None, plan=None, amount=None, self.metadata = metadata or {} @classmethod - def _api_list_all(cls, url, customer=None, limit=None): + def _api_list_all(cls, url, customer=None, limit=None, + starting_after=None): try: if customer is not None: assert type(customer) is str and customer.startswith('cus_') except AssertionError: raise UserError(400, 'Bad request') - li = super(InvoiceItem, cls)._api_list_all(url, limit=limit) + li = super(InvoiceItem, + cls)._api_list_all(url, limit=limit, + starting_after=starting_after) li._list = [ii for ii in li._list if ii.invoice is None] if customer is not None: Customer._api_retrieve(customer) # to return 404 if not existant @@ -1513,11 +1520,13 @@ def _api_delete(cls, id): class List(StripeObject): object = 'list' - def __init__(self, url=None, limit=None): + def __init__(self, url=None, limit=None, starting_after=None): limit = try_convert_to_int(limit) limit = 10 if limit is None else limit try: assert type(limit) is int and limit > 0 + if starting_after is not None: + assert type(starting_after) is str and len(starting_after) > 0 except AssertionError: raise UserError(400, 'Bad request') @@ -1527,11 +1536,16 @@ def __init__(self, url=None, limit=None): self.url = url self._limit = limit + self._starting_after = starting_after + self._starting_pos = None self._list = [] @property def data(self): - return [item._export() for item in self._list][:self._limit] + self._compute_starting_pos() + return [item._export() for item in self._list[ + self._starting_pos:self._starting_pos + self._limit + ]] @property def total_count(self): @@ -1539,7 +1553,21 @@ def total_count(self): @property def has_more(self): - return len(self._list) > self._limit + self._compute_starting_pos() + return len(self._list) > self._limit + self._starting_pos + + def _compute_starting_pos(self): + if self._starting_pos is not None: + return + + self._starting_pos = 0 + if self._starting_after is None: + return + + for i, item in enumerate(self._list): + if getattr(item, 'id', None) == self._starting_after: + self._starting_pos = i + 1 + break class PaymentIntent(StripeObject): @@ -1911,7 +1939,8 @@ def _api_retrieve(cls, id): return super()._api_retrieve(id) @classmethod - def _api_list_all(cls, url, customer=None, type=None, limit=None): + def _api_list_all(cls, url, customer=None, type=None, limit=None, + starting_after=None): try: assert _type(customer) is str and customer.startswith('cus_') assert type in ('card', ) @@ -1920,7 +1949,9 @@ def _api_list_all(cls, url, customer=None, type=None, limit=None): Customer._api_retrieve(customer) # to return 404 if not existant - li = super(PaymentMethod, cls)._api_list_all(url, limit=limit) + li = super(PaymentMethod, + cls)._api_list_all(url, limit=limit, + starting_after=starting_after) li._list = [pm for pm in li._list if pm.customer == customer and pm.type == type] return li @@ -2100,14 +2131,15 @@ def __init__(self, charge=None, amount=None, metadata=None, **kwargs): self.amount = charge_obj.amount @classmethod - def _api_list_all(cls, url, charge=None, limit=None): + def _api_list_all(cls, url, charge=None, limit=None, starting_after=None): try: if charge is not None: assert type(charge) is str and charge.startswith('ch_') except AssertionError: raise UserError(400, 'Bad request') - li = super(Refund, cls)._api_list_all(url, limit=limit) + li = super(Refund, cls)._api_list_all(url, limit=limit, + starting_after=starting_after) if charge is not None: Charge._api_retrieve(charge) # to return 404 if not existant li._list = [r for r in li._list if r.charge == charge] @@ -2658,7 +2690,8 @@ def _api_delete(cls, id): return obj @classmethod - def _api_list_all(cls, url, customer=None, status=None, limit=None): + def _api_list_all(cls, url, customer=None, status=None, limit=None, + starting_after=None): try: if customer is not None: assert type(customer) is str and customer.startswith('cus_') @@ -2669,7 +2702,9 @@ def _api_list_all(cls, url, customer=None, status=None, limit=None): except AssertionError: raise UserError(400, 'Bad request') - li = super(Subscription, cls)._api_list_all(url, limit=limit) + li = super(Subscription, + cls)._api_list_all(url, limit=limit, + starting_after=starting_after) if status is None: li._list = [sub for sub in li._list if sub.status not in ('canceled', 'incomplete_expired')] diff --git a/test.sh b/test.sh index 265307bf..7aa749cc 100755 --- a/test.sh +++ b/test.sh @@ -769,3 +769,15 @@ total_count=$( curl -sSfg -u $SK: $HOST/v1/charges?customer=$cus\&created[gt]=1588166306 \ | grep -oE '"total_count": 6') [ -n "$total_count" ] + +no_more_events=$(curl -sSfg -u $SK: $HOST/v1/events \ + | grep -oE '^ "has_more": false' || true) +[ -z "$no_more_events" ] +last_event=$(curl -sSfg -u $SK: $HOST/v1/events?limit=100 \ + | grep -oE 'evt_\w+' | tail -n 1) +no_more_events=$(curl -sSfg -u $SK: $HOST/v1/events?starting_after=$last_event \ + | grep -oE '^ "has_more": false') +[ -n "$no_more_events" ] +zero_events=$(curl -sSfg -u $SK: $HOST/v1/events?starting_after=$last_event \ + | grep -oE '^ "data": \[\]') +[ -n "$zero_events" ] From e6a6af223831e8066e718bf66fd471835937b41d Mon Sep 17 00:00:00 2001 From: 2pac Date: Mon, 25 Jan 2021 08:10:18 +0100 Subject: [PATCH 16/17] Balance: Implement balance Stripe resource The current implementation supports the Balance resource as described in https://stripe.com/docs/api/balance --- localstripe/resources.py | 47 ++++++++++++++++++++++++++++++++++++++++ test.sh | 2 ++ 2 files changed, 49 insertions(+) diff --git a/localstripe/resources.py b/localstripe/resources.py index 58b64e93..c311c7a0 100644 --- a/localstripe/resources.py +++ b/localstripe/resources.py @@ -244,6 +244,53 @@ def do_expand(path, obj): return obj +class Balance(object): + object = 'balance' + + def __init__(self): + self.livemode = False + self.available = { + 'amount': 2000, + 'currency': 'eur', + 'source_types': { + 'card': 2000 + } + } + self.pending = { + 'amount': 0, + 'currency': 'eur', + 'source_types': { + 'card': 0 + } + } + + store[self.object] = self + + schedule_webhook(Event('balance.available', self)) + + @classmethod + def _api_retrieve(self): + obj = store.get(self.object) + if obj is None: + return self() + return obj + + def _export(self, expand=None): + obj = {} + + for key, value in vars(self).items(): + if not key.startswith('_'): + if isinstance(value, dict): + obj[key] = value.copy() + else: + obj[key] = value + + return obj + + +extra_apis.append(('GET', '/v1/balance', Balance._api_retrieve)) + + class Card(StripeObject): object = 'card' _id_prefix = 'card_' diff --git a/test.sh b/test.sh index 7aa749cc..4029d494 100755 --- a/test.sh +++ b/test.sh @@ -781,3 +781,5 @@ no_more_events=$(curl -sSfg -u $SK: $HOST/v1/events?starting_after=$last_event \ zero_events=$(curl -sSfg -u $SK: $HOST/v1/events?starting_after=$last_event \ | grep -oE '^ "data": \[\]') [ -n "$zero_events" ] + +curl -sSfg -u $SK: $HOST/v1/balance From 01e35a115902ef853ce19b9b660f94da0d107e74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Mon, 25 Jan 2021 08:05:27 +0100 Subject: [PATCH 17/17] refactor: Remove useless 'None' argument in dict.get() `None` is already the implicit default when using `get()` on a dict. --- localstripe/resources.py | 48 ++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/localstripe/resources.py b/localstripe/resources.py index c311c7a0..242a9777 100644 --- a/localstripe/resources.py +++ b/localstripe/resources.py @@ -139,7 +139,7 @@ def _api_create(cls, **data): @classmethod def _api_retrieve(cls, id): - obj = store.get(cls.object + ':' + id, None) + obj = store.get(cls.object + ':' + id) if obj is None: raise UserError(404, 'Not Found') @@ -301,18 +301,18 @@ def __init__(self, source=None, **kwargs): try: assert type(source) is dict - assert source.get('object', None) == 'card' - number = source.get('number', None) - exp_month = try_convert_to_int(source.get('exp_month', None)) - exp_year = try_convert_to_int(source.get('exp_year', None)) - cvc = source.get('cvc', None) - address_city = source.get('address_city', None) - address_country = source.get('address_country', None) - address_line1 = source.get('address_line1', None) - address_line2 = source.get('address_line2', None) - address_state = source.get('address_state', None) - address_zip = source.get('address_zip', None) - name = source.get('name', None) + assert source.get('object') == 'card' + number = source.get('number') + exp_month = try_convert_to_int(source.get('exp_month')) + exp_year = try_convert_to_int(source.get('exp_year')) + cvc = source.get('cvc') + address_city = source.get('address_city') + address_country = source.get('address_country') + address_line1 = source.get('address_line1') + address_line2 = source.get('address_line2') + address_state = source.get('address_state') + address_zip = source.get('address_zip') + name = source.get('name') assert type(number) is str and len(number) == 16 assert type(exp_month) is int assert exp_month >= 1 and exp_month <= 12 @@ -1145,8 +1145,8 @@ def _get_next_invoice(cls, customer=None, subscription=None, if subscription_items is not None: assert type(subscription_items) is list for si in subscription_items: - assert type(si.get('plan', None)) is str - si['tax_rates'] = si.get('tax_rates', None) + assert type(si.get('plan')) is str + si['tax_rates'] = si.get('tax_rates') if si['tax_rates'] is not None: assert type(si['tax_rates']) is list assert all(type(tr) is str for tr in si['tax_rates']) @@ -2430,18 +2430,18 @@ def __init__(self, customer=None, metadata=None, items=None, assert proration_behavior in ['create_prorations', 'none'] assert type(items) is list for item in items: - assert type(item.get('plan', None)) is str - if item.get('quantity', None) is not None: + assert type(item.get('plan')) is str + if item.get('quantity') is not None: item['quantity'] = try_convert_to_int(item['quantity']) assert type(item['quantity']) is int assert item['quantity'] > 0 else: item['quantity'] = 1 - item['tax_rates'] = item.get('tax_rates', None) + item['tax_rates'] = item.get('tax_rates') if item['tax_rates'] is not None: assert type(item['tax_rates']) is list assert all(type(tr) is str for tr in item['tax_rates']) - item['metadata'] = item.get('metadata', None) + item['metadata'] = item.get('metadata') if item['metadata'] is not None: assert type(item['metadata']) is dict assert type(enable_incomplete_payments) is bool @@ -2627,20 +2627,20 @@ def _update(self, metadata=None, items=None, trial_end=None, if items is not None: assert type(items) is list for item in items: - id = item.get('id', None) + id = item.get('id') if id is not None: assert type(id) is str and id.startswith('si_') - if item.get('quantity', None) is not None: + if item.get('quantity') is not None: item['quantity'] = try_convert_to_int(item['quantity']) assert type(item['quantity']) is int assert item['quantity'] > 0 else: item['quantity'] = 1 - item['tax_rates'] = item.get('tax_rates', None) + item['tax_rates'] = item.get('tax_rates') if item['tax_rates'] is not None: assert type(item['tax_rates']) is list assert all(type(tr) is str for tr in item['tax_rates']) - item['metadata'] = item.get('metadata', None) + item['metadata'] = item.get('metadata') if item['metadata'] is not None: assert type(item['metadata']) is dict except AssertionError: @@ -2653,7 +2653,7 @@ def _update(self, metadata=None, items=None, trial_end=None, # If no plan specified in update request, we stay on the current # one - if not items[0].get('plan', None): + if not items[0].get('plan'): items[0]['plan'] = self.plan.id # To return 404 if not existant: