Skip to content

Commit

Permalink
adds marketplace processing fee
Browse files Browse the repository at this point in the history
This commit also documents the various pricing models supported,
including group_buy (#151), usage billing (#136)
  • Loading branch information
smirolo committed Sep 4, 2019
1 parent 7d0cab9 commit cfd0660
Show file tree
Hide file tree
Showing 36 changed files with 1,020 additions and 450 deletions.
3 changes: 1 addition & 2 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,8 @@ These API end points manage the subscription logic, payments excluded.
.. autoclass:: saas.api.organizations.SubscribersAPIView

.. http:get:: /api/profile/:organization/subscriptions/
.. http:post:: /api/profile/:organization/subscriptions/
.. autoclass:: saas.api.subscriptions.SubscriptionListCreateAPIView
.. autoclass:: saas.api.subscriptions.SubscriberSubscriptionListAPIView


.. http:delete:: /api/profile/:organization/subscriptions/<subscribed_plan>/
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@
# built documents.
#
# The short X.Y version.
version = '0.6'
version = '0.7'
# The full version, including alpha/beta/rc tags.
release = '0.6.3'
release = '0.7.0'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
5 changes: 3 additions & 2 deletions docs/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ provider / broker together with three ``Plan`` that belong to the provider
"model" : "saas.Plan", "pk": 3
}]
To setup different pricing models such as a 3 Part Tariff (3PT),
read about the :doc:`supported pricing models<pricing>`.


Selling add-ons plans
---------------------
Expand Down Expand Up @@ -187,5 +190,3 @@ on the subscribed-to Plan is to decorate the view implementing the feature.
requires_paid_subscription(FeatureView.as_view()), name='feature'),
\.\.\.
]
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Contents:

getting-started
subscriptions
pricing
orders
ledger
security
Expand Down
222 changes: 222 additions & 0 deletions docs/pricing.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
Pricing models
==============

Per-period pricing
------------------

This is the simplest form of subscription pricing. The subscriber pays a fixed
amount every single period (ex: $29/month). Setting up a per-period pricing
consists of creating a ``Plan`` with a ``period`` (``HOURLY``, ``DAILY``,
``WEEKLY``, ``MONTHLY``, ``YEARLY``) and a positive ``period_amount``.

Example fixtures for a $29/month::

{
"fields": {
"slug": "indie",
"created_at": "2019-01-01T00:00:00-00:00",
"title": "Indie",
"organization": 2,
"is_active": 1.
"period_amount": 2900,
"period_type": 4
},
"model": "saas.Plan", "pk": 1
}

The ``organization`` field is set to reference the plan provider; here
``Organization`` with ``pk == 2``. The plan is also marked to be available
on the pricing page (``is_active == 1``) such that user can subscribe
to the plan.
The ``period_amount`` is to set as an integer amount of cents, ``2900`` in this
case, while the ``period_type`` is set to ``4`` which is the enum value for
``Plan.MONTHLY``.

On the initial payment, when a user subscribes to the plan through the checkout
workflow, ``Organization.checkout`` will be called to create the initial charge
and the ``Subscription`` record.
Later on whenever the subscription is about to expire, the renewals management
command will call ``extend_subscriptions`` to extend the subscription by one
period, then call ``create_charges_for_balance`` to charge the period amount
to the card on file.


Per-period pricing with setup fee
---------------------------------

It is sometimes necessary to charge a one-time setup fee, maybe because
a human needs to be involved to setup a physical space (in case of a co-working
office) or send a device (in case of an IoT service).

Example fixtures for a $29/month + a one-time $10 setup fee::

{
"fields": {
"slug": "indie",
"created_at": "2019-01-01T00:00:00-00:00",
"title": "Indie",
"organization": 2,
"is_active": 1,
"period_amount": 2900,
"period_type": 4,
**"setup_amount": 1000**
},
"model": "saas.Plan", "pk": 1
}

The ``setup_amount`` (in cents) is automatically added as a one-time charge
on the initial payment.


Per-period pricing with custom periods
--------------------------------------

You might be selling online Continuous Education Units (CEUs) that are valid
for a period of 2 years. Either it is a 2-year period, or a quarter
(3-month period), there are pricing models that align naturally with business
cycles that fall outside the monthly/yearly dichotomy.

For these cases, it makes sense to define a ``period_length`` which a value
grater than 1.

Example fixtures for a $29 per 2-year plan::

{
"fields": {
"slug": "indie",
"created_at": "2019-01-01T00:00:00-00:00",
"title": "Indie",
"organization": 2,
"is_active": 1,
"period_amount": 2900,
**"period_length": 2,**
"period_type": 5
},
"model": "saas.Plan", "pk": 1
}


Per-period pricing with discount for advance payments
-----------------------------------------------------

Software-as-a-Service (SaaS) is a relationship business. It makes sense
to incentivize subscribers to pay in advance by offering discounts.

You can specify an ``advance_discount`` on a plan. When you do so, the checkout
workflow will automatically present the option to pay for a multiple periods
in advance to the customer.

Example fixtures for a $29/month and a 20% discount if paid yearly::

{
"fields": {
"slug": "indie",
"created_at": "2019-01-01T00:00:00-00:00",
"title": "Indie",
"organization": 2,
"is_active": 1,
"period_amount": 2900,
"period_type": 4,
**"advance_discount": 2000**
},
"model": "saas.Plan", "pk": 1
}

Quota pricing
-------------

In some cases, the business model requires to charge base on usage (HTTP
requests, Gigabytes, messages, telephony minutes).
To implement a 3 Part Tariff (3PT) - fixed base, included quota, additional
charges for over quota - we associate a ``UseCharge`` instance to a ``Plan``.

Example fixtures for a $29/month, includes 100 "free" messages,
$0.15 per message afterwards::

{
"fields": {
"slug": "indie",
"created_at": "2019-01-01T00:00:00-00:00",
"title": "Indie",
"organization": 2,
"is_active": 1,
"period_amount": 2900,
"period_type": 4
},
"model": "saas.Plan", "pk": 1
}
{
"fields": {
"slug": "messages",
"created_at": "2019-01-01T00:00:00-00:00",
"title": "Per message",
"plan": 1,
"use_amount": 15,
"quota": 100
},
"model": "saas.UseCharge", "pk": 1
}

The functions ``new_use_charge`` and ``record_use_charge`` are the backbone
to implement quota pricing. Each time an ``UseCharge`` event occurs, call
``record_use_charge`` passing a subscription object and a use_charge object.
``record_use_charge`` will take care of recording the event into the transaction
ledger, applying the "free" quota limit as required.
Later on the :doc:`renewals command<periodic-tasks>` will recognize the revenue
for the over-quota usage and generate the appropriate invoices.

Marketplace transaction fee
---------------------------

If you are using a :doc:`Stripe processor backend<backends>`, it is possible
to setup a marketplace with a broker and multiple providers, collecting
a `broker fee <https://stripe.com/docs/connect/direct-charges#collecting-fees>`_
on transaction between subscribers and providers.

To setup a 10% broker fee, update your settings.py as such::

SAAS = {
'BROKER': {
'FEE_PERCENTAGE': 1000,
}
}

This will set the ``broker_fee_amount`` field on each ``Plan`` created.
When a ``Charge`` is created for an initial or renewed subscription,
the ``broker_fee_amount`` is applied.


Group buy
---------

The payer is not always the subscriber for a SaaS product. It often happens
in enterprise software, but with an increasingly mobile workforce, it often
the case that a contractor will bring his account while the employer will fit
the bill.
In our previous professional certification e-learning example
(`Per-period pricing with custom periods`_), a clinic pays for its staff
to take the online class, the account and completion certificate belongs
to the nurse (i.e. subscriber). This is implemented through a
&quot;Group buy&quot; feature.

To turn on the &quot;Group buy&quot; feature, set ``is_bulk_buyer`` to ``True``
in an ``Organization`` object::

{
"fields": {
"slug": "xia",
"created_at": "2019-01-01T00:00:00-00:00",
"full_name": "Xia Lee",
"processor": 1,
"is_active": 1,
**"is_bulk_buyer": true**
},
"model": "saas.Organization", "pk": 3
}

When a profile with ``is_bulk_buyer == True`` goes through the checkout
workflow, :ref:`steps are added<group_buy>` to allow the user to pay
a subscription on behalf of someone else.
When payment occurs, instead of creating a ``Subscription`` object
for the payer, a one-time ``Coupon`` is mechanically created. The final
subscriber can use that coupon at checkout to zero-out the balance due.
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ djangorestframework>=3.3.1
# We need Python Markdown for django.contrib.markup. markdown2 is not enough.
Markdown>=2.4
python-dateutil>=2.2
stripe>=2.6.0
stripe>=2.35.1
razorpay>=0.2.0
2 changes: 1 addition & 1 deletion saas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@
PEP 386-compliant version number for the saas django app.
"""

__version__ = '0.6.3'
__version__ = '0.7.0-dev'
14 changes: 7 additions & 7 deletions saas/api/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

from .organizations import OrganizationQuerysetMixin
from .serializers import (OrganizationSerializer, UserSerializer)
from .. import filters, settings
from .. import filters
from ..mixins import (OrganizationSmartListMixin, UserSmartListMixin)
from ..pagination import TypeaheadPagination

Expand Down Expand Up @@ -77,9 +77,9 @@ class AccountsSearchAPIView(OrganizationSmartListMixin,
parameters. To reverse the natural order of a field, prefix the field
name by a minus (-) sign.
**Tags: profile
**Tags**: profile
**Examples
**Examples**
.. code-block:: http
Expand Down Expand Up @@ -204,9 +204,9 @@ class ProfilesTypeaheadAPIView(OrganizationSmartListMixin,
parameters. To reverse the natural order of a field, prefix the field
name by a minus (-) sign.
**Tags: profile
**Tags**: profile
**Examples
**Examples**
.. code-block:: http
Expand Down Expand Up @@ -262,9 +262,9 @@ class UsersTypeaheadAPIView(UserSmartListMixin, UserQuerysetMixin,
Query results can be ordered by natural fields (``o``) in either ascending
or descending order (``ot``).
**Tags: profile
**Tags**: profile
**Examples
**Examples**
.. code-block:: http
Expand Down
8 changes: 4 additions & 4 deletions saas/api/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class RetrieveBankAPIView(OrganizationMixin, RetrieveAPIView):
of a subscription cart is done either through the
:ref:`HTML page<pages_cart>` or :ref:`API end point<api_checkout>`.
**Examples
**Examples**
.. code-block:: http
Expand Down Expand Up @@ -84,7 +84,7 @@ class PaymentMethodDetailAPIView(OrganizationMixin,
Pass through to the processor to retrieve some details about
the payment method (ex: credit card) associated to a subscriber.
**Examples
**Examples**
.. code-block:: http
Expand All @@ -108,7 +108,7 @@ def delete(self, request, *args, **kwargs):
Pass through to the processor to remove the payment method (ex: credit
card) associated to a subscriber.
**Examples
**Examples**
.. code-block:: http
Expand All @@ -126,7 +126,7 @@ def put(self, request, *args, **kwargs):
Pass through to the processor to update some details about
the payment method (ex: credit card) associated to a subscriber.
**Examples
**Examples**
.. code-block:: http
Expand Down
Loading

0 comments on commit cfd0660

Please sign in to comment.