Skip to content

Commit

Permalink
Add ADRs on CustomerAgreements and SubscriptionPlanRenewals.
Browse files Browse the repository at this point in the history
  • Loading branch information
iloveagent57 committed Dec 14, 2020
1 parent cfe8d33 commit 5a7abd7
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ dev.stop: # Stops containers so they can be restarted
%-shell: # Run a shell, as root, on the specified service container
docker exec -u 0 -it license_manager.$* bash

mysql-client-shell: # Will drop you directly into a mysql client shell.
docker exec -u 0 -it license_manager.mysql mysql license_manager

%-logs: # View the logs of the specified service container
docker-compose logs -f --tail=500 $*

Expand Down
75 changes: 75 additions & 0 deletions docs/decisions/0002-customer-agreements.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
2. Introduction of a Customer Agreement Model
=============================================


Status
======

Accepted (circa November 2020)


Context
=======

Not all subscriptions sales are simple - there are some transactions that will occur in the subscription life cycle
such as renewals, additional "batches", or staggered starting times that we want to support. We want the structure of
our data models to reflect, within reason, the nature of these business arrangements.


Decision
========

We introduced a new model, ``CustomerAgreement``::

CREATE TABLE `subscriptions_customeragreement` (
`created` datetime(6) NOT NULL,
`modified` datetime(6) NOT NULL,
`uuid` char(32) NOT NULL,
`enterprise_customer_uuid` char(32) NOT NULL,
`enterprise_customer_slug` varchar(128) NOT NULL,
`default_enterprise_catalog_uuid` char(32) DEFAULT NULL,
PRIMARY KEY (`uuid`),
UNIQUE KEY `enterprise_customer_uuid` (`enterprise_customer_uuid`),
UNIQUE KEY `enterprise_customer_slug` (`enterprise_customer_slug`)
)

This model represents an agreement with a specific enterprise customer. It records the customers ``uuid`` and ``slug``
fields from the edx-enterprise ``EnterpriseCustomer`` model.

The intention of the ``default_enterprise_catalog_uuid`` field is to provide the option
to not supply an ``enterprise_catalog_uuid`` on the ``SubscriptionPlan``, and instead use a default defined
in the agreement. It should provide a small convenience for cases where a customer wants to keep renewing
a subscription for the same catalog multiple times.

The ``SubscriptionPlan`` model no longer contains a ``enterprise_customer_uuid`` field. In it's place,
there is a non-null foreign key on ``CustomerAgreement.uuid``.

Here's an example of a ``CustomerAgreement`` record::

+----------------------------------+----------------------------------+--------------------------+---------------------------------+
| customer_agreement_uuid | enterprise_customer_uuid | enterprise_customer_slug | default_enterprise_catalog_uuid |
+----------------------------------+----------------------------------+--------------------------+---------------------------------+
| ea9683443e2148a8aa54dcb1733b80dc | 378d5bf0f67d4bf78b2acbbc53d0f772 | pied-piper | NULL |
+----------------------------------+----------------------------------+--------------------------+---------------------------------+

And an example of two "batches" of ``SubscriptionPlans`` that are defined under this agreement::

+----------------------------------+----------------------------------+----------------------------------+------------+-----------------+----------------------------------+-----------+---------------------------+
| customer_agreement_id | plan_uuid | title | start_date | expiration_date | enterprise_catalog_uuid | is_active | salesforce_opportunity_id |
+----------------------------------+----------------------------------+----------------------------------+------------+-----------------+----------------------------------+-----------+---------------------------+
| ea9683443e2148a8aa54dcb1733b80dc | b0c4d2820dd14ac7bafecf3bf2b38a93 | Pied Piper's Second Subscription | 2021-02-01 | 2022-01-31 | 7467c9d2433c4f7eba2ec5c7798527b2 | 0 | 100000000000000001 |
| ea9683443e2148a8aa54dcb1733b80dc | fe9cc40e24a747a0b8009a11288b3ec2 | Pied Piper's First Subscription | 2020-12-01 | 2021-11-30 | 7467c9d2433c4f7eba2ec5c7798527b2 | 1 | 100000000000000000 |
+----------------------------------+----------------------------------+----------------------------------+------------+-----------------+----------------------------------+-----------+---------------------------+

Consequences
============

The most notable change is that a single customer, via a ``CustomerAgreement``, may now have multiple (active)
``SubscriptionPlans`` at any time. This supports the example use case of rolling "batches" of subscriptions.

The Django Admin page for ``SubscriptionPlans`` will change:

* A ``CustomerAgreement`` instance must now be created during the creation of a ``SubscriptionPlan``. The ``uuid``
and ``slug`` fields should be copied from the ``EnterpriseCustomer`` model (see edx-enterprise).
* The ``enterprise_customer_uuid`` field is no longer directly accessible from this model.
An administrator of license-manager must now access it from plan's related ``CustomerAgreement``.
208 changes: 208 additions & 0 deletions docs/decisions/0003-subscription-renewals.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
3. Introduction of a Subscription Plan Renewal Model
====================================================

Status
======

Accepted (circa November 2020)

Context
=======

We'd like to allow subscriptions customers to schedule a renewal of their existing subscriptions prior to
their active subscription's expiration date. For example, for a subscription with 100 licenses,
a start date of 2021-01-01, and an end date of 2021-12-31, we'd like to allow the customer to renew that subscription
for 150 on, say, 2021-06-17. The renewed subscription would become active on 2022-01-01 and expire on 2022-12-31.
Our data schema should support this business arrangement, and the data models should align closely with the
different logical entities involved in this business arrangement.

Decision
========

We'll introduce a ``SubscriptionPlanRenewal`` model
---------------------------------------------------

A ``SubscriptionPlanRenewal`` is a model that records the intention to renew an existing ``SubscriptionPlan`` into
a new ``SubscriptionPlan``, on or after the expiration date of the original plan.

* It will record references to both the ``SubscriptionPlan`` it is renewed `from` (the "original" plan),
and the plan it renews `into` (the "renewed" plan).
* It records the number of licenses to create in the renewed plan. This number must be greater than or equal
to the number of license allowed under the original plan.
* A renewal must specify a new Salesforce Opportunity ID associated with the renewal business transaction.
* A renewal records a date on or after the original plan's expiration date on which the renewal will be processed
into a new plan.
* It will record a ``processed`` boolean field that indicates whether the renewal has been processed into a renewed plan.

Example
-------
Here's an example ``SubscriptionPlan`` for Pied Piper, set to expire on 2021-11-30::

+----------------------------------+----------------------------------+---------------------------------+------------+-----------------+----------------------------------+-----------+---------------------------+
| customer_agreement_id | plan_uuid | title | start_date | expiration_date | enterprise_catalog_uuid | is_active | salesforce_opportunity_id |
+----------------------------------+----------------------------------+---------------------------------+------------+-----------------+----------------------------------+-----------+---------------------------+
| ea9683443e2148a8aa54dcb1733b80dc | fe9cc40e24a747a0b8009a11288b3ec2 | Pied Piper's First Subscription | 2020-12-01 | 2021-11-30 | 7467c9d2433c4f7eba2ec5c7798527b2 | 1 | 100000000000000000 |
+----------------------------------+----------------------------------+---------------------------------+------------+-----------------+----------------------------------+-----------+---------------------------+

But let's say that Pied Piper decides to renew this plan well in advance of expiration. A ``SubscriptionPlanRenewal``
record should be created that looks like this::

+----------------------------+----------------------------------+---------------------------+--------------------+----------------+-------------------------+-----------+------------------------------+
| created | prior_subscription_plan_id | salesforce_opportunity_id | number_of_licenses | effective_date | renewed_expiration_date | processed | renewed_subscription_plan_id |
+----------------------------+----------------------------------+---------------------------+--------------------+----------------+-------------------------+-----------+------------------------------+
| 2020-12-09 20:01:05.373989 | fe9cc40e24a747a0b8009a11288b3ec2 | 100000000000000002 | 100 | 2021-12-01 | 2022-11-30 | 0 | NULL |
+----------------------------+----------------------------------+---------------------------+--------------------+----------------+-------------------------+-----------+------------------------------+

Note that the renewal has a different ``salesforce_opportunity_id``, because the renewal is a distinct business
transaction from the purchase of the original ``SubscriptionPlan``. Note also that the renewal could be for
the same number of licenses as the original plan, or more licenses than the original plan. When the renewal is
processed, the ``processed`` field will become ``true``, and a new ``SubscriptionPlan`` will be created, the ``uuid``
of which will be placed in the associated renewals ``renewed_subscription_plan_id`` field.

How is the renewal processed?
-----------------------------

The actual renewal process would should primarily be a scheduled daily job that looks at all renewals
and determines if any renewed plans should be created at the time the job is run:

* If there is 1 or fewer days between the current time and the ``effective_date`` of a renewal, the renewal should
be processed into a renewed ``SubscriptionPlan``, whose ``start_date`` is the ``effective_date``.
* The licenses of the original subscription plan should be transferred to the renewed plan. This means simply that
the ``subscription_plan_uuid`` foreign key of each license is updated to point to the renewed plan.
* The original subscription plan should be set to inactive, number of remaining revocations reset, and its license count set to 0.

We will also expose a new API endpoint, which accepts only a ``POST`` request, that will trigger the processing
of a specific ``SubscriptionPlanRenewal``. We want to have this (possibly manual) means of triggering a renewal process
in case of, for example, infrastructure problems related to our cron builds.

What happens to the licenses?
-----------------------------

* The ``subscription_plan_uuid`` foreign key of transferred licenses will be updated from the original plan
to the renewed plan.
* django-simple-history seems to not create historical records from ``bulk_create()`` - the docs indicate that it
only tracks changes on calls to ``create()``, ``update()``, or ``delete()`` by default. Recent versions of the package
provide utility functions to track bulk creation/updates: https://django-simple-history.readthedocs.io/en/latest/common_issues.html#bulk-creating-and-queryset-updating
* We have merged code into license-manager that will ensure, going forward, bulk actions are recorded in the license
history tables.
* Additionally, we will backfill missing creation historical license records, based on the creation date of the license.
This needs to be done before we transfer any licenses to new subscription plans.
* Since we already have licenses in the wild without associated historical creation tracking, we'll
introduce some defensive checking/snapshotting that should occur prior to the license transfer process.
This can be as simple as assuring that a historical record reflecting the license's association with the original
subscription plan exists.

Here's a sample historical sequence of events for a license that was transferred on plan renewal::

*************************** 1. row ***************************
created: 2020-12-10 21:25:27.288376
modified: 2020-12-10 21:25:27.288376
uuid: fcc1e65833bd4d2b992ddd9a14599a37
status: unassigned
activation_date: NULL
last_remind_date: NULL
user_email: NULL
lms_user_id: NULL
history_id: 180
history_date: 2020-12-10 21:25:27.301247
history_change_reason: NULL
history_type: +
history_user_id: NULL
subscription_plan_id: e8f588c38bc040e495817a8eb124faac
activation_key: NULL
assigned_date: NULL
revoked_date: NULL
*************************** 2. row ***************************
created: 2020-12-10 21:25:27.288376
modified: 2020-12-14 16:53:41.699311
uuid: fcc1e65833bd4d2b992ddd9a14599a37
status: unassigned
activation_date: NULL
last_remind_date: NULL
user_email: NULL
lms_user_id: NULL
history_id: 197
history_date: 2020-12-14 16:53:41.702328
history_change_reason: NULL
history_type: ~
history_user_id: NULL
subscription_plan_id: f2e6236b434e4f15a336a5d41beb521c
activation_key: NULL
assigned_date: NULL
revoked_date: NULL

Row 1 represents the creation of the license in the original plan. Row 2 represents the later modification
of the license with a new ``subscription_plan_id`` value.

What happens to the licensed enrollments?
-----------------------------------------

* We will introduce a new field, ``plan_at_time_of_enrollment``, on the ``LicensedEnterpriseEnrollment`` records.
This gives us an even faster way to tie enrollments to plans, plus, it give us a good way to reconcile history, if needed.
* We will backpopulate this field with each license record's current subscription plan UUID. This is only permissible
because we have not yet transferred any licenses between subscription plans.
* After these two steps are completed, licensed enrollment records do not need to be changed during the license transfer,
because the license UUID does not change, nor does the UUID of the subscription plan associated
with the license at the time of enrollment.

What happens if a license is assigned but not activated until after renewal?
----------------------------------------------------------------------------

License activation relies on the association of an activation key to a license UUID. Since the license
will only transfer to a new plan, neither of those identifiers will change, and a user should not be hindered when
a license assigned to the original plan, but that is transferred to a renewed plan, prior to the user's
activation of such a license.

Non-injective renewals
----------------------

We will not support multiple "originating" plans renewing into a new, single future plan via renewal, because this
data model does not cleanly support such an action.

Definitional note: an `injective` function (also known as a `one-to-one` function) maps distinct elements
of its domain to distinct elements of its codomain.

Consequences
============

The consequences of the above decision on our customer learner and administrator user experiences, along with the
impact on our internal business reporting, are described below.

How does this impact the subscription learner experience?
---------------------------------------------------------

* It shouldn't.
* A user's existing license is transferred over to a new, active plan that should have the same catalog.
* Ther license UUID doesn't change.
* Nothing about their enrollment state should change.
* If we start tracking the subscription UUID at time of enrollment in the ``LicensedEnterpriseCourseEnrollment`` model,
that won't change, and we'll have a good (and easy to access) source of truth about the learner's subscription state
at the time of enrollment.

How does this impact the subscription customer admin. experience?
-----------------------------------------------------------------

These data models, and their associated modification logic, should provide us the flexibility we need to implement
whatever user experience we want in the enterprise admin portal. It allows us to represent to the admin user
any of the following pieces of information:

* That there is a renewal scheduled for a current subscription plan.
* When the renewal will take place.
* That a renewed plan now exists and is active.
* How many licenses the renewed and original plans contain.
* When the original and renewed plans expire.

How does this impact our business reporting?
--------------------------------------------

Assuming that the license enrollment ``plan_at_time_of_enrollment`` field is in place and backpopulated,
and that renewed plans record their own ``salesforce_opportunity_id`` and ``netsuite_product_id`` fields (which they do),
then our internal and external business reporting can continue to function as expected,
while still representing the true state of the world.

Misc. Open Questions
--------------------
* Can you renew with a different catalog product? What happens to enrollment/revenue allocation if that action
occurs in the middle of a month? Can we stipulate that all plans have to end at the end of a month, and that
renewals cannot begin in the middle of a month?
* Can we make the product ID/catalog non-editable after licensed enrollments begin to occur?

0 comments on commit 5a7abd7

Please sign in to comment.