Repository: pydanny/dj-stripe Branch: master Commit: b17d637fede5 Files: 290 Total size: 2.0 MB Directory structure: gitextract_rkwri8yg/ ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── api-change-notice.md │ │ ├── bug_report.md │ │ ├── documentation-issue-request.md │ │ ├── feature-or-enhancement-proposal.md │ │ ├── general-bug.md │ │ ├── how-to-usage-question.md │ │ ├── issue-with-webhooks-or-sync.md │ │ ├── migration-upgrade-issue.md │ │ └── other-issue.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ ├── install_poetry_action/ │ │ └── action.yaml │ └── workflows/ │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── docs.yml │ └── linting.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── LICENSE ├── djstripe/ │ ├── __init__.py │ ├── admin/ │ │ ├── __init__.py │ │ ├── actions.py │ │ ├── admin.py │ │ ├── admin_inline.py │ │ ├── filters.py │ │ ├── forms.py │ │ ├── utils.py │ │ └── views.py │ ├── apps.py │ ├── checks.py │ ├── enums.py │ ├── event_handlers.py │ ├── exceptions.py │ ├── fields.py │ ├── locale/ │ │ ├── fr/ │ │ │ └── LC_MESSAGES/ │ │ │ └── django.po │ │ └── ru/ │ │ └── LC_MESSAGES/ │ │ └── django.po │ ├── management/ │ │ ├── __init__.py │ │ └── commands/ │ │ ├── __init__.py │ │ ├── djstripe_clear_expired_idempotency_keys.py │ │ ├── djstripe_init_customers.py │ │ ├── djstripe_process_events.py │ │ ├── djstripe_sync_customers.py │ │ ├── djstripe_sync_models.py │ │ └── djstripe_update_invoiceitem_ids.py │ ├── managers.py │ ├── migrations/ │ │ ├── 0001_initial.py │ │ ├── 0008_2_5.py │ │ ├── 0009_2_6.py │ │ ├── 0010_alter_customer_balance.py │ │ ├── 0011_2_7.py │ │ ├── 0012_auto_20221217_0559.py │ │ ├── 0013_product_default_price.py │ │ ├── 0014_lineitem.py │ │ ├── 0015_alter_payout_destination.py │ │ ├── 0016_alter_payout_destination.py │ │ ├── 0017_invoiceorlineitem.py │ │ ├── 0018_discount.py │ │ ├── 0019_add_customer_discount.py │ │ ├── __init__.py │ │ └── sql/ │ │ ├── migrate_mysql_backward.sql │ │ ├── migrate_mysql_forward.sql │ │ ├── migrate_postgresql_backward.sql │ │ ├── migrate_postgresql_forward.sql │ │ ├── migrate_sqlite_backward.sql │ │ └── migrate_sqlite_forward.sql │ ├── mixins.py │ ├── models/ │ │ ├── __init__.py │ │ ├── account.py │ │ ├── api.py │ │ ├── base.py │ │ ├── billing.py │ │ ├── checkout.py │ │ ├── connect.py │ │ ├── core.py │ │ ├── fraud.py │ │ ├── orders.py │ │ ├── payment_methods.py │ │ ├── sigma.py │ │ └── webhooks.py │ ├── settings.py │ ├── signals.py │ ├── sync.py │ ├── templates/ │ │ └── djstripe/ │ │ └── admin/ │ │ ├── add_form.html │ │ ├── change_form.html │ │ ├── confirm_action.html │ │ └── webhook_endpoint/ │ │ ├── add_form.html │ │ ├── change_form.html │ │ └── delete_confirmation.html │ ├── urls.py │ ├── utils.py │ ├── views.py │ └── webhooks.py ├── docs/ │ ├── CONTRIBUTING.md │ ├── README.md │ ├── __init__.py │ ├── api_keys.md │ ├── api_versions.md │ ├── history/ │ │ ├── 0_x.md │ │ ├── 1_x.md │ │ ├── 2_4_0.md │ │ ├── 2_4_x.md │ │ ├── 2_5_0.md │ │ ├── 2_5_x.md │ │ ├── 2_6_0.md │ │ ├── 2_6_x.md │ │ ├── 2_7_0.md │ │ ├── 2_7_x.md │ │ ├── 2_8_0.md │ │ └── 2_x.md │ ├── installation.md │ ├── project/ │ │ ├── authors.md │ │ ├── release_process.md │ │ ├── sponsors.md │ │ ├── support.md │ │ └── test_fixtures.md │ ├── reference/ │ │ ├── enums.md │ │ ├── managers.md │ │ ├── models.md │ │ ├── project.md │ │ ├── settings.md │ │ └── utils.md │ ├── stripe_elements_js.md │ └── usage/ │ ├── creating_individual_charges.md │ ├── creating_usage_record.md │ ├── local_webhook_testing.md │ ├── managing_subscriptions.md │ ├── manually_syncing_with_stripe.md │ ├── subscribing_customers.md │ ├── using_stripe_checkout.md │ ├── using_with_docker.md │ └── webhooks.md ├── manage.py ├── mkdocs.yml ├── pyproject.toml ├── tests/ │ ├── __init__.py │ ├── apps/ │ │ ├── __init__.py │ │ ├── example/ │ │ │ ├── __init__.py │ │ │ ├── forms.py │ │ │ ├── management/ │ │ │ │ ├── __init__.py │ │ │ │ └── commands/ │ │ │ │ ├── __init__.py │ │ │ │ └── regenerate_test_fixtures.py │ │ │ ├── templates/ │ │ │ │ ├── checkout.html │ │ │ │ ├── checkout_success.html │ │ │ │ ├── example_base.html │ │ │ │ ├── payment_intent.html │ │ │ │ ├── purchase_subscription.html │ │ │ │ └── purchase_subscription_success.html │ │ │ ├── urls.py │ │ │ └── views.py │ │ ├── testapp/ │ │ │ ├── __init__.py │ │ │ ├── models.py │ │ │ └── urls.py │ │ ├── testapp_content/ │ │ │ ├── README.md │ │ │ ├── __init__.py │ │ │ ├── models.py │ │ │ └── urls.py │ │ └── testapp_namespaced/ │ │ ├── __init__.py │ │ ├── models.py │ │ └── urls.py │ ├── conftest.py │ ├── fields/ │ │ ├── admin.py │ │ └── models.py │ ├── fixtures/ │ │ ├── account_custom_acct_1IuHosQveW0ONQsd.json │ │ ├── account_express_acct_1IuHosQveW0ONQsd.json │ │ ├── account_standard_acct_1Fg9jUA3kq9o1aTc.json │ │ ├── balance_transaction_txn_fake_ch_fakefakefakefakefake0001.json │ │ ├── bank_account_ba_fakefakefakefakefake0003.json │ │ ├── bank_account_ba_fakefakefakefakefake0004.json │ │ ├── card_card_fakefakefakefakefake0001.json │ │ ├── card_card_fakefakefakefakefake0002.json │ │ ├── card_card_fakefakefakefakefake0003.json │ │ ├── card_card_fakefakefakefakefake0004.json │ │ ├── charge_ch_fakefakefakefakefake0001.json │ │ ├── customer_cus_4QWKsZuuTHcs7X.json │ │ ├── customer_cus_4UbFSo9tl62jqj.json │ │ ├── customer_cus_6lsBvm5rJ0zyHc.json │ │ ├── customer_cus_example_with_bank_account.json │ │ ├── dispute_ch_fakefakefakefake01.json │ │ ├── dispute_dp_fakefakefakefake01.json │ │ ├── dispute_dp_fakefakefakefake02.json │ │ ├── dispute_dp_funds_reinstated_full.json │ │ ├── dispute_pi_fakefakefakefake01.json │ │ ├── dispute_pm_fakefakefakefake01.json │ │ ├── dispute_txn_fakefakefakefake01.json │ │ ├── event_account_application_authorized.json │ │ ├── event_account_application_deauthorized.json │ │ ├── event_account_updated_custom.json │ │ ├── event_account_updated_express.json │ │ ├── event_account_updated_standard.json │ │ ├── event_external_account_bank_account_created.json │ │ ├── event_external_account_bank_account_deleted.json │ │ ├── event_external_account_bank_account_updated.json │ │ ├── event_external_account_card_created.json │ │ ├── event_external_account_card_deleted.json │ │ ├── event_external_account_card_updated.json │ │ ├── invoice_in_fakefakefakefakefake0001.json │ │ ├── invoice_in_fakefakefakefakefake0004.json │ │ ├── line_item_il_invoice_item_fakefakefakefakefake0001.json │ │ ├── line_item_il_invoice_item_fakefakefakefakefake0002.json │ │ ├── order_order_fakefakefakefake0001.json │ │ ├── payment_intent_pi_destination_charge.json │ │ ├── payment_intent_pi_fakefakefakefakefake0001.json │ │ ├── payment_method_card_fakefakefakefakefake0001.json │ │ ├── payment_method_pm_fakefakefakefake0001.json │ │ ├── payout_custom_bank_account.json │ │ ├── payout_custom_card.json │ │ ├── plan_gold21323.json │ │ ├── plan_silver41294.json │ │ ├── price_gold21323.json │ │ ├── price_silver41294.json │ │ ├── product_prod_fake1.json │ │ ├── setup_intent_pi_destination_charge.json │ │ ├── shipping_rate_shr_fakefakefakefakefake0001.json │ │ ├── shipping_rate_shr_fakefakefakefakefake0002.json │ │ ├── source_src_fakefakefakefakefake0001.json │ │ ├── subscription_sub_fakefakefakefakefake0001.json │ │ ├── subscription_sub_fakefakefakefakefake0002.json │ │ ├── subscription_sub_fakefakefakefakefake0003.json │ │ ├── subscription_sub_fakefakefakefakefake0004.json │ │ ├── tax_code_txcd_fakefakefakefakefake0001.json │ │ ├── tax_id_txi_fakefakefakefakefake0001.json │ │ ├── tax_rate_txr_fakefakefakefakefake0001.json │ │ ├── tax_rate_txr_fakefakefakefakefake0002.json │ │ ├── usage_record_summary_sis_fakefakefakefakefake0001.json │ │ └── webhook_endpoint_fake0001.json │ ├── settings.py │ ├── templates/ │ │ └── base.html │ ├── test_account.py │ ├── test_admin.py │ ├── test_api_keys.py │ ├── test_apikey.py │ ├── test_balance_transaction.py │ ├── test_bank_account.py │ ├── test_card.py │ ├── test_charge.py │ ├── test_checks.py │ ├── test_coupon.py │ ├── test_customer.py │ ├── test_discount.py │ ├── test_dispute.py │ ├── test_django.py │ ├── test_enums.py │ ├── test_event.py │ ├── test_event_handlers.py │ ├── test_fields.py │ ├── test_file_link.py │ ├── test_file_upload.py │ ├── test_forms.py │ ├── test_idempotency_keys.py │ ├── test_integrations/ │ │ ├── README.md │ │ └── __init__.py │ ├── test_invoice.py │ ├── test_invoiceitem.py │ ├── test_line_item.py │ ├── test_managers.py │ ├── test_migrations.py │ ├── test_mixins.py │ ├── test_order.py │ ├── test_payment_intent.py │ ├── test_payment_method.py │ ├── test_payout.py │ ├── test_plan.py │ ├── test_price.py │ ├── test_product.py │ ├── test_refund.py │ ├── test_session.py │ ├── test_settings.py │ ├── test_setup_intent.py │ ├── test_shipping_rate.py │ ├── test_source.py │ ├── test_stripe_model.py │ ├── test_subscription.py │ ├── test_subscription_item.py │ ├── test_subscription_schedule.py │ ├── test_sync.py │ ├── test_tax_code.py │ ├── test_tax_id.py │ ├── test_tax_rates.py │ ├── test_transfer.py │ ├── test_transfer_reversal.py │ ├── test_usage_record.py │ ├── test_usage_record_summary.py │ ├── test_utils.py │ ├── test_views.py │ ├── test_webhooks.py │ └── urls.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # EditorConfig: http://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf indent_size = 4 indent_style = space quote_type = double insert_final_newline = true trim_trailing_whitespace = true max_line_length = 88 [*.json] insert_final_newline = false ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: dj-stripe open_collective: dj-stripe ================================================ FILE: .github/ISSUE_TEMPLATE/api-change-notice.md ================================================ --- name: API Change Notice about: Has something changed in the Stripe API that we haven't gotten to? Let us know title: API change for [Stripe Object Names] in version [New Stripe Version] labels: api change assignees: '' --- **Changelog Link** **Describe the Change** ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Any other type of bug or crash title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** **Software versions** - Dj-Stripe version: - Python version: - Django version: - Stripe API version: - Database type and version: ================================================ FILE: .github/ISSUE_TEMPLATE/documentation-issue-request.md ================================================ --- name: Documentation Issue/Request about: Is something missing from the docs? Is a section of the docs outdated or incorrect? Let us know. title: '' labels: documentation assignees: '' --- **What did you run into?** **Why would it be helpful to document or fix this?** **To which section of the docs does this belong?** ================================================ FILE: .github/ISSUE_TEMPLATE/feature-or-enhancement-proposal.md ================================================ --- name: Feature or Enhancement Proposal about: Suggest an idea for this project title: '' labels: discussion assignees: '' --- **Is your request related to a problem? Please describe.** **Describe the solution you'd like** **Additional context** ================================================ FILE: .github/ISSUE_TEMPLATE/general-bug.md ================================================ --- name: General Bug about: Any type of bug or crash not related to webhooks or syncing title: '' labels: bug assignees: '' --- **Describe the bug** **Software versions** - dj-stripe version: - Python version: - Django version: - Stripe API version: - Database type and version: **Steps To Reproduce** **Can you reproduce the issue with the latest version of master?** **Expected Behavior** **Actual Behavior** ================================================ FILE: .github/ISSUE_TEMPLATE/how-to-usage-question.md ================================================ --- name: How To/Usage Question about: Have a question on how to use a dj-stripe feature? title: How do I [short description]? labels: question assignees: '' --- **What are you trying to accomplish?** ================================================ FILE: .github/ISSUE_TEMPLATE/issue-with-webhooks-or-sync.md ================================================ --- name: Issue with webhooks or sync about: For issues happening specifically when data is being synced from Stripe to Dj-Stripe title: '' labels: webhook / sync issues assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: e.g.: 1. Enable stripe webhook to dj-stripe 2. In stripe dashboard create a billing product with feature X 3. See attached error on webhook **Expected behavior** A clear and concise description of what you expected to happen. If relevant it's very helpful to include webhook tracebacks and content (note that these are logged in the database and are visible in django admin - eg http://127.0.0.1:8000/admin/djstripe/webhookeventtrigger/ ) **Environment** - dj-stripe version: [e.g. master at , 2.0.0 etc] - Your Stripe account's default API version: [e.g. 2019-02-19 - shown as "default" on https://dashboard.stripe.com/developers] - Database: [e.g. MySQL 5.7.25] - Python version: [e.g. 3.8.2] - Django version: [e.g. 2.1.7] **Can you reproduce the issue with the latest version of master?** [Yes / No] ================================================ FILE: .github/ISSUE_TEMPLATE/migration-upgrade-issue.md ================================================ --- name: Migration/Upgrade Issue about: Having trouble upgrading versions of dj-stripe? Let us know title: Failure when attempting to upgrade from dj-strive [old version] to [new version] labels: migrations assignees: '' --- **Software versions** - Upgrading from dj-stripe version: - Upgrading to dj-stripe version: - Python version: - Django version: - Stripe API version: - Database type and version: - Failing migration id: **Can you reproduce the issue with the latest version of master?** **Describe the issue** ================================================ FILE: .github/ISSUE_TEMPLATE/other-issue.md ================================================ --- name: Other Issue about: For anything else title: '' labels: '' assignees: '' --- ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: # Update Github actions in workflows - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" ================================================ FILE: .github/install_poetry_action/action.yaml ================================================ name: Setup and install poetry description: Install and Setup Poetry inputs: POETRY_VERSION: required: true type: string python_version: required: true type: string runs: using: "composite" steps: - name: Set up Python ${{ inputs.python_version }} uses: actions/setup-python@v4 with: python-version: ${{ inputs.python_version }} - name: Install poetry (${{ inputs.POETRY_VERSION }}) binary on runner run: | curl -sL https://install.python-poetry.org | python - --version ${{ inputs.POETRY_VERSION }} shell: bash - name: Set up cache uses: actions/cache@v3 id: cache with: path: .venv key: venv-${{ inputs.python_version }} - name: Ensure cache is healthy if: steps.cache.outputs.cache-hit == 'true' run: timeout 10s poetry run pip --version || rm -rf .venv shell: bash ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI tests on: push: paths-ignore: - "docs/**" - "pyproject.toml" - "mkdocs.yml" - ".readthedocs.yml" - ".github/workflows/docs.yml" pull_request: # The branches below must be a subset of the branches above branches: [master] env: POETRY_VERSION: "1.2.2" POETRY_VIRTUALENVS_IN_PROJECT: "1" jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: true matrix: python-version: ["3.8", "3.9", "3.10", "3.11"] services: postgres: image: postgres:12 env: POSTGRES_PASSWORD: djstripe POSTGRES_DB: djstripe ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 mysql: image: mysql:5.7 env: MYSQL_ROOT_PASSWORD: djstripe MYSQL_DATABASE: djstripe ports: - 3306:3306 options: >- --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v3 - uses: ./.github/install_poetry_action with: POETRY_VERSION: ${{ env.POETRY_VERSION }} python_version: ${{ matrix.python-version }} - name: Install dependencies run: poetry install --with ci - name: Test with tox for ${{ matrix.python-version }} run: poetry run tox - name: Convert coverage run: poetry run coverage xml ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ name: "CodeQL" on: push: branches: [master] pull_request: # The branches below must be a subset of the branches above branches: [master] schedule: - cron: "0 11 * * 2" jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: # Override automatic language detection by changing the below list # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] language: ["python"] # Learn more... # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - name: Checkout repository uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 ================================================ FILE: .github/workflows/docs.yml ================================================ name: Build and deploy docs on: push: branches: - "master" # Push events to branches matching "stable/*" - "stable/.+" workflow_dispatch: # to trigger manually env: POETRY_VERSION: "1.2.2" POETRY_VIRTUALENVS_IN_PROJECT: "1" LATEST_STABLE_BRANCH: "stable/2.7" jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ./.github/install_poetry_action with: POETRY_VERSION: ${{ env.POETRY_VERSION }} python_version: "3.11" - name: Install dependencies run: poetry install --with docs - name: Configure git user to make commit run: | git config user.name "dj-stripe commit bot" git config user.email "admin@djstripe.dev" - name: Fetch gh-pages remote changes (if any) run: git fetch origin gh-pages --depth=1 - name: Deploy (and Update) docs for the branch, ${GITHUB_REF##*/} run: poetry run mike deploy --push --rebase "${GITHUB_REF##*/}" - name: Set default docs to ${LATEST_STABLE_BRANCH##*/} run: poetry run mike set-default --push --rebase "${LATEST_STABLE_BRANCH##*/}" ================================================ FILE: .github/workflows/linting.yml ================================================ name: Linting on: push: pull_request: # The branches below must be a subset of the branches above branches: [master] env: POETRY_VERSION: "1.2.2" POETRY_VIRTUALENVS_IN_PROJECT: "1" jobs: linting: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ./.github/install_poetry_action with: POETRY_VERSION: ${{ env.POETRY_VERSION }} python_version: "3.11" - name: Install pre-commit run: poetry install --with dev - name: Run pre-commit run: poetry run pre-commit run --all-files --show-diff-on-failure ================================================ FILE: .gitignore ================================================ *.py[cod] __pycache__ .tox dj_stripe.egg-info *.mo # Environments .venv # mkdocs site/ # SQLite *.sqlite3 # Coverage /cover .coverage # Editor files .vscode .idea # Do not commit Poetry lockfiles for libraries poetry.lock ================================================ FILE: .pre-commit-config.yaml ================================================ exclude: ".git|.tox|.pytest_cache" default_stages: [commit] fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: "v4.4.0" hooks: - id: check-builtin-literals - id: check-case-conflict - id: check-toml - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black rev: "23.1.0" hooks: - id: black - repo: https://github.com/timothycrosley/isort rev: "5.12.0" hooks: - id: isort - repo: https://github.com/adamchainz/django-upgrade rev: "1.12.0" hooks: - id: django-upgrade args: [--target-version, "3.2"] ================================================ FILE: .readthedocs.yml ================================================ # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 mkdocs: configuration: mkdocs.yml python: version: "3.11" install: - method: pip path: . extra_requirements: - docs ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 The @dj-stripe Organization 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. ================================================ FILE: djstripe/__init__.py ================================================ ================================================ FILE: djstripe/admin/__init__.py ================================================ from .admin import StripeModelAdmin # Do not remove: This ensures loading of djstripe.admin.admin __all__ = ["StripeModelAdmin"] ================================================ FILE: djstripe/admin/actions.py ================================================ """ Django Administration Custom Actions Module """ from django.contrib import admin from django.contrib.admin import helpers from django.contrib.admin.utils import quote from django.shortcuts import render from django.urls import path, reverse from django.utils.html import format_html from django.utils.text import capfirst from . import views from .forms import CustomActionForm class CustomActionMixin: # So that actions get shown even if there are 0 instances # https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.show_full_result_count show_full_result_count = False def get_urls(self): custom_urls = [ path( "action///", self.admin_site.admin_view(views.ConfirmCustomAction.as_view()), name="djstripe_custom_action", ), ] return custom_urls + super().get_urls() def get_admin_action_context(self, queryset, action_name, form_class): context = { "action_name": action_name, "model_name": self.model._meta.model_name, "info": [], "queryset": queryset, "changelist_url": reverse( f"admin:{self.model._meta.app_label}_{self.model._meta.model_name}_changelist" ), "ACTION_CHECKBOX_NAME": helpers.ACTION_CHECKBOX_NAME, "form": form_class( initial={ helpers.ACTION_CHECKBOX_NAME: queryset.values_list("pk", flat=True) }, model_name=self.model._meta.model_name, action_name=action_name, ), } if action_name == "_sync_all_instances": context["form"] = form_class( initial={helpers.ACTION_CHECKBOX_NAME: [action_name]}, model_name=self.model._meta.model_name, action_name=action_name, ) else: for obj in queryset: admin_url = reverse( f"admin:{obj._meta.app_label}_{obj._meta.model_name}_change", None, (quote(obj.pk),), ) context["info"].append( format_html( '{}: {}', capfirst(obj._meta.verbose_name), admin_url, obj, ) ) return context def get_actions(self, request): """ Returns _resync_instances only for models with a defined model.stripe_class.retrieve """ actions = super().get_actions(request) # ensure we return "_resync_instances" ONLY for # models that have a GET method if not getattr(self.model.stripe_class, "retrieve", None): actions.pop("_resync_instances", None) return actions @admin.action(description="Re-Sync Selected Instances") def _resync_instances(self, request, queryset): """Admin Action to resync selected instances""" context = self.get_admin_action_context( queryset, "_resync_instances", CustomActionForm ) return render(request, "djstripe/admin/confirm_action.html", context) @admin.action(description="Sync All Instances for all API Keys") def _sync_all_instances(self, request, queryset): """Admin Action to Sync All Instances""" context = self.get_admin_action_context( queryset, "_sync_all_instances", CustomActionForm ) return render(request, "djstripe/admin/confirm_action.html", context) def changelist_view(self, request, extra_context=None): # we fool it into thinking we have selected some query # since we need to sync all instances post = request.POST.copy() if ( helpers.ACTION_CHECKBOX_NAME not in post and post.get("action") == "_sync_all_instances" ): post[helpers.ACTION_CHECKBOX_NAME] = None request._set_post(post) return super().changelist_view(request, extra_context) ================================================ FILE: djstripe/admin/admin.py ================================================ """ Django Administration interface definitions """ from typing import Any, Dict from django.contrib import admin from django.db import IntegrityError, transaction from django.shortcuts import render from stripe.error import InvalidRequestError from djstripe import models from .actions import CustomActionMixin from .admin_inline import ( InvoiceItemInline, LineItemInline, SubscriptionInline, SubscriptionItemInline, SubscriptionScheduleInline, TaxIdInline, ) from .filters import CustomerHasSourceListFilter, CustomerSubscriptionStatusListFilter from .forms import ( APIKeyAdminCreateForm, CustomActionForm, WebhookEndpointAdminCreateForm, WebhookEndpointAdminEditForm, ) from .utils import ReadOnlyMixin, get_forward_relation_fields_for_model @admin.register(models.IdempotencyKey) class IdempotencyKeyAdmin(ReadOnlyMixin, admin.ModelAdmin): list_display = ("uuid", "action", "created", "is_expired", "livemode") list_filter = ("livemode",) search_fields = ("uuid", "action") @admin.register(models.WebhookEventTrigger) class WebhookEventTriggerAdmin(ReadOnlyMixin, admin.ModelAdmin): list_display = ( "created", "event", "stripe_trigger_account", "webhook_endpoint", "remote_ip", "processed", "valid", "exception", "djstripe_version", ) list_filter = ("created", "valid", "processed") list_select_related = ("event",) raw_id_fields = get_forward_relation_fields_for_model(models.WebhookEventTrigger) def reprocess(self, request, queryset): for trigger in queryset: if not trigger.valid: self.message_user(request, "Skipped invalid trigger {trigger!r}") continue trigger.process() def get_queryset(self, request): return ( super() .get_queryset(request) .select_related("stripe_trigger_account", "event", "webhook_endpoint") ) class StripeModelAdmin(CustomActionMixin, admin.ModelAdmin): """Base class for all StripeModel-based model admins""" change_form_template = "djstripe/admin/change_form.html" add_form_template = "djstripe/admin/add_form.html" actions = ("_resync_instances", "_sync_all_instances") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.raw_id_fields = get_forward_relation_fields_for_model(self.model) def get_list_display(self, request): return ( ("__str__", "id", "djstripe_owner_account") + self.list_display + ("created", "livemode") ) def get_list_filter(self, request): return self.list_filter + ("created", "livemode") def get_readonly_fields(self, request, obj=None): return self.readonly_fields + ("id", "djstripe_owner_account", "created") def get_search_fields(self, request): return self.search_fields + ("id",) def get_fieldsets(self, request, obj=None): common_fields = ("livemode", "id", "djstripe_owner_account", "created") # Have to remove the fields from the common set, # otherwise they'll show up twice. fields = [f for f in self.get_fields(request, obj) if f not in common_fields] return ( (None, {"fields": common_fields}), (self.model.__name__, {"fields": fields}), ) def get_queryset(self, request): return super().get_queryset(request).select_related("djstripe_owner_account") @admin.register(models.Account) class AccountAdmin(StripeModelAdmin): list_display = ("business_url", "country", "default_currency") list_filter = ("details_submitted",) search_fields = ("settings", "business_profile") @admin.register(models.APIKey) class APIKeyAdmin(admin.ModelAdmin): add_form_template = "djstripe/admin/add_form.html" change_form_template = "djstripe/admin/change_form.html" list_display = ("__str__", "type", "djstripe_owner_account", "livemode") readonly_fields = ("djstripe_owner_account", "livemode", "type", "secret") search_fields = ("name",) def get_readonly_fields(self, request, obj=None): if obj is None: return ["djstripe_owner_account", "livemode", "type"] return super().get_readonly_fields(request, obj=obj) def get_fields(self, request, obj=None): if obj is None: return APIKeyAdminCreateForm.Meta.fields return ["type", "djstripe_owner_account", "livemode", "name", "secret"] def get_form(self, request, obj=None, **kwargs): if obj is None: return APIKeyAdminCreateForm return super().get_form(request, obj, **kwargs) def get_queryset(self, request): return super().get_queryset(request).select_related("djstripe_owner_account") def save_model(self, request: Any, obj, form: Any, change: Any) -> None: try: # for non-existent Platform Accounts, because of Account._find_owner_account() # it will try to retrieve by api_key, Account.get_or_retrieve_for_api_key(). # Account.get_or_retrieve_for_api_key will create this APIKey! This would cause # an IntegrityError as the APIKey gets created before this form gets saved with transaction.atomic(): obj.save() except IntegrityError: pass @admin.register(models.BalanceTransaction) class BalanceTransactionAdmin(ReadOnlyMixin, StripeModelAdmin): list_display = ( "type", "net", "amount", "fee", "currency", "available_on", "status", ) list_filter = ("status", "type") @admin.register(models.Charge) class ChargeAdmin(StripeModelAdmin): list_display = ( "customer", "amount", "invoice", "payment_method", "description", "paid", "disputed", "refunded", "fee", ) search_fields = ("customer__id", "invoice__id") list_filter = ("status", "paid", "refunded", "captured") def get_queryset(self, request): return ( super() .get_queryset(request) .select_related( "balance_transaction", "customer", "invoice", "payment_method", "payment_method__customer", ) ) @admin.register(models.Coupon) class CouponAdmin(StripeModelAdmin): list_display = ( "amount_off", "percent_off", "duration", "duration_in_months", "redeem_by", "max_redemptions", "times_redeemed", ) list_filter = ("duration", "redeem_by") radio_fields = {"duration": admin.HORIZONTAL} @admin.register(models.Customer) class CustomerAdmin(StripeModelAdmin): list_display = ( "deleted", "subscriber", "email", "currency", "default_source", "default_payment_method", "coupon", "balance", ) list_filter = ( CustomerHasSourceListFilter, CustomerSubscriptionStatusListFilter, "deleted", ) search_fields = ("email", "description", "deleted") inlines = (SubscriptionInline, SubscriptionScheduleInline, TaxIdInline) def get_queryset(self, request): return ( super() .get_queryset(request) .select_related( "subscriber", "default_source", "default_payment_method", "coupon" ) ) @admin.register(models.Discount) class DiscountAdmin(ReadOnlyMixin, StripeModelAdmin): list_display = ( "customer", "coupon", "invoice_item", "promotion_code", "subscription", ) list_filter = ("customer", "start", "end", "promotion_code", "coupon") def get_actions(self, request): """ Returns _resync_instances only for models with a defined model.stripe_class.retrieve """ actions = super().get_actions(request) # remove "_sync_all_instances" as Discounts cannot be listed actions.pop("_sync_all_instances", None) return actions @admin.register(models.Dispute) class DisputeAdmin(ReadOnlyMixin, StripeModelAdmin): list_display = ("reason", "status", "amount", "currency", "is_charge_refundable") list_filter = ("is_charge_refundable", "reason", "status") @admin.register(models.Event) class EventAdmin(ReadOnlyMixin, StripeModelAdmin): list_display = ("type", "request_id") list_filter = ("type", "created") search_fields = ("request_id",) @admin.register(models.File) class FileAdmin(StripeModelAdmin): list_display = ("purpose", "size", "type") list_filter = ("purpose", "type") search_fields = ("filename",) @admin.register(models.FileLink) class FileLinkAdmin(StripeModelAdmin): list_display = ("url",) list_filter = ("expires_at",) def get_queryset(self, request): return super().get_queryset(request).select_related("file") @admin.register(models.Order) class OrderAdmin(StripeModelAdmin): list_display = ( "amount_total", "customer", "status", ) list_filter = ( "customer", "status", ) list_select_related = ("customer", "payment_intent") @admin.register(models.PaymentIntent) class PaymentIntentAdmin(StripeModelAdmin): list_display = ( "on_behalf_of", "customer", "amount", "payment_method", "currency", "description", "amount_capturable", "amount_received", "receipt_email", ) search_fields = ("customer__id", "invoice__id") def get_queryset(self, request): return ( super() .get_queryset(request) .select_related( "customer", "payment_method", "payment_method__customer", "on_behalf_of" ) ) @admin.register(models.Payout) class PayoutAdmin(StripeModelAdmin): list_display = ( "destination", "amount", "arrival_date", "method", "status", "type", ) list_filter = ("destination__id",) search_fields = ("destination__id", "balance_transaction__id") def get_queryset(self, request): return ( super() .get_queryset(request) .select_related("balance_transaction", "destination") ) @admin.register(models.SetupIntent) class SetupIntentAdmin(StripeModelAdmin): list_display = ( "created", "customer", "description", "on_behalf_of", "payment_method", "payment_method_types", "status", ) list_filter = ("status",) search_fields = ("customer__id", "status") def get_queryset(self, request): return ( super() .get_queryset(request) .select_related( "on_behalf_of", "customer", "customer__subscriber", "payment_method", "payment_method__customer", ) ) @admin.register(models.Session) class SessionAdmin(StripeModelAdmin): list_display = ("customer", "customer_email", "subscription") list_filter = ("customer", "mode") search_fields = ("customer__id", "customer_email") def get_queryset(self, request): return super().get_queryset(request).select_related("customer", "subscription") @admin.register(models.Invoice) class InvoiceAdmin(StripeModelAdmin): list_display = ( "total", "get_default_tax_rates", "paid", "currency", "number", "customer", "due_date", ) list_filter = ( "status", "paid", "attempted", "created", "due_date", "period_start", "period_end", ) search_fields = ("customer__id", "number", "receipt_number") inlines = (InvoiceItemInline,) @admin.display(description="Default Tax Rates") def get_default_tax_rates(self, obj): result = [str(tax_rate) for tax_rate in obj.default_tax_rates.all()] if result: return ", ".join(result) def get_queryset(self, request): return ( super() .get_queryset(request) .select_related("customer") .prefetch_related("default_tax_rates") ) @admin.register(models.LineItem) class LineItemAdmin(StripeModelAdmin): list_display = ( "amount", "invoice_item", "subscription", "subscription_item", "type", ) list_filter = ("type", "invoice_item", "subscription", "subscription_item") list_select_related = ("invoice_item", "subscription", "subscription_item") @admin.register(models.Mandate) class MandateAdmin(StripeModelAdmin): list_display = ("status", "type", "payment_method") list_filter = ("multi_use", "single_use") search_fields = ("payment_method__id",) def get_queryset(self, request): return super().get_queryset(request).select_related("payment_method") def get_actions(self, request): """ Returns _resync_instances only for models with a defined model.stripe_class.retrieve """ actions = super().get_actions(request) # remove "_sync_all_instances" as Mandates cannot be listed actions.pop("_sync_all_instances", None) return actions @admin.register(models.Plan) class PlanAdmin(StripeModelAdmin): radio_fields = {"interval": admin.HORIZONTAL} def get_readonly_fields(self, request, obj=None): """Return extra readonly_fields.""" readonly_fields = super().get_readonly_fields(request, obj) if obj: readonly_fields += ( "amount", "currency", "interval", "interval_count", "trial_period_days", ) return readonly_fields def get_queryset(self, request): return ( super() .get_queryset(request) .select_related("product") .prefetch_related("subscriptions") ) @admin.register(models.Price) class PriceAdmin(StripeModelAdmin): list_display = ("product", "currency", "active") list_filter = ("active", "type", "billing_scheme", "tiers_mode") raw_id_fields = ("product",) search_fields = ("nickname",) radio_fields = {"type": admin.HORIZONTAL} def get_queryset(self, request): return ( super() .get_queryset(request) .select_related("product") .prefetch_related("product__prices") ) @admin.register(models.Product) class ProductAdmin(StripeModelAdmin): list_display = ( "name", "default_price", "type", "active", "url", "statement_descriptor", ) list_filter = ("type", "active", "shippable") search_fields = ("name", "statement_descriptor") def get_queryset(self, request): return super().get_queryset(request).prefetch_related("prices") @admin.register(models.Refund) class RefundAdmin(StripeModelAdmin): list_display = ( "amount", "currency", "charge", "reason", "status", "failure_reason", ) list_filter = ("reason", "status") search_fields = ("receipt_number",) def get_queryset(self, request): return super().get_queryset(request).select_related("charge") @admin.register(models.Source) class SourceAdmin(StripeModelAdmin): list_display = ("customer", "type", "status", "amount", "currency", "usage", "flow") list_filter = ("type", "status", "usage", "flow") def get_queryset(self, request): return ( super() .get_queryset(request) .select_related("customer", "customer__subscriber") ) @admin.register(models.PaymentMethod) class PaymentMethodAdmin(StripeModelAdmin): list_display = ("customer", "type", "billing_details") list_filter = ("type",) search_fields = ("customer__id",) def get_queryset(self, request): return ( super() .get_queryset(request) .select_related("customer", "customer__subscriber") ) @admin.register(models.Card) class CardAdmin(StripeModelAdmin): list_display = ("customer", "account") search_fields = ("customer__id", "account__id") def get_queryset(self, request): return ( super() .get_queryset(request) .select_related( "customer", "customer__default_source", "customer__default_payment_method", "account", ) ) @admin.register(models.BankAccount) class BankAccountAdmin(StripeModelAdmin): list_display = ("customer", "account") search_fields = ("customer__id", "account__id") def get_queryset(self, request): return ( super() .get_queryset(request) .select_related( "customer", "customer__default_source", "customer__default_payment_method", "account", ) ) @admin.register(models.ShippingRate) class ShippingRateAdmin(StripeModelAdmin): list_display = ("display_name", "active", "tax_behavior", "tax_code") list_filter = ("active", "tax_behavior") list_select_related = ("tax_code",) @admin.register(models.Subscription) class SubscriptionAdmin(StripeModelAdmin): list_display = ("customer", "status", "get_product_name", "get_default_tax_rates") list_filter = ("status", "cancel_at_period_end") inlines = (SubscriptionItemInline, SubscriptionScheduleInline, LineItemInline) def get_actions(self, request): # get all actions actions = super().get_actions(request) actions["_cancel"] = self.get_action("_cancel") return actions @admin.action(description="Cancel selected subscriptions") def _cancel(self, request, queryset): """Cancel a subscription.""" context = self.get_admin_action_context(queryset, "_cancel", CustomActionForm) return render(request, "djstripe/admin/confirm_action.html", context) def get_queryset(self, request): return ( super() .get_queryset(request) .select_related( "customer", "plan", "plan__product", ) .prefetch_related( "customer__subscriptions", "customer__subscriptions__plan", "customer__subscriptions__plan__product", "default_tax_rates", ) ) @admin.display(description="Default Tax Rates") def get_default_tax_rates(self, obj): result = [str(tax_rate) for tax_rate in obj.default_tax_rates.all()] if result: return ", ".join(result) @admin.display(description="Product Name") def get_product_name(self, obj): if obj.plan and obj.plan.product: return obj.plan.product.name @admin.register(models.SubscriptionSchedule) class SubscriptionScheduleAdmin(StripeModelAdmin): list_display = ("status", "subscription", "current_phase", "customer") list_filter = ("status", "subscription", "customer") list_select_related = ("customer", "customer__subscriber", "subscription") @admin.display(description="Release Selected Subscription Schedules") def _release_subscription_schedule(self, request, queryset): """Release a SubscriptionSchedule.""" context = self.get_admin_action_context( queryset, "_release_subscription_schedule", CustomActionForm ) return render(request, "djstripe/admin/confirm_action.html", context) @admin.display(description="Cancel Selected Subscription Schedules") def _cancel_subscription_schedule(self, request, queryset): """Cancel a SubscriptionSchedule.""" context = self.get_admin_action_context( queryset, "_cancel_subscription_schedule", CustomActionForm ) return render(request, "djstripe/admin/confirm_action.html", context) def get_actions(self, request): # get all actions actions = super().get_actions(request) actions["_release_subscription_schedule"] = self.get_action( "_release_subscription_schedule" ) actions["_cancel_subscription_schedule"] = self.get_action( "_cancel_subscription_schedule" ) return actions @admin.register(models.TaxCode) class TaxCodeAdmin(StripeModelAdmin): list_display = ("name", "description") list_filter = ("name",) @admin.register(models.TaxRate) class TaxRateAdmin(StripeModelAdmin): list_display = ("active", "display_name", "inclusive", "jurisdiction", "percentage") list_filter = ("active", "inclusive", "jurisdiction") @admin.register(models.Transfer) class TransferAdmin(StripeModelAdmin): list_display = ("amount", "description") @admin.register(models.TransferReversal) class TransferReversalAdmin(StripeModelAdmin): list_display = ("amount", "transfer") @admin.register(models.ApplicationFee) class ApplicationFeeAdmin(StripeModelAdmin): list_display = ("amount", "account") @admin.register(models.ApplicationFeeRefund) class ApplicationFeeReversalAdmin(StripeModelAdmin): list_display = ("amount", "fee") @admin.register(models.UsageRecord) class UsageRecordAdmin(StripeModelAdmin): list_display = ("quantity", "subscription_item", "timestamp") def get_queryset(self, request): return super().get_queryset(request).select_related("subscription_item") def get_actions(self, request): """ Returns _resync_instances only for models with a defined model.stripe_class.retrieve """ actions = super().get_actions(request) # remove "_sync_all_instances" as UsageRecords cannot be listed actions.pop("_sync_all_instances", None) return actions @admin.register(models.UsageRecordSummary) class UsageRecordSummaryAdmin(StripeModelAdmin): list_display = ("invoice", "subscription_item", "total_usage") def get_queryset(self, request): return ( super().get_queryset(request).select_related("invoice", "subscription_item") ) @admin.register(models.WebhookEndpoint) class WebhookEndpointAdmin(CustomActionMixin, admin.ModelAdmin): change_form_template = "djstripe/admin/webhook_endpoint/change_form.html" delete_confirmation_template = ( "djstripe/admin/webhook_endpoint/delete_confirmation.html" ) add_form_template = "djstripe/admin/webhook_endpoint/add_form.html" readonly_fields = ("url",) list_display = ( "__str__", "djstripe_owner_account", "livemode", "created", "api_version", ) actions = ("_resync_instances", "_sync_all_instances") def get_actions(self, request): actions = super().get_actions(request) # Disable the mass-delete action for webhook endpoints. # We don't want to enable deleting multiple endpoints on Stripe at once. if "delete_selected" in actions: del actions["delete_selected"] return actions def get_form(self, request, obj=None, **kwargs): if obj: return WebhookEndpointAdminEditForm return WebhookEndpointAdminCreateForm def get_readonly_fields(self, request, obj=None): if obj: return ( "id", "livemode", "api_version", "url", "created", "djstripe_owner_account", "djstripe_uuid", ) return super().get_readonly_fields(request, obj=obj) def get_fieldsets(self, request, obj=None): if obj: # if djstripe_uuid is null, this is not a dj-stripe webhook header_fields = ["id", "livemode", "djstripe_owner_account", "url"] advanced_fields = [ "enabled_events", "metadata", "api_version", "djstripe_uuid", ] if obj.djstripe_uuid: core_fields = ["enabled", "base_url", "description"] else: core_fields = ["enabled", "description"] else: header_fields = ["djstripe_owner_account", "livemode"] core_fields = ["description", "base_url", "connect"] advanced_fields = ["metadata", "api_version", "enabled_events"] return [ (None, {"fields": header_fields}), ("Endpoint configuration", {"fields": core_fields}), ( "Advanced", {"fields": advanced_fields, "classes": ["collapse"]}, ), ] def get_changeform_initial_data(self, request) -> Dict[str, str]: ret = super().get_changeform_initial_data(request) base_url = f"{request.scheme}://{request.get_host()}" ret.setdefault("base_url", base_url) return ret def delete_model(self, request, obj: models.WebhookEndpoint): try: obj._api_delete() except InvalidRequestError as e: if e.user_message.startswith("No such webhook endpoint: "): # Webhook was already deleted in Stripe pass else: raise return super().delete_model(request, obj) def get_queryset(self, request): return super().get_queryset(request).select_related("djstripe_owner_account") ================================================ FILE: djstripe/admin/admin_inline.py ================================================ """ Django Administration Inline interface definitions """ from django.contrib import admin from djstripe import models from .utils import get_forward_relation_fields_for_model class SubscriptionInline(admin.StackedInline): """A TabularInline for use models.Subscription.""" model = models.Subscription extra = 0 readonly_fields = ("id", "created", "djstripe_owner_account") raw_id_fields = get_forward_relation_fields_for_model(model) show_change_link = True class SubscriptionScheduleInline(admin.StackedInline): """A TabularInline for use models.SubscriptionSchedule.""" model = models.SubscriptionSchedule extra = 0 readonly_fields = ("id", "created", "djstripe_owner_account") raw_id_fields = get_forward_relation_fields_for_model(model) show_change_link = True def __init__(self, parent_model, admin_site): super().__init__(parent_model, admin_site) # dynamically set fk_name as SubscriptionScheduleInline is used # in CustomerAdmin as well as SubscriptionAdmin if parent_model is models.Subscription: self.fk_name = "subscription" class TaxIdInline(admin.TabularInline): """A TabularInline for use models.Subscription.""" model = models.TaxId extra = 0 max_num = 5 readonly_fields = ( "id", "created", "verification", "livemode", "country", "djstripe_owner_account", ) show_change_link = True class SubscriptionItemInline(admin.StackedInline): """A TabularInline for use models.Subscription.""" model = models.SubscriptionItem extra = 0 readonly_fields = ("id", "created", "djstripe_owner_account") raw_id_fields = get_forward_relation_fields_for_model(model) show_change_link = True class InvoiceItemInline(admin.StackedInline): """A TabularInline for use InvoiceItem.""" model = models.InvoiceItem extra = 0 readonly_fields = ("id", "created", "djstripe_owner_account") raw_id_fields = get_forward_relation_fields_for_model(model) show_change_link = True class LineItemInline(admin.StackedInline): """A TabularInline for LineItem.""" model = models.LineItem extra = 0 readonly_fields = ("id", "created", "djstripe_owner_account") raw_id_fields = get_forward_relation_fields_for_model(model) show_change_link = True ================================================ FILE: djstripe/admin/filters.py ================================================ """ Django Administration Custom Filters Module """ from django.contrib import admin from djstripe import models class BaseHasSourceListFilter(admin.SimpleListFilter): title = "source presence" parameter_name = "has_source" def lookups(self, request, model_admin): """ Return a list of tuples. The first element in each tuple is the coded value for the option that will appear in the URL query. The second element is the human-readable name for the option that will appear in the right sidebar. source: https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter """ return (("yes", "Has a source"), ("no", "Has no source")) def queryset(self, request, queryset): """ Return the filtered queryset based on the value provided in the query string. source: https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter """ filter_args = {self._filter_arg_key: None} if self.value() == "yes": return queryset.exclude(**filter_args) if self.value() == "no": return queryset.filter(**filter_args) class CustomerHasSourceListFilter(BaseHasSourceListFilter): _filter_arg_key = "default_source" class InvoiceCustomerHasSourceListFilter(BaseHasSourceListFilter): _filter_arg_key = "customer__default_source" class CustomerSubscriptionStatusListFilter(admin.SimpleListFilter): """A SimpleListFilter used with Customer admin.""" title = "subscription status" parameter_name = "sub_status" def lookups(self, request, model_admin): """ Return a list of tuples. The first element in each tuple is the coded value for the option that will appear in the URL query. The second element is the human-readable name for the option that will appear in the right sidebar. source: https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter """ statuses = [ [x, x.replace("_", " ").title()] for x in models.Subscription.objects.values_list( "status", flat=True ).distinct() ] statuses.append(["none", "No Subscription"]) return statuses def queryset(self, request, queryset): """ Return the filtered queryset based on the value provided in the query string. source: https://docs.djangoproject.com/en/1.10/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter """ if self.value() is None: return queryset.all() else: return queryset.filter(subscriptions__status=self.value()).distinct() ================================================ FILE: djstripe/admin/forms.py ================================================ """ Module for all dj-stripe Admin app forms """ from typing import Optional from urllib.parse import urljoin from django import forms from django.contrib.admin import helpers from django.urls import reverse from stripe.error import AuthenticationError, InvalidRequestError from djstripe import enums, models, utils from djstripe.signals import ENABLED_EVENTS class CustomActionForm(forms.Form): """Form for Custom Django Admin Actions""" def __init__(self, *args, **kwargs): # remove model_name kwarg model_name = kwargs.pop("model_name") # remove action_name kwarg action_name = kwargs.pop("action_name") super().__init__(*args, **kwargs) model = utils.get_model(model_name) # set choices attribute # form field to keep track of all selected instances # for the Custom Django Admin Action if action_name == "_sync_all_instances": self.fields[helpers.ACTION_CHECKBOX_NAME] = forms.MultipleChoiceField( widget=forms.MultipleHiddenInput, choices=[(action_name, action_name)], ) else: self.fields[helpers.ACTION_CHECKBOX_NAME] = forms.MultipleChoiceField( widget=forms.MultipleHiddenInput, choices=zip( model.objects.values_list("pk", flat=True), model.objects.values_list("pk", flat=True), ), ) class APIKeyAdminCreateForm(forms.ModelForm): class Meta: model = models.APIKey fields = ["name", "secret"] def _post_clean(self): super()._post_clean() if not self.errors: if ( self.instance.type == enums.APIKeyType.secret and self.instance.djstripe_owner_account is None ): try: self.instance.refresh_account() except AuthenticationError as e: self.add_error("secret", str(e)) class WebhookEndpointAdminBaseForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["description"].help_text = "" self.fields["description"].widget.attrs["rows"] = 3 def _get_field_name(self, stripe_field: Optional[str]) -> Optional[str]: if stripe_field is None: return None if stripe_field == "url": return "base_url" else: return stripe_field.partition("[")[0] def save(self, commit: bool = False): # If we do the following in _post_clean(), the data doesn't save properly. if not self._stripe_data: raise ValueError("_stripe_data is not present. ") # Update scenario # Add back secret if endpoint already exists if self.instance.pk and not self._stripe_data.get("secret"): self._stripe_data["secret"] = self.instance.secret # Retrieve the api key that was used to create the endpoint api_key = getattr(self, "_stripe_api_key", None) if api_key: self.instance = models.WebhookEndpoint.sync_from_stripe_data( self._stripe_data, api_key=api_key ) else: self.instance = models.WebhookEndpoint.sync_from_stripe_data( self._stripe_data ) return super().save(commit=commit) class WebhookEndpointAdminCreateForm(WebhookEndpointAdminBaseForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["djstripe_owner_account"].label = "Stripe account" self.fields["djstripe_owner_account"].help_text = "" enabled_events = forms.MultipleChoiceField( label="Enabled Events", required=True, help_text=( "The list of events to enable for this endpoint. " "['*'] indicates that all events are enabled, except those that require explicit selection." ), choices=zip(ENABLED_EVENTS, ENABLED_EVENTS), initial=["*"], ) livemode = forms.BooleanField( label="Live mode", required=False, help_text="Whether to create this endpoint in live mode or test mode", ) base_url = forms.URLField( required=True, help_text=( "Sets the base URL (scheme and host) for the endpoint. " "The final full URL will be auto-generated by dj-stripe." ), ) connect = forms.BooleanField( label="Listen to events on Connected accounts", initial=False, required=False, help_text=( "Clients can make requests as connected accounts using the special " "header `Stripe-Account` which should contain a Stripe account ID " "(usually starting with the prefix `acct_`)." ), ) class Meta: model = models.WebhookEndpoint fields = ( "enabled_events", "livemode", "djstripe_owner_account", "description", "base_url", "connect", "api_version", "metadata", ) # Hook into _post_clean() instead of save(). # This is used by Django for ModelForm logic. It's internal, but exactly # what we need to add errors after the data has been validated locally. def _post_clean(self): base_url = self.cleaned_data["base_url"] url_path = reverse( "djstripe:djstripe_webhook_by_uuid", kwargs={"uuid": self.instance.djstripe_uuid}, ) url = urljoin(base_url, url_path, allow_fragments=False) metadata = self.instance.metadata or {} metadata["djstripe_uuid"] = str(self.instance.djstripe_uuid) _api_key = {} account = self.cleaned_data["djstripe_owner_account"] livemode = self.cleaned_data["livemode"] if account: self._stripe_api_key = _api_key["api_key"] = account.get_default_api_key( livemode=livemode ) try: self._stripe_data = models.WebhookEndpoint._api_create( url=url, api_version=self.cleaned_data["api_version"] or None, description=self.cleaned_data["description"], enabled_events=self.cleaned_data.get("enabled_events"), metadata=metadata, connect=self.cleaned_data["connect"], **_api_key, ) except InvalidRequestError as e: field_name = self._get_field_name(e.param) self.add_error(field_name, e.user_message) return super()._post_clean() class WebhookEndpointAdminEditForm(WebhookEndpointAdminBaseForm): enabled_events = forms.MultipleChoiceField( label="Enabled Events", required=True, help_text=( "The list of events to enable for this endpoint. " "['*'] indicates that all events are enabled, except those that require explicit selection." ), choices=zip(ENABLED_EVENTS, ENABLED_EVENTS), ) base_url = forms.URLField( required=False, help_text=( "Updating this changes the base URL of the endpoint. " "MUST be publicly-accessible." ), ) enabled = forms.BooleanField( initial=True, required=False, help_text="When disabled, the endpoint will not receive events.", ) class Meta: model = models.WebhookEndpoint fields = ("description", "base_url", "enabled_events", "metadata") def get_initial_for_field(self, field, field_name): if field_name == "base_url": metadata = self.instance.metadata or {} djstripe_uuid = metadata.get("djstripe_uuid") if djstripe_uuid: # if a djstripe_uuid is set (for dj-stripe endpoints), set the base_url endpoint_path = reverse( "djstripe:djstripe_webhook_by_uuid", kwargs={"uuid": djstripe_uuid} ) return self.instance.url.replace(endpoint_path, "") return super().get_initial_for_field(field, field_name) def _post_clean(self): base_url = self.cleaned_data.get("base_url", "") if base_url and self.instance.djstripe_uuid: url_path = reverse( "djstripe:djstripe_webhook_by_uuid", kwargs={"uuid": self.instance.djstripe_uuid}, ) url = urljoin(base_url, url_path, allow_fragments=False) else: url = self.instance.url try: self._stripe_data = self.instance._api_update( url=url, description=self.cleaned_data.get("description"), enabled_events=self.cleaned_data.get("enabled_events"), metadata=self.cleaned_data.get("metadata"), disabled=(not self.cleaned_data.get("enabled")), ) except InvalidRequestError as e: field_name = self._get_field_name(e.param) self.add_error(field_name, e.user_message) return super()._post_clean() ================================================ FILE: djstripe/admin/utils.py ================================================ """ Django Administration Utils Module """ class ReadOnlyMixin: def has_add_permission(self, request): return False def has_change_permission(self, request, obj=None): return False def get_forward_relation_fields_for_model(model): """Return an iterable of the field names that are forward relations, I.E ManyToManyField, OneToOneField, and ForeignKey. Useful for perhaps ensuring the admin is always using raw ID fields for newly added forward relation fields. """ return [ field.name for field in model._meta.get_fields() # Get only relation fields if field.is_relation # Exclude auto relation fields, like reverse one to one. and not field.auto_created # We only want forward relations. and any((field.many_to_many, field.one_to_one, field.many_to_one)) ] ================================================ FILE: djstripe/admin/views.py ================================================ """ dj-stripe - Views related to the djstripe app. """ import logging import stripe from django.contrib import messages from django.contrib.admin import helpers, site from django.core.management import call_command from django.http import HttpResponseRedirect from django.urls import reverse from django.views.generic import FormView from djstripe import utils from .forms import CustomActionForm logger = logging.getLogger(__name__) class ConfirmCustomAction(FormView): template_name = "djstripe/admin/confirm_action.html" form_class = CustomActionForm def form_valid(self, form): model_name = self.kwargs.get("model_name") action_name = self.kwargs.get("action_name") model = utils.get_model(model_name) pks = form.cleaned_data.get(helpers.ACTION_CHECKBOX_NAME) # get the handler handler = getattr(self, action_name) if action_name == "_sync_all_instances": # Create Empty Queryset to be able to extract the model name # as sync all would sync all instances anyway and there is no guarantee # that the local db already has all the instances. qs = model.objects.none() else: qs = utils.get_queryset(pks, model_name) # Process Request handler(self.request, qs) return HttpResponseRedirect( reverse( f"admin:{model._meta.app_label}_{model._meta.model_name}_changelist" ) ) def form_invalid(self, form): model_name = self.kwargs.get("model_name") action_name = self.kwargs.get("action_name") model = utils.get_model(model_name) pks = form.data.getlist(helpers.ACTION_CHECKBOX_NAME) pks = list(map(int, pks)) queryset = utils.get_queryset(pks, model_name) model_admin = site._registry.get(model) for msg in form.errors.values(): messages.add_message(self.request, messages.ERROR, msg.as_text()) return model_admin.get_action(action_name)[0]( model_admin, self.request, queryset ) def get_form_kwargs(self): form_kwargs = super().get_form_kwargs() form_kwargs["model_name"] = self.kwargs.get("model_name") form_kwargs["action_name"] = self.kwargs.get("action_name") return form_kwargs def _resync_instances(self, request, queryset): for instance in queryset: api_key = instance.default_api_key try: if instance.djstripe_owner_account: stripe_data = instance.api_retrieve( stripe_account=instance.djstripe_owner_account.id, api_key=api_key, ) else: stripe_data = instance.api_retrieve() instance.__class__.sync_from_stripe_data(stripe_data, api_key=api_key) messages.success(request, f"Successfully Synced: {instance}") except stripe.error.PermissionError as error: messages.warning(request, error) except stripe.error.InvalidRequestError: raise def _sync_all_instances(self, request, queryset): """Admin Action to Sync All Instances""" call_command("djstripe_sync_models", queryset.model.__name__) messages.success(request, "Successfully Synced All Instances") def _cancel(self, request, queryset): """Cancel a subscription.""" for subscription in queryset: try: instance = subscription.cancel() messages.success(request, f"Successfully Canceled: {instance}") except stripe.error.InvalidRequestError as error: messages.warning(request, error) def _release_subscription_schedule(self, request, queryset): """Release a SubscriptionSchedule.""" for subscription_schedule in queryset: try: instance = subscription_schedule.release() messages.success(request, f"Successfully Released: {instance}") except stripe.error.InvalidRequestError as error: messages.warning(request, error) def _cancel_subscription_schedule(self, request, queryset): """Cancel a SubscriptionSchedule.""" for subscription_schedule in queryset: try: instance = subscription_schedule.cancel() messages.success(request, f"Successfully Canceled: {instance}") except stripe.error.InvalidRequestError as error: messages.warning(request, error) ================================================ FILE: djstripe/apps.py ================================================ """ dj-stripe - Django + Stripe Made Easy """ import pkg_resources from django.apps import AppConfig __version__ = pkg_resources.get_distribution("dj-stripe").version class DjstripeAppConfig(AppConfig): """ An AppConfig for dj-stripe which loads system checks and event handlers once Django is ready. """ name = "djstripe" default_auto_field = "django.db.models.AutoField" def ready(self): import stripe from . import checks, event_handlers # noqa (register event handlers) # Set app info # https://stripe.com/docs/building-plugins#setappinfo stripe.set_app_info( "dj-stripe", version=__version__, url="https://github.com/dj-stripe/dj-stripe", ) ================================================ FILE: djstripe/checks.py ================================================ """ dj-stripe System Checks """ import re from django.core import checks from django.db.utils import DatabaseError STRIPE_API_VERSION_PATTERN = re.compile( r"(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})(; [\w=]*)?$" ) # 4 possibilities: # Keys in admin and in settings # Keys in admin and not in settings # Keys not in admin but in settings # Keys not in admin and not in settings @checks.register("djstripe") def check_stripe_api_key(app_configs=None, **kwargs): """Check the user has configured API live/test keys correctly.""" def _check_stripe_api_in_settings(messages): if djstripe_settings.STRIPE_LIVE_MODE: if not djstripe_settings.LIVE_API_KEY.startswith(("sk_live_", "rk_live_")): msg = "Bad Stripe live API key." hint = 'STRIPE_LIVE_SECRET_KEY should start with "sk_live_"' messages.append(checks.Info(msg, hint=hint, id="djstripe.I003")) elif not djstripe_settings.TEST_API_KEY.startswith(("sk_test_", "rk_test_")): msg = "Bad Stripe test API key." hint = 'STRIPE_TEST_SECRET_KEY should start with "sk_test_"' messages.append(checks.Info(msg, hint=hint, id="djstripe.I004")) from djstripe.models import APIKey from .settings import djstripe_settings messages = [] try: # get all APIKey objects in the db api_qs = APIKey.objects.all() if not api_qs.exists(): msg = "You don't have any API Keys in the database. Did you forget to add them?" hint = "Add STRIPE_TEST_SECRET_KEY and STRIPE_LIVE_SECRET_KEY directly from the Django Admin." messages.append(checks.Info(msg, hint=hint, id="djstripe.I001")) # Keys not in admin but in settings if djstripe_settings.STRIPE_SECRET_KEY: msg = "Your keys are defined in the settings files. You can now add and manage them directly from the django admin." hint = "Add STRIPE_TEST_SECRET_KEY and STRIPE_LIVE_SECRET_KEY directly from the Django Admin." messages.append(checks.Info(msg, hint=hint, id="djstripe.I002")) # Ensure keys defined in settings files are valid _check_stripe_api_in_settings(messages) # Keys in admin and in settings elif djstripe_settings.STRIPE_SECRET_KEY: msg = "Your keys are defined in the settings files and are also in the admin. You can now add and manage them directly from the django admin." hint = "We suggest adding STRIPE_TEST_SECRET_KEY and STRIPE_LIVE_SECRET_KEY directly from the Django Admin. And removing them from the settings files." messages.append(checks.Info(msg, hint=hint, id="djstripe.I002")) # Ensure keys defined in settings files are valid _check_stripe_api_in_settings(messages) except DatabaseError: # Skip the check - Database most likely not migrated yet return [] return messages def validate_stripe_api_version(version): """ Check the API version is formatted correctly for Stripe. The expected format is `YYYY-MM-DD` (an iso8601 date) or for access to alpha or beta releases the expected format is: `YYYY-MM-DD; modelname_version=version_number`. Ex "2020-08-27; orders_beta=v3" :param version: The version to set for the Stripe API. :type version: ``str`` :returns bool: Whether the version is formatted correctly. """ return re.match(STRIPE_API_VERSION_PATTERN, version) @checks.register("djstripe") def check_stripe_api_version(app_configs=None, **kwargs): """Check the user has configured API version correctly.""" from .settings import djstripe_settings messages = [] default_version = djstripe_settings.DEFAULT_STRIPE_API_VERSION version = djstripe_settings.STRIPE_API_VERSION if not validate_stripe_api_version(version): msg = f"Invalid Stripe API version: {version!r}" hint = "STRIPE_API_VERSION should be formatted as: YYYY-MM-DD" messages.append(checks.Critical(msg, hint=hint, id="djstripe.C004")) if version != default_version: msg = ( f"The Stripe API version has a non-default value of '{version!r}'. " "Non-default versions are not explicitly supported, and may " "cause compatibility issues." ) hint = f"Use the dj-stripe default for Stripe API version: {default_version}" messages.append(checks.Warning(msg, hint=hint, id="djstripe.W001")) return messages @checks.register("djstripe") def check_stripe_api_host(app_configs=None, **kwargs): """ Check that STRIPE_API_HOST is not being used in production. """ from django.conf import settings messages = [] if not settings.DEBUG and hasattr(settings, "STRIPE_API_HOST"): messages.append( checks.Warning( "STRIPE_API_HOST should not be set in production! " "This is most likely unintended.", hint="Remove STRIPE_API_HOST from your Django settings.", id="djstripe.W002", ) ) return messages @checks.register("djstripe") def check_webhook_secret(app_configs=None, **kwargs): """ Check that DJSTRIPE_WEBHOOK_SECRET looks correct """ def check_webhook_endpoint_secret(secret, messages, endpoint=None): if secret and not secret.startswith("whsec_"): if endpoint: extra_msg = ( f"The secret for Webhook Endpoint: {endpoint} does not look valid" ) else: extra_msg = "DJSTRIPE_WEBHOOK_SECRET does not look valid" messages.append( checks.Warning( extra_msg, hint="It should start with whsec_...", id="djstripe.W003", ) ) return messages from .models import WebhookEndpoint from .settings import djstripe_settings messages = [] try: webhooks = list(WebhookEndpoint.objects.all()) except DatabaseError: # skip the db-based check (db most likely not migrated yet) webhooks = [] if webhooks: for endpoint in webhooks: secret = endpoint.secret # check secret check_webhook_endpoint_secret(secret, messages, endpoint=endpoint) else: secret = djstripe_settings.WEBHOOK_SECRET # check secret check_webhook_endpoint_secret(secret, messages) return messages def _check_webhook_endpoint_validation(secret, messages, endpoint=None): if not secret: if endpoint: extra_msg = f"but Webhook Endpoint: {endpoint} has no secret set" secret_attr = "secret" else: extra_msg = "but DJSTRIPE_WEBHOOK_SECRET is not set" secret_attr = "DJSTRIPE_WEBHOOK_SECRET" messages.append( checks.Info( f"DJSTRIPE_WEBHOOK_VALIDATION is set to 'verify_signature' {extra_msg}", hint=f"Set {secret_attr} from Django shell or set DJSTRIPE_WEBHOOK_VALIDATION='retrieve_event'", id="djstripe.I006", ) ) return messages @checks.register("djstripe") def check_webhook_validation(app_configs=None, **kwargs): """ Check that DJSTRIPE_WEBHOOK_VALIDATION is valid """ from .models import WebhookEndpoint from .settings import djstripe_settings setting_name = "DJSTRIPE_WEBHOOK_VALIDATION" messages = [] validation_options = ("verify_signature", "retrieve_event") if djstripe_settings.WEBHOOK_VALIDATION is None: messages.append( checks.Warning( "Webhook validation is disabled, this is a security risk if the " "webhook view is enabled", hint=f"Set {setting_name} to one of: {validation_options}", id="djstripe.W004", ) ) elif djstripe_settings.WEBHOOK_VALIDATION == "verify_signature": try: webhooks = list(WebhookEndpoint.objects.all()) except DatabaseError: # Skip the db-based check (database most likely not migrated yet) webhooks = [] if webhooks: for endpoint in webhooks: secret = endpoint.secret # check secret _check_webhook_endpoint_validation(secret, messages, endpoint=endpoint) else: secret = djstripe_settings.WEBHOOK_SECRET # check secret _check_webhook_endpoint_validation(secret, messages) elif djstripe_settings.WEBHOOK_VALIDATION not in validation_options: messages.append( checks.Critical( f"{setting_name} is invalid", hint=f"Set {setting_name} to one of: {validation_options} or None", id="djstripe.C007", ) ) return messages @checks.register("djstripe") def check_webhook_endpoint_has_secret(app_configs=None, **kwargs): """Checks if all Webhook Endpoints have not empty secrets.""" from djstripe.models import WebhookEndpoint messages = [] try: qs = list(WebhookEndpoint.objects.filter(secret="").all()) except DatabaseError: # Skip the check - Database most likely not migrated yet return [] for webhook in qs: webhook_url = webhook.get_stripe_dashboard_url() messages.append( checks.Warning( ( f"The secret of Webhook Endpoint: {webhook} is not populated " "in the db. Events sent to it will not work properly." ), hint=( "This can happen if it was deleted and resynced as Stripe " "sends the webhook secret ONLY on the creation call. " "Please use the django shell and update the secret with " f"the value from {webhook_url}" ), id="djstripe.W005", ) ) return messages @checks.register("djstripe") def check_subscriber_key_length(app_configs=None, **kwargs): """ Check that DJSTRIPE_SUBSCRIBER_CUSTOMER_KEY fits in metadata. Docs: https://stripe.com/docs/api#metadata """ from .settings import djstripe_settings messages = [] key = djstripe_settings.SUBSCRIBER_CUSTOMER_KEY key_max_length = 40 if key and len(key) > key_max_length: messages.append( checks.Error( "DJSTRIPE_SUBSCRIBER_CUSTOMER_KEY must be no more than " f"{key_max_length} characters long", hint=f"Current value: {key!r}", id="djstripe.E001", ) ) return messages @checks.register("djstripe") def check_djstripe_settings_foreign_key_to_field(app_configs=None, **kwargs): """ Check that DJSTRIPE_FOREIGN_KEY_TO_FIELD is set to a valid value. """ from django.conf import settings setting_name = "DJSTRIPE_FOREIGN_KEY_TO_FIELD" hint = ( f'Set {setting_name} to "id" if this is a new installation, ' f'otherwise set it to "djstripe_id".' ) messages = [] if not hasattr(settings, setting_name): messages.append( checks.Error( f"{setting_name} is not set.", hint=hint, id="djstripe.E002", ) ) elif getattr(settings, setting_name) not in ("id", "djstripe_id"): setting_value = getattr(settings, setting_name) messages.append( checks.Error( f"{setting_value} is not a valid value for {setting_name}.", hint=hint, id="djstripe.E003", ) ) return messages @checks.register("djstripe") def check_webhook_event_callback_accepts_api_key(app_configs=None, **kwargs): """ Checks if the custom callback accepts atleast 2 mandatory positional arguments """ from inspect import signature from .settings import djstripe_settings messages = [] # callable can have exactly 2 arguments or # if more than two, the rest need to be optional. callable = djstripe_settings.WEBHOOK_EVENT_CALLBACK if callable: # Deprecated in 2.8.0. Raise a warning. messages.append( checks.Warning( "DJSTRIPE_WEBHOOK_EVENT_CALLBACK is deprecated. See release notes for details.", hint=( "If you need to trigger a function during webhook processing, " "you can use djstripe.signals instead.\n" "Available signals:\n" "- djstripe.signals.webhook_pre_validate\n" "- djstripe.signals.webhook_post_validate\n" "- djstripe.signals.webhook_pre_process\n" "- djstripe.signals.webhook_post_process\n" "- djstripe.signals.webhook_processing_error" ), ) ) sig = signature(callable) signature_sz = len(sig.parameters.keys()) if signature_sz < 2: messages.append( checks.Error( f"{callable} accepts {signature_sz} arguments.", hint="You may have forgotten to add api_key parameter to your custom callback.", id="djstripe.E004", ) ) return messages ================================================ FILE: djstripe/enums.py ================================================ import operator from collections import OrderedDict from django.utils.translation import gettext_lazy as _ class EnumMetaClass(type): def __init__(cls, name, bases, classdict): def _human_enum_values(enum): return cls.__choices__[enum] # add a class attribute cls.humanize = _human_enum_values @classmethod def __prepare__(cls, name, bases): return OrderedDict() def __new__(cls, name, bases, classdict): members = [] keys = {} choices = OrderedDict() for key, value in classdict.items(): if key.startswith("__"): continue members.append(key) if isinstance(value, tuple): value, alias = value keys[alias] = key else: alias = None keys[alias or key] = key choices[alias or key] = value for k, v in keys.items(): classdict[v] = k classdict["__choices__"] = choices classdict["__members__"] = members # Note: Differences between Python 2.x and Python 3.x force us to # explicitly use unicode here, and to explicitly sort the list. In # Python 2.x, class members are unordered and so the ordering will # vary on different systems based on internal hashing. Without this # Django will continually require new no-op migrations. classdict["choices"] = tuple( (str(k), str(v)) for k, v in sorted(choices.items(), key=operator.itemgetter(0)) ) return type.__new__(cls, name, bases, classdict) class Enum(metaclass=EnumMetaClass): pass class APIKeyType(Enum): """ API Key Types (internal model only) """ publishable = _("Publishable key") secret = _("Secret key") restricted = _("Restricted key") class ApiErrorCode(Enum): """ Charge failure error codes. https://stripe.com/docs/error-codes """ account_already_exists = _("Account already exists") account_country_invalid_address = _("Account country invalid address") account_invalid = _("Account invalid") account_number_invalid = _("Account number invalid") alipay_upgrade_required = _("Alipay upgrade required") amount_too_large = _("Amount too large") amount_too_small = _("Amount too small") api_key_expired = _("Api key expired") balance_insufficient = _("Balance insufficient") bank_account_exists = _("Bank account exists") bank_account_unusable = _("Bank account unusable") bank_account_unverified = _("Bank account unverified") bitcoin_upgrade_required = _("Bitcoin upgrade required") card_declined = _("Card was declined") charge_already_captured = _("Charge already captured") charge_already_refunded = _("Charge already refunded") charge_disputed = _("Charge disputed") charge_exceeds_source_limit = _("Charge exceeds source limit") charge_expired_for_capture = _("Charge expired for capture") country_unsupported = _("Country unsupported") coupon_expired = _("Coupon expired") customer_max_subscriptions = _("Customer max subscriptions") email_invalid = _("Email invalid") expired_card = _("Expired card") idempotency_key_in_use = _("Idempotency key in use") incorrect_address = _("Incorrect address") incorrect_cvc = _("Incorrect security code") incorrect_number = _("Incorrect number") incorrect_zip = _("ZIP code failed validation") instant_payouts_unsupported = _("Instant payouts unsupported") invalid_card_type = _("Invalid card type") invalid_charge_amount = _("Invalid charge amount") invalid_cvc = _("Invalid security code") invalid_expiry_month = _("Invalid expiration month") invalid_expiry_year = _("Invalid expiration year") invalid_number = _("Invalid number") invalid_source_usage = _("Invalid source usage") invoice_no_customer_line_items = _("Invoice no customer line items") invoice_no_subscription_line_items = _("Invoice no subscription line items") invoice_not_editable = _("Invoice not editable") invoice_upcoming_none = _("Invoice upcoming none") livemode_mismatch = _("Livemode mismatch") missing = _("No card being charged") not_allowed_on_standard_account = _("Not allowed on standard account") order_creation_failed = _("Order creation failed") order_required_settings = _("Order required settings") order_status_invalid = _("Order status invalid") order_upstream_timeout = _("Order upstream timeout") out_of_inventory = _("Out of inventory") parameter_invalid_empty = _("Parameter invalid empty") parameter_invalid_integer = _("Parameter invalid integer") parameter_invalid_string_blank = _("Parameter invalid string blank") parameter_invalid_string_empty = _("Parameter invalid string empty") parameter_missing = _("Parameter missing") parameter_unknown = _("Parameter unknown") parameters_exclusive = _("Parameters exclusive") payment_intent_authentication_failure = _("Payment intent authentication failure") payment_intent_incompatible_payment_method = _( "Payment intent incompatible payment method" ) payment_intent_invalid_parameter = _("Payment intent invalid parameter") payment_intent_payment_attempt_failed = _("Payment intent payment attempt failed") payment_intent_unexpected_state = _("Payment intent unexpected state") payment_method_unactivated = _("Payment method unactivated") payment_method_unexpected_state = _("Payment method unexpected state") payouts_not_allowed = _("Payouts not allowed") platform_api_key_expired = _("Platform api key expired") postal_code_invalid = _("Postal code invalid") processing_error = _("Processing error") product_inactive = _("Product inactive") rate_limit = _("Rate limit") resource_already_exists = _("Resource already exists") resource_missing = _("Resource missing") routing_number_invalid = _("Routing number invalid") secret_key_required = _("Secret key required") sepa_unsupported_account = _("SEPA unsupported account") shipping_calculation_failed = _("Shipping calculation failed") sku_inactive = _("SKU inactive") state_unsupported = _("State unsupported") tax_id_invalid = _("Tax id invalid") taxes_calculation_failed = _("Taxes calculation failed") testmode_charges_only = _("Testmode charges only") tls_version_unsupported = _("TLS version unsupported") token_already_used = _("Token already used") token_in_use = _("Token in use") transfers_not_allowed = _("Transfers not allowed") upstream_order_creation_failed = _("Upstream order creation failed") url_invalid = _("URL invalid") # deprecated invalid_swipe_data = _("Invalid swipe data") class AccountType(Enum): standard = _("Standard") express = _("Express") custom = _("Custom") class BalanceTransactionReportingCategory(Enum): """ https://stripe.com/docs/reports/reporting-categories """ advance = _("Advance") advance_funding = _("Advance funding") anticipation_repayment = _("Anticipation loan repayment (BR)") charge = _("Charge") charge_failure = _("Charge failure") connect_collection_transfer = _("Stripe Connect collection transfer") connect_reserved_funds = _("Stripe Connect reserved funds") dispute = _("Dispute") dispute_reversal = _("Dispute reversal") fee = _("Stripe fee") issuing_authorization_hold = _("Issuing authorization hold") issuing_authorization_release = _("Issuing authorization release") issuing_dispute = _("Issuing dispute") issuing_transaction = _("Issuing transaction") other_adjustment = _("Other adjustment") partial_capture_reversal = _("Partial capture reversal") payout = _("Payout") payout_reversal = _("Payout reversal") platform_earning = _("Stripe Connect platform earning") platform_earning_refund = _("Stripe Connect platform earning refund") refund = _("Refund") refund_failure = _("Refund failure") risk_reserved_funds = _("Risk-reserved funds") tax = _("Tax") topup = _("Top-up") topup_reversal = _("Top-up reversal") transfer = _("Stripe Connect transfer") transfer_reversal = _("Stripe Connect transfer reversal") class BalanceTransactionStatus(Enum): available = _("Available") pending = _("Pending") class BalanceTransactionType(Enum): # https://stripe.com/docs/reports/balance-transaction-types adjustment = _("Adjustment") advance = _("Advance") advance_funding = _("Advance funding") anticipation_repayment = _("Anticipation loan repayment") application_fee = _("Application fee") application_fee_refund = _("Application fee refund") balance_transfer_inbound = _("Balance transfer (inbound)") balance_transfer_outbound = _("Balance transfer (outbound)") charge = _("Charge") connect_collection_transfer = _("Connect collection transfer") contribution = _("Charitable contribution") issuing_authorization_hold = _("Issuing authorization hold") issuing_authorization_release = _("Issuing authorization release") issuing_dispute = _("Issuing dispute") issuing_transaction = _("Issuing transaction") network_cost = _("Network cost") payment = _("Payment") payment_failure_refund = _("Payment failure refund") payment_refund = _("Payment refund") payout = _("Payout") payout_cancel = _("Payout cancellation") payout_failure = _("Payout failure") refund = _("Refund") refund_failure = _("Refund failure") reserve_transaction = _("Reserve transaction") reserved_funds = _("Reserved funds") stripe_fee = _("Stripe fee") stripe_fx_fee = _("Stripe currency conversion fee") tax_fee = _("Tax fee") topup = _("Topup") topup_reversal = _("Topup reversal") transfer = _("Transfer") transfer_cancel = _("Transfer cancel") transfer_failure = _("Transfer failure") transfer_refund = _("Transfer refund") validation = _("Validation") class BankAccountHolderType(Enum): individual = _("Individual") company = _("Company") class BankAccountStatus(Enum): new = _("New") validated = _("Validated") verified = _("Verified") verification_failed = _("Verification failed") errored = _("Errored") class BillingScheme(Enum): per_unit = _("Per-unit") tiered = _("Tiered") class BusinessType(Enum): individual = _("Individual") company = _("Company") non_profit = _("Non Profit") government_entity = _("Government Entity") class CaptureMethod(Enum): automatic = _("Automatic") manual = _("Manual") class CardCheckResult(Enum): pass_ = (_("Pass"), "pass") fail = _("Fail") unavailable = _("Unavailable") unchecked = _("Unchecked") class CardBrand(Enum): AmericanExpress = (_("American Express"), "American Express") DinersClub = (_("Diners Club"), "Diners Club") Discover = _("Discover") JCB = _("JCB") MasterCard = _("MasterCard") UnionPay = _("UnionPay") Visa = _("Visa") Unknown = _("Unknown") class CardFundingType(Enum): credit = _("Credit") debit = _("Debit") prepaid = _("Prepaid") unknown = _("Unknown") class CardTokenizationMethod(Enum): apple_pay = _("Apple Pay") android_pay = _("Android Pay") class ChargeStatus(Enum): succeeded = _("Succeeded") pending = _("Pending") failed = _("Failed") class ConfirmationMethod(Enum): automatic = _("Automatic") manual = _("Manual") class CouponDuration(Enum): once = _("Once") repeating = _("Multi-month") forever = _("Forever") class CustomerTaxExempt(Enum): none = _("None") exempt = _("Exempt") reverse = _("Reverse") class DisputeReason(Enum): duplicate = _("Duplicate") fraudulent = _("Fraudulent") subscription_canceled = _("Subscription canceled") product_unacceptable = _("Product unacceptable") product_not_received = _("Product not received") unrecognized = _("Unrecognized") credit_not_processed = _("Credit not processed") general = _("General") incorrect_account_details = _("Incorrect account details") insufficient_funds = _("Insufficient funds") bank_cannot_process = _("Bank cannot process") debit_not_authorized = _("Debit not authorized") customer_initiated = _("Customer-initiated") class DisputeStatus(Enum): warning_needs_response = _("Warning needs response") warning_under_review = _("Warning under review") warning_closed = _("Warning closed") needs_response = _("Needs response") under_review = _("Under review") charge_refunded = _("Charge refunded") won = _("Won") lost = _("Lost") class FilePurpose(Enum): account_requirement = _("Account requirement") additional_verification = _("Additional verification") business_icon = _("Business icon") business_logo = _("Business logo") customer_signature = _("Customer signature") credit_note = _("Credit Note") dispute_evidence = _("Dispute evidence") document_provider_identity_document = _("Document provider identity document") finance_report_run = _("Finance report run") identity_document = _("Identity document") identity_document_downloadable = _("Identity document (downloadable)") invoice_statement = _("Invoice statement") pci_document = _("PCI document") selfie = _("Selfie (Stripe Identity)") sigma_scheduled_query = _("Sigma scheduled query") tax_document_user_upload = _("Tax document user upload") class FileType(Enum): pdf = _("PDF") jpg = _("JPG") png = _("PNG") csv = _("CSV") xls = _("XLS") xlsx = _("XLSX") docx = _("DOCX") class InvoiceBillingReason(Enum): subscription_cycle = _("Subscription cycle") subscription_create = _("Subscription create") subscription_update = _("Subscription update") subscription = _("Subscription") manual = _("Manual") upcoming = _("Upcoming") subscription_threshold = _("Subscription threshold") automatic_pending_invoice_item_invoice = _("Automatic pending invoice item invoice") class InvoiceCollectionMethod(Enum): charge_automatically = _("Charge automatically") send_invoice = _("Send invoice") class InvoiceStatus(Enum): draft = _("Draft") open = _("Open") paid = _("Paid") uncollectible = _("Uncollectible") void = _("Void") class InvoiceorLineItemType(Enum): invoice_item = _("Invoice Item") line_item = _("Line Item") unsupported = _("Unsupported") class IntentUsage(Enum): on_session = _("On session") off_session = _("Off session") class IntentStatus(Enum): """ Status of Intents which apply both to PaymentIntents and SetupIntents. """ requires_payment_method = _( "Intent created and requires a Payment Method to be attached." ) requires_confirmation = _("Intent is ready to be confirmed.") requires_action = _("Payment Method require additional action, such as 3D secure.") processing = _("Required actions have been handled.") canceled = _( "Cancellation invalidates the intent for future confirmation and " "cannot be undone." ) class LineItem(Enum): invoiceitem = _("Invoice Item") subscription = _("Subscription") class MandateStatus(Enum): active = _("Active") inactive = _("Inactive") pending = _("Pending") class MandateType(Enum): multi_use = _("Multi-use") single_use = _("Single-use") class OrderStatus(Enum): open = _("Open") submitted = _("Submitted") processing = _("Processing") complete = _("Complete") canceled = _("Canceled") # TODO - maybe refactor Enum so that inheritance works, # then PaymentIntentStatus/SetupIntentStatus can inherit from IntentStatus class PaymentIntentStatus(Enum): requires_payment_method = _( "Intent created and requires a Payment Method to be attached." ) requires_confirmation = _("Intent is ready to be confirmed.") requires_action = _("Payment Method require additional action, such as 3D secure.") processing = _("Required actions have been handled.") requires_capture = _("Capture the funds on the cards which have been put on holds.") canceled = _( "Cancellation invalidates the intent for future confirmation and " "cannot be undone." ) succeeded = _("The funds are in your account.") class SetupIntentStatus(Enum): requires_payment_method = _( "Intent created and requires a Payment Method to be attached." ) requires_confirmation = _("Intent is ready to be confirmed.") requires_action = _("Payment Method require additional action, such as 3D secure.") processing = _("Required actions have been handled.") canceled = _( "Cancellation invalidates the intent for future confirmation and " "cannot be undone." ) succeeded = _( "Setup was successful and the payment method is optimized for future payments." ) class PaymentMethodType(Enum): acss_debit = _("Acss Dbit") affirm = _("Affirm") afterpay_clearpay = _("Afterpay Clearpay") alipay = _("Alipay") au_becs_debit = _("BECS Debit (Australia)") bacs_debit = _("Bacs Direct Debit") bancontact = _("Bancontact") blik = _("BLIK") boleto = _("Boleto") card = _("Card") card_present = _("Card present") customer_balance = _("Customer Balance") eps = _("EPS") fpx = _("FPX") giropay = _("Giropay") grabpay = _("Grabpay") ideal = _("iDEAL") interac_present = _("Interac (card present)") klarna = _("Klarna") konbini = _("Konbini") link = _("Link") oxxo = _("OXXO") p24 = _("Przelewy24") paynow = _("PayNow") pix = _("Pix") promptpay = _("PromptPay") sepa_debit = _("SEPA Direct Debit") sofort = _("SOFORT") us_bank_account = _("ACH Direct Debit") wechat_pay = _("Wechat Pay") class PayoutFailureCode(Enum): """ Payout failure error codes. https://stripe.com/docs/api#payout_failures """ account_closed = _("Bank account has been closed.") account_frozen = _("Bank account has been frozen.") bank_account_restricted = _("Bank account has restrictions on payouts allowed.") bank_ownership_changed = _("Destination bank account has changed ownership.") could_not_process = _("Bank could not process payout.") debit_not_authorized = _("Debit transactions not approved on the bank account.") declined = _( "The bank has declined this transfer. Please contact the bank before retrying." ) insufficient_funds = _("Stripe account has insufficient funds.") invalid_account_number = _("Invalid account number") incorrect_account_holder_name = _( "Your bank notified us that the bank account holder name on file is incorrect." ) incorrect_account_holder_address = _( "Your bank notified us that the bank account holder address on file is incorrect." ) incorrect_account_holder_tax_id = _( "Your bank notified us that the bank account holder tax ID on file is incorrect." ) invalid_currency = _("Bank account does not support currency.") no_account = _("Bank account could not be located.") unsupported_card = _("Card no longer supported.") class PayoutMethod(Enum): standard = _("Standard") instant = _("Instant") class PayoutSourceType(Enum): bank_account = _("Bank account") fpx = _("Financial Process Exchange (FPX)") card = _("Card") class PayoutStatus(Enum): paid = _("Paid") pending = _("Pending") in_transit = _("In transit") canceled = _("Canceled") failed = _("Failed") class PayoutType(Enum): bank_account = _("Bank account") card = _("Card") class PaymentIntentCancellationReason(Enum): # see also SetupIntentCancellationReason # User provided reasons: duplicate = _("Duplicate") fraudulent = _("Fraudulent") abandoned = _("Abandoned") requested_by_customer = _("Requested by Customer") # Reasons generated by Stripe internally failed_invoice = _("Failed invoice") void_invoice = _("Void invoice") automatic = _("Automatic") class PlanAggregateUsage(Enum): last_during_period = _("Last during period") last_ever = _("Last ever") max = _("Max") sum = _("Sum") class PlanInterval(Enum): day = _("Day") week = _("Week") month = _("Month") year = _("Year") class PriceTiersMode(Enum): graduated = _("Graduated") volume = _("Volume-based") class PriceType(Enum): one_time = _("One-time") recurring = _("Recurring") class PriceUsageType(Enum): metered = _("Metered") licensed = _("Licensed") # Legacy PlanTiersMode = PriceTiersMode PlanUsageType = PriceUsageType class ProductType(Enum): good = _("Good") service = _("Service") class SetupIntentCancellationReason(Enum): # see also PaymentIntentCancellationReason abandoned = _("Abandoned") requested_by_customer = _("Requested by Customer") duplicate = _("Duplicate") class ScheduledQueryRunStatus(Enum): canceled = _("Canceled") failed = _("Failed") timed_out = _("Timed out") class SourceFlow(Enum): redirect = _("Redirect") receiver = _("Receiver") code_verification = _("Code verification") none = _("None") class SourceStatus(Enum): canceled = _("Canceled") chargeable = _("Chargeable") consumed = _("Consumed") failed = _("Failed") pending = _("Pending") class SourceType(Enum): ach_credit_transfer = _("ACH Credit Transfer") ach_debit = _("ACH Debit") acss_debit = _("ACSS Debit") alipay = _("Alipay") au_becs_debit = _("BECS Debit (AU)") bancontact = _("Bancontact") bitcoin = _("Bitcoin (Legacy)") card = _("Card") card_present = _("Card present") eps = _("EPS") giropay = _("Giropay") ideal = _("iDEAL") klarna = _("Klarna") multibanco = _("Multibanco") p24 = _("P24") paper_check = _("Paper check") sepa_credit_transfer = _("SEPA credit transfer") sepa_debit = _("SEPA Direct Debit") sofort = _("SOFORT") three_d_secure = _("3D Secure") wechat = _("WeChat") class LegacySourceType(Enum): card = _("Card") bank_account = _("Bank account") bitcoin_receiver = _("Bitcoin receiver") alipay_account = _("Alipay account") class RefundFailureReason(Enum): lost_or_stolen_card = _("Lost or stolen card") expired_or_canceled_card = _("Expired or canceled card") unknown = _("Unknown") class RefundReason(Enum): duplicate = _("Duplicate charge") fraudulent = _("Fraudulent") requested_by_customer = _("Requested by customer") expired_uncaptured_charge = _("Expired uncaptured charge") class RefundStatus(Enum): pending = _("Pending") succeeded = _("Succeeded") failed = _("Failed") canceled = _("Canceled") class SessionBillingAddressCollection(Enum): auto = _("Auto") required = _("Required") class SessionMode(Enum): payment = _("Payment") setup = _("Setup") subscription = _("Subscription") class SourceUsage(Enum): reusable = _("Reusable") single_use = _("Single-use") class SourceCodeVerificationStatus(Enum): pending = _("Pending") succeeded = _("Succeeded") failed = _("Failed") class SourceRedirectFailureReason(Enum): user_abort = _("User-aborted") declined = _("Declined") processing_error = _("Processing error") class SourceRedirectStatus(Enum): pending = _("Pending") succeeded = _("Succeeded") not_required = _("Not required") failed = _("Failed") class SubmitTypeStatus(Enum): auto = _("Auto") book = _("Book") donate = _("donate") pay = _("pay") class SubscriptionScheduleEndBehavior(Enum): release = _("Release") cancel = _("Cancel") class SubscriptionScheduleStatus(Enum): not_started = _("Not started") active = _("Active") completed = _("Completed") released = _("Released") canceled = _("Canceled") class SubscriptionStatus(Enum): incomplete = _("Incomplete") incomplete_expired = _("Incomplete Expired") trialing = _("Trialing") active = _("Active") past_due = _("Past due") canceled = _("Canceled") unpaid = _("Unpaid") class SubscriptionProrationBehavior(Enum): create_prorations = _("Create prorations") always_invoice = _("Always invoice") none = _("None") class ShippingRateType(Enum): fixed_amount = _("Fixed Amount") class ShippingRateTaxBehavior(Enum): inclusive = _("Inclusive") exclusive = _("Exclusive") unspecified = _("Unspecified") class TaxIdType(Enum): ae_trn = _("AE TRN") au_abn = _("AU ABN") br_cnp = _("BR CNP") br_cpf = _("BR CPF") ca_bn = _("CA BN") ca_qst = _("CA QST") ch_vat = _("CH VAT") cl_tin = _("CL TIN") es_cif = _("ES CIF") eu_vat = _("EU VAT") hk_br = _("HK BR") id_npw = _("ID NPW") in_gst = _("IN GST") jp_cn = _("JP CN") jp_rn = _("JP RN") kr_brn = _("KR BRN") li_uid = _("LI UID") mx_rfc = _("MX RFC") my_frp = _("MY FRP") my_itn = _("MY ITN") my_sst = _("MY SST") no_vat = _("NO VAT") nz_gst = _("NZ GST") ru_inn = _("RU INN") ru_kpp = _("RU KPP") sa_vat = _("SA VAT") sg_gst = _("SG GST") sg_uen = _("SG UEN") th_vat = _("TH VAT") tw_vat = _("TW VAT") us_ein = _("US EIN") za_vat = _("ZA VAT") unknown = _("Unknown") class UsageAction(Enum): increment = _("increment") set = _("set") class WebhookEndpointStatus(Enum): enabled = _("enabled") disabled = _("disabled") class DjstripePaymentMethodType(Enum): """ A djstripe-specific enum for the DjStripePaymentMethod model. """ alipay_account = _("Alipay account") card = _("Card") bank_account = _("Bank account") source = _("Source") ================================================ FILE: djstripe/event_handlers.py ================================================ """ Webhook event handlers for the various models Stripe docs for Events: https://stripe.com/docs/api/events Stripe docs for Webhooks: https://stripe.com/docs/webhooks TODO: Implement webhook event handlers for all the models that need to respond to webhook events. NOTE: Event data is not guaranteed to be in the correct API version format. See #116. When writing a webhook handler, make sure to first re-retrieve the object you wish to process. """ import logging from enum import Enum from django.core.exceptions import ObjectDoesNotExist from djstripe.settings import djstripe_settings from . import models, webhooks from .enums import PayoutType, SourceType from .utils import convert_tstamp logger = logging.getLogger(__name__) def update_customer_helper(metadata, customer_id, subscriber_key): """ A helper function that updates customer's subscriber and metadata fields """ # only update customer.subscriber if both the customer and subscriber already exist subscriber_id = metadata.get(subscriber_key, "") if subscriber_key not in ("", None) and subscriber_id and customer_id: subscriber_model = djstripe_settings.get_subscriber_model() try: subscriber = subscriber_model.objects.get(id=subscriber_id) customer = models.Customer.objects.get(id=customer_id) except ObjectDoesNotExist: pass else: customer.subscriber = subscriber customer.metadata = metadata customer.save() @webhooks.handler("customer") def customer_webhook_handler(event): """Handle updates to customer objects. First determines the crud_type and then handles the event if a customer exists locally. As customers are tied to local users, djstripe will not create customers that do not already exist locally. And updates to the subscriber model and metadata fields of customer if present in checkout.sessions metadata key. Docs and an example customer webhook response: https://stripe.com/docs/api#customer_object """ # will recieve all events of the type customer.X.Y so # need to ensure the data object is related to Customer Object target_object_type = event.data.get("object", {}).get("object", {}) if event.customer and target_object_type == "customer": metadata = event.data.get("object", {}).get("metadata", {}) customer_id = event.data.get("object", {}).get("id", "") subscriber_key = djstripe_settings.SUBSCRIBER_CUSTOMER_KEY # only update customer.subscriber if both the customer and subscriber already exist update_customer_helper(metadata, customer_id, subscriber_key) _handle_crud_like_event(target_cls=models.Customer, event=event) @webhooks.handler("customer.discount") def customer_discount_webhook_handler(event): """Handle updates to customer discount objects. Docs: https://stripe.com/docs/api#discounts Because there is no concept of a "Discount" model in dj-stripe (due to the lack of a stripe id on them), this is a little different to the other handlers. """ crud_type = CrudType.determine(event=event) discount_data = event.data.get("object", {}) coupon_data = discount_data.get("coupon", {}) customer = event.customer if crud_type is CrudType.DELETED: coupon = None coupon_start = None coupon_end = None else: coupon = _handle_crud_like_event( target_cls=models.Coupon, event=event, data=coupon_data, id=coupon_data.get("id"), ) coupon_start = discount_data.get("start") coupon_end = discount_data.get("end") customer.coupon = coupon customer.coupon_start = convert_tstamp(coupon_start) customer.coupon_end = convert_tstamp(coupon_end) customer.save() @webhooks.handler("customer.source") def customer_source_webhook_handler(event): """Handle updates to customer payment-source objects. Docs: https://stripe.com/docs/api/sources """ customer_data = event.data.get("object", {}) source_type = customer_data.get("object", {}) # TODO: handle other types of sources # (https://stripe.com/docs/api/sources) if source_type == SourceType.card: if event.verb.endswith("deleted") and customer_data: # On customer.source.deleted, we do not delete the object, # we merely unlink it. # customer = Customer.objects.get(id=customer_data["id"]) # NOTE: for now, customer.sources still points to Card # Also, https://github.com/dj-stripe/dj-stripe/issues/576 models.Card.objects.filter(id=customer_data.get("id", "")).delete() models.DjstripePaymentMethod.objects.filter( id=customer_data.get("id", "") ).delete() else: _handle_crud_like_event(target_cls=models.Card, event=event) @webhooks.handler("customer.subscription") def customer_subscription_webhook_handler(event): """Handle updates to customer subscription objects. Docs an example subscription webhook response: https://stripe.com/docs/api#subscription_object """ # customer.subscription.deleted doesn't actually delete the subscription # on the stripe side, it updates it to canceled status, so override # crud_type to update to match. crud_type = CrudType.determine(event=event) if crud_type is CrudType.DELETED: crud_type = CrudType.UPDATED _handle_crud_like_event( target_cls=models.Subscription, event=event, crud_type=crud_type ) @webhooks.handler("customer.tax_id") def customer_tax_id_webhook_handler(event): """ Handle updates to customer tax ID objects. """ _handle_crud_like_event( target_cls=models.TaxId, event=event, crud_type=CrudType.determine(event=event) ) @webhooks.handler("payment_method") def payment_method_handler(event): """ Handle updates to payment_method objects :param event: :return: Docs for: - payment_method: https://stripe.com/docs/api/payment_methods """ # will recieve all events of the type payment_method.X.Y so # need to ensure the data object is related to PaymentMethod Object target_object_type = event.data.get("object", {}).get("object", {}) if target_object_type == "payment_method": id_ = event.data.get("object", {}).get("id", None) if ( event.parts == ["payment_method", "detached"] and id_ and id_.startswith("card_") ): # Special case to handle a quirk in stripe's wrapping of legacy "card" objects # with payment_methods - card objects are deleted on detach, so treat this as # a delete event _handle_crud_like_event( target_cls=models.PaymentMethod, event=event, crud_type=CrudType.DELETED, ) else: _handle_crud_like_event(target_cls=models.PaymentMethod, event=event) @webhooks.handler("account.external_account") def account_application_webhook_handler(event): """ Handles updates to Connected Accounts External Accounts """ source_type = event.data.get("object", {}).get("object") if source_type == PayoutType.card: _handle_crud_like_event(target_cls=models.Card, event=event) if source_type == PayoutType.bank_account: _handle_crud_like_event(target_cls=models.BankAccount, event=event) @webhooks.handler("account.updated") def account_updated_webhook_handler(event): """ Handles updates to Connected Accounts - account: https://stripe.com/docs/api/accounts """ _handle_crud_like_event( target_cls=models.Account, event=event, crud_type=CrudType.UPDATED, ) @webhooks.handler("charge") def charge_webhook_handler(event): """Handle updates to Charge objects - charge: https://stripe.com/docs/api/charges """ # will recieve all events of the type charge.X.Y so # need to ensure the data object is related to Charge Object target_object_type = event.data.get("object", {}).get("object", {}) if target_object_type == "charge": _handle_crud_like_event(target_cls=models.Charge, event=event) @webhooks.handler("charge.dispute") def dispute_webhook_handler(event): """Handle updates to Dispute objects - dispute: https://stripe.com/docs/api/disputes """ # will recieve all events of the type charge.dispute.Y so # need to ensure the data object is related to Dispute Object target_object_type = event.data.get("object", {}).get("object", {}) if target_object_type == "dispute": _handle_crud_like_event(target_cls=models.Dispute, event=event) @webhooks.handler( "checkout", "coupon", "file", "invoice", "invoiceitem", "order", "payment_intent", "payout", "plan", "price", "product", "setup_intent", "subscription_schedule", "source", "tax_rate", "transfer", ) def other_object_webhook_handler(event): """ Handle updates to checkout, coupon, file, invoice, invoiceitem, payment_intent, plan, product, setup_intent, subscription_schedule, source, tax_rate and transfer objects. Docs for: - checkout: https://stripe.com/docs/api/checkout/sessions - coupon: https://stripe.com/docs/api/coupons - file: https://stripe.com/docs/api/files - invoice: https://stripe.com/docs/api/invoices - invoiceitem: https://stripe.com/docs/api/invoiceitems - order: https://stripe.com/docs/api/orders_v2 - payment_intent: https://stripe.com/docs/api/payment_intents - payout: https://stripe.com/docs/api/payouts - plan: https://stripe.com/docs/api/plans - price: https://stripe.com/docs/api/prices - product: https://stripe.com/docs/api/products - setup_intent: https://stripe.com/docs/api/setup_intents - subscription_schedule: https://stripe.com/docs/api/subscription_schedules - source: https://stripe.com/docs/api/sources - tax_rate: https://stripe.com/docs/api/tax_rates/ - transfer: https://stripe.com/docs/api/transfers """ target_cls = { "checkout": models.Session, "coupon": models.Coupon, "file": models.File, "invoice": models.Invoice, "invoiceitem": models.InvoiceItem, "order": models.Order, "payment_intent": models.PaymentIntent, "payout": models.Payout, "plan": models.Plan, "price": models.Price, "product": models.Product, "transfer": models.Transfer, "setup_intent": models.SetupIntent, "subscription_schedule": models.SubscriptionSchedule, "source": models.Source, "tax_rate": models.TaxRate, }.get(event.category) _handle_crud_like_event(target_cls=target_cls, event=event) # # Helpers # class CrudType(Enum): """Helper object to determine CRUD-like event state.""" UPDATED = "updated" DELETED = "deleted" @classmethod def determine(cls, event, verb=None): """ Determine if the event verb is a crud_type (without the 'R') event. :param event: :type event: models.Event :param verb: The event verb to examine. :type verb: str :returns: The CrudType state object. :rtype: CrudType """ verb = verb or event.verb for enum in CrudType: if verb.endswith(enum.value): return enum # in case nothing matches return def _handle_crud_like_event( target_cls, event: "models.Event", data=None, id: str = None, crud_type=None ): """ Helper to process crud_type-like events for objects. Non-deletes (creates, updates and "anything else" events) are treated as update_or_create events - The object will be retrieved locally, then it is synchronised with the Stripe API for parity. Deletes only occur for delete events and cause the object to be deleted from the local database, if it existed. If it doesn't exist then it is ignored (but the event processing still succeeds). :param target_cls: The djstripe model being handled. :type target_cls: Type[models.StripeModel] :param event: The event object :param data: The event object data (defaults to ``event.data``). :param id: The object Stripe ID (defaults to ``object.id``). :param crud_type: The CrudType object (determined by default). :returns: The object (if any) and the event CrudType. :rtype: Tuple[models.StripeModel, CrudType] """ data = data or event.data id = id or data.get("object", {}).get("id", None) stripe_account = getattr(event.djstripe_owner_account, "id", None) if not id: # We require an object when applying CRUD-like events, so if there's # no ID the event is ignored/dropped. This happens in events such as # invoice.upcoming, which refer to a future (non-existant) invoice. logger.debug("Ignoring Stripe event %r without object ID", event.id) return crud_type = crud_type or CrudType.determine(event=event, verb=event.verb) if crud_type is CrudType.DELETED: qs = target_cls.objects.filter(id=id) if target_cls is models.Customer and qs.exists(): qs.get().purge() obj = None else: obj = target_cls.objects.filter(id=id).delete() else: # Any other event type (creates, updates, etc.) - This can apply to # verbs that aren't strictly CRUD but Stripe do intend an update. Such # as invoice.payment_failed. kwargs = {"id": id} if hasattr(target_cls, "customer"): kwargs["customer"] = event.customer # For account.external_account.* events if event.parts[:2] == ["account", "external_account"] and stripe_account: kwargs["account"] = models.Account._get_or_retrieve(id=stripe_account) # Stripe doesn't allow retrieval of Discount Objects if target_cls not in (models.Discount,): data = target_cls(**kwargs).api_retrieve( stripe_account=stripe_account, api_key=event.default_api_key ) else: data = data.get("object") # create or update the object from the retrieved Stripe Data obj = target_cls.sync_from_stripe_data(data, api_key=event.default_api_key) return obj ================================================ FILE: djstripe/exceptions.py ================================================ """ dj-stripe Exceptions. """ class MultipleSubscriptionException(Exception): """Raised when a Customer has multiple Subscriptions and only one is expected.""" pass class StripeObjectManipulationException(Exception): """ Raised when an attempt to manipulate a non-standalone stripe object is made not through its parent object. """ pass class InvalidStripeAPIKey(ValueError): """ Raised when a clearly-invalid Stripe API key is used. """ pass class ImpossibleAPIRequest(Exception): """ Raised when dj-stripe attempts to make an impossible API request """ pass ================================================ FILE: djstripe/fields.py ================================================ """ dj-stripe Custom Field Definitions """ import decimal from django.conf import SettingsReference, settings from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import JSONField as BaseJSONField from .utils import convert_tstamp class FieldDeconstructMixin: IGNORED_ATTRS = [ "verbose_name", "help_text", "choices", "get_latest_by", "ordering", ] def deconstruct(self): """Remove field attributes that have nothing to do with the database. Otherwise unencessary migrations are generated.""" name, path, args, kwargs = super().deconstruct() for attr in self.IGNORED_ATTRS: kwargs.pop(attr, None) return name, path, args, kwargs class StripeForeignKey(models.ForeignKey): setting_name = "DJSTRIPE_FOREIGN_KEY_TO_FIELD" def __init__(self, *args, **kwargs): # The default value will only come into play if the check for # that setting has been disabled. kwargs["to_field"] = getattr(settings, self.setting_name, "id") super().__init__(*args, **kwargs) def deconstruct(self): name, path, args, kwargs = super().deconstruct() kwargs["to_field"] = SettingsReference( getattr(settings, self.setting_name, "id"), self.setting_name ) return name, path, args, kwargs def get_default(self): # Override to bypass a weird bug in Django # https://stackoverflow.com/a/14390402/227443 if isinstance(self.remote_field.model, str): return self._get_default() return super().get_default() class PaymentMethodForeignKey(FieldDeconstructMixin, models.ForeignKey): def __init__(self, **kwargs): kwargs.setdefault("to", "DjstripePaymentMethod") super().__init__(**kwargs) class InvoiceOrLineItemForeignKey(models.ForeignKey): def __init__(self, **kwargs): kwargs.setdefault("to", "InvoiceOrLineItem") super().__init__(**kwargs) class StripePercentField(FieldDeconstructMixin, models.DecimalField): """A field used to define a percent according to djstripe logic.""" def __init__(self, *args, **kwargs): """Assign default args to this field.""" defaults = { "decimal_places": 2, "max_digits": 5, "validators": [MinValueValidator(1), MaxValueValidator(100)], } defaults.update(kwargs) super().__init__(*args, **defaults) class StripeCurrencyCodeField(FieldDeconstructMixin, models.CharField): """ A field used to store a three-letter currency code (eg. usd, eur, ...) """ def __init__(self, *args, **kwargs): defaults = {"max_length": 3, "help_text": "Three-letter ISO currency code"} defaults.update(kwargs) super().__init__(*args, **defaults) class StripeQuantumCurrencyAmountField(FieldDeconstructMixin, models.BigIntegerField): """ A field used to store currency amounts in cents (etc) as per stripe. By contacting stripe support, some accounts will have their limit raised to 11 digits, hence the use of BigIntegerField instead of IntegerField """ pass class StripeDecimalCurrencyAmountField(FieldDeconstructMixin, models.DecimalField): """ A legacy field to store currency amounts in dollars (etc). Stripe is always in cents. Historically djstripe stored everything in dollars. Note: Don't use this for new fields, use StripeQuantumCurrencyAmountField instead. We're planning on migrating existing fields in dj-stripe 3.0, see https://github.com/dj-stripe/dj-stripe/issues/955 """ def __init__(self, *args, **kwargs): """ Assign default args to this field. By contacting stripe support, some accounts will have their limit raised to 11 digits """ defaults = {"decimal_places": 2, "max_digits": 11} defaults.update(kwargs) super().__init__(*args, **defaults) def stripe_to_db(self, data): """Convert the raw value to decimal representation.""" val = data.get(self.name) # If already a string, it's decimal in the API (eg. Prices). if isinstance(val, str): return decimal.Decimal(val) # Note: 0 is a possible return value, which is 'falseish' if val is not None: return val / decimal.Decimal("100") class StripeEnumField(FieldDeconstructMixin, models.CharField): def __init__(self, enum, *args, **kwargs): self.enum = enum choices = enum.choices defaults = {"choices": choices, "max_length": max(len(k) for k, v in choices)} defaults.update(kwargs) super().__init__(*args, **defaults) def deconstruct(self): name, path, args, kwargs = super().deconstruct() kwargs["enum"] = self.enum return name, path, args, kwargs class StripeIdField(FieldDeconstructMixin, models.CharField): """A field with enough space to hold any stripe ID.""" def __init__(self, *args, **kwargs): """ Assign default args to this field. As per: https://stripe.com/docs/upgrades You can safely assume object IDs we generate will never exceed 255 characters, but you should be able to handle IDs of up to that length. """ defaults = {"max_length": 255, "blank": False, "null": False} defaults.update(kwargs) super().__init__(*args, **defaults) class StripeDateTimeField(FieldDeconstructMixin, models.DateTimeField): """A field used to define a DateTimeField value according to djstripe logic.""" def stripe_to_db(self, data): """Convert the raw timestamp value to a DateTime representation.""" val = data.get(self.name) # Note: 0 is a possible return value, which is 'falseish' if val is not None: return convert_tstamp(val) class JSONField(FieldDeconstructMixin, BaseJSONField): """A field used to define a JSONField value according to djstripe logic.""" pass ================================================ FILE: djstripe/locale/fr/LC_MESSAGES/django.po ================================================ msgid "" msgstr "" "Project-Id-Version: 1.3.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-01-28 12:02+1300\n" "Last-Translator: Jerome Leclanche \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: enums.py:57 msgid "Account already exists" msgstr "" #: enums.py:58 msgid "Account country invalid address" msgstr "" #: enums.py:59 msgid "Account invalid" msgstr "" #: enums.py:60 msgid "Account number invalid" msgstr "" #: enums.py:61 msgid "Alipay upgrade required" msgstr "" #: enums.py:62 msgid "Amount too large" msgstr "" #: enums.py:63 msgid "Amount too small" msgstr "" #: enums.py:64 msgid "Api key expired" msgstr "" #: enums.py:65 msgid "Balance insufficient" msgstr "" #: enums.py:66 #, fuzzy #| msgid "Bank account" msgid "Bank account exists" msgstr "Compte bancaire" #: enums.py:67 #, fuzzy #| msgid "Bank account" msgid "Bank account unusable" msgstr "Compte bancaire" #: enums.py:68 #, fuzzy #| msgid "Bank account" msgid "Bank account unverified" msgstr "Compte bancaire" #: enums.py:69 #, fuzzy #| msgid "Bitcoin receiver" msgid "Bitcoin upgrade required" msgstr "Récipient Bitcoin" #: enums.py:70 msgid "Card was declined" msgstr "Carte rejetée" #: enums.py:71 #, fuzzy #| msgid "Charge refunded" msgid "Charge already captured" msgstr "Charge remboursée" #: enums.py:72 #, fuzzy #| msgid "Charge refunded" msgid "Charge already refunded" msgstr "Charge remboursée" #: enums.py:73 #, fuzzy #| msgid "Charge refunded" msgid "Charge disputed" msgstr "Charge remboursée" #: enums.py:74 msgid "Charge exceeds source limit" msgstr "" #: enums.py:75 msgid "Charge expired for capture" msgstr "" #: enums.py:76 #, fuzzy #| msgid "Card no longer supported." msgid "Country unsupported" msgstr "La carte n'est plus supportée." #: enums.py:77 msgid "Coupon expired" msgstr "" #: enums.py:78 msgid "Customer max subscriptions" msgstr "" #: enums.py:79 msgid "Email invalid" msgstr "" #: enums.py:80 msgid "Expired card" msgstr "Carte expirée" #: enums.py:81 msgid "Idempotency key in use" msgstr "" #: enums.py:82 #, fuzzy #| msgid "Incorrect account details" msgid "Incorrect address" msgstr "Détails de compte incorrects" #: enums.py:83 msgid "Incorrect security code" msgstr "Code de sécurité incorrect" #: enums.py:84 msgid "Incorrect number" msgstr "Numéro incorrect" #: enums.py:85 msgid "ZIP code failed validation" msgstr "Validation du code postal échouée" #: enums.py:86 msgid "Instant payouts unsupported" msgstr "" #: enums.py:87 #, fuzzy #| msgid "Invalid security code" msgid "Invalid card type" msgstr "Code de sécurité invalide" #: enums.py:88 #, fuzzy #| msgid "Invalid expiration month" msgid "Invalid charge amount" msgstr "Mois d'expiration invalide" #: enums.py:89 msgid "Invalid security code" msgstr "Code de sécurité invalide" #: enums.py:90 msgid "Invalid expiration month" msgstr "Mois d'expiration invalide" #: enums.py:91 msgid "Invalid expiration year" msgstr "Année d'expiration invalide" #: enums.py:92 msgid "Invalid number" msgstr "Numéro invalide" #: enums.py:93 #, fuzzy #| msgid "Invalid security code" msgid "Invalid source usage" msgstr "Code de sécurité invalide" #: enums.py:94 msgid "Invoice no customer line items" msgstr "" #: enums.py:95 msgid "Invoice no subscription line items" msgstr "" #: enums.py:96 msgid "Invoice not editable" msgstr "" #: enums.py:97 msgid "Invoice upcoming none" msgstr "" #: enums.py:98 msgid "Livemode mismatch" msgstr "" #: enums.py:99 msgid "No card being charged" msgstr "Aucune carte chargée" #: enums.py:100 msgid "Not allowed on standard account" msgstr "" #: enums.py:101 #, fuzzy #| msgid "Verification failed" msgid "Order creation failed" msgstr "Vérification échouée" #: enums.py:102 msgid "Order required settings" msgstr "" #: enums.py:103 msgid "Order status invalid" msgstr "" #: enums.py:104 msgid "Order upstream timeout" msgstr "" #: enums.py:105 msgid "Out of inventory" msgstr "" #: enums.py:106 msgid "Parameter invalid empty" msgstr "" #: enums.py:107 msgid "Parameter invalid integer" msgstr "" #: enums.py:108 msgid "Parameter invalid string blank" msgstr "" #: enums.py:109 msgid "Parameter invalid string empty" msgstr "" #: enums.py:110 msgid "Parameter missing" msgstr "" #: enums.py:111 msgid "Parameter unknown" msgstr "" #: enums.py:112 msgid "Parameters exclusive" msgstr "" #: enums.py:113 msgid "Payment intent authentication failure" msgstr "" #: enums.py:114 msgid "Payment intent incompatible payment method" msgstr "" #: enums.py:115 msgid "Payment intent invalid parameter" msgstr "" #: enums.py:116 msgid "Payment intent payment attempt failed" msgstr "" #: enums.py:117 msgid "Payment intent unexpected state" msgstr "" #: enums.py:118 #, fuzzy #| msgid "Payment refund" msgid "Payment method unactivated" msgstr "Remboursement de paiment" #: enums.py:119 msgid "Payment method unexpected state" msgstr "" #: enums.py:120 #, fuzzy #| msgid "Payout failure" msgid "Payouts not allowed" msgstr "Échec de virement" #: enums.py:121 msgid "Platform api key expired" msgstr "" #: enums.py:122 msgid "Postal code invalid" msgstr "" #: enums.py:123 enums.py:433 msgid "Processing error" msgstr "Erreur de traitement" #: enums.py:124 #, fuzzy #| msgid "Product unacceptable" msgid "Product inactive" msgstr "Produit inacceptable" #: enums.py:125 msgid "Rate limit" msgstr "" #: enums.py:126 msgid "Resource already exists" msgstr "" #: enums.py:127 msgid "Resource missing" msgstr "" #: enums.py:128 msgid "Routing number invalid" msgstr "" #: enums.py:129 #, fuzzy #| msgid "Not required" msgid "Secret key required" msgstr "Non requis" #: enums.py:130 msgid "SEPA unsupported account" msgstr "" #: enums.py:131 #, fuzzy #| msgid "Verification failed" msgid "Shipping calculation failed" msgstr "Vérification échouée" #: enums.py:132 msgid "SKU inactive" msgstr "" #: enums.py:133 msgid "State unsupported" msgstr "" #: enums.py:134 msgid "Tax id invalid" msgstr "" #: enums.py:135 #, fuzzy #| msgid "Verification failed" msgid "Taxes calculation failed" msgstr "Vérification échouée" #: enums.py:136 msgid "Testmode charges only" msgstr "" #: enums.py:137 msgid "TLS version unsupported" msgstr "" #: enums.py:138 msgid "Token already used" msgstr "" #: enums.py:139 msgid "Token in use" msgstr "" #: enums.py:140 #, fuzzy #| msgid "Transfer refund" msgid "Transfers not allowed" msgstr "Remboursement de transfert" #: enums.py:141 #, fuzzy #| msgid "Verification failed" msgid "Upstream order creation failed" msgstr "Vérification échouée" #: enums.py:142 msgid "URL invalid" msgstr "" #: enums.py:145 msgid "Invalid swipe data" msgstr "Données swipe invalide" #: enums.py:149 enums.py:302 msgid "Standard" msgstr "Standard" #: enums.py:150 msgid "Express" msgstr "Express" #: enums.py:151 msgid "Custom" msgstr "Custom" #: enums.py:155 msgid "Available" msgstr "Disponible" #: enums.py:156 enums.py:223 enums.py:308 enums.py:371 enums.py:413 #: enums.py:425 enums.py:437 msgid "Pending" msgstr "En attente" #: enums.py:160 msgid "Adjustment" msgstr "Ajustement" #: enums.py:161 msgid "Application fee" msgstr "Frais d'application" #: enums.py:162 msgid "Application fee refund" msgstr "Remboursement de frais d'application" #: enums.py:163 msgid "Charge" msgstr "Charge" #: enums.py:164 msgid "Network cost" msgstr "Coût de réseau" #: enums.py:165 msgid "Payment" msgstr "Paiment" #: enums.py:166 msgid "Payment failure refund" msgstr "Remboursement d'échec de paiment" #: enums.py:167 msgid "Payment refund" msgstr "Remboursement de paiment" #: enums.py:168 msgid "Payout" msgstr "Virement" #: enums.py:169 msgid "Payout cancellation" msgstr "Annulation de virement" #: enums.py:170 msgid "Payout failure" msgstr "Échec de virement" #: enums.py:171 msgid "Refund" msgstr "Remboursement" #: enums.py:172 msgid "Stripe fee" msgstr "Frais Stripe" #: enums.py:173 msgid "Transfer" msgstr "Transfert" #: enums.py:174 msgid "Transfer refund" msgstr "Remboursement de transfert" #: enums.py:175 msgid "Validation" msgstr "Validation" #: enums.py:179 msgid "Individual" msgstr "Particulier" #: enums.py:180 msgid "Company" msgstr "Companie" #: enums.py:184 msgid "New" msgstr "Nouveau" #: enums.py:185 msgid "Validated" msgstr "Validé" #: enums.py:186 msgid "Verified" msgstr "Vérifié" #: enums.py:187 msgid "Verification failed" msgstr "Vérification échouée" #: enums.py:188 msgid "Errored" msgstr "Erreur" #: enums.py:192 msgid "Pass" msgstr "OK" #: enums.py:193 msgid "Fail" msgstr "Échoué" #: enums.py:194 msgid "Unavailable" msgstr "Indisponible" #: enums.py:195 msgid "Unchecked" msgstr "Non vérifié" #: enums.py:199 msgid "American Express" msgstr "American Express" #: enums.py:200 msgid "Diners Club" msgstr "Diners Club" #: enums.py:201 msgid "Discover" msgstr "Discover" #: enums.py:202 msgid "JCB" msgstr "JCB" #: enums.py:203 msgid "MasterCard" msgstr "masterCard" #: enums.py:204 msgid "UnionPay" msgstr "" #: enums.py:205 msgid "Visa" msgstr "Visa" #: enums.py:206 enums.py:213 enums.py:403 msgid "Unknown" msgstr "Inconnu" #: enums.py:210 msgid "Credit" msgstr "Crédit" #: enums.py:211 msgid "Debit" msgstr "Débit" #: enums.py:212 msgid "Prepaid" msgstr "Prépayée" #: enums.py:217 msgid "Apple Pay" msgstr "Apple Pay" #: enums.py:218 msgid "Android Pay" msgstr "Android Pay" #: enums.py:222 enums.py:414 enums.py:426 enums.py:438 msgid "Succeeded" msgstr "Réussi" #: enums.py:224 enums.py:311 enums.py:355 enums.py:370 enums.py:415 #: enums.py:427 enums.py:440 msgid "Failed" msgstr "Échoué" #: enums.py:228 msgid "Once" msgstr "Simple" #: enums.py:229 msgid "Multi-month" msgstr "Multi-mois" #: enums.py:230 msgid "Forever" msgstr "Illimité" #: enums.py:234 msgid "Duplicate" msgstr "Doublon" #: enums.py:235 enums.py:408 msgid "Fraudulent" msgstr "Frauduleux" #: enums.py:236 msgid "Subscription canceled" msgstr "Abonnement annulé" #: enums.py:237 msgid "Product unacceptable" msgstr "Produit inacceptable" #: enums.py:238 msgid "Product not received" msgstr "Produit non reçu" #: enums.py:239 msgid "Unrecognized" msgstr "Non reconnu" #: enums.py:240 msgid "Credit not processed" msgstr "Crédit non traité" #: enums.py:241 msgid "General" msgstr "Général" #: enums.py:242 msgid "Incorrect account details" msgstr "Détails de compte incorrects" #: enums.py:243 msgid "Insufficient funds" msgstr "Fonds insuffisants" #: enums.py:244 msgid "Bank cannot process" msgstr "Banque ne peut traiter" #: enums.py:245 msgid "Debit not authorized" msgstr "Débit interdit" #: enums.py:246 msgid "Customer-initiated" msgstr "Initié par le client" #: enums.py:250 msgid "Warning needs response" msgstr "Avertissement en attente de réponse" #: enums.py:251 msgid "Warning under review" msgstr "Avertissement en revue" #: enums.py:252 msgid "Warning closed" msgstr "Avertissement " #: enums.py:253 msgid "Needs response" msgstr "En attente de réponse" #: enums.py:254 msgid "Under review" msgstr "En revue" #: enums.py:255 msgid "Charge refunded" msgstr "Charge remboursée" #: enums.py:256 msgid "Won" msgstr "Gagnée" #: enums.py:257 msgid "Lost" msgstr "Perdue" #: enums.py:261 msgid "Dispute evidence" msgstr "Preuve de dispute" #: enums.py:262 msgid "Identity document" msgstr "Document d'identité" #: enums.py:263 msgid "Tax document user upload" msgstr "Document de taxes de l'utilisateur" #: enums.py:267 msgid "PDF" msgstr "PDF" #: enums.py:268 msgid "JPG" msgstr "JPG" #: enums.py:269 msgid "PNG" msgstr "PNG" #: enums.py:270 msgid "CSV" msgstr "CSV" #: enums.py:271 msgid "XLS" msgstr "XLS" #: enums.py:272 msgid "XLSX" msgstr "XLSX" #: enums.py:273 msgid "DOCX" msgstr "DOCX" #: enums.py:277 msgid "Charge automatically" msgstr "Débit automatique" #: enums.py:278 msgid "Send invoice" msgstr "Envoi de facture" #: enums.py:288 msgid "Bank account has been closed." msgstr "Compte bancaire fermé" #: enums.py:289 msgid "Bank account has been frozen." msgstr "Compte bancaire gelé" #: enums.py:290 msgid "Bank account has restrictions on payouts allowed." msgstr "Le compte bancaire a des restrictions sur les virements autorisés" #: enums.py:291 msgid "Destination bank account has changed ownership." msgstr "Le compte bancaire destinataire a changé de propriétaire." #: enums.py:292 msgid "Bank could not process payout." msgstr "Le compte bancaire n'a pas pu traiter le virement." #: enums.py:293 msgid "Debit transactions not approved on the bank account." msgstr "" "Les transactions de débit ne sont pas autorisées sur le compte bancaire." #: enums.py:294 msgid "Stripe account has insufficient funds." msgstr "Stripe n'a pas suffisamment de fonds." #: enums.py:295 msgid "Invalid account number" msgstr "Numéro de compte invalide" #: enums.py:296 msgid "Bank account does not support currency." msgstr "Le compte bancaire ne supporte pas la monnaie." #: enums.py:297 msgid "Bank account could not be located." msgstr "Le compte bancaire n'a pas été trouvé." #: enums.py:298 msgid "Card no longer supported." msgstr "La carte n'est plus supportée." #: enums.py:303 msgid "Instant" msgstr "Instant" #: enums.py:307 msgid "Paid" msgstr "Payé" #: enums.py:309 msgid "In transit" msgstr "En cours" #: enums.py:310 enums.py:354 enums.py:367 enums.py:416 enums.py:447 msgid "Canceled" msgstr "Annulé" #: enums.py:315 enums.py:395 enums.py:457 msgid "Bank account" msgstr "Compte bancaire" #: enums.py:316 enums.py:380 enums.py:394 enums.py:456 msgid "Card" msgstr "Carte" #: enums.py:320 msgid "Last during period" msgstr "Dernier cette période" #: enums.py:321 msgid "Last ever" msgstr "Dernier" #: enums.py:322 msgid "Max" msgstr "Max" #: enums.py:323 msgid "Sum" msgstr "Somme" #: enums.py:327 msgid "Per unit" msgstr "Par unité" #: enums.py:328 msgid "Tiered" msgstr "En tiers" #: enums.py:332 msgid "Day" msgstr "Jour" #: enums.py:333 msgid "Week" msgstr "Semaine" #: enums.py:334 msgid "Month" msgstr "Mois" #: enums.py:335 msgid "Year" msgstr "Année" #: enums.py:339 msgid "Metered" msgstr "Compté" #: enums.py:340 msgid "Licensed" msgstr "Sous license" #: enums.py:344 msgid "Graduated" msgstr "Gradué" #: enums.py:345 msgid "Volume-based" msgstr "Par volume" #: enums.py:349 msgid "Good" msgstr "Bien" #: enums.py:350 msgid "Service" msgstr "Service" #: enums.py:356 msgid "Timed out" msgstr "" #: enums.py:360 msgid "Redirect" msgstr "Redirection" #: enums.py:361 msgid "Receiver" msgstr "Récipient" #: enums.py:362 msgid "Code verification" msgstr "Vérification de code" #: enums.py:363 msgid "None" msgstr "Aucune" #: enums.py:368 msgid "Chargeable" msgstr "Prête a débiter" #: enums.py:369 msgid "Consumed" msgstr "Consommée" #: enums.py:375 msgid "ACH Credit Transfer" msgstr "Transfert crédit ACH" #: enums.py:376 msgid "ACH Debit" msgstr "Débit ACH" #: enums.py:377 msgid "Alipay" msgstr "Alipay" #: enums.py:378 msgid "Bancontact" msgstr "Bancontact" #: enums.py:379 msgid "Bitcoin" msgstr "Bitcoin" #: enums.py:381 msgid "Card present" msgstr "Carte présentée" #: enums.py:382 msgid "EPS" msgstr "EPS" #: enums.py:383 msgid "Giropay" msgstr "Giropay" #: enums.py:384 msgid "iDEAL" msgstr "iDEAL" #: enums.py:385 msgid "P24" msgstr "P24" #: enums.py:386 msgid "Paper check" msgstr "Cheque papier" #: enums.py:387 msgid "SEPA Direct Debit" msgstr "Débit direct SEPA" #: enums.py:388 msgid "SEPA credit transfer" msgstr "Transfert crédit SEPA" #: enums.py:389 msgid "SOFORT" msgstr "SOFORT" #: enums.py:390 msgid "3D Secure" msgstr "3D Secure" #: enums.py:396 msgid "Bitcoin receiver" msgstr "Récipient Bitcoin" #: enums.py:397 msgid "Alipay account" msgstr "Compte Alipay" #: enums.py:401 msgid "Lost or stolen card" msgstr "Carte perdue ou volée" #: enums.py:402 msgid "Expired or canceled card" msgstr "Carte expirée ou annulée" #: enums.py:407 msgid "Duplicate charge" msgstr "Charge double" #: enums.py:409 msgid "Requested by customer" msgstr "Demandé par le client" #: enums.py:420 msgid "Reusable" msgstr "Réutilisable" #: enums.py:421 msgid "Single-use" msgstr "Usage unique" #: enums.py:431 msgid "User-aborted" msgstr "Annulé par l'utilisateur" #: enums.py:432 msgid "Declined" msgstr "Rejeté" #: enums.py:439 msgid "Not required" msgstr "Non requis" #: enums.py:444 msgid "Trialing" msgstr "En période d'essai" #: enums.py:445 msgid "Active" msgstr "Active" #: enums.py:446 msgid "Past due" msgstr "Arriéré dû" #: enums.py:448 msgid "Unpaid" msgstr "Non payé" #: enums.py:458 msgid "Source" msgstr "Source" #: models/billing.py:863 msgid "day" msgstr "jour" #: models/billing.py:864 msgid "week" msgstr "semaine" #: models/billing.py:865 msgid "month" msgstr "mois" #: models/billing.py:866 msgid "year" msgstr "an" #: models/billing.py:868 #, python-brace-format msgid "{amount}/{interval}" msgstr "{amount}/{interval}" #: models/billing.py:871 msgid "days" msgstr "jours" #: models/billing.py:872 msgid "weeks" msgstr "semaines" #: models/billing.py:873 msgid "months" msgstr "mois" #: models/billing.py:874 msgid "years" msgstr "ans" #: models/billing.py:876 #, python-brace-format msgid "{amount} every {interval_count} {interval}" msgstr "{amount} tous les {interval_count} {interval}" #: templates/djstripe/admin/change_form.html:8 msgid "View on Stripe Dashboard" msgstr "Voir sur l'administration Stripe" ================================================ FILE: djstripe/locale/ru/LC_MESSAGES/django.po ================================================ msgid "" msgstr "" "Project-Id-Version: 1.3.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-01-28 12:02+1300\n" "Last-Translator: Kirill Shilimanov \n" "Language-Team: Russian \n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: enums.py:57 msgid "Account already exists" msgstr "" #: enums.py:58 msgid "Account country invalid address" msgstr "" #: enums.py:59 msgid "Account invalid" msgstr "" #: enums.py:60 msgid "Account number invalid" msgstr "" #: enums.py:61 msgid "Alipay upgrade required" msgstr "" #: enums.py:62 msgid "Amount too large" msgstr "" #: enums.py:63 msgid "Amount too small" msgstr "" #: enums.py:64 msgid "Api key expired" msgstr "" #: enums.py:65 msgid "Balance insufficient" msgstr "" #: enums.py:66 #, fuzzy #| msgid "Bank account" msgid "Bank account exists" msgstr "Банковский счет" #: enums.py:67 #, fuzzy #| msgid "Bank account" msgid "Bank account unusable" msgstr "Банковский счет" #: enums.py:68 #, fuzzy #| msgid "Bank account" msgid "Bank account unverified" msgstr "Банковский счет" #: enums.py:69 #, fuzzy #| msgid "Bitcoin receiver" msgid "Bitcoin upgrade required" msgstr "Получатель Bitcoin" #: enums.py:70 msgid "Card was declined" msgstr "Карта отклонена" #: enums.py:71 #, fuzzy #| msgid "Charge refunded" msgid "Charge already captured" msgstr "Платеж возвращен" #: enums.py:72 #, fuzzy #| msgid "Charge refunded" msgid "Charge already refunded" msgstr "Платеж возвращен" #: enums.py:73 #, fuzzy #| msgid "Charge refunded" msgid "Charge disputed" msgstr "Платеж возвращен" #: enums.py:74 msgid "Charge exceeds source limit" msgstr "" #: enums.py:75 msgid "Charge expired for capture" msgstr "" #: enums.py:76 #, fuzzy #| msgid "Card no longer supported." msgid "Country unsupported" msgstr "Карта не поддерживается." #: enums.py:77 msgid "Coupon expired" msgstr "" #: enums.py:78 msgid "Customer max subscriptions" msgstr "" #: enums.py:79 msgid "Email invalid" msgstr "" #: enums.py:80 msgid "Expired card" msgstr "Срок действия карты истек" #: enums.py:81 msgid "Idempotency key in use" msgstr "" #: enums.py:82 #, fuzzy #| msgid "Incorrect account details" msgid "Incorrect address" msgstr "Неверные данные счета" #: enums.py:83 msgid "Incorrect security code" msgstr "Неверный код безопасности" #: enums.py:84 msgid "Incorrect number" msgstr "Неверный номер" #: enums.py:85 msgid "ZIP code failed validation" msgstr "Ошибка проверки почтового индекса" #: enums.py:86 msgid "Instant payouts unsupported" msgstr "" #: enums.py:87 #, fuzzy #| msgid "Invalid security code" msgid "Invalid card type" msgstr "Недействительный код безопасности" #: enums.py:88 #, fuzzy #| msgid "Invalid expiration month" msgid "Invalid charge amount" msgstr "Неверный месяц истечения" #: enums.py:89 msgid "Invalid security code" msgstr "Недействительный код безопасности" #: enums.py:90 msgid "Invalid expiration month" msgstr "Неверный месяц истечения" #: enums.py:91 msgid "Invalid expiration year" msgstr "Неверный год истечения" #: enums.py:92 msgid "Invalid number" msgstr "Неверный номер" #: enums.py:93 #, fuzzy #| msgid "Invalid security code" msgid "Invalid source usage" msgstr "Недействительный код безопасности" #: enums.py:94 msgid "Invoice no customer line items" msgstr "" #: enums.py:95 msgid "Invoice no subscription line items" msgstr "" #: enums.py:96 msgid "Invoice not editable" msgstr "" #: enums.py:97 msgid "Invoice upcoming none" msgstr "" #: enums.py:98 msgid "Livemode mismatch" msgstr "" #: enums.py:99 msgid "No card being charged" msgstr "Деньги с карты не сняты" #: enums.py:100 msgid "Not allowed on standard account" msgstr "" #: enums.py:101 #, fuzzy #| msgid "Verification failed" msgid "Order creation failed" msgstr "Проверка не удалась" #: enums.py:102 msgid "Order required settings" msgstr "" #: enums.py:103 msgid "Order status invalid" msgstr "" #: enums.py:104 msgid "Order upstream timeout" msgstr "" #: enums.py:105 msgid "Out of inventory" msgstr "" #: enums.py:106 msgid "Parameter invalid empty" msgstr "" #: enums.py:107 msgid "Parameter invalid integer" msgstr "" #: enums.py:108 msgid "Parameter invalid string blank" msgstr "" #: enums.py:109 msgid "Parameter invalid string empty" msgstr "" #: enums.py:110 msgid "Parameter missing" msgstr "" #: enums.py:111 msgid "Parameter unknown" msgstr "" #: enums.py:112 msgid "Parameters exclusive" msgstr "" #: enums.py:113 msgid "Payment intent authentication failure" msgstr "" #: enums.py:114 msgid "Payment intent incompatible payment method" msgstr "" #: enums.py:115 msgid "Payment intent invalid parameter" msgstr "" #: enums.py:116 msgid "Payment intent payment attempt failed" msgstr "" #: enums.py:117 msgid "Payment intent unexpected state" msgstr "" #: enums.py:118 #, fuzzy #| msgid "Payment refund" msgid "Payment method unactivated" msgstr "Возврат платежа" #: enums.py:119 msgid "Payment method unexpected state" msgstr "" #: enums.py:120 #, fuzzy #| msgid "Payout failure" msgid "Payouts not allowed" msgstr "Сбой выплаты" #: enums.py:121 msgid "Platform api key expired" msgstr "" #: enums.py:122 msgid "Postal code invalid" msgstr "" #: enums.py:123 enums.py:433 msgid "Processing error" msgstr "Ошибка обработки" #: enums.py:124 #, fuzzy #| msgid "Product unacceptable" msgid "Product inactive" msgstr "Продукт неприемлем" #: enums.py:125 msgid "Rate limit" msgstr "" #: enums.py:126 msgid "Resource already exists" msgstr "" #: enums.py:127 msgid "Resource missing" msgstr "" #: enums.py:128 msgid "Routing number invalid" msgstr "" #: enums.py:129 #, fuzzy #| msgid "Not required" msgid "Secret key required" msgstr "Не требуется" #: enums.py:130 msgid "SEPA unsupported account" msgstr "" #: enums.py:131 #, fuzzy #| msgid "Verification failed" msgid "Shipping calculation failed" msgstr "Проверка не удалась" #: enums.py:132 msgid "SKU inactive" msgstr "" #: enums.py:133 msgid "State unsupported" msgstr "" #: enums.py:134 msgid "Tax id invalid" msgstr "" #: enums.py:135 #, fuzzy #| msgid "Verification failed" msgid "Taxes calculation failed" msgstr "Проверка не удалась" #: enums.py:136 msgid "Testmode charges only" msgstr "" #: enums.py:137 msgid "TLS version unsupported" msgstr "" #: enums.py:138 msgid "Token already used" msgstr "" #: enums.py:139 msgid "Token in use" msgstr "" #: enums.py:140 #, fuzzy #| msgid "Transfer refund" msgid "Transfers not allowed" msgstr "Возврат перевода" #: enums.py:141 #, fuzzy #| msgid "Verification failed" msgid "Upstream order creation failed" msgstr "Проверка не удалась" #: enums.py:142 msgid "URL invalid" msgstr "" #: enums.py:145 msgid "Invalid swipe data" msgstr "Ошибка чтения магнитной ленты" #: enums.py:149 enums.py:302 msgid "Standard" msgstr "Standard" #: enums.py:150 msgid "Express" msgstr "Express" #: enums.py:151 msgid "Custom" msgstr "На заказ" #: enums.py:155 msgid "Available" msgstr "Доступно" #: enums.py:156 enums.py:223 enums.py:308 enums.py:371 enums.py:413 #: enums.py:425 enums.py:437 msgid "Pending" msgstr "в ожидании" #: enums.py:160 msgid "Adjustment" msgstr "Корректировка" #: enums.py:161 msgid "Application fee" msgstr "Абонентская плата" #: enums.py:162 msgid "Application fee refund" msgstr "Возврат абонентской платы" #: enums.py:163 msgid "Charge" msgstr "Расход" #: enums.py:164 msgid "Network cost" msgstr "Сетевая стоимость" #: enums.py:165 msgid "Payment" msgstr "Платеж" #: enums.py:166 msgid "Payment failure refund" msgstr "Возврат неудачного платежа" #: enums.py:167 msgid "Payment refund" msgstr "Возврат платежа" #: enums.py:168 msgid "Payout" msgstr "Выплата" #: enums.py:169 msgid "Payout cancellation" msgstr "Отмена выплаты" #: enums.py:170 msgid "Payout failure" msgstr "Сбой выплаты" #: enums.py:171 msgid "Refund" msgstr "Возврат" #: enums.py:172 msgid "Stripe fee" msgstr "Взнос в Stripe" #: enums.py:173 msgid "Transfer" msgstr "Перевод" #: enums.py:174 msgid "Transfer refund" msgstr "Возврат перевода" #: enums.py:175 msgid "Validation" msgstr "Проверка" #: enums.py:179 msgid "Individual" msgstr "Частное лицо" #: enums.py:180 msgid "Company" msgstr "Компания" #: enums.py:184 msgid "New" msgstr "Новый" #: enums.py:185 msgid "Validated" msgstr "Подтвержденный" #: enums.py:186 msgid "Verified" msgstr "Проверенный" #: enums.py:187 msgid "Verification failed" msgstr "Проверка не удалась" #: enums.py:188 msgid "Errored" msgstr "Ошибочный" #: enums.py:192 msgid "Pass" msgstr "Успех" #: enums.py:193 msgid "Fail" msgstr "Отказ" #: enums.py:194 msgid "Unavailable" msgstr "Недоступен" #: enums.py:195 msgid "Unchecked" msgstr "Не проверен" #: enums.py:199 msgid "American Express" msgstr "American Express" #: enums.py:200 msgid "Diners Club" msgstr "Diners Club" #: enums.py:201 msgid "Discover" msgstr "Discover" #: enums.py:202 msgid "JCB" msgstr "JCB" #: enums.py:203 msgid "MasterCard" msgstr "MasterCard" #: enums.py:204 msgid "UnionPay" msgstr "" #: enums.py:205 msgid "Visa" msgstr "Visa" #: enums.py:206 enums.py:213 enums.py:403 msgid "Unknown" msgstr "Неизвестная" #: enums.py:210 msgid "Credit" msgstr "Кредитная" #: enums.py:211 msgid "Debit" msgstr "Дебитовая" #: enums.py:212 msgid "Prepaid" msgstr "Предоплаченная" #: enums.py:217 msgid "Apple Pay" msgstr "Apple Pay" #: enums.py:218 msgid "Android Pay" msgstr "Android Pay" #: enums.py:222 enums.py:414 enums.py:426 enums.py:438 msgid "Succeeded" msgstr "Успешно" #: enums.py:224 enums.py:311 enums.py:355 enums.py:370 enums.py:415 #: enums.py:427 enums.py:440 msgid "Failed" msgstr "Не удалось" #: enums.py:228 msgid "Once" msgstr "Единоразовый" #: enums.py:229 msgid "Multi-month" msgstr "Многомесячный" #: enums.py:230 msgid "Forever" msgstr "Постоянный" #: enums.py:234 msgid "Duplicate" msgstr "Повторный" #: enums.py:235 enums.py:408 msgid "Fraudulent" msgstr "Мошеннический" #: enums.py:236 msgid "Subscription canceled" msgstr "Подписка отменена" #: enums.py:237 msgid "Product unacceptable" msgstr "Продукт неприемлем" #: enums.py:238 msgid "Product not received" msgstr "Продукт не получен" #: enums.py:239 msgid "Unrecognized" msgstr "Не распознано" #: enums.py:240 msgid "Credit not processed" msgstr "Кредит не обработан" #: enums.py:241 msgid "General" msgstr "Основной" #: enums.py:242 msgid "Incorrect account details" msgstr "Неверные данные счета" #: enums.py:243 msgid "Insufficient funds" msgstr "Недостаточно средств" #: enums.py:244 msgid "Bank cannot process" msgstr "Банк не может обработать" #: enums.py:245 msgid "Debit not authorized" msgstr "Дебит не разрешен" #: enums.py:246 msgid "Customer-initiated" msgstr "По инициативе заказчика" #: enums.py:250 msgid "Warning needs response" msgstr "Предупреждение требует ответа" #: enums.py:251 msgid "Warning under review" msgstr "Предупреждение в рассмотрении" #: enums.py:252 msgid "Warning closed" msgstr "Предупреждение закрыто" #: enums.py:253 msgid "Needs response" msgstr "Требуется ответ" #: enums.py:254 msgid "Under review" msgstr "В рассмотрении" #: enums.py:255 msgid "Charge refunded" msgstr "Платеж возвращен" #: enums.py:256 msgid "Won" msgstr "Выиграл" #: enums.py:257 msgid "Lost" msgstr "Проиграл" #: enums.py:261 msgid "Dispute evidence" msgstr "Доказательства по спору" #: enums.py:262 msgid "Identity document" msgstr "Удостоверение личности" #: enums.py:263 msgid "Tax document user upload" msgstr "" #: enums.py:267 msgid "PDF" msgstr "PDF" #: enums.py:268 msgid "JPG" msgstr "JPG" #: enums.py:269 msgid "PNG" msgstr "PNG" #: enums.py:270 msgid "CSV" msgstr "CSV" #: enums.py:271 msgid "XLS" msgstr "XLS" #: enums.py:272 msgid "XLSX" msgstr "XLSX" #: enums.py:273 msgid "DOCX" msgstr "DOCX" #: enums.py:277 msgid "Charge automatically" msgstr "Снимать автоматически" #: enums.py:278 msgid "Send invoice" msgstr "Отправить счет-фактуру" #: enums.py:288 msgid "Bank account has been closed." msgstr "Банковский счет закрыт." #: enums.py:289 msgid "Bank account has been frozen." msgstr "Банковский счет заморожен." #: enums.py:290 msgid "Bank account has restrictions on payouts allowed." msgstr "Банковский счет имеет ограничения по платежам." #: enums.py:291 msgid "Destination bank account has changed ownership." msgstr "Банковский счет назначения сменил владельца." #: enums.py:292 msgid "Bank could not process payout." msgstr "Банк не может обработать платеж." #: enums.py:293 msgid "Debit transactions not approved on the bank account." msgstr "Дебетовые транзакции не утверждены на банковском счете." #: enums.py:294 msgid "Stripe account has insufficient funds." msgstr "На счете Stripe недостаточно средств." #: enums.py:295 msgid "Invalid account number" msgstr "Неверный номер счета" #: enums.py:296 msgid "Bank account does not support currency." msgstr "Валюта не поддерживается банковским счетом." #: enums.py:297 msgid "Bank account could not be located." msgstr "Банковский счет не найден." #: enums.py:298 msgid "Card no longer supported." msgstr "Карта не поддерживается." #: enums.py:303 msgid "Instant" msgstr "Мгновенный" #: enums.py:307 msgid "Paid" msgstr "Оплачено" #: enums.py:309 msgid "In transit" msgstr "В пути" #: enums.py:310 enums.py:354 enums.py:367 enums.py:416 enums.py:447 msgid "Canceled" msgstr "Отменен" #: enums.py:315 enums.py:395 enums.py:457 msgid "Bank account" msgstr "Банковский счет" #: enums.py:316 enums.py:380 enums.py:394 enums.py:456 msgid "Card" msgstr "Карта" #: enums.py:320 msgid "Last during period" msgstr "Последний за период" #: enums.py:321 msgid "Last ever" msgstr "Последний" #: enums.py:322 msgid "Max" msgstr "Максимум" #: enums.py:323 msgid "Sum" msgstr "Сумма" #: enums.py:327 msgid "Per unit" msgstr "За единицу" #: enums.py:328 msgid "Tiered" msgstr "Многоуровневый" #: enums.py:332 msgid "Day" msgstr "День" #: enums.py:333 msgid "Week" msgstr "Неделя" #: enums.py:334 msgid "Month" msgstr "Месяц" #: enums.py:335 msgid "Year" msgstr "Год" #: enums.py:339 msgid "Metered" msgstr "Измеряемый" #: enums.py:340 msgid "Licensed" msgstr "Лицензированный" #: enums.py:344 msgid "Graduated" msgstr "Гарантированный" #: enums.py:345 msgid "Volume-based" msgstr "По объему" #: enums.py:349 msgid "Good" msgstr "Хороший" #: enums.py:350 msgid "Service" msgstr "Сервис" #: enums.py:356 msgid "Timed out" msgstr "" #: enums.py:360 msgid "Redirect" msgstr "Перенаправить" #: enums.py:361 msgid "Receiver" msgstr "Получатель" #: enums.py:362 msgid "Code verification" msgstr "Проверка кода" #: enums.py:363 msgid "None" msgstr "Отсутствует" #: enums.py:368 msgid "Chargeable" msgstr "Подлежащий оплате" #: enums.py:369 msgid "Consumed" msgstr "Потребленный" #: enums.py:375 msgid "ACH Credit Transfer" msgstr "Кредитный перевод ACH" #: enums.py:376 msgid "ACH Debit" msgstr "Дебит ACH" #: enums.py:377 msgid "Alipay" msgstr "Alipay" #: enums.py:378 msgid "Bancontact" msgstr "Bancontact" #: enums.py:379 msgid "Bitcoin" msgstr "Bitcoin" #: enums.py:381 msgid "Card present" msgstr "Подарочная карта" #: enums.py:382 msgid "EPS" msgstr "EPS" #: enums.py:383 msgid "Giropay" msgstr "Giropay" #: enums.py:384 msgid "iDEAL" msgstr "iDEAL" #: enums.py:385 msgid "P24" msgstr "P24" #: enums.py:386 msgid "Paper check" msgstr "Бумажный чек" #: enums.py:387 msgid "SEPA Direct Debit" msgstr "Прямой дебет SEPA" #: enums.py:388 msgid "SEPA credit transfer" msgstr "Кредитный перевод SEPA" #: enums.py:389 msgid "SOFORT" msgstr "SOFORT" #: enums.py:390 msgid "3D Secure" msgstr "3D Secure" #: enums.py:396 msgid "Bitcoin receiver" msgstr "Получатель Bitcoin" #: enums.py:397 msgid "Alipay account" msgstr "Счет Alipay" #: enums.py:401 msgid "Lost or stolen card" msgstr "Утерянная/украденная карта" #: enums.py:402 msgid "Expired or canceled card" msgstr "Истекшая/отозванная карта" #: enums.py:407 msgid "Duplicate charge" msgstr "Дублированный платеж" #: enums.py:409 msgid "Requested by customer" msgstr "По запросу покупателя" #: enums.py:420 msgid "Reusable" msgstr "Многоразовый" #: enums.py:421 msgid "Single-use" msgstr "Одноразовый" #: enums.py:431 msgid "User-aborted" msgstr "Отменен пользователем" #: enums.py:432 msgid "Declined" msgstr "Отказано" #: enums.py:439 msgid "Not required" msgstr "Не требуется" #: enums.py:444 msgid "Trialing" msgstr "Пробный период" #: enums.py:445 msgid "Active" msgstr "Активен" #: enums.py:446 msgid "Past due" msgstr "Просроченный" #: enums.py:448 msgid "Unpaid" msgstr "Не оплачен" #: enums.py:458 msgid "Source" msgstr "Источник" #: models/billing.py:863 msgid "day" msgstr "день" #: models/billing.py:864 msgid "week" msgstr "неделя" #: models/billing.py:865 msgid "month" msgstr "месяц" #: models/billing.py:866 msgid "year" msgstr "год" #: models/billing.py:868 #, python-brace-format msgid "{amount}/{interval}" msgstr "{amount}/{interval}" #: models/billing.py:871 msgid "days" msgstr "дней" #: models/billing.py:872 msgid "weeks" msgstr "недель" #: models/billing.py:873 msgid "months" msgstr "месяцев" #: models/billing.py:874 msgid "years" msgstr "лет" #: models/billing.py:876 #, python-brace-format msgid "{amount} every {interval_count} {interval}" msgstr "{amount} за {interval_count} {interval}" #: templates/djstripe/admin/change_form.html:8 msgid "View on Stripe Dashboard" msgstr "Посмотреть в панели управления Stripe" #~ msgid "Загрузка налогового документа пользователя" #~ msgstr "Document de taxes de l'utilisateur" ================================================ FILE: djstripe/management/__init__.py ================================================ ================================================ FILE: djstripe/management/commands/__init__.py ================================================ ================================================ FILE: djstripe/management/commands/djstripe_clear_expired_idempotency_keys.py ================================================ from django.core.management.base import BaseCommand from ...utils import clear_expired_idempotency_keys class Command(BaseCommand): help = "Deleted expired Stripe idempotency keys." def handle(self, *args, **options): clear_expired_idempotency_keys() ================================================ FILE: djstripe/management/commands/djstripe_init_customers.py ================================================ """ init_customers command. """ from django.core.management.base import BaseCommand from ...models import Customer from ...settings import djstripe_settings class Command(BaseCommand): """Create customer objects for existing subscribers that don't have one.""" help = "Create customer objects for existing subscribers that don't have one" def handle(self, *args, **options): """ Create Customer objects for Subscribers without Customer objects associated. """ subscriber_qs = djstripe_settings.get_subscriber_model().objects.filter( djstripe_customers=None ) if subscriber_qs: for subscriber in subscriber_qs: # use get_or_create in case of race conditions on large subscriber bases Customer.get_or_create(subscriber=subscriber) self.stdout.write(f"Created subscriber for {subscriber.email}") else: self.stdout.write("All Customers already have subscribers") ================================================ FILE: djstripe/management/commands/djstripe_process_events.py ================================================ from django.core.management.base import BaseCommand from ... import models from ...mixins import VerbosityAwareOutputMixin from ...settings import djstripe_settings class Command(VerbosityAwareOutputMixin, BaseCommand): """Command to process all Events. Optional arguments are provided to limit the number of Events processed. Note: this is only guaranteed go back at most 30 days based on the current limitation of stripe's events API. See: https://stripe.com/docs/api/events """ help = ( "Process all Events. Use optional arguments to limit the Events to process. " "Note: this is only guaranteed go back at most 30 days based on the current " "limitation of stripe's events API. See: https://stripe.com/docs/api/events" ) def add_arguments(self, parser): """Add optional arguments to filter Events by.""" # Use a mutually exclusive group to prevent multiple arguments being # specified together. group = parser.add_mutually_exclusive_group() group.add_argument( "--ids", nargs="*", help="An optional space separated list of specific Event IDs to sync.", ) group.add_argument( "--failed", action="store_true", help="Syncs and processes only the events that have failed webhooks.", ) group.add_argument( "--type", help=( "A string containing a specific event name," " or group of events using * as a wildcard." " The list will be filtered to include only" " events with a matching event property." ), ) def handle(self, *args, **options): """Try to process Events listed from the API.""" # Set the verbosity to determine how much we output, if at all. self.set_verbosity(options) event_ids = options["ids"] failed = options["failed"] type_filter = options["type"] # Args are mutually exclusive, # so output what we are doing based on that assumption. if failed: self.output("Processing all failed events") elif type_filter: self.output(f"Processing all events that match {type_filter}") elif event_ids: self.output(f"Processing specific events {event_ids}") else: self.output("Processing all available events") # Either use the specific event IDs to retrieve data, or use the api_list # if no specific event IDs are specified. if event_ids: listed_events = ( models.Event.stripe_class.retrieve( id=event_id, api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) for event_id in event_ids ) else: list_kwargs = {} if failed: list_kwargs["delivery_success"] = False if type_filter: list_kwargs["type"] = type_filter listed_events = models.Event.api_list(**list_kwargs) self.process_events(listed_events) def process_events(self, listed_events): # Process each listed event. Capture failures and continue, # outputting debug information as verbosity dictates. count = 0 total = 0 for event_data in listed_events: try: total += 1 event = models.Event.process(data=event_data) count += 1 self.verbose_output(f"\tSynced Event {event.id}") except Exception as exception: self.verbose_output(f"\tFailed processing Event {event_data['id']}") self.output(f"\t{exception}") self.verbose_traceback() if total == 0: self.output("\t(no results)") else: self.output(f"\tProcessed {count} out of {total} Events") ================================================ FILE: djstripe/management/commands/djstripe_sync_customers.py ================================================ """ sync_customer command. """ from django.core.management.base import BaseCommand from ...settings import djstripe_settings from ...sync import sync_subscriber class Command(BaseCommand): """Sync subscriber data with stripe.""" help = "Sync subscriber data with stripe." def handle(self, *args, **options): """Call sync_subscriber on Subscribers without customers associated to them.""" qs = djstripe_settings.get_subscriber_model().objects.filter( djstripe_customers__isnull=True ) count = 0 total = qs.count() for subscriber in qs: count += 1 pc = int(round(100 * (float(count) / float(total)))) print( f"[{count}/{total} {pc}%] Syncing {subscriber.email} [{subscriber.pk}]" ) sync_subscriber(subscriber) ================================================ FILE: djstripe/management/commands/djstripe_sync_models.py ================================================ """Module for the djstripe_sync_model management command to sync all Stripe objects to the local db. Invoke like so: 1) To sync all Objects for all API keys: python manage.py djstripe_sync_models 2) To sync all Objects only for sk_test_XXX API key: python manage.py djstripe_sync_models --api-keys sk_test_XXX 3) To sync all Objects only for sk_test_XXX and sk_test_YYY API keys: python manage.py djstripe_sync_models --api-keys sk_test_XXX sk_test_XXX OR python manage.py djstripe_sync_models --api-keys sk_test_XXX --api-keys sk_test_XXX 4) To only sync Stripe Accounts for all API keys: python manage.py djstripe_sync_models Account 5) To only sync Stripe Accounts for sk_test_XXX API key: python manage.py djstripe_sync_models Account --api-keys sk_test_XXX 6) To only sync Stripe Accounts for sk_test_XXX and sk_test_YYY API keys: python manage.py djstripe_sync_models Account --api-keys sk_test_XXX sk_test_YYY 7) To only sync Stripe Accounts and Charges for sk_test_XXX and sk_test_YYY API keys: python manage.py djstripe_sync_models Account Charge --api-keys sk_test_XXX sk_test_YYY """ import typing from django.apps import apps from django.core.exceptions import FieldDoesNotExist from django.core.management.base import BaseCommand, CommandError from django.db import models as django_models from ... import enums, models from ...models.base import StripeBaseModel from ...settings import djstripe_settings # TODO Improve performance using multiprocessing class Command(BaseCommand): """Sync models from stripe.""" help = "Sync models from stripe." def add_arguments(self, parser): parser.add_argument( "args", metavar="ModelName", nargs="*", help="restricts sync to these model names (default is to sync all " "supported models)", ) # Named (optional) arguments parser.add_argument( "--api-keys", metavar="ApiKeys", nargs="*", type=str, action="extend", help="Specify the api_keys you would like to perform this sync for.", ) def handle(self, *args, api_keys: typing.List[str], **options): app_label = "djstripe" app_config = apps.get_app_config(app_label) model_list: typing.List[models.StripeModel] = [] if args: for model_label in args: try: model = app_config.get_model(model_label) except LookupError: raise CommandError(f"Unknown model: {app_label}.{model_label}") model_list.append(model) else: model_list = app_config.get_models() if api_keys is not None: for api_key in api_keys: try: # check to ensure the given key is in the DB models.APIKey.objects.get(secret=api_key) except models.APIKey.DoesNotExist: raise CommandError(f"APIKey: {api_key} is not in the database.") api_qs = models.APIKey.objects.filter(secret__in=api_keys) else: # get all APIKey objects in the db api_qs = models.APIKey.objects.all() if not api_qs.exists(): self.stderr.write( "You don't have any API Keys in the database. Did you forget to add them?" ) return for model in model_list: for api_key in api_qs: self.sync_model(model, api_key=api_key) def _should_sync_model(self, model): if not issubclass(model, StripeBaseModel): return False, "not a StripeModel" if model.stripe_class is None: return False, "no stripe_class" if not hasattr(model.stripe_class, "list"): if model in ( models.ApplicationFeeRefund, models.LineItem, models.Source, models.TransferReversal, models.TaxId, models.UsageRecordSummary, ): return True, "" return False, "no stripe_class.list" if model is models.UpcomingInvoice: return False, "Upcoming Invoices are virtual only" if not djstripe_settings.STRIPE_LIVE_MODE: if model is models.ScheduledQueryRun: return False, "only available in live mode" return True, "" def sync_model(self, model, api_key: str): model_name = model.__name__ should_sync, reason = self._should_sync_model(model) if not should_sync: self.stderr.write(f"Skipping {model}: {reason}") return self.stdout.write(f"Syncing {model_name} for key {api_key}:") count = 0 try: # todo convert get_list_kwargs into a generator to make the code memory effecient. for list_kwargs in self.get_list_kwargs(model, api_key=api_key.secret): stripe_account = list_kwargs.get("stripe_account", "") if ( model is models.Account and stripe_account == models.Account.get_default_account(api_key=api_key.secret).id ): # special case, since own account isn't returned by Account.api_list stripe_obj = models.Account.stripe_class.retrieve( api_key=api_key.secret, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) djstripe_obj = model.sync_from_stripe_data( stripe_obj, api_key=api_key.secret ) self.stdout.write( f" id={djstripe_obj.id}, pk={djstripe_obj.pk} ({djstripe_obj} on {stripe_account} for {api_key})" ) # syncing BankAccount and Card objects of Stripe Connected Express and Custom Accounts self.sync_bank_accounts_and_cards( djstripe_obj, stripe_account=stripe_account, api_key=api_key.secret, ) count += 1 try: for stripe_obj in model.api_list(**list_kwargs): # Skip model instances that throw an error try: djstripe_obj = model.sync_from_stripe_data( stripe_obj, api_key=api_key.secret ) self.stdout.write( f" id={djstripe_obj.id}, pk={djstripe_obj.pk} ({djstripe_obj} on {stripe_account} for {api_key})" ) # syncing BankAccount and Card objects of Stripe Connected Express and Custom Accounts self.sync_bank_accounts_and_cards( djstripe_obj, stripe_account=stripe_account, api_key=api_key.secret, ) count += 1 except Exception as e: self.stderr.write(f"Skipping {stripe_obj.get('id')}: {e}") continue except Exception as e: self.stderr.write(f"Skipping: {e}") if count == 0: self.stdout.write(" (no results)") else: self.stdout.write(f" Synced {count} {model_name} for {api_key}") except Exception as e: self.stderr.write(str(e)) @classmethod def get_stripe_account(cls, api_key: str, *args, **kwargs): """Get set of all stripe account ids including the Platform Acccount""" accs_set = set() # special case, since own account isn't returned by Account.api_list stripe_platform_obj = models.Account.stripe_class.retrieve( api_key=api_key, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) accs_set.add(stripe_platform_obj.id) for stripe_connected_obj in models.Account.api_list(api_key=api_key, **kwargs): accs_set.add(stripe_connected_obj.id) return accs_set # todo simplfy this code by spliting into 1-2 functions @staticmethod def get_default_list_kwargs(model, accounts_set, api_key: str): """Returns default sequence of kwargs to sync all Stripe Accounts""" expand = [] try: # get all forward and reverse relations for given cls for field in model.expand_fields: # add expand_field on the current model expand.append(f"data.{field}") try: field_inst = model._meta.get_field(field) # get expand_fields on Forward FK and OneToOneField relations on the current model if isinstance( field_inst, (django_models.ForeignKey, django_models.OneToOneField), ): try: for ( related_model_expand_field ) in field_inst.related_model.expand_fields: # add expand_field on the current model expand.append( f"data.{field}.{related_model_expand_field}" ) related_model_expand_field_inst = ( field_inst.related_model._meta.get_field( related_model_expand_field ) ) # get expand_fields on Forward FK and OneToOneField relations on the current model if isinstance( related_model_expand_field_inst, ( django_models.ForeignKey, django_models.OneToOneField, ), ): try: # need to prepend "field_name." to each of the entry in the expand_fields list related_model_expand_fields = map( lambda i: f"data.{field_inst.name}.{related_model_expand_field}.{i}", related_model_expand_field_inst.related_model.expand_fields, ) expand = [ *expand, *related_model_expand_fields, ] except AttributeError: continue except AttributeError: continue except FieldDoesNotExist: pass except AttributeError: pass if expand: default_list_kwargs = [ { "expand": expand, "stripe_account": account, "api_key": api_key, } for account in accounts_set ] else: default_list_kwargs = [ { "stripe_account": account, "api_key": api_key, } for account in accounts_set ] return default_list_kwargs @staticmethod def get_list_kwargs_il(default_list_kwargs): """Returns sequence of kwargs to sync Line Items for all Stripe Accounts""" all_list_kwargs = [] for def_kwarg in default_list_kwargs: stripe_account = def_kwarg.get("stripe_account") api_key = def_kwarg.get("api_key") for stripe_invoice in models.Invoice.api_list( stripe_account=stripe_account, api_key=api_key ): all_list_kwargs.append({"id": stripe_invoice.id, **def_kwarg}) return all_list_kwargs @staticmethod def get_list_kwargs_pm(default_list_kwargs): """Returns sequence of kwargs to sync Payment Methods for all Stripe Accounts""" all_list_kwargs = [] payment_method_types = enums.PaymentMethodType.__members__ for def_kwarg in default_list_kwargs: stripe_account = def_kwarg.get("stripe_account") api_key = def_kwarg.get("api_key") for stripe_customer in models.Customer.api_list( stripe_account=stripe_account, api_key=api_key ): for type in payment_method_types: all_list_kwargs.append( {"customer": stripe_customer.id, "type": type, **def_kwarg} ) return all_list_kwargs @staticmethod def get_list_kwargs_src(default_list_kwargs): """Returns sequence of kwargs to sync Sources for all Stripe Accounts""" all_list_kwargs = [] for def_kwarg in default_list_kwargs: stripe_account = def_kwarg.get("stripe_account") api_key = def_kwarg.get("api_key") for stripe_customer in models.Customer.api_list( stripe_account=stripe_account, api_key=api_key ): all_list_kwargs.append({"id": stripe_customer.id, **def_kwarg}) return all_list_kwargs @staticmethod def get_list_kwargs_si(default_list_kwargs): """Returns sequence of kwargs to sync Subscription Items for all Stripe Accounts""" all_list_kwargs = [] for def_kwarg in default_list_kwargs: stripe_account = def_kwarg.get("stripe_account") api_key = def_kwarg.get("api_key") for subscription in models.Subscription.api_list( stripe_account=stripe_account, api_key=api_key ): all_list_kwargs.append({"subscription": subscription.id, **def_kwarg}) return all_list_kwargs @staticmethod def get_list_kwargs_country_spec(default_list_kwargs): """Returns sequence of kwargs to sync Country Specs for all Stripe Accounts""" all_list_kwargs = [] for def_kwarg in default_list_kwargs: all_list_kwargs.append({"limit": 50, **def_kwarg}) return all_list_kwargs @staticmethod def get_list_kwargs_txcd(default_list_kwargs): """Returns sequence of kwargs to sync Tax Codes for all Stripe Accounts""" # tax codes are the same for all Stripe Accounts return [{}] @staticmethod def get_list_kwargs_trr(default_list_kwargs): """Returns sequence of kwargs to sync Transfer Reversals for all Stripe Accounts""" all_list_kwargs = [] for def_kwarg in default_list_kwargs: stripe_account = def_kwarg.get("stripe_account") api_key = def_kwarg.get("api_key") for transfer in models.Transfer.api_list( stripe_account=stripe_account, api_key=api_key ): all_list_kwargs.append({"id": transfer.id, **def_kwarg}) return all_list_kwargs @staticmethod def get_list_kwargs_fee_refund(default_list_kwargs): """Returns sequence of kwargs to sync Application Fee Refunds for all Stripe Accounts""" all_list_kwargs = [] for def_kwarg in default_list_kwargs: stripe_account = def_kwarg.get("stripe_account") api_key = def_kwarg.get("api_key") for fee in models.ApplicationFee.api_list( stripe_account=stripe_account, api_key=api_key ): all_list_kwargs.append({"id": fee.id, **def_kwarg}) return all_list_kwargs @staticmethod def get_list_kwargs_tax_id(default_list_kwargs): """Returns sequence of kwargs to sync Tax Ids for all Stripe Accounts""" all_list_kwargs = [] for def_kwarg in default_list_kwargs: stripe_account = def_kwarg.get("stripe_account") api_key = def_kwarg.get("api_key") for customer in models.Customer.api_list( stripe_account=stripe_account, api_key=api_key ): all_list_kwargs.append({"id": customer.id, **def_kwarg}) return all_list_kwargs @staticmethod def get_list_kwargs_sis(default_list_kwargs): """Returns sequence of kwargs to sync Usage Record Summarys for all Stripe Accounts""" all_list_kwargs = [] for def_kwarg in default_list_kwargs: stripe_account = def_kwarg.get("stripe_account") api_key = def_kwarg.get("api_key") for subscription in models.Subscription.api_list( stripe_account=stripe_account, api_key=api_key ): for subscription_item in models.SubscriptionItem.api_list( subscription=subscription.id, stripe_account=stripe_account, api_key=api_key, ): all_list_kwargs.append({"id": subscription_item.id, **def_kwarg}) return all_list_kwargs # todo handle supoorting double + nested fields like data.invoice.subscriptions.customer etc? def get_list_kwargs(self, model, api_key: str): """ Returns a sequence of kwargs dicts to pass to model.api_list This allows us to sync models that require parameters to api_list :param model: :return: Sequence[dict] """ list_kwarg_handlers_dict = { "LineItem": self.get_list_kwargs_il, "PaymentMethod": self.get_list_kwargs_pm, "Source": self.get_list_kwargs_src, "SubscriptionItem": self.get_list_kwargs_si, "CountrySpec": self.get_list_kwargs_country_spec, "TransferReversal": self.get_list_kwargs_trr, "ApplicationFeeRefund": self.get_list_kwargs_fee_refund, "TaxCode": self.get_list_kwargs_txcd, "TaxId": self.get_list_kwargs_tax_id, "UsageRecordSummary": self.get_list_kwargs_sis, } # get all Stripe Accounts for the given platform account. # note that we need to fetch from Stripe as we have no way of knowing that the ones in the local db are up to date # as this can also be the first time the user runs sync. accs_set = self.get_stripe_account(api_key=api_key) default_list_kwargs = self.get_default_list_kwargs( model, accs_set, api_key=api_key ) handler = list_kwarg_handlers_dict.get( model.__name__, lambda _: default_list_kwargs ) return handler(default_list_kwargs) def sync_bank_accounts_and_cards(self, instance, *, stripe_account, api_key): """ Syncs Bank Accounts and Cards for both customers and all external accounts """ type = getattr(instance, "type", None) kwargs = { "id": instance.id, "api_key": api_key, "stripe_account": stripe_account, "stripe_version": djstripe_settings.STRIPE_API_VERSION, } if type in (enums.AccountType.custom, enums.AccountType.express) and isinstance( instance, models.Account ): # fetch all Card and BankAccount objects associated with the instance items = models.Account.stripe_class.list_external_accounts( **kwargs ).auto_paging_iter() self.start_sync(items, instance, api_key=api_key) elif isinstance(instance, models.Customer): for object in ("card", "bank_account"): kwargs["object"] = object # fetch all Card and BankAccount objects associated with the instance items = models.Customer.stripe_class.list_sources( **kwargs ).auto_paging_iter() self.start_sync(items, instance, api_key=api_key) def start_sync(self, items, instance, api_key: str): bank_count = 0 card_count = 0 for item in items: if item.object == "bank_account": model = models.BankAccount bank_count += 1 elif item.object == "card": model = models.Card card_count += 1 item_obj = model.sync_from_stripe_data(item, api_key=api_key) self.stdout.write( f"\tSyncing {model._meta.verbose_name} ({instance}): id={item_obj.id}, pk={item_obj.pk}" ) if bank_count + card_count > 0: self.stdout.write( f"\tSynced {bank_count} BankAccounts and {card_count} Cards" ) ================================================ FILE: djstripe/management/commands/djstripe_update_invoiceitem_ids.py ================================================ from django.core.management.base import BaseCommand from django.db import transaction from ...models.billing import InvoiceItem no_results_msg = ( "There are no more potential InvoiceItems to migrate. " "You do not need to run this command anymore." ) class Command(BaseCommand): help = "Update old InvoiceItem IDs to the new, 2019-12-03 format." def add_arguments(self, parser): """Add optional arguments to filter Events by.""" # Use a mutually exclusive group to prevent multiple arguments being # specified together. group = parser.add_mutually_exclusive_group() group.add_argument( "--i-understand", action="store_true", help="Run the command, once you've read the warning and understand it.", ) def handle(self, *args, **options): invoice_items = InvoiceItem.objects.filter(id__contains="-il_") count = invoice_items.count() if not options["i_understand"]: self.stderr.write( "In Stripe API 2019-12-03, the format of invoice line items changed. " "This means that existing InvoiceItem objects with the old ID format " "may still be in the database and need to be migrated.\n" "This is a destructive migration, but this command will attempt to " "perform it as safely as possible.\n" "More information: https://stripe.com/docs/upgrades#2019-12-03\n\n" ) if count: first_few_ids = invoice_items[:10].values_list("id", flat=True) self.stdout.write(f"I have found {count} InvoiceItems to migrate:") self.stdout.write( " " + ", ".join(first_few_ids) + f", … (and {count-10} more)" if count > 10 else "" ) self.stderr.write( "To perform this migration, run this again with `--i-understand`." ) else: self.stdout.write(no_results_msg) return if not count: self.stdout.write(no_results_msg) return for ii in invoice_items: old_id = ii.id new_id = old_id.partition("-")[2] if "-" in new_id or not new_id.startswith("il_"): self.stderr.write( f"Don't know how to migrate {old_id!r}. This is a bug. " "Could you report it?\n https://github.com/dj-stripe/dj-stripe" ) continue self.stdout.write(f"Migrating {old_id} => {new_id}") with transaction.atomic(): ii.id = new_id stripe_data = ii.api_retrieve() ii.save() InvoiceItem.sync_from_stripe_data(stripe_data) ================================================ FILE: djstripe/managers.py ================================================ """ dj-stripe model managers """ import decimal from django.db import models class StripeModelManager(models.Manager): """Manager used in StripeModel.""" pass class SubscriptionManager(models.Manager): """Manager used in models.Subscription.""" def started_during(self, year, month): """Return Subscriptions not in trial status between a certain time range.""" return self.exclude(status="trialing").filter( start_date__year=year, start_date__month=month ) def active(self): """Return active Subscriptions.""" return self.filter(status="active") def canceled(self): """Return canceled Subscriptions.""" return self.filter(status="canceled") def canceled_during(self, year, month): """Return Subscriptions canceled during a certain time range.""" return self.canceled().filter(canceled_at__year=year, canceled_at__month=month) def started_plan_summary_for(self, year, month): """Return started_during Subscriptions with plan counts annotated.""" return ( self.started_during(year, month) .values("plan") .order_by() .annotate(count=models.Count("plan")) ) def active_plan_summary(self): """Return active Subscriptions with plan counts annotated.""" return ( self.active().values("plan").order_by().annotate(count=models.Count("plan")) ) def canceled_plan_summary_for(self, year, month): """ Return Subscriptions canceled within a time range with plan counts annotated. """ return ( self.canceled_during(year, month) .values("plan") .order_by() .annotate(count=models.Count("plan")) ) def churn(self): """Return number of canceled Subscriptions divided by active Subscriptions.""" canceled = self.canceled().count() active = self.active().count() return decimal.Decimal(str(canceled)) / decimal.Decimal(str(active)) class TransferManager(models.Manager): """Manager used by models.Transfer.""" def during(self, year, month): """Return Transfers between a certain time range.""" return self.filter(created__year=year, created__month=month) def paid_totals_for(self, year, month): """ Return paid Transfers during a certain year, month with total amounts annotated. """ return self.during(year, month).aggregate(total_amount=models.Sum("amount")) class ChargeManager(models.Manager): """Manager used by models.Charge.""" def during(self, year, month): """Return Charges between a certain time range based on `created`.""" return self.filter(created__year=year, created__month=month) def paid_totals_for(self, year, month): """ Return paid Charges during a certain year, month with total amount, fee and refunded annotated. """ return ( self.during(year, month) .filter(paid=True) .aggregate( total_amount=models.Sum("amount"), total_refunded=models.Sum("amount_refunded"), ) ) ================================================ FILE: djstripe/migrations/0001_initial.py ================================================ # Generated by Django 3.2.11 on 2022-01-17 03:13 import uuid import django.core.validators import django.db.models.deletion from django.conf import settings from django.db import migrations, models import djstripe.enums import djstripe.fields import djstripe.models.api import djstripe.models.webhooks DJSTRIPE_SUBSCRIBER_MODEL: str = getattr( settings, "DJSTRIPE_SUBSCRIBER_MODEL", settings.AUTH_USER_MODEL ) # type: ignore # Needed here for external apps that have added the DJSTRIPE_SUBSCRIBER_MODEL # *not* in the '__first__' migration of the app, which results in: # ValueError: Related model 'DJSTRIPE_SUBSCRIBER_MODEL' cannot be resolved # Context: https://github.com/dj-stripe/dj-stripe/issues/707 DJSTRIPE_SUBSCRIBER_MODEL_MIGRATION_DEPENDENCY = getattr( settings, "DJSTRIPE_SUBSCRIBER_MODEL_MIGRATION_DEPENDENCY", "__first__" ) DJSTRIPE_SUBSCRIBER_MODEL_DEPENDENCY = migrations.swappable_dependency( DJSTRIPE_SUBSCRIBER_MODEL ) if DJSTRIPE_SUBSCRIBER_MODEL != settings.AUTH_USER_MODEL: DJSTRIPE_SUBSCRIBER_MODEL_DEPENDENCY = migrations.migration.SwappableTuple( ( DJSTRIPE_SUBSCRIBER_MODEL.split(".", 1)[0], DJSTRIPE_SUBSCRIBER_MODEL_MIGRATION_DEPENDENCY, ), DJSTRIPE_SUBSCRIBER_MODEL, ) class Migration(migrations.Migration): initial = True dependencies = [DJSTRIPE_SUBSCRIBER_MODEL_DEPENDENCY] operations = [ migrations.CreateModel( name="Account", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ("business_profile", djstripe.fields.JSONField(blank=True, null=True)), ( "business_type", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.BusinessType, max_length=10, ), ), ( "charges_enabled", models.BooleanField( help_text="Whether the account can create live charges" ), ), ( "country", models.CharField( help_text="The country of the account", max_length=2 ), ), ("company", djstripe.fields.JSONField(blank=True, null=True)), ( "default_currency", djstripe.fields.StripeCurrencyCodeField(max_length=3), ), ( "details_submitted", models.BooleanField( help_text="Whether account details have been submitted. Standard accounts cannot receive payouts before this is true." ), ), ( "email", models.CharField( help_text="The primary user's email address.", max_length=255 ), ), ("individual", djstripe.fields.JSONField(blank=True, null=True)), ( "payouts_enabled", models.BooleanField( help_text="Whether Stripe can send payouts to this account", null=True, ), ), ( "product_description", models.CharField( blank=True, default="", help_text="Internal-only description of the product sold or service provided by the business. It's used by Stripe for risk and underwriting purposes.", max_length=255, ), ), ("requirements", djstripe.fields.JSONField(blank=True, null=True)), ("settings", djstripe.fields.JSONField(blank=True, null=True)), ( "type", djstripe.fields.StripeEnumField( enum=djstripe.enums.AccountType, max_length=8 ), ), ("tos_acceptance", djstripe.fields.JSONField(blank=True, null=True)), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="Charge", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "amount", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11 ), ), ( "amount_refunded", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11 ), ), ( "captured", models.BooleanField( default=False, help_text="If the charge was created without capturing, this boolean represents whether or not it is still uncaptured or has since been captured.", ), ), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ( "failure_code", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.ApiErrorCode, max_length=42, ), ), ( "failure_message", models.TextField( blank=True, default="", help_text="Message to user further explaining reason for charge failure if available.", max_length=5000, ), ), ("fraud_details", djstripe.fields.JSONField(blank=True, null=True)), ("outcome", djstripe.fields.JSONField(blank=True, null=True)), ( "paid", models.BooleanField( default=False, help_text="True if the charge succeeded, or was successfully authorized for later capture, False otherwise.", ), ), ( "payment_method_details", djstripe.fields.JSONField(blank=True, null=True), ), ( "receipt_email", models.TextField( blank=True, default="", help_text="The email address that the receipt for this charge was sent to.", max_length=800, ), ), ( "receipt_number", models.CharField( blank=True, default="", help_text="The transaction number that appears on email receipts sent for this charge.", max_length=14, ), ), ( "receipt_url", models.TextField( blank=True, default="", help_text="This is the URL to view the receipt for this charge. The receipt is kept up-to-date to the latest state of the charge, including any refunds. If the charge is for an Invoice, the receipt will be stylized as an Invoice receipt.", max_length=5000, ), ), ( "refunded", models.BooleanField( default=False, help_text="Whether or not the charge has been fully refunded. If the charge is only partially refunded, this attribute will still be false.", ), ), ("shipping", djstripe.fields.JSONField(blank=True, null=True)), ( "statement_descriptor", models.CharField( blank=True, help_text="For card charges, use statement_descriptor_suffix instead. Otherwise, you can use this value as the complete description of a charge on your customers' statements. Must contain at least one letter, maximum 22 characters.", max_length=22, null=True, ), ), ( "status", djstripe.fields.StripeEnumField( enum=djstripe.enums.ChargeStatus, max_length=9 ), ), ( "transfer_group", models.CharField( blank=True, help_text="A string that identifies this transaction as part of a group.", max_length=255, null=True, ), ), ( "on_behalf_of", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="charges", to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The account (if any) the charge was made on behalf of without triggering an automatic transfer.", ), ), ( "amount_captured", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11, null=True ), ), ( "application", models.CharField( blank=True, help_text="ID of the Connect application that created the charge.", max_length=255, ), ), ( "application_fee_amount", djstripe.fields.StripeDecimalCurrencyAmountField( blank=True, decimal_places=2, max_digits=11, null=True ), ), ( "billing_details", djstripe.fields.JSONField(null=True), ), ( "calculated_statement_descriptor", models.CharField( default="", help_text="The full statement descriptor that is passed to card networks, and that is displayed on your customers' credit card and bank statements. Allows you to see what the statement descriptor looks like after the static and dynamic portions are combined.", max_length=22, ), ), ( "disputed", models.BooleanField( default=False, help_text="Whether the charge has been disputed." ), ), ( "statement_descriptor_suffix", models.CharField( blank=True, help_text="Provides information about the charge that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that's set on the account to form the complete statement descriptor. Maximum 22 characters for the concatenated descriptor.", max_length=22, null=True, ), ), ( "transfer_data", djstripe.fields.JSONField(blank=True, null=True), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="Coupon", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ("id", djstripe.fields.StripeIdField(max_length=500)), ( "amount_off", djstripe.fields.StripeDecimalCurrencyAmountField( blank=True, decimal_places=2, max_digits=11, null=True ), ), ( "currency", djstripe.fields.StripeCurrencyCodeField( blank=True, max_length=3, null=True ), ), ( "duration", djstripe.fields.StripeEnumField( default="once", enum=djstripe.enums.CouponDuration, max_length=9 ), ), ( "duration_in_months", models.PositiveIntegerField( blank=True, help_text="If `duration` is `repeating`, the number of months the coupon applies.", null=True, ), ), ( "max_redemptions", models.PositiveIntegerField( blank=True, help_text="Maximum number of times this coupon can be redeemed, in total, before it is no longer valid.", null=True, ), ), ( "percent_off", djstripe.fields.StripePercentField( blank=True, decimal_places=2, max_digits=5, null=True, validators=[ django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100), ], ), ), ( "redeem_by", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "times_redeemed", models.PositiveIntegerField( default=0, editable=False, help_text="Number of times this coupon has been applied to a customer.", ), ), ( "name", models.TextField( blank=True, default="", help_text="Name of the coupon displayed to customers on for instance invoices or receipts.", max_length=5000, ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={ "get_latest_by": "created", "unique_together": {("id", "livemode")}, }, ), migrations.CreateModel( name="PaymentMethod", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ("billing_details", djstripe.fields.JSONField()), ("card", djstripe.fields.JSONField(blank=True, null=True)), ("card_present", djstripe.fields.JSONField(blank=True, null=True)), ( "type", djstripe.fields.StripeEnumField( enum=djstripe.enums.PaymentMethodType, max_length=15 ), ), ("alipay", djstripe.fields.JSONField(blank=True, null=True)), ("au_becs_debit", djstripe.fields.JSONField(blank=True, null=True)), ("bacs_debit", djstripe.fields.JSONField(blank=True, null=True)), ("bancontact", djstripe.fields.JSONField(blank=True, null=True)), ("eps", djstripe.fields.JSONField(blank=True, null=True)), ("fpx", djstripe.fields.JSONField(blank=True, null=True)), ("giropay", djstripe.fields.JSONField(blank=True, null=True)), ("ideal", djstripe.fields.JSONField(blank=True, null=True)), ("interac_present", djstripe.fields.JSONField(blank=True, null=True)), ("oxxo", djstripe.fields.JSONField(blank=True, null=True)), ("p24", djstripe.fields.JSONField(blank=True, null=True)), ("sepa_debit", djstripe.fields.JSONField(blank=True, null=True)), ("sofort", djstripe.fields.JSONField(blank=True, null=True)), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="Customer", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "balance", djstripe.fields.StripeQuantumCurrencyAmountField(default=0), ), ( "currency", djstripe.fields.StripeCurrencyCodeField( blank=True, default="", max_length=3 ), ), ( "delinquent", models.BooleanField( default=False, help_text="Whether or not the latest charge for the customer's latest invoice has failed.", ), ), ( "coupon_start", djstripe.fields.StripeDateTimeField( blank=True, editable=False, null=True ), ), ( "coupon_end", djstripe.fields.StripeDateTimeField( blank=True, editable=False, null=True ), ), ("email", models.TextField(blank=True, default="", max_length=5000)), ("shipping", djstripe.fields.JSONField(blank=True, null=True)), ("date_purged", models.DateTimeField(editable=False, null=True)), ( "coupon", models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.coupon", ), ), ( "default_source", djstripe.fields.PaymentMethodForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="customers", to="djstripe.paymentmethod", ), ), ( "subscriber", models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="djstripe_customers", to=DJSTRIPE_SUBSCRIBER_MODEL, ), ), ("address", djstripe.fields.JSONField(blank=True, null=True)), ( "invoice_prefix", models.CharField( blank=True, default="", help_text="The prefix for the customer used to generate unique invoice numbers.", max_length=255, ), ), ("invoice_settings", djstripe.fields.JSONField(blank=True, null=True)), ( "name", models.TextField( blank=True, default="", help_text="The customer's full name or business name.", max_length=5000, ), ), ( "phone", models.TextField( blank=True, default="", help_text="The customer's phone number.", max_length=5000, ), ), ("preferred_locales", djstripe.fields.JSONField(blank=True, null=True)), ( "tax_exempt", djstripe.fields.StripeEnumField( default="", enum=djstripe.enums.CustomerTaxExempt, max_length=7 ), ), ( "default_payment_method", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="+", to="djstripe.paymentmethod", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="default payment method used for subscriptions and invoices for the customer.", ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={ "get_latest_by": "created", "unique_together": { ("subscriber", "livemode", "djstripe_owner_account") }, }, ), migrations.CreateModel( name="Dispute", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ("amount", djstripe.fields.StripeQuantumCurrencyAmountField()), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ("evidence", djstripe.fields.JSONField()), ("evidence_details", djstripe.fields.JSONField()), ( "is_charge_refundable", models.BooleanField( help_text="If true, it is still possible to refund the disputed payment. Once the payment has been fully refunded, no further funds will be withdrawn from your Stripe account as a result of this dispute." ), ), ( "reason", djstripe.fields.StripeEnumField( enum=djstripe.enums.DisputeReason, max_length=25 ), ), ( "status", djstripe.fields.StripeEnumField( enum=djstripe.enums.DisputeStatus, max_length=22 ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="Event", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "api_version", models.CharField( blank=True, help_text="the API version at which the event data was rendered. Blank for old entries only, all new entries will have this value", max_length=15, ), ), ("data", djstripe.fields.JSONField()), ( "request_id", models.CharField( blank=True, default="", help_text="Information about the request that triggered this event, for traceability purposes. If empty string then this is an old entry without that data. If Null then this is not an old entry, but a Stripe 'automated' event with no associated request.", max_length=50, ), ), ("idempotency_key", models.TextField(blank=True, default="")), ( "type", models.CharField( help_text="Stripe's event description code", max_length=250 ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="FileUpload", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "filename", models.CharField( help_text="A filename for the file, suitable for saving to a filesystem.", max_length=255, ), ), ( "purpose", djstripe.fields.StripeEnumField( enum=djstripe.enums.FilePurpose, max_length=35 ), ), ( "size", models.IntegerField( help_text="The size in bytes of the file upload object." ), ), ( "type", djstripe.fields.StripeEnumField( enum=djstripe.enums.FileType, max_length=4 ), ), ( "url", models.CharField( help_text="A read-only URL where the uploaded file can be accessed.", max_length=200, ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="DjstripePaymentMethod", fields=[ ( "id", models.CharField(max_length=255, primary_key=True, serialize=False), ), ("type", models.CharField(db_index=True, max_length=50)), ], ), migrations.CreateModel( name="Plan", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "active", models.BooleanField( help_text="Whether the plan can be used for new purchases." ), ), ( "aggregate_usage", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.PlanAggregateUsage, max_length=18, ), ), ( "amount", djstripe.fields.StripeDecimalCurrencyAmountField( blank=True, decimal_places=2, max_digits=11, null=True ), ), ( "amount_decimal", djstripe.fields.StripeDecimalCurrencyAmountField( blank=True, decimal_places=12, max_digits=19, null=True ), ), ( "billing_scheme", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.BillingScheme, max_length=8, ), ), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ( "interval", djstripe.fields.StripeEnumField( enum=djstripe.enums.PlanInterval, max_length=5 ), ), ( "interval_count", models.PositiveIntegerField( blank=True, help_text="The number of intervals (specified in the interval property) between each subscription billing.", null=True, ), ), ( "nickname", models.TextField( blank=True, default="", help_text="A brief description of the plan, hidden from customers.", max_length=5000, ), ), ("tiers", djstripe.fields.JSONField(blank=True, null=True)), ( "tiers_mode", djstripe.fields.StripeEnumField( blank=True, enum=djstripe.enums.PriceTiersMode, max_length=9, null=True, ), ), ("transform_usage", djstripe.fields.JSONField(blank=True, null=True)), ( "trial_period_days", models.IntegerField( blank=True, help_text="Number of trial period days granted when subscribing a customer to this plan. Null if the plan has no trial period.", null=True, ), ), ( "usage_type", djstripe.fields.StripeEnumField( default="licensed", enum=djstripe.enums.PriceUsageType, max_length=8, ), ), ], options={"ordering": ["amount"]}, ), migrations.CreateModel( name="Product", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "name", models.TextField( help_text="The product's name, meant to be displayable to the customer. Applicable to both `service` and `good` types.", max_length=5000, ), ), ( "type", djstripe.fields.StripeEnumField( enum=djstripe.enums.ProductType, max_length=7 ), ), ( "active", models.BooleanField( help_text="Whether the product is currently available for purchase. Only applicable to products of `type=good`.", null=True, ), ), ("attributes", djstripe.fields.JSONField(blank=True, null=True)), ( "caption", models.TextField( blank=True, default="", help_text="A short one-line description of the product, meant to be displayableto the customer. Only applicable to products of `type=good`.", max_length=5000, ), ), ("deactivate_on", djstripe.fields.JSONField(blank=True, null=True)), ("images", djstripe.fields.JSONField(blank=True, null=True)), ( "package_dimensions", djstripe.fields.JSONField(blank=True, null=True), ), ( "shippable", models.BooleanField( blank=True, help_text="Whether this product is a shipped good. Only applicable to products of `type=good`.", null=True, ), ), ( "url", models.CharField( blank=True, help_text="A URL of a publicly-accessible webpage for this product. Only applicable to products of `type=good`.", max_length=799, null=True, ), ), ( "statement_descriptor", models.CharField( blank=True, default="", help_text="Extra information about a product which will appear on your customer's credit card statement. In the case that multiple products are billed at once, the first statement descriptor will be used. Only available on products of type=`service`.", max_length=22, ), ), ("unit_label", models.CharField(blank=True, default="", max_length=12)), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="Subscription", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "application_fee_percent", djstripe.fields.StripePercentField( blank=True, decimal_places=2, max_digits=5, null=True, validators=[ django.core.validators.MinValueValidator(1.0), django.core.validators.MaxValueValidator(100.0), ], ), ), ( "collection_method", djstripe.fields.StripeEnumField( enum=djstripe.enums.InvoiceCollectionMethod, max_length=20 ), ), ( "billing_cycle_anchor", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "cancel_at_period_end", models.BooleanField( default=False, help_text="If the subscription has been canceled with the ``at_period_end`` flag set to true, ``cancel_at_period_end`` on the subscription will be true. You can use this attribute to determine whether a subscription that has a status of active is scheduled to be canceled at the end of the current period.", ), ), ( "canceled_at", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ("current_period_end", djstripe.fields.StripeDateTimeField()), ("current_period_start", djstripe.fields.StripeDateTimeField()), ( "days_until_due", models.IntegerField( blank=True, help_text="Number of days a customer has to pay invoices generated by this subscription. This value will be `null` for subscriptions where `billing=charge_automatically`.", null=True, ), ), ("discount", djstripe.fields.JSONField(blank=True, null=True)), ( "ended_at", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "next_pending_invoice_item_invoice", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "pending_invoice_item_interval", djstripe.fields.JSONField(blank=True, null=True), ), ("pending_update", djstripe.fields.JSONField(blank=True, null=True)), ( "quantity", models.IntegerField( blank=True, help_text="The quantity applied to this subscription. This value will be `null` for multi-plan subscriptions", null=True, ), ), ( "start_date", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "status", djstripe.fields.StripeEnumField( enum=djstripe.enums.SubscriptionStatus, max_length=18 ), ), ( "tax_percent", djstripe.fields.StripePercentField( blank=True, decimal_places=2, max_digits=5, null=True, validators=[ django.core.validators.MinValueValidator(1.0), django.core.validators.MaxValueValidator(100.0), ], ), ), ( "trial_end", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "trial_start", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "customer", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="subscriptions", to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The customer associated with this subscription.", ), ), ( "plan", models.ForeignKey( blank=True, help_text="The plan associated with this subscription. This value will be `null` for multi-plan subscriptions", null=True, on_delete=django.db.models.deletion.CASCADE, related_name="subscriptions", to="djstripe.plan", ), ), ( "billing_thresholds", djstripe.fields.JSONField(blank=True, null=True), ), ( "cancel_at", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="Transfer", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "amount", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11 ), ), ( "amount_reversed", djstripe.fields.StripeDecimalCurrencyAmountField( blank=True, decimal_places=2, max_digits=11, null=True ), ), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ("destination", djstripe.fields.StripeIdField(max_length=255)), ( "destination_payment", djstripe.fields.StripeIdField( blank=True, max_length=255, null=True ), ), ( "reversed", models.BooleanField( default=False, help_text="Whether or not the transfer has been fully reversed. If the transfer is only partially reversed, this attribute will still be false.", ), ), ( "source_transaction", djstripe.fields.StripeIdField(max_length=255, null=True), ), ( "source_type", djstripe.fields.StripeEnumField( enum=djstripe.enums.LegacySourceType, max_length=16 ), ), ( "transfer_group", models.CharField( blank=True, default="", help_text="A string that identifies this transaction as part of a group.", max_length=255, ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="WebhookEventTrigger", fields=[ ("id", models.BigAutoField(primary_key=True, serialize=False)), ( "remote_ip", models.GenericIPAddressField( help_text="IP address of the request client." ), ), ("headers", djstripe.fields.JSONField()), ("body", models.TextField(blank=True)), ( "valid", models.BooleanField( default=False, help_text="Whether or not the webhook event has passed validation", ), ), ( "processed", models.BooleanField( default=False, help_text="Whether or not the webhook event has been successfully processed", ), ), ("exception", models.CharField(blank=True, max_length=128)), ( "traceback", models.TextField( blank=True, help_text="Traceback if an exception was thrown during processing", ), ), ( "djstripe_version", models.CharField( default=djstripe.models.webhooks._get_version, help_text="The version of dj-stripe when the webhook was received", max_length=32, ), ), ("created", models.DateTimeField(auto_now_add=True)), ("updated", models.DateTimeField(auto_now=True)), ( "event", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.event", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Event object contained in the (valid) Webhook", ), ), ], ), migrations.AddField( model_name="paymentmethod", name="customer", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="payment_methods", to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Customer to which this PaymentMethod is saved. This will not be set when the PaymentMethod has not been saved to a Customer.", ), ), migrations.AddField( model_name="plan", name="product", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.product", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The product whose pricing this plan determines.", ), ), migrations.CreateModel( name="Invoice", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "amount_due", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11 ), ), ( "amount_paid", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11, null=True ), ), ( "amount_remaining", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11, null=True ), ), ( "application_fee_amount", djstripe.fields.StripeDecimalCurrencyAmountField( blank=True, decimal_places=2, max_digits=11, null=True ), ), ( "attempt_count", models.IntegerField( help_text="Number of payment attempts made for this invoice, from the perspective of the payment retry schedule. Any payment attempt counts as the first attempt, and subsequently only automatic retries increment the attempt count. In other words, manual payment attempts after the first attempt do not affect the retry schedule." ), ), ( "attempted", models.BooleanField( default=False, help_text="Whether or not an attempt has been made to pay the invoice. An invoice is not attempted until 1 hour after the ``invoice.created`` webhook, for example, so you might not want to display that invoice as unpaid to your users.", ), ), ( "collection_method", djstripe.fields.StripeEnumField( enum=djstripe.enums.InvoiceCollectionMethod, max_length=20, null=True, ), ), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ( "due_date", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "ending_balance", djstripe.fields.StripeQuantumCurrencyAmountField(null=True), ), ( "hosted_invoice_url", models.TextField( blank=True, default="", help_text="The URL for the hosted invoice page, which allows customers to view and pay an invoice. If the invoice has not been frozen yet, this will be null.", max_length=799, ), ), ( "invoice_pdf", models.TextField( blank=True, default="", help_text="The link to download the PDF for the invoice. If the invoice has not been frozen yet, this will be null.", max_length=799, ), ), ( "next_payment_attempt", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "number", models.CharField( blank=True, default="", help_text="A unique, identifying string that appears on emails sent to the customer for this invoice. This starts with the customer's unique invoice_prefix if it is specified.", max_length=64, ), ), ( "paid", models.BooleanField( default=False, help_text="Whether payment was successfully collected for this invoice. An invoice can be paid (most commonly) with a charge or with credit from the customer's account balance.", ), ), ("period_end", djstripe.fields.StripeDateTimeField()), ("period_start", djstripe.fields.StripeDateTimeField()), ( "receipt_number", models.CharField( blank=True, help_text="This is the transaction number that appears on email receipts sent for this invoice.", max_length=64, null=True, ), ), ( "starting_balance", djstripe.fields.StripeQuantumCurrencyAmountField(), ), ( "statement_descriptor", models.CharField( blank=True, default="", help_text="An arbitrary string to be displayed on your customer's credit card statement. The statement description may not include <>\"' characters, and will appear on your customer's statement in capital letters. Non-ASCII characters are automatically stripped. While most banks display this information consistently, some may display it incorrectly or not at all.", max_length=22, ), ), ( "subscription_proration_date", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "subtotal", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11 ), ), ( "tax", djstripe.fields.StripeDecimalCurrencyAmountField( blank=True, decimal_places=2, max_digits=11, null=True ), ), ( "tax_percent", djstripe.fields.StripePercentField( blank=True, decimal_places=2, max_digits=5, null=True, validators=[ django.core.validators.MinValueValidator(1.0), django.core.validators.MaxValueValidator(100.0), ], ), ), ( "total", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11, verbose_name="Total (as decimal) after discount.", ), ), ( "webhooks_delivered_at", djstripe.fields.StripeDateTimeField(null=True), ), ( "charge", models.OneToOneField( help_text="The latest charge generated for this invoice, if any.", null=True, on_delete=django.db.models.deletion.CASCADE, related_name="latest_%(class)s", to="djstripe.charge", ), ), ( "customer", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="%(class)ss", to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The customer associated with this invoice.", ), ), ( "subscription", djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="%(class)ss", to="djstripe.subscription", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The subscription that this invoice was prepared for, if any.", ), ), ( "auto_advance", models.BooleanField( help_text="Controls whether Stripe will perform automatic collection of the invoice. When false, the invoice's state will not automatically advance without an explicit action.", null=True, ), ), ( "status_transitions", djstripe.fields.JSONField(blank=True, null=True), ), ( "account_country", models.CharField( blank=True, default="", help_text="The country of the business associated with this invoice, most often the business creating the invoice.", max_length=2, ), ), ( "account_name", models.TextField( blank=True, help_text="The public name of the business associated with this invoice, most often the business creating the invoice.", max_length=5000, ), ), ( "billing_reason", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.InvoiceBillingReason, max_length=22, ), ), ( "customer_address", djstripe.fields.JSONField(blank=True, null=True), ), ( "customer_email", models.TextField( blank=True, help_text="The customer's email. Until the invoice is finalized, this field will equal customer.email. Once the invoice is finalized, this field will no longer be updated.", max_length=5000, ), ), ( "customer_name", models.TextField( blank=True, help_text="The customer's name. Until the invoice is finalized, this field will equal customer.name. Once the invoice is finalized, this field will no longer be updated.", max_length=5000, ), ), ( "customer_phone", models.TextField( blank=True, help_text="The customer's phone number. Until the invoice is finalized, this field will equal customer.phone. Once the invoice is finalized, this field will no longer be updated.", max_length=5000, ), ), ( "customer_shipping", djstripe.fields.JSONField(blank=True, null=True), ), ( "customer_tax_exempt", djstripe.fields.StripeEnumField( default="", enum=djstripe.enums.CustomerTaxExempt, max_length=7 ), ), ( "footer", models.TextField( blank=True, help_text="Footer displayed on the invoice.", max_length=5000, ), ), ( "post_payment_credit_notes_amount", djstripe.fields.StripeQuantumCurrencyAmountField( blank=True, null=True ), ), ( "pre_payment_credit_notes_amount", djstripe.fields.StripeQuantumCurrencyAmountField( blank=True, null=True ), ), ( "threshold_reason", djstripe.fields.JSONField(blank=True, null=True), ), ( "status", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.InvoiceStatus, max_length=13, ), ), ( "discount", djstripe.fields.JSONField(blank=True, null=True), ), ], options={"get_latest_by": "created", "ordering": ["-created"]}, ), migrations.CreateModel( name="IdempotencyKey", fields=[ ( "uuid", models.UUIDField( default=uuid.uuid4, editable=False, primary_key=True, serialize=False, ), ), ("action", models.CharField(max_length=100)), ( "livemode", models.BooleanField( help_text="Whether the key was used in live or test mode." ), ), ("created", models.DateTimeField(auto_now_add=True)), ], options={"unique_together": {("action", "livemode")}}, ), migrations.AddField( model_name="charge", name="customer", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="charges", to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The customer associated with this charge.", ), ), migrations.AddField( model_name="charge", name="dispute", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="charges", to="djstripe.dispute", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Details about the dispute if the charge has been disputed.", ), ), migrations.AddField( model_name="charge", name="invoice", field=djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, related_name="charges", to="djstripe.invoice", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The invoice this charge is for if one exists.", ), ), migrations.AddField( model_name="charge", name="source", field=djstripe.fields.PaymentMethodForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="charges", to="djstripe.paymentmethod", ), ), migrations.AddField( model_name="charge", name="transfer", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.transfer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The transfer to the `destination` account (only applicable if the charge was created using the `destination` parameter).", ), ), migrations.CreateModel( name="BankAccount", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "account_holder_name", models.TextField( blank=True, help_text="The name of the person or business that owns the bank account.", max_length=5000, ), ), ( "account_holder_type", djstripe.fields.StripeEnumField( enum=djstripe.enums.BankAccountHolderType, max_length=10 ), ), ( "bank_name", models.CharField( help_text="Name of the bank associated with the routing number (e.g., `WELLS FARGO`).", max_length=255, ), ), ( "country", models.CharField( help_text="Two-letter ISO code representing the country the bank account is located in.", max_length=2, ), ), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ( "default_for_currency", models.BooleanField( help_text="Whether this external account is the default account for its currency.", null=True, ), ), ( "fingerprint", models.CharField( help_text="Uniquely identifies this particular bank account. You can use this attribute to check whether two bank accounts are the same.", max_length=16, ), ), ("last4", models.CharField(max_length=4)), ( "routing_number", models.CharField( help_text="The routing transit number for the bank account.", max_length=255, ), ), ( "status", djstripe.fields.StripeEnumField( enum=djstripe.enums.BankAccountStatus, max_length=19 ), ), ( "account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name="bank_account", to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "customer", djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="bank_account", to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="BalanceTransaction", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ("amount", djstripe.fields.StripeQuantumCurrencyAmountField()), ("available_on", djstripe.fields.StripeDateTimeField()), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ( "exchange_rate", models.DecimalField(decimal_places=6, max_digits=8, null=True), ), ("fee", djstripe.fields.StripeQuantumCurrencyAmountField()), ("fee_details", djstripe.fields.JSONField()), ("net", djstripe.fields.StripeQuantumCurrencyAmountField()), ( "status", djstripe.fields.StripeEnumField( enum=djstripe.enums.BalanceTransactionStatus, max_length=9 ), ), ( "type", djstripe.fields.StripeEnumField( enum=djstripe.enums.BalanceTransactionType, max_length=29 ), ), ( "reporting_category", djstripe.fields.StripeEnumField( enum=djstripe.enums.BalanceTransactionReportingCategory, max_length=29, ), ), ("source", djstripe.fields.StripeIdField(max_length=255)), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="ApplicationFee", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ("amount", djstripe.fields.StripeQuantumCurrencyAmountField()), ("amount_refunded", djstripe.fields.StripeQuantumCurrencyAmountField()), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ( "refunded", models.BooleanField( help_text="Whether the fee has been fully refunded. If the fee is only partially refunded, this attribute will still be false." ), ), ( "balance_transaction", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, to="djstripe.balancetransaction", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Balance transaction that describes the impact on your account balance.", ), ), ( "charge", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, to="djstripe.charge", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The charge that the application fee was taken from.", ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.AddField( model_name="charge", name="balance_transaction", field=djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.balancetransaction", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The balance transaction that describes the impact of this charge on your account balance (not including refunds or disputes).", ), ), migrations.AddField( model_name="transfer", name="balance_transaction", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.balancetransaction", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Balance transaction that describes the impact on your account balance.", ), ), migrations.CreateModel( name="SetupIntent", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "application", models.CharField( blank=True, help_text="ID of the Connect application that created the SetupIntent.", max_length=255, ), ), ( "cancellation_reason", djstripe.fields.StripeEnumField( blank=True, enum=djstripe.enums.SetupIntentCancellationReason, max_length=21, ), ), ( "client_secret", models.TextField( blank=True, help_text="The client secret of this SetupIntent. Used for client-side retrieval using a publishable key.", max_length=5000, ), ), ("last_setup_error", djstripe.fields.JSONField(blank=True, null=True)), ("next_action", djstripe.fields.JSONField(blank=True, null=True)), ("payment_method_types", djstripe.fields.JSONField()), ( "status", djstripe.fields.StripeEnumField( enum=djstripe.enums.SetupIntentStatus, max_length=23 ), ), ( "usage", djstripe.fields.StripeEnumField( default="off_session", enum=djstripe.enums.IntentUsage, max_length=11, ), ), ( "customer", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Customer this SetupIntent belongs to, if one exists.", ), ), ( "on_behalf_of", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="setup_intents", to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The account (if any) for which the setup is intended.", ), ), ( "payment_method", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.paymentmethod", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Payment method used in this PaymentIntent.", ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="PaymentIntent", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ("amount", djstripe.fields.StripeQuantumCurrencyAmountField()), ( "amount_capturable", djstripe.fields.StripeQuantumCurrencyAmountField(), ), ("amount_received", djstripe.fields.StripeQuantumCurrencyAmountField()), ( "canceled_at", djstripe.fields.StripeDateTimeField( blank=True, default=None, null=True ), ), ( "cancellation_reason", djstripe.fields.StripeEnumField( blank=True, enum=djstripe.enums.PaymentIntentCancellationReason, max_length=21, ), ), ( "capture_method", djstripe.fields.StripeEnumField( enum=djstripe.enums.CaptureMethod, max_length=9 ), ), ( "client_secret", models.TextField( help_text="The client secret of this PaymentIntent. Used for client-side retrieval using a publishable key.", max_length=5000, ), ), ( "confirmation_method", djstripe.fields.StripeEnumField( enum=djstripe.enums.ConfirmationMethod, max_length=9 ), ), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ( "description", models.TextField( blank=True, default="", help_text="An arbitrary string attached to the object. Often useful for displaying to users.", max_length=1000, ), ), ( "last_payment_error", djstripe.fields.JSONField(blank=True, null=True), ), ("next_action", djstripe.fields.JSONField(blank=True, null=True)), ("payment_method_types", djstripe.fields.JSONField()), ( "receipt_email", models.CharField( blank=True, help_text="Email address that the receipt for the resulting payment will be sent to.", max_length=255, ), ), ( "setup_future_usage", djstripe.fields.StripeEnumField( blank=True, enum=djstripe.enums.IntentUsage, max_length=11, null=True, ), ), ("shipping", djstripe.fields.JSONField(blank=True, null=True)), ( "statement_descriptor", models.CharField( blank=True, help_text="For non-card charges, you can use this value as the complete description that appears on your customers' statements. Must contain at least one letter, maximum 22 characters.", max_length=22, ), ), ( "status", djstripe.fields.StripeEnumField( enum=djstripe.enums.PaymentIntentStatus, max_length=23 ), ), ("transfer_data", djstripe.fields.JSONField(blank=True, null=True)), ( "transfer_group", models.CharField( blank=True, help_text="A string that identifies the resulting payment as part of a group. See the PaymentIntents Connect usage guide for details.", max_length=255, ), ), ( "customer", djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Customer this PaymentIntent is for if one exists.", ), ), ( "on_behalf_of", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="payment_intents", to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The account (if any) for which the funds of the PaymentIntent are intended.", ), ), ( "payment_method", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.paymentmethod", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Payment method used in this PaymentIntent.", ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.AddField( model_name="charge", name="payment_intent", field=djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="charges", to="djstripe.paymentintent", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="PaymentIntent associated with this charge, if one exists.", ), ), migrations.AddField( model_name="invoice", name="payment_intent", field=models.OneToOneField( help_text="The PaymentIntent associated with this invoice. The PaymentIntent is generated when the invoice is finalized, and can then be used to pay the invoice.Note that voiding an invoice will cancel the PaymentIntent", null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.paymentintent", ), ), migrations.AddField( model_name="subscription", name="pending_setup_intent", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="setup_intents", to="djstripe.setupintent", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="We can use this SetupIntent to collect user authentication when creating a subscription without immediate payment or updating a subscription's payment method, allowing you to optimize for off-session payments.", ), ), migrations.AddField( model_name="charge", name="payment_method", field=djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="charges", to="djstripe.paymentmethod", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="PaymentMethod used in this charge.", ), ), migrations.AddField( model_name="invoice", name="default_payment_method", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="+", to="djstripe.paymentmethod", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Default payment method for the invoice. It must belong to the customer associated with the invoice. If not set, defaults to the subscription's default payment method, if any, or to the default payment method in the customer's invoice settings.", ), ), migrations.CreateModel( name="UpcomingInvoice", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "account_country", models.CharField( blank=True, default="", help_text="The country of the business associated with this invoice, most often the business creating the invoice.", max_length=2, ), ), ( "account_name", models.TextField( blank=True, help_text="The public name of the business associated with this invoice, most often the business creating the invoice.", max_length=5000, ), ), ( "amount_due", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11 ), ), ( "amount_paid", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11, null=True ), ), ( "amount_remaining", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11, null=True ), ), ( "application_fee_amount", djstripe.fields.StripeDecimalCurrencyAmountField( blank=True, decimal_places=2, max_digits=11, null=True ), ), ( "attempt_count", models.IntegerField( help_text="Number of payment attempts made for this invoice, from the perspective of the payment retry schedule. Any payment attempt counts as the first attempt, and subsequently only automatic retries increment the attempt count. In other words, manual payment attempts after the first attempt do not affect the retry schedule." ), ), ( "attempted", models.BooleanField( default=False, help_text="Whether or not an attempt has been made to pay the invoice. An invoice is not attempted until 1 hour after the ``invoice.created`` webhook, for example, so you might not want to display that invoice as unpaid to your users.", ), ), ( "auto_advance", models.BooleanField( help_text="Controls whether Stripe will perform automatic collection of the invoice. When false, the invoice's state will not automatically advance without an explicit action.", null=True, ), ), ( "billing_reason", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.InvoiceBillingReason, max_length=22, ), ), ( "collection_method", djstripe.fields.StripeEnumField( enum=djstripe.enums.InvoiceCollectionMethod, max_length=20, null=True, ), ), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ("customer_address", djstripe.fields.JSONField(blank=True, null=True)), ( "customer_email", models.TextField( blank=True, help_text="The customer's email. Until the invoice is finalized, this field will equal customer.email. Once the invoice is finalized, this field will no longer be updated.", max_length=5000, ), ), ( "customer_name", models.TextField( blank=True, help_text="The customer's name. Until the invoice is finalized, this field will equal customer.name. Once the invoice is finalized, this field will no longer be updated.", max_length=5000, ), ), ( "customer_phone", models.TextField( blank=True, help_text="The customer's phone number. Until the invoice is finalized, this field will equal customer.phone. Once the invoice is finalized, this field will no longer be updated.", max_length=5000, ), ), ("customer_shipping", djstripe.fields.JSONField(blank=True, null=True)), ( "customer_tax_exempt", djstripe.fields.StripeEnumField( default="", enum=djstripe.enums.CustomerTaxExempt, max_length=7 ), ), ( "due_date", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "ending_balance", djstripe.fields.StripeQuantumCurrencyAmountField(null=True), ), ( "footer", models.TextField( blank=True, help_text="Footer displayed on the invoice.", max_length=5000, ), ), ( "hosted_invoice_url", models.TextField( blank=True, default="", help_text="The URL for the hosted invoice page, which allows customers to view and pay an invoice. If the invoice has not been frozen yet, this will be null.", max_length=799, ), ), ( "invoice_pdf", models.TextField( blank=True, default="", help_text="The link to download the PDF for the invoice. If the invoice has not been frozen yet, this will be null.", max_length=799, ), ), ( "next_payment_attempt", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "number", models.CharField( blank=True, default="", help_text="A unique, identifying string that appears on emails sent to the customer for this invoice. This starts with the customer's unique invoice_prefix if it is specified.", max_length=64, ), ), ( "paid", models.BooleanField( default=False, help_text="Whether payment was successfully collected for this invoice. An invoice can be paid (most commonly) with a charge or with credit from the customer's account balance.", ), ), ("period_end", djstripe.fields.StripeDateTimeField()), ("period_start", djstripe.fields.StripeDateTimeField()), ( "post_payment_credit_notes_amount", djstripe.fields.StripeQuantumCurrencyAmountField( blank=True, null=True ), ), ( "pre_payment_credit_notes_amount", djstripe.fields.StripeQuantumCurrencyAmountField( blank=True, null=True ), ), ( "receipt_number", models.CharField( blank=True, help_text="This is the transaction number that appears on email receipts sent for this invoice.", max_length=64, null=True, ), ), ( "starting_balance", djstripe.fields.StripeQuantumCurrencyAmountField(), ), ( "statement_descriptor", models.CharField( blank=True, default="", help_text="An arbitrary string to be displayed on your customer's credit card statement. The statement description may not include <>\"' characters, and will appear on your customer's statement in capital letters. Non-ASCII characters are automatically stripped. While most banks display this information consistently, some may display it incorrectly or not at all.", max_length=22, ), ), ( "status_transitions", djstripe.fields.JSONField(blank=True, null=True), ), ( "subscription_proration_date", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "subtotal", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11 ), ), ( "tax", djstripe.fields.StripeDecimalCurrencyAmountField( blank=True, decimal_places=2, max_digits=11, null=True ), ), ( "tax_percent", djstripe.fields.StripePercentField( blank=True, decimal_places=2, max_digits=5, null=True, validators=[ django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100), ], ), ), ("threshold_reason", djstripe.fields.JSONField(blank=True, null=True)), ( "total", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11, verbose_name="Total (as decimal) after discount.", ), ), ( "webhooks_delivered_at", djstripe.fields.StripeDateTimeField(null=True), ), ( "charge", models.OneToOneField( help_text="The latest charge generated for this invoice, if any.", null=True, on_delete=django.db.models.deletion.CASCADE, related_name="latest_%(class)s", to="djstripe.charge", ), ), ( "customer", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="%(class)ss", to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The customer associated with this invoice.", ), ), ( "default_payment_method", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="+", to="djstripe.paymentmethod", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Default payment method for the invoice. It must belong to the customer associated with the invoice. If not set, defaults to the subscription's default payment method, if any, or to the default payment method in the customer's invoice settings.", ), ), ( "payment_intent", models.OneToOneField( help_text="The PaymentIntent associated with this invoice. The PaymentIntent is generated when the invoice is finalized, and can then be used to pay the invoice.Note that voiding an invoice will cancel the PaymentIntent", null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.paymentintent", ), ), ( "subscription", djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="%(class)ss", to="djstripe.subscription", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The subscription that this invoice was prepared for, if any.", ), ), ( "status", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.InvoiceStatus, max_length=13, ), ), ( "default_source", djstripe.fields.PaymentMethodForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="upcoming_invoices", to="djstripe.djstripepaymentmethod", ), ), ("discount", djstripe.fields.JSONField(blank=True, null=True)), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"get_latest_by": "created", "ordering": ["-created"]}, ), migrations.CreateModel( name="TaxRate", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "active", models.BooleanField( default=True, help_text="Defaults to true. When set to false, this tax rate cannot be applied to objects in the API, but will still be applied to subscriptions and invoices that already have it set.", ), ), ( "display_name", models.CharField( blank=True, default="", help_text="The display name of the tax rates as it will appear to your customer on their receipt email, PDF, and the hosted invoice page.", max_length=50, ), ), ( "inclusive", models.BooleanField( help_text="This specifies if the tax rate is inclusive or exclusive." ), ), ( "jurisdiction", models.CharField( blank=True, default="", help_text="The jurisdiction for the tax rate.", max_length=50, ), ), ( "percentage", djstripe.fields.StripePercentField( decimal_places=2, max_digits=5, validators=[ django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100), ], ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={ "get_latest_by": "created", "verbose_name": "Tax Rate", }, ), migrations.AddField( model_name="invoice", name="default_tax_rates", field=models.ManyToManyField( blank=True, db_table="djstripe_djstripeinvoicedefaulttaxrate", help_text="The tax rates applied to this invoice, if any.", related_name="+", to="djstripe.TaxRate", ), ), migrations.AddField( model_name="subscription", name="default_tax_rates", field=models.ManyToManyField( blank=True, db_table="djstripe_djstripesubscriptiondefaulttaxrate", help_text="The tax rates that will apply to any subscription item that does not have tax_rates set. Invoices created will have their default_tax_rates populated from the subscription.", related_name="+", to="djstripe.TaxRate", ), ), migrations.CreateModel( name="DjstripeUpcomingInvoiceTotalTaxAmount", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("amount", djstripe.fields.StripeQuantumCurrencyAmountField()), ( "inclusive", models.BooleanField( help_text="Whether this tax amount is inclusive or exclusive." ), ), ( "invoice", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="+", to="djstripe.upcominginvoice", ), ), ( "tax_rate", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, to="djstripe.taxrate", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The tax rate that was applied to get this tax amount.", ), ), ], options={"unique_together": {("invoice", "tax_rate")}}, ), migrations.CreateModel( name="DjstripeInvoiceTotalTaxAmount", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("amount", djstripe.fields.StripeQuantumCurrencyAmountField()), ( "inclusive", models.BooleanField( help_text="Whether this tax amount is inclusive or exclusive." ), ), ( "invoice", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="total_tax_amounts", to="djstripe.invoice", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "tax_rate", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, to="djstripe.taxrate", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The tax rate that was applied to get this tax amount.", ), ), ], options={"unique_together": {("invoice", "tax_rate")}}, ), migrations.AddField( model_name="subscription", name="default_payment_method", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="+", to="djstripe.paymentmethod", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The default payment method for the subscription. It must belong to the customer associated with the subscription. If not set, invoices will use the default payment method in the customer's invoice settings.", ), ), migrations.AddField( model_name="invoice", name="default_source", field=djstripe.fields.PaymentMethodForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="invoices", to="djstripe.djstripepaymentmethod", ), ), migrations.AddField( model_name="subscription", name="default_source", field=djstripe.fields.PaymentMethodForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="subscriptions", to="djstripe.djstripepaymentmethod", ), ), migrations.CreateModel( name="ApplicationFeeRefund", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ("amount", djstripe.fields.StripeQuantumCurrencyAmountField()), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ( "balance_transaction", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, to="djstripe.balancetransaction", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Balance transaction that describes the impact on your account balance.", ), ), ( "fee", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="refunds", to="djstripe.applicationfee", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The application fee that was refunded", ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="Card", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "address_city", models.TextField( blank=True, default="", help_text="City/District/Suburb/Town/Village.", max_length=5000, ), ), ( "address_country", models.TextField( blank=True, default="", help_text="Billing address country.", max_length=5000, ), ), ( "address_line1", models.TextField( blank=True, default="", help_text="Street address/PO Box/Company name.", max_length=5000, ), ), ( "address_line1_check", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.CardCheckResult, max_length=11, ), ), ( "address_line2", models.TextField( blank=True, default="", help_text="Apartment/Suite/Unit/Building.", max_length=5000, ), ), ( "address_state", models.TextField( blank=True, default="", help_text="State/County/Province/Region.", max_length=5000, ), ), ( "address_zip", models.TextField( blank=True, default="", help_text="ZIP or postal code.", max_length=5000, ), ), ( "address_zip_check", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.CardCheckResult, max_length=11, ), ), ( "brand", djstripe.fields.StripeEnumField( enum=djstripe.enums.CardBrand, max_length=16 ), ), ( "country", models.CharField( blank=True, default="", help_text="Two-letter ISO code representing the country of the card.", max_length=2, ), ), ( "cvc_check", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.CardCheckResult, max_length=11, ), ), ( "dynamic_last4", models.CharField( blank=True, default="", help_text="(For tokenized numbers only.) The last four digits of the device account number.", max_length=4, ), ), ("exp_month", models.IntegerField(help_text="Card expiration month.")), ("exp_year", models.IntegerField(help_text="Card expiration year.")), ( "fingerprint", models.CharField( blank=True, default="", help_text="Uniquely identifies this particular card number.", max_length=16, ), ), ( "funding", djstripe.fields.StripeEnumField( enum=djstripe.enums.CardFundingType, max_length=7 ), ), ( "last4", models.CharField( help_text="Last four digits of Card number.", max_length=4 ), ), ( "name", models.TextField( blank=True, default="", help_text="Cardholder name.", max_length=5000, ), ), ( "tokenization_method", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.CardTokenizationMethod, max_length=11, ), ), ( "customer", djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="legacy_cards", to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.AddField( model_name="charge", name="djstripe_owner_account", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), migrations.CreateModel( name="CountrySpec", fields=[ ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "id", models.CharField(max_length=2, primary_key=True, serialize=False), ), ( "default_currency", djstripe.fields.StripeCurrencyCodeField(max_length=3), ), ("supported_bank_account_currencies", djstripe.fields.JSONField()), ("supported_payment_currencies", djstripe.fields.JSONField()), ("supported_payment_methods", djstripe.fields.JSONField()), ("supported_transfer_countries", djstripe.fields.JSONField()), ("verification_fields", djstripe.fields.JSONField()), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ], options={"abstract": False}, ), migrations.AddField( model_name="invoice", name="djstripe_owner_account", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), migrations.AddField( model_name="paymentmethod", name="djstripe_owner_account", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), migrations.AddField( model_name="plan", name="djstripe_owner_account", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), migrations.CreateModel( name="Refund", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ("amount", djstripe.fields.StripeQuantumCurrencyAmountField()), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ( "failure_reason", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.RefundFailureReason, max_length=24, ), ), ( "reason", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.RefundReason, max_length=25, ), ), ( "receipt_number", models.CharField( blank=True, default="", help_text="The transaction number that appears on email receipts sent for this charge.", max_length=9, ), ), ( "status", djstripe.fields.StripeEnumField( blank=True, enum=djstripe.enums.RefundStatus, max_length=9 ), ), ( "charge", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="refunds", to="djstripe.charge", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The charge that was refunded", ), ), ( "balance_transaction", djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.balancetransaction", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Balance transaction that describes the impact on your account balance.", ), ), ( "failure_balance_transaction", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="failure_refunds", to="djstripe.balancetransaction", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="If the refund failed, this balance transaction describes the adjustment made on your account balance that reverses the initial balance transaction.", ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="ScheduledQueryRun", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ("data_load_time", djstripe.fields.StripeDateTimeField()), ("error", djstripe.fields.JSONField(blank=True, null=True)), ("result_available_until", djstripe.fields.StripeDateTimeField()), ( "sql", models.TextField(help_text="SQL for the query.", max_length=5000), ), ( "status", djstripe.fields.StripeEnumField( enum=djstripe.enums.ScheduledQueryRunStatus, max_length=9 ), ), ( "title", models.TextField(help_text="Title of the query.", max_length=5000), ), ( "file", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.fileupload", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The file object representing the results of the query.", ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="Session", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "billing_address_collection", djstripe.fields.StripeEnumField( blank=True, enum=djstripe.enums.SessionBillingAddressCollection, max_length=8, ), ), ( "cancel_url", models.TextField( blank=True, help_text="The URL the customer will be directed to if theydecide to cancel payment and return to your website.", max_length=5000, ), ), ( "client_reference_id", models.TextField( blank=True, help_text="A unique string to reference the Checkout Session.This can be a customer ID, a cart ID, or similar, andcan be used to reconcile the session with your internal systems.", max_length=5000, ), ), ( "customer_email", models.CharField( blank=True, help_text="If provided, this value will be used when the Customer object is created.", max_length=255, ), ), ("display_items", djstripe.fields.JSONField(blank=True, null=True)), ( "locale", models.CharField( blank=True, help_text="The IETF language tag of the locale Checkout is displayed in.If blank or auto, the browser's locale is used.", max_length=255, ), ), ("payment_method_types", djstripe.fields.JSONField()), ( "submit_type", djstripe.fields.StripeEnumField( blank=True, enum=djstripe.enums.SubmitTypeStatus, max_length=6 ), ), ( "success_url", models.TextField( blank=True, help_text="The URL the customer will be directed to after the payment or subscriptioncreation is successful.", max_length=5000, ), ), ( "customer", djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Customer this Checkout is for if one exists.", ), ), ( "payment_intent", djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.paymentintent", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="PaymentIntent created if SKUs or line items were provided.", ), ), ( "subscription", djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.subscription", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Subscription created if one or more plans were provided.", ), ), ( "mode", djstripe.fields.StripeEnumField( blank=True, enum=djstripe.enums.SessionMode, max_length=12 ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="Source", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "amount", djstripe.fields.StripeDecimalCurrencyAmountField( blank=True, decimal_places=2, max_digits=11, null=True ), ), ( "client_secret", models.CharField( help_text="The client secret of the source. Used for client-side retrieval using a publishable key.", max_length=255, ), ), ( "currency", djstripe.fields.StripeCurrencyCodeField( blank=True, default="", max_length=3 ), ), ( "flow", djstripe.fields.StripeEnumField( enum=djstripe.enums.SourceFlow, max_length=17 ), ), ("owner", djstripe.fields.JSONField()), ( "statement_descriptor", models.CharField( blank=True, default="", help_text="Extra information about a source. This will appear on your customer's statement every time you charge the source.", max_length=255, ), ), ( "status", djstripe.fields.StripeEnumField( enum=djstripe.enums.SourceStatus, max_length=10 ), ), ( "type", djstripe.fields.StripeEnumField( enum=djstripe.enums.SourceType, max_length=20 ), ), ( "usage", djstripe.fields.StripeEnumField( enum=djstripe.enums.SourceUsage, max_length=10 ), ), ("code_verification", djstripe.fields.JSONField(blank=True, null=True)), ("receiver", djstripe.fields.JSONField(blank=True, null=True)), ("redirect", djstripe.fields.JSONField(blank=True, null=True)), ("source_data", djstripe.fields.JSONField()), ( "customer", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="sources", to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.AddField( model_name="subscription", name="djstripe_owner_account", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), migrations.CreateModel( name="SubscriptionItem", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "quantity", models.PositiveIntegerField( blank=True, help_text="The quantity of the plan to which the customer should be subscribed.", null=True, ), ), ( "plan", models.ForeignKey( help_text="The plan the customer is subscribed to.", on_delete=django.db.models.deletion.CASCADE, related_name="subscription_items", to="djstripe.plan", ), ), ( "subscription", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="items", to="djstripe.subscription", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The subscription this subscription item belongs to.", ), ), ( "tax_rates", models.ManyToManyField( blank=True, db_table="djstripe_djstripesubscriptionitemtaxrate", help_text="The tax rates which apply to this subscription_item. When set, the default_tax_rates on the subscription do not apply to this subscription_item.", related_name="+", to="djstripe.TaxRate", ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ( "billing_thresholds", djstripe.fields.JSONField(blank=True, null=True), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.AddField( model_name="transfer", name="djstripe_owner_account", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), migrations.CreateModel( name="TransferReversal", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ("amount", djstripe.fields.StripeQuantumCurrencyAmountField()), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ( "balance_transaction", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="transfer_reversals", to="djstripe.balancetransaction", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Balance transaction that describes the impact on your account balance.", ), ), ( "transfer", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="reversals", to="djstripe.transfer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The transfer that was reversed.", ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="UsageRecord", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "quantity", models.PositiveIntegerField( help_text="The quantity of the plan to which the customer should be subscribed." ), ), ( "subscription_item", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="usage_records", to="djstripe.subscriptionitem", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The subscription item this usage record contains data for.", ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="Price", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "active", models.BooleanField( help_text="Whether the price can be used for new purchases." ), ), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ( "nickname", models.CharField( blank=True, help_text="A brief description of the plan, hidden from customers.", max_length=250, ), ), ( "recurring", djstripe.fields.JSONField(blank=True, default=None, null=True), ), ( "type", djstripe.fields.StripeEnumField( enum=djstripe.enums.PriceType, max_length=9 ), ), ( "unit_amount", djstripe.fields.StripeQuantumCurrencyAmountField( blank=True, null=True ), ), ( "unit_amount_decimal", djstripe.fields.StripeDecimalCurrencyAmountField( blank=True, decimal_places=12, max_digits=19, null=True ), ), ( "billing_scheme", djstripe.fields.StripeEnumField( blank=True, enum=djstripe.enums.BillingScheme, max_length=8 ), ), ("tiers", djstripe.fields.JSONField(blank=True, null=True)), ( "tiers_mode", djstripe.fields.StripeEnumField( blank=True, enum=djstripe.enums.PriceTiersMode, max_length=9, null=True, ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ( "product", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="prices", to="djstripe.product", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The product this price is associated with.", ), ), ( "lookup_key", models.CharField( blank=True, help_text="A lookup key used to retrieve prices dynamically from a static string.", max_length=250, null=True, ), ), ( "transform_quantity", djstripe.fields.JSONField(blank=True, null=True), ), ], options={"abstract": False, "ordering": ["unit_amount"]}, ), migrations.AddField( model_name="subscriptionitem", name="price", field=models.ForeignKey( blank=True, help_text="The price the customer is subscribed to.", null=True, on_delete=django.db.models.deletion.CASCADE, related_name="subscription_items", to="djstripe.price", ), ), migrations.CreateModel( name="TaxId", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "country", models.CharField( help_text="Two-letter ISO code representing the country of the tax ID.", max_length=2, ), ), ( "type", djstripe.fields.StripeEnumField( enum=djstripe.enums.TaxIdType, max_length=7 ), ), ( "value", models.CharField(help_text="Value of the tax ID.", max_length=50), ), ("verification", djstripe.fields.JSONField()), ( "customer", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="tax_ids", to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"get_latest_by": "created", "verbose_name": "Tax ID"}, ), migrations.CreateModel( name="InvoiceItem", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "amount", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11 ), ), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ("date", djstripe.fields.StripeDateTimeField()), ( "discountable", models.BooleanField( default=False, help_text="If True, discounts will apply to this invoice item. Always False for prorations.", ), ), ("period", djstripe.fields.JSONField()), ("period_end", djstripe.fields.StripeDateTimeField()), ("period_start", djstripe.fields.StripeDateTimeField()), ( "proration", models.BooleanField( default=False, help_text="Whether or not the invoice item was created automatically as a proration adjustment when the customer switched plans.", ), ), ( "quantity", models.IntegerField( blank=True, help_text="If the invoice item is a proration, the quantity of the subscription for which the proration was computed.", null=True, ), ), ( "customer", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="invoiceitems", to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The customer associated with this invoiceitem.", ), ), ( "invoice", djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, related_name="invoiceitems", to="djstripe.invoice", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The invoice to which this invoiceitem is attached.", ), ), ( "plan", models.ForeignKey( help_text="If the invoice item is a proration, the plan of the subscription for which the proration was computed.", null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.plan", ), ), ( "subscription", djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="invoiceitems", to="djstripe.subscription", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The subscription that this invoice item has been created for, if any.", ), ), ( "tax_rates", models.ManyToManyField( blank=True, db_table="djstripe_djstripeinvoiceitemtaxrate", help_text="The tax rates which apply to this invoice item. When set, the default_tax_rates on the invoice do not apply to this invoice item.", related_name="+", to="djstripe.TaxRate", ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ( "unit_amount", djstripe.fields.StripeQuantumCurrencyAmountField( blank=True, null=True ), ), ( "unit_amount_decimal", djstripe.fields.StripeDecimalCurrencyAmountField( blank=True, decimal_places=12, max_digits=19, null=True ), ), ( "price", models.ForeignKey( help_text="If the invoice item is a proration, the price of the subscription for which the proration was computed.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="invoiceitems", to="djstripe.price", ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.CreateModel( name="SubscriptionSchedule", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "canceled_at", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "completed_at", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ("current_phase", djstripe.fields.JSONField(blank=True, null=True)), ("default_settings", djstripe.fields.JSONField(blank=True, null=True)), ( "end_behavior", djstripe.fields.StripeEnumField( enum=djstripe.enums.SubscriptionScheduleEndBehavior, max_length=7, ), ), ("phases", djstripe.fields.JSONField(blank=True, null=True)), ( "released_at", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "status", djstripe.fields.StripeEnumField( enum=djstripe.enums.SubscriptionScheduleStatus, max_length=11 ), ), ( "customer", models.ForeignKey( help_text="The customer who owns the subscription schedule.", on_delete=django.db.models.deletion.CASCADE, related_name="schedules", to="djstripe.customer", ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ( "released_subscription", models.ForeignKey( blank=True, help_text="The subscription once managed by this subscription schedule (if it is released).", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="released_schedules", to="djstripe.subscription", ), ), ( "billing_thresholds", djstripe.fields.JSONField(blank=True, null=True), ), ], options={"get_latest_by": "created", "abstract": False}, ), migrations.AddField( model_name="subscription", name="schedule", field=models.ForeignKey( blank=True, help_text="The schedule associated with this subscription.", null=True, on_delete=django.db.models.deletion.CASCADE, related_name="subscriptions", to="djstripe.subscriptionschedule", ), ), migrations.CreateModel( name="Payout", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "amount", djstripe.fields.StripeDecimalCurrencyAmountField( decimal_places=2, max_digits=11 ), ), ("arrival_date", djstripe.fields.StripeDateTimeField()), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ( "failure_code", djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.PayoutFailureCode, max_length=23, ), ), ( "failure_message", models.TextField( blank=True, default="", help_text="Message to user further explaining reason for payout failure if available.", ), ), ( "method", djstripe.fields.StripeEnumField( enum=djstripe.enums.PayoutMethod, max_length=8 ), ), ( "statement_descriptor", models.CharField( blank=True, default="", help_text="Extra information about a payout to be displayed on the user's bank statement.", max_length=255, ), ), ( "status", djstripe.fields.StripeEnumField( enum=djstripe.enums.PayoutStatus, max_length=10 ), ), ( "type", djstripe.fields.StripeEnumField( enum=djstripe.enums.PayoutType, max_length=12 ), ), ( "destination", djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.PROTECT, to="djstripe.bankaccount", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Bank account or card the payout was sent to.", ), ), ( "balance_transaction", djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.balancetransaction", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Balance transaction that describes the impact on your account balance.", ), ), ( "failure_balance_transaction", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="failure_payouts", to="djstripe.balancetransaction", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="If the payout failed or was canceled, this will be the balance transaction that reversed the initial balance transaction, and puts the funds from the failed payout back in your balance.", ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ( "automatic", models.BooleanField( help_text="`true` if the payout was created by an automated payout schedule, and `false` if it was requested manually.", ), ), ( "source_type", djstripe.fields.StripeEnumField( enum=djstripe.enums.PayoutSourceType, max_length=12, ), ), ], options={"abstract": False, "get_latest_by": "created"}, ), migrations.AddField( model_name="charge", name="source_transfer", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="+", to="djstripe.transfer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The transfer which created this charge. Only present if the charge came from another Stripe account.", ), ), migrations.AddField( model_name="charge", name="application_fee", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="fee_for_charge", to="djstripe.applicationfee", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The application fee (if any) for the charge.", ), ), migrations.CreateModel( name="APIKey", fields=[ ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ( "livemode", models.BooleanField( help_text="Whether the key is valid for live or test mode." ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "id", models.CharField( default=djstripe.models.api.generate_api_key_id, editable=False, max_length=255, ), ), ( "type", djstripe.fields.StripeEnumField( enum=djstripe.enums.APIKeyType, max_length=11 ), ), ( "name", models.CharField( blank=True, help_text="An optional name to identify the key.", max_length=100, verbose_name="Key name", ), ), ( "secret", models.CharField( help_text="The value of the key.", max_length=128, unique=True, validators=[ django.core.validators.RegexValidator( regex="^(pk|sk|rk)_(test|live)_([a-zA-Z0-9]{24,99})" ) ], ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={"get_latest_by": "created", "abstract": False}, ), ] ================================================ FILE: djstripe/migrations/0008_2_5.py ================================================ # Generated by Django 3.2.3 on 2021-05-30 23:47 import django.db.models.deletion from django.conf import settings from django.db import migrations, models import djstripe.enums import djstripe.fields class Migration(migrations.Migration): dependencies = [ ("djstripe", "0001_initial"), ] operations = [ migrations.RemoveField( model_name="subscription", name="tax_percent", ), migrations.RemoveField( model_name="countryspec", name="djstripe_owner_account", ), migrations.AddField( model_name="card", name="account", field=djstripe.fields.StripeForeignKey( blank=True, help_text="The external account the charge was made on behalf of. Null here indicates that this value was never set.", null=True, on_delete=django.db.models.deletion.PROTECT, related_name="cards", to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), migrations.AddField( model_name="card", name="default_for_currency", field=models.BooleanField( help_text="Whether this external account (Card) is the default account for its currency.", null=True, ), ), migrations.AlterField( model_name="bankaccount", name="account", field=djstripe.fields.StripeForeignKey( blank=True, help_text="The external account the charge was made on behalf of. Null here indicates that this value was never set.", null=True, on_delete=django.db.models.deletion.PROTECT, related_name="bank_accounts", to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), migrations.AlterField( model_name="bankaccount", name="default_for_currency", field=models.BooleanField( help_text="Whether this external account (BankAccount) is the default account for its currency.", null=True, ), ), migrations.RenameModel( old_name="FileUpload", new_name="File", ), migrations.CreateModel( name="FileLink", fields=[ ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ( "created", djstripe.fields.StripeDateTimeField( blank=True, help_text="The datetime this object was created in stripe.", null=True, ), ), ( "metadata", djstripe.fields.JSONField( blank=True, help_text="A set of key/value pairs that you can attach to an object. It can be useful for storing additional information about an object in a structured format.", null=True, ), ), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ( "expires_at", djstripe.fields.StripeDateTimeField( blank=True, help_text="Time at which the link expires.", null=True, ), ), ( "url", models.URLField( help_text="The publicly accessible URL to download the file." ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, help_text="The Stripe Account this object belongs to.", null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "file", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, to="djstripe.file", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ], options={ "get_latest_by": "created", "abstract": False, }, ), migrations.CreateModel( name="Mandate", fields=[ ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ( "created", djstripe.fields.StripeDateTimeField( blank=True, help_text="The datetime this object was created in stripe.", null=True, ), ), ( "metadata", djstripe.fields.JSONField( blank=True, help_text="A set of key/value pairs that you can attach to an object. It can be useful for storing additional information about an object in a structured format.", null=True, ), ), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ( "customer_acceptance", djstripe.fields.JSONField( help_text="Details about the customer's acceptance of the mandate." ), ), ( "payment_method_details", djstripe.fields.JSONField( help_text="Additional mandate information specific to the payment method type." ), ), ( "status", djstripe.fields.StripeEnumField( enum=djstripe.enums.MandateStatus, help_text="The status of the mandate, which indicates whether it can be used to initiate a payment.", max_length=8, ), ), ( "type", djstripe.fields.StripeEnumField( enum=djstripe.enums.MandateType, help_text="The status of the mandate, which indicates whether it can be used to initiate a payment.", max_length=10, ), ), ( "multi_use", djstripe.fields.JSONField( blank=True, help_text="If this is a `multi_use` mandate, this hash contains details about the mandate.", null=True, ), ), ( "single_use", djstripe.fields.JSONField( blank=True, help_text="If this is a `single_use` mandate, this hash contains details about the mandate.", null=True, ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, help_text="The Stripe Account this object belongs to.", null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "payment_method", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, to="djstripe.paymentmethod", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ], options={ "get_latest_by": "created", "abstract": False, }, ), migrations.AlterField( model_name="charge", name="source", field=djstripe.fields.PaymentMethodForeignKey( blank=True, help_text="The source used for this charge.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="charges", to="djstripe.djstripepaymentmethod", ), ), migrations.AlterField( model_name="customer", name="default_source", field=djstripe.fields.PaymentMethodForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="customers", to="djstripe.djstripepaymentmethod", ), ), ] ================================================ FILE: djstripe/migrations/0009_2_6.py ================================================ # Generated by Django 3.2.4 on 2021-06-22 01:38 import uuid import django.core.validators import django.db.models.deletion from django.conf import settings from django.db import migrations, models import djstripe.enums import djstripe.fields class Migration(migrations.Migration): dependencies = [ ("djstripe", "0008_2_5"), ] operations = [ migrations.CreateModel( name="WebhookEndpoint", fields=[ ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ( "created", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "metadata", djstripe.fields.JSONField(blank=True, null=True), ), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ( "api_version", models.CharField( blank=True, help_text="The API version events are rendered as for this webhook endpoint.", max_length=10, ), ), ( "enabled_events", djstripe.fields.JSONField(), ), ( "secret", models.CharField( blank=True, help_text="The endpoint's secret, used to generate webhook signatures.", max_length=256, editable=False, ), ), ( "status", djstripe.fields.StripeEnumField( enum=djstripe.enums.WebhookEndpointStatus, max_length=8 ), ), ( "url", models.URLField( help_text="The URL of the webhook endpoint.", max_length=2048 ), ), ( "application", models.CharField( blank=True, help_text="The ID of the associated Connect application.", max_length=255, ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ( "djstripe_uuid", models.UUIDField( null=True, unique=True, default=uuid.uuid4, help_text="A UUID specific to dj-stripe generated for the endpoint", ), ), ], options={"get_latest_by": "created", "abstract": False}, ), migrations.CreateModel( name="UsageRecordSummary", fields=[ ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ( "created", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "period", djstripe.fields.JSONField(blank=True, null=True), ), ( "period_end", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "period_start", djstripe.fields.StripeDateTimeField(blank=True, null=True), ), ( "total_usage", models.PositiveIntegerField( help_text="The quantity of the plan to which the customer should be subscribed." ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ( "invoice", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="usage_record_summaries", to="djstripe.invoice", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "subscription_item", djstripe.fields.StripeForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="usage_record_summaries", to="djstripe.subscriptionitem", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The subscription item this usage record contains data for.", ), ), ], options={"get_latest_by": "created", "abstract": False}, ), migrations.AddField( model_name="applicationfee", name="account", field=djstripe.fields.StripeForeignKey( default=1, on_delete=django.db.models.deletion.PROTECT, related_name="application_fees", to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="ID of the Stripe account this fee was taken from.", ), preserve_default=False, ), migrations.AddField( model_name="customer", name="deleted", field=models.BooleanField( blank=True, default=False, help_text="Whether the Customer instance has been deleted upstream in Stripe or not.", null=True, ), ), migrations.AddField( model_name="dispute", name="balance_transaction", field=djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, related_name="disputes", to="djstripe.balancetransaction", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="Balance transaction that describes the impact on your account balance.", ), ), migrations.AddField( model_name="dispute", name="charge", field=djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, related_name="disputes", to="djstripe.charge", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The charge that was disputed", ), ), migrations.AddField( model_name="dispute", name="payment_intent", field=djstripe.fields.StripeForeignKey( null=True, on_delete=django.db.models.deletion.CASCADE, related_name="disputes", to="djstripe.paymentintent", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The PaymentIntent that was disputed", ), ), migrations.AddField( model_name="dispute", name="balance_transactions", field=djstripe.fields.JSONField( default=list, ), ), migrations.AlterField( model_name="taxrate", name="percentage", field=djstripe.fields.StripePercentField( decimal_places=4, max_digits=7, validators=[ django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100), ], ), ), migrations.AlterField( model_name="transfer", name="destination", field=djstripe.fields.StripeIdField( max_length=255, null=True, ), ), migrations.AddField( model_name="usagerecord", name="action", field=djstripe.fields.StripeEnumField( default="increment", enum=djstripe.enums.UsageAction, max_length=9, ), ), migrations.AddField( model_name="usagerecord", name="timestamp", field=djstripe.fields.StripeDateTimeField( blank=True, null=True, ), ), migrations.AddField( model_name="webhookeventtrigger", name="stripe_trigger_account", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), migrations.RemoveField(model_name="usagerecord", name="description"), migrations.RemoveField(model_name="usagerecord", name="metadata"), migrations.AlterField( model_name="paymentmethod", name="type", field=djstripe.fields.StripeEnumField( enum=djstripe.enums.PaymentMethodType, max_length=17 ), ), migrations.AddField( model_name="paymentmethod", name="acss_debit", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="paymentmethod", name="afterpay_clearpay", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="paymentmethod", name="boleto", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="paymentmethod", name="grabpay", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="paymentmethod", name="wechat_pay", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="subscription", name="latest_invoice", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="+", to="djstripe.invoice", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The most recent invoice this subscription has generated.", ), ), migrations.AlterField( model_name="customer", name="delinquent", field=models.BooleanField( blank=True, default=False, help_text="Whether or not the latest charge for the customer's latest invoice has failed.", null=True, ), ), ] ================================================ FILE: djstripe/migrations/0010_alter_customer_balance.py ================================================ # Generated by Django 4.0.1 on 2022-01-12 13:12 from django.db import migrations import djstripe.fields class Migration(migrations.Migration): dependencies = [ ("djstripe", "0009_2_6"), ] operations = [ migrations.AlterField( model_name="customer", name="balance", field=djstripe.fields.StripeQuantumCurrencyAmountField( blank=True, default=0, null=True ), ), ] ================================================ FILE: djstripe/migrations/0011_2_7.py ================================================ # Generated by Django 3.2.11 on 2022-01-19 04:59 import django.db.models.deletion from django.conf import settings from django.db import migrations, models import djstripe.enums import djstripe.fields class Migration(migrations.Migration): dependencies = [ ("djstripe", "0010_alter_customer_balance"), ] operations = [ migrations.RemoveField( model_name="subscriptionschedule", name="billing_thresholds", ), migrations.AddField( model_name="webhookeventtrigger", name="webhook_endpoint", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.webhookendpoint", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The endpoint this webhook was received on", ), ), migrations.AddField( model_name="account", name="djstripe_owner_account", field=djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), migrations.AlterField( model_name="event", name="api_version", field=models.CharField( blank=True, help_text="the API version at which the event data was rendered. Blank for old entries only, all new entries will have this value", max_length=64, ), ), migrations.AlterField( model_name="webhookendpoint", name="api_version", field=models.CharField( max_length=64, blank=True, help_text="The API version events are rendered as for this webhook endpoint. Defaults to the configured Stripe API Version.", ), ), migrations.AddField( model_name="coupon", name="applies_to", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="subscriptionschedule", name="subscription", field=models.ForeignKey( blank=True, help_text="ID of the subscription managed by the subscription schedule.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="subscriptions", to="djstripe.subscription", ), ), migrations.AddField( model_name="taxrate", name="country", field=models.CharField( blank=True, default="", help_text="Two-letter country code.", max_length=2, ), ), migrations.AddField( model_name="taxrate", name="state", field=models.CharField( blank=True, default="", help_text="ISO 3166-2 subdivision code, without country prefix.", max_length=2, ), ), migrations.AddField( model_name="taxrate", name="tax_type", field=models.CharField( blank=True, default="", help_text="The high-level tax type, such as vat, gst, sales_tax or custom.", max_length=50, ), ), migrations.AlterField( model_name="payout", name="failure_code", field=djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.PayoutFailureCode, max_length=32, ), ), migrations.AlterField( model_name="invoice", name="billing_reason", field=djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.InvoiceBillingReason, max_length=38, ), ), migrations.AlterField( model_name="upcominginvoice", name="billing_reason", field=djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.InvoiceBillingReason, max_length=38, ), ), migrations.AddField( model_name="subscriptionitem", name="proration_behavior", field=djstripe.fields.StripeEnumField( blank=True, default="create_prorations", enum=djstripe.enums.SubscriptionProrationBehavior, help_text="Determines how to handle prorations when the billing cycle changes (e.g., when switching plans, resetting billing_cycle_anchor=now, or starting a trial), or if an item’s quantity changes", max_length=17, ), ), migrations.AddField( model_name="subscriptionitem", name="proration_date", field=djstripe.fields.StripeDateTimeField( blank=True, help_text="If set, the proration will be calculated as though the subscription was updated at the given time. This can be used to apply exactly the same proration that was previewed with upcoming invoice endpoint. It can also be used to implement custom proration logic, such as prorating by day instead of by second, by providing the time that you wish to use for proration calculations", null=True, ), ), migrations.AddField( model_name="subscription", name="proration_behavior", field=djstripe.fields.StripeEnumField( blank=True, default="create_prorations", enum=djstripe.enums.SubscriptionProrationBehavior, help_text="Determines how to handle prorations when the billing cycle changes (e.g., when switching plans, resetting billing_cycle_anchor=now, or starting a trial), or if an item’s quantity changes", max_length=17, ), ), migrations.AddField( model_name="subscription", name="proration_date", field=djstripe.fields.StripeDateTimeField( blank=True, help_text="If set, the proration will be calculated as though the subscription was updated at the given time. This can be used to apply exactly the same proration that was previewed with upcoming invoice endpoint. It can also be used to implement custom proration logic, such as prorating by day instead of by second, by providing the time that you wish to use for proration calculations", null=True, ), ), migrations.AddField( model_name="subscription", name="pause_collection", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.CreateModel( name="TaxCode", fields=[ ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ( "name", models.CharField( help_text="A short name for the tax code.", max_length=128 ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ], options={ "get_latest_by": "created", "verbose_name": "Tax Code", }, ), migrations.CreateModel( name="Order", fields=[ ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("amount_subtotal", djstripe.fields.StripeQuantumCurrencyAmountField()), ("amount_total", djstripe.fields.StripeQuantumCurrencyAmountField()), ( "application", models.CharField( blank=True, help_text="ID of the Connect application that created the Order, if any.", max_length=255, ), ), ("automatic_tax", djstripe.fields.JSONField()), ("billing_details", djstripe.fields.JSONField(blank=True, null=True)), ( "client_secret", models.TextField( help_text="The client secret of this PaymentIntent. Used for client-side retrieval using a publishable key.", max_length=5000, ), ), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ("discounts", djstripe.fields.JSONField(blank=True, null=True)), ( "ip_address", models.GenericIPAddressField( blank=True, help_text="A recent IP address of the purchaser used for tax reporting and tax location inference.", null=True, ), ), ("line_items", djstripe.fields.JSONField()), ("payment", djstripe.fields.JSONField()), ("shipping_cost", djstripe.fields.JSONField(blank=True, null=True)), ("shipping_details", djstripe.fields.JSONField(blank=True, null=True)), ( "status", djstripe.fields.StripeEnumField( enum=djstripe.enums.OrderStatus, max_length=10 ), ), ("tax_details", djstripe.fields.JSONField(blank=True, null=True)), ("total_details", djstripe.fields.JSONField()), ( "customer", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The customer which this orders belongs to.", ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ( "payment_intent", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="djstripe.paymentintent", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="ID of the payment intent associated with this order. Null when the order is open.", ), ), ], options={ "get_latest_by": "created", "abstract": False, }, ), migrations.CreateModel( name="ShippingRate", fields=[ ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "active", models.BooleanField( default=True, help_text="Whether the shipping rate can be used for new purchases. Defaults to true", ), ), ( "display_name", models.CharField( blank=True, default="", help_text="The name of the shipping rate, meant to be displayable to the customer. This will appear on CheckoutSessions.", max_length=50, ), ), ("fixed_amount", djstripe.fields.JSONField()), ( "type", djstripe.fields.StripeEnumField( default="fixed_amount", enum=djstripe.enums.ShippingRateType, max_length=12, ), ), ("delivery_estimate", djstripe.fields.JSONField(blank=True, null=True)), ( "tax_behavior", djstripe.fields.StripeEnumField( enum=djstripe.enums.ShippingRateTaxBehavior, max_length=11 ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The Stripe Account this object belongs to.", ), ), ( "tax_code", djstripe.fields.StripeForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.taxcode", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, help_text="The shipping tax code", ), ), ], options={ "get_latest_by": "created", "verbose_name": "Shipping Rate", }, ), ] ================================================ FILE: djstripe/migrations/0012_auto_20221217_0559.py ================================================ # Generated by Django 3.2.15 on 2022-12-17 05:59 from django.db import migrations import djstripe.enums import djstripe.fields class Migration(migrations.Migration): dependencies = [ ("djstripe", "0011_2_7"), ] operations = [ migrations.AddField( model_name="paymentmethod", name="affirm", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="paymentmethod", name="blik", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="paymentmethod", name="customer_balance", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="paymentmethod", name="klarna", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="paymentmethod", name="konbini", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="paymentmethod", name="link", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="paymentmethod", name="paynow", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="paymentmethod", name="pix", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="paymentmethod", name="promptpay", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="paymentmethod", name="us_bank_account", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AlterField( model_name="account", name="business_type", field=djstripe.fields.StripeEnumField( blank=True, default="", enum=djstripe.enums.BusinessType, max_length=17 ), ), ] ================================================ FILE: djstripe/migrations/0013_product_default_price.py ================================================ # Generated by Django 3.2.15 on 2022-12-21 10:57 import django.db.models.deletion from django.conf import settings from django.db import migrations import djstripe.fields class Migration(migrations.Migration): dependencies = [ ("djstripe", "0012_auto_20221217_0559"), ] operations = [ migrations.AddField( model_name="product", name="default_price", field=djstripe.fields.StripeForeignKey( blank=True, help_text="The default price this product is associated with.", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="products", to="djstripe.price", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ] ================================================ FILE: djstripe/migrations/0014_lineitem.py ================================================ # Generated by Django 3.2.16 on 2023-01-26 05:14 import django.db.models.deletion from django.conf import settings from django.db import migrations, models import djstripe.enums import djstripe.fields class Migration(migrations.Migration): dependencies = [ ("djstripe", "0013_product_default_price"), ] operations = [ migrations.CreateModel( name="LineItem", fields=[ ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("amount", djstripe.fields.StripeQuantumCurrencyAmountField()), ( "amount_excluding_tax", djstripe.fields.StripeQuantumCurrencyAmountField(), ), ("currency", djstripe.fields.StripeCurrencyCodeField(max_length=3)), ("discount_amounts", djstripe.fields.JSONField(blank=True, null=True)), ( "discountable", models.BooleanField( default=False, help_text="If True, discounts will apply to this line item. Always False for prorations.", ), ), ("discounts", djstripe.fields.JSONField(blank=True, null=True)), ("period", djstripe.fields.JSONField()), ("period_end", djstripe.fields.StripeDateTimeField()), ("period_start", djstripe.fields.StripeDateTimeField()), ("price", djstripe.fields.JSONField()), ( "proration", models.BooleanField( default=False, help_text="Whether or not the invoice item was created automatically as a proration adjustment when the customer switched plans.", ), ), ("proration_details", djstripe.fields.JSONField()), ("tax_amounts", djstripe.fields.JSONField(blank=True, null=True)), ("tax_rates", djstripe.fields.JSONField(blank=True, null=True)), ( "type", djstripe.fields.StripeEnumField( enum=djstripe.enums.LineItem, max_length=12 ), ), ( "unit_amount_excluding_tax", djstripe.fields.StripeDecimalCurrencyAmountField( blank=True, decimal_places=2, max_digits=11, null=True ), ), ( "quantity", models.IntegerField( blank=True, help_text="The quantity of the subscription, if the line item is a subscription or a proration.", null=True, ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, help_text="The Stripe Account this object belongs to.", null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "invoice_item", djstripe.fields.StripeForeignKey( blank=True, help_text="The ID of the invoice item associated with this line item if any.", null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.invoiceitem", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "subscription", djstripe.fields.StripeForeignKey( blank=True, help_text="The subscription that the invoice item pertains to, if any.", null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.subscription", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "subscription_item", djstripe.fields.StripeForeignKey( blank=True, help_text="The subscription item that generated this invoice item. Left empty if the line item is not an explicit result of a subscription.", null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.subscriptionitem", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ], options={ "get_latest_by": "created", "abstract": False, }, ), ] ================================================ FILE: djstripe/migrations/0015_alter_payout_destination.py ================================================ # Generated by Django 3.2.12 on 2022-03-29 06:49 from pathlib import Path from django.db import migrations def get_sql_for_connection(schema_editor, direction: str) -> str: """ Returns the vendor and the collection of SQL Statements depending on: 1. The SQL Engine 2. Direction of Migrations: forward or Backward """ vendor = schema_editor.connection.vendor # Construct Path to SQL file_path = Path(__file__).parent / "sql" / f"migrate_{vendor}_{direction}.sql" try: sql_statement = Path(file_path).read_text() except FileNotFoundError as error: # In case it's oracle or some other django supported db that we do not support yet. raise RuntimeError( f"We currently do not support {vendor}. Please open an issue at https://github.com/dj-stripe/dj-stripe/issues/new?assignees=&labels=discussion&template=feature-or-enhancement-proposal.md&title= if you'd like it supported.", ) from error return vendor, sql_statement def forwards_func(apps, schema_editor): vendor, sql_statement = get_sql_for_connection(schema_editor, "forward") with schema_editor.connection.cursor() as cursor: if vendor == "sqlite": cursor.executescript(sql_statement) else: cursor.execute(sql_statement) def reverse_func(apps, schema_editor): vendor, sql_statement = get_sql_for_connection(schema_editor, "backward") with schema_editor.connection.cursor() as cursor: if vendor == "sqlite": cursor.executescript(sql_statement) else: cursor.execute(sql_statement) class Migration(migrations.Migration): dependencies = [ ("djstripe", "0014_lineitem"), ] operations = [ migrations.RunPython(forwards_func, reverse_func), ] ================================================ FILE: djstripe/migrations/0016_alter_payout_destination.py ================================================ # Generated by Django 3.2.16 on 2023-01-26 14:47 import django.db.models.deletion from django.db import migrations import djstripe.fields class Migration(migrations.Migration): dependencies = [ ("djstripe", "0015_alter_payout_destination"), ] operations = [ migrations.AlterField( model_name="payout", name="destination", field=djstripe.fields.PaymentMethodForeignKey( null=True, on_delete=django.db.models.deletion.PROTECT, to="djstripe.djstripepaymentmethod", ), ), ] ================================================ FILE: djstripe/migrations/0017_invoiceorlineitem.py ================================================ # Generated by Django 3.2.13 on 2022-07-09 08:04 import django.db.models.deletion from django.conf import settings from django.db import migrations, models import djstripe.enums import djstripe.fields class Migration(migrations.Migration): dependencies = [ ("djstripe", "0016_alter_payout_destination"), ] operations = [ migrations.CreateModel( name="InvoiceOrLineItem", fields=[ ( "id", models.CharField(max_length=255, primary_key=True, serialize=False), ), ( "type", djstripe.fields.StripeEnumField( enum=djstripe.enums.InvoiceorLineItemType, max_length=12 ), ), ], ), ] ================================================ FILE: djstripe/migrations/0018_discount.py ================================================ # Generated by Django 3.2.16 on 2023-01-28 06:04 import django.db.models.deletion from django.conf import settings from django.db import migrations, models import djstripe.fields class Migration(migrations.Migration): dependencies = [ ("djstripe", "0017_invoiceorlineitem"), ] operations = [ migrations.CreateModel( name="Discount", fields=[ ("djstripe_created", models.DateTimeField(auto_now_add=True)), ("djstripe_updated", models.DateTimeField(auto_now=True)), ( "djstripe_id", models.BigAutoField( primary_key=True, serialize=False, verbose_name="ID" ), ), ("id", djstripe.fields.StripeIdField(max_length=255, unique=True)), ( "livemode", models.BooleanField( blank=True, default=None, help_text="Null here indicates that the livemode status is unknown or was previously unrecorded. Otherwise, this field indicates whether this record comes from Stripe test mode or live mode operation.", null=True, ), ), ("created", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ("metadata", djstripe.fields.JSONField(blank=True, null=True)), ( "description", models.TextField( blank=True, help_text="A description of this object.", null=True ), ), ("coupon", djstripe.fields.JSONField(blank=True, null=True)), ("end", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ( "promotion_code", models.CharField( blank=True, help_text="The promotion code applied to create this discount.", max_length=255, ), ), ("start", djstripe.fields.StripeDateTimeField(blank=True, null=True)), ( "checkout_session", djstripe.fields.StripeForeignKey( blank=True, help_text="The Checkout session that this coupon is applied to, if it is applied to a particular session in payment mode. Will not be present for subscription mode.", null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.session", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "customer", djstripe.fields.StripeForeignKey( blank=True, help_text="The ID of the customer associated with this discount.", null=True, on_delete=django.db.models.deletion.CASCADE, related_name="customer_discounts", to="djstripe.customer", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "djstripe_owner_account", djstripe.fields.StripeForeignKey( blank=True, help_text="The Stripe Account this object belongs to.", null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.account", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "invoice", djstripe.fields.StripeForeignKey( blank=True, help_text="The invoice that the discount’s coupon was applied to, if it was applied directly to a particular invoice.", null=True, on_delete=django.db.models.deletion.CASCADE, related_name="invoice_discounts", to="djstripe.invoice", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ( "invoice_item", djstripe.fields.InvoiceOrLineItemForeignKey( blank=True, help_text="The invoice item id (or invoice line item id for invoice line items of type=‘subscription’) that the discount’s coupon was applied to, if it was applied directly to a particular invoice item or invoice line item.", null=True, on_delete=django.db.models.deletion.CASCADE, to="djstripe.invoiceorlineitem", ), ), ( "subscription", djstripe.fields.StripeForeignKey( blank=True, help_text="The subscription that this coupon is applied to, if it is applied to a particular subscription.", null=True, on_delete=django.db.models.deletion.CASCADE, related_name="subscription_discounts", to="djstripe.subscription", to_field=settings.DJSTRIPE_FOREIGN_KEY_TO_FIELD, ), ), ], options={ "get_latest_by": "created", "abstract": False, }, ), ] ================================================ FILE: djstripe/migrations/0019_add_customer_discount.py ================================================ # Generated by Django 3.2.13 on 2022-07-09 08:09 import django.db.models.deletion from django.conf import settings from django.db import migrations import djstripe.fields class Migration(migrations.Migration): dependencies = [ ("djstripe", "0018_discount"), ] operations = [ migrations.AddField( model_name="customer", name="discount", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="invoice", name="discounts", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="upcominginvoice", name="discounts", field=djstripe.fields.JSONField(blank=True, null=True), ), migrations.AddField( model_name="invoiceitem", name="discounts", field=djstripe.fields.JSONField(blank=True, null=True), ), ] ================================================ FILE: djstripe/migrations/__init__.py ================================================ ================================================ FILE: djstripe/migrations/sql/migrate_mysql_backward.sql ================================================ -- -- Rename field destination_clone on payout to destination -- ALTER TABLE `djstripe_payout` CHANGE `destination_id` `destination_clone_id` varchar(255) NULL; -- -- Remove field destination from payout -- ALTER TABLE `djstripe_payout` ADD COLUMN `destination_id` bigint NULL , ADD CONSTRAINT `djstripe_payout_destination_id_a5fa55c2_fk_djstripe_` FOREIGN KEY (`destination_id`) REFERENCES `djstripe_bankaccount`(`djstripe_id`); CREATE INDEX `djstripe_payout_destination_id_a5fa55c2` ON `djstripe_payout` (`destination_id`); -- -- Raw SQL operation -- UPDATE djstripe_payout SET destination_id = (select djstripe_id from djstripe_bankaccount where djstripe_bankaccount.id = djstripe_payout. destination_clone_id) WHERE EXISTS(select * from djstripe_bankaccount where djstripe_bankaccount.id = djstripe_payout.destination_clone_id); -- -- Add field destination_clone to payout -- ALTER TABLE `djstripe_payout` DROP FOREIGN KEY `djstripe_payout_destination_clone_id_ff0fec04_fk_djstripe_`; ALTER TABLE `djstripe_payout` DROP COLUMN `destination_clone_id`; ================================================ FILE: djstripe/migrations/sql/migrate_mysql_forward.sql ================================================ -- -- Add field destination_clone to payout -- ALTER TABLE `djstripe_payout` ADD COLUMN `destination_clone_id` varchar(255) NULL , ADD CONSTRAINT `djstripe_payout_destination_clone_id_ff0fec04_fk_djstripe_` FOREIGN KEY (`destination_clone_id`) REFERENCES `djstripe_djstripepaymentmethod`(`id`); CREATE INDEX `djstripe_payout_destination_clone_id_ff0fec04` ON `djstripe_payout` (`destination_clone_id`); -- -- Raw SQL operation -- UPDATE djstripe_payout SET destination_clone_id = (SELECT id from djstripe_bankaccount WHERE djstripe_bankaccount.djstripe_id = djstripe_payout.destination_id) WHERE EXISTS(SELECT * from djstripe_bankaccount WHERE djstripe_bankaccount.djstripe_id = djstripe_payout.destination_id); -- -- Remove field destination from payout -- ALTER TABLE `djstripe_payout` DROP FOREIGN KEY `djstripe_payout_destination_id_a5fa55c2_fk_djstripe_`; ALTER TABLE `djstripe_payout` DROP COLUMN `destination_id`; -- -- Rename field destination_clone on payout to destination -- ALTER TABLE `djstripe_payout` CHANGE `destination_clone_id` `destination_id` varchar(255) NULL; ================================================ FILE: djstripe/migrations/sql/migrate_postgresql_backward.sql ================================================ -- -- Rename field destination_clone on payout to destination -- SET CONSTRAINTS "djstripe_payout_destination_clone_id_ff0fec04_fk_djstripe_" IMMEDIATE; ALTER TABLE "djstripe_payout" DROP CONSTRAINT "djstripe_payout_destination_clone_id_ff0fec04_fk_djstripe_"; ALTER TABLE "djstripe_payout" RENAME COLUMN "destination_id" TO "destination_clone_id"; ALTER TABLE "djstripe_payout" ADD CONSTRAINT "djstripe_payout_destination_clone_id_ff0fec04_fk_djstripe_" FOREIGN KEY ("destination_clone_id") REFERENCES "djstripe_djstripepaymentmethod" ("id") DEFERRABLE INITIALLY DEFERRED; -- -- Remove field destination from payout -- ALTER TABLE "djstripe_payout" ADD COLUMN "destination_id" bigint NULL CONSTRAINT "djstripe_payout_destination_id_a5fa55c2_fk_djstripe_" REFERENCES "djstripe_bankaccount"("djstripe_id") DEFERRABLE INITIALLY DEFERRED; SET CONSTRAINTS "djstripe_payout_destination_id_a5fa55c2_fk_djstripe_" IMMEDIATE; CREATE INDEX "djstripe_payout_destination_id_a5fa55c2" ON "djstripe_payout" ("destination_id"); -- -- Raw SQL operation -- UPDATE djstripe_payout SET destination_id = (select djstripe_id from djstripe_bankaccount where djstripe_bankaccount.id = djstripe_payout. destination_clone_id) WHERE EXISTS(select * from djstripe_bankaccount where djstripe_bankaccount.id = djstripe_payout.destination_clone_id); -- -- Add field destination_clone to payout -- ALTER TABLE "djstripe_payout" DROP COLUMN "destination_clone_id" CASCADE; ================================================ FILE: djstripe/migrations/sql/migrate_postgresql_forward.sql ================================================ -- -- Add field destination_clone to payout -- ALTER TABLE "djstripe_payout" ADD COLUMN "destination_clone_id" varchar(255) NULL CONSTRAINT "djstripe_payout_destination_clone_id_ff0fec04_fk_djstripe_" REFERENCES "djstripe_djstripepaymentmethod"("id") DEFERRABLE INITIALLY IMMEDIATE; SET CONSTRAINTS "djstripe_payout_destination_clone_id_ff0fec04_fk_djstripe_" IMMEDIATE; CREATE INDEX "djstripe_payout_destination_clone_id_ff0fec04" ON "djstripe_payout" ("destination_clone_id"); CREATE INDEX "djstripe_payout_destination_clone_id_ff0fec04_like" ON "djstripe_payout" ("destination_clone_id" varchar_pattern_ops); -- -- Copy data from old column to new column -- UPDATE djstripe_payout SET destination_clone_id = (SELECT id from djstripe_bankaccount WHERE djstripe_bankaccount.djstripe_id = djstripe_payout.destination_id) WHERE EXISTS(SELECT * from djstripe_bankaccount WHERE djstripe_bankaccount.djstripe_id = djstripe_payout.destination_id); -- -- Remove field destination from payout -- ALTER TABLE "djstripe_payout" DROP COLUMN "destination_id" CASCADE; -- -- Rename field destination_clone on payout to destination -- ALTER TABLE "djstripe_payout" RENAME COLUMN "destination_clone_id" TO "destination_id"; ================================================ FILE: djstripe/migrations/sql/migrate_sqlite_backward.sql ================================================ -- -- Rename field destination_clone on payout to destination -- CREATE TABLE "new__djstripe_payout" ("destination_clone_id" varchar(255) NULL REFERENCES "djstripe_djstripepaymentmethod" ("id") DEFERRABLE INITIALLY DEFERRED, "djstripe_id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "id" varchar(255) NOT NULL UNIQUE, "livemode" bool NULL, "created" datetime NULL, "metadata" text NULL CHECK ((JSON_VALID("metadata") OR "metadata" IS NULL)), "description" text NULL, "djstripe_created" datetime NOT NULL, "djstripe_updated" datetime NOT NULL, "amount" decimal NOT NULL, "arrival_date" datetime NOT NULL, "currency" varchar(3) NOT NULL, "failure_code" varchar(32) NOT NULL, "failure_message" text NOT NULL, "method" varchar(8) NOT NULL, "statement_descriptor" varchar(255) NOT NULL, "status" varchar(10) NOT NULL, "type" varchar(12) NOT NULL, "balance_transaction_id" bigint NULL REFERENCES "djstripe_balancetransaction" ("djstripe_id") DEFERRABLE INITIALLY DEFERRED, "failure_balance_transaction_id" bigint NULL REFERENCES "djstripe_balancetransaction" ("djstripe_id") DEFERRABLE INITIALLY DEFERRED, "djstripe_owner_account_id" bigint NULL REFERENCES "djstripe_account" ("djstripe_id") DEFERRABLE INITIALLY DEFERRED, "automatic" bool NOT NULL, "source_type" varchar(12) NOT NULL); INSERT INTO "new__djstripe_payout" ("djstripe_id", "id", "livemode", "created", "metadata", "description", "djstripe_created", "djstripe_updated", "amount", "arrival_date", "currency", "failure_code", "failure_message", "method", "statement_descriptor", "status", "type", "balance_transaction_id", "failure_balance_transaction_id", "djstripe_owner_account_id", "automatic", "source_type", "destination_clone_id") SELECT "djstripe_id", "id", "livemode", "created", "metadata", "description", "djstripe_created", "djstripe_updated", "amount", "arrival_date", "currency", "failure_code", "failure_message", "method", "statement_descriptor", "status", "type", "balance_transaction_id", "failure_balance_transaction_id", "djstripe_owner_account_id", "automatic", "source_type", "destination_id" FROM "djstripe_payout"; DROP TABLE "djstripe_payout"; ALTER TABLE "new__djstripe_payout" RENAME TO "djstripe_payout"; CREATE INDEX "djstripe_payout_destination_clone_id_ff0fec04" ON "djstripe_payout" ("destination_clone_id"); CREATE INDEX "djstripe_payout_balance_transaction_id_a9393fb6" ON "djstripe_payout" ("balance_transaction_id"); CREATE INDEX "djstripe_payout_failure_balance_transaction_id_77d442db" ON "djstripe_payout" ("failure_balance_transaction_id"); CREATE INDEX "djstripe_payout_djstripe_owner_account_id_8aac4e8e" ON "djstripe_payout" ("djstripe_owner_account_id"); -- -- Remove field destination from payout -- ALTER TABLE "djstripe_payout" ADD COLUMN "destination_id" bigint NULL REFERENCES "djstripe_bankaccount" ("djstripe_id") DEFERRABLE INITIALLY DEFERRED; CREATE INDEX "djstripe_payout_destination_id_a5fa55c2" ON "djstripe_payout" ("destination_id"); -- -- Raw SQL operation -- UPDATE djstripe_payout SET destination_id = (select djstripe_id from djstripe_bankaccount where djstripe_bankaccount.id = djstripe_payout. destination_clone_id) WHERE EXISTS(select * from djstripe_bankaccount where djstripe_bankaccount.id = djstripe_payout.destination_clone_id); -- -- Add field destination_clone to payout -- CREATE TABLE "new__djstripe_payout" ("djstripe_id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "id" varchar(255) NOT NULL UNIQUE, "livemode" bool NULL, "created" datetime NULL, "metadata" text NULL CHECK ((JSON_VALID("metadata") OR "metadata" IS NULL)), "description" text NULL, "djstripe_created" datetime NOT NULL, "djstripe_updated" datetime NOT NULL, "amount" decimal NOT NULL, "arrival_date" datetime NOT NULL, "currency" varchar(3) NOT NULL, "failure_code" varchar(32) NOT NULL, "failure_message" text NOT NULL, "method" varchar(8) NOT NULL, "statement_descriptor" varchar(255) NOT NULL, "status" varchar(10) NOT NULL, "type" varchar(12) NOT NULL, "destination_id" bigint NULL REFERENCES "djstripe_bankaccount" ("djstripe_id") DEFERRABLE INITIALLY DEFERRED, "balance_transaction_id" bigint NULL REFERENCES "djstripe_balancetransaction" ("djstripe_id") DEFERRABLE INITIALLY DEFERRED, "failure_balance_transaction_id" bigint NULL REFERENCES "djstripe_balancetransaction" ("djstripe_id") DEFERRABLE INITIALLY DEFERRED, "djstripe_owner_account_id" bigint NULL REFERENCES "djstripe_account" ("djstripe_id") DEFERRABLE INITIALLY DEFERRED, "automatic" bool NOT NULL, "source_type" varchar(12) NOT NULL); INSERT INTO "new__djstripe_payout" ("djstripe_id", "id", "livemode", "created", "metadata", "description", "djstripe_created", "djstripe_updated", "amount", "arrival_date", "currency", "failure_code", "failure_message", "method", "statement_descriptor", "status", "type", "destination_id", "balance_transaction_id", "failure_balance_transaction_id", "djstripe_owner_account_id", "automatic", "source_type") SELECT "djstripe_id", "id", "livemode", "created", "metadata", "description", "djstripe_created", "djstripe_updated", "amount", "arrival_date", "currency", "failure_code", "failure_message", "method", "statement_descriptor", "status", "type", "destination_id", "balance_transaction_id", "failure_balance_transaction_id", "djstripe_owner_account_id", "automatic", "source_type" FROM "djstripe_payout"; DROP TABLE "djstripe_payout"; ALTER TABLE "new__djstripe_payout" RENAME TO "djstripe_payout"; CREATE INDEX "djstripe_payout_destination_id_a5fa55c2" ON "djstripe_payout" ("destination_id"); CREATE INDEX "djstripe_payout_balance_transaction_id_a9393fb6" ON "djstripe_payout" ("balance_transaction_id"); CREATE INDEX "djstripe_payout_failure_balance_transaction_id_77d442db" ON "djstripe_payout" ("failure_balance_transaction_id"); CREATE INDEX "djstripe_payout_djstripe_owner_account_id_8aac4e8e" ON "djstripe_payout" ("djstripe_owner_account_id"); ================================================ FILE: djstripe/migrations/sql/migrate_sqlite_forward.sql ================================================ -- -- Add field destination_clone to payout -- ALTER TABLE "djstripe_payout" ADD COLUMN "destination_clone_id" varchar(255) NULL REFERENCES "djstripe_djstripepaymentmethod" ("id") DEFERRABLE INITIALLY DEFERRED; CREATE INDEX "djstripe_payout_destination_clone_id_ff0fec04" ON "djstripe_payout" ("destination_clone_id"); -- -- Raw SQL operation -- UPDATE djstripe_payout SET destination_clone_id = (SELECT id from djstripe_bankaccount WHERE djstripe_bankaccount.djstripe_id = djstripe_payout.destination_id) WHERE EXISTS(SELECT * from djstripe_bankaccount WHERE djstripe_bankaccount.djstripe_id = djstripe_payout.destination_id); -- -- Rename field destination_clone on payout to destination -- CREATE TABLE "new__djstripe_payout" ("destination_id" varchar(255) NULL REFERENCES "djstripe_djstripepaymentmethod" ("id") DEFERRABLE INITIALLY DEFERRED, "djstripe_id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "id" varchar(255) NOT NULL UNIQUE, "livemode" bool NULL, "created" datetime NULL, "metadata" text NULL CHECK ((JSON_VALID("metadata") OR "metadata" IS NULL)), "description" text NULL, "djstripe_created" datetime NOT NULL, "djstripe_updated" datetime NOT NULL, "amount" decimal NOT NULL, "arrival_date" datetime NOT NULL, "currency" varchar(3) NOT NULL, "failure_code" varchar(32) NOT NULL, "failure_message" text NOT NULL, "method" varchar(8) NOT NULL, "statement_descriptor" varchar(255) NOT NULL, "status" varchar(10) NOT NULL, "type" varchar(12) NOT NULL, "balance_transaction_id" bigint NULL REFERENCES "djstripe_balancetransaction" ("djstripe_id") DEFERRABLE INITIALLY DEFERRED, "failure_balance_transaction_id" bigint NULL REFERENCES "djstripe_balancetransaction" ("djstripe_id") DEFERRABLE INITIALLY DEFERRED, "djstripe_owner_account_id" bigint NULL REFERENCES "djstripe_account" ("djstripe_id") DEFERRABLE INITIALLY DEFERRED, "automatic" bool NOT NULL, "source_type" varchar(12) NOT NULL); INSERT INTO "new__djstripe_payout" ("djstripe_id", "id", "livemode", "created", "metadata", "description", "djstripe_created", "djstripe_updated", "amount", "arrival_date", "currency", "failure_code", "failure_message", "method", "statement_descriptor", "status", "type", "balance_transaction_id", "failure_balance_transaction_id", "djstripe_owner_account_id", "automatic", "source_type", "destination_id") SELECT "djstripe_id", "id", "livemode", "created", "metadata", "description", "djstripe_created", "djstripe_updated", "amount", "arrival_date", "currency", "failure_code", "failure_message", "method", "statement_descriptor", "status", "type", "balance_transaction_id", "failure_balance_transaction_id", "djstripe_owner_account_id", "automatic", "source_type", "destination_clone_id" FROM "djstripe_payout"; DROP TABLE "djstripe_payout"; ALTER TABLE "new__djstripe_payout" RENAME TO "djstripe_payout"; CREATE INDEX "djstripe_payout_destination_id_a5fa55c2" ON "djstripe_payout" ("destination_id"); CREATE INDEX "djstripe_payout_balance_transaction_id_a9393fb6" ON "djstripe_payout" ("balance_transaction_id"); CREATE INDEX "djstripe_payout_failure_balance_transaction_id_77d442db" ON "djstripe_payout" ("failure_balance_transaction_id"); CREATE INDEX "djstripe_payout_djstripe_owner_account_id_8aac4e8e" ON "djstripe_payout" ("djstripe_owner_account_id"); ================================================ FILE: djstripe/mixins.py ================================================ """ dj-stripe mixins """ import sys import traceback from .models import Customer, Plan from .settings import djstripe_settings class PaymentsContextMixin: """Adds plan context to a view.""" def get_context_data(self, **kwargs): """Inject STRIPE_PUBLIC_KEY and plans into context_data.""" context = super().get_context_data(**kwargs) context.update( { "STRIPE_PUBLIC_KEY": djstripe_settings.STRIPE_PUBLIC_KEY, "plans": Plan.objects.all(), } ) return context class SubscriptionMixin(PaymentsContextMixin): """Adds customer subscription context to a view.""" def get_context_data(self, *args, **kwargs): """Inject is_plans_plural and customer into context_data.""" context = super().get_context_data(**kwargs) context["is_plans_plural"] = Plan.objects.count() > 1 context["customer"], _created = Customer.get_or_create( subscriber=djstripe_settings.subscriber_request_callback(self.request) ) context["subscription"] = context["customer"].subscription return context class VerbosityAwareOutputMixin: """ A mixin class to provide verbosity aware output functions for management commands. """ def set_verbosity(self, options): """Set the verbosity based off the passed in options.""" self.verbosity = options["verbosity"] def output(self, arg): """Print if output is not silenced.""" if self.verbosity > 0: print(arg) def verbose_output(self, arg): """Print only if output is verbose.""" if self.verbosity > 1: print(arg) def verbose_traceback(self): """Print out a traceback if the output is verbose.""" if self.verbosity > 1: exc_type, exc_value, exc_traceback = sys.exc_info() traceback.print_exception(exc_type, exc_value, exc_traceback) ================================================ FILE: djstripe/models/__init__.py ================================================ from .account import Account from .api import APIKey from .base import IdempotencyKey, StripeModel from .billing import ( Coupon, Discount, Invoice, InvoiceItem, InvoiceOrLineItem, LineItem, Plan, ShippingRate, Subscription, SubscriptionItem, SubscriptionSchedule, TaxCode, TaxId, TaxRate, UpcomingInvoice, UsageRecord, UsageRecordSummary, ) from .checkout import Session from .connect import ( ApplicationFee, ApplicationFeeRefund, CountrySpec, Transfer, TransferReversal, ) from .core import ( BalanceTransaction, Charge, Customer, Dispute, Event, File, FileLink, FileUpload, Mandate, PaymentIntent, Payout, Price, Product, Refund, SetupIntent, ) from .orders import Order from .payment_methods import ( BankAccount, Card, DjstripePaymentMethod, PaymentMethod, Source, ) from .sigma import ScheduledQueryRun from .webhooks import WebhookEndpoint, WebhookEventTrigger __all__ = [ "Account", "APIKey", "ApplicationFee", "ApplicationFeeRefund", "BalanceTransaction", "BankAccount", "Card", "Charge", "CountrySpec", "Coupon", "Customer", "Discount", "Dispute", "DjstripePaymentMethod", "Event", "File", "FileLink", "FileUpload", "IdempotencyKey", "Invoice", "InvoiceItem", "LineItem", "InvoiceOrLineItem", "Mandate", "Order", "PaymentIntent", "PaymentMethod", "Payout", "Plan", "Price", "Product", "Refund", "ShippingRate", "ScheduledQueryRun", "SetupIntent", "Session", "Source", "StripeModel", "Subscription", "SubscriptionItem", "SubscriptionSchedule", "TaxCode", "TaxId", "TaxRate", "Transfer", "TransferReversal", "UpcomingInvoice", "UsageRecord", "UsageRecordSummary", "WebhookEndpoint", "WebhookEventTrigger", ] ================================================ FILE: djstripe/models/account.py ================================================ import stripe from django.db import models, transaction from .. import enums from ..enums import APIKeyType from ..fields import JSONField, StripeCurrencyCodeField, StripeEnumField from ..settings import djstripe_settings from .api import APIKey, get_api_key_details_by_prefix from .base import StripeModel, logger class Account(StripeModel): """ This is an object representing a Stripe account. You can retrieve it to see properties on the account like its current e-mail address or if the account is enabled yet to make live charges. Stripe documentation: https://stripe.com/docs/api/accounts?lang=python """ stripe_class = stripe.Account business_profile = JSONField( null=True, blank=True, help_text="Optional information related to the business." ) business_type = StripeEnumField( enum=enums.BusinessType, default="", blank=True, help_text="The business type." ) charges_enabled = models.BooleanField( help_text="Whether the account can create live charges" ) country = models.CharField(max_length=2, help_text="The country of the account") company = JSONField( null=True, blank=True, help_text=( "Information about the company or business. " "This field is null unless business_type is set to company." ), ) default_currency = StripeCurrencyCodeField( help_text="The currency this account has chosen to use as the default" ) details_submitted = models.BooleanField( help_text=( "Whether account details have been submitted. " "Standard accounts cannot receive payouts before this is true." ) ) email = models.CharField( max_length=255, help_text="The primary user's email address." ) # TODO external_accounts = ... individual = JSONField( null=True, blank=True, help_text=( "Information about the person represented by the account. " "This field is null unless business_type is set to individual." ), ) payouts_enabled = models.BooleanField( null=True, help_text="Whether Stripe can send payouts to this account" ) product_description = models.CharField( max_length=255, default="", blank=True, help_text="Internal-only description of the product sold or service provided " "by the business. It's used by Stripe for risk and underwriting purposes.", ) requirements = JSONField( null=True, blank=True, help_text="Information about the requirements for the account, " "including what information needs to be collected, and by when.", ) settings = JSONField( null=True, blank=True, help_text=( "Account options for customizing how the account functions within Stripe." ), ) type = StripeEnumField(enum=enums.AccountType, help_text="The Stripe account type.") tos_acceptance = JSONField( null=True, blank=True, help_text="Details on the acceptance of the Stripe Services Agreement", ) def get_stripe_dashboard_url(self) -> str: """Get the stripe dashboard url for this object.""" return ( f"https://dashboard.stripe.com/{self.id}/" f"{'test/' if not self.livemode else ''}dashboard" ) @property def default_api_key(self) -> str: return self.get_default_api_key() def get_default_api_key(self, livemode: bool = None) -> str: if livemode is None: livemode = self.livemode api_key = APIKey.objects.filter( djstripe_owner_account=self, type=APIKeyType.secret ).first() else: api_key = APIKey.objects.filter( djstripe_owner_account=self, type=APIKeyType.secret, livemode=livemode ).first() if api_key: return api_key.secret return djstripe_settings.get_default_api_key(livemode) @property def business_url(self) -> str: """ The business's publicly available website. """ if self.business_profile: return self.business_profile.get("url", "") return "" @classmethod def get_default_account(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY): # As of API version 2020-03-02, there is no permission that can allow # restricted keys to call GET /v1/account if djstripe_settings.STRIPE_SECRET_KEY.startswith("rk_"): return None account_data = cls.stripe_class.retrieve( api_key=api_key, stripe_version=djstripe_settings.STRIPE_API_VERSION ) return cls._get_or_create_from_stripe_object(account_data, api_key=api_key)[0] @classmethod def get_or_retrieve_for_api_key(cls, api_key: str): with transaction.atomic(): apikey_instance, _ = APIKey.objects.get_or_create_by_api_key(api_key) if not apikey_instance.djstripe_owner_account: apikey_instance.refresh_account() return apikey_instance.djstripe_owner_account def __str__(self): settings = self.settings or {} business_profile = self.business_profile or {} return ( settings.get("dashboard", {}).get("display_name") or business_profile.get("name") or super().__str__() ) def api_reject(self, api_key=None, stripe_account=None, **kwargs): """ Call the stripe API's reject operation for Account model :param api_key: The api key to use for this request. Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string """ api_key = api_key or self.default_api_key # Prefer passed in stripe_account if set. if not stripe_account: stripe_account = self._get_stripe_account_id(api_key) return self.stripe_class.reject( self.id, api_key=api_key, stripe_account=stripe_account, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ) @classmethod def _create_from_stripe_object( cls, data, current_ids=None, pending_relations=None, save=True, stripe_account=None, api_key=djstripe_settings.STRIPE_SECRET_KEY, ): """ Set the stripe_account to the id of the Account instance being created. This ensures that the foreign-key relations that may exist in stripe are fetched using the appropriate connected account ID. """ return super()._create_from_stripe_object( data=data, current_ids=current_ids, pending_relations=pending_relations, save=save, stripe_account=data["id"] if not stripe_account else stripe_account, api_key=api_key, ) # "Special" handling of the icon and logo fields # Previously available as properties, they moved to # settings.branding in Stripe 2019-02-19. # Currently, they return a File ID @property def branding_icon(self): from ..models.core import File id = self.settings.get("branding", {}).get("icon") return File.objects.filter(id=id).first() if id else None @property def branding_logo(self): from ..models.core import File id = self.settings.get("branding", {}).get("logo") return File.objects.filter(id=id).first() if id else None def _attach_objects_post_save_hook( self, cls, data, pending_relations=None, api_key=djstripe_settings.STRIPE_SECRET_KEY, ): from ..models.core import File super()._attach_objects_post_save_hook( cls, data, pending_relations=pending_relations, api_key=api_key ) # set the livemode if not returned by data if "livemode" not in data.keys() and self.djstripe_owner_account is not None: # Platform Account if self == self.djstripe_owner_account: self.livemode = None else: # Connected Account _, self.livemode = get_api_key_details_by_prefix(api_key) # save the updates self.save() # Retrieve and save the Files in the settings.branding object. for field in "icon", "logo": file_upload_id = self.settings and self.settings.get("branding", {}).get( field ) if file_upload_id: try: File.sync_from_stripe_data( File(id=file_upload_id).api_retrieve( stripe_account=self.id, api_key=api_key ), api_key=api_key, ) except stripe.error.PermissionError: # No permission to retrieve the data with the key logger.warning( f"Cannot retrieve business branding {field} for acct {self.id} with the key." ) except stripe.error.InvalidRequestError as e: if "a similar object exists in" in str(e): # HACK around a Stripe bug. # See #830 and commit c09d25f52bfdcf883e9eec0bf6c25af1771a644a pass else: raise except stripe.error.AuthenticationError: # This may happen if saving an account that has a logo, using # a different API key to the default. # OK, concretely, there is a chicken-and-egg problem here. # But, the logo file object is not a particularly important thing. # Until we have a better solution, just ignore this error. pass ================================================ FILE: djstripe/models/api.py ================================================ import re from base64 import b64encode from uuid import uuid4 from django.core.validators import RegexValidator from django.db import IntegrityError, models, transaction from django.forms import ValidationError from ..enums import APIKeyType from ..exceptions import InvalidStripeAPIKey from ..fields import StripeEnumField from ..settings import djstripe_settings from .base import StripeModel # A regex to validate API key format API_KEY_REGEX = r"^(pk|sk|rk)_(test|live)_([a-zA-Z0-9]{24,99})" def generate_api_key_id() -> str: b64_id = b64encode(uuid4().bytes).decode() generated_id = b64_id.rstrip("=").replace("+", "").replace("/", "") return f"djstripe_mk_{generated_id}" def get_api_key_details_by_prefix(api_key: str): sre = re.match(API_KEY_REGEX, api_key) if not sre: raise InvalidStripeAPIKey(f"Invalid API key: {api_key!r}") key_type = { "pk": APIKeyType.publishable, "sk": APIKeyType.secret, "rk": APIKeyType.restricted, }.get(sre.group(1), "") livemode = {"test": False, "live": True}.get(sre.group(2)) return key_type, livemode class APIKeyManager(models.Manager): def get_or_create_by_api_key(self, secret: str): key_type, livemode = get_api_key_details_by_prefix(secret) return super().get_or_create( secret=secret, defaults={"type": key_type, "livemode": livemode} ) class APIKey(StripeModel): object = "api_key" id = models.CharField(max_length=255, default=generate_api_key_id, editable=False) type = StripeEnumField(enum=APIKeyType) name = models.CharField( "Key name", max_length=100, blank=True, help_text="An optional name to identify the key.", ) secret = models.CharField( max_length=128, validators=[RegexValidator(regex=API_KEY_REGEX)], unique=True, help_text="The value of the key.", ) livemode = models.BooleanField( help_text="Whether the key is valid for live or test mode." ) description = None metadata = None objects = APIKeyManager() def get_stripe_dashboard_url(self): return self._get_base_stripe_dashboard_url() + "apikeys" def __str__(self): return self.name or self.secret_redacted def clean(self): if self.livemode is None or self.type is None: try: self.type, self.livemode = get_api_key_details_by_prefix(self.secret) except InvalidStripeAPIKey as e: raise ValidationError(str(e)) def refresh_account(self, commit=True): from .account import Account if self.type != APIKeyType.secret: return account_data = Account.stripe_class.retrieve( api_key=self.secret, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) # NOTE: Do not immediately use _get_or_create_from_stripe_object() here. # Account needs to exist for things to work. Make a stub if necessary. account, created = Account.objects.get_or_create( id=account_data["id"], defaults={"charges_enabled": False, "details_submitted": False}, ) if created: # If it's just been created, now we can sync the account. Account.sync_from_stripe_data(account_data, api_key=self.secret) self.djstripe_owner_account = account if commit: try: # for non-existent accounts, due to djstripe_owner_account search for the # accounts themselves, trigerred by this method, the APIKey gets created before this method # can "commit". This results in an Integrity Error with transaction.atomic(): self.save() except IntegrityError: pass @property def secret_redacted(self) -> str: """ Returns a redacted version of the secret, suitable for display purposes. Same algorithm used on the Stripe dashboard. """ secret_prefix, _, secret_part = self.secret.rpartition("_") secret_part = secret_part[-4:] return f"{secret_prefix}_...{secret_part}" ================================================ FILE: djstripe/models/base.py ================================================ import logging import uuid from datetime import timedelta from typing import Dict, List, Optional, Type from django.db import IntegrityError, models, transaction from django.utils import dateformat, timezone from stripe.api_resources.abstract.api_resource import APIResource from stripe.error import InvalidRequestError from stripe.util import convert_to_stripe_object from ..exceptions import ImpossibleAPIRequest from ..fields import ( JSONField, StripeDateTimeField, StripeForeignKey, StripeIdField, StripePercentField, ) from ..managers import StripeModelManager from ..settings import djstripe_settings from ..utils import get_id_from_stripe_data logger = logging.getLogger(__name__) class StripeBaseModel(models.Model): stripe_class: Type[APIResource] = APIResource djstripe_created = models.DateTimeField(auto_now_add=True, editable=False) djstripe_updated = models.DateTimeField(auto_now=True, editable=False) class Meta: abstract = True @classmethod def api_list(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs): """ Call the stripe API's list operation for this model. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string See Stripe documentation for accepted kwargs for each object. :returns: an iterator over all items in the query """ return cls.stripe_class.list( api_key=api_key, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ).auto_paging_iter() class StripeModel(StripeBaseModel): # This must be defined in descendants of this model/mixin # e.g. Event, Charge, Customer, etc. expand_fields: List[str] = [] stripe_dashboard_item_name = "" objects = models.Manager() stripe_objects = StripeModelManager() djstripe_id = models.BigAutoField( verbose_name="ID", serialize=False, primary_key=True ) id = StripeIdField(unique=True) djstripe_owner_account: Optional[StripeForeignKey] = StripeForeignKey( "djstripe.Account", on_delete=models.CASCADE, to_field="id", null=True, blank=True, help_text="The Stripe Account this object belongs to.", ) livemode = models.BooleanField( null=True, default=None, blank=True, help_text="Null here indicates that the livemode status is unknown or was " "previously unrecorded. Otherwise, this field indicates whether this record " "comes from Stripe test mode or live mode operation.", ) created = StripeDateTimeField( null=True, blank=True, help_text="The datetime this object was created in stripe.", ) metadata = JSONField( null=True, blank=True, help_text="A set of key/value pairs that you can attach to an object. " "It can be useful for storing additional information about an object in " "a structured format.", ) description = models.TextField( null=True, blank=True, help_text="A description of this object." ) class Meta(StripeBaseModel.Meta): abstract = True get_latest_by = "created" def _get_base_stripe_dashboard_url(self): owner_path_prefix = ( (self.djstripe_owner_account.id + "/") if self.djstripe_owner_account else "" ) suffix = "test/" if not self.livemode else "" return f"https://dashboard.stripe.com/{owner_path_prefix}{suffix}" def get_stripe_dashboard_url(self) -> str: """Get the stripe dashboard url for this object.""" if not self.stripe_dashboard_item_name or not self.id: return "" else: base_url = self._get_base_stripe_dashboard_url() item = self.stripe_dashboard_item_name return f"{base_url}{item}/{self.id}" @property def default_api_key(self) -> str: # If the class is abstract (StripeModel), fall back to default key. if not self._meta.abstract: if self.djstripe_owner_account: return self.djstripe_owner_account.get_default_api_key(self.livemode) return djstripe_settings.get_default_api_key(self.livemode) def _get_stripe_account_id(self, api_key=None) -> Optional[str]: """ Call the stripe API's retrieve operation for this model. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string """ from djstripe.models import Account api_key = api_key or self.default_api_key try: djstripe_owner_account = self.djstripe_owner_account if djstripe_owner_account is not None: return djstripe_owner_account.id except (AttributeError, KeyError, ValueError): pass # Get reverse foreign key relations to Account in case we need to # retrieve ourselves using that Account ID. reverse_account_relations = ( field for field in self._meta.get_fields(include_parents=True) if field.is_relation and field.one_to_many and field.related_model is Account ) # Handle case where we have a reverse relation to Account and should pass # that account ID to the retrieve call. for field in reverse_account_relations: # Grab the related object, using the first one we find. reverse_lookup_attr = field.get_accessor_name() try: account = getattr(self, reverse_lookup_attr).first() except ValueError: if isinstance(self, Account): # return the id if self is the Account model itself. return self.id else: if account is not None: return account.id return None def api_retrieve(self, api_key=None, stripe_account=None): """ Call the stripe API's retrieve operation for this model. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string """ # Prefer passed in stripe_account if set. if not stripe_account: stripe_account = self._get_stripe_account_id(api_key) return self.stripe_class.retrieve( id=self.id, api_key=api_key or self.default_api_key, stripe_version=djstripe_settings.STRIPE_API_VERSION, expand=self.expand_fields, stripe_account=stripe_account, ) @classmethod def _api_create(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs): """ Call the stripe API's create operation for this model. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string """ return cls.stripe_class.create( api_key=api_key, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ) def _api_delete(self, api_key=None, stripe_account=None, **kwargs): """ Call the stripe API's delete operation for this model :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string """ api_key = api_key or self.default_api_key # Prefer passed in stripe_account if set. if not stripe_account: stripe_account = self._get_stripe_account_id(api_key) return self.stripe_class.delete( self.id, api_key=api_key, stripe_account=stripe_account, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ) def _api_update(self, api_key=None, stripe_account=None, **kwargs): """ Call the stripe API's modify operation for this model :param api_key: The api key to use for this request. Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string """ api_key = api_key or self.default_api_key # Prefer passed in stripe_account if set. if not stripe_account: stripe_account = self._get_stripe_account_id(api_key) return self.stripe_class.modify( self.id, api_key=api_key, stripe_account=stripe_account, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ) @classmethod def _manipulate_stripe_object_hook(cls, data): """ Gets called by this object's stripe object conversion method just before conversion. Use this to populate custom fields in a StripeModel from stripe data. """ return data @classmethod def _find_owner_account(cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY): """ Fetches the Stripe Account (djstripe_owner_account model field) linked to the class, cls. Tries to retreive using the Stripe_account if given. Otherwise uses the api_key. """ from .account import Account # try to fetch by stripe_account. Also takes care of Stripe Connected Accounts if data: # case of Webhook Event Trigger if data.get("object") == "event": # if account key exists and has a not null value stripe_account_id = get_id_from_stripe_data(data.get("account")) if stripe_account_id: return Account._get_or_retrieve( id=stripe_account_id, api_key=api_key ) else: stripe_account = getattr(data, "stripe_account", None) stripe_account_id = get_id_from_stripe_data(stripe_account) if stripe_account_id: return Account._get_or_retrieve( id=stripe_account_id, api_key=api_key ) # try to fetch by the given api_key. return Account.get_or_retrieve_for_api_key(api_key) @classmethod def _stripe_object_to_record( cls, data: dict, current_ids=None, pending_relations: list = None, stripe_account: str = None, api_key=djstripe_settings.STRIPE_SECRET_KEY, ) -> Dict: """ This takes an object, as it is formatted in Stripe's current API for our object type. In return, it provides a dict. The dict can be used to create a record or to update a record This function takes care of mapping from one field name to another, converting from cents to dollars, converting timestamps, and eliminating unused fields (so that an objects.create() call would not fail). :param data: the object, as sent by Stripe. Parsed from JSON, into a dict :param current_ids: stripe ids of objects that are currently being processed :type current_ids: set :param pending_relations: list of tuples of relations to be attached post-save :param stripe_account: The optional connected account \ for which this request is being made. :return: All the members from the input, translated, mutated, etc """ from .webhooks import WebhookEndpoint manipulated_data = cls._manipulate_stripe_object_hook(data) if not cls.is_valid_object(manipulated_data): raise ValueError( "Trying to fit a %r into %r. Aborting." % (manipulated_data.get("object", ""), cls.__name__) ) result = {} if current_ids is None: current_ids = set() # Iterate over all the fields that we know are related to Stripe, # let each field work its own magic ignore_fields = ["date_purged", "subscriber"] # XXX: Customer hack # get all forward and reverse relations for given cls for field in cls._meta.get_fields(): if field.name.startswith("djstripe_") or field.name in ignore_fields: continue # todo add support reverse ManyToManyField sync if isinstance( field, (models.ManyToManyRel, models.ManyToOneRel) ) and not isinstance(field, models.OneToOneRel): # We don't currently support syncing from # reverse side of Many relationship continue # todo for ManyToManyField one would also need to handle the case of an intermediate model being used # todo add support ManyToManyField sync if field.many_to_many: # We don't currently support syncing ManyToManyField continue # will work for Forward FK and OneToOneField relations and reverse OneToOneField relations if isinstance(field, (models.ForeignKey, models.OneToOneRel)): field_data, skip, is_nulled = cls._stripe_object_field_to_foreign_key( field=field, manipulated_data=manipulated_data, current_ids=current_ids, pending_relations=pending_relations, stripe_account=stripe_account, api_key=api_key, ) if skip and not is_nulled: continue else: if hasattr(field, "stripe_to_db"): field_data = field.stripe_to_db(manipulated_data) else: field_data = manipulated_data.get(field.name) if ( isinstance(field, (models.CharField, models.TextField)) and field_data is None ): # do not add empty secret field for WebhookEndpoint model # as stripe does not return the secret except for the CREATE call if cls is WebhookEndpoint and field.name == "secret": continue else: # TODO - this applies to StripeEnumField as well, since it # sub-classes CharField, is that intentional? field_data = "" result[field.name] = field_data # For all objects other than the account object itself, get the API key # attached to the request, and get the matching Account for that key. owner_account = cls._find_owner_account(data, api_key=api_key) if owner_account: result["djstripe_owner_account"] = owner_account return result @classmethod def _stripe_object_field_to_foreign_key( cls, field, manipulated_data, current_ids=None, pending_relations=None, stripe_account=None, api_key=djstripe_settings.STRIPE_SECRET_KEY, ): """ This converts a stripe API field to the dj stripe object it references, so that foreign keys can be connected up automatically. :param field: :type field: models.ForeignKey :param manipulated_data: :type manipulated_data: dict :param current_ids: stripe ids of objects that are currently being processed :type current_ids: set :param pending_relations: list of tuples of relations to be attached post-save :type pending_relations: list :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string :return: """ from djstripe.models import DjstripePaymentMethod, InvoiceOrLineItem field_data = None field_name = field.name refetch = False skip = False # a flag to indicate if the given field is null upstream on Stripe is_nulled = False if current_ids is None: current_ids = set() if issubclass( field.related_model, (StripeModel, DjstripePaymentMethod, InvoiceOrLineItem) ): if field_name in manipulated_data: raw_field_data = manipulated_data.get(field_name) # field's value is None. Skip syncing but set as None. # Otherwise nulled FKs sync gets skipped. if not raw_field_data: is_nulled = True skip = True else: # field does not exist in manipulated_data dict. Skip Syncing skip = True raw_field_data = None id_ = get_id_from_stripe_data(raw_field_data) if id_ == raw_field_data: # A field like {"subscription": "sub_6lsC8pt7IcFpjA", ...} refetch = True else: # A field like {"subscription": {"id": sub_6lsC8pt7IcFpjA", ...}} pass if id_ in current_ids: # this object is currently being fetched, don't try to fetch again, # to avoid recursion instead, record the relation that should be # created once "object_id" object exists if pending_relations is not None: object_id = manipulated_data["id"] pending_relations.append((object_id, field, id_)) skip = True # sync only if field exists and is not null if not skip and not is_nulled: # add the id of the current object to the list # of ids being processed. # This will avoid infinite recursive syncs in case a relatedmodel # requests the same object current_ids.add(id_) try: ( field_data, _, ) = field.related_model._get_or_create_from_stripe_object( manipulated_data, field_name, refetch=refetch, current_ids=current_ids, pending_relations=pending_relations, stripe_account=stripe_account, api_key=api_key, ) except ImpossibleAPIRequest: # Found to happen in the following situation: # Customer has a `default_source` set to a `card_` object, # and neither the Customer nor the Card are present in db. # This skip is a hack, but it will prevent a crash. skip = True # Remove the id of the current object from the list # after it has been created or retrieved current_ids.remove(id_) else: # eg PaymentMethod, handled in hooks skip = True return field_data, skip, is_nulled @classmethod def is_valid_object(cls, data): """ Returns whether the data is a valid object for the class """ # .OBJECT_NAME will not exist on the base type itself object_name: str = getattr(cls.stripe_class, "OBJECT_NAME", "") if not object_name: return False return data and data.get("object") == object_name def _attach_objects_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, current_ids=None ): """ Gets called by this object's create and sync methods just before save. Use this to populate fields before the model is saved. :param cls: The target class for the instantiated object. :param data: The data dictionary received from the Stripe API. :type data: dict :param current_ids: stripe ids of objects that are currently being processed :type current_ids: set """ pass def _attach_objects_post_save_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, pending_relations=None, ): """ Gets called by this object's create and sync methods just after save. Use this to populate fields after the model is saved. :param cls: The target class for the instantiated object. :param data: The data dictionary received from the Stripe API. :type data: dict """ unprocessed_pending_relations = [] if pending_relations is not None: for post_save_relation in pending_relations: object_id, field, id_ = post_save_relation if self.id == id_: # the target instance now exists target = field.model.objects.get(id=object_id) setattr(target, field.name, self) if isinstance(field, models.OneToOneRel): # this is a reverse relationship, so the relation exists on self self.save() else: # this is a forward relation on the target, # so we need to save it target.save() # reload so that indirect relations back to this object # eg self.charge.invoice = self are set # TODO - reverse the field reference here to avoid hitting the DB? self.refresh_from_db() else: unprocessed_pending_relations.append(post_save_relation) if len(pending_relations) != len(unprocessed_pending_relations): # replace in place so passed in list is updated in calling method pending_relations[:] = unprocessed_pending_relations @classmethod def _create_from_stripe_object( cls, data, current_ids=None, pending_relations=None, save=True, stripe_account=None, api_key=djstripe_settings.STRIPE_SECRET_KEY, ): """ Instantiates a model instance using the provided data object received from Stripe, and saves it to the database if specified. :param data: The data dictionary received from the Stripe API. :type data: dict :param current_ids: stripe ids of objects that are currently being processed :type current_ids: set :param pending_relations: list of tuples of relations to be attached post-save :type pending_relations: list :param save: If True, the object is saved after instantiation. :type save: bool :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string :returns: The instantiated object. """ stripe_data = cls._stripe_object_to_record( data, current_ids=current_ids, pending_relations=pending_relations, stripe_account=stripe_account, api_key=api_key, ) try: id_ = get_id_from_stripe_data(stripe_data) if id_ is not None: instance = cls.stripe_objects.get(id=id_) else: # Raise error on purpose to resume the _create_from_stripe_object flow raise cls.DoesNotExist except cls.DoesNotExist: # try to create iff instance doesn't already exist in the DB # TODO dictionary unpacking will not work if cls has any ManyToManyField instance = cls(**stripe_data) instance._attach_objects_hook( cls, data, api_key=api_key, current_ids=current_ids ) if save: instance.save() instance._attach_objects_post_save_hook( cls, data, api_key=api_key, pending_relations=pending_relations ) return instance @classmethod def _get_or_create_from_stripe_object( cls, data, field_name="id", refetch=True, current_ids=None, pending_relations=None, save=True, stripe_account=None, api_key=djstripe_settings.STRIPE_SECRET_KEY, ): """ :param data: :param field_name: :param refetch: :param current_ids: stripe ids of objects that are currently being processed :type current_ids: set :param pending_relations: list of tuples of relations to be attached post-save :type pending_relations: list :param save: :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string :return: :rtype: cls, bool """ field = data.get(field_name) is_nested_data = field_name != "id" should_expand = False if pending_relations is None: pending_relations = [] id_ = get_id_from_stripe_data(field) if not field: # An empty field - We need to return nothing here because there is # no way of knowing what needs to be fetched! raise RuntimeError( f"dj-stripe encountered an empty field {cls.__name__}.{field_name} = {field}" ) elif id_ == field: # A field like {"subscription": "sub_6lsC8pt7IcFpjA", ...} # We'll have to expand if the field is not "id" (= is nested) should_expand = is_nested_data else: # A field like {"subscription": {"id": sub_6lsC8pt7IcFpjA", ...}} data = field try: return cls.stripe_objects.get(id=id_), False except cls.DoesNotExist: if is_nested_data and refetch: # This is what `data` usually looks like: # {"id": "cus_XXXX", "default_source": "card_XXXX"} # Leaving the default field_name ("id") will get_or_create the customer. # If field_name="default_source", we get_or_create the card instead. cls_instance = cls(id=id_) try: data = cls_instance.api_retrieve( stripe_account=stripe_account, api_key=api_key ) except InvalidRequestError as e: if "a similar object exists in" in str(e): # HACK around a Stripe bug. # When a File is retrieved from the Account object, # a mismatch between live and test mode is possible depending # on whether the file (usually the logo) was uploaded in live # or test. Reported to Stripe in August 2020. # Context: https://github.com/dj-stripe/dj-stripe/issues/830 pass elif "No such PaymentMethod:" in str(e): # payment methods (card_… etc) can be irretrievably deleted, # but still present during sync. For example, if a refund is # issued on a charge whose payment method has been deleted. return None, False else: raise should_expand = False # The next thing to happen will be the "create from stripe object" call. # At this point, if we don't have data to start with (field is a str), # *and* we didn't refetch by id, then `should_expand` is True and we # don't have the data to actually create the object. # If this happens when syncing Stripe data, it's a djstripe bug. Report it! if should_expand: raise ValueError(f"No data to create {cls.__name__} from {field_name}") try: # We wrap the `_create_from_stripe_object` in a transaction to # avoid TransactionManagementError on subsequent queries in case # of the IntegrityError catch below. See PR #903 with transaction.atomic(): return ( cls._create_from_stripe_object( data, current_ids=current_ids, pending_relations=pending_relations, save=save, stripe_account=stripe_account, api_key=api_key, ), True, ) except IntegrityError: # Handle the race condition that something else created the object # after the `get` and before `_create_from_stripe_object`. # This is common during webhook handling, since Stripe sends # multiple webhook events simultaneously, # each of which will cause recursive syncs. See issue #429 return cls.stripe_objects.get(id=id_), False @classmethod def _stripe_object_to_customer( cls, target_cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, current_ids=None, ): """ Search the given manager for the Customer matching this object's ``customer`` field. :param target_cls: The target class :type target_cls: Customer :param data: stripe object :type data: dict :param current_ids: stripe ids of objects that are currently being processed :type current_ids: set """ if "customer" in data and data["customer"]: return target_cls._get_or_create_from_stripe_object( data, "customer", current_ids=current_ids, api_key=api_key )[0] @classmethod def _stripe_object_to_default_tax_rates( cls, target_cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY ): """ Retrieves TaxRates for a Subscription or Invoice :param target_cls: :param data: :param instance: :type instance: Union[djstripe.models.Invoice, djstripe.models.Subscription] :return: """ tax_rates = [] for tax_rate_data in data.get("default_tax_rates", []): tax_rate, _ = target_cls._get_or_create_from_stripe_object( tax_rate_data, refetch=False, api_key=api_key ) tax_rates.append(tax_rate) return tax_rates @classmethod def _stripe_object_to_tax_rates( cls, target_cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY ): """ Retrieves TaxRates for a SubscriptionItem or InvoiceItem :param target_cls: :param data: :return: """ tax_rates = [] for tax_rate_data in data.get("tax_rates", []): tax_rate, _ = target_cls._get_or_create_from_stripe_object( tax_rate_data, refetch=False, api_key=api_key ) tax_rates.append(tax_rate) return tax_rates @classmethod def _stripe_object_set_total_tax_amounts( cls, target_cls, data, instance, api_key=djstripe_settings.STRIPE_SECRET_KEY ): """ Set total tax amounts on Invoice instance :param target_cls: :param data: :param instance: :type instance: djstripe.models.Invoice :return: """ from .billing import TaxRate pks = [] for tax_amount_data in data.get("total_tax_amounts", []): tax_rate_data = tax_amount_data["tax_rate"] if isinstance(tax_rate_data, str): tax_rate_data = {"tax_rate": tax_rate_data} tax_rate, _ = TaxRate._get_or_create_from_stripe_object( tax_rate_data, field_name="tax_rate", refetch=True, api_key=api_key, ) tax_amount, _ = target_cls.objects.update_or_create( invoice=instance, tax_rate=tax_rate, defaults={ "amount": tax_amount_data["amount"], "inclusive": tax_amount_data["inclusive"], }, ) pks.append(tax_amount.pk) instance.total_tax_amounts.exclude(pk__in=pks).delete() @classmethod def _stripe_object_to_line_items( cls, target_cls, data, invoice, api_key=djstripe_settings.STRIPE_SECRET_KEY ): """ Retrieves LineItems for an invoice. If the line item doesn't exist already then it is created. If the invoice is an upcoming invoice that doesn't persist to the database (i.e. ephemeral) then the line items are also not saved. :param target_cls: The target class to instantiate per line item. :type target_cls: Type[djstripe.models.LineItem] :param data: The data dictionary received from the Stripe API. :type data: dict :param invoice: The invoice object that should hold the line items. :type invoice: ``djstripe.models.Invoice`` """ lines = data.get("lines") if not lines: return [] lineitems = [] for line in lines.auto_paging_iter(): if invoice.id: save = True line.setdefault("invoice", invoice.id) else: # Don't save invoice items for ephemeral invoices save = False line.setdefault("customer", invoice.customer.id) line.setdefault("date", int(dateformat.format(invoice.created, "U"))) item, _ = target_cls._get_or_create_from_stripe_object( line, refetch=False, save=save, api_key=api_key ) lineitems.append(item) return lineitems @classmethod def _stripe_object_to_subscription_items( cls, target_cls, data, subscription, api_key=djstripe_settings.STRIPE_SECRET_KEY ): """ Retrieves SubscriptionItems for a subscription. If the subscription item doesn't exist already then it is created. :param target_cls: The target class to instantiate per invoice item. :type target_cls: Type[djstripe.models.SubscriptionItem] :param data: The data dictionary received from the Stripe API. :type data: dict :param subscription: The subscription object that should hold the items. :type subscription: djstripe.models.Subscription """ items = data.get("items") if not items: subscription.items.delete() return [] pks = [] subscriptionitems = [] for item_data in items.auto_paging_iter(): item, _ = target_cls._get_or_create_from_stripe_object( item_data, refetch=False, api_key=api_key ) # sync the SubscriptionItem target_cls.sync_from_stripe_data(item_data, api_key=api_key) pks.append(item.pk) subscriptionitems.append(item) subscription.items.exclude(pk__in=pks).delete() return subscriptionitems @classmethod def _stripe_object_to_refunds( cls, target_cls, data, charge, api_key=djstripe_settings.STRIPE_SECRET_KEY ): """ Retrieves Refunds for a charge :param target_cls: The target class to instantiate per refund :type target_cls: Type[djstripe.models.Refund] :param data: The data dictionary received from the Stripe API. :type data: dict :param charge: The charge object that refunds are for. :type charge: djstripe.models.Refund :return: """ stripe_refunds = convert_to_stripe_object(data.get("refunds")) if not stripe_refunds: return [] refund_objs = [] for refund_data in stripe_refunds.auto_paging_iter(): item, _ = target_cls._get_or_create_from_stripe_object( refund_data, refetch=False, api_key=api_key, ) refund_objs.append(item) return refund_objs @classmethod def sync_from_stripe_data( cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, ): """ Syncs this object from the stripe data provided. Foreign keys will also be retrieved and synced recursively. :param data: stripe object :type data: dict :rtype: cls """ current_ids = set() data_id = data.get("id") stripe_account = getattr(data, "stripe_account", None) if data_id: # stop nested objects from trying to retrieve this object before # initial sync is complete current_ids.add(data_id) instance, created = cls._get_or_create_from_stripe_object( data, current_ids=current_ids, stripe_account=stripe_account, api_key=api_key, ) if not created: record_data = cls._stripe_object_to_record( data, api_key=api_key, stripe_account=stripe_account ) for attr, value in record_data.items(): setattr(instance, attr, value) instance._attach_objects_hook( cls, data, api_key=api_key, current_ids=current_ids ) instance.save() instance._attach_objects_post_save_hook(cls, data, api_key=api_key) for field in instance._meta.concrete_fields: if isinstance(field, (StripePercentField, models.UUIDField)): # get rid of cached values delattr(instance, field.name) return instance @classmethod def _get_or_retrieve(cls, id, stripe_account=None, **kwargs): """ Retrieve object from the db, if it exists. If it doesn't, query Stripe to fetch the object and sync with the db. """ try: return cls.objects.get(id=id) except cls.DoesNotExist: pass if stripe_account: kwargs["stripe_account"] = str(stripe_account) # If no API key is specified, use the default one for the specified livemode # (or if no livemode is specified, the default one altogether) kwargs.setdefault( "api_key", djstripe_settings.get_default_api_key(livemode=kwargs.get("livemode")), ) data = cls.stripe_class.retrieve( id=id, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs ) instance = cls.sync_from_stripe_data(data, api_key=kwargs.get("api_key")) return instance def __str__(self): return f"" class IdempotencyKey(models.Model): uuid = models.UUIDField( max_length=36, primary_key=True, editable=False, default=uuid.uuid4 ) action = models.CharField(max_length=100) livemode = models.BooleanField( help_text="Whether the key was used in live or test mode." ) created = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ("action", "livemode") def __str__(self): return str(self.uuid) @property def is_expired(self) -> bool: return timezone.now() > self.created + timedelta(hours=24) ================================================ FILE: djstripe/models/billing.py ================================================ import logging import warnings from typing import Optional, Union import stripe from django.db import models from django.utils import timezone from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ from stripe.error import InvalidRequestError from .. import enums from ..fields import ( InvoiceOrLineItemForeignKey, JSONField, PaymentMethodForeignKey, StripeCurrencyCodeField, StripeDateTimeField, StripeDecimalCurrencyAmountField, StripeEnumField, StripeForeignKey, StripeIdField, StripePercentField, StripeQuantumCurrencyAmountField, ) from ..managers import SubscriptionManager from ..settings import djstripe_settings from ..utils import QuerySetMock, get_friendly_currency_amount, get_id_from_stripe_data from .base import StripeModel from .core import Customer logger = logging.getLogger(__name__) # TODO Mimic stripe-python decorator pattern to easily add and expose CRUD operations like create, update, delete etc on models # TODO Add Tests class DjstripeInvoiceTotalTaxAmount(models.Model): """ An internal model that holds the value of elements of Invoice.total_tax_amounts Note that this is named with the prefix Djstripe to avoid potential collision with a Stripe API object name. """ invoice = StripeForeignKey( "Invoice", on_delete=models.CASCADE, related_name="total_tax_amounts" ) amount = StripeQuantumCurrencyAmountField( help_text="The amount, in cents, of the tax." ) inclusive = models.BooleanField( help_text="Whether this tax amount is inclusive or exclusive." ) tax_rate = StripeForeignKey( "TaxRate", on_delete=models.CASCADE, help_text="The tax rate that was applied to get this tax amount.", ) class Meta: unique_together = ["invoice", "tax_rate"] # TODO Add Tests class DjstripeUpcomingInvoiceTotalTaxAmount(models.Model): """ As per DjstripeInvoiceTotalTaxAmount, except for UpcomingInvoice """ invoice = models.ForeignKey( # Don't define related_name since property is defined in UpcomingInvoice "UpcomingInvoice", on_delete=models.CASCADE, related_name="+", ) amount = StripeQuantumCurrencyAmountField( help_text="The amount, in cents, of the tax." ) inclusive = models.BooleanField( help_text="Whether this tax amount is inclusive or exclusive." ) tax_rate = StripeForeignKey( "TaxRate", on_delete=models.CASCADE, help_text="The tax rate that was applied to get this tax amount.", ) class Meta: unique_together = ["invoice", "tax_rate"] class Coupon(StripeModel): """ A coupon contains information about a percent-off or amount-off discount you might want to apply to a customer. Coupons may be applied to invoices or orders. Coupons do not work with conventional one-off charges. Stripe documentation: https://stripe.com/docs/api/coupons?lang=python """ stripe_class = stripe.Coupon expand_fields = ["applies_to"] stripe_dashboard_item_name = "coupons" id = StripeIdField(max_length=500) applies_to = JSONField( null=True, blank=True, help_text="Contains information about what this coupon applies to.", ) amount_off = StripeDecimalCurrencyAmountField( null=True, blank=True, help_text="Amount (as decimal) that will be taken off the subtotal of any " "invoices for this customer.", ) currency = StripeCurrencyCodeField(null=True, blank=True) duration = StripeEnumField( enum=enums.CouponDuration, help_text=( "Describes how long a customer who applies this coupon " "will get the discount." ), default=enums.CouponDuration.once, ) duration_in_months = models.PositiveIntegerField( null=True, blank=True, help_text="If `duration` is `repeating`, the number of months " "the coupon applies.", ) max_redemptions = models.PositiveIntegerField( null=True, blank=True, help_text="Maximum number of times this coupon can be redeemed, in total, " "before it is no longer valid.", ) name = models.TextField( max_length=5000, default="", blank=True, help_text=( "Name of the coupon displayed to customers on for instance invoices " "or receipts." ), ) percent_off = StripePercentField( null=True, blank=True, help_text=( "Percent that will be taken off the subtotal of any invoices for " "this customer for the duration of the coupon. " "For example, a coupon with percent_off of 50 will make a " "$100 invoice $50 instead." ), ) redeem_by = StripeDateTimeField( null=True, blank=True, help_text="Date after which the coupon can no longer be redeemed. " "Max 5 years in the future.", ) times_redeemed = models.PositiveIntegerField( editable=False, default=0, help_text="Number of times this coupon has been applied to a customer.", ) # valid = models.BooleanField(editable=False) class Meta(StripeModel.Meta): unique_together = ("id", "livemode") def __str__(self): if self.name: return self.name return self.human_readable @property def human_readable_amount(self): if self.percent_off: amount = f"{self.percent_off}%" elif self.currency: amount = get_friendly_currency_amount(self.amount_off or 0, self.currency) else: amount = "(invalid amount)" return f"{amount} off" @property def human_readable(self): if self.duration == enums.CouponDuration.repeating: if self.duration_in_months == 1: duration = "for 1 month" else: duration = f"for {self.duration_in_months} months" else: duration = self.duration return f"{self.human_readable_amount} {duration}" class Discount(StripeModel): """ A discount represents the actual application of a coupon or promotion code. It contains information about when the discount began, when it will end, and what it is applied to. Stripe documentation: https://stripe.com/docs/api/discounts """ expand_fields = ["customer"] stripe_class = None checkout_session = StripeForeignKey( "Session", null=True, blank=True, on_delete=models.CASCADE, help_text="The Checkout session that this coupon is applied to, if it is applied to a particular session in payment mode. Will not be present for subscription mode.", ) coupon = JSONField( null=True, blank=True, help_text="Hash describing the coupon applied to create this discount.", ) customer = StripeForeignKey( "Customer", null=True, blank=True, on_delete=models.CASCADE, help_text="The ID of the customer associated with this discount.", related_name="customer_discounts", ) end = StripeDateTimeField( null=True, blank=True, help_text=( "If the coupon has a duration of repeating, the date that this discount will end. If the coupon has a duration of once or forever, this attribute will be null." ), ) invoice = StripeForeignKey( "Invoice", null=True, blank=True, on_delete=models.CASCADE, help_text="The invoice that the discount’s coupon was applied to, if it was applied directly to a particular invoice.", related_name="invoice_discounts", ) invoice_item = InvoiceOrLineItemForeignKey( null=True, blank=True, on_delete=models.CASCADE, help_text="The invoice item id (or invoice line item id for invoice line items of type=‘subscription’) that the discount’s coupon was applied to, if it was applied directly to a particular invoice item or invoice line item.", ) promotion_code = models.CharField( max_length=255, blank=True, help_text="The promotion code applied to create this discount.", ) start = StripeDateTimeField( null=True, blank=True, help_text=("Date that the coupon was applied."), ) subscription = StripeForeignKey( "subscription", null=True, blank=True, on_delete=models.CASCADE, help_text="The subscription that this coupon is applied to, if it is applied to a particular subscription.", related_name="subscription_discounts", ) @classmethod def is_valid_object(cls, data): """ Returns whether the data is a valid object for the class """ return "object" in data and data["object"] == "discount" class BaseInvoice(StripeModel): """ The abstract base model shared by Invoice and UpcomingInvoice Note: Most fields are defined on BaseInvoice so they're available to both models. ManyToManyFields are an exception, since UpcomingInvoice doesn't exist in the db. """ stripe_class = stripe.Invoice stripe_dashboard_item_name = "invoices" expand_fields = ["discounts"] account_country = models.CharField( max_length=2, default="", blank=True, help_text="The country of the business associated with this invoice, " "most often the business creating the invoice.", ) account_name = models.TextField( max_length=5000, blank=True, help_text="The public name of the business associated with this invoice, " "most often the business creating the invoice.", ) amount_due = StripeDecimalCurrencyAmountField( help_text="Final amount due (as decimal) at this time for this invoice. " "If the invoice's total is smaller than the minimum charge amount, " "for example, or if there is account credit that can be applied to the " "invoice, the amount_due may be 0. If there is a positive starting_balance " "for the invoice (the customer owes money), the amount_due will also take that " "into account. The charge that gets generated for the invoice will be for " "the amount specified in amount_due." ) amount_paid = StripeDecimalCurrencyAmountField( null=True, # XXX: This is not nullable, but it's a new field help_text="The amount, (as decimal), that was paid.", ) amount_remaining = StripeDecimalCurrencyAmountField( null=True, # XXX: This is not nullable, but it's a new field help_text="The amount remaining, (as decimal), that is due.", ) application_fee_amount = StripeDecimalCurrencyAmountField( null=True, blank=True, help_text="The fee (as decimal) that will be applied to the invoice and " "transferred to the application owner's " "Stripe account when the invoice is paid.", ) attempt_count = models.IntegerField( help_text="Number of payment attempts made for this invoice, " "from the perspective of the payment retry schedule. " "Any payment attempt counts as the first attempt, and subsequently " "only automatic retries increment the attempt count. " "In other words, manual payment attempts after the first attempt do not affect " "the retry schedule." ) attempted = models.BooleanField( default=False, help_text="Whether or not an attempt has been made to pay the invoice. " "An invoice is not attempted until 1 hour after the ``invoice.created`` " "webhook, for example, so you might not want to display that invoice as " "unpaid to your users.", ) auto_advance = models.BooleanField( null=True, help_text="Controls whether Stripe will perform automatic collection of the " "invoice. When false, the invoice's state will not automatically " "advance without an explicit action.", ) billing_reason = StripeEnumField( default="", blank=True, enum=enums.InvoiceBillingReason, help_text="Indicates the reason why the invoice was created. " "subscription_cycle indicates an invoice created by a subscription advancing " "into a new period. subscription_create indicates an invoice created due to " "creating a subscription. subscription_update indicates an invoice created due " "to updating a subscription. subscription is set for all old invoices to " "indicate either a change to a subscription or a period advancement. " "manual is set for all invoices unrelated to a subscription " "(for example: created via the invoice editor). The upcoming value is " "reserved for simulated invoices per the upcoming invoice endpoint. " "subscription_threshold indicates an invoice created due to a billing " "threshold being reached.", ) charge = models.OneToOneField( "Charge", on_delete=models.CASCADE, null=True, # we need to use the %(class)s placeholder to avoid related name # clashes between Invoice and UpcomingInvoice related_name="latest_%(class)s", help_text="The latest charge generated for this invoice, if any.", ) collection_method = StripeEnumField( enum=enums.InvoiceCollectionMethod, null=True, help_text=( "When charging automatically, Stripe will attempt to pay this invoice " "using the default source attached to the customer. " "When sending an invoice, Stripe will email this invoice to the customer " "with payment instructions." ), ) currency = StripeCurrencyCodeField() customer = StripeForeignKey( "Customer", on_delete=models.CASCADE, # we need to use the %(class)s placeholder to avoid related name # clashes between Invoice and UpcomingInvoice related_name="%(class)ss", help_text="The customer associated with this invoice.", ) customer_address = JSONField( null=True, blank=True, help_text="The customer's address. Until the invoice is finalized, this " "field will equal customer.address. Once the invoice is finalized, this field " "will no longer be updated.", ) customer_email = models.TextField( max_length=5000, blank=True, help_text="The customer's email. Until the invoice is finalized, this field " "will equal customer.email. Once the invoice is finalized, this field will no " "longer be updated.", ) customer_name = models.TextField( max_length=5000, blank=True, help_text="The customer's name. Until the invoice is finalized, this field " "will equal customer.name. Once the invoice is finalized, this field will no " "longer be updated.", ) customer_phone = models.TextField( max_length=5000, blank=True, help_text="The customer's phone number. Until the invoice is finalized, " "this field will equal customer.phone. Once the invoice is finalized, " "this field will no longer be updated.", ) customer_shipping = JSONField( null=True, blank=True, help_text="The customer's shipping information. Until the invoice is " "finalized, this field will equal customer.shipping. Once the invoice is " "finalized, this field will no longer be updated.", ) customer_tax_exempt = StripeEnumField( enum=enums.CustomerTaxExempt, default="", help_text="The customer's tax exempt status. Until the invoice is finalized, " "this field will equal customer.tax_exempt. Once the invoice is " "finalized, this field will no longer be updated.", ) default_payment_method = StripeForeignKey( "PaymentMethod", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="Default payment method for the invoice. It must belong to the " "customer associated with the invoice. If not set, defaults to the " "subscription's default payment method, if any, or to the default payment " "method in the customer's invoice settings.", ) # Note: default_tax_rates is handled in the subclasses since it's a # ManyToManyField, otherwise reverse accessors clash discount = JSONField( null=True, blank=True, help_text="Deprecated! Please use discounts instead. Describes the current discount applied to this " "subscription, if there is one. When billing, a discount applied to a " "subscription overrides a discount applied on a customer-wide basis.", ) discounts = JSONField( null=True, blank=True, help_text="The discounts applied to the invoice. Line item discounts are applied before invoice discounts.", ) due_date = StripeDateTimeField( null=True, blank=True, help_text=( "The date on which payment for this invoice is due. " "This value will be null for invoices where billing=charge_automatically." ), ) ending_balance = StripeQuantumCurrencyAmountField( null=True, help_text="Ending customer balance (in cents) after attempting to pay invoice. " "If the invoice has not been attempted yet, this will be null.", ) footer = models.TextField( max_length=5000, blank=True, help_text="Footer displayed on the invoice." ) hosted_invoice_url = models.TextField( max_length=799, default="", blank=True, help_text="The URL for the hosted invoice page, which allows customers to view " "and pay an invoice. If the invoice has not been frozen yet, " "this will be null.", ) invoice_pdf = models.TextField( max_length=799, default="", blank=True, help_text=( "The link to download the PDF for the invoice. " "If the invoice has not been frozen yet, this will be null." ), ) # TODO: Implement "lines" (InvoiceLineItem related_field) next_payment_attempt = StripeDateTimeField( null=True, blank=True, help_text="The time at which payment will next be attempted.", ) number = models.CharField( max_length=64, default="", blank=True, help_text=( "A unique, identifying string that appears on emails sent to the customer " "for this invoice. " "This starts with the customer's unique invoice_prefix if it is specified." ), ) paid = models.BooleanField( default=False, help_text=( "Whether payment was successfully collected for this invoice. An invoice " "can be paid (most commonly) with a charge or with credit from the " "customer's account balance." ), ) payment_intent = models.OneToOneField( "PaymentIntent", on_delete=models.CASCADE, null=True, help_text=( "The PaymentIntent associated with this invoice. " "The PaymentIntent is generated when the invoice is finalized, " "and can then be used to pay the invoice." "Note that voiding an invoice will cancel the PaymentIntent" ), ) period_end = StripeDateTimeField( help_text="End of the usage period during which invoice items were " "added to this invoice." ) period_start = StripeDateTimeField( help_text="Start of the usage period during which invoice items were " "added to this invoice." ) post_payment_credit_notes_amount = StripeQuantumCurrencyAmountField( # This is not nullable, but it's a new field null=True, blank=True, help_text="Total amount (in cents) of all post-payment credit notes issued " "for this invoice.", ) pre_payment_credit_notes_amount = StripeQuantumCurrencyAmountField( # This is not nullable, but it's a new field null=True, blank=True, help_text="Total amount (in cents) of all pre-payment credit notes issued " "for this invoice.", ) receipt_number = models.CharField( max_length=64, null=True, blank=True, help_text=( "This is the transaction number that appears on email receipts " "sent for this invoice." ), ) starting_balance = StripeQuantumCurrencyAmountField( help_text="Starting customer balance (in cents) before attempting to pay " "invoice. If the invoice has not been attempted yet, this will be the " "current customer balance." ) statement_descriptor = models.CharField( max_length=22, default="", blank=True, help_text="An arbitrary string to be displayed on your customer's " "credit card statement. The statement description may not include <>\"' " "characters, and will appear on your customer's statement in capital letters. " "Non-ASCII characters are automatically stripped. " "While most banks display this information consistently, " "some may display it incorrectly or not at all.", ) status = StripeEnumField( default="", blank=True, enum=enums.InvoiceStatus, help_text="The status of the invoice, one of draft, open, paid, " "uncollectible, or void.", ) status_transitions = JSONField(null=True, blank=True) subscription = StripeForeignKey( "Subscription", null=True, # we need to use the %(class)s placeholder to avoid related name # clashes between Invoice and UpcomingInvoice related_name="%(class)ss", on_delete=models.SET_NULL, help_text="The subscription that this invoice was prepared for, if any.", ) subscription_proration_date = StripeDateTimeField( null=True, blank=True, help_text="Only set for upcoming invoices that preview prorations. " "The time used to calculate prorations.", ) subtotal = StripeDecimalCurrencyAmountField( help_text="Total (as decimal) of all subscriptions, invoice items, " "and prorations on the invoice before any discount or tax is applied." ) tax = StripeDecimalCurrencyAmountField( null=True, blank=True, help_text="The amount (as decimal) of tax included in the total, calculated " "from ``tax_percent`` and the subtotal. If no " "``tax_percent`` is defined, this value will be null.", ) tax_percent = StripePercentField( null=True, blank=True, help_text="This percentage of the subtotal has been added to the total amount " "of the invoice, including invoice line items and discounts. " "This field is inherited from the subscription's ``tax_percent`` field, " "but can be changed before the invoice is paid. This field defaults to null.", ) threshold_reason = JSONField( null=True, blank=True, help_text="If billing_reason is set to subscription_threshold this returns " "more information on which threshold rules triggered the invoice.", ) total = StripeDecimalCurrencyAmountField("Total (as decimal) after discount.") webhooks_delivered_at = StripeDateTimeField( null=True, help_text=( "The time at which webhooks for this invoice were successfully delivered " "(if the invoice had no webhooks to deliver, this will match `date`). " "Invoice payment is delayed until webhooks are delivered, or until all " "webhook delivery attempts have been exhausted." ), ) class Meta(StripeModel.Meta): abstract = True ordering = ["-created"] def __str__(self): invoice_number = self.number or self.receipt_number or self.id amount = get_friendly_currency_amount(self.amount_paid or 0, self.currency) return f"Invoice #{invoice_number} for {amount} ({self.status})" @classmethod def upcoming( cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, customer=None, subscription=None, subscription_plan=None, **kwargs, ) -> Optional["UpcomingInvoice"]: """ Gets the upcoming preview invoice (singular) for a customer. At any time, you can preview the upcoming invoice for a customer. This will show you all the charges that are pending, including subscription renewal charges, invoice item charges, etc. It will also show you any discount that is applicable to the customer. (Source: https://stripe.com/docs/api#upcoming_invoice) .. important:: Note that when you are viewing an upcoming invoice, you are simply viewing a preview. :param customer: The identifier of the customer whose upcoming invoice \ you'd like to retrieve. :type customer: Customer or string (customer ID) :param coupon: The code of the coupon to apply. :type coupon: str :param subscription: The identifier of the subscription to retrieve an \ invoice for. :type subscription: Subscription or string (subscription ID) :param subscription_plan: If set, the invoice returned will preview \ updating the subscription given to this plan, or creating a new \ subscription to this plan if no subscription is given. :type subscription_plan: Plan or string (plan ID) """ # Convert Customer to id if customer is not None and isinstance(customer, StripeModel): customer = customer.id # Convert Subscription to id if subscription is not None and isinstance(subscription, StripeModel): subscription = subscription.id # Convert Plan to id if subscription_plan is not None and isinstance(subscription_plan, StripeModel): subscription_plan = subscription_plan.id try: upcoming_stripe_invoice = cls.stripe_class.upcoming( api_key=api_key, customer=customer, subscription=subscription, subscription_plan=subscription_plan, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ) except InvalidRequestError as exc: if str(exc) != "Nothing to invoice for customer": raise return None # Workaround for "id" being missing (upcoming invoices don't persist). upcoming_stripe_invoice["id"] = "upcoming" return UpcomingInvoice._create_from_stripe_object( upcoming_stripe_invoice, save=False, api_key=api_key, ) def retry(self): """Retry payment on this invoice if it isn't paid.""" if self.status != enums.InvoiceStatus.paid and self.auto_advance: stripe_invoice = self.api_retrieve() updated_stripe_invoice = ( stripe_invoice.pay() ) # pay() throws an exception if the charge is not successful. type(self).sync_from_stripe_data( updated_stripe_invoice, api_key=self.default_api_key ) return True return False def _attach_objects_post_save_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, pending_relations=None, ): super()._attach_objects_post_save_hook( cls, data, api_key=api_key, pending_relations=pending_relations ) # LineItems need a saved invoice because they're associated via a # RelatedManager, so this must be done as part of the post save hook. cls._stripe_object_to_line_items( target_cls=LineItem, data=data, invoice=self, api_key=api_key ) # sync every discount for discount in self.discounts: Discount.sync_from_stripe_data(discount, api_key=api_key) @property def plan(self) -> Optional["Plan"]: """Gets the associated plan for this invoice. In order to provide a consistent view of invoices, the plan object should be taken from the first invoice item that has one, rather than using the plan associated with the subscription. Subscriptions (and their associated plan) are updated by the customer and represent what is current, but invoice items are immutable within the invoice and stay static/unchanged. In other words, a plan retrieved from an invoice item will represent the plan as it was at the time an invoice was issued. The plan retrieved from the subscription will be the currently active plan. :returns: The associated plan for the invoice. """ for invoiceitem in self.invoiceitems.all(): if invoiceitem.plan: return invoiceitem.plan if self.subscription: return self.subscription.plan return None class Invoice(BaseInvoice): """ Invoices are statements of what a customer owes for a particular billing period, including subscriptions, invoice items, and any automatic proration adjustments if necessary. Once an invoice is created, payment is automatically attempted. Note that the payment, while automatic, does not happen exactly at the time of invoice creation. If you have configured webhooks, the invoice will wait until one hour after the last webhook is successfully sent (or the last webhook times out after failing). Any customer credit on the account is applied before determining how much is due for that invoice (the amount that will be actually charged). If the amount due for the invoice is less than 50 cents (the minimum for a charge), we add the amount to the customer's running account balance to be added to the next invoice. If this amount is negative, it will act as a credit to offset the next invoice. Note that the customer account balance does not include unpaid invoices; it only includes balances that need to be taken into account when calculating the amount due for the next invoice. Stripe documentation: https://stripe.com/docs/api?lang=python#invoices """ default_source = PaymentMethodForeignKey( on_delete=models.SET_NULL, null=True, blank=True, related_name="invoices", help_text="The default payment source for the invoice. " "It must belong to the customer associated with the invoice and be " "in a chargeable state. If not set, defaults to the subscription's " "default source, if any, or to the customer's default source.", ) # Note: # Most fields are defined on BaseInvoice so they're shared with UpcomingInvoice. # ManyToManyFields are an exception, since UpcomingInvoice doesn't exist in the db. default_tax_rates = models.ManyToManyField( "TaxRate", # explicitly specify the joining table name as though the joining model # was defined with through="DjstripeInvoiceDefaultTaxRate" db_table="djstripe_djstripeinvoicedefaulttaxrate", related_name="+", blank=True, help_text="The tax rates applied to this invoice, if any.", ) def _attach_objects_post_save_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, pending_relations=None, ): super()._attach_objects_post_save_hook( cls, data, api_key=api_key, pending_relations=pending_relations ) self.default_tax_rates.set( cls._stripe_object_to_default_tax_rates( target_cls=TaxRate, data=data, api_key=api_key ) ) cls._stripe_object_set_total_tax_amounts( target_cls=DjstripeInvoiceTotalTaxAmount, data=data, instance=self, api_key=api_key, ) class UpcomingInvoice(BaseInvoice): """ The preview of an upcoming invoice - does not exist in the Django database. See BaseInvoice.upcoming() Logically it should be set abstract, but that doesn't quite work since we do actually want to instantiate the model and use relations. """ default_source = PaymentMethodForeignKey( on_delete=models.SET_NULL, null=True, related_name="upcoming_invoices", help_text="The default payment source for the invoice. " "It must belong to the customer associated with the invoice and be " "in a chargeable state. If not set, defaults to the subscription's " "default source, if any, or to the customer's default source.", ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._lineitems = [] self._default_tax_rates = [] self._total_tax_amounts = [] def get_stripe_dashboard_url(self): return "" def _attach_objects_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, current_ids=None ): super()._attach_objects_hook( cls, data, api_key=api_key, current_ids=current_ids ) self._lineitems = cls._stripe_object_to_line_items( target_cls=LineItem, data=data, invoice=self, api_key=api_key ) def _attach_objects_post_save_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, pending_relations=None, ): super()._attach_objects_post_save_hook( cls, data, api_key=api_key, pending_relations=pending_relations ) self._default_tax_rates = cls._stripe_object_to_default_tax_rates( target_cls=TaxRate, data=data, api_key=api_key ) total_tax_amounts = [] for tax_amount_data in data.get("total_tax_amounts", []): tax_rate_id = tax_amount_data["tax_rate"] if not isinstance(tax_rate_id, str): tax_rate_id = tax_rate_id["tax_rate"] tax_rate = TaxRate._get_or_retrieve(id=tax_rate_id, api_key=api_key) tax_amount = DjstripeUpcomingInvoiceTotalTaxAmount( invoice=self, amount=tax_amount_data["amount"], inclusive=tax_amount_data["inclusive"], tax_rate=tax_rate, ) total_tax_amounts.append(tax_amount) self._total_tax_amounts = total_tax_amounts @property def invoiceitems(self): """ Gets the invoice items associated with this upcoming invoice. This differs from normal (non-upcoming) invoices, in that upcoming invoices are in-memory and do not persist to the database. Therefore, all of the data comes from the Stripe API itself. Instead of returning a normal queryset for the invoiceitems, this will return a mock of a queryset, but with the data fetched from Stripe - It will act like a normal queryset, but mutation will silently fail. """ # filter lineitems with type="invoice_item" and fetch all the actual InvoiceItem objects items = [] for item in self._lineitems: if item.type == "invoice_item": items.append(item.invoice_item) return QuerySetMock.from_iterable(InvoiceItem, items) @property def lineitems(self): """ Gets the line items associated with this upcoming invoice. This differs from normal (non-upcoming) invoices, in that upcoming invoices are in-memory and do not persist to the database. Therefore, all of the data comes from the Stripe API itself. Instead of returning a normal queryset for the lineitems, this will return a mock of a queryset, but with the data fetched from Stripe - It will act like a normal queryset, but mutation will silently fail. """ return QuerySetMock.from_iterable(LineItem, self._lineitems) @property def default_tax_rates(self): """ Gets the default tax rates associated with this upcoming invoice. :return: """ return QuerySetMock.from_iterable(TaxRate, self._default_tax_rates) @property def total_tax_amounts(self): """ Gets the total tax amounts associated with this upcoming invoice. :return: """ return QuerySetMock.from_iterable( DjstripeUpcomingInvoiceTotalTaxAmount, self._total_tax_amounts ) @property def id(self): return None @id.setter def id(self, value): return # noop def save(self, *args, **kwargs): return # noop class InvoiceItem(StripeModel): """ Sometimes you want to add a charge or credit to a customer but only actually charge the customer's card at the end of a regular billing cycle. This is useful for combining several charges to minimize per-transaction fees or having Stripe tabulate your usage-based billing totals. Stripe documentation: https://stripe.com/docs/api?lang=python#invoiceitems """ stripe_class = stripe.InvoiceItem expand_fields = ["discounts"] amount = StripeDecimalCurrencyAmountField(help_text="Amount invoiced (as decimal).") currency = StripeCurrencyCodeField() customer = StripeForeignKey( "Customer", on_delete=models.CASCADE, related_name="invoiceitems", help_text="The customer associated with this invoiceitem.", ) date = StripeDateTimeField(help_text="The date on the invoiceitem.") discountable = models.BooleanField( default=False, help_text="If True, discounts will apply to this invoice item. " "Always False for prorations.", ) discounts = JSONField( null=True, blank=True, help_text="The discounts which apply to the invoice item. Item discounts are applied before invoice discounts.", ) invoice = StripeForeignKey( "Invoice", on_delete=models.CASCADE, null=True, related_name="invoiceitems", help_text="The invoice to which this invoiceitem is attached.", ) period = JSONField() period_end = StripeDateTimeField( help_text="Might be the date when this invoiceitem's invoice was sent." ) period_start = StripeDateTimeField( help_text="Might be the date when this invoiceitem was added to the invoice" ) plan = models.ForeignKey( "Plan", null=True, on_delete=models.SET_NULL, help_text="If the invoice item is a proration, the plan of the subscription " "for which the proration was computed.", ) price = models.ForeignKey( "Price", null=True, related_name="invoiceitems", on_delete=models.SET_NULL, help_text="If the invoice item is a proration, the price of the subscription " "for which the proration was computed.", ) proration = models.BooleanField( default=False, help_text="Whether or not the invoice item was created automatically as a " "proration adjustment when the customer switched plans.", ) quantity = models.IntegerField( null=True, blank=True, help_text="If the invoice item is a proration, the quantity of the " "subscription for which the proration was computed.", ) subscription = StripeForeignKey( "Subscription", null=True, related_name="invoiceitems", on_delete=models.SET_NULL, help_text="The subscription that this invoice item has been created for, " "if any.", ) # XXX: subscription_item tax_rates = models.ManyToManyField( "TaxRate", # explicitly specify the joining table name as though the joining model # was defined with through="DjstripeInvoiceItemTaxRate" db_table="djstripe_djstripeinvoiceitemtaxrate", related_name="+", blank=True, help_text="The tax rates which apply to this invoice item. When set, " "the default_tax_rates on the invoice do not apply to this " "invoice item.", ) unit_amount = StripeQuantumCurrencyAmountField( null=True, blank=True, help_text="Unit amount (in the `currency` specified) of the invoice item.", ) unit_amount_decimal = StripeDecimalCurrencyAmountField( null=True, blank=True, max_digits=19, decimal_places=12, help_text=( "Same as `unit_amount`, but contains a decimal value with " "at most 12 decimal places." ), ) @classmethod def _manipulate_stripe_object_hook(cls, data): data["period_start"] = data["period"]["start"] data["period_end"] = data["period"]["end"] return data def _attach_objects_post_save_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, pending_relations=None, ): super()._attach_objects_post_save_hook( cls, data, api_key=api_key, pending_relations=pending_relations ) if self.pk: # only call .set() on saved instance (ie don't on items of UpcomingInvoice) self.tax_rates.set( cls._stripe_object_to_tax_rates( target_cls=TaxRate, data=data, api_key=api_key ) ) # sync every discount for discount in self.discounts: Discount.sync_from_stripe_data(discount, api_key=api_key) def __str__(self): return self.description def get_stripe_dashboard_url(self): return self.invoice.get_stripe_dashboard_url() def api_retrieve(self, *args, **kwargs): if "-il_" in self.id: warnings.warn( f"Attempting to retrieve InvoiceItem with id={self.id!r}" " will most likely fail. " "Run manage.py djstripe_update_invoiceitem_ids if this is a problem." ) return super().api_retrieve(*args, **kwargs) class LineItem(StripeModel): """ The individual line items that make up the invoice. Stripe documentation: https://stripe.com/docs/api/invoices/line_item """ stripe_class = stripe.InvoiceLineItem expand_fields = ["discounts"] amount = StripeQuantumCurrencyAmountField(help_text="The amount, in cents.") amount_excluding_tax = StripeQuantumCurrencyAmountField( help_text="The integer amount in cents representing the amount for this line item, excluding all tax and discounts." ) currency = StripeCurrencyCodeField() discount_amounts = JSONField( null=True, blank=True, help_text="The amount of discount calculated per discount for this line item.", ) discountable = models.BooleanField( default=False, help_text="If True, discounts will apply to this line item. " "Always False for prorations.", ) discounts = JSONField( null=True, blank=True, help_text="The discounts applied to the invoice line item. Line item discounts are applied before invoice discounts.", ) invoice_item = StripeForeignKey( "InvoiceItem", null=True, blank=True, on_delete=models.CASCADE, help_text="The ID of the invoice item associated with this line item if any.", ) period = JSONField( help_text="The period this line_item covers. For subscription line items, this is the subscription period. For prorations, this starts when the proration was calculated, and ends at the period end of the subscription. For invoice items, this is the time at which the invoice item was created or the period of the item." ) period_end = StripeDateTimeField( help_text="The end of the period, which must be greater than or equal to the start." ) period_start = StripeDateTimeField(help_text="The start of the period.") price = JSONField( help_text="The price of the line item.", ) proration = models.BooleanField( default=False, help_text="Whether or not the invoice item was created automatically as a " "proration adjustment when the customer switched plans.", ) proration_details = JSONField( help_text="Additional details for proration line items" ) subscription = StripeForeignKey( "Subscription", null=True, blank=True, on_delete=models.CASCADE, help_text="The subscription that the invoice item pertains to, if any.", ) subscription_item = StripeForeignKey( "SubscriptionItem", null=True, blank=True, on_delete=models.CASCADE, help_text="The subscription item that generated this invoice item. Left empty if the line item is not an explicit result of a subscription.", ) tax_amounts = JSONField( null=True, blank=True, help_text="The amount of tax calculated per tax rate for this line item", ) tax_rates = JSONField( null=True, blank=True, help_text="The tax rates which apply to the line item." ) type = StripeEnumField(enum=enums.LineItem) unit_amount_excluding_tax = StripeDecimalCurrencyAmountField( null=True, blank=True, help_text=( "The amount in cents representing the unit amount for this line item, excluding all tax and discounts." ), ) quantity = models.IntegerField( null=True, blank=True, help_text="The quantity of the subscription, if the line item is a subscription or a proration.", ) @classmethod def _manipulate_stripe_object_hook(cls, data): data["period_start"] = data["period"]["start"] data["period_end"] = data["period"]["end"] return data def _attach_objects_post_save_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, pending_relations=None, ): super()._attach_objects_post_save_hook( cls, data, api_key=api_key, pending_relations=pending_relations ) # sync every discount for discount in self.discounts: Discount.sync_from_stripe_data(discount, api_key=api_key) @classmethod def api_list(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs): """ Call the stripe API's list operation for this model. Note that we only iterate and sync the LineItem associated with the passed in Invoice. Upcoming invoices are virtual and are not saved and hence their line items are also not retrieved and synced :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string See Stripe documentation for accepted kwargs for each object. :returns: an iterator over all items in the query """ # get current invoice if any invoice_id = kwargs.pop("id") # get expand parameter that needs to be passed to invoice.lines.list call expand_fields = kwargs.pop("expand") invoice = Invoice.stripe_class.retrieve(invoice_id, api_key=api_key, **kwargs) # iterate over all the line items on the current invoice return invoice.lines.list( api_key=api_key, expand=expand_fields, **kwargs ).auto_paging_iter() class InvoiceOrLineItem(models.Model): """An Internal Model that abstracts InvoiceItem and lineItem objects Contains two fields: `id` and `type`: - `id` is the id of the Stripe object. - `type` can be `line_item`, `invoice_item` or `unsupported` """ id = models.CharField(max_length=255, primary_key=True) type = StripeEnumField( enum=enums.InvoiceorLineItemType, help_text="Indicates whether the underlying model is LineItem or InvoiceItem. Can be one of: 'invoice_item', 'line_item' or 'unsupported'", ) @classmethod def _model_type(cls, id_): if id_.startswith("ii"): return InvoiceItem, "invoice_item" elif id_.startswith("il"): return LineItem, "line_item" raise ValueError(f"Unknown object type with id: {id_}") @classmethod def _get_or_create_from_stripe_object( cls, data, field_name="id", refetch=True, current_ids=None, pending_relations=None, save=True, stripe_account=None, api_key=djstripe_settings.STRIPE_SECRET_KEY, ): raw_field_data = data.get(field_name) id_ = get_id_from_stripe_data(raw_field_data) try: object_cls, object_type = cls._model_type(id_) except ValueError: # This may happen if we have object types we don't know about. # Let's not make dj-stripe entirely unusable if that happens. logger.warning(f"Unknown Object. Could not sync object with id: {id_}") return cls.objects.get_or_create(id=id_, defaults={"type": "unsupported"}) # call model's _get_or_create_from_stripe_object to ensure # that object exists before getting or creating its InvoiceorLineItem mapping object_cls._get_or_create_from_stripe_object( data, field_name, refetch=refetch, current_ids=current_ids, pending_relations=pending_relations, stripe_account=stripe_account, api_key=api_key, ) return cls.objects.get_or_create(id=id_, defaults={"type": object_type}) class Plan(StripeModel): """ A subscription plan contains the pricing information for different products and feature levels on your site. Stripe documentation: https://stripe.com/docs/api/plans?lang=python NOTE: The Stripe Plans API has been deprecated in favor of the Prices API. You may want to upgrade to use the Price model instead of the Plan model. """ stripe_class = stripe.Plan expand_fields = ["product", "tiers"] stripe_dashboard_item_name = "plans" active = models.BooleanField( help_text="Whether the plan can be used for new purchases." ) aggregate_usage = StripeEnumField( enum=enums.PlanAggregateUsage, default="", blank=True, help_text=( "Specifies a usage aggregation strategy for plans of usage_type=metered. " "Allowed values are `sum` for summing up all usage during a period, " "`last_during_period` for picking the last usage record reported within a " "period, `last_ever` for picking the last usage record ever (across period " "bounds) or max which picks the usage record with the maximum reported " "usage during a period. Defaults to `sum`." ), ) amount = StripeDecimalCurrencyAmountField( null=True, blank=True, help_text="Amount (as decimal) to be charged on the interval specified.", ) amount_decimal = StripeDecimalCurrencyAmountField( null=True, blank=True, max_digits=19, decimal_places=12, help_text=( "The unit amount in cents to be charged, represented as a decimal " "string with at most 12 decimal places." ), ) billing_scheme = StripeEnumField( enum=enums.BillingScheme, default="", blank=True, help_text=( "Describes how to compute the price per period. " "Either `per_unit` or `tiered`. " "`per_unit` indicates that the fixed amount (specified in amount) " "will be charged per unit in quantity " "(for plans with `usage_type=licensed`), or per unit of total " "usage (for plans with `usage_type=metered`). " "`tiered` indicates that the unit pricing will be computed using " "a tiering strategy as defined using the tiers and tiers_mode attributes." ), ) currency = StripeCurrencyCodeField() interval = StripeEnumField( enum=enums.PlanInterval, help_text="The frequency with which a subscription should be billed.", ) interval_count = models.PositiveIntegerField( null=True, blank=True, help_text=( "The number of intervals (specified in the interval property) " "between each subscription billing." ), ) nickname = models.TextField( max_length=5000, default="", blank=True, help_text="A brief description of the plan, hidden from customers.", ) product = StripeForeignKey( "Product", on_delete=models.SET_NULL, null=True, blank=True, help_text="The product whose pricing this plan determines.", ) tiers = JSONField( null=True, blank=True, help_text=( "Each element represents a pricing tier. " "This parameter requires `billing_scheme` to be set to `tiered`." ), ) tiers_mode = StripeEnumField( enum=enums.PriceTiersMode, null=True, blank=True, help_text=( "Defines if the tiering price should be `graduated` or `volume` based. " "In `volume`-based tiering, the maximum quantity within a period " "determines the per unit price, in `graduated` tiering pricing can " "successively change as the quantity grows." ), ) transform_usage = JSONField( null=True, blank=True, help_text=( "Apply a transformation to the reported usage or set quantity " "before computing the billed price. Cannot be combined with `tiers`." ), ) trial_period_days = models.IntegerField( null=True, blank=True, help_text=( "Number of trial period days granted when subscribing a customer " "to this plan. Null if the plan has no trial period." ), ) usage_type = StripeEnumField( enum=enums.PriceUsageType, default=enums.PriceUsageType.licensed, help_text=( "Configures how the quantity per period should be determined, " "can be either `metered` or `licensed`. `licensed` will automatically " "bill the `quantity` set for a plan when adding it to a subscription, " "`metered` will aggregate the total usage based on usage records. " "Defaults to `licensed`." ), ) class Meta(object): ordering = ["amount"] @classmethod def get_or_create(cls, **kwargs): """Get or create a Plan.""" try: return cls.objects.get(id=kwargs["id"]), False except cls.DoesNotExist: return cls.create(**kwargs), True @classmethod def create(cls, **kwargs): # A few minor things are changed in the api-version of the create call api_kwargs = dict(kwargs) api_kwargs["amount"] = int(api_kwargs["amount"] * 100) if isinstance(api_kwargs.get("product"), StripeModel): api_kwargs["product"] = api_kwargs["product"].id stripe_plan = cls._api_create(**api_kwargs) api_key = api_kwargs.get("api_key") or djstripe_settings.STRIPE_SECRET_KEY plan = cls.sync_from_stripe_data(stripe_plan, api_key=api_key) return plan def __str__(self): if self.product and self.product.name: return f"{self.human_readable_price} for {self.product.name}" return self.human_readable_price @property def amount_in_cents(self): return int(self.amount * 100) @property def human_readable_price(self) -> str: if self.billing_scheme == "per_unit": unit_amount = self.amount amount = get_friendly_currency_amount(unit_amount, self.currency) else: # tiered billing scheme tier_1 = self.tiers[0] flat_amount_tier_1 = tier_1["flat_amount"] formatted_unit_amount_tier_1 = get_friendly_currency_amount( (tier_1["unit_amount"] or 0) / 100, self.currency ) amount = f"Starts at {formatted_unit_amount_tier_1} per unit" # stripe shows flat fee even if it is set to 0.00 if flat_amount_tier_1 is not None: formatted_flat_amount_tier_1 = get_friendly_currency_amount( flat_amount_tier_1 / 100, self.currency ) amount = f"{amount} + {formatted_flat_amount_tier_1}" format_args = {"amount": amount} interval_count = self.interval_count if interval_count == 1: interval = { "day": _("day"), "week": _("week"), "month": _("month"), "year": _("year"), }[self.interval] template = _("{amount}/{interval}") format_args["interval"] = interval else: interval = { "day": _("days"), "week": _("weeks"), "month": _("months"), "year": _("years"), }[self.interval] template = _("{amount} / every {interval_count} {interval}") format_args["interval"] = interval format_args["interval_count"] = interval_count return str(format_lazy(template, **format_args)) class Subscription(StripeModel): """ Subscriptions allow you to charge a customer's card on a recurring basis. A subscription ties a customer to a particular plan you've created. A subscription still in its trial period is ``trialing`` and moves to ``active`` when the trial period is over. When payment to renew the subscription fails, the subscription becomes ``past_due``. After Stripe has exhausted all payment retry attempts, the subscription ends up with a status of either ``canceled`` or ``unpaid`` depending on your retry settings. Note that when a subscription has a status of ``unpaid``, no subsequent invoices will be attempted (invoices will be created, but then immediately automatically closed. Additionally, updating customer card details will not lead to Stripe retrying the latest invoice.). After receiving updated card details from a customer, you may choose to reopen and pay their closed invoices. Stripe documentation: https://stripe.com/docs/api?lang=python#subscriptions """ stripe_class = stripe.Subscription stripe_dashboard_item_name = "subscriptions" application_fee_percent = StripePercentField( null=True, blank=True, help_text="A positive decimal that represents the fee percentage of the " "subscription invoice amount that will be transferred to the application " "owner's Stripe account each billing period.", ) billing_cycle_anchor = StripeDateTimeField( null=True, blank=True, help_text=( "Determines the date of the first full invoice, and, for plans " "with `month` or `year` intervals, the day of the month for subsequent " "invoices." ), ) billing_thresholds = JSONField( null=True, blank=True, help_text="Define thresholds at which an invoice will be sent, and the " "subscription advanced to a new billing period.", ) cancel_at = StripeDateTimeField( null=True, blank=True, help_text="A date in the future at which the subscription will automatically " "get canceled.", ) cancel_at_period_end = models.BooleanField( default=False, help_text="If the subscription has been canceled with the ``at_period_end`` " "flag set to true, ``cancel_at_period_end`` on the subscription will be true. " "You can use this attribute to determine whether a subscription that has a " "status of active is scheduled to be canceled at the end of the " "current period.", ) canceled_at = StripeDateTimeField( null=True, blank=True, help_text="If the subscription has been canceled, the date of that " "cancellation. If the subscription was canceled with ``cancel_at_period_end``, " "canceled_at will still reflect the date of the initial cancellation request, " "not the end of the subscription period when the subscription is automatically " "moved to a canceled state.", ) collection_method = StripeEnumField( enum=enums.InvoiceCollectionMethod, help_text="Either `charge_automatically`, or `send_invoice`. When charging " "automatically, Stripe will attempt to pay this subscription at the end of the " "cycle using the default source attached to the customer. " "When sending an invoice, Stripe will email your customer an invoice with " "payment instructions.", ) current_period_end = StripeDateTimeField( help_text="End of the current period for which the subscription has been " "invoiced. At the end of this period, a new invoice will be created." ) current_period_start = StripeDateTimeField( help_text="Start of the current period for which the subscription has " "been invoiced." ) customer = StripeForeignKey( "Customer", on_delete=models.CASCADE, related_name="subscriptions", help_text="The customer associated with this subscription.", ) days_until_due = models.IntegerField( null=True, blank=True, help_text="Number of days a customer has to pay invoices generated by this " "subscription. This value will be `null` for subscriptions where " "`billing=charge_automatically`.", ) default_payment_method = StripeForeignKey( "PaymentMethod", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="The default payment method for the subscription. " "It must belong to the customer associated with the subscription. " "If not set, invoices will use the default payment method in the " "customer's invoice settings.", ) default_source = PaymentMethodForeignKey( on_delete=models.SET_NULL, null=True, blank=True, related_name="subscriptions", help_text="The default payment source for the subscription. " "It must belong to the customer associated with the subscription " "and be in a chargeable state. If not set, defaults to the customer's " "default source.", ) default_tax_rates = models.ManyToManyField( "TaxRate", # explicitly specify the joining table name as though the joining model # was defined with through="DjstripeSubscriptionDefaultTaxRate" db_table="djstripe_djstripesubscriptiondefaulttaxrate", related_name="+", blank=True, help_text="The tax rates that will apply to any subscription item " "that does not have tax_rates set. Invoices created will have their " "default_tax_rates populated from the subscription.", ) discount = JSONField( null=True, blank=True, help_text="Describes the current discount applied to this subscription, if there is one. When billing, a discount applied to a subscription overrides a discount applied on a customer-wide basis.", ) ended_at = StripeDateTimeField( null=True, blank=True, help_text="If the subscription has ended (either because it was canceled or " "because the customer was switched to a subscription to a new plan), " "the date the subscription ended.", ) latest_invoice = StripeForeignKey( "Invoice", null=True, blank=True, related_name="+", on_delete=models.SET_NULL, help_text="The most recent invoice this subscription has generated.", ) next_pending_invoice_item_invoice = StripeDateTimeField( null=True, blank=True, help_text="Specifies the approximate timestamp on which any pending " "invoice items will be billed according to the schedule provided at " "pending_invoice_item_interval.", ) pause_collection = JSONField( null=True, blank=True, help_text="If specified, payment collection for this subscription will be paused.", ) pending_invoice_item_interval = JSONField( null=True, blank=True, help_text="Specifies an interval for how often to bill for any " "pending invoice items. It is analogous to calling Create an invoice " "for the given subscription at the specified interval.", ) pending_setup_intent = StripeForeignKey( "SetupIntent", null=True, blank=True, on_delete=models.CASCADE, related_name="setup_intents", help_text="We can use this SetupIntent to collect user authentication " "when creating a subscription without immediate payment or updating a " "subscription's payment method, allowing you to " "optimize for off-session payments.", ) pending_update = JSONField( null=True, blank=True, help_text="If specified, pending updates that will be applied to the " "subscription once the latest_invoice has been paid.", ) plan = models.ForeignKey( "Plan", null=True, blank=True, on_delete=models.CASCADE, related_name="subscriptions", help_text="The plan associated with this subscription. This value will be " "`null` for multi-plan subscriptions", ) proration_behavior = StripeEnumField( enum=enums.SubscriptionProrationBehavior, help_text="Determines how to handle prorations when the billing cycle changes (e.g., when switching plans, resetting billing_cycle_anchor=now, or starting a trial), or if an item’s quantity changes", default=enums.SubscriptionProrationBehavior.create_prorations, blank=True, ) proration_date = StripeDateTimeField( null=True, blank=True, help_text="If set, the proration will be calculated as though the subscription was updated at the given time. This can be used to apply exactly the same proration that was previewed with upcoming invoice endpoint. It can also be used to implement custom proration logic, such as prorating by day instead of by second, by providing the time that you wish to use for proration calculations", ) quantity = models.IntegerField( null=True, blank=True, help_text="The quantity applied to this subscription. This value will be " "`null` for multi-plan subscriptions", ) schedule = models.ForeignKey( "SubscriptionSchedule", null=True, blank=True, on_delete=models.CASCADE, related_name="subscriptions", help_text="The schedule associated with this subscription.", ) start_date = StripeDateTimeField( null=True, blank=True, help_text="Date when the subscription was first created. The date " "might differ from the created date due to backdating.", ) status = StripeEnumField( enum=enums.SubscriptionStatus, help_text="The status of this subscription." ) trial_end = StripeDateTimeField( null=True, blank=True, help_text="If the subscription has a trial, the end of that trial.", ) trial_start = StripeDateTimeField( null=True, blank=True, help_text="If the subscription has a trial, the beginning of that trial.", ) objects = SubscriptionManager() @classmethod def api_list(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs): """ Call the stripe API's list operation for this model. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string See Stripe documentation for accepted kwargs for each object. :returns: an iterator over all items in the query """ if not kwargs.get("status"): # special case: https://stripe.com/docs/api/subscriptions/list#list_subscriptions-status # See Issue: https://github.com/dj-stripe/dj-stripe/issues/1763 kwargs["status"] = "all" return super().api_list(api_key=api_key, **kwargs) def update(self, plan: Union[StripeModel, str] = None, **kwargs): """ See `Customer.subscribe() <#djstripe.models.Customer.subscribe>`__ :param plan: The plan to which to subscribe the customer. :type plan: Plan or string (plan ID) .. important:: Updating a subscription by changing the plan or quantity \ creates a new ``Subscription`` in \ Stripe (and dj-stripe). """ # Convert Plan to id if plan is not None and isinstance(plan, StripeModel): plan = plan.id stripe_subscription = self._api_update(plan=plan, **kwargs) api_key = kwargs.get("api_key") or self.default_api_key return Subscription.sync_from_stripe_data(stripe_subscription, api_key=api_key) def extend(self, delta): """ Extends this subscription by the provided delta. :param delta: The timedelta by which to extend this subscription. :type delta: timedelta """ if delta.total_seconds() < 0: raise ValueError("delta must be a positive timedelta.") if self.trial_end is not None and self.trial_end > timezone.now(): period_end = self.trial_end else: period_end = self.current_period_end period_end += delta return self.update(proration_behavior="none", trial_end=period_end) def cancel(self, at_period_end: bool = False, **kwargs): """ Cancels this subscription. If you set the at_period_end parameter to true, the subscription will remain active until the end of the period, at which point it will be canceled and not renewed. By default, the subscription is terminated immediately. In either case, the customer will not be charged again for the subscription. Note, however, that any pending invoice items or metered usage will still be charged at the end of the period unless manually deleted. Depending on how `proration_behavior` is set, any pending prorations will also be left in place and collected at the end of the period. However, if the subscription is set to cancel immediately, you can pass the `prorate` and `invoice_now` flags in `kwargs` to configure how the pending metered usage is invoiced and how proration must work. By default, all unpaid invoices for the customer will be closed upon subscription cancellation. We do this in order to prevent unexpected payment retries once the customer has canceled a subscription. However, you can reopen the invoices manually after subscription cancellation to have us proceed with automatic retries, or you could even re-attempt payment yourself on all unpaid invoices before allowing the customer to cancel the subscription at all. :param at_period_end: A flag that if set to true will delay the cancellation \ of the subscription until the end of the current period. Default is False. :type at_period_end: boolean .. important:: If a subscription is canceled during a trial period, \ the ``at_period_end`` flag will be overridden to False so that the trial ends \ immediately and the customer's card isn't charged. """ # If plan has trial days and customer cancels before # trial period ends, then end subscription now, # i.e. at_period_end=False if self.trial_end and self.trial_end > timezone.now(): at_period_end = False if at_period_end: stripe_subscription = self._api_update(cancel_at_period_end=True) else: try: stripe_subscription = self._api_delete(**kwargs) except InvalidRequestError as exc: if "No such subscription:" in str(exc): # cancel() works by deleting the subscription. The object still # exists in Stripe however, and can still be retrieved. # If the subscription was already canceled (status=canceled), # that api_retrieve() call will fail with "No such subscription". # However, this may also happen if the subscription legitimately # does not exist, in which case the following line will re-raise. stripe_subscription = self.api_retrieve() else: raise return Subscription.sync_from_stripe_data( stripe_subscription, api_key=self.default_api_key ) def reactivate(self): """ Reactivates this subscription. If a customer's subscription is canceled with ``at_period_end`` set to True and it has not yet reached the end of the billing period, it can be reactivated. Subscriptions canceled immediately cannot be reactivated. (Source: https://stripe.com/docs/billing/subscriptions/cancel) .. warning:: Reactivating a fully canceled Subscription will fail silently. \ Be sure to check the returned Subscription's status. """ stripe_subscription = self.api_retrieve() stripe_subscription.plan = self.plan.id stripe_subscription.cancel_at_period_end = False return Subscription.sync_from_stripe_data(stripe_subscription.save()) def is_period_current(self): """ Returns True if this subscription's period is current, false otherwise. """ return self.current_period_end > timezone.now() or ( self.trial_end and self.trial_end > timezone.now() ) def is_status_current(self): """ Returns True if this subscription's status is current (active or trialing), false otherwise. """ return self.status in ["trialing", "active"] def is_status_temporarily_current(self): """ A status is temporarily current when the subscription is canceled with the ``at_period_end`` flag. The subscription is still active, but is technically canceled and we're just waiting for it to run out. You could use this method to give customers limited service after they've canceled. For example, a video on demand service could only allow customers to download their libraries and do nothing else when their subscription is temporarily current. """ return ( self.canceled_at and self.cancel_at_period_end and timezone.now() < self.current_period_end ) def is_valid(self): """ Returns True if this subscription's status and period are current, false otherwise. """ if not self.is_status_current(): return False if not self.is_period_current(): return False return True def _attach_objects_post_save_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, pending_relations=None, ): super()._attach_objects_post_save_hook( cls, data, api_key=api_key, pending_relations=pending_relations ) cls._stripe_object_to_subscription_items( target_cls=SubscriptionItem, data=data, subscription=self, api_key=api_key ) self.default_tax_rates.set( cls._stripe_object_to_default_tax_rates( target_cls=TaxRate, data=data, api_key=api_key ) ) class SubscriptionItem(StripeModel): """ Subscription items allow you to create customer subscriptions with more than one plan, making it easy to represent complex billing relationships. Stripe documentation: https://stripe.com/docs/api?lang=python#subscription_items """ stripe_class = stripe.SubscriptionItem billing_thresholds = JSONField( null=True, blank=True, help_text="Define thresholds at which an invoice will be sent, and the " "related subscription advanced to a new billing period.", ) plan = models.ForeignKey( "Plan", on_delete=models.CASCADE, related_name="subscription_items", help_text="The plan the customer is subscribed to.", ) price = models.ForeignKey( "Price", null=True, blank=True, on_delete=models.CASCADE, related_name="subscription_items", help_text="The price the customer is subscribed to.", ) proration_behavior = StripeEnumField( enum=enums.SubscriptionProrationBehavior, help_text="Determines how to handle prorations when the billing cycle changes (e.g., when switching plans, resetting billing_cycle_anchor=now, or starting a trial), or if an item’s quantity changes", default=enums.SubscriptionProrationBehavior.create_prorations, blank=True, ) proration_date = StripeDateTimeField( null=True, blank=True, help_text="If set, the proration will be calculated as though the subscription was updated at the given time. This can be used to apply exactly the same proration that was previewed with upcoming invoice endpoint. It can also be used to implement custom proration logic, such as prorating by day instead of by second, by providing the time that you wish to use for proration calculations", ) quantity = models.PositiveIntegerField( null=True, blank=True, help_text=( "The quantity of the plan to which the customer should be subscribed." ), ) subscription = StripeForeignKey( "Subscription", on_delete=models.CASCADE, related_name="items", help_text="The subscription this subscription item belongs to.", ) tax_rates = models.ManyToManyField( "TaxRate", # explicitly specify the joining table name as though the joining model # was defined with through="DjstripeSubscriptionItemTaxRate" db_table="djstripe_djstripesubscriptionitemtaxrate", related_name="+", blank=True, help_text="The tax rates which apply to this subscription_item. When set, " "the default_tax_rates on the subscription do not apply to this " "subscription_item.", ) def _attach_objects_post_save_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, pending_relations=None, ): super()._attach_objects_post_save_hook( cls, data, api_key=api_key, pending_relations=pending_relations ) self.tax_rates.set( cls._stripe_object_to_tax_rates( target_cls=TaxRate, data=data, api_key=api_key ) ) class SubscriptionSchedule(StripeModel): """ Subscription schedules allow you to create and manage the lifecycle of a subscription by predefining expected changes. Stripe documentation: https://stripe.com/docs/api/subscription_schedules?lang=python """ stripe_class = stripe.SubscriptionSchedule stripe_dashboard_item_name = "subscription_schedules" canceled_at = StripeDateTimeField( null=True, blank=True, help_text="Time at which the subscription schedule was canceled.", ) completed_at = StripeDateTimeField( null=True, blank=True, help_text="Time at which the subscription schedule was completed.", ) current_phase = JSONField( null=True, blank=True, help_text="Object representing the start and end dates for the " "current phase of the subscription schedule, if it is `active`.", ) customer = models.ForeignKey( "Customer", on_delete=models.CASCADE, related_name="schedules", help_text="The customer who owns the subscription schedule.", ) default_settings = JSONField( null=True, blank=True, help_text="Object representing the subscription schedule's default settings.", ) end_behavior = StripeEnumField( enum=enums.SubscriptionScheduleEndBehavior, help_text="Behavior of the subscription schedule and underlying " "subscription when it ends.", ) phases = JSONField( null=True, blank=True, help_text="Configuration for the subscription schedule's phases.", ) released_at = StripeDateTimeField( null=True, blank=True, help_text="Time at which the subscription schedule was released.", ) released_subscription = models.ForeignKey( "Subscription", null=True, blank=True, on_delete=models.SET_NULL, related_name="released_schedules", help_text="The subscription once managed by this subscription schedule " "(if it is released).", ) status = StripeEnumField( enum=enums.SubscriptionScheduleStatus, help_text="The present status of the subscription schedule. Possible " "values are `not_started`, `active`, `completed`, `released`, and " "`canceled`.", ) subscription = models.ForeignKey( "Subscription", null=True, blank=True, on_delete=models.SET_NULL, related_name="subscriptions", help_text="ID of the subscription managed by the subscription schedule.", ) def release(self, api_key=None, stripe_account=None, **kwargs): """ Releases the subscription schedule immediately, which will stop scheduling of its phases, but leave any existing subscription in place. A schedule can only be released if its status is not_started or active. If the subscription schedule is currently associated with a subscription, releasing it will remove its subscription property and set the subscription’s ID to the released_subscription property and returns the Released SubscriptionSchedule. :param api_key: The api key to use for this request. Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string """ api_key = api_key or self.default_api_key # Prefer passed in stripe_account if set. if not stripe_account: stripe_account = self._get_stripe_account_id(api_key) stripe_subscription_schedule = self.stripe_class.release( self.id, api_key=api_key, stripe_account=stripe_account, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ) return SubscriptionSchedule.sync_from_stripe_data(stripe_subscription_schedule) def cancel(self, api_key=None, stripe_account=None, **kwargs): """ Cancels a subscription schedule and its associated subscription immediately (if the subscription schedule has an active subscription). A subscription schedule can only be canceled if its status is not_started or active and returns the Canceled SubscriptionSchedule. :param api_key: The api key to use for this request. Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string """ api_key = api_key or self.default_api_key # Prefer passed in stripe_account if set. if not stripe_account: stripe_account = self._get_stripe_account_id(api_key) stripe_subscription_schedule = self.stripe_class.cancel( self.id, api_key=api_key, stripe_account=stripe_account, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ) return SubscriptionSchedule.sync_from_stripe_data(stripe_subscription_schedule) def update(self, api_key=None, stripe_account=None, **kwargs): """ Updates an existing subscription schedule and returns the updated SubscriptionSchedule. :param api_key: The api key to use for this request. Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string """ stripe_subscription_schedule = self._api_update( api_key=api_key, stripe_account=stripe_account, **kwargs ) return SubscriptionSchedule.sync_from_stripe_data(stripe_subscription_schedule) class ShippingRate(StripeModel): """ Shipping rates describe the price of shipping presented to your customers and can be applied to Checkout Sessions to collect shipping costs. Stripe documentation: https://stripe.com/docs/api/shipping_rates """ stripe_class = stripe.ShippingRate stripe_dashboard_item_name = "shipping-rates" description = None active = models.BooleanField( default=True, help_text="Whether the shipping rate can be used for new purchases. Defaults to true", ) display_name = models.CharField( max_length=50, default="", blank=True, help_text="The name of the shipping rate, meant to be displayable to the customer. This will appear on CheckoutSessions.", ) fixed_amount = JSONField( help_text="Describes a fixed amount to charge for shipping. Must be present if type is fixed_amount", ) type = StripeEnumField( enum=enums.ShippingRateType, default=enums.ShippingRateType.fixed_amount, help_text=_( "The type of calculation to use on the shipping rate. Can only be fixed_amount for now." ), ) delivery_estimate = JSONField( null=True, blank=True, help_text="The estimated range for how long shipping will take, meant to be displayable to the customer. This will appear on CheckoutSessions.", ) tax_behavior = StripeEnumField( enum=enums.ShippingRateTaxBehavior, help_text=_( "Specifies whether the rate is considered inclusive of taxes or exclusive of taxes." ), ) tax_code = StripeForeignKey( "TaxCode", null=True, blank=True, on_delete=models.CASCADE, help_text="The shipping tax code", ) class Meta(StripeModel.Meta): verbose_name = "Shipping Rate" def __str__(self): amount = get_friendly_currency_amount( self.fixed_amount.get("amount") / 100, self.fixed_amount.get("currency") ) if self.active: return f"{self.display_name} - {amount} (Active)" else: return f"{self.display_name} - {amount} (Archived)" class TaxCode(StripeModel): """ Tax codes classify goods and services for tax purposes. Stripe documentation: https://stripe.com/docs/api/tax_codes """ stripe_class = stripe.TaxCode metadata = None name = models.CharField( max_length=128, help_text="A short name for the tax code.", ) class Meta(StripeModel.Meta): verbose_name = "Tax Code" def __str__(self): return f"{self.name}: {self.id}" @classmethod def _find_owner_account(cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY): # Tax Codes do not belong to any Stripe Account pass class TaxId(StripeModel): """ Add one or multiple tax IDs to a customer. A customer's tax IDs are displayed on invoices and credit notes issued for the customer. Stripe documentation: https://stripe.com/docs/api/customer_tax_ids?lang=python """ stripe_class = stripe.TaxId description = None metadata = None country = models.CharField( max_length=2, help_text="Two-letter ISO code representing the country of the tax ID.", ) customer = StripeForeignKey( "djstripe.customer", on_delete=models.CASCADE, related_name="tax_ids" ) type = StripeEnumField( enum=enums.TaxIdType, help_text="The status of this subscription." ) value = models.CharField(max_length=50, help_text="Value of the tax ID.") verification = JSONField(help_text="Tax ID verification information.") def __str__(self): return f"{enums.TaxIdType.humanize(self.type)} {self.value} ({self.verification.get('status')})" class Meta(StripeModel.Meta): verbose_name = "Tax ID" @classmethod def _api_create(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs): """ Call the stripe API's create operation for this model. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string """ if not kwargs.get("id"): raise KeyError("Customer Object ID is missing") try: Customer.objects.get(id=kwargs["id"]) except Customer.DoesNotExist: raise return stripe.Customer.create_tax_id(api_key=api_key, **kwargs) def api_retrieve(self, api_key=None, stripe_account=None): """ Call the stripe API's retrieve operation for this model. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string """ nested_id = self.id id = self.customer.id # Prefer passed in stripe_account if set. if not stripe_account: stripe_account = self._get_stripe_account_id(api_key) return stripe.Customer.retrieve_tax_id( id=id, nested_id=nested_id, api_key=api_key or self.default_api_key, expand=self.expand_fields, stripe_account=stripe_account, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) @classmethod def api_list(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs): """ Call the stripe API's list operation for this model. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string See Stripe documentation for accepted kwargs for each object. :returns: an iterator over all items in the query """ return stripe.Customer.list_tax_ids( api_key=api_key, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ).auto_paging_iter() class TaxRate(StripeModel): """ Tax rates can be applied to invoices and subscriptions to collect tax. Stripe documentation: https://stripe.com/docs/api/tax_rates?lang=python """ stripe_class = stripe.TaxRate stripe_dashboard_item_name = "tax-rates" active = models.BooleanField( default=True, help_text="Defaults to true. When set to false, this tax rate cannot be " "applied to objects in the API, but will still be applied to subscriptions " "and invoices that already have it set.", ) country = models.CharField( max_length=2, default="", blank=True, help_text="Two-letter country code.", ) display_name = models.CharField( max_length=50, default="", blank=True, help_text="The display name of the tax rates as it will appear to your " "customer on their receipt email, PDF, and the hosted invoice page.", ) inclusive = models.BooleanField( help_text="This specifies if the tax rate is inclusive or exclusive." ) jurisdiction = models.CharField( max_length=50, default="", blank=True, help_text="The jurisdiction for the tax rate.", ) percentage = StripePercentField( decimal_places=4, max_digits=7, help_text="This represents the tax rate percent out of 100.", ) state = models.CharField( max_length=2, default="", blank=True, help_text="ISO 3166-2 subdivision code, without country prefix.", ) tax_type = models.CharField( default="", blank=True, max_length=50, help_text="The high-level tax type, such as vat, gst, sales_tax or custom.", ) def __str__(self): return f"{self.display_name} at {self.percentage}%" class Meta(StripeModel.Meta): verbose_name = "Tax Rate" class UsageRecord(StripeModel): """ Usage records allow you to continually report usage and metrics to Stripe for metered billing of plans. Stripe documentation: https://stripe.com/docs/api?lang=python#usage_records """ description = None metadata = None stripe_class = stripe.UsageRecord quantity = models.PositiveIntegerField( help_text=( "The quantity of the plan to which the customer should be subscribed." ) ) subscription_item = StripeForeignKey( "SubscriptionItem", on_delete=models.CASCADE, related_name="usage_records", help_text="The subscription item this usage record contains data for.", ) timestamp = StripeDateTimeField( null=True, blank=True, help_text="The timestamp for the usage event. This timestamp must be within the current billing period of the subscription of the provided subscription_item.", ) action = StripeEnumField( enum=enums.UsageAction, default=enums.UsageAction.increment, help_text="When using increment the specified quantity will be added to the usage at the specified timestamp. The set action will overwrite the usage quantity at that timestamp. If the subscription has billing thresholds, increment is the only allowed value.", ) def __str__(self): return f"Usage for {self.subscription_item} ({self.action}) is {self.quantity}" @classmethod def _api_create(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs): """ Call the stripe API's create operation for this model. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string """ if not kwargs.get("id"): raise KeyError("SubscriptionItem Object ID is missing") try: SubscriptionItem.objects.get(id=kwargs["id"]) except SubscriptionItem.DoesNotExist: raise usage_stripe_data = stripe.SubscriptionItem.create_usage_record( api_key=api_key, **kwargs ) # ! Hack: there is no way to retrieve a UsageRecord object from Stripe, # ! which is why we create and sync it right here cls.sync_from_stripe_data(usage_stripe_data, api_key=api_key) return usage_stripe_data @classmethod def create(cls, **kwargs): """ A wrapper around _api_create() to allow one to create and sync UsageRecord Objects """ return cls._api_create(**kwargs) class UsageRecordSummary(StripeModel): """ Usage record summaries provides usage information that's been summarized from multiple usage records and over a subscription billing period (e.g., 15 usage records in the month of September). Since new usage records can still be added, the returned summary information for the subscription item's ID should be seen as unstable until the subscription billing period ends. Stripe documentation: https://stripe.com/docs/api/usage_records/subscription_item_summary_list?lang=python """ stripe_class = stripe.UsageRecordSummary description = None metadata = None invoice = StripeForeignKey( "Invoice", null=True, blank=True, on_delete=models.CASCADE, related_name="usage_record_summaries", ) period = JSONField( null=True, blank=True, help_text="Subscription Billing period for the SubscriptionItem", ) period_end = StripeDateTimeField( null=True, blank=True, help_text="End of the Subscription Billing period for the SubscriptionItem", ) period_start = StripeDateTimeField( null=True, blank=True, help_text="Start of the Subscription Billing period for the SubscriptionItem", ) total_usage = models.PositiveIntegerField( help_text=( "The quantity of the plan to which the customer should be subscribed." ) ) subscription_item = StripeForeignKey( "SubscriptionItem", on_delete=models.CASCADE, related_name="usage_record_summaries", help_text="The subscription item this usage record contains data for.", ) def __str__(self): return f"Usage Summary for {self.subscription_item} ({self.invoice}) is {self.total_usage}" @classmethod def _manipulate_stripe_object_hook(cls, data): data["period_start"] = data["period"]["start"] data["period_end"] = data["period"]["end"] return data @classmethod def api_list(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs): """ Call the stripe API's list operation for this model. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string See Stripe documentation for accepted kwargs for each object. :returns: an iterator over all items in the query """ if not kwargs.get("id"): raise KeyError("SubscriptionItem Object ID is missing") try: SubscriptionItem.objects.get(id=kwargs["id"]) except SubscriptionItem.DoesNotExist: raise return stripe.SubscriptionItem.list_usage_record_summaries( api_key=api_key, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ).auto_paging_iter() ================================================ FILE: djstripe/models/checkout.py ================================================ import stripe from django.db import models from djstripe.settings import djstripe_settings from .. import enums from ..fields import JSONField, StripeEnumField, StripeForeignKey from .base import StripeModel class Session(StripeModel): """ A Checkout Session represents your customer's session as they pay for one-time purchases or subscriptions through Checkout. Stripe documentation: https://stripe.com/docs/api/checkout/sessions?lang=python """ stripe_class = stripe.checkout.Session billing_address_collection = StripeEnumField( enum=enums.SessionBillingAddressCollection, blank=True, help_text=( "The value (auto or required) for whether Checkout" "collected the customer's billing address." ), ) cancel_url = models.TextField( max_length=5000, blank=True, help_text=( "The URL the customer will be directed to if they" "decide to cancel payment and return to your website." ), ) client_reference_id = models.TextField( max_length=5000, blank=True, help_text=( "A unique string to reference the Checkout Session." "This can be a customer ID, a cart ID, or similar, and" "can be used to reconcile the session with your internal systems." ), ) customer = StripeForeignKey( "Customer", null=True, on_delete=models.SET_NULL, help_text=("Customer this Checkout is for if one exists."), ) customer_email = models.CharField( max_length=255, blank=True, help_text=( "If provided, this value will be used when the Customer object is created." ), ) display_items = JSONField( null=True, blank=True, help_text=("The line items, plans, or SKUs purchased by the customer."), ) locale = models.CharField( max_length=255, blank=True, help_text=( "The IETF language tag of the locale Checkout is displayed in." "If blank or auto, the browser's locale is used." ), ) mode = StripeEnumField( enum=enums.SessionMode, blank=True, help_text="The mode of the Checkout Session, " "one of payment, setup, or subscription.", ) payment_intent = StripeForeignKey( "PaymentIntent", null=True, on_delete=models.SET_NULL, help_text=("PaymentIntent created if SKUs or line items were provided."), ) payment_method_types = JSONField( help_text="The list of payment method types (e.g. card) that this " "Checkout Session is allowed to accept." ) submit_type = StripeEnumField( enum=enums.SubmitTypeStatus, blank=True, help_text="Describes the type of transaction being performed by Checkout" "in order to customize relevant text on the page, such as the submit button.", ) subscription = StripeForeignKey( "Subscription", null=True, on_delete=models.SET_NULL, help_text=("Subscription created if one or more plans were provided."), ) success_url = models.TextField( max_length=5000, blank=True, help_text=( "The URL the customer will be directed to after the payment or subscription" "creation is successful." ), ) def _attach_objects_post_save_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, pending_relations=None, ): from ..event_handlers import update_customer_helper super()._attach_objects_post_save_hook( cls, data, api_key=api_key, pending_relations=pending_relations ) # only update if customer and metadata exist if self.customer and self.metadata: key = djstripe_settings.SUBSCRIBER_CUSTOMER_KEY current_value = self.metadata.get(key) # only update if metadata has the SUBSCRIBER_CUSTOMER_KEY if current_value: metadata = {key: current_value} # Update the customer with ONLY the customer specific metadata update_customer_helper( metadata, self.customer.id, key, ) # Update metadata in the Upstream Customer Object on Stripe self.customer._api_update(metadata=metadata) ================================================ FILE: djstripe/models/connect.py ================================================ import stripe from django.db import models from djstripe.utils import get_friendly_currency_amount from .. import enums from ..fields import ( JSONField, StripeCurrencyCodeField, StripeDecimalCurrencyAmountField, StripeEnumField, StripeForeignKey, StripeIdField, StripeQuantumCurrencyAmountField, ) from ..managers import TransferManager from ..settings import djstripe_settings from .base import StripeBaseModel, StripeModel # TODO Implement Full Webhook event support for ApplicationFee and ApplicationFee Refund Objects class ApplicationFee(StripeModel): """ When you collect a transaction fee on top of a charge made for your user (using Connect), an ApplicationFee is created in your account. Please note the model field charge exists on the Stripe Connected Account while the application_fee modelfield on Charge model exists on the Platform Account! Stripe documentation: https://stripe.com/docs/api?lang=python#application_fees """ stripe_class = stripe.ApplicationFee account = StripeForeignKey( "Account", on_delete=models.PROTECT, related_name="application_fees", help_text="ID of the Stripe account this fee was taken from.", ) amount = StripeQuantumCurrencyAmountField(help_text="Amount earned, in cents.") amount_refunded = StripeQuantumCurrencyAmountField( help_text="Amount in cents refunded (can be less than the amount attribute " "on the fee if a partial refund was issued)" ) # TODO application = ... # balance_transaction exists on the platform account balance_transaction = StripeForeignKey( "BalanceTransaction", on_delete=models.CASCADE, help_text="Balance transaction that describes the impact on your account" " balance.", ) # charge exists on the Stripe Connected Account and not the Platform Account charge = StripeForeignKey( "Charge", on_delete=models.CASCADE, help_text="The charge that the application fee was taken from.", ) currency = StripeCurrencyCodeField() # TODO originating_transaction = ... (refs. both Charge and Transfer) refunded = models.BooleanField( help_text=( "Whether the fee has been fully refunded. If the fee is only " "partially refunded, this attribute will still be false." ) ) # TODO Add Tests class ApplicationFeeRefund(StripeModel): """ ApplicationFeeRefund objects allow you to refund an ApplicationFee that has previously been created but not yet refunded. Funds will be refunded to the Stripe account from which the fee was originally collected. Stripe documentation: https://stripe.com/docs/api?lang=python#fee_refunds """ description = None stripe_class = stripe.ApplicationFeeRefund amount = StripeQuantumCurrencyAmountField(help_text="Amount refunded, in cents.") balance_transaction = StripeForeignKey( "BalanceTransaction", on_delete=models.CASCADE, help_text="Balance transaction that describes the impact on your account " "balance.", ) currency = StripeCurrencyCodeField() fee = StripeForeignKey( "ApplicationFee", on_delete=models.CASCADE, related_name="refunds", help_text="The application fee that was refunded", ) class CountrySpec(StripeBaseModel): """ Stripe documentation: https://stripe.com/docs/api?lang=python#country_specs """ stripe_class = stripe.CountrySpec id = models.CharField(max_length=2, primary_key=True, serialize=True) default_currency = StripeCurrencyCodeField( help_text=( "The default currency for this country. " "This applies to both payment methods and bank accounts." ) ) supported_bank_account_currencies = JSONField( help_text="Currencies that can be accepted in the specific country" " (for transfers)." ) supported_payment_currencies = JSONField( help_text="Currencies that can be accepted in the specified country" " (for payments)." ) supported_payment_methods = JSONField( help_text="Payment methods available in the specified country." ) supported_transfer_countries = JSONField( help_text="Countries that can accept transfers from the specified country." ) verification_fields = JSONField( help_text="Lists the types of verification data needed to keep an account open." ) @classmethod def sync_from_stripe_data( cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY ) -> "CountrySpec": """ Syncs this object from the stripe data provided. Foreign keys will also be retrieved and synced recursively. :param data: stripe object :type data: dict :rtype: cls """ data_id = data["id"] supported_fields = ( "default_currency", "supported_bank_account_currencies", "supported_payment_currencies", "supported_payment_methods", "supported_transfer_countries", "verification_fields", ) instance, created = cls.objects.get_or_create( id=data_id, defaults={k: data[k] for k in supported_fields}, ) return instance def api_retrieve(self, api_key: str = None, stripe_account=None): if api_key is None: api_key = djstripe_settings.get_default_api_key(livemode=None) return self.stripe_class.retrieve( id=self.id, api_key=api_key, stripe_version=djstripe_settings.STRIPE_API_VERSION, stripe_account=stripe_account, ) class Transfer(StripeModel): """ When Stripe sends you money or you initiate a transfer to a bank account, debit card, or connected Stripe account, a transfer object will be created. Stripe documentation: https://stripe.com/docs/api?lang=python#transfers """ stripe_class = stripe.Transfer expand_fields = ["balance_transaction"] stripe_dashboard_item_name = "transfers" objects = TransferManager() amount = StripeDecimalCurrencyAmountField(help_text="The amount transferred") amount_reversed = StripeDecimalCurrencyAmountField( null=True, blank=True, help_text="The amount (as decimal) reversed (can be less than the amount " "attribute on the transfer if a partial reversal was issued).", ) balance_transaction = StripeForeignKey( "BalanceTransaction", on_delete=models.SET_NULL, null=True, blank=True, help_text="Balance transaction that describes the impact on your account" " balance.", ) currency = StripeCurrencyCodeField() destination = StripeIdField( max_length=255, null=True, help_text="ID of the bank account, card, or Stripe account the transfer was sent to.", ) # todo implement payment model (for some reason py ids are showing up in the charge model) destination_payment = StripeIdField( null=True, blank=True, help_text="If the destination is a Stripe account, this will be the ID of the " "payment that the destination account received for the transfer.", ) reversed = models.BooleanField( default=False, help_text="Whether or not the transfer has been fully reversed. " "If the transfer is only partially reversed, this attribute will still " "be false.", ) source_transaction = StripeIdField( null=True, help_text="ID of the charge (or other transaction) that was used to fund " "the transfer. If null, the transfer was funded from the available balance.", ) source_type = StripeEnumField( enum=enums.LegacySourceType, help_text="The source balance from which this transfer came.", ) transfer_group = models.CharField( max_length=255, default="", blank=True, help_text="A string that identifies this transaction as part of a group.", ) @property def fee(self): if self.balance_transaction: return self.balance_transaction.fee def __str__(self): amount = get_friendly_currency_amount(self.amount, self.currency) if self.reversed: # Complete Reversal return f"{amount} Reversed" elif self.amount_reversed: # Partial Reversal return f"{amount} Partially Reversed" # No Reversal return f"{amount}" def _attach_objects_post_save_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, pending_relations=None, ): """ Iterate over reversals on the Transfer object to create and/or sync TransferReversal objects """ super()._attach_objects_post_save_hook( cls, data, api_key=api_key, pending_relations=pending_relations ) # Transfer Reversals exist as a list on the Transfer Object for reversals_data in data.get("reversals").auto_paging_iter(): TransferReversal.sync_from_stripe_data(reversals_data, api_key=api_key) def get_stripe_dashboard_url(self) -> str: return ( f"{self._get_base_stripe_dashboard_url()}" f"connect/{self.stripe_dashboard_item_name}/{self.id}" ) # TODO Add Tests class TransferReversal(StripeModel): """ Stripe documentation: https://stripe.com/docs/api?lang=python#transfer_reversals """ expand_fields = ["balance_transaction", "transfer"] stripe_dashboard_item_name = "transfer_reversals" # TransferReversal classmethods are derived from # and attached to the stripe.Transfer class stripe_class = stripe.Transfer amount = StripeQuantumCurrencyAmountField(help_text="Amount, in cents.") balance_transaction = StripeForeignKey( "BalanceTransaction", on_delete=models.SET_NULL, null=True, blank=True, related_name="transfer_reversals", help_text="Balance transaction that describes the impact on your account " "balance.", ) currency = StripeCurrencyCodeField() transfer = StripeForeignKey( "Transfer", on_delete=models.CASCADE, help_text="The transfer that was reversed.", related_name="reversals", ) def __str__(self): return str(self.transfer) @classmethod def _api_create(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs): """ Call the stripe API's create operation for this model. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string """ if not kwargs.get("id"): raise KeyError("Transfer Object ID is missing") try: Transfer.objects.get(id=kwargs["id"]) except Transfer.DoesNotExist: raise return stripe.Transfer.create_reversal( api_key=api_key, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ) def api_retrieve(self, api_key=None, stripe_account=None): """ Call the stripe API's retrieve operation for this model. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string """ nested_id = self.id id = self.transfer.id # Prefer passed in stripe_account if set. if not stripe_account: stripe_account = self._get_stripe_account_id(api_key) return stripe.Transfer.retrieve_reversal( id=id, nested_id=nested_id, api_key=api_key or self.default_api_key, stripe_version=djstripe_settings.STRIPE_API_VERSION, expand=self.expand_fields, stripe_account=stripe_account, ) @classmethod def api_list(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs): """ Call the stripe API's list operation for this model. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string See Stripe documentation for accepted kwargs for each object. :returns: an iterator over all items in the query """ return stripe.Transfer.list_reversals( api_key=api_key, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ).auto_paging_iter() @classmethod def is_valid_object(cls, data): """ Returns whether the data is a valid object for the class """ return data and data.get("object") == "transfer_reversal" ================================================ FILE: djstripe/models/core.py ================================================ from decimal import Decimal from typing import Optional, Union import stripe from django.apps import apps from django.db import models, transaction from django.utils import timezone from django.utils.functional import cached_property from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ from stripe.error import InvalidRequestError from .. import enums, webhooks from ..exceptions import MultipleSubscriptionException from ..fields import ( JSONField, PaymentMethodForeignKey, StripeCurrencyCodeField, StripeDateTimeField, StripeDecimalCurrencyAmountField, StripeEnumField, StripeForeignKey, StripeIdField, StripeQuantumCurrencyAmountField, ) from ..managers import ChargeManager from ..settings import djstripe_settings from ..signals import WEBHOOK_SIGNALS from ..utils import get_friendly_currency_amount, get_id_from_stripe_data from .base import IdempotencyKey, StripeModel, logger def _sanitise_price(price=None, plan=None, **kwargs): """ Helper for Customer.subscribe() """ if price and plan: raise TypeError("price and plan arguments cannot both be defined.") price = price or plan if not price: raise TypeError("you need to set either price or plan") # Convert Price to id if isinstance(price, StripeModel): price = price.id return price, kwargs class BalanceTransaction(StripeModel): """ A single transaction that updates the Stripe balance. Stripe documentation: https://stripe.com/docs/api?lang=python#balance_transaction_object """ stripe_class = stripe.BalanceTransaction amount = StripeQuantumCurrencyAmountField( help_text="Gross amount of the transaction, in cents." ) available_on = StripeDateTimeField( help_text=( "The date the transaction's net funds " "will become available in the Stripe balance." ) ) currency = StripeCurrencyCodeField() exchange_rate = models.DecimalField(null=True, decimal_places=6, max_digits=8) fee = StripeQuantumCurrencyAmountField( help_text="Fee (in cents) paid for this transaction." ) fee_details = JSONField() net = StripeQuantumCurrencyAmountField( help_text="Net amount of the transaction, in cents." ) source = StripeIdField() reporting_category = StripeEnumField( enum=enums.BalanceTransactionReportingCategory, help_text=( "More information: https://stripe.com/docs/reports/reporting-categories" ), ) status = StripeEnumField(enum=enums.BalanceTransactionStatus) type = StripeEnumField(enum=enums.BalanceTransactionType) def __str__(self): amount = get_friendly_currency_amount(self.amount / 100, self.currency) status = enums.BalanceTransactionStatus.humanize(self.status) return f"{amount} ({status})" def get_source_class(self): try: return apps.get_model("djstripe", self.type) except LookupError: raise def get_source_instance(self): return self.get_source_class().objects.get(id=self.source) def get_stripe_dashboard_url(self): return self.get_source_instance().get_stripe_dashboard_url() class Charge(StripeModel): """ To charge a credit or a debit card, you create a charge object. You can retrieve and refund individual charges as well as list all charges. Charges are identified by a unique random ID. Stripe documentation: https://stripe.com/docs/api?lang=python#charges """ stripe_class = stripe.Charge expand_fields = ["balance_transaction"] stripe_dashboard_item_name = "payments" amount = StripeDecimalCurrencyAmountField(help_text="Amount charged (as decimal).") amount_captured = StripeDecimalCurrencyAmountField( null=True, help_text=( "Amount (as decimal) captured (can be less than the amount attribute " "on the charge if a partial capture was issued)." ), ) amount_refunded = StripeDecimalCurrencyAmountField( help_text=( "Amount (as decimal) refunded (can be less than the amount attribute on " "the charge if a partial refund was issued)." ) ) application = models.CharField( max_length=255, blank=True, help_text="ID of the Connect application that created the charge.", ) application_fee = StripeForeignKey( "ApplicationFee", on_delete=models.SET_NULL, null=True, blank=True, related_name="fee_for_charge", help_text="The application fee (if any) for the charge.", ) application_fee_amount = StripeDecimalCurrencyAmountField( null=True, blank=True, help_text="The amount (as decimal) of the application fee (if any) " "requested for the charge.", ) balance_transaction = StripeForeignKey( "BalanceTransaction", on_delete=models.SET_NULL, null=True, help_text=( "The balance transaction that describes the impact of this charge " "on your account balance (not including refunds or disputes)." ), ) billing_details = JSONField( null=True, help_text="Billing information associated with the PaymentMethod at the " "time of the transaction.", ) calculated_statement_descriptor = models.CharField( max_length=22, default="", help_text="The full statement descriptor that is passed to card networks, " "and that is displayed on your customers' credit card and bank statements. " "Allows you to see what the statement descriptor looks like after the " "static and dynamic portions are combined.", ) captured = models.BooleanField( default=False, help_text="If the charge was created without capturing, this boolean " "represents whether or not it is still uncaptured or has since been captured.", ) currency = StripeCurrencyCodeField( help_text="The currency in which the charge was made." ) customer = StripeForeignKey( "Customer", on_delete=models.SET_NULL, null=True, blank=True, related_name="charges", help_text="The customer associated with this charge.", ) dispute = StripeForeignKey( "Dispute", on_delete=models.SET_NULL, null=True, blank=True, related_name="charges", help_text="Details about the dispute if the charge has been disputed.", ) disputed = models.BooleanField( default=False, help_text="Whether the charge has been disputed.", ) failure_code = StripeEnumField( enum=enums.ApiErrorCode, default="", blank=True, help_text="Error code explaining reason for charge failure if available.", ) failure_message = models.TextField( max_length=5000, default="", blank=True, help_text="Message to user further explaining reason " "for charge failure if available.", ) fraud_details = JSONField( help_text="Hash with information on fraud assessments for the charge.", null=True, blank=True, ) invoice = StripeForeignKey( "Invoice", on_delete=models.CASCADE, null=True, related_name="charges", help_text="The invoice this charge is for if one exists.", ) # TODO: order (requires Order model) on_behalf_of = StripeForeignKey( "Account", on_delete=models.CASCADE, null=True, blank=True, related_name="charges", help_text="The account (if any) the charge was made on behalf of " "without triggering an automatic transfer.", ) outcome = JSONField( help_text="Details about whether or not the payment was accepted, and why.", null=True, blank=True, ) paid = models.BooleanField( default=False, help_text="True if the charge succeeded, " "or was successfully authorized for later capture, False otherwise.", ) payment_intent = StripeForeignKey( "PaymentIntent", null=True, on_delete=models.SET_NULL, related_name="charges", help_text="PaymentIntent associated with this charge, if one exists.", ) payment_method = StripeForeignKey( "PaymentMethod", null=True, on_delete=models.SET_NULL, related_name="charges", help_text="PaymentMethod used in this charge.", ) payment_method_details = JSONField( help_text="Details about the payment method at the time of the transaction.", null=True, blank=True, ) receipt_email = models.TextField( max_length=800, # yup, 800. default="", blank=True, help_text="The email address that the receipt for this charge was sent to.", ) receipt_number = models.CharField( max_length=14, default="", blank=True, help_text="The transaction number that appears " "on email receipts sent for this charge.", ) receipt_url = models.TextField( max_length=5000, default="", blank=True, help_text="This is the URL to view the receipt for this charge. " "The receipt is kept up-to-date to the latest state of the charge, " "including any refunds. If the charge is for an Invoice, " "the receipt will be stylized as an Invoice receipt.", ) refunded = models.BooleanField( default=False, help_text="Whether or not the charge has been fully refunded. " "If the charge is only partially refunded, " "this attribute will still be false.", ) # TODO: review (requires Review model) shipping = JSONField( null=True, blank=True, help_text="Shipping information for the charge" ) source = PaymentMethodForeignKey( on_delete=models.SET_NULL, null=True, blank=True, related_name="charges", help_text="The source used for this charge.", ) source_transfer = StripeForeignKey( "Transfer", null=True, blank=True, on_delete=models.CASCADE, help_text="The transfer which created this charge. Only present if the " "charge came from another Stripe account.", related_name="+", ) statement_descriptor = models.CharField( max_length=22, null=True, blank=True, help_text="For card charges, use statement_descriptor_suffix instead. " "Otherwise, you can use this value as the complete description of a " "charge on your customers' statements. Must contain at least one letter, " "maximum 22 characters.", ) statement_descriptor_suffix = models.CharField( max_length=22, null=True, blank=True, help_text="Provides information about the charge that customers see on " "their statements. Concatenated with the prefix (shortened descriptor) " "or statement descriptor that's set on the account to form the " "complete statement descriptor. " "Maximum 22 characters for the concatenated descriptor.", ) status = StripeEnumField( enum=enums.ChargeStatus, help_text="The status of the payment." ) transfer = StripeForeignKey( "Transfer", on_delete=models.CASCADE, null=True, blank=True, help_text=( "The transfer to the `destination` account (only applicable if " "the charge was created using the `destination` parameter)." ), ) transfer_data = JSONField( null=True, blank=True, help_text="An optional dictionary including the account to automatically " "transfer to as part of a destination charge.", ) transfer_group = models.CharField( max_length=255, null=True, blank=True, help_text="A string that identifies this transaction as part of a group.", ) objects = ChargeManager() def __str__(self): amount = get_friendly_currency_amount(self.amount, self.currency) return f"{amount} ({self.human_readable_status})" @property def fee(self): if self.balance_transaction: return self.balance_transaction.fee @property def human_readable_status(self) -> str: if not self.captured: return "Uncaptured" elif self.disputed: return "Disputed" elif self.refunded: return "Refunded" return enums.ChargeStatus.humanize(self.status) @property def fraudulent(self) -> bool: return ( self.fraud_details and list(self.fraud_details.values())[0] == "fraudulent" ) def _calculate_refund_amount(self, amount: Optional[Decimal]) -> int: """ Returns the amount that can be refunded (in cents) """ eligible_to_refund = self.amount - (self.amount_refunded or 0) amount_to_refund = ( min(eligible_to_refund, amount) if amount else eligible_to_refund ) return int(amount_to_refund * 100) def refund(self, amount: Decimal = None, reason: str = None) -> "Charge": """ Initiate a refund. Returns the charge object. :param amount: A positive decimal amount representing how much of this charge to refund. If amount is not provided, then this will be a full refund. Can only refund up to the unrefunded amount remaining of the charge. :param reason: String indicating the reason for the refund. If set, possible values are ``duplicate``, ``fraudulent``, and ``requested_by_customer``. Specifying ``fraudulent`` as the reason when you believe the charge to be fraudulent will help Stripe improve their fraud detection algorithms. """ charge_obj = self.api_retrieve().refund( amount=self._calculate_refund_amount(amount=amount), reason=reason ) return self.__class__.sync_from_stripe_data( charge_obj, api_key=self.default_api_key ) def capture(self, **kwargs) -> "Charge": """ Capture the payment of an existing, uncaptured, charge. This is the second half of the two-step payment flow, where first you created a charge with the capture option set to False. See https://stripe.com/docs/api#capture_charge """ captured_charge = self.api_retrieve().capture(**kwargs) return self.__class__.sync_from_stripe_data( captured_charge, api_key=self.default_api_key ) def _attach_objects_post_save_hook( self, cls, data, pending_relations=None, api_key=djstripe_settings.STRIPE_SECRET_KEY, ): super()._attach_objects_post_save_hook( cls, data, pending_relations=pending_relations, api_key=api_key ) cls._stripe_object_to_refunds( target_cls=Refund, data=data, charge=self, api_key=api_key ) # TODO Add Tests class Mandate(StripeModel): """ A Mandate is a record of the permission a customer has given you to debit their payment method. https://stripe.com/docs/api/mandates """ stripe_class = stripe.Mandate customer_acceptance = JSONField( help_text="Details about the customer's acceptance of the mandate." ) payment_method = StripeForeignKey("paymentmethod", on_delete=models.CASCADE) payment_method_details = JSONField( help_text="Additional mandate information specific to the payment method type." ) status = StripeEnumField( enum=enums.MandateStatus, help_text="The status of the mandate, which indicates whether it can be used to initiate a payment.", ) type = StripeEnumField( enum=enums.MandateType, help_text="The status of the mandate, which indicates whether it can be used to initiate a payment.", ) multi_use = JSONField( null=True, blank=True, help_text="If this is a `multi_use` mandate, this hash contains details about the mandate.", ) single_use = JSONField( null=True, blank=True, help_text="If this is a `single_use` mandate, this hash contains details about the mandate.", ) class Product(StripeModel): """ Products describe the specific goods or services you offer to your customers. For example, you might offer a Standard and Premium version of your goods or service; each version would be a separate Product. They can be used in conjunction with Prices to configure pricing in Payment Links, Checkout, and Subscriptions. Stripe documentation: https://stripe.com/docs/api?lang=python#products """ stripe_class = stripe.Product stripe_dashboard_item_name = "products" # Fields applicable to both `good` and `service` name = models.TextField( max_length=5000, help_text=( "The product's name, meant to be displayable to the customer. " "Applicable to both `service` and `good` types." ), ) default_price = StripeForeignKey( "Price", on_delete=models.SET_NULL, null=True, blank=True, related_name="products", help_text="The default price this product is associated with.", ) type = StripeEnumField( enum=enums.ProductType, help_text=( "The type of the product. The product is either of type `good`, which is " "eligible for use with Orders and SKUs, or `service`, which is eligible " "for use with Subscriptions and Plans." ), ) # Fields applicable to `good` only active = models.BooleanField( null=True, help_text=( "Whether the product is currently available for purchase. " "Only applicable to products of `type=good`." ), ) attributes = JSONField( null=True, blank=True, help_text=( "A list of up to 5 attributes that each SKU can provide values for " '(e.g., `["color", "size"]`). Only applicable to products of `type=good`.' ), ) caption = models.TextField( default="", blank=True, max_length=5000, help_text=( "A short one-line description of the product, meant to be displayable" "to the customer. Only applicable to products of `type=good`." ), ) deactivate_on = JSONField( null=True, blank=True, help_text=( "An array of connect application identifiers that cannot purchase " "this product. Only applicable to products of `type=good`." ), ) images = JSONField( null=True, blank=True, help_text=( "A list of up to 8 URLs of images for this product, meant to be " "displayable to the customer. Only applicable to products of `type=good`." ), ) package_dimensions = JSONField( null=True, blank=True, help_text=( "The dimensions of this product for shipping purposes. " "A SKU associated with this product can override this value by having its " "own `package_dimensions`. Only applicable to products of `type=good`." ), ) shippable = models.BooleanField( null=True, blank=True, help_text=( "Whether this product is a shipped good. " "Only applicable to products of `type=good`." ), ) url = models.CharField( max_length=799, null=True, blank=True, help_text=( "A URL of a publicly-accessible webpage for this product. " "Only applicable to products of `type=good`." ), ) # Fields available to `service` only statement_descriptor = models.CharField( max_length=22, default="", blank=True, help_text=( "Extra information about a product which will appear on your customer's " "credit card statement. In the case that multiple products are billed at " "once, the first statement descriptor will be used. " "Only available on products of type=`service`." ), ) unit_label = models.CharField(max_length=12, default="", blank=True) def __str__(self): # 1 product can have 1 or more than 1 related price price_qs = self.prices.all() price_count = price_qs.count() if price_count > 1: return f"{self.name} ({price_count} prices)" elif price_count == 1: return f"{self.name} ({price_qs[0].human_readable_price})" else: return self.name class Customer(StripeModel): """ Customer objects allow you to perform recurring charges and track multiple charges that are associated with the same customer. Stripe documentation: https://stripe.com/docs/api?lang=python#customers """ stripe_class = stripe.Customer expand_fields = ["default_source", "sources"] stripe_dashboard_item_name = "customers" address = JSONField(null=True, blank=True, help_text="The customer's address.") balance = StripeQuantumCurrencyAmountField( null=True, blank=True, default=0, help_text=( "Current balance (in cents), if any, being stored on the customer's " "account. " "If negative, the customer has credit to apply to the next invoice. " "If positive, the customer has an amount owed that will be added to the " "next invoice. The balance does not refer to any unpaid invoices; it " "solely takes into account amounts that have yet to be successfully " "applied to any invoice. This balance is only taken into account for " "recurring billing purposes (i.e., subscriptions, invoices, invoice items)." ), ) currency = StripeCurrencyCodeField( blank=True, default="", help_text="The currency the customer can be charged in for " "recurring billing purposes", ) default_source = PaymentMethodForeignKey( on_delete=models.SET_NULL, null=True, blank=True, related_name="customers" ) delinquent = models.BooleanField( null=True, blank=True, default=False, help_text="Whether or not the latest charge for the customer's " "latest invoice has failed.", ) # Stripe API returns deleted customers like so: # { # "id": "cus_KX439W5dKrpi22", # "object": "customer", # "deleted": true, # } deleted = models.BooleanField( default=False, null=True, blank=True, help_text="Whether the Customer instance has been deleted upstream in Stripe or not.", ) # coupon = models.ForeignKey( "Coupon", null=True, blank=True, on_delete=models.SET_NULL ) coupon_start = StripeDateTimeField( null=True, blank=True, editable=False, help_text="If a coupon is present, the date at which it was applied.", ) coupon_end = StripeDateTimeField( null=True, blank=True, editable=False, help_text="If a coupon is present and has a limited duration, " "the date that the discount will end.", ) # discount = JSONField( null=True, blank=True, help_text="Describes the current discount active on the customer, if there is one.", ) email = models.TextField(max_length=5000, default="", blank=True) invoice_prefix = models.CharField( default="", blank=True, max_length=255, help_text=( "The prefix for the customer used to generate unique invoice numbers." ), ) invoice_settings = JSONField( null=True, blank=True, help_text="The customer's default invoice settings." ) # default_payment_method is actually nested inside invoice_settings # this field is a convenience to provide the foreign key default_payment_method = StripeForeignKey( "PaymentMethod", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", help_text="default payment method used for subscriptions and invoices " "for the customer.", ) name = models.TextField( max_length=5000, default="", blank=True, help_text="The customer's full name or business name.", ) phone = models.TextField( max_length=5000, default="", blank=True, help_text="The customer's phone number.", ) preferred_locales = JSONField( null=True, blank=True, help_text=( "The customer's preferred locales (languages), ordered by preference." ), ) shipping = JSONField( null=True, blank=True, help_text="Shipping information associated with the customer.", ) tax_exempt = StripeEnumField( enum=enums.CustomerTaxExempt, default="", help_text="Describes the customer's tax exemption status. When set to reverse, " 'invoice and receipt PDFs include the text "Reverse charge".', ) # dj-stripe fields subscriber = models.ForeignKey( djstripe_settings.get_subscriber_model_string(), blank=True, null=True, on_delete=models.SET_NULL, related_name="djstripe_customers", ) date_purged = models.DateTimeField(null=True, editable=False) class Meta(StripeModel.Meta): unique_together = ("subscriber", "livemode", "djstripe_owner_account") def __str__(self): if self.subscriber: return str(self.subscriber) return self.name or self.description or self.id @classmethod def _manipulate_stripe_object_hook(cls, data): # stripe adds a deleted attribute if the Customer has been deleted upstream if data.get("deleted"): logger.warning( f"This customer ({data.get('id')}) has been deleted upstream, in Stripe" ) else: # set "deleted" key to False (default) data["deleted"] = False discount = data.get("discount") if discount: data["coupon_start"] = discount["start"] data["coupon_end"] = discount["end"] # Populate the object id for our default_payment_method field (or set it None) data["default_payment_method"] = data.get("invoice_settings", {}).get( "default_payment_method" ) return data @classmethod def get_or_create( cls, subscriber, livemode=djstripe_settings.STRIPE_LIVE_MODE, stripe_account=None, ): """ Get or create a dj-stripe customer. :param subscriber: The subscriber model instance for which to get or create a customer. :type subscriber: User :param livemode: Whether to get the subscriber in live or test mode. :type livemode: bool """ try: return cls.objects.get(subscriber=subscriber, livemode=livemode), False except cls.DoesNotExist: action = f"create:{subscriber.pk}" idempotency_key = djstripe_settings.get_idempotency_key( "customer", action, livemode ) return ( cls.create( subscriber, idempotency_key=idempotency_key, stripe_account=stripe_account, ), True, ) @classmethod def create(cls, subscriber, idempotency_key=None, stripe_account=None): metadata = {} subscriber_key = djstripe_settings.SUBSCRIBER_CUSTOMER_KEY if subscriber_key not in ("", None): metadata[subscriber_key] = subscriber.pk stripe_customer = cls._api_create( email=subscriber.email, idempotency_key=idempotency_key, metadata=metadata, stripe_account=stripe_account, ) customer, created = cls.objects.get_or_create( id=stripe_customer["id"], defaults={ "subscriber": subscriber, "livemode": stripe_customer["livemode"], "balance": stripe_customer.get("balance", 0), "delinquent": stripe_customer.get("delinquent", False), }, ) return customer @property def credits(self): """ The customer is considered to have credits if their balance is below 0. """ return abs(min(self.balance, 0)) @property def customer_payment_methods(self): """ An iterable of all of the customer's payment methods (sources, then legacy cards) """ for source in self.sources.iterator(): yield source for card in self.legacy_cards.iterator(): yield card @property def pending_charges(self): """ The customer is considered to have pending charges if their balance is above 0. """ return max(self.balance, 0) def subscribe(self, *, items=None, price=None, plan=None, **kwargs): """ Subscribes this customer to all the prices or plans in the items dict (Recommended). :param items: A list of up to 20 subscription items, each with an attached price :type list: :param items: A dictionary of Plan (or Plan ID) or Price (or Price ID) :type dict: The price or plan to which to subscribe the customer. :param price: The price to which to subscribe the customer. :type price: Price or string (price ID) :param plan: The plan to which to subscribe the customer. :type plan: Plan or string (plan ID) """ from .billing import Subscription if (items and price) or (items and plan) or (price and plan): raise TypeError("Please define only one of items, price or plan arguments.") if items is None: _items = [{"price": price}] else: _items = [] for item in items: price = item.get("price", "") plan = item.get("plan", "") price, kwargs = _sanitise_price(price, plan, **kwargs) if "price" in item: _items.append({"price": price}) if "plan" in item: _items.append({"plan": price}) stripe_subscription = Subscription._api_create( items=_items, customer=self.id, **kwargs ) api_key = kwargs.get("api_key") or self.default_api_key return Subscription.sync_from_stripe_data(stripe_subscription, api_key=api_key) def charge( self, amount: Decimal, *, application_fee: Decimal = None, source: Union[str, StripeModel] = None, **kwargs, ) -> Charge: """ Creates a charge for this customer. :param amount: The amount to charge. :type amount: Decimal. Precision is 2; anything more will be ignored. :param source: The source to use for this charge. Must be a source attributed to this customer. If None, the customer's default source is used. Can be either the id of the source or the source object itself. :type source: string, Source """ if not isinstance(amount, Decimal): raise ValueError("You must supply a decimal value representing dollars.") # Convert Source to id if source and isinstance(source, StripeModel): source = source.id stripe_charge = Charge._api_create( customer=self.id, amount=int(amount * 100), # Convert dollars into cents application_fee=int(application_fee * 100) if application_fee else None, # Convert dollars into cents source=source, **kwargs, ) api_key = kwargs.get("api_key") or self.default_api_key return Charge.sync_from_stripe_data(stripe_charge, api_key=api_key) def add_invoice_item( self, amount, currency, description=None, discountable=None, invoice=None, metadata=None, subscription=None, ): """ Adds an arbitrary charge or credit to the customer's upcoming invoice. Different than creating a charge. Charges are separate bills that get processed immediately. Invoice items are appended to the customer's next invoice. This is extremely useful when adding surcharges to subscriptions. :param amount: The amount to charge. :type amount: Decimal. Precision is 2; anything more will be ignored. :param currency: 3-letter ISO code for currency :type currency: string :param description: An arbitrary string. :type description: string :param discountable: Controls whether discounts apply to this invoice item. Defaults to False for prorations or negative invoice items, and True for all other invoice items. :type discountable: boolean :param invoice: An existing invoice to add this invoice item to. When left blank, the invoice item will be added to the next upcoming \ scheduled invoice. \ Use this when adding invoice items in response to an \ ``invoice.created`` webhook. You cannot add an invoice \ item to an invoice that has already been paid, attempted or closed. :type invoice: Invoice or string (invoice ID) :param metadata: A set of key/value pairs useful for storing additional information. :type metadata: dict :param subscription: A subscription to add this invoice item to. When left blank, the invoice item will be be added to the next upcoming \ scheduled invoice. When set, scheduled invoices for subscriptions other \ than the specified subscription will ignore the invoice item. \ Use this when you want to express that an invoice item has been accrued \ within the context of a particular subscription. :type subscription: Subscription or string (subscription ID) .. Notes: .. if you're using ``Customer.add_invoice_item()`` instead of .. ``Customer.add_invoice_item()``, ``invoice`` and ``subscriptions`` .. can only be strings """ from .billing import InvoiceItem if not isinstance(amount, Decimal): raise ValueError("You must supply a decimal value representing dollars.") # Convert Invoice to id if invoice is not None and isinstance(invoice, StripeModel): invoice = invoice.id # Convert Subscription to id if subscription is not None and isinstance(subscription, StripeModel): subscription = subscription.id stripe_invoiceitem = InvoiceItem._api_create( amount=int(amount * 100), # Convert dollars into cents currency=currency, customer=self.id, description=description, discountable=discountable, invoice=invoice, metadata=metadata, subscription=subscription, ) return InvoiceItem.sync_from_stripe_data( stripe_invoiceitem, api_key=self.default_api_key ) def add_card(self, source, set_default=True): """ Adds a card to this customer's account. :param source: Either a token, like the ones returned by our Stripe.js, or a dictionary containing a user's credit card details. Stripe will automatically validate the card. :type source: string, dict :param set_default: Whether or not to set the source as the customer's default source :type set_default: boolean """ from .payment_methods import DjstripePaymentMethod stripe_customer = self.api_retrieve() new_stripe_payment_method = stripe_customer.sources.create(source=source) if set_default: stripe_customer.default_source = new_stripe_payment_method["id"] stripe_customer.save() new_payment_method = DjstripePaymentMethod.from_stripe_object( new_stripe_payment_method ) # Change the default source if set_default: self.default_source = new_payment_method self.save() return new_payment_method.resolve() def add_payment_method(self, payment_method, set_default=True): """ Adds an already existing payment method to this customer's account :param payment_method: PaymentMethod to be attached to the customer :type payment_method: str, PaymentMethod :param set_default: If true, this will be set as the default_payment_method :type set_default: bool :rtype: PaymentMethod """ from .payment_methods import PaymentMethod stripe_customer = self.api_retrieve() payment_method = PaymentMethod.attach(payment_method, stripe_customer) if set_default: stripe_customer["invoice_settings"][ "default_payment_method" ] = payment_method.id stripe_customer.save() # Refresh self from the stripe customer, this should have two effects: # 1) sets self.default_payment_method (we rely on logic in # Customer._manipulate_stripe_object_hook to do this) # 2) updates self.invoice_settings.default_payment_methods self.sync_from_stripe_data(stripe_customer, api_key=self.default_api_key) self.refresh_from_db() return payment_method def purge(self): """Customers are soft deleted as deleted customers are still accessible by the Stripe API and sync for all RelatedModels would fail""" try: self._api_delete() except InvalidRequestError as exc: if "No such customer:" in str(exc): # The exception was thrown because the stripe customer was already # deleted on the stripe side, ignore the exception pass else: # The exception was raised for another reason, re-raise it raise # toggle the deleted flag on Customer to indicate it has been # deleted upstream in Stripe self.deleted = True if self.subscriber: # Delete the idempotency key used by Customer.create() # So re-creating a customer for this subscriber before the key expires # doesn't return the older Customer data idempotency_key_action = f"customer:create:{self.subscriber.pk}" IdempotencyKey.objects.filter(action=idempotency_key_action).delete() self.subscriber = None # Remove sources self.default_source = None for source in self.legacy_cards.all(): source.remove() for source in self.sources.all(): source.detach() self.date_purged = timezone.now() self.save() def _get_valid_subscriptions(self): """Get a list of this customer's valid subscriptions.""" return [ subscription for subscription in self.subscriptions.all() if subscription.is_valid() ] def is_subscribed_to(self, product: Union[Product, str]) -> bool: """ Checks to see if this customer has an active subscription to the given product. :param product: The product for which to check for an active subscription. :type product: Product or string (product ID) :returns: True if there exists an active subscription, False otherwise. """ if isinstance(product, StripeModel): product = product.id for subscription in self._get_valid_subscriptions(): for item in subscription.items.all(): if item.price and item.price.product.id == product: return True return False def has_any_active_subscription(self): """ Checks to see if this customer has an active subscription to any plan. :returns: True if there exists an active subscription, False otherwise. """ return len(self._get_valid_subscriptions()) != 0 @property def active_subscriptions(self): """ Returns active subscriptions (subscriptions with an active status that end in the future). """ return self.subscriptions.filter( status=enums.SubscriptionStatus.active, current_period_end__gt=timezone.now(), ) @property def valid_subscriptions(self): """ Returns this customer's valid subscriptions (subscriptions that aren't canceled or incomplete_expired). """ return self.subscriptions.exclude( status__in=[ enums.SubscriptionStatus.canceled, enums.SubscriptionStatus.incomplete_expired, ] ) @property def subscription(self): """ Shortcut to get this customer's subscription. :returns: None if the customer has no subscriptions, the subscription if the customer has a subscription. :raises MultipleSubscriptionException: Raised if the customer has multiple subscriptions. In this case, use ``Customer.subscriptions`` instead. """ subscriptions = self.valid_subscriptions if subscriptions.count() > 1: raise MultipleSubscriptionException( "This customer has multiple subscriptions. Use Customer.subscriptions " "to access them." ) else: return subscriptions.first() def send_invoice(self): """ Pay and send the customer's latest invoice. :returns: True if an invoice was able to be created and paid, False otherwise (typically if there was nothing to invoice). """ from .billing import Invoice try: invoice = Invoice._api_create(customer=self.id) invoice.pay() return True except InvalidRequestError: # TODO: Check this for a more # specific error message. return False # There was nothing to invoice def retry_unpaid_invoices(self): """Attempt to retry collecting payment on the customer's unpaid invoices.""" self._sync_invoices() for invoice in self.invoices.filter(auto_advance=True).exclude(status="paid"): try: invoice.retry() # Always retry unpaid invoices except InvalidRequestError as exc: if str(exc) != "Invoice is already paid": raise def add_coupon(self, coupon, idempotency_key=None): """ Add a coupon to a Customer. The coupon can be a Coupon object, or a valid Stripe Coupon ID. """ if isinstance(coupon, StripeModel): coupon = coupon.id stripe_customer = self.api_retrieve() stripe_customer["coupon"] = coupon stripe_customer.save(idempotency_key=idempotency_key) return self.__class__.sync_from_stripe_data( stripe_customer, api_key=self.default_api_key ) def upcoming_invoice(self, **kwargs): """Gets the upcoming preview invoice (singular) for this customer. See `Invoice.upcoming() <#djstripe.Invoice.upcoming>`__. The ``customer`` argument to the ``upcoming()`` call is automatically set by this method. """ from .billing import Invoice kwargs["customer"] = self return Invoice.upcoming(**kwargs) def _attach_objects_post_save_hook( self, cls, data, pending_relations=None, api_key=djstripe_settings.STRIPE_SECRET_KEY, ): from .billing import Coupon from .payment_methods import DjstripePaymentMethod super()._attach_objects_post_save_hook( cls, data, pending_relations=pending_relations, api_key=api_key ) save = False customer_sources = data.get("sources") sources = {} if customer_sources: # Have to create sources before we handle the default_source # We save all of them in the `sources` dict, so that we can find them # by id when we look at the default_source (we need the source type). for source in customer_sources["data"]: obj, _ = DjstripePaymentMethod._get_or_create_source( source, source["object"], api_key=api_key ) sources[source["id"]] = obj discount = data.get("discount") if discount: coupon, _created = Coupon._get_or_create_from_stripe_object( discount, "coupon", api_key=api_key ) if coupon and coupon != self.coupon: self.coupon = coupon save = True elif self.coupon: self.coupon = None save = True if save: self.save() def _attach_objects_hook( self, cls, data, current_ids=None, api_key=djstripe_settings.STRIPE_SECRET_KEY ): # When we save a customer to Stripe, we add a reference to its Django PK # in the `django_account` key. If we find that, we re-attach that PK. subscriber_key = djstripe_settings.SUBSCRIBER_CUSTOMER_KEY if subscriber_key in ("", None): # Disabled. Nothing else to do. return subscriber_id = data.get("metadata", {}).get(subscriber_key) if subscriber_id: cls = djstripe_settings.get_subscriber_model() try: # We have to perform a get(), instead of just attaching the PK # blindly as the object may have been deleted or not exist. # Attempting to save that would cause an IntegrityError. self.subscriber = cls.objects.get(pk=subscriber_id) except (cls.DoesNotExist, ValueError): logger.warning( "Could not find subscriber %r matching customer %r", subscriber_id, self.id, ) self.subscriber = None # SYNC methods should be dropped in favor of the master sync infrastructure proposed def _sync_invoices(self, **kwargs): from .billing import Invoice api_key = kwargs.get("api_key") or self.default_api_key for stripe_invoice in Invoice.api_list(customer=self.id, **kwargs): Invoice.sync_from_stripe_data(stripe_invoice, api_key=api_key) def _sync_charges(self, **kwargs): api_key = kwargs.get("api_key") or self.default_api_key for stripe_charge in Charge.api_list(customer=self.id, **kwargs): Charge.sync_from_stripe_data(stripe_charge, api_key=api_key) def _sync_cards(self, **kwargs): from .payment_methods import Card api_key = kwargs.get("api_key") or self.default_api_key for stripe_card in Card.api_list(customer=self, **kwargs): Card.sync_from_stripe_data(stripe_card, api_key=api_key) def _sync_subscriptions(self, **kwargs): from .billing import Subscription api_key = kwargs.get("api_key") or self.default_api_key for stripe_subscription in Subscription.api_list( customer=self.id, status="all", **kwargs ): Subscription.sync_from_stripe_data(stripe_subscription, api_key=api_key) class Dispute(StripeModel): """ A dispute occurs when a customer questions your charge with their card issuer. When this happens, you're given the opportunity to respond to the dispute with evidence that shows that the charge is legitimate Stripe documentation: https://stripe.com/docs/api?lang=python#disputes """ stripe_class = stripe.Dispute stripe_dashboard_item_name = "payments" amount = StripeQuantumCurrencyAmountField( help_text=( "Disputed amount (in cents). Usually the amount of the charge, " "but can differ " "(usually because of currency fluctuation or because only part of " "the order is disputed)." ) ) balance_transaction = StripeForeignKey( "BalanceTransaction", null=True, on_delete=models.CASCADE, related_name="disputes", help_text="Balance transaction that describes the impact on your " "account balance.", ) balance_transactions = JSONField( default=list, help_text="List of 0, 1 or 2 Balance Transactions that show funds withdrawn and reinstated to your Stripe account as a result of this dispute.", ) # charge is nullable to avoid infinite sync as Charge model has a dispute field as well charge = StripeForeignKey( "Charge", null=True, on_delete=models.CASCADE, related_name="disputes", help_text="The charge that was disputed", ) currency = StripeCurrencyCodeField() evidence = JSONField(help_text="Evidence provided to respond to a dispute.") evidence_details = JSONField(help_text="Information about the evidence submission.") is_charge_refundable = models.BooleanField( help_text=( "If true, it is still possible to refund the disputed payment. " "Once the payment has been fully refunded, no further funds will " "be withdrawn from your Stripe account as a result of this dispute." ) ) payment_intent = StripeForeignKey( "PaymentIntent", null=True, on_delete=models.CASCADE, related_name="disputes", help_text="The PaymentIntent that was disputed", ) reason = StripeEnumField(enum=enums.DisputeReason) status = StripeEnumField(enum=enums.DisputeStatus) def __str__(self): amount = get_friendly_currency_amount(self.amount / 100, self.currency) status = enums.DisputeStatus.humanize(self.status) return f"{amount} ({status}) " def get_stripe_dashboard_url(self) -> str: """Get the stripe dashboard url for this object.""" return ( f"{self._get_base_stripe_dashboard_url()}" f"{self.stripe_dashboard_item_name}/{self.payment_intent.id}" ) def _attach_objects_post_save_hook( self, cls, data, pending_relations=None, api_key=djstripe_settings.STRIPE_SECRET_KEY, ): super()._attach_objects_post_save_hook( cls, data, pending_relations=pending_relations, api_key=api_key ) # Retrieve and save files from the dispute.evidence object. # todo find a better way of retrieving and syncing File Type fields from Dispute object for field in ( "cancellation_policy", "customer_communication", "customer_signature", "duplicate_charge_documentation", "receipt", "refund_policy", "service_documentation", "shipping_documentation", "uncategorized_file", ): file_upload_id = self.evidence.get(field, None) if file_upload_id: try: File.sync_from_stripe_data( File(id=file_upload_id).api_retrieve(api_key=api_key), api_key=api_key, ) except stripe.error.PermissionError: # No permission to retrieve the data with the key # Log a warning message logger.warning( "No permission to retrieve the File Evidence Object." ) except stripe.error.InvalidRequestError: raise # iterate and sync every balance transaction for stripe_balance_transaction in self.balance_transactions: BalanceTransaction.sync_from_stripe_data( stripe_balance_transaction, api_key=api_key ) class Event(StripeModel): """ Events are Stripe's way of letting you know when something interesting happens in your account. When an interesting event occurs, a new Event object is created and POSTed to the configured webhook URL if the Event type matches. Stripe documentation: https://stripe.com/docs/api/events?lang=python """ stripe_class = stripe.Event stripe_dashboard_item_name = "events" api_version = models.CharField( max_length=64, blank=True, help_text="the API version at which the event data was " "rendered. Blank for old entries only, all new entries will have this value", ) data = JSONField( help_text="data received at webhook. data should be considered to be garbage " "until validity check is run and valid flag is set" ) request_id = models.CharField( max_length=50, help_text="Information about the request that triggered this event, " "for traceability purposes. If empty string then this is an old entry " "without that data. If Null then this is not an old entry, but a Stripe " "'automated' event with no associated request.", default="", blank=True, ) idempotency_key = models.TextField(default="", blank=True) type = models.CharField(max_length=250, help_text="Stripe's event description code") def __str__(self): return f"type={self.type}, id={self.id}" def _attach_objects_hook( self, cls, data, current_ids=None, api_key=djstripe_settings.STRIPE_SECRET_KEY ): if self.api_version is None: # as of api version 2017-02-14, the account.application.deauthorized # event sends None as api_version. # If we receive that, store an empty string instead. # Remove this hack if this gets fixed upstream. self.api_version = "" request_obj = data.get("request", None) if isinstance(request_obj, dict): # Format as of 2017-05-25 self.request_id = request_obj.get("id") or "" self.idempotency_key = request_obj.get("idempotency_key") or "" else: # Format before 2017-05-25 self.request_id = request_obj or "" @classmethod def process(cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY): qs = cls.objects.filter(id=data["id"]) if qs.exists(): return qs.first() # Rollback any DB operations in the case of failure so # we will retry creating and processing the event the # next time the webhook fires. with transaction.atomic(): # process the event and create an Event Object ret = cls._create_from_stripe_object(data, api_key=api_key) ret.invoke_webhook_handlers() return ret def invoke_webhook_handlers(self): """ Invokes any webhook handlers that have been registered for this event based on event type or event sub-type. See event handlers registered in the ``djstripe.event_handlers`` module (or handlers registered in djstripe plugins or contrib packages). """ webhooks.call_handlers(event=self) signal = WEBHOOK_SIGNALS.get(self.type) if signal: return signal.send(sender=Event, event=self) @cached_property def parts(self): """Gets the event category/verb as a list of parts.""" return str(self.type).split(".") @cached_property def category(self): """Gets the event category string (e.g. 'customer').""" return self.parts[0] @cached_property def verb(self): """Gets the event past-tense verb string (e.g. 'updated').""" return ".".join(self.parts[1:]) @property def customer(self): data = self.data["object"] if data["object"] == "customer": customer_id = get_id_from_stripe_data(data.get("id")) else: customer_id = get_id_from_stripe_data(data.get("customer")) if customer_id: return Customer._get_or_retrieve( id=customer_id, stripe_account=getattr(self.djstripe_owner_account, "id", None), api_key=self.default_api_key, ) class File(StripeModel): """ This is an object representing a file hosted on Stripe's servers. The file may have been uploaded by yourself using the create file request (for example, when uploading dispute evidence) or it may have been created by Stripe (for example, the results of a Sigma scheduled query). Stripe documentation: https://stripe.com/docs/api/files?lang=python """ stripe_class = stripe.File filename = models.CharField( max_length=255, help_text="A filename for the file, suitable for saving to a filesystem.", ) purpose = StripeEnumField( enum=enums.FilePurpose, help_text="The purpose of the uploaded file." ) size = models.IntegerField(help_text="The size in bytes of the file upload object.") type = StripeEnumField( enum=enums.FileType, help_text="The type of the file returned." ) url = models.CharField( max_length=200, help_text="A read-only URL where the uploaded file can be accessed.", ) @classmethod def is_valid_object(cls, data): return data and data.get("object") in ("file", "file_upload") def __str__(self): return f"{self.filename}, {enums.FilePurpose.humanize(self.purpose)}" # Alias for compatibility # Stripe's SDK has the same alias. # Do not remove/deprecate as long as it's present there. FileUpload = File class FileLink(StripeModel): """ To share the contents of a File object with non-Stripe users, you can create a FileLink. FileLinks contain a URL that can be used to retrieve the contents of the file without authentication. Stripe documentation: https://stripe.com/docs/api/file_links?lang=python """ stripe_class = stripe.FileLink expires_at = StripeDateTimeField( null=True, blank=True, help_text="Time at which the link expires." ) file = StripeForeignKey("File", on_delete=models.CASCADE) url = models.URLField(help_text="The publicly accessible URL to download the file.") def __str__(self): return f"{self.file.filename}, {self.url}" class PaymentIntent(StripeModel): """ A PaymentIntent guides you through the process of collecting a payment from your customer. We recommend that you create exactly one PaymentIntent for each order or customer session in your system. You can reference the PaymentIntent later to see the history of payment attempts for a particular session. A PaymentIntent transitions through multiple statuses throughout its lifetime as it interfaces with Stripe.js to perform authentication flows and ultimately creates at most one successful charge. Stripe documentation: https://stripe.com/docs/api?lang=python#payment_intents """ stripe_class = stripe.PaymentIntent stripe_dashboard_item_name = "payments" amount = StripeQuantumCurrencyAmountField( help_text="Amount (in cents) intended to be collected by this PaymentIntent." ) amount_capturable = StripeQuantumCurrencyAmountField( help_text="Amount (in cents) that can be captured from this PaymentIntent." ) amount_received = StripeQuantumCurrencyAmountField( help_text="Amount (in cents) that was collected by this PaymentIntent." ) # application # application_fee_amount canceled_at = StripeDateTimeField( null=True, blank=True, default=None, help_text=( "Populated when status is canceled, this is the time at which the " "PaymentIntent was canceled. Measured in seconds since the Unix epoch." ), ) cancellation_reason = StripeEnumField( enum=enums.PaymentIntentCancellationReason, blank=True, help_text=( "Reason for cancellation of this PaymentIntent, either user-provided " "(duplicate, fraudulent, requested_by_customer, or abandoned) or " "generated by Stripe internally (failed_invoice, void_invoice, " "or automatic)." ), ) capture_method = StripeEnumField( enum=enums.CaptureMethod, help_text="Capture method of this PaymentIntent, one of automatic or manual.", ) client_secret = models.TextField( max_length=5000, help_text=( "The client secret of this PaymentIntent. " "Used for client-side retrieval using a publishable key." ), ) confirmation_method = StripeEnumField( enum=enums.ConfirmationMethod, help_text=( "Confirmation method of this PaymentIntent, one of manual or automatic." ), ) currency = StripeCurrencyCodeField() customer = StripeForeignKey( "Customer", null=True, on_delete=models.CASCADE, help_text="Customer this PaymentIntent is for if one exists.", ) description = models.TextField( max_length=1000, default="", blank=True, help_text=( "An arbitrary string attached to the object. " "Often useful for displaying to users." ), ) last_payment_error = JSONField( null=True, blank=True, help_text=( "The payment error encountered in the previous PaymentIntent confirmation." ), ) next_action = JSONField( null=True, blank=True, help_text=( "If present, this property tells you what actions you need to take " "in order for your customer to fulfill a payment using the provided source." ), ) on_behalf_of = StripeForeignKey( "Account", on_delete=models.CASCADE, null=True, blank=True, help_text="The account (if any) for which the funds of the " "PaymentIntent are intended.", related_name="payment_intents", ) payment_method = StripeForeignKey( "PaymentMethod", on_delete=models.SET_NULL, null=True, blank=True, help_text="Payment method used in this PaymentIntent.", ) payment_method_types = JSONField( help_text=( "The list of payment method types (e.g. card) that this " "PaymentIntent is allowed to use." ) ) receipt_email = models.CharField( blank=True, max_length=255, help_text=( "Email address that the receipt for the resulting payment will be sent to." ), ) # TODO: Add `review` field after we add Review model. setup_future_usage = StripeEnumField( enum=enums.IntentUsage, null=True, blank=True, help_text=( "Indicates that you intend to make future payments with this " "PaymentIntent's payment method. " "If present, the payment method used with this PaymentIntent can " "be attached to a Customer, even after the transaction completes. " "Use `on_session` if you intend to only reuse the payment method " "when your customer is present in your checkout flow. Use `off_session` " "if your customer may or may not be in your checkout flow. " "Stripe uses `setup_future_usage` to dynamically optimize " "your payment flow and comply with regional legislation and network rules. " "For example, if your customer is impacted by SCA, using `off_session` " "will ensure that they are authenticated while processing this " "PaymentIntent. You will then be able to make later off-session payments " "for this customer." ), ) shipping = JSONField( null=True, blank=True, help_text="Shipping information for this PaymentIntent." ) statement_descriptor = models.CharField( max_length=22, blank=True, help_text=( "For non-card charges, you can use this value as the complete description " "that appears on your customers' statements. Must contain at least one " "letter, maximum 22 characters." ), ) status = StripeEnumField( enum=enums.PaymentIntentStatus, help_text=( "Status of this PaymentIntent, one of requires_payment_method, " "requires_confirmation, requires_action, processing, requires_capture, " "canceled, or succeeded. " "You can read more about PaymentIntent statuses here." ), ) transfer_data = JSONField( null=True, blank=True, help_text=( "The data with which to automatically create a Transfer when the payment " "is finalized. " "See the PaymentIntents Connect usage guide for details." ), ) transfer_group = models.CharField( blank=True, max_length=255, help_text=( "A string that identifies the resulting payment as part of a group. " "See the PaymentIntents Connect usage guide for details." ), ) def __str__(self): account = self.on_behalf_of customer = self.customer amount = get_friendly_currency_amount(self.amount / 100, self.currency) status = enums.PaymentIntentStatus.humanize(self.status) if account and customer: return f"{amount} ({status}) for {account} by {customer}" if account: return f"{amount} for {account}. {status}" if customer: return f"{amount} by {customer}. {status}" return f"{amount} ({status})" def update(self, api_key=None, **kwargs): """ Call the stripe API's modify operation for this model :param api_key: The api key to use for this request. Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string """ api_key = api_key or self.default_api_key response = self.api_retrieve(api_key=api_key) return response.modify(response.stripe_id, api_key=api_key, **kwargs) def _api_cancel(self, api_key=None, **kwargs): """ Call the stripe API's cancel operation for this model :param api_key: The api key to use for this request. Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string """ api_key = api_key or self.default_api_key return self.api_retrieve(api_key=api_key).cancel(**kwargs) def _api_confirm(self, api_key=None, **kwargs): """ Call the stripe API's confirm operation for this model. Confirm that your customer intends to pay with current or provided payment method. Upon confirmation, the PaymentIntent will attempt to initiate a payment. :param api_key: The api key to use for this request. Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string """ api_key = api_key or self.default_api_key return self.api_retrieve(api_key=api_key).confirm(**kwargs) class SetupIntent(StripeModel): """ A SetupIntent guides you through the process of setting up a customer's payment credentials for future payments. For example, you could use a SetupIntent to set up your customer's card without immediately collecting a payment. Later, you can use PaymentIntents to drive the payment flow. NOTE: You should not maintain long-lived, unconfirmed SetupIntents. For security purposes, SetupIntents older than 24 hours may no longer be valid. Stripe documentation: https://stripe.com/docs/api?lang=python#setup_intents """ stripe_class = stripe.SetupIntent application = models.CharField( max_length=255, blank=True, help_text="ID of the Connect application that created the SetupIntent.", ) cancellation_reason = StripeEnumField( enum=enums.SetupIntentCancellationReason, blank=True, help_text=( "Reason for cancellation of this SetupIntent, one of abandoned, " "requested_by_customer, or duplicate" ), ) client_secret = models.TextField( max_length=5000, blank=True, help_text=( "The client secret of this SetupIntent. " "Used for client-side retrieval using a publishable key." ), ) customer = StripeForeignKey( "Customer", null=True, blank=True, on_delete=models.SET_NULL, help_text="Customer this SetupIntent belongs to, if one exists.", ) last_setup_error = JSONField( null=True, blank=True, help_text="The error encountered in the previous SetupIntent confirmation.", ) next_action = JSONField( null=True, blank=True, help_text=( "If present, this property tells you what actions you need to take in" "order for your customer to continue payment setup." ), ) on_behalf_of = StripeForeignKey( "Account", on_delete=models.SET_NULL, null=True, blank=True, help_text="The account (if any) for which the setup is intended.", related_name="setup_intents", ) payment_method = StripeForeignKey( "PaymentMethod", on_delete=models.SET_NULL, null=True, blank=True, help_text="Payment method used in this PaymentIntent.", ) payment_method_types = JSONField( help_text=( "The list of payment method types (e.g. card) that this PaymentIntent is " "allowed to use." ) ) status = StripeEnumField( enum=enums.SetupIntentStatus, help_text=( "Status of this SetupIntent, one of requires_payment_method, " "requires_confirmation, requires_action, processing, " "canceled, or succeeded." ), ) usage = StripeEnumField( enum=enums.IntentUsage, default=enums.IntentUsage.off_session, help_text=( "Indicates how the payment method is intended to be used in the future." ), ) def __str__(self): account = self.on_behalf_of customer = self.customer if account and customer: return ( f"{self.payment_method} ({enums.SetupIntentStatus.humanize(self.status)}) " f"for {account} " f"by {customer}" ) if account: return f"{self.payment_method} for {account}. {enums.SetupIntentStatus.humanize(self.status)}" if customer: return f"{self.payment_method} by {customer}. {enums.SetupIntentStatus.humanize(self.status)}" return ( f"{self.payment_method} ({enums.SetupIntentStatus.humanize(self.status)})" ) # TODO Add Tests class Payout(StripeModel): """ A Payout object is created when you receive funds from Stripe, or when you initiate a payout to either a bank account or debit card of a connected Stripe account. Stripe documentation: https://stripe.com/docs/api?lang=python#payouts """ expand_fields = ["destination"] stripe_class = stripe.Payout stripe_dashboard_item_name = "payouts" amount = StripeDecimalCurrencyAmountField( help_text="Amount (as decimal) to be transferred to your bank account or " "debit card." ) arrival_date = StripeDateTimeField( help_text=( "Date the payout is expected to arrive in the bank. " "This factors in delays like weekends or bank holidays." ) ) automatic = models.BooleanField( help_text=( "`true` if the payout was created by an automated payout schedule, " "and `false` if it was requested manually." ) ) balance_transaction = StripeForeignKey( "BalanceTransaction", on_delete=models.SET_NULL, null=True, help_text="Balance transaction that describes the impact on your " "account balance.", ) currency = StripeCurrencyCodeField() destination = PaymentMethodForeignKey( on_delete=models.PROTECT, null=True, help_text="Bank account or card the payout was sent to.", ) failure_balance_transaction = StripeForeignKey( "BalanceTransaction", on_delete=models.SET_NULL, related_name="failure_payouts", null=True, blank=True, help_text=( "If the payout failed or was canceled, this will be the balance " "transaction that reversed the initial balance transaction, and " "puts the funds from the failed payout back in your balance." ), ) failure_code = StripeEnumField( enum=enums.PayoutFailureCode, default="", blank=True, help_text=( "Error code explaining reason for transfer failure if available. " "See https://stripe.com/docs/api?lang=python#transfer_failures." ), ) failure_message = models.TextField( default="", blank=True, help_text=( "Message to user further explaining reason for " "payout failure if available." ), ) method = StripeEnumField( max_length=8, enum=enums.PayoutMethod, help_text=( "The method used to send this payout. " "`instant` is only supported for payouts to debit cards." ), ) # TODO: `original_payout` impl as OneToOne, with `reversed_by` reverse relation # original_payout = StripeForeignKey( # "Payout", # on_delete=models.SET_NULL, # null=True, # blank=True, # help_text="If the payout reverses another, this is the original payout.", # ) source_type = StripeEnumField( enum=enums.PayoutSourceType, help_text="The source balance this payout came from.", ) statement_descriptor = models.CharField( max_length=255, default="", blank=True, help_text="Extra information about a payout to be displayed " "on the user's bank statement.", ) status = StripeEnumField( enum=enums.PayoutStatus, help_text=( "Current status of the payout. " "A payout will be `pending` until it is submitted to the bank, " "at which point it becomes `in_transit`. " "It will then change to paid if the transaction goes through. " "If it does not go through successfully, " "its status will change to `failed` or `canceled`." ), ) type = StripeEnumField(enum=enums.PayoutType) def __str__(self): return f"{self.amount} ({enums.PayoutStatus.humanize(self.status)})" class Price(StripeModel): """ Prices define the unit cost, currency, and (optional) billing cycle for both recurring and one-time purchases of products. Price and Plan objects are the same, but use a different representation. Creating a recurring Price in Stripe also makes a Plan available, and vice versa. This is not the case for a Price with interval=one_time. Price objects are a more recent API representation, support more features and its usage is encouraged instead of Plan objects. Stripe documentation: - https://stripe.com/docs/api/prices - https://stripe.com/docs/billing/prices-guide """ stripe_class = stripe.Price expand_fields = ["product", "tiers"] stripe_dashboard_item_name = "prices" active = models.BooleanField( help_text="Whether the price can be used for new purchases." ) currency = StripeCurrencyCodeField() nickname = models.CharField( max_length=250, blank=True, help_text="A brief description of the plan, hidden from customers.", ) product = StripeForeignKey( "Product", on_delete=models.CASCADE, related_name="prices", help_text="The product this price is associated with.", ) recurring = JSONField( default=None, blank=True, null=True, help_text=( "The recurring components of a price such as `interval` and `usage_type`." ), ) type = StripeEnumField( enum=enums.PriceType, help_text=( "Whether the price is for a one-time purchase or a recurring " "(subscription) purchase." ), ) unit_amount = StripeQuantumCurrencyAmountField( null=True, blank=True, help_text=( "The unit amount in cents to be charged, represented as a whole " "integer if possible. Null if a sub-cent precision is required." ), ) unit_amount_decimal = StripeDecimalCurrencyAmountField( null=True, blank=True, max_digits=19, decimal_places=12, help_text=( "The unit amount in cents to be charged, represented as a decimal " "string with at most 12 decimal places." ), ) # More attributes… billing_scheme = StripeEnumField( enum=enums.BillingScheme, blank=True, help_text=( "Describes how to compute the price per period. " "Either `per_unit` or `tiered`. " "`per_unit` indicates that the fixed amount (specified in `unit_amount` " "or `unit_amount_decimal`) will be charged per unit in `quantity` " "(for prices with `usage_type=licensed`), or per unit of total " "usage (for prices with `usage_type=metered`). " "`tiered` indicates that the unit pricing will be computed using " "a tiering strategy as defined using the `tiers` and `tiers_mode` " "attributes." ), ) lookup_key = models.CharField( max_length=250, null=True, blank=True, help_text="A lookup key used to retrieve prices dynamically from a " "static string.", ) tiers = JSONField( null=True, blank=True, help_text=( "Each element represents a pricing tier. " "This parameter requires `billing_scheme` to be set to `tiered`." ), ) tiers_mode = StripeEnumField( enum=enums.PriceTiersMode, null=True, blank=True, help_text=( "Defines if the tiering price should be `graduated` or `volume` based. " "In `volume`-based tiering, the maximum quantity within a period " "determines the per unit price, in `graduated` tiering pricing can " "successively change as the quantity grows." ), ) transform_quantity = JSONField( null=True, blank=True, help_text=( "Apply a transformation to the reported usage or set quantity " "before computing the amount billed. Cannot be combined with `tiers`." ), ) class Meta(object): ordering = ["unit_amount"] @classmethod def get_or_create(cls, **kwargs): """Get or create a Price.""" try: return cls.objects.get(id=kwargs["id"]), False except cls.DoesNotExist: return cls.create(**kwargs), True @classmethod def create(cls, **kwargs): # A few minor things are changed in the api-version of the create call api_kwargs = dict(kwargs) if api_kwargs["unit_amount"]: api_kwargs["unit_amount"] = int(api_kwargs["unit_amount"] * 100) if isinstance(api_kwargs.get("product"), StripeModel): api_kwargs["product"] = api_kwargs["product"].id stripe_price = cls._api_create(**api_kwargs) api_key = api_kwargs.get("api_key") or djstripe_settings.STRIPE_SECRET_KEY price = cls.sync_from_stripe_data(stripe_price, api_key=api_key) return price def __str__(self): return f"{self.human_readable_price} for {self.product.name}" @property def human_readable_price(self): if self.billing_scheme == "per_unit": unit_amount = (self.unit_amount or 0) / 100 amount = get_friendly_currency_amount(unit_amount, self.currency) else: # tiered billing scheme tier_1 = self.tiers[0] formatted_unit_amount_tier_1 = get_friendly_currency_amount( (tier_1["unit_amount"] or 0) / 100, self.currency ) amount = f"Starts at {formatted_unit_amount_tier_1} per unit" # stripe shows flat fee even if it is set to 0.00 flat_amount_tier_1 = tier_1["flat_amount"] if flat_amount_tier_1 is not None: formatted_flat_amount_tier_1 = get_friendly_currency_amount( flat_amount_tier_1 / 100, self.currency ) amount = f"{amount} + {formatted_flat_amount_tier_1}" format_args = {"amount": amount} if self.recurring: interval_count = self.recurring["interval_count"] if interval_count == 1: interval = { "day": _("day"), "week": _("week"), "month": _("month"), "year": _("year"), }[self.recurring["interval"]] template = _("{amount}/{interval}") format_args["interval"] = interval else: interval = { "day": _("days"), "week": _("weeks"), "month": _("months"), "year": _("years"), }[self.recurring["interval"]] template = _("{amount} / every {interval_count} {interval}") format_args["interval"] = interval format_args["interval_count"] = interval_count else: template = _("{amount} (one time)") return format_lazy(template, **format_args) class Refund(StripeModel): """ Refund objects allow you to refund a charge that has previously been created but not yet refunded. Funds will be refunded to the credit or debit card that was originally charged. Stripe documentation: https://stripe.com/docs/api?lang=python#refund_object """ stripe_class = stripe.Refund amount = StripeQuantumCurrencyAmountField(help_text="Amount, in cents.") balance_transaction = StripeForeignKey( "BalanceTransaction", on_delete=models.SET_NULL, null=True, help_text="Balance transaction that describes the impact on your account " "balance.", ) charge = StripeForeignKey( "Charge", on_delete=models.CASCADE, related_name="refunds", help_text="The charge that was refunded", ) currency = StripeCurrencyCodeField() failure_balance_transaction = StripeForeignKey( "BalanceTransaction", on_delete=models.SET_NULL, related_name="failure_refunds", null=True, blank=True, help_text="If the refund failed, this balance transaction describes the " "adjustment made on your account balance that reverses the initial " "balance transaction.", ) failure_reason = StripeEnumField( enum=enums.RefundFailureReason, default="", blank=True, help_text="If the refund failed, the reason for refund failure if known.", ) reason = StripeEnumField( enum=enums.RefundReason, blank=True, default="", help_text="Reason for the refund.", ) receipt_number = models.CharField( max_length=9, default="", blank=True, help_text="The transaction number that appears on email receipts sent " "for this charge.", ) status = StripeEnumField( blank=True, enum=enums.RefundStatus, help_text="Status of the refund." ) # todo implement source_transfer_reversal and transfer_reversal def get_stripe_dashboard_url(self): return self.charge.get_stripe_dashboard_url() def __str__(self): amount = get_friendly_currency_amount(self.amount / 100, self.currency) status = enums.RefundStatus.humanize(self.status) return f"{amount} ({status})" ================================================ FILE: djstripe/models/fraud.py ================================================ ================================================ FILE: djstripe/models/orders.py ================================================ import stripe from django.db import models from djstripe.models.billing import Discount from djstripe.settings import djstripe_settings from ..enums import OrderStatus from ..fields import ( JSONField, StripeCurrencyCodeField, StripeEnumField, StripeForeignKey, StripeQuantumCurrencyAmountField, ) from ..settings import djstripe_settings from .base import StripeModel class Order(StripeModel): """ An Order describes a purchase being made by a customer, including the products & quantities being purchased, the order status, the payment information, and the billing/shipping details. Stripe documentation: https://stripe.com/docs/api/orders_v2/object?lang=python """ stripe_class = stripe.Order expand_fields = ["customer", "line_items", "discounts", "total_details.breakdown"] stripe_dashboard_item_name = "orders" amount_subtotal = StripeQuantumCurrencyAmountField( help_text="Order cost before any discounts or taxes are applied. A positive integer representing the subtotal of the order in the smallest currency unit (e.g., 100 cents to charge $1.00 or 100 to charge ¥100, a zero-decimal currency)." ) amount_total = StripeQuantumCurrencyAmountField( help_text="Total order cost after discounts and taxes are applied. A positive integer representing the cost of the order in the smallest currency unit (e.g., 100 cents to charge $1.00 or 100 to charge ¥100, a zero-decimal currency). To submit an order, the total must be either 0 or at least $0.50 USD or equivalent in charge currency." ) application = models.CharField( max_length=255, blank=True, help_text="ID of the Connect application that created the Order, if any.", ) automatic_tax = JSONField( help_text="Settings and latest results for automatic tax lookup for this Order." ) billing_details = JSONField( null=True, blank=True, help_text="Customer billing details associated with the order.", ) client_secret = models.TextField( max_length=5000, help_text=( "The client secret of this PaymentIntent. " "Used for client-side retrieval using a publishable key." ), ) currency = StripeCurrencyCodeField( help_text="Three-letter ISO currency code, in lowercase. Must be a supported currency." ) # not deleting order when customer is deleted, because order may be important for taxation and audit purposes customer = StripeForeignKey( "Customer", on_delete=models.SET_NULL, null=True, blank=True, help_text="The customer which this orders belongs to.", ) discounts = JSONField( null=True, blank=True, help_text="The discounts applied to the order.", ) ip_address = models.GenericIPAddressField( null=True, blank=True, help_text="A recent IP address of the purchaser used for tax reporting and tax location inference.", ) line_items = JSONField( help_text="A list of line items the customer is ordering. Each line item includes information about the product, the quantity, and the resulting cost. There is a maximum of 100 line items.", ) payment = JSONField( help_text="Payment information associated with the order. Includes payment status, settings, and a PaymentIntent ID", ) payment_intent = StripeForeignKey( "PaymentIntent", on_delete=models.SET_NULL, null=True, blank=True, help_text="ID of the payment intent associated with this order. Null when the order is open.", ) shipping_cost = JSONField( null=True, blank=True, help_text="The details of the customer cost of shipping, including the customer chosen ShippingRate.", ) shipping_details = JSONField( null=True, blank=True, help_text="Customer shipping information associated with the order.", ) status = StripeEnumField( enum=OrderStatus, help_text="The overall status of the order." ) tax_details = JSONField( null=True, blank=True, help_text="Tax details about the purchaser for this order.", ) total_details = JSONField( help_text="Tax, discount, and shipping details for the computed total amount of this order.", ) def __str__(self): template = f"on {self.created.strftime('%m/%d/%Y')} ({self.status})" if self.status in (OrderStatus.open, OrderStatus.canceled): return "Created " + template elif self.status in ( OrderStatus.submitted, OrderStatus.complete, OrderStatus.processing, ): return "Placed " + template return self.id @classmethod def _manipulate_stripe_object_hook(cls, data): data["payment_intent"] = data["payment"]["payment_intent"] return data def _attach_objects_post_save_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, pending_relations=None, ): super()._attach_objects_post_save_hook( cls, data, api_key=api_key, pending_relations=pending_relations ) # sync every discount for discount in self.discounts: Discount.sync_from_stripe_data(discount, api_key=api_key) def cancel(self, api_key=None, stripe_account=None, **kwargs): """ Cancels the order as well as the payment intent if one is attached. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string """ api_key = api_key or self.default_api_key # Prefer passed in stripe_account if set. if not stripe_account: stripe_account = self._get_stripe_account_id(api_key) return self.stripe_class.cancel( self.id, api_key=api_key, stripe_account=stripe_account, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ) def reopen(self, api_key=None, stripe_account=None, **kwargs): """ Reopens a submitted order. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string """ api_key = api_key or self.default_api_key # Prefer passed in stripe_account if set. if not stripe_account: stripe_account = self._get_stripe_account_id(api_key) return self.stripe_class.reopen( self.id, api_key=api_key, stripe_account=stripe_account, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ) def submit(self, api_key=None, stripe_account=None, **kwargs): """ Submitting an Order transitions the status to processing and creates a PaymentIntent object so the order can be paid. If the Order has an amount_total of 0, no PaymentIntent object will be created. Once the order is submitted, its contents cannot be changed, unless the reopen method is called. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string :param stripe_account: The optional connected account \ for which this request is being made. :type stripe_account: string """ api_key = api_key or self.default_api_key # Prefer passed in stripe_account if set. if not stripe_account: stripe_account = self._get_stripe_account_id(api_key) return self.stripe_class.submit( self.id, api_key=api_key, stripe_account=stripe_account, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ) ================================================ FILE: djstripe/models/payment_methods.py ================================================ from typing import Optional, Union import stripe from django.db import models, transaction from stripe.error import InvalidRequestError from .. import enums from ..exceptions import ImpossibleAPIRequest, StripeObjectManipulationException from ..fields import ( JSONField, StripeCurrencyCodeField, StripeDecimalCurrencyAmountField, StripeEnumField, StripeForeignKey, ) from ..settings import djstripe_settings from ..utils import get_id_from_stripe_data from .account import Account from .base import StripeModel, logger from .core import Customer class DjstripePaymentMethod(models.Model): """ An internal model that abstracts the legacy Card and BankAccount objects with Source objects. Contains two fields: `id` and `type`: - `id` is the id of the Stripe object. - `type` can be `card`, `bank_account` `account` or `source`. """ id = models.CharField(max_length=255, primary_key=True) type = models.CharField(max_length=50, db_index=True) @classmethod def from_stripe_object(cls, data): source_type = data["object"] model = cls._model_for_type(source_type) with transaction.atomic(): model.sync_from_stripe_data(data) instance, _ = cls.objects.get_or_create( id=data["id"], defaults={"type": source_type} ) return instance @classmethod def _get_or_create_source( cls, data, source_type=None, api_key=djstripe_settings.STRIPE_SECRET_KEY ): # prefer passed in source_type if not source_type: source_type = data["object"] try: model = cls._model_for_type(source_type) model._get_or_create_from_stripe_object(data, api_key=api_key) except ValueError as e: # This may happen if we have source types we don't know about. # Let's not make dj-stripe entirely unusable if that happens. logger.warning("Could not sync source of type %r: %s", source_type, e) return cls.objects.get_or_create(id=data["id"], defaults={"type": source_type}) @classmethod def _model_for_type(cls, type): if type == "card": return Card elif type == "source": return Source elif type == "bank_account": return BankAccount elif type == "account": return Account raise ValueError(f"Unknown source type: {type}") @property def object_model(self): return self._model_for_type(self.type) def resolve(self): return self.object_model.objects.get(id=self.id) @classmethod def _get_or_create_from_stripe_object( cls, data, field_name="id", refetch=True, current_ids=None, pending_relations=None, save=True, stripe_account=None, api_key=djstripe_settings.STRIPE_SECRET_KEY, ): raw_field_data = data.get(field_name) id_ = get_id_from_stripe_data(raw_field_data) if not id_: raise ValueError(f"ID not found in Stripe data: {raw_field_data!r}") if id_.startswith("card"): source_cls = Card source_type = "card" elif id_.startswith("src"): source_cls = Source source_type = "source" elif id_.startswith("ba"): source_cls = BankAccount source_type = "bank_account" elif id_.startswith("acct"): source_cls = Account source_type = "account" else: # This may happen if we have source types we don't know about. # Let's not make dj-stripe entirely unusable if that happens. logger.warning(f"Unknown Object. Could not sync source with id: {id_}") return cls.objects.get_or_create( id=id_, defaults={"type": f"UNSUPPORTED_{id_}"} ) # call model's _get_or_create_from_stripe_object to ensure # that object exists before getting or creating its source object source_cls._get_or_create_from_stripe_object( data, field_name, refetch=refetch, current_ids=current_ids, pending_relations=pending_relations, stripe_account=stripe_account, api_key=api_key, ) return cls.objects.get_or_create(id=id_, defaults={"type": source_type}) class LegacySourceMixin: """ Mixin for functionality shared between the legacy Card & BankAccount sources """ customer: Optional[StripeForeignKey] account: Optional[StripeForeignKey] id: str default_api_key: str @classmethod def _get_customer_or_account_from_kwargs(cls, **kwargs): account = kwargs.get("account") customer = kwargs.get("customer") if not account and not customer: raise StripeObjectManipulationException( f"{cls.__name__} objects must be manipulated through either a " "Stripe Connected Account or a customer. " "Pass a Customer or an Account object into this call." ) if account and not isinstance(account, Account): raise StripeObjectManipulationException( f"{cls.__name__} objects must be manipulated through a Stripe Connected Account. " "Pass an Account object into this call." ) if customer and not isinstance(customer, Customer): raise StripeObjectManipulationException( f"{cls.__name__} objects must be manipulated through a Customer. " "Pass a Customer object into this call." ) if account: del kwargs["account"] if customer: del kwargs["customer"] return account, customer, kwargs @classmethod def _api_create(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs): # OVERRIDING the parent version of this function # Cards & Bank Accounts must be manipulated through a customer or account. account, customer, clean_kwargs = cls._get_customer_or_account_from_kwargs( **kwargs ) # First we try to retrieve by customer attribute, # then by account attribute if customer and account: try: # retrieve by customer return customer.api_retrieve(api_key=api_key).sources.create( api_key=api_key, **clean_kwargs ) except Exception as customer_exc: try: # retrieve by account return account.api_retrieve( api_key=api_key ).external_accounts.create(api_key=api_key, **clean_kwargs) except Exception: raise customer_exc if customer: return customer.api_retrieve(api_key=api_key).sources.create( api_key=api_key, **clean_kwargs ) if account: return account.api_retrieve(api_key=api_key).external_accounts.create( api_key=api_key, **clean_kwargs ) @classmethod def api_list(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs): # OVERRIDING the parent version of this function # Cards & Bank Accounts must be manipulated through a customer or account. account, customer, clean_kwargs = cls._get_customer_or_account_from_kwargs( **kwargs ) object_name = cls.stripe_class.OBJECT_NAME # First we try to retrieve by customer attribute, # then by account attribute if customer and account: try: # retrieve by customer return ( customer.api_retrieve(api_key=api_key) .sources.list(object=object_name, **clean_kwargs) .auto_paging_iter() ) except Exception as customer_exc: try: # retrieve by account return ( account.api_retrieve(api_key=api_key) .external_accounts.list(object=object_name, **clean_kwargs) .auto_paging_iter() ) except Exception: raise customer_exc if customer: return ( customer.api_retrieve(api_key=api_key) .sources.list(object=object_name, **clean_kwargs) .auto_paging_iter() ) if account: return ( account.api_retrieve(api_key=api_key) .external_accounts.list(object=object_name, **clean_kwargs) .auto_paging_iter() ) raise ImpossibleAPIRequest( f"Can't list {object_name} without a customer or account object." " This may happen if not all accounts or customer objects are in the db." ' Please run "python manage.py djstripe_sync_models Account Customer" as a potential fix.' ) def get_stripe_dashboard_url(self) -> str: if self.customer: return self.customer.get_stripe_dashboard_url() elif self.account: return f"https://dashboard.stripe.com/{self.account.id}/settings/payouts" else: return "" def remove(self): """ Removes a legacy source from this customer's account. """ # First, wipe default source on all customers that use this card. Customer.objects.filter(default_source=self.id).update(default_source=None) try: self._api_delete() except InvalidRequestError as exc: if "No such source:" in str(exc) or "No such customer:" in str(exc): # The exception was thrown because the stripe customer or card # was already deleted on the stripe side, ignore the exception pass else: # The exception was raised for another reason, re-raise it raise self.delete() def api_retrieve(self, api_key=None, stripe_account=None): # OVERRIDING the parent version of this function # Cards & Banks Accounts must be manipulated through a customer or account. api_key = api_key or self.default_api_key if self.customer: return stripe.Customer.retrieve_source( self.customer.id, self.id, expand=self.expand_fields, stripe_account=stripe_account, api_key=api_key, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) # try to retrieve by account attribute if retrieval by customer fails. if self.account: return stripe.Account.retrieve_external_account( self.account.id, self.id, expand=self.expand_fields, stripe_account=stripe_account, api_key=api_key, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) raise ImpossibleAPIRequest( f"Can't retrieve {self.__class__} without a customer or account object." " This may happen if not all accounts or customer objects are in the db." ' Please run "python manage.py djstripe_sync_models Account Customer" as a potential fix.' ) def _api_delete(self, api_key=None, stripe_account=None, **kwargs): # OVERRIDING the parent version of this function # Cards & Banks Accounts must be manipulated through a customer or account. api_key = api_key or self.default_api_key # Prefer passed in stripe_account if set. if not stripe_account: stripe_account = self._get_stripe_account_id(api_key) if self.customer: return stripe.Customer.delete_source( self.customer.id, self.id, api_key=api_key, stripe_account=stripe_account, **kwargs, ) if self.account: return stripe.Account.delete_external_account( self.account.id, self.id, api_key=api_key, stripe_account=stripe_account, **kwargs, ) raise ImpossibleAPIRequest( f"Can't delete {self.__class__} without a customer or account object." " This may happen if not all accounts or customer objects are in the db." ' Please run "python manage.py djstripe_sync_models Account Customer" as a potential fix.' ) class BankAccount(LegacySourceMixin, StripeModel): """ These bank accounts are payment methods on Customer objects. On the other hand External Accounts are transfer destinations on Account objects for Custom accounts. They can be bank accounts or debit cards as well. Stripe documentation:https://stripe.com/docs/api/customer_bank_accounts """ stripe_class = stripe.BankAccount account = StripeForeignKey( "Account", on_delete=models.PROTECT, null=True, blank=True, related_name="bank_accounts", help_text="The external account the charge was made on behalf of. Null here indicates " "that this value was never set.", ) account_holder_name = models.TextField( max_length=5000, blank=True, help_text="The name of the person or business that owns the bank account.", ) account_holder_type = StripeEnumField( enum=enums.BankAccountHolderType, help_text="The type of entity that holds the account.", ) bank_name = models.CharField( max_length=255, help_text="Name of the bank associated with the routing number " "(e.g., `WELLS FARGO`).", ) country = models.CharField( max_length=2, help_text="Two-letter ISO code representing the country the bank account " "is located in.", ) currency = StripeCurrencyCodeField() customer = StripeForeignKey( "Customer", on_delete=models.SET_NULL, null=True, related_name="bank_account" ) default_for_currency = models.BooleanField( null=True, help_text="Whether this external account (BankAccount) is the default account for " "its currency.", ) fingerprint = models.CharField( max_length=16, help_text=( "Uniquely identifies this particular bank account. " "You can use this attribute to check whether two bank accounts are " "the same." ), ) last4 = models.CharField(max_length=4) routing_number = models.CharField( max_length=255, help_text="The routing transit number for the bank account." ) status = StripeEnumField(enum=enums.BankAccountStatus) def __str__(self): default = False # prefer to show it by customer format if present if self.customer: default_source = self.customer.default_source default_payment_method = self.customer.default_payment_method if (default_payment_method and self.id == default_payment_method.id) or ( default_source and self.id == default_source.id ): # current card is the default payment method or source default = True customer_template = f"{self.bank_name} {self.routing_number} ({self.human_readable_status}) {'Default' if default else ''} {self.currency}" return customer_template default = getattr(self, "default_for_currency", False) account_template = f"{self.bank_name} {self.currency} {'Default' if default else ''} {self.routing_number} {self.last4}" return account_template @property def human_readable_status(self): if self.status == "new": return "Pending Verification" return enums.BankAccountStatus.humanize(self.status) def api_retrieve(self, **kwargs): if not self.customer and not self.account: raise ImpossibleAPIRequest( "Can't retrieve a bank account without a customer or account object." " This may happen if not all accounts or customer objects are in the db." ' Please run "python manage.py djstripe_sync_models Account Customer" as a potential fix.' ) return super().api_retrieve(**kwargs) class Card(LegacySourceMixin, StripeModel): """ You can store multiple cards on a customer in order to charge the customer later. This is a legacy model which only applies to the "v2" Stripe API (eg. Checkout.js). You should strive to use the Stripe "v3" API (eg. Stripe Elements). Also see: https://stripe.com/docs/stripe-js/elements/migrating When using Elements, you will not be using Card objects. Instead, you will use Source objects. A Source object of type "card" is equivalent to a Card object. However, Card objects cannot be converted into Source objects by Stripe at this time. Stripe documentation: https://stripe.com/docs/api?lang=python#cards """ stripe_class = stripe.Card # Stripe Custom Connected Accounts can have cards as "Payout Sources" account = StripeForeignKey( "Account", on_delete=models.PROTECT, null=True, blank=True, related_name="cards", help_text="The external account the charge was made on behalf of. Null here indicates " "that this value was never set.", ) address_city = models.TextField( max_length=5000, blank=True, default="", help_text="City/District/Suburb/Town/Village.", ) address_country = models.TextField( max_length=5000, blank=True, default="", help_text="Billing address country." ) address_line1 = models.TextField( max_length=5000, blank=True, default="", help_text="Street address/PO Box/Company name.", ) address_line1_check = StripeEnumField( enum=enums.CardCheckResult, blank=True, default="", help_text="If `address_line1` was provided, results of the check.", ) address_line2 = models.TextField( max_length=5000, blank=True, default="", help_text="Apartment/Suite/Unit/Building.", ) address_state = models.TextField( max_length=5000, blank=True, default="", help_text="State/County/Province/Region.", ) address_zip = models.TextField( max_length=5000, blank=True, default="", help_text="ZIP or postal code." ) address_zip_check = StripeEnumField( enum=enums.CardCheckResult, blank=True, default="", help_text="If `address_zip` was provided, results of the check.", ) brand = StripeEnumField(enum=enums.CardBrand, help_text="Card brand.") country = models.CharField( max_length=2, default="", blank=True, help_text="Two-letter ISO code representing the country of the card.", ) customer = StripeForeignKey( "Customer", on_delete=models.SET_NULL, null=True, related_name="legacy_cards" ) cvc_check = StripeEnumField( enum=enums.CardCheckResult, default="", blank=True, help_text="If a CVC was provided, results of the check.", ) default_for_currency = models.BooleanField( null=True, help_text="Whether this external account (Card) is the default account for " "its currency.", ) dynamic_last4 = models.CharField( max_length=4, default="", blank=True, help_text="(For tokenized numbers only.) The last four digits of the device " "account number.", ) exp_month = models.IntegerField(help_text="Card expiration month.") exp_year = models.IntegerField(help_text="Card expiration year.") fingerprint = models.CharField( default="", blank=True, max_length=16, help_text="Uniquely identifies this particular card number.", ) funding = StripeEnumField( enum=enums.CardFundingType, help_text="Card funding type." ) last4 = models.CharField(max_length=4, help_text="Last four digits of Card number.") name = models.TextField( max_length=5000, default="", blank=True, help_text="Cardholder name." ) tokenization_method = StripeEnumField( enum=enums.CardTokenizationMethod, default="", blank=True, help_text="If the card number is tokenized, this is the method that was used.", ) def __str__(self): default = False # prefer to show it by customer format if present if self.customer: default_source = self.customer.default_source default_payment_method = self.customer.default_payment_method if (default_payment_method and self.id == default_payment_method.id) or ( default_source and self.id == default_source.id ): # current card is the default payment method or source default = True customer_template = f"{enums.CardBrand.humanize(self.brand)} {self.last4} {'Default' if default else ''} Expires {self.exp_month} {self.exp_year}" return customer_template elif self.account: default = getattr(self, "default_for_currency", False) account_template = f"{enums.CardBrand.humanize(self.brand)} {self.account.default_currency} {'Default' if default else ''} {self.last4}" return account_template return self.id or "" @classmethod def create_token( cls, number: str, exp_month: int, exp_year: int, cvc: str, api_key: str = djstripe_settings.STRIPE_SECRET_KEY, **kwargs, ) -> stripe.Token: """ Creates a single use token that wraps the details of a credit card. This token can be used in place of a credit card dictionary with any API method. These tokens can only be used once: by creating a new charge object, or attaching them to a customer. (Source: https://stripe.com/docs/api?lang=python#create_card_token) :param number: The card number without any separators (no spaces) :param exp_month: The card's expiration month. (two digits) :param exp_year: The card's expiration year. (four digits) :param cvc: Card security code. :param api_key: The API key to use """ card = { "number": number, "exp_month": exp_month, "exp_year": exp_year, "cvc": cvc, } card.update(kwargs) return stripe.Token.create(api_key=api_key, card=card) class Source(StripeModel): """ Source objects allow you to accept a variety of payment methods. They represent a customer's payment instrument, and can be used with the Stripe API just like a Card object: once chargeable, they can be charged, or can be attached to customers. Stripe documentation: https://stripe.com/docs/api?lang=python#sources """ amount = StripeDecimalCurrencyAmountField( null=True, blank=True, help_text=( "Amount (as decimal) associated with the source. " "This is the amount for which the source will be chargeable once ready. " "Required for `single_use` sources." ), ) client_secret = models.CharField( max_length=255, help_text=( "The client secret of the source. " "Used for client-side retrieval using a publishable key." ), ) currency = StripeCurrencyCodeField(default="", blank=True) flow = StripeEnumField( enum=enums.SourceFlow, help_text="The authentication flow of the source." ) owner = JSONField( help_text=( "Information about the owner of the payment instrument that may be " "used or required by particular source types." ) ) statement_descriptor = models.CharField( max_length=255, default="", blank=True, help_text="Extra information about a source. This will appear on your " "customer's statement every time you charge the source.", ) status = StripeEnumField( enum=enums.SourceStatus, help_text="The status of the source. Only `chargeable` sources can be used " "to create a charge.", ) type = StripeEnumField(enum=enums.SourceType, help_text="The type of the source.") usage = StripeEnumField( enum=enums.SourceUsage, help_text="Whether this source should be reusable or not. " "Some source types may or may not be reusable by construction, " "while other may leave the option at creation.", ) # Flows code_verification = JSONField( null=True, blank=True, help_text="Information related to the code verification flow. " "Present if the source is authenticated by a verification code " "(`flow` is `code_verification`).", ) receiver = JSONField( null=True, blank=True, help_text="Information related to the receiver flow. " "Present if the source is a receiver (`flow` is `receiver`).", ) redirect = JSONField( null=True, blank=True, help_text="Information related to the redirect flow. " "Present if the source is authenticated by a redirect (`flow` is `redirect`).", ) source_data = JSONField(help_text="The data corresponding to the source type.") customer = StripeForeignKey( "Customer", on_delete=models.SET_NULL, null=True, blank=True, related_name="sources", ) stripe_class = stripe.Source stripe_dashboard_item_name = "sources" def __str__(self): return f"{self.type} {self.id}" @classmethod def _manipulate_stripe_object_hook(cls, data): # The source_data dict is an alias of all the source types data["source_data"] = data[data["type"]] return data def _attach_objects_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, current_ids=None ): customer = None # "customer" key could be like "cus_6lsBvm5rJ0zyHc" or {"id": "cus_6lsBvm5rJ0zyHc"} customer_id = get_id_from_stripe_data(data.get("customer")) if current_ids is None or customer_id not in current_ids: customer = cls._stripe_object_to_customer( target_cls=Customer, data=data, current_ids=current_ids, api_key=api_key ) if customer: self.customer = customer else: self.customer = None def detach(self) -> bool: """ Detach the source from its customer. """ # First, wipe default source on all customers that use this. Customer.objects.filter(default_source=self.id).update(default_source=None) api_key = self.default_api_key try: # TODO - we could use the return value of sync_from_stripe_data # or call its internals - self._sync/_attach_objects_hook etc here # to update `self` at this point? self.sync_from_stripe_data( self.api_retrieve(api_key=api_key).detach(), api_key=api_key ) return True except InvalidRequestError: # The source was already detached. Resyncing. self.sync_from_stripe_data( self.api_retrieve(api_key=self.default_api_key), api_key=self.default_api_key, ) return False @classmethod def api_list(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs): """ Call the stripe API's list operation for this model. :param api_key: The api key to use for this request. \ Defaults to djstripe_settings.STRIPE_SECRET_KEY. :type api_key: string See Stripe documentation for accepted kwargs for each object. :returns: an iterator over all items in the query """ return Customer.stripe_class.list_sources( object="source", api_key=api_key, stripe_version=djstripe_settings.STRIPE_API_VERSION, **kwargs, ).auto_paging_iter() class PaymentMethod(StripeModel): """ PaymentMethod objects represent your customer's payment instruments. You can use them with PaymentIntents to collect payments or save them to Customer objects to store instrument details for future payments. Stripe documentation: https://stripe.com/docs/api?lang=python#payment_methods """ stripe_class = stripe.PaymentMethod description = None billing_details = JSONField( help_text=( "Billing information associated with the PaymentMethod that may be used or " "required by particular types of payment methods." ) ) customer = StripeForeignKey( "Customer", on_delete=models.SET_NULL, null=True, blank=True, related_name="payment_methods", help_text=( "Customer to which this PaymentMethod is saved. " "This will not be set when the PaymentMethod has " "not been saved to a Customer." ), ) type = StripeEnumField( enum=enums.PaymentMethodType, help_text="The type of the PaymentMethod.", ) acss_debit = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `acss_debit`", ) affirm = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `affirm`", ) afterpay_clearpay = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `afterpay_clearpay`", ) alipay = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `alipay`", ) au_becs_debit = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `au_becs_debit`", ) bacs_debit = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `bacs_debit`", ) bancontact = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `bancontact`", ) blik = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `blik`", ) boleto = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `boleto`", ) card = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `card`", ) card_present = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `card_present`", ) customer_balance = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `customer_balance`", ) eps = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `eps`", ) fpx = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `fpx`", ) giropay = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `giropay`", ) grabpay = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `grabpay`", ) ideal = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `ideal`", ) interac_present = JSONField( null=True, blank=True, help_text=( "Additional information for payment methods of type `interac_present`" ), ) klarna = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `klarna`", ) konbini = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `konbini`", ) link = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `link`", ) oxxo = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `oxxo`", ) p24 = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `p24`", ) paynow = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `paynow`", ) pix = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `pix`", ) promptpay = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `promptpay`", ) sepa_debit = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `sepa_debit`", ) sofort = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `sofort`", ) us_bank_account = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `us_bank_account`", ) wechat_pay = JSONField( null=True, blank=True, help_text="Additional information for payment methods of type `wechat_pay`", ) def __str__(self): if self.customer: return f"{enums.PaymentMethodType.humanize(self.type)} for {self.customer}" return f"{enums.PaymentMethodType.humanize(self.type)} is not associated with any customer" def get_stripe_dashboard_url(self) -> str: if self.customer: return self.customer.get_stripe_dashboard_url() return "" def _attach_objects_hook( self, cls, data, api_key=djstripe_settings.STRIPE_SECRET_KEY, current_ids=None ): customer = None # "customer" key could be like "cus_6lsBvm5rJ0zyHc" or {"id": "cus_6lsBvm5rJ0zyHc"} customer_id = get_id_from_stripe_data(data.get("customer")) if current_ids is None or customer_id not in current_ids: customer = cls._stripe_object_to_customer( target_cls=Customer, data=data, current_ids=current_ids, api_key=api_key ) if customer: self.customer = customer else: self.customer = None @classmethod def attach( cls, payment_method: Union[str, "PaymentMethod"], customer: Union[str, Customer], api_key: str = djstripe_settings.STRIPE_SECRET_KEY, ) -> "PaymentMethod": """ Attach a payment method to a customer """ if isinstance(payment_method, StripeModel): payment_method = payment_method.id if isinstance(customer, StripeModel): customer = customer.id extra_kwargs = {} if not isinstance(payment_method, stripe.PaymentMethod): # send api_key if we're not passing in a Stripe object # avoids "Received unknown parameter: api_key" since api uses the # key cached in the Stripe object extra_kwargs = {"api_key": api_key} stripe_payment_method = stripe.PaymentMethod.attach( payment_method, customer=customer, **extra_kwargs ) return cls.sync_from_stripe_data(stripe_payment_method, api_key=api_key) def detach(self): """ Detach the payment method from its customer. :return: Returns true if the payment method was newly detached, \ false if it was already detached :rtype: bool """ # Find customers that use this customers = Customer.objects.filter(default_payment_method=self).all() changed = True # special handling is needed for legacy "card"-type PaymentMethods, # since detaching them deletes them within Stripe. # see https://github.com/dj-stripe/dj-stripe/pull/967 is_legacy_card = self.id.startswith("card_") try: self.sync_from_stripe_data(self.api_retrieve().detach()) # resync customer to update .default_payment_method and # .invoice_settings.default_payment_method for customer in customers: Customer.sync_from_stripe_data(customer.api_retrieve()) except (InvalidRequestError,): # The source was already detached. Resyncing. if self.pk and not is_legacy_card: self.sync_from_stripe_data(self.api_retrieve()) changed = False if self.pk: if is_legacy_card: self.delete() else: self.refresh_from_db() return changed ================================================ FILE: djstripe/models/sigma.py ================================================ import stripe from django.db import models from .. import enums from ..fields import JSONField, StripeDateTimeField, StripeEnumField, StripeForeignKey from .base import StripeModel # TODO Add Tests class ScheduledQueryRun(StripeModel): """ Stripe documentation: https://stripe.com/docs/api?lang=python#scheduled_queries """ stripe_class = stripe.sigma.ScheduledQueryRun data_load_time = StripeDateTimeField( help_text="When the query was run, Sigma contained a snapshot of your " "Stripe data at this time." ) error = JSONField( null=True, blank=True, help_text="If the query run was not succeesful, contains information " "about the failure.", ) file = StripeForeignKey( "file", on_delete=models.SET_NULL, null=True, blank=True, help_text="The file object representing the results of the query.", ) result_available_until = StripeDateTimeField( help_text="Time at which the result expires and is no longer available " "for download." ) sql = models.TextField(max_length=5000, help_text="SQL for the query.") status = StripeEnumField( enum=enums.ScheduledQueryRunStatus, help_text="The query's execution status." ) title = models.TextField(max_length=5000, help_text="Title of the query.") # TODO Write corresponding test def __str__(self): return f"{self.title or self.id} ({self.status})" ================================================ FILE: djstripe/models/webhooks.py ================================================ """ Module for dj-stripe Webhook models """ import json import warnings from traceback import format_exc from uuid import uuid4 import stripe from django.db import models from django.utils.datastructures import CaseInsensitiveMapping from django.utils.functional import cached_property from .. import signals from ..enums import WebhookEndpointStatus from ..fields import JSONField, StripeEnumField, StripeForeignKey from ..settings import djstripe_settings from .base import StripeModel, logger from .core import Event # TODO: Add Tests class WebhookEndpoint(StripeModel): stripe_class = stripe.WebhookEndpoint stripe_dashboard_item_name = "webhooks" api_version = models.CharField( max_length=64, blank=True, help_text="The API version events are rendered as for this webhook endpoint. Defaults to the configured Stripe API Version.", ) enabled_events = JSONField( help_text=( "The list of events to enable for this endpoint. " "['*'] indicates that all events are enabled, except those that require explicit selection." ) ) secret = models.CharField( max_length=256, blank=True, editable=False, help_text="The endpoint's secret, used to generate webhook signatures.", ) status = StripeEnumField( enum=WebhookEndpointStatus, help_text="The status of the webhook. It can be enabled or disabled.", ) url = models.URLField(help_text="The URL of the webhook endpoint.", max_length=2048) application = models.CharField( max_length=255, blank=True, help_text="The ID of the associated Connect application.", ) djstripe_uuid = models.UUIDField( null=True, unique=True, default=uuid4, help_text="A UUID specific to dj-stripe generated for the endpoint", ) def __str__(self): return self.url or str(self.djstripe_uuid) def _attach_objects_hook( self, cls, data, current_ids=None, api_key=djstripe_settings.STRIPE_SECRET_KEY ): """ Gets called by this object's create and sync methods just before save. Use this to populate fields before the model is saved. """ super()._attach_objects_hook( cls, data, current_ids=current_ids, api_key=api_key ) self.djstripe_uuid = data.get("metadata", {}).get("djstripe_uuid") def _get_version(): from ..apps import __version__ return __version__ def get_remote_ip(request): """Given the HTTPRequest object return the IP Address of the client :param request: client request :type request: HTTPRequest :Returns: the client ip address """ # x-forwarded-for is relevant for django running behind a proxy x_forwarded_for = request.headers.get("x-forwarded-for") if x_forwarded_for: ip = x_forwarded_for.split(",")[0] else: ip = request.META.get("REMOTE_ADDR") if not ip: warnings.warn( "Could not determine remote IP (missing REMOTE_ADDR). " "This is likely an issue with your wsgi/server setup." ) ip = "0.0.0.0" return ip class WebhookEventTrigger(models.Model): """ An instance of a request that reached the server endpoint for Stripe webhooks. Webhook Events are initially **UNTRUSTED**, as it is possible for any web entity to post any data to our webhook url. Data posted may be valid Stripe information, garbage, or even malicious. The 'valid' flag in this model monitors this. """ id = models.BigAutoField(primary_key=True) remote_ip = models.GenericIPAddressField( help_text="IP address of the request client." ) headers = JSONField() body = models.TextField(blank=True) valid = models.BooleanField( default=False, help_text="Whether or not the webhook event has passed validation", ) processed = models.BooleanField( default=False, help_text="Whether or not the webhook event has been successfully processed", ) exception = models.CharField(max_length=128, blank=True) traceback = models.TextField( blank=True, help_text="Traceback if an exception was thrown during processing" ) event = StripeForeignKey( "Event", on_delete=models.SET_NULL, null=True, blank=True, help_text="Event object contained in the (valid) Webhook", ) djstripe_version = models.CharField( max_length=32, default=_get_version, # Needs to be a callable, otherwise it's a db default. help_text="The version of dj-stripe when the webhook was received", ) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) stripe_trigger_account = StripeForeignKey( "djstripe.Account", on_delete=models.CASCADE, to_field="id", null=True, blank=True, help_text="The Stripe Account this object belongs to.", ) webhook_endpoint = StripeForeignKey( "WebhookEndpoint", on_delete=models.SET_NULL, null=True, blank=True, help_text="The endpoint this webhook was received on", ) def __str__(self): return f"id={self.id}, valid={self.valid}, processed={self.processed}" @classmethod def from_request(cls, request, *, webhook_endpoint: WebhookEndpoint = None): """ Create, validate and process a WebhookEventTrigger given a Django request object. The process is three-fold: 1. Create a WebhookEventTrigger object from a Django request. 2. Validate the WebhookEventTrigger as a Stripe event using the API. 3. If valid, process it into an Event object (and child resource). """ try: body = request.body.decode(request.encoding or "utf-8") except Exception: body = "(error decoding body)" ip = get_remote_ip(request) try: data = json.loads(body) except ValueError: data = {} if webhook_endpoint is None: stripe_account = StripeModel._find_owner_account(data=data) secret = djstripe_settings.WEBHOOK_SECRET else: stripe_account = webhook_endpoint.djstripe_owner_account secret = webhook_endpoint.secret obj = cls.objects.create( headers=dict(request.headers), body=body, remote_ip=ip, stripe_trigger_account=stripe_account, webhook_endpoint=webhook_endpoint, ) api_key = ( stripe_account.default_api_key or djstripe_settings.get_default_api_key(obj.livemode) ) try: # Validate the webhook first signals.webhook_pre_validate.send(sender=cls, instance=obj) obj.valid = obj.validate(secret=secret, api_key=api_key) signals.webhook_post_validate.send( sender=cls, instance=obj, valid=obj.valid ) if obj.valid: signals.webhook_pre_process.send(sender=cls, instance=obj) if djstripe_settings.WEBHOOK_EVENT_CALLBACK: # If WEBHOOK_EVENT_CALLBACK, pass it for processing djstripe_settings.WEBHOOK_EVENT_CALLBACK(obj, api_key=api_key) else: # Process the item (do not save it, it'll get saved below) obj.process(save=False, api_key=api_key) signals.webhook_post_process.send( sender=cls, instance=obj, api_key=api_key ) except Exception as e: max_length = cls._meta.get_field("exception").max_length obj.exception = str(e)[:max_length] obj.traceback = format_exc() # Send the exception as the webhook_processing_error signal signals.webhook_processing_error.send( sender=cls, instance=obj, api_key=api_key, exception=e, data=getattr(e, "http_body", ""), ) # re-raise the exception so Django sees it raise e finally: obj.save() return obj @cached_property def json_body(self): try: return json.loads(self.body) except ValueError: return {} @property def is_test_event(self): event_id = self.json_body.get("id") return event_id and event_id.endswith("_00000000000000") def verify_signature( self, secret: str, tolerance: int = djstripe_settings.WEBHOOK_TOLERANCE ) -> bool: if not secret: raise ValueError("Cannot verify event signature without a secret") # HTTP headers are case-insensitive, but we store them as a dict. headers = CaseInsensitiveMapping(self.headers) signature = headers.get("stripe-signature") local_cli_signing_secret = headers.get("x-djstripe-webhook-secret") try: # check if the x-djstripe-webhook-secret Custom Header exists if local_cli_signing_secret: # Set Endpoint Signing Secret to the output of Stripe CLI # for signature verification secret = local_cli_signing_secret stripe.WebhookSignature.verify_header( self.body, signature, secret, tolerance ) except stripe.error.SignatureVerificationError: logger.exception("Failed to verify header") return False else: return True def validate( self, api_key: str = None, secret: str = djstripe_settings.WEBHOOK_SECRET, tolerance: int = djstripe_settings.WEBHOOK_TOLERANCE, validation_method=djstripe_settings.WEBHOOK_VALIDATION, ): """ The original contents of the Event message must be confirmed by refetching it and comparing the fetched data with the original data. This function makes an API call to Stripe to redownload the Event data and returns whether or not it matches the WebhookEventTrigger data. """ local_data = self.json_body if "id" not in local_data or "livemode" not in local_data: logger.error( '"id" not in json body or "livemode" not in json body(%s)', local_data ) return False if self.is_test_event: logger.info("Test webhook received and discarded: %s", local_data) return False if validation_method is None: # validation disabled warnings.warn("WEBHOOK VALIDATION is disabled.") return True elif validation_method == "verify_signature": return self.verify_signature(secret=secret) livemode = local_data["livemode"] api_key = api_key or djstripe_settings.get_default_api_key(livemode) # Retrieve the event using the api_version specified in itself remote_data = Event.stripe_class.retrieve( id=local_data["id"], api_key=api_key, stripe_version=local_data["api_version"], ) return local_data["data"] == remote_data["data"] def process(self, save=True, api_key: str = None): # Reset traceback and exception in case of reprocessing self.exception = "" self.traceback = "" self.event = Event.process(self.json_body, api_key=api_key) self.processed = True if save: self.save() return self.event ================================================ FILE: djstripe/settings.py ================================================ """ dj-stripe settings """ import stripe from django.apps import apps as django_apps from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.utils.module_loading import import_string class DjstripeSettings: """Container for Dj-stripe settings :return: Initialised settings for Dj-stripe. :rtype: object """ DEFAULT_STRIPE_API_VERSION = "2020-08-27" ZERO_DECIMAL_CURRENCIES = { "bif", "clp", "djf", "gnf", "jpy", "kmf", "krw", "mga", "pyg", "rwf", "vnd", "vuv", "xaf", "xof", "xpf", } def __init__(self): # Set STRIPE_API_HOST if you want to use a different Stripe API server # Example: https://github.com/stripe/stripe-mock if hasattr(settings, "STRIPE_API_HOST"): stripe.api_base = getattr(settings, "STRIPE_API_HOST") # generic setter and deleter methods to ensure object patching works def __setattr__(self, name, value): self.__dict__[name] = value def __delattr__(self, name): del self.__dict__[name] @property def subscriber_request_callback(self): return self.get_callback_function( "DJSTRIPE_SUBSCRIBER_MODEL_REQUEST_CALLBACK", default=(lambda request: request.user), ) @property def get_idempotency_key(self): return self.get_callback_function( "DJSTRIPE_IDEMPOTENCY_KEY_CALLBACK", self._get_idempotency_key ) @property def DJSTRIPE_WEBHOOK_URL(self): return getattr(settings, "DJSTRIPE_WEBHOOK_URL", r"^webhook/$") @property def WEBHOOK_TOLERANCE(self): return getattr( settings, "DJSTRIPE_WEBHOOK_TOLERANCE", stripe.Webhook.DEFAULT_TOLERANCE ) @property def WEBHOOK_VALIDATION(self): return getattr(settings, "DJSTRIPE_WEBHOOK_VALIDATION", "verify_signature") @property def WEBHOOK_SECRET(self): return getattr(settings, "DJSTRIPE_WEBHOOK_SECRET", "") # Webhook event callbacks allow an application to take control of what happens # when an event from Stripe is received. One suggestion is to put the event # onto a task queue (such as celery) for asynchronous processing. @property def WEBHOOK_EVENT_CALLBACK(self): return self.get_callback_function("DJSTRIPE_WEBHOOK_EVENT_CALLBACK") @property def SUBSCRIBER_CUSTOMER_KEY(self): return getattr( settings, "DJSTRIPE_SUBSCRIBER_CUSTOMER_KEY", "djstripe_subscriber" ) @property def TEST_API_KEY(self): return getattr(settings, "STRIPE_TEST_SECRET_KEY", "") @property def LIVE_API_KEY(self): return getattr(settings, "STRIPE_LIVE_SECRET_KEY", "") # Determines whether we are in live mode or test mode @property def STRIPE_LIVE_MODE(self): return getattr(settings, "STRIPE_LIVE_MODE", False) @property def STRIPE_SECRET_KEY(self): # Default secret key if hasattr(settings, "STRIPE_SECRET_KEY"): STRIPE_SECRET_KEY = settings.STRIPE_SECRET_KEY else: STRIPE_SECRET_KEY = ( self.LIVE_API_KEY if self.STRIPE_LIVE_MODE else self.TEST_API_KEY ) return STRIPE_SECRET_KEY @property def STRIPE_PUBLIC_KEY(self): # Default public key if hasattr(settings, "STRIPE_PUBLIC_KEY"): STRIPE_PUBLIC_KEY = settings.STRIPE_PUBLIC_KEY elif self.STRIPE_LIVE_MODE: STRIPE_PUBLIC_KEY = getattr(settings, "STRIPE_LIVE_PUBLIC_KEY", "") else: STRIPE_PUBLIC_KEY = getattr(settings, "STRIPE_TEST_PUBLIC_KEY", "") return STRIPE_PUBLIC_KEY @property def STRIPE_API_VERSION(self) -> str: """ Get the desired API version to use for Stripe requests. """ version = getattr(settings, "STRIPE_API_VERSION", stripe.api_version) return version or self.DEFAULT_STRIPE_API_VERSION def get_callback_function(self, setting_name, default=None): """ Resolve a callback function based on a setting name. If the setting value isn't set, default is returned. If the setting value is already a callable function, that value is used - If the setting value is a string, an attempt is made to import it. Anything else will result in a failed import causing ImportError to be raised. :param setting_name: The name of the setting to resolve a callback from. :type setting_name: string (``str``/``unicode``) :param default: The default to return if setting isn't populated. :type default: ``bool`` :returns: The resolved callback function (if any). :type: ``callable`` """ func = getattr(settings, setting_name, None) if not func: return default if callable(func): return func if isinstance(func, str): func = import_string(func) if not callable(func): raise ImproperlyConfigured(f"{setting_name} must be callable.") return func def _get_idempotency_key(self, object_type, action, livemode) -> str: from .models import IdempotencyKey action = f"{object_type}:{action}" idempotency_key, _created = IdempotencyKey.objects.get_or_create( action=action, livemode=livemode ) return str(idempotency_key.uuid) def get_default_api_key(self, livemode) -> str: """ Returns the default API key for a value of `livemode`. """ if livemode is None: # Livemode is unknown. Use the default secret key. return self.STRIPE_SECRET_KEY elif livemode: # Livemode is true, use the live secret key return self.LIVE_API_KEY or self.STRIPE_SECRET_KEY else: # Livemode is false, use the test secret key return self.TEST_API_KEY or self.STRIPE_SECRET_KEY def get_subscriber_model_string(self) -> str: """Get the configured subscriber model as a module path string.""" return getattr(settings, "DJSTRIPE_SUBSCRIBER_MODEL", settings.AUTH_USER_MODEL) # type: ignore def get_subscriber_model(self): """ Attempt to pull settings.DJSTRIPE_SUBSCRIBER_MODEL. Users have the option of specifying a custom subscriber model via the DJSTRIPE_SUBSCRIBER_MODEL setting. This methods falls back to AUTH_USER_MODEL if DJSTRIPE_SUBSCRIBER_MODEL is not set. Returns the subscriber model that is active in this project. """ model_name = self.get_subscriber_model_string() # Attempt a Django 1.7 app lookup try: subscriber_model = django_apps.get_model(model_name) except ValueError: raise ImproperlyConfigured( "DJSTRIPE_SUBSCRIBER_MODEL must be of the form 'app_label.model_name'." ) except LookupError: raise ImproperlyConfigured( f"DJSTRIPE_SUBSCRIBER_MODEL refers to model '{model_name}' " "that has not been installed." ) if ( "email" not in [field_.name for field_ in subscriber_model._meta.get_fields()] ) and not hasattr(subscriber_model, "email"): raise ImproperlyConfigured( "DJSTRIPE_SUBSCRIBER_MODEL must have an email attribute." ) if model_name != settings.AUTH_USER_MODEL: # Custom user model detected. Make sure the callback is configured. func = self.get_callback_function( "DJSTRIPE_SUBSCRIBER_MODEL_REQUEST_CALLBACK" ) if not func: raise ImproperlyConfigured( "DJSTRIPE_SUBSCRIBER_MODEL_REQUEST_CALLBACK must be implemented " "if a DJSTRIPE_SUBSCRIBER_MODEL is defined." ) return subscriber_model # initialise the settings object djstripe_settings = DjstripeSettings() ================================================ FILE: djstripe/signals.py ================================================ """ signals are sent for each event Stripe sends to the app Stripe docs for Webhooks: https://stripe.com/docs/webhooks """ from django.dispatch import Signal # providing_args=["instance", "api_key"] webhook_pre_validate = Signal() # providing_args=["instance", "api_key", "valid"] webhook_post_validate = Signal() # providing_args=["instance", "api_key"] webhook_pre_process = Signal() # providing_args=["instance", "api_key"] webhook_post_process = Signal() # providing_args=["instance", "api_key", "exception", "data"] webhook_processing_error = Signal() ENABLED_EVENTS = [ # Update this by copy-pasting the "enabled_events" enum values from # https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json "*", "account.application.authorized", "account.application.deauthorized", "account.external_account.created", "account.external_account.deleted", "account.external_account.updated", "account.updated", "application_fee.created", "application_fee.refund.updated", "application_fee.refunded", "balance.available", "billing_portal.configuration.created", "billing_portal.configuration.updated", "billing_portal.session.created", "capability.updated", "cash_balance.funds_available", "charge.captured", "charge.dispute.closed", "charge.dispute.created", "charge.dispute.funds_reinstated", "charge.dispute.funds_withdrawn", "charge.dispute.updated", "charge.expired", "charge.failed", "charge.pending", "charge.refund.updated", "charge.refunded", "charge.succeeded", "charge.updated", "checkout.session.async_payment_failed", "checkout.session.async_payment_succeeded", "checkout.session.completed", "checkout.session.expired", "coupon.created", "coupon.deleted", "coupon.updated", "credit_note.created", "credit_note.updated", "credit_note.voided", "customer.created", "customer.deleted", "customer.discount.created", "customer.discount.deleted", "customer.discount.updated", "customer.source.created", "customer.source.deleted", "customer.source.expiring", "customer.source.updated", "customer.subscription.created", "customer.subscription.deleted", "customer.subscription.pending_update_applied", "customer.subscription.pending_update_expired", "customer.subscription.trial_will_end", "customer.subscription.updated", "customer.tax_id.created", "customer.tax_id.deleted", "customer.tax_id.updated", "customer.updated", "file.created", "identity.verification_session.canceled", "identity.verification_session.created", "identity.verification_session.processing", "identity.verification_session.redacted", "identity.verification_session.requires_input", "identity.verification_session.verified", "invoice.created", "invoice.deleted", "invoice.finalization_failed", "invoice.finalized", "invoice.marked_uncollectible", "invoice.paid", "invoice.payment_action_required", "invoice.payment_failed", "invoice.payment_succeeded", "invoice.sent", "invoice.upcoming", "invoice.updated", "invoice.voided", "invoiceitem.created", "invoiceitem.deleted", "invoiceitem.updated", "issuing_authorization.created", "issuing_authorization.request", "issuing_authorization.updated", "issuing_card.created", "issuing_card.updated", "issuing_cardholder.created", "issuing_cardholder.updated", "issuing_dispute.closed", "issuing_dispute.created", "issuing_dispute.funds_reinstated", "issuing_dispute.submitted", "issuing_dispute.updated", "issuing_transaction.created", "issuing_transaction.updated", "mandate.updated", "order.canceled", "order.completed", "order.created", "order.inventory_reservation_expired", "order.payment_completed", "order.payment_failed", "order.payment_succeeded", "order.processing", "order.reopened", "order.submitted", "order.updated", "order_return.created", "payment_intent.amount_capturable_updated", "payment_intent.canceled", "payment_intent.created", "payment_intent.partially_funded", "payment_intent.payment_failed", "payment_intent.processing", "payment_intent.requires_action", "payment_intent.succeeded", "payment_link.created", "payment_link.updated", "payment_method.attached", "payment_method.automatically_updated", "payment_method.detached", "payment_method.updated", "payout.canceled", "payout.created", "payout.failed", "payout.paid", "payout.updated", "person.created", "person.deleted", "person.updated", "plan.created", "plan.deleted", "plan.updated", "price.created", "price.deleted", "price.updated", "product.created", "product.deleted", "product.updated", "promotion_code.created", "promotion_code.updated", "quote.accepted", "quote.canceled", "quote.created", "quote.finalized", "radar.early_fraud_warning.created", "radar.early_fraud_warning.updated", "recipient.created", "recipient.deleted", "recipient.updated", "reporting.report_run.failed", "reporting.report_run.succeeded", "reporting.report_type.updated", "review.closed", "review.opened", "setup_intent.canceled", "setup_intent.created", "setup_intent.requires_action", "setup_intent.setup_failed", "setup_intent.succeeded", "sigma.scheduled_query_run.created", "sku.created", "sku.deleted", "sku.updated", "source.canceled", "source.chargeable", "source.failed", "source.mandate_notification", "source.refund_attributes_required", "source.transaction.created", "source.transaction.updated", "subscription_schedule.aborted", "subscription_schedule.canceled", "subscription_schedule.completed", "subscription_schedule.created", "subscription_schedule.expiring", "subscription_schedule.released", "subscription_schedule.updated", "tax_rate.created", "tax_rate.updated", "terminal.reader.action_failed", "terminal.reader.action_succeeded", "test_helpers.test_clock.advancing", "test_helpers.test_clock.created", "test_helpers.test_clock.deleted", "test_helpers.test_clock.internal_failure", "test_helpers.test_clock.ready", "topup.canceled", "topup.created", "topup.failed", "topup.reversed", "topup.succeeded", "transfer.created", "transfer.failed", "transfer.paid", "transfer.reversed", "transfer.updated", "treasury.credit_reversal.created", "treasury.credit_reversal.posted", "treasury.debit_reversal.completed", "treasury.debit_reversal.created", "treasury.debit_reversal.initial_credit_granted", "treasury.financial_account.closed", "treasury.financial_account.created", "treasury.financial_account.features_status_updated", "treasury.inbound_transfer.canceled", "treasury.inbound_transfer.created", "treasury.inbound_transfer.failed", "treasury.inbound_transfer.succeeded", "treasury.outbound_payment.canceled", "treasury.outbound_payment.created", "treasury.outbound_payment.expected_arrival_date_updated", "treasury.outbound_payment.failed", "treasury.outbound_payment.posted", "treasury.outbound_payment.returned", "treasury.outbound_transfer.canceled", "treasury.outbound_transfer.created", "treasury.outbound_transfer.expected_arrival_date_updated", "treasury.outbound_transfer.failed", "treasury.outbound_transfer.posted", "treasury.outbound_transfer.returned", "treasury.received_credit.created", "treasury.received_credit.failed", "treasury.received_credit.reversed", "treasury.received_credit.succeeded", "treasury.received_debit.created", # deprecated (no longer in events_types list) - TODO can be deleted? "checkout_beta.session_succeeded", "issuer_fraud_record.created", "payment_intent.requires_capture", "payment_method.card_automatically_updated", "issuing_dispute.created", "issuing_dispute.updated", "issuing_settlement.created", "issuing_settlement.updated", # special case? - TODO can be deleted? "ping", ] # A signal for each Event type. See https://stripe.com/docs/api/events/types WEBHOOK_SIGNALS = dict( [ # providing_args=["event"] (hook, Signal()) for hook in ENABLED_EVENTS if hook != "*" ] ) ================================================ FILE: djstripe/sync.py ================================================ """ Utility functions used for syncing data. """ from stripe.error import InvalidRequestError from .models import Customer def sync_subscriber(subscriber): """Sync a Customer with Stripe api data.""" customer, _created = Customer.get_or_create(subscriber=subscriber) try: customer.sync_from_stripe_data(customer.api_retrieve()) customer._sync_subscriptions() customer._sync_invoices() customer._sync_cards() customer._sync_charges() except InvalidRequestError as e: print("ERROR: " + str(e)) return customer ================================================ FILE: djstripe/templates/djstripe/admin/add_form.html ================================================ {% extends "./change_form.html" %} {% block djstripewarning %}

{{opts.verbose_name | capfirst}} created here will not be created on your Stripe Account.

{% endblock djstripewarning %} ================================================ FILE: djstripe/templates/djstripe/admin/change_form.html ================================================ {% extends "admin/change_form.html" %} {% load i18n %} {% block object-tools-items %} {{ block.super }} {% with original.get_stripe_dashboard_url as stripe_url %} {% if stripe_url %}
  • {% trans "View on Stripe Dashboard" %}
  • {% endif %} {% endwith %} {% endblock %} {% block extrastyle %} {{ block.super }} {% endblock extrastyle %} {% block content %} {% block djstripewarning %}

    Any Changes made here will not be updated on your Stripe Account.

    {% endblock djstripewarning %} {{ block.super }} {% endblock content %} ================================================ FILE: djstripe/templates/djstripe/admin/confirm_action.html ================================================ {% extends "admin/base_site.html" %} {% load i18n static l10n admin_urls %} {% block extrahead %} {{ block.super }} {{ media.js }} {% comment %} Load Jquery shipped with Django {% endcomment %} {% endblock %} {% block extrastyle %} {{ block.super }} {% endblock extrastyle %} {% block bodyclass %}{{ block.super }} delete-confirmation{% endblock %} {% block content %} {{ block.super }} {% comment %} Page Loader Icon {% endcomment %}

    Are you Sure?

    {% if action_name == "_resync_instances" %}

    Are you sure you want to sync the selected instances? All of the following objects and their related items will be synced.

    {% elif action_name == "_sync_all_instances" %}

    Are you sure you want to sync all instances? Please note this will be a best effort sync and we will silence any errors encountered.

    {% if info %}

    All of the following objects and their related items will be synced:

    {% endif %} {% elif action_name == "_cancel" %}

    Are you sure you want to cancel the selected subscriptions? The following subscriptions will be cancelled.

    {% endif %}
      {% for instance in info %}
    • {{instance}}
    • {% endfor %}
    {% csrf_token %} {{form}}
    {% endblock content %} ================================================ FILE: djstripe/templates/djstripe/admin/webhook_endpoint/add_form.html ================================================ {% extends "./change_form.html" %} {% block djstripewarning %}

    {{opts.verbose_name | capfirst}} created here will also be created on your Stripe Account. Proceed with caution.

    {% endblock djstripewarning %} ================================================ FILE: djstripe/templates/djstripe/admin/webhook_endpoint/change_form.html ================================================ {% extends "../change_form.html" %} {% block djstripewarning %}

    Any Changes made here will also be updated on your Stripe Account. Proceed with Caution.

    {% endblock djstripewarning %} ================================================ FILE: djstripe/templates/djstripe/admin/webhook_endpoint/delete_confirmation.html ================================================ {% extends "admin/delete_confirmation.html" %} {% block extrahead %} {{ block.super }} {% endblock %} {% block extrastyle %} {{ block.super }} {% endblock extrastyle %} {% block content %} {% comment %} Page Loader Icon {% endcomment %}

    Warning: This {{ object_name }} will also be deleted from Stripe.

    {{ block.super }} {% endblock %} ================================================ FILE: djstripe/urls.py ================================================ """ Urls related to the djstripe app. Wire this into the root URLConf this way:: path("stripe/", include("djstripe.urls", namespace="djstripe")), # url can be changed # Call to 'djstripe.urls' and 'namespace' must stay as is """ from django.urls import path, re_path from . import views from .settings import djstripe_settings as app_settings app_name = "djstripe" urlpatterns = [ # Webhook re_path( app_settings.DJSTRIPE_WEBHOOK_URL, views.ProcessWebhookView.as_view(), name="webhook", ), path( "webhook//", views.ProcessWebhookView.as_view(), name="djstripe_webhook_by_uuid", ), ] ================================================ FILE: djstripe/utils.py ================================================ """ Utility functions related to the djstripe app. """ import datetime from typing import Optional import stripe from django.apps import apps from django.conf import settings from django.contrib.humanize.templatetags.humanize import intcomma from django.db.models.query import QuerySet from django.utils import timezone def get_supported_currency_choices(api_key): """ Pull a stripe account's supported currencies and returns a choices tuple of those supported currencies. :param api_key: The api key associated with the account from which to pull data. :type api_key: str """ account = stripe.Account.retrieve(api_key=api_key) supported_payment_currencies = stripe.CountrySpec.retrieve( account["country"], api_key=api_key )["supported_payment_currencies"] return [(currency, currency.upper()) for currency in supported_payment_currencies] def clear_expired_idempotency_keys(): from .models import IdempotencyKey threshold = timezone.now() - datetime.timedelta(hours=24) IdempotencyKey.objects.filter(created__lt=threshold).delete() def convert_tstamp(response) -> Optional[datetime.datetime]: """ Convert a Stripe API timestamp response (unix epoch) to a native datetime. """ if response is None: # Allow passing None to convert_tstamp() return response # Overrides the set timezone to UTC - I think... tz = get_timezone_utc() if settings.USE_TZ else None return datetime.datetime.fromtimestamp(response, tz) # TODO: Finish this. CURRENCY_SIGILS = {"CAD": "$", "EUR": "€", "GBP": "£", "USD": "$"} def get_friendly_currency_amount(amount, currency: str) -> str: currency = currency.upper() sigil = CURRENCY_SIGILS.get(currency, "") amount_two_decimals = f"{amount:.2f}" return f"{sigil}{intcomma(amount_two_decimals)} {currency}" class QuerySetMock(QuerySet): """ A mocked QuerySet class that does not handle updates. Used by UpcomingInvoice.invoiceitems (deprecated) and UpcomingInvoice.lineitems. """ @classmethod def from_iterable(cls, model, iterable): instance = cls(model) instance._result_cache = list(iterable) instance._prefetch_done = True return instance def _clone(self): return self.__class__.from_iterable(self.model, self._result_cache) def update(self): return 0 def delete(self): return 0 def get_id_from_stripe_data(data): """ Extract stripe id from stripe field data """ if isinstance(data, str): # data like "sub_6lsC8pt7IcFpjA" return data elif data: # data like {"id": sub_6lsC8pt7IcFpjA", ...} return data.get("id") else: return None def get_model(model_name): app_label = "djstripe" app_config = apps.get_app_config(app_label) model = app_config.get_model(model_name) return model def get_queryset(pks, model_name): model = get_model(model_name) return model.objects.filter(pk__in=pks) def get_timezone_utc(): """ Returns UTC attribute in a backwards compatible way. UTC attribute has been moved from django.utils.timezone module to datetime.timezone class """ try: # Django 4+ return datetime.timezone.utc except AttributeError: return timezone.utc ================================================ FILE: djstripe/views.py ================================================ """ dj-stripe - Views related to the djstripe app. """ import logging from django.http import HttpResponse, HttpResponseBadRequest from django.shortcuts import get_object_or_404 from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from django.views.generic import View from .models import WebhookEndpoint, WebhookEventTrigger logger = logging.getLogger(__name__) @method_decorator(csrf_exempt, name="dispatch") class ProcessWebhookView(View): """ A Stripe Webhook handler view. This will create a WebhookEventTrigger instance, verify it, then attempt to process it. If the webhook cannot be verified, returns HTTP 400. If an exception happens during processing, returns HTTP 500. """ def post(self, request, uuid=None): # https://stripe.com/docs/webhooks/signatures if "stripe-signature" not in request.headers: # Do not even attempt to process/store the event if there is # no signature in the headers so we avoid overfilling the db. logger.error("HTTP_STRIPE_SIGNATURE is missing") return HttpResponseBadRequest() # uuid is passed for new-style webhook views only. # old-style defaults to no account. if uuid: # If the UUID is invalid (does not exist), this will throw a 404. # Note that this happens after the HTTP_STRIPE_SIGNATURE check on purpose. webhook_endpoint = get_object_or_404(WebhookEndpoint, djstripe_uuid=uuid) else: webhook_endpoint = None trigger = WebhookEventTrigger.from_request( request, webhook_endpoint=webhook_endpoint ) if trigger.is_test_event: # Since we don't do signature verification, we have to skip trigger.valid return HttpResponse("Test webhook successfully received and discarded!") if not trigger.valid: # Webhook Event did not validate, return 400 logger.error("Trigger object did not validate") return HttpResponseBadRequest() return HttpResponse(str(trigger.id)) ================================================ FILE: djstripe/webhooks.py ================================================ """ Utils related to processing or registering for webhooks A model registers itself here if it wants to be in the list of processing functions for a particular webhook. Each processor will have the ability to modify the event object, access event data, and do what it needs to do registrations are keyed by top-level event type (e.g. "invoice", "customer", etc) Each registration entry is a list of processors Each processor in these lists is a function to be called The function signature is: There is also a "global registry" which is just a list of processors (as defined above) NOTE: global processors are called before other processors. """ import functools import itertools from collections import defaultdict __all__ = ["handler", "handler_all", "call_handlers"] registrations = defaultdict(list) registrations_global = [] # Legacy. In previous versions of Stripe API, all test events used this ID. # Check out issue #779 for more information. TEST_EVENT_ID = "evt_00000000000000" def handler(*event_types): """ Decorator that registers a function as a webhook handler. Functions can be registered for event types (e.g. 'customer') or fully qualified event sub-types (e.g. 'customer.subscription.deleted'). If an event type is specified, the handler will receive callbacks for ALL webhook events of that type. For example, if 'customer' is specified, the handler will receive events for 'customer.subscription.created', 'customer.subscription.updated', etc. :param event_types: The event type(s) that should be handled. :type event_types: str. """ def decorator(func): for event_type in event_types: registrations[event_type].append(func) return func return decorator def handler_all(func=None): """ Decorator that registers a function as a webhook handler for ALL webhook events. Handles all webhooks regardless of event type or sub-type. """ if not func: return functools.partial(handler_all) registrations_global.append(func) return func def call_handlers(event): """ Invoke all handlers for the provided event type/sub-type. The handlers are invoked in the following order: 1. Global handlers 2. Event type handlers 3. Event sub-type handlers Handlers within each group are invoked in order of registration. :param event: The event model object. :type event: ``djstripe.models.Event`` """ chain = [registrations_global] # Build up a list of handlers with each qualified part of the event # category and verb. For example, "customer.subscription.created" creates: # 1. "customer" # 2. "customer.subscription" # 3. "customer.subscription.created" for index, _ in enumerate(event.parts): qualified_event_type = ".".join(event.parts[: (index + 1)]) chain.append(registrations[qualified_event_type]) for handler_func in itertools.chain(*chain): handler_func(event=event) ================================================ FILE: docs/CONTRIBUTING.md ================================================ # Contributing Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. You can contribute in many ways: ## Types of Contributions ### Report Bugs Report bugs at . If you are reporting a bug, please include: - The version of python and Django you're running - Detailed steps to reproduce the bug. ### Fix Bugs Look through the GitHub issues for bugs. Anything tagged with "bug" is open to whoever wants to implement it. ### Implement Features Look through the GitHub issues for features. Anything tagged with "feature" is open to whoever wants to implement it. ### Write Documentation dj-stripe could always use more documentation, whether as part of the official dj-stripe docs, in docstrings, or even on the web in blog posts, articles, and such. To see the project's documentation live, run the following command: mkdocs serve The documentation site will then be served on . !!! attention "In case of any installation error" In case you get the error that some plugin is not installed, please run: ``` bash poetry install --with docs ``` If you wish to just generate the documentation, you can replace `serve` with `build`, and the docs will be generated into the `site/` folder. ### Submit Feedback The best way to send feedback is to file an issue at . If you are proposing a feature: - Explain in detail how it would work. - Keep the scope as narrow as possible, to make it easier to implement. - Remember that this is a volunteer-driven project, and that contributions are welcome :) ### Contributor Discussion For questions regarding contributions to dj-stripe, another avenue is our Discord channel at . ## Get Started! Ready to contribute? Here's how to set up local development. 1. Fork [dj-stripe on Github](https://github.com/dj-stripe/dj-stripe). 1. Clone your fork locally: $ git clone git@github.com:your_name_here/dj-stripe.git 1. Set up [pre-commit](https://pre-commit.com/): $ git init # A git repo is required to install pre-commit $ pre-commit install 1. Set up your test database. If you're running tests using PostgreSQL: $ createdb djstripe or if you want to test vs sqlite (for convenience) or MySQL, they can be selected by setting this environment variable: $ export DJSTRIPE_TEST_DB_VENDOR=sqlite # or: export DJSTRIPE_TEST_DB_VENDOR=mysql For postgres and mysql, the database host,port,username and password can be set with environment variables, see `tests/settings.py` 1. Install [Poetry](https://python-poetry.org/) if you do not have it already. You can set up a virtual environment with: $ poetry install You can then, at any time, open a shell into that environment with: $ poetry shell 1. When you're done making changes, check that your changes pass the tests. A quick test run can be done as follows: $ DJSTRIPE_TEST_DB_VENDOR=sqlite poetry run pytest --reuse-db You should also check that the tests pass with other python and Django versions with tox. pytest will output both command line and html coverage statistics and will warn you if your changes caused code coverage to drop.: $ pip install tox $ tox 1. If your changes altered the models you may need to generate Django migrations: $ DJSTRIPE_TEST_DB_VENDOR=sqlite poetry run ./manage.py makemigrations 1. Commit your changes and push your branch to GitHub: $ git add . $ git commit -m "Your detailed description of your changes." $ git push 1. Submit a pull request through the GitHub website. Congratulations, you're now a dj-stripe contributor! Have some ♥ from us. ## Preferred Django Model Field Types When mapping from Stripe API field types to Django model fields, we try to follow Django best practises where practical. The following types should be preferred for fields that map to the Stripe API (which is almost all fields in our models). ### Strings - Stripe API string fields have a [default maximum length of 5,000 characters](https://github.com/stripe/openapi/issues/26#issuecomment-392957633). - In some cases a maximum length (`maxLength`) is specified in the [Stripe OpenAPI schema](https://github.com/stripe/openapi/tree/master/openapi). - We follow [Django's recommendation](https://docs.djangoproject.com/en/dev/ref/models/fields/#null) and avoid using null on string fields (which means we store `""` for string fields that are `null` in stripe). Note that is enforced in the sync logic in [StripeModel.\_stripe_object_to_record](https://github.com/dj-stripe/dj-stripe/blob/master/djstripe/models/base.py). - For long string fields (eg above 255 characters) we prefer `TextField` over `Charfield`. Therefore the default type for string fields that don't have a maxLength specified in the [Stripe OpenAPI schema](https://github.com/stripe/openapi/tree/master/openapi) should usually be: str_field = TextField(max_length=5000, default=", blank=True, help_text="...") ### Enumerations Fields that have a defined set of values can be implemented using `StripeEnumField`. ### Hash (dictionaries) Use the `JSONField` in `djstripe.fields`. ### Currency amounts Stripe handles all currency amounts as integer cents, we currently have a mixture of fields as integer cents and decimal (eg dollar, euro etc) values, but we are aiming to standardise on cents (see ). All new currency amount fields should use `StripeQuantumCurrencyAmountField`. ### Dates and Datetimes The Stripe API uses an integer timestamp (seconds since the Unix epoch) for dates and datetimes. We store this as a datetime field, using `StripeDateTimeField`. ## Django Migration Policy Migrations are considered a breaking change, so it's not usually not acceptable to add a migration to a stable branch, it will be a new `MAJOR.MINOR.0` release. A workaround to this in the case that the Stripe API data isn't compatible with out model (eg Stripe is sending `null` to a non-null field) is to implement the `_manipulate_stripe_object_hook` classmethod on the model. ### Avoid new migrations with non-schema changes If a code change produces a migration that doesn't alter the database schema (eg changing `help_text`) then instead of adding a new migration you can edit the most recent migration that affects the field in question. e.g.: ## Pull Request Guidelines Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. 1. The pull request must not drop code coverage below the current level. 1. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring. 1. If the pull request makes changes to a model, include Django migrations. 1. The pull request should work for Python 3.6+. Check [Github Actions](https://github.com/dj-stripe/dj-stripe/actions) and make sure that the tests pass for all supported Python versions. 1. Code formatting: Make sure to install `pre-commit` to automatically run it on `staged files` or run manually with `pre-commit run --all-files` at the dj-stripe root to keep a consistent style. ================================================ FILE: docs/README.md ================================================ # dj-stripe - Django + Stripe Made Easy [![Stripe Verified Partner](https://img.shields.io/static/v1?label=Stripe&message=Verified%20Partner&color=red&style=for-the-badge)](https://stripe.com/docs/libraries#community-libraries)
    [![CI tests](https://github.com/dj-stripe/dj-stripe/actions/workflows/ci.yml/badge.svg)](https://github.com/dj-stripe/dj-stripe/actions/workflows/ci.yml) [![Package Downloads](https://img.shields.io/pypi/dm/dj-stripe)](https://pypi.org/project/dj-stripe/) [![Documentation](https://img.shields.io/static/v1?label=Docs&message=READ&color=informational&style=plastic)](https://dj-stripe.github.io/dj-stripe/) [![Sponsor dj-stripe](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=red&style=plastic)](https://github.com/sponsors/dj-stripe) [![MIT License](https://img.shields.io/static/v1?label=License&message=MIT&color=informational&style=plastic)](https://github.com/sponsors/dj-stripe) Stripe Models for Django. ## Introduction dj-stripe implements all of the Stripe models, for Django. Set up your webhook endpoint and start receiving model updates. You will then have a copy of all the Stripe models available in Django models, as soon as they are updated! The full documentation is available [on Read the Docs](https://dj-stripe.github.io/dj-stripe/). ## Features - Stripe Core - Stripe Billing - Stripe Cards (JS v2) and Sources (JS v3) - Payment Methods and Payment Intents (SCA support) - Support for multiple accounts and API keys - Stripe Connect (partial support) - Tested with Stripe API `2020-08-27` (see [API versions](api_versions.md#dj-stripe_latest_tested_version)) ## Requirements - Django >=3.2 - Python >=3.8 - PostgreSQL engine (recommended) >=9.6 - MySQL engine: MariaDB >=10.2 or MySQL >=5.7 - SQLite: Not recommended in production. Version >=3.26 required. ## Installation See [installation](installation.md) instructions. ## Changelog [See release notes on Read the Docs](history/2_7_0/). ## Funding and Support Stripe Logo You can now become a sponsor to dj-stripe with [GitHub Sponsors](https://github.com/sponsors/dj-stripe). We've been bringing dj-stripe to the world for over 7 years and are excited to be able to start dedicating some real resources to the project. Your sponsorship helps us keep a team of maintainers actively working to improve dj-stripe and ensure it stays up-to-date with the latest Stripe changes. If you use dj-stripe commercially, we would encourage you to invest in its continued development by [signing up for a paid plan](https://github.com/sponsors/dj-stripe). Corporate sponsors [receive priority support and development time](project/support.md). All contributions through GitHub sponsors flow into our [Open Collective](https://opencollective.com/dj-stripe), which holds our funds and keeps an open ledger on how donations are spent. ## Our Gold sponsors Stripe Logo ## Similar libraries - [dj-paypal](https://github.com/HearthSim/dj-paypal) ([PayPal](https://www.paypal.com/)) - [dj-paddle](https://github.com/paddle-python/dj-paddle) ([Paddle](https://paddle.com/)) ================================================ FILE: docs/__init__.py ================================================ ================================================ FILE: docs/api_keys.md ================================================ # Managing Stripe API keys Stripe API keys are stored in the database, and editable from the Django admin. !!! attention "Important Note" By default, keys are visible by anyone who has access to the dj-stripe administration. ## Adding new API keys You may add new API keys via the Dj-Stripe "API key" administration. The only required value is the key's "secret" value itself. Example: ![Adding an API key from the Django administration](https://user-images.githubusercontent.com/235410/99198962-2a1f2e00-279c-11eb-96cc-96dee0ba03ac.png) Once saved, Dj-Stripe will automatically detect whether the key is a public, restricted or secret key, and whether it's for live or test mode. If it's a secret key, the matching Account object will automatically be fetched as well and the key will be associated with it, so that it can be used to communicate with the Stripe API when dealing with objects belonging to that Account. ## Updating the API keys When expiring or rolling new secret keys, you should create the new API key in Stripe, then add it from the Django administration. Whenever you are ready, you may delete the old key. (It is safe to keep it around, as long as it hasn't expired. Keeping expired keys in the database may result in errors during usage). ## FAQ ### Why store them in the database? As we work on supporting multiple Stripe accounts per instance, it is vital for dj-stripe to have a mechanism to store more than one Stripe API key. It also became obvious that we may want proper programmatic access to create and delete keys. Furthermore, API keys are a legitimate upstream Stripe object, and it is not unlikely the API may allow access to listing other API keys in the future, in which case we will want to move them to the database anyway. ### Isn't that insecure? Please do keep your billing database encrypted. There's a copy of all your customers' billing data on it! You may also instead create a read-only restricted key with all-read permissions for dj-stripe. There is no added risk there, given that dj-stripe holds a copy of all your data regardless. ### I'm using environment variables. Do I need to change anything? Not at this time. The settings `STRIPE_LIVE_SECRET_KEY` and `STRIPE_TEST_SECRET_KEY` can still be used. Their values will however be automatically saved to the database at the earliest opportunity. ### What about public keys? Setting `STRIPE_LIVE_PUBLIC_KEY` and `STRIPE_TEST_PUBLIC_KEY` will be deprecated in 2.5.0. You do not risk anything by leaving them in your settings: They are not used by Dj-Stripe outside of the Dj-Stripe mixins, which are now themselves deprecated. So you can safely leave them in your settings, or you can move them to the database as well (Keys beginning in `pk_test_` and `pk_live_` will be detected as publishable keys). ================================================ FILE: docs/api_versions.md ================================================ # A note on Stripe API versions A point that can cause confusion to new users of dj-stripe is that there are several different Stripe API versions in play at once. ## Your Stripe account's API version This is the version used by Stripe when sending webhook data to you and the default version used by the Stripe API. You can find this on [your Stripe dashboard](https://dashboard.stripe.com/developers) labelled "**default**". New Stripe accounts are always on the latest version. Read more about it on [stripe.com/docs/api/versioning](https://stripe.com/docs/api/versioning). ## Stripe's current latest API version You can find this on your Stripe dashboard labelled "**latest**" or in [Stripe's API documentation](https://stripe.com/docs/upgrades#api-changelog) See [stripe.com/docs/upgrades](https://stripe.com/docs/upgrades#how-can-i-upgrade-my-api) on how to upgrade your Stripe API version. Stripe will only allow upgrades to the **latest** version. ## Dj-stripe API version This is the Stripe API version used by dj-stripe in all communication with Stripe, including when processing webhooks (though webhook data is sent to you by Stripe with your API version, we re-fetch the data with dj-stripe's API version), this is because the API schema needs to match dj-stripe's Django model schema. It is defined by [`STRIPE_API_VERSION`](reference/settings.md#stripe_api_version-2020-08-27) with a default of [`DEFAULT_STRIPE_API_VERSION`][djstripe.settings.DjstripeSettings.DEFAULT_STRIPE_API_VERSION]. You mustn't change this as it ensures that dj-stripe receives data in the format it expects. !!! note dj-stripe will always use `STRIPE_API_VERSION` in its requests regardless of what `stripe.api_version` is set to. ## Dj-stripe Latest Tested Version This is the most recent Stripe account API version used by the maintainers during testing, more recent versions account versions are probably fine though. ================================================ FILE: docs/history/0_x.md ================================================ # dj-stripe 0.x release notes ## 0.8.0 (2015-12-30) - better plan ordering documentation (Thanks @cjrh) - added a confirmation page when choosing a subscription (Thanks @chrissmejia, @areski) - setup.py reverse dependency fix (\#258/\#268) (Thanks @ticosax) - Dropped official support for Django 1.7 (no code changes were made) - Python 3.5 support, Django 1.9.1 support - Migration improvements (Thanks @michi88) - Fixed "Invoice matching query does not exist" bug (\#263) (Thanks @mthornhill) - Fixed duplicate content in account view (Thanks @areski) ## 0.7.0 (2015-09-22) - dj-stripe now responds to the invoice.created event (Thanks @wahuneke) - dj-stripe now cancels subscriptions and purges customers during sync if they were deleted from the stripe dashboard (Thanks @unformatt) - dj-stripe now checks for an active stripe subscription in the `update_plan_quantity` call (Thanks @ctrengove) - Event processing is now handled by "event handlers" - functions outside of models that respond to various event types and subtypes. Documentation on how to tie into the event handler system coming soon. (Thanks @wahuneke) - Experimental Python 3.5 support - Support for Django 1.6 and lower is now officially gone. - Much, much more! ## 0.6.0 (2015-07-12) - Support for Django 1.6 and lower is now deprecated. - Improved test harness now tests coverage and pep8 - SubscribeFormView and ChangePlanView no longer populate self.error with form errors - InvoiceItems.plan can now be null (as it is with individual charges), resolving \#140 (Thanks @awechsler and @MichelleGlauser for help troubleshooting) - Email templates are now packaged during distribution. - sync_plans now takes an optional api_key - 100% test coverage - Stripe ID is now returned as part of each model's str method (Thanks @areski) - Customer model now stores card expiration month and year (Thanks @jpadilla) - Ability to extend subscriptions (Thanks @TigerDX) - Support for plan heirarchies (Thanks @chrissmejia) - Rest API endpoints for Subscriptions \[contrib\] (Thanks @philippeluickx) - Admin interface search by email funtionality is removed (\#221) (Thanks @jpadilla) ## 0.5.0 (2015-05-25) - Began deprecation of support for Django 1.6 and lower. - Added formal support for Django 1.8. - Removed the StripeSubscriptionSignupForm - Removed `djstripe.safe_settings`. Settings are now all located in `djstripe.settings` - `DJSTRIPE_TRIAL_PERIOD_FOR_SUBSCRIBER_CALLBACK` can no longer be a module string - The sync_subscriber argument has been renamed from subscriber_model to subscriber - Moved available currencies to the DJSTRIPE_CURRENCIES setting (Thanks @martinhill) - Allow passing of extra parameters to stripe Charge API (Thanks @mthornhill) - Support for all available arguments when syncing plans (Thanks @jamesbrobb) - charge.refund() now returns the refunded charge object (Thanks @mthornhill) - Charge model now has captured field and a capture method (Thanks @mthornhill) - Subscription deleted webhook bugfix - South migrations are now up to date (Thanks @Tyrdall) ## 0.4.0 (2015-04-05) - Formal Python 3.3+/Django 1.7 Support (including migrations) - Removed Python 2.6 from Travis CI build. (Thanks @audreyr) - Dropped Django 1.4 support. (Thanks @audreyr) - Deprecated the `djstripe.forms.StripeSubscriptionSignupForm`. Making this form work easily with both `dj-stripe` and `django-allauth` required too much abstraction. It will be removed in the 0.5.0 release. - Add the ability to add invoice items for a customer (Thanks @kavdev) - Add the ability to use a custom customer model (Thanks @kavdev) - Added setting to disable Invoice receipt emails (Thanks Chris Halpert) - Enable proration when customer upgrades plan, and pass proration policy and cancellation at period end for upgrades in settings. (Thanks Yasmine Charif) - Removed the redundant context processor. (Thanks @kavdev) - Fixed create a token call in change_card.html (Thanks @dollydagr) - Fix `charge.dispute.closed` typo. (Thanks @ipmb) - Fix contributing docs formatting. (Thanks @audreyr) - Fix subscription canceled_at_period_end field sync on plan upgrade (Thanks @nigma) - Remove "account" bug in Middleware (Thanks @sromero84) - Fix correct plan selection on subscription in subscribe_form template. (Thanks Yasmine Charif) - Fix subscription status in account, \_subscription_status, and cancel_subscription templates. (Thanks Yasmine Charif) - Now using `user.get_username()` instead of `user.username`, to support custom User models. (Thanks @shvechikov) - Update remaining DOM Ids for Bootstrap 3. (Thanks Yasmine Charif) - Update publish command in setup.py. (Thanks @pydanny) - Explicitly specify tox's virtual environment names. (Thanks @audreyr) - Manually call django.setup() to populate apps registry. (Thanks @audreyr) ## 0.3.5 (2014-05-01) - Fixed `djstripe_init_customers` management command so it works with custom user models. ## 0.3.4 (2014-05-01) - Clarify documentation for redirects on app_name. - If settings.DEBUG is True, then django-debug-toolbar is exempt from redirect to subscription form. - Use collections.OrderedDict to ensure that plans are listed in order of price. - Add `ordereddict` library to support Python 2.6 users. - Switch from `__unicode__` to `__str__` methods on models to better support Python 3. - Add `python_2_unicode_compatible` decorator to Models. - Check for PY3 so the `unicode(self.user)` in models.Customer doesn't blow up in Python 3. ## 0.3.3 (2014-04-24) - Increased the extendability of the views by removing as many hard-coded URLs as possible and replacing them with `success_url` and other attributes/methods. - Added single unit purchasing to the cookbook ## 0.3.2 (2014-01-16) - Made Yasmine Charif a core committer - Take into account trial days in a subscription plan (Thanks Yasmine Charif) - Correct invoice period end value (Thanks Yasmine Charif) - Make plan cancellation and plan change consistently not prorating (Thanks Yasmine Charif) - Fix circular import when ACCOUNT_SIGNUP_FORM_CLASS is defined (Thanks Dustin Farris) - Add send e-mail receipt action in charges admin panel (Thanks Buddy Lindsay) - Add `created` field to all ModelAdmins to help with internal auditing (Thanks Kulbir Singh) ## 0.3.1 (2013-11-14) - Cancellation fix (Thanks Yasmine Charif) - Add setup.cfg for wheel generation (Thanks Charlie Denton) ## 0.3.0 (2013-11-12) - Fully tested against Django 1.6, 1.5, and 1.4 - Fix boolean default issue in models (from now on they are all default to `False`). - Replace duplicated code with `djstripe.utils.user_has_active_subscription`. ## 0.2.9 (2013-09-06) - Cancellation added to views. - Support for kwargs on charge and invoice fetching. - def charge() now supports send_receipt flag, default to True. - Fixed templates to work with Bootstrap 3.0.0 column design. ## 0.2.8 (2013-09-02) - Improved usage documentation. - Corrected order of fields in StripeSubscriptionSignupForm. - Corrected transaction history template layout. - Updated models to take into account when settings.USE_TZ is disabled. ## 0.2.7 (2013-08-24) - Add handy rest_framework permission class. - Fixing attribution for django-stripe-payments. - Add new status to Invoice model. ## 0.2.6 (2013-08-20) - Changed name of division tag to djdiv. - Added `safe_setting.py` module to handle edge cases when working with custom user models. - Added cookbook page in the documentation. ## 0.2.5 (2013-08-18) - Fixed bug in initial checkout - You can't purchase the same plan that you currently have. ## 0.2.4 (2013-08-18) - Recursive package finding. ## 0.2.3 (2013-08-16) - Fix packaging so all submodules are loaded ## 0.2.2 (2013-08-15) - Added Registration + Subscription form ## 0.2.1 (2013-08-12) - Fixed a bug on CurrentSubscription tests - Improved usage documentation - Added to migration from other tools documentation ## 0.2.0 (2013-08-12) - Cancellation of plans now works. - Upgrades and downgrades of plans now work. - Changing of cards now works. - Added breadcrumbs to improve navigation. - Improved installation instructions. - Consolidation of test instructions. - Minor improvement to django-stripe-payments documentation - Added coverage.py to test process. - Added south migrations. - Fixed the subscription_payment_required function-based view decorator. - Removed unnecessary django-crispy-forms ## 0.1.7 (2013-08-08) - Middleware excepts all of the djstripe namespaced URLs. This way people can pay. ## 0.1.6 (2013-08-08) - Fixed a couple template paths - Fixed the manifest so we include html, images. ## 0.1.5 (2013-08-08) - Fixed the manifest so we include html, css, js, images. ## 0.1.4 (2013-08-08) - Change PaymentRequiredMixin to SubscriptionPaymentRequiredMixin - Add subscription_payment_required function-based view decorator - Added SubscriptionPaymentRedirectMiddleware - Much nicer accounts view display - Much improved subscription form display - Payment plans can have decimals - Payment plans can have custom images ## 0.1.3 (2013-08-7) - Added account view - Added Customer.get_or_create method - Added djstripe_sync_customers management command - sync file for all code that keeps things in sync with stripe - Use client-side JavaScript to get history data asynchronously - More user friendly action views ## 0.1.2 (2013-08-6) - Admin working - Better publish statement - Fix dependencies ## 0.1.1 (2013-08-6) - Ported internals from django-stripe-payments - Began writing the views - Travis-CI - All tests passing on Python 2.7 and 3.3 - All tests passing on Django 1.4 and 1.5 - Began model cleanup - Better form - Provide better response from management commands ## 0.1.0 (2013-08-5) - First release on PyPI. ================================================ FILE: docs/history/1_x.md ================================================ # dj-stripe 1.x release notes ## 1.2.4 (2019-02-27) This is a bugfix-only version: - Allow billing_cycle_anchor argument when creating a subscription (\#814) - Fixup plan amount null with tier plans (\#781) - Update Cancel subscription view tests to match backport in f64af57 - Implement Invoice.\_manipulate_stripe_object_hook for compatability with API 2018-11-08 (\#771) - Fix product webhook for type="good" (\#724) - Add trial_from_plan, trial_period_days args to Customer.subscribe() (\#709) ## 1.2.3 (2018-10-13) This is a bugfix-only version: - Updated Subscription.cancel() for compatibility with Stripe 2018-08-23 (\#723) ## 1.2.2 (2018-08-11) This is a bugfix-only version: - Fixed an error with request.urlconf in some setups (\#562) - Always save text-type fields as empty strings in db instead of null (\#713) - Fix support for DJSTRIPE_SUBSCRIBER_MODEL_MIGRATION_DEPENDENCY (\#707) - Fix reactivate() with Stripe API 2018-02-28 and above ## 1.2.1 (2018-07-18) This is a bugfix-only version: - Fixed various Python 2.7 compatibility issues - Fixed issues with max_length of receipt_number - Fixed various fields incorrectly marked as required - Handle product webhook calls - Fix compatibility with stripe-python 2.0.0 ## 1.2.0 (2018-06-11) The dj-stripe 1.2.0 release resets all migrations. **Do not upgrade to 1.2.0 directly from 1.0.1 or below. You must upgrade to 1.1.0 first.** Please read the 1.1.0 release notes below for more information. ## 1.1.0 (2018-06-11) In dj-stripe 1.1.0, we made a _lot_ of changes to models in order to bring the dj-stripe model state much closer to the upstream API objects. If you are a current user of dj-stripe, you will most likely have to make changes in order to upgrade. Please read the full changelog below. If you are having trouble upgrading, you may ask for help [by filing an issue on GitHub](https://github.com/dj-stripe/dj-stripe/issues). ### Migration reset The next version of dj-stripe, **1.2.0**, will reset all the migrations to `0001_initial`. Migrations are currently in an unmaintainable state. **What this means is you will not be able to upgrade directly to dj-stripe 1.2.0. You must go through 1.1.0 first, run \`\`manage.py migrate djstripe\`\`, then upgrade to 1.2.0.** ### Python 2.7 end-of-life dj-stripe 1.1.0 drops support for Django 1.10 and adds support for Django 2.0. Django 1.11+ and Python 2.7+ or 3.4+ are required. Support for Python versions older than 3.5, and Django versions older than 2.0, will be dropped in dj-stripe 2.0.0. ### Backwards-incompatible changes and deprecations #### Removal of polymorphic models The model architecture of dj-stripe has been simplified. Polymorphic models have been dropped and the old base StripeCustomer, StripeCharge, StripeInvoice, etc models have all been merged into the top-level Customer, Charge, Invoice, etc models. Importing those legacy models from `djstripe.stripe_objects` will yield the new ones. This is deprecated and support for this will be dropped in dj-stripe 2.0.0. #### Full support for Stripe Sources (Support for v3 stripe.js) Stripe sources (`src_XXXX`) are objects that can arbitrarily reference any of the payment method types that Stripe supports. However, the legacy `Card` object (with object IDs like `card_XXXX` or `cc_XXXX`) is not a Source object, and cannot be turned into a Source object at this time. In order to support both Card and Source objects in ForeignKeys, a new model `PaymentMethod` has been devised (renamed to `DjstripePaymentMethod` in 2.0). That model can resolve into a Card, a Source, or a BankAccount object. - **The \`\`default_source\`\` attribute on \`\`Customer\`\` now refers to a \`\`PaymentMethod\`\` object**. You will need to call `.resolve()` on it to get the Card or Source in question. - References to `Customer.sources` expecting a queryset of Card objects should be updated to `Customer.legacy_cards`. - The legacy `StripeSource` name refers to the `Card` model. This will be removed in dj-stripe 2.0.0. Update your references to either `Card` or `Source`. - `enums.SourceType` has been renamed to `enums.LegacySourceType`. `enums.SourceType` now refers to the actual Stripe Source types enum. #### Core fields renamed - The numeric `id` field has been renamed to `djstripe_id`. This avoids a clash with the upstream stripe id. Accessing `.id` is deprecated and \*\*will reference the upstream `stripe_id` in dj-stripe 2.0.0 ## 1.0.0 (2017-08-12) It's finally here! We've made significant changes to the codebase and are now compliant with stripe API version **2017-06-05**. I want to give a huge thanks to all of our contributors for their help in making this happen, especially Bill Huneke (@wahuneke) for his impressive design work and @jleclanche for really pushing this release along. I also want to welcome onboard two more maintainers, @jleclanche and @lskillen. They've stepped up and have graciously dedicated their resources to making dj-stripe such an amazing package. Almost all methods now mimic the parameters of those same methods in the stripe API. Note that some methods do not have some parameters implemented. This is intentional. That being said, expect all method signatures to be different than those in previous versions of dj-stripe. Finally, please note that there is still a bit of work ahead of us. Not everything in the Stripe API is currently supported by dj-stripe -- we're working on it. That said, v1.0.0 has been thoroughly tested and is verified stable in production applications. ### A few things to get excited for - Multiple subscription support (finally) - Multiple sources support (currently limited to Cards) - Idempotency support (See \#455, \#460 for discussion -- big thanks to @jleclanche) - Full model documentation - Objects that come through webhooks are now tied to the API version set in dj-stripe. No more errors if dj-stripe falls behind the newest stripe API version. - Any create/update action on an object automatically syncs the object. - Concurrent LIVE and TEST mode support (Thanks to @jleclanche). Note that you'll run into issues if `livemode` isn't set on your existing customer objects. - All choices are now enum-based (Thanks @jleclanche, See \#520). Access them from the new `djstripe.enums` module. The ability to check against model property based choices will be deprecated in 1.1 - Support for the Coupon model, and coupons on Customer objects. - Support for the [Payout/Transfer split](https://stripe.com/docs/transfer-payout-split) from api version `2017-04-06`. ### What still needs to be done (in v1.1.0) - **Documentation**. Our original documentation was not very helpful, but it covered the important bits. It will be very out of date after this update and will need to be rewritten. If you feel like helping, we could use all the help we can get to get this pushed out asap. - **Master sync re-write**. This sounds scary, but really isn't. The current management methods run sync methods on Customer that aren't very helpful and are due for removal. My plan is to write something that first updates local data (via `api_retrieve` and `sync_from_stripe_data`) and then pulls all objects from Stripe and populates the local database with any records that don't already exist there. You might be wondering, "Why are they releasing this if there are only a few things left?" Well, that thinking turned this into a two year release... Trust me, this is a good thing. ### Significant changes (mostly backwards-incompatible) - **Idempotency**. \#460 introduces idempotency keys and implements idempotency for `Customer.get_or_create()`. Idempotency will be enabled for all calls that need it. - **Improved Admin Interface**. This is almost complete. See \#451 and \#452. - **Drop non-trivial endpoint views**. We're dropping everything except the webhook endpoint and the subscription cancel endpoint. See \#428. - **Drop support for sending receipts**. Stripe now handles this for you. See \#478. - **Drop support for plans as settings**, including custom plan hierarchy (if you want this, write something custom) and the dynamic trial callback. We've decided to gut having plans as settings. Stripe should be your source of truth; create your plans there and sync them down manually. If you need to create plans locally for testing, etc., simply use the ORM to create Plan models. The sync rewrite will make this drop less annoying. - **Orphan Customer Sync**. We will now sync Customer objects from Stripe even if they aren't linked to local subscriber objects. You can link up subscribers to those Customers manually. - **Concurrent Live and Test Mode**. dj-stripe now supports test-mode and live-mode Customer objects concurrently. As a result, the User.customer One-to-One reverse-relationship is now the User.djstripe_customers RelatedManager. (Thanks @jleclanche) \#440. You'll run into some dj-stripe check issues if you don't update your KEY settings accordingly. Check our GitHub issue tracker for help on this. ### SETTINGS - The `PLAN_CHOICES`, `PLAN_LIST`, and `PAYMENT_PLANS` objects are removed. Use Plan.objects.all() instead. - The `plan_from_stripe_id` function is removed. Use Plan.objects.get(stripe_id=) ### SYNCING - sync_plans no longer takes an api_key - sync methods no longer take a `cu` parameter - All sync methods are now private. We're in the process of building a better syncing mechanism. ### UTILITIES - dj-stripe decorators now take a plan argument. If you're passing in a custom test function to `subscriber_passes_pay_test`, be sure to account for this new argument. ### MIXINS - The context provided by dj-stripe's mixins has changed. `PaymentsContextMixin` now provides `STRIPE_PUBLIC_KEY` and `plans` (changed to `Plan.objects.all()`). `SubscriptionMixin` now provides `customer` and `is_plans_plural`. - We've removed the SubscriptionPaymentRequiredMixin. Use `@method_decorator("dispatch",`[subscription_payment_required](https://github.com/kavdev/dj-stripe/blob/1.0.0/djstripe/decorators.py#L39)`)` instead. ### MIDDLEWARE - dj-stripe middleware doesn't support multiple subscriptions. ### SIGNALS - Local custom signals are deprecated in favor of Stripe webhooks: - `cancelled` -> WEBHOOK_SIGNALS\["customer.subscription.deleted"\] - `card_changed` -> WEBHOOK_SIGNALS\["customer.source.updated"\] - `subscription_made` -> WEBHOOK_SIGNALS\["customer.subscription.created"\] ### WEBHOOK EVENTS - The Event Handlers designed by @wahuneke are the new way to handle events that come through webhooks. Definitely take a look at `event_handlers.py` and `webhooks.py`. ### EXCEPTIONS - `SubscriptionUpdateFailure` and `SubscriptionCancellationFailure` exceptions are removed. There should no longer be a case where they would have been useful. Catch native stripe errors in their place instead. ### MODELS > **CHARGE** - `Charge.charge_created` -> `Charge.stripe_timestamp` - `Charge.card_last_4` and `Charge.card_kind` are removed. Use `Charge.source.last4` and `Charge.source.brand` (if the source is a Card) - `Charge.invoice` is no longer a foreign key to the Invoice model. `Invoice` now has a OneToOne relationship with `Charge`. (`Charge.invoice` will still work, but will no longer be represented in the database). **CUSTOMER** - dj-stripe now supports test mode and live mode Customer objects concurrently (See \#440). As a result, the `.customer` OneToOne reverse relationship is no longer a thing. You should now instead add a `customer` property to your subscriber model that checks whether you're in live or test mode (see djstripe.settings.STRIPE_LIVE_MODE as an example) and grabs the customer from `.djstripe_customers` with a simple `livemode=` filter. - Customer no longer has a `current_subscription` property. We've added a `subscription` property that should suit your needs. - With the advent of multiple subscriptions, the behavior of `Customer.subscribe()` has changed. Before, `calling subscribe()` when a customer was already subscribed to a plan would switch the customer to the new plan with an option to prorate. Now calling `subscribe()` simply subscribes that customer to a new plan in addition to it's current subsription. Use `Subscription.update()` to change a subscription's plan instead. - `Customer.cancel_subscription()` is removed. Use `Subscription.cancel()` instead. - The `Customer.update_plan_quantity()` method is removed. Use `Subscription.update()` instead. - `CustomerManager` is now `SubscriptionManager` and works on the `Subscription` model instead of the `Customer` model. - `Customer.has_valid_card()` is now `Customer.has_valid_source()`. - `Customer.update_card()` now takes an id. If the id is not supplied, the default source is updated. - `Customer.stripe_customer` property is removed. Use `Customer.api_retrieve()` instead. - The `at_period_end` parameter of `Customer.cancel_subscription()` now actually follows the [DJSTRIPE_PRORATION_POLICY](../reference/settings.md#djstripe_proration_policy-false) setting. - `Customer.card_fingerprint`, `Customer.card_last_4`, `Customer.card_kind`, `Customer.card_exp_month`, `Customer.card_exp_year` are all removed. Check `Customer.default_source` (if it's a Card) or one of the sources in `Customer.sources` (again, if it's a Card) instead. - The `invoice_id` parameter of `Customer.add_invoice_item` is now named `invoice` and can be either an Invoice object or the stripe_id of an Invoice. **EVENT** - `Event.kind` -> `Event.type` - Removed `Event.validated_message`. Just check if the event is valid - no need to double check (we do that for you) **TRANSFER** - Removed `Transfer.update_status()` - Removed `Transfer.event` - `TransferChargeFee` is removed. It hasn't been used in a while due to a broken API version. Use `Transfer.fee_details` instead. - Any fields that were in `Transfer.summary` no longer exist and are therefore deprecated (unused but not removed from the database). Because of this, `TransferManager` now only aggregates `total_sum` **INVOICE** - `Invoice.attempts` -> `Invoice.attempt_count` - InvoiceItems are no longer created when Invoices are synced. You must now sync InvoiceItems directly. **INVOICEITEM** - Removed `InvoiceItem.line_type` **PLAN** - Plan no longer has a `stripe_plan` property. Use `api_retrieve()` instead. - `Plan.currency` no longer uses choices. Use the `get_supported_currency_choices()` utility and create your own custom choices list instead. - Plan interval choices are now in `Plan.INTERVAL_TYPE_CHOICES` **SUBSCRIPTION** - `Subscription.is_period_current()` now checks for a current trial end if the current period has ended. This change means subscriptions extended with `Subscription.extend()` will now be seen as valid. ### MIGRATIONS We'll sync your current records with Stripe in a migration. It will take a while, but it's the only way we can ensure data integrity. There were some fields for which we needed to temporarily add placeholder defaults, so just make sure you have a customer with ID 1 and a plan with ID 1 and you shouldn't run into any issues (create dummy values for these if need be and delete them after the migration). ### BIG HUGE NOTE - DON'T OVERLOOK THIS !!! warning Subscription and InvoiceItem migration is not possible because old records don't have Stripe IDs (so we can't sync them). Our approach is to delete all local subscription and invoiceitem objects and re-sync them from Stripe. We 100% recommend you create a backup of your database before performing this upgrade. ### Other changes - Postgres users now have access to the `DJSTRIPE_USE_NATIVE_JSONFIELD` setting. (Thanks @jleclanche) \#517, \#523 - Charge receipts now take `DJSTRIPE_SEND_INVOICE_RECEIPT_EMAILS` into account (Thanks @r0fls) - Clarified/modified installation documentation (Thanks @pydanny) - Corrected and revised ANONYMOUS_USER_ERROR_MSG (Thanks @pydanny) - Added fnmatching to `SubscriptionPaymentMiddleware` (Thanks @pydanny) - `SubscriptionPaymentMiddleware.process_request()` functionality broken up into multiple methods, making local customizations easier (Thanks @pydanny) - Fully qualified events are now supported by event handlers as strings e.g. 'customer.subscription.deleted' (Thanks @lskillen) \#316 - runtests now accepts positional arguments for declaring which tests to run (Thanks @lskillen) \#317 - It is now possible to reprocess events in both code and the admin interface (Thanks @lskillen) \#318 - The confirm page now checks that a valid card exists. (Thanks @scream4ik) \#325 - Added support for viewing upcoming invoices (Thanks @lskillen) \#320 - Event handler improvements and bugfixes (Thanks @lskillen) \#321 - API list() method bugfixes (Thanks @lskillen) \#322 - Added support for a custom webhook event handler (Thanks @lskillen) \#323 - Django REST Framework contrib package improvements (Thanks @aleccool213) \#334 - Added `tax_percent` to CreateSubscriptionSerializer (Thanks @aleccool213) \#349 - Fixed incorrectly assigned `application_fee` in Charge calls (Thanks @kronok) \#382 - Fixed bug caused by API change (Thanks @jessamynsmith) \#353 - Added inline documentation to pretty much everything and enforced docsytle via flake8 (Thanks @aleccool213) - Fixed outdated method call in template (Thanks @kandoio) \#391 - Customer is correctly purged when subscriber is deleted, regardless of how the deletion happened (Thanks @lskillen) \#396 - Test webhooks are now properly captured and logged. No more bounced requests to Stripe! (Thanks @jameshiew) \#408 - CancelSubscriptionView redirect is now more flexible (Thanks @jleclanche) \#418 - Customer.sync_cards() (Thanks @jleclanche) \#438 - Many stability fixes, bugfixes, and code cleanup (Thanks @jleclanche) - Support syncing canceled subscriptions (Thanks @jleclanche) \#443 - Improved admin interface (Thanks @jleclanche with @jameshiew) \#451 - Support concurrent TEST + LIVE API keys (Fix webhook event processing for both modes) (Thanks @jleclanche) \#461 - Added Stripe Dashboard link to admin change panel (Thanks @jleclanche) \#465 - Implemented `Plan.amount_in_cents` (Thanks @jleclanche) \#466 - Implemented `Subscription.reactivate()` (Thanks @jleclanche) \#470 - Added `Plan.human_readable_price` (Thanks @jleclanche) \#498 - (Re)attach the Subscriber when we find it's id attached to a customer on Customer sync (Thanks @jleclanche) \#500 - Made API version configurable (with dj-stripe recommended default) (Thanks @lskillen) \#504 ================================================ FILE: docs/history/2_4_0.md ================================================ # dj-stripe 2.4.0 release notes (2020-11-19) !!! attention To upgrade to 2.4.0 from older versions of dj-stripe, scroll down to the [Upgrade Guide](#upgrade-guide). ## Introducing sponsorships and our first sponsor We're excited to introduce our [Sponsorship tiers](https://github.com/sponsors/dj-stripe). Individuals may back dj-stripe to assist with development. Larger backers may choose one the [paid support plans available](../project/support.md#support_plans) to receive support on top of ensuring the long-term viability of the project! And this release was made possible by none other than… [Stripe](https://stripe.com)! Our very first Gold sponsor. Their financial backing has allowed us to pour a lot of work that could not have otherwise happened. ## Release notes - Support for Django 3.1 and Python 3.8. - Minimum stripe-python version is now 2.48.0. - Default Stripe API version is now `2020-08-27`. - First-class support for the Price model, replacing Plans. - Support multi-item subscriptions. - Support for API keys in the database (see [Managing Stripe API keys](../api_keys.md#managing_stripe_api_keys)). - Support for syncing objects for multiple, different Stripe accounts. - Use Django 3.1 native JSONField when available. - The field `djstripe_owner_account` has been added to all Stripe models, and is automatically populated with the Account that owns the API key used to retrieve it. - Support for subscription schedules (#899). - Add support for Reporting categories and TaxIds - Update many models to match latest version of the Stripe API. - Fixed Account.get_default_account() for Restricted API Keys. - Allow passing arbitrary arguments (any valid SDK argument) to the following methods: - `Customer.charge()` - `Customer.subscribe()`, - `Charge.capture()` - `Subscription.update()` - New management command: `djstripe_update_invoiceitem_ids`. This command migrates InvoiceItems using Stripe's old IDs to the new ones. - Hundreds of other bugfixes. ## New feature: in-database Stripe API keys Stripe API keys are now stored in the database, and are now editable in the admin. !!! warning By default, all keys are visible by anyone who has access to the dj-stripe administration. ### Why? As we work on supporting multiple Stripe accounts per instance, it is vital for dj-stripe to have a mechanism to store more than one Stripe API key. It also became obvious that we may want proper programmatic access to create and delete keys. Furthermore, API keys are a legitimate upstream Stripe object, and it is not unlikely the API may allow access to listing other API keys in the future, in which case we will want to move them to the database anyway. In the next release, we are planning to make WebhookEndpoints (and thus webhook secrets) manageable via the database as well. ### Do I need to change anything? Not at this time. The settings `STRIPE_LIVE_SECRET_KEY` and `STRIPE_TEST_SECRET_KEY` can still be used. Their values will however be automatically saved to the database at the earliest opportunity. ### What about public keys? Setting `STRIPE_LIVE_PUBLIC_KEY` and `STRIPE_TEST_PUBLIC_KEY` will be deprecated next release. You do not risk anything by leaving them in your settings: They are not used by Dj-Stripe outside of the Dj-Stripe mixins, which are now themselves deprecated. So you can safely leave them in your settings, or you can move them to the database as well (Keys beginning in `pk_test_` and `pk_live_` will be detected as publishable keys). ## Deprecated features Nobody likes features being removed. However, the last few releases we have had to remove features that were not core to what dj-stripe does, or simply poorly-maintained. To keep up with the trend, we are making three major deprecations this release: ### Creating Plans from the Django Admin is no longer supported The `Plan` model was special cased in various places, including being the only one which supported being created from the Django administration. This is no longer supported. We have plans to allow creating arbitrary Stripe objects from the Django Admin, but until it can be done consistently, we have decided to remove the feature for Plans (which are deprecated by Stripe anyway). The only object type you should be dealing with from the admin is the new APIKey model. Along with this, we are also deprecating the `djstripe_sync_plans_from_stripe` management command. You can instead use the `djstripe_sync_models` management command, which supports arbitrary models. ### Deprecating the REST API We are dropping all support for the REST API and will be fully removing it in 2.5.0. We're doing this because we wish to keep such an API separate from dj-stripe. Work has already started on a new project, and we will be sharing more details about it soon. If you're interested in helping out, please reach out on [Github](https://github.com/dj-stripe/dj-stripe/issues/new)! ### Deprecating `djstripe.middleware.SubscriptionPaymentMiddleware` Large parts of dj-stripe, including this middleware, were designed before Stripe's major revamps of the old Plan model into Prices, Products, and multi-plan subscriptions. The functionality offered by the middleware is no longer adequate, and building on top of it would not be particularly robust. We may bring similar functionality back in the future, but the middleware as it is is going away (as well as the undocumented `djstripe.utils.subscriber_has_active_subscription` utility function). If you want to keep the functionality for your project, you may wish to [copy the latest version of the middleware](https://github.com/dj-stripe/dj-stripe/blob/2.4.0/djstripe/middleware.py). ### Deprecating `djstripe.mixins` This is being deprecated for similar reasons as the SubscriptionPaymentMiddleware. However, the mixins module was undocumented and never officially supported. ### Other deprecations - The `account` field on `Charge` has been renamed to `on_behalf_of`, to be consistent with Stripe's upstream model. Note that this field is separate from `djstripe_owner_account`, which is set by dj-stripe itself to match the account of the API key used. - `Account.get_connected_account_from_token()` is deprecated in favour of `Account.get_or_retrieve_for_api_key()`, which supports more than just Connect accounts. - `Customer.has_active_subscription()` is deprecated in favour of `Customer.is_subscribed_to()`. Note that the former takes a plan as argument, whereas the latter takes a product as argument. - The `tax_percent` attribute of `Invoice` is no longer populated and will be removed in 2.5.0. You may want to use `Invoice.default_tax_rates` instead, which uses the new TaxId functionality. - `Customer.business_vat_id` is being deprecated in favour of using TaxId models directly. ## Breaking changes - Rename PlanBillingScheme to BillingScheme. - Remove `Plan.update_name()` and these previously-deprecated fields: - `Customer.business_vat_id` - `Subscription.start` - `Subscription.billing` ## Upgrade Guide Before you upgrade to dj-stripe 2.4.0, we recommend upgrading to dj-stripe 2.3.0. Upgrading one major release at a time minimizes the risk of issues arising. Upgrading directly to 2.4.0 from dj-stripe versions older than 2.2.0 is unsupported. To upgrade dj-stripe, run `pip install --upgrade dj-stripe==2.4.0`. Once installed, you can run `manage.py migrate djstripe` to migrate the database models. !!! attention If you are doing multiple major dj-stripe upgrades in a row, remember to run the migrate command after every upgrade. Skipping this step WILL cause errors. !!! note Migrating the database models may take a long time on databases with large amounts of customers. ### Settings changes A new mandatory setting `DJSTRIPE_FOREIGN_KEY_TO_FIELD` has been added. If you are upgrading from an older version, you need to set it to `"djstripe_id"`. Setting it to `"id"` will make dj-stripe use the Stripe IDs as foreign keys. Although this is recommended for new installations, there is currently no migration available for going from `"djstripe_id"` to `"id"`. For more information on this setting, see [Settings](../reference/settings.md#djstripe_foreign_key_to_field). ================================================ FILE: docs/history/2_4_x.md ================================================ # dj-stripe 2.4.4 release notes (2021-05-22) - Fix syncing of tax IDs in management commands - Set `default_auto_field` in migrations to prevent creation of extra migrations - Misc test and documentation fixes # dj-stripe 2.4.3 release notes (2021-02-08) - Fix webhook error when processing events that contain a reference to a deleted payment method (such as a refund on a payment whose card has been detached or removed) - Fix a couple of regressions in `djstripe_sync_models` management command. # dj-stripe 2.4.2 release notes (2021-01-24) ## Release notes - Fix error in `Customer.add_card()` due to Stripe's `sources` deprecation. (#1293) - Fix `Subscription.update()` usage of the deprecated Stripe `prorate` argument. dj-stripe now explicitly uses `proration_behavior`, setting it to `"none"` when `prorate` is `False`, and `"create_prorations"` when `prorate` is `True`. # dj-stripe 2.4.1 release notes (2020-11-29) ## Release notes - Upgrade default Stripe API version to `2020-08-27`. Although we documented doing so in 2.4.0, it was not correctly set as such. This has been fixed for consistency. - The `Price` model was incorrectly released with an `amount_in_cents` property, matching that of the `Plan` model. However, Price amounts are already in cent. The property has been removed, use `unit_amount` instead. - Fix `Price.human_readable_price` calculation - Fix non-blank nullable `Charge` fields - Fix Price.tiers not being synced correctly with `djstripe_sync_models` (#1284) - Fix sync model recursion loop (see #1288) ================================================ FILE: docs/history/2_5_0.md ================================================ # dj-stripe 2.5.0 (2021-06-06) !!! attention It is not possible to upgrade to dj-stripe 2.5.0 from versions older than 2.2.2. To upgrade from an older version, first upgrade to `dj-stripe 2.2.2`. ## Release notes - Minimum Python version is now 3.6.2. - Support for Python 3.9 and Django 3.2. - In keeping with upstream's cycle, Django 3.0 is no longer officially supported. (Note that it will still work, because Django 2.2 LTS is still supported.) - SQLite versions older than 3.26 are no longer supported. - New models: FileLink, Mandate - Cards and Bank Accounts are now visible in the admin interface. - Lots of model sync fixes since 2.4.0. ## Deprecated features - The `FileUpload` model has been renamed `File`, for consistency with Stripe's SDK. Although the old name is still supported, it will eventually be removed. - Deprecate `charge_immediately` argument to `Customer.subscribe()`. It did not behave as expected on recent versions of Stripe. If you were using it set to `charge_immediately=False`, you can instead pass `collection_method="send_invoice"`, which will send the Customer the invoice to manually pay, instead. ## Breaking changes - When calling `Customer.delete()` in prior versions of dj-stripe, the Customer object would be deleted in the upstream API and the Customer object would be retained but with a `date_purged` attribute. This was the only model behaving this way, and it is no longer the case. If you wish to purge a customer like before, you may call `Customer.purge()` instead, though that method may be removed in future versions as well. - Remove deprecated DRF integration (`djstripe.contrib.rest_framework`) - Remove deprecated `djstripe.decorators` module - Remove deprecated `djstripe.middleware` module - Remove deprecated fields `Account.business_vat_id` and `Subscription.tax_percent` - Remove deprecated method `Account.get_connected_account_from_token()`. Use `Account.get_or_retrieve_for_api_key()` instead. - Remove deprecated `Charge.account` property. Use `Charge.on_behalf_of` instead. - Remove deprecated `Customer.has_active_subscription()` method. Use `Customer.is_subscribed_to(product)` instead. - `FileUploadPurpose` enum has been renamed `FilePurpose`. - `FileUploadType` enum has been renamed `FileType`. ================================================ FILE: docs/history/2_5_x.md ================================================ # dj-stripe 2.5.1 (2021-07-02) ## Release notes - Fixed migration issue for new setups using custom `DJSTRIPE_CUSTOMER_MODEL`. - Display correct JSON for JSONFields in the Django admin. - Fix manual syncing of `SubscriptionItem`. ================================================ FILE: docs/history/2_6_0.md ================================================ # dj-stripe 2.6.0 (2022-01-15) !!! attention It is not possible to upgrade to dj-stripe 2.6.0 from versions older than 2.3.0. To upgrade from an older version, first upgrade to `dj-stripe 2.3.0`. ## Release highlights - Support for Python 3.10 and Django 4.0. - New models: Mandate, Payout, UsageRecordSummary, WebhookEndpoint (unused) - Significant improvements and fixes to Stripe Connect features. - Storing Stripe API keys by adding them to the Admin is now supported. This allows for use of multiple Stripe API keys (multiple Stripe accounts). - Support for syncing Connect accounts via `djstripe_sync_models`. ## Deprecated features - The use of the old `jsonfield`-based `JSONField` is deprecated and support for it will be dropped in dj-stripe 2.8.0. `django.models.JSONField` is available since Django 3.1.0. To switch to the newer JSONFields, set `DJSTRIPE_USE_NATIVE_JSONFIELD` to `True`. Set it to `False` to remain on the `jsonfield`-powered text-based fields. A manual migration is necessary to convert existing databases from text to json. - The `DJSTRIPE_PRORATION_POLICY` setting is deprecated and will be ignored in 2.8. Specify `proration_policy` in the `Subscription.update()` method explicitly instead. - `Customer.can_charge()` is now deprecated. This was a very misleading method which resulted in incorrect behaviour when Customers had multiple payment methods. It will be removed in dj-stripe 2.8.0. You can use `Customer.payment_methods.all()` instead. - For similar reasons, `Customer.has_valid_source()` is deprecated and will be removed in dj-stripe 2.8.0. You can use `Customer.sources.all()` instead. ## Breaking changes - Python 3.6 is no longer supported. The new minimum version of Python is 3.7.12. - Django 2.2 and 3.1 are no longer supported. - `DJSTRIPE_USE_NATIVE_JSONFIELD` now defaults to `True`. If you previously had it set to `False`, or did not have it set, you may want to explicitly set it to `False` in order to support a pre-existing database. A migration path will later be provided for this use case. - The undocumented `get_stripe_api_version()` helper function has been removed. - Settings for dj-stripe are now in `djstripe.settings.djstripe_settings` (as opposed to top-level in `djstripe.settings`) - `Customer.subscribe()` method no longer accepts positional arguments, only keywords. - `charge_immediately` support in Customer.subscribe() has been removed (deprecated in 2.4). Set `collection_method` instead. - The `at_period_end` argument to `Subscription.cancel()` now defaults to `False`, instead of the value of `DJSTRIPE_PRORATION_POLICY`. ## Other changes - The Stripe Account that triggered an Event is now available on the field `WebhookEventTrigger.stripe_trigger_account`. - Fixed recursive fetch/update loop errors in `djstripe_sync_models`. - Migrations have been optimized and should be faster. - dj-stripe now checks the apparent validity of API keys used and will raise `InvalidStripeAPIKey` if the API key looks completely incorrect. - `Customers` can now be subscribed to multiple prices and/or plans by passing the `items` argument to `Customer.subscribe()`. - Checkout Session metadata can be used to create/link a Stripe `Customer` to the `Customer` instance specified by the `djstripe_settings.SUBSCRIBER_CUSTOMER_KEY`. ================================================ FILE: docs/history/2_6_x.md ================================================ # dj-stripe 2.6.2 (2022-07-02) This is a maintenance release to remove the generation of an unnecessary migration when running dj-stripe on Django 4.0. This release does not guarantee Django 4.0 compatibility. Run at your own risk. ## Release notes - Update migrations to be compatible with Django 4.0 # dj-stripe 2.6.1 (2022-02-07) ## Release notes - Fix issue saving a new WebhookEndpoint from the admin - Fix potential IntegrityError when syncing models ================================================ FILE: docs/history/2_7_0.md ================================================ # dj-stripe 2.7.0 (2022-10-17) !!! attention It is not possible to upgrade to dj-stripe 2.7.0 from versions older than 2.4.0. To upgrade from an older version, first upgrade to dj-stripe 2.4.0. This release focuses on Webhook Endpoints. For more information on the reasoning behind the changes, please see the discussion on Github: ## Release highlights - Webhook Endpoints are now configured via the Django administration. - Multiple Webhook Endpoints are now supported. - Webhook Endpoints now have a unique, non-guessable URL. ## Deprecated features - The `DJSTRIPE_WEBHOOK_URL` setting is deprecated. It will be removed in dj-stripe 2.9. It was added to give a way of "hiding" the webhook endpoint URL, but that is no longer necessary with the new webhook endpoint system. ## Breaking changes - Remove the deprecated middleware `djstripe.middleware.SubscriptionPaymentMiddleware` - Remove support for the deprecated `DJSTRIPE_SUBSCRIPTION_REDIRECT` setting - Remove support for the `DJSTRIPE_SUBSCRIPTION_REQUIRED_EXCEPTION_URLS` setting ## Other changes - Many Stripe Connect related fixes (Special thanks to Dominik Bartenstein of Zemtu) - Allow passing stripe kwargs in Subscription.cancel() - Various admin improvements - Add support for managing subscription schedules from the admin ================================================ FILE: docs/history/2_7_x.md ================================================ # dj-stripe 2.7.2 (2022-10-21) ## Release notes - Fix installing with Poetry on Django 4.0 and higher # dj-stripe 2.7.1 (2022-10-20) ## Release notes - Remove an enum value generating an extra migration - Allow Django 4.1 as a dependency (Note: Running dj-stripe 2.7.x with Django 4.1 is untested) ================================================ FILE: docs/history/2_8_0.md ================================================ # dj-stripe 2.8.0 (202X-XX-XX) !!! attention It is not possible to upgrade to dj-stripe 2.8.0 from versions older than 2.5.0. To upgrade from an older version, first upgrade to dj-stripe 2.5.0. ## Release highlights - Python 3.11 is now supported. - Django 4.1 is now supported. - Python 3.7 is no longer supported. Python 3.8 or higher is required. - Added `LineItem` model. - Added `Discount` model. - New webhook signals are available: - `djstripe.signals.webhook_pre_validate(instance, api_key)`: Fired before webhook validation - `djstripe.signals.webhook_post_validate(instance, api_key, valid)`: Fired after validation (even unsuccessful validations) - `djstripe.signals.webhook_pre_process(instance, api_key)`: Fired before webhook processing. Not fired if the validation failed. - `djstripe.signals.webhook_post_process(instance, api_key)`: Fired after webhook successful processing. - `djstripe.signals.webhook_processing_error` now also takes `instance` and `api_key` arguments - `stripe.api_version` is no longer manipulated by dj-stripe. - Resolved ambiguity between `LineItem` and `InvoiceItem` models. It was incorrectly assumed that the `lines` List object on `Invoice` and `UpcomingInvoice` models only return `InvoiceItem` objects. Moreover `LineItem` objects can also be of type `subscription` if the user adds a Subscription to their `Invoice` as a lineitem. ## Deprecated features - `DJSTRIPE_WEBHOOK_EVENT_CALLBACK` is deprecated in favour of the new webhook signals. ## Breaking changes - Remove legacy JSONField support. This drops support for installations with the `DJSTRIPE_USE_NATIVE_JSONFIELD` setting set to `False`. NOTE: No migration path is available yet. https://github.com/dj-stripe/dj-stripe/issues/1820 - Remove `djstripe_sync_plans_from_stripe` command (deprecated in 2.4.0). Use `djstripe_sync_models price` instead. - Remove `Customer.can_charge()`, `Customer.has_valid_source()` () - Remove `DJSTRIPE_PRORATION_POLICY` setting (deprecated in 2.6.0) - Remove deprecated `prorate` argument to `Subscription.update()` (Use Stripe's `proration_behavior` argument instead) - Remove undocumented `set_stripe_api_version()` helper function and context manager `stripe_temporary_api_version()`. The API version is now set on each request individually. ## Other changes - Updated `check_stripe_api_key` django system check to not be a blocker for new dj-stripe users by raising Info warnings on the console. If the Stripe keys were not defined in the settings file, the `Critical` warning was preventing users to add them directly from the admin as mentioned in the docs. This was creating a chicken-egg situation where one could only add keys in the admin before they were defined in settings. - `check_stripe_api_key` will raise appropriate warnings on the console directing users to add keys directly from the django admin. - Swapped Critical Error to Info for `_check_webhook_endpoint_validation` check to allow the users to use the django admin. - `LineItem` instances can also get synced using the `djstripe_sync_models` management command. ================================================ FILE: docs/history/2_x.md ================================================ # dj-stripe 2.0 ~ 2.3 release notes ## 2.3.0 (2020-04-19) - The minimum version of Django is now 2.1, and Python 3.6. - Changed `JSONField` dependency back to [jsonfield](https://github.com/rpkilby/jsonfield/) from [jsonfield2](https://github.com/rpkilby/jsonfield2/) (see [Warning about safe uninstall of jsonfield2 on upgrade](#warning-about-safe-uninstall-of-jsonfield2-on-upgrade)). - Fixed handling of `TaxRate` events (#1094). - Fixed pagination issue in `Invoice.sync_from_stripe_data` (#1052). - Fixed pagination issues in `Subscription` & `Charge` `.sync_from_stripe_data` (#1054). - Tidyup `_stripe_object_set_total_tax_amounts` unique handling (#1139). - Dropped previously-deprecated `Invoice` fields (see ): - `.closed` - `.forgiven` - `.billing` (renamed to `.collection_method`) - Dropped previously-deprecated `enums.InvoiceStatus` (#1020). - Deprecated the following fields - will be removed in 2.4 (#1087): - `Subscription.billing` (use `.collection_method` instead) - `Subscription.start` (use `.start_date` instead) - `Subscription.tax_percent` (use `.default_tax_rates` instead) - Added `Invoice.status` and `enums.InvoiceStatus` (#1020). - Added new `Invoice` fields (\#1020, \#1087): - `.discount` - `.default_source` - `.status` - Added new `Subscription` fields (\#1087): - `.default_payment_method` - `.default_source` - `.next_pending_invoice_item_invoice` - `.pending_invoice_item_interval` - `.pending_update` - `.start_date` ### Warning about safe uninstall of jsonfield2 on upgrade Both **jsonfield** and **jsonfield2** use the same import path, so if upgrading from dj-stripe\~=2.2.0 in an existing virtualenv, be sure to uninstall jsonfield2 first. eg: ```bash # ensure jsonfield is uninstalled before we install jsonfield2 pip uninstall jsonfield2 -y && pip install "dj-stripe>=2.3.0dev" ``` Otherwise, `pip uninstall jsonfield2` will remove jsonfield's `jsonfield` module from `site-packages`, which would cause errors like `ImportError: cannot import name 'JSONField' from 'jsonfield' (unknown location)` If you have hit this ImportError already after upgrading, running this should resolve it: ```bash # remove both jsonfield packages before reinstall to fix ImportError: pip uninstall jsonfield jsonfield2 -y && pip install "dj-stripe>=2.3.0" ``` Note that this is only necessary if upgrading from dj-stripe 2.2.x, which temporarily depended on jsonfield2. This process is not necessary if upgrading from an earlier version of dj-stripe. ## 2.2.2 (2020-01-20) This is a bugfix-only version: - Fixed handling of `TaxRate` events (#1094). ## 2.2.1 (2020-01-14) This is a bugfix-only version: - Fixed bad package build. ## 2.2.0 (2020-01-13) - Changed `JSONField` dependency package from [jsonfield](https://github.com/rpkilby/jsonfield/) to [jsonfield2](https://github.com/rpkilby/jsonfield2/), for Django 3 compatibility (see [Warning about safe uninstall of jsonfield on upgrade](#warning-about-safe-uninstall-of-jsonfield-on-upgrade)). Note that Django 2.1 requires jsonfield<3.1. - Added support for Django 3.0 (requires jsonfield2>=3.0.3). - Added support for python 3.8. - Refactored `UpcomingInvoice`, so it's no longer a subclass of `Invoice` (to allow `Invoice` to use `ManyToManyFields`). - Dropped previously-deprecated `Account` fields (see ): - `.business_name` - `.business_primary_color` - `.business_url` (changed to a property) - `.debit_negative_balances` - `.decline_charge_on` - `.display_name` - `.legal_entity` - `.payout_schedule` - `.payout_statement_descriptor` - `.statement_descriptor` - `.support_email` - `.support_phone` - `.support_url` - `.timezone` - `.verification` - Dropped previously-deprecated `Account.business_logo` property (renamed to `.branding_icon`) - Dropped previously-deprecated `Customer.account_balance` property (renamed to `.balance`) - Dropped previously-deprecated properties `Invoice.application_fee`, `Invoice.date` - Dropped previously-deprecated enum `PaymentMethodType` (use `DjstripePaymentMethodType` instead) - Renamed `Invoice.billing` to `.collection_method` (added deprecated property for the old name). - Updated `Invoice` model to add missing fields. - Added `TaxRate` model, and `Invoice.default_tax_rates`, `InvoiceItem.tax_rates`, `Invoice.total_tax_amounts`, `Subscription.default_tax_rates`, `SubscriptionItem.tax_rates` (#1027). - Change urls.py to use the new style urls. - Update forward relation fields in the admin to be raw id fields. - Updated `StripeQuantumCurrencyAmountField` and `StripeDecimalCurrencyAmountField` to support Stripe Large Charges (#1045). - Update event handling so `customer.subscription.deleted` updates subscriptions to `status="canceled"` instead of deleting it from our database, to match Stripe's behaviour (#599). - Added missing `Refund.reason` value, increases field width (#1075). - Fixed `Refund.status` definition, reduces field width (#1076). - Deprecated non-standard `Invoice.status` (renamed to `Invoice.legacy_status`) to make way for the Stripe field (preparation for #1020). ### Warning about safe uninstall of jsonfield on upgrade Both **jsonfield** and **jsonfield2** use the same import path, so if upgrading to dj-stripe>=2.2 in an existing virtualenv, be sure to uninstall jsonfield first. eg: ```bash # ensure jsonfield is uninstalled before we install jsonfield2 pip uninstall jsonfield -y && pip install "dj-stripe>=2.2.0" ``` Otherwise, `pip uninstall jsonfield` will remove jsonfield2's `jsonfield` module from `site-packages`, which would cause errors like `ImportError: cannot import name 'JSONField' from 'jsonfield' (unknown location)` If you have hit this ImportError already after upgrading, running this should resolve it: ```bash # remove both jsonfield packages before reinstall to fix ImportError: pip uninstall jsonfield jsonfield2 -y && pip install "dj-stripe>=2.2.0" ``` ### Note on usage of Stripe Elements JS See [Integrating Stripe Elements](https://dj-stripe.github.io/dj-stripe/en/master/stripe_elements_js/) for notes about usage of the Stripe Elements frontend JS library. In summary: If you haven't yet migrated to PaymentIntents, prefer `stripe.createSource()` to `stripe.createToken()`. ## 2.1.1 (2019-10-01) This is a bugfix-only release: - Updated webhook signals list (#1000). - Fixed issue syncing PaymentIntent with destination charge (#960). - Fixed `Customer.subscription` and `.valid_subscriptions()` to ignore `status=incomplete_expired` (#1006). - Fixed error on `paymentmethod.detached` event with `card_xxx` payment methods (#967). - Added `PaymentMethod.detach()` (#943). - Updated `help_text` on all currency fields to make it clear if they're holding integer cents (`StripeQuantumCurrencyAmountField`) or decimal dollar (or euro, pound etc) (`StripeDecimalCurrencyAmountField`) (#999) - Documented our preferred Django model field types (#986) ### Upcoming migration of currency fields (storage as cents instead of dollars) Please be aware that we're looking at standardising our currency storage fields as integer quanta (cents) instead of Decimal (dollar) values, to match stripe. This is intended to be part of the 3.0 release, since it will involve some breaking changes. See \#955 for details and discussion. ## 2.1.0 (2019-09-12) - Dropped Django 2.0 support - The Python stripe library minimum version is now `2.32.0`, also `2.36.0` is excluded due to a regression (#991). - Dropped previously-deprecated `Charge.fee_details` property. - Dropped previously-deprecated `Transfer.fee_details` property. - Dropped previously-deprecated `field_name` parameter to `sync_from_stripe_data` - Dropped previously-deprecated alias `StripeObject` of `StripeModel` - Dropped previously-deprecated alias `PaymentMethod` of `DjstripePaymentMethod` - Dropped previously-deprecated properties `Charge.source_type` and `Charge.source_stripe_id` - `enums.PaymentMethodType` has been deprecated, use `enums.DjstripePaymentMethodType` - Made `SubscriptionItem.quantity` nullable as per Plans with `usage_type="metered"` (follow-up to #865) - Added manage commands `djstripe_sync_models` and `djstripe_process_events` (#727, #89) - Fixed issue with re-creating a customer after `Customer.purge()` (#916) - Fixed sync of Customer Bank Accounts (#829) - Fixed `Subscription.is_status_temporarily_current()` (#852) - New models - Payment Intent - Setup Intent - Payment Method - Session - Added fields to `Customer` model: `address`, `invoice_prefix`, `invoice_settings`, `phone`, `preferred_locales`, `tax_exempt` Changes from API 2018-11-08: - Added `Invoice.auto_advance`, deprecated `Invoice.closed` and `Invoice.forgiven`, see Changes from API 2019-02-19: - Major changes to Account fields, see , updated Account fields to match API 2019-02-19: - Added `Account.business_profile`, `.business_type`, `.company`, `.individual`, `.requirements`, `.settings` - Deprecated the existing fields, to be removed in 2.2 - Special handling of the icon and logo fields: > - Renamed `Account.business_logo` to `Account.branding_icon` > (note that in Stripe's API `Account.business_logo` was renamed > to `Account.settings.branding_icon`, and > `Account.business_logo_large` (which we didn't have a field > for) was renamed to `Account.settings.branding_logo`) > - Added deprecated property for `Account.business_logo` > - Added `Account.branding_logo` as a ForeignKey > - Populate `Account.branding_icon` and `.branding_logo` from the > new `Account.settings.branding.icon` and `.logo` Changes from API 2019-03-14: - Renamed `Invoice.application_fee` to `Invoice.application_fee_amount` (added deprecated property for the old name) - Removed `Invoice.date`, in place of `Invoice.created` (added deprecated property for the old name) - Added `Invoice.status_transitions` - Renamed `Customer.account_balance` to `Customer.balance` (added deprecated property for the old name) - Renamed `Customer.payment_methods` to `Customer.customer_payment_methods` - Added new `SubscriptionStatus.incomplete` and `SubscriptionStatus.incomplete_expired` statuses (#974) - Added new `BalanceTransactionType` values (#983) ### Squashed dev migrations As per our [migration policy](../project/contributing.md#django_migration_policy), unreleased migrations on the master branch have been squashed. If you have been using the 2.1.0dev branch from master, you'll need to run the squashed migrations migrations before upgrading to >=2.1.0. The simplest way to do this is to `pip install dj-stripe==2.1.0rc0` and migrate, alternatively check out the `2.1.0rc0` git tag. ## 2.0.5 (2019-09-12) This is a bugfix-only version: - Avoid stripe==2.36.0 due to regression (#991) ## 2.0.4 (2019-09-09) This is a bugfix-only version: - Fixed irreversible migration (#909) ## 2.0.3 (2019-06-11) This is a bugfix-only version: - In `_get_or_create_from_stripe_object`, wrap create `_create_from_stripe_object` in transaction, fixes `TransactionManagementError` on race condition in webhook processing (#877, #903). ## 2.0.2 (2019-06-09) This is a bugfix-only version: - Don't save event objects if the webhook processing fails (#832). - Fixed IntegrityError when `REMOTE_ADDR` is an empty string. - Deprecated `field_name` parameter to `sync_from_stripe_data` ## 2.0.1 (2019-04-29) This is a bugfix-only version: - Fixed an error on `invoiceitem.updated` (#848). - Handle test webhook properly in recent versions of Stripe API (#779). At some point 2018 Stripe silently changed the ID used for test events and `evt_00000000000000` is not used anymore. - Fixed OperationalError seen in migration 0003 on postgres (#850). - Fixed issue with migration 0003 not being unapplied correctly (#882). - Fixed missing `SubscriptionItem.quantity` on metered Plans (#865). - Fixed `Plan.create()` (#870). ## 2.0.0 (2019-03-01) - The Python stripe library minimum version is now `2.3.0`. - `PaymentMethod` has been renamed to `DjstripePaymentMethod` (#841). An alias remains but will be removed in the next version. - Dropped support for Django<2.0, Python<3.4. - Dropped previously-deprecated `stripe_objects` module. - Dropped previously-deprecated `stripe_timestamp` field. - Dropped previously-deprecated `Charge.receipt_number` field. - Dropped previously-deprecated `StripeSource` alias for `Card` - Dropped previously-deprecated `SubscriptionView`, `CancelSubscriptionView` and `CancelSubscriptionForm`. - Removed the default value from `DJSTRIPE_SUBSCRIPTION_REDIRECT`. - All `stripe_id` fields have been renamed `id`. - `Charge.source_type` has been deprecated. Use `Charge.source.type`. - `Charge.source_stripe_id` has been deprecated. Use `Charge.source.id`. - All deprecated Transfer fields (Stripe API 2017-04-06 and older), have been dropped. This includes `date`, `destination_type` (`type`), `failure_code`, `failure_message`, `statement_descriptor` and `status`. - Fixed IntegrityError when `REMOTE_ADDR` is missing (#640). - New models: - `ApplicationFee` - `ApplicationFeeRefund` - `BalanceTransaction` - `CountrySpec` - `ScheduledQuery` - `SubscriptionItem` - `TransferReversal` - `UsageRecord` - The `fee` and `fee_details` attributes of both the `Charge` and `Transfer` objects are no longer stored in the database. Instead, they access their respective new `balance_transaction` foreign key. Note that `fee_details` has been deprecated on both models. - The `fraudulent` attribute on `Charge` is now a property that checks the `fraud_details` field. - Object key validity is now always enforced (\#503). - `Customer.sources` no longer refers to a Card queryset, but to a Source queryset. In order to correctly transition, you should change all your references to `customer.sources` to `customer.legacy_cards` instead. The `legacy_cards` attribute already exists in 1.2.0. - `Customer.sources_v3` is now named `Customer.sources`. - A new property `Customer.payment_methods` is now available, which allows you to iterate over all of a customer's payment methods (sources then cards). - `Card.customer` is now nullable and cards are no longer deleted when their corresponding customer is deleted (#654). - Webhook signature verification is now available and is preferred. Set the `DJSTRIPE_WEBHOOK_SECRET` setting to your secret to start using it. - `StripeObject` has been renamed `StripeModel`. An alias remains but will be removed in the next version. - The metadata key used in the `Customer` object can now be configured by changing the `DJSTRIPE_SUBSCRIBER_CUSTOMER_KEY` setting. Setting this to None or an empty string now also disables the behaviour altogether. - Text-type fields in dj-stripe will no longer ever be None. Instead, any falsy text field will return an empty string. - Switched test runner to pytest-django - `StripeModel.sync_from_stripe_data()` will now automatically retrieve related objects and populate foreign keys (#681) - Added `Coupon.name` - Added `Transfer.balance_transaction` - Exceptions in webhooks are now re-raised as well as saved in the database (#833) ================================================ FILE: docs/installation.md ================================================ ## Installation ### Get the distribution Install dj-stripe with pip: ```bash pip install dj-stripe ``` Or with [Poetry](https://python-poetry.org/) (recommended): ```bash poetry add dj-stripe ``` ### Configuration Add `djstripe` to your `INSTALLED_APPS`: ```bash INSTALLED_APPS =( ... "djstripe", ... ) ``` Add to urls.py: ```bash path("stripe/", include("djstripe.urls", namespace="djstripe")), ``` Tell Stripe about the webhook (Stripe webhook docs can be found [here](https://stripe.com/docs/webhooks)) using the full URL of your endpoint from the urls.py step above (e.g. `https://example.com/stripe/webhook`). Add your Stripe keys and set the operating mode: ```bash STRIPE_LIVE_SECRET_KEY = os.environ.get("STRIPE_LIVE_SECRET_KEY", "") STRIPE_TEST_SECRET_KEY = os.environ.get("STRIPE_TEST_SECRET_KEY", "") STRIPE_LIVE_MODE = False # Change to True in production DJSTRIPE_WEBHOOK_SECRET = "whsec_xxx" # Get it from the section in the Stripe dashboard where you added the webhook endpoint DJSTRIPE_FOREIGN_KEY_TO_FIELD = "id" ``` !!! note djstripe expects `STRIPE_LIVE_MODE` to be a Boolean Type. In case you use `Bash env vars or equivalent` to inject its value, make sure to convert it to a Boolean type. We highly recommended the library [django-environ](https://django-environ.readthedocs.io/en/latest/) Sync data from Stripe: !!! note djstripe expects `APIKeys` of all Stripe Accounts you'd like to sync data for to already be in the DB. They can be Added from Django Admin. Run the commands: ```bash python manage.py migrate python manage.py djstripe_sync_models ``` See [here](stripe_elements_js.md#integrating_stripe_elements-js_sdk) for notes about usage of the Stripe Elements frontend JS library. ### Running Tests Assuming the tests are run against PostgreSQL: ```bash createdb djstripe pip install tox tox ``` ================================================ FILE: docs/project/authors.md ================================================ # Credits ## Core contributors - [Alexander Kavanaugh](https://github.com/kavdev) (Co-maintainer) - [Jerome Leclanche](https://github.com/jleclanche) (Co-maintainer) - [Arnav Choudhury](https://github.com/arnav13081994) ## Former core contributors - [John Carter](https://github.com/therefromhere) - [Pablo Castellano](https://github.com/PabloCastellano) - [Daniel Greenfeld](https://github.com/pydanny) - [Lee Skillen](https://github.com/lskillen) ## Contributors dj-stripe is brought to you by many more open source contributors. [See the complete list on Github](https://github.com/dj-stripe/dj-stripe/graphs/contributors). ================================================ FILE: docs/project/release_process.md ================================================ # Release Process !!! note Before `MAJOR` or `MINOR` releases: - Review deprecation notes (eg search for "deprecated") and remove deprecated features as appropriate - Squash migrations (ONLY on unreleased migrations) - see below ## Squash migrations If there's more than one unreleased migration on master consider squashing them with `squashmigrations`, immediately before tagging the new release: - Create a new squashed migration with `./manage.py squashmigrations` (only squash migrations that have never been in a tagged release) - Commit the squashed migration on master with a commit message like "Squash x.y.0dev migrations" (this will allow users who running master to safely upgrade, see note below about rc package) - Then transition the squashed migration to a normal migration as per Django: - Delete all the migration files it replaces - Update all migrations that depend on the deleted migrations to depend on the squashed migration instead - Remove the `replaces` attribute in the Migration class of the squashed migration (this is how Django tells that it is a squashed migration) - Commit these changes to master with a message like "Transition squashed migration to normal migration" - Then do the normal release process - bump version as another commit and tag the release See ## Tag + package squashed migrations as rc package (optional) As a convenience to users who are running master, an rc version can be created to package the squashed migration. To do this, immediately after the "Squash x.y.0dev migrations" commit, follow the steps below but with a x.y.0rc0 version to tag and package a rc version. Users who have been using the x.y.0dev code from master can then run the squashed migrations migrations before upgrading to >=x.y.0. The simplest way to do this is to `pip install dj-stripe==x.y.0rc0` and migrate, or alternatively check out the `x.y.0rc0` git tag and migrate. ## Prepare changes for the release commit - Choose your version number (using ) - if there's a new migration, it should be a `MAJOR.0.0` or `MAJOR.MINOR.0` version. - Review and update `HISTORY.md` - Add a section for this release version - Set date on this release version - Check that summary of feature/fixes is since the last release is up to date - Update package version number in `setup.cfg` - Review and update supported API version in `README.md` (this is the most recent Stripe account version tested against, not `DEFAULT_STRIPE_API_VERSION`) - `git add` to stage these changes ## Create signed release commit tag !!! note Before doing this you should have a GPG key set up on github If you don't have a GPG key already, one method is via , and then add it to your github profile. - Create a release tag with the above staged changes (where `$VERSION` is the version number to be released: $ git commit -m "Release $VERSION" $ git tag -fsm "Release $VERSION" $VERSION This can be expressed as a bash function as follows: git_release() { git commit -m "Release $1" && git tag -fsm "Release $1" $1; } - Push the commit and tag: $ git push --follow-tags ## Update/create stable branch Push these changes to the appropriate `stable/MAJOR.MINOR` version branch (eg `stable/2.0`) if they're not already - note that this will trigger the readthedocs build ## Release on pypi See ================================================ FILE: docs/project/sponsors.md ================================================ # Sponsors ## Gold Sponsors [![Stripe Logo](../logos/stripe_blurple.svg)](https://stripe.com) This project is sponsored by none other than [Stripe](https://stripe.com/), since August 2020. We're thankful for their contribution, which has allowed us to greatly improve the project, increase QA and testing, and push forward with the dj-stripe 2.4.0 release. ## Silver Sponsors We do not currently have any Silver sponsors. [Want to be the first?](https://github.com/sponsors/dj-stripe) ================================================ FILE: docs/project/support.md ================================================ # Support ## Support plans dj-stripe offers paid support plans via Github Sponsors: All issues and feature requests raised by corporate sponsors will be prioritized. Gold Sponsors also get a dedicated developer point of contact, to help with any questions, issues, or general inquiries relating to dj-stripe. ## Bug reports and feature requests Please report any issues you come across [on the Github issue tracker](https://github.com/dj-stripe/dj-stripe/issues). Do not hesitate to leave feedback and suggestions there as well. You may also ask usage questions on the issue tracker. ================================================ FILE: docs/project/test_fixtures.md ================================================ # Test Fixtures dj-stripe's unit tests rely on fixtures to represent Stripe API and webhook data. ## Rationale These fixtures are partly hand-coded and partly generated by creating objects in Stripe and then retrieved via the API. Each approach has pros and cons: Hand-coding the fixtures allows them to be crafted specifically for a test case. They can also be terse, and nested objects can be done by reference to avoid duplication. But maintaining or upgrading them is a painstaking manual process. Generating the fixtures via Stripe gives the big advantage that Stripe schema changes are automatically represented in the fixtures, which should allow us to upgrade dj-stripe's schema to match Stripe much more easily. This would be done by updating dj-stripe's targeted API version (`DEFAULT_STRIPE_API_VERSION`), regenerating the fixtures, and updating the model to match the fixture changes. The down side is it's tricky to regenerate fixture files without introducing big changes (eg to object ids) - the script does this by mapping a dummy id to various objects. ## Regenerating the test fixtures To regenerate the test fixtures (e.g. to populate the fixtures with new API fields from Stripe), do the following: 1. (one time only) Create a new Stripe account called "dj-stripe scratch", with country set to United States. (we use US so the currency matches the existing fixtures matches, in the future it would be good to test for other countries). 2. If you already had this account ready and want to start again from scratch, you can delete all of the test data via the button in Settings > Data 3. Activate a virtualenv with the dj-stripe project (see Getting Started) 4. Set the dj-stripe secret key environment variable to the secret key for this account (`export STRIPE_SECRET_KEY=sk_test_...`) 5. Run the manage command to create the test objects in your stripe account if they don't already exist, and regenerate the local fixture files from them: $ ./manage.py regenerate_test_fixtures The command tries to avoid inconsequential changes to the fixtures (e.g the `created` timestamp) by restoring a whitelist of values from the existing fixtures. This functionality can be disabled by passing `--update-sideeffect-fields`. ================================================ FILE: docs/reference/enums.md ================================================ # Enumerations ::: djstripe.enums ================================================ FILE: docs/reference/managers.md ================================================ # Managers ::: djstripe.managers ================================================ FILE: docs/reference/models.md ================================================ # Models Models hold the bulk of the functionality included in the dj-stripe package. Each model is tied closely to its corresponding object in the stripe dashboard. Fields that are not implemented for each model have a short reason behind the decision in the docstring for each model. ## Core Resources ::: djstripe.models.core ## Payment Methods ::: djstripe.models.payment_methods selection: filters: ["!LegacySourceMixin$", "!DjstripePaymentMethod$"] ## Billing ::: djstripe.models.billing selection: filters: ["!DjstripeInvoiceTotalTaxAmount$", "!DjstripeUpcomingInvoiceTotalTaxAmount$", "!BaseInvoice$"] ## Connect ::: djstripe.models.account ::: djstripe.models.connect ## Fraud ::: djstripe.models.fraud ## Orders ::: djstripe.models.orders ## Sigma ::: djstripe.models.sigma ## Webhooks ::: djstripe.models.webhooks ================================================ FILE: docs/reference/project.md ================================================ ::: djstripe ::: tests ================================================ FILE: docs/reference/settings.md ================================================ # Settings ## STRIPE_API_VERSION (='2020-08-27') The API version used to communicate with the Stripe API is configurable, and defaults to the latest version that has been tested as working. Using a value other than the default is allowed, as a string in the format of YYYY-MM-DD. For example, you can specify `"2020-03-02"` to use that API version: ```py STRIPE_API_VERSION = "2020-03-02" ``` However you do so at your own risk, as using a value other than the default might result in incompatibilities between Stripe and this library, especially if Stripe has labelled the differences between API versions as "Major". Even small differences such as a new enumeration value might cause issues. For this reason it is best to assume that only the default version is supported. For more information on API versioning, see the [stripe documentation](https://stripe.com/docs/upgrades). See also [API Versions](../api_versions.md#a_note_on_stripe_api_versions). ## DJSTRIPE_FOREIGN_KEY_TO_FIELD _(Introduced in 2.4.0)_ `DJSTRIPE_FOREIGN_KEY_TO_FIELD` is a setting introduced in dj-stripe version 2.4.0. You are required to set it in 2.4.0: It does not have a default value. In 3.0.0, the default will be "id", and we recommend setting it to "id" for new installations. Older installations should set it to "djstripe_id". Explanation below. In dj-stripe 2.3 and before, foreign keys for Stripe models were set to point to the foreign model's djstripe_id field, a numeric integer generated by the local database. This new setting allows dj-stripe users to change it to use the "id" field, which is the upstream, non-numeric Stripe identifier. When using the Stripe identifier as a foreign key, synchronization between Stripe and dj-stripe can be made far more efficient and robust. Furthermore, it removes the per-installation instability of a critical value. The plan is to get rid of djstripe_id altogether for the 3.0 release (we may retain the field itself until 4.0, but it will no longer be a primary key). **How to migrate older installations from "djstripe_id" to "id"?** Such a migration path has not been designed at the moment. Currently if you want to switch an older installation to "id", the easiest way is to wipe the djstripe db and sync again from scratch. This is obviously not ideal, and we will design a proper migration path before 3.0. ## DJSTRIPE_IDEMPOTENCY_KEY_CALLBACK (=djstripe.settings.djstripe_settings.\_get_idempotency_key) A function which will return an idempotency key for a particular object_type and action pair. By default, this is set to a function which will create a `djstripe.IdempotencyKey` object and return its `uuid`. You may want to customize this if you want to give your idempotency keys a different lifecycle than they normally would get. The function takes the following signature: ```py def get_idempotency_key(object_type: str, action: str, livemode: bool): return "" ``` The function MUST return a string suitably random for the object_type/action pair, and usable in the Stripe `Idempotency-Key` HTTP header. For more information, see the [stripe documentation](https://stripe.com/docs/upgrades). ## DJSTRIPE_SUBSCRIBER_CUSTOMER_KEY (="djstripe_subscriber") Every Customer object created in Stripe is tagged with [metadata](https://stripe.com/docs/api#metadata) This setting controls what the name of the key in Stripe should be. The key name must be a string no more than 40 characters long. You may set this to `None` or `""` to disable that behaviour altogether. This is probably not something you want to do, though. ## DJSTRIPE_SUBSCRIBER_MODEL (=settings.AUTH_USER_MODEL) If the AUTH_USER_MODEL doesn't represent the object your application's subscription holder, you may define a subscriber model to use here. It should be a string in the form of 'app.model'. !!! note DJSTRIPE_SUBSCRIBER_MODEL must have an `email` field. If your existing model has no email field, add an email property that defines an email address to use. Example Model: ```py class Organization(models.Model): name = CharField(max_length=200, unique=True) admin = ForeignKey(settings.AUTH_USER_MODEL, on_delete=CASCADE) @property def email(self): return self.admin.email ``` ## DJSTRIPE_SUBSCRIBER_MODEL_MIGRATION_DEPENDENCY (="\_\_first\_\_") If the model referenced in DJSTRIPE_SUBSCRIBER_MODEL is not created in the `__first__` migration of an app you can specify the migration name to depend on here. For example: "0003_here_the_subscriber_model_was_added" ## DJSTRIPE_WEBHOOK_URL (=r"^webhook/$") !!! warning This setting is deprecated and will be removed in dj-stripe 2.9. This is where you can tell Stripe to send webhook responses. You can set this to what you want to prevent unnecessary hijinks from unfriendly people. As this is embedded in the URLConf, this must be a resolvable regular expression. ## DJSTRIPE_WEBHOOK_SECRET (="") If this is set to a non-empty value, webhook signatures will be verified. [Learn more about webhook signature verification](https://stripe.com/docs/webhooks/signatures). ## DJSTRIPE_WEBHOOK_VALIDATION= (="verify_signature") This setting controls which type of validation is done on webhooks. Value can be `"verify_signature"` for signature verification (recommended default), `"retrieve_event"` for event retrieval (makes an extra HTTP request), or `None` for no validation at all. ## DJSTRIPE_WEBHOOK_TOLERANCE (=300) Controls the milliseconds tolerance which wards against replay attacks. Leave this to its default value unless you know what you're doing. ## DJSTRIPE_WEBHOOK_EVENT_CALLBACK (=None) Webhook event callbacks allow an application to take control of what happens when an event from Stripe is received. It must be a callable or importable string to a callable that takes an event object. One suggestion is to put the event onto a task queue (such as celery) for asynchronous processing. Examples: ```py # callbacks.py def webhook_event_callback(event, api_key): """ Dispatches the event to celery for processing. """ from . import tasks # Ansychronous hand-off to celery so that we can continue immediately tasks.process_webhook_event.s(event.pk).apply_async() ``` ```py # tasks.py from djstripe.models import WebhookEventTrigger from stripe.error import StripeError @shared_task(bind=True) def process_webhook_event(self, pk): """ Processes events from Stripe asynchronously. """ logger.info(f"Processing Stripe event: {pk}") try: # get the event obj = WebhookEventTrigger.objects.get(pk=pk) # process the event. # internally, this creates a Stripe WebhookEvent Object and invokes the respective Webhooks event = obj.process() except StripeError as exc: logger.error(f"Failed to process Stripe event: {pk}. Retrying in 60 seconds.") raise self.retry(exc=exc, countdown=60) # retry after 60 seconds except WebhookEventTrigger.DoesNotExist as exc: # This can happen in case the celery task got executed before the actual model got saved to the DB raise self.retry(exc=exc, countdown=10) # retry after 10 seconds return event.type or "Stripe Event Processed" ``` ```py # settings.py DJSTRIPE_WEBHOOK_EVENT_CALLBACK = 'callbacks.webhook_event_callback' ``` ## STRIPE_API_HOST (= unset) If set, this sets the base API host for Stripe. You may want to set this to, for example, `"http://localhost:12111"` if you are running [stripe-mock](https://github.com/stripe/stripe-mock). If this is set in production (DEBUG=False), a warning will be raised on `manage.py check`. ## Source Code ::: djstripe.settings selection: filters: - "!^_[^_]" ================================================ FILE: docs/reference/utils.md ================================================ # Utilities ::: djstripe.utils ================================================ FILE: docs/stripe_elements_js.md ================================================ # Integrating Stripe Elements (JS SDK) !!! tip TLDR: If you haven't yet migrated to PaymentIntents, prefer `stripe.createSource()` over `stripe.createToken()` for better compatibility with PaymentMethods. !!! attention A point that can cause confusion when integrating Stripe on the web is that there are multiple generations of frontend JS APIs that use Stripe Elements with stripe js v3. ## In descending order of preference these are: ### [Payment Intents](https://stripe.com/docs/payments/payment-intents) (SCA compliant) The newest and preferred way of handling payments, which supports SCA compliance (3D secure etc). ### [Charges using stripe.createSource()](https://stripe.com/docs/js/tokens_sources/create_source) This creates Source objects within Stripe, and can be used for various different methods of payment (including, but not limited to cards), but isn't SCA compliant. The [Card Elements Quickstart JS](https://stripe.com/docs/payments/accept-a-payment-charges?platform=web) example can be used, except use `stripe.createSource` instead of `stripe.createToken` and the `result.source` instead of `result.token`. [`Checkout a working example of this`][tests.apps.example.views.PurchaseSubscriptionView] ### Charges using stripe.createToken() This predates `stripe.createSource`, and creates legacy Card objects within Stripe, which have some compatibility issues with Payment Methods. If you're using `stripe.createToken`, see if you can upgrade to `stripe.createSource` or ideally to Payment Intents . !!! tip Checkout [Card Elements Quickstart JS](https://stripe.com/docs/payments/accept-a-payment-charges?platform=web) ================================================ FILE: docs/usage/creating_individual_charges.md ================================================ # Creating individual charges On the subscriber's customer object, use the [`charge`][djstripe.models.core.Customer.charge] method to generate a Stripe charge. In this example, we're using the user named `admin` as the subscriber. ```python from decimal import Decimal from django.contrib.auth import get_user_model from djstripe.models import Customer user = get_user_model().objects.get(username="admin") customer, created = Customer.get_or_create(subscriber=user) customer.charge(Decimal("10.00"), currency="usd") # Create charge for 10.00 USD ``` ================================================ FILE: docs/usage/creating_usage_record.md ================================================ # Create a Stripe Usage Record Usage records allow you to report customer usage and metrics to Stripe for metered billing of subscription prices Usage records created using Djstripe's [`UsageRecord.create()`][djstripe.models.billing.UsageRecord.create] method will both create and sync the created `UsageRecord` object with your db. !!! note UsageRecord objects created directly will not sync because Stripe does not expose a way to retrieve UsageRecord objects directly. They can thus only be synced at creation time. ## Code: ```python from djstripe.models import UsageRecord # create and sync UsageRecord object UsageRecord.create(id=, quantity=, timestamp=timestamp) ``` ================================================ FILE: docs/usage/local_webhook_testing.md ================================================ # Local Webhook Testing The [Stripe CLI][cli] allows receiving webhooks events from Stripe on your local machine via a direct connection to Stripe's API. Set the `--forward-to` flag to the URL of a local webhook endpoint you created via the Django admin or the Stripe Dashboard. New Style `UUID` urls are also supported from `v2.7` onwards. For example: ```sh stripe listen --forward-to http://localhost:8000/stripe/webhook/ ``` The [signatures of events sent by Stripe to the webhooks are verified][signatures] to prevent third-parties from interacting with the endpoints. Events will be signed with a webhook secret different from existing endpoints (because Stripe CLI doesn't require a webhook endpoint to be set up). You can obtain this secret by looking at the output of `stripe listen` or by running `stripe listen --print-secret`. In order to let dj-stripe know about the secret key to verify the signature, it can be passed as an HTTP header; dj-stripe looks for a header called `X-Djstripe-Webhook-Secret`: ```sh stripe listen \ --forward-to http://localhost:8000/stripe/webhook/ \ -H "x-djstripe-webhook-secret: $(stripe listen --print-secret)" ``` From now on, whenever you make changes on the Stripe Dashboard, the webhook endpoint you specified with `--forward-to` will called with the respective changes. !!! hint If the webhook secret is not passed to dj-stripe, signature validation will fail with an HTTP status code 400 and the message "Failed to verify header". Stripe events can now be triggered like so: ```sh stripe trigger customer.created ``` [cli]: https://stripe.com/docs/cli [signatures]: https://stripe.com/docs/webhooks/signatures ================================================ FILE: docs/usage/managing_subscriptions.md ================================================ # Managing subscriptions and payment sources ## Extending subscriptions For your convenience, dj-stripe provides a [`Subscription.extend(*delta*)`][djstripe.models.billing.Subscription.extend] method Subscriptions can be extended by using the `Subscription.extend` method, which takes a positive `timedelta` as its only property. This method is useful if you want to offer time-cards, gift-cards, or some other external way of subscribing users or extending subscriptions, while keeping the billing handling within Stripe. !!! warning Subscription extensions are achieved by manipulating the `trial_end` of the subscription instance, which means that Stripe will change the status to `trialing`. ================================================ FILE: docs/usage/manually_syncing_with_stripe.md ================================================ # Manually syncing data with Stripe If you're using dj-stripe's webhook handlers then data will be automatically synced from Stripe to the Django database, but in some circumstances you may want to manually sync Stripe API data as well. ## Command line You can sync your database with stripe using the management command [`djstripe_sync_models`][djstripe.management.commands.djstripe_sync_models], e.g. to populate an empty database from an existing Stripe account. ```bash ./manage.py djstripe_sync_models ``` With no arguments this will sync all supported models for all in database API Keys , or a list of models to sync can also be provided. ```bash ./manage.py djstripe_sync_models Invoice Subscription ``` Note that this may be redundant since we recursively sync related objects. A list of models to sync can also be provided along with the API Keys. ```bash ./manage.py djstripe_sync_models Invoice Subscription --api-keys sk_test_XXX sk_test_YYY ``` This will sync all the Invoice and Subscription data for the given API Keys. Please note that the API Keys sk_test_YYY and sk_test_XXX need to be in the database. You can manually reprocess events using the management commands [`djstripe_process_events`][djstripe.management.commands.djstripe_process_events]. By default this processes all events, but options can be passed to limit the events processed. Note the Stripe API documents a limitation where events are only guaranteed to be available for 30 days. ```bash # all events ./manage.py djstripe_process_events # failed events (events with pending webhooks or where all webhook delivery attempts failed) ./manage.py djstripe_process_events --failed # filter by event type (all payment_intent events in this example) ./manage.py djstripe_process_events --type payment_intent.* # specific events by ID ./manage.py djstripe_process_events --ids evt_foo evt_bar # more output for debugging processing failures ./manage.py djstripe_process_events -v 2 ``` ## In Code To sync in code, for example if you write to the Stripe API and want to work with the resulting dj-stripe object without having to wait for the webhook trigger. This can be done using the classmethod [`sync_from_stripe_data`][djstripe.models.base.StripeModel.sync_from_stripe_data] that exists on all dj-stripe model classes. E.g. creating a product using the Stripe API, and then syncing the API return data to Django using dj-stripe: ================================================ FILE: docs/usage/subscribing_customers.md ================================================ # Subscribing a customer to one or more prices (or plans) ## Recommended Approach ```python # Recommended Approach to use items dict with Prices ## This will subscribe to both and price_1 = Price.objects.get(nickname="one_price") price_2 = Price.objects.get(nickname="two_price") customer = Customer.objects.first() customer.subscribe(items=[{"price": price_1}, {"price": price_2}]) ## This will subscribe to price_1 = Price.objects.get(nickname="one_price") customer = Customer.objects.first() customer.subscribe(items=[{"price": price_1}]) ``` ## Alternate Approach 1 (with legacy Plans) ```python ## (Alternate Approach) This will subscribe to price_1 = Price.objects.get(nickname="one_price") customer = Customer.objects.first() customer.subscribe(price=price_1) # If you still use legacy Plans... ## This will subscribe to both and plan_1 = Plan.objects.get(nickname="one_plan") plan_2 = Plan.objects.get(nickname="two_plan") customer = Customer.objects.first() customer.subscribe(items=[{"plan": plan_1}, {"plan": plan_2}]) ## This will subscribe to plan_1 = Plan.objects.get(nickname="one_plan") customer = Customer.objects.first() customer.subscribe(items=[{"plan": plan_1}]) ``` ## Alternate Approach 2 ```python ## (Alternate Approach) This will subscribe to plan_1 = Plan.objects.get(nickname="one_plan") customer = Customer.objects.first() customer.subscribe(plan=plan_1) ``` However in some cases `subscribe()` might not support all the arguments you need for your implementation. When this happens you can just call the official `stripe.Customer.subscribe()`. !!! tip Check out the following examples: - [`form_valid view example`][tests.apps.example.views.PurchaseSubscriptionView.form_valid] - [`djstripe.models.Customer.add_payment_method`][djstripe.models.core.Customer.add_payment_method] Note that PaymentMethods can be used instead of Cards/Source by substituting ```py # Add the payment method customer's default customer.add_payment_method(payment_method) ``` instead of ```py # Add the source as the customer's default card customer.add_card(stripe_source) ``` in the above example. ================================================ FILE: docs/usage/using_stripe_checkout.md ================================================ # Create a Stripe Checkout Session For your convenience, dj-stripe has provided an example implementation on how to use [`Checkouts`][tests.apps.example.views.CreateCheckoutSessionView] Please note that in order for dj-stripe to create a link between your `customers` and your `subscribers`, you need to add the `DJSTRIPE_SUBSCRIBER_CUSTOMER_KEY` key to the `metadata` parameter of `Checkout`. This has also been demonstrated in the aforementioned [example][tests.apps.example.views.CreateCheckoutSessionView] ================================================ FILE: docs/usage/using_with_docker.md ================================================ # Using with Docker A [Docker image](https://hub.docker.com/r/stripe/stripe-cli) allows you to run the Stripe CLI in a container. Here is a sample `docker-compose.yaml` file that sets up all the services to use `Stripe CLI` in a `dockerised django container (with djstripe)` ```yaml version: "3.9" volumes: postgres-data: {} services: db: image: postgres:12 volumes: - postgres-data:/var/lib/postgresql/data environment: - POSTGRES_DB=random_number - POSTGRES_USER=root - POSTGRES_PASSWORD=random_number web: build: context: . dockerfile: command: python manage.py runserver 0.0.0.0:8000 volumes: - .:/app ports: - "8000:8000" depends_on: - db environment: # Stripe specific keys - STRIPE_PUBLIC_KEY=pk_test_****** - STRIPE_SECRET_KEY=sk_test_****** - DJSTRIPE_TEST_WEBHOOK_SECRET=whsec_****** # Database Specific Settings - DJSTRIPE_TEST_DB_VENDOR=postgres - DJSTRIPE_TEST_DB_PORT=5432 - DJSTRIPE_TEST_DB_USER=root - DJSTRIPE_TEST_DB_NAME=random_number - DJSTRIPE_TEST_DB_PASS=random_number - DJSTRIPE_TEST_DB_HOST=db stripe: image: stripe/stripe-cli:v1.7.4 # In case Stripe CLI is used to perform local webhook testing, set x-djstripe-webhook-secret custom header to output of Stripe CLI. command: ["listen", "-H", "x-djstripe-webhook-secret: whsec_******", "--forward-to", "http://web:8000/djstripe/webhook/"] depends_on: - web environment: - STRIPE_API_KEY=sk_test_****** - STRIPE_DEVICE_NAME=djstripe_docker ``` !!! note In case the `Stripe CLI` is used to perform local webhook testing, set `x-djstripe-webhook-secret` Custom Header in Stripe `listen` to the `Webhook Signing Secret` output of `Stripe CLI`. That is what Stripe expects and uses to create the `stripe-signature` header. ================================================ FILE: docs/usage/webhooks.md ================================================ # Using Stripe Webhooks ## Setting up a new webhook endpoint in dj-stripe As of dj-stripe 2.7.0, dj-stripe can create its own webhook endpoints on Stripe from the Django administration. Create a new webhook endpoint from the Django administration by going to dj-stripe -> Webhook endpoints -> Add webhook endpoint (or `/admin/djstripe/webhookendpoint/add/`). From there, you can choose an account to create the endpoint for. If no account is chosen, the default Stripe API key will be used to create the endpoint. You can also choose to create the endpoint in test mode or live mode. You may want to change the base URL of the endpoint. This field will be prefilled with the current site. If you're running on the local development server, you may see `http://localhost:8000` or similar in there. Stripe won't let you save webhook endpoints with such a value, so you will want to change it to a real website URL. When saved from the admin, the endpoint will be created in Stripe with a dj-stripe specific UUID which will be part of the URL, making it impossible to guess externally by brute-force. ## Legacy setup Before dj-stripe 2.7.0, dj-stripe included a global webhook endpoint URL, which uses the setting [`DJSTRIPE_WEBHOOK_SECRET`][djstripe.settings.DjstripeSettings.WEBHOOK_SECRET] to validate incoming webhooks. This is not recommended as it makes the URL guessable, and may be removed in the future. ## Extra configuration dj-stripe provides the following settings to tune how your webhooks work: - [`DJSTRIPE_WEBHOOK_VALIDATION`][djstripe.settings.DjstripeSettings.WEBHOOK_VALIDATION] - [`DJSTRIPE_WEBHOOK_TOLERANCE`][djstripe.settings.DjstripeSettings.WEBHOOK_TOLERANCE] - [`DJSTRIPE_WEBHOOK_EVENT_CALLBACK`][djstripe.settings.DjstripeSettings.WEBHOOK_EVENT_CALLBACK] ## Advanced usage dj-stripe comes with native support for webhooks as event listeners. Events allow you to do things like sending an email to a customer when his payment has [failed](https://stripe.com/docs/receipts#failed-payment-alerts) or trial period is ending. This is how you use them: ```python from djstripe import webhooks @webhooks.handler("customer.subscription.trial_will_end") def my_handler(event, **kwargs): print("We should probably notify the user at this point") ``` You can handle all events related to customers like this: ```py from djstripe import webhooks @webhooks.handler("customer") def my_handler(event, **kwargs): print("We should probably notify the user at this point") ``` You can also handle different events in the same handler: ```py from djstripe import webhooks @webhooks.handler("price", "product") def my_handler(event, **kwargs): print("Triggered webhook " + event.type) ``` !!! warning In order to get registrations picked up, you need to put them in a module that is imported like models.py or make sure you import it manually. Webhook event creation and processing is now wrapped in a `transaction.atomic()` block to better handle webhook errors. This will prevent any additional database modifications you may perform in your custom handler from being committed should something in the webhook processing chain fail. You can also take advantage of Django's `transaction.on_commit()` function to only perform an action if the transaction successfully commits (meaning the Event processing worked): ```py from django.db import transaction from djstripe import webhooks def do_something(): pass # send a mail, invalidate a cache, fire off a Celery task, etc. @webhooks.handler("price", "product") def my_handler(event, **kwargs): transaction.on_commit(do_something) ``` ## Official documentation Stripe docs for types of Events: Stripe docs for Webhooks: Django docs for transactions: ================================================ FILE: manage.py ================================================ #!/usr/bin/env python import os import sys try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. " "Run `poetry shell` to activate a virtual environment first." ) from exc def main(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") execute_from_command_line(sys.argv) if __name__ == "__main__": main() ================================================ FILE: mkdocs.yml ================================================ site_name: Dj-Stripe site_url: https://dj-stripe.github.io/dj-stripe/ site_description: Django + Stripe Made Easy site_author: Dj-Stripe Team repo_url: https://github.com/dj-stripe/dj-stripe/ theme: name: readthedocs features: - search.suggest - search.highligh markdown_extensions: - admonition - codehilite - pymdownx.highlight - pymdownx.inlinehilite - pymdownx.superfences - pymdownx.snippets plugins: - autorefs - search - mkdocstrings: default_handler: python handlers: python: options: # show_root_heading: true show_object_full_path: true show_category_heading: true show_if_no_docstring: true setup_commands: - import os - import sys - import django - sys.path.insert(0, os.path.abspath(".")) - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") - django.setup() watch: - . enable_inventory: true - mike: canonical_version: "2.7" nav: - Home: README.md - Sponsors: project/sponsors.md - Getting Started: - Installation: installation.md - Managing Stripe API Keys: api_keys.md - A note on Stripe API Versions: api_versions.md - Integrating Stripe Elements: stripe_elements_js.md - Release notes: - dj-stripe 2.7 release notes: history/2_7_0.md - dj-stripe 2.6 release notes: history/2_6_0.md - dj-stripe 2.5 release notes: history/2_5_0.md - dj-stripe 2.4.1 release notes: history/2_4_x.md - dj-stripe 2.4 release notes: history/2_4_0.md - dj-stripe 2.0 ~ 2.3 release notes: history/2_x.md - dj-stripe 1.x release notes: history/1_x.md - dj-stripe 0.x release notes: history/0_x.md - Usage: - Using Stripe Webhooks: usage/webhooks.md - Subscribing a customer to a plan: usage/subscribing_customers.md - Managing subscriptions and payment sources: usage/managing_subscriptions.md - Manually syncing data with Stripe: usage/manually_syncing_with_stripe.md - Creating individual charges: usage/creating_individual_charges.md - Creating Usage Records: usage/creating_usage_record.md - Using Stripe Checkout: usage/using_stripe_checkout.md - Using with Docker: usage/using_with_docker.md - Development with local webhooks: usage/local_webhook_testing.md - Project: - Contributing: project/contributing.md - Test Fixtures: project/test_fixtures.md - Credits: project/authors.md - Support: project/support.md - Release Process: project/release_process.md - Reference: - Enumerations: reference/enums.md - Managers: reference/managers.md - Models: reference/models.md - Settings: reference/settings.md - Utilities: reference/utils.md ================================================ FILE: pyproject.toml ================================================ [tool.poetry] name = "dj-stripe" version = "2.8.0-dev.0" description = "Django + Stripe made easy" license = "MIT" authors = [ "Alexander Kavanaugh ", "Jerome Leclanche ", ] readme = "docs/README.md" homepage = "https://dj-stripe.dev" repository = "https://github.com/dj-stripe/dj-stripe" documentation = "https://dj-stripe.github.io/dj-stripe/" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "Topic :: Office/Business :: Financial", "Topic :: Software Development :: Libraries :: Python Modules", "Framework :: Django", "Framework :: Django :: 3.2", "Framework :: Django :: 4.0", "Framework :: Django :: 4.1", ] packages = [ { include = "djstripe" } ] include = [ "AUTHORS.md", "CONTRIBUTING.md", "HISTORY.md", "LICENSE", ] exclude = [ "manage.py" ] [tool.poetry.urls] "Funding" = "https://github.com/sponsors/dj-stripe" [tool.poetry.dependencies] python = "^3.8.0" django = ">=3.2" stripe = ">=2.48.0,<5.0.0" psycopg2 = { version = "^2.8.5", optional = true } mysqlclient = { version = ">=1.4.0", optional = true } [tool.poetry.group.dev] optional = true [tool.poetry.group.dev.dependencies] black = ">=22.6.0" isort = ">=5.10.1" pytest = ">=7.1.2" pytest-django = ">=4.5.2" mypy = ">=0.971" # flake8 = ">=5.0.4" # Why is flake8 commented out? # The short version: flake8 is pinned to an old version of importlib-metadata # which clashes with other dependencies (such as mkdocs in our case). # # For the longer version, you have to look at these links: # https://github.com/PyCQA/flake8/pull/1438 # https://github.com/PyCQA/flake8/issues/1474 # https://github.com/PyCQA/flake8/issues/1503 # https://github.com/PyCQA/flake8/issues/1522 # https://github.com/PyCQA/flake8/issues/1526 # # So the flake8 team ran into a deprecation path they didn't like, and got so # upset about it that… it seems they just decided to never unpin it, even though # the maintainers of importlib-metadata have since fixed the issue flake8 was # complaining about. # # Now, they're getting issue, after issue, after issue opened on Github because: # - importlib-metadata 4.4+ is required by many popular libs (markdown, sphinx…) # - The pin looks unnecessary at first glance # - They don't document the reasoning for their choice # - There probably ISN'T any valid reasoning for their choice # - They are closing & locking any reference to this as off-topic # - They refuse to address the issue, and complain about anyone who brings it up. # # The reasonable approach would of course be to remove the useless pin, but by # doing so they fear they would "increase the maintenance burden" because they're # a small team (in other words, they're worried about more bug tracker activity). # Obviously this makes zero sense since the issue is gone, and they are instead # generating lots of bug tracker activity, which they're promptly ignoring. # # So they're wasting my time, and everybody else's, including yours who's reading # this little rant right now. But I'd like this to serve as a lesson on how not # to maintain a popular open source project. # Many OSS devs are finding out that, with increased popularity comes increased # responsibility. If you don't want the responsibility, you are not forced to # continue maintaining the library; but as long as you do, your reach puts you in # a position of power. # Wasting your users' time is abusing that position of power. # Think of the underpaid bureaucrat who, by the stroke of a pen, makes or breaks # the lives of others. Who was a little too grumpy at work and decided to take it # out on someone and waste hours of their life by making illogical demands, just # out of spite. # # I'm grateful flake8 exists. But it's not a useful enough tool to allow it to # waste our time like this, and it's largely been displaced by the other tools: # mypy, isort, and black. So let's stick to those instead. pre-commit = "^3.0.4" [tool.poetry.group.docs] optional = true [tool.poetry.group.docs.dependencies] mkdocs = ">=1.3.1" mkdocs-material = ">=8.4.2" mkdocs-autorefs = ">=0.4.1" mkdocstrings = {extras = ["python"], version = ">=0.19.0"} mike = ">=1.1.2" jinja2 = "<3.1.0" [tool.poetry.group.ci] optional = true [tool.poetry.group.ci.dependencies] coverage = {version = "^6.5.0", extras = ["toml"]} tox = "^3.26.0" tox-gh-actions = "^2.10.0" [tool.poetry.extras] postgres = ["psycopg2"] mysql = ["mysqlclient"] [tool.isort] profile = "black" [build-system] requires = ["poetry_core>=1.1.0"] build-backend = "poetry.core.masonry.api" ================================================ FILE: tests/__init__.py ================================================ """ A Fake or multiple fakes for each stripe object. Originally collected using API VERSION 2015-07-28. Updated to API VERSION 2016-03-07 with bogus fields. """ from __future__ import absolute_import, division, print_function, unicode_literals import datetime import json import logging import os from copy import deepcopy from pathlib import Path from typing import Any, Dict from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.utils import dateformat from djstripe.utils import get_timezone_utc from djstripe.webhooks import TEST_EVENT_ID os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") logger = logging.getLogger(__name__) FUTURE_DATE = datetime.datetime(2100, 4, 30, tzinfo=get_timezone_utc()) FIXTURE_DIR_PATH = Path(__file__).parent.joinpath("fixtures") class AssertStripeFksMixin: def _get_field_str(self, field) -> str: if isinstance(field, models.OneToOneRel): if field.parent_link: return "" else: reverse_id_name = str(field.remote_field.foreign_related_fields[0]) return ( reverse_id_name.replace("djstripe_id", field.name) + " (related name)" ) elif isinstance(field, models.ForeignKey): return str(field) else: return "" def assert_fks(self, obj, expected_blank_fks, processed_stripe_ids=None): """ Recursively walk through fks on obj, asserting they're not-none :param obj: :param expected_blank_fks: fields that are expected to be None :param processed_stripe_ids: set of objects ids already processed :return: """ if processed_stripe_ids is None: processed_stripe_ids = set() processed_stripe_ids.add(obj.id) for field in obj._meta.get_fields(): field_str = self._get_field_str(field) if not field_str or field_str.endswith(".djstripe_owner_account"): continue try: field_value = getattr(obj, field.name) except ObjectDoesNotExist: field_value = None if field_str in expected_blank_fks: self.assertIsNone(field_value, field_str) else: self.assertIsNotNone(field_value, field_str) if field_value.id not in processed_stripe_ids: # recurse into the object if it's not already been checked self.assert_fks( field_value, expected_blank_fks, processed_stripe_ids ) logger.warning("checked %s", field_str) def load_fixture(filename): with FIXTURE_DIR_PATH.joinpath(filename).open("r") as f: return json.load(f) def datetime_to_unix(datetime_): return int(dateformat.format(datetime_, "U")) class StripeItem(dict): """Flexible class built to mock any generic Stripe object. Implements object access + deletion methods to match the behavior of Stripe's library, which allows both object + dictionary access. Has a delete method since (most) Stripe objects can be deleted. """ def __getattr__(self, name): """Give StripeItem normal object access to match Stripe behavior.""" if name in self: return self[name] else: raise AttributeError("No such attribute: " + name) def __setattr__(self, name, value): self[name] = value def __delattr__(self, name): if name in self: del self[name] else: raise AttributeError("No such attribute: " + name) def delete(self) -> bool: """Superficial mock that adds a deleted attribute.""" self.deleted = True return self.deleted @classmethod def class_url(cls): return "/v1/test-items/" def instance_url(self): """Superficial mock that emulates instance_url.""" id = self.get("id") base = self.class_url() return "%s/%s" % (base, id) def request(self, method, url, params) -> Dict: """Superficial mock that emulates request method.""" assert method == "post" for key, value in params.items(): self.__setattr__(key, value) return self class StripeList(dict): """Mock a generic Stripe Iterable. It has the relevant attributes of a stripe iterable (has_more, data). This mock is important so we can use stripe's `list` method in our testing. StripeList.list() will return the StripeList. Additionally, iterating over instances of MockStripeIterable will iterate over the data attribute, just like Stripe iterables. Attributes: has_more: mock has_more flag. Default False. **kwargs: all of the fields of the stripe object, generally as a dictionary. """ object = "list" url = "/v1/fakes" has_more = False def __getattr__(self, name): """Give StripeItem normal object access to match Stripe behavior.""" if name in self: return self[name] else: raise AttributeError("No such attribute: " + name) def __setattr__(self, name, value): self[name] = value def __delattr__(self, name): if name in self: del self[name] else: raise AttributeError("No such attribute: " + name) def __iter__(self) -> Any: """Make StripeList an iterable, to match the Stripe iterable behavior.""" self.iter_copy = self.data.copy() return self def __next__(self) -> StripeItem: """Define iteration for StripeList.""" if len(self.iter_copy) > 0: return self.iter_copy.pop(0) else: raise StopIteration() def list(self, **kwargs: Any) -> "StripeList": """Add a list method to the StripeList which returns itself. list() accepts arbitrary kwargs, be careful is you expect the argument-accepting functionality of Stripe's list() method. """ return self def auto_paging_iter(self) -> "StripeList": """Add an auto_paging_iter method to the StripeList which returns itself. The StripeList is an iterable, so this mimics the real behavior. """ return self @property def total_count(self): return len(self.data) class ExternalAccounts(object): def __init__(self, external_account_fakes): self.external_account_fakes = external_account_fakes def create(self, source, api_key=None): for fake_external_account in self.external_account_fakes: if fake_external_account["id"] == source: return fake_external_account def retrieve(self, id, expand=None): for fake_external_account in self.external_account_fakes: if fake_external_account["id"] == id: return fake_external_account def list(self, **kwargs): return StripeList(data=self.external_account_fakes) class AccountDict(dict): def save(self, idempotency_key=None): return self @property def external_accounts(self): return ExternalAccounts( external_account_fakes=self["external_accounts"]["data"] ) def create(self): from djstripe.models import Account return Account.sync_from_stripe_data(self) FAKE_STANDARD_ACCOUNT = AccountDict( load_fixture("account_standard_acct_1Fg9jUA3kq9o1aTc.json") ) # Stripe Platform Account to which the STRIPE_SECRET_KEY belongs to FAKE_PLATFORM_ACCOUNT = deepcopy(FAKE_STANDARD_ACCOUNT) FAKE_PLATFORM_ACCOUNT["settings"]["dashboard"]["display_name"] = "djstripe-platform" FAKE_CUSTOM_ACCOUNT = AccountDict( load_fixture("account_custom_acct_1IuHosQveW0ONQsd.json") ) FAKE_EXPRESS_ACCOUNT = AccountDict( load_fixture("account_express_acct_1IuHosQveW0ONQsd.json") ) FAKE_BALANCE_TRANSACTION = load_fixture( "balance_transaction_txn_fake_ch_fakefakefakefakefake0001.json" ) FAKE_BALANCE_TRANSACTION_II = { "id": "txn_16g5h62eZvKYlo2CQ2AHA89s", "object": "balance_transaction", "amount": 65400, "available_on": 1441670400, "created": 1441079064, "currency": "usd", "description": None, "fee": 1927, "fee_details": [ { "amount": 1927, "currency": "usd", "type": "stripe_fee", "description": "Stripe processing fees", "application": None, } ], "net": 63473, "source": "ch_16g5h62eZvKYlo2CMRXkSqa0", "sourced_transfers": { "object": "list", "total_count": 0, "has_more": False, "url": "/v1/transfers?source_transaction=ch_16g5h62eZvKYlo2CMRXkSqa0", "data": [], }, "status": "pending", "type": "charge", } FAKE_BALANCE_TRANSACTION_III = { "id": "txn_16g5h62eZvKYlo2CQ2AHA89s", "object": "balance_transaction", "amount": 2000, "available_on": 1441670400, "created": 1441079064, "currency": "usd", "description": None, "fee": 1927, "fee_details": [ { "amount": 1927, "currency": "usd", "type": "stripe_fee", "description": "Stripe processing fees", "application": None, } ], "net": 73, "source": "ch_16g5h62eZvKYlo2CMRXkSqa0", "sourced_transfers": { "object": "list", "total_count": 0, "has_more": False, "url": "/v1/transfers?source_transaction=ch_16g5h62eZvKYlo2CMRXkSqa0", "data": [], }, "status": "pending", "type": "charge", } FAKE_BALANCE_TRANSACTION_IV = { "id": "txn_16g5h62eZvKYlo2CQ2AHA89s", "object": "balance_transaction", "amount": 19010, "available_on": 1441670400, "created": 1441079064, "currency": "usd", "description": None, "fee": 1927, "fee_details": [ { "amount": 1927, "currency": "usd", "type": "stripe_fee", "description": "Stripe processing fees", "application": None, } ], "net": 17083, "source": "ch_16g5h62eZvKYlo2CMRXkSqa0", "sourced_transfers": { "object": "list", "total_count": 0, "has_more": False, "url": "/v1/transfers?source_transaction=ch_16g5h62eZvKYlo2CMRXkSqa0", "data": [], }, "status": "pending", "type": "charge", } class LegacySourceDict(dict): def delete(self): return self class BankAccountDict(LegacySourceDict): pass FAKE_BANK_ACCOUNT = { "id": "ba_16hTzo2eZvKYlo2CeSjfb0tS", "object": "bank_account", "account_holder_name": None, "account_holder_type": None, "bank_name": "STRIPE TEST BANK", "country": "US", "currency": "usd", "fingerprint": "1JWtPxqbdX5Gamtc", "last4": "6789", "routing_number": "110000000", "status": "new", } FAKE_BANK_ACCOUNT_II = { "id": "ba_17O4Tz2eZvKYlo2CMYsxroV5", "object": "bank_account", "account_holder_name": None, "account_holder_type": None, "bank_name": None, "country": "US", "currency": "usd", "fingerprint": "1JWtPxqbdX5Gamtc", "last4": "6789", "routing_number": "110000000", "status": "new", } # Stripe Customer Bank Account Payment Source FAKE_BANK_ACCOUNT_SOURCE = BankAccountDict( load_fixture("bank_account_ba_fakefakefakefakefake0003.json") ) FAKE_BANK_ACCOUNT_IV = BankAccountDict( load_fixture("bank_account_ba_fakefakefakefakefake0004.json") ) class CardDict(LegacySourceDict): pass FAKE_CARD = CardDict(load_fixture("card_card_fakefakefakefakefake0001.json")) FAKE_CARD_II = CardDict(load_fixture("card_card_fakefakefakefakefake0002.json")) FAKE_CARD_III = CardDict(load_fixture("card_card_fakefakefakefakefake0003.json")) # Stripe Custom Connected Account Card Payout Source FAKE_CARD_IV = CardDict(load_fixture("card_card_fakefakefakefakefake0004.json")) class SourceDict(dict): def detach(self): self.pop("customer") self.update({"status": "consumed"}) return self # Attached, chargeable source FAKE_SOURCE = SourceDict(load_fixture("source_src_fakefakefakefakefake0001.json")) # Detached, consumed source FAKE_SOURCE_II = SourceDict( { "id": "src_1DuuGjkE6hxDGaasasjdlajl", "object": "source", "amount": None, "card": { "address_line1_check": None, "address_zip_check": "pass", "brand": "Visa", "country": "US", "cvc_check": "pass", "dynamic_last4": None, "exp_month": 10, "exp_year": 2029, "fingerprint": "TmOrYzPdAoO6YFNB", "funding": "credit", "last4": "4242", "name": None, "three_d_secure": "optional", "tokenization_method": None, }, "client_secret": "src_client_secret_ENg5dyB1KTXCAEJGJQWEf67X", "created": 1548046215, "currency": None, "flow": "none", "livemode": False, "metadata": {"djstripe_test_fake_id": "src_fakefakefakefakefake0002"}, "owner": { "address": { "city": None, "country": None, "line1": None, "line2": None, "postal_code": "90210", "state": None, }, "email": None, "name": None, "phone": None, "verified_address": None, "verified_email": None, "verified_name": None, "verified_phone": None, }, "statement_descriptor": None, "status": "consumed", "type": "card", "usage": "reusable", } ) FAKE_PAYMENT_INTENT_I = load_fixture("payment_intent_pi_fakefakefakefakefake0001.json") FAKE_PAYMENT_INTENT_II = deepcopy(FAKE_PAYMENT_INTENT_I) FAKE_PAYMENT_INTENT_II["customer"] = "cus_4UbFSo9tl62jqj" # FAKE_CUSTOMER_II FAKE_PAYMENT_INTENT_DESTINATION_CHARGE = load_fixture( "payment_intent_pi_destination_charge.json" ) class PaymentMethodDict(dict): def detach(self): self.pop("customer") return self FAKE_PAYMENT_METHOD_I = PaymentMethodDict( load_fixture("payment_method_pm_fakefakefakefake0001.json") ) FAKE_PAYMENT_METHOD_II = deepcopy(FAKE_PAYMENT_METHOD_I) FAKE_PAYMENT_METHOD_II["customer"] = "cus_4UbFSo9tl62jqj" # FAKE_CUSTOMER_II # FAKE_CARD, but accessed as a PaymentMethod FAKE_CARD_AS_PAYMENT_METHOD = PaymentMethodDict( load_fixture("payment_method_card_fakefakefakefakefake0001.json") ) FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT = load_fixture( "order_order_fakefakefakefake0001.json" ) FAKE_ORDER_WITHOUT_CUSTOMER_WITH_PAYMENT_INTENT = deepcopy( FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT ) FAKE_ORDER_WITHOUT_CUSTOMER_WITH_PAYMENT_INTENT["customer"] = None FAKE_ORDER_WITH_CUSTOMER_WITHOUT_PAYMENT_INTENT = deepcopy( FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT ) FAKE_ORDER_WITH_CUSTOMER_WITHOUT_PAYMENT_INTENT["payment_intent"] = None FAKE_ORDER_WITH_CUSTOMER_WITHOUT_PAYMENT_INTENT["payment"]["payment_intent"] = None FAKE_ORDER_WITHOUT_CUSTOMER_WITHOUT_PAYMENT_INTENT = deepcopy( FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT ) FAKE_ORDER_WITHOUT_CUSTOMER_WITHOUT_PAYMENT_INTENT["customer"] = None FAKE_ORDER_WITHOUT_CUSTOMER_WITHOUT_PAYMENT_INTENT["payment_intent"] = None FAKE_ORDER_WITHOUT_CUSTOMER_WITHOUT_PAYMENT_INTENT["payment"]["payment_intent"] = None # Created Orders have their status="open" FAKE_EVENT_ORDER_CREATED = { "id": "evt_16igNU2eZvKYlo2CYyMkYvet", "object": "event", "api_version": "2016-03-07", "created": 1441696732, "data": {"object": deepcopy(FAKE_ORDER_WITH_CUSTOMER_WITHOUT_PAYMENT_INTENT)}, "livemode": False, "pending_webhooks": 0, "request": "req_6wZW9MskhYU15Y", "type": "order.created", } FAKE_EVENT_ORDER_CREATED["data"]["object"]["status"] = "open" FAKE_EVENT_ORDER_UPDATED = { "id": "evt_16igNU2eZvKYlo2CYyMkYvet", "object": "event", "api_version": "2016-03-07", "created": 1441696732, "data": {"object": deepcopy(FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT)}, "livemode": False, "pending_webhooks": 0, "request": "req_6wZW9MskhYU15Y", "type": "order.created", } FAKE_EVENT_ORDER_UPDATED["data"]["object"]["status"] = "open" FAKE_EVENT_ORDER_UPDATED["type"] = "order.updated" FAKE_EVENT_ORDER_UPDATED["data"]["object"]["billing_details"][ "email" ] = "testuser@example.com" FAKE_EVENT_ORDER_SUBMITTED = deepcopy(FAKE_EVENT_ORDER_UPDATED) FAKE_EVENT_ORDER_SUBMITTED["type"] = "order.submitted" FAKE_EVENT_ORDER_SUBMITTED["data"]["object"]["status"] = "submitted" FAKE_EVENT_ORDER_PROCESSING = deepcopy(FAKE_EVENT_ORDER_UPDATED) FAKE_EVENT_ORDER_PROCESSING["type"] = "order.processing" FAKE_EVENT_ORDER_PROCESSING["data"]["object"]["status"] = "processing" FAKE_EVENT_ORDER_CANCELLED = deepcopy(FAKE_EVENT_ORDER_UPDATED) FAKE_EVENT_ORDER_CANCELLED["type"] = "order.canceled" FAKE_EVENT_ORDER_CANCELLED["data"]["object"]["status"] = "canceled" FAKE_EVENT_ORDER_COMPLETED = deepcopy(FAKE_EVENT_ORDER_UPDATED) FAKE_EVENT_ORDER_COMPLETED["type"] = "order.complete" FAKE_EVENT_ORDER_COMPLETED["data"]["object"]["status"] = "complete" # TODO - add to regenerate_test_fixtures and replace this with a JSON fixture FAKE_SETUP_INTENT_I = { "id": "seti_fakefakefakefake0001", "object": "setup_intent", "cancellation_reason": None, "payment_method_types": ["card"], "status": "requires_payment_method", "usage": "off_session", "payment_method": None, "on_behalf_of": None, "customer": None, } FAKE_SETUP_INTENT_II = { "application": None, "cancellation_reason": None, "client_secret": "seti_1J0g0WJSZQVUcJYgWE2XSi1K_secret_Jdxw2mOaIEHBdE6eTsfJ2IfmamgNJaF", "created": 1623301244, "customer": "cus_6lsBvm5rJ0zyHc", "description": None, "id": "seti_1J0g0WJSZQVUcJYgWE2XSi1K", "last_setup_error": None, "latest_attempt": "setatt_1J0g0WJSZQVUcJYgsrFgwxVh", "livemode": False, "mandate": None, "metadata": {}, "next_action": None, "object": "setup_intent", "on_behalf_of": None, "payment_method": "pm_fakefakefakefake0001", "payment_method_options": {"card": {"request_three_d_secure": "automatic"}}, "payment_method_types": ["card"], "single_use_mandate": None, "status": "succeeded", "usage": "off_session", } FAKE_SETUP_INTENT_DESTINATION_CHARGE = load_fixture( "setup_intent_pi_destination_charge.json" ) # TODO - add to regenerate_test_fixtures and replace this with a JSON fixture # (will need to use a different payment_intent fixture) FAKE_SESSION_I = { "id": "cs_test_OAgNmy75Td25OeREvKUs8XZ7SjMPO9qAplqHO1sBaEjOg9fYbaeMh2nA", "object": "checkout.session", "billing_address_collection": None, "cancel_url": "https://example.com/cancel", "client_reference_id": None, "customer": "cus_6lsBvm5rJ0zyHc", "customer_email": None, "display_items": [ { "amount": 1500, "currency": "usd", "custom": { "description": "Comfortable cotton t-shirt", "images": None, "name": "T-shirt", }, "quantity": 2, "type": "custom", } ], "livemode": False, "locale": None, "mode": None, "payment_intent": FAKE_PAYMENT_INTENT_I["id"], "payment_method_types": ["card"], "setup_intent": None, "submit_type": None, "subscription": None, "success_url": "https://example.com/success", "metadata": {}, } class ChargeDict(StripeItem): def __init__(self, *args, **kwargs): """Match Stripe's behavior: return a stripe iterable on `charge.refunds`.""" super().__init__(*args, **kwargs) self.refunds = StripeList(self.refunds) def refund(self, amount=None, reason=None): self.update({"refunded": True, "amount_refunded": amount}) return self def capture(self): self.update({"captured": True}) return self FAKE_CHARGE = ChargeDict(load_fixture("charge_ch_fakefakefakefakefake0001.json")) FAKE_CHARGE_II = ChargeDict( { "id": "ch_16ag432eZvKYlo2CGDe6lvVs", "object": "charge", "amount": 3000, "amount_captured": 0, "amount_refunded": 0, "application_fee": None, "application_fee_amount": None, "balance_transaction": FAKE_BALANCE_TRANSACTION["id"], "billing_details": { "address": { "city": None, "country": "US", "line1": None, "line2": None, "postal_code": "92082", "state": None, }, "email": "kyoung@hotmail.com", "name": "John Foo", "phone": None, }, "calculated_statement_descriptor": "Stripe", "captured": False, "created": 1439788903, "currency": "usd", "customer": "cus_4UbFSo9tl62jqj", "description": None, "destination": None, "dispute": None, "disputed": False, "failure_code": "expired_card", "failure_message": "Your card has expired.", "fraud_details": {}, "invoice": "in_16af5A2eZvKYlo2CJjANLL81", "livemode": False, "metadata": {}, "on_behalf_of": None, "order": None, "outcome": { "network_status": "declined_by_network", "reason": "expired_card", "risk_level": "normal", "risk_score": 1, "seller_message": "The bank returned the decline code `expired_card`.", "type": "issuer_declined", }, "paid": False, "payment_intent": FAKE_PAYMENT_INTENT_II["id"], "payment_method": FAKE_CARD_AS_PAYMENT_METHOD["id"], "payment_method_details": { "card": { "brand": "visa", "checks": { "address_line1_check": None, "address_postal_code_check": None, "cvc_check": None, }, "country": "US", "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "installments": None, "last4": "4242", "network": "visa", "three_d_secure": None, "wallet": None, }, "type": "card", }, "receipt_email": None, "receipt_number": None, "receipt_url": None, "refunded": False, "refunds": { "object": "list", "total_count": 0, "has_more": False, "url": "/v1/charges/ch_16ag432eZvKYlo2CGDe6lvVs/refunds", "data": [], }, "review": None, "shipping": None, "source": deepcopy(FAKE_CARD_II), "source_transfer": None, "statement_descriptor": None, "statement_descriptor_suffix": None, "status": "failed", "transfer_data": None, "transfer_group": None, } ) FAKE_CHARGE_REFUNDED = deepcopy(FAKE_CHARGE) FAKE_CHARGE_REFUNDED = FAKE_CHARGE_REFUNDED.refund( amount=FAKE_CHARGE_REFUNDED["amount"] ) FAKE_REFUND = { "id": "re_1E0he8KatMEEd8456454S01Vc", "object": "refund", "amount": FAKE_CHARGE_REFUNDED["amount_refunded"], "balance_transaction": "txn_1E0he8KaGRDEd998TDswMZuN", "charge": FAKE_CHARGE_REFUNDED["id"], "created": 1549425864, "currency": "usd", "metadata": {}, "reason": None, "receipt_number": None, "source_transfer_reversal": None, "status": "succeeded", "transfer_reversal": None, } # Balance transaction associated with the refund FAKE_BALANCE_TRANSACTION_REFUND = { "id": "txn_1E0he8KaGRDEd998TDswMZuN", "amount": -1 * FAKE_CHARGE_REFUNDED["amount_refunded"], "available_on": 1549425864, "created": 1549425864, "currency": "usd", "description": "REFUND FOR CHARGE (Payment for invoice G432DF1C-0028)", "exchange_rate": None, "fee": 0, "fee_details": [], "net": -1 * FAKE_CHARGE_REFUNDED["amount_refunded"], "object": "balance_transaction", "source": FAKE_REFUND["id"], "status": "available", "type": "refund", } FAKE_CHARGE_REFUNDED["refunds"].update( {"total_count": 1, "data": [deepcopy(FAKE_REFUND)]} ) FAKE_COUPON = { "id": "fake-coupon-1", "object": "coupon", "applies_to": {"products": ["prod_fake1"]}, "amount_off": None, "created": 1490157071, "currency": None, "duration": "once", "duration_in_months": None, "livemode": False, "max_redemptions": None, "metadata": {}, "percent_off": 1, "redeem_by": None, "times_redeemed": 0, "valid": True, } FAKE_DISPUTE_CHARGE = load_fixture("dispute_ch_fakefakefakefake01.json") FAKE_DISPUTE_BALANCE_TRANSACTION = load_fixture("dispute_txn_fakefakefakefake01.json") # case when a dispute gets closed and the funds get reinstated (full) FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_FULL = deepcopy( FAKE_DISPUTE_BALANCE_TRANSACTION ) FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_FULL["amount"] = ( -1 * FAKE_DISPUTE_BALANCE_TRANSACTION["amount"] ) FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_FULL["fee"] = ( -1 * FAKE_DISPUTE_BALANCE_TRANSACTION["fee"] ) FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_FULL["net"] = ( -1 * FAKE_DISPUTE_BALANCE_TRANSACTION["net"] ) FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_FULL["fee_details"][0]["amount"] = ( -1 * FAKE_DISPUTE_BALANCE_TRANSACTION["fee_details"][0]["amount"] ) # case when a dispute gets closed and the funds get reinstated (partial) FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_PARTIAL = deepcopy( FAKE_DISPUTE_BALANCE_TRANSACTION ) FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_PARTIAL["amount"] = ( -0.9 * FAKE_DISPUTE_BALANCE_TRANSACTION["amount"] ) FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_PARTIAL["fee"] = ( -0.9 * FAKE_DISPUTE_BALANCE_TRANSACTION["fee"] ) FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_PARTIAL["net"] = ( -0.9 * FAKE_DISPUTE_BALANCE_TRANSACTION["net"] ) FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_PARTIAL["fee_details"][0]["amount"] = ( -0.9 * FAKE_DISPUTE_BALANCE_TRANSACTION["fee_details"][0]["amount"] ) FAKE_DISPUTE_PAYMENT_INTENT = load_fixture("dispute_pi_fakefakefakefake01.json") FAKE_DISPUTE_PAYMENT_METHOD = load_fixture("dispute_pm_fakefakefakefake01.json") # case when dispute gets created FAKE_DISPUTE_I = load_fixture("dispute_dp_fakefakefakefake01.json") # case when funds get withdrawn from platform account due to dispute FAKE_DISPUTE_II = load_fixture("dispute_dp_fakefakefakefake02.json") # case when dispute gets updated FAKE_DISPUTE_III = deepcopy(FAKE_DISPUTE_II) FAKE_DISPUTE_III["evidence"]["receipt"] = "file_4hshrsKatMEEd6736724HYAXyj" # case when dispute gets closed FAKE_DISPUTE_IV = deepcopy(FAKE_DISPUTE_II) FAKE_DISPUTE_IV["evidence"]["receipt"] = "file_4hshrsKatMEEd6736724HYAXyj" FAKE_DISPUTE_IV["status"] = "won" # case when dispute funds get reinstated (partial) FAKE_DISPUTE_V_PARTIAL = load_fixture("dispute_dp_funds_reinstated_full.json") FAKE_DISPUTE_V_PARTIAL["balance_transactions"][ 1 ] = FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_PARTIAL # case when dispute funds get reinstated (full) FAKE_DISPUTE_V_FULL = load_fixture("dispute_dp_funds_reinstated_full.json") FAKE_DISPUTE_V_FULL["balance_transactions"][ 1 ] = FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_FULL FAKE_PRODUCT = load_fixture("product_prod_fake1.json") FAKE_PLAN = load_fixture("plan_gold21323.json") FAKE_PLAN_II = load_fixture("plan_silver41294.json") for plan in (FAKE_PLAN, FAKE_PLAN_II): # sanity check assert plan["product"] == FAKE_PRODUCT["id"] FAKE_TIER_PLAN = { "id": "tier21323", "object": "plan", "active": True, "amount": None, "created": 1386247539, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": False, "metadata": {}, "nickname": "New plan name", "product": FAKE_PRODUCT["id"], "trial_period_days": None, "usage_type": "licensed", "tiers_mode": "graduated", "tiers": [ {"flat_amount": 4900, "unit_amount": 1000, "up_to": 5}, {"flat_amount": None, "unit_amount": 900, "up_to": None}, ], } FAKE_PLAN_METERED = { "id": "plan_fakemetered", "billing_scheme": "per_unit", "object": "plan", "active": True, "aggregate_usage": "sum", "amount": 200, "collection_method": "per_unit", "created": 1552632817, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": False, "metadata": {}, "nickname": "Sum Metered Plan", "product": FAKE_PRODUCT["id"], "tiers": None, "tiers_mode": None, "transform_usage": None, "trial_period_days": None, "usage_type": "metered", } FAKE_PRICE = load_fixture("price_gold21323.json") FAKE_PRICE_II = load_fixture("price_silver41294.json") for price in (FAKE_PRICE, FAKE_PRICE_II): # sanity check assert price["product"] == FAKE_PRODUCT["id"] FAKE_PRICE_TIER = { "active": True, "billing_scheme": "tiered", "created": 1386247539, "currency": "usd", "id": "price_tier21323", "livemode": False, "lookup_key": None, "metadata": {}, "nickname": "New price name", "object": "price", "product": FAKE_PRODUCT["id"], "recurring": { "aggregate_usage": None, "interval": "month", "interval_count": 1, "trial_period_days": None, "usage_type": "licensed", }, "tiers": [ { "flat_amount": 4900, "flat_amount_decimal": "4900", "unit_amount": 1000, "unit_amount_decimal": "1000", "up_to": 5, }, { "flat_amount": None, "flat_amount_decimal": None, "unit_amount": 900, "unit_amount_decimal": "900", "up_to": None, }, ], "tiers_mode": "graduated", "transform_quantity": None, "type": "recurring", "unit_amount": None, "unit_amount_decimal": None, } FAKE_PRICE_METERED = { "active": True, "billing_scheme": "per_unit", "created": 1552632817, "currency": "usd", "id": "price_fakemetered", "livemode": False, "lookup_key": None, "metadata": {}, "nickname": "Sum Metered Price", "object": "price", "product": FAKE_PRODUCT["id"], "recurring": { "aggregate_usage": "sum", "interval": "month", "interval_count": 1, "trial_period_days": None, "usage_type": "metered", }, "tiers_mode": None, "transform_quantity": None, "type": "recurring", "unit_amount": 200, "unit_amount_decimal": "200", } FAKE_PRICE_ONETIME = { "active": True, "billing_scheme": "per_unit", "created": 1552632818, "currency": "usd", "id": "price_fakeonetime", "livemode": False, "lookup_key": None, "metadata": {}, "nickname": "One-Time Price", "object": "price", "product": FAKE_PRODUCT["id"], "recurring": None, "tiers_mode": None, "transform_quantity": None, "type": "one_time", "unit_amount": 2000, "unit_amount_decimal": "2000", } class SubscriptionDict(StripeItem): def __init__(self, *args, **kwargs): """Match Stripe's behavior: return a stripe iterable on `subscription.items`.""" super().__init__(*args, **kwargs) self["items"] = StripeList(self["items"]) def __setattr__(self, name, value): if type(value) == datetime.datetime: value = datetime_to_unix(value) # Special case for price and plan if name == "price": for price in [ FAKE_PRICE, FAKE_PRICE_II, FAKE_PRICE_TIER, FAKE_PRICE_METERED, ]: if value == price["id"]: value = price elif name == "plan": for plan in [FAKE_PLAN, FAKE_PLAN_II, FAKE_TIER_PLAN, FAKE_PLAN_METERED]: if value == plan["id"]: value = plan self[name] = value def delete(self, **kwargs): if "at_period_end" in kwargs: self["cancel_at_period_end"] = kwargs["at_period_end"] return self def save(self, idempotency_key=None): return self FAKE_SUBSCRIPTION = SubscriptionDict( load_fixture("subscription_sub_fakefakefakefakefake0001.json") ) FAKE_SUBSCRIPTION_NOT_PERIOD_CURRENT = deepcopy(FAKE_SUBSCRIPTION) FAKE_SUBSCRIPTION_NOT_PERIOD_CURRENT.update( {"current_period_end": 1441907581, "current_period_start": 1439229181} ) FAKE_SUBSCRIPTION_CANCELED = deepcopy(FAKE_SUBSCRIPTION) FAKE_SUBSCRIPTION_CANCELED["status"] = "canceled" FAKE_SUBSCRIPTION_CANCELED["canceled_at"] = 1440907580 FAKE_SUBSCRIPTION_CANCELED_AT_PERIOD_END = deepcopy(FAKE_SUBSCRIPTION) FAKE_SUBSCRIPTION_CANCELED_AT_PERIOD_END["canceled_at"] = 1440907580 FAKE_SUBSCRIPTION_CANCELED_AT_PERIOD_END["cancel_at_period_end"] = True FAKE_SUBSCRIPTION_II = SubscriptionDict( load_fixture("subscription_sub_fakefakefakefakefake0002.json") ) FAKE_SUBSCRIPTION_III = SubscriptionDict( load_fixture("subscription_sub_fakefakefakefakefake0003.json") ) FAKE_SUBSCRIPTION_MULTI_PLAN = SubscriptionDict( load_fixture("subscription_sub_fakefakefakefakefake0004.json") ) FAKE_SUBSCRIPTION_METERED = SubscriptionDict( { "id": "sub_1rn1dp7WgjMtx9", "object": "subscription", "application_fee_percent": None, "collection_method": "charge_automatically", "cancel_at_period_end": False, "canceled_at": None, "current_period_end": 1441907581, "current_period_start": 1439229181, "customer": "cus_6lsBvm5rJ0zyHc", "discount": None, "ended_at": None, "metadata": {"djstripe_test_fake_id": "sub_fakefakefakefakefake0005"}, "items": { "data": [ { "created": 1441907581, "id": "si_UXYmKmJp6aWTw6", "metadata": {}, "object": "subscription_item", "plan": deepcopy(FAKE_PLAN_METERED), "subscription": "sub_1rn1dp7WgjMtx9", } ] }, "pause_collection": None, "plan": deepcopy(FAKE_PLAN_METERED), "quantity": 1, "start": 1439229181, "start_date": 1439229181, "status": "active", "tax_percent": None, "trial_end": None, "trial_start": None, } ) FAKE_SUBSCRIPTION_ITEM_METERED = { "id": "si_JiphMAMFxZKW8s", "object": "subscription_item", "metadata": {}, "billing_thresholds": "", "created": 1441907581, "plan": deepcopy(FAKE_PLAN_METERED), "price": deepcopy(FAKE_PRICE_METERED), "quantity": 1, "subscription": FAKE_SUBSCRIPTION_METERED["id"], "tax_rates": [], } FAKE_SUBSCRIPTION_ITEM_MULTI_PLAN = { "id": "si_JiphMAMFxZKW8s", "object": "subscription_item", "metadata": {}, "billing_thresholds": "", "created": 1441907581, "plan": deepcopy(FAKE_PLAN), "price": deepcopy(FAKE_PRICE), "quantity": 1, "subscription": FAKE_SUBSCRIPTION_MULTI_PLAN["id"], "tax_rates": [], } FAKE_SUBSCRIPTION_ITEM_TAX_RATES = { "id": "si_JiphMAMFxZKW8s", "object": "subscription_item", "metadata": {}, "billing_thresholds": "", "created": 1441907581, "plan": deepcopy(FAKE_PLAN_II), "price": deepcopy(FAKE_PRICE_II), "quantity": 1, "subscription": FAKE_SUBSCRIPTION_II["id"], "tax_rates": [ { "id": "txr_fakefakefakefakefake0001", "object": "tax_rate", "active": True, "created": 1593225980, "description": None, "display_name": "VAT", "inclusive": True, "jurisdiction": "Example1", "livemode": False, "metadata": {"djstripe_test_fake_id": "txr_fakefakefakefakefake0001"}, "percentage": 15.0, } ], } FAKE_SUBSCRIPTION_SCHEDULE = { "id": "sub_sched_1Hm7q6Fz0jfFqjGs2OxOSCzD", "object": "subscription_schedule", "canceled_at": None, "completed_at": None, "created": 1605056974, "current_phase": {}, "customer": "cus_6lsBvm5rJ0zyHc", # FAKE_CUSTOMER "default_settings": { "billing_cycle_anchor": "automatic", "billing_thresholds": None, "collection_method": "charge_automatically", "default_payment_method": None, "default_source": None, "invoice_settings": None, "transfer_data": None, }, "end_behavior": "release", "livemode": False, "metadata": {}, "phases": [ { "add_invoice_items": [], "application_fee_percent": None, "billing_cycle_anchor": None, "billing_thresholds": None, "collection_method": None, "coupon": None, "default_payment_method": None, "default_tax_rates": [], "end_date": 1637195591, "invoice_settings": None, "plans": [ { "billing_thresholds": None, "plan": FAKE_PLAN_II["id"], "price": FAKE_PRICE_II["id"], "quantity": None, "tax_rates": [], } ], "prorate": True, "proration_behavior": "create_prorations", "start_date": 1605659591, "tax_percent": None, "transfer_data": None, "trial_end": None, } ], "released_at": None, "released_subscription": None, "renewal_interval": None, "status": "not_started", "subscription": FAKE_SUBSCRIPTION["id"], } FAKE_SHIPPING_RATE = load_fixture("shipping_rate_shr_fakefakefakefakefake0001.json") FAKE_SHIPPING_RATE_WITH_TAX_CODE = load_fixture( "shipping_rate_shr_fakefakefakefakefake0002.json" ) class Sources(object): def __init__(self, card_fakes): self.card_fakes = card_fakes def create(self, source, api_key=None): for fake_card in self.card_fakes: if fake_card["id"] == source: return fake_card def retrieve(self, id, expand=None): for fake_card in self.card_fakes: if fake_card["id"] == id: return fake_card def list(self, **kwargs): return StripeList(data=self.card_fakes) def convert_source_dict(data): if data: source_type = data["object"] if source_type == "card": data = CardDict(data) elif source_type == "bank_account": data = BankAccountDict(data) elif source_type == "source": data = SourceDict(data) else: raise ValueError(f"Unknown source type: {source_type}") return data class CustomerDict(dict): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self["default_source"] = convert_source_dict(self["default_source"]) for n, d in enumerate(self["sources"].get("data", [])): self["sources"]["data"][n] = convert_source_dict(d) def save(self, idempotency_key=None): return self def delete(self): return self @property def sources(self): return Sources(card_fakes=self["sources"]["data"]) def create_for_user(self, user): from djstripe.models import Customer stripe_customer = Customer.sync_from_stripe_data(self) stripe_customer.subscriber = user stripe_customer.save() return stripe_customer FAKE_CUSTOMER = CustomerDict(load_fixture("customer_cus_6lsBvm5rJ0zyHc.json")) # Customer with multiple subscriptions (all licensed usagetype) FAKE_CUSTOMER_II = CustomerDict(load_fixture("customer_cus_4UbFSo9tl62jqj.json")) # Customer with a Source (instead of Card) as default_source FAKE_CUSTOMER_III = CustomerDict(load_fixture("customer_cus_4QWKsZuuTHcs7X.json")) # Customer with a Bank Account as default_source FAKE_CUSTOMER_IV = CustomerDict( load_fixture("customer_cus_example_with_bank_account.json") ) FAKE_DISCOUNT = { "id": "di_fakefakefakefakefake0001", "object": "discount", "description": "", "checkout_session": None, "coupon": deepcopy(FAKE_COUPON), "customer": FAKE_CUSTOMER, "end": None, "invoice": None, "invoice_item": None, "promotion_code": "", "start": 1493206114, "subscription": "sub_fakefakefakefakefake0001", } FAKE_DISCOUNT_CUSTOMER = { "id": "di_fakefakefakefakefake0002", "object": "discount", "coupon": deepcopy(FAKE_COUPON), "customer": deepcopy(FAKE_CUSTOMER), "start": 1493206114, "end": None, "subscription": None, } FAKE_LINE_ITEM = load_fixture("line_item_il_invoice_item_fakefakefakefakefake0001.json") FAKE_LINE_ITEM["discounts"] = [deepcopy(FAKE_DISCOUNT_CUSTOMER)] FAKE_LINE_ITEM_SUBSCRIPTION = load_fixture( "line_item_il_invoice_item_fakefakefakefakefake0002.json" ) FAKE_LINE_ITEM_SUBSCRIPTION["discounts"] = [deepcopy(FAKE_DISCOUNT_CUSTOMER)] FAKE_LINE_ITEM_SUBSCRIPTION["discounts"][0]["subscription"] = "sub_1rn1dp7WgjMtx9" class InvoiceDict(StripeItem): def __init__(self, *args, **kwargs): """Match Stripe's behavior: return a stripe iterable on `invoice.lines`.""" super().__init__(*args, **kwargs) self.lines = StripeList(self.lines) def pay(self): return self FAKE_INVOICE = load_fixture("invoice_in_fakefakefakefakefake0001.json") FAKE_INVOICE["lines"] = { "object": "list", "data": [deepcopy(FAKE_LINE_ITEM)], "has_more": False, "total_count": 1, "url": "/v1/invoices/in_fakefakefakefakefake0001/lines", } FAKE_INVOICE = InvoiceDict(FAKE_INVOICE) FAKE_INVOICE_IV = InvoiceDict(load_fixture("invoice_in_fakefakefakefakefake0004.json")) FAKE_INVOICE_II = InvoiceDict( { "id": "in_16af5A2eZvKYlo2CJjANLL81", "object": "invoice", "amount_due": 3000, "amount_paid": 0, "amount_remaining": 3000, "application_fee_amount": None, "attempt_count": 1, "attempted": True, "auto_advance": True, "collection_method": "charge_automatically", "charge": FAKE_CHARGE_II["id"], "currency": "usd", "customer": "cus_4UbFSo9tl62jqj", "created": 1439785128, "description": None, "discount": None, "discounts": [], "due_date": None, "ending_balance": 0, "lines": { "data": [deepcopy(FAKE_LINE_ITEM_SUBSCRIPTION)], "total_count": 1, "object": "list", "url": "/v1/invoices/in_16af5A2eZvKYlo2CJjANLL81/lines", }, "livemode": False, "metadata": {}, "next_payment_attempt": 1440048103, "number": "XXXXXXX-0002", "paid": False, "period_end": 1439784771, "period_start": 1439698371, "receipt_number": None, "starting_balance": 0, "statement_descriptor": None, "subscription": FAKE_SUBSCRIPTION_III["id"], "subtotal": 3000, "tax": None, "tax_percent": None, "total": 3000, "webhooks_delivered_at": 1439785139, } ) FAKE_INVOICE_III = InvoiceDict( { "id": "in_16Z9dP2eZvKYlo2CgFHgFx2Z", "object": "invoice", "amount_due": 0, "amount_paid": 0, "amount_remaining": 0, "application_fee_amount": None, "attempt_count": 0, "attempted": True, "auto_advance": True, "collection_method": "charge_automatically", "charge": None, "created": 1439425915, "currency": "usd", "customer": "cus_6lsBvm5rJ0zyHc", "description": None, "discount": None, "discounts": [], "due_date": None, "ending_balance": 20, "lines": { "data": [deepcopy(FAKE_LINE_ITEM_SUBSCRIPTION)], "total_count": 1, "object": "list", "url": "/v1/invoices/in_16Z9dP2eZvKYlo2CgFHgFx2Z/lines", }, "livemode": False, "metadata": {}, "next_payment_attempt": None, "number": "XXXXXXX-0003", "paid": False, "period_end": 1439424571, "period_start": 1436746171, "receipt_number": None, "starting_balance": 0, "statement_descriptor": None, "subscription": FAKE_SUBSCRIPTION["id"], "subtotal": 20, "tax": None, "tax_percent": None, "total": 20, "webhooks_delivered_at": 1439426955, } ) FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE = deepcopy(FAKE_SUBSCRIPTION_METERED) FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE["customer"] = FAKE_CUSTOMER_II["id"] FAKE_SUBSCRIPTION_ITEM = { "id": "si_JiphMAMFxZKW8s", "object": "subscription_item", "metadata": {}, "billing_thresholds": "", "created": 1441907581, "plan": deepcopy(FAKE_PLAN_METERED), "price": deepcopy(FAKE_PRICE_METERED), "quantity": 1, "subscription": FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE["id"], "tax_rates": [], } FAKE_INVOICE_METERED_SUBSCRIPTION = InvoiceDict( { "id": "in_1JGGM6JSZQVUcJYgpWqfBOIl", "livemode": False, "created": 1439425915, "metadata": {}, "description": "", "amount_due": "1.05", "amount_paid": "1.05", "amount_remaining": "0.00", "application_fee_amount": None, "attempt_count": 1, "attempted": True, "auto_advance": False, "collection_method": "charge_automatically", "currency": "usd", "customer": FAKE_CUSTOMER_II["id"], "object": "invoice", "charge": None, "discount": None, "discounts": [], "due_date": None, "ending_balance": 0, "lines": { "data": [deepcopy(FAKE_LINE_ITEM_SUBSCRIPTION)], "total_count": 1, "object": "list", "url": "/v1/invoices/in_1JGGM6JSZQVUcJYgpWqfBOIl/lines", }, "next_payment_attempt": None, "number": "84DE1540-0004", "paid": True, "period_end": 1439424571, "period_start": 1436746171, "receipt_number": None, "starting_balance": 0, "statement_descriptor": None, "subscription": FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE["id"], "subtotal": "1.00", "tax": None, "tax_percent": None, "total": "1.00", "webhooks_delivered_at": 1439426955, } ) FAKE_UPCOMING_INVOICE = InvoiceDict( { "id": "in", "object": "invoice", "amount_due": 2000, "amount_paid": 0, "amount_remaining": 2000, "application_fee_amount": None, "attempt_count": 1, "attempted": False, "collection_method": "charge_automatically", "charge": None, "created": 1439218864, "currency": "usd", "customer": FAKE_CUSTOMER["id"], "description": None, "default_tax_rates": [ { "id": "txr_fakefakefakefakefake0001", "object": "tax_rate", "active": True, "created": 1570921289, "description": None, "display_name": "VAT", "inclusive": True, "jurisdiction": "Example1", "livemode": False, "metadata": {"djstripe_test_fake_id": "txr_fakefakefakefakefake0001"}, "percentage": 15.0, } ], "discount": None, "discounts": [], "due_date": None, "ending_balance": None, "lines": { "data": [deepcopy(FAKE_LINE_ITEM_SUBSCRIPTION)], "total_count": 1, "object": "list", "url": "/v1/invoices/in_fakefakefakefakefake0001/lines", }, "livemode": False, "metadata": {}, "next_payment_attempt": 1439218689, "number": None, "paid": False, "period_end": 1439218689, "period_start": 1439132289, "receipt_number": None, "starting_balance": 0, "statement_descriptor": None, "subscription": FAKE_SUBSCRIPTION["id"], "subtotal": 2000, "tax": 261, "tax_percent": None, "total": 2000, "total_tax_amounts": [ { "amount": 261, "inclusive": True, "tax_rate": "txr_fakefakefakefakefake0001", } ], "webhooks_delivered_at": 1439218870, } ) FAKE_TAX_RATE_EXAMPLE_1_VAT = load_fixture("tax_rate_txr_fakefakefakefakefake0001.json") FAKE_TAX_RATE_EXAMPLE_2_SALES = load_fixture( "tax_rate_txr_fakefakefakefakefake0002.json" ) FAKE_TAX_ID = load_fixture("tax_id_txi_fakefakefakefakefake0001.json") FAKE_EVENT_TAX_ID_CREATED = { "id": "evt_16YKQi2eZvKYlo2CT2oe5ff3", "object": "event", "api_version": "2020-08-27", "created": 1439229084, "data": {"object": deepcopy(FAKE_TAX_ID)}, "livemode": False, "pending_webhooks": 0, "request": "req_ZoH080M8fny6yR", "type": "customer.tax_id.created", } FAKE_TAX_ID_UPDATED = deepcopy(FAKE_TAX_ID) FAKE_TAX_ID_UPDATED["verification"] = { "status": "verified", "verified_address": None, "verified_name": "Test", } FAKE_EVENT_TAX_ID_UPDATED = { "id": "evt_1J6Fy3JSZQVUcJYgnddjnMzx", "object": "event", "api_version": "2020-08-27", "created": 1439229084, "data": {"object": deepcopy(FAKE_TAX_ID_UPDATED)}, "livemode": False, "pending_webhooks": 0, "request": "req_ZoH080M8fny6yR", "type": "customer.tax_id.updated", } FAKE_EVENT_TAX_ID_DELETED = deepcopy(FAKE_EVENT_TAX_ID_UPDATED) FAKE_EVENT_TAX_ID_DELETED["type"] = "customer.tax_id.deleted" FAKE_TAX_CODE = load_fixture("tax_code_txcd_fakefakefakefakefake0001.json") FAKE_INVOICEITEM_II = { "id": "ii_fakefakefakefakefake0001", "object": "invoiceitem", "amount": 2000, "currency": "usd", "customer": FAKE_CUSTOMER_II["id"], "date": 1439033216, "description": "One-time setup fee", "discountable": True, "discounts": [], "invoice": FAKE_INVOICE_II["id"], "livemode": False, "metadata": {"key1": "value1", "key2": "value2"}, "period": {"start": 1439033216, "end": 1439033216}, "plan": None, "price": None, "proration": False, "quantity": None, "subscription": None, "subscription_item": None, "tax_rates": [], "unit_amount": 2000, "unit_amount_decimal": "2000", } FAKE_INVOICEITEM = { "id": "ii_fakefakefakefakefake0001", # todo make these ids unique as well "object": "invoiceitem", "amount": 2000, "currency": "usd", "customer": FAKE_CUSTOMER["id"], "date": 1439033216, "description": "One-time setup fee", "discountable": True, "discounts": [], "invoice": FAKE_INVOICE["id"], "livemode": False, "metadata": {"key1": "value1", "key2": "value2"}, "period": {"start": 1439033216, "end": 1439033216}, "plan": None, "price": None, "proration": False, "quantity": None, "subscription": None, "subscription_item": None, "tax_rates": [], "unit_amount": 2000, "unit_amount_decimal": "2000", } # Invoice item with tax_rates # TODO generate this FAKE_INVOICEITEM_III = { "id": "ii_fakefakefakefakefake0001", # todo make these ids unique as well "object": "invoiceitem", "amount": 2000, "currency": "usd", "customer": FAKE_CUSTOMER_II["id"], "date": 1439033216, "description": "One-time setup fee", "discountable": True, "discounts": [], "invoice": FAKE_INVOICE_II["id"], "livemode": False, "metadata": {"key1": "value1", "key2": "value2"}, "period": {"start": 1439033216, "end": 1439033216}, "plan": None, "price": None, "proration": False, "quantity": None, "subscription": None, "subscription_item": None, "tax_rates": [FAKE_TAX_RATE_EXAMPLE_1_VAT], "unit_amount": 2000, "unit_amount_decimal": "2000", } FAKE_TRANSFER = { "id": "tr_16Y9BK2eZvKYlo2CR0ySu1BA", "object": "transfer", "amount": 100, "amount_reversed": 0, "application_fee_amount": None, "balance_transaction": deepcopy(FAKE_BALANCE_TRANSACTION_II), "created": 1439185846, "currency": "usd", "description": "Test description - 1439185984", "destination": FAKE_STANDARD_ACCOUNT["id"], "destination_payment": "py_16Y9BKFso9hLaeLueFmWAYUi", "livemode": False, "metadata": {}, "recipient": None, "reversals": { "object": "list", "total_count": 0, "has_more": False, "url": "/v1/transfers/tr_16Y9BK2eZvKYlo2CR0ySu1BA/reversals", "data": [], }, "reversed": False, "source_transaction": None, "source_type": "bank_account", } FAKE_TRANSFER_WITH_1_REVERSAL = { "id": "tr_16Y9BK2eZvKYlo2CR0ySu1BA", "object": "transfer", "amount": 100, "amount_reversed": 0, "application_fee_amount": None, "balance_transaction": deepcopy(FAKE_BALANCE_TRANSACTION_II), "created": 1439185846, "currency": "usd", "description": "Test description - 1439185984", "destination": FAKE_STANDARD_ACCOUNT["id"], "destination_payment": "py_16Y9BKFso9hLaeLueFmWAYUi", "livemode": False, "metadata": {}, "recipient": None, "reversals": { "object": "list", "total_count": 1, "has_more": False, "url": "/v1/transfers/tr_16Y9BK2eZvKYlo2CR0ySu1BA/reversals", "data": [ { "id": "trr_1J5UlFJSZQVUcJYgb38m1OZO", "object": "transfer_reversal", "amount": 20, "balance_transaction": deepcopy(FAKE_BALANCE_TRANSACTION_II), "created": 1624449653, "currency": "usd", "destination_payment_refund": "pyr_1J5UlFR44xKqawmIBvFa6gW9", "metadata": {}, "source_refund": None, "transfer": deepcopy(FAKE_TRANSFER), } ], }, "reversed": False, "source_transaction": None, "source_type": "bank_account", } FAKE_USAGE_RECORD = { "id": "mbur_1JPJz2JSZQVUcJYgK4otTE2V", "livemode": False, "object": "usage_record", "quantity": 100, "subscription_item": FAKE_SUBSCRIPTION_ITEM["id"], "timestamp": 1629174774, "action": "increment", } class UsageRecordSummaryDict(StripeItem): pass FAKE_USAGE_RECORD_SUMMARY = UsageRecordSummaryDict( load_fixture("usage_record_summary_sis_fakefakefakefakefake0001.json") ) class WebhookEndpointDict(StripeItem): pass FAKE_WEBHOOK_ENDPOINT_1 = WebhookEndpointDict( load_fixture("webhook_endpoint_fake0001.json") ) class PayoutDict(StripeItem): pass FAKE_PAYOUT_CUSTOM_BANK_ACCOUNT = PayoutDict( load_fixture("payout_custom_bank_account.json") ) FAKE_PAYOUT_CUSTOM_CARD = PayoutDict(load_fixture("payout_custom_card.json")) FAKE_ACCOUNT = { "id": "acct_1032D82eZvKYlo2C", "object": "account", "business_profile": { "name": "dj-stripe", "support_email": "djstripe@example.com", "support_phone": None, "support_url": "https://djstripe.com/support/", "url": "https://djstripe.com", }, "settings": { "branding": { "icon": "file_4hshrsKatMEEd6736724HYAXyj", "logo": "file_1E3fssKatMEEd6736724HYAXyj", "primary_color": "#092e20", }, "dashboard": {"display_name": "dj-stripe", "timezone": "Etc/UTC"}, "payments": {"statement_descriptor": "DJSTRIPE"}, }, "charges_enabled": True, "country": "US", "default_currency": "usd", "details_submitted": True, "email": "djstripe@example.com", "payouts_enabled": True, "type": "standard", } FAKE_FILEUPLOAD_LOGO = { "created": 1550134074, "filename": "logo_preview.png", "id": "file_1E3fssKatMEEd6736724HYAXyj", "links": { "data": [ { "created": 1550134074, "expired": False, "expires_at": 1850134074, "file": "file_1E3fssKatMEEd6736724HYAXyj", "id": "link_1E3fssKatMEEd673672V0JSH", "livemode": False, "metadata": {}, "object": "file_link", "url": ( "https://files.stripe.com/links/fl_test_69vG4ISDx9Chjklasrf06BJeQo" ), } ], "has_more": False, "object": "list", "url": "/v1/file_links?file=file_1E3fssKatMEEd6736724HYAXyj", }, "object": "file_upload", "purpose": "business_logo", "size": 6650, "type": "png", "url": "https://files.stripe.com/files/f_test_BTJFKcS7VDahgkjqw8EVNWlM", } FAKE_FILEUPLOAD_ICON = { "created": 1550134074, "filename": "icon_preview.png", "id": "file_4hshrsKatMEEd6736724HYAXyj", "links": { "data": [ { "created": 1550134074, "expired": False, "expires_at": 1850134074, "file": "file_4hshrsKatMEEd6736724HYAXyj", "id": "link_4jsdgsKatMEEd673672V0JSH", "livemode": False, "metadata": {}, "object": "file_link", "url": ( "https://files.stripe.com/links/fl_test_69vG4ISDx9Chjklasrf06BJeQo" ), } ], "has_more": False, "object": "list", "url": "/v1/file_links?file=file_4hshrsKatMEEd6736724HYAXyj", }, "object": "file_upload", # Note that purpose="business_logo" for both icon and logo fields "purpose": "business_logo", "size": 6650, "type": "png", "url": "https://files.stripe.com/files/f_test_BTJFKcS7VDahgkjqw8EVNWlM", } FAKE_EVENT_FILE_CREATED = { "id": "evt_1J5TusR44xKqawmIQVXSrGyf", "object": "event", "api_version": "2020-08-27", "created": 1439229084, "data": {"object": deepcopy(FAKE_FILEUPLOAD_ICON)}, "livemode": False, "pending_webhooks": 0, "request": "req_sTSstDDIOpKi2w", "type": "file.created", } FAKE_EVENT_ACCOUNT_APPLICATION_DEAUTHORIZED = dict( load_fixture("event_account_application_deauthorized.json") ) FAKE_EVENT_ACCOUNT_APPLICATION_AUTHORIZED = dict( load_fixture("event_account_application_authorized.json") ) FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_CREATED = dict( load_fixture("event_external_account_bank_account_created.json") ) FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_CREATED = dict( load_fixture("event_external_account_card_created.json") ) FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_DELETED = dict( load_fixture("event_external_account_bank_account_deleted.json") ) FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_DELETED = dict( load_fixture("event_external_account_card_deleted.json") ) FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_UPDATED = dict( load_fixture("event_external_account_bank_account_updated.json") ) FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_UPDATED = dict( load_fixture("event_external_account_card_updated.json") ) FAKE_EVENT_STANDARD_ACCOUNT_UPDATED = dict( load_fixture("event_account_updated_standard.json") ) FAKE_EVENT_EXPRESS_ACCOUNT_UPDATED = dict( load_fixture("event_account_updated_express.json") ) FAKE_EVENT_CUSTOM_ACCOUNT_UPDATED = dict( load_fixture("event_account_updated_custom.json") ) # 2017-05-25 api changed request from id to object with id and idempotency_key # issue #541 FAKE_EVENT_PLAN_REQUEST_IS_OBJECT = { "id": "evt_1AcdbXXXXXXXXXXXXXXXXXXX", "object": "event", "api_version": "2017-06-05", "created": 1499361420, "data": {"object": FAKE_PLAN, "previous_attributes": {"name": "Plan anual test4"}}, "livemode": False, "pending_webhooks": 1, "request": {"id": "req_AyamqQWoi5AMR2", "idempotency_key": None}, "type": "plan.updated", } FAKE_EVENT_CHARGE_SUCCEEDED = { "id": "evt_16YKQi2eZvKYlo2CT2oe5ff3", "object": "event", "api_version": "2016-03-07", "created": 1439229084, "data": {"object": deepcopy(FAKE_CHARGE)}, "livemode": False, "pending_webhooks": 0, "request": "req_6lsB7hkicwhaDj", "type": "charge.succeeded", } FAKE_EVENT_TEST_CHARGE_SUCCEEDED = deepcopy(FAKE_EVENT_CHARGE_SUCCEEDED) FAKE_EVENT_TEST_CHARGE_SUCCEEDED["id"] = TEST_EVENT_ID FAKE_EVENT_CUSTOMER_CREATED = { "id": "evt_38DHch3whaDvKYlo2CT2oe5ff3", "object": "event", "api_version": "2016-03-07; orders_beta=v3", "created": 1439229084, "data": {"object": deepcopy(FAKE_CUSTOMER)}, "livemode": False, "pending_webhooks": 0, "request": "req_6l38DHch3whaDj", "type": "customer.created", } FAKE_EVENT_CUSTOMER_UPDATED = deepcopy(FAKE_EVENT_CUSTOMER_CREATED) FAKE_EVENT_CUSTOMER_UPDATED["type"] = "customer.updated" FAKE_EVENT_CUSTOMER_DELETED = deepcopy(FAKE_EVENT_CUSTOMER_CREATED) FAKE_EVENT_CUSTOMER_DELETED.update( {"id": "evt_38DHch3whaDvKYlo2jksfsFFxy", "type": "customer.deleted"} ) FAKE_EVENT_CUSTOMER_DISCOUNT_CREATED = { "id": "AGBWvF5zBm4sMCsLLPZrw9YY", "object": "event", "api_version": "2018-05-21", "created": 1439229084, "data": {"object": deepcopy(FAKE_DISCOUNT_CUSTOMER)}, "livemode": False, "pending_webhooks": 0, "request": "req_6l38DHch3whaDj", "type": "customer.discount.created", } FAKE_EVENT_CUSTOMER_DISCOUNT_DELETED = { "id": "AGBWvF5zBm4sMCsLLPZrw9XX", "type": "customer.discount.deleted", "api_version": "2017-02-14", "created": 1439229084, "object": "event", "livemode": False, "pending_webhooks": 0, "request": "req_6l38DHch3whaDj", "data": {"object": deepcopy(FAKE_DISCOUNT_CUSTOMER)}, } FAKE_EVENT_CUSTOMER_SOURCE_CREATED = { "id": "evt_DvKYlo38huDvKYlo2C7SXedrZk", "object": "event", "api_version": "2016-03-07", "created": 1439229084, "data": {"object": deepcopy(FAKE_CARD)}, "livemode": False, "pending_webhooks": 0, "request": "req_o3whaDvh3whaDj", "type": "customer.source.created", } FAKE_EVENT_CUSTOMER_SOURCE_DELETED = deepcopy(FAKE_EVENT_CUSTOMER_SOURCE_CREATED) FAKE_EVENT_CUSTOMER_SOURCE_DELETED.update( {"id": "evt_DvKYlo38huDvKYlo2C7SXedrYk", "type": "customer.source.deleted"} ) FAKE_EVENT_CUSTOMER_SOURCE_DELETED_DUPE = deepcopy(FAKE_EVENT_CUSTOMER_SOURCE_DELETED) FAKE_EVENT_CUSTOMER_SOURCE_DELETED_DUPE.update({"id": "evt_DvKYlo38huDvKYlo2C7SXedzAk"}) FAKE_EVENT_CUSTOMER_SUBSCRIPTION_CREATED = { "id": "evt_38DHch3wHD2eZvKYlCT2oe5ff3", "object": "event", "api_version": "2016-03-07", "created": 1439229084, "data": {"object": deepcopy(FAKE_SUBSCRIPTION)}, "livemode": False, "pending_webhooks": 0, "request": "req_6l87IHch3diaDj", "type": "customer.subscription.created", } FAKE_EVENT_CUSTOMER_SUBSCRIPTION_DELETED = deepcopy( FAKE_EVENT_CUSTOMER_SUBSCRIPTION_CREATED ) FAKE_EVENT_CUSTOMER_SUBSCRIPTION_DELETED.update( {"id": "evt_38DHch3wHD2eZvKYlCT2oeryaf", "type": "customer.subscription.deleted"} ) FAKE_EVENT_DISPUTE_CREATED = { "id": "evt_16YKQi2eZvKYlo2CT2oe5ff3", "object": "event", "api_version": "2017-08-15", "created": 1439229084, "data": {"object": deepcopy(FAKE_DISPUTE_I)}, "livemode": False, "pending_webhooks": 0, "request": "req_6lsB7hkicwhaDj", "type": "charge.dispute.created", } FAKE_EVENT_DISPUTE_FUNDS_WITHDRAWN = { "id": "evt_1JAyTxJSZQVUcJYgNk1Jqu8o", "object": "event", "api_version": "2020-08-27", "created": 1439229084, "data": {"object": deepcopy(FAKE_DISPUTE_II)}, "livemode": False, "pending_webhooks": 0, "request": "req_6lsB7hkicwhaDj", "type": "charge.dispute.funds_withdrawn", } FAKE_EVENT_DISPUTE_UPDATED = { "id": "evt_1JAyTxJSZQVUcJYgNk1Jqu8o", "object": "event", "api_version": "2020-08-27", "created": 1439229084, "data": {"object": deepcopy(FAKE_DISPUTE_III)}, "livemode": False, "pending_webhooks": 0, "request": "req_6lsB7hkicwhaDj", "type": "charge.dispute.funds_withdrawn", } FAKE_EVENT_DISPUTE_CLOSED = { "id": "evt_1JAyTxJSZQVUcJYgNk1Jqu8o", "object": "event", "api_version": "2020-08-27", "created": 1439229084, "data": {"object": deepcopy(FAKE_DISPUTE_IV)}, "livemode": False, "pending_webhooks": 0, "request": "req_6lsB7hkicwhaDj", "type": "charge.dispute.closed", } FAKE_EVENT_DISPUTE_FUNDS_REINSTATED_FULL = { "id": "evt_1JAyTxJSZQVUcJYgNk1Jqu8o", "object": "event", "api_version": "2020-08-27", "created": 1439229084, "data": {"object": deepcopy(FAKE_DISPUTE_V_FULL)}, "livemode": False, "pending_webhooks": 0, "request": "req_6lsB7hkicwhaDj", "type": "charge.dispute.funds_reinstated", } FAKE_EVENT_DISPUTE_FUNDS_REINSTATED_PARTIAL = { "id": "evt_1JAyTxJSZQVUcJYgNk1Jqu8o", "object": "event", "api_version": "2020-08-27", "created": 1439229084, "data": {"object": deepcopy(FAKE_DISPUTE_V_PARTIAL)}, "livemode": False, "pending_webhooks": 0, "request": "req_6lsB7hkicwhaDj", "type": "charge.dispute.funds_reinstated", } FAKE_EVENT_SESSION_COMPLETED = { "id": "evt_1JAyTxJSZQVUcJYgNk1Jqu8o", "object": "event", "api_version": "2020-08-27", "created": 1439229084, "data": {"object": deepcopy(FAKE_SESSION_I)}, "livemode": False, "pending_webhooks": 0, "request": "req_6lsB7hkicwhaDj", "type": "checkout.session.completed", } FAKE_EVENT_INVOICE_CREATED = { "id": "evt_187IHD2eZvKYlo2C6YKQi2eZ", "object": "event", "api_version": "2016-03-07", "created": 1462338623, "data": {"object": deepcopy(FAKE_INVOICE)}, "livemode": False, "pending_webhooks": 0, "request": "req_8O4sB7hkDobVT", "type": "invoice.created", } FAKE_EVENT_INVOICE_DELETED = deepcopy(FAKE_EVENT_INVOICE_CREATED) FAKE_EVENT_INVOICE_DELETED.update( {"id": "evt_187IHD2eZvKYlo2Cjkjsr34H", "type": "invoice.deleted"} ) FAKE_EVENT_INVOICE_UPCOMING = { "id": "evt_187IHD2eZvKYlo2C6YKQi2bc", "object": "event", "api_version": "2017-02-14", "created": 1501859641, "data": {"object": deepcopy(FAKE_INVOICE)}, "livemode": False, "pending_webhooks": 0, "request": "req_8O4sB7hkDobZA", "type": "invoice.upcoming", } del FAKE_EVENT_INVOICE_UPCOMING["data"]["object"]["id"] FAKE_EVENT_INVOICEITEM_CREATED = { "id": "evt_187IHD2eZvKYlo2C7SXedrZk", "object": "event", "api_version": "2016-03-07", "created": 1462338623, "data": {"object": deepcopy(FAKE_INVOICEITEM)}, "livemode": False, "pending_webhooks": 0, "request": "req_8O4Qbs2EDobDVT", "type": "invoiceitem.created", } FAKE_EVENT_INVOICEITEM_DELETED = deepcopy(FAKE_EVENT_INVOICEITEM_CREATED) FAKE_EVENT_INVOICEITEM_DELETED.update( {"id": "evt_187IHD2eZvKYloJfdsnnfs34", "type": "invoiceitem.deleted"} ) FAKE_EVENT_PAYMENT_METHOD_ATTACHED = { "id": "evt_1FDOwDKatMEEd998o5FyxxAB", "object": "event", "api_version": "2019-08-14", "created": 1567228549, "data": {"object": deepcopy(FAKE_PAYMENT_METHOD_I)}, "livemode": False, "pending_webhooks": 0, "request": {"id": "req_9c9djVqxUZIKNr", "idempotency_key": None}, "type": "payment_method.attached", } FAKE_EVENT_PAYMENT_METHOD_DETACHED = { "id": "evt_1FDOwDKatMEEd998o5Fdadfds", "object": "event", "api_version": "2019-08-14", "created": 1567228549, "data": {"object": deepcopy(FAKE_PAYMENT_METHOD_I)}, "livemode": False, "pending_webhooks": 0, "request": {"id": "req_9c9djVqxcxgdfg", "idempotency_key": None}, "type": "payment_method.detached", } FAKE_EVENT_PAYMENT_METHOD_DETACHED["data"]["object"]["customer"] = None FAKE_EVENT_CARD_PAYMENT_METHOD_ATTACHED = { "id": "evt_1FDOwDKatMEEd998o5Fghgfh", "object": "event", "api_version": "2019-08-14", "created": 1567228549, "data": {"object": deepcopy(FAKE_CARD_AS_PAYMENT_METHOD)}, "livemode": False, "pending_webhooks": 0, "request": {"id": "req_9c9djVqxUhgfh", "idempotency_key": None}, "type": "payment_method.attached", } FAKE_EVENT_CARD_PAYMENT_METHOD_DETACHED = { "id": "evt_1FDOwDKatMEEd998o5435345", "object": "event", "api_version": "2019-08-14", "created": 1567228549, "data": {"object": deepcopy(FAKE_CARD_AS_PAYMENT_METHOD)}, "livemode": False, "pending_webhooks": 0, "request": {"id": "req_9c9djVqx6tgeg", "idempotency_key": None}, "type": "payment_method.detached", } # Note that the event from Stripe doesn't have customer = None FAKE_EVENT_PLAN_CREATED = { "id": "evt_1877X72eZvKYlo2CLK6daFxu", "object": "event", "api_version": "2016-03-07", "created": 1462297325, "data": {"object": deepcopy(FAKE_PLAN)}, "livemode": False, "pending_webhooks": 0, "request": "req_8NtJXPttxSvFyM", "type": "plan.created", } FAKE_EVENT_PLAN_DELETED = deepcopy(FAKE_EVENT_PLAN_CREATED) FAKE_EVENT_PLAN_DELETED.update( {"id": "evt_1877X72eZvKYl2jkds32jJFc", "type": "plan.deleted"} ) FAKE_EVENT_PRICE_CREATED = { "id": "evt_1HlZWCFz0jfFqjGsXOiPW10r", "object": "event", "api_version": "2020-03-02", "created": 1604925044, "data": {"object": deepcopy(FAKE_PRICE)}, "livemode": False, "pending_webhooks": 0, "request": {"id": "req_Nq7dDuP0HRrqcP", "idempotency_key": None}, "type": "price.created", } FAKE_EVENT_PRICE_UPDATED = { "id": "evt_1HlZbxFz0jfFqjGsZwiHHf7h", "object": "event", "api_version": "2020-03-02", "created": 1604925401, "data": { "object": FAKE_PRICE, "previous_attributes": {"unit_amount": 2000, "unit_amount_decimal": "2000"}, }, "livemode": False, "pending_webhooks": 0, "request": {"id": "req_78pnxbwPMvOIwe", "idempotency_key": None}, "type": "price.updated", } FAKE_EVENT_PRICE_DELETED = deepcopy(FAKE_EVENT_PRICE_CREATED) FAKE_EVENT_PRICE_DELETED.update( {"id": "evt_1HlZelFz0jfFqjGs0F4BML2l", "type": "price.deleted"} ) FAKE_EVENT_TRANSFER_CREATED = { "id": "evt_16igNU2eZvKYlo2CYyMkYvet", "object": "event", "api_version": "2016-03-07", "created": 1441696732, "data": {"object": deepcopy(FAKE_TRANSFER)}, "livemode": False, "pending_webhooks": 0, "request": "req_6wZW9MskhYU15Y", "type": "transfer.created", } FAKE_EVENT_TRANSFER_DELETED = deepcopy(FAKE_EVENT_TRANSFER_CREATED) FAKE_EVENT_TRANSFER_DELETED.update( {"id": "evt_16igNU2eZvKjklfsdjk232Mf", "type": "transfer.deleted"} ) FAKE_TOKEN = { "id": "tok_16YDIe2eZvKYlo2CPvqprIJd", "object": "token", "card": deepcopy(FAKE_CARD), "client_ip": None, "created": 1439201676, "livemode": False, "type": "card", "used": False, } FAKE_EVENT_PAYMENT_INTENT_SUCCEEDED_DESTINATION_CHARGE = { "id": "evt_1FG74XB7kbjcJ8Qq22i2BPdt", "object": "event", "api_version": "2019-05-16", "created": 1567874857, "data": {"object": deepcopy(FAKE_PAYMENT_INTENT_DESTINATION_CHARGE)}, "livemode": False, "pending_webhooks": 1, "request": {"id": "req_AJAmnJE4eiPIzb", "idempotency_key": None}, "type": "payment_intent.succeeded", } FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED = { "id": "evt_1Hm7q6Fz0jfFqjGsJSG4N91w", "object": "event", "api_version": "2020-03-02", "created": 1605056974, "data": {"object": deepcopy(FAKE_SUBSCRIPTION_SCHEDULE)}, "livemode": False, "pending_webhooks": 0, "request": { "id": "req_Pttj3aW5RJwees", "idempotency_key": "d2a77191-cc07-4c60-abab-5fb11357bd63", }, "type": "subscription_schedule.created", } FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED["data"]["object"]["status"] = "active" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED["data"]["object"]["current_phase"][ "start_data" ] = 1602464974 FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED["data"]["object"]["current_phase"][ "end_data" ] = 1605056974 FAKE_EVENT_SUBSCRIPTION_SCHEDULE_UPDATED = deepcopy( FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED ) FAKE_EVENT_SUBSCRIPTION_SCHEDULE_UPDATED["id"] = "sub_sched_1Hm86MFz0jfFqjGsc5iEdZee" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_UPDATED["type"] = "subscription_schedule.updated" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_RELEASED = deepcopy( FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED ) FAKE_EVENT_SUBSCRIPTION_SCHEDULE_RELEASED["id"] = "evt_1Hm878Fz0jfFqjGsClU9gE79" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_RELEASED["type"] = "subscription_schedule.released" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_RELEASED["data"]["object"]["released_at"] = 1605058030 FAKE_EVENT_SUBSCRIPTION_SCHEDULE_RELEASED["data"]["object"]["status"] = "released" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CANCELED = deepcopy( FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED ) FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CANCELED["id"] = "evt_1Hm80YFz0jfFqjGs7kKvT7RE" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CANCELED["type"] = "subscription_schedule.canceled" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CANCELED["data"]["object"]["canceled_at"] = 1605057622 FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CANCELED["data"]["object"]["status"] = "canceled" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CANCELED["data"]["previous_attributes"] = { "released_at": None, "status": "not_started", } FAKE_EVENT_SUBSCRIPTION_SCHEDULE_COMPLETED = deepcopy( FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED ) FAKE_EVENT_SUBSCRIPTION_SCHEDULE_COMPLETED["id"] = "evt_1Hm80YFz0jfFqjGs7kKvT7RE" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_COMPLETED["type"] = "subscription_schedule.completed" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_COMPLETED["data"]["object"][ "completed_at" ] = 1605057622 FAKE_EVENT_SUBSCRIPTION_SCHEDULE_COMPLETED["data"]["object"]["status"] = "completed" # would get emmited 7 days before the scheduled end_date FAKE_EVENT_SUBSCRIPTION_SCHEDULE_EXPIRING = deepcopy( FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED ) FAKE_EVENT_SUBSCRIPTION_SCHEDULE_EXPIRING["id"] = "evt_1Hm80YFz0jfFqjGs7kKvT7RE" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_EXPIRING["type"] = "subscription_schedule.expiring" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_EXPIRING["created"] = 1602464900 FAKE_EVENT_SUBSCRIPTION_SCHEDULE_ABORTED = deepcopy( FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED ) FAKE_EVENT_SUBSCRIPTION_SCHEDULE_ABORTED["id"] = "evt_1Hm80YFz0jfFqjGs7kKvT7RE" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_ABORTED["type"] = "subscription_schedule.aborted" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_ABORTED["data"]["object"]["canceled_at"] = 1605057622 FAKE_EVENT_SUBSCRIPTION_SCHEDULE_ABORTED["data"]["object"]["status"] = "canceled" FAKE_EVENT_SUBSCRIPTION_SCHEDULE_ABORTED["data"]["previous_attributes"] = { "released_at": None, "status": "not_started", } ================================================ FILE: tests/apps/__init__.py ================================================ ================================================ FILE: tests/apps/example/__init__.py ================================================ ================================================ FILE: tests/apps/example/forms.py ================================================ from django import forms import djstripe.models class PurchaseSubscriptionForm(forms.Form): email = forms.EmailField() plan = forms.ModelChoiceField(queryset=djstripe.models.Plan.objects.all()) stripe_source = forms.CharField( max_length="255", widget=forms.HiddenInput(), required=False ) class PaymentIntentForm(forms.Form): pass ================================================ FILE: tests/apps/example/management/__init__.py ================================================ ================================================ FILE: tests/apps/example/management/commands/__init__.py ================================================ ================================================ FILE: tests/apps/example/management/commands/regenerate_test_fixtures.py ================================================ import json from copy import deepcopy import stripe.api_resources import stripe.stripe_object from django.core.management import BaseCommand from stripe.error import InvalidRequestError import djstripe.models import tests from djstripe import settings as djstripe_settings from djstripe.utils import get_id_from_stripe_data """ Key used to store fake ids in the real stripe object's metadata dict """ FAKE_ID_METADATA_KEY = "djstripe_test_fake_id" class Command(BaseCommand): """ This does the following: 1) Load existing fixtures from JSON files 2) Attempts to read the corresponding objects from Stripe 3) If found, for types Stripe doesn't allow us to choose ids for, we build a map between the fake ids in the fixtures and real Stripe ids 3) If not found, creates objects in Stripe from the fixtures 4) Save objects back as fixtures, using fake ids if available The rationale for this is so that the fixtures can automatically be updated with Stripe schema changes running this command. This should make keeping our tests and model schema compatible with Stripe schema changes less pain-staking and simplify the process of upgrading the targeted Stripe API version. """ help = "Command to update test fixtures using a real Stripe account." fake_data_map = {} # type: Dict[Type[djstripe.models.StripeModel], List] fake_id_map = {} # type: Dict[str, str] def add_arguments(self, parser): parser.add_argument( "--delete-stale", action="store_true", help="Delete any untouched fixtures in the directory", ) parser.add_argument( "--update-sideeffect-fields", action="store_true", help="Don't preserve sideeffect fields such as 'created'", ) def handle(self, *args, **options): do_delete_stale_fixtures = options["delete_stale"] do_preserve_sideeffect_fields = not options["update_sideeffect_fields"] common_readonly_fields = ["object", "created", "updated", "livemode"] common_sideeffect_fields = ["created"] # TODO - is it be possible to get a list of which fields are writable from # the API? maybe using https://github.com/stripe/openapi ? # (though that's only for current version) """ Fields that we treat as read-only. Most of these will cause an error if sent to the Stripe API. """ model_extra_readonly_fields = { djstripe.models.Account: ["id"], djstripe.models.Customer: [ "account_balance", "currency", "default_source", "delinquent", "invoice_prefix", "subscriptions", "sources", ], djstripe.models.BankAccount: [ "id", "bank_name", "customer", "last4", "fingerprint", "status", ], djstripe.models.Card: [ "id", "address_line1_check", "address_zip_check", "brand", "country", "customer", "cvc_check", "dynamic_last4", "exp_month", "exp_year", "fingerprint", "funding", "last4", "tokenization_method", ], djstripe.models.PaymentIntent: ["id"], djstripe.models.PaymentMethod: ["id"], djstripe.models.Plan: [ # Can only specify one of amount and amount_decimal "amount_decimal" ], djstripe.models.Source: [ "id", "amount", "card", "client_secret", "currency", "customer", "flow", "owner", "statement_descriptor", "status", "type", "usage", ], djstripe.models.Subscription: [ "id", # not actually read-only "billing_cycle_anchor", "billing", "current_period_end", "current_period_start", # workaround for "the # `invoice_customer_balance_settings[consume_applied_balance_on_void]` # parameter is only supported in API version 2019-11-05 and below. # See # https://stripe.com/docs/api#versioning and # https://stripe.com/docs/upgrades#2019-12-03 for more detail. "invoice_customer_balance_settings", "latest_invoice", "start", "start_date", "status", ], djstripe.models.TaxRate: ["id"], } # type: Dict[Type[djstripe.models.StripeModel], List[str]] """ Fields that we don't care about the value of, and that preserving allows us to avoid churn in the fixtures """ model_sideeffect_fields = { djstripe.models.BalanceTransaction: ["available_on"], djstripe.models.Source: ["client_secret"], djstripe.models.Charge: ["receipt_url"], djstripe.models.Subscription: [ "billing_cycle_anchor", "current_period_start", "current_period_end", "start", "start_date", ], djstripe.models.SubscriptionItem: [ # we don't currently track separate fixtures for SubscriptionItems "id" ], djstripe.models.Product: ["updated"], djstripe.models.Invoice: [ "date", "finalized_at", "hosted_invoice_url", "invoice_pdf", "webhooks_delivered_at", "period_start", "period_end", # we don't currently track separate fixtures for SubscriptionItems "subscription_item", ], } # type: Dict[Type[djstripe.models.StripeModel], List[str]] object_sideeffect_fields = { model.stripe_class.OBJECT_NAME: set(v) for model, v in model_sideeffect_fields.items() } # type: Dict[str, Set[str]] self.fake_data_map = { # djstripe.models.Account: [tests.FAKE_ACCOUNT], djstripe.models.Customer: [ tests.FAKE_CUSTOMER, tests.FAKE_CUSTOMER_II, tests.FAKE_CUSTOMER_III, tests.FAKE_CUSTOMER_IV, ], djstripe.models.BankAccount: [tests.FAKE_BANK_ACCOUNT_SOURCE], djstripe.models.Card: [ tests.FAKE_CARD, tests.FAKE_CARD_II, tests.FAKE_CARD_III, ], djstripe.models.Source: [tests.FAKE_SOURCE], djstripe.models.Plan: [tests.FAKE_PLAN, tests.FAKE_PLAN_II], djstripe.models.Price: [tests.FAKE_PRICE, tests.FAKE_PRICE_II], djstripe.models.Product: [tests.FAKE_PRODUCT], djstripe.models.TaxRate: [ tests.FAKE_TAX_RATE_EXAMPLE_1_VAT, tests.FAKE_TAX_RATE_EXAMPLE_2_SALES, ], djstripe.models.Subscription: [ tests.FAKE_SUBSCRIPTION, tests.FAKE_SUBSCRIPTION_II, tests.FAKE_SUBSCRIPTION_III, tests.FAKE_SUBSCRIPTION_MULTI_PLAN, ], djstripe.models.SubscriptionSchedule: [ tests.FAKE_SUBSCRIPTION_SCHEDULE, ], djstripe.models.Invoice: [tests.FAKE_INVOICE, tests.FAKE_INVOICE_IV], djstripe.models.Charge: [tests.FAKE_CHARGE], djstripe.models.PaymentIntent: [tests.FAKE_PAYMENT_INTENT_I], djstripe.models.PaymentMethod: [ tests.FAKE_PAYMENT_METHOD_I, tests.FAKE_CARD_AS_PAYMENT_METHOD, ], djstripe.models.BalanceTransaction: [tests.FAKE_BALANCE_TRANSACTION], } self.init_fake_id_map() objs = [] # Regenerate each of the fixture objects via Stripe # We re-fetch objects in a second pass if they were created during # the first pass, to ensure nested objects are up to date # (eg Customer.subscriptions), for n in range(2): any_created = False self.stdout.write(f"Updating fixture objects, pass {n}") # reset the objects list since we don't want to keep those from # the first pass objs.clear() for model_class, old_objs in self.fake_data_map.items(): readonly_fields = ( common_readonly_fields + model_extra_readonly_fields.get(model_class, []) ) for old_obj in old_objs: created, obj = self.update_fixture_obj( old_obj=deepcopy(old_obj), model_class=model_class, readonly_fields=readonly_fields, do_preserve_sideeffect_fields=do_preserve_sideeffect_fields, object_sideeffect_fields=object_sideeffect_fields, common_sideeffect_fields=common_sideeffect_fields, ) objs.append(obj) any_created = created or any_created if not any_created: # nothing created on this pass, no need to continue break else: self.stderr.write( "Warning, unexpected behaviour - some fixtures still being created " "in second pass?" ) # Now the fake_id_map should be complete and the objs should be up to date, # save all the fixtures paths = set() for obj in objs: path = self.save_fixture(obj) paths.add(path) if do_delete_stale_fixtures: for path in tests.FIXTURE_DIR_PATH.glob("*.json"): if path in paths: continue else: self.stdout.write(f"deleting {path!r}") path.unlink() def init_fake_id_map(self): """ Build a mapping between fake ids stored in Stripe metadata and those obj's actual ids We do this so we can have fixtures with stable ids for objects Stripe doesn't allow us to specify an id for (eg Card). Fixtures and tests will use the fake ids, when we talk to stripe we use the real ids :return: """ for fake_customer in self.fake_data_map[djstripe.models.Customer]: try: # can only access Cards via the customer customer = djstripe.models.Customer( id=fake_customer["id"] ).api_retrieve() except InvalidRequestError: self.stdout.write( f"Fake customer {fake_customer['id']} doesn't exist in Stripe yet" ) return # assume that test customers don't have more than 100 cards... for card in customer.sources.list(limit=100): self.update_fake_id_map(card) for payment_method in djstripe.models.PaymentMethod.api_list( customer=customer.id, type="card" ): self.update_fake_id_map(payment_method) for subscription in customer["subscriptions"]["data"]: self.update_fake_id_map(subscription) for tax_rate in djstripe.models.TaxRate.api_list(): self.update_fake_id_map(tax_rate) def update_fake_id_map(self, obj): fake_id = self.get_fake_id(obj) actual_id = obj["id"] if fake_id: if fake_id in self.fake_id_map: assert self.fake_id_map[fake_id] == actual_id, ( f"Duplicate fake_id {fake_id} - reset your test Stripe data at " f"https://dashboard.stripe.com/account/data" ) self.fake_id_map[fake_id] = actual_id return fake_id else: return actual_id def get_fake_id(self, obj): """ Get a stable fake id from a real Stripe object, we use this so that fixtures are stable :param obj: :return: """ fake_id = None if isinstance(obj, str): real_id = obj real_id_map = {v: k for k, v in self.fake_id_map.items()} fake_id = real_id_map.get(real_id) elif "metadata" in obj: # Note: not all objects have a metadata dict # (eg Account, BalanceTransaction don't) fake_id = obj.get("metadata", {}).get(FAKE_ID_METADATA_KEY) elif obj.get("object") == "balance_transaction": # assume for purposes of fixture generation that 1 balance_transaction per # source charge (etc) fake_source_id = self.get_fake_id(obj["source"]) fake_id = f"txn_fake_{fake_source_id}" return fake_id def fake_json_ids(self, json_str): """ Replace real ids with fakes ones in the JSON fixture Do this on the serialized JSON string since it's a simple string replace :param json_str: :return: """ for fake_id, actual_id in self.fake_id_map.items(): json_str = json_str.replace(actual_id, fake_id) return json_str def unfake_json_ids(self, json_str): """ Replace fake ids with actual ones in the JSON fixture Do this on the serialized JSON string since it's a simple string replace :param json_str: :return: """ for fake_id, actual_id in self.fake_id_map.items(): json_str = json_str.replace(fake_id, actual_id) # special-case: undo the replace for the djstripe_test_fake_id in metadata json_str = json_str.replace( f'"{FAKE_ID_METADATA_KEY}": "{actual_id}"', f'"{FAKE_ID_METADATA_KEY}": "{fake_id}"', ) return json_str def update_fixture_obj( self, old_obj, model_class, readonly_fields, do_preserve_sideeffect_fields, object_sideeffect_fields, common_sideeffect_fields, ): """ Given a fixture object, update it via stripe :param model_class: :param old_obj: :param readonly_fields: :return: """ # restore real ids from Stripe old_obj = json.loads(self.unfake_json_ids(json.dumps(old_obj))) id_ = old_obj["id"] self.stdout.write(f"{model_class.__name__} {id_}", ending="") # For objects that we can't directly choose the ids of # (and that will thus vary between stripe accounts) # we fetch the id from a related object if issubclass(model_class, djstripe.models.Account): created, obj = self.get_or_create_stripe_account( old_obj=old_obj, readonly_fields=readonly_fields ) elif issubclass(model_class, djstripe.models.BankAccount): created, obj = self.get_or_create_stripe_bank_account( old_obj=old_obj, readonly_fields=readonly_fields ) elif issubclass(model_class, djstripe.models.Card): created, obj = self.get_or_create_stripe_card( old_obj=old_obj, readonly_fields=readonly_fields ) elif issubclass(model_class, djstripe.models.Source): created, obj = self.get_or_create_stripe_source( old_obj=old_obj, readonly_fields=readonly_fields ) elif issubclass(model_class, djstripe.models.Invoice): created, obj = self.get_or_create_stripe_invoice( old_obj=old_obj, writable_fields=["metadata"] ) elif issubclass(model_class, djstripe.models.Charge): created, obj = self.get_or_create_stripe_charge( old_obj=old_obj, writable_fields=["metadata"] ) elif issubclass(model_class, djstripe.models.PaymentIntent): created, obj = self.get_or_create_stripe_payment_intent( old_obj=old_obj, writable_fields=["metadata"] ) elif issubclass(model_class, djstripe.models.PaymentMethod): created, obj = self.get_or_create_stripe_payment_method( old_obj=old_obj, writable_fields=["metadata"] ) elif issubclass(model_class, djstripe.models.BalanceTransaction): created, obj = self.get_or_create_stripe_balance_transaction( old_obj=old_obj ) else: try: # fetch from Stripe, using the active API version # this allows us regenerate the fixtures from Stripe # and hopefully, automatically get schema changes obj = model_class(id=id_).api_retrieve() created = False self.stdout.write(" found") except InvalidRequestError: self.stdout.write(" creating") create_obj = deepcopy(old_obj) # create in Stripe for k in readonly_fields: create_obj.pop(k, None) if issubclass(model_class, djstripe.models.Subscription): create_obj = self.pre_process_subscription(create_obj=create_obj) obj = model_class._api_create(**create_obj) created = True self.update_fake_id_map(obj) if do_preserve_sideeffect_fields: obj = self.preserve_old_sideeffect_values( old_obj=old_obj, new_obj=obj, object_sideeffect_fields=object_sideeffect_fields, common_sideeffect_fields=common_sideeffect_fields, ) return created, obj def get_or_create_stripe_account(self, old_obj, readonly_fields): obj = djstripe.models.Account().api_retrieve() return True, obj def get_or_create_stripe_bank_account(self, old_obj, readonly_fields): customer_id = old_obj["customer"] try: obj = stripe.Customer.retrieve_source(customer_id, old_obj["id"]) created = False self.stdout.write(" found") except InvalidRequestError: self.stdout.write(" creating") create_obj = deepcopy(old_obj) # create in Stripe for k in readonly_fields: create_obj.pop(k, None) # see https://stripe.com/docs/connect/testing#account-numbers # we've stash the account number in the metadata # so we can regenerate the fixture create_obj["account_number"] = old_obj["metadata"][ "djstripe_test_fixture_account_number" ] create_obj["object"] = "bank_account" obj = stripe.Customer.create_source(customer_id, source=create_obj) created = True return created, obj def get_or_create_stripe_card(self, old_obj, readonly_fields): customer_id = old_obj["customer"] try: obj = stripe.Customer.retrieve_source(customer_id, old_obj["id"]) created = False self.stdout.write(" found") except InvalidRequestError: self.stdout.write(" creating") create_obj = deepcopy(old_obj) # create in Stripe for k in readonly_fields: create_obj.pop(k, None) obj = stripe.Customer.create_source(**{"source": "tok_visa"}) for k, v in create_obj.items(): setattr(obj, k, v) obj.save() created = True return created, obj def get_or_create_stripe_source(self, old_obj, readonly_fields): customer_id = old_obj["customer"] try: obj = stripe.Customer.retrieve_source(customer_id, old_obj["id"]) created = False self.stdout.write(" found") except InvalidRequestError: self.stdout.write(" creating") create_obj = deepcopy(old_obj) # create in Stripe for k in readonly_fields: create_obj.pop(k, None) source_obj = djstripe.models.Source._api_create( **{"token": "tok_visa", "type": "card"} ) obj = stripe.Customer.create_source(**{"source": source_obj.id}) for k, v in create_obj.items(): setattr(obj, k, v) obj.save() created = True return created, obj def get_or_create_stripe_invoice(self, old_obj, writable_fields): subscription = djstripe.models.Subscription( id=old_obj["subscription"] ).api_retrieve() id_ = subscription["latest_invoice"] try: obj = djstripe.models.Invoice(id=id_).api_retrieve() created = False self.stdout.write(f" found {id_}") except InvalidRequestError: assert False, "Expected to find invoice via subscription" for k in writable_fields: if isinstance(obj.get(k), dict): # merge dicts (eg metadata) obj[k].update(old_obj.get(k, {})) else: obj[k] = old_obj[k] obj.save() return created, obj def get_or_create_stripe_charge(self, old_obj, writable_fields): invoice = djstripe.models.Invoice(id=old_obj["invoice"]).api_retrieve() id_ = invoice["charge"] try: obj = djstripe.models.Charge(id=id_).api_retrieve() created = False self.stdout.write(f" found {id_}") except InvalidRequestError: assert False, "Expected to find charge via invoice" for k in writable_fields: if isinstance(obj.get(k), dict): # merge dicts (eg metadata) obj[k].update(old_obj.get(k, {})) else: obj[k] = old_obj[k] obj.save() return created, obj def get_or_create_stripe_payment_intent(self, old_obj, writable_fields): invoice = djstripe.models.Invoice(id=old_obj["invoice"]).api_retrieve() id_ = invoice["payment_intent"] try: obj = djstripe.models.PaymentIntent(id=id_).api_retrieve() created = False self.stdout.write(f" found {id_}") except InvalidRequestError: assert False, "Expected to find payment_intent via invoice" for k in writable_fields: if isinstance(obj.get(k), dict): # merge dicts (eg metadata) obj[k].update(old_obj.get(k, {})) else: obj[k] = old_obj[k] obj.save() return created, obj def get_or_create_stripe_payment_method(self, old_obj, writable_fields): id_ = old_obj["id"] customer_id = old_obj["customer"] type_ = old_obj["type"] try: obj = djstripe.models.PaymentMethod(id=id_).api_retrieve() created = False self.stdout.write(" found") except InvalidRequestError: self.stdout.write(" creating") obj = djstripe.models.PaymentMethod()._api_create( type=type_, card={"token": "tok_visa"} ) stripe.PaymentMethod.attach( obj["id"], customer=customer_id, api_key=djstripe_settings.djstripe_settings.STRIPE_SECRET_KEY, ) for k in writable_fields: if isinstance(obj.get(k), dict): # merge dicts (eg metadata) obj[k].update(old_obj.get(k, {})) else: obj[k] = old_obj[k] obj.save() created = True return created, obj def get_or_create_stripe_balance_transaction(self, old_obj): source = old_obj["source"] if source.startswith("ch_"): charge = djstripe.models.Charge(id=source).api_retrieve() id_ = get_id_from_stripe_data(charge["balance_transaction"]) try: obj = djstripe.models.BalanceTransaction(id=id_).api_retrieve() created = False self.stdout.write(f" found {id_}") except InvalidRequestError: assert False, "Expected to find balance transaction via source" return created, obj def save_fixture(self, obj): type_name = obj["object"] id_ = self.update_fake_id_map(obj) fixture_path = tests.FIXTURE_DIR_PATH.joinpath(f"{type_name}_{id_}.json") with fixture_path.open("w") as f: json_str = self.fake_json_ids(json.dumps(obj, indent=4)) f.write(json_str) return fixture_path def pre_process_subscription(self, create_obj): # flatten plan/items/tax rates on create items = create_obj.get("items", {}).get("data", []) if len(items): # don't try and create with both plan and item (list of plans) create_obj.pop("plan", None) create_obj.pop("quantity", None) # TODO - move this to SubscriptionItem handling? subscription_item_create_fields = { "plan", "billing_thresholds", "metadata", "quantity", "tax_rates", } create_items = [] for item in items: create_item = { k: v for k, v in item.items() if k in subscription_item_create_fields } create_item["plan"] = get_id_from_stripe_data(create_item["plan"]) if create_item.get("tax_rates", []): create_item["tax_rates"] = [ get_id_from_stripe_data(t) for t in create_item["tax_rates"] ] create_items.append(create_item) create_obj["items"] = create_items else: # don't try and send empty items list create_obj.pop("items", None) create_obj["plan"] = get_id_from_stripe_data(create_obj["plan"]) if create_obj.get("default_tax_rates", []): create_obj["default_tax_rates"] = [ get_id_from_stripe_data(t) for t in create_obj["default_tax_rates"] ] # don't send both default_tax_rates and tax_percent create_obj.pop("tax_percent", None) return create_obj def preserve_old_sideeffect_values( self, old_obj, new_obj, object_sideeffect_fields, common_sideeffect_fields ): """ Try to preserve values of side-effect fields from old_obj, to reduce churn in fixtures """ object_name = new_obj.get("object") sideeffect_fields = object_sideeffect_fields.get(object_name, set()).union( set(common_sideeffect_fields) ) old_obj = old_obj or {} for f, old_val in old_obj.items(): try: new_val = new_obj[f] except KeyError: continue if isinstance(new_val, stripe.api_resources.ListObject): # recursively process nested lists for n, (old_val_item, new_val_item) in enumerate( zip(old_val.get("data", []), new_val.data) ): new_val.data[n] = self.preserve_old_sideeffect_values( old_obj=old_val_item, new_obj=new_val_item, object_sideeffect_fields=object_sideeffect_fields, common_sideeffect_fields=common_sideeffect_fields, ) elif isinstance(new_val, stripe.stripe_object.StripeObject): # recursively process nested objects new_obj[f] = self.preserve_old_sideeffect_values( old_obj=old_val, new_obj=new_val, object_sideeffect_fields=object_sideeffect_fields, common_sideeffect_fields=common_sideeffect_fields, ) elif ( f in sideeffect_fields and type(old_val) == type(new_val) and old_val != new_val ): # only preserve old values if the type is the same new_obj[f] = old_val return new_obj ================================================ FILE: tests/apps/example/templates/checkout.html ================================================ Checkout ================================================ FILE: tests/apps/example/templates/checkout_success.html ================================================ {% extends "base.html" %} {% block title %}Thanks for your order!{% endblock %} {% block content %}

    We appreciate your business! If you have any questions, please email orders@example.com.

    {% endblock content %} ================================================ FILE: tests/apps/example/templates/example_base.html ================================================ {% extends "base.html" %} {% block header_css %} {{ block.super }} {% endblock %} ================================================ FILE: tests/apps/example/templates/payment_intent.html ================================================ {% extends "example_base.html" %} {% block header_js %} {{ block.super }} {% endblock %} {% block header_css %} {{ block.super }} {% endblock %} {% block content %}

    Example Payment Intent Manual Configuration

    {% endblock %} ================================================ FILE: tests/apps/example/templates/purchase_subscription.html ================================================ {% extends "example_base.html" %} {% comment%} example subscription purchase page, very closely based on https://stripe.com/docs/stripe-js/elements/quickstart {% endcomment %} {% block header_js %} {{ block.super }} {% endblock %} {% block header_css %} {{ block.super }} {% endblock %} {% block content %}

    Example purchase of a Subscription

    {% csrf_token %} {{form}}
    {% endblock %} ================================================ FILE: tests/apps/example/templates/purchase_subscription_success.html ================================================ {% extends "example_base.html" %} {% block content %}
    Subscription "{{ subscription }}" created
    {% endblock %}} ================================================ FILE: tests/apps/example/urls.py ================================================ from django.urls import path from . import views app_name = "djstripe_example" urlpatterns = [ path( "checkout/", views.CreateCheckoutSessionView.as_view(), name="checkout", ), path("success/", views.CheckoutSessionSuccessView.as_view(), name="success"), path( "purchase-subscription", views.PurchaseSubscriptionView.as_view(), name="purchase_subscription", ), path( "purchase-subscription-success/", views.PurchaseSubscriptionSuccessView.as_view(), name="purchase_subscription_success", ), path("payment-intent", views.create_payment_intent, name="payment_intent"), ] ================================================ FILE: tests/apps/example/views.py ================================================ import json import logging import stripe from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponse from django.template.response import TemplateResponse from django.urls import reverse from django.views.generic import DetailView, FormView from django.views.generic.base import TemplateView from djstripe import models from djstripe import settings as djstripe_settings from . import forms logger = logging.getLogger(__name__) User = get_user_model() stripe.api_key = djstripe_settings.djstripe_settings.STRIPE_SECRET_KEY class CreateCheckoutSessionView(LoginRequiredMixin, TemplateView): """ Example View to demonstrate how to use dj-stripe to: * Create a Stripe Checkout Session (for a new and a returning customer) * Add SUBSCRIBER_CUSTOMER_KEY to metadata to populate customer.subscriber model field * Fill out Payment Form and Complete Payment Redirects the User to Stripe Checkout Session. This does a logged in purchase for a new and a returning customer using Stripe Checkout """ template_name = "checkout.html" def get_context_data(self, **kwargs): """ Creates and returns a Stripe Checkout Session """ # Get Parent Context context = super().get_context_data(**kwargs) # to initialise Stripe.js on the front end context[ "STRIPE_PUBLIC_KEY" ] = djstripe_settings.djstripe_settings.STRIPE_PUBLIC_KEY success_url = self.request.build_absolute_uri( reverse("djstripe_example:success") ) cancel_url = self.request.build_absolute_uri(reverse("home")) # get the id of the Model instance of djstripe_settings.djstripe_settings.get_subscriber_model() # here we have assumed it is the Django User model. It could be a Team, Company model too. # note that it needs to have an email field. id = self.request.user.id # example of how to insert the SUBSCRIBER_CUSTOMER_KEY: id in the metadata # to add customer.subscriber to the newly created/updated customer. metadata = { f"{djstripe_settings.djstripe_settings.SUBSCRIBER_CUSTOMER_KEY}": id } try: # retreive the Stripe Customer. customer = models.Customer.objects.get(subscriber=self.request.user) print("Customer Object in DB.") # ! Note that Stripe will always create a new Customer Object if customer id not provided # ! even if customer_email is provided! session = stripe.checkout.Session.create( payment_method_types=["card"], customer=customer.id, # payment_method_types=["bacs_debit"], # for bacs_debit payment_intent_data={ "setup_future_usage": "off_session", # so that the metadata gets copied to the associated Payment Intent and Charge Objects "metadata": metadata, }, line_items=[ { "price_data": { "currency": "usd", # "currency": "gbp", # for bacs_debit "unit_amount": 2000, "product_data": { "name": "Sample Product Name", "images": ["https://i.imgur.com/EHyR2nP.png"], "description": "Sample Description", }, }, "quantity": 1, }, ], mode="payment", success_url=success_url, cancel_url=cancel_url, metadata=metadata, ) except models.Customer.DoesNotExist: print("Customer Object not in DB.") session = stripe.checkout.Session.create( payment_method_types=["card"], # payment_method_types=["bacs_debit"], # for bacs_debit payment_intent_data={ "setup_future_usage": "off_session", # so that the metadata gets copied to the associated Payment Intent and Charge Objects "metadata": metadata, }, line_items=[ { "price_data": { "currency": "usd", # "currency": "gbp", # for bacs_debit "unit_amount": 2000, "product_data": { "name": "Sample Product Name", "images": ["https://i.imgur.com/EHyR2nP.png"], "description": "Sample Description", }, }, "quantity": 1, }, ], mode="payment", success_url=success_url, cancel_url=cancel_url, metadata=metadata, ) context["CHECKOUT_SESSION_ID"] = session.id return context class CheckoutSessionSuccessView(TemplateView): """ Template View for showing Checkout Payment Success """ template_name = "checkout_success.html" class PurchaseSubscriptionView(FormView): """ Example view to demonstrate how to use dj-stripe to: * create a Customer * add a card to the Customer * create a Subscription using that card This does a non-logged in purchase for the user of the provided email """ template_name = "purchase_subscription.html" form_class = forms.PurchaseSubscriptionForm def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) if models.Plan.objects.count() == 0: raise Exception( "No Product Plans in the dj-stripe database - create some in your " "stripe account and then " "run `./manage.py djstripe_sync_models Plan` " "(or use the dj-stripe webhooks)" ) context[ "STRIPE_PUBLIC_KEY" ] = djstripe_settings.djstripe_settings.STRIPE_PUBLIC_KEY return context def form_valid(self, form): stripe_source = form.cleaned_data["stripe_source"] email = form.cleaned_data["email"] plan = form.cleaned_data["plan"] # Guest checkout with the provided email try: user = User.objects.get(email=email) except User.DoesNotExist: user = User.objects.create(username=email, email=email) # Create the stripe Customer, by default subscriber Model is User, # this can be overridden with djstripe_settings.djstripe_settings.DJSTRIPE_SUBSCRIBER_MODEL customer, created = models.Customer.get_or_create(subscriber=user) # Add the source as the customer's default card customer.add_card(stripe_source) # Using the Stripe API, create a subscription for this customer, # using the customer's default payment source stripe_subscription = stripe.Subscription.create( customer=customer.id, items=[{"plan": plan.id}], collection_method="charge_automatically", # tax_percent=15, api_key=djstripe_settings.djstripe_settings.STRIPE_SECRET_KEY, ) # Sync the Stripe API return data to the database, # this way we don't need to wait for a webhook-triggered sync subscription = models.Subscription.sync_from_stripe_data(stripe_subscription) self.request.subscription = subscription return super().form_valid(form) def get_success_url(self): return reverse( "djstripe_example:purchase_subscription_success", kwargs={"id": self.request.subscription.id}, ) class PurchaseSubscriptionSuccessView(DetailView): template_name = "purchase_subscription_success.html" queryset = models.Subscription.objects.all() slug_field = "id" slug_url_kwarg = "id" context_object_name = "subscription" def create_payment_intent(request): if request.method == "POST": intent = None data = json.loads(request.body) try: if "payment_method_id" in data: # Create the PaymentIntent intent = stripe.PaymentIntent.create( payment_method=data["payment_method_id"], amount=1099, currency="usd", confirmation_method="manual", confirm=True, api_key=djstripe_settings.djstripe_settings.STRIPE_SECRET_KEY, ) elif "payment_intent_id" in data: intent = stripe.PaymentIntent.confirm( data["payment_intent_id"], api_key=djstripe_settings.djstripe_settings.STRIPE_SECRET_KEY, ) except stripe.error.CardError as e: # Display error on client return_data = json.dumps({"error": e.user_message}), 200 return HttpResponse( return_data[0], content_type="application/json", status=return_data[1] ) if ( intent.status == "requires_action" and intent.next_action.type == "use_stripe_sdk" ): # Tell the client to handle the action return_data = ( json.dumps( { "requires_action": True, "payment_intent_client_secret": intent.client_secret, } ), 200, ) elif intent.status == "succeeded": # The payment did not need any additional actions and completed! # Handle post-payment fulfillment return_data = json.dumps({"success": True}), 200 else: # Invalid status return_data = json.dumps({"error": "Invalid PaymentIntent status"}), 500 return HttpResponse( return_data[0], content_type="application/json", status=return_data[1] ) else: context = { "STRIPE_PUBLIC_KEY": djstripe_settings.djstripe_settings.STRIPE_PUBLIC_KEY } return TemplateResponse(request, "payment_intent.html", context) ================================================ FILE: tests/apps/testapp/__init__.py ================================================ ================================================ FILE: tests/apps/testapp/models.py ================================================ from django.db.models.base import Model from django.db.models.fields import CharField, EmailField class Organization(Model): """Model used to test the new custom model setting.""" email = EmailField() class StaticEmailOrganization(Model): """Model used to test the new custom model setting.""" name = CharField(max_length=200, unique=True) @property def email(self): return "static@example.com" class NoEmailOrganization(Model): """Model used to test the new custom model setting.""" name = CharField(max_length=200, unique=True) ================================================ FILE: tests/apps/testapp/urls.py ================================================ from django.http import HttpResponse from django.urls import include, path def empty_view(request): return HttpResponse() urlpatterns = [ path("", empty_view, name="test_url_name"), path("djstripe/", include("djstripe.urls", namespace="djstripe")), ] ================================================ FILE: tests/apps/testapp_content/README.md ================================================ Represents protected content ================================================ FILE: tests/apps/testapp_content/__init__.py ================================================ ================================================ FILE: tests/apps/testapp_content/models.py ================================================ ================================================ FILE: tests/apps/testapp_content/urls.py ================================================ """ Represents protected content """ from django.http import HttpResponse from django.urls import path def testview(request): return HttpResponse() urlpatterns = [path("", testview, name="test_url_content")] ================================================ FILE: tests/apps/testapp_namespaced/__init__.py ================================================ ================================================ FILE: tests/apps/testapp_namespaced/models.py ================================================ ================================================ FILE: tests/apps/testapp_namespaced/urls.py ================================================ from django.http import HttpResponse from django.urls import path def testview(request): return HttpResponse() app_name = "testapp_namespaced" urlpatterns = [path("", testview, name="test_url_namespaced")] ================================================ FILE: tests/conftest.py ================================================ """ Module for creating re-usable fixtures to be used across the test suite """ import pytest import stripe from django.contrib.auth import get_user_model from . import FAKE_CUSTOMER, FAKE_PLATFORM_ACCOUNT pytestmark = pytest.mark.django_db @pytest.fixture(autouse=True) def create_account(monkeypatch): """ Fixture to automatically create and assign the default testing keys to the Platform Account """ def mock_account_retrieve(*args, **kwargs): return FAKE_PLATFORM_ACCOUNT monkeypatch.setattr(stripe.Account, "retrieve", mock_account_retrieve) # create a Stripe Platform Account FAKE_PLATFORM_ACCOUNT.create() @pytest.fixture def fake_user(): user = get_user_model().objects.create_user( username="testuser", email="testuser@example.com" ) return user @pytest.fixture def fake_customer(fake_user): customer = FAKE_CUSTOMER.create_for_user(fake_user) return customer ================================================ FILE: tests/fields/admin.py ================================================ from django.contrib import admin from django.shortcuts import render from djstripe.admin.admin import StripeModelAdmin from djstripe.admin.forms import CustomActionForm from .models import CustomActionModel @admin.register(CustomActionModel) class CustomActionModelAdmin(StripeModelAdmin): def get_actions(self, request): # get all actions actions = super().get_actions(request) # For Subscription model's custom action, _cancel actions["_cancel"] = self.get_action("_cancel") # For SubscriptionSchedule's custom action, _release_subscription_schedule actions["_release_subscription_schedule"] = self.get_action( "_release_subscription_schedule" ) # For SubscriptionSchedule's custom action, _cancel_subscription_schedule actions["_cancel_subscription_schedule"] = self.get_action( "_cancel_subscription_schedule" ) return actions @admin.action(description="Cancel selected subscriptions") def _cancel(self, request, queryset): """Cancel a subscription.""" context = self.get_admin_action_context(queryset, "_cancel", CustomActionForm) return render(request, "djstripe/admin/confirm_action.html", context) @admin.display(description="Release Selected Subscription Schedules") def _release_subscription_schedule(self, request, queryset): """Release a SubscriptionSchedule.""" context = self.get_admin_action_context( queryset, "_release_subscription_schedule", CustomActionForm ) return render(request, "djstripe/admin/confirm_action.html", context) def _cancel_subscription_schedule(self, request, queryset): """Cancel a SubscriptionSchedule.""" context = self.get_admin_action_context( queryset, "_cancel_subscription_schedule", CustomActionForm ) return render(request, "djstripe/admin/confirm_action.html", context) ================================================ FILE: tests/fields/models.py ================================================ """Models used exclusively for testing""" from django.db import models from djstripe.fields import StripePercentField from djstripe.models import StripeModel class ExampleDecimalModel(models.Model): noval = StripePercentField() class MockStripeClass: def retrieve(self): return self class CustomActionModel(StripeModel): # for some reason having a FK here throws relation doesn't exist even though # djstripe is also one of the installed apps in tests.settings djstripe_owner_account = None stripe_class = MockStripeClass # For Subscription model's custom action, _cancel def cancel(self, at_period_end: bool = False, **kwargs): pass ================================================ FILE: tests/fixtures/account_custom_acct_1IuHosQveW0ONQsd.json ================================================ { "id": "acct_1IuHosQveW0ONQsd", "object": "account", "business_profile": { "mcc": null, "name": null, "product_description": null, "support_address": null, "support_email": null, "support_phone": null, "support_url": null, "url": null }, "business_type": "individual", "capabilities": { "transfers": "inactive" }, "charges_enabled": false, "company": { "address": { "city": null, "country": "US", "line1": null, "line2": null, "postal_code": null, "state": null }, "directors_provided": true, "executives_provided": true, "name": null, "owners_provided": true, "tax_id_provided": false, "verification": { "document": { "back": null, "details": null, "details_code": null, "front": null } } }, "country": "US", "created": 1621778298, "default_currency": "usd", "details_submitted": false, "email": null, "external_accounts": { "object": "list", "data": [ { "id": "ba_1IuVozQveW0ONQsd3dWG85e2", "object": "bank_account", "account": "acct_1IuHosQveW0ONQsd", "account_holder_name": "Jenny Rosen", "account_holder_type": "individual", "available_payout_methods": [ "standard" ], "bank_name": "STRIPE TEST BANK", "country": "US", "currency": "usd", "default_for_currency": false, "fingerprint": "OAI2ZEI2iIJVbF1o", "last4": "1116", "metadata": {}, "routing_number": "110000000", "status": "verified" }, { "id": "card_1IuVlSQveW0ONQsdkXBUUHyE", "object": "card", "account": "acct_1IuHosQveW0ONQsd", "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "available_payout_methods": [ "standard", "instant" ], "brand": "MasterCard", "country": "US", "currency": "usd", "cvc_check": null, "default_for_currency": true, "dynamic_last4": null, "exp_month": 5, "exp_year": 2022, "fingerprint": "SgIrwXiq2B8M5ab2", "funding": "debit", "last4": "8210", "metadata": {}, "name": null, "tokenization_method": null } ], "has_more": false, "total_count": 2, "url": "/v1/accounts/acct_1IuHosQveW0ONQsd/external_accounts" }, "metadata": {}, "payouts_enabled": false, "requirements": { "current_deadline": null, "currently_due": [ "business_profile.url", "individual.first_name", "individual.last_name" ], "disabled_reason": "requirements.past_due", "errors": [], "eventually_due": [ "business_profile.url", "individual.dob.day", "individual.dob.month", "individual.dob.year", "individual.first_name", "individual.last_name", "individual.ssn_last_4" ], "past_due": [ "business_profile.url", "individual.first_name", "individual.last_name" ], "pending_verification": [] }, "settings": { "bacs_debit_payments": {}, "branding": { "icon": null, "logo": null, "primary_color": null, "secondary_color": null }, "card_issuing": { "tos_acceptance": { "date": null, "ip": null } }, "card_payments": { "decline_on": { "avs_failure": false, "cvc_failure": false }, "statement_descriptor_prefix": null }, "dashboard": { "display_name": "djstripe-custom", "timezone": "Etc/UTC" }, "payments": { "statement_descriptor": null, "statement_descriptor_kana": null, "statement_descriptor_kanji": null }, "payouts": { "debit_negative_balances": false, "schedule": { "delay_days": 2, "interval": "daily" }, "statement_descriptor": null }, "sepa_debit_payments": {} }, "tos_acceptance": { "date": 1621778297, "ip": "127.0.0.1", "user_agent": null }, "type": "custom" } ================================================ FILE: tests/fixtures/account_express_acct_1IuHosQveW0ONQsd.json ================================================ { "business_profile": { "mcc": null, "name": "Express Account", "support_address": null, "support_email": null, "support_phone": null, "support_url": null, "url": "https://djstripe.com/" }, "capabilities": { "transfers": "active" }, "charges_enabled": true, "country": "US", "created": 1622034893, "default_currency": "usd", "details_submitted": true, "email": "djstripe@example.com", "external_accounts": { "data": [ { "account": "acct_1IuHosQveW0ONQsd", "account_holder_name": null, "account_holder_type": null, "available_payout_methods": [ "standard" ], "bank_name": "STRIPE TEST BANK", "country": "US", "currency": "usd", "default_for_currency": true, "fingerprint": "E4D818AgpkNPGkvl", "id": "ba_1IuVozQveW0ONQsd3dWG85e2", "last4": "6789", "metadata": {}, "object": "bank_account", "routing_number": "110000000", "status": "new" }, { "id": "card_1IuVlSQveW0ONQsdkXBUUHyE", "object": "card", "account": "acct_1IuHosQveW0ONQsd", "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "available_payout_methods": [ "standard", "instant" ], "brand": "MasterCard", "country": "US", "currency": "usd", "cvc_check": null, "default_for_currency": true, "dynamic_last4": null, "exp_month": 5, "exp_year": 2022, "fingerprint": "SgIrwXiq2B8M5ab2", "funding": "debit", "last4": "8210", "metadata": {}, "name": null, "tokenization_method": null } ], "has_more": false, "object": "list", "total_count": 1, "url": "/v1/accounts/acct_1IuHosQveW0ONQsd/external_accounts" }, "id": "acct_1IuHosQveW0ONQsd", "login_links": { "data": [], "has_more": false, "object": "list", "total_count": 0, "url": "/v1/accounts/acct_1IuHosQveW0ONQsd/login_links" }, "metadata": {}, "object": "account", "payouts_enabled": true, "requirements": { "current_deadline": null, "currently_due": [], "disabled_reason": null, "errors": [], "eventually_due": [ "individual.verification.document" ], "past_due": [], "pending_verification": [] }, "settings": { "bacs_debit_payments": {}, "branding": { "icon": null, "logo": null, "primary_color": null, "secondary_color": null }, "card_issuing": { "tos_acceptance": { "date": null, "ip": null } }, "card_payments": { "decline_on": { "avs_failure": false, "cvc_failure": false }, "statement_descriptor_prefix": null }, "dashboard": { "display_name": "Djstripe", "timezone": "Etc/UTC" }, "payments": { "statement_descriptor": "DJSTRIPE.COM", "statement_descriptor_kana": null, "statement_descriptor_kanji": null }, "payouts": { "debit_negative_balances": true, "schedule": { "delay_days": 2, "interval": "daily" }, "statement_descriptor": null }, "sepa_debit_payments": {} }, "tos_acceptance": { "date": 1622034889 }, "type": "express" } ================================================ FILE: tests/fixtures/account_standard_acct_1Fg9jUA3kq9o1aTc.json ================================================ { "id": "acct_1Fg9jUA3kq9o1aTc", "object": "account", "metadata": {}, "business_profile": { "name": "dj-stripe", "support_email": "djstripe@example.com", "support_phone": null, "support_url": "https://djstripe.com/support/", "url": "https://djstripe.com" }, "capabilities": {}, "charges_enabled": true, "country": "US", "default_currency": "usd", "details_submitted": true, "payouts_enabled": true, "settings": { "bacs_debit_payments": {}, "branding": { "icon":null, "logo":null, "primary_color": null, "secondary_color": null }, "card_issuing": { "tos_acceptance": { "date": null, "ip": null } }, "card_payments": { "statement_descriptor_prefix": null }, "dashboard": { "display_name": "djstripe-standard", "timezone": "America/Los_Angeles" }, "payments": { "statement_descriptor": "DJSTRIPE", "statement_descriptor_kana": null, "statement_descriptor_kanji": null }, "payouts": { "debit_negative_balances": true, "schedule": { "delay_days": 2, "interval": "daily" }, "statement_descriptor": null }, "sepa_debit_payments": {} }, "type": "standard", "email": "djstripe@example.com" } ================================================ FILE: tests/fixtures/balance_transaction_txn_fake_ch_fakefakefakefakefake0001.json ================================================ { "id": "txn_fake_ch_fakefakefakefakefake0001", "object": "balance_transaction", "amount": 2000, "available_on": 1558569600, "created": 1557995177, "currency": "usd", "description": "Subscription creation", "exchange_rate": null, "fee": 88, "fee_details": [ { "amount": 88, "application": null, "currency": "usd", "description": "Stripe processing fees", "type": "stripe_fee" } ], "net": 1912, "reporting_category": "charge", "source": "ch_fakefakefakefakefake0001", "status": "pending", "type": "charge" } ================================================ FILE: tests/fixtures/bank_account_ba_fakefakefakefakefake0003.json ================================================ { "id": "ba_fakefakefakefakefake0003", "object": "bank_account", "account_holder_name": "Jane Austen", "account_holder_type": "individual", "bank_name": "STRIPE TEST BANK", "country": "US", "currency": "usd", "customer": "cus_example_with_bank_account", "fingerprint": "PzC4f6ki9HTDR1cc", "last4": "6789", "metadata": { "djstripe_test_fake_id": "ba_fakefakefakefakefake0003", "djstripe_test_fixture_account_number": "000123456789" }, "routing_number": "110000000", "status": "new" } ================================================ FILE: tests/fixtures/bank_account_ba_fakefakefakefakefake0004.json ================================================ { "id": "ba_1IuVozQveW0ONQsd3dWG85e2", "object": "bank_account", "account": "acct_1IuHosQveW0ONQsd", "account_holder_name": "Jenny Rosen", "account_holder_type": "individual", "available_payout_methods": [ "standard" ], "bank_name": "STRIPE TEST BANK", "country": "US", "currency": "usd", "default_for_currency": false, "fingerprint": "OAI2ZEI2iIJVbF1o", "last4": "1116", "metadata": {}, "routing_number": "110000000", "status": "verified" } ================================================ FILE: tests/fixtures/card_card_fakefakefakefakefake0001.json ================================================ { "id": "card_fakefakefakefakefake0001", "object": "card", "account": null, "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "brand": "Visa", "country": "US", "customer": "cus_6lsBvm5rJ0zyHc", "cvc_check": null, "dynamic_last4": null, "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "last4": "4242", "metadata": { "djstripe_test_fake_id": "card_fakefakefakefakefake0001" }, "name": "alex-nesnes@hotmail.fr", "tokenization_method": null } ================================================ FILE: tests/fixtures/card_card_fakefakefakefakefake0002.json ================================================ { "id": "card_fakefakefakefakefake0002", "object": "card", "account": null, "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "brand": "Visa", "country": "US", "customer": "cus_4UbFSo9tl62jqj", "cvc_check": null, "dynamic_last4": null, "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "last4": "4242", "metadata": { "djstripe_test_fake_id": "card_fakefakefakefakefake0002" }, "name": null, "tokenization_method": null } ================================================ FILE: tests/fixtures/card_card_fakefakefakefakefake0003.json ================================================ { "id": "card_fakefakefakefakefake0003", "object": "card", "account": null, "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "brand": "Visa", "country": "US", "customer": "cus_6lsBvm5rJ0zyHc", "cvc_check": null, "dynamic_last4": null, "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "last4": "4242", "metadata": { "djstripe_test_fake_id": "card_fakefakefakefakefake0003" }, "name": null, "tokenization_method": null } ================================================ FILE: tests/fixtures/card_card_fakefakefakefakefake0004.json ================================================ { "id": "card_1IuVlSQveW0ONQsdkXBUUHyE", "object": "card", "account": "acct_1IuHosQveW0ONQsd", "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "available_payout_methods": [ "standard", "instant" ], "brand": "MasterCard", "country": "US", "currency": "usd", "cvc_check": null, "default_for_currency": true, "dynamic_last4": null, "exp_month": 5, "exp_year": 2022, "fingerprint": "SgIrwXiq2B8M5ab2", "funding": "debit", "last4": "8210", "metadata": {}, "name": null, "tokenization_method": null } ================================================ FILE: tests/fixtures/charge_ch_fakefakefakefakefake0001.json ================================================ { "id": "ch_fakefakefakefakefake0001", "object": "charge", "amount": 2000, "amount_captured": 0, "amount_refunded": 0, "application": null, "application_fee": null, "application_fee_amount": null, "balance_transaction": "txn_fake_ch_fakefakefakefakefake0001", "billing_details": { "address": { "city": null, "country": null, "line1": null, "line2": null, "postal_code": null, "state": null }, "email": null, "name": "alex-nesnes@hotmail.fr", "phone": null }, "calculated_statement_descriptor": "Stripe", "captured": true, "created": 1557995177, "currency": "usd", "customer": "cus_6lsBvm5rJ0zyHc", "description": "Subscription creation", "destination": null, "dispute": null, "disputed": false, "failure_code": null, "failure_message": null, "fraud_details": {}, "invoice": "in_fakefakefakefakefake0001", "livemode": false, "metadata": { "djstripe_test_fake_id": "ch_fakefakefakefakefake0001" }, "on_behalf_of": null, "order": null, "outcome": { "network_status": "approved_by_network", "reason": null, "risk_level": "normal", "risk_score": 2, "seller_message": "Payment complete.", "type": "authorized" }, "paid": true, "payment_intent": "pi_fakefakefakefakefake0001", "payment_method": "card_fakefakefakefakefake0001", "payment_method_details": { "card": { "brand": "visa", "checks": { "address_line1_check": null, "address_postal_code_check": null, "cvc_check": null }, "country": "US", "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "installments": null, "last4": "4242", "network": "visa", "three_d_secure": null, "wallet": null }, "type": "card" }, "receipt_email": null, "receipt_number": null, "receipt_url": "https://pay.stripe.com/receipts/acct_1EaetYCOCguPTL2B/ch_fakefakefakefakefake0001/rcpt_F4oXnrfNz0ijmlgXiDK31ArpYEgLEko", "refunded": false, "refunds": { "object": "list", "data": [], "has_more": false, "url": "/v1/charges/ch_fakefakefakefakefake0001/refunds" }, "review": null, "shipping": null, "source": { "id": "card_fakefakefakefakefake0001", "object": "card", "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "brand": "Visa", "country": "US", "customer": "cus_6lsBvm5rJ0zyHc", "cvc_check": null, "dynamic_last4": null, "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "last4": "4242", "metadata": { "djstripe_test_fake_id": "card_fakefakefakefakefake0001" }, "name": "alex-nesnes@hotmail.fr", "tokenization_method": null }, "source_transfer": null, "statement_descriptor": null, "statement_descriptor_suffix": null, "status": "succeeded", "transfer_data": null, "transfer_group": null } ================================================ FILE: tests/fixtures/customer_cus_4QWKsZuuTHcs7X.json ================================================ { "id": "cus_4QWKsZuuTHcs7X", "object": "customer", "address": null, "balance": 0, "created": 1557995167, "currency": null, "default_source": { "id": "src_fakefakefakefakefake0001", "object": "source", "amount": null, "card": { "exp_month": 6, "exp_year": 2021, "last4": "4242", "country": "US", "brand": "Visa", "funding": "credit", "fingerprint": "88PuXw9tEmvYe69o", "three_d_secure": "optional", "name": null, "address_line1_check": null, "address_zip_check": null, "cvc_check": null, "tokenization_method": null, "dynamic_last4": null }, "client_secret": "src_client_secret_F5psHouOOldEtvBHgyz6y3FC", "created": 1558230761, "currency": null, "customer": "cus_4QWKsZuuTHcs7X", "flow": "none", "livemode": false, "metadata": { "djstripe_test_fake_id": "src_fakefakefakefakefake0001" }, "owner": { "address": null, "email": null, "name": null, "phone": null, "verified_address": null, "verified_email": null, "verified_name": null, "verified_phone": null }, "statement_descriptor": null, "status": "chargeable", "type": "card", "usage": "reusable" }, "delinquent": false, "description": "John Doe", "discount": null, "email": "john.doe@example.com", "invoice_prefix": "90175264", "invoice_settings": { "custom_fields": null, "default_payment_method": null, "footer": null }, "livemode": false, "metadata": {}, "name": null, "next_invoice_sequence": 1, "phone": null, "preferred_locales": [], "shipping": null, "sources": { "object": "list", "data": [ { "id": "src_fakefakefakefakefake0001", "object": "source", "amount": null, "card": { "exp_month": 6, "exp_year": 2021, "last4": "4242", "country": "US", "brand": "Visa", "funding": "credit", "fingerprint": "88PuXw9tEmvYe69o", "three_d_secure": "optional", "name": null, "address_line1_check": null, "address_zip_check": null, "cvc_check": null, "tokenization_method": null, "dynamic_last4": null }, "client_secret": "src_client_secret_F5psHouOOldEtvBHgyz6y3FC", "created": 1558230761, "currency": null, "customer": "cus_4QWKsZuuTHcs7X", "flow": "none", "livemode": false, "metadata": { "djstripe_test_fake_id": "src_fakefakefakefakefake0001" }, "owner": { "address": null, "email": null, "name": null, "phone": null, "verified_address": null, "verified_email": null, "verified_name": null, "verified_phone": null }, "statement_descriptor": null, "status": "chargeable", "type": "card", "usage": "reusable" } ], "has_more": false, "total_count": 1, "url": "/v1/customers/cus_4QWKsZuuTHcs7X/sources" }, "subscriptions": {}, "tax_exempt": "none", "tax_ids": {} } ================================================ FILE: tests/fixtures/customer_cus_4UbFSo9tl62jqj.json ================================================ { "id": "cus_4UbFSo9tl62jqj", "object": "customer", "address": null, "balance": 0, "created": 1557995167, "currency": "usd", "default_source": { "id": "card_fakefakefakefakefake0002", "object": "card", "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "brand": "Visa", "country": "US", "customer": "cus_4UbFSo9tl62jqj", "cvc_check": null, "dynamic_last4": null, "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "last4": "4242", "metadata": { "djstripe_test_fake_id": "card_fakefakefakefakefake0002" }, "name": null, "tokenization_method": null }, "delinquent": false, "description": "John Snow", "discount": null, "email": "john.snow@thewall.com", "invoice_prefix": "D7B0B3B6", "invoice_settings": { "custom_fields": null, "default_payment_method": null, "footer": null }, "livemode": false, "metadata": {}, "name": null, "next_invoice_sequence": 3, "phone": null, "preferred_locales": [], "shipping": null, "sources": { "object": "list", "data": [ { "id": "card_fakefakefakefakefake0002", "object": "card", "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "brand": "Visa", "country": "US", "customer": "cus_4UbFSo9tl62jqj", "cvc_check": null, "dynamic_last4": null, "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "last4": "4242", "metadata": { "djstripe_test_fake_id": "card_fakefakefakefakefake0002" }, "name": null, "tokenization_method": null } ], "has_more": false, "total_count": 1, "url": "/v1/customers/cus_4UbFSo9tl62jqj/sources" }, "subscriptions": { "object": "list", "data": [ { "id": "sub_fakefakefakefakefake0004", "object": "subscription", "application_fee_percent": null, "billing_cycle_anchor": 1558230771, "billing_thresholds": null, "cancel_at": null, "cancel_at_period_end": false, "canceled_at": null, "collection_method": "charge_automatically", "created": 1558230771, "current_period_end": 1560909171, "current_period_start": 1558230771, "customer": "cus_4UbFSo9tl62jqj", "days_until_due": null, "default_payment_method": null, "default_source": null, "default_tax_rates": [], "discount": null, "ended_at": null, "items": { "object": "list", "data": [ { "id": "si_F5uk9HMrUwrmUJ", "object": "subscription_item", "billing_thresholds": null, "created": 1558230772, "metadata": {}, "plan": { "id": "gold21323", "object": "plan", "active": true, "aggregate_usage": null, "amount": 2000, "amount_decimal": "2000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed" }, "price": { "id": "gold21323", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1593225979, "currency": "usd", "livemode": false, "lookup_key": null, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "recurring": { "aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": null, "usage_type": "licensed" }, "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 2000, "unit_amount_decimal": "2000" }, "quantity": 1, "subscription": "sub_fakefakefakefakefake0004", "tax_rates": [] }, { "id": "si_F5uk81B1xGi3Vr", "object": "subscription_item", "billing_thresholds": null, "created": 1558230772, "metadata": {}, "plan": { "id": "silver41294", "object": "plan", "active": true, "aggregate_usage": null, "amount": 4000, "amount_decimal": "4000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": 12, "usage_type": "licensed" }, "price": { "id": "silver41294", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1593225979, "currency": "usd", "livemode": false, "lookup_key": null, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "recurring": { "aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": 12, "usage_type": "licensed" }, "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 4000, "unit_amount_decimal": "4000" }, "quantity": 1, "subscription": "sub_fakefakefakefakefake0004", "tax_rates": [] } ], "has_more": false, "total_count": 2, "url": "/v1/subscription_items?subscription=sub_fakefakefakefakefake0004" }, "latest_invoice": "in_1GyU3kCOCguPTL2BO0EVwuzj", "livemode": false, "metadata": { "djstripe_test_fake_id": "sub_fakefakefakefakefake0004" }, "next_pending_invoice_item_invoice": null, "pause_collection": null, "pending_invoice_item_interval": null, "pending_setup_intent": null, "pending_update": null, "plan": null, "quantity": null, "schedule": null, "start_date": 1559476706, "status": "active", "tax_percent": null, "transfer_data": null, "trial_end": null, "trial_start": null }, { "id": "sub_fakefakefakefakefake0003", "object": "subscription", "application_fee_percent": null, "billing_cycle_anchor": 1558230769, "billing_thresholds": null, "cancel_at": null, "cancel_at_period_end": false, "canceled_at": null, "collection_method": "charge_automatically", "created": 1558230769, "current_period_end": 1560909169, "current_period_start": 1558230769, "customer": "cus_4UbFSo9tl62jqj", "days_until_due": null, "default_payment_method": null, "default_source": null, "default_tax_rates": [], "discount": null, "ended_at": null, "items": { "object": "list", "data": [ { "id": "si_F5ukGdpR4EejF9", "object": "subscription_item", "billing_thresholds": null, "created": 1558230770, "metadata": {}, "plan": { "id": "gold21323", "object": "plan", "active": true, "aggregate_usage": null, "amount": 2000, "amount_decimal": "2000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed" }, "price": { "id": "gold21323", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1593225979, "currency": "usd", "livemode": false, "lookup_key": null, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "recurring": { "aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": null, "usage_type": "licensed" }, "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 2000, "unit_amount_decimal": "2000" }, "quantity": 1, "subscription": "sub_fakefakefakefakefake0003", "tax_rates": [] } ], "has_more": false, "total_count": 1, "url": "/v1/subscription_items?subscription=sub_fakefakefakefakefake0003" }, "latest_invoice": "in_1GyU3iCOCguPTL2BDgqo4dj7", "livemode": false, "metadata": { "djstripe_test_fake_id": "sub_fakefakefakefakefake0003" }, "next_pending_invoice_item_invoice": null, "pause_collection": null, "pending_invoice_item_interval": null, "pending_setup_intent": null, "pending_update": null, "plan": { "id": "gold21323", "object": "plan", "active": true, "aggregate_usage": null, "amount": 2000, "amount_decimal": "2000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed" }, "quantity": 1, "schedule": null, "start_date": 1559476704, "status": "active", "tax_percent": null, "transfer_data": null, "trial_end": null, "trial_start": null } ], "has_more": false, "total_count": 2, "url": "/v1/customers/cus_4UbFSo9tl62jqj/subscriptions" }, "tax_exempt": "none", "tax_ids": {} } ================================================ FILE: tests/fixtures/customer_cus_6lsBvm5rJ0zyHc.json ================================================ { "id": "cus_6lsBvm5rJ0zyHc", "object": "customer", "address": null, "balance": 0, "created": 1557995166, "currency": "usd", "default_source": { "id": "card_fakefakefakefakefake0001", "object": "card", "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "brand": "Visa", "country": "US", "customer": "cus_6lsBvm5rJ0zyHc", "cvc_check": null, "dynamic_last4": null, "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "last4": "4242", "metadata": { "djstripe_test_fake_id": "card_fakefakefakefakefake0001" }, "name": "alex-nesnes@hotmail.fr", "tokenization_method": null }, "delinquent": false, "description": "Michael Smith", "discount": null, "email": "michael.smith@example.com", "invoice_prefix": "E5B23224", "invoice_settings": { "custom_fields": null, "default_payment_method": null, "footer": null }, "livemode": false, "metadata": {}, "name": null, "next_invoice_sequence": 3, "phone": null, "preferred_locales": [], "shipping": null, "sources": { "object": "list", "data": [ { "id": "card_fakefakefakefakefake0001", "object": "card", "account": null, "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "brand": "Visa", "country": "US", "customer": "cus_6lsBvm5rJ0zyHc", "cvc_check": null, "dynamic_last4": null, "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "last4": "4242", "metadata": { "djstripe_test_fake_id": "card_fakefakefakefakefake0001" }, "name": "alex-nesnes@hotmail.fr", "tokenization_method": null }, { "id": "card_fakefakefakefakefake0003", "object": "card", "account": null, "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "brand": "Visa", "country": "US", "customer": "cus_6lsBvm5rJ0zyHc", "cvc_check": null, "dynamic_last4": null, "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "last4": "4242", "metadata": { "djstripe_test_fake_id": "card_fakefakefakefakefake0003" }, "name": null, "tokenization_method": null } ], "has_more": false, "total_count": 2, "url": "/v1/customers/cus_6lsBvm5rJ0zyHc/sources" }, "subscriptions": { "object": "list", "data": [ { "id": "sub_fakefakefakefakefake0002", "object": "subscription", "application_fee_percent": null, "billing_cycle_anchor": 1558230766, "billing_thresholds": null, "cancel_at": null, "cancel_at_period_end": false, "canceled_at": null, "collection_method": "charge_automatically", "created": 1558230766, "current_period_end": 1560909166, "current_period_start": 1558230766, "customer": "cus_6lsBvm5rJ0zyHc", "days_until_due": null, "default_payment_method": null, "default_source": null, "default_tax_rates": [], "discount": null, "ended_at": null, "items": { "object": "list", "data": [ { "id": "si_F5ukq6eM2QV9g5", "object": "subscription_item", "billing_thresholds": null, "created": 1558230767, "metadata": {}, "plan": { "id": "silver41294", "object": "plan", "active": true, "aggregate_usage": null, "amount": 4000, "amount_decimal": "4000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": 12, "usage_type": "licensed" }, "price": { "id": "silver41294", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1593225979, "currency": "usd", "livemode": false, "lookup_key": null, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "recurring": { "aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": 12, "usage_type": "licensed" }, "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 4000, "unit_amount_decimal": "4000" }, "quantity": 1, "subscription": "sub_fakefakefakefakefake0002", "tax_rates": [ { "id": "txr_fakefakefakefakefake0001", "object": "tax_rate", "active": true, "created": 1593225980, "description": null, "display_name": "VAT", "inclusive": true, "jurisdiction": "Example1", "livemode": false, "metadata": { "djstripe_test_fake_id": "txr_fakefakefakefakefake0001" }, "percentage": 15.0 } ] } ], "has_more": false, "total_count": 1, "url": "/v1/subscription_items?subscription=sub_fakefakefakefakefake0002" }, "latest_invoice": "in_fakefakefakefakefake0004", "livemode": false, "metadata": { "djstripe_test_fake_id": "sub_fakefakefakefakefake0002" }, "next_pending_invoice_item_invoice": null, "pause_collection": null, "pending_invoice_item_interval": null, "pending_setup_intent": null, "pending_update": null, "plan": { "id": "silver41294", "object": "plan", "active": true, "aggregate_usage": null, "amount": 4000, "amount_decimal": "4000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": 12, "usage_type": "licensed" }, "quantity": 1, "schedule": null, "start_date": 1559476702, "status": "active", "tax_percent": null, "transfer_data": null, "trial_end": null, "trial_start": null }, { "id": "sub_fakefakefakefakefake0001", "object": "subscription", "application_fee_percent": null, "billing_cycle_anchor": 1558230764, "billing_thresholds": null, "cancel_at": null, "cancel_at_period_end": false, "canceled_at": null, "collection_method": "charge_automatically", "created": 1558230764, "current_period_end": 1560909164, "current_period_start": 1558230764, "customer": "cus_6lsBvm5rJ0zyHc", "days_until_due": null, "default_payment_method": null, "default_source": null, "default_tax_rates": [ { "id": "txr_fakefakefakefakefake0001", "object": "tax_rate", "active": true, "created": 1593225980, "description": null, "display_name": "VAT", "inclusive": true, "jurisdiction": "Example1", "livemode": false, "metadata": { "djstripe_test_fake_id": "txr_fakefakefakefakefake0001" }, "percentage": 15.0 } ], "discount": null, "ended_at": null, "items": { "object": "list", "data": [ { "id": "si_F5ukmkS6Bxi90Y", "object": "subscription_item", "billing_thresholds": null, "created": 1558230764, "metadata": {}, "plan": { "id": "gold21323", "object": "plan", "active": true, "aggregate_usage": null, "amount": 2000, "amount_decimal": "2000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed" }, "price": { "id": "gold21323", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1593225979, "currency": "usd", "livemode": false, "lookup_key": null, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "recurring": { "aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": null, "usage_type": "licensed" }, "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 2000, "unit_amount_decimal": "2000" }, "quantity": 1, "subscription": "sub_fakefakefakefakefake0001", "tax_rates": [] } ], "has_more": false, "total_count": 1, "url": "/v1/subscription_items?subscription=sub_fakefakefakefakefake0001" }, "latest_invoice": "in_fakefakefakefakefake0001", "livemode": false, "metadata": { "djstripe_test_fake_id": "sub_fakefakefakefakefake0001" }, "next_pending_invoice_item_invoice": null, "pause_collection": null, "pending_invoice_item_interval": null, "pending_setup_intent": null, "pending_update": null, "plan": { "id": "gold21323", "object": "plan", "active": true, "aggregate_usage": null, "amount": 2000, "amount_decimal": "2000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed" }, "quantity": 1, "schedule": null, "start_date": 1559476700, "status": "active", "tax_percent": null, "transfer_data": null, "trial_end": null, "trial_start": null } ], "has_more": false, "total_count": 2, "url": "/v1/customers/cus_6lsBvm5rJ0zyHc/subscriptions" }, "tax_exempt": "none", "tax_ids": {} } ================================================ FILE: tests/fixtures/customer_cus_example_with_bank_account.json ================================================ { "id": "cus_example_with_bank_account", "object": "customer", "address": null, "balance": 0, "created": 1566463428, "currency": null, "default_source": { "id": "ba_fakefakefakefakefake0003", "object": "bank_account", "account_holder_name": "Jane Austen", "account_holder_type": "individual", "bank_name": "STRIPE TEST BANK", "country": "US", "currency": "usd", "customer": "cus_example_with_bank_account", "fingerprint": "PzC4f6ki9HTDR1cc", "last4": "6789", "metadata": { "djstripe_test_fake_id": "ba_fakefakefakefakefake0003", "djstripe_test_fixture_account_number": "000123456789" }, "routing_number": "110000000", "status": "new" }, "delinquent": false, "description": null, "discount": null, "email": "jane.austen@example.com", "invoice_prefix": "905A7617", "invoice_settings": { "custom_fields": null, "default_payment_method": null, "footer": null }, "livemode": false, "metadata": {}, "name": "Jane Austen", "next_invoice_sequence": 1, "phone": null, "preferred_locales": [], "shipping": null, "sources": { "object": "list", "data": [ { "id": "ba_fakefakefakefakefake0003", "object": "bank_account", "account_holder_name": "Jane Austen", "account_holder_type": "individual", "bank_name": "STRIPE TEST BANK", "country": "US", "currency": "usd", "customer": "cus_example_with_bank_account", "fingerprint": "PzC4f6ki9HTDR1cc", "last4": "6789", "metadata": { "djstripe_test_fake_id": "ba_fakefakefakefakefake0003", "djstripe_test_fixture_account_number": "000123456789" }, "routing_number": "110000000", "status": "new" } ], "has_more": false, "total_count": 1, "url": "/v1/customers/cus_example_with_bank_account/sources" }, "subscriptions": {}, "tax_exempt": "none", "tax_ids": {} } ================================================ FILE: tests/fixtures/dispute_ch_fakefakefakefake01.json ================================================ { "id": "ch_1JHRCWJSZQVUcJYgOCavbxio", "object": "charge", "livemode": false, "created": 1557995177, "metadata": {}, "description": "", "amount": "10.00", "amount_captured": "10.00", "amount_refunded": "0.00", "application": "", "application_fee": null, "application_fee_amount": null, "balance_transaction": "txn_1JHRCWJSZQVUcJYgYwW8XpF4", "billing_details": { "address": { "city": null, "country": null, "line1": null, "line2": null, "postal_code": null, "state": null }, "email": null, "name": null, "phone": null }, "calculated_statement_descriptor": "CONSULTING", "captured": true, "currency": "usd", "customer": "cus_6lsBvm5rJ0zyHc", "dispute": "dp_1JAyTwJSZQVUcJYgPqasUEn1", "disputed": true, "failure_code": "", "failure_message": "", "fraud_details": {}, "invoice": null, "on_behalf_of": null, "outcome": { "network_status": "approved_by_network", "reason": null, "risk_level": "normal", "risk_score": 8, "seller_message": "Payment complete.", "type": "authorized" }, "paid": true, "payment_intent": "pi_1JAyTwJSZQVUcJYgBbsz0NuH", "payment_method": "card_fakefakefakefakefake0001", "payment_method_details": { "card": { "brand": "visa", "checks": { "address_line1_check": null, "address_postal_code_check": null, "cvc_check": "pass" }, "country": "US", "exp_month": 2, "exp_year": 2022, "fingerprint": "Js2Tq8SMwjNsGwEr", "funding": "credit", "installments": null, "last4": "0259", "network": "visa", "three_d_secure": null, "wallet": null }, "type": "card" }, "receipt_email": "", "receipt_number": "", "receipt_url": "https://pay.stripe.com/receipts/acct_1ItQ7cJSZQVUcJYg/ch_1JHRCWJSZQVUcJYgOCavbxio/rcpt_JvHmTnLVQmraFXrLuQq5RT9oF14tFOL", "refunded": false, "shipping": "", "source_transfer": null, "statement_descriptor": "consulting", "statement_descriptor_suffix": "", "status": "succeeded", "transfer": null, "transfer_data": "", "transfer_group": "" } ================================================ FILE: tests/fixtures/dispute_dp_fakefakefakefake01.json ================================================ { "id": "dp_1JAyTwJSZQVUcJYgPqasUEn1", "object": "dispute", "amount": 1000, "balance_transaction": "txn_16g5h62eZvKYlo2CQ2AHA89s", "balance_transactions": [ { "id": "txn_16g5h62eZvKYlo2CQ2AHA89s", "object": "balance_transaction", "amount": -1000, "available_on": 1626307200, "created": 1625755540, "currency": "usd", "description": "Chargeback withdrawal for ch_fakefakefakefakefake0001", "exchange_rate": null, "fee": 1500, "fee_details": [ { "amount": 1500, "application": null, "currency": "usd", "description": "Dispute fee", "type": "stripe_fee" } ], "net": -2500, "reporting_category": "dispute", "source": "dp_1JAyTwJSZQVUcJYgPqasUEn1", "status": "pending", "type": "adjustment" } ], "charge": "ch_fakefakefakefakefake0001", "created": 1625755540, "currency": "usd", "evidence": { "access_activity_log": null, "billing_address": null, "cancellation_policy": null, "cancellation_policy_disclosure": null, "cancellation_rebuttal": null, "customer_communication": null, "customer_email_address": "test1@user.com", "customer_name": "Jane Doe Test", "customer_purchase_ip": "223.182.253.19", "customer_signature": null, "duplicate_charge_documentation": null, "duplicate_charge_explanation": null, "duplicate_charge_id": null, "product_description": null, "receipt": null, "refund_policy": null, "refund_policy_disclosure": null, "refund_refusal_explanation": null, "service_date": null, "service_documentation": null, "shipping_address": null, "shipping_carrier": null, "shipping_date": null, "shipping_documentation": null, "shipping_tracking_number": null, "uncategorized_file": null, "uncategorized_text": null }, "evidence_details": { "due_by": 1626566399, "has_evidence": false, "past_due": false, "submission_count": 0 }, "is_charge_refundable": false, "livemode": false, "metadata": {}, "payment_intent": "pi_1JAyTwJSZQVUcJYgBbsz0NuH", "reason": "fraudulent", "status": "needs_response" } ================================================ FILE: tests/fixtures/dispute_dp_fakefakefakefake02.json ================================================ { "id": "dp_1JAyTwJSZQVUcJYgPqasUEn1", "object": "dispute", "amount": 1000, "balance_transaction": "txn_1JHRCWJSZQVUcJYgYwW8XpF4", "balance_transactions": [ { "id": "txn_1JHRCWJSZQVUcJYgYwW8XpF4", "object": "balance_transaction", "amount": -1000, "available_on": 1626307200, "created": 1625755540, "currency": "usd", "description": "Chargeback withdrawal for ch_1JHRCWJSZQVUcJYgOCavbxio", "exchange_rate": null, "fee": 1500, "fee_details": [ { "amount": 1500, "application": null, "currency": "usd", "description": "Dispute fee", "type": "stripe_fee" } ], "net": -2500, "reporting_category": "dispute", "source": "dp_1JAyTwJSZQVUcJYgPqasUEn1", "status": "pending", "type": "adjustment" } ], "charge": "ch_1JHRCWJSZQVUcJYgOCavbxio", "created": 1625755540, "currency": "usd", "evidence": { "access_activity_log": null, "billing_address": null, "cancellation_policy": null, "cancellation_policy_disclosure": null, "cancellation_rebuttal": null, "customer_communication": null, "customer_email_address": "test1@user.com", "customer_name": "Jane Doe Test", "customer_purchase_ip": "223.182.253.19", "customer_signature": null, "duplicate_charge_documentation": null, "duplicate_charge_explanation": null, "duplicate_charge_id": null, "product_description": null, "receipt": null, "refund_policy": null, "refund_policy_disclosure": null, "refund_refusal_explanation": null, "service_date": null, "service_documentation": null, "shipping_address": null, "shipping_carrier": null, "shipping_date": null, "shipping_documentation": null, "shipping_tracking_number": null, "uncategorized_file": null, "uncategorized_text": null }, "evidence_details": { "due_by": 1626566399, "has_evidence": false, "past_due": false, "submission_count": 0 }, "is_charge_refundable": false, "livemode": false, "metadata": {}, "payment_intent": "pi_1JAyTwJSZQVUcJYgBbsz0NuH", "reason": "fraudulent", "status": "needs_response" } ================================================ FILE: tests/fixtures/dispute_dp_funds_reinstated_full.json ================================================ { "id": "dp_1JAyTwJSZQVUcJYgPqasUEn1", "object": "dispute", "amount": 1000, "balance_transaction": "txn_16g5h62eZvKYlo2CQ2AHA89s", "balance_transactions": [ { "id": "txn_16g5h62eZvKYlo2CQ2AHA89s", "object": "balance_transaction", "amount": -1000, "available_on": 1626307200, "created": 1625755540, "currency": "usd", "description": "Chargeback withdrawal for ch_fakefakefakefakefake0001", "exchange_rate": null, "fee": 1500, "fee_details": [ { "amount": 1500, "application": null, "currency": "usd", "description": "Dispute fee", "type": "stripe_fee" } ], "net": -2500, "reporting_category": "dispute", "source": "dp_1JAyTwJSZQVUcJYgPqasUEn1", "status": "pending", "type": "adjustment" }, { "id": "txn_1JAyV2JSZQVUcJYgETKXUN9k", "object": "balance_transaction", "amount": 1000, "available_on": 1626307200, "created": 1625755608, "currency": "usd", "description": "Chargeback reversal for ch_fakefakefakefakefake0001", "exchange_rate": null, "fee": -1500, "fee_details": [ { "amount": -1500, "application": null, "currency": "usd", "description": "Dispute fee refund", "type": "stripe_fee" } ], "net": 2500, "reporting_category": "dispute_reversal", "source": "dp_1JAyTwJSZQVUcJYgPqasUEn1", "status": "pending", "type": "adjustment" } ], "charge": "ch_fakefakefakefakefake0001", "created": 1625755540, "currency": "usd", "evidence": { "access_activity_log": null, "billing_address": null, "cancellation_policy": null, "cancellation_policy_disclosure": null, "cancellation_rebuttal": null, "customer_communication": null, "customer_email_address": "test1@user.com", "customer_name": "Jane Doe Test", "customer_purchase_ip": "223.182.253.19", "customer_signature": null, "duplicate_charge_documentation": null, "duplicate_charge_explanation": null, "duplicate_charge_id": null, "product_description": null, "receipt": null, "refund_policy": null, "refund_policy_disclosure": null, "refund_refusal_explanation": null, "service_date": null, "service_documentation": null, "shipping_address": null, "shipping_carrier": null, "shipping_date": null, "shipping_documentation": null, "shipping_tracking_number": null, "uncategorized_file": null, "uncategorized_text": "winning_evidence" }, "evidence_details": { "due_by": 1626566399, "has_evidence": true, "past_due": false, "submission_count": 1 }, "is_charge_refundable": false, "livemode": false, "metadata": {}, "payment_intent": "pi_1JAyTwJSZQVUcJYgBbsz0NuH", "reason": "fraudulent", "status": "under_review" } ================================================ FILE: tests/fixtures/dispute_pi_fakefakefakefake01.json ================================================ { "id": "pi_1JAyTwJSZQVUcJYgBbsz0NuH", "object": "payment_intent", "livemode": false, "created": 1557995177, "metadata": {}, "amount": 1000, "amount_capturable": 0, "amount_received": 1000, "canceled_at": null, "cancellation_reason": "", "capture_method": "automatic", "client_secret": "pi_1JAyTwJSZQVUcJYgBbsz0NuH_secret_IooFCSw3PeIIqRmJ7SK4yEFVr", "confirmation_method": "automatic", "currency": "usd", "customer": "cus_6lsBvm5rJ0zyHc", "description": "", "last_payment_error": "", "next_action": "", "on_behalf_of": null, "payment_method": null, "payment_method_types": [ "card" ], "receipt_email": "", "setup_future_usage": "off_session", "shipping": "", "statement_descriptor": "consulting", "status": "succeeded", "transfer_data": "", "transfer_group": "" } ================================================ FILE: tests/fixtures/dispute_pm_fakefakefakefake01.json ================================================ { "id": "card_fakefakefakefakefake0001", "object": "payment_method", "billing_details": { "address": { "city": null, "country": null, "line1": null, "line2": null, "postal_code": null, "state": null }, "email": null, "name": "alex-nesnes@hotmail.fr", "phone": null }, "card": { "brand": "visa", "checks": { "address_line1_check": null, "address_postal_code_check": null, "cvc_check": "pass" }, "country": "US", "exp_month": 2, "exp_year": 2022, "fingerprint": "Js2Tq8SMwjNsGwEr", "funding": "credit", "installments": null, "last4": "0259", "network": "visa", "three_d_secure": null, "wallet": null }, "created": 1567571746, "customer": "cus_6lsBvm5rJ0zyHc", "livemode": false, "metadata": {}, "type": "card" } ================================================ FILE: tests/fixtures/dispute_txn_fakefakefakefake01.json ================================================ { "id": "txn_1JHRCWJSZQVUcJYgYwW8XpF4", "object": "balance_transaction", "livemode": null, "created": 1557995177, "metadata": "", "description": "Chargeback withdrawal for ch_1JHRCWJSZQVUcJYgOCavbxio", "amount": -1000, "available_on": 1558569600, "currency": "usd", "exchange_rate": null, "fee": 1500, "fee_details": [ { "amount": 1500, "application": null, "currency": "usd", "description": "Dispute fee", "type": "stripe_fee" } ], "net": -2500, "source": "dp_1JHRCWJSZQVUcJYgEHzUiuvb", "reporting_category": "dispute", "status": "pending", "type": "adjustment" } ================================================ FILE: tests/fixtures/event_account_application_authorized.json ================================================ { "id": "evt_1Iu8ZfA3kq9o1aTcf3b7EknK", "object": "event", "account": "acct_1Fg9jUA3kq9o1aTc", "api_version": "2020-08-27", "created": 1621742759, "data": { "object": { "id": "ca_JWVN6vOJJs3RKsff0VUwIUfmUmN0VYqe", "object": "application", "name": null } }, "livemode": false, "pending_webhooks": 2, "request": { "id": null, "idempotency_key": null }, "type": "account.application.deauthorized" } ================================================ FILE: tests/fixtures/event_account_application_deauthorized.json ================================================ { "id": "evt_1Iu8ZfA3kq9o1aTcf3b7EknK", "object": "event", "account": "acct_1Fg9jUA3kq9o1aTc", "api_version": "2020-08-27", "created": 1621742759, "data": { "object": { "id": "ca_JWVN6vOJJs3RKsff0VUwIUfmUmN0VYqe", "object": "application", "name": null } }, "livemode": false, "pending_webhooks": 2, "request": { "id": null, "idempotency_key": null }, "type": "account.application.deauthorized" } ================================================ FILE: tests/fixtures/event_account_updated_custom.json ================================================ { "id": "evt_1Itt6eB9wPxT0ovY3LLhi5bw", "object": "event", "account": "acct_1IuHosQveW0ONQsd", "api_version": "2020-08-27", "created": 1621683300, "data": { "object": { "business_profile": { "mcc": null, "name": "Express Account", "support_address": null, "support_email": null, "support_phone": null, "support_url": null, "url": "https://djstripe.com/" }, "capabilities": { "transfers": "active" }, "charges_enabled": true, "country": "US", "created": 1622034893, "default_currency": "usd", "details_submitted": true, "email": "djstripe@example.com", "external_accounts": { "data": [ { "account": "acct_1IuHosQveW0ONQsd", "account_holder_name": null, "account_holder_type": null, "available_payout_methods": [ "standard" ], "bank_name": "STRIPE TEST BANK", "country": "US", "currency": "usd", "default_for_currency": true, "fingerprint": "E4D818AgpkNPGkvl", "id": "ba_1IuVozQveW0ONQsd3dWG85e2", "last4": "6789", "metadata": {}, "object": "bank_account", "routing_number": "110000000", "status": "new" }, { "id": "card_1IuVlSQveW0ONQsdkXBUUHyE", "object": "card", "account": "acct_1IuHosQveW0ONQsd", "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "available_payout_methods": [ "standard", "instant" ], "brand": "MasterCard", "country": "US", "currency": "usd", "cvc_check": null, "default_for_currency": true, "dynamic_last4": null, "exp_month": 5, "exp_year": 2022, "fingerprint": "SgIrwXiq2B8M5ab2", "funding": "debit", "last4": "8210", "metadata": {}, "name": null, "tokenization_method": null } ], "has_more": false, "object": "list", "total_count": 1, "url": "/v1/accounts/acct_1IuHosQveW0ONQsd/external_accounts" }, "id": "acct_1IuHosQveW0ONQsd", "login_links": { "data": [], "has_more": false, "object": "list", "total_count": 0, "url": "/v1/accounts/acct_1IuHosQveW0ONQsd/login_links" }, "metadata": { "foo": "bar" }, "object": "account", "payouts_enabled": true, "requirements": { "current_deadline": null, "currently_due": [], "disabled_reason": null, "errors": [], "eventually_due": [ "individual.verification.document" ], "past_due": [], "pending_verification": [] }, "settings": { "bacs_debit_payments": {}, "branding": { "icon": null, "logo": null, "primary_color": null, "secondary_color": null }, "card_issuing": { "tos_acceptance": { "date": null, "ip": null } }, "card_payments": { "decline_on": { "avs_failure": false, "cvc_failure": false }, "statement_descriptor_prefix": null }, "dashboard": { "display_name": "Djstripe", "timezone": "Etc/UTC" }, "payments": { "statement_descriptor": "DJSTRIPE.COM", "statement_descriptor_kana": null, "statement_descriptor_kanji": null }, "payouts": { "debit_negative_balances": true, "schedule": { "delay_days": 2, "interval": "daily" }, "statement_descriptor": null }, "sepa_debit_payments": {} }, "tos_acceptance": { "date": 1622034889 }, "type": "express" }, "previous_attributes": { "metadata": {} } }, "livemode": false, "pending_webhooks": 2, "request": { "id": "req_A60l4iFvAHopqU", "idempotency_key": null }, "type": "account.updated" } ================================================ FILE: tests/fixtures/event_account_updated_express.json ================================================ { "id": "evt_1Itt6eB9wPxT0ovY3LLhi5bw", "object": "event", "account": "acct_1IuHosQveW0ONQsd", "api_version": "2020-08-27", "created": 1621683300, "data": { "object": { "business_profile": { "mcc": null, "name": "Express Account", "support_address": null, "support_email": null, "support_phone": null, "support_url": null, "url": "https://djstripe.com/" }, "capabilities": { "transfers": "active" }, "charges_enabled": true, "country": "US", "created": 1622034893, "default_currency": "usd", "details_submitted": true, "email": "djstripe@example.com", "external_accounts": { "data": [ { "account": "acct_1IuHosQveW0ONQsd", "account_holder_name": null, "account_holder_type": null, "available_payout_methods": [ "standard" ], "bank_name": "STRIPE TEST BANK", "country": "US", "currency": "usd", "default_for_currency": true, "fingerprint": "E4D818AgpkNPGkvl", "id": "ba_1IuVozQveW0ONQsd3dWG85e2", "last4": "6789", "metadata": {}, "object": "bank_account", "routing_number": "110000000", "status": "new" }, { "id": "card_1IuVlSQveW0ONQsdkXBUUHyE", "object": "card", "account": "acct_1IuHosQveW0ONQsd", "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "available_payout_methods": [ "standard", "instant" ], "brand": "MasterCard", "country": "US", "currency": "usd", "cvc_check": null, "default_for_currency": true, "dynamic_last4": null, "exp_month": 5, "exp_year": 2022, "fingerprint": "SgIrwXiq2B8M5ab2", "funding": "debit", "last4": "8210", "metadata": {}, "name": null, "tokenization_method": null } ], "has_more": false, "object": "list", "total_count": 1, "url": "/v1/accounts/acct_1IuHosQveW0ONQsd/external_accounts" }, "id": "acct_1IuHosQveW0ONQsd", "login_links": { "data": [], "has_more": false, "object": "list", "total_count": 0, "url": "/v1/accounts/acct_1IuHosQveW0ONQsd/login_links" }, "metadata": { "foo": "bar" }, "object": "account", "payouts_enabled": true, "requirements": { "current_deadline": null, "currently_due": [], "disabled_reason": null, "errors": [], "eventually_due": [ "individual.verification.document" ], "past_due": [], "pending_verification": [] }, "settings": { "bacs_debit_payments": {}, "branding": { "icon": null, "logo": null, "primary_color": null, "secondary_color": null }, "card_issuing": { "tos_acceptance": { "date": null, "ip": null } }, "card_payments": { "decline_on": { "avs_failure": false, "cvc_failure": false }, "statement_descriptor_prefix": null }, "dashboard": { "display_name": "Djstripe", "timezone": "Etc/UTC" }, "payments": { "statement_descriptor": "DJSTRIPE.COM", "statement_descriptor_kana": null, "statement_descriptor_kanji": null }, "payouts": { "debit_negative_balances": true, "schedule": { "delay_days": 2, "interval": "daily" }, "statement_descriptor": null }, "sepa_debit_payments": {} }, "tos_acceptance": { "date": 1622034889 }, "type": "express" }, "previous_attributes": { "metadata": {} } }, "livemode": false, "pending_webhooks": 2, "request": { "id": "req_A60l4iFvAHopqU", "idempotency_key": null }, "type": "account.updated" } ================================================ FILE: tests/fixtures/event_account_updated_standard.json ================================================ { "id": "evt_1Itt6eB9wPxT0ovY3LLhi5bw", "object": "event", "account": "acct_1Fg9jUA3kq9o1aTc", "api_version": "2020-08-27", "created": 1621683300, "data": { "object": { "id": "acct_1Fg9jUA3kq9o1aTc", "object": "account", "business_profile": { "mcc": null, "name": null, "support_address": null, "support_email": null, "support_phone": null, "support_url": null, "url": null, "product_description": null }, "capabilities": {}, "charges_enabled": false, "country": "US", "default_currency": "usd", "details_submitted": false, "email": null, "payouts_enabled": false, "settings": { "bacs_debit_payments": {}, "branding": { "icon": null, "logo": null, "primary_color": null, "secondary_color": null }, "card_issuing": { "tos_acceptance": { "date": null, "ip": null } }, "card_payments": { "statement_descriptor_prefix": null, "decline_on": { "avs_failure": false, "cvc_failure": true } }, "dashboard": { "display_name": null, "timezone": "Etc/UTC" }, "payments": { "statement_descriptor": null, "statement_descriptor_kana": null, "statement_descriptor_kanji": null }, "sepa_debit_payments": {}, "payouts": { "debit_negative_balances": true, "schedule": { "delay_days": 2, "interval": "daily" }, "statement_descriptor": null } }, "type": "standard", "business_type": null, "created": 1621683297, "external_accounts": { "object": "list", "data": [], "has_more": false, "total_count": 0, "url": "/v1/accounts/acct_1Fg9jUA3kq9o1aTc/external_accounts" }, "metadata": { "foo": "bar" }, "requirements": { "current_deadline": null, "currently_due": [ "business_profile.product_description", "business_profile.support_phone", "business_profile.url", "email", "external_account", "tos_acceptance.date", "tos_acceptance.ip" ], "disabled_reason": "requirements.past_due", "errors": [], "eventually_due": [ "business_profile.product_description", "business_profile.support_phone", "business_profile.url", "email", "external_account", "tos_acceptance.date", "tos_acceptance.ip" ], "past_due": [ "external_account", "tos_acceptance.date", "tos_acceptance.ip" ], "pending_verification": [] }, "tos_acceptance": { "date": null, "ip": null, "user_agent": null } }, "previous_attributes": { "metadata": {} } }, "livemode": false, "pending_webhooks": 2, "request": { "id": "req_A60l4iFvAHopqU", "idempotency_key": null }, "type": "account.updated" } ================================================ FILE: tests/fixtures/event_external_account_bank_account_created.json ================================================ { "id": "evt_1IuKmFQveW0ONQsdEAB1O64Y", "object": "event", "account": "acct_1IuHosQveW0ONQsd", "api_version": "2020-08-27", "created": 1621789667, "data": { "object": { "id": "ba_1IuVozQveW0ONQsd3dWG85e2", "object": "bank_account", "account": "acct_1IuHosQveW0ONQsd", "account_holder_name": "Jenny Rosen", "account_holder_type": "individual", "available_payout_methods": [ "standard" ], "bank_name": "STRIPE TEST BANK", "country": "US", "currency": "usd", "default_for_currency": false, "fingerprint": "OAI2ZEI2iIJVbF1o", "last4": "1116", "metadata": {}, "routing_number": "110000000", "status": "verified" } }, "livemode": false, "pending_webhooks": 2, "request": { "id": "req_mIvRpJ6b3Y2pOW", "idempotency_key": null }, "type": "account.external_account.created" } ================================================ FILE: tests/fixtures/event_external_account_bank_account_deleted.json ================================================ { "id": "evt_1IuKmFQveW0ONQsdEAB1O64Y", "object": "event", "account": "acct_1IuHosQveW0ONQsd", "api_version": "2020-08-27", "created": 1621789667, "data": { "object": { "id": "ba_1IuVozQveW0ONQsd3dWG85e2", "object": "bank_account", "account": "acct_1IuHosQveW0ONQsd", "account_holder_name": "Jenny Rosen", "account_holder_type": "individual", "available_payout_methods": [ "standard" ], "bank_name": "STRIPE TEST BANK", "country": "US", "currency": "usd", "default_for_currency": false, "fingerprint": "OAI2ZEI2iIJVbF1o", "last4": "1116", "metadata": {}, "routing_number": "110000000", "status": "verified" } }, "livemode": false, "pending_webhooks": 2, "request": { "id": "req_mIvRpJ6b3Y2pOW", "idempotency_key": null }, "type": "account.external_account.deleted" } ================================================ FILE: tests/fixtures/event_external_account_bank_account_updated.json ================================================ { "id": "evt_1IuKmFQveW0ONQsdEAB1O64Y", "object": "event", "account": "acct_1IuHosQveW0ONQsd", "api_version": "2020-08-27", "created": 1621789667, "data": { "object": { "id": "ba_1IuVozQveW0ONQsd3dWG85e2", "object": "bank_account", "account": "acct_1IuHosQveW0ONQsd", "account_holder_name": "Jenny Rosen-Updated", "account_holder_type": "individual", "available_payout_methods": [ "standard" ], "bank_name": "STRIPE TEST BANK", "country": "US", "currency": "usd", "default_for_currency": false, "fingerprint": "OAI2ZEI2iIJVbF1o", "last4": "1116", "metadata": {}, "routing_number": "110000000", "status": "verified" } }, "livemode": false, "pending_webhooks": 2, "request": { "id": "req_mIvRpJ6b3Y2pOW", "idempotency_key": null }, "type": "account.external_account.updated" } ================================================ FILE: tests/fixtures/event_external_account_card_created.json ================================================ { "id": "evt_1IuIg0QveW0ONQsdDLp7otQC", "object": "event", "account": "acct_1IuHosQveW0ONQsd", "api_version": "2020-08-27", "created": 1621781592, "data": { "object": { "id": "card_1IuVlSQveW0ONQsdkXBUUHyE", "object": "card", "account": "acct_1IuHosQveW0ONQsd", "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "available_payout_methods": [ "standard", "instant" ], "brand": "MasterCard", "country": "US", "currency": "usd", "cvc_check": null, "default_for_currency": true, "dynamic_last4": null, "exp_month": 5, "exp_year": 2022, "fingerprint": "SgIrwXiq2B8M5ab2", "funding": "debit", "last4": "8210", "metadata": {}, "name": null, "tokenization_method": null } }, "livemode": false, "pending_webhooks": 2, "request": { "id": "req_IQUywOh6PF8AyM", "idempotency_key": null }, "type": "account.external_account.created" } ================================================ FILE: tests/fixtures/event_external_account_card_deleted.json ================================================ { "id": "evt_1IuIg0QveW0ONQsdDLp7otQC", "object": "event", "account": "acct_1IuHosQveW0ONQsd", "api_version": "2020-08-27", "created": 1621781592, "data": { "object": { "id": "card_1IuVlSQveW0ONQsdkXBUUHyE", "object": "card", "account": "acct_1IuHosQveW0ONQsd", "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "available_payout_methods": [ "standard", "instant" ], "brand": "MasterCard", "country": "US", "currency": "usd", "cvc_check": null, "default_for_currency": true, "dynamic_last4": null, "exp_month": 5, "exp_year": 2022, "fingerprint": "SgIrwXiq2B8M5ab2", "funding": "debit", "last4": "8210", "metadata": {}, "name": null, "tokenization_method": null } }, "livemode": false, "pending_webhooks": 2, "request": { "id": "req_IQUywOh6PF8AyM", "idempotency_key": null }, "type": "account.external_account.deleted" } ================================================ FILE: tests/fixtures/event_external_account_card_updated.json ================================================ { "id": "evt_1IuIg0QveW0ONQsdDLp7otQC", "object": "event", "account": "acct_1IuHosQveW0ONQsd", "api_version": "2020-08-27", "created": 1621781592, "data": { "object": { "id": "card_1IuVlSQveW0ONQsdkXBUUHyE", "object": "card", "account": "acct_1IuHosQveW0ONQsd", "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "available_payout_methods": [ "standard", "instant" ], "brand": "MasterCard", "country": "US", "currency": "usd", "cvc_check": null, "default_for_currency": true, "dynamic_last4": null, "exp_month": 5, "exp_year": 2022, "fingerprint": "SgIrwXiq2B8M5ab2", "funding": "debit", "last4": "8210", "metadata": {}, "name": "Test Card", "tokenization_method": null } }, "livemode": false, "pending_webhooks": 2, "request": { "id": "req_IQUywOh6PF8AyM", "idempotency_key": null }, "type": "account.external_account.updated" } ================================================ FILE: tests/fixtures/invoice_in_fakefakefakefakefake0001.json ================================================ { "id": "in_fakefakefakefakefake0001", "object": "invoice", "account_country": "US", "account_name": "dj-stripe scratch", "amount_due": 2000, "amount_paid": 2000, "amount_remaining": 0, "application_fee_amount": null, "attempt_count": 1, "attempted": true, "auto_advance": false, "billing_reason": "subscription_create", "charge": "ch_fakefakefakefakefake0001", "collection_method": "charge_automatically", "created": 1557995176, "currency": "usd", "custom_fields": null, "customer": "cus_6lsBvm5rJ0zyHc", "customer_address": null, "customer_email": "michael.smith@example.com", "customer_name": null, "customer_phone": null, "customer_shipping": null, "customer_tax_exempt": "none", "customer_tax_ids": [], "default_payment_method": null, "default_source": null, "default_tax_rates": [{ "id": "txr_fakefakefakefakefake0001", "object": "tax_rate", "active": true, "created": 1593225980, "description": null, "display_name": "VAT", "inclusive": true, "jurisdiction": "Example1", "livemode": false, "metadata": { "djstripe_test_fake_id": "txr_fakefakefakefakefake0001" }, "percentage": 15.0 }], "description": null, "discount": null, "discounts": [], "due_date": null, "ending_balance": 0, "footer": null, "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_5Z1RsP0atfAS4t9CCnnEDTDyUG", "invoice_pdf": "https://pay.stripe.com/invoice/invst_5Z1RsP0atfAS4t9CCnnEDTDyUG/pdf", "lines": {}, "livemode": false, "metadata": { "djstripe_test_fake_id": "in_fakefakefakefakefake0001" }, "next_payment_attempt": null, "number": "E5B23224-0001", "paid": true, "payment_intent": "pi_fakefakefakefakefake0001", "period_end": 1557995176, "period_start": 1557995176, "post_payment_credit_notes_amount": 0, "pre_payment_credit_notes_amount": 0, "receipt_number": null, "starting_balance": 0, "statement_descriptor": null, "status": "paid", "status_transitions": { "finalized_at": 1593225981, "marked_uncollectible_at": null, "paid_at": 1593225983, "voided_at": null }, "subscription": "sub_fakefakefakefakefake0001", "subtotal": 2000, "tax": 261, "tax_percent": null, "total": 2000, "total_tax_amounts": [{ "amount": 261, "inclusive": true, "tax_rate": "txr_fakefakefakefakefake0001" }], "transfer_data": null, "webhooks_delivered_at": 1557995178 } ================================================ FILE: tests/fixtures/invoice_in_fakefakefakefakefake0004.json ================================================ { "id": "in_fakefakefakefakefake0004", "object": "invoice", "account_country": "US", "account_name": "dj-stripe scratch", "amount_due": 4000, "amount_paid": 4000, "amount_remaining": 0, "application_fee_amount": null, "attempt_count": 1, "attempted": true, "auto_advance": false, "billing_reason": "subscription_create", "charge": "ch_1GyU3gCOCguPTL2BnyYlJe2x", "collection_method": "charge_automatically", "created": 1570941590, "currency": "usd", "custom_fields": null, "customer": "cus_6lsBvm5rJ0zyHc", "customer_address": null, "customer_email": "michael.smith@example.com", "customer_name": null, "customer_phone": null, "customer_shipping": null, "customer_tax_exempt": "none", "customer_tax_ids": [], "default_payment_method": null, "default_source": null, "default_tax_rates": [], "description": null, "discount": null, "discounts": [], "due_date": null, "ending_balance": 0, "footer": null, "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_ttp3K5exxywlAI3cGqCDxEWfsM", "invoice_pdf": "https://pay.stripe.com/invoice/invst_ttp3K5exxywlAI3cGqCDxEWfsM/pdf", "lines": { "object": "list", "data": [{ "id": "il_1GyU3gCOCguPTL2B69cIweEY", "object": "line_item", "amount": 4000, "currency": "usd", "description": "1 \u00d7 Fake Product (at $40.00 / month)", "discountable": true, "livemode": false, "metadata": { "djstripe_test_fake_id": "sub_fakefakefakefakefake0002" }, "period": { "end": 1595817984, "start": 1593225984 }, "plan": { "id": "silver41294", "object": "plan", "active": true, "aggregate_usage": null, "amount": 4000, "amount_decimal": "4000", "billing_scheme": "per_unit", "created": 1570941587, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": 12, "usage_type": "licensed" }, "price": { "id": "silver41294", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1593225979, "currency": "usd", "livemode": false, "lookup_key": null, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "recurring": { "aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": 12, "usage_type": "licensed" }, "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 4000, "unit_amount_decimal": "4000" }, "proration": false, "quantity": 1, "subscription": "sub_fakefakefakefakefake0002", "subscription_item": "si_HXZCuxDh3W7EQm", "tax_amounts": [{ "amount": 522, "inclusive": true, "tax_rate": "txr_fakefakefakefakefake0001" }], "tax_rates": [{ "id": "txr_fakefakefakefakefake0001", "object": "tax_rate", "active": true, "created": 1593225980, "description": null, "display_name": "VAT", "inclusive": true, "jurisdiction": "Example1", "livemode": false, "metadata": { "djstripe_test_fake_id": "txr_fakefakefakefakefake0001" }, "percentage": 15.0 }], "type": "subscription" }], "has_more": false, "total_count": 1, "url": "/v1/invoices/in_fakefakefakefakefake0004/lines" }, "livemode": false, "metadata": { "djstripe_test_fake_id": "in_fakefakefakefakefake0004" }, "next_payment_attempt": null, "number": "E5B23224-0002", "paid": true, "payment_intent": "pi_1GyU3gCOCguPTL2BVH2OIzjf", "period_end": 1570941590, "period_start": 1570941590, "post_payment_credit_notes_amount": 0, "pre_payment_credit_notes_amount": 0, "receipt_number": null, "starting_balance": 0, "statement_descriptor": null, "status": "paid", "status_transitions": { "finalized_at": 1593225984, "marked_uncollectible_at": null, "paid_at": 1593225985, "voided_at": null }, "subscription": "sub_fakefakefakefakefake0002", "subtotal": 4000, "tax": 522, "tax_percent": null, "total": 4000, "total_tax_amounts": [{ "amount": 522, "inclusive": true, "tax_rate": "txr_fakefakefakefakefake0001" }], "transfer_data": null, "webhooks_delivered_at": 1570941591 } ================================================ FILE: tests/fixtures/line_item_il_invoice_item_fakefakefakefakefake0001.json ================================================ { "id": "il_fakefakefakefakefake0001", "object": "line_item", "livemode": false, "created": null, "metadata": {}, "description": "LINE ITEM 1", "amount": 27560, "amount_excluding_tax": 27560, "currency": "usd", "discount_amounts": [{ "amount": 7028, "discount": "di_fakefakefakefakefake0001" }], "discountable": true, "discounts": [ "di_fakefakefakefakefake0001" ], "invoice_item": "ii_fakefakefakefakefake0001", "period": { "end": 1666876260, "start": 1666876260 }, "period_end": 123456789, "period_start": 12111, "price": { "id": "price_1LKNRmJSZQVUcJYgRkQDOYBL", "type": "one_time", "active": false, "object": "price", "created": 1657549130, "product": "prod_M2SMqVbyQ81ihY", "currency": "usd", "livemode": false, "metadata": {}, "nickname": null, "recurring": null, "lookup_key": null, "tiers_mode": null, "unit_amount": 5512, "tax_behavior": "exclusive", "billing_scheme": "per_unit", "custom_unit_amount": null, "transform_quantity": null, "unit_amount_decimal": "5512" }, "proration": false, "proration_details": { "credited_items": null }, "subscription": "sub_fakefakefakefakefake0001", "subscription_item": "si_JiphMAMFxZKW8s", "tax_amounts": [], "tax_rates": [], "type": "invoiceitem", "unit_amount_excluding_tax": "5512.00", "quantity": 5 } ================================================ FILE: tests/fixtures/line_item_il_invoice_item_fakefakefakefakefake0002.json ================================================ { "id": "il_fakefakefakefakefake0002", "object": "line_item", "livemode": false, "created": null, "metadata": {}, "description": "LINE ITEM 1", "amount": 2000, "amount_excluding_tax": 2000, "currency": "usd", "discount_amounts": [{ "amount": 7028, "discount": "di_fakefakefakefakefake0001" }], "discountable": true, "discounts": [ "di_fakefakefakefakefake0001" ], "invoice_item": "ii_fakefakefakefakefake0001", "period": { "end": 1442111228, "start": 1444703228 }, "period_end": 123456789, "period_start": 12111, "price": { "id": "price_1LKNRmJSZQVUcJYgRkQDOYBL", "type": "one_time", "active": false, "object": "price", "created": 1657549130, "product": "prod_M2SMqVbyQ81ihY", "currency": "usd", "livemode": false, "metadata": {}, "nickname": null, "recurring": null, "lookup_key": null, "tiers_mode": null, "unit_amount": 5512, "tax_behavior": "exclusive", "billing_scheme": "per_unit", "custom_unit_amount": null, "transform_quantity": null, "unit_amount_decimal": "5512" }, "proration": false, "proration_details": { "credited_items": null }, "subscription": "sub_1rn1dp7WgjMtx9", "subscription_item": "si_JiphMAMFxZKW8s", "tax_amounts": [], "tax_rates": [], "type": "subscription", "unit_amount_excluding_tax": "2000.00", "quantity": 1 } ================================================ FILE: tests/fixtures/order_order_fakefakefakefake0001.json ================================================ { "id": "order_fakefakefakefakefake0001", "object": "order", "livemode": false, "created": 1562801159, "metadata": {}, "description": "", "amount_subtotal": 500, "amount_total": 500, "application": "", "automatic_tax": { "status": null, "enabled": false }, "billing_details": { "name": "Jane Doe", "email": null, "phone": null, "address": null }, "client_secret": "order_fakefakefakefakefake0001_secret_CQ2wCUrrDiDjp5YyocdjHonYz00s2YdRrKT", "currency": "usd", "customer": "cus_6lsBvm5rJ0zyHc", "discounts": [], "ip_address": null, "line_items": { "url": "/v1/orders/order_fakefakefakefakefake0001/line_items", "data": [{ "id": "li_1L3Hf7JSZQVUcJYghrqBjGnv", "price": { "id": "price_1J5O0hJSZQVUcJYgYpVPGEb9", "type": "one_time", "active": true, "object": "price", "created": 1624423702, "product": "prod_JipgBUhNfhFYlt", "currency": "usd", "livemode": false, "metadata": {}, "nickname": "One Time Updated", "recurring": null, "lookup_key": null, "tiers_mode": null, "unit_amount": 500, "tax_behavior": "unspecified", "billing_scheme": "per_unit", "transform_quantity": null, "unit_amount_decimal": "500" }, "object": "item", "product": "prod_JipgBUhNfhFYlt", "currency": "usd", "quantity": 1, "amount_tax": 0, "description": "Test Product 1", "amount_total": 500, "amount_discount": 0, "amount_subtotal": 500 }], "object": "list", "has_more": false }, "payment": { "status": "complete", "settings": null, "payment_intent": "pi_fakefakefakefakefake0001" }, "payment_intent": "pi_fakefakefakefakefake0001", "shipping_cost": null, "shipping_details": null, "status": "complete", "tax_details": { "tax_ids": [], "tax_exempt": "none" }, "total_details": { "amount_tax": 0, "amount_discount": 0, "amount_shipping": 0 } } ================================================ FILE: tests/fixtures/payment_intent_pi_destination_charge.json ================================================ { "id": "pi_1FG742B7kbjcJ8QqGKF6qIM0", "object": "payment_intent", "amount": 190200, "amount_capturable": 0, "amount_received": 190200, "application": null, "application_fee_amount": null, "canceled_at": null, "cancellation_reason": null, "capture_method": "automatic", "charges": { "object": "list", "data": [ { "id": "ch_fakefakefakefakefake0001", "object": "charge", "amount": 190200, "amount_refunded": 0, "application": null, "application_fee": null, "application_fee_amount": null, "balance_transaction": "txn_fake_ch_fakefakefakefakefake0001", "billing_details": { "address": { "city": null, "country": "US", "line1": null, "line2": null, "postal_code": "92082", "state": null }, "email": "kyoung@hotmail.com", "name": "John Foo", "phone": null }, "captured": true, "created": 1567874856, "currency": "usd", "customer": "cus_6lsBvm5rJ0zyHc", "description": "Online payment for FOO", "destination": "acct_1032D82eZvKYlo2C", "dispute": null, "failure_code": null, "failure_message": null, "fraud_details": {}, "invoice": null, "livemode": false, "metadata": {"foo": "bar"}, "on_behalf_of": "acct_1032D82eZvKYlo2C", "order": null, "outcome": { "network_status": "approved_by_network", "reason": null, "risk_level": "normal", "risk_score": 47, "seller_message": "Payment complete.", "type": "authorized" }, "paid": true, "payment_intent": "pi_1FG742B7kbjcJ8QqGKF6qIM0", "payment_method": "pm_1FG74VB7kbjcJ8QqXqULdSAV", "payment_method_details": { "card": { "brand": "visa", "checks": { "address_line1_check": null, "address_postal_code_check": "pass", "cvc_check": "pass" }, "country": "US", "exp_month": 10, "exp_year": 2020, "fingerprint": "sb2OAOijRKy8wYHu", "funding": "credit", "last4": "4242", "three_d_secure": null, "wallet": null }, "type": "card" }, "receipt_email": "kyoung@hotmail.com", "receipt_number": null, "receipt_url": "https://pay.stripe.com/receipts/acct_1DrGYIB7kbjcJ8Qq/ch_1FG74WB7kbjcJ8Qqx1oIdqfG/rcpt_FleN33oToRTXKCy6sxd5Stnh0ttnxYT", "refunded": false, "refunds": { "object": "list", "data": [], "has_more": false, "total_count": 0, "url": "/v1/charges/ch_1FG74WB7kbjcJ8Qqx1oIdqfG/refunds" }, "review": null, "shipping": null, "source": null, "source_transfer": null, "statement_descriptor": "FOO DESCRIPTOR", "statement_descriptor_suffix": null, "status": "succeeded", "transfer": "tr_16Y9BK2eZvKYlo2CR0ySu1BA", "transfer_data": { "amount": null, "destination": "acct_1032D82eZvKYlo2C" }, "transfer_group": "group_pi_1FG742B7kbjcJ8QqGKF6qIM0" } ], "has_more": false, "total_count": 1, "url": "/v1/charges?payment_intent=pi_1FG742B7kbjcJ8QqGKF6qIM0" }, "client_secret": "pi_1FG742B7kbjcJ8QqGKF6qIM0_secret_yeRoAechksXUy2HdUydIKlGbw", "confirmation_method": "automatic", "created": 1567874826, "currency": "usd", "customer": "cus_6lsBvm5rJ0zyHc", "description": "Online payment for FOO", "invoice": null, "last_payment_error": null, "livemode": false, "metadata": {"foo": "bar"}, "next_action": null, "on_behalf_of": "acct_1032D82eZvKYlo2C", "payment_method": "pm_fakefakefakefake0001", "payment_method_options": {"card": {"request_three_d_secure": "automatic"}}, "payment_method_types": ["card"], "receipt_email": "kyoung@hotmail.com", "review": null, "setup_future_usage": null, "shipping": null, "source": null, "statement_descriptor": "FOO DESCRIPTOR", "statement_descriptor_suffix": null, "status": "succeeded", "transfer_data": {"destination": "acct_1032D82eZvKYlo2C"}, "transfer_group": "group_pi_1FG742B7kbjcJ8QqGKF6qIM0" } ================================================ FILE: tests/fixtures/payment_intent_pi_fakefakefakefakefake0001.json ================================================ { "id": "pi_fakefakefakefakefake0001", "object": "payment_intent", "amount": 2000, "amount_capturable": 0, "amount_received": 2000, "application": null, "application_fee_amount": null, "canceled_at": null, "cancellation_reason": null, "capture_method": "automatic", "charges": { "object": "list", "data": [ { "id": "ch_fakefakefakefakefake0001", "object": "charge", "amount": 2000, "amount_refunded": 0, "application": null, "application_fee": null, "application_fee_amount": null, "balance_transaction": "txn_fake_ch_fakefakefakefakefake0001", "billing_details": { "address": { "city": null, "country": null, "line1": null, "line2": null, "postal_code": null, "state": null }, "email": null, "name": "alex-nesnes@hotmail.fr", "phone": null }, "calculated_statement_descriptor": "Stripe", "captured": true, "created": 1562801159, "currency": "usd", "customer": "cus_6lsBvm5rJ0zyHc", "description": "Subscription creation", "destination": null, "dispute": null, "disputed": false, "failure_code": null, "failure_message": null, "fraud_details": {}, "invoice": "in_fakefakefakefakefake0001", "livemode": false, "metadata": { "djstripe_test_fake_id": "ch_fakefakefakefakefake0001" }, "on_behalf_of": null, "order": null, "outcome": { "network_status": "approved_by_network", "reason": null, "risk_level": "normal", "risk_score": 2, "seller_message": "Payment complete.", "type": "authorized" }, "paid": true, "payment_intent": "pi_fakefakefakefakefake0001", "payment_method": "card_fakefakefakefakefake0001", "payment_method_details": { "card": { "brand": "visa", "checks": { "address_line1_check": null, "address_postal_code_check": null, "cvc_check": null }, "country": "US", "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "installments": null, "last4": "4242", "network": "visa", "three_d_secure": null, "wallet": null }, "type": "card" }, "receipt_email": null, "receipt_number": null, "receipt_url": "https://pay.stripe.com/receipts/acct_1DeuAXKatMEEd998/ch_fakefakefakefakefake0001/rcpt_FPeTB5h3PS9fE4GLTncDw2zC1bRRmmY", "refunded": false, "refunds": {}, "review": null, "shipping": null, "source": { "id": "card_fakefakefakefakefake0001", "object": "card", "address_city": null, "address_country": null, "address_line1": null, "address_line1_check": null, "address_line2": null, "address_state": null, "address_zip": null, "address_zip_check": null, "brand": "Visa", "country": "US", "customer": "cus_6lsBvm5rJ0zyHc", "cvc_check": null, "dynamic_last4": null, "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "last4": "4242", "metadata": { "djstripe_test_fake_id": "card_fakefakefakefakefake0001" }, "name": "alex-nesnes@hotmail.fr", "tokenization_method": null }, "source_transfer": null, "statement_descriptor": null, "statement_descriptor_suffix": null, "status": "succeeded", "transfer_data": null, "transfer_group": null } ], "has_more": false, "total_count": 1, "url": "/v1/charges?payment_intent=pi_fakefakefakefakefake0001" }, "client_secret": "pi_fakefakefakefakefake0001_secret_fu8Bf1uEyWIbQwB7f4xCQ7iTX", "confirmation_method": "automatic", "created": 1562801159, "currency": "usd", "customer": "cus_6lsBvm5rJ0zyHc", "description": "Subscription creation", "invoice": "in_fakefakefakefakefake0001", "last_payment_error": null, "livemode": false, "metadata": { "djstripe_test_fake_id": "pi_fakefakefakefakefake0001" }, "next_action": null, "on_behalf_of": null, "payment_method": null, "payment_method_options": { "card": { "installments": null, "network": null, "request_three_d_secure": "automatic" } }, "payment_method_types": [ "card" ], "receipt_email": null, "review": null, "setup_future_usage": "off_session", "shipping": null, "source": "card_fakefakefakefakefake0001", "statement_descriptor": null, "statement_descriptor_suffix": null, "status": "succeeded", "transfer_data": null, "transfer_group": null } ================================================ FILE: tests/fixtures/payment_method_card_fakefakefakefakefake0001.json ================================================ { "id": "card_fakefakefakefakefake0001", "object": "payment_method", "billing_details": { "address": { "city": null, "country": null, "line1": null, "line2": null, "postal_code": null, "state": null }, "email": null, "name": "alex-nesnes@hotmail.fr", "phone": null }, "card": { "brand": "visa", "checks": { "address_line1_check": null, "address_postal_code_check": null, "cvc_check": null }, "country": "US", "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "generated_from": null, "last4": "4242", "networks": { "available": [ "visa" ], "preferred": null }, "three_d_secure_usage": { "supported": true }, "wallet": null }, "created": 1567571746, "customer": "cus_6lsBvm5rJ0zyHc", "livemode": false, "metadata": { "djstripe_test_fake_id": "card_fakefakefakefakefake0001" }, "type": "card" } ================================================ FILE: tests/fixtures/payment_method_pm_fakefakefakefake0001.json ================================================ { "id": "pm_fakefakefakefake0001", "object": "payment_method", "billing_details": { "address": { "city": null, "country": null, "line1": null, "line2": null, "postal_code": null, "state": null }, "email": null, "name": null, "phone": null }, "card": { "brand": "visa", "checks": { "address_line1_check": null, "address_postal_code_check": null, "cvc_check": null }, "country": "US", "exp_month": 6, "exp_year": 2021, "fingerprint": "88PuXw9tEmvYe69o", "funding": "credit", "generated_from": null, "last4": "4242", "networks": { "available": [ "visa" ], "preferred": null }, "three_d_secure_usage": { "supported": true }, "wallet": null }, "created": 123456789, "customer": "cus_6lsBvm5rJ0zyHc", "livemode": false, "metadata": { "order_id": "123456789", "djstripe_test_fake_id": "pm_fakefakefakefake0001" }, "type": "card" } ================================================ FILE: tests/fixtures/payout_custom_bank_account.json ================================================ { "id": "po_1JR569QuFmP1Mw5uxasKoRx5", "object": "payout", "djstripe_owner_account": "acct_1IuHosQveW0ONQsd", "livemode": false, "created": 1629594221, "metadata": {}, "description": "STRIPE PAYOUT", "amount": "10.00", "arrival_date": 1629676800, "automatic": true, "balance_transaction": "txn_fake_ch_fakefakefakefakefake0001", "currency": "usd", "destination": "ba_16hTzo2eZvKYlo2CeSjfb0tS", "failure_balance_transaction": null, "failure_code": "", "failure_message": "", "method": "standard", "source_type": "card", "statement_descriptor": "", "status": "paid", "type": "bank_account" } ================================================ FILE: tests/fixtures/payout_custom_card.json ================================================ { "id": "po_1JR569QuFmP1Mw5uxasKoRx5", "object": "payout", "djstripe_owner_account": "acct_1IuHosQveW0ONQsd", "livemode": false, "created": 1629594221, "metadata": {}, "description": "STRIPE PAYOUT", "amount": "10.00", "arrival_date": 1629676800, "automatic": true, "balance_transaction": "txn_fake_ch_fakefakefakefakefake0001", "currency": "usd", "destination": "card_fakefakefakefakefake0001", "failure_balance_transaction": null, "failure_code": "", "failure_message": "", "method": "standard", "source_type": "card", "statement_descriptor": "", "status": "paid", "type": "card" } ================================================ FILE: tests/fixtures/plan_gold21323.json ================================================ { "id": "gold21323", "object": "plan", "active": true, "aggregate_usage": null, "amount": 2000, "amount_decimal": "2000", "billing_scheme": "per_unit", "created": 1557995175, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed" } ================================================ FILE: tests/fixtures/plan_silver41294.json ================================================ { "id": "silver41294", "object": "plan", "active": true, "aggregate_usage": null, "amount": 4000, "amount_decimal": "4000", "billing_scheme": "per_unit", "created": 1557995176, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": 12, "usage_type": "licensed" } ================================================ FILE: tests/fixtures/price_gold21323.json ================================================ { "active": true, "billing_scheme": "per_unit", "created": 1557995175, "currency": "usd", "id": "gold21323", "livemode": false, "lookup_key": null, "metadata": {}, "nickname": "New price name", "object": "price", "product": "prod_fake1", "recurring": { "aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": null, "usage_type": "licensed" }, "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 2000, "unit_amount_decimal": "2000" } ================================================ FILE: tests/fixtures/price_silver41294.json ================================================ { "active": true, "billing_scheme": "per_unit", "created": 1557995176, "currency": "usd", "id": "silver41294", "livemode": false, "lookup_key": null, "metadata": {}, "nickname": "New price name", "object": "price", "product": "prod_fake1", "recurring": { "aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": 12, "usage_type": "licensed" }, "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 4000, "unit_amount_decimal": "4000" } ================================================ FILE: tests/fixtures/product_prod_fake1.json ================================================ { "id": "prod_fake1", "object": "product", "active": true, "attributes": [], "created": 1557995174, "description": null, "images": [], "livemode": false, "default_price": null, "metadata": {}, "name": "Fake Product", "statement_descriptor": null, "type": "service", "unit_label": null, "updated": 1557995176 } ================================================ FILE: tests/fixtures/setup_intent_pi_destination_charge.json ================================================ { "application": null, "cancellation_reason": null, "client_secret": "seti_1J0g0WJSZQVUcJYgWE2XSi1K_secret_Jdxw2mOaIEHBdE6eTsfJ2IfmamgNJaF", "created": 1567874826, "customer": "cus_6lsBvm5rJ0zyHc", "description": null, "id": "seti_1J0g0WJSZQVUcJYgWE2XSi1K", "last_setup_error": null, "latest_attempt": "setatt_1J0g0WJSZQVUcJYgsrFgwxVh", "livemode": false, "mandate": null, "metadata": {}, "next_action": null, "object": "setup_intent", "payment_method": "pm_fakefakefakefake0001", "payment_method_options": { "card": { "request_three_d_secure": "automatic" } }, "payment_method_types": [ "card" ], "single_use_mandate": null, "status": "succeeded", "usage": "off_session", "on_behalf_of": "acct_1Fg9jUA3kq9o1aTc" } ================================================ FILE: tests/fixtures/shipping_rate_shr_fakefakefakefakefake0001.json ================================================ { "id": "shr_1L0co1JSZQVUcJYgcYfX1tZj", "object": "shipping_rate", "livemode": false, "created": 1570435136, "metadata": {}, "active": true, "display_name": "Test Shipping Code with no Tax Code", "fixed_amount": { "amount": 125, "currency": "usd" }, "type": "fixed_amount", "delivery_estimate": null, "tax_behavior": "exclusive", "tax_code": null } ================================================ FILE: tests/fixtures/shipping_rate_shr_fakefakefakefakefake0002.json ================================================ { "id": "shr_1L0co1JSZQVUcJYgcYfX1tZj", "object": "shipping_rate", "livemode": false, "created": 1570435136, "metadata": {}, "active": true, "display_name": "Test Shipping Code with no Tax Code", "fixed_amount": { "amount": 125, "currency": "usd" }, "type": "fixed_amount", "delivery_estimate": null, "tax_behavior": "exclusive", "tax_code": "txcd_99999999" } ================================================ FILE: tests/fixtures/source_src_fakefakefakefakefake0001.json ================================================ { "id": "src_fakefakefakefakefake0001", "object": "source", "amount": null, "card": { "exp_month": 6, "exp_year": 2021, "last4": "4242", "country": "US", "brand": "Visa", "funding": "credit", "fingerprint": "88PuXw9tEmvYe69o", "three_d_secure": "optional", "name": null, "address_line1_check": null, "address_zip_check": null, "cvc_check": null, "tokenization_method": null, "dynamic_last4": null }, "client_secret": "src_client_secret_F4oXYTWIpC9GV8qeLoVS9Wqk", "created": 1557995173, "currency": null, "customer": "cus_4QWKsZuuTHcs7X", "flow": "none", "livemode": false, "metadata": { "djstripe_test_fake_id": "src_fakefakefakefakefake0001" }, "owner": { "address": null, "email": null, "name": null, "phone": null, "verified_address": null, "verified_email": null, "verified_name": null, "verified_phone": null }, "statement_descriptor": null, "status": "chargeable", "type": "card", "usage": "reusable" } ================================================ FILE: tests/fixtures/subscription_sub_fakefakefakefakefake0001.json ================================================ { "id": "sub_fakefakefakefakefake0001", "object": "subscription", "application_fee_percent": null, "billing_cycle_anchor": 1557995176, "billing_thresholds": null, "cancel_at": null, "cancel_at_period_end": false, "canceled_at": null, "collection_method": "charge_automatically", "created": 1557995176, "current_period_end": 1560673576, "current_period_start": 1557995176, "customer": "cus_6lsBvm5rJ0zyHc", "days_until_due": null, "default_payment_method": null, "default_source": null, "default_tax_rates": [ { "id": "txr_fakefakefakefakefake0001", "object": "tax_rate", "active": true, "created": 1593225980, "description": null, "display_name": "VAT", "inclusive": true, "jurisdiction": "Example1", "livemode": false, "metadata": { "djstripe_test_fake_id": "txr_fakefakefakefakefake0001" }, "percentage": 15.0 } ], "discount": null, "ended_at": null, "items": { "object": "list", "data": [ { "id": "si_F5ukmkS6Bxi90Y", "object": "subscription_item", "billing_thresholds": null, "created": 1558230764, "metadata": {}, "plan": { "id": "gold21323", "object": "plan", "active": true, "aggregate_usage": null, "amount": 2000, "amount_decimal": "2000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed" }, "price": { "id": "gold21323", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1593225979, "currency": "usd", "livemode": false, "lookup_key": null, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "recurring": { "aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": null, "usage_type": "licensed" }, "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 2000, "unit_amount_decimal": "2000" }, "quantity": 1, "subscription": "sub_fakefakefakefakefake0001", "tax_rates": [] } ], "has_more": false, "total_count": 1, "url": "/v1/subscription_items?subscription=sub_fakefakefakefakefake0001" }, "latest_invoice": "in_fakefakefakefakefake0001", "livemode": false, "metadata": { "djstripe_test_fake_id": "sub_fakefakefakefakefake0001" }, "next_pending_invoice_item_invoice": null, "pause_collection": null, "pending_invoice_item_interval": null, "pending_setup_intent": null, "pending_update": null, "plan": { "id": "gold21323", "object": "plan", "active": true, "aggregate_usage": null, "amount": 2000, "amount_decimal": "2000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed" }, "quantity": 1, "schedule": null, "start_date": 1559476700, "status": "active", "tax_percent": null, "transfer_data": null, "trial_end": null, "trial_start": null } ================================================ FILE: tests/fixtures/subscription_sub_fakefakefakefakefake0002.json ================================================ { "id": "sub_fakefakefakefakefake0002", "object": "subscription", "application_fee_percent": null, "billing_cycle_anchor": 1557995178, "billing_thresholds": null, "cancel_at": null, "cancel_at_period_end": false, "canceled_at": null, "collection_method": "charge_automatically", "created": 1557995178, "current_period_end": 1560673578, "current_period_start": 1557995178, "customer": "cus_6lsBvm5rJ0zyHc", "days_until_due": null, "default_payment_method": null, "default_source": null, "default_tax_rates": [], "discount": null, "ended_at": null, "items": { "object": "list", "data": [ { "id": "si_F5ukq6eM2QV9g5", "object": "subscription_item", "billing_thresholds": null, "created": 1558230767, "metadata": {}, "plan": { "id": "silver41294", "object": "plan", "active": true, "aggregate_usage": null, "amount": 4000, "amount_decimal": "4000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": 12, "usage_type": "licensed" }, "price": { "id": "silver41294", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1593225979, "currency": "usd", "livemode": false, "lookup_key": null, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "recurring": { "aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": 12, "usage_type": "licensed" }, "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 4000, "unit_amount_decimal": "4000" }, "quantity": 1, "subscription": "sub_fakefakefakefakefake0002", "tax_rates": [ { "id": "txr_fakefakefakefakefake0001", "object": "tax_rate", "active": true, "created": 1593225980, "description": null, "display_name": "VAT", "inclusive": true, "jurisdiction": "Example1", "livemode": false, "metadata": { "djstripe_test_fake_id": "txr_fakefakefakefakefake0001" }, "percentage": 15.0 } ] } ], "has_more": false, "total_count": 1, "url": "/v1/subscription_items?subscription=sub_fakefakefakefakefake0002" }, "latest_invoice": "in_fakefakefakefakefake0004", "livemode": false, "metadata": { "djstripe_test_fake_id": "sub_fakefakefakefakefake0002" }, "next_pending_invoice_item_invoice": null, "pause_collection": null, "pending_invoice_item_interval": null, "pending_setup_intent": null, "pending_update": null, "plan": { "id": "silver41294", "object": "plan", "active": true, "aggregate_usage": null, "amount": 4000, "amount_decimal": "4000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": 12, "usage_type": "licensed" }, "quantity": 1, "schedule": null, "start_date": 1559476702, "status": "active", "tax_percent": null, "transfer_data": null, "trial_end": null, "trial_start": null } ================================================ FILE: tests/fixtures/subscription_sub_fakefakefakefakefake0003.json ================================================ { "id": "sub_fakefakefakefakefake0003", "object": "subscription", "application_fee_percent": null, "billing_cycle_anchor": 1557995180, "billing_thresholds": null, "cancel_at": null, "cancel_at_period_end": false, "canceled_at": null, "collection_method": "charge_automatically", "created": 1557995180, "current_period_end": 1560673580, "current_period_start": 1557995180, "customer": "cus_4UbFSo9tl62jqj", "days_until_due": null, "default_payment_method": null, "default_source": null, "default_tax_rates": [], "discount": null, "ended_at": null, "items": { "object": "list", "data": [ { "id": "si_F5ukGdpR4EejF9", "object": "subscription_item", "billing_thresholds": null, "created": 1558230770, "metadata": {}, "plan": { "id": "gold21323", "object": "plan", "active": true, "aggregate_usage": null, "amount": 2000, "amount_decimal": "2000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed" }, "price": { "id": "gold21323", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1593225979, "currency": "usd", "livemode": false, "lookup_key": null, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "recurring": { "aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": null, "usage_type": "licensed" }, "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 2000, "unit_amount_decimal": "2000" }, "quantity": 1, "subscription": "sub_fakefakefakefakefake0003", "tax_rates": [] } ], "has_more": false, "total_count": 1, "url": "/v1/subscription_items?subscription=sub_fakefakefakefakefake0003" }, "latest_invoice": "in_1GyU3iCOCguPTL2BDgqo4dj7", "livemode": false, "metadata": { "djstripe_test_fake_id": "sub_fakefakefakefakefake0003" }, "next_pending_invoice_item_invoice": null, "pause_collection": null, "pending_invoice_item_interval": null, "pending_setup_intent": null, "pending_update": null, "plan": { "id": "gold21323", "object": "plan", "active": true, "aggregate_usage": null, "amount": 2000, "amount_decimal": "2000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed" }, "quantity": 1, "schedule": null, "start_date": 1559476704, "status": "active", "tax_percent": null, "transfer_data": null, "trial_end": null, "trial_start": null } ================================================ FILE: tests/fixtures/subscription_sub_fakefakefakefakefake0004.json ================================================ { "id": "sub_fakefakefakefakefake0004", "object": "subscription", "application_fee_percent": null, "billing_cycle_anchor": 1557995182, "billing_thresholds": null, "cancel_at": null, "cancel_at_period_end": false, "canceled_at": null, "collection_method": "charge_automatically", "created": 1557995182, "current_period_end": 1560673582, "current_period_start": 1557995182, "customer": "cus_4UbFSo9tl62jqj", "days_until_due": null, "default_payment_method": null, "default_source": null, "default_tax_rates": [], "discount": null, "ended_at": null, "items": { "object": "list", "data": [ { "id": "si_F5uk9HMrUwrmUJ", "object": "subscription_item", "billing_thresholds": null, "created": 1558230772, "metadata": {}, "plan": { "id": "gold21323", "object": "plan", "active": true, "aggregate_usage": null, "amount": 2000, "amount_decimal": "2000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": null, "usage_type": "licensed" }, "price": { "id": "gold21323", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1593225979, "currency": "usd", "livemode": false, "lookup_key": null, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "recurring": { "aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": null, "usage_type": "licensed" }, "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 2000, "unit_amount_decimal": "2000" }, "quantity": 1, "subscription": "sub_fakefakefakefakefake0004", "tax_rates": [] }, { "id": "si_F5uk81B1xGi3Vr", "object": "subscription_item", "billing_thresholds": null, "created": 1558230772, "metadata": {}, "plan": { "id": "silver41294", "object": "plan", "active": true, "aggregate_usage": null, "amount": 4000, "amount_decimal": "4000", "billing_scheme": "per_unit", "created": 1558230763, "currency": "usd", "interval": "month", "interval_count": 1, "livemode": false, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "tiers": null, "tiers_mode": null, "transform_usage": null, "trial_period_days": 12, "usage_type": "licensed" }, "price": { "id": "silver41294", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1593225979, "currency": "usd", "livemode": false, "lookup_key": null, "metadata": {}, "nickname": "New plan name", "product": "prod_fake1", "recurring": { "aggregate_usage": null, "interval": "month", "interval_count": 1, "trial_period_days": 12, "usage_type": "licensed" }, "tiers_mode": null, "transform_quantity": null, "type": "recurring", "unit_amount": 4000, "unit_amount_decimal": "4000" }, "quantity": 1, "subscription": "sub_fakefakefakefakefake0004", "tax_rates": [] } ], "has_more": false, "total_count": 2, "url": "/v1/subscription_items?subscription=sub_fakefakefakefakefake0004" }, "latest_invoice": "in_1GyU3kCOCguPTL2BO0EVwuzj", "livemode": false, "metadata": { "djstripe_test_fake_id": "sub_fakefakefakefakefake0004" }, "next_pending_invoice_item_invoice": null, "pause_collection": null, "pending_invoice_item_interval": null, "pending_setup_intent": null, "pending_update": null, "plan": null, "quantity": null, "schedule": null, "start_date": 1559476706, "status": "active", "tax_percent": null, "transfer_data": null, "trial_end": null, "trial_start": null } ================================================ FILE: tests/fixtures/tax_code_txcd_fakefakefakefakefake0001.json ================================================ { "id": "txcd_99999999", "object": "tax_code", "livemode": null, "created": null, "description": "Any tangible or physical good. For jurisdictions that impose a tax, the standard rate is applied.", "name": "General - Tangible Goods" } ================================================ FILE: tests/fixtures/tax_id_txi_fakefakefakefakefake0001.json ================================================ { "id": "txi_1J5NznJSZQVUcJYgGWkyTcvb", "object": "tax_id", "livemode": false, "created": 1570435136, "country": "US", "customer": "cus_6lsBvm5rJ0zyHc", "type": "us_ein", "value": "12-0982347", "verification": { "status": "unavailable", "verified_address": null, "verified_name": null } } ================================================ FILE: tests/fixtures/tax_rate_txr_fakefakefakefakefake0001.json ================================================ { "id": "txr_fakefakefakefakefake0001", "object": "tax_rate", "active": true, "created": 1570435136, "description": null, "display_name": "VAT", "inclusive": true, "jurisdiction": "Example1", "livemode": false, "metadata": { "djstripe_test_fake_id": "txr_fakefakefakefakefake0001" }, "percentage": 15.0 } ================================================ FILE: tests/fixtures/tax_rate_txr_fakefakefakefakefake0002.json ================================================ { "id": "txr_fakefakefakefakefake0002", "object": "tax_rate", "active": true, "created": 1570436740, "description": "Example2 Sales Tax", "display_name": "Sales tax", "inclusive": false, "jurisdiction": "Example2", "livemode": false, "metadata": { "djstripe_test_fake_id": "txr_fakefakefakefakefake0002" }, "percentage": 4.25 } ================================================ FILE: tests/fixtures/usage_record_summary_sis_fakefakefakefakefake0001.json ================================================ { "data": [ { "id": "sis_1JPOIXJSZQVUcJYgoaYmbznL", "invoice": null, "livemode": false, "object": "usage_record_summary", "period": { "end": null, "start": 1627015586 }, "subscription_item": "si_JiphMAMFxZKW8s", "total_usage": 700 }, { "id": "sis_1JGGM6JSZQVUcJYgCljH2Mkl", "invoice": "in_1JGGM6JSZQVUcJYgpWqfBOIl", "livemode": false, "object": "usage_record_summary", "period": { "end": 1627015586, "start": 1624423771 }, "subscription_item": "si_JiphMAMFxZKW8s", "total_usage": 0 } ], "has_more": false, "object": "list", "url": "/v1/subscription_items/si_JiphMAMFxZKW8s/usage_record_summaries" } ================================================ FILE: tests/fixtures/webhook_endpoint_fake0001.json ================================================ { "id": "we_1Jto4CJSZQVUcJYgJKJPEBll", "livemode": false, "metadata": { "djstripe_uuid": "f6f9aa0e-cb6c-4e0f-b5ee-5e2b9e0716d8" }, "created": 1570435136, "description": "This is the first webhook of a different website example.com", "api_version": "", "application": "ca_JWVN6vOJJs3RKsff0VUwIUfmUmN0VYqe", "enabled_events": ["*"], "secret": "", "status": "enabled", "url": "https://dev.example.com/stripe/webhook/f6f9aa0e-cb6c-4e0f-b5ee-5e2b9e0716d8", "djstripe_uuid": "f6f9aa0e-cb6c-4e0f-b5ee-5e2b9e0716d8", "djstripe_tolerance_ms": 300.0, "djstripe_validation_type": "verify_signature", "object": "webhook_endpoint" } ================================================ FILE: tests/settings.py ================================================ import json import os test_db_vendor = os.environ.get("DJSTRIPE_TEST_DB_VENDOR", "postgres") test_db_name = os.environ.get("DJSTRIPE_TEST_DB_NAME", "djstripe") test_db_user = os.environ.get("DJSTRIPE_TEST_DB_USER", test_db_vendor) test_db_pass = os.environ.get("DJSTRIPE_TEST_DB_PASS", "djstripe") test_db_port = os.environ.get("DJSTRIPE_TEST_DB_PORT", "") DEBUG = True SECRET_KEY = os.environ.get("DJSTRIPE_TEST_DJANGO_SECRET_KEY", "djstripe") SITE_ID = 1 TIME_ZONE = "UTC" USE_TZ = True PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(PROJECT_DIR) ALLOWED_HOSTS = json.loads(os.environ.get("DJSTRIPE_TEST_ALLOWED_HOSTS_JSON", '["*"]')) if test_db_vendor == "postgres": DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": test_db_name, "USER": test_db_user, "PASSWORD": test_db_pass, "HOST": os.environ.get("DJSTRIPE_TEST_DB_HOST", "localhost"), "PORT": test_db_port, } } elif test_db_vendor == "mysql": DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": test_db_name, "USER": os.environ.get("DJSTRIPE_TEST_DB_USER", "root"), "PASSWORD": test_db_pass, "HOST": os.environ.get("DJSTRIPE_TEST_DB_HOST", "127.0.0.1"), "PORT": test_db_port, } } elif test_db_vendor == "sqlite": # sqlite is not officially supported, but useful for quick testing. # may be dropped if we can't maintain compatibility. DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), # use a on-disk db for test so --reuse-db can be used "TEST": {"NAME": os.path.join(BASE_DIR, "test_db.sqlite3")}, } } else: raise NotImplementedError(f"DJSTRIPE_TEST_DB_VENDOR = {test_db_vendor}") TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", "django.template.context_processors.request", ] }, } ] ROOT_URLCONF = "tests.urls" INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.sites", "django.contrib.staticfiles", "djstripe", "tests", # to load custom models defined to test fields.py "tests.fields", "tests.apps.testapp", "tests.apps.example", ] MIDDLEWARE = ( "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ) STRIPE_LIVE_PUBLIC_KEY = os.environ.get( "STRIPE_PUBLIC_KEY", "pk_live_XXXXXXXXXXXXXXXXXXXXXXXXX" ) STRIPE_LIVE_SECRET_KEY = os.environ.get( "STRIPE_SECRET_KEY", "sk_live_XXXXXXXXXXXXXXXXXXXXXXXXX" ) STRIPE_TEST_PUBLIC_KEY = os.environ.get( "STRIPE_PUBLIC_KEY", "pk_test_XXXXXXXXXXXXXXXXXXXXXXXXX", ) STRIPE_TEST_SECRET_KEY = os.environ.get( "STRIPE_SECRET_KEY", "sk_test_XXXXXXXXXXXXXXXXXXXXXXXXX", ) DJSTRIPE_FOREIGN_KEY_TO_FIELD = ( "id" if os.environ.get("USE_NATIVE_STRIPE_ID", "") == "1" else "djstripe_id" ) DJSTRIPE_WEBHOOK_VALIDATION = "verify_signature" DJSTRIPE_WEBHOOK_SECRET = os.environ.get("DJSTRIPE_TEST_WEBHOOK_SECRET", "whsec_XXXXX") STATIC_URL = "/static/" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" ================================================ FILE: tests/templates/base.html ================================================ {% block head_title %}{% endblock %} {% block header_js %}{% endblock %} {% block header_css %}{% endblock %} {% block content %} {% endblock %} ================================================ FILE: tests/test_account.py ================================================ """ dj-stripe Account Tests. """ from copy import deepcopy from unittest.mock import patch import pytest from django.test.testcases import TestCase from django.test.utils import override_settings from djstripe.models import Account from djstripe.settings import djstripe_settings from . import ( FAKE_ACCOUNT, FAKE_CUSTOM_ACCOUNT, FAKE_EXPRESS_ACCOUNT, FAKE_FILEUPLOAD_ICON, FAKE_FILEUPLOAD_LOGO, FAKE_PLATFORM_ACCOUNT, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class TestAccount(AssertStripeFksMixin, TestCase): @patch("stripe.Account.retrieve", autospec=True) @patch( "stripe.File.retrieve", side_effect=[deepcopy(FAKE_FILEUPLOAD_ICON), deepcopy(FAKE_FILEUPLOAD_LOGO)], autospec=True, ) def test_get_default_account(self, file_retrieve_mock, account_retrieve_mock): account_retrieve_mock.return_value = deepcopy(FAKE_ACCOUNT) account = Account.get_default_account() account_retrieve_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) self.assertGreater(len(account.business_profile), 0) self.assertGreater(len(account.settings), 0) self.assertEqual(account.branding_icon.id, FAKE_FILEUPLOAD_ICON["id"]) self.assertEqual(account.branding_logo.id, FAKE_FILEUPLOAD_LOGO["id"]) self.assertEqual(account.settings["branding"]["icon"], account.branding_icon.id) self.assertEqual(account.settings["branding"]["logo"], account.branding_logo.id) self.assertNotEqual(account.branding_logo.id, account.branding_icon.id) self.assert_fks(account, expected_blank_fks={}) self.assertEqual(account.business_url, "https://djstripe.com") account.business_profile = None self.assertEqual(account.business_url, "") @patch( "stripe.Account.retrieve", autospec=True, return_value=deepcopy(FAKE_ACCOUNT), ) @patch( "stripe.File.retrieve", side_effect=[deepcopy(FAKE_FILEUPLOAD_ICON), deepcopy(FAKE_FILEUPLOAD_LOGO)], autospec=True, ) def test_sync_from_stripe_data( self, fileupload_retrieve_mock, account_retrieve_mock ): fake_account = deepcopy(FAKE_ACCOUNT) account = Account.sync_from_stripe_data(fake_account) self.assertGreater(len(account.business_profile), 0) self.assertGreater(len(account.settings), 0) self.assertEqual(account.branding_icon.id, FAKE_FILEUPLOAD_ICON["id"]) self.assertEqual(account.branding_logo.id, FAKE_FILEUPLOAD_LOGO["id"]) self.assertEqual(account.settings["branding"]["icon"], account.branding_icon.id) self.assertEqual(account.settings["branding"]["logo"], account.branding_logo.id) self.assertNotEqual(account.branding_logo.id, account.branding_icon.id) self.assert_fks(account, expected_blank_fks={}) self.assertEqual(account.business_url, "https://djstripe.com") @patch( "stripe.Account.retrieve", autospec=True, return_value=deepcopy(FAKE_ACCOUNT), ) @patch( "stripe.File.retrieve", side_effect=[deepcopy(FAKE_FILEUPLOAD_ICON), deepcopy(FAKE_FILEUPLOAD_LOGO)], autospec=True, ) def test__find_owner_account(self, fileupload_retrieve_mock, account_retrieve_mock): fake_account = deepcopy(FAKE_ACCOUNT) account = Account.sync_from_stripe_data(fake_account) self.assertEqual(account.djstripe_owner_account.id, FAKE_PLATFORM_ACCOUNT["id"]) @patch( "stripe.Account.retrieve", autospec=True, return_value=deepcopy(FAKE_ACCOUNT), ) @patch( "stripe.File.retrieve", side_effect=[deepcopy(FAKE_FILEUPLOAD_ICON), deepcopy(FAKE_FILEUPLOAD_LOGO)], autospec=True, ) def test_business_url(self, fileupload_retrieve_mock, account_retrieve_mock): fake_account = deepcopy(FAKE_ACCOUNT) account = Account.sync_from_stripe_data(fake_account) self.assertEqual(fake_account["business_profile"]["url"], account.business_url) @patch( "stripe.Account.retrieve", autospec=True, return_value=deepcopy(FAKE_ACCOUNT), ) @patch( "stripe.File.retrieve", side_effect=[deepcopy(FAKE_FILEUPLOAD_ICON), deepcopy(FAKE_FILEUPLOAD_LOGO)], autospec=True, ) def test_branding_logo(self, fileupload_retrieve_mock, account_retrieve_mock): fake_account = deepcopy(FAKE_ACCOUNT) account = Account.sync_from_stripe_data(fake_account) self.assertEqual( fake_account["settings"]["branding"]["logo"], account.branding_logo.id ) @patch( "stripe.Account.retrieve", autospec=True, return_value=deepcopy(FAKE_ACCOUNT), ) @patch( "stripe.File.retrieve", side_effect=[deepcopy(FAKE_FILEUPLOAD_ICON), deepcopy(FAKE_FILEUPLOAD_LOGO)], autospec=True, ) def test_branding_icon(self, fileupload_retrieve_mock, account_retrieve_mock): fake_account = deepcopy(FAKE_ACCOUNT) account = Account.sync_from_stripe_data(fake_account) self.assertEqual( fake_account["settings"]["branding"]["icon"], account.branding_icon.id ) @patch("stripe.Account.retrieve", autospec=True) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_LOGO), autospec=True, ) def test__attach_objects_post_save_hook( self, fileupload_retrieve_mock, account_retrieve_mock ): fake_account = deepcopy(FAKE_ACCOUNT) fake_account["settings"]["branding"]["icon"] = None account_retrieve_mock.return_value = fake_account account = Account.sync_from_stripe_data(fake_account) assert account.livemode is False fileupload_retrieve_mock.assert_called_with( id=fake_account["settings"]["branding"]["logo"], api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], stripe_account=fake_account["id"], stripe_version=djstripe_settings.STRIPE_API_VERSION, ) @patch("stripe.Account.retrieve", autospec=True) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_LOGO), autospec=True, ) def test_get_default_account_null_logo( self, fileupload_retrieve_mock, account_retrieve_mock ): fake_account = deepcopy(FAKE_ACCOUNT) fake_account["settings"]["branding"]["icon"] = None fake_account["settings"]["branding"]["logo"] = None account_retrieve_mock.return_value = fake_account account = Account.get_default_account() account_retrieve_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) self.assert_fks( account, expected_blank_fks={ "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", }, ) @patch( "stripe.Account.retrieve", autospec=True, return_value=deepcopy(FAKE_ACCOUNT), ) @patch( "stripe.File.retrieve", side_effect=[deepcopy(FAKE_FILEUPLOAD_ICON), deepcopy(FAKE_FILEUPLOAD_LOGO)], autospec=True, ) def test_get_stripe_dashboard_url( self, fileupload_retrieve_mock, account_retrieve_mock ): fake_account = deepcopy(FAKE_ACCOUNT) account = Account.sync_from_stripe_data(fake_account) self.assertEqual( account.get_stripe_dashboard_url(), f"https://dashboard.stripe.com/{account.id}/" f"{'test/' if not account.livemode else ''}dashboard", ) class TestAccountMethods: @pytest.mark.parametrize( ( "business_profile_update", "settings_dashboard_update", "expected_account_str", ), ( ({}, {}, "dj-stripe"), ({}, {"display_name": "some display name"}, "some display name"), ( {"name": "some business name"}, {"display_name": ""}, "some business name", ), ({"name": ""}, {"display_name": ""}, ""), ), ) @patch("stripe.Account.retrieve", autospec=True) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_LOGO), autospec=True, ) def test_account_str( self, fileupload_retrieve_mock, account_retrieve_mock, business_profile_update, settings_dashboard_update, expected_account_str, ): fake_account = deepcopy(FAKE_ACCOUNT) fake_account["business_profile"].update(business_profile_update) fake_account["settings"]["dashboard"].update(settings_dashboard_update) account_retrieve_mock.return_value = fake_account account = Account.get_default_account() assert str(account) == expected_account_str @patch("stripe.Account.retrieve", autospec=True) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_LOGO), autospec=True, ) def test__str__null_settings_null_business_profile( self, fileupload_retrieve_mock, account_retrieve_mock, ): """Test that __str__ doesn't crash when settings and business_profile are NULL.""" fake_account = deepcopy(FAKE_ACCOUNT) fake_account["settings"] = None fake_account["business_profile"] = None account_retrieve_mock.return_value = fake_account account = Account.sync_from_stripe_data(fake_account) assert str(account) == "" @override_settings( STRIPE_SECRET_KEY="sk_live_XXXXXXXXXXXXXXXXXXXX5678", STRIPE_LIVE_MODE=True, ) @pytest.mark.parametrize( "_account,is_platform", [ (deepcopy(FAKE_ACCOUNT), False), (deepcopy(FAKE_CUSTOM_ACCOUNT), False), (deepcopy(FAKE_EXPRESS_ACCOUNT), False), (deepcopy(FAKE_PLATFORM_ACCOUNT), True), ], ) @patch("stripe.Account.retrieve", autospec=True) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_LOGO), autospec=True, ) def test_livemode_populates_correctly_for_livemode( self, fileupload_retrieve_mock, account_retrieve_mock, _account, is_platform, ): fake_account = _account fake_account["settings"]["branding"]["icon"] = None account_retrieve_mock.return_value = fake_account platform_account = FAKE_PLATFORM_ACCOUNT.create() # Account.get_or_retrieve_for_api_key is called and since the passed in api_key doesn't have an owner acount, # key is refreshed and the current mocked _account is assigned as the owner account. # This essentially turns all these cases into Platform Account cases. # And that is why Account.get_or_retrieve_for_api_key is patched with patch.object( Account, "get_or_retrieve_for_api_key", return_value=platform_account ): account = Account.sync_from_stripe_data( fake_account, api_key=djstripe_settings.STRIPE_SECRET_KEY ) assert account.djstripe_owner_account == platform_account if is_platform is True: assert account.livemode is None else: assert account.livemode is True @override_settings( STRIPE_SECRET_KEY="sk_test_XXXXXXXXXXXXXXXXXXXX5678", STRIPE_LIVE_MODE=False, ) @pytest.mark.parametrize( "_account,is_platform", [ (deepcopy(FAKE_ACCOUNT), False), (deepcopy(FAKE_CUSTOM_ACCOUNT), False), (deepcopy(FAKE_EXPRESS_ACCOUNT), False), (deepcopy(FAKE_PLATFORM_ACCOUNT), True), ], ) @patch("stripe.Account.retrieve", autospec=True) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_LOGO), autospec=True, ) def test_livemode_populates_correctly_for_testmode( self, fileupload_retrieve_mock, account_retrieve_mock, _account, is_platform ): fake_account = _account fake_account["settings"]["branding"]["icon"] = None account_retrieve_mock.return_value = fake_account platform_account = FAKE_PLATFORM_ACCOUNT.create() # Account.get_or_retrieve_for_api_key is called and since the passed in api_key doesn't have an owner acount, # key is refreshed and the current mocked _account is assigned as the owner account. # This essentially turns all these cases into Platform Account cases. # And that is why Account.get_or_retrieve_for_api_key is patched with patch.object( Account, "get_or_retrieve_for_api_key", return_value=platform_account ): account = Account.sync_from_stripe_data( fake_account, api_key=djstripe_settings.STRIPE_SECRET_KEY ) assert account.djstripe_owner_account == platform_account if is_platform is True: assert account.livemode is None else: assert account.livemode is False class TestAccountRestrictedKeys(TestCase): @override_settings( STRIPE_TEST_SECRET_KEY="rk_test_blah", STRIPE_TEST_PUBLIC_KEY="pk_test_foo", STRIPE_LIVE_MODE=False, ) @patch("stripe.Account.retrieve", autospec=True) def test_account_str_restricted_key(self, account_retrieve_mock): """ Test that we do not attempt to retrieve account ID with restricted keys. """ assert djstripe_settings.STRIPE_SECRET_KEY == "rk_test_blah" account = Account.get_default_account() assert account is None account_retrieve_mock.assert_not_called() @pytest.mark.parametrize( "mock_account_id, other_mock_account_id, expected_stripe_account", ( ("acct_fakefakefakefake001", None, "acct_fakefakefakefake001"), ( "acct_fakefakefakefake001", "acct_fakefakefakefake002", "acct_fakefakefakefake002", ), ), ) @patch( target="djstripe.models.connect.StripeModel._create_from_stripe_object", autospec=True, ) def test_account__create_from_stripe_object( mock_super__create_from_stripe_object, mock_account_id, other_mock_account_id, expected_stripe_account, ): """Ensure that we are setting the ID value correctly.""" mock_data = {"id": mock_account_id} Account._create_from_stripe_object( data=mock_data, stripe_account=other_mock_account_id ) mock_super__create_from_stripe_object.assert_called_once_with( data=mock_data, current_ids=None, pending_relations=None, save=True, stripe_account=expected_stripe_account, api_key=djstripe_settings.STRIPE_SECRET_KEY, ) @pytest.mark.parametrize("stripe_account", (None, "acct_fakefakefakefake001")) @pytest.mark.parametrize( "api_key, expected_api_key", ( (None, djstripe_settings.STRIPE_SECRET_KEY), ("sk_fakefakefake01", "sk_fakefakefake01"), ), ) @pytest.mark.parametrize("extra_kwargs", ({"reason": "fraud"}, {"reason": "other"})) @patch( "stripe.Account.retrieve", autospec=True, return_value=deepcopy(FAKE_ACCOUNT), ) @patch( "stripe.Account.reject", ) @patch( "stripe.File.retrieve", side_effect=[deepcopy(FAKE_FILEUPLOAD_ICON), deepcopy(FAKE_FILEUPLOAD_LOGO)], autospec=True, ) def test_api_reject( fileupload_retrieve_mock, account_reject_mock, account_retrieve_mock, extra_kwargs, api_key, expected_api_key, stripe_account, ): """Test that API reject properly uses the passed in parameters.""" fake_account = deepcopy(FAKE_ACCOUNT) fake_account_rejected = deepcopy(FAKE_ACCOUNT) fake_account_rejected["charges_enabled"] = False fake_account_rejected["payouts_enabled"] = False account_reject_mock.return_value = fake_account_rejected account = Account.sync_from_stripe_data(fake_account) # invoke api_reject() account_rejected = account.api_reject( api_key=api_key, stripe_account=stripe_account, **extra_kwargs ) assert account_rejected["charges_enabled"] is False assert account_rejected["payouts_enabled"] is False Account.stripe_class.reject.assert_called_once_with( account.id, api_key=expected_api_key, stripe_account=stripe_account or FAKE_PLATFORM_ACCOUNT["id"], stripe_version=djstripe_settings.STRIPE_API_VERSION, **extra_kwargs, ) ================================================ FILE: tests/test_admin.py ================================================ """ dj-stripe Admin Tests. """ from copy import deepcopy from typing import Sequence import pytest import stripe from django.apps import apps from django.contrib.admin import helpers, site from django.contrib.auth import get_user_model from django.core.exceptions import FieldError from django.test import TestCase from django.test.client import RequestFactory from django.urls import reverse from pytest_django.asserts import assertQuerysetEqual from djstripe import models, utils from djstripe.admin import admin as djstripe_admin from djstripe.admin.forms import CustomActionForm from djstripe.models.account import Account from tests import ( FAKE_BALANCE_TRANSACTION, FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CHARGE, FAKE_CUSTOMER, FAKE_INVOICE, FAKE_INVOICEITEM, FAKE_PAYMENT_INTENT_I, FAKE_PLAN, FAKE_PRODUCT, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_ITEM, FAKE_SUBSCRIPTION_SCHEDULE, ) from .fields.models import CustomActionModel pytestmark = pytest.mark.django_db @pytest.mark.parametrize( "output,input", [ ( ["event", "stripe_trigger_account", "webhook_endpoint"], models.WebhookEventTrigger, ), ( [ "djstripe_owner_account", "customer", "default_payment_method", "default_source", "latest_invoice", "pending_setup_intent", "plan", "schedule", "default_tax_rates", ], models.Subscription, ), ( [ "djstripe_owner_account", "default_source", "coupon", "default_payment_method", "subscriber", ], models.Customer, ), ], ) def test_get_forward_relation_fields_for_model(output, input): assert output == djstripe_admin.get_forward_relation_fields_for_model(input) class TestAdminRegisteredModelsChildrenOfStripeModel(TestCase): def setUp(self): self.admin = get_user_model().objects.create_superuser( username="admin", email="admin@djstripe.com", password="xxx" ) self.factory = RequestFactory() # the 4 models that do not inherit from StripeModel and hence # do not inherit from StripeModelAdmin self.ignore_models = [ "WebhookEventTrigger", "WebhookEndpoint", "IdempotencyKey", "APIKey", ] def test_get_list_display_links(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard changelist_view url url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) # add the admin user to the mocked request request = self.factory.get(url) request.user = self.admin response = model_admin.changelist_view(request) list_display = model_admin.get_changelist_instance(request).list_display # get_changelist_instance to get an instance of the ChangelistView for logged in admin user self.assertEqual(response.status_code, 200) self.assertEqual( list(response.context_data["cl"].list_display_links), list( model_admin.get_changelist_instance(request).list_display_links ), ) # ensure all the fields in list_display_links are valid for field in model_admin.get_list_display_links(request, list_display): model_admin.get_changelist_instance(request).get_ordering_field( field ) def test_get_list_display(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard changelist_view url url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) # add the admin user to the mocked request request = self.factory.get(url) request.user = self.admin response = model_admin.changelist_view(request) # get_changelist_instance to get an instance of the ChangelistView for logged in admin user list_display = model_admin.get_changelist_instance(request).list_display self.assertEqual(response.status_code, 200) self.assertEqual( list(response.context_data["cl"].list_display), list(list_display), ) # ensure all the fields in list_display are valid for field in model_admin.get_list_display(request): model_admin.get_changelist_instance(request).get_ordering_field( field ) # for models inheriting from StripeModelAdmin verify: if model.__name__ not in self.ignore_models: self.assertTrue( all( [ 1 for i in [ "__str__", "id", "djstripe_owner_account", "created", "livemode", ] if i in list_display ] ) ) def test_get_list_filter(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard changelist_view url url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) # add the admin user to the mocked request request = self.factory.get(url) request.user = self.admin response = model_admin.changelist_view(request) # get_changelist_instance to get an instance of the ChangelistView for logged in admin user list_filter = model_admin.get_changelist_instance(request).list_filter self.assertEqual(response.status_code, 200) self.assertEqual( list(response.context_data["cl"].list_filter), list(list_filter), ) # ensure all the filters get formed correctly chl = model_admin.get_changelist_instance(request) chl.get_filters(request) chl.get_queryset(request) # for models inheriting from StripeModelAdmin verify: if model.__name__ not in self.ignore_models: self.assertTrue( all([1 for i in ["created", "livemode"] if i in list_filter]) ) def test_get_readonly_fields(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard changelist_view url url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) # add the admin user to the mocked request request = self.factory.get(url) request.user = self.admin response = model_admin.changelist_view(request) # get_changelist_instance to get an instance of the ChangelistView for logged in admin user readonly_fields = model_admin.get_changelist_instance( request ).model_admin.readonly_fields self.assertEqual(response.status_code, 200) self.assertEqual( response.context_data["cl"].model_admin.readonly_fields, readonly_fields, ) # ensure all the fields in readonly_fields are valid for field in model_admin.get_readonly_fields(request): # ensure the given field is on model, or model_admin or modelform model_admin.get_changelist_instance(request).get_ordering_field( field ) # for models inheriting from StripeModelAdmin verify: if model.__name__ not in self.ignore_models: self.assertTrue( all( [ 1 for i in ["created", "djstripe_owner_account", "id"] if i in readonly_fields ] ) ) def test_get_list_select_related(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard changelist_view url url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) # add the admin user to the mocked request request = self.factory.get(url) request.user = self.admin response = model_admin.changelist_view(request) # get_changelist_instance to get an instance of the ChangelistView for logged in admin user list_select_related = model_admin.get_changelist_instance( request ).list_select_related self.assertEqual(response.status_code, 200) self.assertEqual( response.context_data["cl"].list_select_related, list_select_related, ) # ensure all the fields in list_select_related are valid list_select_related_fields = model_admin.get_list_select_related( request ) if isinstance(list_select_related_fields, Sequence): # need to force the returned queryset to get evaluated list(model.objects.select_related(*list_select_related_fields)) # todo complete after djstripe has integrated ModelFactory # def test_get_fieldsets_change(self): # pass def test_get_fieldsets_add(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard add url add_url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_add" ) # add the admin user to the mocked request request = self.factory.get(add_url) request.user = self.admin # skip model if model doesn't have "has_add_permission" if not model_admin.has_add_permission(request): continue response = model_admin.add_view(request) fieldsets = model_admin.get_fieldsets(request) self.assertEqual(response.status_code, 200) self.assertEqual( response.context_data["adminform"].fieldsets, [*fieldsets], ) # for models inheriting from StripeModelAdmin verify: if model.__name__ not in self.ignore_models: self.assertTrue( all( [ 1 for i in [ "created", "livemode", "djstripe_owner_account", "id", ] if i in fieldsets ] ) ) # todo complete after djstripe has integrated ModelFactory # def test_get_fields_change(self): # pass def test_get_fields_add(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard add url add_url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_add" ) # add the admin user to the mocked request request = self.factory.get(add_url) request.user = self.admin # skip model if model doesn't have "has_add_permission" if not model_admin.has_add_permission(request): continue response = model_admin.add_view(request) fields = model_admin.get_fields(request) self.assertEqual(response.status_code, 200) self.assertEqual( response.context_data["adminform"].model_admin.get_fields(request), list(fields), ) # ensure all the fields in model_admin are valid for field in model_admin.get_fields(request): # as these fields are form field and not modelform fields if model_admin.model is models.WebhookEndpoint and field in ( "base_url", "enabled", "connect", ): continue model_admin.get_changelist_instance(request).get_ordering_field( field ) def test_get_search_fields(self): """ Ensure all fields in model_admin.get_search_fields exist on the model or the related model """ app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard changelist_view url and make a sample query to trigger search url = ( reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) + "?q=bar" ) # add the admin user to the mocked request request = self.factory.get(url) request.user = self.admin response = model_admin.changelist_view(request) search_fields = model_admin.get_changelist_instance( request ).search_fields self.assertEqual(response.status_code, 200) self.assertEqual( response.context_data["cl"].search_fields, search_fields, ) try: # ensure all the fields in search_fields are valid # need to force the returned queryset to get evaluated list(model.objects.select_related(*search_fields)) except FieldError as error: if "Non-relational field given in select_related" not in str(error): self.fail(error) # for models inheriting from StripeModelAdmin verify: if model.__name__ not in self.ignore_models: self.assertTrue("id" in search_fields) def test_get_actions(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard changelist_view url url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) # add the admin user to the mocked request request = self.factory.get(url) request.user = self.admin actions = model_admin.get_actions(request) # sub-classes of StripeModel if model.__name__ not in self.ignore_models: if model.__name__ in ("UsageRecordSummary", "LineItem"): assert "_resync_instances" not in actions assert "_sync_all_instances" in actions elif model.__name__ == "Subscription": assert "_resync_instances" in actions assert "_sync_all_instances" in actions assert "_cancel" in actions elif model.__name__ in ("Mandate", "UsageRecord"): assert "_resync_instances" in actions assert "_sync_all_instances" not in actions elif model.__name__ == "Discount": assert "_resync_instances" not in actions assert "_sync_all_instances" not in actions else: assert "_resync_instances" in actions assert "_sync_all_instances" in actions # not sub-classes of StripeModel else: if model.__name__ == "WebhookEndpoint": assert "delete_selected" not in actions assert "_resync_instances" in actions assert "_sync_all_instances" in actions else: assert "_resync_instances" not in actions assert "_sync_all_instances" not in actions class TestAdminRegisteredModelsNotChildrenOfStripeModel(TestCase): def setUp(self): self.admin = get_user_model().objects.create_superuser( username="admin", email="admin@djstripe.com", password="xxx" ) self.factory = RequestFactory() def test_get_list_display_links(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard changelist_view url url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) # add the admin user to the mocked request request = self.factory.get(url) request.user = self.admin response = model_admin.changelist_view(request) list_display = model_admin.get_changelist_instance(request).list_display # get_changelist_instance to get an instance of the ChangelistView for logged in admin user self.assertEqual(response.status_code, 200) self.assertEqual( list(response.context_data["cl"].list_display_links), list( model_admin.get_changelist_instance(request).list_display_links ), ) # ensure all the fields in list_display_links are valid for field in model_admin.get_list_display_links(request, list_display): model_admin.get_changelist_instance(request).get_ordering_field( field ) def test_get_list_display(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard changelist_view url url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) # add the admin user to the mocked request request = self.factory.get(url) request.user = self.admin response = model_admin.changelist_view(request) # get_changelist_instance to get an instance of the ChangelistView for logged in admin user list_display = model_admin.get_changelist_instance(request).list_display self.assertEqual(response.status_code, 200) self.assertEqual( list(response.context_data["cl"].list_display), list(list_display), ) # ensure all the fields in list_display are valid for field in model_admin.get_list_display(request): model_admin.get_changelist_instance(request).get_ordering_field( field ) self.assertTrue( all( [ 1 for i in [ "__str__", "id", "djstripe_owner_account", "created", "livemode", ] if i in list_display ] ) ) def test_get_list_filter(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard changelist_view url url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) # add the admin user to the mocked request request = self.factory.get(url) request.user = self.admin response = model_admin.changelist_view(request) # get_changelist_instance to get an instance of the ChangelistView for logged in admin user list_filter = model_admin.get_changelist_instance(request).list_filter self.assertEqual(response.status_code, 200) self.assertEqual( list(response.context_data["cl"].list_filter), list(list_filter), ) # ensure all the filters get formed correctly chl = model_admin.get_changelist_instance(request) chl.get_filters(request) chl.get_queryset(request) self.assertTrue( all([1 for i in ["created", "livemode"] if i in list_filter]) ) def test_get_readonly_fields(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard changelist_view url url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) # add the admin user to the mocked request request = self.factory.get(url) request.user = self.admin response = model_admin.changelist_view(request) # get_changelist_instance to get an instance of the ChangelistView for logged in admin user readonly_fields = model_admin.get_changelist_instance( request ).model_admin.readonly_fields self.assertEqual(response.status_code, 200) self.assertEqual( response.context_data["cl"].model_admin.readonly_fields, readonly_fields, ) # ensure all the fields in readonly_fields are valid for field in model_admin.get_readonly_fields(request): # ensure the given field is on model, or model_admin or modelform model_admin.get_changelist_instance(request).get_ordering_field( field ) self.assertTrue( all( [ 1 for i in ["created", "djstripe_owner_account", "id"] if i in readonly_fields ] ) ) def test_get_list_select_related(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard changelist_view url url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) # add the admin user to the mocked request request = self.factory.get(url) request.user = self.admin response = model_admin.changelist_view(request) # get_changelist_instance to get an instance of the ChangelistView for logged in admin user list_select_related = model_admin.get_changelist_instance( request ).list_select_related self.assertEqual(response.status_code, 200) self.assertEqual( response.context_data["cl"].list_select_related, list_select_related, ) # ensure all the fields in list_select_related are valid list_select_related_fields = model_admin.get_list_select_related( request ) if isinstance(list_select_related_fields, Sequence): # need to force the returned queryset to get evaluated list(model.objects.select_related(*list_select_related_fields)) # todo complete after djstripe has integrated ModelFactory # def test_get_fieldsets_change(self): # pass def test_get_fieldsets_add(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard add url add_url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_add" ) # add the admin user to the mocked request request = self.factory.get(add_url) request.user = self.admin # skip model if model doesn't have "has_add_permission" if not model_admin.has_add_permission(request): continue response = model_admin.add_view(request) fieldsets = model_admin.get_fieldsets(request) self.assertEqual(response.status_code, 200) self.assertEqual( response.context_data["adminform"].fieldsets, [*fieldsets], ) self.assertTrue( all( [ 1 for i in [ "created", "livemode", "djstripe_owner_account", "id", ] if i in fieldsets ] ) ) # todo complete after djstripe has integrated ModelFactory # def test_get_fields_change(self): # pass def test_get_fields_add(self): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard add url add_url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_add" ) # add the admin user to the mocked request request = self.factory.get(add_url) request.user = self.admin # skip model if model doesn't have "has_add_permission" if not model_admin.has_add_permission(request): continue response = model_admin.add_view(request) fields = model_admin.get_fields(request) self.assertEqual(response.status_code, 200) self.assertEqual( response.context_data["adminform"].model_admin.get_fields(request), list(fields), ) # ensure all the fields in model_admin are valid for field in model_admin.get_fields(request): # as these fields are form field and not modelform fields if model_admin.model is models.WebhookEndpoint and field in ( "base_url", "enabled", "connect", ): continue model_admin.get_changelist_instance(request).get_ordering_field( field ) def test_get_search_fields(self): """ Ensure all fields in model_admin.get_search_fields exist on the model or the related model """ app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard changelist_view url and make a sample query to trigger search url = ( reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) + "?q=bar" ) # add the admin user to the mocked request request = self.factory.get(url) request.user = self.admin response = model_admin.changelist_view(request) search_fields = model_admin.get_changelist_instance( request ).search_fields self.assertEqual(response.status_code, 200) self.assertEqual( response.context_data["cl"].search_fields, search_fields, ) try: # ensure all the fields in search_fields are valid # need to force the returned queryset to get evaluated list(model.objects.select_related(*search_fields)) except FieldError as error: if "Non-relational field given in select_related" not in str(error): self.fail(error) class TestAdminInlineModels(TestCase): def test_readonly_fields_exist(self): """ Ensure all fields in BaseModelAdmin.readonly_fields exist on the model """ for model, model_admin in site._registry.items(): for inline_admin in model_admin.inlines: fields = getattr(inline_admin, "readonly_fields", []) try: # need to force the returned queryset to get evaluated list(inline_admin.model.objects.select_related(*fields)) except FieldError as error: if "Non-relational field given in select_related" not in str(error): self.fail(error) class TestAdminSite(TestCase): def setUp(self): self.empty_value = "-empty-" def test_search_fields(self): """ Search for errors like this: Bad search field for Customer model. """ for _model, model_admin in site._registry.items(): model_name = model_admin.model.__name__ table_name = model_name.lower() for search_field in getattr(model_admin, "search_fields", []): self.assertFalse( search_field.startswith(f"{table_name}__"), f"Bad search field <{search_field}> for {model_name} model.", ) def test_search_fields_exist(self): """ Ensure all fields in model_admin.search_fields exist on the model or the related model """ for model, model_admin in site._registry.items(): fields = getattr(model_admin, "search_fields", []) try: # need to force the returned queryset to get evaluated list(model.objects.select_related(*fields)) except FieldError as error: if "Non-relational field given in select_related" not in str(error): self.fail(error) def test_list_select_related_fields_exist(self): """ Ensure all fields in model_admin.list_select_related exist on the model or the related model """ for model, model_admin in site._registry.items(): fields = getattr(model_admin, "list_select_related", False) if isinstance(fields, Sequence): try: # need to force the returned queryset to get evaluated list(model.objects.select_related(*fields)) except FieldError as error: self.fail(error) class TestCustomActionMixin: # the 4 models that do not inherit from StripeModel and hence # do not inherit from StripeModelAdmin ignore_models = [ "WebhookEventTrigger", "WebhookEndpoint", "IdempotencyKey", "APIKey", ] @pytest.mark.parametrize( "action_name", ["_sync_all_instances", "_resync_instances"] ) @pytest.mark.parametrize("djstripe_owner_account_exists", [False, True]) def test_get_admin_action_context( self, djstripe_owner_account_exists, action_name, monkeypatch ): # monkeypatch utils.get_model def mock_get_model(*args, **kwargs): return model monkeypatch.setattr(utils, "get_model", mock_get_model) model = CustomActionModel # create instance to be used in the Django Admin Action instance = model.objects.create(id="test") if djstripe_owner_account_exists: account_instance = Account.objects.first() instance.djstripe_owner_account = account_instance instance.save() queryset = model.objects.all() model_admin = site._registry.get(model) context = model_admin.get_admin_action_context( queryset, action_name, CustomActionForm ) assert context.get("queryset") == queryset assert context.get("action_name") == action_name assert context.get("model_name") == "customactionmodel" assert context.get("changelist_url") == "/admin/fields/customactionmodel/" assert context.get("ACTION_CHECKBOX_NAME") == helpers.ACTION_CHECKBOX_NAME if action_name == "_sync_all_instances": assert context.get("info") == [] assertQuerysetEqual( context.get("form").initial.get(helpers.ACTION_CHECKBOX_NAME), ["_sync_all_instances"], ) assert context.get("form").fields.get( helpers.ACTION_CHECKBOX_NAME ).choices == [("_sync_all_instances", "_sync_all_instances")] else: assert context.get("info") == [ f'Custom action model: <id=test>' ] assertQuerysetEqual( context.get("form").initial.get(helpers.ACTION_CHECKBOX_NAME), queryset.values_list("pk", flat=True), ) assert context.get("form").fields.get( helpers.ACTION_CHECKBOX_NAME ).choices == list( zip( queryset.values_list("pk", flat=True), queryset.values_list("pk", flat=True), ) ) def test_get_actions(self, admin_user): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys(): model_admin = site._registry.get(model) # get the standard changelist_view url url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) # add the admin user to the mocked request request = RequestFactory().get(url) request.user = admin_user actions = model_admin.get_actions(request) # sub-classes of StripeModel if model.__name__ not in self.ignore_models: if getattr(model.stripe_class, "retrieve", None): # assert "_resync_instances" action is present assert "_resync_instances" in actions else: # assert "_resync_instances" action is not present assert "_resync_instances" not in actions @pytest.mark.parametrize("fake_selected_pks", [None, [1, 2]]) def test_changelist_view(self, admin_client, fake_selected_pks): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys() and ( model.__name__ == "WebhookEndpoint" or model.__name__ not in self.ignore_models ): # get the standard changelist_view url change_url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) data = {"action": "_sync_all_instances"} if fake_selected_pks is not None: # add key helpers.ACTION_CHECKBOX_NAME when it is not None data[helpers.ACTION_CHECKBOX_NAME] = fake_selected_pks # get the response. This will invoke the changelist_view response = admin_client.post(change_url, data=data, follow=True) assert response.status_code == 200 @pytest.mark.parametrize("djstripe_owner_account_exists", [False, True]) def test__resync_instances( self, djstripe_owner_account_exists, admin_client, monkeypatch ): model = CustomActionModel model_admin = site._registry.get(model) # monkeypatch utils.get_model def mock_get_model(*args, **kwargs): return model # monkeypatch modeladmin.get_admin_action_context def mock_get_admin_action_context(*args, **kwargs): return { "action_name": "_resync_instances", "model_name": "customactionmodel", } monkeypatch.setattr( model_admin, "get_admin_action_context", mock_get_admin_action_context ) monkeypatch.setattr(utils, "get_model", mock_get_model) # create instance to be used in the Django Admin Action instance = model.objects.create(id="test") if djstripe_owner_account_exists: account_instance = Account.objects.first() instance.djstripe_owner_account = account_instance instance.save() data = { "action": "_resync_instances", helpers.ACTION_CHECKBOX_NAME: [instance.pk], } # get the standard changelist_view url change_url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) response = admin_client.post(change_url, data) # assert user got 200 status code assert response.status_code == 200 @pytest.mark.parametrize("fake_selected_pks", [None, [1, 2]]) def test__sync_all_instances(self, admin_client, fake_selected_pks): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if ( model in site._registry.keys() and model.__name__ not in ("Mandate", "UsageRecord", "Discount") and ( model.__name__ == "WebhookEndpoint" or model.__name__ not in self.ignore_models ) ): # get the standard changelist_view url change_url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) data = {"action": "_sync_all_instances"} if fake_selected_pks is not None: data[helpers.ACTION_CHECKBOX_NAME] = fake_selected_pks response = admin_client.post(change_url, data) # assert user got 200 status code assert response.status_code == 200 class TestSubscriptionAdminCustomAction: def test__cancel_subscription_instances( # noqa: C901 self, admin_client, monkeypatch, ): def mock_invoice_get(*args, **kwargs): return FAKE_INVOICE def mock_invoice_item_get(*args, **kwargs): return FAKE_INVOICEITEM def mock_customer_get(*args, **kwargs): return FAKE_CUSTOMER def mock_charge_get(*args, **kwargs): return FAKE_CHARGE def mock_payment_method_get(*args, **kwargs): return FAKE_CARD_AS_PAYMENT_METHOD def mock_payment_intent_get(*args, **kwargs): return FAKE_PAYMENT_INTENT_I def mock_subscription_get(*args, **kwargs): return FAKE_SUBSCRIPTION def mock_subscriptionitem_get(*args, **kwargs): return FAKE_SUBSCRIPTION_ITEM def mock_balance_transaction_get(*args, **kwargs): return FAKE_BALANCE_TRANSACTION def mock_product_get(*args, **kwargs): return FAKE_PRODUCT def mock_plan_get(*args, **kwargs): return FAKE_PLAN # monkeypatch stripe retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Invoice, "retrieve", mock_invoice_get) monkeypatch.setattr(stripe.InvoiceItem, "retrieve", mock_invoice_item_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr( stripe.BalanceTransaction, "retrieve", mock_balance_transaction_get ) monkeypatch.setattr(stripe.Subscription, "retrieve", mock_subscription_get) monkeypatch.setattr( stripe.SubscriptionItem, "retrieve", mock_subscriptionitem_get ) monkeypatch.setattr(stripe.Charge, "retrieve", mock_charge_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) monkeypatch.setattr(stripe.PaymentIntent, "retrieve", mock_payment_intent_get) monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Plan, "retrieve", mock_plan_get) # Create Latest Invoice models.Invoice.sync_from_stripe_data(FAKE_INVOICE) model = models.Subscription subscription_fake = deepcopy(FAKE_SUBSCRIPTION) instance = model.sync_from_stripe_data(subscription_fake) # get the standard changelist_view url change_url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) data = {"action": "_cancel", helpers.ACTION_CHECKBOX_NAME: [instance.pk]} response = admin_client.post(change_url, data) # assert user got 200 status code assert response.status_code == 200 class TestSubscriptionScheduleAdminCustomAction: def test__release_subscription_schedule( # noqa: C901 self, admin_client, monkeypatch, ): def mock_balance_transaction_get(*args, **kwargs): return FAKE_BALANCE_TRANSACTION def mock_subscription_get(*args, **kwargs): return FAKE_SUBSCRIPTION def mock_subscriptionitem_get(*args, **kwargs): return FAKE_SUBSCRIPTION_ITEM def mock_charge_get(*args, **kwargs): return FAKE_CHARGE def mock_payment_method_get(*args, **kwargs): return FAKE_CARD_AS_PAYMENT_METHOD def mock_payment_intent_get(*args, **kwargs): return FAKE_PAYMENT_INTENT_I def mock_product_get(*args, **kwargs): return FAKE_PRODUCT def mock_invoice_get(*args, **kwargs): return FAKE_INVOICE def mock_invoice_item_get(*args, **kwargs): return FAKE_INVOICEITEM def mock_customer_get(*args, **kwargs): return FAKE_CUSTOMER def mock_plan_get(*args, **kwargs): return FAKE_PLAN # monkeypatch stripe retrieve calls to return # the desired json response. monkeypatch.setattr( stripe.BalanceTransaction, "retrieve", mock_balance_transaction_get ) monkeypatch.setattr(stripe.Subscription, "retrieve", mock_subscription_get) monkeypatch.setattr( stripe.SubscriptionItem, "retrieve", mock_subscriptionitem_get ) monkeypatch.setattr(stripe.Charge, "retrieve", mock_charge_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) monkeypatch.setattr(stripe.PaymentIntent, "retrieve", mock_payment_intent_get) monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Invoice, "retrieve", mock_invoice_get) monkeypatch.setattr(stripe.InvoiceItem, "retrieve", mock_invoice_item_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr(stripe.Plan, "retrieve", mock_plan_get) # create latest invoice models.Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) model = models.SubscriptionSchedule subscription_schedule_fake = deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) instance = model.sync_from_stripe_data(subscription_schedule_fake) # get the standard changelist_view url change_url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) data = { "action": "_release_subscription_schedule", helpers.ACTION_CHECKBOX_NAME: [instance.pk], } response = admin_client.post(change_url, data) # assert user got 200 status code assert response.status_code == 200 def test__cancel_subscription_schedule( # noqa: C901 self, admin_client, monkeypatch, ): def mock_balance_transaction_get(*args, **kwargs): return FAKE_BALANCE_TRANSACTION def mock_subscription_get(*args, **kwargs): return FAKE_SUBSCRIPTION def mock_subscriptionitem_get(*args, **kwargs): return FAKE_SUBSCRIPTION_ITEM def mock_charge_get(*args, **kwargs): return FAKE_CHARGE def mock_payment_method_get(*args, **kwargs): return FAKE_CARD_AS_PAYMENT_METHOD def mock_payment_intent_get(*args, **kwargs): return FAKE_PAYMENT_INTENT_I def mock_product_get(*args, **kwargs): return FAKE_PRODUCT def mock_invoice_get(*args, **kwargs): return FAKE_INVOICE def mock_invoice_item_get(*args, **kwargs): return FAKE_INVOICEITEM def mock_customer_get(*args, **kwargs): return FAKE_CUSTOMER def mock_plan_get(*args, **kwargs): return FAKE_PLAN # monkeypatch stripe retrieve calls to return # the desired json response. monkeypatch.setattr( stripe.BalanceTransaction, "retrieve", mock_balance_transaction_get ) monkeypatch.setattr(stripe.Subscription, "retrieve", mock_subscription_get) monkeypatch.setattr( stripe.SubscriptionItem, "retrieve", mock_subscriptionitem_get ) monkeypatch.setattr(stripe.Charge, "retrieve", mock_charge_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) monkeypatch.setattr(stripe.PaymentIntent, "retrieve", mock_payment_intent_get) monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Invoice, "retrieve", mock_invoice_get) monkeypatch.setattr(stripe.InvoiceItem, "retrieve", mock_invoice_item_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr(stripe.Plan, "retrieve", mock_plan_get) # create latest invoice models.Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) model = models.SubscriptionSchedule subscription_schedule_fake = deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) instance = model.sync_from_stripe_data(subscription_schedule_fake) # get the standard changelist_view url change_url = reverse( f"admin:{model._meta.app_label}_{model.__name__.lower()}_changelist" ) data = { "action": "_cancel_subscription_schedule", helpers.ACTION_CHECKBOX_NAME: [instance.pk], } response = admin_client.post(change_url, data) # assert user got 200 status code assert response.status_code == 200 ================================================ FILE: tests/test_api_keys.py ================================================ from copy import deepcopy from unittest.mock import patch from django.conf import settings from django.test import TestCase from django.test.utils import override_settings from djstripe import models from djstripe.enums import APIKeyType from djstripe.settings import djstripe_settings from . import FAKE_ACCOUNT, FAKE_FILEUPLOAD_LOGO class TestCheckApiKeySettings(TestCase): @override_settings( STRIPE_LIVE_SECRET_KEY="sk_live_foo", STRIPE_LIVE_PUBLIC_KEY="sk_live_foo", STRIPE_LIVE_MODE=True, ) @patch("stripe.Account.retrieve", autospec=True) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_LOGO), autospec=True, ) def test_global_api_keys_live_mode( self, fileupload_retrieve_mock, account_retrieve_mock, ): fake_account = deepcopy(FAKE_ACCOUNT) fake_account["settings"]["branding"]["icon"] = None account_retrieve_mock.return_value = fake_account with patch.object( models.api, "get_api_key_details_by_prefix", return_value=(APIKeyType.secret, True), ): account = models.Account.sync_from_stripe_data( fake_account, api_key=djstripe_settings.STRIPE_SECRET_KEY ) self.assertEqual(account.default_api_key, "sk_live_foo") self.assertEqual(djstripe_settings.STRIPE_LIVE_MODE, True) self.assertEqual(djstripe_settings.STRIPE_SECRET_KEY, "sk_live_foo") self.assertEqual(djstripe_settings.LIVE_API_KEY, "sk_live_foo") @override_settings( STRIPE_TEST_SECRET_KEY="sk_test_foo", STRIPE_TEST_PUBLIC_KEY="pk_test_foo", STRIPE_LIVE_MODE=False, ) @patch("stripe.Account.retrieve", autospec=True) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_LOGO), autospec=True, ) def test_global_api_keys_test_mode( self, fileupload_retrieve_mock, account_retrieve_mock, ): fake_account = deepcopy(FAKE_ACCOUNT) fake_account["settings"]["branding"]["icon"] = None account_retrieve_mock.return_value = fake_account with patch.object( models.api, "get_api_key_details_by_prefix", return_value=(APIKeyType.secret, False), ): account = models.Account.sync_from_stripe_data( fake_account, api_key=djstripe_settings.STRIPE_SECRET_KEY ) self.assertEqual(account.default_api_key, "sk_test_foo") self.assertEqual(djstripe_settings.STRIPE_LIVE_MODE, False) self.assertEqual(djstripe_settings.STRIPE_SECRET_KEY, "sk_test_foo") self.assertEqual(djstripe_settings.TEST_API_KEY, "sk_test_foo") @override_settings( STRIPE_TEST_SECRET_KEY="sk_test_foo", STRIPE_LIVE_SECRET_KEY="sk_live_foo", STRIPE_TEST_PUBLIC_KEY="pk_test_foo", STRIPE_LIVE_PUBLIC_KEY="pk_live_foo", STRIPE_LIVE_MODE=True, ) @patch("stripe.Account.retrieve", autospec=True) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_LOGO), autospec=True, ) def test_api_key_live_mode( self, fileupload_retrieve_mock, account_retrieve_mock, ): fake_account = deepcopy(FAKE_ACCOUNT) fake_account["settings"]["branding"]["icon"] = None account_retrieve_mock.return_value = fake_account with patch.object( models.api, "get_api_key_details_by_prefix", return_value=(APIKeyType.secret, True), ): account = models.Account.sync_from_stripe_data( fake_account, api_key=djstripe_settings.STRIPE_SECRET_KEY ) self.assertEqual(account.default_api_key, "sk_live_foo") del settings.STRIPE_SECRET_KEY, settings.STRIPE_TEST_SECRET_KEY del settings.STRIPE_PUBLIC_KEY, settings.STRIPE_TEST_PUBLIC_KEY self.assertEqual(djstripe_settings.STRIPE_LIVE_MODE, True) self.assertEqual(djstripe_settings.STRIPE_SECRET_KEY, "sk_live_foo") self.assertEqual(djstripe_settings.STRIPE_PUBLIC_KEY, "pk_live_foo") @override_settings( STRIPE_TEST_SECRET_KEY="sk_test_foo", STRIPE_LIVE_SECRET_KEY="sk_live_foo", STRIPE_TEST_PUBLIC_KEY="pk_test_foo", STRIPE_LIVE_PUBLIC_KEY="pk_live_foo", STRIPE_LIVE_MODE=False, ) @patch("stripe.Account.retrieve", autospec=True) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_LOGO), autospec=True, ) def test_secret_key_test_mode( self, fileupload_retrieve_mock, account_retrieve_mock, ): fake_account = deepcopy(FAKE_ACCOUNT) fake_account["settings"]["branding"]["icon"] = None account_retrieve_mock.return_value = fake_account with patch.object( models.api, "get_api_key_details_by_prefix", return_value=(APIKeyType.secret, False), ): account = models.Account.sync_from_stripe_data( fake_account, api_key=djstripe_settings.STRIPE_SECRET_KEY ) self.assertEqual(account.default_api_key, "sk_test_foo") del settings.STRIPE_SECRET_KEY del settings.STRIPE_PUBLIC_KEY self.assertEqual(djstripe_settings.STRIPE_LIVE_MODE, False) self.assertEqual(djstripe_settings.STRIPE_SECRET_KEY, "sk_test_foo") self.assertEqual(djstripe_settings.STRIPE_PUBLIC_KEY, "pk_test_foo") self.assertEqual(djstripe_settings.TEST_API_KEY, "sk_test_foo") ================================================ FILE: tests/test_apikey.py ================================================ """ dj-stripe APIKey model tests """ from copy import deepcopy from unittest.mock import patch import pytest from django.test import TestCase from djstripe.admin.admin import APIKeyAdminCreateForm from djstripe.enums import APIKeyType from djstripe.exceptions import InvalidStripeAPIKey from djstripe.models import Account, APIKey from djstripe.models.api import get_api_key_details_by_prefix from . import FAKE_FILEUPLOAD_ICON, FAKE_FILEUPLOAD_LOGO, FAKE_PLATFORM_ACCOUNT # avoid literal api keys to prevent git secret scanners false-positives SK_TEST = "sk_test_" + "XXXXXXXXXXXXXXXXXXXX1234" SK_LIVE = "sk_live_" + "XXXXXXXXXXXXXXXXXXXX5678" RK_TEST = "rk_test_" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX9876" RK_LIVE = "rk_live_" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX5432" PK_TEST = "pk_test_" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXAAAA" PK_LIVE = "pk_live_" + "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXBBBB" pytestmark = pytest.mark.django_db def test_get_api_key_details_by_prefix(): assert get_api_key_details_by_prefix(SK_TEST) == (APIKeyType.secret, False) assert get_api_key_details_by_prefix(SK_LIVE) == (APIKeyType.secret, True) assert get_api_key_details_by_prefix(RK_TEST) == (APIKeyType.restricted, False) assert get_api_key_details_by_prefix(RK_LIVE) == (APIKeyType.restricted, True) assert get_api_key_details_by_prefix(PK_TEST) == (APIKeyType.publishable, False) assert get_api_key_details_by_prefix(PK_LIVE) == (APIKeyType.publishable, True) def test_get_api_key_details_by_prefix_bad_values(): with pytest.raises(InvalidStripeAPIKey): get_api_key_details_by_prefix("pk_a") with pytest.raises(InvalidStripeAPIKey): get_api_key_details_by_prefix("sk_a") with pytest.raises(InvalidStripeAPIKey): get_api_key_details_by_prefix("rk_nope_1234") def test_clean_public_apikey(): key = APIKey(type=APIKeyType.publishable, livemode=False, secret=PK_TEST) assert not key.djstripe_owner_account key.clean() assert not key.djstripe_owner_account @patch("stripe.Account.retrieve", return_value=deepcopy(FAKE_PLATFORM_ACCOUNT)) @patch("stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_ICON)) def test_apikey_detect_livemode_and_type( fileupload_retrieve_mock, account_retrieve_mock ): keys_and_values = ( (PK_TEST, False, APIKeyType.publishable), (RK_TEST, False, APIKeyType.restricted), (SK_TEST, False, APIKeyType.secret), (PK_LIVE, True, APIKeyType.publishable), (RK_LIVE, True, APIKeyType.restricted), (SK_LIVE, True, APIKeyType.secret), ) for secret, livemode, type in keys_and_values: # need to use ModelAdmin Form to create the APIKey instance form = APIKeyAdminCreateForm( data={"secret": secret}, ) form.save() key = form.instance assert key.livemode is livemode assert key.type is type class APIKeyTest(TestCase): def setUp(self): # create a Stripe Platform Account self.account = FAKE_PLATFORM_ACCOUNT.create() self.apikey_test = APIKey.objects.create( type=APIKeyType.secret, name="Test Secret Key", secret=SK_TEST, livemode=False, djstripe_owner_account=self.account, ) self.apikey_live = APIKey.objects.create( type=APIKeyType.secret, name="Live Secret Key", secret=SK_LIVE, livemode=True, djstripe_owner_account=self.account, ) def test_get_stripe_dashboard_url(self): self.assertEqual( self.apikey_test.get_stripe_dashboard_url(), "https://dashboard.stripe.com/acct_1Fg9jUA3kq9o1aTc/test/apikeys", ) self.assertEqual( self.apikey_live.get_stripe_dashboard_url(), "https://dashboard.stripe.com/acct_1Fg9jUA3kq9o1aTc/apikeys", ) def test___str__(self): assert str(self.apikey_live) == "Live Secret Key" assert str(self.apikey_test) == "Test Secret Key" # update name of apikey_live to "" self.apikey_live.name = "" self.apikey_live.save() assert str(self.apikey_live) == "sk_live_...5678" def test_secret_redacted(self): self.assertEqual(self.apikey_test.secret_redacted, "sk_test_...1234") self.assertEqual(self.apikey_live.secret_redacted, "sk_live_...5678") def test_secret_not_in_str(self): assert self.apikey_test.secret not in str(self.apikey_test) assert self.apikey_live.secret not in str(self.apikey_live) def test_get_account_by_api_key(self): account = Account.get_or_retrieve_for_api_key(self.apikey_test.secret) assert account == self.account @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_PLATFORM_ACCOUNT), autospec=True, ) @patch( "stripe.File.retrieve", side_effect=[deepcopy(FAKE_FILEUPLOAD_ICON), deepcopy(FAKE_FILEUPLOAD_LOGO)], autospec=True, ) def test_refresh_account(self, fileupload_retrieve_mock, account_retrieve_mock): # remove djstripe_owner_account field self.apikey_test.djstripe_owner_account = None self.apikey_test.save() # invoke refresh_Account() self.apikey_test.refresh_account() assert self.apikey_test.djstripe_owner_account.id == FAKE_PLATFORM_ACCOUNT["id"] ================================================ FILE: tests/test_balance_transaction.py ================================================ """ dj-stripe BalanceTransaction model tests """ from copy import deepcopy from unittest.mock import patch import pytest from django.test.testcases import TestCase from djstripe import models from djstripe.enums import BalanceTransactionStatus from . import ( FAKE_BALANCE_TRANSACTION, FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CHARGE, FAKE_CUSTOMER, FAKE_INVOICE, FAKE_INVOICEITEM, FAKE_PAYMENT_INTENT_I, FAKE_PLAN, FAKE_PRODUCT, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_ITEM, ) pytestmark = pytest.mark.django_db class TestBalanceTransactionStr: @pytest.mark.parametrize("transaction_status", BalanceTransactionStatus.__members__) def test___str__(self, transaction_status): modified_balance_transaction = deepcopy(FAKE_BALANCE_TRANSACTION) modified_balance_transaction["status"] = transaction_status balance_transaction = models.BalanceTransaction.sync_from_stripe_data( modified_balance_transaction ) assert ( str(balance_transaction) == f"$20.00 USD ({BalanceTransactionStatus.humanize(modified_balance_transaction['status'])})" ) class TestBalanceTransactionSourceClass: @pytest.mark.parametrize("transaction_type", ["card", "payout", "refund"]) def test_get_source_class_success(self, transaction_type): modified_balance_transaction = deepcopy(FAKE_BALANCE_TRANSACTION) modified_balance_transaction["type"] = transaction_type balance_transaction = models.BalanceTransaction.sync_from_stripe_data( modified_balance_transaction ) assert balance_transaction.get_source_class() is getattr( models, transaction_type.capitalize(), None ) @pytest.mark.parametrize("transaction_type", ["network_cost", "payment_refund"]) def test_get_source_class_failure(self, transaction_type): modified_balance_transaction = deepcopy(FAKE_BALANCE_TRANSACTION) modified_balance_transaction["type"] = transaction_type balance_transaction = models.BalanceTransaction.sync_from_stripe_data( modified_balance_transaction ) with pytest.raises(LookupError): balance_transaction.get_source_class() class TestBalanceTransaction(TestCase): @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", autospec=True, return_value=deepcopy(FAKE_PAYMENT_INTENT_I), ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Charge.retrieve", autospec=True, return_value=deepcopy(FAKE_CHARGE), ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) def test_sync_from_stripe_data( self, subscription_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, charge_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, balance_transaction_retrieve_mock, customer_retrieve_mock, invoice_retrieve_mock, ): balance_transaction = models.BalanceTransaction.sync_from_stripe_data( deepcopy(FAKE_BALANCE_TRANSACTION) ) balance_transaction_retrieve_mock.assert_not_called() assert balance_transaction.type == FAKE_BALANCE_TRANSACTION["type"] assert balance_transaction.amount == FAKE_BALANCE_TRANSACTION["amount"] assert balance_transaction.status == FAKE_BALANCE_TRANSACTION["status"] @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", autospec=True, return_value=deepcopy(FAKE_PAYMENT_INTENT_I), ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Charge.retrieve", autospec=True, return_value=deepcopy(FAKE_CHARGE), ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", autospec=True, return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) def test_get_source_instance( self, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, charge_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, balance_transaction_retrieve_mock, customer_retrieve_mock, invoice_retrieve_mock, invoiceitem_retrieve_mock, ): balance_transaction = models.BalanceTransaction.sync_from_stripe_data( deepcopy(FAKE_BALANCE_TRANSACTION) ) charge = models.Charge.sync_from_stripe_data(deepcopy(FAKE_CHARGE)) assert balance_transaction.get_source_instance() == charge @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", autospec=True, return_value=deepcopy(FAKE_PAYMENT_INTENT_I), ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Charge.retrieve", autospec=True, return_value=deepcopy(FAKE_CHARGE), ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", autospec=True, return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) def test_get_stripe_dashboard_url( self, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, charge_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, balance_transaction_retrieve_mock, customer_retrieve_mock, invoice_retrieve_mock, invoiceitem_retrieve_mock, ): balance_transaction = models.BalanceTransaction.sync_from_stripe_data( deepcopy(FAKE_BALANCE_TRANSACTION) ) charge = models.Charge.sync_from_stripe_data(deepcopy(FAKE_CHARGE)) assert ( balance_transaction.get_stripe_dashboard_url() == charge.get_stripe_dashboard_url() ) ================================================ FILE: tests/test_bank_account.py ================================================ """ dj-stripe Bank Account Model Tests. """ from copy import deepcopy from unittest.mock import patch import pytest import stripe from django.contrib.auth import get_user_model from django.test import TestCase from djstripe import enums from djstripe.exceptions import StripeObjectManipulationException from djstripe.models import BankAccount, Customer from . import ( FAKE_BANK_ACCOUNT_IV, FAKE_BANK_ACCOUNT_SOURCE, FAKE_CUSTOM_ACCOUNT, FAKE_CUSTOMER_IV, FAKE_STANDARD_ACCOUNT, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class TestStrBankAccount: @pytest.mark.parametrize( "fake_stripe_data, has_account, has_customer", [ (deepcopy(FAKE_BANK_ACCOUNT_IV), True, False), (deepcopy(FAKE_BANK_ACCOUNT_SOURCE), False, True), (deepcopy(FAKE_BANK_ACCOUNT_IV), False, False), ], ) def test__str__(self, fake_stripe_data, has_account, has_customer, monkeypatch): def mock_customer_get(*args, **kwargs): data = deepcopy(FAKE_CUSTOMER_IV) data["default_source"] = None data["sources"] = [] return data def mock_account_get(*args, **kwargs): return deepcopy(FAKE_CUSTOM_ACCOUNT) # monkeypatch stripe.Account.retrieve and stripe.Customer.retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Account, "retrieve", mock_account_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) bankaccount = BankAccount.sync_from_stripe_data(fake_stripe_data) default = False if has_account: default = fake_stripe_data["default_for_currency"] assert ( f"{fake_stripe_data['bank_name']} {fake_stripe_data['currency']} {'Default' if default else ''} {fake_stripe_data['routing_number']} {fake_stripe_data['last4']}" == str(bankaccount) ) if has_customer: customer = Customer.objects.filter(id=fake_stripe_data["customer"]).first() default_source = customer.default_source default_payment_method = customer.default_payment_method if ( default_payment_method and fake_stripe_data["id"] == default_payment_method.id ) or (default_source and fake_stripe_data["id"] == default_source.id): # current bankaccount is the default payment method or source default = True assert ( f"{fake_stripe_data['bank_name']} {fake_stripe_data['routing_number']} ({bankaccount.human_readable_status}) {'Default' if default else ''} {fake_stripe_data['currency']}" == str(bankaccount) ) if not has_account and not has_customer: # ensure account and customer do not exist fake_stripe_data_2 = deepcopy(fake_stripe_data) fake_stripe_data_2["account"] = None fake_stripe_data_2["customer"] = None bankaccount = BankAccount.sync_from_stripe_data(fake_stripe_data_2) default = fake_stripe_data_2["default_for_currency"] assert ( f"{fake_stripe_data_2['bank_name']} {fake_stripe_data_2['currency']} {'Default' if default else ''} {fake_stripe_data_2['routing_number']} {fake_stripe_data_2['last4']}" == str(bankaccount) ) @pytest.mark.parametrize( "fake_stripe_data", [ deepcopy(FAKE_BANK_ACCOUNT_IV), deepcopy(FAKE_BANK_ACCOUNT_SOURCE), ], ) def test_human_readable_status(self, fake_stripe_data, monkeypatch): def mock_customer_get(*args, **kwargs): data = deepcopy(FAKE_CUSTOMER_IV) data["default_source"] = None data["sources"] = [] return data def mock_account_get(*args, **kwargs): return deepcopy(FAKE_CUSTOM_ACCOUNT) # monkeypatch stripe.Account.retrieve and stripe.Customer.retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Account, "retrieve", mock_account_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) bankaccount = BankAccount.sync_from_stripe_data(fake_stripe_data) if fake_stripe_data["status"] == "new": assert bankaccount.human_readable_status == "Pending Verification" else: assert ( bankaccount.human_readable_status == enums.BankAccountStatus.humanize(fake_stripe_data["status"]) ) class BankAccountTest(AssertStripeFksMixin, TestCase): def setUp(self): # create a Standard Stripe Account self.standard_account = FAKE_STANDARD_ACCOUNT.create() # create a Custom Stripe Account self.custom_account = FAKE_CUSTOM_ACCOUNT.create() user = get_user_model().objects.create_user( username="testuser", email="djstripe@example.com" ) fake_empty_customer = deepcopy(FAKE_CUSTOMER_IV) fake_empty_customer["default_source"] = None fake_empty_customer["sources"] = [] self.customer = fake_empty_customer.create_for_user(user) def test_attach_objects_hook_without_customer(self): FAKE_BANK_ACCOUNT_DICT = deepcopy(FAKE_BANK_ACCOUNT_SOURCE) FAKE_BANK_ACCOUNT_DICT["customer"] = None bank_account = BankAccount.sync_from_stripe_data(FAKE_BANK_ACCOUNT_DICT) self.assertEqual(bank_account.customer, None) def test_attach_objects_hook_without_account(self): bank_account = BankAccount.sync_from_stripe_data(FAKE_BANK_ACCOUNT_SOURCE) self.assertEqual(bank_account.account, None) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_IV), autospec=True, ) @patch( "stripe.Account.retrieve_external_account", return_value=deepcopy(FAKE_BANK_ACCOUNT_IV), autospec=True, ) def test_api_retrieve_by_customer_equals_retrieval_by_account( self, account_retrieve_external_account_mock, customer_retrieve_mock ): # deepcopy the BankAccount object FAKE_BANK_ACCOUNT_DICT = deepcopy(FAKE_BANK_ACCOUNT_IV) bankaccount = BankAccount.sync_from_stripe_data(FAKE_BANK_ACCOUNT_DICT) bankaccount_by_customer = bankaccount.api_retrieve() # Add account FAKE_BANK_ACCOUNT_DICT["account"] = FAKE_CUSTOM_ACCOUNT["id"] FAKE_BANK_ACCOUNT_DICT["customer"] = None bankaccount = BankAccount.sync_from_stripe_data(FAKE_BANK_ACCOUNT_DICT) bankaccount_by_account = bankaccount.api_retrieve() # assert the same bankaccount object gets retrieved self.assertCountEqual(bankaccount_by_customer, bankaccount_by_account) def test_create_bank_account_finds_customer_with_account_absent(self): bank_account = BankAccount.sync_from_stripe_data(FAKE_BANK_ACCOUNT_SOURCE) self.assertEqual(self.customer, bank_account.customer) self.assertEqual( bank_account.get_stripe_dashboard_url(), self.customer.get_stripe_dashboard_url(), ) self.assert_fks( bank_account, expected_blank_fks={ "djstripe.BankAccount.account", "djstripe.Customer.default_payment_method", "djstripe.Customer.default_source", "djstripe.Customer.coupon", }, ) def test_create_bank_account_finds_customer_with_account_present(self): FAKE_BANK_ACCOUNT_DICT = deepcopy(FAKE_BANK_ACCOUNT_SOURCE) FAKE_BANK_ACCOUNT_DICT["account"] = self.standard_account.id bank_account = BankAccount.sync_from_stripe_data(FAKE_BANK_ACCOUNT_DICT) self.assertEqual(self.customer, bank_account.customer) self.assertEqual(self.standard_account, bank_account.account) self.assertEqual( bank_account.get_stripe_dashboard_url(), self.customer.get_stripe_dashboard_url(), ) self.assert_fks( bank_account, expected_blank_fks={ "djstripe.Customer.default_payment_method", "djstripe.Customer.default_source", "djstripe.Customer.coupon", }, ) def test_create_bank_account_finds_account_with_customer_absent(self): FAKE_BANK_ACCOUNT_DICT = deepcopy(FAKE_BANK_ACCOUNT_SOURCE) FAKE_BANK_ACCOUNT_DICT["account"] = self.standard_account.id FAKE_BANK_ACCOUNT_DICT["customer"] = None bank_account = BankAccount.sync_from_stripe_data(FAKE_BANK_ACCOUNT_DICT) self.assertEqual(self.standard_account, bank_account.account) self.assertEqual( bank_account.get_stripe_dashboard_url(), f"https://dashboard.stripe.com/{bank_account.account.id}/settings/payouts", ) self.assert_fks( bank_account, expected_blank_fks={ "djstripe.BankAccount.customer", "djstripe.Customer.default_payment_method", "djstripe.Customer.default_source", "djstripe.Customer.coupon", }, ) def test_api_call_no_customer_and_no_account(self): exception_message = ( "BankAccount objects must be manipulated through either a Stripe Connected Account or a customer. " "Pass a Customer or an Account object into this call." ) with self.assertRaisesMessage( StripeObjectManipulationException, exception_message ): BankAccount._api_create() with self.assertRaisesMessage( StripeObjectManipulationException, exception_message ): BankAccount.api_list() def test_api_call_bad_customer(self): exception_message = ( "BankAccount objects must be manipulated through a Customer. " "Pass a Customer object into this call." ) with self.assertRaisesMessage( StripeObjectManipulationException, exception_message ): BankAccount._api_create(customer="fish") with self.assertRaisesMessage( StripeObjectManipulationException, exception_message ): BankAccount.api_list(customer="fish") def test_api_call_bad_account(self): exception_message = ( "BankAccount objects must be manipulated through a Stripe Connected Account. " "Pass an Account object into this call." ) with self.assertRaisesMessage( StripeObjectManipulationException, exception_message ): BankAccount._api_create(account="fish") with self.assertRaisesMessage( StripeObjectManipulationException, exception_message ): BankAccount.api_list(account="fish") @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_IV), autospec=True, ) def test__api_create_with_account_absent(self, customer_retrieve_mock): stripe_bank_account = BankAccount._api_create( customer=self.customer, source=FAKE_BANK_ACCOUNT_SOURCE["id"] ) self.assertEqual(FAKE_BANK_ACCOUNT_SOURCE, stripe_bank_account) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_IV), autospec=True, ) @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_CUSTOM_ACCOUNT), autospec=True, ) def test__api_create_with_customer_and_account( self, account_retrieve_mock, customer_retrieve_mock ): FAKE_BANK_ACCOUNT_DICT = deepcopy(FAKE_BANK_ACCOUNT_SOURCE) FAKE_BANK_ACCOUNT_DICT["account"] = FAKE_CUSTOM_ACCOUNT["id"] stripe_bank_account = BankAccount._api_create( account=self.custom_account, customer=self.customer, source=FAKE_BANK_ACCOUNT_DICT["id"], ) self.assertEqual(FAKE_BANK_ACCOUNT_SOURCE, stripe_bank_account) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_IV), autospec=True, ) @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_CUSTOM_ACCOUNT), autospec=True, ) def test__api_create_with_customer_absent( self, account_retrieve_mock, customer_retrieve_mock ): stripe_bank_account = BankAccount._api_create( account=self.custom_account, source=FAKE_BANK_ACCOUNT_IV["id"] ) self.assertEqual(FAKE_BANK_ACCOUNT_IV, stripe_bank_account) @patch( "stripe.Customer.delete_source", autospec=True, ) @patch( "stripe.BankAccount.retrieve", return_value=deepcopy(FAKE_BANK_ACCOUNT_SOURCE), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_IV), autospec=True, ) @patch( "stripe.Customer.retrieve_source", return_value=deepcopy(FAKE_BANK_ACCOUNT_SOURCE), autospec=True, ) def test_remove_bankaccount_by_customer( self, customer_retrieve_source_mock, customer_retrieve_mock, bank_account_retrieve_mock, bank_account_delete_mock, ): stripe_bank_account = BankAccount._api_create( customer=self.customer, source=FAKE_BANK_ACCOUNT_SOURCE["id"] ) BankAccount.sync_from_stripe_data(stripe_bank_account) self.assertEqual( 1, BankAccount.objects.filter(id=stripe_bank_account["id"]).count() ) bank_account = self.customer.bank_account.all()[0] bank_account.remove() self.assertEqual( 0, BankAccount.objects.filter(id=stripe_bank_account["id"]).count() ) api_key = bank_account.default_api_key stripe_account = bank_account._get_stripe_account_id(api_key) bank_account_delete_mock.assert_called_once_with( self.customer.id, bank_account.id, api_key=api_key, stripe_account=stripe_account, ) @patch( "stripe.Account.delete_external_account", autospec=True, ) @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_CUSTOM_ACCOUNT), autospec=True, ) def test_remove_bankaccount_by_account( self, account_retrieve_mock, bank_account_delete_mock, ): stripe_bank_account = BankAccount._api_create( account=self.custom_account, source=FAKE_BANK_ACCOUNT_IV["id"] ) bank_account = BankAccount.sync_from_stripe_data(stripe_bank_account) self.assertEqual( 1, BankAccount.objects.filter(id=stripe_bank_account["id"]).count() ) api_key = bank_account.default_api_key stripe_account = bank_account._get_stripe_account_id(api_key) assert bank_account.customer is None assert bank_account.account is not None # remove BankAccount bank_account.remove() bank_account_delete_mock.assert_called_once_with( self.custom_account.id, bank_account.id, api_key=api_key, stripe_account=stripe_account, ) self.assertEqual( 0, BankAccount.objects.filter(id=stripe_bank_account["id"]).count() ) @patch( "stripe.Account.delete_external_account", autospec=True, ) @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_CUSTOM_ACCOUNT), autospec=True, ) def test_remove_already_deleted_bankaccount_by_account( self, account_retrieve_mock, bank_account_delete_mock, ): stripe_bank_account = BankAccount._api_create( account=self.custom_account, source=FAKE_BANK_ACCOUNT_IV["id"] ) bank_account = BankAccount.sync_from_stripe_data(stripe_bank_account) self.assertEqual( 1, BankAccount.objects.filter(id=stripe_bank_account["id"]).count() ) api_key = bank_account.default_api_key stripe_account = bank_account._get_stripe_account_id(api_key) assert bank_account.customer is None assert bank_account.account is not None # remove BankAccount bank_account.remove() self.assertEqual( 0, BankAccount.objects.filter(id=stripe_bank_account["id"]).count() ) bank_account_delete_mock.assert_called_once_with( self.custom_account.id, bank_account.id, api_key=api_key, stripe_account=stripe_account, ) # remove BankAccount again count, _ = BankAccount.objects.filter(id=stripe_bank_account["id"]).delete() self.assertEqual(0, count) @patch( "stripe.Customer.delete_source", autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_IV), autospec=True, ) @patch( "stripe.Customer.retrieve_source", return_value=deepcopy(FAKE_BANK_ACCOUNT_SOURCE), autospec=True, ) def test_remove_already_deleted_bank_account( self, customer_retrieve_source_mock, customer_retrieve_mock, bank_account_delete_mock, ): stripe_bank_account = BankAccount._api_create( customer=self.customer, source=FAKE_BANK_ACCOUNT_SOURCE["id"] ) BankAccount.sync_from_stripe_data(stripe_bank_account) self.assertEqual(self.customer.bank_account.count(), 1) bank_account_object = self.customer.bank_account.first() BankAccount.objects.filter(id=stripe_bank_account["id"]).delete() self.assertEqual(self.customer.bank_account.count(), 0) bank_account_object.remove() self.assertEqual(self.customer.bank_account.count(), 0) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_IV), autospec=True, ) def test_api_list(self, customer_retrieve_mock): bank_account_list = BankAccount.api_list(customer=self.customer) self.assertCountEqual( [FAKE_BANK_ACCOUNT_SOURCE], [i for i in bank_account_list] ) ================================================ FILE: tests/test_card.py ================================================ """ dj-stripe Card Model Tests. """ from copy import deepcopy from unittest.mock import ANY, patch import pytest import stripe from django.contrib.auth import get_user_model from django.test import TestCase from stripe.error import InvalidRequestError from djstripe import enums from djstripe.exceptions import StripeObjectManipulationException from djstripe.models import Account, Card, Customer from . import ( FAKE_CARD, FAKE_CARD_III, FAKE_CARD_IV, FAKE_CUSTOM_ACCOUNT, FAKE_CUSTOMER, FAKE_STANDARD_ACCOUNT, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class TestStrCard: @pytest.mark.parametrize( "fake_stripe_data, has_account, has_customer", [ (deepcopy(FAKE_CARD), False, True), (deepcopy(FAKE_CARD_IV), True, False), ], ) def test__str__(self, fake_stripe_data, has_account, has_customer, monkeypatch): def mock_customer_get(*args, **kwargs): data = deepcopy(FAKE_CUSTOMER) data["default_source"] = None data["sources"] = [] return data def mock_account_get(*args, **kwargs): return deepcopy(FAKE_CUSTOM_ACCOUNT) # monkeypatch stripe.Account.retrieve and stripe.Customer.retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Account, "retrieve", mock_account_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) card = Card.sync_from_stripe_data(fake_stripe_data) default = False if has_account: account = Account.objects.filter(id=fake_stripe_data["account"]).first() default = fake_stripe_data["default_for_currency"] assert ( f"{enums.CardBrand.humanize(fake_stripe_data['brand'])} {account.default_currency} {'Default' if default else ''} {fake_stripe_data['last4']}" == str(card) ) if has_customer: customer = Customer.objects.filter(id=fake_stripe_data["customer"]).first() default_source = customer.default_source default_payment_method = customer.default_payment_method if ( default_payment_method and fake_stripe_data["id"] == default_payment_method.id ) or (default_source and fake_stripe_data["id"] == default_source.id): # current card is the default payment method or source default = True assert ( f"{enums.CardBrand.humanize(fake_stripe_data['brand'])} {fake_stripe_data['last4']} {'Default' if default else ''} Expires {fake_stripe_data['exp_month']} {fake_stripe_data['exp_year']}" == str(card) ) class CardTest(AssertStripeFksMixin, TestCase): def setUp(self): # create a Standard Stripe Account self.standard_account = FAKE_STANDARD_ACCOUNT.create() # create a Custom Stripe Account self.custom_account = FAKE_CUSTOM_ACCOUNT.create() user = get_user_model().objects.create_user( username="testuser", email="djstripe@example.com" ) fake_empty_customer = deepcopy(FAKE_CUSTOMER) fake_empty_customer["default_source"] = None fake_empty_customer["sources"] = [] self.customer = fake_empty_customer.create_for_user(user) def test_attach_objects_hook_without_customer(self): FAKE_CARD_DICT = deepcopy(FAKE_CARD) FAKE_CARD_DICT["customer"] = None card = Card.sync_from_stripe_data(FAKE_CARD_DICT) self.assertEqual(card.customer, None) def test_attach_objects_hook_without_account(self): card = Card.sync_from_stripe_data(FAKE_CARD) self.assertEqual(card.account, None) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Account.retrieve_external_account", return_value=deepcopy(FAKE_CARD), autospec=True, ) @patch( "stripe.Customer.retrieve_source", return_value=deepcopy(FAKE_CARD), autospec=True, ) def test_api_retrieve_by_customer_equals_retrieval_by_account( self, customer_retrieve_source_mock, account_retrieve_external_account_mock, customer_retrieve_mock, ): # deepcopy the CardDict object FAKE_CARD_DICT = deepcopy(FAKE_CARD) card = Card.sync_from_stripe_data(deepcopy(FAKE_CARD_DICT)) card_by_customer = card.api_retrieve() # Add account FAKE_CARD_DICT["account"] = FAKE_CUSTOM_ACCOUNT["id"] FAKE_CARD_DICT["customer"] = None card = Card.sync_from_stripe_data(FAKE_CARD_DICT) card_by_account = card.api_retrieve() # assert the same card object gets retrieved self.assertCountEqual(card_by_customer, card_by_account) def test_create_card_finds_customer_with_account_absent(self): card = Card.sync_from_stripe_data(FAKE_CARD) self.assertEqual(self.customer, card.customer) self.assertEqual( card.get_stripe_dashboard_url(), self.customer.get_stripe_dashboard_url() ) self.assert_fks( card, expected_blank_fks={ "djstripe.Card.account", "djstripe.BankAccount.account", "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.default_source", }, ) def test_create_card_finds_customer_with_account_present(self): # deepcopy the CardDict object FAKE_CARD_DICT = deepcopy(FAKE_CARD) # Add account FAKE_CARD_DICT["account"] = self.standard_account.id card = Card.sync_from_stripe_data(FAKE_CARD_DICT) self.assertEqual(self.customer, card.customer) self.assertEqual(self.standard_account, card.account) self.assertEqual( card.get_stripe_dashboard_url(), self.customer.get_stripe_dashboard_url(), ) self.assert_fks( card, expected_blank_fks={ "djstripe.BankAccount.account", "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.default_source", }, ) def test_create_card_finds_account_with_customer_absent(self): # deepcopy the CardDict object FAKE_CARD_DICT = deepcopy(FAKE_CARD) # Add account and remove customer FAKE_CARD_DICT["account"] = self.standard_account.id FAKE_CARD_DICT["customer"] = None card = Card.sync_from_stripe_data(FAKE_CARD_DICT) self.assertEqual(self.standard_account, card.account) self.assertEqual( card.get_stripe_dashboard_url(), f"https://dashboard.stripe.com/{card.account.id}/settings/payouts", ) self.assert_fks( card, expected_blank_fks={ "djstripe.Card.customer", "djstripe.BankAccount.account", "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.default_source", }, ) @patch("stripe.Token.create", autospec=True) def test_card_create_token(self, token_create_mock): card = {"number": "4242", "exp_month": 5, "exp_year": 2012, "cvc": 445} Card.create_token(**card) token_create_mock.assert_called_with(api_key=ANY, card=card) def test_api_call_no_customer_and_no_account(self): exception_message = ( "Card objects must be manipulated through either a Stripe Connected Account or a customer. " "Pass a Customer or an Account object into this call." ) with self.assertRaisesMessage( StripeObjectManipulationException, exception_message ): Card._api_create() with self.assertRaisesMessage( StripeObjectManipulationException, exception_message ): Card.api_list() def test_api_call_bad_customer(self): exception_message = ( "Card objects must be manipulated through a Customer. " "Pass a Customer object into this call." ) with self.assertRaisesMessage( StripeObjectManipulationException, exception_message ): Card._api_create(customer="fish") with self.assertRaisesMessage( StripeObjectManipulationException, exception_message ): Card.api_list(customer="fish") def test_api_call_bad_account(self): exception_message = ( "Card objects must be manipulated through a Stripe Connected Account. " "Pass an Account object into this call." ) with self.assertRaisesMessage( StripeObjectManipulationException, exception_message ): Card._api_create(account="fish") with self.assertRaisesMessage( StripeObjectManipulationException, exception_message ): Card.api_list(account="fish") @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test__api_create_with_account_absent(self, customer_retrieve_mock): stripe_card = Card._api_create(customer=self.customer, source=FAKE_CARD["id"]) self.assertEqual(FAKE_CARD, stripe_card) @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_CUSTOM_ACCOUNT), autospec=True, ) def test__api_create_with_customer_absent(self, account_retrieve_mock): stripe_card = Card._api_create( account=self.custom_account, source=FAKE_CARD_IV["id"] ) self.assertEqual(FAKE_CARD_IV, stripe_card) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_CUSTOM_ACCOUNT), autospec=True, ) def test__api_create_with_customer_and_account( self, account_retrieve_mock, customer_retrieve_mock ): FAKE_CARD_DICT = deepcopy(FAKE_CARD) FAKE_CARD_DICT["account"] = FAKE_CUSTOM_ACCOUNT["id"] stripe_card = Card._api_create( account=self.custom_account, customer=self.customer, source=FAKE_CARD_DICT["id"], ) self.assertEqual(FAKE_CARD, stripe_card) @patch( "stripe.Customer.delete_source", autospec=True, ) @patch("stripe.Card.retrieve", return_value=deepcopy(FAKE_CARD), autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Customer.retrieve_source", return_value=deepcopy(FAKE_CARD), autospec=True, ) def test_remove_card_by_customer( self, customer_retrieve_source_mock, customer_retrieve_mock, card_retrieve_mock, card_delete_mock, ): stripe_card = Card._api_create(customer=self.customer, source=FAKE_CARD["id"]) Card.sync_from_stripe_data(stripe_card) self.assertEqual(1, self.customer.legacy_cards.count()) # remove card card = self.customer.legacy_cards.all()[0] card.remove() self.assertEqual(0, self.customer.legacy_cards.count()) api_key = card.default_api_key stripe_account = card._get_stripe_account_id(api_key) card_delete_mock.assert_called_once_with( self.customer.id, card.id, api_key=api_key, stripe_account=stripe_account ) @patch( "stripe.Account.delete_external_account", autospec=True, ) @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_CUSTOM_ACCOUNT), autospec=True, ) def test_remove_card_by_account(self, account_retrieve_mock, card_delete_mock): stripe_card = Card._api_create( account=self.custom_account, source=FAKE_CARD_IV["id"] ) card = Card.sync_from_stripe_data(stripe_card) self.assertEqual(1, Card.objects.filter(id=stripe_card["id"]).count()) # remove card card.remove() self.assertEqual(0, Card.objects.filter(id=stripe_card["id"]).count()) api_key = card.default_api_key stripe_account = card._get_stripe_account_id(api_key) card_delete_mock.assert_called_once_with( self.custom_account.id, card.id, api_key=api_key, stripe_account=stripe_account, ) @patch( "stripe.Account.delete_external_account", autospec=True, ) @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_CUSTOM_ACCOUNT), autospec=True, ) def test_remove_already_deleted_card_by_account( self, account_retrieve_mock, card_delete_mock ): stripe_card = Card._api_create( account=self.custom_account, source=FAKE_CARD_IV["id"] ) card = Card.sync_from_stripe_data(stripe_card) self.assertEqual(1, Card.objects.filter(id=stripe_card["id"]).count()) # remove card card.remove() self.assertEqual(0, Card.objects.filter(id=stripe_card["id"]).count()) # remove card again count, _ = Card.objects.filter(id=stripe_card["id"]).delete() self.assertEqual(0, count) api_key = card.default_api_key stripe_account = card._get_stripe_account_id(api_key) card_delete_mock.assert_called_once_with( self.custom_account.id, card.id, api_key=api_key, stripe_account=stripe_account, ) @patch( "stripe.Customer.delete_source", autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Customer.retrieve_source", return_value=deepcopy(FAKE_CARD), autospec=True, ) def test_remove_already_deleted_card( self, customer_retrieve_source_mock, customer_retrieve_mock, card_delete_mock, ): stripe_card = Card._api_create(customer=self.customer, source=FAKE_CARD["id"]) Card.sync_from_stripe_data(stripe_card) self.assertEqual(self.customer.legacy_cards.count(), 1) card_object = self.customer.legacy_cards.first() Card.objects.filter(id=stripe_card["id"]).delete() self.assertEqual(self.customer.legacy_cards.count(), 0) card_object.remove() self.assertEqual(self.customer.legacy_cards.count(), 0) @patch("djstripe.models.Card._api_delete", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_remove_no_such_source(self, customer_retrieve_mock, card_delete_mock): stripe_card = Card._api_create(customer=self.customer, source=FAKE_CARD["id"]) Card.sync_from_stripe_data(stripe_card) card_delete_mock.side_effect = InvalidRequestError("No such source:", "blah") self.assertEqual(1, self.customer.legacy_cards.count()) card = self.customer.legacy_cards.all()[0] card.remove() self.assertEqual(0, self.customer.legacy_cards.count()) self.assertTrue(card_delete_mock.called) @patch("djstripe.models.Card._api_delete", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_remove_no_such_customer(self, customer_retrieve_mock, card_delete_mock): stripe_card = Card._api_create(customer=self.customer, source=FAKE_CARD["id"]) Card.sync_from_stripe_data(stripe_card) card_delete_mock.side_effect = InvalidRequestError("No such customer:", "blah") self.assertEqual(1, self.customer.legacy_cards.count()) card = self.customer.legacy_cards.all()[0] card.remove() self.assertEqual(0, self.customer.legacy_cards.count()) self.assertTrue(card_delete_mock.called) @patch("djstripe.models.Card._api_delete", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_remove_unexpected_exception( self, customer_retrieve_mock, card_delete_mock ): stripe_card = Card._api_create(customer=self.customer, source=FAKE_CARD["id"]) Card.sync_from_stripe_data(stripe_card) card_delete_mock.side_effect = InvalidRequestError( "Unexpected Exception", "blah" ) self.assertEqual(1, self.customer.legacy_cards.count()) card = self.customer.legacy_cards.all()[0] with self.assertRaisesMessage(InvalidRequestError, "Unexpected Exception"): card.remove() @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_api_list(self, customer_retrieve_mock): card_list = Card.api_list(customer=self.customer) self.assertCountEqual([FAKE_CARD, FAKE_CARD_III], [i for i in card_list]) ================================================ FILE: tests/test_charge.py ================================================ """ dj-stripe Charge Model Tests. """ from copy import deepcopy from decimal import Decimal from unittest.mock import call, create_autospec, patch from django.contrib.auth import get_user_model from django.test.testcases import TestCase from djstripe.enums import ChargeStatus, LegacySourceType from djstripe.models import Charge, DjstripePaymentMethod, Transfer from djstripe.settings import djstripe_settings from . import ( FAKE_BALANCE_TRANSACTION, FAKE_BALANCE_TRANSACTION_REFUND, FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CHARGE, FAKE_CHARGE_REFUNDED, FAKE_CUSTOMER, FAKE_FILEUPLOAD_ICON, FAKE_FILEUPLOAD_LOGO, FAKE_INVOICE, FAKE_INVOICEITEM, FAKE_PAYMENT_INTENT_I, FAKE_PLAN, FAKE_PLATFORM_ACCOUNT, FAKE_PRODUCT, FAKE_REFUND, FAKE_STANDARD_ACCOUNT, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_ITEM, FAKE_TRANSFER, AssertStripeFksMixin, ) class ChargeTest(AssertStripeFksMixin, TestCase): @classmethod def setUp(self): # create a Stripe Platform Account self.account = FAKE_PLATFORM_ACCOUNT.create() user = get_user_model().objects.create_user( username="testuser", email="djstripe@example.com" ) self.customer = FAKE_CUSTOMER.create_for_user(user) self.default_expected_blank_fks = { "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.Product.default_price", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", } def test___str__(self): charge = Charge( amount=50, currency="usd", id="ch_test", status=ChargeStatus.failed, captured=False, paid=False, ) self.assertEqual(str(charge), "$50.00 USD (Uncaptured)") charge.captured = True self.assertEqual(str(charge), "$50.00 USD (Failed)") charge.status = ChargeStatus.succeeded charge.disputed = True self.assertEqual(str(charge), "$50.00 USD (Disputed)") charge.disputed = False charge.refunded = True charge.amount_refunded = 50 self.assertEqual(str(charge), "$50.00 USD (Refunded)") charge.refunded = False charge.amount_refunded = 0 self.assertEqual(str(charge), "$50.00 USD (Succeeded)") charge.status = ChargeStatus.pending self.assertEqual(str(charge), "$50.00 USD (Pending)") @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.PaymentIntent.retrieve", autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) def test_capture_charge( self, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, balance_transaction_retrieve_mock, charge_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_charge_no_invoice = deepcopy(FAKE_CHARGE) fake_charge_no_invoice.update({"invoice": None}) charge_retrieve_mock.return_value = fake_charge_no_invoice # TODO - I think this is needed in line with above? fake_payment_intent_no_invoice = deepcopy(FAKE_PAYMENT_INTENT_I) fake_payment_intent_no_invoice.update({"invoice": None}) payment_intent_retrieve_mock.return_value = fake_payment_intent_no_invoice charge, created = Charge._get_or_create_from_stripe_object( fake_charge_no_invoice ) self.assertTrue(created) captured_charge = charge.capture() self.assertTrue(captured_charge.captured) self.assertFalse(captured_charge.fraudulent) self.assert_fks( charge, expected_blank_fks=self.default_expected_blank_fks | { "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", "djstripe.Charge.invoice", "djstripe.Charge.latest_invoice (related name)", "djstripe.Invoice.charge", "djstripe.PaymentIntent.invoice (related name)", "djstripe.Plan.product", }, ) @patch("djstripe.models.Account.get_default_account", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) def test_sync_from_stripe_data( self, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, customer_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_charge_copy = deepcopy(FAKE_CHARGE) charge = Charge.sync_from_stripe_data(fake_charge_copy) self.assertEqual(Decimal("20"), charge.amount) self.assertEqual(True, charge.paid) self.assertEqual(False, charge.refunded) self.assertEqual(True, charge.captured) self.assertEqual(False, charge.disputed) self.assertEqual("Subscription creation", charge.description) self.assertEqual(0, charge.amount_refunded) self.assertEqual(self.customer.default_source.id, charge.source_id) self.assertEqual(charge.source.type, LegacySourceType.card) self.assertGreater(len(charge.receipt_url), 1) self.assertTrue(charge.payment_method_details["type"]) charge_retrieve_mock.assert_not_called() balance_transaction_retrieve_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_BALANCE_TRANSACTION["id"], stripe_account=None, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) self.assert_fks( charge, expected_blank_fks=self.default_expected_blank_fks | { "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", }, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) def test_sync_from_stripe_data_refunded_on_update( self, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, charge_retrieve_mock, default_account_mock, customer_retrieve_mock, ): # first sync charge (as per test_sync_from_stripe_data) # then sync refunded version, to hit the update code-path instead of insert default_account_mock.return_value = self.account fake_charge_copy = deepcopy(FAKE_CHARGE) # charge_retrieve_mock.return_value = fake_charge_copy with patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), ): charge = Charge.sync_from_stripe_data(fake_charge_copy) self.assertEqual(Decimal("20"), charge.amount) self.assertEqual(True, charge.paid) self.assertEqual(False, charge.refunded) self.assertEqual(True, charge.captured) self.assertEqual(False, charge.disputed) self.assertEqual(len(charge.refunds.all()), 0) fake_charge_refunded_copy = deepcopy(FAKE_CHARGE_REFUNDED) with patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION_REFUND), ) as balance_transaction_retrieve_mock: charge_refunded = Charge.sync_from_stripe_data(fake_charge_refunded_copy) self.assertEqual(charge.id, charge_refunded.id) self.assertEqual(Decimal("20"), charge_refunded.amount) self.assertEqual(True, charge_refunded.paid) self.assertEqual(True, charge_refunded.refunded) self.assertEqual(True, charge_refunded.captured) self.assertEqual(False, charge_refunded.disputed) self.assertEqual("Subscription creation", charge_refunded.description) self.assertEqual(charge_refunded.amount, charge_refunded.amount_refunded) charge_retrieve_mock.assert_not_called() balance_transaction_retrieve_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_BALANCE_TRANSACTION_REFUND["id"], stripe_account=None, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) refunds = list(charge_refunded.refunds.all()) self.assertEqual(len(refunds), 1) refund = refunds[0] self.assertEqual(refund.id, FAKE_REFUND["id"]) self.assertNotEqual( charge_refunded.balance_transaction.id, refund.balance_transaction.id ) self.assertEqual( charge_refunded.balance_transaction.id, FAKE_BALANCE_TRANSACTION["id"] ) self.assertEqual( refund.balance_transaction.id, FAKE_BALANCE_TRANSACTION_REFUND["id"] ) self.assert_fks( charge_refunded, expected_blank_fks=self.default_expected_blank_fks | { "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", }, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", side_effect=[ deepcopy(FAKE_BALANCE_TRANSACTION), deepcopy(FAKE_BALANCE_TRANSACTION_REFUND), ], ) @patch("stripe.Charge.retrieve", autospec=True) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) def test_sync_from_stripe_data_refunded( self, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, customer_retrieve_mock, ): default_account_mock.return_value = self.account fake_charge_copy = deepcopy(FAKE_CHARGE_REFUNDED) charge = Charge.sync_from_stripe_data(fake_charge_copy) self.assertEqual(Decimal("20"), charge.amount) self.assertEqual(True, charge.paid) self.assertEqual(True, charge.refunded) self.assertEqual(True, charge.captured) self.assertEqual(False, charge.disputed) self.assertEqual("Subscription creation", charge.description) self.assertEqual(charge.amount, charge.amount_refunded) charge_retrieve_mock.assert_not_called() # We expect two calls - for charge and then for charge.refunds balance_transaction_retrieve_mock.assert_has_calls( [ call( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_BALANCE_TRANSACTION["id"], stripe_account=None, stripe_version=djstripe_settings.STRIPE_API_VERSION, ), call( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_BALANCE_TRANSACTION_REFUND["id"], stripe_account=None, stripe_version=djstripe_settings.STRIPE_API_VERSION, ), ] ) refunds = list(charge.refunds.all()) self.assertEqual(len(refunds), 1) refund = refunds[0] self.assertEqual(refund.id, FAKE_REFUND["id"]) self.assertNotEqual( charge.balance_transaction.id, refund.balance_transaction.id ) self.assertEqual(charge.balance_transaction.id, FAKE_BALANCE_TRANSACTION["id"]) self.assertEqual( refund.balance_transaction.id, FAKE_BALANCE_TRANSACTION_REFUND["id"] ) self.assert_fks( charge, expected_blank_fks=self.default_expected_blank_fks | { "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", }, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) def test_sync_from_stripe_data_max_amount( self, default_account_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, customer_retrieve_mock, ): default_account_mock.return_value = self.account fake_charge_copy = deepcopy(FAKE_CHARGE) # https://support.stripe.com/questions/what-is-the-maximum-amount-i-can-charge-with-stripe fake_charge_copy.update({"amount": 99999999}) charge = Charge.sync_from_stripe_data(fake_charge_copy) self.assertEqual(Decimal("999999.99"), charge.amount) self.assertEqual(True, charge.paid) self.assertEqual(False, charge.refunded) self.assertEqual(True, charge.captured) self.assertEqual(False, charge.disputed) self.assertEqual(0, charge.amount_refunded) charge_retrieve_mock.assert_not_called() self.assert_fks( charge, expected_blank_fks=self.default_expected_blank_fks | { "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", }, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) def test_sync_from_stripe_data_unsupported_source( self, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, customer_retrieve_mock, ): default_account_mock.return_value = self.account fake_charge_copy = deepcopy(FAKE_CHARGE) fake_charge_copy.update({"source": {"id": "test_id", "object": "unsupported"}}) charge = Charge.sync_from_stripe_data(fake_charge_copy) self.assertEqual("test_id", charge.source_id) self.assertEqual("UNSUPPORTED_test_id", charge.source.type) self.assertEqual(charge.source, DjstripePaymentMethod.objects.get(id="test_id")) charge_retrieve_mock.assert_not_called() balance_transaction_retrieve_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_BALANCE_TRANSACTION["id"], stripe_account=None, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) self.assert_fks( charge, expected_blank_fks=self.default_expected_blank_fks | { "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", }, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch("stripe.PaymentIntent.retrieve", autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) def test_sync_from_stripe_data_no_customer( self, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_charge_copy = deepcopy(FAKE_CHARGE) fake_charge_copy.pop("customer", None) # remove invoice since it requires a customer fake_charge_copy.pop("invoice", None) fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_I) fake_payment_intent["invoice"] = None payment_intent_retrieve_mock.return_value = fake_payment_intent Charge.sync_from_stripe_data(fake_charge_copy) assert Charge.objects.count() == 1 charge = Charge.objects.get() assert charge.customer is None charge_retrieve_mock.assert_not_called() balance_transaction_retrieve_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_BALANCE_TRANSACTION["id"], stripe_account=None, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) self.assert_fks( charge, expected_blank_fks=self.default_expected_blank_fks | { "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", "djstripe.Charge.customer", "djstripe.Charge.invoice", "djstripe.Charge.latest_invoice (related name)", "djstripe.Invoice.charge", "djstripe.PaymentIntent.invoice (related name)", "djstripe.Plan.product", }, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch("stripe.Transfer.retrieve", autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) def test_sync_from_stripe_data_with_transfer( self, default_account_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, transfer_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, transfer__attach_object_post_save_hook_mock, customer_retrieve_mock, ): default_account_mock.return_value = self.account fake_transfer = deepcopy(FAKE_TRANSFER) fake_charge_copy = deepcopy(FAKE_CHARGE) fake_charge_copy.update({"transfer": fake_transfer["id"]}) transfer_retrieve_mock.return_value = fake_transfer charge_retrieve_mock.return_value = fake_charge_copy charge, created = Charge._get_or_create_from_stripe_object( fake_charge_copy, current_ids={fake_charge_copy["id"]} ) self.assertTrue(created) self.assertNotEqual(None, charge.transfer) self.assertEqual(fake_transfer["id"], charge.transfer.id) charge_retrieve_mock.assert_not_called() balance_transaction_retrieve_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_BALANCE_TRANSACTION["id"], stripe_account=None, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) self.assert_fks( charge, expected_blank_fks=( self.default_expected_blank_fks | { "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", } ) - {"djstripe.Charge.transfer"}, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch("stripe.Account.retrieve", autospec=True) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.File.retrieve", side_effect=[deepcopy(FAKE_FILEUPLOAD_ICON), deepcopy(FAKE_FILEUPLOAD_LOGO)], autospec=True, ) def test_sync_from_stripe_data_with_destination( self, file_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, balance_transaction_retrieve_mock, account_retrieve_mock, charge_retrieve_mock, customer_retrieve_mock, ): account_retrieve_mock.return_value = FAKE_STANDARD_ACCOUNT fake_charge_copy = deepcopy(FAKE_CHARGE) fake_charge_copy.update({"destination": FAKE_STANDARD_ACCOUNT["id"]}) charge, created = Charge._get_or_create_from_stripe_object( fake_charge_copy, current_ids={fake_charge_copy["id"]} ) self.assertTrue(created) charge_retrieve_mock.assert_not_called() balance_transaction_retrieve_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_BALANCE_TRANSACTION["id"], stripe_account=None, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) self.assert_fks(charge, expected_blank_fks=self.default_expected_blank_fks) @patch.object(target=Charge, attribute="source", autospec=True) @patch( target="djstripe.models.payment_methods.DjstripePaymentMethod", autospec=True ) @patch(target="djstripe.models.account.Account", autospec=True) def test__attach_objects_hook_missing_source_data( self, mock_account, mock_payment_method, mock_charge_source ): """ Make sure we handle the case where the source data is empty or insufficient. """ charge = Charge( amount=50, currency="usd", id="ch_test", status=ChargeStatus.failed, captured=False, paid=False, ) mock_cls = create_autospec(spec=Charge, spec_set=True) # Empty data dict works for this test since we only look up the source key and # everything else is mocked. mock_data = {} starting_source = charge.source charge._attach_objects_hook(cls=mock_cls, data=mock_data) # source shouldn't be touched self.assertEqual(starting_source, charge.source) mock_payment_method._get_or_create_source.assert_not_called() # try again with a source key, but no object sub key. mock_data = {"source": {"foo": "bar"}} charge._attach_objects_hook(cls=mock_cls, data=mock_data) # source shouldn't be touched self.assertEqual(starting_source, charge.source) mock_payment_method._get_or_create_source.assert_not_called() @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch("djstripe.models.Account.get_default_account", autospec=True) @patch("stripe.BalanceTransaction.retrieve", autospec=True) @patch("stripe.Charge.retrieve", autospec=True) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) def test_max_size_large_charge_on_decimal_amount( self, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, customer_retrieve_mock, ): """ By contacting stripe support, some accounts will have their limit raised to 11 digits """ amount = 99999999999 assert len(str(amount)) == 11 fake_transaction = deepcopy(FAKE_BALANCE_TRANSACTION) fake_transaction.update({"amount": amount}) default_account_mock.return_value = self.account balance_transaction_retrieve_mock.return_value = fake_transaction fake_charge = deepcopy(FAKE_CHARGE) fake_charge.update({"amount": amount}) charge = Charge.sync_from_stripe_data(fake_charge) charge_retrieve_mock.assert_not_called() self.assertTrue(bool(charge.pk)) self.assertEqual(charge.amount, Decimal("999999999.99")) self.assertEqual(charge.balance_transaction.amount, 99999999999) ================================================ FILE: tests/test_checks.py ================================================ # import pytest # from django.core.checks import Error # from django.test import TestCase, override_settings # # errors = checked_object.check() # # expected_errors = [ # # Error( # # 'an error', # # hint='A hint.', # # obj=checked_object, # # id='myapp.E001', # # ) # # ] # # self.assertEqual(errors, expected_errors) # class TestChecks(TestCase): # def foo_1(a, b, *args, **kwargs): # pass # def foo_2(a, *args): # pass # def foo_3(a, **kwargs): # pass # def foo_4(a): # pass # @override_settings(DJSTRIPE_WEBHOOK_EVENT_CALLBACK=foo_4) # def test_check_webhook_event_callback_accepts_api_key(self): # a = 5 # breakpoint() ================================================ FILE: tests/test_coupon.py ================================================ from copy import deepcopy from decimal import Decimal import pytest from django.test.testcases import TestCase from djstripe.models import Coupon from . import FAKE_COUPON pytestmark = pytest.mark.django_db class TransferTest(TestCase): def test_retrieve_coupon(self): coupon_data = deepcopy(FAKE_COUPON) coupon = Coupon.sync_from_stripe_data(coupon_data) self.assertEqual(coupon.id, FAKE_COUPON["id"]) class CouponTest(TestCase): def test_blank_coupon_str(self): coupon = Coupon() self.assertEqual(str(coupon).strip(), "(invalid amount) off once") def test___str__(self): coupon = Coupon.objects.create( id="coupon-test-amount-off-forever", amount_off=10, currency="usd", duration="forever", name="Test coupon", ) self.assertEqual(str(coupon), "Test coupon") def test_human_readable_usd_off_forever(self): coupon = Coupon.objects.create( id="coupon-test-amount-off-forever", amount_off=10, currency="usd", duration="forever", ) self.assertEqual(coupon.human_readable, "$10.00 USD off forever") self.assertEqual(str(coupon), coupon.human_readable) def test_human_readable_eur_off_forever(self): coupon = Coupon.objects.create( id="coupon-test-amount-off-forever", amount_off=10, currency="eur", duration="forever", ) self.assertEqual(coupon.human_readable, "€10.00 EUR off forever") self.assertEqual(str(coupon), coupon.human_readable) def test_human_readable_percent_off_forever(self): coupon = Coupon.objects.create( id="coupon-test-percent-off-forever", percent_off=10.25, currency="usd", duration="forever", ) self.assertEqual(coupon.human_readable, "10.25% off forever") self.assertEqual(str(coupon), coupon.human_readable) def test_human_readable_percent_off_once(self): coupon = Coupon.objects.create( id="coupon-test-percent-off-once", percent_off=10.25, currency="usd", duration="once", ) self.assertEqual(coupon.human_readable, "10.25% off once") self.assertEqual(str(coupon), coupon.human_readable) def test_human_readable_percent_off_one_month(self): coupon = Coupon.objects.create( id="coupon-test-percent-off-1month", percent_off=10.25, currency="usd", duration="repeating", duration_in_months=1, ) self.assertEqual(coupon.human_readable, "10.25% off for 1 month") self.assertEqual(str(coupon), coupon.human_readable) def test_human_readable_percent_off_three_months(self): coupon = Coupon.objects.create( id="coupon-test-percent-off-3month", percent_off=10.25, currency="usd", duration="repeating", duration_in_months=3, ) self.assertEqual(coupon.human_readable, "10.25% off for 3 months") self.assertEqual(str(coupon), coupon.human_readable) def test_human_readable_integer_percent_off_forever(self): coupon = Coupon.objects.create( id="coupon-test-percent-off-forever", percent_off=10, currency="usd", duration="forever", ) self.assertEqual(coupon.human_readable, "10% off forever") self.assertEqual(str(coupon), coupon.human_readable) class TestCouponDecimal: @pytest.mark.parametrize( "inputted,expected", [ (Decimal("1"), Decimal("1.00")), (Decimal("1.5234567"), Decimal("1.52")), (Decimal("0"), Decimal("0.00")), (Decimal("23.2345678"), Decimal("23.23")), ("1", Decimal("1.00")), ("1.5234567", Decimal("1.52")), ("0", Decimal("0.00")), ("23.2345678", Decimal("23.23")), (1, Decimal("1.00")), (1.5234567, Decimal("1.52")), (0, Decimal("0.00")), (23.2345678, Decimal("23.24")), ], ) def test_decimal_percent_off_coupon(self, inputted, expected): fake_coupon = deepcopy(FAKE_COUPON) fake_coupon["percent_off"] = inputted coupon = Coupon.sync_from_stripe_data(fake_coupon) field_data = coupon.percent_off assert isinstance(field_data, Decimal) assert field_data == expected ================================================ FILE: tests/test_customer.py ================================================ """ Customer Model Tests. """ import decimal from copy import deepcopy from unittest.mock import ANY, call, patch from django.contrib.auth import get_user_model from django.test import TestCase, override_settings from django.utils import timezone from stripe.error import InvalidRequestError from djstripe.exceptions import MultipleSubscriptionException from djstripe.models import ( Card, Charge, Coupon, Customer, DjstripePaymentMethod, IdempotencyKey, Invoice, PaymentMethod, Plan, Price, Product, Source, Subscription, ) from djstripe.settings import djstripe_settings from . import ( FAKE_BALANCE_TRANSACTION, FAKE_CARD, FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CARD_III, FAKE_CHARGE, FAKE_COUPON, FAKE_CUSTOMER, FAKE_CUSTOMER_II, FAKE_CUSTOMER_III, FAKE_CUSTOMER_IV, FAKE_DISCOUNT_CUSTOMER, FAKE_INVOICE, FAKE_INVOICE_III, FAKE_INVOICEITEM, FAKE_PAYMENT_INTENT_I, FAKE_PAYMENT_METHOD_I, FAKE_PLAN, FAKE_PLATFORM_ACCOUNT, FAKE_PRICE, FAKE_PRODUCT, FAKE_SOURCE, FAKE_SOURCE_II, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_II, FAKE_SUBSCRIPTION_ITEM, FAKE_UPCOMING_INVOICE, AssertStripeFksMixin, StripeList, datetime_to_unix, ) class TestCustomer(AssertStripeFksMixin, TestCase): def setUp(self): # create a Stripe Platform Account self.account = FAKE_PLATFORM_ACCOUNT.create() self.user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) self.customer = FAKE_CUSTOMER.create_for_user(self.user) self.payment_method, _ = DjstripePaymentMethod._get_or_create_source( FAKE_CARD, "card" ) self.card = self.payment_method.resolve() self.customer.default_source = self.payment_method self.customer.save() def test___str__(self): self.assertEqual(str(self.customer), str(self.user)) self.customer.subscriber = None self.assertEqual(str(self.customer), self.customer.description) def test_balance(self): self.assertEqual(self.customer.balance, 0) self.assertEqual(self.customer.credits, 0) self.customer.balance = 1000 self.assertEqual(self.customer.balance, 1000) self.assertEqual(self.customer.credits, 0) self.assertEqual(self.customer.pending_charges, 1000) self.customer.balance = -1000 self.assertEqual(self.customer.balance, -1000) self.assertEqual(self.customer.credits, 1000) self.assertEqual(self.customer.pending_charges, 0) def test_customer_dashboard_url(self): expected_url = f"https://dashboard.stripe.com/{self.customer.djstripe_owner_account.id}/test/customers/{self.customer.id}" self.assertEqual(self.customer.get_stripe_dashboard_url(), expected_url) self.customer.livemode = True expected_url = f"https://dashboard.stripe.com/{self.customer.djstripe_owner_account.id}/customers/{self.customer.id}" self.assertEqual(self.customer.get_stripe_dashboard_url(), expected_url) unsaved_customer = Customer() self.assertEqual(unsaved_customer.get_stripe_dashboard_url(), "") def test_customer_sync_unsupported_source(self): fake_customer = deepcopy(FAKE_CUSTOMER_II) fake_customer["default_source"]["object"] = fake_customer["sources"]["data"][0][ "object" ] = "fish" user = get_user_model().objects.create_user( username="test_user_sync_unsupported_source" ) self.assertRaisesRegex( ValueError, "Trying to fit a 'fish' into 'Card'. Aborting.", fake_customer.create_for_user, user, ) def test_customer_sync_has_subscriber_metadata(self): user = get_user_model().objects.create(username="test_metadata", id=12345) fake_customer = deepcopy(FAKE_CUSTOMER) fake_customer["id"] = "cus_sync_has_subscriber_metadata" fake_customer["metadata"] = {"djstripe_subscriber": "12345"} customer = Customer.sync_from_stripe_data(fake_customer) self.assertEqual(customer.subscriber, user) self.assertEqual(customer.metadata, {"djstripe_subscriber": "12345"}) @override_settings(DJSTRIPE_SUBSCRIBER_CUSTOMER_KEY="") def test_customer_sync_has_subscriber_metadata_disabled(self): user = get_user_model().objects.create( username="test_metadata_disabled", id=98765 ) fake_customer = deepcopy(FAKE_CUSTOMER) fake_customer["id"] = "cus_test_metadata_disabled" fake_customer["metadata"] = {"djstripe_subscriber": "98765"} customer = Customer.sync_from_stripe_data(fake_customer) self.assertNotEqual(customer.subscriber, user) self.assertNotEqual(customer.subscriber_id, 98765) self.assert_fks( customer, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.subscriber", }, ) def test_customer_sync_has_bad_subscriber_metadata(self): fake_customer = deepcopy(FAKE_CUSTOMER) fake_customer["id"] = "cus_sync_has_bad_subscriber_metadata" fake_customer["metadata"] = {"djstripe_subscriber": "does_not_exist"} customer = Customer.sync_from_stripe_data(fake_customer) self.assertEqual(customer.subscriber, None) self.assertEqual(customer.metadata, {"djstripe_subscriber": "does_not_exist"}) self.assert_fks( customer, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.subscriber", }, ) @override_settings(DJSTRIPE_SUBSCRIBER_CUSTOMER_KEY="") @patch("stripe.Customer.create", autospec=True) def test_customer_create_metadata_disabled(self, customer_mock): user = get_user_model().objects.create_user( username="test_user_create_metadata_disabled" ) fake_customer = deepcopy(FAKE_CUSTOMER) fake_customer["id"] = "cus_test_create_metadata_disabled" customer_mock.return_value = fake_customer customer = Customer.create(user) customer_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, email="", idempotency_key=None, metadata={}, stripe_account=None, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) self.assertEqual(customer.metadata, None) self.assert_fks( customer, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.default_source", }, ) @patch.object(Card, "_get_or_create_from_stripe_object") @patch("stripe.Customer.retrieve", autospec=True) @patch( "stripe.Card.retrieve", autospec=True, ) def test_customer_sync_non_local_card( self, card_retrieve_mock, customer_retrieve_mock, card_get_or_create_mock ): fake_customer = deepcopy(FAKE_CUSTOMER_II) fake_customer["id"] = fake_customer["sources"]["data"][0][ "customer" ] = "cus_test_sync_non_local_card" fake_customer["default_source"]["id"] = fake_customer["sources"]["data"][0][ "id" ] = "card_cus_test_sync_non_local_card" customer_retrieve_mock.return_value = fake_customer fake_card = deepcopy(fake_customer["default_source"]) fake_card["customer"] = "cus_test_sync_non_local_card" card_retrieve_mock.return_value = fake_card card_get_or_create_mock.return_value = fake_card user = get_user_model().objects.create_user( username="test_user_sync_non_local_card" ) # create a source object so that FAKE_CUSTOMER_III with a default source # can be created correctly. fake_source_data = deepcopy(FAKE_SOURCE_II) fake_source_data["card"] = deepcopy(fake_card) fake_source_data["customer"] = fake_customer Source.sync_from_stripe_data(fake_source_data) customer = fake_customer.create_for_user(user) self.assertEqual(customer.sources.count(), 1) self.assertEqual(customer.legacy_cards.count(), 0) self.assertEqual( customer.default_source.id, fake_customer["default_source"]["id"] ) @patch( "stripe.BankAccount.retrieve", return_value=FAKE_CUSTOMER_IV["default_source"], autospec=True, ) def test_customer_sync_bank_account_source(self, bank_account_retrieve_mock): fake_customer = deepcopy(FAKE_CUSTOMER_IV) user = get_user_model().objects.create_user( username="test_user_sync_bank_account_source" ) customer = fake_customer.create_for_user(user) self.assertEqual(customer.deleted, False) self.assertEqual(customer.sources.count(), 0) self.assertEqual(customer.legacy_cards.count(), 0) self.assertEqual(customer.bank_account.count(), 1) self.assertEqual( customer.default_source.id, fake_customer["default_source"]["id"] ) self.assert_fks( customer, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", }, ) @patch("stripe.Customer.create", autospec=True) def test_customer_sync_no_sources(self, customer_mock): fake_customer = deepcopy(FAKE_CUSTOMER) fake_customer["id"] = "cus_test_sync_no_sources" fake_customer["default_source"] = None fake_customer["sources"] = None customer_mock.return_value = fake_customer user = get_user_model().objects.create_user( username="test_user_sync_non_local_card" ) customer = Customer.create(user) self.assertEqual( customer_mock.call_args_list[0][1].get("metadata"), {"djstripe_subscriber": user.pk}, ) self.assertEqual(customer.sources.count(), 0) self.assertEqual(customer.legacy_cards.count(), 0) self.assertEqual(customer.default_source, None) self.assert_fks( customer, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.default_source", }, ) def test_customer_sync_default_source_string(self): Customer.objects.all().delete() Card.objects.all().delete() customer_fake = deepcopy(FAKE_CUSTOMER) customer = Customer.sync_from_stripe_data(customer_fake) self.assertEqual( customer.default_source.id, customer_fake["default_source"]["id"] ) self.assertEqual(customer.legacy_cards.count(), 2) self.assertEqual(len(list(customer.customer_payment_methods)), 2) self.assert_fks( customer, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.subscriber", }, ) @patch("stripe.Customer.retrieve", autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I) ) def test_customer_sync_default_payment_method_string( self, attach_mock, customer_retrieve_mock ): Customer.objects.all().delete() PaymentMethod.objects.all().delete() customer_fake = deepcopy(FAKE_CUSTOMER) customer_fake["invoice_settings"][ "default_payment_method" ] = FAKE_PAYMENT_METHOD_I["id"] customer_retrieve_mock.return_value = customer_fake customer = Customer.sync_from_stripe_data(customer_fake) self.assertEqual( customer.default_payment_method.id, customer_fake["invoice_settings"]["default_payment_method"], ) self.assertEqual(customer.payment_methods.count(), 1) self.assert_fks( customer, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.subscriber", }, ) @patch("stripe.Customer.retrieve", autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I) ) def test_customer_sync_null_default_payment_method( self, attach_mock, customer_retrieve_mock ): """Test to make sure a custom'er default_payment_method gets updated to None if they remove their only attached payment method""" Customer.objects.all().delete() PaymentMethod.objects.all().delete() customer_fake = deepcopy(FAKE_CUSTOMER) customer_fake["invoice_settings"][ "default_payment_method" ] = FAKE_PAYMENT_METHOD_I["id"] customer_retrieve_mock.return_value = customer_fake customer = Customer.sync_from_stripe_data(customer_fake) self.assertEqual( customer.default_payment_method.id, customer_fake["invoice_settings"]["default_payment_method"], ) self.assertEqual(customer.payment_methods.count(), 1) self.assert_fks( customer, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.subscriber", }, ) # update customer_retrieve_mock return value customer_fake = deepcopy(FAKE_CUSTOMER) customer_fake["invoice_settings"]["default_payment_method"] = None customer_retrieve_mock.return_value = customer_fake # now detach the payment method from customer is_detached = customer.default_payment_method.detach() assert is_detached is True # refresh customer from db customer.refresh_from_db() self.assertEqual( customer.default_payment_method, None, ) self.assertEqual(customer.payment_methods.count(), 0) self.assert_fks( customer, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.subscriber", "djstripe.Customer.default_payment_method", }, ) @patch( "stripe.Customer.delete_source", autospec=True, ) @patch("stripe.Customer.delete", autospec=True) @patch("stripe.Customer.retrieve", autospec=True) @patch( "stripe.Customer.retrieve_source", side_effect=[deepcopy(FAKE_CARD), deepcopy(FAKE_CARD_III)], autospec=True, ) def test_customer_purge_leaves_customer_record( self, customer_retrieve_source_mock, customer_retrieve_fake, customer_delete_mock, customer_source_delete_mock, ): self.customer.purge() customer = Customer.objects.get(id=self.customer.id) self.assertTrue(customer.subscriber is None) self.assertTrue(customer.default_source is None) self.assertTrue(customer.deleted is True) self.assertTrue(not customer.legacy_cards.all()) self.assertTrue(not customer.sources.all()) self.assertTrue(get_user_model().objects.filter(pk=self.user.pk).exists()) @patch("stripe.Customer.create", autospec=True) def test_customer_purge_detaches_sources( self, customer_api_create_fake, ): fake_customer = deepcopy(FAKE_CUSTOMER_III) customer_api_create_fake.return_value = fake_customer user = get_user_model().objects.create_user( username="blah", email=FAKE_CUSTOMER_III["email"] ) Customer.get_or_create(user) customer = Customer.sync_from_stripe_data(deepcopy(FAKE_CUSTOMER_III)) self.assertIsNotNone(customer.default_source) self.assertNotEqual(customer.sources.count(), 0) with patch("stripe.Customer.delete", autospec=True), patch( "stripe.Source.retrieve", return_value=deepcopy(FAKE_SOURCE), autospec=True ): customer.purge() self.assertIsNone(customer.default_source) self.assertEqual(customer.sources.count(), 0) @patch( "stripe.Customer.create", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True ) def test_customer_purge_deletes_idempotency_key(self, customer_api_create_fake): # We need to call Customer.get_or_create (which setUp doesn't) # to get an idempotency key user = get_user_model().objects.create_user( username="blah", email=FAKE_CUSTOMER_II["email"] ) idempotency_key_action = f"customer:create:{user.pk}" self.assertFalse( IdempotencyKey.objects.filter(action=idempotency_key_action).exists() ) customer, created = Customer.get_or_create(user) self.assertTrue( IdempotencyKey.objects.filter(action=idempotency_key_action).exists() ) with patch("stripe.Customer.delete", autospec=True): customer.purge() self.assertFalse( IdempotencyKey.objects.filter(action=idempotency_key_action).exists() ) @patch( "stripe.Customer.delete_source", autospec=True, ) @patch("stripe.Customer.delete", autospec=True) @patch( "stripe.Customer.retrieve", side_effect=InvalidRequestError("No such customer:", "blah"), autospec=True, ) def test_customer_purge_raises_customer_exception( self, customer_retrieve_mock, customer_delete_mock, customer_source_delete_mock ): self.customer.purge() customer = Customer.objects.get(id=self.customer.id) self.assertTrue(customer.subscriber is None) self.assertTrue(customer.default_source is None) self.assertTrue(not customer.legacy_cards.all()) self.assertTrue(not customer.sources.all()) self.assertTrue(get_user_model().objects.filter(pk=self.user.pk).exists()) customer_delete_mock.assert_called_once_with( self.customer.id, api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_account=self.customer.djstripe_owner_account.id, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) self.assertEqual(0, customer_retrieve_mock.call_count) self.assertEqual(2, customer_source_delete_mock.call_count) @patch("stripe.Customer.delete_source", autospec=True) @patch("stripe.Customer.delete", autospec=True) @patch("stripe.Customer.retrieve", autospec=True) @patch( "stripe.Customer.retrieve_source", return_value=deepcopy(FAKE_CARD), autospec=True, ) def test_customer_delete_raises_unexpected_exception( self, customer_retrieve_source_mock, customer_retrieve_mock, customer_delete_mock, customer_source_delete_mock, ): customer_delete_mock.side_effect = InvalidRequestError( "Unexpected Exception", "blah" ) with self.assertRaisesMessage(InvalidRequestError, "Unexpected Exception"): self.customer.purge() customer_delete_mock.assert_called_once_with( self.customer.id, api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_account=self.customer.djstripe_owner_account.id, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_add_card_set_default_true(self, customer_retrieve_mock): self.customer.add_card(FAKE_CARD["id"]) self.customer.add_card(FAKE_CARD_III["id"]) self.assertEqual(2, Card.objects.count()) self.assertEqual(FAKE_CARD_III["id"], self.customer.default_source.id) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_add_card_set_default_false(self, customer_retrieve_mock): # self.customer already has FAKE_CARD as its default payment method self.customer.add_card(FAKE_CARD_III["id"], set_default=False) self.assertEqual(2, Card.objects.count()) self.assertEqual(FAKE_CARD["id"], self.customer.default_source.id) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_add_card_set_default_false_with_single_card_still_becomes_default( self, customer_retrieve_mock ): # delete all already added cards to self.customer Card.objects.all().delete() # assert self.customer has no cards self.assertEqual(0, self.customer.legacy_cards.count()) self.assertEqual(0, self.customer.sources.count()) self.customer.add_card(FAKE_CARD["id"], set_default=False) # assert new card got added to self.customer self.assertEqual(1, Card.objects.count()) # self.customer already has FAKE_CARD as its default payment method self.assertEqual(FAKE_CARD["id"], self.customer.default_source.id) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch("stripe.PaymentMethod.attach", return_value=deepcopy(FAKE_PAYMENT_METHOD_I)) def test_add_payment_method_obj(self, attach_mock, customer_retrieve_mock): self.assertEqual( self.customer.payment_methods.filter( id=FAKE_PAYMENT_METHOD_I["id"] ).count(), 0, ) payment_method = PaymentMethod.sync_from_stripe_data(FAKE_PAYMENT_METHOD_I) payment_method = self.customer.add_payment_method(payment_method) self.assertEqual(payment_method.customer.id, self.customer.id) self.assertEqual( self.customer.payment_methods.filter( id=FAKE_PAYMENT_METHOD_I["id"] ).count(), 1, ) self.assertEqual( self.customer.payment_methods.filter( id=FAKE_PAYMENT_METHOD_I["id"] ).first(), self.customer.default_payment_method, ) self.assertEqual( self.customer.default_payment_method.id, self.customer.invoice_settings["default_payment_method"], ) self.assert_fks(self.customer, expected_blank_fks={"djstripe.Customer.coupon"}) @patch("stripe.Customer.retrieve", autospec=True) @patch("stripe.PaymentMethod.attach", return_value=deepcopy(FAKE_PAYMENT_METHOD_I)) def test_add_payment_method_set_default_true( self, attach_mock, customer_retrieve_mock ): fake_customer = deepcopy(FAKE_CUSTOMER) fake_customer["default_source"] = None customer_retrieve_mock.return_value = fake_customer self.customer.default_source = None self.customer.save() self.assertEqual( self.customer.payment_methods.filter( id=FAKE_PAYMENT_METHOD_I["id"] ).count(), 0, ) payment_method = self.customer.add_payment_method(FAKE_PAYMENT_METHOD_I["id"]) self.assertEqual(payment_method.customer.id, self.customer.id) self.assertEqual( self.customer.payment_methods.filter( id=FAKE_PAYMENT_METHOD_I["id"] ).count(), 1, ) self.assertEqual( self.customer.payment_methods.filter( id=FAKE_PAYMENT_METHOD_I["id"] ).first(), self.customer.default_payment_method, ) self.assertEqual( self.customer.default_payment_method.id, self.customer.invoice_settings["default_payment_method"], ) self.assert_fks( self.customer, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_source", }, ) @patch("stripe.Customer.retrieve", autospec=True) @patch("stripe.PaymentMethod.attach", return_value=deepcopy(FAKE_PAYMENT_METHOD_I)) def test_add_payment_method_set_default_false( self, attach_mock, customer_retrieve_mock ): fake_customer = deepcopy(FAKE_CUSTOMER) fake_customer["default_source"] = None customer_retrieve_mock.return_value = fake_customer self.customer.default_source = None self.customer.save() self.assertEqual( self.customer.payment_methods.filter( id=FAKE_PAYMENT_METHOD_I["id"] ).count(), 0, ) payment_method = self.customer.add_payment_method( FAKE_PAYMENT_METHOD_I["id"], set_default=False ) self.assertEqual(payment_method.customer.id, self.customer.id) self.assertEqual( self.customer.payment_methods.filter( id=FAKE_PAYMENT_METHOD_I["id"] ).count(), 1, ) self.assert_fks( self.customer, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.default_source", }, ) def test_charge_accepts_only_decimals(self): with self.assertRaises(ValueError): self.customer.charge(10) @patch("stripe.Coupon.retrieve", return_value=deepcopy(FAKE_COUPON), autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_add_coupon_by_id(self, customer_retrieve_mock, coupon_retrieve_mock): self.assertEqual(self.customer.coupon, None) self.customer.add_coupon(FAKE_COUPON["id"]) customer_retrieve_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=ANY, id=FAKE_CUSTOMER["id"], stripe_account=self.customer.djstripe_owner_account.id, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) @patch("stripe.Coupon.retrieve", return_value=deepcopy(FAKE_COUPON), autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_add_coupon_by_object(self, customer_retrieve_mock, coupon_retrieve_mock): self.assertEqual(self.customer.coupon, None) coupon = Coupon.sync_from_stripe_data(FAKE_COUPON) fake_discount = deepcopy(FAKE_DISCOUNT_CUSTOMER) def fake_customer_save(self, *args, **kwargs): # fake the api coupon update behaviour coupon = self.pop("coupon", None) if coupon: self["discount"] = fake_discount else: self["discount"] = None return self with patch("tests.CustomerDict.save", new=fake_customer_save): self.customer.add_coupon(coupon) customer_retrieve_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=ANY, id=FAKE_CUSTOMER["id"], stripe_account=self.customer.djstripe_owner_account.id, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) self.customer.refresh_from_db() self.assert_fks( self.customer, expected_blank_fks={"djstripe.Customer.default_payment_method"}, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch("stripe.PaymentIntent.retrieve", autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) def test_refund_charge( self, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_charge_no_invoice = deepcopy(FAKE_CHARGE) fake_charge_no_invoice.update({"invoice": None}) charge_retrieve_mock.return_value = fake_charge_no_invoice fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_I) fake_payment_intent.update({"invoice": None}) payment_intent_retrieve_mock.return_value = fake_payment_intent charge, created = Charge._get_or_create_from_stripe_object( fake_charge_no_invoice ) self.assertTrue(created) self.assert_fks( charge, expected_blank_fks={ "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.latest_invoice (related name)", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.invoice", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.PaymentIntent.invoice (related name)", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", }, ) charge.refund() refunded_charge, created2 = Charge._get_or_create_from_stripe_object( fake_charge_no_invoice ) self.assertFalse(created2) self.assertEqual(refunded_charge.refunded, True) self.assertEqual(refunded_charge.amount_refunded, decimal.Decimal("20.00")) self.assert_fks( refunded_charge, expected_blank_fks={ "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.latest_invoice (related name)", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.invoice", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.PaymentIntent.invoice (related name)", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", }, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch("stripe.PaymentIntent.retrieve", autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) def test_refund_charge_object_returned( self, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_charge_no_invoice = deepcopy(FAKE_CHARGE) fake_charge_no_invoice.update({"invoice": None}) charge_retrieve_mock.return_value = fake_charge_no_invoice fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_I) fake_payment_intent.update({"invoice": None}) payment_intent_retrieve_mock.return_value = fake_payment_intent charge, created = Charge._get_or_create_from_stripe_object( fake_charge_no_invoice ) self.assertTrue(created) self.assert_fks( charge, expected_blank_fks={ "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.latest_invoice (related name)", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.invoice", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.PaymentIntent.invoice (related name)", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", }, ) refunded_charge = charge.refund() self.assertEqual(refunded_charge.refunded, True) self.assertEqual(refunded_charge.amount_refunded, decimal.Decimal("20.00")) self.assert_fks( refunded_charge, expected_blank_fks={ "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.latest_invoice (related name)", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.invoice", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.PaymentIntent.invoice (related name)", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", }, ) def test_calculate_refund_amount_partial_refund(self): charge = Charge( id="ch_111111", customer=self.customer, amount=decimal.Decimal("500.00") ) self.assertEqual( charge._calculate_refund_amount(amount=decimal.Decimal("300.00")), 30000 ) def test_calculate_refund_above_max_refund(self): charge = Charge( id="ch_111111", customer=self.customer, amount=decimal.Decimal("500.00") ) self.assertEqual( charge._calculate_refund_amount(amount=decimal.Decimal("600.00")), 50000 ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch("stripe.Charge.create", autospec=True) @patch("stripe.PaymentIntent.retrieve", autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) def test_charge_converts_dollars_into_cents( self, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_create_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_charge_copy = deepcopy(FAKE_CHARGE) fake_charge_copy.update({"invoice": None, "amount": 1000}) charge_create_mock.return_value = fake_charge_copy charge_retrieve_mock.return_value = fake_charge_copy fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_I) fake_payment_intent.update({"invoice": None}) payment_intent_retrieve_mock.return_value = fake_payment_intent self.customer.charge(amount=decimal.Decimal("10.00")) _, kwargs = charge_create_mock.call_args self.assertEqual(kwargs["amount"], 1000) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch("stripe.Charge.create", autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch("stripe.Invoice.retrieve", autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) def test_charge_doesnt_require_invoice( self, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_create_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, customer_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_charge_copy = deepcopy(FAKE_CHARGE) fake_charge_copy.update( {"invoice": FAKE_INVOICE["id"], "amount": FAKE_INVOICE["amount_due"]} ) fake_invoice_copy = deepcopy(FAKE_INVOICE) charge_create_mock.return_value = fake_charge_copy charge_retrieve_mock.return_value = fake_charge_copy invoice_retrieve_mock.return_value = fake_invoice_copy try: self.customer.charge(amount=decimal.Decimal("20.00")) except Invoice.DoesNotExist: self.fail(msg="Stripe Charge shouldn't throw Invoice DoesNotExist.") @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch("stripe.Charge.create", autospec=True) @patch("stripe.PaymentIntent.retrieve", autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) def test_charge_passes_extra_arguments( self, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_create_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_charge_copy = deepcopy(FAKE_CHARGE) fake_charge_copy.update({"invoice": None}) charge_create_mock.return_value = fake_charge_copy charge_retrieve_mock.return_value = fake_charge_copy fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_I) fake_payment_intent.update({"invoice": None}) payment_intent_retrieve_mock.return_value = fake_payment_intent self.customer.charge( amount=decimal.Decimal("10.00"), capture=True, destination=FAKE_PLATFORM_ACCOUNT["id"], ) _, kwargs = charge_create_mock.call_args self.assertEqual(kwargs["capture"], True) self.assertEqual(kwargs["destination"], FAKE_PLATFORM_ACCOUNT["id"]) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch("stripe.Charge.create", autospec=True) @patch("stripe.PaymentIntent.retrieve", autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) def test_charge_string_source( self, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_create_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_charge_copy = deepcopy(FAKE_CHARGE) fake_charge_copy.update({"invoice": None}) charge_create_mock.return_value = fake_charge_copy charge_retrieve_mock.return_value = fake_charge_copy fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_I) fake_payment_intent.update({"invoice": None}) payment_intent_retrieve_mock.return_value = fake_payment_intent self.customer.charge(amount=decimal.Decimal("10.00"), source=self.card.id) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", autospec=True) @patch("stripe.Charge.create", autospec=True) @patch("stripe.PaymentIntent.retrieve", autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) def test_charge_card_source( self, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_create_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_charge_copy = deepcopy(FAKE_CHARGE) fake_charge_copy.update({"invoice": None}) charge_create_mock.return_value = fake_charge_copy charge_retrieve_mock.return_value = fake_charge_copy fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_I) fake_payment_intent.update({"invoice": None}) payment_intent_retrieve_mock.return_value = fake_payment_intent self.customer.charge(amount=decimal.Decimal("10.00"), source=self.card) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE_III), ) @patch( "stripe.Invoice.list", return_value=StripeList( data=[deepcopy(FAKE_INVOICE), deepcopy(FAKE_INVOICE_III)] ), autospec=True, ) @patch("djstripe.models.Invoice.retry", autospec=True) def test_retry_unpaid_invoices( self, invoice_retry_mock, invoice_list_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, customer_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account self.customer.retry_unpaid_invoices() invoice = Invoice.objects.get(id=FAKE_INVOICE_III["id"]) invoice_retry_mock.assert_called_once_with(invoice) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE) ) @patch( "stripe.Invoice.list", return_value=StripeList(data=[deepcopy(FAKE_INVOICE)]), autospec=True, ) @patch("djstripe.models.Invoice.retry", autospec=True) def test_retry_unpaid_invoices_none_unpaid( self, invoice_retry_mock, invoice_list_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, customer_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account self.customer.retry_unpaid_invoices() self.assertFalse(invoice_retry_mock.called) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE_III), ) @patch( "stripe.Invoice.list", return_value=StripeList(data=[deepcopy(FAKE_INVOICE_III)]), ) @patch("djstripe.models.Invoice.retry", autospec=True) def test_retry_unpaid_invoices_expected_exception( self, invoice_retry_mock, invoice_list_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, charge_retrieve_mock, customer_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_subscription = deepcopy(FAKE_SUBSCRIPTION) # latest invoice should be the unpaid one fake_subscription["latest_invoice"] = FAKE_INVOICE_III subscription_retrieve_mock.return_value = fake_subscription invoice_retry_mock.side_effect = InvalidRequestError( "Invoice is already paid", "blah" ) try: self.customer.retry_unpaid_invoices() except Exception: self.fail("Exception was unexpectedly raised.") @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE_III), ) @patch( "stripe.Invoice.list", return_value=StripeList(data=[deepcopy(FAKE_INVOICE_III)]), ) @patch("djstripe.models.Invoice.retry", autospec=True) def test_retry_unpaid_invoices_unexpected_exception( self, invoice_retry_mock, invoice_list_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, charge_retrieve_mock, customer_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_subscription = deepcopy(FAKE_SUBSCRIPTION) # latest invoice should be the unpaid one fake_subscription["latest_invoice"] = FAKE_INVOICE_III subscription_retrieve_mock.return_value = fake_subscription invoice_retry_mock.side_effect = InvalidRequestError( "This should fail!", "blah" ) with self.assertRaisesMessage(InvalidRequestError, "This should fail!"): self.customer.retry_unpaid_invoices() @patch("stripe.Invoice.create", autospec=True) def test_send_invoice_success(self, invoice_create_mock): return_status = self.customer.send_invoice() self.assertTrue(return_status) invoice_create_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, customer=self.customer.id, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) @patch("stripe.Invoice.create", autospec=True) def test_send_invoice_failure(self, invoice_create_mock): invoice_create_mock.side_effect = InvalidRequestError( "Invoice creation failed.", "blah" ) return_status = self.customer.send_invoice() self.assertFalse(return_status) invoice_create_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, customer=self.customer.id, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) @patch("stripe.Coupon.retrieve", return_value=deepcopy(FAKE_COUPON), autospec=True) def test_sync_customer_with_discount(self, coupon_retrieve_mock): self.assertIsNone(self.customer.coupon) fake_customer = deepcopy(FAKE_CUSTOMER) fake_customer["discount"] = deepcopy(FAKE_DISCOUNT_CUSTOMER) customer = Customer.sync_from_stripe_data(fake_customer) self.assertEqual(customer.coupon.id, FAKE_COUPON["id"]) self.assertIsNotNone(customer.coupon_start) self.assertIsNone(customer.coupon_end) @patch("stripe.Coupon.retrieve", return_value=deepcopy(FAKE_COUPON), autospec=True) def test_sync_customer_discount_already_present(self, coupon_retrieve_mock): fake_customer = deepcopy(FAKE_CUSTOMER) fake_customer["discount"] = deepcopy(FAKE_DISCOUNT_CUSTOMER) # Set the customer's coupon to be what we'll sync customer = Customer.objects.get(id=FAKE_CUSTOMER["id"]) customer.coupon = Coupon.sync_from_stripe_data(FAKE_COUPON) customer.save() customer = Customer.sync_from_stripe_data(fake_customer) self.assertEqual(customer.coupon.id, FAKE_COUPON["id"]) def test_sync_customer_delete_discount(self): test_coupon = Coupon.sync_from_stripe_data(FAKE_COUPON) self.customer.coupon = test_coupon self.customer.save() self.assertEqual(self.customer.coupon.id, FAKE_COUPON["id"]) customer = Customer.sync_from_stripe_data(FAKE_CUSTOMER) self.assertEqual(customer.coupon, None) @patch( "djstripe.models.Invoice.sync_from_stripe_data", autospec=True, ) @patch( "stripe.Invoice.list", return_value=StripeList( data=[deepcopy(FAKE_INVOICE), deepcopy(FAKE_INVOICE_III)] ), ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_sync_invoices( self, customer_retrieve_mock, invoice_list_mock, invoice_sync_mock ): self.customer._sync_invoices() self.assertEqual(2, invoice_sync_mock.call_count) @patch( "djstripe.models.Invoice.sync_from_stripe_data", autospec=True, ) @patch("stripe.Invoice.list", return_value=StripeList(data=[]), autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_sync_invoices_none( self, customer_retrieve_mock, invoice_list_mock, invoice_sync_mock ): self.customer._sync_invoices() self.assertEqual(0, invoice_sync_mock.call_count) @patch( "djstripe.models.Charge.sync_from_stripe_data", autospec=True, ) @patch( "stripe.Charge.list", return_value=StripeList(data=[deepcopy(FAKE_CHARGE)]), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_sync_charges( self, customer_retrieve_mock, charge_list_mock, charge_sync_mock ): self.customer._sync_charges() self.assertEqual(1, charge_sync_mock.call_count) @patch( "djstripe.models.Charge.sync_from_stripe_data", autospec=True, ) @patch("stripe.Charge.list", return_value=StripeList(data=[]), autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_sync_charges_none( self, customer_retrieve_mock, charge_list_mock, charge_sync_mock ): self.customer._sync_charges() self.assertEqual(0, charge_sync_mock.call_count) @patch( "djstripe.models.Subscription.sync_from_stripe_data", autospec=True, ) @patch( "stripe.Subscription.list", return_value=StripeList( data=[deepcopy(FAKE_SUBSCRIPTION), deepcopy(FAKE_SUBSCRIPTION_II)] ), ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_sync_subscriptions( self, customer_retrieve_mock, subscription_list_mock, subscription_sync_mock ): self.customer._sync_subscriptions() self.assertEqual(2, subscription_sync_mock.call_count) @patch( "djstripe.models.Subscription.sync_from_stripe_data", autospec=True, ) @patch("stripe.Subscription.list", return_value=StripeList(data=[]), autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_sync_subscriptions_none( self, customer_retrieve_mock, subscription_list_mock, subscription_sync_mock ): self.customer._sync_subscriptions() self.assertEqual(0, subscription_sync_mock.call_count) @patch("stripe.Subscription.create", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_subscribe_price_string_new_style( self, product_retrieve_mock, customer_retrieve_mock, subscription_create_mock, ): fake_subscription = deepcopy(FAKE_SUBSCRIPTION) # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet fake_subscription["latest_invoice"] = None subscription_create_mock.return_value = fake_subscription current_subscriptions = self.customer.subscriptions.count() price = Price.sync_from_stripe_data(deepcopy(FAKE_PRICE)) self.assert_fks( price, expected_blank_fks={ "djstripe.Product.default_price", }, ) self.customer.subscribe(items=[{"price": price.id}]) updated_subscriptions = self.customer.subscriptions.count() # assert 1 new subscription got created assert updated_subscriptions == current_subscriptions + 1 @patch("stripe.Subscription.create", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_subscribe_price_string_old_style( self, product_retrieve_mock, customer_retrieve_mock, subscription_create_mock, ): fake_subscription = deepcopy(FAKE_SUBSCRIPTION) # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet fake_subscription["latest_invoice"] = None subscription_create_mock.return_value = fake_subscription current_subscriptions = self.customer.subscriptions.count() price = Price.sync_from_stripe_data(deepcopy(FAKE_PRICE)) self.assert_fks( price, expected_blank_fks={ "djstripe.Product.default_price", }, ) self.customer.subscribe(price=price.id) updated_subscriptions = self.customer.subscriptions.count() # assert 1 new subscription got created assert updated_subscriptions == current_subscriptions + 1 @patch("stripe.Subscription.create", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_subscription_shortcut_with_multiple_subscriptions_old_style( self, product_retrieve_mock, customer_retrieve_mock, subscription_create_mock ): price = Price.sync_from_stripe_data(deepcopy(FAKE_PRICE)) self.assert_fks( price, expected_blank_fks={ "djstripe.Product.default_price", }, ) subscription_fake_duplicate = deepcopy(FAKE_SUBSCRIPTION) subscription_fake_duplicate["id"] = "sub_6lsC8pt7IcF8jd" # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet subscription_fake_duplicate["latest_invoice"] = None fake_subscription = deepcopy(FAKE_SUBSCRIPTION) # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet fake_subscription["latest_invoice"] = None subscription_create_mock.side_effect = [ fake_subscription, subscription_fake_duplicate, ] self.customer.subscribe(price=price) self.customer.subscribe(price=price) self.assertEqual(2, self.customer.subscriptions.count()) self.assertEqual(2, len(self.customer.valid_subscriptions)) with self.assertRaises(MultipleSubscriptionException): self.customer.subscription @patch.object(Subscription, "_api_create", autospec=True) @patch("stripe.Subscription.create", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_subscription_shortcut_with_multiple_subscriptions_new_style_by_price( self, product_retrieve_mock, customer_retrieve_mock, subscription_create_mock, subscription_api_create_mock, ): price = Price.sync_from_stripe_data(deepcopy(FAKE_PRICE)) self.assert_fks( price, expected_blank_fks={ "djstripe.Product.default_price", }, ) subscription_fake_duplicate = deepcopy(FAKE_SUBSCRIPTION) subscription_fake_duplicate["id"] = "sub_6lsC8pt7IcF8jd" # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet subscription_fake_duplicate["latest_invoice"] = None fake_subscription = deepcopy(FAKE_SUBSCRIPTION) # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet fake_subscription["latest_invoice"] = None subscription_create_mock.side_effect = [ fake_subscription, subscription_fake_duplicate, ] subscription_api_create_mock.side_effect = [ fake_subscription, subscription_fake_duplicate, ] self.customer.subscribe(items=[{"price": price}, {"price": price}]) self.assertEqual(1, self.customer.subscriptions.count()) self.assertEqual(1, len(self.customer.valid_subscriptions)) subscription_api_create_mock.assert_called_once_with( items=[{"price": price.id}, {"price": price.id}], customer=self.customer.id ) @patch.object(Subscription, "_api_create", autospec=True) @patch("stripe.Subscription.create", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_subscription_shortcut_with_multiple_subscriptions_new_style_by_price_and_plan( self, product_retrieve_mock, customer_retrieve_mock, subscription_create_mock, subscription_api_create_mock, ): price = Price.sync_from_stripe_data(deepcopy(FAKE_PRICE)) plan = Plan.sync_from_stripe_data(deepcopy(FAKE_PLAN)) self.assert_fks( price, expected_blank_fks={ "djstripe.Product.default_price", }, ) self.assert_fks( plan, expected_blank_fks={ "djstripe.Product.default_price", }, ) subscription_fake_duplicate = deepcopy(FAKE_SUBSCRIPTION) subscription_fake_duplicate["id"] = "sub_6lsC8pt7IcF8jd" # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet subscription_fake_duplicate["latest_invoice"] = None fake_subscription = deepcopy(FAKE_SUBSCRIPTION) # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet fake_subscription["latest_invoice"] = None subscription_create_mock.side_effect = [ fake_subscription, subscription_fake_duplicate, ] subscription_api_create_mock.side_effect = [ fake_subscription, subscription_fake_duplicate, ] self.customer.subscribe(items=[{"price": price}, {"plan": plan}]) self.assertEqual(1, self.customer.subscriptions.count()) self.assertEqual(1, len(self.customer.valid_subscriptions)) subscription_api_create_mock.assert_called_once_with( items=[{"price": price.id}, {"plan": plan.id}], customer=self.customer.id ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_subscription_shortcut_with_invalid_subscriptions( self, product_retrieve_mock, customer_retrieve_mock ): price = Price.sync_from_stripe_data(deepcopy(FAKE_PRICE)) self.assert_fks( price, expected_blank_fks={ "djstripe.Product.default_price", }, ) fake_subscription_upd = deepcopy(FAKE_SUBSCRIPTION) # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet fake_subscription_upd["latest_invoice"] = None fake_subscriptions = [ deepcopy(fake_subscription_upd), deepcopy(fake_subscription_upd), deepcopy(fake_subscription_upd), ] # update the status of all but one to be invalid, # we need to also change the id for sync to work fake_subscriptions[1]["status"] = "canceled" fake_subscriptions[1]["id"] = fake_subscriptions[1]["id"] + "foo1" fake_subscriptions[2]["status"] = "incomplete_expired" fake_subscriptions[2]["id"] = fake_subscriptions[2]["id"] + "foo2" for _fake_subscription in fake_subscriptions: with patch( "stripe.Subscription.create", autospec=True, side_effect=[_fake_subscription], ): self.customer.subscribe(items=[{"price": price}]) self.assertEqual(3, self.customer.subscriptions.count()) self.assertEqual(1, len(self.customer.valid_subscriptions)) self.assertEqual( self.customer.valid_subscriptions[0], self.customer.subscription ) self.assertEqual(fake_subscriptions[0]["id"], self.customer.subscription.id) @patch( "djstripe.models.InvoiceItem.sync_from_stripe_data", return_value="pancakes", autospec=True, ) @patch( "stripe.InvoiceItem.create", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) def test_add_invoice_item(self, invoiceitem_create_mock, invoiceitem_sync_mock): invoiceitem = self.customer.add_invoice_item( amount=decimal.Decimal("50.00"), currency="eur", description="test", invoice=77, subscription=25, ) self.assertEqual("pancakes", invoiceitem) invoiceitem_create_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, amount=5000, customer=self.customer.id, currency="eur", description="test", discountable=None, invoice=77, metadata=None, subscription=25, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) @patch( "djstripe.models.InvoiceItem.sync_from_stripe_data", return_value="pancakes", autospec=True, ) @patch( "stripe.InvoiceItem.create", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) def test_add_invoice_item_djstripe_objects( self, invoiceitem_create_mock, invoiceitem_sync_mock ): invoiceitem = self.customer.add_invoice_item( amount=decimal.Decimal("50.00"), currency="eur", description="test", invoice=Invoice(id=77), subscription=Subscription(id=25), ) self.assertEqual("pancakes", invoiceitem) invoiceitem_create_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, amount=5000, customer=self.customer.id, currency="eur", description="test", discountable=None, invoice=77, metadata=None, subscription=25, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) def test_add_invoice_item_bad_decimal(self): with self.assertRaisesMessage( ValueError, "You must supply a decimal value representing dollars." ): self.customer.add_invoice_item(amount=5000, currency="usd") @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE) ) @patch( "stripe.Invoice.upcoming", autospec=True, ) def test_upcoming_invoice_plan( self, invoice_upcoming_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, ): fake_upcoming_invoice_data = deepcopy(FAKE_UPCOMING_INVOICE) fake_upcoming_invoice_data["lines"]["data"][0][ "subscription" ] = FAKE_SUBSCRIPTION["id"] invoice_upcoming_mock.return_value = fake_upcoming_invoice_data fake_subscription_item_data = deepcopy(FAKE_SUBSCRIPTION_ITEM) fake_subscription_item_data["plan"] = deepcopy(FAKE_PLAN) fake_subscription_item_data["subscription"] = deepcopy(FAKE_SUBSCRIPTION)["id"] subscription_item_retrieve_mock.return_value = fake_subscription_item_data invoice = self.customer.upcoming_invoice() self.assertIsNotNone(invoice) self.assertIsNone(invoice.id) self.assertIsNone(invoice.save()) # one more because of creating the associated line item subscription_retrieve_mock.assert_has_calls( [ call( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_SUBSCRIPTION["id"], stripe_account=None, stripe_version="2020-08-27", ), call( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_SUBSCRIPTION["id"], stripe_account=None, stripe_version="2020-08-27", ), ] ) plan_retrieve_mock.assert_not_called() items = invoice.lineitems.all() self.assertEqual(1, len(items)) self.assertEqual("il_fakefakefakefakefake0002", items[0].id) self.assertEqual(0, invoice.invoiceitems.count()) self.assertIsNotNone(invoice.plan) self.assertEqual(FAKE_PLAN["id"], invoice.plan.id) invoice._lineitems = [] items = invoice.lineitems.all() self.assertEqual(0, len(items)) self.assertIsNotNone(invoice.plan) @patch("stripe.Subscription.create", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_is_subscribed_to_with_product_old_style( self, product_retrieve_mock, customer_retrieve_mock, subscription_create_mock, ): price = Price.sync_from_stripe_data(deepcopy(FAKE_PRICE)) product = Product.sync_from_stripe_data(deepcopy(FAKE_PRODUCT)) subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription_fake["current_period_end"] = datetime_to_unix( timezone.now() + timezone.timedelta(days=7) ) # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet subscription_fake["latest_invoice"] = None subscription_create_mock.return_value = subscription_fake self.customer.subscribe(items=[{"price": price}]) assert self.customer.is_subscribed_to(product) @patch("stripe.Subscription.create", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_is_subscribed_to_with_product_new_style( self, product_retrieve_mock, customer_retrieve_mock, subscription_create_mock ): price = Price.sync_from_stripe_data(deepcopy(FAKE_PRICE)) product = Product.sync_from_stripe_data(deepcopy(FAKE_PRODUCT)) subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription_fake["current_period_end"] = datetime_to_unix( timezone.now() + timezone.timedelta(days=7) ) # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet subscription_fake["latest_invoice"] = None subscription_create_mock.return_value = subscription_fake self.customer.subscribe(items=[{"price": price}]) assert self.customer.is_subscribed_to(product) @patch("stripe.Subscription.create", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_is_subscribed_to_with_product_string_new_style( self, product_retrieve_mock, customer_retrieve_mock, subscription_create_mock ): price = Price.sync_from_stripe_data(deepcopy(FAKE_PRICE)) product = Product.sync_from_stripe_data(deepcopy(FAKE_PRODUCT)) subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription_fake["current_period_end"] = datetime_to_unix( timezone.now() + timezone.timedelta(days=7) ) # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet subscription_fake["latest_invoice"] = None subscription_create_mock.return_value = subscription_fake self.customer.subscribe(items=[{"price": price}]) assert self.customer.is_subscribed_to(product.id) @patch("stripe.Subscription.create", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_is_subscribed_to_with_product_string_by_price( self, product_retrieve_mock, customer_retrieve_mock, subscription_create_mock ): price = Price.sync_from_stripe_data(deepcopy(FAKE_PRICE)) product = Product.sync_from_stripe_data(deepcopy(FAKE_PRODUCT)) subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription_fake["current_period_end"] = datetime_to_unix( timezone.now() + timezone.timedelta(days=7) ) # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet subscription_fake["latest_invoice"] = None subscription_create_mock.return_value = subscription_fake self.customer.subscribe(price=price) assert self.customer.is_subscribed_to(product.id) # These tests use Plan which is deprecated in favor of Price class TestCustomerLegacy(AssertStripeFksMixin, TestCase): def setUp(self): # create a Stripe Platform Account self.account = FAKE_PLATFORM_ACCOUNT.create() self.user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) self.customer = FAKE_CUSTOMER.create_for_user(self.user) self.payment_method, _ = DjstripePaymentMethod._get_or_create_source( FAKE_CARD, "card" ) self.card = self.payment_method.resolve() self.customer.default_source = self.payment_method self.customer.save() @patch( "stripe.Subscription.create", autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_subscribe_plan_string_new_style( self, product_retrieve_mock, customer_retrieve_mock, subscription_create_mock, ): plan = Plan.sync_from_stripe_data(deepcopy(FAKE_PLAN)) self.assert_fks( plan, expected_blank_fks={ "djstripe.Product.default_price", }, ) fake_subscription = deepcopy(FAKE_SUBSCRIPTION) # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet fake_subscription["latest_invoice"] = None subscription_create_mock.return_value = fake_subscription current_subscriptions = self.customer.subscriptions.count() self.customer.subscribe(items=[{"plan": plan.id}]) updated_subscriptions = self.customer.subscriptions.count() # assert 1 new subscription got created assert updated_subscriptions == current_subscriptions + 1 @patch( "stripe.Subscription.create", autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_subscribe_plan_string_old_style( self, product_retrieve_mock, customer_retrieve_mock, subscription_create_mock, ): plan = Plan.sync_from_stripe_data(deepcopy(FAKE_PLAN)) self.assert_fks( plan, expected_blank_fks={ "djstripe.Product.default_price", }, ) fake_subscription = deepcopy(FAKE_SUBSCRIPTION) # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet fake_subscription["latest_invoice"] = None subscription_create_mock.return_value = fake_subscription current_subscriptions = self.customer.subscriptions.count() self.customer.subscribe(plan=plan.id) updated_subscriptions = self.customer.subscriptions.count() # assert 1 new subscription got created assert updated_subscriptions == current_subscriptions + 1 @patch.object(Subscription, "_api_create", autospec=True) @patch("stripe.Subscription.create", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_subscription_shortcut_with_multiple_subscriptions_new_style_by_plan( self, product_retrieve_mock, customer_retrieve_mock, subscription_create_mock, subscription_api_create_mock, ): plan = Plan.sync_from_stripe_data(deepcopy(FAKE_PLAN)) self.assert_fks( plan, expected_blank_fks={ "djstripe.Product.default_price", }, ) subscription_fake_duplicate = deepcopy(FAKE_SUBSCRIPTION) subscription_fake_duplicate["id"] = "sub_6lsC8pt7IcF8jd" # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet subscription_fake_duplicate["latest_invoice"] = None fake_subscription = deepcopy(FAKE_SUBSCRIPTION) # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet fake_subscription["latest_invoice"] = None subscription_create_mock.side_effect = [ fake_subscription, subscription_fake_duplicate, ] subscription_api_create_mock.side_effect = [ fake_subscription, subscription_fake_duplicate, ] self.customer.subscribe(items=[{"plan": plan}, {"plan": plan}]) self.assertEqual(1, self.customer.subscriptions.count()) self.assertEqual(1, len(self.customer.valid_subscriptions)) subscription_api_create_mock.assert_called_once_with( items=[{"plan": plan.id}, {"plan": plan.id}], customer=self.customer.id ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_subscription_shortcut_with_invalid_subscriptions( self, product_retrieve_mock, customer_retrieve_mock ): plan = Plan.sync_from_stripe_data(deepcopy(FAKE_PLAN)) self.assert_fks( plan, expected_blank_fks={ "djstripe.Product.default_price", }, ) fake_subscription_upd = deepcopy(FAKE_SUBSCRIPTION) # latest_invoice has to be None for an invoice that doesn't exist yet # and hence cannot have been billed yet fake_subscription_upd["latest_invoice"] = None fake_subscriptions = [ deepcopy(fake_subscription_upd), deepcopy(fake_subscription_upd), deepcopy(fake_subscription_upd), ] # update the status of all but one to be invalid, # we need to also change the id for sync to work fake_subscriptions[1]["status"] = "canceled" fake_subscriptions[1]["id"] = fake_subscriptions[1]["id"] + "foo1" fake_subscriptions[2]["status"] = "incomplete_expired" fake_subscriptions[2]["id"] = fake_subscriptions[2]["id"] + "foo2" for _fake_subscription in fake_subscriptions: with patch( "stripe.Subscription.create", autospec=True, side_effect=[_fake_subscription], ): self.customer.subscribe(items=[{"plan": plan}]) self.assertEqual(3, self.customer.subscriptions.count()) self.assertEqual(1, len(self.customer.valid_subscriptions)) self.assertEqual( self.customer.valid_subscriptions[0], self.customer.subscription ) self.assertEqual(fake_subscriptions[0]["id"], self.customer.subscription.id) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE) ) @patch( "stripe.Invoice.upcoming", autospec=True, ) def test_upcoming_invoice( self, invoice_upcoming_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, ): fake_upcoming_invoice_data = deepcopy(FAKE_UPCOMING_INVOICE) fake_upcoming_invoice_data["lines"]["data"][0][ "subscription" ] = FAKE_SUBSCRIPTION["id"] invoice_upcoming_mock.return_value = fake_upcoming_invoice_data fake_subscription_item_data = deepcopy(FAKE_SUBSCRIPTION_ITEM) fake_subscription_item_data["plan"] = deepcopy(FAKE_PLAN) fake_subscription_item_data["subscription"] = deepcopy(FAKE_SUBSCRIPTION)["id"] subscription_item_retrieve_mock.return_value = fake_subscription_item_data invoice = self.customer.upcoming_invoice() self.assertIsNotNone(invoice) self.assertIsNone(invoice.id) self.assertIsNone(invoice.save()) # one more because of creating the associated line item subscription_retrieve_mock.assert_has_calls( [ call( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_SUBSCRIPTION["id"], stripe_account=None, stripe_version="2020-08-27", ), call( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_SUBSCRIPTION["id"], stripe_account=None, stripe_version="2020-08-27", ), ] ) plan_retrieve_mock.assert_not_called() items = invoice.lineitems.all() self.assertEqual(1, len(items)) self.assertEqual("il_fakefakefakefakefake0002", items[0].id) self.assertEqual(0, invoice.invoiceitems.count()) self.assertIsNotNone(invoice.plan) self.assertEqual(FAKE_PLAN["id"], invoice.plan.id) invoice._lineitems = [] items = invoice.lineitems.all() self.assertEqual(0, len(items)) self.assertIsNotNone(invoice.plan) ================================================ FILE: tests/test_discount.py ================================================ """ dj-stripe Discount model tests """ from copy import deepcopy from unittest.mock import patch import pytest from django.test.testcases import TestCase from djstripe.models.billing import Discount from . import ( FAKE_BALANCE_TRANSACTION, FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CHARGE, FAKE_CUSTOMER, FAKE_DISCOUNT, FAKE_INVOICE, FAKE_INVOICEITEM, FAKE_PAYMENT_INTENT_I, FAKE_PRODUCT, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_ITEM, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class TestDiscount(AssertStripeFksMixin, TestCase): @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE) ) def test_sync_from_stripe_data( self, invoice_retrieve_mock, invoice_item_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, ): fake_discount_data = deepcopy(FAKE_DISCOUNT) discount = Discount.sync_from_stripe_data(fake_discount_data) self.assertEqual(discount.id, fake_discount_data["id"]) self.assertEqual(discount.customer.id, fake_discount_data["customer"]["id"]) self.assertEqual(discount.subscription.id, fake_discount_data["subscription"]) self.assert_fks( discount, expected_blank_fks={ "djstripe.Customer.default_payment_method", "djstripe.Customer.subscriber", "djstripe.Customer.coupon", "djstripe.Customer.discount", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.Discount.checkout_session", "djstripe.Discount.invoice", "djstripe.Discount.invoice_item", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.Product.default_price", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.Invoice.subscription", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", }, ) ================================================ FILE: tests/test_dispute.py ================================================ """ dj-stripe Dispute model tests """ from copy import deepcopy from unittest.mock import patch import pytest from django.contrib.auth import get_user_model from django.test.testcases import TestCase from djstripe.models import Dispute from djstripe.settings import djstripe_settings from . import ( FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CUSTOMER, FAKE_DISPUTE_BALANCE_TRANSACTION, FAKE_DISPUTE_CHARGE, FAKE_DISPUTE_I, FAKE_DISPUTE_III, FAKE_DISPUTE_PAYMENT_INTENT, FAKE_FILEUPLOAD_ICON, ) pytestmark = pytest.mark.django_db class TestDispute(TestCase): def setUp(self): self.user = get_user_model().objects.create_user( username="fake_customer_1", email=FAKE_CUSTOMER["email"] ) self.customer = FAKE_CUSTOMER.create_for_user(self.user) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_DISPUTE_PAYMENT_INTENT), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_DISPUTE_CHARGE), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_DISPUTE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_ICON), autospec=True, ) @patch( "stripe.Dispute.retrieve", return_value=deepcopy(FAKE_DISPUTE_I), autospec=True ) def test___str__( self, dispute_retrieve_mock, file_retrieve_mock, balance_transaction_retrieve_mock, charge_retrieve_mock, payment_intent_retrieve_mock, payment_method_retrieve_mock, ): dispute = Dispute.sync_from_stripe_data(FAKE_DISPUTE_I) self.assertEqual(str(dispute), "$10.00 USD (Needs response) ") @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_DISPUTE_PAYMENT_INTENT), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_DISPUTE_CHARGE), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_DISPUTE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_ICON), autospec=True, ) @patch( "stripe.Dispute.retrieve", return_value=deepcopy(FAKE_DISPUTE_I), autospec=True ) def test_sync_from_stripe_data( self, dispute_retrieve_mock, file_retrieve_mock, balance_transaction_retrieve_mock, charge_retrieve_mock, payment_intent_retrieve_mock, payment_method_retrieve_mock, ): dispute = Dispute.sync_from_stripe_data(FAKE_DISPUTE_I) assert dispute.id == FAKE_DISPUTE_I["id"] @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_DISPUTE_PAYMENT_INTENT), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_DISPUTE_CHARGE), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_DISPUTE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_ICON), autospec=True, ) @patch( "stripe.Dispute.retrieve", return_value=deepcopy(FAKE_DISPUTE_III), autospec=True, ) def test__attach_objects_post_save_hook( self, dispute_retrieve_mock, file_retrieve_mock, balance_transaction_retrieve_mock, charge_retrieve_mock, payment_intent_retrieve_mock, payment_method_retrieve_mock, ): dispute = Dispute.sync_from_stripe_data(FAKE_DISPUTE_III) assert dispute.id == FAKE_DISPUTE_III["id"] # assert File was retrieved correctly file_retrieve_mock.assert_called_once_with( id=FAKE_DISPUTE_III["evidence"]["receipt"], api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], stripe_account=None, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) # assert Balance Transactions were retrieved correctly balance_transaction_retrieve_mock.assert_called_once_with( id=FAKE_DISPUTE_BALANCE_TRANSACTION["id"], api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, expand=[], stripe_account=None, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_DISPUTE_PAYMENT_INTENT), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_DISPUTE_CHARGE), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_DISPUTE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_ICON), autospec=True, ) @patch( "stripe.Dispute.retrieve", return_value=deepcopy(FAKE_DISPUTE_I), autospec=True ) def test_get_stripe_dashboard_url( self, dispute_retrieve_mock, file_retrieve_mock, balance_transaction_retrieve_mock, charge_retrieve_mock, payment_intent_retrieve_mock, payment_method_retrieve_mock, ): dispute = Dispute.sync_from_stripe_data(FAKE_DISPUTE_I) self.assertEqual( dispute.get_stripe_dashboard_url(), f"{dispute._get_base_stripe_dashboard_url()}" f"{dispute.stripe_dashboard_item_name}/{dispute.payment_intent.id}", ) ================================================ FILE: tests/test_django.py ================================================ from django.core.management import call_command from django.test import TestCase from django.test.utils import override_settings class TestRunManagePyCheck(TestCase): @override_settings( STRIPE_TEST_SECRET_KEY="sk_test_foo", STRIPE_LIVE_SECRET_KEY="sk_live_foo", STRIPE_TEST_PUBLIC_KEY="pk_test_foo", STRIPE_LIVE_PUBLIC_KEY="pk_live_foo", STRIPE_LIVE_MODE=True, ) def test_manage_py_check(self): call_command("check") ================================================ FILE: tests/test_enums.py ================================================ from collections import OrderedDict from django.test import TestCase from django.utils.translation import gettext_lazy as _ from djstripe.enums import Enum, EnumMetaClass class TestEnumMetaClass(TestCase): def test_python2_prepare(self): # Python 2 hack to ensure __prepare__ is called... self.assertEqual(EnumMetaClass.__prepare__(None, None), OrderedDict()) class TestEnumHumanize(TestCase): def test_humanize(self): class TestEnum(Enum): red = _("Red") blue = _("Blue") self.assertEqual(TestEnum.humanize("red"), _("Red")) ================================================ FILE: tests/test_event.py ================================================ """ dj-stripe Event Model Tests. """ from copy import deepcopy from unittest.mock import patch from django.contrib.auth import get_user_model from django.test import TestCase from stripe.error import StripeError from djstripe import webhooks from djstripe.models import Event, Transfer from djstripe.settings import djstripe_settings from . import ( FAKE_CUSTOMER, FAKE_EVENT_TRANSFER_CREATED, FAKE_PLATFORM_ACCOUNT, FAKE_TRANSFER, ) class EventTest(TestCase): def setUp(self): self.user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) self.customer = FAKE_CUSTOMER.create_for_user(self.user) patcher = patch.object(webhooks, "call_handlers") self.addCleanup(patcher.stop) self.call_handlers = patcher.start() def test___str__(self): event = self._create_event(FAKE_EVENT_TRANSFER_CREATED) self.assertEqual( f"type={FAKE_EVENT_TRANSFER_CREATED['type']}, id={FAKE_EVENT_TRANSFER_CREATED['id']}", str(event), ) def test_invoke_webhook_handlers_event_with_log_stripe_error(self): event = self._create_event(FAKE_EVENT_TRANSFER_CREATED) self.call_handlers.side_effect = StripeError("Boom!") with self.assertRaises(StripeError): event.invoke_webhook_handlers() def test_invoke_webhook_handlers_event_with_raise_stripe_error(self): event = self._create_event(FAKE_EVENT_TRANSFER_CREATED) self.call_handlers.side_effect = StripeError("Boom!") with self.assertRaises(StripeError): event.invoke_webhook_handlers() def test_invoke_webhook_handlers_event_when_invalid(self): event = self._create_event(FAKE_EVENT_TRANSFER_CREATED) event.valid = False event.invoke_webhook_handlers() @patch(target="djstripe.models.core.transaction.atomic", autospec=True) @patch.object(target=Event, attribute="_create_from_stripe_object", autospec=True) @patch.object(target=Event, attribute="objects", autospec=True) def test_process_event( self, mock_objects, mock__create_from_stripe_object, mock_atomic ): """Test that process event creates a new event and invokes webhooks when the event doesn't already exist. """ # Set up mocks mock_objects.filter.return_value.exists.return_value = False mock_data = {"id": "foo_id", "other_stuff": "more_things"} result = Event.process(data=mock_data) # Check that all the expected work was performed mock_objects.filter.assert_called_once_with(id=mock_data["id"]) mock_objects.filter.return_value.exists.assert_called_once_with() mock_atomic.return_value.__enter__.assert_called_once_with() mock__create_from_stripe_object.assert_called_once_with( mock_data, api_key=djstripe_settings.STRIPE_SECRET_KEY ) ( mock__create_from_stripe_object.return_value.invoke_webhook_handlers ).assert_called_once_with() # Make sure the event was returned. self.assertEqual(mock__create_from_stripe_object.return_value, result) @patch(target="djstripe.models.core.transaction.atomic", autospec=True) @patch.object(target=Event, attribute="_create_from_stripe_object", autospec=True) @patch.object(target=Event, attribute="objects", autospec=True) def test_process_event_exists( self, mock_objects, mock__create_from_stripe_object, mock_atomic ): """ Test that process event returns the existing event and skips webhook processing when the event already exists. """ # Set up mocks mock_objects.filter.return_value.exists.return_value = True mock_data = {"id": "foo_id", "other_stuff": "more_things"} result = Event.process(data=mock_data) # Make sure that the db was queried and the existing results used. mock_objects.filter.assert_called_once_with(id=mock_data["id"]) mock_objects.filter.return_value.exists.assert_called_once_with() mock_objects.filter.return_value.first.assert_called_once_with() # Make sure the webhook actions and event object creation were not performed. mock_atomic.return_value.__enter__.assert_not_called() mock__create_from_stripe_object.assert_not_called() ( mock__create_from_stripe_object.return_value.invoke_webhook_handlers ).assert_not_called() # Make sure the existing event was returned. self.assertEqual(mock_objects.filter.return_value.first.return_value, result) @patch("djstripe.models.Event.invoke_webhook_handlers", autospec=True) def test_process_event_failure_rolls_back(self, invoke_webhook_handlers_mock): """Test that process event rolls back event creation on error""" class HandlerException(Exception): pass invoke_webhook_handlers_mock.side_effect = HandlerException real_create_from_stripe_object = Event._create_from_stripe_object def side_effect(*args, **kwargs): return real_create_from_stripe_object(*args, **kwargs) event_data = deepcopy(FAKE_EVENT_TRANSFER_CREATED) self.assertFalse( Event.objects.filter(id=FAKE_EVENT_TRANSFER_CREATED["id"]).exists() ) with self.assertRaises(HandlerException), patch( "djstripe.models.Event._create_from_stripe_object", side_effect=side_effect, autospec=True, ) as create_from_stripe_object_mock: Event.process(data=event_data) create_from_stripe_object_mock.assert_called_once_with( event_data, api_key=djstripe_settings.STRIPE_SECRET_KEY ) self.assertFalse( Event.objects.filter(id=FAKE_EVENT_TRANSFER_CREATED["id"]).exists() ) # # Helpers # @patch("stripe.Event.retrieve", autospec=True) def _create_event(self, event_data, event_retrieve_mock): event_data = deepcopy(event_data) event_retrieve_mock.return_value = event_data event = Event.sync_from_stripe_data(event_data) return event class EventRaceConditionTest(TestCase): @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_PLATFORM_ACCOUNT), autospec=True, ) @patch( "stripe.Transfer.retrieve", return_value=deepcopy(FAKE_TRANSFER), autospec=True ) def test_process_event_race_condition( self, transfer_retrieve_mock, account_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): transfer = Transfer.sync_from_stripe_data(deepcopy(FAKE_TRANSFER)) transfer_retrieve_mock.reset_mock() event_data = deepcopy(FAKE_EVENT_TRANSFER_CREATED) # emulate the race condition in _get_or_create_from_stripe_object where # an object is created by a different request during the call # # Sequence of events: # 1) first Transfer.stripe_objects.get fails with DoesNotExist # (due to it not existing in reality, but due to our side_effect in the test) # 2) object is really created by a different request in reality # 3) Transfer._create_from_stripe_object fails with IntegrityError due to # duplicate id # 4) second Transfer.stripe_objects.get succeeds # (due to being created by step 2 in reality, due to side effect in the test) side_effect = [Transfer.DoesNotExist(), transfer] with patch( "djstripe.models.Transfer.stripe_objects.get", side_effect=side_effect, autospec=True, ) as transfer_objects_get_mock: Event.process(event_data) self.assertEqual(transfer_objects_get_mock.call_count, 2) self.assertEqual(transfer_retrieve_mock.call_count, 1) ================================================ FILE: tests/test_event_handlers.py ================================================ """ dj-stripe Event Handler tests """ from copy import deepcopy from decimal import Decimal from unittest.mock import ANY, call, patch from django.contrib.auth import get_user_model from django.test import TestCase from stripe.error import InvalidRequestError from djstripe.enums import SubscriptionStatus from djstripe.models import ( Card, Charge, Coupon, Customer, Dispute, DjstripePaymentMethod, Event, Invoice, InvoiceItem, PaymentMethod, Plan, Price, Subscription, SubscriptionSchedule, Transfer, ) from djstripe.models.account import Account from djstripe.models.billing import TaxId from djstripe.models.checkout import Session from djstripe.models.core import File from djstripe.models.orders import Order from djstripe.models.payment_methods import BankAccount from djstripe.settings import djstripe_settings from . import ( FAKE_ACCOUNT, FAKE_BALANCE_TRANSACTION, FAKE_BANK_ACCOUNT_IV, FAKE_CARD, FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CARD_II, FAKE_CARD_III, FAKE_CARD_IV, FAKE_CHARGE, FAKE_CHARGE_II, FAKE_COUPON, FAKE_CUSTOM_ACCOUNT, FAKE_CUSTOMER, FAKE_CUSTOMER_II, FAKE_DISPUTE_BALANCE_TRANSACTION, FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_FULL, FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_PARTIAL, FAKE_DISPUTE_CHARGE, FAKE_DISPUTE_I, FAKE_DISPUTE_II, FAKE_DISPUTE_III, FAKE_DISPUTE_PAYMENT_INTENT, FAKE_DISPUTE_PAYMENT_METHOD, FAKE_DISPUTE_V_FULL, FAKE_DISPUTE_V_PARTIAL, FAKE_EVENT_ACCOUNT_APPLICATION_AUTHORIZED, FAKE_EVENT_ACCOUNT_APPLICATION_DEAUTHORIZED, FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_CREATED, FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_DELETED, FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_UPDATED, FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_CREATED, FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_DELETED, FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_UPDATED, FAKE_EVENT_CARD_PAYMENT_METHOD_ATTACHED, FAKE_EVENT_CARD_PAYMENT_METHOD_DETACHED, FAKE_EVENT_CHARGE_SUCCEEDED, FAKE_EVENT_CUSTOM_ACCOUNT_UPDATED, FAKE_EVENT_CUSTOMER_CREATED, FAKE_EVENT_CUSTOMER_DELETED, FAKE_EVENT_CUSTOMER_DISCOUNT_CREATED, FAKE_EVENT_CUSTOMER_DISCOUNT_DELETED, FAKE_EVENT_CUSTOMER_SOURCE_CREATED, FAKE_EVENT_CUSTOMER_SOURCE_DELETED, FAKE_EVENT_CUSTOMER_SOURCE_DELETED_DUPE, FAKE_EVENT_CUSTOMER_SUBSCRIPTION_CREATED, FAKE_EVENT_CUSTOMER_SUBSCRIPTION_DELETED, FAKE_EVENT_CUSTOMER_UPDATED, FAKE_EVENT_DISPUTE_CLOSED, FAKE_EVENT_DISPUTE_CREATED, FAKE_EVENT_DISPUTE_FUNDS_REINSTATED_FULL, FAKE_EVENT_DISPUTE_FUNDS_REINSTATED_PARTIAL, FAKE_EVENT_DISPUTE_FUNDS_WITHDRAWN, FAKE_EVENT_DISPUTE_UPDATED, FAKE_EVENT_EXPRESS_ACCOUNT_UPDATED, FAKE_EVENT_FILE_CREATED, FAKE_EVENT_INVOICE_CREATED, FAKE_EVENT_INVOICE_DELETED, FAKE_EVENT_INVOICE_UPCOMING, FAKE_EVENT_INVOICEITEM_CREATED, FAKE_EVENT_INVOICEITEM_DELETED, FAKE_EVENT_ORDER_CANCELLED, FAKE_EVENT_ORDER_COMPLETED, FAKE_EVENT_ORDER_CREATED, FAKE_EVENT_ORDER_PROCESSING, FAKE_EVENT_ORDER_SUBMITTED, FAKE_EVENT_ORDER_UPDATED, FAKE_EVENT_PAYMENT_INTENT_SUCCEEDED_DESTINATION_CHARGE, FAKE_EVENT_PAYMENT_METHOD_ATTACHED, FAKE_EVENT_PAYMENT_METHOD_DETACHED, FAKE_EVENT_PLAN_CREATED, FAKE_EVENT_PLAN_DELETED, FAKE_EVENT_PLAN_REQUEST_IS_OBJECT, FAKE_EVENT_PRICE_CREATED, FAKE_EVENT_PRICE_DELETED, FAKE_EVENT_PRICE_UPDATED, FAKE_EVENT_SESSION_COMPLETED, FAKE_EVENT_STANDARD_ACCOUNT_UPDATED, FAKE_EVENT_SUBSCRIPTION_SCHEDULE_ABORTED, FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CANCELED, FAKE_EVENT_SUBSCRIPTION_SCHEDULE_COMPLETED, FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED, FAKE_EVENT_SUBSCRIPTION_SCHEDULE_EXPIRING, FAKE_EVENT_SUBSCRIPTION_SCHEDULE_RELEASED, FAKE_EVENT_SUBSCRIPTION_SCHEDULE_UPDATED, FAKE_EVENT_TAX_ID_CREATED, FAKE_EVENT_TAX_ID_DELETED, FAKE_EVENT_TAX_ID_UPDATED, FAKE_EVENT_TRANSFER_CREATED, FAKE_EVENT_TRANSFER_DELETED, FAKE_EXPRESS_ACCOUNT, FAKE_FILEUPLOAD_ICON, FAKE_FILEUPLOAD_LOGO, FAKE_INVOICE, FAKE_INVOICE_II, FAKE_INVOICEITEM, FAKE_PAYMENT_INTENT_DESTINATION_CHARGE, FAKE_PAYMENT_INTENT_I, FAKE_PAYMENT_INTENT_II, FAKE_PAYMENT_METHOD_I, FAKE_PAYMENT_METHOD_II, FAKE_PLAN, FAKE_PLATFORM_ACCOUNT, FAKE_PRICE, FAKE_PRODUCT, FAKE_SESSION_I, FAKE_STANDARD_ACCOUNT, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_CANCELED, FAKE_SUBSCRIPTION_III, FAKE_SUBSCRIPTION_ITEM, FAKE_SUBSCRIPTION_SCHEDULE, FAKE_TAX_ID, FAKE_TAX_ID_UPDATED, FAKE_TRANSFER, AssertStripeFksMixin, ) class EventTestCase(TestCase): # # Helpers # @patch("stripe.Event.retrieve", autospec=True) def _create_event(self, event_data, event_retrieve_mock, patch_data=None): event_data = deepcopy(event_data) if patch_data: event_data.update(patch_data) event_retrieve_mock.return_value = event_data event = Event.sync_from_stripe_data(event_data) return event class TestAccountEvents(EventTestCase): def setUp(self): # create a Custom Stripe Account self.custom_account = FAKE_CUSTOM_ACCOUNT.create() # create a Standard Stripe Account self.standard_account = FAKE_STANDARD_ACCOUNT.create() # create an Express Stripe Account self.express_account = FAKE_EXPRESS_ACCOUNT.create() @patch("stripe.Event.retrieve", autospec=True) def test_account_deauthorized_event(self, event_retrieve_mock): fake_stripe_event = deepcopy(FAKE_EVENT_ACCOUNT_APPLICATION_DEAUTHORIZED) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() @patch("stripe.Event.retrieve", autospec=True) def test_account_authorized_event(self, event_retrieve_mock): fake_stripe_event = deepcopy(FAKE_EVENT_ACCOUNT_APPLICATION_AUTHORIZED) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() # account.external_account.* events are fired for Custom and Express Accounts @patch( "stripe.Account.retrieve_external_account", return_value=deepcopy(FAKE_BANK_ACCOUNT_IV), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_custom_account_external_account_created_bank_account_event( self, event_retrieve_mock, account_retrieve_external_account_mock ): fake_stripe_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_CREATED ) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() # fetch the newly created BankAccount object bankaccount = BankAccount.objects.get(account=self.custom_account) # assert the ids of the Bank Account and the Accounts were synced correctly. self.assertEqual( bankaccount.id, fake_stripe_event["data"]["object"]["id"], ) self.assertEqual( self.custom_account.id, fake_stripe_event["data"]["object"]["account"], ) @patch( "stripe.Account.retrieve_external_account", return_value=deepcopy(FAKE_BANK_ACCOUNT_IV), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_custom_account_external_account_deleted_bank_account_event( self, event_retrieve_mock, account_retrieve_external_account_mock ): fake_stripe_create_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_CREATED ) event = Event.sync_from_stripe_data(fake_stripe_create_event) event.invoke_webhook_handlers() fake_stripe_delete_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_DELETED ) event = Event.sync_from_stripe_data(fake_stripe_delete_event) event.invoke_webhook_handlers() # assert the BankAccount object no longer exists self.assertFalse( BankAccount.objects.filter( id=fake_stripe_create_event["data"]["object"]["id"] ).exists() ) @patch( "stripe.Account.retrieve_external_account", return_value=deepcopy(FAKE_BANK_ACCOUNT_IV), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_custom_account_external_account_updated_bank_account_event( self, event_retrieve_mock, account_retrieve_external_account_mock ): fake_stripe_create_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_CREATED ) event = Event.sync_from_stripe_data(fake_stripe_create_event) event.invoke_webhook_handlers() fake_stripe_update_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_UPDATED ) event = Event.sync_from_stripe_data(fake_stripe_update_event) event.invoke_webhook_handlers() # fetch the updated BankAccount object bankaccount = BankAccount.objects.get(account=self.custom_account) # assert we are updating the account_holder_name self.assertNotEqual( fake_stripe_update_event["data"]["object"]["account_holder_name"], fake_stripe_create_event["data"]["object"]["account_holder_name"], ) # assert the account_holder_name got updated self.assertNotEqual( bankaccount.account_holder_name, fake_stripe_update_event["data"]["object"]["account_holder_name"], ) # assert the expected BankAccount object got updated self.assertEqual( bankaccount.id, fake_stripe_create_event["data"]["object"]["id"] ) @patch( "stripe.Account.retrieve_external_account", return_value=deepcopy(FAKE_CARD_IV), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_custom_account_external_account_created_card_event( self, event_retrieve_mock, account_retrieve_external_account_mock ): fake_stripe_event = deepcopy(FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_CREATED) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() # fetch the newly created Card object card = Card.objects.get(account=self.custom_account) # assert the ids of the Card and the Accounts were synced correctly. self.assertEqual( card.id, fake_stripe_event["data"]["object"]["id"], ) self.assertEqual( self.custom_account.id, fake_stripe_event["data"]["object"]["account"], ) @patch( "stripe.Account.retrieve_external_account", return_value=deepcopy(FAKE_CARD_IV), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_custom_account_external_account_deleted_card_event( self, event_retrieve_mock, account_retrieve_external_account_mock ): fake_stripe_create_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_CREATED ) event = Event.sync_from_stripe_data(fake_stripe_create_event) event.invoke_webhook_handlers() fake_stripe_delete_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_DELETED ) event = Event.sync_from_stripe_data(fake_stripe_delete_event) event.invoke_webhook_handlers() # assert Card Object no longer exists self.assertFalse( Card.objects.filter( id=fake_stripe_create_event["data"]["object"]["id"] ).exists() ) @patch( "stripe.Account.retrieve_external_account", return_value=deepcopy(FAKE_CARD_IV), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_custom_account_external_account_updated_card_event( self, event_retrieve_mock, account_retrieve_external_account_mock ): fake_stripe_create_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_CREATED ) event = Event.sync_from_stripe_data(fake_stripe_create_event) event.invoke_webhook_handlers() fake_stripe_update_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_UPDATED ) event = Event.sync_from_stripe_data(fake_stripe_update_event) event.invoke_webhook_handlers() # fetch the updated Card object card = Card.objects.get(account=self.custom_account) # assert we are updating the name self.assertNotEqual( fake_stripe_update_event["data"]["object"]["name"], fake_stripe_create_event["data"]["object"]["name"], ) # assert the name got updated self.assertNotEqual( card.name, fake_stripe_update_event["data"]["object"]["name"] ) # assert the expected Card object got updated self.assertEqual(card.id, fake_stripe_create_event["data"]["object"]["id"]) @patch( "stripe.Account.retrieve_external_account", return_value=deepcopy(FAKE_BANK_ACCOUNT_IV), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_express_account_external_account_created_bank_account_event( self, event_retrieve_mock, account_retrieve_external_account_mock ): fake_stripe_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_CREATED ) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() # fetch the newly created BankAccount object bankaccount = BankAccount.objects.get(account=self.express_account) # assert the ids of the Bank Account and the Accounts were synced correctly. self.assertEqual( bankaccount.id, fake_stripe_event["data"]["object"]["id"], ) self.assertEqual( self.express_account.id, fake_stripe_event["data"]["object"]["account"], ) @patch( "stripe.Account.retrieve_external_account", return_value=deepcopy(FAKE_BANK_ACCOUNT_IV), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_express_account_external_account_deleted_bank_account_event( self, event_retrieve_mock, account_retrieve_external_account_mock ): fake_stripe_create_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_CREATED ) event = Event.sync_from_stripe_data(fake_stripe_create_event) event.invoke_webhook_handlers() fake_stripe_delete_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_DELETED ) event = Event.sync_from_stripe_data(fake_stripe_delete_event) event.invoke_webhook_handlers() # assert the BankAccount object no longer exists self.assertFalse( BankAccount.objects.filter( id=fake_stripe_create_event["data"]["object"]["id"] ).exists() ) @patch( "stripe.Account.retrieve_external_account", return_value=deepcopy(FAKE_BANK_ACCOUNT_IV), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_express_account_external_account_updated_bank_account_event( self, event_retrieve_mock, account_retrieve_external_account_mock ): fake_stripe_create_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_CREATED ) event = Event.sync_from_stripe_data(fake_stripe_create_event) event.invoke_webhook_handlers() fake_stripe_update_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_BANK_ACCOUNT_UPDATED ) event = Event.sync_from_stripe_data(fake_stripe_update_event) event.invoke_webhook_handlers() # fetch the updated BankAccount object bankaccount = BankAccount.objects.get(account=self.express_account) # assert we are updating the account_holder_name self.assertNotEqual( fake_stripe_update_event["data"]["object"]["account_holder_name"], fake_stripe_create_event["data"]["object"]["account_holder_name"], ) # assert the account_holder_name got updated self.assertNotEqual( bankaccount.account_holder_name, fake_stripe_update_event["data"]["object"]["account_holder_name"], ) # assert the expected BankAccount object got updated self.assertEqual( bankaccount.id, fake_stripe_create_event["data"]["object"]["id"] ) @patch( "stripe.Account.retrieve_external_account", return_value=deepcopy(FAKE_CARD_IV), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_express_account_external_account_created_card_event( self, event_retrieve_mock, account_retrieve_external_account_mock ): fake_stripe_event = deepcopy(FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_CREATED) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() # fetch the newly created Card object card = Card.objects.get(account=self.express_account) # assert the ids of the Card and the Accounts were synced correctly. self.assertEqual( card.id, fake_stripe_event["data"]["object"]["id"], ) self.assertEqual( self.express_account.id, fake_stripe_event["data"]["object"]["account"], ) @patch( "stripe.Account.retrieve_external_account", return_value=deepcopy(FAKE_CARD_IV), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_express_account_external_account_deleted_card_event( self, event_retrieve_mock, account_retrieve_external_account_mock ): fake_stripe_create_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_CREATED ) event = Event.sync_from_stripe_data(fake_stripe_create_event) event.invoke_webhook_handlers() fake_stripe_delete_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_DELETED ) event = Event.sync_from_stripe_data(fake_stripe_delete_event) event.invoke_webhook_handlers() # assert Card Object no longer exists self.assertFalse( Card.objects.filter( id=fake_stripe_create_event["data"]["object"]["id"] ).exists() ) @patch( "stripe.Account.retrieve_external_account", return_value=deepcopy(FAKE_CARD_IV), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_express_account_external_account_updated_card_event( self, event_retrieve_mock, account_retrieve_external_account_mock ): fake_stripe_create_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_CREATED ) event = Event.sync_from_stripe_data(fake_stripe_create_event) event.invoke_webhook_handlers() fake_stripe_update_event = deepcopy( FAKE_EVENT_ACCOUNT_EXTERNAL_ACCOUNT_CARD_UPDATED ) event = Event.sync_from_stripe_data(fake_stripe_update_event) event.invoke_webhook_handlers() # fetch the updated Card object card = Card.objects.get(account=self.express_account) # assert we are updating the name self.assertNotEqual( fake_stripe_update_event["data"]["object"]["name"], fake_stripe_create_event["data"]["object"]["name"], ) # assert the name got updated self.assertNotEqual( card.name, fake_stripe_update_event["data"]["object"]["name"] ) # assert the expected Card object got updated self.assertEqual(card.id, fake_stripe_create_event["data"]["object"]["id"]) # account.updated events @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_EVENT_STANDARD_ACCOUNT_UPDATED["data"]["object"]), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_standard_account_updated_event( self, event_retrieve_mock, account_retrieve_mock ): # fetch the Stripe Account standard_account = self.standard_account # assert metadata is empty self.assertEqual(standard_account.metadata, {}) fake_stripe_update_event = deepcopy(FAKE_EVENT_STANDARD_ACCOUNT_UPDATED) event = Event.sync_from_stripe_data(fake_stripe_update_event) event.invoke_webhook_handlers() # fetch the updated Account object updated_standard_account = Account.objects.get(id=standard_account.id) # assert we are updating the metadata self.assertNotEqual( updated_standard_account.metadata, standard_account.metadata, ) # assert the meta got updated self.assertEqual( updated_standard_account.metadata, fake_stripe_update_event["data"]["object"]["metadata"], ) @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_EVENT_EXPRESS_ACCOUNT_UPDATED["data"]["object"]), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_express_account_updated_event( self, event_retrieve_mock, account_retrieve_mock ): # fetch the Stripe Account express_account = self.express_account # assert metadata is empty self.assertEqual(express_account.metadata, {}) fake_stripe_update_event = deepcopy(FAKE_EVENT_EXPRESS_ACCOUNT_UPDATED) event = Event.sync_from_stripe_data(fake_stripe_update_event) event.invoke_webhook_handlers() # fetch the updated Account object updated_express_account = Account.objects.get(id=express_account.id) # assert we are updating the metadata self.assertNotEqual( updated_express_account.metadata, express_account.metadata, ) # assert the meta got updated self.assertEqual( updated_express_account.metadata, fake_stripe_update_event["data"]["object"]["metadata"], ) @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_EVENT_CUSTOM_ACCOUNT_UPDATED["data"]["object"]), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_custom_account_updated_event( self, event_retrieve_mock, account_retrieve_mock ): # fetch the Stripe Account custom_account = self.custom_account # assert metadata is empty self.assertEqual(custom_account.metadata, {}) fake_stripe_update_event = deepcopy(FAKE_EVENT_CUSTOM_ACCOUNT_UPDATED) event = Event.sync_from_stripe_data(fake_stripe_update_event) event.invoke_webhook_handlers() # fetch the updated Account object updated_custom_account = Account.objects.get(id=custom_account.id) # assert we are updating the metadata self.assertNotEqual( updated_custom_account.metadata, custom_account.metadata, ) # assert the meta got updated self.assertEqual( updated_custom_account.metadata, fake_stripe_update_event["data"]["object"]["metadata"], ) class TestChargeEvents(EventTestCase): def setUp(self): # create a Stripe Platform Account self.account = FAKE_PLATFORM_ACCOUNT.create() self.user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), ) @patch("stripe.Charge.retrieve", autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) def test_charge_created( self, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, event_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, customer_retrieve_mock, account_mock, ): FAKE_CUSTOMER.create_for_user(self.user) fake_stripe_event = deepcopy(FAKE_EVENT_CHARGE_SUCCEEDED) event_retrieve_mock.return_value = fake_stripe_event charge_retrieve_mock.return_value = fake_stripe_event["data"]["object"] account_mock.return_value = self.account event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() charge = Charge.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual( charge.amount, fake_stripe_event["data"]["object"]["amount"] / Decimal("100"), ) self.assertEqual(charge.status, fake_stripe_event["data"]["object"]["status"]) class TestCheckoutEvents(EventTestCase): def setUp(self): self.user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) self.customer = FAKE_CUSTOMER.create_for_user(self.user) @patch( "stripe.checkout.Session.retrieve", return_value=FAKE_SESSION_I, autospec=True ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_checkout_session_completed( self, event_retrieve_mock, payment_intent_retrieve_mock, customer_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, session_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_SESSION_COMPLETED) event_retrieve_mock.return_value = fake_stripe_event event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() session = Session.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual(session.customer.id, self.customer.id) @patch( "stripe.checkout.Session.retrieve", return_value=FAKE_SESSION_I, autospec=True ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_checkout_session_async_payment_succeeded( self, event_retrieve_mock, payment_intent_retrieve_mock, customer_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, session_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_SESSION_COMPLETED) fake_stripe_event["type"] = "checkout.session.async_payment_succeeded" event_retrieve_mock.return_value = fake_stripe_event event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() session = Session.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual(session.customer.id, self.customer.id) @patch( "stripe.checkout.Session.retrieve", return_value=FAKE_SESSION_I, autospec=True ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_checkout_session_async_payment_failed( self, event_retrieve_mock, payment_intent_retrieve_mock, customer_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, session_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_SESSION_COMPLETED) fake_stripe_event["type"] = "checkout.session.async_payment_failed" event_retrieve_mock.return_value = fake_stripe_event event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() session = Session.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual(session.customer.id, self.customer.id) @patch("stripe.checkout.Session.retrieve", autospec=True) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.Customer.modify", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=FAKE_PAYMENT_INTENT_I, autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_checkout_session_completed_customer_subscriber_added( self, event_retrieve_mock, payment_intent_retrieve_mock, customer_modify_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, session_retrieve_mock, ): # because create_for_user method adds subscriber self.customer.subcriber = None self.customer.save() # update metadata in deepcopied FAKE_SEESION_1 Object fake_stripe_event = deepcopy(FAKE_EVENT_SESSION_COMPLETED) fake_stripe_event["data"]["object"]["metadata"] = { "djstripe_subscriber": self.user.id } event_retrieve_mock.return_value = fake_stripe_event # update metadata in FAKE_SEESION_1 Object fake_stripe_session = deepcopy(FAKE_SESSION_I) fake_stripe_session["metadata"] = {"djstripe_subscriber": self.user.id} session_retrieve_mock.return_value = fake_stripe_session event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() # refresh self.customer from db self.customer.refresh_from_db() session = Session.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual(session.customer.id, self.customer.id) self.assertEqual(self.customer.subscriber, self.user) self.assertEqual(self.customer.metadata, {"djstripe_subscriber": self.user.id}) class TestCustomerEvents(EventTestCase): def setUp(self): self.user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) self.customer = FAKE_CUSTOMER.create_for_user(self.user) @patch("stripe.Customer.retrieve", return_value=FAKE_CUSTOMER, autospec=True) @patch("stripe.Event.retrieve", autospec=True) def test_customer_created(self, event_retrieve_mock, customer_retrieve_mock): fake_stripe_event = deepcopy(FAKE_EVENT_CUSTOMER_CREATED) event_retrieve_mock.return_value = fake_stripe_event event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() customer = Customer.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual( customer.balance, fake_stripe_event["data"]["object"]["balance"] ) self.assertEqual( customer.currency, fake_stripe_event["data"]["object"]["currency"] ) @patch("stripe.Customer.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) def test_customer_metadata_created( self, event_retrieve_mock, customer_retrieve_mock ): fake_customer = deepcopy(FAKE_CUSTOMER) fake_customer["metadata"] = {"djstripe_subscriber": self.user.id} fake_stripe_event = deepcopy(FAKE_EVENT_CUSTOMER_CREATED) fake_stripe_event["data"]["object"] = fake_customer event_retrieve_mock.return_value = fake_stripe_event customer_retrieve_mock.return_value = fake_customer event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() customer = Customer.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual( customer.balance, fake_stripe_event["data"]["object"]["balance"] ) self.assertEqual( customer.currency, fake_stripe_event["data"]["object"]["currency"] ) self.assertEqual(customer.subscriber, self.user) self.assertEqual(customer.metadata, {"djstripe_subscriber": self.user.id}) @patch("stripe.Customer.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) def test_customer_metadata_updated( self, event_retrieve_mock, customer_retrieve_mock ): fake_customer = deepcopy(FAKE_CUSTOMER) fake_customer["metadata"] = {"djstripe_subscriber": self.user.id} fake_stripe_event = deepcopy(FAKE_EVENT_CUSTOMER_UPDATED) fake_stripe_event["data"]["object"] = fake_customer event_retrieve_mock.return_value = fake_stripe_event customer_retrieve_mock.return_value = fake_customer event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() customer = Customer.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual( customer.balance, fake_stripe_event["data"]["object"]["balance"] ) self.assertEqual( customer.currency, fake_stripe_event["data"]["object"]["currency"] ) self.assertEqual(customer.subscriber, self.user) self.assertEqual(customer.metadata, {"djstripe_subscriber": self.user.id}) @patch( "stripe.Customer.delete_source", autospec=True, ) @patch("stripe.Customer.delete", autospec=True) @patch( "stripe.Customer.retrieve_source", side_effect=[deepcopy(FAKE_CARD), deepcopy(FAKE_CARD_III)], autospec=True, ) @patch("stripe.Customer.retrieve", return_value=FAKE_CUSTOMER, autospec=True) def test_customer_deleted( self, customer_retrieve_mock, customer_retrieve_source_mock, customer_delete_mock, customer_source_delete_mock, ): FAKE_CUSTOMER.create_for_user(self.user) event = self._create_event(FAKE_EVENT_CUSTOMER_CREATED) event.invoke_webhook_handlers() event = self._create_event(FAKE_EVENT_CUSTOMER_DELETED) event.invoke_webhook_handlers() customer = Customer.objects.get(id=FAKE_CUSTOMER["id"]) self.assertIsNotNone(customer.date_purged) @patch("stripe.Coupon.retrieve", return_value=FAKE_COUPON, autospec=True) @patch( "stripe.Event.retrieve", return_value=FAKE_EVENT_CUSTOMER_DISCOUNT_CREATED, autospec=True, ) def test_customer_discount_created(self, event_retrieve_mock, coupon_retrieve_mock): fake_stripe_event = deepcopy(FAKE_EVENT_CUSTOMER_DISCOUNT_CREATED) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() self.assertIsNotNone(event.customer) self.assertEqual(event.customer.id, FAKE_CUSTOMER["id"]) self.assertIsNotNone(event.customer.coupon) @patch("stripe.Coupon.retrieve", return_value=FAKE_COUPON, autospec=True) @patch( "stripe.Event.retrieve", return_value=FAKE_EVENT_CUSTOMER_DISCOUNT_DELETED, autospec=True, ) def test_customer_discount_deleted(self, event_retrieve_mock, coupon_retrieve_mock): coupon = Coupon.sync_from_stripe_data(FAKE_COUPON) self.customer.coupon = coupon fake_stripe_event = deepcopy(FAKE_EVENT_CUSTOMER_DISCOUNT_DELETED) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() self.assertIsNotNone(event.customer) self.assertEqual(event.customer.id, FAKE_CUSTOMER["id"]) self.assertIsNone(event.customer.coupon) @patch("stripe.Customer.retrieve", return_value=FAKE_CUSTOMER, autospec=True) @patch("stripe.Event.retrieve", autospec=True) @patch( "stripe.Customer.retrieve_source", return_value=deepcopy(FAKE_CARD), autospec=True, ) def test_customer_card_created( self, customer_retrieve_source_mock, event_retrieve_mock, customer_retrieve_mock ): fake_stripe_event = deepcopy(FAKE_EVENT_CUSTOMER_SOURCE_CREATED) event_retrieve_mock.return_value = fake_stripe_event event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() card = Card.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertIn(card, self.customer.legacy_cards.all()) self.assertEqual(card.brand, fake_stripe_event["data"]["object"]["brand"]) self.assertEqual(card.last4, fake_stripe_event["data"]["object"]["last4"]) @patch("stripe.Event.retrieve", autospec=True) @patch("stripe.Customer.retrieve", return_value=FAKE_CUSTOMER, autospec=True) def test_customer_unknown_source_created( self, customer_retrieve_mock, event_retrieve_mock ): fake_stripe_event = deepcopy(FAKE_EVENT_CUSTOMER_SOURCE_CREATED) fake_stripe_event["data"]["object"]["object"] = "unknown" fake_stripe_event["data"]["object"][ "id" ] = "card_xxx_test_customer_unk_source_created" event_retrieve_mock.return_value = fake_stripe_event FAKE_CUSTOMER.create_for_user(self.user) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() self.assertFalse( Card.objects.filter(id=fake_stripe_event["data"]["object"]["id"]).exists() ) @patch("stripe.Customer.retrieve", return_value=FAKE_CUSTOMER, autospec=True) def test_customer_default_source_deleted(self, customer_retrieve_mock): self.customer.default_source = DjstripePaymentMethod.objects.get( id=FAKE_CARD["id"] ) self.customer.save() self.assertIsNotNone(self.customer.default_source) event = self._create_event(FAKE_EVENT_CUSTOMER_SOURCE_DELETED) event.invoke_webhook_handlers() # fetch the customer. Doubles up as a check that the customer didn't get # deleted customer = Customer.objects.get(id=FAKE_CUSTOMER["id"]) self.assertIsNone(customer.default_source) @patch("stripe.Customer.retrieve", return_value=FAKE_CUSTOMER, autospec=True) def test_customer_source_double_delete(self, customer_retrieve_mock): event = self._create_event(FAKE_EVENT_CUSTOMER_SOURCE_DELETED) event.invoke_webhook_handlers() event = self._create_event(FAKE_EVENT_CUSTOMER_SOURCE_DELETED_DUPE) event.invoke_webhook_handlers() # fetch the customer. Doubles up as a check that the customer didn't get # deleted customer = Customer.objects.get(id=FAKE_CUSTOMER["id"]) self.assertIsNone(customer.default_source) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch("stripe.Subscription.retrieve", autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch("stripe.Event.retrieve", autospec=True) @patch("stripe.Customer.retrieve", return_value=FAKE_CUSTOMER, autospec=True) def test_customer_subscription_created( self, customer_retrieve_mock, event_retrieve_mock, product_retrieve_mock, subscription_retrieve_mock, plan_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_CUSTOMER_SUBSCRIPTION_CREATED) event_retrieve_mock.return_value = fake_stripe_event fake_subscription = deepcopy(FAKE_SUBSCRIPTION) # latest_invoice has to be None for a Subscription that has not been created yet. fake_subscription["latest_invoice"] = None subscription_retrieve_mock.return_value = fake_subscription event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() subscription = Subscription.objects.get( id=fake_stripe_event["data"]["object"]["id"] ) self.assertIn(subscription, self.customer.subscriptions.all()) self.assertEqual( subscription.status, fake_stripe_event["data"]["object"]["status"] ) self.assertEqual( subscription.quantity, fake_stripe_event["data"]["object"]["quantity"] ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_customer_subscription_deleted( self, customer_retrieve_mock, product_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, plan_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, ): fake_subscription = deepcopy(FAKE_SUBSCRIPTION) # A just created Subscription cannot have latest_invoice fake_subscription["latest_invoice"] = None subscription_retrieve_mock.return_value = fake_subscription fake_event = deepcopy(FAKE_EVENT_CUSTOMER_SUBSCRIPTION_CREATED) fake_event["data"]["object"] = fake_subscription event = self._create_event(fake_event) event.invoke_webhook_handlers() sub = Subscription.objects.get(id=fake_subscription["id"]) self.assertEqual(sub.status, SubscriptionStatus.active) # create invoice for latest_invoice in subscription to work. Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) subscription_retrieve_mock.return_value = deepcopy(FAKE_SUBSCRIPTION_CANCELED) event = self._create_event(FAKE_EVENT_CUSTOMER_SUBSCRIPTION_DELETED) event.invoke_webhook_handlers() sub = Subscription.objects.get(id=FAKE_SUBSCRIPTION["id"]) # Check that Subscription is canceled and not deleted self.assertEqual(sub.status, SubscriptionStatus.canceled) self.assertIsNotNone(sub.canceled_at) @patch("stripe.Customer.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) def test_customer_bogus_event_type( self, event_retrieve_mock, customer_retrieve_mock ): fake_stripe_event = deepcopy(FAKE_EVENT_CUSTOMER_CREATED) fake_stripe_event["data"]["object"]["customer"] = fake_stripe_event["data"][ "object" ]["id"] fake_stripe_event["type"] = "customer.praised" event_retrieve_mock.return_value = fake_stripe_event customer_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() class TestDisputeEvents(EventTestCase): def setUp(self): self.user = get_user_model().objects.create_user( username="fake_customer_1", email=FAKE_CUSTOMER["email"] ) self.customer = FAKE_CUSTOMER.create_for_user(self.user) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_DISPUTE_PAYMENT_INTENT), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_DISPUTE_CHARGE), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_DISPUTE_BALANCE_TRANSACTION), ) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_ICON), autospec=True, ) @patch( "stripe.Dispute.retrieve", return_value=deepcopy(FAKE_DISPUTE_I), autospec=True ) @patch( "stripe.Event.retrieve", return_value=deepcopy(FAKE_EVENT_DISPUTE_CREATED), autospec=True, ) def test_dispute_created( self, event_retrieve_mock, dispute_retrieve_mock, file_retrieve_mock, balance_transaction_retrieve_mock, charge_retrieve_mock, payment_intent_retrieve_mock, payment_method_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_DISPUTE_CREATED) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() dispute = Dispute.objects.get() self.assertEqual(dispute.id, FAKE_DISPUTE_I["id"]) # funds get withdrawn from the account as soon as a charge is # disputed so practically there is no difference between # charge.dispute.created and charge.dispute.funds_withdrawn @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_DISPUTE_PAYMENT_INTENT), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_DISPUTE_CHARGE), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_DISPUTE_BALANCE_TRANSACTION), ) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_ICON), autospec=True, ) @patch( "stripe.Dispute.retrieve", return_value=deepcopy(FAKE_DISPUTE_II), autospec=True ) @patch( "stripe.Event.retrieve", return_value=deepcopy(FAKE_EVENT_DISPUTE_FUNDS_WITHDRAWN), autospec=True, ) def test_dispute_funds_withdrawn( self, event_retrieve_mock, dispute_retrieve_mock, file_retrieve_mock, balance_transaction_retrieve_mock, charge_retrieve_mock, payment_intent_retrieve_mock, payment_method_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_DISPUTE_FUNDS_WITHDRAWN) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() dispute = Dispute.objects.get() self.assertEqual(dispute.id, FAKE_DISPUTE_II["id"]) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_DISPUTE_PAYMENT_INTENT), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_DISPUTE_CHARGE), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_DISPUTE_BALANCE_TRANSACTION), ) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_ICON), autospec=True, ) @patch( "stripe.Dispute.retrieve", return_value=deepcopy(FAKE_DISPUTE_III), autospec=True, ) @patch( "stripe.Event.retrieve", return_value=deepcopy(FAKE_EVENT_DISPUTE_UPDATED), autospec=True, ) def test_dispute_updated( self, event_retrieve_mock, dispute_retrieve_mock, file_retrieve_mock, balance_transaction_retrieve_mock, charge_retrieve_mock, payment_intent_retrieve_mock, payment_method_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_DISPUTE_UPDATED) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() dispute = Dispute.objects.get() self.assertEqual(dispute.id, FAKE_DISPUTE_III["id"]) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_DISPUTE_PAYMENT_INTENT), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_DISPUTE_CHARGE), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_DISPUTE_BALANCE_TRANSACTION), ) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_ICON), autospec=True, ) @patch( "stripe.Dispute.retrieve", return_value=deepcopy(FAKE_DISPUTE_III), autospec=True, ) @patch( "stripe.Event.retrieve", return_value=deepcopy(FAKE_EVENT_DISPUTE_CLOSED), autospec=True, ) def test_dispute_closed( self, event_retrieve_mock, dispute_retrieve_mock, file_retrieve_mock, balance_transaction_retrieve_mock, charge_retrieve_mock, payment_intent_retrieve_mock, payment_method_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_DISPUTE_CLOSED) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() dispute = Dispute.objects.get() self.assertEqual(dispute.id, FAKE_DISPUTE_III["id"]) # funds get reinstated after the dispute is closed # includes full fund reinstatements as well as partial refunds @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_DISPUTE_PAYMENT_INTENT), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_DISPUTE_CHARGE), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", side_effect=[ FAKE_DISPUTE_BALANCE_TRANSACTION, FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_FULL, ], ) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_ICON), autospec=True, ) @patch( "stripe.Dispute.retrieve", return_value=deepcopy(FAKE_DISPUTE_V_FULL), autospec=True, ) @patch( "stripe.Event.retrieve", return_value=deepcopy(FAKE_EVENT_DISPUTE_FUNDS_REINSTATED_FULL), autospec=True, ) def test_dispute_funds_reinstated_full( self, event_retrieve_mock, dispute_retrieve_mock, file_retrieve_mock, balance_transaction_retrieve_mock, charge_retrieve_mock, payment_intent_retrieve_mock, payment_method_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_DISPUTE_FUNDS_REINSTATED_FULL) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() dispute = Dispute.objects.get() self.assertEqual(dispute.id, FAKE_DISPUTE_V_FULL["id"]) # funds get reinstated after the dispute is closed # includes full fund reinstatements as well as partial refunds @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_DISPUTE_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_DISPUTE_PAYMENT_INTENT), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_DISPUTE_CHARGE), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", side_effect=[ FAKE_DISPUTE_BALANCE_TRANSACTION, FAKE_DISPUTE_BALANCE_TRANSACTION_REFUND_PARTIAL, ], ) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_ICON), autospec=True, ) @patch( "stripe.Dispute.retrieve", return_value=deepcopy(FAKE_DISPUTE_V_PARTIAL), autospec=True, ) @patch( "stripe.Event.retrieve", return_value=deepcopy(FAKE_EVENT_DISPUTE_FUNDS_REINSTATED_PARTIAL), autospec=True, ) def test_dispute_funds_reinstated_partial( self, event_retrieve_mock, dispute_retrieve_mock, file_retrieve_mock, balance_transaction_retrieve_mock, charge_retrieve_mock, payment_intent_retrieve_mock, payment_method_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_DISPUTE_FUNDS_REINSTATED_PARTIAL) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() dispute = Dispute.objects.get() self.assertGreaterEqual(len(dispute.balance_transactions), 2) self.assertEqual(dispute.id, FAKE_DISPUTE_V_PARTIAL["id"]) class TestFileEvents(EventTestCase): def setUp(self): self.user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) self.customer = FAKE_CUSTOMER.create_for_user(self.user) @patch( "stripe.File.retrieve", return_value=deepcopy(FAKE_FILEUPLOAD_ICON), autospec=True, ) @patch( "stripe.Event.retrieve", return_value=deepcopy(FAKE_EVENT_FILE_CREATED), autospec=True, ) def test_file_created(self, event_retrieve_mock, file_retrieve_mock): fake_stripe_event = deepcopy(FAKE_EVENT_FILE_CREATED) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() file = File.objects.get() self.assertEqual(file.id, FAKE_FILEUPLOAD_ICON["id"]) class TestInvoiceEvents(EventTestCase): def setUp(self): self.user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) @patch( "djstripe.models.Account.get_default_account", return_value=deepcopy(FAKE_PLATFORM_ACCOUNT), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch("stripe.Event.retrieve", autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_invoice_created_no_existing_customer( self, product_retrieve_mock, paymentmethod_card_retrieve_mock, event_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, customer_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_INVOICE_CREATED) event_retrieve_mock.return_value = fake_stripe_event invoice_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() self.assertEqual(Customer.objects.count(), 1) customer = Customer.objects.get() self.assertEqual(customer.subscriber, None) @patch( "djstripe.models.Account.get_default_account", return_value=deepcopy(FAKE_PLATFORM_ACCOUNT), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch("stripe.Invoice.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_invoice_created( self, product_retrieve_mock, paymentmethod_card_retrieve_mock, event_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, customer_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): FAKE_CUSTOMER.create_for_user(self.user) fake_stripe_event = deepcopy(FAKE_EVENT_INVOICE_CREATED) event_retrieve_mock.return_value = fake_stripe_event invoice_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() invoice = Invoice.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual( invoice.amount_due, fake_stripe_event["data"]["object"]["amount_due"] / Decimal("100"), ) self.assertEqual(invoice.paid, fake_stripe_event["data"]["object"]["paid"]) @patch( "djstripe.models.Account.get_default_account", return_value=deepcopy(FAKE_PLATFORM_ACCOUNT), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_invoice_deleted( self, product_retrieve_mock, paymentmethod_card_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): FAKE_CUSTOMER.create_for_user(self.user) event = self._create_event(FAKE_EVENT_INVOICE_CREATED) event.invoke_webhook_handlers() Invoice.objects.get(id=FAKE_INVOICE["id"]) event = self._create_event(FAKE_EVENT_INVOICE_DELETED) event.invoke_webhook_handlers() with self.assertRaises(Invoice.DoesNotExist): Invoice.objects.get(id=FAKE_INVOICE["id"]) def test_invoice_upcoming(self): # Ensure that invoice upcoming events are processed - No actual # process occurs so the operation is an effective no-op. event = self._create_event(FAKE_EVENT_INVOICE_UPCOMING) event.invoke_webhook_handlers() class TestInvoiceItemEvents(EventTestCase): def setUp(self): self.user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) @patch( "djstripe.models.Account.get_default_account", return_value=deepcopy(FAKE_PLATFORM_ACCOUNT), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE_II), autospec=True ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_II), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE_II), autospec=True ) @patch("stripe.InvoiceItem.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) def test_invoiceitem_created( self, customer_retrieve_mock, product_retrieve_mock, event_retrieve_mock, invoiceitem_retrieve_mock, invoice_retrieve_mock, paymentintent_retrieve_mock, paymentmethod_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_II) fake_payment_intent["invoice"] = FAKE_INVOICE_II["id"] paymentintent_retrieve_mock.return_value = fake_payment_intent fake_subscription = deepcopy(FAKE_SUBSCRIPTION_III) fake_subscription["latest_invoice"] = FAKE_INVOICE_II["id"] subscription_retrieve_mock.return_value = fake_subscription fake_stripe_event = deepcopy(FAKE_EVENT_INVOICEITEM_CREATED) event_retrieve_mock.return_value = fake_stripe_event invoiceitem_retrieve_mock.return_value = fake_stripe_event["data"]["object"] fake_card = deepcopy(FAKE_CARD_II) fake_card["customer"] = None # create Card for FAKE_CUSTOMER_III Card.sync_from_stripe_data(fake_card) # create invoice Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE_II)) FAKE_CUSTOMER_II.create_for_user(self.user) event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() invoiceitem = InvoiceItem.objects.get( id=fake_stripe_event["data"]["object"]["id"] ) self.assertEqual( invoiceitem.amount, fake_stripe_event["data"]["object"]["amount"] / Decimal("100"), ) @patch( "djstripe.models.Account.get_default_account", return_value=deepcopy(FAKE_PLATFORM_ACCOUNT), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE_II), autospec=True ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_II), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE_II), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) def test_invoiceitem_deleted( self, customer_retrieve_mock, product_retrieve_mock, invoiceitem_retrieve_mock, invoice_retrieve_mock, paymentintent_retrieve_mock, paymentmethod_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_II) fake_payment_intent["invoice"] = FAKE_INVOICE_II["id"] paymentintent_retrieve_mock.return_value = fake_payment_intent fake_subscription = deepcopy(FAKE_SUBSCRIPTION_III) fake_subscription["latest_invoice"] = FAKE_INVOICE_II["id"] subscription_retrieve_mock.return_value = fake_subscription fake_card = deepcopy(FAKE_CARD_II) fake_card["customer"] = None # create Card for FAKE_CUSTOMER_III Card.sync_from_stripe_data(fake_card) # create invoice Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE_II)) FAKE_CUSTOMER_II.create_for_user(self.user) event = self._create_event(FAKE_EVENT_INVOICEITEM_CREATED) event.invoke_webhook_handlers() InvoiceItem.objects.get(id=FAKE_INVOICEITEM["id"]) event = self._create_event(FAKE_EVENT_INVOICEITEM_DELETED) event.invoke_webhook_handlers() with self.assertRaises(InvoiceItem.DoesNotExist): InvoiceItem.objects.get(id=FAKE_INVOICEITEM["id"]) class TestPlanEvents(EventTestCase): @patch("stripe.Plan.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_plan_created( self, product_retrieve_mock, event_retrieve_mock, plan_retrieve_mock ): fake_stripe_event = deepcopy(FAKE_EVENT_PLAN_CREATED) event_retrieve_mock.return_value = fake_stripe_event plan_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() plan = Plan.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual(plan.nickname, fake_stripe_event["data"]["object"]["nickname"]) @patch("stripe.Plan.retrieve", return_value=FAKE_PLAN, autospec=True) @patch( "stripe.Event.retrieve", return_value=FAKE_EVENT_PLAN_REQUEST_IS_OBJECT, autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_plan_updated_request_object( self, product_retrieve_mock, event_retrieve_mock, plan_retrieve_mock ): plan_retrieve_mock.return_value = FAKE_EVENT_PLAN_REQUEST_IS_OBJECT["data"][ "object" ] event = Event.sync_from_stripe_data(FAKE_EVENT_PLAN_REQUEST_IS_OBJECT) event.invoke_webhook_handlers() plan = Plan.objects.get( id=FAKE_EVENT_PLAN_REQUEST_IS_OBJECT["data"]["object"]["id"] ) self.assertEqual( plan.nickname, FAKE_EVENT_PLAN_REQUEST_IS_OBJECT["data"]["object"]["nickname"], ) @patch("stripe.Plan.retrieve", return_value=FAKE_PLAN, autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_plan_deleted(self, product_retrieve_mock, plan_retrieve_mock): event = self._create_event(FAKE_EVENT_PLAN_CREATED) event.invoke_webhook_handlers() Plan.objects.get(id=FAKE_PLAN["id"]) event = self._create_event(FAKE_EVENT_PLAN_DELETED) event.invoke_webhook_handlers() with self.assertRaises(Plan.DoesNotExist): Plan.objects.get(id=FAKE_PLAN["id"]) class TestPriceEvents(EventTestCase): @patch("stripe.Price.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_price_created( self, product_retrieve_mock, event_retrieve_mock, price_retrieve_mock ): fake_stripe_event = deepcopy(FAKE_EVENT_PRICE_CREATED) event_retrieve_mock.return_value = fake_stripe_event price_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() price = Price.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual( price.nickname, fake_stripe_event["data"]["object"]["nickname"] ) @patch("stripe.Price.retrieve", return_value=FAKE_PRICE, autospec=True) @patch( "stripe.Event.retrieve", return_value=FAKE_EVENT_PRICE_UPDATED, autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_price_updated( self, product_retrieve_mock, event_retrieve_mock, price_retrieve_mock ): price_retrieve_mock.return_value = FAKE_EVENT_PRICE_UPDATED["data"]["object"] event = Event.sync_from_stripe_data(FAKE_EVENT_PRICE_UPDATED) event.invoke_webhook_handlers() price = Price.objects.get(id=FAKE_EVENT_PRICE_UPDATED["data"]["object"]["id"]) self.assertEqual( price.unit_amount, FAKE_EVENT_PRICE_UPDATED["data"]["object"]["unit_amount"], ) self.assertEqual( price.unit_amount_decimal, Decimal(FAKE_EVENT_PRICE_UPDATED["data"]["object"]["unit_amount_decimal"]), ) @patch("stripe.Price.retrieve", return_value=FAKE_PRICE, autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_price_deleted(self, product_retrieve_mock, price_retrieve_mock): event = self._create_event(FAKE_EVENT_PRICE_CREATED) event.invoke_webhook_handlers() Price.objects.get(id=FAKE_PRICE["id"]) event = self._create_event(FAKE_EVENT_PRICE_DELETED) event.invoke_webhook_handlers() with self.assertRaises(Price.DoesNotExist): Price.objects.get(id=FAKE_PRICE["id"]) class TestPaymentMethodEvents(AssertStripeFksMixin, EventTestCase): def setUp(self): self.user = get_user_model().objects.create_user( username="fake_customer_1", email=FAKE_CUSTOMER["email"] ) self.customer = FAKE_CUSTOMER.create_for_user(self.user) @patch("stripe.PaymentMethod.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) def test_payment_method_attached( self, event_retrieve_mock, payment_method_retrieve_mock ): fake_stripe_event = deepcopy(FAKE_EVENT_PAYMENT_METHOD_ATTACHED) event_retrieve_mock.return_value = fake_stripe_event payment_method_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() payment_method = PaymentMethod.objects.get( id=fake_stripe_event["data"]["object"]["id"] ) self.assert_fks( payment_method, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", }, ) @patch("stripe.PaymentMethod.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) def test_card_payment_method_attached( self, event_retrieve_mock, payment_method_retrieve_mock ): # Attach of a legacy id="card_xxx" payment method should behave exactly # as per a normal "native" id="pm_yyy" payment_method. fake_stripe_event = deepcopy(FAKE_EVENT_CARD_PAYMENT_METHOD_ATTACHED) event_retrieve_mock.return_value = fake_stripe_event payment_method_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() payment_method = PaymentMethod.objects.get( id=fake_stripe_event["data"]["object"]["id"] ) self.assert_fks( payment_method, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", }, ) @patch("stripe.PaymentMethod.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) def test_payment_method_detached( self, event_retrieve_mock, payment_method_retrieve_mock ): fake_stripe_event = deepcopy(FAKE_EVENT_PAYMENT_METHOD_DETACHED) event_retrieve_mock.return_value = fake_stripe_event payment_method_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() payment_method = PaymentMethod.objects.get( id=fake_stripe_event["data"]["object"]["id"] ) self.assertIsNone( payment_method.customer, "Detach of a payment_method should set customer to null", ) self.assert_fks( payment_method, expected_blank_fks={"djstripe.PaymentMethod.customer"} ) @patch( "stripe.PaymentMethod.retrieve", side_effect=InvalidRequestError( message="No such payment_method: card_xxxx", param="payment_method", code="resource_missing", ), autospec=True, ) @patch("stripe.Event.retrieve", autospec=True) def test_card_payment_method_detached( self, event_retrieve_mock, payment_method_retrieve_mock ): # Detach of a legacy id="card_xxx" payment method is handled specially, # since the card is deleted by Stripe and therefore PaymetMethod.retrieve fails fake_stripe_event = deepcopy(FAKE_EVENT_CARD_PAYMENT_METHOD_DETACHED) event_retrieve_mock.return_value = fake_stripe_event payment_method_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() self.assertEqual( PaymentMethod.objects.filter( id=fake_stripe_event["data"]["object"]["id"] ).count(), 0, "Detach of a 'card_' payment_method should delete it", ) class TestPaymentIntentEvents(EventTestCase): """Test case for payment intent event handling.""" @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_ACCOUNT), autospec=True, ) @patch( "stripe.File.retrieve", side_effect=(deepcopy(FAKE_FILEUPLOAD_ICON), deepcopy(FAKE_FILEUPLOAD_LOGO)), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_DESTINATION_CHARGE), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) def test_payment_intent_succeeded_with_destination_charge( self, customer_retrieve_mock, account_retrieve_mock, file_upload_retrieve_mock, payment_intent_retrieve_mock, payment_method_retrieve_mock, ): """Test that the payment intent succeeded event can create all related objects. This should exercise the machinery to set `stripe_account` when recursing into objects related to a connect `Account`. """ event = self._create_event( FAKE_EVENT_PAYMENT_INTENT_SUCCEEDED_DESTINATION_CHARGE ) event.invoke_webhook_handlers() # Make sure the file uploads were retrieved using the account ID. file_upload_retrieve_mock.assert_has_calls( ( call( id=FAKE_FILEUPLOAD_ICON["id"], api_key=ANY, expand=ANY, stripe_account=FAKE_ACCOUNT["id"], stripe_version=djstripe_settings.STRIPE_API_VERSION, ), call( id=FAKE_FILEUPLOAD_LOGO["id"], api_key=ANY, expand=ANY, stripe_account=FAKE_ACCOUNT["id"], stripe_version=djstripe_settings.STRIPE_API_VERSION, ), ) ) class TestSubscriptionScheduleEvents(EventTestCase): @patch( "stripe.SubscriptionSchedule.retrieve", autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) def test_subscription_schedule_created( self, customer_retrieve_mock, schedule_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED) fake_stripe_event["data"]["object"]["subscription"] = None fake_subscription_schedule = deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) fake_subscription_schedule["subscription"] = None schedule_retrieve_mock.return_value = fake_subscription_schedule event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() schedule = SubscriptionSchedule.objects.get( id=fake_stripe_event["data"]["object"]["id"] ) assert schedule.id == fake_stripe_event["data"]["object"]["id"] assert schedule.status == "not_started" @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE) ) @patch( "stripe.SubscriptionSchedule.retrieve", return_value=FAKE_SUBSCRIPTION_SCHEDULE, autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) def test_subscription_schedule_and_subscription_created( self, customer_retrieve_mock, schedule_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): # create latest invoice Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) event = Event.sync_from_stripe_data(FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED) event.invoke_webhook_handlers() schedule = SubscriptionSchedule.objects.get( id=FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED["data"]["object"]["id"] ) assert ( schedule.id == FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED["data"]["object"]["id"] ) assert schedule.status == "not_started" @patch("stripe.SubscriptionSchedule.retrieve", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) def test_subscription_schedule_canceled( self, customer_retrieve_mock, schedule_retrieve_mock ): fake_stripe_event = deepcopy(FAKE_EVENT_SUBSCRIPTION_SCHEDULE_UPDATED) fake_stripe_event["data"]["previous_attributes"] = { "canceled_at": None, "status": "not_started", } fake_stripe_event["data"]["object"]["subscription"] = None fake_subscription_schedule = deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) fake_subscription_schedule["subscription"] = None fake_subscription_schedule["canceled_at"] = 1605058030 fake_subscription_schedule["status"] = "canceled" schedule_retrieve_mock.return_value = fake_subscription_schedule event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() schedule = SubscriptionSchedule.objects.get( id=fake_stripe_event["data"]["object"]["id"] ) assert schedule.status == "canceled" assert schedule.canceled_at is not None fake_stripe_event_2 = deepcopy(FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CANCELED) fake_stripe_event_2["data"]["object"]["subscription"] = None schedule_retrieve_mock.return_value = fake_stripe_event_2["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event_2) event.invoke_webhook_handlers() schedule.refresh_from_db() assert schedule.status == "canceled" assert schedule.canceled_at is not None @patch("stripe.SubscriptionSchedule.retrieve", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) def test_subscription_schedule_completed( self, customer_retrieve_mock, schedule_retrieve_mock ): fake_stripe_event = deepcopy(FAKE_EVENT_SUBSCRIPTION_SCHEDULE_UPDATED) fake_stripe_event["data"]["object"]["subscription"] = None fake_subscription_schedule = deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) fake_subscription_schedule["subscription"] = None fake_subscription_schedule["completed_at"] = 1605058030 fake_subscription_schedule["status"] = "completed" schedule_retrieve_mock.return_value = fake_subscription_schedule event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() schedule = SubscriptionSchedule.objects.get( id=fake_stripe_event["data"]["object"]["id"] ) assert schedule.status == "completed" assert schedule.completed_at is not None fake_stripe_event_2 = deepcopy(FAKE_EVENT_SUBSCRIPTION_SCHEDULE_COMPLETED) fake_stripe_event_2["data"]["object"]["subscription"] = None schedule_retrieve_mock.return_value = fake_stripe_event_2["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event_2) event.invoke_webhook_handlers() schedule.refresh_from_db() assert schedule.status == "completed" assert schedule.completed_at is not None @patch("stripe.SubscriptionSchedule.retrieve", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) def test_subscription_schedule_expiring( self, customer_retrieve_mock, schedule_retrieve_mock ): fake_stripe_event = deepcopy(FAKE_EVENT_SUBSCRIPTION_SCHEDULE_UPDATED) fake_stripe_event["data"]["object"]["subscription"] = None fake_subscription_schedule = deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) fake_subscription_schedule["subscription"] = None fake_subscription_schedule["status"] = "active" schedule_retrieve_mock.return_value = fake_subscription_schedule event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() schedule = SubscriptionSchedule.objects.get( id=fake_stripe_event["data"]["object"]["id"] ) assert schedule.status == "active" assert schedule.completed_at is None fake_stripe_event_2 = deepcopy(FAKE_EVENT_SUBSCRIPTION_SCHEDULE_EXPIRING) fake_stripe_event_2["data"]["object"]["subscription"] = None schedule_retrieve_mock.return_value = fake_stripe_event_2["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event_2) event.invoke_webhook_handlers() schedule.refresh_from_db() assert schedule.status == "active" assert schedule.completed_at is None @patch("stripe.SubscriptionSchedule.retrieve", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) def test_subscription_schedule_released( self, customer_retrieve_mock, schedule_retrieve_mock ): fake_stripe_event = deepcopy(FAKE_EVENT_SUBSCRIPTION_SCHEDULE_UPDATED) fake_stripe_event["data"]["previous_attributes"] = { "released_at": None, "status": "not_started", } fake_stripe_event["data"]["object"]["subscription"] = None fake_subscription_schedule = deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) fake_subscription_schedule["subscription"] = None fake_subscription_schedule["released_at"] = 1605058030 fake_subscription_schedule["status"] = "released" schedule_retrieve_mock.return_value = fake_subscription_schedule event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() schedule = SubscriptionSchedule.objects.get( id=fake_stripe_event["data"]["object"]["id"] ) assert schedule.status == "released" assert schedule.released_at is not None fake_stripe_event_2 = deepcopy(FAKE_EVENT_SUBSCRIPTION_SCHEDULE_RELEASED) fake_stripe_event_2["data"]["object"]["subscription"] = None schedule_retrieve_mock.return_value = fake_stripe_event_2["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event_2) event.invoke_webhook_handlers() schedule.refresh_from_db() assert schedule.status == "released" assert schedule.released_at is not None @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE) ) @patch("stripe.SubscriptionSchedule.retrieve", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) def test_subscription_schedule_updated( self, customer_retrieve_mock, schedule_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED) fake_stripe_event["data"]["object"]["subscription"] = None fake_subscription_schedule = deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) schedule_retrieve_mock.return_value = fake_subscription_schedule event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() schedule = SubscriptionSchedule.objects.get( id=fake_stripe_event["data"]["object"]["id"] ) assert schedule.released_at is None fake_stripe_event = deepcopy(FAKE_EVENT_SUBSCRIPTION_SCHEDULE_UPDATED) fake_stripe_event["data"]["object"]["released_at"] = 1605058030 fake_stripe_event["data"]["object"]["status"] = "released" fake_stripe_event["data"]["previous_attributes"] = { "released_at": None, "status": "not_started", } schedule_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() schedule = SubscriptionSchedule.objects.get( id=fake_stripe_event["data"]["object"]["id"] ) assert schedule.status == "released" assert schedule.released_at is not None @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE) ) @patch( "stripe.SubscriptionSchedule.retrieve", return_value=FAKE_SUBSCRIPTION_SCHEDULE, autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) def test_subscription_schedule_aborted( self, customer_retrieve_mock, schedule_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): fake_subscription = deepcopy(FAKE_SUBSCRIPTION) subscription_retrieve_mock.return_value = fake_subscription # create latest invoice (and the associated subscription) Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) event = Event.sync_from_stripe_data(FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED) event.invoke_webhook_handlers() schedule = SubscriptionSchedule.objects.get( id=FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED["data"]["object"]["id"] ) assert ( schedule.id == FAKE_EVENT_SUBSCRIPTION_SCHEDULE_CREATED["data"]["object"]["id"] ) assert schedule.subscription.id == FAKE_SUBSCRIPTION["id"] assert schedule.subscription.status == "active" # cancel the subscription fake_subscription["status"] = "canceled" Subscription.sync_from_stripe_data(fake_subscription) fake_stripe_event_2 = deepcopy(FAKE_EVENT_SUBSCRIPTION_SCHEDULE_ABORTED) schedule_retrieve_mock.return_value = fake_stripe_event_2["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event_2) event.invoke_webhook_handlers() schedule.refresh_from_db() assert schedule.status == "canceled" assert schedule.subscription.status == "canceled" assert schedule.canceled_at is not None class TestTaxIdEvents(EventTestCase): @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.Customer.retrieve_tax_id", return_value=deepcopy(FAKE_TAX_ID), autospec=True, ) @patch( "stripe.Event.retrieve", return_value=deepcopy(FAKE_EVENT_TAX_ID_CREATED), autospec=True, ) def test_tax_id_created( self, event_retrieve_mock, tax_id_retrieve_mock, customer_retrieve_mock ): event = Event.sync_from_stripe_data(FAKE_EVENT_TAX_ID_CREATED) event.invoke_webhook_handlers() tax_id = TaxId.objects.get() self.assertEqual(tax_id.id, FAKE_TAX_ID["id"]) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.Customer.retrieve_tax_id", autospec=True, ) @patch( "stripe.Event.retrieve", autospec=True, ) def test_tax_id_updated( self, event_retrieve_mock, tax_id_retrieve_mock, customer_retrieve_mock ): tax_id_retrieve_mock.return_value = FAKE_TAX_ID fake_stripe_create_event = deepcopy(FAKE_EVENT_TAX_ID_CREATED) event = Event.sync_from_stripe_data(fake_stripe_create_event) event.invoke_webhook_handlers() tax_id_retrieve_mock.return_value = FAKE_TAX_ID_UPDATED fake_stripe_update_event = deepcopy(FAKE_EVENT_TAX_ID_UPDATED) event = Event.sync_from_stripe_data(fake_stripe_update_event) event.invoke_webhook_handlers() tax_id = TaxId.objects.get() self.assertEqual(tax_id.id, FAKE_TAX_ID["id"]) self.assertEqual(tax_id.verification.get("status"), "verified") self.assertEqual(tax_id.verification.get("verified_name"), "Test") @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.Customer.retrieve_tax_id", autospec=True, ) @patch( "stripe.Event.retrieve", autospec=True, ) def test_tax_id_deleted( self, event_retrieve_mock, tax_id_retrieve_mock, customer_retrieve_mock ): tax_id_retrieve_mock.return_value = FAKE_TAX_ID fake_stripe_create_event = deepcopy(FAKE_EVENT_TAX_ID_CREATED) event = Event.sync_from_stripe_data(fake_stripe_create_event) event.invoke_webhook_handlers() tax_id_retrieve_mock.return_value = FAKE_EVENT_TAX_ID_DELETED fake_stripe_delete_event = deepcopy(FAKE_EVENT_TAX_ID_DELETED) event = Event.sync_from_stripe_data(fake_stripe_delete_event) event.invoke_webhook_handlers() self.assertFalse(TaxId.objects.filter(id=FAKE_TAX_ID["id"]).exists()) class TestTransferEvents(EventTestCase): @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_PLATFORM_ACCOUNT), autospec=True, ) @patch("stripe.Transfer.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) def test_transfer_created( self, event_retrieve_mock, transfer_retrieve_mock, account_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_TRANSFER_CREATED) event_retrieve_mock.return_value = fake_stripe_event transfer_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() transfer = Transfer.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual( transfer.amount, fake_stripe_event["data"]["object"]["amount"] / Decimal("100"), ) @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_PLATFORM_ACCOUNT), autospec=True, ) @patch("stripe.Transfer.retrieve", return_value=FAKE_TRANSFER, autospec=True) def test_transfer_deleted( self, transfer_retrieve_mock, account_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): event = self._create_event(FAKE_EVENT_TRANSFER_CREATED) event.invoke_webhook_handlers() Transfer.objects.get(id=FAKE_TRANSFER["id"]) event = self._create_event(FAKE_EVENT_TRANSFER_DELETED) event.invoke_webhook_handlers() with self.assertRaises(Transfer.DoesNotExist): Transfer.objects.get(id=FAKE_TRANSFER["id"]) event = self._create_event(FAKE_EVENT_TRANSFER_DELETED) event.invoke_webhook_handlers() class TestOrderEvents(EventTestCase): @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch("stripe.Order.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) def test_order_created( self, event_retrieve_mock, order_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_ORDER_CREATED) event_retrieve_mock.return_value = fake_stripe_event order_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() order = Order.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual(order.status, "open") self.assertEqual(order.payment_intent, None) self.assertEqual(order.customer.id, FAKE_CUSTOMER["id"]) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch("stripe.Order.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) def test_order_updated( self, event_retrieve_mock, order_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_ORDER_UPDATED) event_retrieve_mock.return_value = fake_stripe_event order_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() order = Order.objects.get(id=fake_stripe_event["data"]["object"]["id"]) # assert email got updated self.assertEqual(order.billing_details["email"], "testuser@example.com") self.assertEqual(order.payment_intent.id, FAKE_PAYMENT_INTENT_I["id"]) self.assertEqual(order.customer.id, FAKE_CUSTOMER["id"]) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch("stripe.Order.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) def test_order_submitted( self, event_retrieve_mock, order_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_ORDER_SUBMITTED) event_retrieve_mock.return_value = fake_stripe_event order_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() order = Order.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual(order.status, "submitted") self.assertEqual(order.billing_details["email"], "testuser@example.com") self.assertEqual(order.payment_intent.id, FAKE_PAYMENT_INTENT_I["id"]) self.assertEqual(order.customer.id, FAKE_CUSTOMER["id"]) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch("stripe.Order.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) def test_order_processing( self, event_retrieve_mock, order_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_ORDER_PROCESSING) event_retrieve_mock.return_value = fake_stripe_event order_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() order = Order.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual(order.status, "processing") self.assertEqual(order.billing_details["email"], "testuser@example.com") self.assertEqual(order.payment_intent.id, FAKE_PAYMENT_INTENT_I["id"]) self.assertEqual(order.customer.id, FAKE_CUSTOMER["id"]) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch("stripe.Order.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) def test_order_cancelled( self, event_retrieve_mock, order_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_ORDER_CANCELLED) event_retrieve_mock.return_value = fake_stripe_event order_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() order = Order.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual(order.status, "canceled") self.assertEqual(order.billing_details["email"], "testuser@example.com") self.assertEqual(order.payment_intent.id, FAKE_PAYMENT_INTENT_I["id"]) self.assertEqual(order.customer.id, FAKE_CUSTOMER["id"]) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch("stripe.Order.retrieve", autospec=True) @patch("stripe.Event.retrieve", autospec=True) def test_order_complet( self, event_retrieve_mock, order_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): fake_stripe_event = deepcopy(FAKE_EVENT_ORDER_COMPLETED) event_retrieve_mock.return_value = fake_stripe_event order_retrieve_mock.return_value = fake_stripe_event["data"]["object"] event = Event.sync_from_stripe_data(fake_stripe_event) event.invoke_webhook_handlers() order = Order.objects.get(id=fake_stripe_event["data"]["object"]["id"]) self.assertEqual(order.status, "complete") self.assertEqual(order.billing_details["email"], "testuser@example.com") self.assertEqual(order.payment_intent.id, FAKE_PAYMENT_INTENT_I["id"]) self.assertEqual(order.customer.id, FAKE_CUSTOMER["id"]) ================================================ FILE: tests/test_fields.py ================================================ """ dj-stripe Custom Field Tests. """ from datetime import datetime from decimal import Decimal import pytest from django.test.testcases import TestCase from django.test.utils import override_settings from djstripe.fields import StripeDateTimeField, StripeDecimalCurrencyAmountField from djstripe.utils import get_timezone_utc from tests.fields.models import ExampleDecimalModel pytestmark = pytest.mark.django_db class TestStripeDecimalCurrencyAmountField: noval = StripeDecimalCurrencyAmountField(name="noval") def test_stripe_to_db_none_val(self): assert self.noval.stripe_to_db({"noval": None}) is None @pytest.mark.parametrize( "expected,inputted", [ (Decimal("1"), Decimal("100")), (Decimal("1.5"), Decimal("150")), (Decimal("0"), Decimal("0")), ], ) def test_stripe_to_db_decimal_val(self, expected, inputted): assert expected == self.noval.stripe_to_db({"noval": inputted}) @override_settings(USE_TZ=get_timezone_utc()) class TestStripeDateTimeField(TestCase): noval = StripeDateTimeField(name="noval") def test_stripe_to_db_none_val(self): self.assertEqual(None, self.noval.stripe_to_db({"noval": None})) def test_stripe_to_db_datetime_val(self): self.assertEqual( datetime(1997, 9, 18, 7, 48, 35, tzinfo=get_timezone_utc()), self.noval.stripe_to_db({"noval": 874568915}), ) class TestStripePercentField: @pytest.mark.parametrize( "inputted,expected", [ (Decimal("1"), Decimal("1.00")), (Decimal("1.5234567"), Decimal("1.52")), (Decimal("0"), Decimal("0.00")), (Decimal("23.2345678"), Decimal("23.23")), ("1", Decimal("1.00")), ("1.5234567", Decimal("1.52")), ("0", Decimal("0.00")), ("23.2345678", Decimal("23.23")), (1, Decimal("1.00")), (1.5234567, Decimal("1.52")), (0, Decimal("0.00")), (23.2345678, Decimal("23.24")), ], ) def test_stripe_percent_field(self, inputted, expected): # create a model with the StripePercentField model_field = ExampleDecimalModel(noval=inputted) model_field.save() # get the field data field_data = ExampleDecimalModel.objects.get(pk=model_field.pk).noval assert isinstance(field_data, Decimal) assert field_data == expected ================================================ FILE: tests/test_file_link.py ================================================ """ dj-stripe FileLink model tests """ from copy import deepcopy from unittest.mock import patch import pytest from django.test import TestCase from djstripe.models import File, FileLink from djstripe.settings import djstripe_settings from . import FAKE_FILEUPLOAD_ICON pytestmark = pytest.mark.django_db class TestFileLink(TestCase): @patch( target="stripe.File.retrieve", autospec=True, return_value=deepcopy(FAKE_FILEUPLOAD_ICON), ) @patch( target="stripe.FileLink.retrieve", autospec=True, return_value=deepcopy(FAKE_FILEUPLOAD_ICON["links"]["data"][0]), ) def test___str__(self, mock_file_link_retrieve, mock_file_upload_retrieve): file_link_data = deepcopy(FAKE_FILEUPLOAD_ICON["links"]["data"][0]) file_link = FileLink.sync_from_stripe_data(file_link_data) assert (f"{FAKE_FILEUPLOAD_ICON['filename']}, {file_link_data['url']}") == str( file_link ) @patch( target="stripe.File.retrieve", autospec=True, return_value=deepcopy(FAKE_FILEUPLOAD_ICON), ) @patch( target="stripe.FileLink.retrieve", autospec=True, return_value=deepcopy(FAKE_FILEUPLOAD_ICON["links"]["data"][0]), ) def test_sync_from_stripe_data( self, mock_file_link_retrieve, mock_file_upload_retrieve ): file_link_data = deepcopy(FAKE_FILEUPLOAD_ICON["links"]["data"][0]) file_link = FileLink.sync_from_stripe_data(file_link_data) mock_file_link_retrieve.assert_not_called() mock_file_upload_retrieve.assert_called_once_with( id=file_link_data["file"], api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], stripe_account=None, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) assert file_link.file == File.objects.get(id=file_link_data["file"]) assert file_link.url == file_link_data["url"] ================================================ FILE: tests/test_file_upload.py ================================================ """ dj-stripe File model tests """ from copy import deepcopy from unittest.mock import ANY, call, patch import pytest from django.test import TestCase from djstripe.enums import FilePurpose from djstripe.models import Account, File from djstripe.settings import djstripe_settings from . import FAKE_ACCOUNT, FAKE_FILEUPLOAD_ICON, FAKE_FILEUPLOAD_LOGO pytestmark = pytest.mark.django_db class TestFileLink(TestCase): @patch( target="stripe.File.retrieve", autospec=True, return_value=deepcopy(FAKE_FILEUPLOAD_ICON), ) def test_file_upload_api_retrieve(self, mock_file_upload_retrieve): """Expect file_upload to use the ID of the account referring to it to retrieve itself. """ # Create files icon_file = File._get_or_create_from_stripe_object(data=FAKE_FILEUPLOAD_ICON)[0] logo_file = File._get_or_create_from_stripe_object(data=FAKE_FILEUPLOAD_LOGO)[0] # Create account to associate the files to it account = Account._get_or_create_from_stripe_object(data=FAKE_ACCOUNT)[0] # Call the API retrieve methods. icon_file.api_retrieve() logo_file.api_retrieve() # Ensure the correct Account ID was used in retrieval mock_file_upload_retrieve.assert_has_calls( ( call( id=icon_file.id, api_key=ANY, expand=ANY, stripe_account=account.id, stripe_version=djstripe_settings.STRIPE_API_VERSION, ), call( id=logo_file.id, api_key=ANY, expand=ANY, stripe_account=account.id, stripe_version=djstripe_settings.STRIPE_API_VERSION, ), ) ) @patch( target="stripe.File.retrieve", autospec=True, return_value=deepcopy(FAKE_FILEUPLOAD_ICON), ) def test_sync_from_stripe_data(self, mock_file_upload_retrieve): file = File.sync_from_stripe_data(deepcopy(FAKE_FILEUPLOAD_ICON)) mock_file_upload_retrieve.assert_not_called() assert file.id == FAKE_FILEUPLOAD_ICON["id"] assert file.purpose == FAKE_FILEUPLOAD_ICON["purpose"] assert file.type == FAKE_FILEUPLOAD_ICON["type"] class TestFileUploadStr: @pytest.mark.parametrize("file_purpose", FilePurpose.__members__) def test___str__(self, file_purpose): modified_file_data = deepcopy(FAKE_FILEUPLOAD_ICON) modified_file_data["purpose"] = file_purpose file = File.sync_from_stripe_data(modified_file_data) assert ( f"{modified_file_data['filename']}, {FilePurpose.humanize(modified_file_data['purpose'])}" ) == str(file) ================================================ FILE: tests/test_forms.py ================================================ """ dj-stripe form tests """ import pytest from django import forms from django.contrib.admin import helpers from django.forms.utils import ErrorDict from djstripe import enums, utils from djstripe.admin.forms import APIKeyAdminCreateForm, CustomActionForm from tests import FAKE_PLATFORM_ACCOUNT from .fields.models import CustomActionModel from .test_apikey import RK_LIVE, RK_TEST, SK_LIVE, SK_TEST pytestmark = pytest.mark.django_db class TestCustomActionForm: @pytest.mark.parametrize( "action_name", ["_sync_all_instances", "_resync_instances"] ) def test___init__(self, action_name, monkeypatch): # monkeypatch utils.get_model def mock_get_model(*args, **kwargs): return model monkeypatch.setattr(utils, "get_model", mock_get_model) model = CustomActionModel # create instances to be used in the Django Admin Action inst_1 = model.objects.create(id="test") inst_2 = model.objects.create(id="test-2") pk_values = [inst_1.pk, inst_2.pk] form = CustomActionForm( model_name=CustomActionModel._meta.model_name, action_name=action_name, ) # assert _selected_action_field has been added to the form _selected_action_field = form.fields[helpers.ACTION_CHECKBOX_NAME] assert _selected_action_field is not None # assert _selected_action_field is an instance of MultipleHiddenInput assert isinstance(_selected_action_field.widget, forms.MultipleHiddenInput) if action_name == "_sync_all_instances": assert _selected_action_field.choices == [ ("_sync_all_instances", "_sync_all_instances") ] else: assert _selected_action_field.choices == list(zip(pk_values, pk_values)) class TestAPIKeyAdminCreateForm: @pytest.mark.parametrize("secret", [SK_TEST, SK_LIVE, RK_TEST, RK_LIVE]) def test__post_clean(self, secret, monkeypatch): form = APIKeyAdminCreateForm(data={"name": "Test Secret Key", "secret": secret}) # Manually invoking internals of Form.full_clean() to isolate # Form._post_clean form._errors = ErrorDict() form.cleaned_data = {} form._clean_fields() form._clean_form() # assert form is valid but instance is not yet saved in the db. assert form.instance.pk is None assert form.is_valid() is True # assert that the instance does not have the owner account populated assert form.instance.djstripe_owner_account is None # Invoke _post_clean() form._post_clean() if secret.startswith("sk_"): assert form.instance.type == enums.APIKeyType.secret assert ( form.instance.djstripe_owner_account.id == FAKE_PLATFORM_ACCOUNT["id"] ) elif secret.startswith("rk_"): assert form.instance.type == enums.APIKeyType.restricted assert form.instance.djstripe_owner_account is None ================================================ FILE: tests/test_idempotency_keys.py ================================================ from datetime import timedelta from django.test import TestCase from django.utils.timezone import now from djstripe.models import IdempotencyKey from djstripe.settings import djstripe_settings from djstripe.utils import clear_expired_idempotency_keys class IdempotencyKeyTest(TestCase): def test_generate_idempotency_key(self): key1 = djstripe_settings.get_idempotency_key("customer", "create:1", False) key2 = djstripe_settings.get_idempotency_key("customer", "create:1", False) self.assertTrue(key1 == key2) key3 = djstripe_settings.get_idempotency_key("customer", "create:2", False) self.assertTrue(key1 != key3) key4 = djstripe_settings.get_idempotency_key("charge", "create:1", False) self.assertTrue(key1 != key4) self.assertEqual(IdempotencyKey.objects.count(), 3) key1_obj = IdempotencyKey.objects.get( action="customer:create:1", livemode=False ) self.assertFalse(key1_obj.is_expired) self.assertEqual(str(key1_obj), str(key1_obj.uuid)) def test_clear_expired_idempotency_keys(self): expired_key = djstripe_settings.get_idempotency_key( "customer", "create:1", False ) expired_key_obj = IdempotencyKey.objects.get(uuid=expired_key) expired_key_obj.created = now() - timedelta(hours=25) expired_key_obj.save() valid_key = djstripe_settings.get_idempotency_key("customer", "create:2", False) self.assertEqual(IdempotencyKey.objects.count(), 2) clear_expired_idempotency_keys() self.assertEqual(IdempotencyKey.objects.count(), 1) self.assertEqual(str(IdempotencyKey.objects.get().uuid), valid_key) ================================================ FILE: tests/test_integrations/README.md ================================================ Integration Tests ================= All the tests in this directory interact with the stripe API. In order to make them fire, you need to set the STRIPE\_PUBLIC\_KEY and STRIPE\_PRIVATE\_KEY in your environment. Otherwise these tests will fail. ================================================ FILE: tests/test_integrations/__init__.py ================================================ ================================================ FILE: tests/test_invoice.py ================================================ """ dj-stripe Invoice Model Tests. """ from copy import deepcopy from decimal import Decimal from unittest.mock import call, patch import pytest import stripe from django.contrib.auth import get_user_model from django.test.testcases import TestCase from stripe.error import InvalidRequestError from djstripe.enums import InvoiceStatus from djstripe.models import Invoice, Plan, Subscription, UpcomingInvoice from djstripe.settings import djstripe_settings from . import ( FAKE_BALANCE_TRANSACTION, FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CHARGE, FAKE_CUSTOMER, FAKE_INVOICE, FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE, FAKE_INVOICEITEM, FAKE_LINE_ITEM_SUBSCRIPTION, FAKE_PAYMENT_INTENT_I, FAKE_PLAN, FAKE_PLATFORM_ACCOUNT, FAKE_PRODUCT, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_ITEM, FAKE_TAX_RATE_EXAMPLE_1_VAT, FAKE_TAX_RATE_EXAMPLE_2_SALES, FAKE_UPCOMING_INVOICE, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class InvoiceTest(AssertStripeFksMixin, TestCase): def setUp(self): # create a Stripe Platform Account self.account = FAKE_PLATFORM_ACCOUNT.create() user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) self.customer = FAKE_CUSTOMER.create_for_user(user) self.default_expected_blank_fks = { "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.Product.default_price", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", } @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE) ) def test_sync_from_stripe_data( self, invoice_retrieve_mock, invoice_item_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, customer_retrieve_mock, ): default_account_mock.return_value = self.account invoice = Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) assert invoice assert ( str(invoice) == f"Invoice #{FAKE_INVOICE['number']} for $20.00 USD (paid)" ) self.assertGreater(len(invoice.status_transitions.keys()), 1) self.assertTrue(bool(invoice.account_country)) self.assertTrue(bool(invoice.account_name)) self.assertTrue(bool(invoice.collection_method)) self.assertEqual(invoice.default_tax_rates.count(), 1) self.assertEqual( invoice.default_tax_rates.first().id, FAKE_TAX_RATE_EXAMPLE_1_VAT["id"] ) self.assertEqual(invoice.total_tax_amounts.count(), 1) first_tax_amount = invoice.total_tax_amounts.first() self.assertEqual( first_tax_amount.tax_rate.id, FAKE_TAX_RATE_EXAMPLE_1_VAT["id"] ) self.assertEqual( first_tax_amount.inclusive, FAKE_TAX_RATE_EXAMPLE_1_VAT["inclusive"] ) self.assertEqual(first_tax_amount.amount, 261) self.assert_fks(invoice, expected_blank_fks=self.default_expected_blank_fks) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE) ) def test_sync_from_stripe_data_update_total_tax_amounts( self, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, customer_retrieve_mock, ): default_account_mock.return_value = self.account invoice = Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) # as per basic sync test self.assertEqual(invoice.default_tax_rates.count(), 1) self.assertEqual( invoice.default_tax_rates.first().id, FAKE_TAX_RATE_EXAMPLE_1_VAT["id"] ) self.assertEqual(invoice.total_tax_amounts.count(), 1) first_tax_amount = invoice.total_tax_amounts.first() self.assertEqual( first_tax_amount.tax_rate.id, FAKE_TAX_RATE_EXAMPLE_1_VAT["id"] ) self.assertEqual( first_tax_amount.inclusive, FAKE_TAX_RATE_EXAMPLE_1_VAT["inclusive"] ) self.assertEqual(first_tax_amount.amount, 261) self.assert_fks(invoice, expected_blank_fks=self.default_expected_blank_fks) # Now update with a different tax rate # TODO - should update tax rate in invoice items etc as well, # but here we're mainly testing that invoice.total_tax_rates is # correctly updated fake_updated_invoice = deepcopy(FAKE_INVOICE) fake_tax_rate_2 = deepcopy(FAKE_TAX_RATE_EXAMPLE_2_SALES) new_tax_amount = int( fake_updated_invoice["total"] * fake_tax_rate_2["percentage"] / 100 ) fake_updated_invoice.update( { "default_tax_rates": [fake_tax_rate_2], "tax": new_tax_amount, "total": fake_updated_invoice["total"] + new_tax_amount, "total_tax_amounts": [ { "amount": new_tax_amount, "inclusive": False, "tax_rate": fake_tax_rate_2["id"], } ], } ) invoice_updated = Invoice.sync_from_stripe_data(fake_updated_invoice) self.assertEqual(invoice_updated.default_tax_rates.count(), 1) self.assertEqual( invoice_updated.default_tax_rates.first().id, fake_tax_rate_2["id"] ) self.assertEqual(invoice_updated.total_tax_amounts.count(), 1) first_tax_amount = invoice_updated.total_tax_amounts.first() self.assertEqual(first_tax_amount.tax_rate.id, fake_tax_rate_2["id"]) self.assertEqual(first_tax_amount.inclusive, fake_tax_rate_2["inclusive"]) self.assertEqual(first_tax_amount.amount, new_tax_amount) self.assert_fks( invoice_updated, expected_blank_fks=self.default_expected_blank_fks ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, ) def test_sync_from_stripe_data_default_payment_method( self, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, customer_retrieve_mock, ): default_account_mock.return_value = self.account fake_invoice = deepcopy(FAKE_INVOICE) fake_invoice["default_payment_method"] = deepcopy(FAKE_CARD_AS_PAYMENT_METHOD) invoice_retrieve_mock.return_value = fake_invoice invoice = Invoice.sync_from_stripe_data(fake_invoice) self.assertEqual( invoice.default_payment_method.id, FAKE_CARD_AS_PAYMENT_METHOD["id"] ) self.assert_fks( invoice, expected_blank_fks=self.default_expected_blank_fks - {"djstripe.Invoice.default_payment_method"}, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) def test_billing_reason_enum( self, invoice_item_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_invoice = deepcopy(FAKE_INVOICE) for billing_reason in ( "subscription_cycle", "subscription_create", "subscription_update", "subscription", "manual", "upcoming", "subscription_threshold", ): fake_invoice["billing_reason"] = billing_reason invoice = Invoice.sync_from_stripe_data(fake_invoice) self.assertEqual(invoice.billing_reason, billing_reason) # trigger model field validation (including enum value choices check) invoice.full_clean() @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) def test_invoice_status_enum( self, invoice_item_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_invoice = deepcopy(FAKE_INVOICE) for status in ( "draft", "open", "paid", "uncollectible", "void", ): fake_invoice["status"] = status invoice = Invoice.sync_from_stripe_data(fake_invoice) self.assertEqual(invoice.status, status) # trigger model field validation (including enum value choices check) invoice.full_clean() @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch("stripe.Invoice.retrieve", autospec=True) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_retry_true( self, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, ): default_account_mock.return_value = self.account fake_invoice = deepcopy(FAKE_INVOICE) fake_invoice.update({"paid": False, "status": "open"}) fake_invoice.update({"auto_advance": True}) invoice_retrieve_mock.return_value = fake_invoice invoice = Invoice.sync_from_stripe_data(fake_invoice) return_value = invoice.retry() invoice_retrieve_mock.assert_called_once_with( id=invoice.id, api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=["discounts"], stripe_account=invoice.djstripe_owner_account.id, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) self.assertTrue(return_value) self.assert_fks(invoice, expected_blank_fks=self.default_expected_blank_fks) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch("stripe.Invoice.retrieve", autospec=True) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_retry_false( self, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, ): default_account_mock.return_value = self.account fake_invoice = deepcopy(FAKE_INVOICE) invoice_retrieve_mock.return_value = fake_invoice invoice = Invoice.sync_from_stripe_data(fake_invoice) return_value = invoice.retry() self.assertFalse(invoice_retrieve_mock.called) self.assertFalse(return_value) self.assert_fks(invoice, expected_blank_fks=self.default_expected_blank_fks) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) def test_status_draft( self, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account invoice_data = deepcopy(FAKE_INVOICE) invoice_data.update({"paid": False, "status": "draft"}) invoice = Invoice.sync_from_stripe_data(invoice_data) self.assertEqual(InvoiceStatus.draft, invoice.status) self.assert_fks(invoice, expected_blank_fks=self.default_expected_blank_fks) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) def test_status_open( self, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account invoice_data = deepcopy(FAKE_INVOICE) invoice_data.update({"paid": False, "status": "open"}) invoice = Invoice.sync_from_stripe_data(invoice_data) self.assertEqual(InvoiceStatus.open, invoice.status) self.assert_fks(invoice, expected_blank_fks=self.default_expected_blank_fks) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) def test_status_paid( self, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account invoice = Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) self.assertEqual(InvoiceStatus.paid, invoice.status) self.assert_fks(invoice, expected_blank_fks=self.default_expected_blank_fks) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) def test_status_uncollectible( self, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account invoice_data = deepcopy(FAKE_INVOICE) invoice_data.update({"paid": False, "status": "uncollectible"}) invoice = Invoice.sync_from_stripe_data(invoice_data) self.assertEqual(InvoiceStatus.uncollectible, invoice.status) self.assert_fks(invoice, expected_blank_fks=self.default_expected_blank_fks) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) def test_status_void( self, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account invoice_data = deepcopy(FAKE_INVOICE) invoice_data.update({"paid": False, "status": "void"}) invoice = Invoice.sync_from_stripe_data(invoice_data) self.assertEqual(InvoiceStatus.void, invoice.status) self.assert_fks(invoice, expected_blank_fks=self.default_expected_blank_fks) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, return_value=deepcopy(FAKE_SUBSCRIPTION), ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_sync_no_subscription( self, product_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, subscription_item_retrieve_mock, plan_retrieve_mock, paymentmethod_card_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, customer_retrieve_mock, ): default_account_mock.return_value = self.account invoice_data = deepcopy(FAKE_INVOICE) invoice_data.update({"subscription": None}) invoice_data["lines"]["data"][0]["subscription"] = None invoice_retrieve_mock.return_value = invoice_data invoice = Invoice.sync_from_stripe_data(invoice_data) self.assertEqual(None, invoice.subscription) self.assertEqual(FAKE_CHARGE["id"], invoice.charge.id) plan_retrieve_mock.assert_not_called() self.assert_fks( invoice, expected_blank_fks=self.default_expected_blank_fks | {"djstripe.Invoice.subscription"}, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) def test_invoice_with_subscription_invoice_items( self, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account invoice_data = deepcopy(FAKE_INVOICE) invoice = Invoice.sync_from_stripe_data(invoice_data) items = invoice.invoiceitems.all() self.assertEqual(1, len(items)) self.assertEqual(items[0].id, "ii_fakefakefakefakefake0001") self.assertIsNone(items[0].subscription) self.assert_fks(invoice, expected_blank_fks=self.default_expected_blank_fks) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_invoice_with_no_invoice_items( self, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account invoice_data = deepcopy(FAKE_INVOICE) invoice_data["lines"] = [] invoice = Invoice.sync_from_stripe_data(invoice_data) self.assertIsNotNone(invoice.plan) # retrieved from invoice item self.assertEqual(FAKE_PLAN["id"], invoice.plan.id) self.assert_fks(invoice, expected_blank_fks=self.default_expected_blank_fks) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) def test_invoice_with_non_subscription_invoice_items( self, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account invoice_data = deepcopy(FAKE_INVOICE) invoice_data["lines"]["data"].append(deepcopy(FAKE_LINE_ITEM_SUBSCRIPTION)) invoice_data["lines"]["total_count"] += 1 invoice = Invoice.sync_from_stripe_data(invoice_data) self.assertIsNotNone(invoice) # assert only 1 line item of type="invoice_item" self.assertEqual(1, len(invoice.invoiceitems.all())) self.assert_fks(invoice, expected_blank_fks=self.default_expected_blank_fks) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) def test_invoice_plan_from_invoice_items( self, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account invoice_data = deepcopy(FAKE_INVOICE) invoice = Invoice.sync_from_stripe_data(invoice_data) self.assertIsNotNone(invoice.plan) # retrieved from invoice item self.assertEqual(FAKE_PLAN["id"], invoice.plan.id) self.assert_fks(invoice, expected_blank_fks=self.default_expected_blank_fks) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) def test_invoice_plan_from_subscription( self, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account invoice_data = deepcopy(FAKE_INVOICE) invoice_data["lines"]["data"][0]["plan"] = None invoice = Invoice.sync_from_stripe_data(invoice_data) self.assertIsNotNone(invoice.plan) # retrieved from subscription self.assertEqual(FAKE_PLAN["id"], invoice.plan.id) self.assert_fks(invoice, expected_blank_fks=self.default_expected_blank_fks) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch("stripe.Subscription.retrieve", autospec=True) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", autospec=True, ) def test_invoice_without_plan( self, invoice_item_retrieve_mock, product_retrieve_mock, charge_retrieve_mock, paymentmethod_card_retrieve_mock, payment_intent_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account invoice_data = deepcopy(FAKE_INVOICE) invoice_data["lines"]["data"][0]["plan"] = None invoice_data["lines"]["data"][0]["subscription"] = None invoice_data["subscription"] = None fake_invoice_item = deepcopy(FAKE_INVOICEITEM) fake_invoice_item["subscription"] = None invoice_item_retrieve_mock.return_value = fake_invoice_item subscription_retrieve_mock.return_value = deepcopy(FAKE_SUBSCRIPTION) invoice = Invoice.sync_from_stripe_data(invoice_data) self.assertIsNone(invoice.plan) self.assert_fks( invoice, expected_blank_fks=self.default_expected_blank_fks | {"djstripe.Invoice.subscription"}, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE) ) @patch( "stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.Invoice.upcoming", autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_upcoming_invoice( self, product_retrieve_mock, invoice_upcoming_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, plan_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, ): fake_upcoming_invoice_data = deepcopy(FAKE_UPCOMING_INVOICE) fake_upcoming_invoice_data["lines"]["data"][0][ "subscription" ] = FAKE_SUBSCRIPTION["id"] invoice_upcoming_mock.return_value = fake_upcoming_invoice_data fake_subscription_item_data = deepcopy(FAKE_SUBSCRIPTION_ITEM) fake_subscription_item_data["plan"] = deepcopy(FAKE_PLAN) fake_subscription_item_data["subscription"] = deepcopy(FAKE_SUBSCRIPTION)["id"] subscription_item_retrieve_mock.return_value = fake_subscription_item_data invoice = UpcomingInvoice.upcoming() self.assertIsNotNone(invoice) self.assertIsNone(invoice.id) self.assertIsNone(invoice.save()) self.assertEqual(invoice.get_stripe_dashboard_url(), "") invoice.id = "foo" self.assertIsNone(invoice.id) # one more because of creating the associated line item subscription_retrieve_mock.assert_has_calls( [ call( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_SUBSCRIPTION["id"], stripe_account=None, stripe_version="2020-08-27", ), call( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_SUBSCRIPTION["id"], stripe_account=None, stripe_version="2020-08-27", ), ] ) plan_retrieve_mock.assert_not_called() items = invoice.lineitems.all() self.assertEqual(1, len(items)) self.assertEqual("il_fakefakefakefakefake0002", items[0].id) self.assertEqual(0, invoice.invoiceitems.count()) # delete/update should do nothing self.assertEqual(invoice.lineitems.update(), 0) self.assertEqual(invoice.lineitems.delete(), 0) self.assertIsNotNone(invoice.plan) self.assertEqual(FAKE_PLAN["id"], invoice.plan.id) invoice._lineitems = [] items = invoice.lineitems.all() self.assertEqual(0, len(items)) self.assertIsNotNone(invoice.plan) self.assertEqual(invoice.default_tax_rates.count(), 1) self.assertEqual( invoice.default_tax_rates.first().id, FAKE_TAX_RATE_EXAMPLE_1_VAT["id"] ) self.assertEqual(invoice.total_tax_amounts.count(), 1) first_tax_amount = invoice.total_tax_amounts.first() self.assertEqual( first_tax_amount.tax_rate.id, FAKE_TAX_RATE_EXAMPLE_1_VAT["id"] ) self.assertEqual( first_tax_amount.inclusive, FAKE_TAX_RATE_EXAMPLE_1_VAT["inclusive"] ) self.assertEqual(first_tax_amount.amount, 261) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE) ) @patch("stripe.Plan.retrieve", autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.Invoice.upcoming", autospec=True, ) def test_upcoming_invoice_with_subscription( self, invoice_upcoming_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, ): fake_upcoming_invoice_data = deepcopy(FAKE_UPCOMING_INVOICE) fake_upcoming_invoice_data["lines"]["data"][0][ "subscription" ] = FAKE_SUBSCRIPTION["id"] invoice_upcoming_mock.return_value = fake_upcoming_invoice_data fake_subscription_item_data = deepcopy(FAKE_SUBSCRIPTION_ITEM) fake_subscription_item_data["plan"] = deepcopy(FAKE_PLAN) fake_subscription_item_data["subscription"] = deepcopy(FAKE_SUBSCRIPTION)["id"] subscription_item_retrieve_mock.return_value = fake_subscription_item_data invoice = Invoice.upcoming( subscription=Subscription(id=FAKE_SUBSCRIPTION["id"]) ) self.assertIsNotNone(invoice) self.assertIsNone(invoice.id) self.assertIsNone(invoice.save()) # one more because of creating the associated line item subscription_retrieve_mock.assert_has_calls( [ call( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_SUBSCRIPTION["id"], stripe_account=None, stripe_version="2020-08-27", ), call( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_SUBSCRIPTION["id"], stripe_account=None, stripe_version="2020-08-27", ), ] ) plan_retrieve_mock.assert_not_called() self.assertIsNotNone(invoice.plan) self.assertEqual(FAKE_PLAN["id"], invoice.plan.id) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch("stripe.Invoice.retrieve", autospec=True) @patch("stripe.Plan.retrieve", autospec=True) @patch( "stripe.SubscriptionItem.retrieve", autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, ) @patch( "stripe.Invoice.upcoming", autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_upcoming_invoice_with_subscription_plan( self, product_retrieve_mock, invoice_upcoming_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, plan_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, customer_retrieve_mock, ): fake_upcoming_invoice_data = deepcopy(FAKE_UPCOMING_INVOICE) fake_upcoming_invoice_data[ "subscription" ] = FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE["id"] invoice_upcoming_mock.return_value = fake_upcoming_invoice_data fake_invoice_data = deepcopy(FAKE_INVOICE) fake_invoice_data["subscription"] = FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE[ "id" ] fake_invoice_data["lines"]["data"][0][ "subscription" ] = FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE["id"] fake_invoice_data["lines"]["data"][0]["discounts"][0][ "subscription" ] = FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE["id"] invoice_retrieve_mock.return_value = fake_invoice_data fake_subscription_item_data = deepcopy(FAKE_SUBSCRIPTION_ITEM) fake_subscription_item_data["plan"] = deepcopy(FAKE_PLAN) subscription_item_retrieve_mock.return_value = fake_subscription_item_data fake_subscription_data = deepcopy(FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE) fake_subscription_data["plan"] = deepcopy(FAKE_PLAN) subscription_retrieve_mock.return_value = fake_subscription_data invoice = Invoice.upcoming(subscription_plan=Plan(id=FAKE_PLAN["id"])) self.assertIsNotNone(invoice) self.assertIsNone(invoice.id) self.assertIsNone(invoice.save()) subscription_retrieve_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=[], id=FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE["id"], stripe_account=None, stripe_version="2020-08-27", ) plan_retrieve_mock.assert_not_called() self.assertIsNotNone(invoice.plan) self.assertEqual(FAKE_PLAN["id"], invoice.plan.id) @patch( "stripe.Invoice.upcoming", side_effect=InvalidRequestError("Nothing to invoice for customer", None), ) def test_no_upcoming_invoices(self, invoice_upcoming_mock): invoice = Invoice.upcoming() self.assertIsNone(invoice) @patch( "stripe.Invoice.upcoming", side_effect=InvalidRequestError("Some other error", None), ) def test_upcoming_invoice_error(self, invoice_upcoming_mock): with self.assertRaises(InvalidRequestError): Invoice.upcoming() class TestInvoiceDecimal: @pytest.mark.parametrize( "inputted,expected", [ (Decimal("1"), Decimal("1.00")), (Decimal("1.5234567"), Decimal("1.52")), (Decimal("0"), Decimal("0.00")), (Decimal("23.2345678"), Decimal("23.23")), ("1", Decimal("1.00")), ("1.5234567", Decimal("1.52")), ("0", Decimal("0.00")), ("23.2345678", Decimal("23.23")), (1, Decimal("1.00")), (1.5234567, Decimal("1.52")), (0, Decimal("0.00")), (23.2345678, Decimal("23.24")), ], ) def test_decimal_tax_percent(self, inputted, expected, monkeypatch): # noqa: C901 fake_invoice = deepcopy(FAKE_INVOICE) fake_invoice["tax_percent"] = inputted def mock_invoice_get(*args, **kwargs): return fake_invoice def mock_invoice_item_get(*args, **kwargs): return FAKE_INVOICEITEM def mock_customer_get(*args, **kwargs): return FAKE_CUSTOMER def mock_charge_get(*args, **kwargs): return FAKE_CHARGE def mock_payment_method_get(*args, **kwargs): return FAKE_CARD_AS_PAYMENT_METHOD def mock_payment_intent_get(*args, **kwargs): return FAKE_PAYMENT_INTENT_I def mock_subscription_get(*args, **kwargs): return FAKE_SUBSCRIPTION def mock_subscriptionitem_get(*args, **kwargs): return FAKE_SUBSCRIPTION_ITEM def mock_balance_transaction_get(*args, **kwargs): return FAKE_BALANCE_TRANSACTION def mock_product_get(*args, **kwargs): return FAKE_PRODUCT # monkeypatch stripe retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Invoice, "retrieve", mock_invoice_get) monkeypatch.setattr(stripe.InvoiceItem, "retrieve", mock_invoice_item_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr( stripe.BalanceTransaction, "retrieve", mock_balance_transaction_get ) monkeypatch.setattr(stripe.Subscription, "retrieve", mock_subscription_get) monkeypatch.setattr( stripe.SubscriptionItem, "retrieve", mock_subscriptionitem_get ) monkeypatch.setattr(stripe.Charge, "retrieve", mock_charge_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) monkeypatch.setattr(stripe.PaymentIntent, "retrieve", mock_payment_intent_get) monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) invoice = Invoice.sync_from_stripe_data(fake_invoice) field_data = invoice.tax_percent assert isinstance(field_data, Decimal) assert field_data == expected ================================================ FILE: tests/test_invoiceitem.py ================================================ """ dj-stripe InvoiceItem Model Tests. """ from copy import deepcopy from unittest.mock import patch from django.test.testcases import TestCase from djstripe.models import Invoice, InvoiceItem from djstripe.models.payment_methods import Card from djstripe.settings import djstripe_settings from . import ( FAKE_BALANCE_TRANSACTION, FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CARD_II, FAKE_CHARGE, FAKE_CHARGE_II, FAKE_CUSTOMER, FAKE_CUSTOMER_II, FAKE_INVOICE, FAKE_INVOICE_II, FAKE_INVOICEITEM, FAKE_INVOICEITEM_III, FAKE_PAYMENT_INTENT_I, FAKE_PAYMENT_INTENT_II, FAKE_PAYMENT_METHOD_II, FAKE_PLAN, FAKE_PLAN_II, FAKE_PLATFORM_ACCOUNT, FAKE_PRICE_II, FAKE_PRODUCT, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_III, FAKE_SUBSCRIPTION_ITEM, FAKE_TAX_RATE_EXAMPLE_1_VAT, AssertStripeFksMixin, ) class InvoiceItemTest(AssertStripeFksMixin, TestCase): def setUp(self): # create a Stripe Platform Account self.account = FAKE_PLATFORM_ACCOUNT.create() self.default_expected_blank_fks = { "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.subscriber", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.Invoice.payment_intent", "djstripe.PaymentIntent.invoice (related name)", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.Product.default_price", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", } @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, return_value=deepcopy(FAKE_SUBSCRIPTION), ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True, ) def test___str__( self, invoice_retrieve_mock, invoice_item_retrieve_mock, charge_retrieve_mock, customer_retrieve_mock, paymentintent_retrieve_mock, paymentmethod_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, balance_transaction_retrieve_mock, ): fake_card = deepcopy(FAKE_CARD_II) fake_card["customer"] = None # create Card for FAKE_CUSTOMER_III Card.sync_from_stripe_data(fake_card) # create invoice for latest_invoice in subscription to work. Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) invoiceitem = InvoiceItem.sync_from_stripe_data(deepcopy(FAKE_INVOICEITEM)) self.assertEqual( invoiceitem.get_stripe_dashboard_url(), invoiceitem.invoice.get_stripe_dashboard_url(), ) assert str(invoiceitem) == invoiceitem.description @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_II), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", autospec=True, ) @patch( "stripe.Customer.retrieve", autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE_II), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE_II), autospec=True, ) def test_sync_with_subscription( self, invoice_retrieve_mock, invoice_item_retrieve_mock, charge_retrieve_mock, customer_retrieve_mock, paymentintent_retrieve_mock, paymentmethod_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_II) fake_payment_intent["invoice"] = FAKE_INVOICE_II["id"] paymentintent_retrieve_mock.return_value = fake_payment_intent fake_subscription = deepcopy(FAKE_SUBSCRIPTION_III) fake_subscription["latest_invoice"] = FAKE_INVOICE_II["id"] subscription_retrieve_mock.return_value = fake_subscription fake_customer = deepcopy(FAKE_CUSTOMER_II) customer_retrieve_mock.return_value = fake_customer fake_card = deepcopy(FAKE_CARD_II) fake_card["customer"] = None # create Card for FAKE_CUSTOMER_II Card.sync_from_stripe_data(fake_card) default_account_mock.return_value = self.account invoiceitem_data = deepcopy(FAKE_INVOICEITEM) invoiceitem_data.update({"subscription": fake_subscription["id"]}) invoiceitem_data.update({"invoice": FAKE_INVOICE_II["id"]}) invoice_item_retrieve_mock.return_value = invoiceitem_data invoiceitem = InvoiceItem.sync_from_stripe_data(invoiceitem_data) expected_blank_fks = self.default_expected_blank_fks | { "djstripe.InvoiceItem.plan", "djstripe.InvoiceItem.price", } expected_blank_fks.difference_update( { "djstripe.PaymentIntent.invoice (related name)", "djstripe.Invoice.payment_intent", } ) self.assert_fks(invoiceitem, expected_blank_fks=expected_blank_fks) # Coverage of sync of existing data invoiceitem = InvoiceItem.sync_from_stripe_data(invoiceitem_data) self.assert_fks(invoiceitem, expected_blank_fks=expected_blank_fks) invoice_retrieve_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, expand=["discounts"], id=FAKE_INVOICE_II["id"], stripe_account=None, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_II), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE_II), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE_II), autospec=True ) def test_sync_expanded_invoice_with_subscription( self, invoice_retrieve_mock, invoice_item_retrieve_mock, charge_retrieve_mock, customer_retrieve_mock, paymentintent_retrieve_mock, paymentmethod_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_II) fake_payment_intent["invoice"] = FAKE_INVOICE_II["id"] paymentintent_retrieve_mock.return_value = fake_payment_intent fake_subscription = deepcopy(FAKE_SUBSCRIPTION_III) fake_subscription["latest_invoice"] = FAKE_INVOICE_II["id"] subscription_retrieve_mock.return_value = fake_subscription fake_card = deepcopy(FAKE_CARD_II) fake_card["customer"] = None # create Card for FAKE_CUSTOMER_III Card.sync_from_stripe_data(fake_card) # create invoice Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE_II)) default_account_mock.return_value = self.account invoiceitem_data = deepcopy(FAKE_INVOICEITEM) # Expand the Invoice data invoiceitem_data.update( { "subscription": FAKE_SUBSCRIPTION_III["id"], "invoice": deepcopy(dict(FAKE_INVOICE_II)), } ) invoiceitem = InvoiceItem.sync_from_stripe_data(invoiceitem_data) expected_blank_fks = self.default_expected_blank_fks | { "djstripe.InvoiceItem.plan", "djstripe.InvoiceItem.price", } expected_blank_fks.difference_update( { "djstripe.PaymentIntent.invoice (related name)", "djstripe.Invoice.payment_intent", } ) self.assert_fks(invoiceitem, expected_blank_fks=expected_blank_fks) # Coverage of sync of existing data invoiceitem = InvoiceItem.sync_from_stripe_data(invoiceitem_data) self.assert_fks(invoiceitem, expected_blank_fks=expected_blank_fks) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch("stripe.Price.retrieve", return_value=deepcopy(FAKE_PRICE_II), autospec=True) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN_II), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_II), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE_II), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE_II), autospec=True ) def test_sync_proration( self, invoice_retrieve_mock, invoice_item_retrieve_mock, charge_retrieve_mock, customer_retrieve_mock, paymentintent_retrieve_mock, paymentmethod_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, price_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_II) fake_payment_intent["invoice"] = FAKE_INVOICE_II["id"] paymentintent_retrieve_mock.return_value = fake_payment_intent fake_subscription = deepcopy(FAKE_SUBSCRIPTION_III) fake_subscription["latest_invoice"] = FAKE_INVOICE_II["id"] subscription_retrieve_mock.return_value = fake_subscription fake_card = deepcopy(FAKE_CARD_II) fake_card["customer"] = None # create Card for FAKE_CUSTOMER_III Card.sync_from_stripe_data(fake_card) # create invoice Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE_II)) default_account_mock.return_value = self.account invoiceitem_data = deepcopy(FAKE_INVOICEITEM) invoiceitem_data.update( { "proration": True, "plan": FAKE_PLAN_II["id"], "price": FAKE_PRICE_II["id"], } ) invoiceitem = InvoiceItem.sync_from_stripe_data(invoiceitem_data) self.assertEqual(FAKE_PLAN_II["id"], invoiceitem.plan.id) self.assertEqual(FAKE_PRICE_II["id"], invoiceitem.price.id) expected_blank_fks = self.default_expected_blank_fks | { "djstripe.InvoiceItem.subscription" } expected_blank_fks.difference_update( { "djstripe.PaymentIntent.invoice (related name)", "djstripe.Invoice.payment_intent", } ) self.assert_fks( invoiceitem, expected_blank_fks=expected_blank_fks, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch("stripe.Price.retrieve", return_value=deepcopy(FAKE_PRICE_II), autospec=True) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN_II), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_III), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE_II), autospec=True ) @patch("stripe.Invoice.retrieve", autospec=True) def test_sync_null_invoice( self, invoice_retrieve_mock, charge_retrieve_mock, customer_retrieve_mock, subscription_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, price_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account invoiceitem_data = deepcopy(FAKE_INVOICEITEM) invoiceitem_data.update( { "proration": True, "plan": FAKE_PLAN_II["id"], "price": FAKE_PRICE_II["id"], "invoice": None, } ) invoiceitem = InvoiceItem.sync_from_stripe_data(invoiceitem_data) self.assertEqual(FAKE_PLAN_II["id"], invoiceitem.plan.id) self.assertEqual(FAKE_PRICE_II["id"], invoiceitem.price.id) self.assert_fks( invoiceitem, expected_blank_fks=self.default_expected_blank_fks | { "djstripe.InvoiceItem.invoice", "djstripe.InvoiceItem.subscription", "djstripe.Customer.default_source", }, ) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE_II), autospec=True ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_II), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE_II), autospec=True ) def test_sync_with_taxes( self, invoice_retrieve_mock, invoice_item_retrieve_mock, paymentintent_retrieve_mock, paymentmethod_retrieve_mock, charge_retrieve_mock, customer_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_II) fake_payment_intent["invoice"] = FAKE_INVOICE_II["id"] paymentintent_retrieve_mock.return_value = fake_payment_intent fake_subscription = deepcopy(FAKE_SUBSCRIPTION_III) fake_subscription["latest_invoice"] = FAKE_INVOICE_II["id"] subscription_retrieve_mock.return_value = fake_subscription fake_card = deepcopy(FAKE_CARD_II) fake_card["customer"] = None # create Card for FAKE_CUSTOMER_III Card.sync_from_stripe_data(fake_card) # create invoice Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE_II)) default_account_mock.return_value = self.account invoiceitem_data = deepcopy(FAKE_INVOICEITEM_III) invoiceitem_data["plan"] = FAKE_PLAN_II invoiceitem_data["price"] = FAKE_PRICE_II invoiceitem = InvoiceItem.sync_from_stripe_data(invoiceitem_data) self.assertEqual(invoiceitem.tax_rates.count(), 1) self.assertEqual( invoiceitem.tax_rates.first().id, FAKE_TAX_RATE_EXAMPLE_1_VAT["id"] ) ================================================ FILE: tests/test_line_item.py ================================================ """ dj-stripe LineItem Model Tests. """ from copy import deepcopy from unittest.mock import PropertyMock, patch from django.test.testcases import TestCase from djstripe.models import Invoice from djstripe.models.billing import LineItem from djstripe.settings import djstripe_settings from . import ( FAKE_BALANCE_TRANSACTION, FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CHARGE, FAKE_CUSTOMER, FAKE_INVOICE, FAKE_INVOICEITEM, FAKE_LINE_ITEM, FAKE_PAYMENT_INTENT_I, FAKE_PLATFORM_ACCOUNT, FAKE_PRODUCT, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_ITEM, AssertStripeFksMixin, ) class LineItemTest(AssertStripeFksMixin, TestCase): def setUp(self): # create a Stripe Platform Account self.account = FAKE_PLATFORM_ACCOUNT.create() self.default_expected_blank_fks = { "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.Customer.default_payment_method", "djstripe.Customer.subscriber", "djstripe.Customer.coupon", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.InvoiceItem.plan", "djstripe.InvoiceItem.price", "djstripe.InvoiceItem.subscription", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.Product.default_price", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", } @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.SubscriptionItem.retrieve", autospec=True, return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), ) @patch( "stripe.Subscription.retrieve", autospec=True, return_value=deepcopy(FAKE_SUBSCRIPTION), ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Customer.retrieve", autospec=True, return_value=deepcopy(FAKE_CUSTOMER) ) @patch( "stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True, ) def test_sync_from_stripe_data( self, invoice_retrieve_mock, invoiceitem_retrieve_mock, charge_retrieve_mock, customer_retrieve_mock, paymentintent_retrieve_mock, paymentmethod_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, product_retrieve_mock, balance_transaction_retrieve_mock, ): # create the latest invoice as Line Items (Invoice) need to exist on an Invoice Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) # Create the Line Item il = LineItem.sync_from_stripe_data(deepcopy(FAKE_LINE_ITEM)) self.assertEqual(il.id, "il_fakefakefakefakefake0001") self.assert_fks( il, expected_blank_fks=(self.default_expected_blank_fks), ) @patch( "stripe.Invoice.retrieve", autospec=True, ) def test_api_list( self, invoice_retrieve_mock, ): p = PropertyMock(return_value=deepcopy(FAKE_LINE_ITEM)) with patch.object( invoice_retrieve_mock.return_value.lines, "list" ) as patched_obj: type(patched_obj).auto_paging_iter = p # Invoke LineItem.api_list(...) LineItem.api_list(id=FAKE_INVOICE["id"], expand=["data.discounts"]) # assert invoice_retrieve_mock was called once invoice_retrieve_mock.assert_called_once_with( FAKE_INVOICE["id"], api_key=djstripe_settings.STRIPE_SECRET_KEY, ) # assert invoice.lines.list(...) was called once patched_obj.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=["data.discounts"] ) ================================================ FILE: tests/test_managers.py ================================================ """ dj-stripe Model Manager Tests. """ import datetime import decimal from copy import deepcopy from unittest.mock import patch from django.contrib.auth import get_user_model from django.test import TestCase from djstripe.models import Charge, Customer, Plan, Subscription, Transfer from djstripe.utils import get_timezone_utc from . import ( FAKE_PLAN, FAKE_PLAN_II, FAKE_PLATFORM_ACCOUNT, FAKE_PRODUCT, FAKE_TRANSFER, ) class SubscriptionManagerTest(TestCase): def setUp(self): # create customers and current subscription records period_start = datetime.datetime(2013, 4, 1, tzinfo=get_timezone_utc()) period_end = datetime.datetime(2013, 4, 30, tzinfo=get_timezone_utc()) start = datetime.datetime( 2013, 1, 1, 0, 0, 1, tzinfo=get_timezone_utc() ) # more realistic start with patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True, ): self.plan = Plan.sync_from_stripe_data(FAKE_PLAN) self.plan2 = Plan.sync_from_stripe_data(FAKE_PLAN_II) for i in range(10): user = get_user_model().objects.create_user( username=f"patrick{i}", email=f"patrick{i}@example.com", ) customer = Customer.objects.create( subscriber=user, id=f"cus_xxxxxxxxxxxxxx{i}", livemode=False, balance=0, delinquent=False, ) Subscription.objects.create( id=f"sub_xxxxxxxxxxxxxx{i}", customer=customer, plan=self.plan, current_period_start=period_start, current_period_end=period_end, status="active", start_date=start, quantity=1, ) user = get_user_model().objects.create_user( username="patrick11", email="patrick11@example.com" ) customer = Customer.objects.create( subscriber=user, id="cus_xxxxxxxxxxxxxx11", livemode=False, balance=0, delinquent=False, ) Subscription.objects.create( id="sub_xxxxxxxxxxxxxx11", customer=customer, plan=self.plan, current_period_start=period_start, current_period_end=period_end, status="canceled", canceled_at=period_end, start_date=start, quantity=1, ) user = get_user_model().objects.create_user( username="patrick12", email="patrick12@example.com" ) customer = Customer.objects.create( subscriber=user, id="cus_xxxxxxxxxxxxxx12", livemode=False, balance=0, delinquent=False, ) Subscription.objects.create( id="sub_xxxxxxxxxxxxxx12", customer=customer, plan=self.plan2, current_period_start=period_start, current_period_end=period_end, status="active", start_date=start, quantity=1, ) def test_started_during_no_records(self): self.assertEqual(Subscription.objects.started_during(2013, 4).count(), 0) def test_started_during_has_records(self): self.assertEqual(Subscription.objects.started_during(2013, 1).count(), 12) def test_canceled_during(self): self.assertEqual(Subscription.objects.canceled_during(2013, 4).count(), 1) def test_canceled_all(self): self.assertEqual(Subscription.objects.canceled().count(), 1) def test_active_all(self): self.assertEqual(Subscription.objects.active().count(), 11) def test_started_plan_summary(self): for plan in Subscription.objects.started_plan_summary_for(2013, 1): if plan["plan"] == self.plan: self.assertEqual(plan["count"], 11) if plan["plan"] == self.plan2: self.assertEqual(plan["count"], 1) def test_active_plan_summary(self): for plan in Subscription.objects.active_plan_summary(): if plan["plan"] == self.plan: self.assertEqual(plan["count"], 10) if plan["plan"] == self.plan2: self.assertEqual(plan["count"], 1) def test_canceled_plan_summary(self): for plan in Subscription.objects.canceled_plan_summary_for(2013, 1): if plan["plan"] == self.plan: self.assertEqual(plan["count"], 1) if plan["plan"] == self.plan2: self.assertEqual(plan["count"], 0) def test_churn(self): self.assertEqual( Subscription.objects.churn(), decimal.Decimal("1") / decimal.Decimal("11") ) class TransferManagerTest(TestCase): @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_PLATFORM_ACCOUNT), autospec=True, ) def test_transfer_summary( self, account_retrieve_mock, transfer__attach_object_post_save_hook_mock ): def FAKE_TRANSFER_III(): data = deepcopy(FAKE_TRANSFER) data["id"] = "tr_17O4U52eZvKYlo2CmyYbDAEy" data["amount"] = 19010 data["created"] = 1451560845 return data def FAKE_TRANSFER_II(): data = deepcopy(FAKE_TRANSFER) data["id"] = "tr_16hTzv2eZvKYlo2CWuyMmuvV" data["amount"] = 2000 data["created"] = 1440420000 return data Transfer.sync_from_stripe_data(deepcopy(FAKE_TRANSFER)) Transfer.sync_from_stripe_data(FAKE_TRANSFER_II()) Transfer.sync_from_stripe_data(FAKE_TRANSFER_III()) self.assertEqual(Transfer.objects.during(2015, 8).count(), 2) totals = Transfer.objects.paid_totals_for(2015, 12) self.assertEqual(totals["total_amount"], decimal.Decimal("190.10")) class ChargeManagerTest(TestCase): def setUp(self): customer = Customer.objects.create( id="cus_XXXXXXX", livemode=False, balance=0, delinquent=False ) self.march_charge = Charge.objects.create( id="ch_XXXXMAR1", customer=customer, created=datetime.datetime(2015, 3, 31, tzinfo=get_timezone_utc()), amount=0, amount_refunded=0, currency="usd", status="pending", ) self.april_charge_1 = Charge.objects.create( id="ch_XXXXAPR1", customer=customer, created=datetime.datetime(2015, 4, 1, tzinfo=get_timezone_utc()), amount=decimal.Decimal("20.15"), amount_refunded=0, currency="usd", status="succeeded", paid=True, ) self.april_charge_2 = Charge.objects.create( id="ch_XXXXAPR2", customer=customer, created=datetime.datetime(2015, 4, 18, tzinfo=get_timezone_utc()), amount=decimal.Decimal("10.35"), amount_refunded=decimal.Decimal("5.35"), currency="usd", status="succeeded", paid=True, ) self.april_charge_3 = Charge.objects.create( id="ch_XXXXAPR3", customer=customer, created=datetime.datetime(2015, 4, 30, tzinfo=get_timezone_utc()), amount=decimal.Decimal("100.00"), amount_refunded=decimal.Decimal("80.00"), currency="usd", status="pending", paid=False, ) self.may_charge = Charge.objects.create( id="ch_XXXXMAY1", customer=customer, created=datetime.datetime(2015, 5, 1, tzinfo=get_timezone_utc()), amount=0, amount_refunded=0, currency="usd", status="pending", ) self.november_charge = Charge.objects.create( id="ch_XXXXNOV1", customer=customer, created=datetime.datetime(2015, 11, 16, tzinfo=get_timezone_utc()), amount=0, amount_refunded=0, currency="usd", status="pending", ) self.charge_2014 = Charge.objects.create( id="ch_XXXX20141", customer=customer, created=datetime.datetime(2014, 12, 31, tzinfo=get_timezone_utc()), amount=0, amount_refunded=0, currency="usd", status="pending", ) self.charge_2016 = Charge.objects.create( id="ch_XXXX20161", customer=customer, created=datetime.datetime(2016, 1, 1, tzinfo=get_timezone_utc()), amount=0, amount_refunded=0, currency="usd", status="pending", ) def test_is_during_april_2015(self): raw_charges = Charge.objects.during(year=2015, month=4) charges = [charge.id for charge in raw_charges] self.assertIn(self.april_charge_1.id, charges, "April charge 1 not in charges.") self.assertIn(self.april_charge_2.id, charges, "April charge 2 not in charges.") self.assertIn(self.april_charge_3.id, charges, "April charge 3 not in charges.") self.assertNotIn( self.march_charge.id, charges, "March charge unexpectedly in charges." ) self.assertNotIn( self.may_charge.id, charges, "May charge unexpectedly in charges." ) self.assertNotIn( self.november_charge.id, charges, "November charge unexpectedly in charges." ) self.assertNotIn( self.charge_2014.id, charges, "2014 charge unexpectedly in charges." ) self.assertNotIn( self.charge_2016.id, charges, "2016 charge unexpectedly in charges." ) def test_get_paid_totals_for_april_2015(self): paid_totals = Charge.objects.paid_totals_for(year=2015, month=4) self.assertEqual( decimal.Decimal("30.50"), paid_totals["total_amount"], "Total amount is not correct.", ) self.assertEqual( decimal.Decimal("5.35"), paid_totals["total_refunded"], "Total amount refunded is not correct.", ) ================================================ FILE: tests/test_migrations.py ================================================ """ dj-stripe Migrations Tests """ import pytest from django.conf import settings from django.contrib.auth import get_user_model from django.test import TestCase, override_settings from djstripe.models.core import Customer from djstripe.settings import djstripe_settings class TestCustomerSubscriberFK(TestCase): @override_settings( DJSTRIPE_SUBSCRIBER_MODEL="testapp.Organization", DJSTRIPE_SUBSCRIBER_MODEL_REQUEST_CALLBACK=(lambda request: request.org), ) def setUp(self): return super().setUp() def test_customer_subscriber_fk_to_subscriber_model(self): """ Test to ensure customer.subscriber fk points to the configured model set by DJSTRIPE_SUBSCRIBER_MODEL """ field = Customer._meta.get_field("subscriber") self.assertEqual(field.related_model, djstripe_settings.get_subscriber_model()) self.assertNotEqual(field.related_model, settings.AUTH_USER_MODEL) def test_customer_subscriber_fk_fallback_to_auth_user_model(self): """ Test to ensure customer.subscriber fk points to the fallback AUTH_USER_MODEL when DJSTRIPE_SUBSCRIBER_MODEL is not set """ # assert DJSTRIPE_SUBSCRIBER_MODEL has not been set with pytest.raises(AttributeError): settings.DJSTRIPE_SUBSCRIBER_MODEL field = Customer._meta.get_field("subscriber") self.assertEqual(field.related_model, get_user_model()) ================================================ FILE: tests/test_mixins.py ================================================ """ dj-stripe Mixin Tests. """ from copy import deepcopy from unittest.mock import patch from django.contrib.auth import get_user_model from django.test.client import RequestFactory from django.test.testcases import TestCase from djstripe.mixins import PaymentsContextMixin, SubscriptionMixin from djstripe.models import Plan from djstripe.settings import djstripe_settings from . import FAKE_CUSTOMER, FAKE_PLAN, FAKE_PLAN_II, FAKE_PRODUCT class TestPaymentsContextMixin(TestCase): def test_get_context_data(self): class TestSuperView(object): def get_context_data(self): return {} class TestView(PaymentsContextMixin, TestSuperView): pass context = TestView().get_context_data() self.assertIn( "STRIPE_PUBLIC_KEY", context, "STRIPE_PUBLIC_KEY missing from context." ) self.assertEqual( context["STRIPE_PUBLIC_KEY"], djstripe_settings.STRIPE_PUBLIC_KEY, "Incorrect STRIPE_PUBLIC_KEY.", ) self.assertIn("plans", context, "pans missing from context.") self.assertEqual( list(Plan.objects.all()), list(context["plans"]), "Incorrect plans." ) class TestSubscriptionMixin(TestCase): def setUp(self): with patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True, ): Plan.sync_from_stripe_data(deepcopy(FAKE_PLAN)) Plan.sync_from_stripe_data(deepcopy(FAKE_PLAN_II)) @patch( "stripe.Customer.create", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_get_context_data(self, stripe_create_customer_mock): class TestSuperView(object): def get_context_data(self): return {} class TestView(SubscriptionMixin, TestSuperView): pass test_view = TestView() test_view.request = RequestFactory() test_view.request.user = get_user_model().objects.create( username="x", email="user@test.com" ) context = test_view.get_context_data() self.assertIn( "is_plans_plural", context, "is_plans_plural missing from context." ) self.assertTrue(context["is_plans_plural"], "Incorrect is_plans_plural.") self.assertIn("customer", context, "customer missing from context.") ================================================ FILE: tests/test_order.py ================================================ """ dj-stripe Order model tests """ from copy import deepcopy from unittest.mock import patch import pytest import stripe from django.test import TestCase from djstripe.enums import OrderStatus from djstripe.models import Order from djstripe.settings import djstripe_settings from . import ( FAKE_ACCOUNT, FAKE_BALANCE_TRANSACTION, FAKE_CHARGE, FAKE_CUSTOMER, FAKE_INVOICE, FAKE_INVOICEITEM, FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT, FAKE_ORDER_WITH_CUSTOMER_WITHOUT_PAYMENT_INTENT, FAKE_ORDER_WITHOUT_CUSTOMER_WITH_PAYMENT_INTENT, FAKE_ORDER_WITHOUT_CUSTOMER_WITHOUT_PAYMENT_INTENT, FAKE_PAYMENT_INTENT_I, FAKE_PAYMENT_METHOD_I, FAKE_PLATFORM_ACCOUNT, FAKE_PRODUCT, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_ITEM, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class TestOrder(AssertStripeFksMixin, TestCase): @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) def test_sync_from_stripe_data( self, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): default_expected_blank_fks = { "djstripe.Customer.default_payment_method", "djstripe.Customer.subscriber", "djstripe.Customer.coupon", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.Product.default_price", } # Ensure Order objects with Customer and PaymentIntent data sync correctly order = Order.sync_from_stripe_data( deepcopy(FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT) ) assert order self.assertEqual(order.payment_intent.id, FAKE_PAYMENT_INTENT_I["id"]) self.assertEqual(order.customer.id, FAKE_CUSTOMER["id"]) self.assert_fks(order, expected_blank_fks=default_expected_blank_fks) # Ensure Order objects with Customer and NO PaymentIntent data sync correctly order = Order.sync_from_stripe_data( deepcopy(FAKE_ORDER_WITH_CUSTOMER_WITHOUT_PAYMENT_INTENT) ) assert order assert order.payment_intent is None self.assertEqual(order.customer.id, FAKE_CUSTOMER["id"]) self.assert_fks( order, expected_blank_fks=default_expected_blank_fks | { "djstripe.Order.payment_intent", }, ) # Ensure Order objects with NO Customer and PaymentIntent data sync correctly order = Order.sync_from_stripe_data( deepcopy(FAKE_ORDER_WITHOUT_CUSTOMER_WITH_PAYMENT_INTENT) ) assert order self.assertEqual(order.payment_intent.id, FAKE_PAYMENT_INTENT_I["id"]) self.assertEqual(order.customer, None) self.assert_fks( order, expected_blank_fks=default_expected_blank_fks | { "djstripe.Order.customer", }, ) # Ensure Order objects without Customer and without PaymentIntent data sync correctly order = Order.sync_from_stripe_data( deepcopy(FAKE_ORDER_WITHOUT_CUSTOMER_WITHOUT_PAYMENT_INTENT) ) assert order self.assertEqual(order.payment_intent, None) self.assertEqual(order.customer, None) self.assert_fks( order, expected_blank_fks=default_expected_blank_fks | { "djstripe.Order.customer", "djstripe.Order.payment_intent", }, ) def test__manipulate_stripe_object_hook(self): order_data = deepcopy(FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT) # Remove "payment_intent" key from order_data dictionary del order_data["payment_intent"] # assert "payment_intent" key is removed from the data dictionary self.assertTrue("payment_intent" not in order_data) # Invoke _manipulate_stripe_object_hook modified_order_data = Order._manipulate_stripe_object_hook(order_data) # assert "payment_intent" key gets added to the data dictionary self.assertTrue("payment_intent" in modified_order_data) self.assertEqual( modified_order_data["payment_intent"], order_data["payment"]["payment_intent"], ) class TestOrderStr: @pytest.mark.parametrize( "order_status", [ OrderStatus.open, OrderStatus.canceled, OrderStatus.submitted, OrderStatus.complete, OrderStatus.processing, ], ) def test___str__(self, order_status, monkeypatch): def mock_customer_get(*args, **kwargs): """Monkeypatched stripe.Customer.retrieve""" return deepcopy(FAKE_CUSTOMER) def mock_account_get(*args, **kwargs): """Monkeypatched stripe.Account.retrieve""" data = deepcopy(FAKE_ACCOUNT) # Otherwise Account.api_retrieve will invoke File.api_retrieve... data["settings"]["branding"] = {} return data def mock_payment_intent_get(*args, **kwargs): """Monkeypatched stripe.PaymentIntent.retrieve""" return deepcopy(FAKE_PAYMENT_INTENT_I) def mock_payment_method_get(*args, **kwargs): """Monkeypatched stripe.PaymentMethod.retrieve""" return deepcopy(FAKE_PAYMENT_METHOD_I) def mock_invoice_get(*args, **kwargs): """Monkeypatched stripe.Invoice.retrieve""" return deepcopy(FAKE_INVOICE) def mock_invoice_item_get(*args, **kwargs): return FAKE_INVOICEITEM def mock_subscription_get(*args, **kwargs): """Monkeypatched stripe.Subscription.retrieve""" return deepcopy(FAKE_SUBSCRIPTION) def mock_subscriptionitem_get(*args, **kwargs): return FAKE_SUBSCRIPTION_ITEM def mock_balance_transaction_get(*args, **kwargs): """Monkeypatched stripe.BalanceTransaction.retrieve""" return deepcopy(FAKE_BALANCE_TRANSACTION) def mock_product_get(*args, **kwargs): """Monkeypatched stripe.Product.retrieve""" return deepcopy(FAKE_PRODUCT) def mock_charge_get(*args, **kwargs): """Monkeypatched stripe.Charge.retrieve""" return deepcopy(FAKE_CHARGE) # monkeypatch stripe.Product.retrieve, stripe.Price.retrieve, stripe.PaymentIntent.retrieve, stripe.PaymentMethod.retrieve, and stripe.PaymentIntent.retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Account, "retrieve", mock_account_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr(stripe.PaymentIntent, "retrieve", mock_payment_intent_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) # because of Reverse o2o field sync due to PaymentIntent.sync_from_stripe_data.. monkeypatch.setattr(stripe.Invoice, "retrieve", mock_invoice_get) monkeypatch.setattr(stripe.InvoiceItem, "retrieve", mock_invoice_item_get) monkeypatch.setattr(stripe.Subscription, "retrieve", mock_subscription_get) monkeypatch.setattr( stripe.SubscriptionItem, "retrieve", mock_subscriptionitem_get ) monkeypatch.setattr( stripe.BalanceTransaction, "retrieve", mock_balance_transaction_get ) monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Charge, "retrieve", mock_charge_get) order_data = deepcopy(FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT) order_data["status"] = order_status order = Order.sync_from_stripe_data(order_data) if order_status in (OrderStatus.open, OrderStatus.canceled): assert str(order) == f"Created on 07/10/2019 ({order_status})" elif order_status in ( OrderStatus.submitted, OrderStatus.complete, OrderStatus.processing, ): assert str(order) == f"Placed on 07/10/2019 ({order_status})" class TestOrderMethods: @pytest.mark.parametrize("stripe_account", (None, "acct_fakefakefakefake001")) @pytest.mark.parametrize( "api_key, expected_api_key", ( (None, djstripe_settings.STRIPE_SECRET_KEY), ("sk_fakefakefake01", "sk_fakefakefake01"), ), ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.Order.cancel", ) def test_cancel( self, order_cancel_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, api_key, expected_api_key, stripe_account, ): fake_order = deepcopy(FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT) fake_order_cancel = deepcopy(FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT) fake_order_cancel["status"] = "canceled" order_cancel_mock.return_value = fake_order_cancel order = Order.sync_from_stripe_data(fake_order) assert order # Cancel the order cancelled_order = order.cancel( api_key=api_key, stripe_account=stripe_account, ) # assert Order got cancelled assert cancelled_order["status"] == "canceled" # assert cancel called with the correct kwargs Order.stripe_class.cancel.assert_called_once_with( "order_fakefakefakefakefake0001", api_key=expected_api_key, stripe_account=stripe_account or FAKE_PLATFORM_ACCOUNT["id"], stripe_version=djstripe_settings.STRIPE_API_VERSION, ) @pytest.mark.parametrize("stripe_account", (None, "acct_fakefakefakefake001")) @pytest.mark.parametrize( "api_key, expected_api_key", ( (None, djstripe_settings.STRIPE_SECRET_KEY), ("sk_fakefakefake01", "sk_fakefakefake01"), ), ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.Order.reopen", ) def test_reopen( self, order_reopen_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, api_key, expected_api_key, stripe_account, ): fake_order = deepcopy(FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT) fake_order_reopen = deepcopy(FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT) fake_order_reopen["status"] = "open" order_reopen_mock.return_value = fake_order_reopen order = Order.sync_from_stripe_data(fake_order) assert order # Reopen the order reopened_order = order.reopen( api_key=api_key, stripe_account=stripe_account, ) # assert Order got reopened assert reopened_order["status"] == "open" # assert reopen called with the correct kwargs Order.stripe_class.reopen.assert_called_once_with( "order_fakefakefakefakefake0001", api_key=expected_api_key, stripe_account=stripe_account or FAKE_PLATFORM_ACCOUNT["id"], stripe_version=djstripe_settings.STRIPE_API_VERSION, ) @pytest.mark.parametrize("stripe_account", (None, "acct_fakefakefakefake001")) @pytest.mark.parametrize( "api_key, expected_api_key", ( (None, djstripe_settings.STRIPE_SECRET_KEY), ("sk_fakefakefake01", "sk_fakefakefake01"), ), ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.Order.submit", ) def test_submit( self, order_submit_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, api_key, expected_api_key, stripe_account, ): fake_order = deepcopy(FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT) fake_order_submit = deepcopy(FAKE_ORDER_WITH_CUSTOMER_WITH_PAYMENT_INTENT) fake_order_submit["status"] = "submitted" order_submit_mock.return_value = fake_order_submit order = Order.sync_from_stripe_data(fake_order) assert order expected_total = fake_order["amount_total"] # Submit the order submitted_order = order.submit( api_key=api_key, stripe_account=stripe_account, expected_total=expected_total, ) # assert Order got submitted assert submitted_order["status"] == "submitted" # assert submit called with the correct kwargs Order.stripe_class.submit.assert_called_once_with( "order_fakefakefakefakefake0001", api_key=expected_api_key, stripe_account=stripe_account or FAKE_PLATFORM_ACCOUNT["id"], stripe_version=djstripe_settings.STRIPE_API_VERSION, expected_total=expected_total, ) ================================================ FILE: tests/test_payment_intent.py ================================================ """ dj-stripe PaymentIntent Model Tests. """ from copy import deepcopy from unittest.mock import patch import pytest import stripe from django.test import TestCase from djstripe.models import PaymentIntent from . import ( FAKE_ACCOUNT, FAKE_BALANCE_TRANSACTION, FAKE_CHARGE, FAKE_CUSTOMER, FAKE_INVOICE, FAKE_INVOICEITEM, FAKE_PAYMENT_INTENT_DESTINATION_CHARGE, FAKE_PAYMENT_INTENT_I, FAKE_PAYMENT_METHOD_I, FAKE_PRODUCT, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_ITEM, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db def _get_fake_payment_intent_destination_charge_no_customer(): FAKE_PAYMENT_INTENT_DESTINATION_CHARGE_NO_CUSTOMER = deepcopy( FAKE_PAYMENT_INTENT_DESTINATION_CHARGE ) FAKE_PAYMENT_INTENT_DESTINATION_CHARGE_NO_CUSTOMER["customer"] = None return FAKE_PAYMENT_INTENT_DESTINATION_CHARGE_NO_CUSTOMER def _get_fake_payment_intent_i_no_customer(): FAKE_PAYMENT_INTENT_I_NO_CUSTOMER = deepcopy(FAKE_PAYMENT_INTENT_I) FAKE_PAYMENT_INTENT_I_NO_CUSTOMER["customer"] = None return FAKE_PAYMENT_INTENT_I_NO_CUSTOMER class TestStrPaymentIntent: # # Helpers # @pytest.mark.parametrize( "fake_intent_data, has_account, has_customer", [ (FAKE_PAYMENT_INTENT_I, False, True), (FAKE_PAYMENT_INTENT_DESTINATION_CHARGE, True, True), (_get_fake_payment_intent_destination_charge_no_customer(), True, False), (_get_fake_payment_intent_i_no_customer(), False, False), ], ) def test___str__(self, fake_intent_data, has_account, has_customer, monkeypatch): def mock_customer_get(*args, **kwargs): """Monkeypatched stripe.Customer.retrieve""" return deepcopy(FAKE_CUSTOMER) def mock_account_get(*args, **kwargs): """Monkeypatched stripe.Account.retrieve""" data = deepcopy(FAKE_ACCOUNT) # Otherwise Account.api_retrieve will invoke File.api_retrieve... data["settings"]["branding"] = {} return data def mock_payment_method_get(*args, **kwargs): """Monkeypatched stripe.PaymentMethod.retrieve""" return deepcopy(FAKE_PAYMENT_METHOD_I) def mock_invoice_get(*args, **kwargs): """Monkeypatched stripe.Invoice.retrieve""" return deepcopy(FAKE_INVOICE) def mock_invoice_item_get(*args, **kwargs): return FAKE_INVOICEITEM def mock_subscription_get(*args, **kwargs): """Monkeypatched stripe.Subscription.retrieve""" return deepcopy(FAKE_SUBSCRIPTION) def mock_subscriptionitem_get(*args, **kwargs): return FAKE_SUBSCRIPTION_ITEM def mock_balance_transaction_get(*args, **kwargs): """Monkeypatched stripe.BalanceTransaction.retrieve""" return deepcopy(FAKE_BALANCE_TRANSACTION) def mock_product_get(*args, **kwargs): """Monkeypatched stripe.Product.retrieve""" return deepcopy(FAKE_PRODUCT) def mock_charge_get(*args, **kwargs): """Monkeypatched stripe.Charge.retrieve""" return deepcopy(FAKE_CHARGE) # monkeypatch stripe.Product.retrieve, stripe.Price.retrieve, stripe.PaymentMethod.retrieve, and stripe.PaymentIntent.retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Account, "retrieve", mock_account_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) # because of Reverse o2o field sync due to PaymentIntent.sync_from_stripe_data.. monkeypatch.setattr(stripe.Invoice, "retrieve", mock_invoice_get) monkeypatch.setattr(stripe.InvoiceItem, "retrieve", mock_invoice_item_get) monkeypatch.setattr( stripe.SubscriptionItem, "retrieve", mock_subscriptionitem_get ) monkeypatch.setattr(stripe.Subscription, "retrieve", mock_subscription_get) monkeypatch.setattr( stripe.BalanceTransaction, "retrieve", mock_balance_transaction_get ) monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Charge, "retrieve", mock_charge_get) pi = PaymentIntent.sync_from_stripe_data(fake_intent_data) assert pi # due to reverse o2o sync invoice should also get created if fake_intent_data.get("invoice"): assert pi.invoice is not None if has_account and has_customer: assert ( str(pi) == "$1,902.00 USD (The funds are in your account.) for dj-stripe by Michael Smith" ) elif has_account and not has_customer: assert ( str(pi) ) == "$1,902.00 USD for dj-stripe. The funds are in your account." elif has_customer and not has_account: assert ( str(pi) ) == "$20.00 USD by Michael Smith. The funds are in your account." elif not has_customer and not has_account: assert str(pi) == "$20.00 USD (The funds are in your account.)" class PaymentIntentTest(AssertStripeFksMixin, TestCase): @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) def test_sync_from_stripe_data( self, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, paymentintent_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): payment_intent = PaymentIntent.sync_from_stripe_data( deepcopy(FAKE_PAYMENT_INTENT_I) ) self.assert_fks( payment_intent, expected_blank_fks={ "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.subscriber", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.Product.default_price", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", }, ) assert payment_intent self.assertIsNotNone(payment_intent.invoice) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) def test_status_enum( self, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, paymentintent_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_I) for status in ( "requires_payment_method", "requires_confirmation", "requires_action", "processing", "requires_capture", "canceled", "succeeded", ): fake_payment_intent["status"] = status payment_intent = PaymentIntent.sync_from_stripe_data(fake_payment_intent) # trigger model field validation (including enum value choices check) assert payment_intent payment_intent.full_clean() @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) def test_canceled_intent( self, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, paymentintent_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_I) fake_payment_intent["status"] = "canceled" fake_payment_intent["canceled_at"] = 1567524169 for reason in ( None, "duplicate", "fraudulent", "requested_by_customer", "abandoned", "failed_invoice", "void_invoice", "automatic", ): fake_payment_intent["cancellation_reason"] = reason paymentintent_retrieve_mock.return_value = fake_payment_intent payment_intent = PaymentIntent.sync_from_stripe_data(fake_payment_intent) assert payment_intent if reason is None: # enums nulls are coerced to "" by StripeModel._stripe_object_to_record self.assertEqual(payment_intent.cancellation_reason, "") else: self.assertEqual(payment_intent.cancellation_reason, reason) # trigger model field validation (including enum value choices check) payment_intent.full_clean() ================================================ FILE: tests/test_payment_method.py ================================================ """ dj-stripe PaymenthMethod Model Tests. """ from copy import deepcopy from unittest.mock import patch import pytest import stripe from django.contrib.auth import get_user_model from django.test import TestCase from stripe.error import InvalidRequestError from djstripe import enums, models from . import ( FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CUSTOMER, FAKE_PAYMENT_METHOD_I, AssertStripeFksMixin, PaymentMethodDict, ) pytestmark = pytest.mark.django_db class TestPaymentMethod: # # Helper Methods for monkeypatching # def mock_customer_get(*args, **kwargs): return deepcopy(FAKE_CUSTOMER) @pytest.mark.parametrize("customer_exists", [True, False]) def test___str__(self, monkeypatch, customer_exists): # monkeypatch stripe.Customer.retrieve call to return # the desired json response. monkeypatch.setattr(stripe.Customer, "retrieve", self.mock_customer_get) fake_payment_method_data = deepcopy(FAKE_PAYMENT_METHOD_I) if not customer_exists: fake_payment_method_data["customer"] = None pm = models.PaymentMethod.sync_from_stripe_data(fake_payment_method_data) customer = None assert ( f"{enums.PaymentMethodType.humanize(fake_payment_method_data['type'])} is not associated with any customer" ) == str(pm) else: pm = models.PaymentMethod.sync_from_stripe_data(fake_payment_method_data) customer = models.Customer.objects.get( id=fake_payment_method_data["customer"] ) assert ( f"{enums.PaymentMethodType.humanize(fake_payment_method_data['type'])} for {customer}" ) == str(pm) @pytest.mark.parametrize("customer_exists", [True, False]) def test_get_stripe_dashboard_url(self, monkeypatch, customer_exists): # monkeypatch stripe.Customer.retrieve call to return # the desired json response. monkeypatch.setattr(stripe.Customer, "retrieve", self.mock_customer_get) fake_payment_method_data = deepcopy(FAKE_PAYMENT_METHOD_I) if not customer_exists: fake_payment_method_data["customer"] = None pm = models.PaymentMethod.sync_from_stripe_data(fake_payment_method_data) assert pm assert pm.get_stripe_dashboard_url() == "" else: pm = models.PaymentMethod.sync_from_stripe_data(fake_payment_method_data) assert pm customer = models.Customer.objects.get( id=fake_payment_method_data["customer"] ) assert pm.get_stripe_dashboard_url() == customer.get_stripe_dashboard_url() @pytest.mark.parametrize("customer_exists", [True, False]) def test_sync_from_stripe_data(self, monkeypatch, customer_exists): # monkeypatch stripe.Customer.retrieve call to return # the desired json response. monkeypatch.setattr(stripe.Customer, "retrieve", self.mock_customer_get) fake_payment_method_data = deepcopy(FAKE_PAYMENT_METHOD_I) if not customer_exists: fake_payment_method_data["customer"] = None pm = models.PaymentMethod.sync_from_stripe_data(fake_payment_method_data) assert pm assert pm.id == fake_payment_method_data["id"] class PaymentMethodTest(AssertStripeFksMixin, TestCase): def setUp(self): user = get_user_model().objects.create_user( username="testuser", email="djstripe@example.com" ) self.customer = FAKE_CUSTOMER.create_for_user(user) # stripe modifies attach() at compile time, which is why # another stripe classmethod is decorated. # See Here: # https://github.com/stripe/stripe-python/blob/master/stripe/api_resources/payment_method.py#L10 # https://github.com/stripe/stripe-python/blob/master/stripe/api_resources/abstract/custom_method.py#L35 @patch( "stripe.PaymentMethod._cls_attach", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) def test_attach(self, attach_mock): payment_method = models.PaymentMethod.attach( FAKE_PAYMENT_METHOD_I["id"], customer=FAKE_CUSTOMER["id"] ) self.assert_fks( payment_method, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", }, ) @patch( "stripe.PaymentMethod._cls_attach", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) def test_attach_obj(self, attach_mock): pm = models.PaymentMethod.sync_from_stripe_data(FAKE_PAYMENT_METHOD_I) assert pm payment_method = models.PaymentMethod.attach(pm, customer=self.customer) self.assert_fks( payment_method, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", }, ) @patch( "stripe.PaymentMethod._cls_attach", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) def test_attach_synced(self, attach_mock): fake_payment_method = deepcopy(FAKE_PAYMENT_METHOD_I) fake_payment_method["customer"] = None payment_method = models.PaymentMethod.sync_from_stripe_data(fake_payment_method) assert payment_method self.assert_fks( payment_method, expected_blank_fks={"djstripe.PaymentMethod.customer"} ) payment_method = models.PaymentMethod.attach( payment_method.id, customer=FAKE_CUSTOMER["id"] ) self.assert_fks( payment_method, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", }, ) def test_detach(self): original_detach = PaymentMethodDict.detach def mocked_detach(*args, **kwargs): return original_detach(*args, **kwargs) with patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ): models.PaymentMethod.sync_from_stripe_data(deepcopy(FAKE_PAYMENT_METHOD_I)) assert self.customer self.assertEqual(1, self.customer.payment_methods.count()) payment_method = self.customer.payment_methods.first() with patch( "tests.PaymentMethodDict.detach", side_effect=mocked_detach, autospec=True ) as mock_detach, patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ): self.assertTrue(payment_method.detach()) self.assertEqual(0, self.customer.payment_methods.count()) self.assertIsNone(self.customer.default_payment_method) self.assertIsNone(payment_method.customer) mock_detach.assert_called() self.assert_fks( payment_method, expected_blank_fks={"djstripe.PaymentMethod.customer"} ) with patch( "tests.PaymentMethodDict.detach", side_effect=InvalidRequestError( message="A source must be attached to a customer to be used " "as a `payment_method`", param="payment_method", ), autospec=True, ) as mock_detach, patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) as payment_method_retrieve_mock: payment_method_retrieve_mock.return_value["customer"] = None # Need to re-sync as the PaymentMethod object has been deleted models.PaymentMethod.sync_from_stripe_data( deepcopy(FAKE_CARD_AS_PAYMENT_METHOD) ) self.assertFalse( payment_method.detach(), "Second call to detach should return false" ) def test_detach_card(self): original_detach = PaymentMethodDict.detach # "card_" payment methods are deleted after detach deleted_card_exception = InvalidRequestError( message="No such payment_method: card_xxxx", param="payment_method", code="resource_missing", ) def mocked_detach(*args, **kwargs): return original_detach(*args, **kwargs) with patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ): models.PaymentMethod.sync_from_stripe_data( deepcopy(FAKE_CARD_AS_PAYMENT_METHOD) ) assert self.customer assert self.customer.payment_methods.count() == 1 payment_method = self.customer.payment_methods.first() self.assertTrue( payment_method.id.startswith("card_"), "We expect this to be a 'card_'" ) with patch( "tests.PaymentMethodDict.detach", side_effect=mocked_detach, autospec=True ) as mock_detach, patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ): self.assertTrue(payment_method.detach()) assert self.customer.payment_methods.count() == 0 assert self.customer.default_payment_method is None self.assertEqual( models.PaymentMethod.objects.filter(id=payment_method.id).count(), 0, "We expect PaymentMethod id = card_* to be deleted", ) mock_detach.assert_called() with patch( "tests.PaymentMethodDict.detach", side_effect=InvalidRequestError( message="A source must be attached to a customer to be used " "as a `payment_method`", param="payment_method", ), autospec=True, ) as mock_detach, patch( "stripe.PaymentMethod.retrieve", side_effect=deleted_card_exception, autospec=True, ) as payment_method_retrieve_mock: payment_method_retrieve_mock.return_value["customer"] = None # Need to re-sync as the PaymentMethod object has been deleted models.PaymentMethod.sync_from_stripe_data( deepcopy(FAKE_CARD_AS_PAYMENT_METHOD) ) # Get the Payment Method from the DB payment_method = models.PaymentMethod.objects.filter( id=payment_method.id ).first() self.assertFalse( payment_method.detach(), "Second call to detach should return false" ) def test_sync_null_customer(self): payment_method = models.PaymentMethod.sync_from_stripe_data( deepcopy(FAKE_PAYMENT_METHOD_I) ) assert payment_method self.assertIsNotNone(payment_method.customer) # simulate remote detach fake_payment_method_no_customer = deepcopy(FAKE_PAYMENT_METHOD_I) fake_payment_method_no_customer["customer"] = None payment_method = models.PaymentMethod.sync_from_stripe_data( fake_payment_method_no_customer ) assert payment_method self.assertIsNone(payment_method.customer) self.assert_fks( payment_method, expected_blank_fks={"djstripe.PaymentMethod.customer"} ) ================================================ FILE: tests/test_payout.py ================================================ """ dj-stripe Payout Model Tests. """ from copy import deepcopy from unittest.mock import patch import pytest from django.contrib.auth import get_user_model from django.test import TestCase from djstripe.models import BankAccount, Card, Payout from . import ( FAKE_BALANCE_TRANSACTION, FAKE_BANK_ACCOUNT, FAKE_CARD, FAKE_CUSTOM_ACCOUNT, FAKE_CUSTOMER, FAKE_EXPRESS_ACCOUNT, FAKE_PAYOUT_CUSTOM_BANK_ACCOUNT, FAKE_PAYOUT_CUSTOM_CARD, FAKE_PLATFORM_ACCOUNT, FAKE_STANDARD_ACCOUNT, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class TestPayout(AssertStripeFksMixin, TestCase): def setUp(self): # create a Standard Stripe Account self.standard_account = FAKE_STANDARD_ACCOUNT.create() # create a Custom Stripe Account self.custom_account = FAKE_CUSTOM_ACCOUNT.create() # create an Express Stripe Account self.express_account = FAKE_EXPRESS_ACCOUNT.create() user = get_user_model().objects.create_user( username="arnav13", email="arnav13@gmail.com" ) fake_empty_customer = deepcopy(FAKE_CUSTOMER) fake_empty_customer["default_source"] = None fake_empty_customer["sources"] = [] self.customer = fake_empty_customer.create_for_user(user) self.card = Card.sync_from_stripe_data(FAKE_CARD) self.bank_account = BankAccount.sync_from_stripe_data(FAKE_BANK_ACCOUNT) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) def test_sync_from_stripe_data(self, balance_transaction_retrieve_mock): fake_payout_custom = deepcopy(FAKE_PAYOUT_CUSTOM_BANK_ACCOUNT) payout = Payout.sync_from_stripe_data(fake_payout_custom) self.assertEqual(payout.balance_transaction.id, FAKE_BALANCE_TRANSACTION["id"]) self.assertEqual(payout.destination.id, fake_payout_custom["destination"]) self.assertEqual(payout.djstripe_owner_account.id, FAKE_PLATFORM_ACCOUNT["id"]) self.assert_fks( payout, expected_blank_fks={"djstripe.Payout.failure_balance_transaction"} ) fake_payout_express = deepcopy(FAKE_PAYOUT_CUSTOM_CARD) payout = Payout.sync_from_stripe_data(fake_payout_express) self.assertEqual(payout.balance_transaction.id, FAKE_BALANCE_TRANSACTION["id"]) self.assertEqual(payout.destination.id, fake_payout_express["destination"]) self.assertEqual(payout.djstripe_owner_account.id, FAKE_PLATFORM_ACCOUNT["id"]) self.assert_fks( payout, expected_blank_fks={"djstripe.Payout.failure_balance_transaction"} ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) def test___str__(self, balance_transaction_retrieve_mock): fake_payout_custom = deepcopy(FAKE_PAYOUT_CUSTOM_BANK_ACCOUNT) payout = Payout.sync_from_stripe_data(fake_payout_custom) self.assertEqual(str(payout), "10.00 (Paid)") fake_payout_express = deepcopy(FAKE_PAYOUT_CUSTOM_CARD) payout = Payout.sync_from_stripe_data(fake_payout_express) self.assertEqual(str(payout), "10.00 (Paid)") ================================================ FILE: tests/test_plan.py ================================================ """ dj-stripe Plan Model Tests. """ from copy import deepcopy from unittest.mock import patch import pytest import stripe from django.test import TestCase from djstripe.enums import PriceUsageType from djstripe.models import Plan, Product from djstripe.settings import djstripe_settings from . import ( FAKE_PLAN, FAKE_PLAN_II, FAKE_PLAN_METERED, FAKE_PRODUCT, FAKE_TIER_PLAN, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class PlanCreateTest(AssertStripeFksMixin, TestCase): def setUp(self): with patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True, ): self.stripe_product = Product(id=FAKE_PRODUCT["id"]).api_retrieve() @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch("stripe.Plan.create", return_value=deepcopy(FAKE_PLAN), autospec=True) def test_create_from_product_id(self, plan_create_mock, product_retrieve_mock): fake_plan = deepcopy(FAKE_PLAN) fake_plan["amount"] = fake_plan["amount"] / 100 self.assertIsInstance(fake_plan["product"], str) plan = Plan.create(**fake_plan) expected_create_kwargs = deepcopy(FAKE_PLAN) expected_create_kwargs["api_key"] = djstripe_settings.STRIPE_SECRET_KEY expected_create_kwargs["stripe_version"] = djstripe_settings.STRIPE_API_VERSION plan_create_mock.assert_called_once_with(**expected_create_kwargs) self.assert_fks( plan, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch("stripe.Plan.create", return_value=deepcopy(FAKE_PLAN), autospec=True) def test_create_from_stripe_product(self, plan_create_mock, product_retrieve_mock): fake_plan = deepcopy(FAKE_PLAN) fake_plan["product"] = self.stripe_product fake_plan["amount"] = fake_plan["amount"] / 100 self.assertIsInstance(fake_plan["product"], dict) plan = Plan.create(**fake_plan) expected_create_kwargs = deepcopy(FAKE_PLAN) expected_create_kwargs["product"] = self.stripe_product plan_create_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, **expected_create_kwargs, ) self.assert_fks( plan, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch("stripe.Plan.create", return_value=deepcopy(FAKE_PLAN), autospec=True) def test_create_from_djstripe_product( self, plan_create_mock, product_retrieve_mock ): fake_plan = deepcopy(FAKE_PLAN) fake_plan["product"] = Product.sync_from_stripe_data(self.stripe_product) fake_plan["amount"] = fake_plan["amount"] / 100 self.assertIsInstance(fake_plan["product"], Product) plan = Plan.create(**fake_plan) plan_create_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, **FAKE_PLAN, ) self.assert_fks( plan, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch("stripe.Plan.create", return_value=deepcopy(FAKE_PLAN), autospec=True) def test_create_with_metadata(self, plan_create_mock, product_retrieve_mock): metadata = {"other_data": "more_data"} fake_plan = deepcopy(FAKE_PLAN) fake_plan["amount"] = fake_plan["amount"] / 100 fake_plan["metadata"] = metadata self.assertIsInstance(fake_plan["product"], str) plan = Plan.create(**fake_plan) expected_create_kwargs = deepcopy(FAKE_PLAN) expected_create_kwargs["metadata"] = metadata plan_create_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, **expected_create_kwargs, ) self.assert_fks( plan, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) class PlanTest(AssertStripeFksMixin, TestCase): plan: Plan def setUp(self): self.plan_data = deepcopy(FAKE_PLAN) with patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True, ): self.plan = Plan.sync_from_stripe_data(self.plan_data) def test___str__(self): assert ( str(self.plan) == f"{self.plan.human_readable_price} for {FAKE_PRODUCT['name']}" ) def test___str__null_product(self): plan_data = deepcopy(FAKE_PLAN_II) del plan_data["product"] plan: Plan = Plan.sync_from_stripe_data(plan_data) self.assertIsNone(plan.product) assert str(plan) == plan.human_readable_price @patch("stripe.Plan.retrieve", return_value=FAKE_PLAN, autospec=True) def test_stripe_plan(self, plan_retrieve_mock): stripe_plan = self.plan.api_retrieve() plan_retrieve_mock.assert_called_once_with( id=self.plan_data["id"], api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, expand=["product", "tiers"], stripe_account=self.plan.djstripe_owner_account.id, ) plan = Plan.sync_from_stripe_data(stripe_plan) assert plan.amount_in_cents == plan.amount * 100 assert isinstance(plan.amount_in_cents, int) self.assert_fks( plan, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) def test_stripe_plan_null_product(self): """ assert that plan.Product can be null for backwards compatibility though note that it is a Stripe required field """ plan_data = deepcopy(FAKE_PLAN_II) del plan_data["product"] plan = Plan.sync_from_stripe_data(plan_data) self.assert_fks( plan, expected_blank_fks={"djstripe.Customer.coupon", "djstripe.Plan.product"}, ) def test_stripe_tier_plan(self): tier_plan_data = deepcopy(FAKE_TIER_PLAN) plan = Plan.sync_from_stripe_data(tier_plan_data) self.assertEqual(plan.id, tier_plan_data["id"]) self.assertIsNone(plan.amount) self.assertIsNotNone(plan.tiers, plan.product) self.assert_fks( plan, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) def test_stripe_metered_plan(self): plan_data = deepcopy(FAKE_PLAN_METERED) plan = Plan.sync_from_stripe_data(plan_data) self.assertEqual(plan.id, plan_data["id"]) self.assertEqual(plan.usage_type, PriceUsageType.metered) self.assertIsNotNone(plan.amount, plan.product) self.assert_fks( plan, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) class TestHumanReadablePlan: # # Helpers # def get_fake_price_NONE_flat_amount(): FAKE_PRICE_TIER_NONE_FLAT_AMOUNT = deepcopy(FAKE_TIER_PLAN) FAKE_PRICE_TIER_NONE_FLAT_AMOUNT["tiers"][0]["flat_amount"] = None FAKE_PRICE_TIER_NONE_FLAT_AMOUNT["tiers"][0]["flat_amount_decimal"] = None return FAKE_PRICE_TIER_NONE_FLAT_AMOUNT def get_fake_price_0_flat_amount(): FAKE_PRICE_TIER_0_FLAT_AMOUNT = deepcopy(FAKE_TIER_PLAN) FAKE_PRICE_TIER_0_FLAT_AMOUNT["tiers"][0]["flat_amount"] = 0 FAKE_PRICE_TIER_0_FLAT_AMOUNT["tiers"][0]["flat_amount_decimal"] = 0 return FAKE_PRICE_TIER_0_FLAT_AMOUNT def get_fake_price_0_amount(): FAKE_PRICE_TIER_0_AMOUNT = deepcopy(FAKE_PLAN) FAKE_PRICE_TIER_0_AMOUNT["amount"] = 0 FAKE_PRICE_TIER_0_AMOUNT["amount_decimal"] = 0 return FAKE_PRICE_TIER_0_AMOUNT @pytest.mark.parametrize( "fake_plan_data, expected_str", [ (deepcopy(FAKE_PLAN), "$20.00 USD/month"), (get_fake_price_0_amount(), "$0.00 USD/month"), ( deepcopy(FAKE_TIER_PLAN), "Starts at $10.00 USD per unit + $49.00 USD/month", ), ( get_fake_price_0_flat_amount(), "Starts at $10.00 USD per unit + $0.00 USD/month", ), ( get_fake_price_NONE_flat_amount(), "Starts at $10.00 USD per unit/month", ), (deepcopy(FAKE_PLAN_METERED), "$2.00 USD/month"), ], ) def test_human_readable(self, fake_plan_data, expected_str, monkeypatch): def mock_product_get(*args, **kwargs): return deepcopy(FAKE_PRODUCT) def mock_price_get(*args, **kwargs): return fake_plan_data # monkeypatch stripe.Product.retrieve and stripe.Plan.retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Plan, "retrieve", mock_price_get) plan = Plan.sync_from_stripe_data(fake_plan_data) assert plan.human_readable_price == expected_str ================================================ FILE: tests/test_price.py ================================================ """ dj-stripe Price model tests """ from copy import deepcopy from unittest.mock import patch import pytest import stripe from django.test import TestCase from djstripe.enums import PriceType, PriceUsageType from djstripe.models import Price, Product from djstripe.settings import djstripe_settings from . import ( FAKE_PRICE, FAKE_PRICE_METERED, FAKE_PRICE_ONETIME, FAKE_PRICE_TIER, FAKE_PRODUCT, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class PriceCreateTest(AssertStripeFksMixin, TestCase): def setUp(self): with patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True, ): self.stripe_product = Product(id=FAKE_PRODUCT["id"]).api_retrieve() @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch("stripe.Price.create", return_value=deepcopy(FAKE_PRICE), autospec=True) def test_create_from_product_id(self, price_create_mock, product_retrieve_mock): fake_price = deepcopy(FAKE_PRICE) fake_price["unit_amount"] /= 100 assert isinstance(fake_price["product"], str) price = Price.create(**fake_price) expected_create_kwargs = deepcopy(FAKE_PRICE) expected_create_kwargs["api_key"] = djstripe_settings.STRIPE_SECRET_KEY price_create_mock.assert_called_once_with( stripe_version=djstripe_settings.STRIPE_API_VERSION, **expected_create_kwargs, ) self.assert_fks( price, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch("stripe.Price.create", return_value=deepcopy(FAKE_PRICE), autospec=True) def test_create_from_stripe_product(self, price_create_mock, product_retrieve_mock): fake_price = deepcopy(FAKE_PRICE) fake_price["product"] = self.stripe_product fake_price["unit_amount"] /= 100 assert isinstance(fake_price["product"], dict) price = Price.create(**fake_price) expected_create_kwargs = deepcopy(FAKE_PRICE) expected_create_kwargs["product"] = self.stripe_product price_create_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, **expected_create_kwargs, ) self.assert_fks( price, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch("stripe.Price.create", return_value=deepcopy(FAKE_PRICE), autospec=True) def test_create_from_djstripe_product( self, price_create_mock, product_retrieve_mock ): fake_price = deepcopy(FAKE_PRICE) fake_price["product"] = Product.sync_from_stripe_data(self.stripe_product) fake_price["unit_amount"] /= 100 assert isinstance(fake_price["product"], Product) price = Price.create(**fake_price) price_create_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, **FAKE_PRICE, ) self.assert_fks( price, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch("stripe.Price.create", return_value=deepcopy(FAKE_PRICE), autospec=True) def test_create_with_metadata(self, price_create_mock, product_retrieve_mock): metadata = {"other_data": "more_data"} fake_price = deepcopy(FAKE_PRICE) fake_price["unit_amount"] /= 100 fake_price["metadata"] = metadata assert isinstance(fake_price["product"], str) price = Price.create(**fake_price) expected_create_kwargs = deepcopy(FAKE_PRICE) expected_create_kwargs["metadata"] = metadata price_create_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, **expected_create_kwargs, ) self.assert_fks( price, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) class PriceTest(AssertStripeFksMixin, TestCase): def setUp(self): self.price_data = deepcopy(FAKE_PRICE) with patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True, ): self.price = Price.sync_from_stripe_data(self.price_data) @patch("stripe.Price.retrieve", return_value=FAKE_PRICE, autospec=True) def test_stripe_price(self, price_retrieve_mock): stripe_price = self.price.api_retrieve() price_retrieve_mock.assert_called_once_with( id=self.price_data["id"], api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=["product", "tiers"], stripe_account=self.price.djstripe_owner_account.id, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) price = Price.sync_from_stripe_data(stripe_price) self.assert_fks( price, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) assert price.human_readable_price == "$20.00 USD/month" @patch("stripe.Price.retrieve", autospec=True) def test_stripe_tier_price(self, price_retrieve_mock): price_data = deepcopy(FAKE_PRICE_TIER) price = Price.sync_from_stripe_data(price_data) assert price.id == price_data["id"] assert price.unit_amount is None assert price.tiers is not None self.assert_fks( price, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) @patch("stripe.Price.retrieve", autospec=True) def test_stripe_metered_price(self, price_retrieve_mock): price_data = deepcopy(FAKE_PRICE_METERED) price = Price.sync_from_stripe_data(price_data) assert price.id == price_data["id"] assert price.recurring["usage_type"] == PriceUsageType.metered assert price.unit_amount is not None self.assert_fks( price, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) @patch("stripe.Price.retrieve", autospec=True) def test_stripe_onetime_price(self, price_retrieve_mock): price_data = deepcopy(FAKE_PRICE_ONETIME) price = Price.sync_from_stripe_data(price_data) assert price.id == price_data["id"] assert price.unit_amount is not None assert not price.recurring assert price.type == PriceType.one_time self.assert_fks( price, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Product.default_price", }, ) class TestStrPrice: @pytest.mark.parametrize( "fake_price_data", [ deepcopy(FAKE_PRICE), deepcopy(FAKE_PRICE_ONETIME), deepcopy(FAKE_PRICE_TIER), deepcopy(FAKE_PRICE_METERED), ], ) def test___str__(self, fake_price_data, monkeypatch): def mock_product_get(*args, **kwargs): return deepcopy(FAKE_PRODUCT) def mock_price_get(*args, **kwargs): return fake_price_data # monkeypatch stripe.Product.retrieve and stripe.Price.retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Price, "retrieve", mock_price_get) if not fake_price_data["recurring"]: price = Price.sync_from_stripe_data(fake_price_data) assert (f"{price.human_readable_price} for {FAKE_PRODUCT['name']}") == str( price ) else: price = Price.sync_from_stripe_data(fake_price_data) assert ( str(price) == f"{price.human_readable_price} for {FAKE_PRODUCT['name']}" ) class TestHumanReadablePrice: # # Helpers # def get_fake_price_NONE_flat_amount(): FAKE_PRICE_TIER_NONE_FLAT_AMOUNT = deepcopy(FAKE_PRICE_TIER) FAKE_PRICE_TIER_NONE_FLAT_AMOUNT["tiers"][0]["flat_amount"] = None FAKE_PRICE_TIER_NONE_FLAT_AMOUNT["tiers"][0]["flat_amount_decimal"] = None return FAKE_PRICE_TIER_NONE_FLAT_AMOUNT def get_fake_price_0_flat_amount(): FAKE_PRICE_TIER_0_FLAT_AMOUNT = deepcopy(FAKE_PRICE_TIER) FAKE_PRICE_TIER_0_FLAT_AMOUNT["tiers"][0]["flat_amount"] = 0 FAKE_PRICE_TIER_0_FLAT_AMOUNT["tiers"][0]["flat_amount_decimal"] = 0 return FAKE_PRICE_TIER_0_FLAT_AMOUNT def get_fake_price_0_amount(): FAKE_PRICE_TIER_0_AMOUNT = deepcopy(FAKE_PRICE) FAKE_PRICE_TIER_0_AMOUNT["unit_amount"] = 0 FAKE_PRICE_TIER_0_AMOUNT["unit_amount_decimal"] = 0 return FAKE_PRICE_TIER_0_AMOUNT @pytest.mark.parametrize( "fake_price_data, expected_str", [ (deepcopy(FAKE_PRICE), "$20.00 USD/month"), (get_fake_price_0_amount(), "$0.00 USD/month"), (deepcopy(FAKE_PRICE_ONETIME), "$20.00 USD (one time)"), ( deepcopy(FAKE_PRICE_TIER), "Starts at $10.00 USD per unit + $49.00 USD/month", ), ( get_fake_price_0_flat_amount(), "Starts at $10.00 USD per unit + $0.00 USD/month", ), ( get_fake_price_NONE_flat_amount(), "Starts at $10.00 USD per unit/month", ), (deepcopy(FAKE_PRICE_METERED), "$2.00 USD/month"), ], ) def test_human_readable(self, fake_price_data, expected_str, monkeypatch): def mock_product_get(*args, **kwargs): return deepcopy(FAKE_PRODUCT) def mock_price_get(*args, **kwargs): return fake_price_data # monkeypatch stripe.Product.retrieve and stripe.Price.retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Price, "retrieve", mock_price_get) price = Price.sync_from_stripe_data(fake_price_data) assert price.human_readable_price == expected_str ================================================ FILE: tests/test_product.py ================================================ """ dj-stripe Product model tests """ from copy import deepcopy import pytest import stripe from djstripe.models import Product from djstripe.models.core import Price from . import ( FAKE_FILEUPLOAD_ICON, FAKE_PLATFORM_ACCOUNT, FAKE_PRICE, FAKE_PRICE_METERED, FAKE_PRICE_ONETIME, FAKE_PRICE_TIER, FAKE_PRODUCT, ) pytestmark = pytest.mark.django_db class TestProduct: # # Helper Methods for monkeypatching # def mock_file_retrieve(*args, **kwargs): return deepcopy(FAKE_FILEUPLOAD_ICON) def mock_account_retrieve(*args, **kwargs): return deepcopy(FAKE_PLATFORM_ACCOUNT) def mock_product_get(self, *args, **kwargs): return deepcopy(FAKE_PRODUCT) @pytest.mark.parametrize("count", [1, 2, 3]) def test___str__(self, count, monkeypatch): def mock_price_get(*args, **kwargs): return random_price_data # monkeypatch stripe.Product.retrieve and stripe.Price.retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Product, "retrieve", self.mock_product_get) monkeypatch.setattr(stripe.Price, "retrieve", mock_price_get) product = Product.sync_from_stripe_data(deepcopy(FAKE_PRODUCT)) PRICE_DATA_OPTIONS = [ deepcopy(FAKE_PRICE), deepcopy(FAKE_PRICE_TIER), deepcopy(FAKE_PRICE_METERED), deepcopy(FAKE_PRICE_ONETIME), ] for _ in range(count): random_price_data = PRICE_DATA_OPTIONS.pop() price = Price.sync_from_stripe_data(random_price_data) if count > 1: assert f"{FAKE_PRODUCT['name']} ({count} prices)" == str(product) else: assert f"{FAKE_PRODUCT['name']} ({price.human_readable_price})" == str( product ) def test_sync_from_stripe_data(self, monkeypatch): # monkeypatch stripe.Product.retrieve call to return # the desired json response. monkeypatch.setattr(stripe.Product, "retrieve", self.mock_product_get) product = Product.sync_from_stripe_data(deepcopy(FAKE_PRODUCT)) assert product.id == FAKE_PRODUCT["id"] assert product.name == FAKE_PRODUCT["name"] assert product.type == FAKE_PRODUCT["type"] ================================================ FILE: tests/test_refund.py ================================================ """ dj-stripe Charge Model Tests. """ from copy import deepcopy from unittest.mock import patch from django.contrib.auth import get_user_model from django.test.testcases import TestCase from djstripe import enums from djstripe.models import Refund from . import ( FAKE_BALANCE_TRANSACTION_REFUND, FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CHARGE, FAKE_CUSTOMER, FAKE_INVOICE, FAKE_INVOICEITEM, FAKE_PAYMENT_INTENT_I, FAKE_PLATFORM_ACCOUNT, FAKE_PRODUCT, FAKE_REFUND, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_ITEM, AssertStripeFksMixin, ) class RefundTest(AssertStripeFksMixin, TestCase): def setUp(self): # create a Stripe Platform Account self.account = FAKE_PLATFORM_ACCOUNT.create() user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) self.customer = FAKE_CUSTOMER.create_for_user(user) self.default_expected_blank_fks = { "djstripe.Account.branding_logo", "djstripe.Account.branding_icon", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.on_behalf_of", "djstripe.Charge.refund", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.Product.default_price", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", "djstripe.Refund.failure_balance_transaction", } @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) def test_sync_from_stripe_data( self, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_refund = deepcopy(FAKE_REFUND) balance_transaction_retrieve_mock.return_value = deepcopy( FAKE_BALANCE_TRANSACTION_REFUND ) refund = Refund.sync_from_stripe_data(fake_refund) self.assert_fks(refund, expected_blank_fks=self.default_expected_blank_fks) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) def test___str__( self, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account fake_refund = deepcopy(FAKE_REFUND) fake_refund["reason"] = enums.RefundReason.requested_by_customer balance_transaction_retrieve_mock.return_value = deepcopy( FAKE_BALANCE_TRANSACTION_REFUND ) refund = Refund.sync_from_stripe_data(fake_refund) self.assertEqual(str(refund), "$20.00 USD (Succeeded)") self.assert_fks(refund, expected_blank_fks=self.default_expected_blank_fks) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) def test_reason_enum( self, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account balance_transaction_retrieve_mock.return_value = deepcopy( FAKE_BALANCE_TRANSACTION_REFUND ) fake_refund = deepcopy(FAKE_REFUND) for reason in ( "duplicate", "fraudulent", "requested_by_customer", "expired_uncaptured_charge", ): fake_refund["reason"] = reason refund = Refund.sync_from_stripe_data(fake_refund) self.assertEqual(refund.reason, reason) # trigger model field validation (including enum value choices check) refund.full_clean() self.assert_fks(refund, expected_blank_fks=self.default_expected_blank_fks) @patch( "djstripe.models.Account.get_default_account", autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) def test_status_enum( self, invoice_retrieve_mock, invoice_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, default_account_mock, ): default_account_mock.return_value = self.account balance_transaction_retrieve_mock.return_value = deepcopy( FAKE_BALANCE_TRANSACTION_REFUND ) fake_refund = deepcopy(FAKE_REFUND) for status in ( "pending", "succeeded", "failed", "canceled", ): fake_refund["status"] = status refund = Refund.sync_from_stripe_data(fake_refund) self.assertEqual(refund.status, status) # trigger model field validation (including enum value choices check) refund.full_clean() self.assert_fks(refund, expected_blank_fks=self.default_expected_blank_fks) ================================================ FILE: tests/test_session.py ================================================ """ dj-stripe Session Model Tests. """ from copy import deepcopy from unittest.mock import patch import pytest import stripe from django.test import TestCase from djstripe.models import Session from djstripe.settings import djstripe_settings from tests import ( FAKE_BALANCE_TRANSACTION, FAKE_CHARGE, FAKE_CUSTOMER, FAKE_INVOICE, FAKE_INVOICEITEM, FAKE_PAYMENT_INTENT_I, FAKE_PAYMENT_METHOD_I, FAKE_PRODUCT, FAKE_SESSION_I, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_ITEM, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class SessionTest(AssertStripeFksMixin, TestCase): @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) def test_sync_from_stripe_data( self, payment_intent_retrieve_mock, customer_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): session = Session.sync_from_stripe_data(deepcopy(FAKE_SESSION_I)) self.assert_fks( session, expected_blank_fks={ "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.subscriber", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.Product.default_price", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", "djstripe.Session.subscription", }, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) def test___str__( self, payment_intent_retrieve_mock, customer_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, ): session = Session.sync_from_stripe_data(deepcopy(FAKE_SESSION_I)) self.assertEqual(f"", str(session)) class TestSession: key = djstripe_settings.SUBSCRIBER_CUSTOMER_KEY @pytest.mark.parametrize( "metadata", [ {}, {"key1": "val1", key: "random"}, ], ) def test__attach_objects_post_save_hook( self, monkeypatch, fake_user, fake_customer, metadata ): """ Test for Checkout Session _attach_objects_post_save_hook """ user = fake_user customer = fake_customer # because create_for_user method adds subscriber customer.subcriber = None customer.save() # update metadata if metadata.get(self.key, ""): metadata[self.key] = user.id fake_stripe_session = deepcopy(FAKE_SESSION_I) fake_stripe_session["metadata"] = metadata def mock_checkout_session_get(*args, **kwargs): """Monkeypatched stripe.Session.retrieve""" return deepcopy(fake_stripe_session) def mock_customer_get(*args, **kwargs): """Monkeypatched stripe.Customer.retrieve""" return deepcopy(FAKE_CUSTOMER) def mock_payment_intent_get(*args, **kwargs): """Monkeypatched stripe.PaymentIntent.retrieve""" return deepcopy(FAKE_PAYMENT_INTENT_I) def mock_invoice_get(*args, **kwargs): """Monkeypatched stripe.Invoice.retrieve""" return deepcopy(FAKE_INVOICE) def mock_invoice_item_get(*args, **kwargs): return deepcopy(FAKE_INVOICEITEM) def mock_payment_method_get(*args, **kwargs): """Monkeypatched stripe.PaymentMethod.retrieve""" return deepcopy(FAKE_PAYMENT_METHOD_I) def mock_subscription_get(*args, **kwargs): """Monkeypatched stripe.Subscription.retrieve""" return deepcopy(FAKE_SUBSCRIPTION) def mock_subscriptionitem_get(*args, **kwargs): return deepcopy(FAKE_SUBSCRIPTION_ITEM) def mock_balance_transaction_get(*args, **kwargs): """Monkeypatched stripe.BalanceTransaction.retrieve""" return deepcopy(FAKE_BALANCE_TRANSACTION) def mock_product_get(*args, **kwargs): """Monkeypatched stripe.Product.retrieve""" return deepcopy(FAKE_PRODUCT) def mock_charge_get(*args, **kwargs): """Monkeypatched stripe.Charge.retrieve""" return deepcopy(FAKE_CHARGE) # monkeypatch stripe.checkout.Session.retrieve, stripe.Customer.retrieve, stripe.PaymentIntent.retrieve monkeypatch.setattr( stripe.checkout.Session, "retrieve", mock_checkout_session_get ) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr(stripe.Customer, "modify", mock_customer_get) monkeypatch.setattr(stripe.PaymentIntent, "retrieve", mock_payment_intent_get) # because of Reverse o2o field sync due to PaymentIntent.sync_from_stripe_data.. monkeypatch.setattr(stripe.Invoice, "retrieve", mock_invoice_get) monkeypatch.setattr(stripe.InvoiceItem, "retrieve", mock_invoice_item_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) monkeypatch.setattr(stripe.Subscription, "retrieve", mock_subscription_get) monkeypatch.setattr( stripe.SubscriptionItem, "retrieve", mock_subscriptionitem_get ) monkeypatch.setattr( stripe.BalanceTransaction, "retrieve", mock_balance_transaction_get ) monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Charge, "retrieve", mock_charge_get) # Invoke the sync to invoke _attach_objects_post_save_hook() session = Session.sync_from_stripe_data(fake_stripe_session) # refresh self.customer from db customer.refresh_from_db() assert session assert session.customer.id == customer.id assert customer.subscriber == user if metadata.get(self.key, ""): assert customer.metadata == {self.key: metadata.get(self.key)} else: assert customer.metadata == {} ================================================ FILE: tests/test_settings.py ================================================ """ dj-stripe Settings Tests. """ from unittest.mock import patch import stripe from django.core.exceptions import ImproperlyConfigured from django.db.models.base import ModelBase from django.test import TestCase from django.test.utils import override_settings from djstripe import settings class TestSubscriberModelRetrievalMethod(TestCase): def test_with_user(self): user_model = settings.djstripe_settings.get_subscriber_model() self.assertTrue(isinstance(user_model, ModelBase)) @override_settings( DJSTRIPE_SUBSCRIBER_MODEL="testapp.Organization", DJSTRIPE_SUBSCRIBER_MODEL_REQUEST_CALLBACK=(lambda request: request.org), ) def test_with_org(self): org_model = settings.djstripe_settings.get_subscriber_model() self.assertTrue(isinstance(org_model, ModelBase)) @override_settings( DJSTRIPE_SUBSCRIBER_MODEL="testapp.StaticEmailOrganization", DJSTRIPE_SUBSCRIBER_MODEL_REQUEST_CALLBACK=(lambda request: request.org), ) def test_with_org_static(self): org_model = settings.djstripe_settings.get_subscriber_model() self.assertTrue(isinstance(org_model, ModelBase)) @override_settings( DJSTRIPE_SUBSCRIBER_MODEL="testappStaticEmailOrganization", DJSTRIPE_SUBSCRIBER_MODEL_REQUEST_CALLBACK=(lambda request: request.org), ) def test_bad_model_name(self): self.assertRaisesMessage( ImproperlyConfigured, "DJSTRIPE_SUBSCRIBER_MODEL must be of the form 'app_label.model_name'.", settings.djstripe_settings.get_subscriber_model, ) @override_settings( DJSTRIPE_SUBSCRIBER_MODEL="testapp.UnknownModel", DJSTRIPE_SUBSCRIBER_MODEL_REQUEST_CALLBACK=(lambda request: request.org), ) def test_unknown_model(self): self.assertRaisesMessage( ImproperlyConfigured, "DJSTRIPE_SUBSCRIBER_MODEL refers to model 'testapp.UnknownModel' " "that has not been installed.", settings.djstripe_settings.get_subscriber_model, ) @override_settings( DJSTRIPE_SUBSCRIBER_MODEL="testapp.NoEmailOrganization", DJSTRIPE_SUBSCRIBER_MODEL_REQUEST_CALLBACK=(lambda request: request.org), ) def test_no_email_model(self): self.assertRaisesMessage( ImproperlyConfigured, "DJSTRIPE_SUBSCRIBER_MODEL must have an email attribute.", settings.djstripe_settings.get_subscriber_model, ) @override_settings(DJSTRIPE_SUBSCRIBER_MODEL="testapp.Organization") def test_no_callback(self): self.assertRaisesMessage( ImproperlyConfigured, "DJSTRIPE_SUBSCRIBER_MODEL_REQUEST_CALLBACK must be implemented " "if a DJSTRIPE_SUBSCRIBER_MODEL is defined.", settings.djstripe_settings.get_subscriber_model, ) @override_settings( DJSTRIPE_SUBSCRIBER_MODEL="testapp.Organization", DJSTRIPE_SUBSCRIBER_MODEL_REQUEST_CALLBACK=5, ) def test_bad_callback(self): self.assertRaisesMessage( ImproperlyConfigured, "DJSTRIPE_SUBSCRIBER_MODEL_REQUEST_CALLBACK must be callable.", settings.djstripe_settings.get_subscriber_model, ) @override_settings(DJSTRIPE_TEST_CALLBACK=(lambda: "ok")) def test_get_callback_function_with_valid_func_callable(self): func = settings.djstripe_settings.get_callback_function( "DJSTRIPE_TEST_CALLBACK" ) self.assertEqual("ok", func()) @override_settings(DJSTRIPE_TEST_CALLBACK="foo.valid_callback") @patch.object(settings, "import_string", return_value=(lambda: "ok")) def test_get_callback_function_with_valid_string_callable(self, import_string_mock): func = settings.djstripe_settings.get_callback_function( "DJSTRIPE_TEST_CALLBACK" ) self.assertEqual("ok", func()) import_string_mock.assert_called_with("foo.valid_callback") @override_settings(DJSTRIPE_TEST_CALLBACK="foo.non_existant_callback") def test_get_callback_function_import_error(self): with self.assertRaises(ImportError): settings.djstripe_settings.get_callback_function("DJSTRIPE_TEST_CALLBACK") @override_settings(DJSTRIPE_TEST_CALLBACK="foo.invalid_callback") @patch.object(settings, "import_string", return_value="not_callable") def test_get_callback_function_with_non_callable_string(self, import_string_mock): with self.assertRaises(ImproperlyConfigured): settings.djstripe_settings.get_callback_function("DJSTRIPE_TEST_CALLBACK") import_string_mock.assert_called_with("foo.invalid_callback") @override_settings(DJSTRIPE_TEST_CALLBACK="foo.non_existant_callback") def test_get_callback_function_(self): with self.assertRaises(ImportError): settings.djstripe_settings.get_callback_function("DJSTRIPE_TEST_CALLBACK") @override_settings(STRIPE_API_VERSION="2016-03-07") class TestStripeApiVersion(TestCase): def test_global_stripe_api_version(self): """Test that stripe.api_version is untouched. See https://github.com/dj-stripe/dj-stripe/issues/1854 """ assert stripe.api_version is None @override_settings(STRIPE_API_VERSION=None) class TestGetStripeApiVersion(TestCase): def test_with_default(self): self.assertEqual( settings.djstripe_settings.DEFAULT_STRIPE_API_VERSION, settings.djstripe_settings.STRIPE_API_VERSION, ) @override_settings(STRIPE_API_VERSION="2016-03-07") def test_with_override(self): self.assertEqual( "2016-03-07", settings.djstripe_settings.STRIPE_API_VERSION, ) class TestObjectPatching(TestCase): @patch.object( settings.djstripe_settings, "DJSTRIPE_WEBHOOK_URL", return_value=r"^webhook/sample/$", ) def test_object_patching(self, mock): webhook_url = settings.djstripe_settings.DJSTRIPE_WEBHOOK_URL self.assertTrue(webhook_url, r"^webhook/sample/$") ================================================ FILE: tests/test_setup_intent.py ================================================ """ dj-stripe SetupIntent Model Tests. """ from copy import deepcopy from unittest.mock import patch import pytest import stripe from django.test import TestCase from djstripe.enums import SetupIntentStatus from djstripe.models import Account, Customer, PaymentMethod, SetupIntent from tests import ( FAKE_CUSTOMER, FAKE_PAYMENT_METHOD_I, FAKE_SETUP_INTENT_DESTINATION_CHARGE, FAKE_SETUP_INTENT_I, FAKE_SETUP_INTENT_II, FAKE_STANDARD_ACCOUNT, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class TestStrSetupIntent: # # Helpers # def get_fake_setup_intent_destination_charge_no_customer(): FAKE_SETUP_INTENT_DESTINATION_CHARGE_NO_CUSTOMER = deepcopy( FAKE_SETUP_INTENT_DESTINATION_CHARGE ) FAKE_SETUP_INTENT_DESTINATION_CHARGE_NO_CUSTOMER["customer"] = None return FAKE_SETUP_INTENT_DESTINATION_CHARGE_NO_CUSTOMER @pytest.mark.parametrize( "fake_intent_data, has_account, has_customer", [ (FAKE_SETUP_INTENT_I, False, False), (FAKE_SETUP_INTENT_DESTINATION_CHARGE, True, True), (get_fake_setup_intent_destination_charge_no_customer(), True, False), (FAKE_SETUP_INTENT_II, False, True), ], ) def test___str__(self, fake_intent_data, has_account, has_customer, monkeypatch): def mock_customer_get(*args, **kwargs): return deepcopy(FAKE_CUSTOMER) def mock_account_get(*args, **kwargs): return deepcopy(FAKE_STANDARD_ACCOUNT) def mock_payment_method_get(*args, **kwargs): return deepcopy(FAKE_PAYMENT_METHOD_I) # monkeypatch stripe.Account.retrieve, stripe.Customer.retrieve, and stripe.PaymentMethod.retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Account, "retrieve", mock_account_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) si = SetupIntent.sync_from_stripe_data(fake_intent_data) pm = PaymentMethod.objects.filter(id=fake_intent_data["payment_method"]).first() account = Account.objects.filter(id=fake_intent_data["on_behalf_of"]).first() customer = Customer.objects.filter(id=fake_intent_data["customer"]).first() if has_account and has_customer: assert ( f"{pm} ({SetupIntentStatus.humanize(fake_intent_data['status'])}) " f"for {account} " f"by {customer}" ) == str(si) elif has_account and not has_customer: assert ( f"{pm} for {account}. {SetupIntentStatus.humanize(fake_intent_data['status'])}" ) == str(si) elif has_customer and not has_account: assert ( f"{pm} by {customer}. {SetupIntentStatus.humanize(fake_intent_data['status'])}" ) == str(si) elif not has_customer and not has_account: f"{pm} ({SetupIntentStatus.humanize(fake_intent_data['status'])})" == str( si ) class SetupIntentTest(AssertStripeFksMixin, TestCase): @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_sync_from_stripe_data(self, customer_retrieve_mock): fake_payment_intent = deepcopy(FAKE_SETUP_INTENT_I) setup_intent = SetupIntent.sync_from_stripe_data(fake_payment_intent) self.assertEqual(setup_intent.payment_method_types, ["card"]) self.assert_fks( setup_intent, expected_blank_fks={ "djstripe.SetupIntent.customer", "djstripe.SetupIntent.on_behalf_of", "djstripe.SetupIntent.payment_method", }, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_status_enum(self, customer_retrieve_mock): fake_setup_intent = deepcopy(FAKE_SETUP_INTENT_I) for status in ( "requires_payment_method", "requires_confirmation", "requires_action", "processing", "canceled", "succeeded", ): fake_setup_intent["status"] = status setup_intent = SetupIntent.sync_from_stripe_data(fake_setup_intent) # trigger model field validation (including enum value choices check) setup_intent.full_clean() @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_canceled_intent(self, customer_retrieve_mock): fake_setup_intent = deepcopy(FAKE_SETUP_INTENT_I) fake_setup_intent["status"] = "canceled" fake_setup_intent["canceled_at"] = 1567524169 for reason in (None, "abandoned", "requested_by_customer", "duplicate"): fake_setup_intent["cancellation_reason"] = reason setup_intent = SetupIntent.sync_from_stripe_data(fake_setup_intent) if reason is None: # enums nulls are coerced to "" by StripeModel._stripe_object_to_record self.assertEqual(setup_intent.cancellation_reason, "") else: self.assertEqual(setup_intent.cancellation_reason, reason) # trigger model field validation (including enum value choices check) setup_intent.full_clean() ================================================ FILE: tests/test_shipping_rate.py ================================================ """ dj-stripe ShippingRate Model Tests. """ from copy import deepcopy from unittest.mock import patch import pytest from django.test import TestCase from djstripe.models import ShippingRate from tests import ( FAKE_SHIPPING_RATE, FAKE_SHIPPING_RATE_WITH_TAX_CODE, FAKE_TAX_CODE, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class ShippingRateTest(AssertStripeFksMixin, TestCase): def test_sync_from_stripe_data(self): shipping_rate = ShippingRate.sync_from_stripe_data(deepcopy(FAKE_SHIPPING_RATE)) self.assertEqual( FAKE_SHIPPING_RATE["id"], shipping_rate.id, ) self.assertEqual( FAKE_SHIPPING_RATE["tax_code"], None, ) self.assert_fks( shipping_rate, expected_blank_fks={"djstripe.ShippingRate.tax_code"} ) @patch( "stripe.TaxCode.retrieve", autospec=True, return_value=deepcopy(FAKE_TAX_CODE) ) def test_sync_from_stripe_data_with_tax_code(self, tax_code_retrieve_mock): shipping_rate = ShippingRate.sync_from_stripe_data( deepcopy(FAKE_SHIPPING_RATE_WITH_TAX_CODE) ) self.assertEqual( FAKE_SHIPPING_RATE["id"], shipping_rate.id, ) self.assertEqual( FAKE_SHIPPING_RATE["tax_code"], None, ) self.assert_fks(shipping_rate, expected_blank_fks={}) def test___str__(self): shipping_rate = ShippingRate.sync_from_stripe_data(deepcopy(FAKE_SHIPPING_RATE)) self.assertEqual( "Test Shipping Code with no Tax Code - $1.25 USD (Active)", str(shipping_rate), ) ================================================ FILE: tests/test_source.py ================================================ """ dj-stripe Source Model Tests. """ import sys from copy import deepcopy from unittest.mock import patch from django.contrib.auth import get_user_model from django.test import TestCase from djstripe.models import Source from . import ( FAKE_CUSTOMER_III, FAKE_SOURCE, FAKE_SOURCE_II, AssertStripeFksMixin, SourceDict, ) class SourceTest(AssertStripeFksMixin, TestCase): def setUp(self): user = get_user_model().objects.create_user( username="testuser", email="djstripe@example.com" ) # create a source object so that FAKE_CUSTOMER_III with a default source # can be created correctly. fake_source_data = deepcopy(FAKE_SOURCE) fake_source_data["customer"] = None self.source = Source.sync_from_stripe_data(fake_source_data) self.customer = FAKE_CUSTOMER_III.create_for_user(user) self.customer.sources.all().delete() self.customer.legacy_cards.all().delete() def test_attach_objects_hook_without_customer(self): source = Source.sync_from_stripe_data(deepcopy(FAKE_SOURCE_II)) self.assertEqual(source.customer, None) self.assert_fks( source, expected_blank_fks={ "djstripe.Source.customer", "djstripe.Customer.default_payment_method", }, ) def test_sync_from_stripe_data(self): source = Source.sync_from_stripe_data(deepcopy(FAKE_SOURCE)) self.assertEqual(self.customer, source.customer) self.assert_fks( source, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", }, ) def test___str__(self): fake_source = deepcopy(FAKE_SOURCE) source = Source.sync_from_stripe_data(fake_source) self.assertEqual( f"{fake_source['type']} {fake_source['id']}", str(source), ) self.assert_fks( source, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", }, ) @patch("stripe.Source.retrieve", return_value=deepcopy(FAKE_SOURCE), autospec=True) def test_detach(self, source_retrieve_mock): original_detach = SourceDict.detach def mocked_detach(self): return original_detach(self) Source.sync_from_stripe_data(deepcopy(FAKE_SOURCE)) self.assertEqual(0, self.customer.legacy_cards.count()) self.assertEqual(1, self.customer.sources.count()) source = self.customer.sources.first() with patch( "tests.SourceDict.detach", side_effect=mocked_detach, autospec=True ) as mock_detach: source.detach() self.assertEqual(0, self.customer.sources.count()) # need to refresh_from_db since default_source was cleared with a query self.customer.refresh_from_db() self.assertIsNone(self.customer.default_source) # need to refresh_from_db due to the implementation of Source.detach() - # see TODO in method source.refresh_from_db() self.assertIsNone(source.customer) self.assertEqual(source.status, "consumed") if sys.version_info >= (3, 6): # this mock isn't working on py34, py35, but it's not strictly necessary # for the test mock_detach.assert_called() self.assert_fks( source, expected_blank_fks={ "djstripe.Source.customer", "djstripe.Customer.default_payment_method", }, ) ================================================ FILE: tests/test_stripe_model.py ================================================ """ dj-stripe StripeModel Model Tests. """ from unittest.mock import MagicMock, patch import pytest from django.test import TestCase from djstripe.models import Account, Customer, StripeModel from djstripe.settings import djstripe_settings pytestmark = pytest.mark.django_db class ExampleStripeModel(StripeModel): # exists to avoid "Abstract models cannot be instantiated." error pass class TestStripeModelExceptions(TestCase): def test_no_object_value(self): # Instantiate a stripeobject model class class BasicModel(StripeModel): pass with self.assertRaises(ValueError): # Errors because there's no object value BasicModel._stripe_object_to_record( {"id": "test_XXXXXXXX", "livemode": False} ) def test_bad_object_value(self): with self.assertRaises(ValueError): # Errors because the object is not correct Customer._stripe_object_to_record( {"id": "test_XXXXXXXX", "livemode": False, "object": "not_a_customer"} ) @pytest.mark.parametrize("stripe_account", (None, "acct_fakefakefakefake001")) @pytest.mark.parametrize( "api_key, expected_api_key", ( (None, djstripe_settings.STRIPE_SECRET_KEY), ("sk_fakefakefake01", "sk_fakefakefake01"), ), ) @pytest.mark.parametrize("extra_kwargs", ({}, {"foo": "bar"})) @patch.object(target=StripeModel, attribute="stripe_class") def test__api_delete( mock_stripe_class, stripe_account, api_key, expected_api_key, extra_kwargs ): """Test that API delete properly uses the passed in parameters.""" test_model = ExampleStripeModel() mock_id = "id_fakefakefakefake01" test_model.id = mock_id # invoke _api_delete() test_model._api_delete( api_key=api_key, stripe_account=stripe_account, **extra_kwargs ) mock_stripe_class.delete.assert_called_once_with( mock_id, api_key=expected_api_key, stripe_account=stripe_account, stripe_version=djstripe_settings.STRIPE_API_VERSION, **extra_kwargs ) @pytest.mark.parametrize("stripe_account", (None, "acct_fakefakefakefake001")) @pytest.mark.parametrize( "api_key, expected_api_key", ( (None, djstripe_settings.STRIPE_SECRET_KEY), ("sk_fakefakefake01", "sk_fakefakefake01"), ), ) @pytest.mark.parametrize("expand_fields", ([], ["foo", "bar"])) @patch.object(target=StripeModel, attribute="stripe_class") def test_api_retrieve( mock_stripe_class, stripe_account, api_key, expected_api_key, expand_fields ): """Test that API delete properly uses the passed in parameters.""" test_model = ExampleStripeModel() mock_id = "id_fakefakefakefake01" test_model.id = mock_id test_model.expand_fields = expand_fields test_model.api_retrieve(api_key=api_key, stripe_account=stripe_account) mock_stripe_class.retrieve.assert_called_once_with( id=mock_id, api_key=expected_api_key, stripe_account=stripe_account, stripe_version=djstripe_settings.STRIPE_API_VERSION, expand=expand_fields, ) @patch.object(target=StripeModel, attribute="stripe_class") def test_api_retrieve_reverse_foreign_key_lookup(mock_stripe_class): """Test that the reverse foreign key lookup finds the correct fields.""" # Set up some mock fields that shouldn't be used for reverse lookups mock_field_1 = MagicMock() mock_field_1.is_relation = False mock_field_2 = MagicMock() mock_field_2.is_relation = True mock_field_2.one_to_many = False # Set up a mock reverse foreign key field mock_reverse_foreign_key = MagicMock() mock_reverse_foreign_key.is_relation = True mock_reverse_foreign_key.one_to_many = True mock_reverse_foreign_key.related_model = Account mock_reverse_foreign_key.get_accessor_name.return_value = "foo_account_reverse_attr" # Set up a mock account for the reverse foreign key query to return. mock_account = MagicMock() mock_account_reverse_manager = MagicMock() # Make first return the mock account. mock_account_reverse_manager.first.return_value = mock_account test_model = ExampleStripeModel() mock_id = "id_fakefakefakefake01" test_model.id = mock_id # Set mock reverse manager on the model. test_model.foo_account_reverse_attr = mock_account_reverse_manager # Set the mocked _meta.get_fields to return some mock fields, including the mock # reverse foreign key above. test_model._meta = MagicMock() test_model._meta.get_fields.return_value = ( mock_field_1, mock_field_2, mock_reverse_foreign_key, ) # Call the function with API key set because we mocked _meta mock_api_key = "sk_fakefakefakefake01" test_model.api_retrieve(api_key=mock_api_key) # Expect the retrieve to be done with the reverse look up of the Account ID. mock_stripe_class.retrieve.assert_called_once_with( id=mock_id, api_key=mock_api_key, stripe_account=mock_account.id, expand=[], stripe_version=djstripe_settings.STRIPE_API_VERSION, ) mock_reverse_foreign_key.get_accessor_name.assert_called_once_with() mock_account_reverse_manager.first.assert_called_once_with() @pytest.mark.parametrize("api_key", (None, "sk_fakefakefake01")) @patch.object(target=Account, attribute="get_or_retrieve_for_api_key") def test__find_owner_account_for_empty_data( mock_get_or_retrieve_for_api_key, api_key, ): """ Test that the correct classmethod is invoked with the correct arguments to get the owner account """ fake_data = {} if api_key is None: # invoke _find_owner_account without the api_key parameter StripeModel._find_owner_account(fake_data) else: # invoke _find_owner_account with the api_key parameter StripeModel._find_owner_account(fake_data, api_key=api_key) if api_key: mock_get_or_retrieve_for_api_key.assert_called_once_with(api_key) else: mock_get_or_retrieve_for_api_key.assert_called_once_with( djstripe_settings.STRIPE_SECRET_KEY ) @pytest.mark.parametrize( "has_stripe_account_attr,stripe_account", ((False, None), (True, ""), (True, "acct_fakefakefakefake001")), ) @pytest.mark.parametrize("api_key", (None, "sk_fakefakefake01")) @patch.object(target=Account, attribute="get_or_retrieve_for_api_key") @patch.object(target=Account, attribute="_get_or_retrieve") def test__find_owner_account( mock__get_or_retrieve, mock_get_or_retrieve_for_api_key, api_key, stripe_account, has_stripe_account_attr, monkeypatch, ): """ Test that the correct classmethod is invoked with the correct arguments to get the owner account """ # fake_data_class used to invoke _find_owner_account classmethod class fake_data_class: @property def stripe_account(self): return stripe_account def get(*args, **kwargs): return "customer" fake_data = fake_data_class() if api_key is None: # invoke _find_owner_account without the api_key parameter StripeModel._find_owner_account(fake_data) else: # invoke _find_owner_account with the api_key parameter StripeModel._find_owner_account(fake_data, api_key=api_key) if has_stripe_account_attr and stripe_account: if api_key: mock__get_or_retrieve.assert_called_once_with( id=stripe_account, api_key=api_key ) else: mock__get_or_retrieve.assert_called_once_with( id=stripe_account, api_key=djstripe_settings.STRIPE_SECRET_KEY ) else: if api_key: mock_get_or_retrieve_for_api_key.assert_called_once_with(api_key) else: mock_get_or_retrieve_for_api_key.assert_called_once_with( djstripe_settings.STRIPE_SECRET_KEY ) @pytest.mark.parametrize( "has_account_key,stripe_account", ((False, None), (True, ""), (True, "acct_fakefakefakefake001")), ) @pytest.mark.parametrize("api_key", (None, "sk_fakefakefake01")) @patch.object(target=Account, attribute="get_or_retrieve_for_api_key") @patch.object(target=Account, attribute="_get_or_retrieve") def test__find_owner_account_for_webhook_event_trigger( mock__get_or_retrieve, mock_get_or_retrieve_for_api_key, api_key, stripe_account, has_account_key, ): """ Test that the correct classmethod is invoked with the correct arguments to get the owner account """ # should fake_data have the account key if has_account_key: # fake_data used to invoke _find_owner_account classmethod fake_data = { "id": "test_XXXXXXXX", "livemode": False, "object": "event", "account": stripe_account, } else: # fake_data used to invoke _find_owner_account classmethod fake_data = { "id": "test_XXXXXXXX", "livemode": False, "object": "event", } if api_key is None: # invoke _find_owner_account without the api_key parameter StripeModel._find_owner_account(fake_data) else: # invoke _find_owner_account with the api_key parameter StripeModel._find_owner_account(fake_data, api_key=api_key) if has_account_key and stripe_account: if api_key: mock__get_or_retrieve.assert_called_once_with( id=stripe_account, api_key=api_key ) else: mock__get_or_retrieve.assert_called_once_with( id=stripe_account, api_key=djstripe_settings.STRIPE_SECRET_KEY ) else: if api_key: mock_get_or_retrieve_for_api_key.assert_called_once_with(api_key) else: mock_get_or_retrieve_for_api_key.assert_called_once_with( djstripe_settings.STRIPE_SECRET_KEY ) ================================================ FILE: tests/test_subscription.py ================================================ """ dj-stripe Subscription Model Tests. """ from copy import deepcopy from decimal import Decimal from unittest.mock import PropertyMock, patch import pytest import stripe from django.contrib.auth import get_user_model from django.test import TestCase from django.utils import timezone from stripe.error import InvalidRequestError from djstripe.enums import SubscriptionStatus from djstripe.models import Plan, Product, Subscription from djstripe.models.billing import Invoice from djstripe.settings import djstripe_settings from . import ( FAKE_BALANCE_TRANSACTION, FAKE_CARD, FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CHARGE, FAKE_CHARGE_II, FAKE_CUSTOMER, FAKE_CUSTOMER_II, FAKE_INVOICE, FAKE_INVOICE_II, FAKE_INVOICEITEM, FAKE_PAYMENT_INTENT_I, FAKE_PAYMENT_INTENT_II, FAKE_PAYMENT_METHOD_II, FAKE_PLAN, FAKE_PLAN_II, FAKE_PLAN_METERED, FAKE_PRODUCT, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_CANCELED, FAKE_SUBSCRIPTION_II, FAKE_SUBSCRIPTION_III, FAKE_SUBSCRIPTION_ITEM, FAKE_SUBSCRIPTION_METERED, FAKE_SUBSCRIPTION_MULTI_PLAN, FAKE_SUBSCRIPTION_NOT_PERIOD_CURRENT, FAKE_TAX_RATE_EXAMPLE_1_VAT, AssertStripeFksMixin, datetime_to_unix, ) pytestmark = pytest.mark.django_db # TODO: test with Prices instead of Plans when creating Subscriptions # with Prices is fully supported class SubscriptionStrTest(TestCase): def setUp(self): self.user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) self.customer = FAKE_CUSTOMER_II.create_for_user(self.user) @patch( "stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) def test___str__( self, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION_III) subscription_fake["latest_invoice"] = None # sync subscriptions (to update the changes just made) Subscription.sync_from_stripe_data(subscription_fake) self.assertEqual( str(Subscription.objects.get(id=subscription_fake["id"])), f'', ) class SubscriptionTest(AssertStripeFksMixin, TestCase): @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE) ) def setUp( self, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, customer_retrieve_mock, ): self.user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) self.customer = FAKE_CUSTOMER.create_for_user(self.user) self.default_expected_blank_fks = { "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.Product.default_price", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", } # create latest invoice Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_sync_from_stripe_data( self, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription_fake["pause_collection"] = { "behavior": "keep_as_draft", "resumes_at": 1624553615, } subscription_fake["cancel_at"] = 1624553655 subscription = Subscription.sync_from_stripe_data(subscription_fake) assert subscription self.assertEqual(subscription.default_tax_rates.count(), 1) self.assertEqual( subscription.default_tax_rates.first().id, FAKE_TAX_RATE_EXAMPLE_1_VAT["id"] ) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) self.assertEqual(datetime_to_unix(subscription.cancel_at), 1624553655) self.assertEqual( subscription.pause_collection, subscription_fake["pause_collection"], ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_sync_from_stripe_data_default_source_string( self, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription_fake["default_source"] = FAKE_CARD["id"] subscription = Subscription.sync_from_stripe_data(subscription_fake) assert subscription self.assertEqual(subscription.default_source.id, FAKE_CARD["id"]) # pop out "djstripe.Subscription.default_source" from self.assert_fks expected_blank_fks = deepcopy(self.default_expected_blank_fks) expected_blank_fks.remove("djstripe.Subscription.default_source") self.assert_fks(subscription, expected_blank_fks=expected_blank_fks) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN_II), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Subscription.retrieve", autospec=True, ) def test_sync_items_with_tax_rates( self, subscription_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION_II) subscription_fake["latest_invoice"] = FAKE_INVOICE["id"] subscription_retrieve_mock.return_value = subscription_fake subscription = Subscription.sync_from_stripe_data(subscription_fake) assert subscription self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) self.assertEqual(subscription.default_tax_rates.count(), 0) first_item = subscription.items.first() self.assertEqual(first_item.tax_rates.count(), 1) self.assertEqual( first_item.tax_rates.first().id, FAKE_TAX_RATE_EXAMPLE_1_VAT["id"] ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_is_status_temporarily_current( self, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock ): product = Product.sync_from_stripe_data(deepcopy(FAKE_PRODUCT)) subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription = Subscription.sync_from_stripe_data(subscription_fake) assert subscription subscription.canceled_at = timezone.now() + timezone.timedelta(days=7) subscription.current_period_end = timezone.now() + timezone.timedelta(days=7) subscription.cancel_at_period_end = True subscription.save() self.assertTrue(subscription.is_status_current()) self.assertTrue(subscription.is_status_temporarily_current()) self.assertTrue(subscription.is_valid()) self.assertTrue(subscription in self.customer.active_subscriptions) self.assertTrue(self.customer.is_subscribed_to(product)) self.assertTrue(self.customer.has_any_active_subscription()) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_is_status_temporarily_current_false( self, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription = Subscription.sync_from_stripe_data(subscription_fake) subscription.current_period_end = timezone.now() + timezone.timedelta(days=7) subscription.save() self.assertTrue(subscription.is_status_current()) self.assertFalse(subscription.is_status_temporarily_current()) self.assertTrue(subscription.is_valid()) self.assertTrue(subscription in self.customer.active_subscriptions) self.assertTrue(self.customer.is_subscribed_to(FAKE_PRODUCT["id"])) self.assertTrue(self.customer.has_any_active_subscription()) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_is_status_temporarily_current_false_and_canceled( self, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription = Subscription.sync_from_stripe_data(subscription_fake) assert subscription subscription.status = SubscriptionStatus.canceled subscription.current_period_end = timezone.now() + timezone.timedelta(days=7) subscription.save() self.assertFalse(subscription.is_status_current()) self.assertFalse(subscription.is_status_temporarily_current()) self.assertFalse(subscription.is_valid()) self.assertFalse(subscription in self.customer.active_subscriptions) self.assertFalse(self.customer.is_subscribed_to(FAKE_PRODUCT["id"])) self.assertFalse(self.customer.has_any_active_subscription()) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Subscription.modify", autospec=True, ) @patch("stripe.Subscription.retrieve", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_extend( self, customer_retrieve_mock, subscription_retrieve_mock, subscription_modify_mock, product_retrieve_mock, plan_retrieve_mock, ): current_period_end = timezone.now() - timezone.timedelta(days=20) subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription_fake["current_period_end"] = int(current_period_end.timestamp()) subscription_retrieve_mock.return_value = subscription_fake subscription = Subscription.sync_from_stripe_data(subscription_fake) self.assertFalse(subscription in self.customer.active_subscriptions) self.assertEqual(self.customer.active_subscriptions.count(), 0) # Extend the Subscription by 30 days delta = timezone.timedelta(days=30) subscription_updated = deepcopy(subscription_fake) subscription_updated["trial_end"] = int( (current_period_end + delta).timestamp() ) subscription_modify_mock.return_value = subscription_updated extended_subscription = subscription.extend(delta) product = Product.sync_from_stripe_data(deepcopy(FAKE_PRODUCT)) self.assertNotEqual(None, extended_subscription.trial_end) self.assertTrue(self.customer.is_subscribed_to(product)) self.assertTrue(self.customer.has_any_active_subscription()) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_extend_negative_delta( self, customer_retrieve_mock, subscription_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION_NOT_PERIOD_CURRENT) subscription = Subscription.sync_from_stripe_data(subscription_fake) with self.assertRaises(ValueError): subscription.extend(timezone.timedelta(days=-30)) self.assertFalse(self.customer.is_subscribed_to(FAKE_PRODUCT["id"])) self.assertFalse(self.customer.has_any_active_subscription()) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Subscription.modify", autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_extend_with_trial( self, customer_retrieve_mock, subscription_retrieve_mock, subscription_modify_mock, product_retrieve_mock, plan_retrieve_mock, ): trial_end = timezone.now() + timezone.timedelta(days=5) subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription = Subscription.sync_from_stripe_data(subscription_fake) subscription.trial_end = trial_end subscription.save() # Extend the Subscription by 30 days delta = timezone.timedelta(days=30) subscription_updated = deepcopy(subscription_fake) subscription_updated["trial_end"] = int((trial_end + delta).timestamp()) subscription_modify_mock.return_value = subscription_updated extended_subscription = subscription.extend(delta) new_trial_end = subscription.trial_end + delta self.assertEqual( new_trial_end.replace(microsecond=0), extended_subscription.trial_end ) self.assertTrue(self.customer.is_subscribed_to(FAKE_PRODUCT["id"])) self.assertTrue(self.customer.has_any_active_subscription()) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Subscription.modify", autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_update( self, customer_retrieve_mock, subscription_retrieve_mock, subscription_modify_mock, product_retrieve_mock, plan_retrieve_mock, ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription = Subscription.sync_from_stripe_data(subscription_fake) self.assertEqual(1, subscription.quantity) # Update the quantity of the Subscription subscription_updated = deepcopy(FAKE_SUBSCRIPTION) subscription_updated["quantity"] = 4 subscription_modify_mock.return_value = subscription_updated new_subscription = subscription.update(quantity=4) self.assertEqual(4, new_subscription.quantity) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Subscription.modify", autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_update_deprecation_warnings_raised( self, customer_retrieve_mock, subscription_retrieve_mock, subscription_modify_mock, product_retrieve_mock, plan_retrieve_mock, ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription = Subscription.sync_from_stripe_data(subscription_fake) assert subscription self.assertEqual(1, subscription.quantity) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Subscription.modify", autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_update_with_plan_model( self, customer_retrieve_mock, subscription_retrieve_mock, subscription_modify_mock, product_retrieve_mock, plan_retrieve_mock, ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription = Subscription.sync_from_stripe_data(subscription_fake) new_plan = Plan.sync_from_stripe_data(deepcopy(FAKE_PLAN_II)) self.assertEqual(FAKE_PLAN["id"], subscription.plan.id) # Update the Subscription's plan subscription_updated = deepcopy(FAKE_SUBSCRIPTION) subscription_updated["plan"] = deepcopy(FAKE_PLAN_II) subscription_modify_mock.return_value = subscription_updated new_subscription = subscription.update(plan=new_plan) self.assertEqual(FAKE_PLAN_II["id"], new_subscription.plan.id) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) self.assert_fks( new_plan, expected_blank_fks={ "djstripe.Product.default_price", }, ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch("stripe.Subscription.delete", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_cancel_now( self, customer_retrieve_mock, subscription_delete_mock, product_retrieve_mock, plan_retrieve_mock, ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription = Subscription.sync_from_stripe_data(subscription_fake) subscription.current_period_end = timezone.now() + timezone.timedelta(days=7) subscription.save() cancel_timestamp = datetime_to_unix(timezone.now()) canceled_subscription_fake = deepcopy(FAKE_SUBSCRIPTION) canceled_subscription_fake["status"] = SubscriptionStatus.canceled canceled_subscription_fake["canceled_at"] = cancel_timestamp canceled_subscription_fake["ended_at"] = cancel_timestamp subscription_delete_mock.return_value = canceled_subscription_fake self.assertTrue(self.customer.is_subscribed_to(FAKE_PRODUCT["id"])) self.assertEqual(self.customer.active_subscriptions.count(), 1) self.assertTrue(self.customer.has_any_active_subscription()) new_subscription = subscription.cancel(at_period_end=False) self.assertEqual(SubscriptionStatus.canceled, new_subscription.status) self.assertEqual(False, new_subscription.cancel_at_period_end) self.assertEqual(new_subscription.canceled_at, new_subscription.ended_at) self.assertFalse(new_subscription.is_valid()) self.assertFalse(new_subscription.is_status_temporarily_current()) self.assertFalse(new_subscription in self.customer.active_subscriptions) self.assertFalse(self.customer.is_subscribed_to(FAKE_PRODUCT["id"])) self.assertFalse(self.customer.has_any_active_subscription()) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Subscription.modify", autospec=True, ) @patch("stripe.Subscription.delete", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_cancel_at_period_end( self, customer_retrieve_mock, subscription_delete_mock, subscription_modify_mock, product_retrieve_mock, plan_retrieve_mock, ): current_period_end = timezone.now() + timezone.timedelta(days=7) subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription = Subscription.sync_from_stripe_data(subscription_fake) subscription.current_period_end = current_period_end subscription.save() canceled_subscription_fake = deepcopy(FAKE_SUBSCRIPTION) canceled_subscription_fake["current_period_end"] = datetime_to_unix( current_period_end ) canceled_subscription_fake["canceled_at"] = datetime_to_unix(timezone.now()) subscription_delete_mock.return_value = ( canceled_subscription_fake # retrieve().delete() ) self.assertTrue(self.customer.is_subscribed_to(FAKE_PRODUCT["id"])) self.assertTrue(self.customer.has_any_active_subscription()) self.assertEqual(self.customer.active_subscriptions.count(), 1) self.assertTrue(subscription in self.customer.active_subscriptions) # Update the Subscription by cancelling it at the end of the period subscription_updated = deepcopy(canceled_subscription_fake) subscription_updated["cancel_at_period_end"] = True subscription_modify_mock.return_value = subscription_updated new_subscription = subscription.cancel(at_period_end=True) self.assertEqual(self.customer.active_subscriptions.count(), 1) self.assertTrue(new_subscription in self.customer.active_subscriptions) self.assertEqual(SubscriptionStatus.active, new_subscription.status) self.assertEqual(True, new_subscription.cancel_at_period_end) self.assertNotEqual(new_subscription.canceled_at, new_subscription.ended_at) self.assertTrue(new_subscription.is_valid()) self.assertTrue(new_subscription.is_status_temporarily_current()) self.assertTrue(self.customer.is_subscribed_to(FAKE_PRODUCT["id"])) self.assertTrue(self.customer.has_any_active_subscription()) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch("stripe.Subscription.delete", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_cancel_during_trial_sets_at_period_end( self, customer_retrieve_mock, subscription_delete_mock, product_retrieve_mock, plan_retrieve_mock, ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription = Subscription.sync_from_stripe_data(subscription_fake) subscription.trial_end = timezone.now() + timezone.timedelta(days=7) subscription.save() cancel_timestamp = datetime_to_unix(timezone.now()) canceled_subscription_fake = deepcopy(FAKE_SUBSCRIPTION) canceled_subscription_fake["status"] = SubscriptionStatus.canceled canceled_subscription_fake["canceled_at"] = cancel_timestamp canceled_subscription_fake["ended_at"] = cancel_timestamp subscription_delete_mock.return_value = canceled_subscription_fake self.assertTrue(self.customer.is_subscribed_to(FAKE_PRODUCT["id"])) self.assertTrue(self.customer.has_any_active_subscription()) new_subscription = subscription.cancel(at_period_end=False) self.assertEqual(SubscriptionStatus.canceled, new_subscription.status) self.assertEqual(False, new_subscription.cancel_at_period_end) self.assertEqual(new_subscription.canceled_at, new_subscription.ended_at) self.assertFalse(new_subscription.is_valid()) self.assertFalse(self.customer.is_subscribed_to(FAKE_PRODUCT["id"])) self.assertFalse(self.customer.has_any_active_subscription()) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Subscription.modify", autospec=True, ) @patch("stripe.Subscription.retrieve", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_cancel_and_reactivate( self, customer_retrieve_mock, subscription_retrieve_mock, subscription_modify_mock, product_retrieve_mock, plan_retrieve_mock, ): current_period_end = timezone.now() + timezone.timedelta(days=7) subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription = Subscription.sync_from_stripe_data(subscription_fake) assert subscription subscription.current_period_end = current_period_end subscription.save() canceled_subscription_fake = deepcopy(FAKE_SUBSCRIPTION) canceled_subscription_fake["current_period_end"] = datetime_to_unix( current_period_end ) canceled_subscription_fake["canceled_at"] = datetime_to_unix(timezone.now()) subscription_retrieve_mock.return_value = canceled_subscription_fake self.assertTrue(self.customer.is_subscribed_to(FAKE_PRODUCT["id"])) self.assertTrue(self.customer.has_any_active_subscription()) # Update the Subscription by cancelling it at the end of the period subscription_updated = deepcopy(canceled_subscription_fake) subscription_updated["cancel_at_period_end"] = True subscription_modify_mock.return_value = subscription_updated new_subscription = subscription.cancel(at_period_end=True) self.assertEqual(new_subscription.cancel_at_period_end, True) new_subscription.reactivate() subscription_reactivate_fake = deepcopy(FAKE_SUBSCRIPTION) reactivated_subscription = Subscription.sync_from_stripe_data( subscription_reactivate_fake ) self.assertEqual(reactivated_subscription.cancel_at_period_end, False) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) @patch("djstripe.models.Subscription._api_delete", autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_CANCELED), ) def test_cancel_already_canceled( self, subscription_retrieve_mock, product_retrieve_mock, subscription_delete_mock, ): subscription_delete_mock.side_effect = InvalidRequestError( "No such subscription: sub_xxxx", "blah" ) subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription = Subscription.sync_from_stripe_data(subscription_fake) self.assertEqual(Subscription.objects.filter(status="canceled").count(), 0) subscription.cancel(at_period_end=False) self.assertEqual(Subscription.objects.filter(status="canceled").count(), 1) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) @patch("djstripe.models.Subscription._api_delete", autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) def test_cancel_error_in_cancel( self, product_retrieve_mock, subscription_delete_mock ): subscription_delete_mock.side_effect = InvalidRequestError( "Unexpected error", "blah" ) subscription_fake = deepcopy(FAKE_SUBSCRIPTION) subscription = Subscription.sync_from_stripe_data(subscription_fake) with self.assertRaises(InvalidRequestError): subscription.cancel(at_period_end=False) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks ) @patch("stripe.Plan.retrieve", autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, ) def test_sync_multi_plan( self, subscription_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION_MULTI_PLAN) subscription_fake["latest_invoice"] = FAKE_INVOICE["id"] subscription_retrieve_mock.return_value = subscription_fake subscription = Subscription.sync_from_stripe_data(subscription_fake) self.assertIsNone(subscription.plan) self.assertIsNone(subscription.quantity) items = subscription.items.all() self.assertEqual(2, len(items)) # delete pydanny customer as that causes issues with Invoice and Latest_invoice FKs self.customer.delete() self.assert_fks( subscription, expected_blank_fks=( self.default_expected_blank_fks | { "djstripe.Customer.subscriber", "djstripe.Subscription.plan", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.Invoice.charge", "djstripe.Invoice.customer", "djstripe.Invoice.payment_intent", "djstripe.Invoice.subscription", } ), ) @patch("stripe.Plan.retrieve", autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, ) def test_update_multi_plan( self, subscription_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION_MULTI_PLAN) subscription_fake["latest_invoice"] = FAKE_INVOICE["id"] subscription_retrieve_mock.return_value = subscription_fake subscription = Subscription.sync_from_stripe_data(subscription_fake) self.assertIsNone(subscription.plan) self.assertIsNone(subscription.quantity) items = subscription.items.all() self.assertEqual(2, len(items)) # Simulate a webhook received with one plan that has been removed del subscription_fake["items"]["data"][1] subscription_fake["items"]["total_count"] = 1 subscription = Subscription.sync_from_stripe_data(subscription_fake) items = subscription.items.all() self.assertEqual(1, len(items)) # delete pydanny customer as that causes issues with Invoice and Latest_invoice FKs self.customer.delete() self.assert_fks( subscription, expected_blank_fks=( self.default_expected_blank_fks | { "djstripe.Customer.subscriber", "djstripe.Subscription.plan", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.Invoice.charge", "djstripe.Invoice.customer", "djstripe.Invoice.payment_intent", "djstripe.Invoice.subscription", } ), ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.Charge.retrieve", autospec=True, ) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_PAYMENT_METHOD_II), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE_II), autospec=True ) @patch("stripe.Plan.retrieve", autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, ) def test_remove_all_multi_plan( self, subscription_retrieve_mock, subscription_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, invoice_retrieve_mock, invoice_item_retrieve_mock, paymentintent_retrieve_mock, paymentmethod_retrieve_mock, charge_retrieve_mock, balance_transaction_retrieve_mock, ): # delete pydanny customer as that causes issues with Invoice and Latest_invoice FKs self.customer.delete() fake_payment_intent = deepcopy(FAKE_PAYMENT_INTENT_II) fake_payment_intent["invoice"] = FAKE_INVOICE_II["id"] paymentintent_retrieve_mock.return_value = fake_payment_intent fake_subscription = deepcopy(FAKE_SUBSCRIPTION_MULTI_PLAN) fake_subscription["latest_invoice"] = FAKE_INVOICE_II["id"] subscription_retrieve_mock.return_value = fake_subscription fake_charge = deepcopy(FAKE_CHARGE_II) fake_charge["payment_method"] = FAKE_PAYMENT_METHOD_II["id"] charge_retrieve_mock.return_value = fake_charge # create invoice fake_invoice = deepcopy(FAKE_INVOICE_II) Invoice.sync_from_stripe_data(fake_invoice) subscription = Subscription.sync_from_stripe_data(fake_subscription) self.assertIsNone(subscription.plan) self.assertIsNone(subscription.quantity) items = subscription.items.all() self.assertEqual(2, len(items)) # Simulate a webhook received with no more plan del fake_subscription["items"]["data"][1] del fake_subscription["items"]["data"][0] fake_subscription["items"]["total_count"] = 0 subscription = Subscription.sync_from_stripe_data(fake_subscription) items = subscription.items.all() self.assertEqual(0, len(items)) self.assert_fks( subscription, expected_blank_fks=self.default_expected_blank_fks | { "djstripe.Customer.subscriber", "djstripe.Subscription.plan", }, ) @patch("stripe.Plan.retrieve", autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_METERED) ) def test_sync_metered_plan( self, subscription_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): subscription_fake = deepcopy(FAKE_SUBSCRIPTION_METERED) self.assertNotIn( "quantity", subscription_fake["items"]["data"], "Expect Metered plan SubscriptionItem to have no quantity", ) subscription = Subscription.sync_from_stripe_data(subscription_fake) assert subscription items = subscription.items.all() self.assertEqual(1, len(items)) item = items[0] self.assertEqual(subscription.quantity, 1) # Note that subscription.quantity is 1, # but item.quantity isn't set on metered plans self.assertIsNone(item.quantity) self.assertEqual(item.plan.id, FAKE_PLAN_METERED["id"]) self.assert_fks( subscription, expected_blank_fks=( self.default_expected_blank_fks | {"djstripe.Subscription.latest_invoice"} ), ) @patch("stripe.Subscription.list") def test_api_list(self, subscription_list_mock): p = PropertyMock(return_value=deepcopy(FAKE_SUBSCRIPTION)) type(subscription_list_mock).auto_paging_iter = p # invoke Subscription.api_list with status enum populated Subscription.api_list(status=SubscriptionStatus.canceled) subscription_list_mock.assert_called_once_with( status=SubscriptionStatus.canceled, api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) @patch("stripe.Subscription.list") def test_api_list_with_no_status(self, subscription_list_mock): p = PropertyMock(return_value=deepcopy(FAKE_SUBSCRIPTION)) type(subscription_list_mock).auto_paging_iter = p # invoke Subscription.api_list without status enum populated Subscription.api_list() subscription_list_mock.assert_called_once_with( status="all", api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) class TestSubscriptionDecimal: @pytest.mark.parametrize( "inputted,expected", [ (Decimal("1"), Decimal("1.00")), (Decimal("1.5234567"), Decimal("1.52")), (Decimal("0"), Decimal("0.00")), (Decimal("23.2345678"), Decimal("23.23")), ("1", Decimal("1.00")), ("1.5234567", Decimal("1.52")), ("0", Decimal("0.00")), ("23.2345678", Decimal("23.23")), (1, Decimal("1.00")), (1.5234567, Decimal("1.52")), (0, Decimal("0.00")), (23.2345678, Decimal("23.24")), ], ) def test_decimal_application_fee_percent( # noqa: C901 self, inputted, expected, monkeypatch ): fake_subscription = deepcopy(FAKE_SUBSCRIPTION) fake_subscription["application_fee_percent"] = inputted def mock_invoice_get(*args, **kwargs): return FAKE_INVOICE def mock_invoice_item_get(*args, **kwargs): return FAKE_INVOICEITEM def mock_customer_get(*args, **kwargs): return FAKE_CUSTOMER def mock_charge_get(*args, **kwargs): return FAKE_CHARGE def mock_payment_method_get(*args, **kwargs): return FAKE_CARD_AS_PAYMENT_METHOD def mock_payment_intent_get(*args, **kwargs): return FAKE_PAYMENT_INTENT_I def mock_subscription_get(*args, **kwargs): return fake_subscription def mock_subscriptionitem_get(*args, **kwargs): return FAKE_SUBSCRIPTION_ITEM def mock_balance_transaction_get(*args, **kwargs): return FAKE_BALANCE_TRANSACTION def mock_product_get(*args, **kwargs): return FAKE_PRODUCT def mock_plan_get(*args, **kwargs): return FAKE_PLAN # monkeypatch stripe retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Invoice, "retrieve", mock_invoice_get) monkeypatch.setattr(stripe.InvoiceItem, "retrieve", mock_invoice_item_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr( stripe.BalanceTransaction, "retrieve", mock_balance_transaction_get ) monkeypatch.setattr(stripe.Subscription, "retrieve", mock_subscription_get) monkeypatch.setattr( stripe.SubscriptionItem, "retrieve", mock_subscriptionitem_get ) monkeypatch.setattr(stripe.Charge, "retrieve", mock_charge_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) monkeypatch.setattr(stripe.PaymentIntent, "retrieve", mock_payment_intent_get) monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Plan, "retrieve", mock_plan_get) # Create Latest Invoice Invoice.sync_from_stripe_data(FAKE_INVOICE) subscription = Subscription.sync_from_stripe_data(fake_subscription) field_data = subscription.application_fee_percent assert isinstance(field_data, Decimal) assert field_data == expected ================================================ FILE: tests/test_subscription_item.py ================================================ """ dj-stripe SubscriptionItem model tests """ from copy import deepcopy from unittest.mock import patch from django.contrib.auth import get_user_model from django.test import TestCase from djstripe.models import SubscriptionItem from djstripe.models.billing import Invoice from . import ( FAKE_BALANCE_TRANSACTION, FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CHARGE, FAKE_CUSTOMER, FAKE_CUSTOMER_II, FAKE_INVOICE, FAKE_INVOICEITEM, FAKE_PAYMENT_INTENT_I, FAKE_PLAN, FAKE_PLAN_II, FAKE_PLAN_METERED, FAKE_PRICE, FAKE_PRICE_II, FAKE_PRICE_METERED, FAKE_PRODUCT, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_II, FAKE_SUBSCRIPTION_ITEM, FAKE_SUBSCRIPTION_ITEM_METERED, FAKE_SUBSCRIPTION_ITEM_MULTI_PLAN, FAKE_SUBSCRIPTION_ITEM_TAX_RATES, FAKE_SUBSCRIPTION_METERED, FAKE_SUBSCRIPTION_MULTI_PLAN, FAKE_TAX_RATE_EXAMPLE_1_VAT, AssertStripeFksMixin, ) class SubscriptionItemTest(AssertStripeFksMixin, TestCase): @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE) ) def setUp( self, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, customer_retrieve_mock, ): self.user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) self.customer = FAKE_CUSTOMER.create_for_user(self.user) self.default_expected_blank_fks = { "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Product.default_price", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", } # create latest invoice Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) @patch( "stripe.Price.retrieve", return_value=deepcopy(FAKE_PRICE_METERED), autospec=True, ) @patch( "stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN_METERED), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_METERED), autospec=True, ) def test_sync_from_stripe_data_metered_subscription( self, subscription_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, price_retrieve_mock, ): subscription_item_fake = deepcopy(FAKE_SUBSCRIPTION_ITEM_METERED) subscription_item = SubscriptionItem.sync_from_stripe_data( subscription_item_fake ) self.assertEqual(subscription_item.id, FAKE_SUBSCRIPTION_ITEM_METERED["id"]) self.assertEqual( subscription_item.plan.id, FAKE_SUBSCRIPTION_ITEM_METERED["plan"]["id"] ) self.assertEqual( subscription_item.price.id, FAKE_SUBSCRIPTION_ITEM_METERED["price"]["id"] ) self.assertEqual( subscription_item.subscription.id, FAKE_SUBSCRIPTION_ITEM_METERED["subscription"], ) self.assert_fks( subscription_item, expected_blank_fks=( self.default_expected_blank_fks | {"djstripe.Subscription.latest_invoice"} ), ) @patch( "stripe.Price.retrieve", return_value=deepcopy(FAKE_PRICE_II), autospec=True, ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN_II), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Subscription.retrieve", autospec=True, ) def test_sync_items_with_tax_rates( self, subscription_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, price_retrieve_mock, ): fake_subscription = deepcopy(FAKE_SUBSCRIPTION_II) fake_subscription["latest_invoice"] = FAKE_INVOICE["id"] subscription_retrieve_mock.return_value = fake_subscription subscription_item_fake = deepcopy(FAKE_SUBSCRIPTION_ITEM_TAX_RATES) subscription_item = SubscriptionItem.sync_from_stripe_data( subscription_item_fake ) self.assertEqual(subscription_item.id, FAKE_SUBSCRIPTION_ITEM_TAX_RATES["id"]) self.assertEqual( subscription_item.plan.id, FAKE_SUBSCRIPTION_ITEM_TAX_RATES["plan"]["id"] ) self.assertEqual( subscription_item.price.id, FAKE_SUBSCRIPTION_ITEM_TAX_RATES["price"]["id"] ) self.assertEqual( subscription_item.subscription.id, FAKE_SUBSCRIPTION_ITEM_TAX_RATES["subscription"], ) self.assert_fks( subscription_item, expected_blank_fks=( self.default_expected_blank_fks | { "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", } ), ) self.assertEqual(subscription_item.tax_rates.count(), 1) self.assertEqual( subscription_item.tax_rates.first().id, FAKE_TAX_RATE_EXAMPLE_1_VAT["id"] ) @patch( "stripe.Price.retrieve", side_effect=[deepcopy(FAKE_PRICE), deepcopy(FAKE_PRICE_II)], autospec=True, ) @patch( "stripe.Plan.retrieve", side_effect=[deepcopy(FAKE_PLAN), deepcopy(FAKE_PLAN_II)], autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.Subscription.retrieve", autospec=True, ) def test_sync_multi_plan_subscription( self, subscription_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, price_retrieve_mock, ): fake_subscription = deepcopy(FAKE_SUBSCRIPTION_MULTI_PLAN) fake_subscription["latest_invoice"] = FAKE_INVOICE["id"] subscription_retrieve_mock.return_value = fake_subscription subscription_item_fake = deepcopy(FAKE_SUBSCRIPTION_ITEM_MULTI_PLAN) subscription_item = SubscriptionItem.sync_from_stripe_data( subscription_item_fake ) self.assertEqual(subscription_item.id, FAKE_SUBSCRIPTION_ITEM_MULTI_PLAN["id"]) self.assertEqual( subscription_item.plan.id, FAKE_SUBSCRIPTION_ITEM_MULTI_PLAN["plan"]["id"] ) self.assertEqual( subscription_item.price.id, FAKE_SUBSCRIPTION_ITEM_MULTI_PLAN["price"]["id"] ) self.assertEqual( subscription_item.subscription.id, FAKE_SUBSCRIPTION_ITEM_MULTI_PLAN["subscription"], ) # delete pydanny customer as that causes issues with Invoice and Latest_invoice FKs self.customer.delete() self.assert_fks( subscription_item, expected_blank_fks=( self.default_expected_blank_fks | { "djstripe.Customer.subscriber", "djstripe.Subscription.plan", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.Invoice.charge", "djstripe.Invoice.customer", "djstripe.Invoice.payment_intent", "djstripe.Invoice.subscription", } ), ) ================================================ FILE: tests/test_subscription_schedule.py ================================================ """ dj-stripe SubscriptionSchedule model tests. """ from copy import deepcopy from unittest.mock import patch import stripe from django.contrib.auth import get_user_model from django.test import TestCase from djstripe.enums import SubscriptionScheduleStatus from djstripe.models import Invoice, SubscriptionSchedule from djstripe.settings import djstripe_settings from . import ( FAKE_BALANCE_TRANSACTION, FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CHARGE, FAKE_CUSTOMER, FAKE_INVOICE, FAKE_INVOICEITEM, FAKE_PAYMENT_INTENT_I, FAKE_PLAN, FAKE_PRODUCT, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_ITEM, FAKE_SUBSCRIPTION_SCHEDULE, AssertStripeFksMixin, datetime_to_unix, ) class SubscriptionScheduleTest(AssertStripeFksMixin, TestCase): @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION), autospec=True, ) @patch("stripe.Charge.retrieve", return_value=deepcopy(FAKE_CHARGE), autospec=True) @patch( "stripe.PaymentMethod.retrieve", return_value=deepcopy(FAKE_CARD_AS_PAYMENT_METHOD), autospec=True, ) @patch( "stripe.PaymentIntent.retrieve", return_value=deepcopy(FAKE_PAYMENT_INTENT_I), autospec=True, ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", autospec=True, return_value=deepcopy(FAKE_INVOICE) ) def setUp( self, invoice_retrieve_mock, invoice_item_retrieve_mock, product_retrieve_mock, payment_intent_retrieve_mock, paymentmethod_card_retrieve_mock, charge_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, balance_transaction_retrieve_mock, customer_retrieve_mock, ): self.user = get_user_model().objects.create_user( username="pydanny", email="pydanny@gmail.com" ) self.customer = FAKE_CUSTOMER.create_for_user(self.user) self.default_expected_blank_fks = { "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Charge.application_fee", "djstripe.Charge.dispute", "djstripe.Charge.latest_upcominginvoice (related name)", "djstripe.Charge.on_behalf_of", "djstripe.Charge.source_transfer", "djstripe.Charge.transfer", "djstripe.PaymentIntent.on_behalf_of", "djstripe.PaymentIntent.payment_method", "djstripe.PaymentIntent.upcominginvoice (related name)", "djstripe.Product.default_price", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", "djstripe.SubscriptionSchedule.released_subscription", } # create latest invoice Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_sync_from_stripe_data( self, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): canceled_schedule_fake = deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) canceled_schedule_fake["canceled_at"] = 1624553655 canceled_schedule_fake["status"] = SubscriptionScheduleStatus.canceled schedule = SubscriptionSchedule.sync_from_stripe_data(canceled_schedule_fake) self.assert_fks(schedule, expected_blank_fks=self.default_expected_blank_fks) self.assertEqual(datetime_to_unix(schedule.canceled_at), 1624553655) self.assertEqual(schedule.subscription.id, FAKE_SUBSCRIPTION["id"]) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test___str__( self, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): schedule = SubscriptionSchedule.sync_from_stripe_data( deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) ) self.assertEqual(f"", str(schedule)) self.assert_fks(schedule, expected_blank_fks=self.default_expected_blank_fks) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_release( self, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): schedule = SubscriptionSchedule.sync_from_stripe_data( deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) ) with patch.object( stripe.SubscriptionSchedule, "release", return_value=FAKE_SUBSCRIPTION_SCHEDULE, ) as patched__api_update: schedule.release() patched__api_update.assert_called_once_with( FAKE_SUBSCRIPTION_SCHEDULE["id"], api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_account=schedule.djstripe_owner_account.id, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_cancel( self, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): schedule = SubscriptionSchedule.sync_from_stripe_data( deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) ) with patch.object( stripe.SubscriptionSchedule, "cancel", return_value=FAKE_SUBSCRIPTION_SCHEDULE, ) as patched__api_update: schedule.cancel() patched__api_update.assert_called_once_with( FAKE_SUBSCRIPTION_SCHEDULE["id"], api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_account=schedule.djstripe_owner_account.id, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) @patch("stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN), autospec=True) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_update( self, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): schedule = SubscriptionSchedule.sync_from_stripe_data( deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) ) with patch.object( stripe.SubscriptionSchedule, "modify", return_value=FAKE_SUBSCRIPTION_SCHEDULE, ) as patched__api_update: schedule.update() patched__api_update.assert_called_once_with( FAKE_SUBSCRIPTION_SCHEDULE["id"], api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_account=schedule.djstripe_owner_account.id, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) ================================================ FILE: tests/test_sync.py ================================================ """ dj-stripe Sync Method Tests. """ import contextlib from copy import deepcopy from unittest.mock import patch from django.contrib.auth import get_user_model from django.test.testcases import TestCase from stripe.error import InvalidRequestError from djstripe.models import Customer from djstripe.sync import sync_subscriber from . import FAKE_CUSTOMER @contextlib.contextmanager def capture_stdout(): import sys from io import StringIO old_stdout = sys.stdout sys.stdout = StringIO() try: yield sys.stdout finally: sys.stdout = old_stdout class TestSyncSubscriber(TestCase): def setUp(self): self.user = get_user_model().objects.create_user( username="testuser", email="test@example.com", password="123" ) @patch("djstripe.models.Customer._sync_charges", autospec=True) @patch("djstripe.models.Customer._sync_invoices", autospec=True) @patch("djstripe.models.Customer._sync_subscriptions", autospec=True) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) @patch( "stripe.Customer.create", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_sync_success( self, stripe_customer_create_mock, api_retrieve_mock, _sync_subscriptions_mock, _sync_invoices_mock, _sync_charges_mock, ): sync_subscriber(self.user) self.assertEqual(1, Customer.objects.count()) self.assertEqual( FAKE_CUSTOMER["id"], Customer.objects.get(subscriber=self.user).api_retrieve()["id"], ) _sync_subscriptions_mock.assert_called_once_with(Customer.objects.first()) _sync_invoices_mock.assert_called_once_with(Customer.objects.first()) _sync_charges_mock.assert_called_once_with(Customer.objects.first()) @patch( "djstripe.models.Customer.api_retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.Customer.create", return_value=deepcopy(FAKE_CUSTOMER), autospec=True ) def test_sync_fail(self, stripe_customer_create_mock, api_retrieve_mock): api_retrieve_mock.side_effect = InvalidRequestError("No such customer:", "blah") with capture_stdout() as stdout: sync_subscriber(self.user) self.assertEqual("ERROR: No such customer:", stdout.getvalue().strip()) ================================================ FILE: tests/test_tax_code.py ================================================ """ dj-stripe TaxCode Model Tests. """ from copy import deepcopy import pytest from django.test import TestCase from djstripe.models import TaxCode from tests import FAKE_TAX_CODE pytestmark = pytest.mark.django_db class TaxCodeTest(TestCase): def test_sync_from_stripe_data(self): tax_code = TaxCode.sync_from_stripe_data(deepcopy(FAKE_TAX_CODE)) assert tax_code assert tax_code.id == FAKE_TAX_CODE["id"] def test___str__(self): tax_code = TaxCode.sync_from_stripe_data(deepcopy(FAKE_TAX_CODE)) assert str(tax_code) == "General - Tangible Goods: txcd_99999999" ================================================ FILE: tests/test_tax_id.py ================================================ """ dj-stripe TaxId model tests """ from copy import deepcopy from unittest.mock import PropertyMock, patch import pytest from django.test.testcases import TestCase from djstripe import enums from djstripe.models import Customer, TaxId from djstripe.settings import djstripe_settings from . import FAKE_CUSTOMER, FAKE_TAX_ID, AssertStripeFksMixin pytestmark = pytest.mark.django_db class TestTaxIdStr(TestCase): @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.Customer.retrieve_tax_id", return_value=deepcopy(FAKE_TAX_ID), autospec=True, ) def test___str__( self, tax_id_retrieve_mock, customer_retrieve_mock, ): tax_id = TaxId.sync_from_stripe_data(FAKE_TAX_ID) self.assertEqual( str(tax_id), f"{enums.TaxIdType.humanize(FAKE_TAX_ID['type'])} {FAKE_TAX_ID['value']} ({FAKE_TAX_ID['verification']['status']})", ) class TestTransfer(AssertStripeFksMixin, TestCase): @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.Customer.retrieve_tax_id", return_value=deepcopy(FAKE_TAX_ID), autospec=True, ) def test_sync_from_stripe_data( self, tax_id_retrieve_mock, customer_retrieve_mock, ): tax_id = TaxId.sync_from_stripe_data(FAKE_TAX_ID) assert tax_id assert tax_id.id == FAKE_TAX_ID["id"] assert tax_id.customer.id == FAKE_CUSTOMER["id"] self.assert_fks( tax_id, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.subscriber", }, ) # we are returning any value for the Customer.objects.get as we only need to avoid the Customer.DoesNotExist error @patch( "djstripe.models.core.Customer.objects.get", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.Customer.create_tax_id", return_value=deepcopy(FAKE_TAX_ID), autospec=True, ) def test__api_create( self, tax_id_create_mock, customer_get_mock, ): STRIPE_DATA = TaxId._api_create( id=FAKE_CUSTOMER["id"], type=FAKE_TAX_ID["type"], value=FAKE_TAX_ID["value"] ) assert STRIPE_DATA == FAKE_TAX_ID tax_id_create_mock.assert_called_once_with( id=FAKE_CUSTOMER["id"], type=FAKE_TAX_ID["type"], value=FAKE_TAX_ID["value"], api_key=djstripe_settings.STRIPE_SECRET_KEY, ) # we are returning any value for the Customer.objects.get as we only need to avoid the Customer.DoesNotExist error @patch( "djstripe.models.core.Customer.objects.get", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.Customer.create_tax_id", return_value=deepcopy(FAKE_TAX_ID), autospec=True, ) def test__api_create_no_id_kwarg( self, tax_id_create_mock, customer_get_mock, ): with pytest.raises(KeyError) as exc: TaxId._api_create( FAKE_CUSTOMER["id"], type=FAKE_TAX_ID["type"], value=FAKE_TAX_ID["value"], ) assert "Customer Object ID is missing" in str(exc.value) @patch( "stripe.Customer.create_tax_id", return_value=deepcopy(FAKE_TAX_ID), autospec=True, ) def test__api_create_no_customer( self, tax_id_create_mock, ): with pytest.raises(Customer.DoesNotExist): TaxId._api_create( id=FAKE_CUSTOMER["id"], type=FAKE_TAX_ID["type"], value=FAKE_TAX_ID["value"], ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER), autospec=True, ) @patch( "stripe.Customer.retrieve_tax_id", return_value=deepcopy(FAKE_TAX_ID), autospec=True, ) def test_api_retrieve( self, tax_id_retrieve_mock, customer_retrieve_mock, ): tax_id = TaxId.sync_from_stripe_data(FAKE_TAX_ID) assert tax_id tax_id.api_retrieve() assert tax_id.djstripe_owner_account tax_id_retrieve_mock.assert_called_once_with( id=FAKE_CUSTOMER["id"], nested_id=FAKE_TAX_ID["id"], expand=[], stripe_account=tax_id.djstripe_owner_account.id, stripe_version=djstripe_settings.STRIPE_API_VERSION, api_key=djstripe_settings.STRIPE_SECRET_KEY, ) @patch( "stripe.Customer.list_tax_ids", autospec=True, ) def test_api_list( self, tax_id_list_mock, ): p = PropertyMock(return_value=deepcopy(FAKE_TAX_ID)) type(tax_id_list_mock).auto_paging_iter = p TaxId.api_list(id=FAKE_CUSTOMER["id"]) tax_id_list_mock.assert_called_once_with( id=FAKE_CUSTOMER["id"], api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) ================================================ FILE: tests/test_tax_rates.py ================================================ """ dj-stripe TaxRate Model Tests. """ from copy import deepcopy from decimal import Decimal import pytest from django.test import TestCase from djstripe.models import TaxRate from tests import FAKE_TAX_RATE_EXAMPLE_1_VAT pytestmark = pytest.mark.django_db class TaxRateTest(TestCase): def test_sync_from_stripe_data(self): tax_rate = TaxRate.sync_from_stripe_data(deepcopy(FAKE_TAX_RATE_EXAMPLE_1_VAT)) self.assertEqual( FAKE_TAX_RATE_EXAMPLE_1_VAT["id"], tax_rate.id, ) def test___str__(self): tax_rate = TaxRate.sync_from_stripe_data(deepcopy(FAKE_TAX_RATE_EXAMPLE_1_VAT)) self.assertEqual( f"{FAKE_TAX_RATE_EXAMPLE_1_VAT['display_name']} at {FAKE_TAX_RATE_EXAMPLE_1_VAT['percentage']:.4f}%", str(tax_rate), ) class TestTaxRateDecimal: @pytest.mark.parametrize( "inputted,expected", [ (Decimal("1"), Decimal("1.0000")), (Decimal("1.5234567"), Decimal("1.5235")), (Decimal("0"), Decimal("0.0000")), (Decimal("23.2345678"), Decimal("23.2346")), ("1", Decimal("1.0000")), ("1.5234567", Decimal("1.5235")), ("0", Decimal("0.0000")), ("23.2345678", Decimal("23.2346")), (1, Decimal("1.0000")), (1.5234567, Decimal("1.5235")), (0, Decimal("0.0000")), (23.2345678, Decimal("23.2346")), ], ) def test_decimal_tax_percent(self, inputted, expected): fake_tax_rate = deepcopy(FAKE_TAX_RATE_EXAMPLE_1_VAT) fake_tax_rate["percentage"] = inputted tax_rate = TaxRate.sync_from_stripe_data(fake_tax_rate) field_data = tax_rate.percentage assert isinstance(field_data, Decimal) assert field_data == expected ================================================ FILE: tests/test_transfer.py ================================================ """ dj-stripe Transfer model tests """ from copy import deepcopy from unittest.mock import patch import pytest from django.test.testcases import TestCase from djstripe.models import Transfer from . import ( FAKE_BALANCE_TRANSACTION_II, FAKE_STANDARD_ACCOUNT, FAKE_TRANSFER, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db def FAKE_TRANSFER_COMPLETE_REVERSAL(): data = deepcopy(FAKE_TRANSFER) data["reversed"] = True data["amount_reversed"] = data["amount"] return data def FAKE_TRANSFER_PARTIAL_REVERSAL(): data = deepcopy(FAKE_TRANSFER) assert data["amount"] > 1 data["amount_reversed"] = data["amount"] - 1 return data class TestTransferStr: @pytest.mark.parametrize( "fake_transfer_data", [ deepcopy(FAKE_TRANSFER), FAKE_TRANSFER_COMPLETE_REVERSAL(), FAKE_TRANSFER_PARTIAL_REVERSAL(), ], ) @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_STANDARD_ACCOUNT), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION_II), autospec=True, ) @patch("stripe.Transfer.retrieve", autospec=True) def test___str__( self, transfer_retrieve_mock, balance_transaction_retrieve_mock, account_retrieve_mock, transfer__attach_object_post_save_hook_mock, fake_transfer_data, ): transfer_retrieve_mock.return_value = fake_transfer_data transfer = Transfer.sync_from_stripe_data(fake_transfer_data) if fake_transfer_data["reversed"]: assert "$1.00 USD Reversed" == str(transfer) elif fake_transfer_data["amount_reversed"]: assert "$1.00 USD Partially Reversed" == str(transfer) else: assert "$1.00 USD" == str(transfer) class TestTransfer(AssertStripeFksMixin, TestCase): @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_STANDARD_ACCOUNT), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION_II), autospec=True, ) @patch( "stripe.Transfer.retrieve", return_value=deepcopy(FAKE_TRANSFER), autospec=True ) def test_sync_from_stripe_data( self, transfer_retrieve_mock, balance_transaction_retrieve_mock, account_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): transfer = Transfer.sync_from_stripe_data(deepcopy(FAKE_TRANSFER)) balance_transaction_retrieve_mock.assert_not_called() transfer_retrieve_mock.assert_not_called() assert ( transfer.balance_transaction.id == FAKE_TRANSFER["balance_transaction"]["id"] ) # assert transfer.destination.id == FAKE_TRANSFER["destination"] assert transfer.destination == FAKE_TRANSFER["destination"] self.assert_fks(transfer, expected_blank_fks="") @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_STANDARD_ACCOUNT), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION_II), autospec=True, ) @patch( "stripe.Transfer.retrieve", return_value=deepcopy(FAKE_TRANSFER), autospec=True ) def test_fee( self, transfer_retrieve_mock, balance_transaction_retrieve_mock, account_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): transfer = Transfer.sync_from_stripe_data(deepcopy(FAKE_TRANSFER)) assert transfer.fee == FAKE_BALANCE_TRANSACTION_II["fee"] assert transfer.fee == transfer.balance_transaction.fee @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_STANDARD_ACCOUNT), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION_II), autospec=True, ) @patch( "stripe.Transfer.retrieve", return_value=deepcopy(FAKE_TRANSFER), autospec=True ) def test_get_stripe_dashboard_url( self, transfer_retrieve_mock, balance_transaction_retrieve_mock, account_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): transfer = Transfer.sync_from_stripe_data(deepcopy(FAKE_TRANSFER)) assert transfer.get_stripe_dashboard_url() == ( f"{transfer._get_base_stripe_dashboard_url()}" f"connect/{transfer.stripe_dashboard_item_name}/{transfer.id}" ) ================================================ FILE: tests/test_transfer_reversal.py ================================================ """ dj-stripe TransferReversal model tests """ from copy import deepcopy from unittest.mock import PropertyMock, patch import pytest from django.test.testcases import TestCase from djstripe.models import Transfer, TransferReversal from djstripe.settings import djstripe_settings from . import ( FAKE_BALANCE_TRANSACTION_II, FAKE_PLATFORM_ACCOUNT, FAKE_TRANSFER, FAKE_TRANSFER_WITH_1_REVERSAL, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class TestTransferReversalStr(TestCase): @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_PLATFORM_ACCOUNT), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION_II), autospec=True, ) @patch( "stripe.Transfer.retrieve_reversal", autospec=True, return_value=deepcopy(FAKE_TRANSFER_WITH_1_REVERSAL), ) def test___str__( self, transfer_reversal_retrieve_mock, balance_transaction_retrieve_mock, account_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): transfer_reversal = TransferReversal.sync_from_stripe_data( deepcopy(FAKE_TRANSFER_WITH_1_REVERSAL["reversals"]["data"][0]) ) self.assertEqual(str(f"{transfer_reversal.transfer}"), str(transfer_reversal)) class TestTransfer(AssertStripeFksMixin, TestCase): @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_PLATFORM_ACCOUNT), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION_II), autospec=True, ) @patch( "stripe.Transfer.retrieve_reversal", autospec=True, return_value=deepcopy(FAKE_TRANSFER_WITH_1_REVERSAL["reversals"]["data"][0]), ) def test_sync_from_stripe_data( self, transfer_reversal_retrieve_mock, balance_transaction_retrieve_mock, account_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): transfer_reversal = TransferReversal.sync_from_stripe_data( deepcopy(FAKE_TRANSFER_WITH_1_REVERSAL["reversals"]["data"][0]) ) balance_transaction_retrieve_mock.assert_not_called() transfer_reversal_retrieve_mock.assert_not_called() assert ( transfer_reversal.balance_transaction.id == FAKE_TRANSFER["balance_transaction"]["id"] ) assert ( transfer_reversal.transfer.id == FAKE_TRANSFER_WITH_1_REVERSAL["reversals"]["data"][0]["transfer"]["id"] ) self.assert_fks(transfer_reversal, expected_blank_fks="") @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_PLATFORM_ACCOUNT), autospec=True, ) @patch( "stripe.BalanceTransaction.retrieve", return_value=deepcopy(FAKE_BALANCE_TRANSACTION_II), autospec=True, ) @patch( "stripe.Transfer.retrieve_reversal", autospec=True, return_value=deepcopy(FAKE_TRANSFER_WITH_1_REVERSAL), ) def test_api_retrieve( self, transfer_reversal_retrieve_mock, balance_transaction_retrieve_mock, account_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): transfer_reversal = TransferReversal.sync_from_stripe_data( deepcopy(FAKE_TRANSFER_WITH_1_REVERSAL["reversals"]["data"][0]) ) transfer_reversal.api_retrieve() transfer_reversal_retrieve_mock.assert_called_once_with( id=FAKE_TRANSFER_WITH_1_REVERSAL["id"], nested_id=FAKE_TRANSFER_WITH_1_REVERSAL["reversals"]["data"][0]["id"], api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, expand=["balance_transaction", "transfer"], stripe_account=transfer_reversal.djstripe_owner_account.id, ) @patch.object(Transfer, "_attach_objects_post_save_hook") # we are returning any value for the Transfer.objects.get as we only need to avoid the Transfer.DoesNotExist error @patch( "djstripe.models.connect.Transfer.objects.get", return_value=deepcopy(FAKE_TRANSFER), ) @patch( "stripe.Transfer.create_reversal", autospec=True, return_value=deepcopy(FAKE_TRANSFER_WITH_1_REVERSAL), ) def test__api_create( self, transfer_reversal_create_mock, transfer_get_mock, transfer__attach_object_post_save_hook_mock, ): TransferReversal._api_create( id=FAKE_TRANSFER_WITH_1_REVERSAL["reversals"]["data"][0]["transfer"]["id"] ) transfer_reversal_create_mock.assert_called_once_with( id=FAKE_TRANSFER_WITH_1_REVERSAL["reversals"]["data"][0]["transfer"]["id"], api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) @patch("stripe.Transfer.list_reversals", autospec=True) def test_api_list(self, transfer_reversal_list_mock): p = PropertyMock(return_value=deepcopy(FAKE_TRANSFER_WITH_1_REVERSAL)) type(transfer_reversal_list_mock).auto_paging_iter = p TransferReversal.api_list( id=FAKE_TRANSFER_WITH_1_REVERSAL["reversals"]["data"][0]["transfer"]["id"] ) transfer_reversal_list_mock.assert_called_once_with( id=FAKE_TRANSFER_WITH_1_REVERSAL["reversals"]["data"][0]["transfer"]["id"], api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) def test_is_valid_object(self): assert TransferReversal.is_valid_object( deepcopy(FAKE_TRANSFER_WITH_1_REVERSAL["reversals"]["data"][0]) ) ================================================ FILE: tests/test_usage_record.py ================================================ """ dj-stripe UsageRecord model tests """ from copy import deepcopy from unittest.mock import patch import pytest from django.test.testcases import TestCase from djstripe.models.billing import UsageRecord from djstripe.settings import djstripe_settings from . import ( FAKE_CUSTOMER_II, FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE, FAKE_PLAN_METERED, FAKE_PRODUCT, FAKE_SUBSCRIPTION_ITEM, FAKE_USAGE_RECORD, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class TestUsageRecord(AssertStripeFksMixin, TestCase): @patch( "stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN_METERED), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE), autospec=True, ) def test_sync_from_stripe_data( self, subscription_retrieve_mock, subscription_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): fake_usage_data = deepcopy(FAKE_USAGE_RECORD) usage_record = UsageRecord.sync_from_stripe_data(fake_usage_data) assert usage_record self.assertEqual(usage_record.id, fake_usage_data["id"]) self.assertEqual( usage_record.subscription_item.id, fake_usage_data["subscription_item"] ) self.assert_fks( usage_record, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.subscriber", "djstripe.Product.default_price", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", "djstripe.Subscription.latest_invoice", }, ) @patch( "stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN_METERED), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE), autospec=True, ) def test___str__( self, subscription_retrieve_mock, subscription_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): fake_usage_data = deepcopy(FAKE_USAGE_RECORD) usage_record = UsageRecord.sync_from_stripe_data(fake_usage_data) assert usage_record self.assertEqual( str(usage_record), f"Usage for {str(usage_record.subscription_item)} ({fake_usage_data['action']}) is {fake_usage_data['quantity']}", ) @patch( "stripe.SubscriptionItem.create_usage_record", autospec=True, return_value=deepcopy(FAKE_USAGE_RECORD), ) @patch( "djstripe.models.billing.UsageRecord.sync_from_stripe_data", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), ) @patch( "djstripe.models.billing.SubscriptionItem.objects.get", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), ) @patch( "stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN_METERED), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE), autospec=True, ) def test__api_create( self, subscription_retrieve_mock, subscription_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, subcription_item_get_mock, sync_from_stripe_data_mock, usage_record_creation_mock, ): fake_usage_data = deepcopy(FAKE_USAGE_RECORD) UsageRecord._api_create(id=fake_usage_data["subscription_item"]) # assert usage_record_creation_mock was called as expected usage_record_creation_mock.assert_called_once_with( id=fake_usage_data["subscription_item"], api_key=djstripe_settings.STRIPE_SECRET_KEY, ) # assert usage_record_creation_mock was called as expected sync_from_stripe_data_mock.assert_called_once_with( fake_usage_data, api_key=djstripe_settings.STRIPE_SECRET_KEY ) ================================================ FILE: tests/test_usage_record_summary.py ================================================ """ dj-stripe UsageRecordSummary model tests """ from copy import deepcopy from unittest.mock import PropertyMock, call, patch import pytest from django.test.testcases import TestCase from djstripe.models.billing import UsageRecordSummary from djstripe.settings import djstripe_settings from . import ( FAKE_CUSTOMER_II, FAKE_INVOICE_METERED_SUBSCRIPTION, FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE, FAKE_INVOICEITEM, FAKE_INVOICEITEM_II, FAKE_PLAN_METERED, FAKE_PRODUCT, FAKE_SUBSCRIPTION_ITEM, FAKE_USAGE_RECORD_SUMMARY, AssertStripeFksMixin, ) pytestmark = pytest.mark.django_db class TestUsageRecordSummary(AssertStripeFksMixin, TestCase): @patch( "stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN_METERED), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE), autospec=True, ) def test_sync_from_stripe_data_with_null_invoice( self, subscription_retrieve_mock, subscription_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): fake_usage_data = deepcopy(FAKE_USAGE_RECORD_SUMMARY) usage_record_summary = UsageRecordSummary.sync_from_stripe_data( fake_usage_data["data"][0] ) self.assertEqual(usage_record_summary.id, fake_usage_data["data"][0]["id"]) self.assertEqual( usage_record_summary.subscription_item.id, fake_usage_data["data"][0]["subscription_item"], ) self.assert_fks( usage_record_summary, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.subscriber", "djstripe.Product.default_price", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", "djstripe.Subscription.latest_invoice", "djstripe.UsageRecordSummary.invoice", }, ) @patch( "stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN_METERED), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM_II), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE_METERED_SUBSCRIPTION), autospec=True, ) def test_sync_from_stripe_data( self, invoice_retrieve_mock, invoice_item_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): fake_usage_data = deepcopy(FAKE_USAGE_RECORD_SUMMARY) usage_record_summary = UsageRecordSummary.sync_from_stripe_data( fake_usage_data["data"][1] ) self.assertEqual(usage_record_summary.id, fake_usage_data["data"][1]["id"]) self.assertEqual( usage_record_summary.subscription_item.id, fake_usage_data["data"][1]["subscription_item"], ) self.assert_fks( usage_record_summary, expected_blank_fks={ "djstripe.Customer.coupon", "djstripe.Customer.default_payment_method", "djstripe.Customer.subscriber", "djstripe.Product.default_price", "djstripe.Subscription.default_payment_method", "djstripe.Subscription.default_source", "djstripe.Subscription.pending_setup_intent", "djstripe.Subscription.schedule", "djstripe.Subscription.latest_invoice", "djstripe.Invoice.default_payment_method", "djstripe.Invoice.default_source", "djstripe.Invoice.payment_intent", "djstripe.Invoice.charge", }, ) # assert invoice_retrieve_mock was called like so: invoice_retrieve_mock.assert_has_calls( [ call( id=FAKE_INVOICE_METERED_SUBSCRIPTION["id"], api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=["discounts"], stripe_account=None, stripe_version="2020-08-27", ), call( id="in_16af5A2eZvKYlo2CJjANLL81", api_key=djstripe_settings.STRIPE_SECRET_KEY, expand=["discounts"], stripe_account=None, stripe_version="2020-08-27", ), ] ) @patch( "stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN_METERED), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE), autospec=True, ) @patch( "stripe.InvoiceItem.retrieve", return_value=deepcopy(FAKE_INVOICEITEM), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE_METERED_SUBSCRIPTION), autospec=True, ) def test___str__( self, invoice_retrieve_mock, invoice_item_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, ): fake_usage_data = deepcopy(FAKE_USAGE_RECORD_SUMMARY) usage_record_summary = UsageRecordSummary.sync_from_stripe_data( fake_usage_data["data"][1] ) self.assertEqual( str(usage_record_summary), f"Usage Summary for {str(usage_record_summary.subscription_item)} ({str(usage_record_summary.invoice)}) is {fake_usage_data['data'][1]['total_usage']}", ) @patch( "stripe.SubscriptionItem.list_usage_record_summaries", autospec=True, ) @patch( "djstripe.models.billing.SubscriptionItem.objects.get", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), ) @patch( "stripe.Plan.retrieve", return_value=deepcopy(FAKE_PLAN_METERED), autospec=True ) @patch( "stripe.Product.retrieve", return_value=deepcopy(FAKE_PRODUCT), autospec=True ) @patch( "stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II), autospec=True, ) @patch( "stripe.SubscriptionItem.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_ITEM), autospec=True, ) @patch( "stripe.Subscription.retrieve", return_value=deepcopy(FAKE_INVOICE_METERED_SUBSCRIPTION_USAGE), autospec=True, ) @patch( "stripe.Invoice.retrieve", return_value=deepcopy(FAKE_INVOICE_METERED_SUBSCRIPTION), autospec=True, ) def test_api_list( self, invoice_retrieve_mock, subscription_retrieve_mock, subscription_item_retrieve_mock, customer_retrieve_mock, product_retrieve_mock, plan_retrieve_mock, subcription_item_get_mock, usage_record_list_mock, ): p = PropertyMock(return_value=deepcopy(FAKE_USAGE_RECORD_SUMMARY)) type(usage_record_list_mock).auto_paging_iter = p fake_usage_data = deepcopy(FAKE_USAGE_RECORD_SUMMARY) UsageRecordSummary.api_list(id=fake_usage_data["data"][1]["subscription_item"]) # assert usage_record_list_mock was called as expected usage_record_list_mock.assert_called_once_with( id=fake_usage_data["data"][1]["subscription_item"], api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=djstripe_settings.STRIPE_API_VERSION, ) ================================================ FILE: tests/test_utils.py ================================================ """ dj-stripe Utilities Tests. """ import time from datetime import datetime from decimal import Decimal from unittest import skipIf from unittest.mock import patch from django.test import TestCase from django.test.utils import override_settings from djstripe.utils import ( convert_tstamp, get_friendly_currency_amount, get_supported_currency_choices, get_timezone_utc, ) TZ_IS_UTC = time.tzname == ("UTC", "UTC") class TestTimestampConversion(TestCase): def test_conversion(self): stamp = convert_tstamp(1365567407) self.assertEqual( stamp, datetime(2013, 4, 10, 4, 16, 47, tzinfo=get_timezone_utc()) ) # NOTE: These next two tests will fail if your system clock is not in UTC # Travis CI is, and coverage is good, so... @skipIf(not TZ_IS_UTC, "Skipped because timezone is not UTC.") @override_settings(USE_TZ=False) def test_conversion_no_tz(self): stamp = convert_tstamp(1365567407) self.assertEqual(stamp, datetime(2013, 4, 10, 4, 16, 47)) class TestGetSupportedCurrencyChoices(TestCase): @patch( "stripe.CountrySpec.retrieve", return_value={"supported_payment_currencies": ["usd", "cad", "eur"]}, ) @patch( "stripe.Account.retrieve", return_value={"country": "US"}, autospec=True, ) def test_get_choices( self, stripe_account_retrieve_mock, stripe_countryspec_retrieve_mock ): # Simple test to test sure that at least one currency choice tuple is returned. currency_choices = get_supported_currency_choices(None) stripe_account_retrieve_mock.assert_called_once_with(api_key=None) stripe_countryspec_retrieve_mock.assert_called_once_with("US", api_key=None) self.assertGreaterEqual( len(currency_choices), 1, "Currency choices pull returned an empty list." ) self.assertEqual( tuple, type(currency_choices[0]), "Currency choices are not tuples." ) self.assertIn(("usd", "USD"), currency_choices, "USD not in currency choices.") class TestUtils(TestCase): def test_get_friendly_currency_amount(self): self.assertEqual( get_friendly_currency_amount(Decimal("1.001"), "usd"), "$1.00 USD" ) self.assertEqual( get_friendly_currency_amount(Decimal("10"), "usd"), "$10.00 USD" ) self.assertEqual( get_friendly_currency_amount(Decimal("10.50"), "usd"), "$10.50 USD" ) self.assertEqual( get_friendly_currency_amount(Decimal("10.51"), "cad"), "$10.51 CAD" ) self.assertEqual( get_friendly_currency_amount(Decimal("9.99"), "eur"), "€9.99 EUR" ) ================================================ FILE: tests/test_views.py ================================================ """ dj-stripe Views Tests. """ from copy import deepcopy import pytest import stripe from django.apps import apps from django.contrib import messages from django.contrib.admin import helpers, site from django.contrib.messages.middleware import MessageMiddleware from django.contrib.sessions.middleware import SessionMiddleware from django.test.client import RequestFactory from django.urls import reverse from pytest_django.asserts import assertContains from djstripe import models, utils from djstripe.admin.views import ConfirmCustomAction from tests import ( FAKE_BALANCE_TRANSACTION, FAKE_CARD_AS_PAYMENT_METHOD, FAKE_CHARGE, FAKE_CUSTOMER, FAKE_INVOICE, FAKE_INVOICEITEM, FAKE_PAYMENT_INTENT_I, FAKE_PLAN, FAKE_PRODUCT, FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_ITEM, FAKE_SUBSCRIPTION_SCHEDULE, ) from .fields.models import CustomActionModel pytestmark = pytest.mark.django_db class TestConfirmCustomActionView: # the 4 models that do not inherit from StripeModel and hence # do not inherit from StripeModelAdmin ignore_models = [ "WebhookEventTrigger", "WebhookEndpoint", "IdempotencyKey", "APIKey", ] kwargs_called_with = {} # to get around Session/MessageMiddleware Deprecation Warnings def dummy_get_response(self, request): return None @pytest.mark.parametrize( "action_name", [ "_resync_instances", "_sync_all_instances", "_cancel", "_release_subscription_schedule", "_cancel_subscription_schedule", ], ) def test_get_form_kwargs(self, action_name, admin_user, monkeypatch): model = CustomActionModel # monkeypatch utils.get_model def mock_get_model(*args, **kwargs): return model monkeypatch.setattr(utils, "get_model", mock_get_model) kwargs = { "action_name": action_name, "model_name": model.__name__.lower(), } # get the custom action POST url change_url = reverse("admin:djstripe_custom_action", kwargs=kwargs) request = RequestFactory().get(change_url) # add the admin user to the mocked request request.user = admin_user # Add the session/message middleware to the request SessionMiddleware(self.dummy_get_response).process_request(request) MessageMiddleware(self.dummy_get_response).process_request(request) view = ConfirmCustomAction() view.setup(request, **kwargs) # Invoke the get_form_kwargs method form_kwargs = view.get_form_kwargs() assert form_kwargs.get("model_name") == model.__name__.lower() assert form_kwargs.get("action_name") == action_name @pytest.mark.parametrize( "action_name", [ "_resync_instances", "_sync_all_instances", "_cancel", "_release_subscription_schedule", "_cancel_subscription_schedule", ], ) @pytest.mark.parametrize("djstripe_owner_account_exists", [False, True]) def test_form_valid(self, djstripe_owner_account_exists, action_name, monkeypatch): model = CustomActionModel # create instance to be used in the Django Admin Action instance = model.objects.create(id="test") if djstripe_owner_account_exists: account_instance = models.Account.objects.first() instance.djstripe_owner_account = account_instance instance.save() data = { "action": action_name, helpers.ACTION_CHECKBOX_NAME: [instance.pk], } if action_name == "_sync_all_instances": data[helpers.ACTION_CHECKBOX_NAME] = ["_sync_all_instances"] # monkeypatch utils.get_model and def mock_get_model(*args, **kwargs): return model monkeypatch.setattr(utils, "get_model", mock_get_model) kwargs = { "action_name": action_name, "model_name": model.__name__.lower(), } # get the custom action POST url change_url = reverse("admin:djstripe_custom_action", kwargs=kwargs) request = RequestFactory().post(change_url, data=data, follow=True) # Add the session/message middleware to the request SessionMiddleware(self.dummy_get_response).process_request(request) MessageMiddleware(self.dummy_get_response).process_request(request) view = ConfirmCustomAction() view.setup(request, **kwargs) # monkeypatch Request Handler def mock_request_handler(*args, **kwargs): return None monkeypatch.setattr(view, action_name, mock_request_handler) # get the form form = view.get_form() # Ensure form is valid assert form.is_valid() # Invoke form_valid() response = view.form_valid(form) # assert user redirected to the correct url assert response.status_code == 302 assert response.url == "/admin/fields/customactionmodel/" @pytest.mark.parametrize( "action_name", [ "_resync_instances", "_sync_all_instances", "_cancel", "_release_subscription_schedule", "_cancel_subscription_schedule", ], ) @pytest.mark.parametrize("djstripe_owner_account_exists", [False, True]) def test_form_invalid( self, djstripe_owner_account_exists, action_name, monkeypatch ): model = CustomActionModel # create instance to be used in the Django Admin Action instance = model.objects.create(id="test") if djstripe_owner_account_exists: account_instance = models.Account.objects.first() instance.djstripe_owner_account = account_instance instance.save() data = { "action": action_name, } # monkeypatch utils.get_model and def mock_get_model(*args, **kwargs): return model monkeypatch.setattr(utils, "get_model", mock_get_model) kwargs = { "action_name": action_name, "model_name": model.__name__.lower(), } # get the custom action POST url change_url = reverse("admin:djstripe_custom_action", kwargs=kwargs) request = RequestFactory().post(change_url, data=data, follow=True) # Add the session/message middleware to the request SessionMiddleware(self.dummy_get_response).process_request(request) MessageMiddleware(self.dummy_get_response).process_request(request) view = ConfirmCustomAction() view.setup(request, **kwargs) # get the form form = view.get_form() # Ensure form is not valid assert not form.is_valid() # Invoke form_invalid() response = view.form_invalid(form) # assert user got redirected to the action page with the error rendered assertContains( response, '
      \n
    • * This field is required.
    • \n
    ', html=True, ) @pytest.mark.parametrize("fake_selected_pks", [None, [1, 2]]) def test__sync_all_instances(self, fake_selected_pks, monkeypatch): app_label = "djstripe" app_config = apps.get_app_config(app_label) all_models_lst = app_config.get_models() for model in all_models_lst: if model in site._registry.keys() and ( model.__name__ == "WebhookEndpoint" or model.__name__ not in self.ignore_models ): # monkeypatch utils.get_model def mock_get_model(*args, **kwargs): return model monkeypatch.setattr(utils, "get_model", mock_get_model) data = {"action": "_sync_all_instances"} if fake_selected_pks is not None: data[helpers.ACTION_CHECKBOX_NAME] = fake_selected_pks kwargs = { "action_name": "_sync_all_instances", "model_name": model.__name__.lower(), } # get the custom action POST url change_url = reverse( "admin:djstripe_custom_action", kwargs=kwargs, ) request = RequestFactory().post(change_url, data=data, follow=True) # Add the session/message middleware to the request SessionMiddleware(self.dummy_get_response).process_request(request) MessageMiddleware(self.dummy_get_response).process_request(request) view = ConfirmCustomAction() view.setup(request, **kwargs) # Invoke the Custom Actions view._sync_all_instances(request, model.objects.none()) # assert correct Success messages are emitted messages_sent_dictionary = { m.message: m.level_tag for m in messages.get_messages(request) } # assert correct success message was emitted assert ( messages_sent_dictionary.get("Successfully Synced All Instances") == "success" ) @pytest.mark.parametrize("djstripe_owner_account_exists", [False, True]) def test__resync_instances(self, djstripe_owner_account_exists, monkeypatch): model = CustomActionModel # create instance to be used in the Django Admin Action instance = model.objects.create(id="test") if djstripe_owner_account_exists: account_instance = models.Account.objects.first() instance.djstripe_owner_account = account_instance instance.save() data = { "action": "_resync_instances", helpers.ACTION_CHECKBOX_NAME: [instance.pk], } # monkeypatch instance.api_retrieve, instance.__class__.sync_from_stripe_data, and app_config.get_model def mock_instance_api_retrieve(*args, **keywargs): self.kwargs_called_with = keywargs def mock_instance_sync_from_stripe_data(*args, **kwargs): pass def mock_get_model(*args, **kwargs): return model monkeypatch.setattr(model, "api_retrieve", mock_instance_api_retrieve) monkeypatch.setattr( model, "sync_from_stripe_data", mock_instance_sync_from_stripe_data, ) monkeypatch.setattr(utils, "get_model", mock_get_model) kwargs = { "action_name": "_resync_instances", "model_name": model.__name__.lower(), } # get the custom action POST url change_url = reverse("admin:djstripe_custom_action", kwargs=kwargs) request = RequestFactory().post(change_url, data=data, follow=True) # Add the session/message middleware to the request SessionMiddleware(self.dummy_get_response).process_request(request) MessageMiddleware(self.dummy_get_response).process_request(request) view = ConfirmCustomAction() view.setup(request, **kwargs) # Invoke the Custom Actions view._resync_instances(request, [instance]) # assert correct Success messages are emitted messages_sent_dictionary = { m.message: m.level_tag for m in messages.get_messages(request) } # assert correct success message was emitted assert ( messages_sent_dictionary.get(f"Successfully Synced: {instance}") == "success" ) if djstripe_owner_account_exists: # assert in case djstripe_owner_account exists that kwargs are not empty assert self.kwargs_called_with == { "stripe_account": instance.djstripe_owner_account.id, "api_key": instance.default_api_key, } else: # assert in case djstripe_owner_account does not exist that kwargs are empty assert self.kwargs_called_with == {} def test__resync_instances_stripe_permission_error(self, monkeypatch): model = CustomActionModel # create instance to be used in the Django Admin Action instance = model.objects.create(id="test") data = { "action": "_resync_instances", helpers.ACTION_CHECKBOX_NAME: [instance.pk], } # monkeypatch instance.api_retrieve and app_config.get_model def mock_instance_api_retrieve(*args, **kwargs): raise stripe.error.PermissionError("some random error message") def mock_get_model(*args, **kwargs): return model monkeypatch.setattr(instance, "api_retrieve", mock_instance_api_retrieve) monkeypatch.setattr(utils, "get_model", mock_get_model) kwargs = { "action_name": "_resync_instances", "model_name": model.__name__.lower(), } # get the custom action POST url change_url = reverse("admin:djstripe_custom_action", kwargs=kwargs) request = RequestFactory().post(change_url, data=data, follow=True) # Add the session/message middleware to the request SessionMiddleware(self.dummy_get_response).process_request(request) MessageMiddleware(self.dummy_get_response).process_request(request) view = ConfirmCustomAction() view.setup(request, **kwargs) # Invoke the Custom Actions view._resync_instances(request, [instance]) # assert correct Success messages are emitted messages_sent_dictionary = { m.message.user_message: m.level_tag for m in messages.get_messages(request) } # assert correct success message was emitted assert messages_sent_dictionary.get("some random error message") == "warning" def test__resync_instances_stripe_invalid_request_error(self, monkeypatch): model = CustomActionModel # create instance to be used in the Django Admin Action instance = model.objects.create(id="test") data = { "action": "_resync_instances", helpers.ACTION_CHECKBOX_NAME: [instance.pk], } # monkeypatch instance.api_retrieve and app_config.get_model def mock_instance_api_retrieve(*args, **kwargs): raise stripe.error.InvalidRequestError({}, "some random error message") def mock_get_model(*args, **kwargs): return model monkeypatch.setattr(instance, "api_retrieve", mock_instance_api_retrieve) monkeypatch.setattr(utils, "get_model", mock_get_model) kwargs = { "action_name": "_resync_instances", "model_name": model.__name__.lower(), } # get the custom action POST url change_url = reverse("admin:djstripe_custom_action", kwargs=kwargs) request = RequestFactory().post(change_url, data=data, follow=True) # Add the session/message middleware to the request SessionMiddleware(self.dummy_get_response).process_request(request) MessageMiddleware(self.dummy_get_response).process_request(request) view = ConfirmCustomAction() view.setup(request, **kwargs) with pytest.raises(stripe.error.InvalidRequestError) as exc_info: # Invoke the Custom Actions view._resync_instances(request, [instance]) assert str(exc_info.value.param) == "some random error message" def test__cancel_subscription_instances( self, monkeypatch, ): def mock_invoice_get(*args, **kwargs): return FAKE_INVOICE def mock_invoice_item_get(*args, **kwargs): return FAKE_INVOICEITEM def mock_customer_get(*args, **kwargs): return FAKE_CUSTOMER def mock_charge_get(*args, **kwargs): return FAKE_CHARGE def mock_payment_method_get(*args, **kwargs): return FAKE_CARD_AS_PAYMENT_METHOD def mock_payment_intent_get(*args, **kwargs): return FAKE_PAYMENT_INTENT_I def mock_subscriptionitem_get(*args, **kwargs): return FAKE_SUBSCRIPTION_ITEM def mock_subscription_get(*args, **kwargs): return FAKE_SUBSCRIPTION def mock_balance_transaction_get(*args, **kwargs): return FAKE_BALANCE_TRANSACTION def mock_product_get(*args, **kwargs): return FAKE_PRODUCT def mock_plan_get(*args, **kwargs): return FAKE_PLAN # monkeypatch stripe retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Invoice, "retrieve", mock_invoice_get) monkeypatch.setattr(stripe.InvoiceItem, "retrieve", mock_invoice_item_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr( stripe.BalanceTransaction, "retrieve", mock_balance_transaction_get ) monkeypatch.setattr(stripe.Subscription, "retrieve", mock_subscription_get) monkeypatch.setattr( stripe.SubscriptionItem, "retrieve", mock_subscriptionitem_get ) # si_HXZCDv9ixoUB5u monkeypatch.setattr(stripe.Charge, "retrieve", mock_charge_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) monkeypatch.setattr(stripe.PaymentIntent, "retrieve", mock_payment_intent_get) monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Plan, "retrieve", mock_plan_get) model = models.Subscription # Create Latest Invoice models.Invoice.sync_from_stripe_data(FAKE_INVOICE) subscription_fake = deepcopy(FAKE_SUBSCRIPTION) instance = model.sync_from_stripe_data(subscription_fake) # monkeypatch subscription.cancel() def mock_subscription_cancel(*args, **keywargs): return instance monkeypatch.setattr(instance, "cancel", mock_subscription_cancel) data = {"action": "_cancel", helpers.ACTION_CHECKBOX_NAME: [instance.pk]} kwargs = { "action_name": "_cancel", "model_name": model.__name__.lower(), } # get the custom action POST url change_url = reverse("admin:djstripe_custom_action", kwargs=kwargs) request = RequestFactory().post(change_url, data=data, follow=True) # Add the session/message middleware to the request SessionMiddleware(self.dummy_get_response).process_request(request) MessageMiddleware(self.dummy_get_response).process_request(request) view = ConfirmCustomAction() view.setup(request, **kwargs) # Invoke the Custom Actions view._cancel(request, [instance]) # assert correct Success messages are emitted messages_sent_dictionary = { m.message: m.level_tag for m in messages.get_messages(request) } # assert correct success message was emitted assert ( messages_sent_dictionary.get(f"Successfully Canceled: {instance}") == "success" ) def test__cancel_subscription_instances_stripe_invalid_request_error( self, monkeypatch, ): def mock_invoice_get(*args, **kwargs): return FAKE_INVOICE def mock_invoice_item_get(*args, **kwargs): return FAKE_INVOICEITEM def mock_customer_get(*args, **kwargs): return FAKE_CUSTOMER def mock_charge_get(*args, **kwargs): return FAKE_CHARGE def mock_payment_method_get(*args, **kwargs): return FAKE_CARD_AS_PAYMENT_METHOD def mock_payment_intent_get(*args, **kwargs): return FAKE_PAYMENT_INTENT_I def mock_subscriptionitem_get(*args, **kwargs): return FAKE_SUBSCRIPTION_ITEM def mock_subscription_get(*args, **kwargs): return FAKE_SUBSCRIPTION def mock_balance_transaction_get(*args, **kwargs): return FAKE_BALANCE_TRANSACTION def mock_product_get(*args, **kwargs): return FAKE_PRODUCT def mock_plan_get(*args, **kwargs): return FAKE_PLAN # monkeypatch stripe retrieve calls to return # the desired json response. monkeypatch.setattr(stripe.Invoice, "retrieve", mock_invoice_get) monkeypatch.setattr(stripe.InvoiceItem, "retrieve", mock_invoice_item_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr( stripe.BalanceTransaction, "retrieve", mock_balance_transaction_get ) monkeypatch.setattr(stripe.Subscription, "retrieve", mock_subscription_get) monkeypatch.setattr( stripe.SubscriptionItem, "retrieve", mock_subscriptionitem_get ) monkeypatch.setattr(stripe.Charge, "retrieve", mock_charge_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) monkeypatch.setattr(stripe.PaymentIntent, "retrieve", mock_payment_intent_get) monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Plan, "retrieve", mock_plan_get) model = models.Subscription # Create Latest Invoice models.Invoice.sync_from_stripe_data(FAKE_INVOICE) subscription_fake = deepcopy(FAKE_SUBSCRIPTION) instance = model.sync_from_stripe_data(subscription_fake) # monkeypatch subscription.cancel() def mock_subscription_cancel(*args, **keywargs): raise stripe.error.InvalidRequestError({}, "some random error message") monkeypatch.setattr(instance, "cancel", mock_subscription_cancel) data = {"action": "_cancel", helpers.ACTION_CHECKBOX_NAME: [instance.pk]} kwargs = { "action_name": "_cancel", "model_name": model.__name__.lower(), } # get the custom action POST url change_url = reverse("admin:djstripe_custom_action", kwargs=kwargs) request = RequestFactory().post(change_url, data=data, follow=True) # Add the session/message middleware to the request SessionMiddleware(self.dummy_get_response).process_request(request) MessageMiddleware(self.dummy_get_response).process_request(request) view = ConfirmCustomAction() view.setup(request, **kwargs) with pytest.warns(None, match=r"some random error message"): # Invoke the Custom Actions view._cancel(request, [instance]) def test__release_subscription_schedule( self, monkeypatch, ): def mock_balance_transaction_get(*args, **kwargs): return FAKE_BALANCE_TRANSACTION def mock_subscriptionitem_get(*args, **kwargs): return FAKE_SUBSCRIPTION_ITEM def mock_subscription_get(*args, **kwargs): return FAKE_SUBSCRIPTION def mock_charge_get(*args, **kwargs): return FAKE_CHARGE def mock_payment_method_get(*args, **kwargs): return FAKE_CARD_AS_PAYMENT_METHOD def mock_payment_intent_get(*args, **kwargs): return FAKE_PAYMENT_INTENT_I def mock_product_get(*args, **kwargs): return FAKE_PRODUCT def mock_invoice_get(*args, **kwargs): return FAKE_INVOICE def mock_invoice_item_get(*args, **kwargs): return FAKE_INVOICEITEM def mock_customer_get(*args, **kwargs): return FAKE_CUSTOMER def mock_plan_get(*args, **kwargs): return FAKE_PLAN # monkeypatch stripe retrieve calls to return # the desired json response. monkeypatch.setattr( stripe.BalanceTransaction, "retrieve", mock_balance_transaction_get ) monkeypatch.setattr(stripe.Subscription, "retrieve", mock_subscription_get) monkeypatch.setattr( stripe.SubscriptionItem, "retrieve", mock_subscriptionitem_get ) monkeypatch.setattr(stripe.Charge, "retrieve", mock_charge_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) monkeypatch.setattr(stripe.PaymentIntent, "retrieve", mock_payment_intent_get) monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Invoice, "retrieve", mock_invoice_get) monkeypatch.setattr(stripe.InvoiceItem, "retrieve", mock_invoice_item_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr(stripe.Plan, "retrieve", mock_plan_get) # create latest invoice models.Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) model = models.SubscriptionSchedule subscription_schedule_fake = deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) instance = model.sync_from_stripe_data(subscription_schedule_fake) # monkeypatch subscription_schedule.release() def mock_subscription_schedule_release(*args, **keywargs): return instance monkeypatch.setattr(instance, "release", mock_subscription_schedule_release) data = { "action": "_release_subscription_schedule", helpers.ACTION_CHECKBOX_NAME: [instance.pk], } kwargs = { "action_name": "_release_subscription_schedule", "model_name": model.__name__.lower(), } # get the custom action POST url change_url = reverse("admin:djstripe_custom_action", kwargs=kwargs) request = RequestFactory().post(change_url, data=data, follow=True) # Add the session/message middleware to the request SessionMiddleware(self.dummy_get_response).process_request(request) MessageMiddleware(self.dummy_get_response).process_request(request) view = ConfirmCustomAction() view.setup(request, **kwargs) # Invoke the Custom Actions view._release_subscription_schedule(request, [instance]) # assert correct Success messages are emitted messages_sent_dictionary = { m.message: m.level_tag for m in messages.get_messages(request) } # assert correct success message was emitted assert ( messages_sent_dictionary.get(f"Successfully Released: {instance}") == "success" ) def test__cancel_subscription_schedule( self, monkeypatch, ): def mock_balance_transaction_get(*args, **kwargs): return FAKE_BALANCE_TRANSACTION def mock_subscriptionitem_get(*args, **kwargs): return FAKE_SUBSCRIPTION_ITEM def mock_subscription_get(*args, **kwargs): return FAKE_SUBSCRIPTION def mock_charge_get(*args, **kwargs): return FAKE_CHARGE def mock_payment_method_get(*args, **kwargs): return FAKE_CARD_AS_PAYMENT_METHOD def mock_payment_intent_get(*args, **kwargs): return FAKE_PAYMENT_INTENT_I def mock_product_get(*args, **kwargs): return FAKE_PRODUCT def mock_invoice_get(*args, **kwargs): return FAKE_INVOICE def mock_invoice_item_get(*args, **kwargs): return FAKE_INVOICEITEM def mock_customer_get(*args, **kwargs): return FAKE_CUSTOMER def mock_plan_get(*args, **kwargs): return FAKE_PLAN # monkeypatch stripe retrieve calls to return # the desired json response. monkeypatch.setattr( stripe.BalanceTransaction, "retrieve", mock_balance_transaction_get ) monkeypatch.setattr(stripe.Subscription, "retrieve", mock_subscription_get) monkeypatch.setattr( stripe.SubscriptionItem, "retrieve", mock_subscriptionitem_get ) monkeypatch.setattr(stripe.Charge, "retrieve", mock_charge_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) monkeypatch.setattr(stripe.PaymentIntent, "retrieve", mock_payment_intent_get) monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Invoice, "retrieve", mock_invoice_get) monkeypatch.setattr(stripe.InvoiceItem, "retrieve", mock_invoice_item_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr(stripe.Plan, "retrieve", mock_plan_get) # create latest invoice models.Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) model = models.SubscriptionSchedule subscription_schedule_fake = deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) instance = model.sync_from_stripe_data(subscription_schedule_fake) # monkeypatch subscription_schedule.cancel() def mock_subscription_schedule_cancel(*args, **keywargs): return instance monkeypatch.setattr(instance, "cancel", mock_subscription_schedule_cancel) data = { "action": "_cancel_subscription_schedule", helpers.ACTION_CHECKBOX_NAME: [instance.pk], } kwargs = { "action_name": "_cancel_subscription_schedule", "model_name": model.__name__.lower(), } # get the custom action POST url change_url = reverse("admin:djstripe_custom_action", kwargs=kwargs) request = RequestFactory().post(change_url, data=data, follow=True) # Add the session/message middleware to the request SessionMiddleware(self.dummy_get_response).process_request(request) MessageMiddleware(self.dummy_get_response).process_request(request) view = ConfirmCustomAction() view.setup(request, **kwargs) # Invoke the Custom Actions view._cancel_subscription_schedule(request, [instance]) # assert correct Success messages are emitted messages_sent_dictionary = { m.message: m.level_tag for m in messages.get_messages(request) } # assert correct success message was emitted assert ( messages_sent_dictionary.get(f"Successfully Canceled: {instance}") == "success" ) def test__release_subscription_schedule_stripe_invalid_request_error( self, monkeypatch, ): def mock_balance_transaction_get(*args, **kwargs): return FAKE_BALANCE_TRANSACTION def mock_subscriptionitem_get(*args, **kwargs): return FAKE_SUBSCRIPTION_ITEM def mock_subscription_get(*args, **kwargs): return FAKE_SUBSCRIPTION def mock_charge_get(*args, **kwargs): return FAKE_CHARGE def mock_payment_method_get(*args, **kwargs): return FAKE_CARD_AS_PAYMENT_METHOD def mock_payment_intent_get(*args, **kwargs): return FAKE_PAYMENT_INTENT_I def mock_product_get(*args, **kwargs): return FAKE_PRODUCT def mock_invoice_get(*args, **kwargs): return FAKE_INVOICE def mock_invoice_item_get(*args, **kwargs): return FAKE_INVOICEITEM def mock_customer_get(*args, **kwargs): return FAKE_CUSTOMER def mock_plan_get(*args, **kwargs): return FAKE_PLAN # monkeypatch stripe retrieve calls to return # the desired json response. monkeypatch.setattr( stripe.BalanceTransaction, "retrieve", mock_balance_transaction_get ) monkeypatch.setattr(stripe.Subscription, "retrieve", mock_subscription_get) monkeypatch.setattr( stripe.SubscriptionItem, "retrieve", mock_subscriptionitem_get ) monkeypatch.setattr(stripe.Charge, "retrieve", mock_charge_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) monkeypatch.setattr(stripe.PaymentIntent, "retrieve", mock_payment_intent_get) monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Invoice, "retrieve", mock_invoice_get) monkeypatch.setattr(stripe.InvoiceItem, "retrieve", mock_invoice_item_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr(stripe.Plan, "retrieve", mock_plan_get) # create latest invoice models.Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) model = models.SubscriptionSchedule subscription_schedule_fake = deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) instance = model.sync_from_stripe_data(subscription_schedule_fake) # monkeypatch subscription_schedule.release() def mock_subscription_schedule_release(*args, **keywargs): raise stripe.error.InvalidRequestError({}, "some random error message") monkeypatch.setattr(instance, "release", mock_subscription_schedule_release) data = { "action": "_release_subscription_schedule", helpers.ACTION_CHECKBOX_NAME: [instance.pk], } kwargs = { "action_name": "_release_subscription_schedule", "model_name": model.__name__.lower(), } # get the custom action POST url change_url = reverse("admin:djstripe_custom_action", kwargs=kwargs) request = RequestFactory().post(change_url, data=data, follow=True) # Add the session/message middleware to the request SessionMiddleware(self.dummy_get_response).process_request(request) MessageMiddleware(self.dummy_get_response).process_request(request) view = ConfirmCustomAction() view.setup(request, **kwargs) with pytest.warns(None, match=r"some random error message"): # Invoke the Custom Actions view._release_subscription_schedule(request, [instance]) def test__cancel_subscription_schedule_stripe_invalid_request_error( self, monkeypatch, ): def mock_balance_transaction_get(*args, **kwargs): return FAKE_BALANCE_TRANSACTION def mock_subscriptionitem_get(*args, **kwargs): return FAKE_SUBSCRIPTION_ITEM def mock_subscription_get(*args, **kwargs): return FAKE_SUBSCRIPTION def mock_charge_get(*args, **kwargs): return FAKE_CHARGE def mock_payment_method_get(*args, **kwargs): return FAKE_CARD_AS_PAYMENT_METHOD def mock_payment_intent_get(*args, **kwargs): return FAKE_PAYMENT_INTENT_I def mock_product_get(*args, **kwargs): return FAKE_PRODUCT def mock_invoice_get(*args, **kwargs): return FAKE_INVOICE def mock_invoice_item_get(*args, **kwargs): return FAKE_INVOICEITEM def mock_customer_get(*args, **kwargs): return FAKE_CUSTOMER def mock_plan_get(*args, **kwargs): return FAKE_PLAN # monkeypatch stripe retrieve calls to return # the desired json response. monkeypatch.setattr( stripe.BalanceTransaction, "retrieve", mock_balance_transaction_get ) monkeypatch.setattr(stripe.Subscription, "retrieve", mock_subscription_get) monkeypatch.setattr( stripe.SubscriptionItem, "retrieve", mock_subscriptionitem_get ) monkeypatch.setattr(stripe.Charge, "retrieve", mock_charge_get) monkeypatch.setattr(stripe.PaymentMethod, "retrieve", mock_payment_method_get) monkeypatch.setattr(stripe.PaymentIntent, "retrieve", mock_payment_intent_get) monkeypatch.setattr(stripe.Product, "retrieve", mock_product_get) monkeypatch.setattr(stripe.Invoice, "retrieve", mock_invoice_get) monkeypatch.setattr(stripe.InvoiceItem, "retrieve", mock_invoice_item_get) monkeypatch.setattr(stripe.Customer, "retrieve", mock_customer_get) monkeypatch.setattr(stripe.Plan, "retrieve", mock_plan_get) # create latest invoice models.Invoice.sync_from_stripe_data(deepcopy(FAKE_INVOICE)) model = models.SubscriptionSchedule subscription_schedule_fake = deepcopy(FAKE_SUBSCRIPTION_SCHEDULE) instance = model.sync_from_stripe_data(subscription_schedule_fake) # monkeypatch subscription_schedule.cancel() def mock_subscription_schedule_cancel(*args, **keywargs): raise stripe.error.InvalidRequestError({}, "some random error message") monkeypatch.setattr(instance, "cancel", mock_subscription_schedule_cancel) data = { "action": "_cancel_subscription_schedule", helpers.ACTION_CHECKBOX_NAME: [instance.pk], } kwargs = { "action_name": "_cancel_subscription_schedule", "model_name": model.__name__.lower(), } # get the custom action POST url change_url = reverse("admin:djstripe_custom_action", kwargs=kwargs) request = RequestFactory().post(change_url, data=data, follow=True) # Add the session/message middleware to the request SessionMiddleware(self.dummy_get_response).process_request(request) MessageMiddleware(self.dummy_get_response).process_request(request) view = ConfirmCustomAction() view.setup(request, **kwargs) with pytest.warns(None, match=r"some random error message"): # Invoke the Custom Actions view._cancel_subscription_schedule(request, [instance]) ================================================ FILE: tests/test_webhooks.py ================================================ """ dj-stripe Webhook Tests. """ import json import warnings from collections import defaultdict from copy import deepcopy from unittest.mock import Mock, PropertyMock, call, patch from uuid import UUID import pytest from django.http.request import HttpHeaders from django.test import TestCase, override_settings from django.test.client import Client from django.urls import reverse from djstripe import webhooks from djstripe.models import Event, Transfer, WebhookEventTrigger from djstripe.models.webhooks import WebhookEndpoint, get_remote_ip from djstripe.settings import djstripe_settings from djstripe.webhooks import TEST_EVENT_ID, call_handlers, handler, handler_all from . import ( FAKE_CUSTOM_ACCOUNT, FAKE_EVENT_TEST_CHARGE_SUCCEEDED, FAKE_EVENT_TRANSFER_CREATED, FAKE_STANDARD_ACCOUNT, FAKE_TRANSFER, FAKE_WEBHOOK_ENDPOINT_1, ) pytestmark = pytest.mark.django_db def mock_webhook_handler(webhook_event_trigger): webhook_event_trigger.process() class TestWebhookEventTrigger(TestCase): """Test class to test WebhookEventTrigger model and its methods""" def _send_event(self, event_data): return Client().post( reverse("djstripe:webhook"), json.dumps(event_data), content_type="application/json", HTTP_STRIPE_SIGNATURE="PLACEHOLDER", ) def test_webhook_test_event(self): self.assertEqual(WebhookEventTrigger.objects.count(), 0) resp = self._send_event(FAKE_EVENT_TEST_CHARGE_SUCCEEDED) self.assertEqual(resp.status_code, 200) self.assertFalse(Event.objects.filter(id=TEST_EVENT_ID).exists()) self.assertEqual(WebhookEventTrigger.objects.count(), 1) event_trigger = WebhookEventTrigger.objects.first() self.assertTrue(event_trigger.is_test_event) def test___str__(self): self.assertEqual(WebhookEventTrigger.objects.count(), 0) resp = self._send_event(FAKE_EVENT_TEST_CHARGE_SUCCEEDED) self.assertEqual(resp.status_code, 200) self.assertEqual(WebhookEventTrigger.objects.count(), 1) webhookeventtrigger = WebhookEventTrigger.objects.first() self.assertEqual( f"id={webhookeventtrigger.id}, valid={webhookeventtrigger.valid}, processed={webhookeventtrigger.processed}", str(webhookeventtrigger), ) @override_settings(DJSTRIPE_WEBHOOK_VALIDATION="retrieve_event") @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Transfer.retrieve", return_value=deepcopy(FAKE_TRANSFER), autospec=True ) @patch( "stripe.Event.retrieve", return_value=deepcopy(FAKE_EVENT_TRANSFER_CREATED), autospec=True, ) def test_webhook_retrieve_event_fail( self, event_retrieve_mock, transfer_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): invalid_event = deepcopy(FAKE_EVENT_TRANSFER_CREATED) invalid_event["id"] = "evt_invalid" invalid_event["data"]["valid"] = "not really" resp = self._send_event(invalid_event) self.assertEqual(resp.status_code, 400) self.assertFalse(Event.objects.filter(id="evt_invalid").exists()) @patch.object( WebhookEventTrigger.validate, "__defaults__", (None, "whsec_XXXXX", 300, "retrieve_event"), ) @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_STANDARD_ACCOUNT), autospec=True, ) @patch( "stripe.Transfer.retrieve", return_value=deepcopy(FAKE_TRANSFER), autospec=True ) @patch( "stripe.Event.retrieve", return_value=deepcopy(FAKE_EVENT_TRANSFER_CREATED), autospec=True, ) def test_webhook_retrieve_event_pass( self, event_retrieve_mock, transfer_retrieve_mock, account_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): resp = self._send_event(FAKE_EVENT_TRANSFER_CREATED) self.assertEqual(resp.status_code, 200) event_retrieve_mock.assert_called_once_with( api_key=djstripe_settings.STRIPE_SECRET_KEY, stripe_version=FAKE_EVENT_TRANSFER_CREATED["api_version"], id=FAKE_EVENT_TRANSFER_CREATED["id"], ) @override_settings( DJSTRIPE_WEBHOOK_VALIDATION="verify_signature", DJSTRIPE_WEBHOOK_SECRET="whsec_XXXXX", ) @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Transfer.retrieve", return_value=deepcopy(FAKE_TRANSFER), autospec=True ) @patch( "stripe.Event.retrieve", return_value=deepcopy(FAKE_EVENT_TRANSFER_CREATED), autospec=True, ) def test_webhook_invalid_verify_signature_fail( self, event_retrieve_mock, transfer_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): invalid_event = deepcopy(FAKE_EVENT_TRANSFER_CREATED) invalid_event["id"] = "evt_invalid" invalid_event["data"]["valid"] = "not really" resp = self._send_event(invalid_event) self.assertEqual(resp.status_code, 400) self.assertFalse(Event.objects.filter(id="evt_invalid").exists()) @override_settings( DJSTRIPE_WEBHOOK_VALIDATION="verify_signature", DJSTRIPE_WEBHOOK_SECRET="whsec_XXXXX", ) @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.WebhookSignature.verify_header", return_value=True, autospec=True, ) @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_STANDARD_ACCOUNT), autospec=True, ) @patch( "stripe.Transfer.retrieve", return_value=deepcopy(FAKE_TRANSFER), autospec=True ) @patch( "stripe.Event.retrieve", return_value=deepcopy(FAKE_EVENT_TRANSFER_CREATED), autospec=True, ) def test_webhook_verify_signature_pass( self, event_retrieve_mock, transfer_retrieve_mock, account_retrieve_mock, verify_header_mock, transfer__attach_object_post_save_hook_mock, ): resp = self._send_event(FAKE_EVENT_TRANSFER_CREATED) self.assertEqual(resp.status_code, 200) self.assertFalse(Event.objects.filter(id="evt_invalid").exists()) verify_header_mock.assert_called_once_with( json.dumps(FAKE_EVENT_TRANSFER_CREATED), "PLACEHOLDER", djstripe_settings.WEBHOOK_SECRET, djstripe_settings.WEBHOOK_TOLERANCE, ) event_retrieve_mock.assert_not_called() @patch.object( WebhookEventTrigger.validate, "__defaults__", (None, "whsec_XXXXX", 300, None) ) @patch.object(Transfer, "_attach_objects_post_save_hook") @patch("stripe.WebhookSignature.verify_header", autospec=True) @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_STANDARD_ACCOUNT), autospec=True, ) @patch( "stripe.Transfer.retrieve", return_value=deepcopy(FAKE_TRANSFER), autospec=True ) @patch( "stripe.Event.retrieve", return_value=deepcopy(FAKE_EVENT_TRANSFER_CREATED), autospec=True, ) def test_webhook_no_validation_pass( self, event_retrieve_mock, transfer_retrieve_mock, account_retrieve_mock, verify_header_mock, transfer__attach_object_post_save_hook_mock, ): invalid_event = deepcopy(FAKE_EVENT_TRANSFER_CREATED) invalid_event["id"] = "evt_invalid" invalid_event["data"]["valid"] = "not really" # ensure warning is raised with pytest.warns(None, match=r"WEBHOOK VALIDATION is disabled."): resp = self._send_event(invalid_event) self.assertEqual(resp.status_code, 200) self.assertTrue(Event.objects.filter(id="evt_invalid").exists()) event_retrieve_mock.assert_not_called() verify_header_mock.assert_not_called() def test_webhook_no_signature(self): self.assertEqual(WebhookEventTrigger.objects.count(), 0) resp = Client().post( reverse("djstripe:webhook"), "{}", content_type="application/json" ) self.assertEqual(resp.status_code, 400) self.assertEqual(WebhookEventTrigger.objects.count(), 0) def test_webhook_remote_addr_is_none(self): self.assertEqual(WebhookEventTrigger.objects.count(), 0) with warnings.catch_warnings(): warnings.simplefilter("ignore") Client().post( reverse("djstripe:webhook"), "{}", content_type="application/json", HTTP_STRIPE_SIGNATURE="PLACEHOLDER", REMOTE_ADDR=None, ) self.assertEqual(WebhookEventTrigger.objects.count(), 1) event_trigger = WebhookEventTrigger.objects.first() self.assertEqual(event_trigger.remote_ip, "0.0.0.0") def test_webhook_remote_addr_is_empty_string(self): self.assertEqual(WebhookEventTrigger.objects.count(), 0) with warnings.catch_warnings(): warnings.simplefilter("ignore") Client().post( reverse("djstripe:webhook"), "{}", content_type="application/json", HTTP_STRIPE_SIGNATURE="PLACEHOLDER", REMOTE_ADDR="", ) self.assertEqual(WebhookEventTrigger.objects.count(), 1) event_trigger = WebhookEventTrigger.objects.first() self.assertEqual(event_trigger.remote_ip, "0.0.0.0") @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "djstripe.models.WebhookEventTrigger.validate", return_value=True, autospec=True ) @patch("djstripe.models.WebhookEventTrigger.process", autospec=True) def test_webhook_reraise_exception( self, webhook_event_process_mock, webhook_event_validate_mock, transfer__attach_object_post_save_hook_mock, ): class ProcessException(Exception): pass exception_message = "process fail" webhook_event_process_mock.side_effect = ProcessException(exception_message) self.assertEqual(WebhookEventTrigger.objects.count(), 0) fake_event = deepcopy(FAKE_EVENT_TRANSFER_CREATED) with self.assertRaisesMessage(ProcessException, exception_message): self._send_event(fake_event) self.assertEqual(WebhookEventTrigger.objects.count(), 1) event_trigger = WebhookEventTrigger.objects.first() self.assertEqual(event_trigger.exception, exception_message) @patch.object( WebhookEventTrigger.validate, "__defaults__", (None, "whsec_XXXXX", 300, None) ) @patch.object(Transfer, "_attach_objects_post_save_hook") @patch.object( djstripe_settings, "WEBHOOK_EVENT_CALLBACK", return_value=mock_webhook_handler ) @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_STANDARD_ACCOUNT), autospec=True, ) @patch( "stripe.Transfer.retrieve", return_value=deepcopy(FAKE_TRANSFER), autospec=True ) @patch("stripe.Event.retrieve", autospec=True) def test_webhook_with_custom_callback( self, event_retrieve_mock, transfer_retrieve_mock, account_retrieve_mock, webhook_event_callback_mock, transfer__attach_object_post_save_hook_mock, ): fake_event = deepcopy(FAKE_EVENT_TRANSFER_CREATED) event_retrieve_mock.return_value = fake_event with pytest.warns(None): resp = self._send_event(fake_event) self.assertEqual(resp.status_code, 200) webhook_event_trigger = WebhookEventTrigger.objects.get() webhook_event_callback_mock.called_once_with(webhook_event_trigger) @patch.object( WebhookEventTrigger.validate, "__defaults__", (None, "whsec_XXXXX", 300, None) ) @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_STANDARD_ACCOUNT), autospec=True, ) @patch( "stripe.Transfer.retrieve", return_value=deepcopy(FAKE_TRANSFER), autospec=True ) @patch("stripe.Event.retrieve", autospec=True) def test_webhook_with_transfer_event_duplicate( self, event_retrieve_mock, transfer_retrieve_mock, account_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): fake_event = deepcopy(FAKE_EVENT_TRANSFER_CREATED) event_retrieve_mock.return_value = fake_event with pytest.warns(None): resp = self._send_event(fake_event) self.assertEqual(resp.status_code, 200) self.assertTrue(Event.objects.filter(type="transfer.created").exists()) self.assertEqual(1, Event.objects.filter(type="transfer.created").count()) # Duplication with pytest.warns(None): resp = self._send_event(fake_event) self.assertEqual(resp.status_code, 200) self.assertEqual(1, Event.objects.filter(type="transfer.created").count()) @patch.object( WebhookEventTrigger.validate, "__defaults__", (None, "whsec_XXXXX", 300, None) ) @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_STANDARD_ACCOUNT), autospec=True, ) @patch( "stripe.Transfer.retrieve", return_value=deepcopy(FAKE_TRANSFER), autospec=True ) @patch("stripe.Event.retrieve", autospec=True) def test_webhook_good_platform_account( self, event_retrieve_mock, transfer_retrieve_mock, account_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): fake_event = deepcopy(FAKE_EVENT_TRANSFER_CREATED) event_retrieve_mock.return_value = fake_event with pytest.warns(None): resp = self._send_event(fake_event) self.assertEqual(resp.status_code, 200) self.assertEqual(Event.objects.count(), 1) self.assertEqual(WebhookEventTrigger.objects.count(), 1) event_trigger = WebhookEventTrigger.objects.first() self.assertEqual(event_trigger.is_test_event, False) self.assertEqual( event_trigger.stripe_trigger_account.id, FAKE_STANDARD_ACCOUNT["id"] ) @patch.object( WebhookEventTrigger.validate, "__defaults__", (None, "whsec_XXXXX", 300, None) ) @patch.object(Transfer, "_attach_objects_post_save_hook") @patch( "stripe.Account.retrieve", return_value=deepcopy(FAKE_CUSTOM_ACCOUNT), autospec=True, ) @patch( "stripe.Transfer.retrieve", return_value=deepcopy(FAKE_TRANSFER), autospec=True ) @patch("stripe.Event.retrieve", autospec=True) def test_webhook_good_connect_account( self, event_retrieve_mock, transfer_retrieve_mock, account_retrieve_mock, transfer__attach_object_post_save_hook_mock, ): fake_event = deepcopy(FAKE_EVENT_TRANSFER_CREATED) fake_event["account"] = FAKE_CUSTOM_ACCOUNT["id"] event_retrieve_mock.return_value = fake_event with pytest.warns(None): resp = self._send_event(fake_event) self.assertEqual(resp.status_code, 200) self.assertEqual(Event.objects.count(), 1) self.assertEqual(WebhookEventTrigger.objects.count(), 1) event_trigger = WebhookEventTrigger.objects.first() self.assertEqual(event_trigger.is_test_event, False) self.assertEqual( event_trigger.stripe_trigger_account.id, FAKE_CUSTOM_ACCOUNT["id"] ) @patch.object( WebhookEventTrigger.validate, "__defaults__", (None, "whsec_XXXXX", 300, None) ) @patch.object(target=Event, attribute="invoke_webhook_handlers", autospec=True) @patch( "stripe.Transfer.retrieve", return_value=deepcopy(FAKE_TRANSFER), autospec=True ) @patch("stripe.Event.retrieve", autospec=True) def test_webhook_error( self, event_retrieve_mock, transfer_retrieve_mock, mock_invoke_webhook_handlers, ): """Test the case where webhook processing fails to ensure we rollback and do not commit the Event object to the database. """ mock_invoke_webhook_handlers.side_effect = KeyError("Test error") fake_event = deepcopy(FAKE_EVENT_TRANSFER_CREATED) event_retrieve_mock.return_value = fake_event with self.assertRaises(KeyError): with pytest.warns(None): self._send_event(fake_event) self.assertEqual(Event.objects.count(), 0) self.assertEqual(WebhookEventTrigger.objects.count(), 1) event_trigger = WebhookEventTrigger.objects.first() self.assertEqual(event_trigger.is_test_event, False) self.assertEqual(event_trigger.exception, "'Test error'") class TestWebhookHandlers(TestCase): def setUp(self): # Reset state of registrations per test patcher = patch.object( webhooks, "registrations", new_callable=(lambda: defaultdict(list)) ) self.addCleanup(patcher.stop) self.registrations = patcher.start() patcher = patch.object(webhooks, "registrations_global", new_callable=list) self.addCleanup(patcher.stop) self.registrations_global = patcher.start() def test_global_handler_registration(self): func_mock = Mock() handler_all()(func_mock) event = self._call_handlers("wib.ble", {"data": "foo"}) # handled self.assertEqual(1, func_mock.call_count) func_mock.assert_called_with(event=event) def test_event_handler_registration(self): global_func_mock = Mock() handler_all()(global_func_mock) func_mock = Mock() handler("foo")(func_mock) event = self._call_handlers("foo.bar", {"data": "foo"}) # handled self._call_handlers("bar.foo", {"data": "foo"}) # not handled self.assertEqual(2, global_func_mock.call_count) # called each time self.assertEqual(1, func_mock.call_count) func_mock.assert_called_with(event=event) def test_event_subtype_handler_registration(self): global_func_mock = Mock() handler_all()(global_func_mock) func_mock = Mock() handler("foo.bar")(func_mock) event1 = self._call_handlers("foo.bar", {"data": "foo"}) # handled event2 = self._call_handlers("foo.bar.wib", {"data": "foo"}) # handled self._call_handlers("foo.baz", {"data": "foo"}) # not handled self.assertEqual(3, global_func_mock.call_count) # called each time self.assertEqual(2, func_mock.call_count) func_mock.assert_has_calls([call(event=event1), call(event=event2)]) def test_global_handler_registration_with_function(self): func_mock = Mock() handler_all(func_mock) event = self._call_handlers("wib.ble", {"data": "foo"}) # handled self.assertEqual(1, func_mock.call_count) func_mock.assert_called_with(event=event) def test_event_handle_registation_with_string(self): func_mock = Mock() handler("foo")(func_mock) event = self._call_handlers("foo.bar", {"data": "foo"}) # handled self.assertEqual(1, func_mock.call_count) func_mock.assert_called_with(event=event) def test_event_handle_registation_with_list_of_strings(self): func_mock = Mock() handler("foo", "bar")(func_mock) event1 = self._call_handlers("foo.bar", {"data": "foo"}) # handled event2 = self._call_handlers("bar.foo", {"data": "bar"}) # handled self.assertEqual(2, func_mock.call_count) func_mock.assert_has_calls([call(event=event1), call(event=event2)]) def test_webhook_event_trigger_invalid_body(self): trigger = WebhookEventTrigger(remote_ip="127.0.0.1", body="invalid json") assert not trigger.json_body # # Helpers # @staticmethod def _call_handlers(event_spec, data): event = Mock(spec=Event) parts = event_spec.split(".") category = parts[0] verb = ".".join(parts[1:]) type(event).parts = PropertyMock(return_value=parts) type(event).category = PropertyMock(return_value=category) type(event).verb = PropertyMock(return_value=verb) call_handlers(event=event) return event class TestGetRemoteIp: class RequestClass: def __init__(self, data): self.data = data @property def headers(self): return HttpHeaders(self.META) @property def META(self): return self.data @pytest.mark.parametrize( "data", [ {"HTTP_X_FORWARDED_FOR": "127.0.0.1,345.5.5.3,451.1.1.2"}, { "REMOTE_ADDR": "422.0.0.1", "HTTP_X_FORWARDED_FOR": "127.0.0.1,345.5.5.3,451.1.1.2", }, { "REMOTE_ADDR": "127.0.0.1", }, ], ) def test_get_remote_ip(self, data): request = self.RequestClass(data) assert get_remote_ip(request) == "127.0.0.1" @pytest.mark.parametrize( "data", [ { "REMOTE_ADDR": "", }, { "pqwwe": "127.0.0.1", }, ], ) def test_get_remote_ip_remote_addr_is_none(self, data): request = self.RequestClass(data) # ensure warning is raised with pytest.warns( None, match=r"Could not determine remote IP \(missing REMOTE_ADDR\)\." ): assert get_remote_ip(request) == "0.0.0.0" class TestWebhookEndpoint: """Test Class to test WebhookEndpoint and its methods""" def test_sync_from_stripe_data_non_existent_webhook_endpoint(self): fake_webhook = deepcopy(FAKE_WEBHOOK_ENDPOINT_1) webhook_endpoint = WebhookEndpoint.sync_from_stripe_data(fake_webhook) assert webhook_endpoint.id == fake_webhook["id"] assert isinstance(webhook_endpoint.djstripe_uuid, UUID) # assert WebHookEndpoint's secret does not exist for a new sync assert not webhook_endpoint.secret def test_sync_from_stripe_data_existent_webhook_endpoint(self): fake_webhook_1 = deepcopy(FAKE_WEBHOOK_ENDPOINT_1) webhook_endpoint = WebhookEndpoint.sync_from_stripe_data(fake_webhook_1) assert webhook_endpoint.id == fake_webhook_1["id"] djstripe_uuid = webhook_endpoint.djstripe_uuid assert isinstance(djstripe_uuid, UUID) # assert WebHookEndpoint's secret does not exist for a new sync assert not webhook_endpoint.secret # add a secret to the webhook_endpoint fake_webhook_2 = deepcopy(FAKE_WEBHOOK_ENDPOINT_1) fake_webhook_2["secret"] = "whsec_rguCE5LMINfRKjmIkxDJM1lOPXkAOQp3" webhook_endpoint.secret = fake_webhook_2["secret"] webhook_endpoint.save() # re-sync the WebhookEndpoint instance WebhookEndpoint.sync_from_stripe_data(fake_webhook_2) webhook_endpoint.refresh_from_db() assert webhook_endpoint.id == fake_webhook_2["id"] # assert secret got updated assert webhook_endpoint.secret == fake_webhook_2["secret"] # assert UUID didn't get regenerated assert webhook_endpoint.djstripe_uuid == djstripe_uuid def test___str__(self): fake_webhook = deepcopy(FAKE_WEBHOOK_ENDPOINT_1) webhook_endpoint = WebhookEndpoint.sync_from_stripe_data(fake_webhook) assert str(webhook_endpoint) == webhook_endpoint.url assert ( str(webhook_endpoint) == "https://dev.example.com/stripe/webhook/f6f9aa0e-cb6c-4e0f-b5ee-5e2b9e0716d8" ) ================================================ FILE: tests/urls.py ================================================ from django.contrib import admin from django.http.response import HttpResponse from django.urls import include, path admin.autodiscover() def empty_view(request): return HttpResponse() urlpatterns = [ path("home/", empty_view, name="home"), path("admin/", admin.site.urls), path("djstripe/", include("djstripe.urls", namespace="djstripe")), path("example/", include("tests.apps.example.urls")), path("testapp/", include("tests.apps.testapp.urls")), path( "testapp_namespaced/", include("tests.apps.testapp_namespaced.urls", namespace="testapp_namespaced"), ), # Represents protected content path("testapp_content/", include("tests.apps.testapp_content.urls")), # For testing fnmatches path("test_fnmatch/extra_text/", empty_view, name="test_fnmatch"), ] ================================================ FILE: tox.ini ================================================ [tox] isolated_build = True envlist = # Django 3.2 LTS. Limited support matrix. django32-py{38,39,310}-{postgres,mysql} # Django 4.0 django40-py{38,39,310}-{postgres,mysql,sqlite} # Django 4.1 django41-py{38,39,310}-{postgres,mysql,sqlite} # Django mainline - Only test on latest python / postgres djangomain-py311-postgres skip_missing_interpreters = True [gh-actions] python = 3.8: py38 3.9: py39 3.10: py310 3.11: py311 [testenv] passenv = DJSTRIPE_* setenv = postgres: DJSTRIPE_TEST_DB_VENDOR=postgres mysql: DJSTRIPE_TEST_DB_VENDOR=mysql sqlite: DJSTRIPE_TEST_DB_VENDOR=sqlite PYTHONWARNINGS = all PYTEST_ADDOPTS = --cov --cov-fail-under=90 --cov-report=html --cov-report=term --no-cov-on-fail commands = pytest {posargs} deps = postgres: psycopg2>=2.9 mysql: mysqlclient>=1.4.0 django32: Django==3.2,<3.3 django40: Django==4.0,<4.1 django41: Django==4.1,<4.2 djangomain: https://github.com/django/django/archive/main.tar.gz pytest-django pytest-cov [pytest] DJANGO_SETTINGS_MODULE = tests.settings [coverage:run] branch = True source = djstripe omit = djstripe/migrations/* djstripe/management/* djstripe/admin.py djstripe/checks.py [coverage:html] directory = cover [flake8] exclude = djstripe/migrations/, .tox/, build/lib/, .venv/ ignore = W191, W503, E203, E501 max-line-length = 88