Subscriptions v2.0 Architectural Changes

WooCommerce Subscriptions v2.0 introduced a major architectural change relating to how subscription data is stored and accessed.

This was the most important change in Subscriptions’ code base since it was first developed more than 3 years prior to the version 2.0 release. It was also the first time a major breaking change to Subscriptions’ code base was introduced.

While all reasonable efforts were undertaken to maintain backward compatibility with API functions and existing actions/filters, custom code written against the Subscriptions v1.5 code base may not be compatible with the v2.0. In particular, if code accesses a subscription’s meta data directly in the database, or against order (i.e. WC_Order) or cart (i.e. WC()->cart or WC_Cart) objects, it will most likely be broken.

This guide explains the history behind the old architecture and the reason changes were necessary. It then provides an overview of the changes to help understand how to update code written to integrate with Subscriptions.

If you have not already, you may wish to read the What’s New in Subscriptions v2.0 and Store Manager Guide to Multiple Subscriptions documents to get a non-technical overview of the changes introduced by Subscriptions v2.0.

Need to upgrade a payment gateway for version 2.0? Check out the Payment Gateway Upgrade Guide.

A History of Subscriptions

Almost 3 years from the time of writing, Subscriptions v1.0 was first being designed. It was being created to achieve one simple goal – connect WooCommerce with PayPal Standard’s Subscriptions.

Because PayPal offered off-site payment processing and subscription storage, little consideration was given to how to store and manage a subscription after it was purchased within WooCommerce. The most important aspect of the design was how to create a subscription product in WooCommerce and then how to pass the details of that product to PayPal on checkout. It’s no coincidence that the product components of Subscriptions’ architecture are not changing in v2.0. A subscription is still a custom product type that extends from a base WooCommerce product class.

However, because of the focus on PayPal and the features it provided, the original architecture had some major short-comings:

  1. it was not possible to purchase multiple different subscription products on different billing schedules in the same transaction. PayPal requires a customer to sign up for each subscription individually, so to purchase an annual, monthly and weekly subscription in the same transaction and use PayPal as the payment method, the customer would need to complete checkout with PayPal 3 times.
  2. Subscriptions only needed to know the PayPal Profile ID for the subscription and handle IPN messages in order to handle recurring payments. Because PayPal took care of all of this, the method by which subscription meta data (such as recurring totals, completed renewal payments etc.) were stored in WooCommerce was an afterthought. It was such an after thought, that renewal orders, now the backbone of a subscriptions record keeping and inventory management, did not exist until version v1.2.
  3. subscription management within the store, like modifying a subscription’s billing schedule, shipping, taxes or line items was not possible, because PayPal didn’t allow any of these details to be changed. As a result, when interfaces for these tasks were introduced in later versions of Subscriptions, they were also an afterthought that needed to be retrofitted to the original architecture.

Subscriptions v1.5 Architecture

v1.5 Data Storage – the Order

Because Subscriptions was first designed to simply link an order in a WooCommerce store with PayPal, Subscriptions v1.5 stored all meta data relating to a subscription on the original order created to record the purchase of a subscription product.

Because much of a subscription’s data was not even stored in v1.0, this meant that as newer versions of Subscriptions required new meta data to add new features with other payment gateways (like changing payment method or billing/shipping address), this data was retrofitted to the order. As a result, data was often stored in unexpected and disjointed places.

For example, a subscription’s data in v1.5 was stored in the following database tables:

  • wp_postmeta: some values designed to mirror the original order’s meta data on the subscription were stored in the post meta table against the original order, like _recurring_payment_method (which mirrored the original order’s _payment_method and was the payment method used to process renewals) or _recurring_order_total (which mirrored the original order’s _order_total and was the amount charged for renewal payments)
  • wp_woocommerce_order_items: some values mirroring the original order’s items were stored in the order item table, like recurring_tax item/s (which mirrored the original order’s tax item/s for recurring amounts) or recurring_shipping item/s (which mirrored the original order’s shipping item/s for recurring amounts)
  • wp_woocommerce_order_itemmeta: some values relating to the subscription product’s billing terms were stored in the order item meta table against the subscription product’s line item. For example, a subscription’s billing schedule (_subscription_period and _subscription_interval), status (_subscription_status) and important dates (_subscription_start_date_subscription_expiry_date_subscription_trial_expiry_date and _subscription_completed_payments) were all stored in the order item meta table.

This storage system was:

  • inefficient: querying subscriptions almost always required a number of JOIN statements and queries on columns of a generic varchar type (the type used for meta values). For example, a query to get subscriptions by start date required joining 3 tables: wp_postswp_woocommerce_order_items and wp_woocommerce_order_itemmeta to search for a MySQL formatted date stored as a varchar. This ultimately led to serious scaling issues once a store started to enjoy more than 20,000 subscribers. In v2.0, the same query is on a single table (wp_posts) in a column with a more accurate type (datetime) and it uses a schema proven to scale to hundreds of thousands of rows.
  • cumbersome: to find out any details on a subscription, you needed to know the order ID and often either the user ID and/or product ID for that subscription. Each subscription did not have a simple, unique ID which could be used to access it and pass around its data.
  • unintuitive: as a subscription has evolved into an important object, new developers coming to Subscriptions now expect to find a discrete subscription object, not a collection of data attached to an order.
  • repetitive: because this schema was completely unique to Subscriptions, it was not possible to take advantage of code already written in WordPress or WooCommerce for querying data. This makes simple feature requests, like improved reporting, difficult to implement because it requires custom database queries compared with using well known WordPress API functions like get_posts().

v1.5 Data Structure – the Array

As a subscription had very little data in v1.0, and that data rarely needed to be accessed or modified, the data structure chosen for accessing a subscription’s details at the application level was the most basic object available: an array.

As new versions of Subscriptions began to provide features for modifying a subscription, the array structure was maintained and separate functions were implemented to act on the subscription’s data, including the most primitive WordPress functions for acting on data like update_post_meta() or subscription specific functions like WC_Subscriptions_Manager::update_subscription().

Because an array can not have methods, this was the only approach available, despite native methods on the object offering a much more natural approach (e.g. $subscription->update_status()) and also providing many of the benefits of object oriented programming, like abstraction and encapsulation.

Evolution of the WooCommerce Ecosystem

In addition to the issues outlined above with the existing data storage and structure used for a subscription, the payment gateway and WooCommerce landscape evolved dramatically in the time since Subscriptions 1.0 was designed.

Uptake of Modern Payment Gateway APIs

In the 3 years since first designing Subscriptions, modern payment gateways that provide credit card tokens, like Stripe and CIM, have become more available to non-US markets and easier to access by non-technical store owners. This development has created an opportunity for Subscriptions’ feature set to be designed for these modern, more flexible payment methods, rather than the out-dated APIs offered by PayPal Standard.

Feedback on the Subscriptions feature set has consistently demanded more control over subscription data from within the WooCommerce store. This is possible with these modern payment gateways (but not PayPal Standard).

With Subscriptions 2.0 therefore, there was an opportunity and a need to design a system for these gateways that provides more control to store owners. Fortunately, there was also a recent development in WooCommerce that made such a change possible.

WooCommerce v2.2 and Custom Order Types

WooCommerce v2.2 quietly introduced a new API that could have great implications for plugins like Subscriptions – the custom order types API.

This API allows for more than just the standard simple order type to exist in a WooCommerce store.

An order is WooCommerce’s snapshot of history. In the case of a simple order, it is used to record a purchase transaction. In the case of a refund, it records the return of some or all of an order’s amount to the customer.

Unlike an order, a subscription is not a record of what has happened; instead, it is an agreement for what should happen. However, an order’s meta data, like customer billing and shipping details, product line items, taxes, fees and totals perfectly align with the details required for such an agreement. Therefore, a custom order type is the perfect data store for a subscription.

Store Manager Feedback

The final and most important driving force behind the change was the same factor that drives all of development on Subscriptions – customer feedback.

In addition to technical and market changes, our understanding of what store managers expect from a subscription management plugin evolved in the 3 years since releasing version 1.0.

Although no store manager has ever asked for a subscription to be stored as a custom order type, to support the numerous and diverse requests for all manner of features to be built on top of Subscriptions, the architecture needed to evolve. Many requested features, like upgrades/downgrades, updating payment method, changing shipping address, were implemented, painstakingly in some cases, on the Subscriptions 1.5 architecture.

However, many features could not be feasibly implemented with that architecture, such as the ability for customers to purchase different subscription products in a single transaction. Even simple requests, like gifting subscriptions, improved reporting or having shipping charged only on the initial order were extremely difficult to achieve with the v1.5 data scheme and structure.

The new architecture consolidates 3 years of feedback into a design that can support every feature request and behaviour change we’ve received in that time. Even if these do not make it into Subscriptions core, it will be possible to write them as mini-extensions with significantly less developer time than previously required.

Introducing the Subscription Order Type

The backbone of the v2.0 changes is a new subscription order type.

In much the same way as an order is stored as a 'shop_order' post type and instantiated as an instance of a WC_Order class, the subscription order type consists of a 'shop_subscription' post type and WC_Subscription class.

The 'shop_subscription' Post Type

The 'shop_subscription' post type is a WordPress custom post type created by wc_register_order_type() to be used for the storage of subscription data.

As this post type is an extension of the 'shop_order' post type, a subscription’s meta is stored in the same locations as an order’s data, making it possible for developers to learn where an order’s data is stored, and then instantly understand where a subscription’s data will be stored. For example, after being purchased in an order, a subscription product becomes a line item on both that order and a subscription, meaning its data is consistently stored as a line_item in wp_woocommerce_order_items table for both the 'shop_order' and 'shop_subscription'.

The WC_Subscription Class

The WC_Subscription class extends WC_Order to provide an object for interacting with a subscription’s data.

The object inherits all of the properties and methods of the WC_Order class (which also includes those of its parent – WC_Abstract_Order). These properties and methods make it significantly more intuitive to work with a subscription’s data. For example, in v2.0, changing a subscription’s status can be done with the WC_Abstract_Order->update_status() method (e.g. $subscription->update_status().

In addition, the WC_Subscription class also adds new properties for storing a subscription’s meta data, mainly relating to billing schedule and important dates, like the next payment date, and new methods for interacting with this data, like WC_Subscription->update_dates().

Benefits of the Subscription Order Type

The primary benefits of the subscription order type are:

  • WordPress’s post list table: as a subscription is now a custom post type, the custom list table used on the Manage Subscriptions screen has been replaced by a WordPress post list table. This saved hundreds of lines of code and helps provide better future proofing of the list table for updates to WordPress and WooCommerce markup and CSS.
  • Scalable storage: now that a subscription’s data is stored in the same way as other WordPress custom post types and WooCommerce orders, Subscriptions can take advantage of trusted API functions, like get_posts(), to work with subscription data and be confident that queries will scale.
  • DRY: by using a custom order type, Subscriptions can take advantage of WooCommerce code to add new features more quickly and with less code. An example of this in v2.0, beyond the obvious example of extending WC_Order, is the introduction of API endpoints for subscriptions. This is done by introducing a new WC_API_Subscriptions class which extends WC_API_Orders and benefits from much of the code in that class.
  • Learn once: developers familiar with working with WooCommerce’s orders can now transfer that knowledge entirely to working with subscriptions. Instead of having to learn an entirely new set of API functions and database schema, developers now need to only learn where the extra meta data is stored and how to access it.

Implications of the Subscription Order Type

By changing the database schema and application level data structure of a subscription, code handling everything from the initial purchase through to renewal and management of a subscription has had to be updated.

This is because old functions expected some combination of an order ID or WC_Order, subscription key (consisting of the order ID and product ID, not a unique integer ID for that object), user ID and/or product ID. Similarly, the hooks used for existing actions and filters would pass this data to callbacks; where as in v2.0, those callbacks need the WC_Subscriptions or ID of the 'shop_subscription' to take advantage of the new architecture.

Backward Compatibility

All reasonable steps have been taken to ensure existing functions and actions/filters will continue to work. However, use of deprecated functions, actions and filters will throw deprecated notices and they will be removed in future versions.

Furthermore, some hooks may have been called on events which no longer occur, like creating a pending subscription by adding an order added via the administration screen.

Finally, any code directly querying the database to read or write subscription data will no longer be compatible with the new schema in Subscriptions v2.0.

Other Changes

This guide focused on the introduction of the 'shop_subscription' post type and corresponding WC_Subscription class; however, there are a number of other notable changes introduced in Subscriptions v2.0 that related to developers.

Renewal Order Relationship Change

In Subscriptions v1.5, a subscription was connected to a renewal order using the renewal order’s post_parent column in the wp_posts database table.

Specifically, the post_parent was set to the post ID of the original order used to purchase a subscription (because a subscription in v1.5 did not have its own unique ID).

In v2.0, this relation was changed to use post meta on the subscription post type. A meta_key of _subscription_renewal storing a meta_value of the subscription’s post ID is now used to record this relationship.

This change makes it possible to have a many-to-one relationship between subscriptions and renewal orders. That is to say, a single renewal order can now be related to multiple subscriptions; where as in v1.5, a renewal order could only relate to a single subscription. Although this new capability isn’t being used in any meaningful way with the release of Subscriptions v2.0, it was introduced with v2.0 because the relationship needed to change to use the ID of a 'shop_subscription' post, not the ID of a 'shop_order' post.

As the relationship needed to change in v2.0 anyway, this change was implemented now to avoid another breaking change in the future version.

The many-to-one relationship makes it possible to implement advanced features in the future, like batch processing renewal payments for different subscriptions in a single renewal order. Such a feature would save store owners fees as most payment gateways charge a per transaction fee, like Stripe’s $0.20.

Renewal Order Creation Before Scheduled Payment Hook

Subscriptions v1.0 did not create renewal orders because PayPal recorded transaction details and sent store owners and customers the details of those transactions.

Accordingly, when renewal orders were introduced in version v1.2, renewal order creation was retrofitted to the existing codebase and therefore, renewal orders were only created after the payment gateway processed the payment.

This design had a few significant drawbacks:

  • payment gateways had to use special Subscription API functions and write special code for handling subscription renewal payments, instead of being able to use code written for WooCommerce’s order object that worked for all payments.
  • when WooCommerce v2.2 introduced the _transaction_id API for storing a payment gateway’s transaction on a renewal order, it was not possible for payment gateways to add the transaction ID for renewal orders without hacks.
  • if an unrecoverable error occurred when attempting to process the renewal payment, the renewal order would not be created. This also meant sites which had an unrecoverable error needed to run special code in order to generate missing orders.

Because of these drawbacks, Subscriptions v2.0 changes the flow of operations to create the renewal order before the scheduled subscription payment hook is triggered.

To learn how to take advantage of this change and use the renewal order for processing renewal payments with your payment gateway extension/s, refer to the Payment Gateway Upgrade Guide.

Changes to the Switching Process

If you are not familiar with the switching feature of Subscriptions (i.e. upgrading/downgrading), please read the switching guide before continuing with this section.

In Subscriptions v1.5, switching a subscription from one variation or grouped product to another always created a new subscription and set the status of the old subscription to switched. This was to ensure compatibility of the switching feature with PayPal, and to always avoid rewriting history and changing subscription related meta data on the original order used to purchase the subscription.

However, due to the architectural changes in v2.0, it is now possible to simply change the details of the subscription object without losing the history of the original order.

Furthermore, because a subscription may now contain multiple line items, switching needed to be updated to be per item instead of per subscription.

In v2.0, the switching process now:

  • removes the line item from the old subscription
  • adds the new item as a line item on the existing 'shop_subscription' if the new item is:
    • on the same billing schedule as the old subscription; or
    • on a different billing schedule, but is the only line item on the subscription, and therefore, the subscription’s billing schedule can be changed.
  • creates a new 'shop_subscription' if the new item is on a different billing schedule and the old subscription had more than one line item.

The switching process still uses the cart and checkout as it did in v1.5. A switch order is also still created to record the switch. This order is created regardless of whether proration is enabled and a prorated amount is charged for the switch or the switching cost is $0. Multiple switches can also be completed at the same time, for multiple line items on the same or different subscriptions.

New Pending Cancellation Status

Subscriptions v2.0 introduced a new subscription status: Pending Cancellation.

This status, internally stored as wc-pending-cancel (because wc-pending-cancellation is too long for the post_status column), is given to a subscription to signify that the customer or store manager has cancelled the subscription, but the customer is still entitled to a pre-paid term.

The status will be applied to a subscription whenever a customer or store manage cancels a subscription. It is set automatically when calling WC_Subscription::cancel_order(), but not when calling WC_Susbcription::update_status() to provide an API for developers that immediately transitions a subscription to cancelled status (the wc-cancelled status).

Prior to Subscriptions v2.0, the pre-paid term was signified only by an action triggered at the end of the pre-paid term ('subscription_end_of_prepaid_term'). It was left up to developers to use it as required, and was not used internally by Subscriptions.

Example of Pending Cancellation

If a customer buys a monthly subscription on the 1st January, then cancels the subscription on 15th January, in Subscriptions v1.5, its status would immediately be changed to wc-cancelled, the customer’s role would immediately be transitioned to the default inactive role and then on the 1st February, a 'subscription_end_of_prepaid_term' hook would be triggered.

With v2.0, that subscription’s status will be immediately set to wc-pending-cancel and the customer will keep the default active subscriber role. On the 1st February, the subscription’s status will be changed to wc-cancelled and the customer’s role will be changed to the inactive role. The end of prepaid term hook will also be triggered.

Store Manager Guide to v2.0 Changes

A summary of all the changes in relation to how a store manager interacts with the Subscriptions extension can also be found in the Overview of Subscriptions v2.0’s Changes document.

Was this article helpful?
Dislike 0
Views: 13