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 and prices defined in the
Buyable
Capability are automatically created and synced with Stripe. - 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
field on a Buyable Entity is- statically stored.
- 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
- (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
-
- Required fields
- Billable
- Integrate Laravel Cashier
- Handle webhooks
- Handle product / price creation via bakery planner from the capability
- build subscription route
- build subscription manager (customer portal) route
- Throw and build workflow events
- 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