Metadata-Version: 2.4
Name: django-plans-paypal
Version: 1.7.0
Summary: Integration between django-plans and django-paypal.
Home-page: https://github.com/PetrDlouhy/django-plans-paypal
Author: Petr Dlouhý
Author-email: petr.dlouhy@email.cz
License: MIT
Keywords: django-plans-paypal
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Framework :: Django :: 6.1
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.10
License-File: LICENSE
License-File: AUTHORS.rst
Requires-Dist: django-author>=1.0.3
Requires-Dist: django-extensions
Requires-Dist: django-plans>=2.7.0
Requires-Dist: django-paypal
Requires-Dist: django-related-admin
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: home-page
Dynamic: keywords
Dynamic: license
Dynamic: license-file
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

=============================
Django plans paypal
=============================

.. image:: https://badge.fury.io/py/django-plans-paypal.svg
    :target: https://badge.fury.io/py/django-plans-paypal

.. image:: https://github.com/PetrDlouhy/django-plans-paypal/actions/workflows/main.yml/badge.svg
    :target: https://github.com/PetrDlouhy/django-plans-paypal/actions/workflows/main.yml

.. image:: https://codecov.io/gh/PetrDlouhy/django-plans-paypal/branch/master/graph/badge.svg
    :target: https://codecov.io/gh/PetrDlouhy/django-plans-paypal

Almost automatic integration between `django-plans <https://github.com/django-getpaid/django-plans>`_ and `django-paypal <https://github.com/spookylukey/django-paypal>`_.
This will add subscribe buttons to the order page and automatically confirm the `Order` after the payment.

Currently it is in experimetal stage, wher only recurring payments (subscribtions) are supported.


Documentation
-------------

The full documentation is at https://django-plans-paypal.readthedocs.io.

Quickstart
----------

Install and configure ``django-plans`` and ``django-paypal`` apps.
Capture mode is not yet supported, so ``PAYMENT_VARINANTS`` with ``'capture': False`` will not get confirmed.

Install Django plans paypal::

    pip install django-plans-paypal

Add it to your ``INSTALLED_APPS``, before the ``plans``:

.. code-block:: python

    INSTALLED_APPS = (
        ...
        'payments',
        'paypal.standard.ipn',
        'plans_paypal',
        ...
    )

Add your bussiness account e-mail address to settings:

.. code-block:: python

   PAYPAL_BUSSINESS_EMAIL = "foo@bar.com"

   # To enable encrypted PayPal form:
   PAYPAL_ENCRYPTED_FORM = True
   PAYPAL_PRIVATE_CERT = os.path.join(BASE_DIR, 'certs/paypal_private.pem')
   PAYPAL_PUBLIC_CERT = os.path.join(BASE_DIR, 'certs/paypal_public.pem')
   PAYPAL_CERT = os.path.join(BASE_DIR, 'certs/paypal_cert.pem')
   PAYPAL_CERT_ID = 'xxxxx'


Add Django ``plans_paypal`` to the URL patterns:

.. code-block:: python

    urlpatterns = [
        ...
        url(r'^plans-paypal', include('plans_paypal.urls')),
        ...
    ]

Override `django-plans` class `CreateOrderView` so that `get_success_url()` returns url of `paypal-payment` view:

.. code-block:: python

    def get_success_url(self):
       return reverse("paypal-payment", kwargs={'order_id': self.object.id})

Currency
--------

The subscribe button charges the order's currency (``Order.currency``,
which django-plans sets from ``PLANS_CURRENCY``). PayPal has to support
it for your account, see https://developer.paypal.com/reference/currency-codes/.
A subscription payment that arrives in any other currency is refused.
HUF, JPY and TWD amounts are sent without decimals; an order whose total
has a fraction of those currencies cannot be paid (the payment view
raises ``ValueError``), so price plans in whole units there.

Sandbox testing
---------------

Set following settings:

.. code-block:: python

   PAYPAL_TEST_BUSSINESS_EMAIL = "foo@bar.com"
   PAYPAL_TEST = True

   # For encrypted PayPal sandbox form:
   PAYPAL_TEST_PRIVATE_CERT = os.path.join(BASE_DIR, 'certs/paypal_private.pem')
   PAYPAL_TEST_PUBLIC_CERT = os.path.join(BASE_DIR, 'certs/paypal_public.pem')
   PAYPAL_TEST_CERT = os.path.join(BASE_DIR, 'certs/paypal_sandbox_cert.pem')
   PAYPAL_TEST_CERT_ID = 'xxxx'


Redirect user to `paypal-payment-sandbox` instead of `paypal-payment` view.

Sandbox IPNs are accepted only with ``PAYPAL_TEST = True`` set explicitly
(django-paypal's own default when it is unset does not count); otherwise
they are refused with an exception and nothing is written. Live IPNs are
processed either way, so one site can serve both buttons, but then a
sandbox account can activate its paid plans: keep ``PAYPAL_TEST`` off
production.

Features
--------

* TODO

Running Tests
-------------

Does the code actually work?

::

    source <YOURVIRTUALENV>/bin/activate
    (myenv) $ pip install tox
    (myenv) $ tox

Credits
-------

Tools used in rendering this package:

*  Cookiecutter_
*  `cookiecutter-djangopackage`_

.. _Cookiecutter: https://github.com/audreyr/cookiecutter
.. _`cookiecutter-djangopackage`: https://github.com/pydanny/cookiecutter-djangopackage




History
-------

1.7.0 (2026-10-16)
++++++++++++++++++

**Upgrade note - installations whose** ``PLANS_CURRENCY`` **is not USD:
PayPal subscriptions created before this version keep renewing in USD
and their payments are now refused.** Earlier buttons charged USD
whatever the order's currency, and PayPal cannot change the currency of
an existing subscription. A refused IPN leaves the subscriber charged
but the plan not extended (no renewal order, the IPN view answers
HTTP 500 and PayPal retries). Cancel those subscriptions at PayPal
before upgrading (the plans stay active until their paid period ends)
and have the subscribers subscribe again once this version is deployed,
so the new button charges the order's currency. To find them::

    from django.conf import settings
    from paypal.standard.ipn.models import PayPalIPN

    ended = PayPalIPN.objects.filter(txn_type__in=["subscr_cancel", "subscr_eot"])
    affected = set(
        PayPalIPN.objects.filter(txn_type="subscr_payment", payment_status="Completed")
        .exclude(mc_currency__iexact=settings.PLANS_CURRENCY)
        .exclude(subscr_id__in=ended.values("subscr_id"))
        .values_list("subscr_id", flat=True)
    )

USD installations are not affected.

* the subscribe button charges the order's currency: it sends
  ``currency_code`` = ``order.currency`` (upper-cased). It never set
  one, so django-paypal's form default ``USD`` was sent, and an
  installation whose ``PLANS_CURRENCY`` is not USD charged USD for
  orders invoiced in its own currency (the same number, a different
  amount of money). ``plans_paypal.currency.paypal_currency()`` is the
  one place that names the currency of an order's subscription at PayPal
* security: a completed ``subscr_payment`` IPN, first payment or renewal,
  whose ``mc_currency`` is not the subscription's currency is refused
  like an amount mismatch, nothing written:
  ``Exception("Received currency doesn't match")``. The comparison
  ignores case. The button is a client-side form, so a buyer could edit
  its ``currency_code`` and pay a 9.90 USD order with 9.90 JPY; the
  amount check passed and the order completed (a renewal was even
  invoiced in JPY). The IPN postback only proves PayPal sent the
  notification, not what it charged
* a renewal order is invoiced in the subscription's currency
  (``paypal_currency()`` of the first order) instead of the IPN's
  ``mc_currency``. The notification decided the invoice currency, and
  ``create_new_order()`` can be called without the IPN checks; a
  lower-case ``mc_currency`` also leaked into the order and the
  ``RecurringUserPlan``
* the button amount (``a3``) has no decimals in HUF, JPY and TWD, which
  PayPal rejects even as ``.00``; a total with a fraction of those
  currencies (e.g. 999 JPY + 21 % = 1208.79) raises ``ValueError`` in
  the payment view instead of PayPal refusing the button
* admin: the PayPal payment list shows the charged currency next to the
  gross and fee
* security: a sandbox subscription IPN (``test_ipn=1``) is refused unless
  ``PAYPAL_TEST = True`` is set:
  ``Exception("Sandbox IPN refused: PAYPAL_TEST is not enabled")``,
  nothing written. django-paypal verifies a sandbox IPN against PayPal's
  sandbox on any deployment and the button's ``notify_url`` is editable,
  so a live site that had ``PAYPAL_TEST_BUSSINESS_EMAIL`` set completed
  orders and armed renewals paid with sandbox money. django-paypal's own
  default (``PAYPAL_TEST`` is ``True`` when unset) does not count: a
  sandbox setup relying on it has to set ``PAYPAL_TEST = True``, as the
  README always said. Live IPNs are processed in either mode, unchanged.
  IPNs other than subscription ones are still left to other receivers
* security: the payment page (``paypal-payment`` and
  ``paypal-payment-sandbox``) requires login and serves only the user's
  own orders, someone else's order is a 404, like django-plans'
  ``OrderView``. It looked the order up by id alone, so anyone, even
  anonymous, could open it for any order id and read the owner's email
  from the button's hidden ``custom`` field
* the app pins ``default_auto_field`` to ``AutoField``, the type its
  migrations use: with ``DEFAULT_AUTO_FIELD = BigAutoField`` (the global
  default since Django 6.0) ``makemigrations`` wrote a migration into the
  installed package
* tested on Django 6.0 and 6.1 and on Python 3.14
* Python 3.9 support dropped (end of life since October 2025):
  ``python_requires`` is ``>=3.10``. GitHub's ``ubuntu-latest`` runner
  moves to Ubuntu 26.04, which has no Python 3.9 build
* packaging: the wheel no longer installs a top-level ``tests`` package
  (the test project's settings and urls), which could shadow or be
  shadowed by a project's own ``tests``
* development: ``make test`` and ``make coverage`` run the suite through
  ``manage.py test`` like CI and tox; they ran ``runtests.py tests``,
  which found 0 tests. ``runtests.py`` is gone
* CI: Dependabot checks GitHub Actions and pip requirements weekly

1.6.0 (2026-09-24)
++++++++++++++++++

* renewal orders store the charged gross: ``create_new_order`` sets
  ``Order.gross_amount`` (django-plans 2.7.0) to the IPN's ``mc_gross``
  and derives the net with ``Order.net_from_gross()`` (coefficient
  method, tax = gross x rate / (100 + rate)). Multiplying a net cent
  value by a rate cannot reach every gross cent value, so a renewal
  after a tax-rate change could only pick the nearest net and was
  invoiced a cent below the charge (e.g. 14.89 at 21 %: 12.30 -> 14.88,
  12.31 -> 14.90). The invoice now carries exactly what PayPal charged;
  the "closest total" workaround and its warning are gone
* when the current rate cannot be determined, the copied values
  (``RecurringUserPlan`` expectation, then the first order) are chosen
  by which one's total equals the charged gross. Previously the
  expectation won even when PayPal had charged the first order's total,
  so the order carried an amount other than the one received. If
  neither matches, ``Order.save()`` now rejects the order instead of
  invoicing an amount that was not charged
* requires django-plans >= 2.7.0

1.5.0 (2026-08-25)
++++++++++++++++++

* tests: full 100 % line and branch coverage, and CI now gates on it
  (``coverage report --fail-under=100``). New coverage includes the
  sandbox business-email validation, stale cancellations for users
  without a recurring plan, VAT IDs reaching the taxation policy in
  full form, encrypted-form certificate wiring (sandbox vs. live), and
  a characterization of the week-unit period conversion (fractional
  ``p3`` - PayPal documents an integer; left as-is deliberately, live
  subscriptions depend on current behaviour)
* the received-amount check accepts, besides the first order's total,
  the total implied by the armed ``RecurringUserPlan`` (net x current
  tax). This makes legitimate subscription-amount updates on PayPal's
  side (e.g. after a VAT change) processable: update the
  ``RecurringUserPlan`` expectation, then change the amount at PayPal.
  Renewal orders without a taxation policy configured now also prefer
  that expectation over the first order's frozen copy, so the invoice
  matches what was actually charged.

1.4.0 (2026-08-20)
++++++++++++++++++

* admin: ``PayPalPaymentAdmin`` now extends ``RelatedFieldAdmin`` - the
  ``__`` paths in ``list_display`` only work on plain ``ModelAdmin``
  since Django 5.1, so the admin was broken on Django 4.2/5.0
* debug ``print()`` calls removed from the IPN hook and the payment
  view (the view printed the full payment dict incl. customer e-mail
  into logs on every order page view); the hook logs a single
  ``logger.debug`` line instead
* tests: admin (system checks + changelist), IPN with empty ``custom``
  data, cancellation with a foreign ``subscr_id``, PayPal duration-unit
  selection and sandbox-flag isolation of the payment form (coverage
  84 % -> 93 %)
* release tooling: ``make release``/``make sdist`` use ``build`` +
  ``twine`` (``setup.py sdist upload`` has been dead on PyPI for
  years), the broken ``setup.py publish`` shortcut and the stale
  ``.travis.yml`` are gone
* CI: test matrix updated to Django 4.2-5.2 on Python 3.9-3.13 (dropped
  EOL Django 3.2-4.1 and Python 3.8), GitHub Actions bumped to current
  versions
* CI: Codecov upload repaired - the deprecated ``codecov`` CLI uploader
  has been dead since 2023 (uploads failed silently, so no reports or
  PR comments appeared); now ``codecov-action@v5`` with OIDC
* packaging: classifiers and ``python_requires`` reflect the supported
  versions
* renewal orders recalculate the tax rate for the user's current billing
  data instead of copying it from the subscription's first order forever.
  PayPal charges a fixed gross, so the new rate is applied top-down: the
  gross stays, the net amount adjusts (at most one cent off when no
  cent-exact net exists for the new rate). Without ``PLANS_TAXATION_POLICY``,
  without ``BillingInfo``, or on a failed VIES/rate lookup the previous
  behaviour (copied tax) is kept.

1.3.0 (2026-08-19)
++++++++++++++++++

* ``receive_ipn`` now processes each IPN in a single database
  transaction: a failure while completing the order no longer leaves an
  armed ``RecurringUserPlan`` and a recorded payment behind, so PayPal's
  IPN retries start from a clean slate instead of dying on the leftover
  state as duplicates.
* pending subscription payments (typically eChecks clearing) are
  recorded as ``PayPalPayment`` rows instead of being silently dropped
  -- an in-flight payment is not a decline.

1.2.0 (2026-05-20)
++++++++++++++++++

* ``PaymentFailureView``: redirect already-completed orders to the success
  page (with a warning log) instead of raising ``ValueError``. PayPal can
  send the buyer to the ``cancel_return`` URL even after the IPN has
  marked the order as completed (browser back button, stale tabs, edge
  cases in the PayPal flow); the previous behaviour produced unhandled
  500s for paid users. Reverses the 0.7.1 behaviour change.
* handle empty/whitespace PayPal IPN ``custom`` data without logging an
  error; also catch ``ValueError`` from ``ast.literal_eval`` so future
  Python releases can't break the parser
* admin: add ``paypal_ipn__txn_id`` column to ``PayPalPaymentAdmin.list_display``
* drop conflicting isort ``lines_after_imports = 2`` setting (project
  already uses ``profile = black``) and reformat affected files; the
  ``lint`` CI job is green again

1.1.0 (2025-06-20)
++++++++++++++++++

* add more fields to admin list_display

1.0.1 (2025-05-29)
++++++++++++++++++

* fixes to taking the data from custom_data

1.0.0 (2025-05-23)
++++++++++++++++++

* create order more robust: take the data from custom_data
* Support Django up to 5.2

0.7.1 (2024-04-23)
++++++++++++++++++
* fail if PaymentFailureView is requested on completed orders

0.7.0 (2024-04-12)
++++++++++++++++++
* migrate code to RecurringUserPlan.renewal_triggered_by
* migrate data of RecurringUserPlans with payment_provider="paypal-recurring" and renewal_triggered_by=TASK to renewal_triggered_by=OTHER

0.6.0 (2023-03-22)
+++++++++++++++++++
* Fix amount received on recurring payments, add tests

0.5.1 (2023-02-11)
+++++++++++++++++++
* allow only logged users to access the failure view

0.5.0 (2022-12-14)
+++++++++++++++++++

* Fix tax received on recurring payments
* More robust receiving original Order ID (if PayPal fails to handle custom_data)
* update to Django 4.1
* fix completing recurring payments if first order status is returned
* only log error if custom data can't be parsed

0.4.10 (2022-04-29)
+++++++++++++++++++
* fix problem with duplicate payments
* PayPalPaymentAdmin: display connected IPN fields

0.4.9 (2022-03-07)
++++++++++++++++++
* make parsing custom_data more robust

0.4.8 (2022-01-19)
++++++++++++++++++
* fix last release
* add tests and test on all supported Django/Python versions through GitHub actions

0.4.7 (2022-01-13)
++++++++++++++++++
* fix problem if there was ' in custom payment data

0.4.6 (2021-12-02)
++++++++++++++++++
* fix problem with creating bad JSON

0.4.5 (2021-12-01)
++++++++++++++++++
* fix IPN field editing in PayPalPaymentAdmin
* store also user e-mail in custom to enable search in IPN admin

0.4.4 (2021-12-01)
++++++++++++++++++
* create PayPalPayment before completing the order

0.4.3 (2021-11-30)
++++++++++++++++++
* fix problem with recurring payments
* add created/modified/author auto fields

0.4.2 (2021-11-08)
++++++++++++++++++
* fix problem with other IPN types

0.4.1 (2021-10-18)
++++++++++++++++++
* cancel order after returning to failure URL

0.4.0 (2021-10-18)
++++++++++++++++++
* allow to set up encrypted PayPal form

0.3.0 (2021-10-15)
++++++++++++++++++
* fixes and improvements to the sandbox/production functionality
* reverse sandbox logic - if no `PAYPAL_TEST`, the sandbox view would return production so nobody can pay through sandbox on production server

0.2.2 (2021-10-12)
++++++++++++++++++
* fix foregotten pudb

0.2.1 (2021-10-12)
++++++++++++++++++
* add sandbox view, so both production and sandbox can be used on one server

0.2.0 (2021-10-11)
++++++++++++++++++
* fix periods to complain with PayPal maximal durations

0.1.0 (2021-10-08)
++++++++++++++++++
* hook ipn.PayPalIpn object with plans.Order (for later usage e.g. determining PayPal fee)
* set plan renewal for django-plans

0.0.2 (2018-08-05)
++++++++++++++++++

* Payment process without capturing should work
* Automatic buttons generation

0.0.1 (2018-07-23)
++++++++++++++++++

* First release on PyPI.
