diff --git a/.env-example b/.env-example index 595ae17..47ceeb9 100644 --- a/.env-example +++ b/.env-example @@ -1,6 +1,4 @@ DB_DATABASE=test.db DB_DRIVER=sqlite DB_CONNECTION=sqlite - -STRIPE_CLIENT= -STRIPE_SECRET= +DB_CONFIG_PATH=tests/integrations/config/database diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 0c1fe97..0bb1e55 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -7,33 +7,37 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '3.5', '3.6', '3.7', '3.8' ] + python-version: ["3.7", "3.8", "3.9", "3.10"] name: Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - make init - - name: Test with pytest - run: | - make test + - name: Install dependencies + run: | + make init + - name: Apply migrations + run: | + masonite-orm migrate -C tests/integrations/config/database -d tests/integrations/databases/migrations + - name: Test with pytest + env: + STRIPE_SECRET: ${{ secrets.STRIPE_SECRET }} + STRIPE_CLIENT: ${{ secrets.STRIPE_CLIENT }} + run: | + make test lint: runs-on: ubuntu-latest name: Lint steps: - - uses: actions/checkout@v1 - - name: Set up Python 3.6 - uses: actions/setup-python@v1 - with: - python-version: 3.6 - - name: Intall Flake8 - run: | - pip install flake8 - - name: Lint - run: make lint - - + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Intall Flake8 + run: | + pip install flake8 + - name: Lint + run: make lint diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index 6e974a5..92dbbfd 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -8,21 +8,21 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - make init - make test - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + make init + make test + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.gitignore b/.gitignore index 35d74bf..0ca4271 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,20 @@ venv -masonite_billing.egg-info +venv2 +.python-version .vscode -.cache -.env -dist -.pytest_cache +.idea/ +build/ +.pypirc +.coverage +coverage.xml +.pytest_* +**/*__pycache__* +**/*.DS_Store* **.pyc - -# local test -test.db +dist +.env +*.db +masonite_billing.egg-info +node_modules +tests/integrations/storage/compiled/ +storage/logs/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0340416 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021, Joseph Mancuso + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index 2b0ad82..53b52df 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,22 @@ init: cp .env-example .env pip install --upgrade pip - pip install . + pip install ".[test]" pip install -r requirements.txt - pip install pytest test: - orator migrate -p tests/migrations -c config/database.py -f python -m pytest tests ci: make test make lint lint: - python -m flake8 billing/ --ignore=E501,F401,E128,E402,E731,F821,E712,W503 + python -m flake8 . format: - black billing + black . coverage: - python -m pytest --cov-report term --cov-report xml --cov=billing tests/ + python -m pytest --cov-report term --cov-report xml --cov=src/masonite/billing tests/ python -m coveralls publish: - pip install 'twine>=1.5.0' python setup.py sdist bdist_wheel twine upload dist/* - rm -fr build dist .egg masonite.egg-info + rm -fr build dist .egg src/masonite_billing.egg-info diff --git a/README.md b/README.md index bf0c514..e76d308 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,68 @@ -# Masonite billing package +

+ +

-[Github Actions status](https://github.com/MasoniteFramework/billing/workflows/Test%20Application/badge.svg) -[![Coverage Status](https://coveralls.io/repos/github/MasoniteFramework/billing/badge.svg?branch=master)](https://coveralls.io/github/MasoniteFramework/billing?branch=master) +

+ + Masonite Package + + GitHub Workflow Status (branch) + + Python Version + PyPI + License + Code style: black +

+ +## Introduction + +Easily use Stripe's subscription billing services in your Masonite app. It can handle all of the boilerplate subscription billing code. + +[Documentation 📚](#) + +## Official Masonite Documentation + +New to Masonite ? Please first read the [Official Documentation](https://docs.masoniteproject.com/). +Masonite strives to have extremely comprehensive documentation 😃. It would be wise to go through the tutorials there. +If you find any discrepencies or anything that doesn't make sense, be sure to comment directly on the documentation to start a discussion! + +Also be sure to join the [Slack channel](http://slack.masoniteproject.com/)! + +## Installation + +```bash +pip install masonite-billing +``` + +## Configuration + +Add BillingProvider to your project in `config/providers.py`: + +```python +# config/providers.py +# ... +from masonite.billing import BillingProvider + +# ... +PROVIDERS = [ + # ... + + # Third Party Providers + BillingProvider, +] +``` + +Define your users as billable: + +```python +from masonite.billing import Billable + +class User(Billable): + +``` + +Finally publish the package configuration (to get `config/billing.py`) to your project: + +```bash +python craft install:billing +``` diff --git a/billing/contracts/BillingProcessorContract.py b/billing/contracts/BillingProcessorContract.py deleted file mode 100644 index 3ae5cfb..0000000 --- a/billing/contracts/BillingProcessorContract.py +++ /dev/null @@ -1,127 +0,0 @@ -from abc import ABC as AbstractBaseClass - - -class BillingProcessorContract(AbstractBaseClass): - def subscribe(self, plan, token, customer=None, **kwargs): - """Subscribe user to a billing plan. - - Arguments: - plan {string} -- The plan inside stripe. - token {string} -- The authentication token from a form submission. - - - Keyword Arguments: - customer {None|string} -- If None then the customer will be created based on the user. - Else it requires the customer ID. (default: {None}) - - Raises: - PlanNotFound -- Raised when the plan is not found. - - Returns: - billing.models.Subscription - The subscription billing model. - """ - pass - - def coupon(self, coupon_id): - """Sets the coupon that should be used on inside the subscription arguments. - - Arguments: - coupon_id {string} -- Stripes coupon ID - - Returns: - self - """ - pass - - def trial(self, days): - """Sets the trial days in the subscription args. - - Keyword Arguments: - days {int} -- Number of days to put the user on a trial. (default: {0}) - - Returns: - self - """ - pass - - def cancel(self, plan_id, now=False): - """Cancel the users subscription - - Arguments: - plan_id {string} -- The Stripe plan identifier. - - Keyword Arguments: - now {bool} -- Whether the user should be canceled now or at the end of the billing period. (default: {False}) - - Returns: - False|stripe.subscription.retrieve - """ - pass - - def charge(self, amount, **kwargs): - """Charges the user a specific amount of money - - Arguments: - amount {int} -- The amount to charge the customer in cents. - - Returns: - bool -- Whether the charge succeeded. - """ - pass - - def skip_trial(self): - """Whether the user should skip the trial and be charged right away. - - This updates the subscription arguments. - - Returns: - self - """ - pass - - def swap(self, plan_id, new_plan, **kwargs): - """Swaps the old plan for a new plan. - - Arguments: - plan {string} -- The old plan the user currently has. - new_plan {string} -- The new plan the user should be switched to. - - Returns: - stripe.Subscription.modify - """ - pass - - def resume(self, plan_id): - """Resume the user back on the subscription they may have cancelled. - - Arguments: - plan_id {string} -- The Stripe plan identifier. - - Returns: - True - """ - pass - - def card(self, customer_id, token): - """Updates the card on file with the user. - - Arguments: - customer_id {string} -- The Stripe customer identifier. - token {string} -- The Stripe token from the form submission. - - Returns: - True - """ - pass - - def _create_customer(self, description, token): - """Creates the customer in Stripe. - - Arguments: - description {string} -- The customer description. - token {string} -- The Stripe token from the form submission. - - Returns: - stripe.Customer.create - """ - pass diff --git a/billing/contracts/__init__.py b/billing/contracts/__init__.py deleted file mode 100644 index df8bbc0..0000000 --- a/billing/contracts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .BillingProcessorContract import BillingProcessorContract diff --git a/billing/exceptions.py b/billing/exceptions.py deleted file mode 100644 index 0f8ca02..0000000 --- a/billing/exceptions.py +++ /dev/null @@ -1,2 +0,0 @@ -class PlanNotFound(Exception): - pass diff --git a/billing/factories/BillingFactory.py b/billing/factories/BillingFactory.py deleted file mode 100644 index b30e142..0000000 --- a/billing/factories/BillingFactory.py +++ /dev/null @@ -1,8 +0,0 @@ -from billing.drivers import BillingStripeDriver - - -class BillingFactory: - @staticmethod - def make(driver): - if driver == "stripe": - return BillingStripeDriver() diff --git a/billing/factories/__init__.py b/billing/factories/__init__.py deleted file mode 100644 index 5c5e0c4..0000000 --- a/billing/factories/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .BillingFactory import BillingFactory diff --git a/billing/providers.py b/billing/providers.py deleted file mode 100644 index 3af66f8..0000000 --- a/billing/providers.py +++ /dev/null @@ -1,14 +0,0 @@ -""" A BillingProvider Service Provider """ -from masonite.provider import ServiceProvider -from billing.commands.InstallCommand import InstallCommand - - -class BillingProvider(ServiceProvider): - - wsgi = False - - def register(self): - self.app.bind("BillingInstallCommand", InstallCommand()) - - def boot(self): - pass diff --git a/billing/snippets/billing.py b/billing/snippets/billing.py deleted file mode 100644 index d1e8337..0000000 --- a/billing/snippets/billing.py +++ /dev/null @@ -1,13 +0,0 @@ -""" Masonite Billing Settings """ - -import os - -DRIVER = "stripe" - -DRIVERS = { - "stripe": { - "client": os.getenv("STRIPE_CLIENT"), - "secret": os.getenv("STRIPE_SECRET"), - "currency": "usd", - } -} diff --git a/config/auth.py b/config/auth.py deleted file mode 100644 index 83f281a..0000000 --- a/config/auth.py +++ /dev/null @@ -1,3 +0,0 @@ -AUTH = { - 'model': object -} \ No newline at end of file diff --git a/config/billing.py b/config/billing.py deleted file mode 100644 index 141e3dd..0000000 --- a/config/billing.py +++ /dev/null @@ -1,16 +0,0 @@ -''' Masonite Billing Settings ''' - -import os -from dotenv import find_dotenv, load_dotenv - -load_dotenv(find_dotenv()) - -DRIVER = 'stripe' - -DRIVERS = { - 'stripe': { - 'client': os.getenv('STRIPE_CLIENT'), - 'secret': os.getenv('STRIPE_SECRET'), - 'currency': 'usd', - } -} \ No newline at end of file diff --git a/config/database.py b/config/database.py deleted file mode 100644 index d7a27e6..0000000 --- a/config/database.py +++ /dev/null @@ -1,45 +0,0 @@ -''' Database Settings ''' - -import os - -from dotenv import find_dotenv, load_dotenv -from orator import DatabaseManager, Model -from masonite import env - -''' -|-------------------------------------------------------------------------- -| Load Environment Variables -|-------------------------------------------------------------------------- -| -| Loads in the environment variables when this page is imported. -| -''' - -load_dotenv(find_dotenv()) - -''' -|-------------------------------------------------------------------------- -| Database Settings -|-------------------------------------------------------------------------- -| -| Set connection database settings here as a dictionary. Follow the -| format below to create additional connection settings. -| -| @see Orator migrations documentation for more info -| -''' -DATABASES = { - 'default': os.environ.get('DB_DRIVER'), - 'sqlite': { - 'driver': 'sqlite', - 'database': os.environ.get('DB_DATABASE') - }, - os.environ.get('DB_DRIVER'): { - 'driver': os.environ.get('DB_DRIVER'), - 'database': os.environ.get('DB_DATABASE'), - 'prefix': '' - } -} - -DB = DatabaseManager(DATABASES) -Model.set_connection_resolver(DB) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..eecce90 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.black] +line-length = 99 +target-version = ['py38'] +include = '\.pyi?$' +exclude = ''' +/( + \.git + \.github + \.vscode + | \.venv + | docs + | node_modules + | templates +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +include_trailing_comma = true diff --git a/requirements.txt b/requirements.txt index 7fe8aea..7f6ba81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,4 @@ -python-dotenv -orator -masonite>=2.2 +https://github.com/MasoniteFramework/orm/archive/2.0.zip stripe flake8 pytest diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b08c3b6 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,18 @@ +[flake8] +exclude = + .git, + .github, + .vscode, + __pycache__, + templates, + node_modules, + venv +max-complexity = 10 +max-line-length = 99 + +omit = + */config/* + setup.py + stubs/ + wsgi.py + tests/ diff --git a/setup.py b/setup.py index 0da7c6e..b1da046 100644 --- a/setup.py +++ b/setup.py @@ -1,23 +1,56 @@ from setuptools import setup +with open("README.md", "r") as fh: + long_description = fh.read() setup( name="masonite-billing", - version='3.0.2', + version="4.0.0", packages=[ - 'billing', - 'billing.commands', - 'billing.contracts', - 'billing.controllers', - 'billing.drivers', - 'billing.factories', - 'billing.models', - 'billing.snippets', + "masonite.billing", + "masonite.billing.commands", + "masonite.billing.config", + "masonite.billing.controllers", + "masonite.billing.drivers", + "masonite.billing.models", + "masonite.billing.providers", ], + package_dir={"": "src"}, + description="Masonite billing management", + long_description=long_description, + long_description_content_type="text/markdown", + # The project's main homepage. + url="https://github.com/masoniteframework/billing", + # Author details + author="Joe Mancuso", + author_email="joe@masoniteproject.com", install_requires=[ - 'masonite>=2.2', - 'cleo', - 'stripe==2.40.0', + "masonite>=4.0<5.0", + "cleo", + "stripe==2.40.0", ], + license="MIT", + keywords="Masonite, Python, Stripe", + # If your package should include things you specify in your MANIFEST.in file + # Use this option if your package needs to include files that are not python files + # like html templates or css files include_package_data=True, + # List additional groups of dependencies here (e.g. development + # dependencies). You can install these using the following syntax, + # for example: + # $ pip install -e .[dev,test] + # $ pip install your-package[dev,test] + extras_require={ + "test": [ + "coverage", + "pytest", + "pytest-cov", + "coveralls", + ], + "dev": [ + "black", + "flake8", + "twine>=1.5.0", + ], + }, ) diff --git a/config/__init__.py b/src/masonite/__init__.py similarity index 100% rename from config/__init__.py rename to src/masonite/__init__.py diff --git a/src/masonite/billing/Billing.py b/src/masonite/billing/Billing.py new file mode 100644 index 0000000..73bb8df --- /dev/null +++ b/src/masonite/billing/Billing.py @@ -0,0 +1,18 @@ +from masonite.configuration import config + + +class Billing: + def __init__(self, application): + self.application = application + self.drivers = {} + self.drivers_config = {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def get_driver(self, name=None): + if name is None: + name = config("billing.drivers.default") + driver = self.drivers[name] + return driver.set_options(config(f"billing.drivers.{name}")) diff --git a/src/masonite/billing/__init__.py b/src/masonite/billing/__init__.py new file mode 100644 index 0000000..5cee535 --- /dev/null +++ b/src/masonite/billing/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa: E501 +from .providers.BillingProvider import BillingProvider +from .models import Billable, Subscription diff --git a/billing/commands/InstallCommand.py b/src/masonite/billing/commands/InstallCommand.py similarity index 57% rename from billing/commands/InstallCommand.py rename to src/masonite/billing/commands/InstallCommand.py index 1e8b686..9c44967 100644 --- a/billing/commands/InstallCommand.py +++ b/src/masonite/billing/commands/InstallCommand.py @@ -2,7 +2,8 @@ import os from cleo import Command -from masonite.packages import create_or_append_config + +# from masonite.packages import create_or_append_config package_directory = os.path.dirname(os.path.realpath(__file__)) @@ -15,6 +16,7 @@ class InstallCommand(Command): """ def handle(self): - create_or_append_config( - os.path.join(package_directory, "../snippets/billing.py") - ) + pass + # create_or_append_config( + # os.path.join(package_directory, "../snippets/billing.py") + # ) diff --git a/src/masonite/billing/config/billing.py b/src/masonite/billing/config/billing.py new file mode 100644 index 0000000..733f8cf --- /dev/null +++ b/src/masonite/billing/config/billing.py @@ -0,0 +1,10 @@ +from masonite.environment import env + +DRIVERS = { + "default": "stripe", + "stripe": { + "client": env("STRIPE_CLIENT"), + "secret": env("STRIPE_SECRET"), + "currency": "usd", + }, +} diff --git a/billing/controllers/WebhookController.py b/src/masonite/billing/controllers/WebhookController.py similarity index 85% rename from billing/controllers/WebhookController.py rename to src/masonite/billing/controllers/WebhookController.py index 70ee161..bad9004 100644 --- a/billing/controllers/WebhookController.py +++ b/src/masonite/billing/controllers/WebhookController.py @@ -1,19 +1,19 @@ """ Masonite Billing Controller For Webhooks """ -from billing.models import Subscription -from config import auth import pendulum from masonite.request import Request -from masonite.helpers import config -from config.auth import AUTH +from masonite.configuration import config +from masonite.controllers import Controller +from ..models import Subscription -class WebhookController: + +class WebhookController(Controller): """ Add webhooks to tie into stripe events """ - model = AUTH["guards"]["web"]["model"] + model = config("auth.guards.web.model") def handle(self, request: Request): """ diff --git a/src/masonite/billing/controllers/__init__.py b/src/masonite/billing/controllers/__init__.py new file mode 100644 index 0000000..1f90269 --- /dev/null +++ b/src/masonite/billing/controllers/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa: E501 +from .WebhookController import WebhookController diff --git a/billing/drivers/BillingStripeDriver.py b/src/masonite/billing/drivers/BillingStripeDriver.py similarity index 88% rename from billing/drivers/BillingStripeDriver.py rename to src/masonite/billing/drivers/BillingStripeDriver.py index 7b4f5e4..a2d8202 100644 --- a/billing/drivers/BillingStripeDriver.py +++ b/src/masonite/billing/drivers/BillingStripeDriver.py @@ -1,23 +1,29 @@ -""" The Stripe Billing Driver """ - -import pendulum import stripe from stripe.error import InvalidRequestError -from billing.exceptions import PlanNotFound - -try: - from config import billing - - stripe.api_key = billing.DRIVERS["stripe"]["secret"] -except ImportError: - raise ImportError("Billing configuration found") +from ..exceptions import PlanNotFound, InvalidDriverConfiguration class BillingStripeDriver: + """Stripe billing driver""" _subscription_args = {} + def __init__(self, application): + self.application = application + self.options = {} + + def set_options(self, options): + self.options = options + api_key = self.options.get("secret") + if api_key: + stripe.api_key = api_key + else: + raise InvalidDriverConfiguration( + "Stripe API key not found. Please provide 'secret' in billing configuration file." + ) + return self + def subscribe(self, plan, token, customer=None, **kwargs): """Subscribe user to a billing plan. @@ -81,8 +87,8 @@ def on_trial(self, plan_id=None): """Checks if the user in on a trial Keyword Arguments: - plan_id {string|None} -- If the argument is None it will look up the users current plan. Else - it will find out if the user is subscribed to the plan given. (default: {None}) + plan_id {string|None} -- If None it will look up the users current plan. Else it will + find out if the user is subscribed to the plan given. (default: {None}) Returns: bool @@ -106,7 +112,8 @@ def is_subscribed(self, plan_id, plan_name=None): plan_id {string} -- The plan identifier to check for Keyword Arguments: - plan_name {string|None} -- The plan name the user should be subscribed to. (default: {None}) + plan_name {string|None} -- The plan name the user should be subscribed to. + (default: {None}) Returns: bool @@ -138,10 +145,7 @@ def is_canceled(self, plan_id): try: # get the plan subscription = self._get_subscription(plan_id) - if ( - subscription["cancel_at_period_end"] is True - and subscription["status"] == "active" - ): + if subscription["cancel_at_period_end"] is True and subscription["status"] == "active": return True except InvalidRequestError: return False @@ -155,7 +159,8 @@ def cancel(self, plan_id, now=False): plan_id {string} -- The Stripe plan identifier. Keyword Arguments: - now {bool} -- Whether the user should be canceled now or at the end of the billing period. (default: {False}) + now {bool} -- Whether the user should be canceled now or at the end of the billing + period. (default: {False}) Returns: False|stripe.subscription.retrieve @@ -206,7 +211,7 @@ def charge(self, amount, **kwargs): bool -- Whether the charge succeeded. """ if not kwargs.get("currency"): - kwargs.update({"currency": billing.DRIVERS["stripe"]["currency"]}) + kwargs.update({"currency": self.options.get("currency")}) amount = self._apply_coupon(amount) @@ -228,7 +233,8 @@ def card(self, customer_id, token): True """ stripe.Customer.modify( - customer_id, source=token, + customer_id, + source=token, ) return True @@ -247,7 +253,12 @@ def swap(self, plan, new_plan, **kwargs): subscription = stripe.Subscription.modify( plan, cancel_at_period_end=True, - items=[{"id": subscription["items"]["data"][0].id, "plan": new_plan, }], + items=[ + { + "id": subscription["items"]["data"][0].id, + "plan": new_plan, + } + ], ) return subscription diff --git a/billing/drivers/__init__.py b/src/masonite/billing/drivers/__init__.py similarity index 71% rename from billing/drivers/__init__.py rename to src/masonite/billing/drivers/__init__.py index 337de74..e159b91 100644 --- a/billing/drivers/__init__.py +++ b/src/masonite/billing/drivers/__init__.py @@ -1 +1,2 @@ +# flake8: noqa: E501 from .BillingStripeDriver import BillingStripeDriver diff --git a/src/masonite/billing/exceptions.py b/src/masonite/billing/exceptions.py new file mode 100644 index 0000000..fba8ab6 --- /dev/null +++ b/src/masonite/billing/exceptions.py @@ -0,0 +1,6 @@ +class PlanNotFound(Exception): + pass + + +class InvalidDriverConfiguration(Exception): + pass diff --git a/billing/models/Billable.py b/src/masonite/billing/models/Billable.py similarity index 84% rename from billing/models/Billable.py rename to src/masonite/billing/models/Billable.py index 4d94730..9862cf8 100644 --- a/billing/models/Billable.py +++ b/src/masonite/billing/models/Billable.py @@ -1,22 +1,13 @@ -""" The Billing Model """ - import pendulum -from billing.factories import BillingFactory - from .Subscription import Subscription -try: - from config import billing - - PROCESSOR = BillingFactory.make(billing.DRIVER) -except ImportError: - raise ImportError("No configuration file found") - class Billable: + def get_driver(self): + from wsgi import application - _processor = PROCESSOR + return application.make("billing").get_driver() def subscribe(self, processor_plan, token): """Subscribe user to a billing plan. @@ -39,9 +30,7 @@ def subscribe(self, processor_plan, token): else: customer_id = None - subscription = self._processor.subscribe( - processor_plan, token, customer=customer_id - ) + subscription = self.get_driver().subscribe(processor_plan, token, customer=customer_id) self.plan_id = subscription["id"] self.save() @@ -63,7 +52,7 @@ def coupon(self, coupon_id): Returns: self """ - self._processor.coupon(coupon_id) + self.get_driver().coupon(coupon_id) return self def trial(self, days=False): @@ -75,7 +64,7 @@ def trial(self, days=False): Returns: self """ - self._processor.trial(days) + self.get_driver().trial(days) return self def on_trial(self, plan_id=None): @@ -109,12 +98,13 @@ def cancel(self, now=False): """Cancel a subscription. Keyword Arguments: - now {bool} -- Whether the user should be cancelled now or when the pay period ends. (default: {False}) + now {bool} -- Whether the user should be cancelled now or when the pay period ends. + (default: {False}) Returns: bool -- Whether or not the user has been successfully cancelled. """ - cancel = self._processor.cancel(self.plan_id, now=now) + cancel = self.get_driver().cancel(self.plan_id, now=now) if cancel: if now: @@ -156,7 +146,7 @@ def create_customer(self, description, token): Returns: string -- Returns the customer id. """ - customer = self._processor._create_customer(description, token) + customer = self.get_driver()._create_customer(description, token) self.customer_id = customer["id"] self.save() return self.customer_id @@ -191,19 +181,19 @@ def charge(self, amount, **kwargs): if not kwargs.get("description"): kwargs.update({"description": "Charge For {0}".format(self.email)}) - return self._processor.charge(amount, **kwargs) + return self.get_driver().charge(amount, **kwargs) def on_grace_period(self): - """Check if a user is on a grace period - """ + """Check if a user is on a grace period""" pass def is_subscribed(self, plan_name=None): """Check if a user is subscribed. Keyword Arguments: - plan_name {string} -- The plan name or None. If it is None this will check if the user is subscribed. - If a string exists it will check if a user is subscribed to that plan. (default: {None}) + plan_name {string} -- The plan name or None. If it is None this will check if the user + is subscribed. If a string exists it will check if a user is subscribed to that plan. + (default: {None}) Returns: bool -- Whether the user is subscribed or not. @@ -212,8 +202,7 @@ def is_subscribed(self, plan_name=None): if self._get_subscription(): # 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() + 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: @@ -229,8 +218,9 @@ def was_subscribed(self, plan=None): """Checks if the user was subscribed at one point but is no longer Keyword Arguments: - plan {string|None} -- The plan name or None. If it is None this will check if the user is subscribed. - If a string exists it will check if a user is subscribed to that plan. (default: {None}) + plan {string|None} -- The plan name or None. If it is None this will check if the user + is subscribed. If a string exists it will check if a user is subscribed to that + plan. (default: {None}) Returns: bool -- Whether the user was subscribed at one point but is not currently subscribed. @@ -246,7 +236,8 @@ def was_subscribed(self, plan=None): return False def is_canceled(self): - """Check if the user was subscribed but cancelled their subscription. This is useful if the user is on a grace period. + """Check if the user was subscribed but cancelled their subscription. This is useful if + the user is on a grace period. Returns: bool @@ -279,7 +270,7 @@ def swap(self, new_plan, **kwargs): """ trial_ends_at = None ends_at = None - swapped_subscription = self._processor.swap(self.plan_id, new_plan, **kwargs) + swapped_subscription = self.get_driver().swap(self.plan_id, new_plan, **kwargs) # if swapped_subscription['plan']['trial_end']: # trial_ends_at = pendulum.from_timestamp( @@ -302,7 +293,7 @@ def skip_trial(self): Returns: self """ - self._processor.skip_trial() + self.get_driver().skip_trial() return self def prorate(self, bool): @@ -318,7 +309,7 @@ def resume(self): Returns: processor.resume -- Returns the processor resume method. """ - plan = self._processor.resume(self.plan_id) + plan = self.get_driver().resume(self.plan_id) subscription = self._get_subscription() subscription.ends_at = None subscription.save() @@ -333,7 +324,7 @@ def card(self, token): Returns: processor.card -- Returns the processor card method. """ - return self._processor.card(self.customer_id, token) + return self.get_driver().card(self.customer_id, token) def _get_subscription(self): """Gets the subscription from the subcriptions table. @@ -357,19 +348,22 @@ def _save_subscription_model(self, processor_plan, subscription_object): ends_at = None if subscription_object["plan"]["trial_period_days"]: - trial_ends_at = pendulum.now().add( - days=subscription_object["plan"]["trial_period_days"] + # TODO: when ORM issue is fixed, remove to_datetime_string() + trial_ends_at = ( + pendulum.now() + .add(days=subscription_object["plan"]["trial_period_days"]) + .to_datetime_string() ) if subscription_object["ended_at"]: ends_at = pendulum.from_timestamp(subscription_object["ended_at"]) subscription = Subscription.where("user_id", self.id).first() - # product = self._processor.resubscription_object['plan']['product'] + # product = self.get_driver().resubscription_object['plan']['product'] if subscription: subscription.plan = processor_plan subscription.plan_id = subscription_object["id"] - subscription.plan_name = self._processor.plan(subscription_object["id"]) + subscription.plan_name = self.get_driver().plan(subscription_object["id"]) subscription.trial_ends_at = trial_ends_at subscription.ends_at = ends_at subscription.save() @@ -379,7 +373,7 @@ def _save_subscription_model(self, processor_plan, subscription_object): user_id=self.id, plan=processor_plan, plan_id=subscription_object["id"], - plan_name=self._processor.plan(subscription_object["id"]), + plan_name=self.get_driver().plan(subscription_object["id"]), trial_ends_at=trial_ends_at, ends_at=ends_at, ) diff --git a/billing/models/Subscription.py b/src/masonite/billing/models/Subscription.py similarity index 85% rename from billing/models/Subscription.py rename to src/masonite/billing/models/Subscription.py index 0621c0d..2ebe43c 100644 --- a/billing/models/Subscription.py +++ b/src/masonite/billing/models/Subscription.py @@ -1,4 +1,4 @@ -from config.database import Model +from masoniteorm.models import Model class Subscription(Model): diff --git a/billing/models/__init__.py b/src/masonite/billing/models/__init__.py similarity index 76% rename from billing/models/__init__.py rename to src/masonite/billing/models/__init__.py index e14e350..1a994e6 100644 --- a/billing/models/__init__.py +++ b/src/masonite/billing/models/__init__.py @@ -1,2 +1,3 @@ +# flake8: noqa: E501 from .Billable import Billable from .Subscription import Subscription diff --git a/src/masonite/billing/providers/BillingProvider.py b/src/masonite/billing/providers/BillingProvider.py new file mode 100644 index 0000000..1788744 --- /dev/null +++ b/src/masonite/billing/providers/BillingProvider.py @@ -0,0 +1,21 @@ +from masonite.providers import Provider + +from ..commands.InstallCommand import InstallCommand +from ..Billing import Billing +from ..drivers import BillingStripeDriver + + +class BillingProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + # billing = Billing(self.application).set_configuration(Config.get("mail.drivers")) + billing = Billing(self.application) + billing.add_driver("stripe", BillingStripeDriver(self.application)) + self.application.bind("billing", billing) + + self.application.make("commands").add(InstallCommand()) + + def boot(self): + pass diff --git a/src/masonite/billing/providers/__init__.py b/src/masonite/billing/providers/__init__.py new file mode 100644 index 0000000..015c702 --- /dev/null +++ b/src/masonite/billing/providers/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa: E501 +from .BillingProvider import BillingProvider diff --git a/tests/integrations/Kernel.py b/tests/integrations/Kernel.py new file mode 100644 index 0000000..1639bd2 --- /dev/null +++ b/tests/integrations/Kernel.py @@ -0,0 +1,107 @@ +from masonite.foundation import response_handler +from masonite.storage import StorageCapsule +from masonite.auth import Sign +from masonite.environment import LoadEnvironment +from masonite.utils.structures import load +from masonite.utils.location import base_path +from masonite.middleware import ( + SessionMiddleware, + EncryptCookies, + LoadUserMiddleware, +) +from masonite.routes import Route +from masonite.configuration.Configuration import Configuration +from masonite.configuration import config + +from .app.middleware.VerifyCsrfToken import VerifyCsrfToken + + +class Kernel: + + http_middleware = [EncryptCookies] + + route_middleware = { + "web": [SessionMiddleware, LoadUserMiddleware, VerifyCsrfToken], + } + + def __init__(self, app): + self.application = app + + def register(self): + # Register routes + self.load_environment() + self.register_configurations() + self.register_middleware() + self.register_routes() + self.register_database() + self.register_templates() + self.register_storage() + + def load_environment(self): + LoadEnvironment() + + def register_configurations(self): + # load configuration + self.application.bind("config.location", "tests/integrations/config") + configuration = Configuration(self.application) + configuration.load() + self.application.bind("config", configuration) + key = config("application.key") + self.application.bind("key", key) + self.application.bind("sign", Sign(key)) + # set locations + self.application.bind("controllers.location", "tests/integrations/app/controllers") + self.application.bind("jobs.location", "tests/integrations/jobs") + self.application.bind("providers.location", "tests/integrations/providers") + self.application.bind("mailables.location", "tests/integrations/mailables") + self.application.bind("listeners.location", "tests/integrations/listeners") + self.application.bind("validation.location", "tests/integrations/validation") + self.application.bind("notifications.location", "tests/integrations/notifications") + self.application.bind("events.location", "tests/integrations/events") + self.application.bind("tasks.location", "tests/integrations/tasks") + + self.application.bind("server.runner", "masonite.commands.ServeCommand.main") + + def register_middleware(self): + self.application.make("middleware").add(self.route_middleware).add(self.http_middleware) + + def register_routes(self): + Route.set_controller_locations(self.application.make("controllers.location")) + self.application.bind("routes.location", "tests/integrations/routes/web") + self.application.make("router").add( + Route.group( + load(self.application.make("routes.location"), "ROUTES"), middleware=["web"] + ) + ) + + def register_database(self): + from masoniteorm.query import QueryBuilder + + self.application.bind( + "builder", + QueryBuilder(connection_details=config("database.databases")), + ) + + self.application.bind("migrations.location", "tests/integrations/databases/migrations") + self.application.bind("seeds.location", "tests/integrations/databases/seeds") + + self.application.bind("resolver", config("database.db")) + + def register_templates(self): + self.application.bind("views.location", "tests/integrations/templates/") + + def register_storage(self): + storage = StorageCapsule() + storage.add_storage_assets( + { + # folder # template alias + "tests/integrations/storage/static": "static/", + "tests/integrations/storage/compiled": "static/", + "tests/integrations/storage/uploads": "static/", + "tests/integrations/storage/public": "/", + } + ) + self.application.bind("storage_capsule", storage) + + self.application.set_response_handler(response_handler) + self.application.use_storage_path(base_path("storage")) diff --git a/tests/migrations/__init__.py b/tests/integrations/__init__.py similarity index 100% rename from tests/migrations/__init__.py rename to tests/integrations/__init__.py diff --git a/MANIFEST b/tests/integrations/app/__init__.py similarity index 100% rename from MANIFEST rename to tests/integrations/app/__init__.py diff --git a/tests/integrations/app/controllers/WelcomeController.py b/tests/integrations/app/controllers/WelcomeController.py new file mode 100644 index 0000000..28c74b8 --- /dev/null +++ b/tests/integrations/app/controllers/WelcomeController.py @@ -0,0 +1,10 @@ +"""A WelcomeController Module.""" +from masonite.views import View +from masonite.controllers import Controller + + +class WelcomeController(Controller): + """WelcomeController Controller Class.""" + + def show(self, view: View): + return view.render("welcome") diff --git a/tests/integrations/app/controllers/__init__.py b/tests/integrations/app/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integrations/app/middleware/VerifyCsrfToken.py b/tests/integrations/app/middleware/VerifyCsrfToken.py new file mode 100644 index 0000000..d1f7b11 --- /dev/null +++ b/tests/integrations/app/middleware/VerifyCsrfToken.py @@ -0,0 +1,6 @@ +from masonite.middleware import VerifyCsrfToken as Middleware + + +class VerifyCsrfToken(Middleware): + + exempt = [] diff --git a/tests/integrations/app/models/User.py b/tests/integrations/app/models/User.py new file mode 100644 index 0000000..36f2a77 --- /dev/null +++ b/tests/integrations/app/models/User.py @@ -0,0 +1,11 @@ +"""User Model.""" +from masoniteorm.models import Model +from masoniteorm.scopes import SoftDeletesMixin + + +class User(Model, SoftDeletesMixin): + """User Model.""" + + __fillable__ = ["name", "email", "password"] + __hidden__ = ["password"] + __auth__ = "email" diff --git a/tests/integrations/config/__init__.py b/tests/integrations/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integrations/config/application.py b/tests/integrations/config/application.py new file mode 100644 index 0000000..01c4c2f --- /dev/null +++ b/tests/integrations/config/application.py @@ -0,0 +1,12 @@ +from masonite.environment import env + + +KEY = env("APP_KEY", "-RkDOqXojJIlsF_I8wWiUq_KRZ0PtGWTOZ676u5HtLg=") + +HASHING = { + "default": env("HASHING_FUNCTION", "bcrypt"), + "bcrypt": {"rounds": 10}, + "argon2": {"memory": 1024, "threads": 2, "time": 2}, +} + +APP_URL = env("APP_URL", "http://localhost:8000/") diff --git a/tests/integrations/config/auth.py b/tests/integrations/config/auth.py new file mode 100644 index 0000000..012d650 --- /dev/null +++ b/tests/integrations/config/auth.py @@ -0,0 +1,8 @@ +from ..app.models.User import User + +GUARDS = { + "default": "web", + "web": {"model": User}, + "password_reset_table": "password_resets", + "password_reset_expiration": 1440, # in minutes. 24 hours. None if disabled +} diff --git a/tests/integrations/config/billing.py b/tests/integrations/config/billing.py new file mode 100644 index 0000000..733f8cf --- /dev/null +++ b/tests/integrations/config/billing.py @@ -0,0 +1,10 @@ +from masonite.environment import env + +DRIVERS = { + "default": "stripe", + "stripe": { + "client": env("STRIPE_CLIENT"), + "secret": env("STRIPE_SECRET"), + "currency": "usd", + }, +} diff --git a/tests/integrations/config/broadcast.py b/tests/integrations/config/broadcast.py new file mode 100644 index 0000000..78bc948 --- /dev/null +++ b/tests/integrations/config/broadcast.py @@ -0,0 +1,14 @@ +from masonite.environment import env + + +BROADCASTS = { + "default": "pusher", + "pusher": { + "driver": "pusher", + "client": env("PUSHER_CLIENT"), + "app_id": env("PUSHER_APP_ID"), + "secret": env("PUSHER_SECRET"), + "cluster": env("PUSHER_CLUSTER"), + "ssl": False, + }, +} diff --git a/tests/integrations/config/cache.py b/tests/integrations/config/cache.py new file mode 100644 index 0000000..4f8f095 --- /dev/null +++ b/tests/integrations/config/cache.py @@ -0,0 +1,22 @@ +STORES = { + "default": "local", + "local": { + "driver": "file", + "location": "storage/framework/cache" + # + }, + "redis": { + "driver": "redis", + "host": "127.0.0.1", + "port": "6379", + "password": "", + "name": "project_name", + }, + "memcache": { + "driver": "memcache", + "host": "127.0.0.1", + "port": "11211", + "password": "", + "name": "project_name", + }, +} diff --git a/tests/integrations/config/database.py b/tests/integrations/config/database.py new file mode 100644 index 0000000..091d99f --- /dev/null +++ b/tests/integrations/config/database.py @@ -0,0 +1,56 @@ +from masonite.environment import LoadEnvironment, env +from masoniteorm.connections import ConnectionResolver + +# Loads in the environment variables when this page is imported. +LoadEnvironment() + +""" +The connections here don't determine the database but determine the "connection". +They can be named whatever you want. +""" +DATABASES = { + "default": env("DB_CONNECTION", "sqlite"), + "sqlite": { + "driver": "sqlite", + "database": env("SQLITE_DB_DATABASE", "masonite.sqlite3"), + "prefix": "", + "log_queries": env("DB_LOG"), + }, + "mysql": { + "driver": "mysql", + "host": env("DB_HOST"), + "user": env("DB_USERNAME"), + "password": env("DB_PASSWORD"), + "database": env("DB_DATABASE"), + "port": env("DB_PORT"), + "prefix": "", + "grammar": "mysql", + "options": { + "charset": "utf8mb4", + }, + "log_queries": env("DB_LOG"), + }, + "postgres": { + "driver": "postgres", + "host": env("DB_HOST"), + "user": env("DB_USERNAME"), + "password": env("DB_PASSWORD"), + "database": env("DB_DATABASE"), + "port": env("DB_PORT"), + "prefix": "", + "grammar": "postgres", + "log_queries": env("DB_LOG"), + }, + "mssql": { + "driver": "mssql", + "host": env("MSSQL_DATABASE_HOST"), + "user": env("MSSQL_DATABASE_USER"), + "password": env("MSSQL_DATABASE_PASSWORD"), + "database": env("MSSQL_DATABASE_DATABASE"), + "port": env("MSSQL_DATABASE_PORT"), + "prefix": "", + "log_queries": env("DB_LOG"), + }, +} + +DB = ConnectionResolver().set_connection_details(DATABASES) diff --git a/tests/integrations/config/exceptions.py b/tests/integrations/config/exceptions.py new file mode 100644 index 0000000..006d091 --- /dev/null +++ b/tests/integrations/config/exceptions.py @@ -0,0 +1 @@ +HANDLERS = {"stack_overflow": True, "solutions": True} diff --git a/tests/integrations/config/filesystem.py b/tests/integrations/config/filesystem.py new file mode 100644 index 0000000..8ea88b3 --- /dev/null +++ b/tests/integrations/config/filesystem.py @@ -0,0 +1,14 @@ +from masonite.environment import env +from masonite.utils.location import base_path + + +DISKS = { + "default": "local", + "local": {"driver": "file", "path": base_path("storage/framework/filesystem")}, + "s3": { + "driver": "s3", + "client": env("AWS_CLIENT"), + "secret": env("AWS_SECRET"), + "bucket": env("AWS_BUCKET"), + }, +} diff --git a/tests/integrations/config/mail.py b/tests/integrations/config/mail.py new file mode 100644 index 0000000..63f1867 --- /dev/null +++ b/tests/integrations/config/mail.py @@ -0,0 +1,16 @@ +from masonite.environment import env + + +DRIVERS = { + "default": env("MAIL_DRIVER", "terminal"), + "smtp": { + "host": env("MAIL_HOST"), + "port": env("MAIL_PORT"), + "username": env("MAIL_USERNAME"), + "password": env("MAIL_PASSWORD"), + }, + "mailgun": { + "domain": env("MAILGUN_DOMAIN"), + "secret": env("MAILGUN_SECRET"), + }, +} diff --git a/tests/integrations/config/notification.py b/tests/integrations/config/notification.py new file mode 100644 index 0000000..e12dc32 --- /dev/null +++ b/tests/integrations/config/notification.py @@ -0,0 +1,20 @@ +from masonite.environment import env + + +DRIVERS = { + "slack": { + "token": env("SLACK_TOKEN", ""), # used for API mode + "webhook": env("SLACK_WEBHOOK", ""), # used for webhook mode + }, + "vonage": { + "key": env("VONAGE_KEY", ""), + "secret": env("VONAGE_SECRET", ""), + "sms_from": env("VONAGE_SMS_FROM", "+33000000000"), + }, + "database": { + "connection": "sqlite", + "table": "notifications", + }, +} + +DRY = False diff --git a/tests/integrations/config/providers.py b/tests/integrations/config/providers.py new file mode 100644 index 0000000..f73710e --- /dev/null +++ b/tests/integrations/config/providers.py @@ -0,0 +1,49 @@ +from masonite.providers import ( + RouteProvider, + FrameworkProvider, + ViewProvider, + WhitenoiseProvider, + ExceptionProvider, + MailProvider, + SessionProvider, + QueueProvider, + CacheProvider, + EventProvider, + StorageProvider, + HelpersProvider, + BroadcastProvider, + AuthenticationProvider, + AuthorizationProvider, + HashServiceProvider, +) + + +from masonite.scheduling.providers import ScheduleProvider +from masonite.notification.providers import NotificationProvider +from masonite.validation.providers import ValidationProvider +from masoniteorm.providers import ORMProvider +from src.masonite.billing import BillingProvider + +PROVIDERS = [ + FrameworkProvider, + HelpersProvider, + RouteProvider, + ViewProvider, + WhitenoiseProvider, + ExceptionProvider, + MailProvider, + NotificationProvider, + SessionProvider, + CacheProvider, + QueueProvider, + ScheduleProvider, + EventProvider, + StorageProvider, + BroadcastProvider, + HashServiceProvider, + AuthenticationProvider, + AuthorizationProvider, + ValidationProvider, + ORMProvider, + BillingProvider, +] diff --git a/tests/integrations/config/queue.py b/tests/integrations/config/queue.py new file mode 100644 index 0000000..733f982 --- /dev/null +++ b/tests/integrations/config/queue.py @@ -0,0 +1,28 @@ +DRIVERS = { + "default": "async", + "database": { + "connection": "sqlite", + "table": "jobs", + "failed_table": "failed_jobs", + "attempts": 3, + "poll": 5, + }, + "redis": { + # + }, + "amqp": { + "username": "guest", + "password": "guest", + "port": "5672", + "vhost": "", + "host": "localhost", + "channel": "default", + "queue": "masonite4", + }, + "async": { + "blocking": True, + "callback": "handle", + "mode": "threading", + "workers": 1, + }, +} diff --git a/tests/integrations/config/session.py b/tests/integrations/config/session.py new file mode 100644 index 0000000..9edf079 --- /dev/null +++ b/tests/integrations/config/session.py @@ -0,0 +1,4 @@ +DRIVERS = { + "default": "cookie", + "cookie": {}, +} diff --git a/tests/integrations/databases/migrations/2021_01_09_033202_create_password_reset_table.py b/tests/integrations/databases/migrations/2021_01_09_033202_create_password_reset_table.py new file mode 100644 index 0000000..4fa611f --- /dev/null +++ b/tests/integrations/databases/migrations/2021_01_09_033202_create_password_reset_table.py @@ -0,0 +1,15 @@ +from masoniteorm.migrations import Migration + + +class CreatePasswordResetTable(Migration): + def up(self): + """Run the migrations.""" + with self.schema.create("password_resets") as table: + table.string("email").unique() + table.string("token") + table.datetime("expires_at").nullable() + table.datetime("created_at") + + def down(self): + """Revert the migrations.""" + self.schema.drop("password_resets") diff --git a/tests/integrations/databases/migrations/2021_01_09_043202_create_users_table.py b/tests/integrations/databases/migrations/2021_01_09_043202_create_users_table.py new file mode 100644 index 0000000..a8da836 --- /dev/null +++ b/tests/integrations/databases/migrations/2021_01_09_043202_create_users_table.py @@ -0,0 +1,20 @@ +from masoniteorm.migrations import Migration + + +class CreateUsersTable(Migration): + def up(self): + """Run the migrations.""" + with self.schema.create("users") as table: + table.increments("id") + table.string("name") + table.string("email").unique() + table.string("password") + table.string("second_password").nullable() + table.string("remember_token").nullable() + table.string("phone").nullable() + table.timestamp("verified_at").nullable() + table.timestamps() + + def down(self): + """Revert the migrations.""" + self.schema.drop("users") diff --git a/tests/integrations/databases/migrations/2021_03_18_190410_create_notifications_table.py b/tests/integrations/databases/migrations/2021_03_18_190410_create_notifications_table.py new file mode 100644 index 0000000..af83657 --- /dev/null +++ b/tests/integrations/databases/migrations/2021_03_18_190410_create_notifications_table.py @@ -0,0 +1,17 @@ +from masoniteorm.migrations import Migration + + +class CreateNotificationsTable(Migration): + def up(self): + """Run the migrations.""" + with self.schema.create("notifications") as table: + table.big_increments("id").primary() + table.string("type") + table.text("data") + table.morphs("notifiable") + table.datetime("read_at").nullable() + table.timestamps() + + def down(self): + """Revert the migrations.""" + self.schema.drop("notifications") diff --git a/tests/integrations/databases/migrations/2021_10_30_023016_create_subscriptions_table.py b/tests/integrations/databases/migrations/2021_10_30_023016_create_subscriptions_table.py new file mode 100644 index 0000000..3cea95b --- /dev/null +++ b/tests/integrations/databases/migrations/2021_10_30_023016_create_subscriptions_table.py @@ -0,0 +1,23 @@ +from masoniteorm.migrations import Migration + + +class CreateSubscriptionsTable(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/integrations/databases/seeds/__init__.py b/tests/integrations/databases/seeds/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integrations/databases/seeds/database_seeder.py b/tests/integrations/databases/seeds/database_seeder.py new file mode 100644 index 0000000..4baa61b --- /dev/null +++ b/tests/integrations/databases/seeds/database_seeder.py @@ -0,0 +1,10 @@ +"""Base Database Seeder Module.""" +from masoniteorm.seeds import Seeder + +from .user_table_seeder import UserTableSeeder + + +class DatabaseSeeder(Seeder): + def run(self): + """Run the database seeds.""" + self.call(UserTableSeeder) diff --git a/tests/integrations/databases/seeds/user_table_seeder.py b/tests/integrations/databases/seeds/user_table_seeder.py new file mode 100644 index 0000000..4ef22e7 --- /dev/null +++ b/tests/integrations/databases/seeds/user_table_seeder.py @@ -0,0 +1,17 @@ +"""UserTableSeeder Seeder.""" +from masoniteorm.seeds import Seeder + +from app.models.User import User + + +class UserTableSeeder(Seeder): + def run(self): + """Run the database seeds.""" + User.create( + { + "name": "idmann509", + "email": "idmann509@gmail.com", + "password": "secret", + "phone": "+123456789", + } + ) diff --git a/tests/integrations/resources/css/app.css b/tests/integrations/resources/css/app.css new file mode 100644 index 0000000..b824a33 --- /dev/null +++ b/tests/integrations/resources/css/app.css @@ -0,0 +1 @@ +/* Put your CSS here */ diff --git a/tests/integrations/resources/js/app.js b/tests/integrations/resources/js/app.js new file mode 100644 index 0000000..4ea88fc --- /dev/null +++ b/tests/integrations/resources/js/app.js @@ -0,0 +1,8 @@ +/** + * We'll load the axios HTTP library which allows us to easily issue requests + * to our Laravel back-end. This library automatically handles sending the + * CSRF token as a header based on the value of the "XSRF" token cookie. + */ + +window.axios = require('axios') +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' diff --git a/tests/integrations/routes/web.py b/tests/integrations/routes/web.py new file mode 100644 index 0000000..7bd1e15 --- /dev/null +++ b/tests/integrations/routes/web.py @@ -0,0 +1,3 @@ +from masonite.routes import Route + +ROUTES = [Route.get("/", "WelcomeController@show")] diff --git a/tests/integrations/storage/.gitignore b/tests/integrations/storage/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/tests/integrations/storage/public/favicon.ico b/tests/integrations/storage/public/favicon.ico new file mode 100644 index 0000000..fe97996 Binary files /dev/null and b/tests/integrations/storage/public/favicon.ico differ diff --git a/tests/integrations/storage/public/logo.png b/tests/integrations/storage/public/logo.png new file mode 100644 index 0000000..f3772ca Binary files /dev/null and b/tests/integrations/storage/public/logo.png differ diff --git a/tests/integrations/templates/__init__.py b/tests/integrations/templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integrations/templates/base.html b/tests/integrations/templates/base.html new file mode 100644 index 0000000..14f5f6c --- /dev/null +++ b/tests/integrations/templates/base.html @@ -0,0 +1,19 @@ + + + + + + + {% block title %}Masonite 4{% endblock %} + + + {% block head %}{% endblock %} + + + {% block content %}{% endblock %} + {% block js %}{% endblock %} + + diff --git a/tests/integrations/templates/welcome.html b/tests/integrations/templates/welcome.html new file mode 100644 index 0000000..e20e55e --- /dev/null +++ b/tests/integrations/templates/welcome.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% block title %}Welcome on Masonite 4{% endblock %} + +{% block content %} +
+ Masonite Logo +
+ Masonite 4 +
+ + + +
+{% endblock %} diff --git a/tests/migrations/2018_11_24_023016_make_subscriptions_table.py b/tests/migrations/2018_11_24_023016_make_subscriptions_table.py deleted file mode 100644 index b91d1d4..0000000 --- a/tests/migrations/2018_11_24_023016_make_subscriptions_table.py +++ /dev/null @@ -1,24 +0,0 @@ -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/test_stripe_billing.py b/tests/test_stripe_billing.py deleted file mode 100644 index 2a2b81a..0000000 --- a/tests/test_stripe_billing.py +++ /dev/null @@ -1,242 +0,0 @@ -import os -import pytest -import time - -from dotenv import find_dotenv, load_dotenv -from billing.models import Billable, Subscription -from billing.exceptions import PlanNotFound -from masonite.app import App -import pendulum - -load_dotenv(find_dotenv()) - -class User(Billable): - plan_id = None - id = 1 - def save(self): - pass - -user = User() -user.email = "test@email.com" - -if os.environ.get('STRIPE_CUSTOMER'): - user.customer_id = os.getenv('STRIPE_CUSTOMER') -else: - user.customer_id = user.create_customer('test-customer', 'tok_amex') - -# ensure there is a card on the account -user.card('tok_amex') - -def test_subscription_raises_exeption(): - with pytest.raises(PlanNotFound): - user.subscribe('no-plan', 'tok_amex') - -def test_subscription_subscribes_user(): - user.subscribe('masonite-test', 'tok_amex') - assert user.plan_id.startswith('sub') - user.cancel(now=True) - if os.environ.get('TEST_ENVIRONMENT') == 'travis': - time.sleep(2) - - -def test_is_subscribed(): - user.subscribe('masonite-test', 'tok_amex') - - assert user.is_subscribed() is True - assert user.is_subscribed('masonite-test') is True - assert user.is_canceled() is False - assert user.cancel(now=True) - if os.environ.get('TEST_ENVIRONMENT') == 'travis': - time.sleep(2) - - assert user.is_subscribed('masonite-test') is False - assert user.is_subscribed() is False - - wrong_token_user = User() - wrong_token_user.plan_id = 'incorrect_token' - assert wrong_token_user.is_subscribed() is False - - -def test_cancel_billing(): - user.subscribe('masonite-flash', 'tok_amex') - - assert user.is_subscribed('masonite-flash') is True - assert user.cancel(now=True) is True - if os.environ.get('TEST_ENVIRONMENT') == 'travis': - time.sleep(2) - - assert user.is_subscribed('masonite-flash') is False - assert user.is_subscribed() is False - - -def test_on_trial(): - user.subscribe('masonite-flash', 'tok_amex') - assert user.on_trial() is False - assert user.cancel(now=True) is True - if os.environ.get('TEST_ENVIRONMENT') == 'travis': - time.sleep(2) - - - user.trial(days=7).subscribe('masonite-flash', 'tok_amex') - assert user.on_trial() is True - assert user.cancel(now=False) is True - assert user.on_trial() is True - assert user.cancel(now=True) is True - if os.environ.get('TEST_ENVIRONMENT') == 'travis': - time.sleep(2) - - assert user.on_trial() is False - - -def test_subscribe_cancel_subscription_at_end_of_period(): - user.subscribe('masonite-flash', 'tok_amex') - - assert user.is_subscribed() is True - user.cancel(now=False) - assert user.is_subscribed() is True - user.cancel(now=True) - if os.environ.get('TEST_ENVIRONMENT') == 'travis': - time.sleep(2) - - assert user.is_subscribed() is False - - -def test_subscription_is_canceled(): - user.subscribe('masonite-flash', 'tok_amex') - - assert user.is_canceled() == False - user.cancel(now=False) - assert user.is_canceled() == True - - user.cancel(now=True) - assert user.is_canceled() == False - - -def test_skip_trial(): - user.subscribe('masonite-test', 'tok_amex') - - assert user.on_trial() is True - user.cancel(now=True) - if os.environ.get('TEST_ENVIRONMENT') == 'travis': - time.sleep(2) - - - user.skip_trial().subscribe('masonite-test', 'tok_amex') - - assert user.on_trial() is False - user.cancel(now=True) - if os.environ.get('TEST_ENVIRONMENT') == 'travis': - time.sleep(2) - - - -def test_charge_customer(): - user.email = 'test@email.com' - assert user.charge(999) is True - assert user.charge(999, token='tok_amex') - assert user.charge(299, metadata={'name': 'test'}) - assert user.charge(299, description='Charge For test@email.com') - - -def test_swap_plan(): - user.subscribe('masonite-test', 'tok_amex') - - assert user.is_subscribed('masonite-test') - assert user.swap('masonite-flash') is True - assert user.is_subscribed() is True - assert user.is_subscribed('masonite-flash') is True - assert user.is_subscribed('masonite-test') is False - - assert user.cancel(now=True) - if os.environ.get('TEST_ENVIRONMENT') == 'travis': - time.sleep(2) - - - -def test_change_card(): - assert user.card('tok_amex') is True - - -def test_cancel_and_resume_plan(): - user.skip_trial().subscribe('masonite-test', 'tok_amex') - - # assert user.is_canceled() is False - assert user.cancel() is True - assert user.is_canceled() is True - assert user.resume() - assert user.is_canceled() is False - - user.cancel(now=True) - if os.environ.get('TEST_ENVIRONMENT') == 'travis': - time.sleep(2) - - - -def test_plan_returns_plan_name(): - user.skip_trial().subscribe('masonite-test', 'tok_amex') - - assert user.plan() == 'Masonite Test' - - assert user.cancel(now=True) - if os.environ.get('TEST_ENVIRONMENT') == 'travis': - time.sleep(2) - - - -def test_is_on_trial_after_trial(): - user.subscribe('masonite-test', 'tok_amex') - assert user.on_trial() - - # set the trial to an expired time - subscription = user._get_subscription() - subscription.trial_ends_at = pendulum.now().subtract(days=1) - subscription.save() - - assert user.on_trial() is False - assert user.on_trial() is False - - user.cancel(now=True) - if os.environ.get('TEST_ENVIRONMENT') == 'travis': - time.sleep(2) - - - -def test_subscription_is_over(): - user.skip_trial().subscribe('masonite-test', 'tok_amex') - assert user.is_subscribed() is True - assert user.on_trial() is False - - # set the subscription to an expired time - subscription = user._get_subscription() - subscription.ends_at = pendulum.now().subtract(minutes=1) - subscription.save() - - assert user.on_trial() is False - - assert user.was_subscribed() is True - assert user.was_subscribed('masonite-test') is True - assert user.was_subscribed('masonite-flash') is False - - assert user.is_subscribed() is False - - 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) - diff --git a/tests/travis.sql b/tests/travis.sql deleted file mode 100644 index f044139..0000000 --- a/tests/travis.sql +++ /dev/null @@ -1,20 +0,0 @@ -# Create Testuser -CREATE USER 'dev'@'localhost' IDENTIFIED BY 'dev'; -GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,DROP ON *.* TO 'dev'@'localhost'; -# Create DB -CREATE DATABASE IF NOT EXISTS `demo` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; -USE `demo`; -# Create Table -CREATE TABLE IF NOT EXISTS `subscriptions` ( - `id` int(11) NOT NULL PRIMARY KEY, - `user_id` int(11) NOT NULL, - `plan` varchar(255) DEFAULT NULL, - `plan_id` varchar(50) DEFAULT NULL, - `plan_name` varchar(150) DEFAULT NULL, - `trial_ends_at` timestamp NULL DEFAULT NULL, - `ends_at` timestamp NULL DEFAULT NULL, - `created_at` timestamp NULL DEFAULT NULL, - `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - -# Add Data \ No newline at end of file diff --git a/tests/units/test_stripe.py b/tests/units/test_stripe.py new file mode 100644 index 0000000..e7d8628 --- /dev/null +++ b/tests/units/test_stripe.py @@ -0,0 +1,230 @@ +import os +import time +import pendulum + +from masonite.tests import TestCase +from masonite.environment import env + +from src.masonite.billing import Billable +from src.masonite.billing.exceptions import PlanNotFound + + +class User(Billable): + plan_id = None + id = 1 + + def save(self): + pass + + +class TestStripe(TestCase): + @classmethod + def setUpClass(cls): + "Hook method for setting up class fixture before running tests in the class." + cls.user = User() + cls.user.email = "test@email.com" + if env("STRIPE_CUSTOMER"): + cls.user.customer_id = env("STRIPE_CUSTOMER") + else: + cls.user.customer_id = cls.user.create_customer("test-customer", "tok_amex") + + # ensure there is a card on the account + cls.user.card("tok_amex") + + @classmethod + def tearDownClass(cls): + "Hook method for deconstructing the class fixture after running all tests in the class." + pass + + def test_subscription_raises_exeption(self): + with self.assertRaises(PlanNotFound): + self.user.subscribe("no-plan", "tok_amex") + + def test_subscription_subscribes_user(self): + self.user.subscribe("masonite-test", "tok_amex") + assert self.user.plan_id.startswith("sub") + self.user.cancel(now=True) + # if os.environ.get('TEST_ENVIRONMENT') == 'travis': + # time.sleep(2) + + def test_is_subscribed(self): + self.user.subscribe("masonite-test", "tok_amex") + + assert self.user.is_subscribed() + assert self.user.is_subscribed("masonite-test") + assert not self.user.is_canceled() + assert self.user.cancel(now=True) + # if os.environ.get('TEST_ENVIRONMENT') == 'travis': + # time.sleep(2) + + assert not self.user.is_subscribed("masonite-test") + assert not self.user.is_subscribed() + + wrong_token_user = User() + wrong_token_user.plan_id = "incorrect_token" + assert not wrong_token_user.is_subscribed() + + def test_cancel_billing(self): + self.user.subscribe("masonite-flash", "tok_amex") + + assert self.user.is_subscribed("masonite-flash") + assert self.user.cancel(now=True) + # if os.environ.get('TEST_ENVIRONMENT') == 'travis': + # time.sleep(2) + + assert not self.user.is_subscribed("masonite-flash") + assert not self.user.is_subscribed() + + def test_on_trial(self): + self.user.subscribe("masonite-flash", "tok_amex") + assert not self.user.on_trial() + assert self.user.cancel(now=True) + # if os.environ.get('TEST_ENVIRONMENT') == 'travis': + # time.sleep(2) + + self.user.trial(days=7).subscribe("masonite-flash", "tok_amex") + assert self.user.on_trial() + assert self.user.cancel(now=False) + assert self.user.on_trial() + assert self.user.cancel(now=True) + # if os.environ.get('TEST_ENVIRONMENT') == 'travis': + # time.sleep(2) + + assert not self.user.on_trial() + + def test_subscribe_cancel_subscription_at_end_of_period(self): + self.user.subscribe("masonite-flash", "tok_amex") + + assert self.user.is_subscribed() + self.user.cancel(now=False) + assert self.user.is_subscribed() + self.user.cancel(now=True) + # if os.environ.get('TEST_ENVIRONMENT') == 'travis': + # time.sleep(2) + + assert not self.user.is_subscribed() + + def test_subscription_is_canceled(self): + self.user.subscribe("masonite-flash", "tok_amex") + + assert not self.user.is_canceled() + self.user.cancel(now=False) + assert self.user.is_canceled() + + self.user.cancel(now=True) + assert not self.user.is_canceled() + + def test_skip_trial(self): + self.user.subscribe("masonite-test", "tok_amex") + + assert self.user.on_trial() + self.user.cancel(now=True) + # if os.environ.get('TEST_ENVIRONMENT') == 'travis': + # time.sleep(2) + + self.user.skip_trial().subscribe("masonite-test", "tok_amex") + + assert not self.user.on_trial() + self.user.cancel(now=True) + # if os.environ.get('TEST_ENVIRONMENT') == 'travis': + # time.sleep(2) + + def test_charge_customer(self): + self.user.email = "test@email.com" + assert self.user.charge(999) + assert self.user.charge(999, token="tok_amex") + assert self.user.charge(299, metadata={"name": "test"}) + assert self.user.charge(299, description="Charge For test@email.com") + + def test_swap_plan(self): + self.user.subscribe("masonite-test", "tok_amex") + + assert self.user.is_subscribed("masonite-test") + assert self.user.swap("masonite-flash") + assert self.user.is_subscribed() + assert self.user.is_subscribed("masonite-flash") + assert not self.user.is_subscribed("masonite-test") + + assert self.user.cancel(now=True) + # if os.environ.get('TEST_ENVIRONMENT') == 'travis': + # time.sleep(2) + + def test_change_card(self): + assert self.user.card("tok_amex") + + def test_cancel_and_resume_plan(self): + self.user.skip_trial().subscribe("masonite-test", "tok_amex") + + # assert user.is_canceled() is False + assert self.user.cancel() + assert self.user.is_canceled() + assert self.user.resume() + assert not self.user.is_canceled() + + self.user.cancel(now=True) + if os.environ.get("TEST_ENVIRONMENT") == "travis": + time.sleep(2) + + def test_plan_returns_plan_name(self): + self.user.skip_trial().subscribe("masonite-test", "tok_amex") + + assert self.user.plan() == "Masonite Test" + + assert self.user.cancel(now=True) + # if os.environ.get('TEST_ENVIRONMENT') == 'travis': + # time.sleep(2) + + def test_is_on_trial_after_trial(self): + self.user.subscribe("masonite-test", "tok_amex") + assert self.user.on_trial() + + # set the trial to an expired time + subscription = self.user._get_subscription() + # TODO: when ORM issue is fixed, remove to_datetime_string() + subscription.trial_ends_at = pendulum.now().subtract(days=1).to_datetime_string() + subscription.save() + + assert not self.user.on_trial() + assert not self.user.on_trial() + + self.user.cancel(now=True) + # if os.environ.get('TEST_ENVIRONMENT') == 'travis': + # time.sleep(2) + + def test_subscription_is_over(self): + self.user.skip_trial().subscribe("masonite-test", "tok_amex") + assert self.user.is_subscribed() + assert not self.user.on_trial() + + # set the subscription to an expired time + subscription = self.user._get_subscription() + subscription.ends_at = pendulum.now().subtract(minutes=1) + subscription.save() + + assert not self.user.on_trial() + + assert self.user.was_subscribed() + assert self.user.was_subscribed("masonite-test") + assert not self.user.was_subscribed("masonite-flash") + + assert not self.user.is_subscribed() + + self.user.cancel(now=True) + # if os.environ.get('TEST_ENVIRONMENT') == 'travis': + # time.sleep(2) + + def test_can_use_coupon_on_charge(self): + assert self.user.get_driver()._apply_coupon(1000) == 1000 + assert self.user.coupon("5-off").get_driver()._apply_coupon(500) == 400 + assert self.user.coupon("10-percent-off").get_driver()._apply_coupon(1000) == 900 + assert self.user.coupon("10-percent-off").get_driver()._apply_coupon(1499) == 1349.1 + assert self.user.coupon(0.10).get_driver()._apply_coupon(1499) == 1349.1 + assert self.user.coupon(100).get_driver()._apply_coupon(1000) == 900 + + def test_can_use_coupon_on_subscription(self): + self.user.skip_trial().coupon("5-off").subscribe("masonite-test", "tok_amex") + assert self.user.is_subscribed() + assert not self.user.on_trial() + self.user._get_subscription() + + self.user.cancel(now=True) diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..82de13c --- /dev/null +++ b/wsgi.py @@ -0,0 +1,12 @@ +from masonite.foundation import Application, Kernel +from tests.integrations.config.providers import PROVIDERS +from tests.integrations.Kernel import Kernel as AppKernel + +application = Application("tests/integrations/") + +"""First Bind important providers needed to start the server +""" + +application.register_providers(Kernel, AppKernel) + +application.add_providers(*PROVIDERS)