From f32df161a633f5d8551bd41c5f4995a5384fbc1a Mon Sep 17 00:00:00 2001 From: "Abram C. Isola" Date: Mon, 7 May 2018 09:59:53 -0500 Subject: [PATCH 1/8] Change stripe driver's create_customer method to use arguments --- billing/drivers/BillingStripeDriver.py | 34 +++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/billing/drivers/BillingStripeDriver.py b/billing/drivers/BillingStripeDriver.py index d555eef..5d0dc82 100644 --- a/billing/drivers/BillingStripeDriver.py +++ b/billing/drivers/BillingStripeDriver.py @@ -30,7 +30,7 @@ def subscribe(self, plan, token, customer=None, **kwargs): raise PlanNotFound('The {0} plan was not found in Stripe'.format(plan)) if 'No such customer' in str(e): return False - + return None def trial(self, days=0): @@ -41,15 +41,15 @@ def on_trial(self, plan_id=None): try: if plan_id: subscription = self._get_subscription(plan_id) - + if subscription['status'] == 'trialing': return True return False - + except InvalidRequestError: return False return None - + def is_subscribed(self, plan_id, plan_name=None): try: # get the plan @@ -57,15 +57,15 @@ def is_subscribed(self, plan_id, plan_name=None): if not plan_name: if subscription['status'] in ('active', 'trialing'): return True - + if subscription["items"]["data"][0]['plan']['id'] == plan_name: return True except InvalidRequestError: return False - + return False - + def is_canceled(self, plan_id): try: # get the plan @@ -74,9 +74,9 @@ def is_canceled(self, plan_id): return True except InvalidRequestError: return False - + return False - + def cancel(self, plan_id, now=False): subscription = stripe.Subscription.retrieve(plan_id) @@ -85,12 +85,12 @@ def cancel(self, plan_id, now=False): return False def create_customer(self, description, token): - return self._create_customer('test-customer', 'tok_amex') + return self._create_customer(description, token) def skip_trial(self): self._subscription_args.update({'trial_end': 'now'}) return self - + def charge(self, amount, **kwargs): if not kwargs.get('currency'): kwargs.update({'currency': billing.DRIVERS['stripe']['currency']}) @@ -104,7 +104,7 @@ def charge(self, amount, **kwargs): return True else: return False - + def card(self, customer_id, token): stripe.Customer.modify(customer_id, source=token, @@ -132,7 +132,7 @@ def resume(self, plan_id): }] ) return True - + def plan(self, plan_id): subscription = self._get_subscription(plan_id) return subscription['plan']['name'] @@ -143,7 +143,7 @@ def _create_customer(self, description, token): description=description, source=token # obtained with Stripe.js ) - + def _create_subscription(self, customer, **kwargs): if not isinstance(customer, str): customer = customer['id'] @@ -157,6 +157,6 @@ def _create_subscription(self, customer, **kwargs): ) self._subscription_args = {} return subscription - - def _get_subscription(self, plan_id): - return stripe.Subscription.retrieve(plan_id) \ No newline at end of file + + def _get_subscription(self, plan_id): + return stripe.Subscription.retrieve(plan_id) From 526747d4f7b5bbf4533edf2410a8b155381f63db Mon Sep 17 00:00:00 2001 From: "Abram C. Isola" Date: Mon, 7 May 2018 21:12:37 -0500 Subject: [PATCH 2/8] Add ability to add coupon to subscription --- billing/drivers/BillingStripeDriver.py | 5 +++ billing/models/Billable.py | 58 +++++++++++++++----------- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/billing/drivers/BillingStripeDriver.py b/billing/drivers/BillingStripeDriver.py index 5d0dc82..a4ce539 100644 --- a/billing/drivers/BillingStripeDriver.py +++ b/billing/drivers/BillingStripeDriver.py @@ -9,6 +9,7 @@ except ImportError: raise ImportError('Billing configuration found') + class BillingStripeDriver: _subscription_args = {} @@ -33,6 +34,10 @@ def subscribe(self, plan, token, customer=None, **kwargs): return None + def coupon(self, coupon_id): + self._subscription_args.update({'coupon': coupon_id}) + return self + def trial(self, days=0): self._subscription_args.update({'trial_period_days': days}) return self diff --git a/billing/models/Billable.py b/billing/models/Billable.py index 8581eef..4700ba4 100644 --- a/billing/models/Billable.py +++ b/billing/models/Billable.py @@ -7,6 +7,7 @@ except ImportError: raise ImportError('No configuration file found') + class Billable: _processor = PROCESSOR @@ -17,7 +18,7 @@ def subscribe(self, processor_plan, token): """ if not self.customer_id: self.create_customer('Customer {0}'.format(self.email), token) - + if self.is_subscribed(processor_plan): return True @@ -34,7 +35,14 @@ def subscribe(self, processor_plan, token): self._save_subscription_model(processor_plan, subscription) return True - + + def coupon(self, coupon_id): + """ + Add coupon to subscription + """ + self._processor.coupon(coupon_id) + return self + def trial(self, days=False): """ Put user on trial @@ -51,14 +59,14 @@ def on_trial(self, plan_id=None): if not subscription: return False - + if not plan_id: if subscription.trial_ends_at and subscription.trial_ends_at.is_future(): return True - + if subscription.plan == plan_id and subscription.trial_ends_at and subscription.trial_ends_at.is_future(): return True - + return False def cancel(self, now=False): @@ -82,12 +90,12 @@ def cancel(self, now=False): subscription.save() return True return False - + def plan(self): subscription = self._get_subscription() if subscription: return subscription.plan_name - + return None def create_customer(self, description, token): @@ -95,7 +103,7 @@ def create_customer(self, description, token): self.customer_id = customer['id'] self.save() return self.customer_id - + def quantity(self, quantity): """ Set a quantity amount for a subscription @@ -112,10 +120,10 @@ def charge(self, amount, **kwargs): else: kwargs.update({'source': kwargs.get('token')}) del kwargs['token'] - + if not kwargs.get('description'): kwargs.update({'description': 'Charge For {0}'.format(self.email)}) - + return self._processor.charge(amount, **kwargs) """ Checking Subscription Status """ @@ -126,7 +134,7 @@ def on_grace_period(self): TODO """ pass - + def is_subscribed(self, plan_name=None): """ Check if a user is subscribed @@ -137,9 +145,9 @@ def is_subscribed(self, plan_name=None): # If the subscription does not expire OR the subscription ends at a time in the future if not self._get_subscription().ends_at or ( self._get_subscription().ends_at and self._get_subscription().ends_at.is_future()): # If the plan name equals the plan name specified - if plan_name and self._get_subscription().plan == plan_name: + if plan_name and self._get_subscription().plan == plan_name: return True - + # if the plan name was left out if not plan_name: return True @@ -155,7 +163,7 @@ def was_subscribed(self, plan=None): elif not plan: return True - + return False def is_canceled(self): @@ -184,7 +192,7 @@ def swap(self, new_plan, **kwargs): if swapped_subscription['trial_end']: trial_ends_at = pendulum.from_timestamp(swapped_subscription['trial_end']) - + if swapped_subscription['current_period_end']: ends_at = pendulum.from_timestamp(swapped_subscription['current_period_end']) @@ -194,22 +202,22 @@ def swap(self, new_plan, **kwargs): subscription.trial_ends_at = trial_ends_at subscription.ends_at = ends_at return subscription.save() - - + + def skip_trial(self): """ Skip any trial that the plan may have and charge the user """ self._processor.skip_trial() return self - + def prorate(self, bool): """ Whether the user should be prorated or not TODO: should be only parameters """ pass - + def resume(self): """ Resume a trial @@ -220,23 +228,23 @@ def resume(self): subscription.save() return plan - + def card(self, token): """ Change the card or token """ return self._processor.card(self.customer_id, token) - + def _get_subscription(self): return Subscription.where('user_id', self.id).first() - - def _save_subscription_model(self, processor_plan, subscription_object): + + def _save_subscription_model(self, processor_plan, subscription_object): trial_ends_at = None ends_at = None if subscription_object['trial_end']: trial_ends_at = pendulum.from_timestamp(subscription_object['trial_end']) - + if subscription_object['ended_at']: ends_at = pendulum.from_timestamp(subscription_object['ended_at']) @@ -260,4 +268,4 @@ def _save_subscription_model(self, processor_plan, subscription_object): ends_at = ends_at, ) - return subscription \ No newline at end of file + return subscription From 0565be39308d9262ef4bbd619a559790672388d5 Mon Sep 17 00:00:00 2001 From: "Abram C. Isola" Date: Mon, 7 May 2018 21:45:58 -0500 Subject: [PATCH 3/8] Add coupon method for adding discounts to new subscriptions --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 97f9909..b343a9d 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="masonite-billing", - version='0.1.1', + version='0.1.2', packages=[ 'billing', 'billing.commands', From ab96c0ca8fdf80e6ede9f47e23b0c026adf35178 Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Fri, 23 Nov 2018 21:26:03 -0500 Subject: [PATCH 4/8] bumped version --- .gitignore | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2080a90..3282aa8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ masonite_billing.egg-info .vscode .cache .env +dist +.pytest_cache **.pyc \ No newline at end of file diff --git a/setup.py b/setup.py index b343a9d..c1da820 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="masonite-billing", - version='0.1.2', + version='0.1.3', packages=[ 'billing', 'billing.commands', From 636a0c1a63001ed3b93d1d032b39d2440b221447 Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Fri, 23 Nov 2018 23:04:48 -0500 Subject: [PATCH 5/8] added coupon codes to subscriptions and charges --- billing/drivers/BillingStripeDriver.py | 57 ++++++++++++------- config/database.py | 8 +-- ...8_11_24_023016_make_subscriptions_table.py | 24 ++++++++ tests/migrations/__init__.py | 0 tests/test_stripe_billing.py | 19 +++++++ 5 files changed, 83 insertions(+), 25 deletions(-) create mode 100644 tests/migrations/2018_11_24_023016_make_subscriptions_table.py create mode 100644 tests/migrations/__init__.py diff --git a/billing/drivers/BillingStripeDriver.py b/billing/drivers/BillingStripeDriver.py index a4ce539..def6e90 100644 --- a/billing/drivers/BillingStripeDriver.py +++ b/billing/drivers/BillingStripeDriver.py @@ -21,14 +21,15 @@ def subscribe(self, plan, token, customer=None, **kwargs): try: subscription = self._create_subscription(customer, - plan=plan, - **kwargs - ) + plan=plan, + **kwargs + ) return subscription except InvalidRequestError as e: if 'No such plan' in str(e): - raise PlanNotFound('The {0} plan was not found in Stripe'.format(plan)) + raise PlanNotFound( + 'The {0} plan was not found in Stripe'.format(plan)) if 'No such customer' in str(e): return False @@ -36,6 +37,7 @@ def subscribe(self, plan, token, customer=None, **kwargs): def coupon(self, coupon_id): self._subscription_args.update({'coupon': coupon_id}) + return self def trial(self, days=0): @@ -85,7 +87,7 @@ def is_canceled(self, plan_id): def cancel(self, plan_id, now=False): subscription = stripe.Subscription.retrieve(plan_id) - if subscription.delete(at_period_end= not now): + if subscription.delete(at_period_end=not now): return subscription return False @@ -100,6 +102,8 @@ def charge(self, amount, **kwargs): if not kwargs.get('currency'): kwargs.update({'currency': billing.DRIVERS['stripe']['currency']}) + amount = self._apply_coupon(amount) + charge = stripe.Charge.create( amount=amount, **kwargs @@ -112,41 +116,56 @@ def charge(self, amount, **kwargs): def card(self, customer_id, token): stripe.Customer.modify(customer_id, - source=token, - ) + source=token, + ) return True def swap(self, plan, new_plan, **kwargs): subscription = stripe.Subscription.retrieve(plan) subscription = stripe.Subscription.modify(plan, - cancel_at_period_end=False, - items=[{ - 'id': subscription['items']['data'][0].id, - 'plan': new_plan, - }] - ) + cancel_at_period_end=False, + items=[{ + 'id': subscription['items']['data'][0].id, + 'plan': new_plan, + }] + ) return subscription def resume(self, plan_id): subscription = stripe.Subscription.retrieve(plan_id) stripe.Subscription.modify(plan_id, - cancel_at_period_end = False, - items=[{ - 'id': subscription['items']['data'][0].id - }] - ) + cancel_at_period_end=False, + items=[{ + 'id': subscription['items']['data'][0].id + }] + ) return True def plan(self, plan_id): subscription = self._get_subscription(plan_id) return subscription['plan']['name'] + def _apply_coupon(self, amount): + if 'coupon' in self._subscription_args: + if type(self._subscription_args['coupon']) == str: + coupon = stripe.Coupon.retrieve( + self._subscription_args['coupon']) + if coupon['percent_off']: + return abs((amount * (coupon['percent_off'] / 100)) - amount) + + return amount - coupon['amount_off'] + elif type(self._subscription_args['coupon']) == int: + return amount - self._subscription_args['coupon'] + elif type(self._subscription_args['coupon']) == float: + return abs((amount * (self._subscription_args['coupon'])) - amount) + + return amount def _create_customer(self, description, token): return stripe.Customer.create( description=description, - source=token # obtained with Stripe.js + source=token # obtained with Stripe.js ) def _create_subscription(self, customer, **kwargs): diff --git a/config/database.py b/config/database.py index 3640662..0cd2f23 100644 --- a/config/database.py +++ b/config/database.py @@ -30,12 +30,8 @@ DATABASES = { 'default': { - 'driver': os.environ.get('DB_DRIVER'), - 'host': os.environ.get('DB_HOST'), - 'database': os.environ.get('DB_DATABASE'), - 'user': os.environ.get('DB_USERNAME'), - 'password': os.environ.get('DB_PASSWORD'), - 'prefix': '' + 'driver': 'sqlite', + 'database': 'sqlite.db', } } diff --git a/tests/migrations/2018_11_24_023016_make_subscriptions_table.py b/tests/migrations/2018_11_24_023016_make_subscriptions_table.py new file mode 100644 index 0000000..b91d1d4 --- /dev/null +++ b/tests/migrations/2018_11_24_023016_make_subscriptions_table.py @@ -0,0 +1,24 @@ +from orator.migrations import Migration + + +class MakeSubscriptionsTable(Migration): + + def up(self): + """ + Run the migrations. + """ + with self.schema.create('subscriptions') as table: + table.increments('id') + table.integer('user_id').unsigned() + table.string('plan') + table.string('plan_id') + table.string('plan_name') + table.timestamp('trial_ends_at').nullable() + table.timestamp('ends_at').nullable() + table.timestamps() + + def down(self): + """ + Revert the migrations. + """ + self.schema.drop('subscriptions') diff --git a/tests/migrations/__init__.py b/tests/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_stripe_billing.py b/tests/test_stripe_billing.py index 28beb88..160e212 100644 --- a/tests/test_stripe_billing.py +++ b/tests/test_stripe_billing.py @@ -19,6 +19,7 @@ def save(self): pass user = User() +user.email = "test@email.com" if os.environ.get('STRIPE_CUSTOMER'): user.customer_id = os.getenv('STRIPE_CUSTOMER') @@ -222,3 +223,21 @@ def test_subscription_is_over(): user.cancel(now=True) if os.environ.get('TEST_ENVIRONMENT') == 'travis': time.sleep(2) + +def test_can_use_coupon_on_charge(): + assert user._processor._apply_coupon(1000) == 1000 + assert user.coupon('5-off')._processor._apply_coupon(500) == 400 + assert user.coupon('10-percent-off')._processor._apply_coupon(1000) == 900 + assert user.coupon('10-percent-off')._processor._apply_coupon(1499) == 1349.1 + assert user.coupon(.10)._processor._apply_coupon(1499) == 1349.1 + assert user.coupon(100)._processor._apply_coupon(1000) == 900 + + +def test_can_use_coupon_on_subscription(): + user.skip_trial().coupon('5-off').subscribe('masonite-test', 'tok_amex') + assert user.is_subscribed() is True + assert user.on_trial() is False + subscription = user._get_subscription() + + user.cancel(now=True) + From 1992dd170d5e27e77eb9881039f70a36a9b846ee Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Fri, 23 Nov 2018 23:06:04 -0500 Subject: [PATCH 6/8] updated travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a2a4180..a74089c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ services: - mysql before_install: -- mysql -u root --password="" < tests/travis.sql +- orator migrate -p tests/migrations -c config/database.py - pip install -r requirements.txt - pip install -e . From 636e2e11661208651800d3aa91a040933c56ee2f Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Fri, 23 Nov 2018 23:08:12 -0500 Subject: [PATCH 7/8] updates requirements --- .travis.yml | 2 +- requirements.txt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a74089c..cd1d01d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,8 +9,8 @@ services: - mysql before_install: -- orator migrate -p tests/migrations -c config/database.py - pip install -r requirements.txt +- orator migrate -p tests/migrations -c config/database.py - pip install -e . script: travis_retry pytest diff --git a/requirements.txt b/requirements.txt index 8c99e88..4edff70 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pytest -python-dotenv \ No newline at end of file +python-dotenv +orator \ No newline at end of file From 16c96379443b61f48d86f4ecbd9cace375efb74e Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Fri, 23 Nov 2018 23:12:38 -0500 Subject: [PATCH 8/8] fixed travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cd1d01d..5eb3144 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ services: before_install: - pip install -r requirements.txt -- orator migrate -p tests/migrations -c config/database.py +- orator migrate -p tests/migrations -c config/database.py -f - pip install -e . script: travis_retry pytest