@flowfuse/flowfuse
Version:
An open source low-code development platform
142 lines (133 loc) • 5.2 kB
JavaScript
const {
DataTypes
} = require('sequelize')
const STATUS = {
// Any changes to this list *must* be made via migration.
// See forge/db/migrations/20230130-01-add-subscription-trial-date.js for example
// A subset of the statuses on Stripe that are important to FlowFuse
// https://stripe.com/docs/billing/subscriptions/overview#subscription-statuses
ACTIVE: 'active',
CANCELED: 'canceled',
PAST_DUE: 'past_due',
// Local only status, not from Stripe
TRIAL: 'trial',
// Local only - means this team's subscription on stripe is not
// managed by the platform
UNMANAGED: 'unmanaged'
}
Object.freeze(STATUS)
// Statuses used to track team trial state
const TRIAL_STATUS = {
// Any changes to this list *must* be made via migration.
// See forge/db/migrations/20230130-01-add-subscription-trial-date.js for example
NONE: 'none',
CREATED: 'created',
WEEK_EMAIL_SENT: 'week_email_sent',
DAY_EMAIL_SENT: 'day_email_sent',
ENDED: 'ended'
}
Object.freeze(TRIAL_STATUS)
module.exports = {
name: 'Subscription',
schema: {
// Customer ID from stripe, e.g. cus_xyz123
// Each team has one subscription, that is tied to a single stripe customer, via the customer ID field
customer: {
type: DataTypes.STRING,
allowNull: false
},
// Subscription ID from stripe e.g. sub_xyz123
// Stored for reference only, Stripe events should only be matched to subscription objects via
// the customer field
subscription: {
type: DataTypes.STRING,
allowNull: false
},
status: {
type: DataTypes.ENUM(Object.values(STATUS)),
allowNull: false,
defaultValue: STATUS.ACTIVE
},
trialStatus: {
type: DataTypes.ENUM(Object.values(TRIAL_STATUS)),
allowNull: false,
defaultValue: TRIAL_STATUS.NONE
},
trialEndsAt: { type: DataTypes.DATE, defaultValue: null, allowNull: true }
},
associations: function (M) {
this.belongsTo(M.Team)
M.Team.hasOne(this)
},
finders: function (M) {
const self = this
return {
instance: {
// States:
// isActive : Billing details setup
// isUnmanaged : The subscription on stripe is manually managed - no further setup required
// isTrial : In trial mode, no billing details setup, trial might have ended (isTrialEnded)
// isActive && !isTrialEnded : Started as a trial, added billing, still in trial period
// isActive && isTrialEnded : Started as trial, added billing, trial ended
// Should this subscription be treated as active/usable
isActive () {
return this.status === STATUS.ACTIVE || this.status === STATUS.PAST_DUE
},
isUnmanaged () {
return this.status === STATUS.UNMANAGED
},
isCanceled () {
return this.status === STATUS.CANCELED
},
isPastDue () {
return this.status === STATUS.PAST_DUE
},
isTrial () {
// This subscription is in trial mode without billing setup
return !!this.trialEndsAt || this.status === STATUS.TRIAL
},
isTrialEnded () {
// A Subscription can have status === ACTIVE but still be in
// trial mode. This means they have entered billing details
// but the trial has not yet expired.
//
return !this.trialEndsAt || this.trialEndsAt < Date.now()
},
async clearTrialState () {
this.trialEndsAt = null
this.trialStatus = TRIAL_STATUS.ENDED
return this.save()
}
},
static: {
STATUS,
TRIAL_STATUS,
byTeamId: async function (teamId) {
if (typeof teamId === 'string') {
teamId = M.Team.decodeHashid(teamId)
}
return self.findOne({
where: {
TeamId: teamId
},
include: {
model: M.Team,
attributes: ['id', 'name', 'slug', 'links']
}
})
},
byCustomerId: async function (stripeCustomerId) {
return self.findOne({
where: {
customer: stripeCustomerId
},
include: {
model: M.Team,
attributes: ['id', 'name', 'slug', 'links']
}
})
}
}
}
}
}