Subscription Plans
Basic concept
Some things in a brezel system can be bought / unlocked with a paywall.
For that, there is the concept of “plans” which can be bought/activated via a subscription. These define prices and most importantly, “Features” that can be used to enable or disable things in the system.
Feature summary
- A
User
entity with the “Billable” Capability can buy a plan for anEntity
. - The
User
will be registered as a customer in stripe and charged for the selected plan by stripe. - Only entities from
Modules
that use theBuyable
Capability can be bought. In the options of that capability, thesubscription_plans
which contain feature flags and prices are defined. - The
Entity
will then have the fieldsactive_plans
andactive_features
set, which can be used by workflows, policies, recipes etc. to enable or disable features. - To react on payment events, there are several workflow events that can be used to hook into.
Additional features to manage plans
- Ability to build an overview that see all active plans
- Ability to manually assign active plans to an entity
- Log for payment activities
- “User
A
bought planB
for entityC
for priceD
at timeE
” - “Plan
B
was deactivated for entityC
at timeE
” - “Payment by User
A
for planB
for entityC
failed at timeE
”
- “User
- Ability to start a Stripe customer portal session for a user to manage their subscriptions
Use cases
Buy something for an Entity
A User can buy a plan on an Entity. The system could then enable or disable features for things concerning that Entity.
Examples:
- Unlocking “premium” features for a website in KAB.page
- A user buys a “premium” plan for a website.
- The system enables the “premium” features for that website.
- Unlocking “shop” features for a website in KAB.page
- A user buys a “shop” plan for a website.
- The system enables the “shop” features for that website.
Buy something for a Client
A User can buy a plan for a Client (which is just the same as any entity). The system can then enable or disable features for things concerning that Client. That could include all Users and Entities of that Client.
Examples:
- Enabling client wide features
- The user buys the plan to use feature X for his client
- The system enables feature X for all Users and Entities of that Client.
- e.g. allowing access to a new module / feature-set
- SAAS style plans
- A user buys a “business” plan for a Client.
- The system enables the “business” features for that Client.
- That allows X users to be created for that client
- Enables X feature (e.g. DATEV export) for bills of that client
- The system disables the “business” features for all Users and Entities of that Client when the plan expires.
- All users except the main user that bought the plan (hopefully the boss) are disabled and cannot log in anymore.
- Or just disable the creation of any new entities for that client.
How to react to payment events?
There are several workflow events that can be used to react to payment events.
Some are to be used for feature management and day-to-day operations.
These all return the Entity
in question, the User
entity that bought / tried to buy the plan, and the Plan
itself.
event/planPaymentFailed
: Fires when a payment for a plan failed for an entity of the specifiedmodule
.event/planActivated
: Fires when a plan was activated for an entity of the specifiedmodule
.event/planDeactivated
: Fires when a plan was deactivated (e.g. when a subscription runs out) for an entity of the specifiedmodule
.
Some are to be used for plan management and just return the Plan
in question.
event/planFeaturesChanged
: Fires when the features of a plan were changed for the specifiedmodule
.event/planDeleted
: Fires when an existing plan was deleted for the specifiedmodule
.
Architecture overview
Buyable
Type definitions on module level:
type Plan = { id: string // Unique identifier for that plan. Used as `lookup_key` for stripe product and as identifier to use when checking for features. name: string // Human readable name of the plan and name of the product in stripe. features: Feature[] // Features of the plan. The same feature can be used in multiple plans. price: Price[] // Prices of the plan.}
type Feature = { identifier: string // Unique identifier for the feature. Can be used to enable or disable the feature in the system. name: string // Human readable name of the feature.}
type Price = { identifier: string // Unique identifier for the price. Used as `lookup_key` for stripe price. price: number // Price with 2 decimal places. E.g. 9.99 currency: string // Currency to use in stripe. E.g. "eur", "usd", "inr" interval: 'month'|'year' // Payment interval to be used for the subscription.}
Fields present on an Entity that could have active plans:
type Entity = { active_plans: PurchasedPlan[] // Array of active plans for that entity. active_features: string[] // Array of active features for that entity. This is derived from all features of all active plans. // ... All other fields of the entity, including id, module_id etc.}
// Very similar to the Plan type used in the configuration, but has only one price (the one, that was chosen by the user to purchase the plan).type PurchasedPlan = { id: string // Unique identifier for that plan. Used as `lookup_key` for stripe product and as identifier to use when checking for features. name: string // Human readable name of the plan and name of the product in stripe. features: Feature[] // Features of the plan. price: Price // Price the plan was bought with.}
Billable
Field requirements on entity that should be able to buy something:
[ { "identifier": "name", // Name of the user to use in stripe "type": "text", options: { "rules": "required" // This is required for stripe taxes to work } }, { "identifier": "email", // Email to register the user in stripe with "type": "email", options: { "rules": "required" // This is required for stripe taxes to work } }, { "identifier": "country_code", // ISO 3166-1 alpha-2 country code of the user "type": "text", options: { "rules": "required" // This is required for stripe taxes to work } }, { "identifier": "postal_code", // Postal code of the user "type": "text", options: { "rules": "required" // This is required for stripe taxes to work } }]
Usage
- Add the
Billable
capability to a User entity. - Define a module with the
Buyable
capability. - Configure the available
subscription_plans
in the options of that capability. - Make the user buy a plan for an entity somehow. (TODO)
- Use the
active_features
fields on the entity to enable or disable features in your workflows, policies, recipes etc.
Example configuration of a user
This defines a user with the Billable
capability.
It uses all the default fields (will create them in this case) except for the name
field, which is mapped to the existing full_name
field.
JSON Configuration for a user
{ "resource_user": { "identifier": "user", "type": "user", "options": { "capabilities": [ { "identifier": "Billable", "options": { "field_mapping": { "name": "full_name" } } } ] }, "fields": [ { "identifier": "full_name", "type": "text" } ] }}
Example configuration of a module
This defines two plans with the same features payable either monthly and yearly either in $ or €.
Includes another “lite” plan with only one of the features.
Checking for features has to be done via permissions / policies / workflows using the fields on the Entity.
JSON Configuration for a module
{ "resource_module": "buyable_thingy", "resource": { "identifier": "buyable_thingy", "type": "module", "options": { "capabilities": [ { "identifier": "Buyable", "options": { "subscription_plans": [ { "identifier": "pro_monthly", "name": "Pro subscription monthly", "description": "This gives you cool PRO features", "features": [ { "identifier": "pro_feature_x", "name": "Feature X" }, { "identifier": "pro_feature_y", "name": "Feature Y" } ], "prices": [ { "identifier": "pro_monthly_usd", "price": "21.00", "currency": "usd", "interval": "month" }, { "identifier": "pro_monthly_eur", "price": "20.00", "currency": "eur", "interval": "month" } ] }, { "identifier": "pro_yearly", "name": "Pro subscription yearly", "description": "This gives you cool PRO features", "features": [ { "identifier": "pro_feature_x", "name": "Feature X" }, { "identifier": "pro_feature_y", "name": "Feature Y" } ], "prices": [ { "identifier": "pro_yearly_usd", "price": "180.00", "currency": "usd", "interval": "year" }, { "identifier": "pro_yearly_eur", "price": "180.00", "currency": "eur", "interval": "year" } ] }, { "identifier": "lite", "name": "Lite subscription", "description": "Get a taste of some PRO features", "features": [ { "identifier": "pro_feature_x", "name": "Feature X" } ], "prices": [ { "identifier": "pro_monthly_usd", "price": "16.00", "currency": "usd", "interval": "month" }, { "identifier": "pro_monthly_eur", "price": "15.00", "currency": "eur", "interval": "month" } ] } ] } } ] } }}
Technical implementation details
- Modules with the
Buyable
capability can be bought by users with theBillable
capability. - The plans (products in stripe) and their prices defined in the
Buyable
Capability are automatically created and synced with Stripe at bakery apply time.- Uses the
identifier
aslookup_key
for the price in stripe as this is the “identifying information” we use when buying a plan - The identifier of the plan is set as the metadata key
plan_identifier
on the stripe product. - Combination of price and plan is validated to prevent a user from buying a “higher value” plan with a lower price from another plan
- Uses the
- The
Billable
capability requires the following fields to exist and be filled on the user Entity:email: email
: The email of the user. Should already be there as it is a default field.name: text
: The name of the user in stripe.country_code: text
: ISO 3166-1 alpha-2 country code the user is located in.postal_code: text
: The postal code of the user.
- The fields on the
Billable
capability can be mapped to re-use existing fields in the options of the capability.- This is done via the
field_mapping
option.- The key is one of the field identifiers above, and the value is the identifier of the field to use.
Custom mapped fields have to exist and be filled! It is best to use
"rules": "required"
on them
- The key is one of the field identifiers above, and the value is the identifier of the field to use.
Custom mapped fields have to exist and be filled! It is best to use
- This is done via the
- The
active_features
andactive_plans
fields on a Buyable Entity are- statically stored list of “slugs” (the identifiers of all features (unique) from all plans the entity has and the identifiers of all plans of an entity respectively).
- updated when the important events (that also trigger the workflow events) are fired.
- When the active plans changed
- i.e. new plan was bought or an existing plan was canceled / deleted
- When the underlying plan changed
- i.e. adding or removing features from a plan
- When the active plans changed
active_features
is a list of all “feature flags” that should be enabled for that entity.- They are derived from the features of all active plans and are a unique list of comma separated feature identifiers as configured.
- (Before deleting a plan (or changing its unique identifier), the bakery planner warns and asks confirmation / needs force.)
- When existing plans are DELETED:
- all active subscriptions for that plan are canceled.
- the plan is removed from stripe.
- the plan is removed from all entities.
- including the active features.
Use Cases
Technical step, by step detailing of common use cases.
Buying a plan
- Request to
/modules/<module_identifier>/resources/<entity_id>/subscribe/<plan_identifier>
with payload:{"redirect_url": "<SPA URL to current entity detail view>/subscription", // e.g. /modules/buyable_thingy/1234/subscription"price": "<price_identifier>", // e.g. pro_monthly_usd"user_ip": "<known user ip>", // required for stripe tax location validation"coupon": "[coupon_identifier]" // optional coupon to apply} - User (extracted from auth info) will be redirected to stripe checkout page.
- With prefilled email and name as well as required tax info (country and postal code).
- When finished, User will be redirected by stripe to SPA (e.g.
/modules/buyable_thingy/1234/subscription
)- On success with path params:
res=success
- On cancel with path params:
res=cancelled
- This will cause the user to be shown the (reloaded) detail view of the entity that was bought.
- Can be captured by widgets on the detail view to show subscription status by looking at the new values of
active_features
(oractive_plans
).
- Can be captured by widgets on the detail view to show subscription status by looking at the new values of
- On success with path params:
Canceling a subscription, checking active subscriptions, changing payment method etc.
- Request to
/modules/<module_identifier>/resources/<entity_id>/subscribtion-manager
- User will be redirected to stripe customer portal.
If cancelling something, the application will handle this via the webhooks.
Status / Roadmap
- Writeup concepts of a plan
- Remove old license and subscription code
- brezel/api
- brezel/spa
- Enable Capabilities to take options
- Enable Capabilities to require even existing fields (
rules required
, not just that the field exists at all)
- Enable Capabilities to require even existing fields (
- Implement Capabilities
- Billable
- Required fields
- Buyable
- Required fields
-
active_features
-
active_plans
-
- Pass an entity and set all active plans and features
- Required fields
- Billable
- Integrate Laravel Cashier
- Handle webhooks
- Handle product / price creation via bakery planner from the capability
- create products and prices fresh
- handle existing products / prices
- products are looked up by metadata key and updated if changed
- prices are looked up by lookup_key and if changed, old one is archived and new one is created only when changed
- if prices are removed (i.e. present on the product but NOT defined by us), archive them. But only if they came from us in the first place (i.e. metadata key
managed_by_brezel
is set`)
- build subscription route
- build subscription manager (customer portal) route
- handle taxes
- sync customer data to stripe on Billable user update
- Include e.g. the title of the bought entity in a place, the customer can see on the billing dashboard
- Create endpoint to list all available plans with prices for a module
- Throw and build workflow events
- Handle feature flags / subscription events
- Every time a subscription is activated/deactivated
- set
active_features
on the right entity (based on the metadata of the subscription) - set
active_plans
on the right entity (based on the metadata of the subscription)
- set
- update the
active_*
fields when a user changed the subscription in other ways, i.e. changed to a different plan- TODO: DO WE WANT THIS?
- When the features of a plan changed, update all entities that have that plan active
- Every time a subscription is activated/deactivated
- Build migration
- Maybe migrate over old data from
entity_subscription
table to the new way of storing / registering subscriptions- Plans and prices probably changed IDs, this has to be handled somehow
- Remove old tables and columns
- license stuff
license_plan_module
tablelicense_plans
tablelicenses
table- All fields, need to find them
- old subscription stuff
module_subscription
tablemodule_subscription_prices
tableentity_subscription
table- All fields, need to find them
- license stuff
- Maybe migrate over old data from
- Build some default SPA handling for all of this
TODOs
- Figure out what to do with custom values for features. Currently, when a plan update happens, only features defined in the active plans are kept.
- Do we want to keep the custom / unknown feature flag values for features that are not in any defined plan?
- This would prevent a user from loosing access to a feature that we now no longer sell (i.e. removed from the plan)
- And it would enable us to use custom feature flags for things that are not in any plan.
- Which I think is a good idea. But we kindof already have roles for that (i.e. giving access to a feature without buying it)?
- Do we want to keep the custom / unknown feature flag values for features that are not in any defined plan?