├── tests ├── __init__.py ├── test_version.py ├── test_health.py └── test_payment.py ├── app ├── router │ ├── __init__.py │ ├── v1 │ │ ├── __init__.py │ │ ├── health.py │ │ └── payment.py │ └── includes.py ├── settings │ ├── __init__.py │ └── config.py ├── __init__.py └── main.py ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── lint.yaml │ └── test.yaml ├── scripts ├── format.sh ├── test.sh └── clean.sh ├── codecov.yml ├── pytest.ini ├── mypy.ini ├── README.md ├── .pre-commit-config.yaml ├── LICENSE ├── pyproject.toml └── .gitignore /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/router/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/router/v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Yezz123 2 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.1" 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: [yezz123] 3 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | import app 2 | 3 | 4 | def test_version() -> None: 5 | assert app.__version__ == "0.0.1" 6 | -------------------------------------------------------------------------------- /scripts/format.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | pre-commit run --all-files --verbose --show-diff-on-failure 7 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | # basic 6 | target: auto 7 | threshold: 100% 8 | -------------------------------------------------------------------------------- /app/router/v1/health.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | router = APIRouter() 4 | 5 | 6 | @router.get("/health") 7 | async def health_check() -> str: 8 | return "Pong!" 9 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | echo "ENV=${ENV}" 7 | 8 | export PYTHONPATH=. 9 | pytest --cov=app --cov=tests --cov-report=term-missing --cov-fail-under=80 10 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = 3 | tests/ 4 | log_cli = 1 5 | log_cli_level = INFO 6 | log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) 7 | log_cli_date_format=%Y-%m-%d %H:%M:%S 8 | -------------------------------------------------------------------------------- /app/router/includes.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from app.router.v1 import health, payment 4 | 5 | app = APIRouter() 6 | 7 | 8 | app.include_router(payment.app, tags=["Payment"]) 9 | app.include_router(health.router, tags=["Health"]) 10 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | plugins=pydantic.mypy 3 | 4 | follow_imports = silent 5 | strict_optional = True 6 | warn_redundant_casts = True 7 | warn_unused_ignores = True 8 | disallow_any_generics = True 9 | check_untyped_defs = True 10 | ignore_missing_imports = True 11 | disallow_untyped_defs = True 12 | -------------------------------------------------------------------------------- /tests/test_health.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | 3 | from app.main import app 4 | 5 | client = TestClient(app) 6 | 7 | 8 | def test_health() -> None: 9 | response = client.get("/health") 10 | assert response.status_code == 200 11 | assert response.json() == "Pong!" 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | # GitHub Actions 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | reviewers: 10 | - yezz123 11 | commit-message: 12 | prefix: ⬆ 13 | # Python 14 | - package-ecosystem: "pip" 15 | directory: "/" 16 | schedule: 17 | interval: "daily" 18 | reviewers: 19 | - yezz123 20 | commit-message: 21 | prefix: ⬆ 22 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint and Format 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize] 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ["3.10"] 16 | fail-fast: false 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Python 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install Dependencies 25 | run: pip install -e .[lint] 26 | - name: Lint 27 | run: bash scripts/format.sh 28 | -------------------------------------------------------------------------------- /app/settings/config.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic import BaseSettings, HttpUrl 4 | 5 | 6 | class Settings(BaseSettings): 7 | HOST: HttpUrl = "http://127.0.0.1:8000" 8 | PAYMENT_METHOD_TYPES: List[str] = ["sepa_debit", "card"] 9 | API_DOC_URL: str = "/docs" 10 | API_OPENAPI_URL: str = "/openapi.json" 11 | API_REDOC_URL: str = "/redoc" 12 | API_TITLE: str = "Stripe Template" 13 | API_VERSION: str = "0.0.1" 14 | API_DESCRIPTION: str = ( 15 | "Template for integrating stripe into your FastAPI application" 16 | ) 17 | DEBUG: bool = False 18 | 19 | class Config: 20 | env_file = ".env" 21 | env_file_encoding = "utf-8" 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stripe-Template 2 | 3 | Template for integrating stripe into your FastAPI application 💸, Useful to generate a checkout session needed to save customers payment methods for recurring payments (in particular SEPA debits). 4 | 5 | ## Usage 6 | 7 | ### Install dependencies 8 | 9 | ```bash 10 | pip install -e . 11 | ``` 12 | 13 | ### Run the application 14 | 15 | ```bash 16 | uvicorn app.main:app --reload --workers 1 --host 0.0.0.0 --port 8000 17 | ``` 18 | 19 | ### Run the tests 20 | 21 | ```bash 22 | bash scripts/test.sh 23 | ``` 24 | 25 | __NOTES__: You need to have a `.env` file in the root of the project with the following variables: 26 | 27 | ```bash 28 | STRIPE_API_KEY= sk_test_* # Your stripe API key 29 | ``` 30 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | 4 | from app.router.includes import app as router 5 | from app.settings.config import Settings 6 | 7 | config = Settings() 8 | 9 | app = FastAPI( 10 | description=config.API_DESCRIPTION, 11 | title=config.API_TITLE, 12 | version=config.API_VERSION, 13 | docs_url=config.API_DOC_URL, 14 | openapi_url=config.API_OPENAPI_URL, 15 | redoc_url=config.API_REDOC_URL, 16 | debug=config.DEBUG, 17 | ) 18 | 19 | app.include_router(router) 20 | app.add_middleware( 21 | CORSMiddleware, 22 | allow_origins=["*"], 23 | allow_credentials=True, 24 | allow_methods=["*"], 25 | allow_headers=["*"], 26 | ) 27 | -------------------------------------------------------------------------------- /scripts/clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | rm -f `find . -type f -name '*.py[co]' ` 4 | rm -f `find . -type f -name '*~' ` 5 | rm -f `find . -type f -name '.*~' ` 6 | rm -f `find . -type f -name .coverage` 7 | rm -f `find . -type f -name ".coverage.*"` 8 | rm -rf `find . -name __pycache__` 9 | rm -rf `find . -type d -name '*.egg-info' ` 10 | rm -rf `find . -type d -name 'pip-wheel-metadata' ` 11 | rm -rf `find . -type d -name .pytest_cache` 12 | rm -rf `find . -type d -name .ruff_cache` 13 | rm -rf `find . -type d -name .cache` 14 | rm -rf `find . -type d -name .mypy_cache` 15 | rm -rf `find . -type d -name htmlcov` 16 | rm -rf `find . -type d -name "*.egg-info"` 17 | rm -rf `find . -type d -name build` 18 | rm -rf `find . -type d -name dist` 19 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.3.0 6 | hooks: 7 | - id: check-added-large-files 8 | - id: check-toml 9 | - id: check-yaml 10 | args: 11 | - --unsafe 12 | - id: end-of-file-fixer 13 | - id: trailing-whitespace 14 | - repo: https://github.com/asottile/pyupgrade 15 | rev: v3.2.2 16 | hooks: 17 | - id: pyupgrade 18 | args: 19 | - --py3-plus 20 | - --keep-runtime-typing 21 | - repo: https://github.com/charliermarsh/ruff-pre-commit 22 | rev: v0.0.138 23 | hooks: 24 | - id: ruff 25 | args: 26 | - --fix 27 | - repo: https://github.com/pycqa/isort 28 | rev: 5.10.1 29 | hooks: 30 | - id: isort 31 | name: isort (python) 32 | - repo: https://github.com/psf/black 33 | rev: 22.10.0 34 | hooks: 35 | - id: black 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Yasser Tahiri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test Suite 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize] 9 | 10 | jobs: 11 | tests: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 30 14 | strategy: 15 | matrix: 16 | python-version: ["3.8","3.9","3.10", "3.11"] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Python 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - uses: actions/cache@v3 25 | id: cache 26 | with: 27 | path: ${{ env.pythonLocation }} 28 | key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test-v02 29 | - name: Upgrade pip 30 | run: | 31 | python -m pip install --upgrade pip 32 | - name: Install Dependencies 33 | if: steps.cache.outputs.cache-hit != 'true' 34 | run: pip install -e .[test] 35 | - name: Test with pytest 36 | run: bash scripts/test.sh 37 | env: 38 | STRIPE_API_KEY: ${{ secrets.STRIPE_API_KEY }} 39 | - name: Upload coverage to Codecov 40 | uses: codecov/codecov-action@v3 41 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "stripe-template" 7 | description = 'Template for integrating stripe into your FastAPI application' 8 | readme = "README.md" 9 | requires-python = ">=3.8" 10 | license = "MIT" 11 | keywords = [ 12 | "stripe", 13 | "fastapi", 14 | "template", 15 | ] 16 | authors = [ 17 | { name = "Yasser Tahiri", email = "hello@yezz.me" }, 18 | ] 19 | classifiers = [ 20 | "Programming Language :: Python", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | ] 26 | dependencies = [ 27 | "fastapi", 28 | "stripe", 29 | "pydantic[email]", 30 | "python-dotenv", 31 | "python-decouple", 32 | "uvicorn", 33 | ] 34 | dynamic = ["version"] 35 | 36 | [project.urls] 37 | Documentation = "https://github.com/yezz123/stripe-template#readme" 38 | Issues = "https://github.com/yezz123/stripe-template/issues" 39 | Source = "https://github.com/yezz123/stripe-template" 40 | 41 | [project.optional-dependencies] 42 | lint = [ 43 | "pre-commit==2.21.0", 44 | ] 45 | test = [ 46 | "pytest==7.2.1", 47 | "pytest-asyncio == 0.20.3", 48 | "pytest-mock", 49 | "requests==2.28.2", 50 | "uvicorn==0.20.0", 51 | "asynctest==0.13.0", 52 | "codecov==2.1.12", 53 | "pytest-cov==4.0.0", 54 | "websockets==10.4", 55 | "uvloop==0.17.0", 56 | "httpx" 57 | ] 58 | 59 | [tool.hatch.version] 60 | path = "app/__init__.py" 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /app/router/v1/payment.py: -------------------------------------------------------------------------------- 1 | import stripe 2 | from decouple import config 3 | from fastapi import APIRouter, HTTPException, Request 4 | from fastapi.responses import RedirectResponse 5 | from pydantic import EmailStr, constr 6 | from stripe import Customer, checkout, error 7 | 8 | from app.settings.config import Settings 9 | 10 | setting = Settings() 11 | 12 | 13 | app = APIRouter() 14 | 15 | stripe.api_key = constr(regex=r"sk_.*")(config("STRIPE_API_KEY")) 16 | 17 | 18 | @app.get("/success") 19 | def success() -> str: 20 | """Redirect page on success""" 21 | return "Payment method registered with success ✅" 22 | 23 | 24 | @app.get("/cancel") 25 | def cancel() -> str: 26 | """Redirect page on cancel""" 27 | return "Operation canceled ❌" 28 | 29 | 30 | def session_url(customer_id: str, request: dict) -> str: 31 | """ 32 | Create a new checkout session for the customer to setup a new payment method 33 | More details on https://stripe.com/docs/api/checkout/sessions/create 34 | and on: 35 | https://stripe.com/docs/payments/sepa-debit/set-up-payment?platform=checkout 36 | """ 37 | success_url = f"{request['url']['scheme']}://{request['url']['netloc']}/success" 38 | cancel_url = f"{request['url']['scheme']}://{request['url']['netloc']}/cancel" 39 | 40 | checkout_session = checkout.Session.create( 41 | payment_method_types=setting.PAYMENT_METHOD_TYPES, 42 | mode="setup", 43 | customer=customer_id, 44 | success_url=success_url, 45 | cancel_url=cancel_url, 46 | ) 47 | return checkout_session.id 48 | 49 | 50 | @app.get("/email/{email}", summary="Setup a new payment method by email") 51 | def setup_new_method_by_email(email: EmailStr, request: Request): 52 | """ 53 | Retrieve a customer by email and redirect to the checkout session 54 | More details on https://stripe.com/docs/api/customers/list 55 | """ 56 | customer = Customer.list(email=email) 57 | 58 | if not customer: 59 | raise HTTPException( 60 | status_code=404, detail=f"No customer with this email: {email}" 61 | ) 62 | 63 | if len(customer.data) > 1: 64 | raise HTTPException( 65 | status_code=404, 66 | detail="More than one customer with this email, use the id instead", 67 | ) 68 | 69 | return RedirectResponse(session_url(customer.data[0].id, request), status_code=303) 70 | 71 | 72 | # TODO: Bypassing Ruff for now 73 | customer = constr(regex=r"cus_.*") 74 | 75 | 76 | @app.get("/id/{customer_id}", summary="Setup a new payment method by user id") 77 | def setup_new_method_by_id(customer_id: customer, request: Request): 78 | try: 79 | customer = Customer.retrieve(customer_id) 80 | except error.InvalidRequestError as exc: 81 | raise HTTPException(status_code=404, detail=exc.error.message) from exc 82 | 83 | return RedirectResponse(session_url(customer.id, request), status_code=303) 84 | -------------------------------------------------------------------------------- /tests/test_payment.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch 2 | 3 | import pytest 4 | import stripe 5 | from fastapi.testclient import TestClient 6 | 7 | from app.main import app 8 | from app.router.v1.payment import session_url 9 | from app.settings.config import Settings 10 | 11 | config = Settings() 12 | client = TestClient(app) 13 | 14 | 15 | # Mock the stripe checkout session 16 | @pytest.fixture 17 | def mock_checkout_session(mocker): 18 | session = mocker.Mock() 19 | session.url = "https://checkout.stripe.com/session" 20 | session_create = mocker.Mock(return_value=session) 21 | mocker.patch.object(stripe.checkout.Session, "create", session_create) 22 | 23 | 24 | @pytest.fixture 25 | def mock_session_url(mocker): 26 | mocker.patch("app.router.v1.payment.session_url") 27 | 28 | 29 | # Test for the success page 30 | def test_success_page() -> None: 31 | response = client.get("/success") 32 | assert response.status_code == 200 33 | assert response.text == '"Payment method registered with success ✅"' 34 | 35 | 36 | # Test for the cancel page 37 | def test_cancel_page() -> None: 38 | response = client.get("/cancel") 39 | assert response.status_code == 200 40 | assert response.text == '"Operation canceled ❌"' 41 | 42 | 43 | # Test the session_url function 44 | def test_session_url(mock_checkout_session): 45 | customer_id = "cus_123456" 46 | request = {"url": {"scheme": "http", "netloc": "localhost:8000"}} 47 | url = session_url(customer_id, request) # noqa: F841 48 | stripe.checkout.Session.create.assert_called_with( 49 | payment_method_types=config.PAYMENT_METHOD_TYPES, 50 | mode="setup", 51 | customer=customer_id, 52 | success_url="http://localhost:8000/success", 53 | cancel_url="http://localhost:8000/cancel", 54 | ) 55 | 56 | 57 | # Test the setup_new_method_by_email function 58 | @patch("app.router.v1.payment.setup_new_method_by_email") 59 | def test_setup_new_method_by_email(mock_setup_new_method_by_email: Mock) -> None: 60 | email = "test@example.com" 61 | mock_response = Mock() 62 | mock_response.status_code = 302 63 | mock_response.email = email 64 | mock_setup_new_method_by_email.return_value = mock_response 65 | response = client.get(f"/email/{email}") 66 | # TODO: I notice that the status code is 404 67 | # if you didn't create already a customer with the email 68 | assert response.status_code == 404 69 | 70 | 71 | # Test the setup_new_method_by_id function 72 | @patch("app.router.v1.payment.setup_new_method_by_id") 73 | def test_setup_new_method_by_id(mock_setup_new_method_by_id: Mock) -> None: 74 | customer_id = "cus_123456" 75 | mock_response = Mock() 76 | mock_response.status_code = 302 77 | mock_response.customer_id = customer_id 78 | mock_setup_new_method_by_id.return_value = mock_response 79 | response = client.get(f"/id/{customer_id}") 80 | # TODO: I notice that the status code is 404 81 | # if you didn't create already a customer with the id 82 | assert response.status_code == 404 83 | --------------------------------------------------------------------------------