mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 05:37:34 +03:00
5f928794c3
refs https://github.com/TryGhost/Team/issues/2078 These events are all required for other parts of the Ghost system to stay in sync. The events on each Tier object will be dispatched by the TierRepository once they've been persisted. TierCreatedEvent - generate Stripe Products & Prices TierNameChangeEvent - update Stripe Products TierPriceChangeEvent - update Stripe Products & Prices TierArchivedEvent - update the Portal settings for visible tiers - disable Stripe Products & Prices TierActivatedEvent - enable Stripe Products & Prices
486 lines
13 KiB
JavaScript
486 lines
13 KiB
JavaScript
const ObjectID = require('bson-objectid').default;
|
|
const {ValidationError} = require('@tryghost/errors');
|
|
|
|
const TierActivatedEvent = require('./TierActivatedEvent');
|
|
const TierArchivedEvent = require('./TierArchivedEvent');
|
|
const TierCreatedEvent = require('./TierCreatedEvent');
|
|
const TierNameChangeEvent = require('./TierNameChangeEvent');
|
|
const TierPriceChangeEvent = require('./TierPriceChangeEvent');
|
|
|
|
module.exports = class Tier {
|
|
/** @type {BaseEvent[]} */
|
|
events = [];
|
|
|
|
/** @type {ObjectID} */
|
|
#id;
|
|
get id() {
|
|
return this.#id;
|
|
}
|
|
|
|
/** @type {string} */
|
|
#slug;
|
|
get slug() {
|
|
return this.#slug;
|
|
}
|
|
|
|
/** @type {string} */
|
|
#name;
|
|
get name() {
|
|
return this.#name;
|
|
}
|
|
set name(value) {
|
|
const newName = validateName(value);
|
|
if (newName === this.#name) {
|
|
return;
|
|
}
|
|
this.events.push(TierNameChangeEvent.create({tier: this}));
|
|
this.#name = newName;
|
|
}
|
|
|
|
/** @type {string[]} */
|
|
#benefits;
|
|
get benefits() {
|
|
return this.#benefits;
|
|
}
|
|
set benefits(value) {
|
|
this.#benefits = validateBenefits(value);
|
|
}
|
|
|
|
/** @type {string} */
|
|
#description;
|
|
get description() {
|
|
return this.#description;
|
|
}
|
|
set description(value) {
|
|
this.#description = validateDescription(value);
|
|
}
|
|
|
|
/** @type {string} */
|
|
#welcomePageURL;
|
|
get welcomePageURL() {
|
|
return this.#welcomePageURL;
|
|
}
|
|
set welcomePageURL(value) {
|
|
this.#welcomePageURL = validateWelcomePageURL(value);
|
|
}
|
|
|
|
/** @type {'active'|'archived'} */
|
|
#status;
|
|
get status() {
|
|
return this.#status;
|
|
}
|
|
set status(value) {
|
|
const newStatus = validateStatus(value);
|
|
if (newStatus === this.#status) {
|
|
return;
|
|
}
|
|
if (newStatus === 'active') {
|
|
this.events.push(TierActivatedEvent.create({tier: this}));
|
|
} else {
|
|
this.events.push(TierArchivedEvent.create({tier: this}));
|
|
}
|
|
this.#status = newStatus;
|
|
}
|
|
|
|
/** @type {'public'|'none'} */
|
|
#visibility;
|
|
get visibility() {
|
|
return this.#visibility;
|
|
}
|
|
set visibility(value) {
|
|
this.#visibility = validateVisibility(value);
|
|
}
|
|
|
|
/** @type {'paid'|'free'} */
|
|
#type;
|
|
get type() {
|
|
return this.#type;
|
|
}
|
|
|
|
/** @type {number|null} */
|
|
#trialDays;
|
|
get trialDays() {
|
|
return this.#trialDays;
|
|
}
|
|
set trialDays(value) {
|
|
this.#trialDays = validateTrialDays(value, this.#type);
|
|
}
|
|
|
|
/** @type {string|null} */
|
|
#currency;
|
|
get currency() {
|
|
return this.#currency;
|
|
}
|
|
set currency(value) {
|
|
this.#currency = validateCurrency(value, this.#type);
|
|
}
|
|
|
|
/** @type {number|null} */
|
|
#monthlyPrice;
|
|
get monthlyPrice() {
|
|
return this.#monthlyPrice;
|
|
}
|
|
set monthlyPrice(value) {
|
|
this.#monthlyPrice = validateMonthlyPrice(value, this.#type);
|
|
}
|
|
|
|
/** @type {number|null} */
|
|
#yearlyPrice;
|
|
get yearlyPrice() {
|
|
return this.#yearlyPrice;
|
|
}
|
|
set yearlyPrice(value) {
|
|
this.#yearlyPrice = validateYearlyPrice(value, this.#type);
|
|
}
|
|
|
|
updatePricing({currency, monthlyPrice, yearlyPrice}) {
|
|
if (this.#type !== 'paid') {
|
|
throw new ValidationError({
|
|
message: 'Cannot set pricing for free tiers'
|
|
});
|
|
}
|
|
|
|
const newCurrency = validateCurrency(currency, this.#type);
|
|
const newMonthlyPrice = validateMonthlyPrice(monthlyPrice, this.#type);
|
|
const newYearlyPrice = validateYearlyPrice(yearlyPrice, this.#type);
|
|
|
|
if (newCurrency === this.#currency && newMonthlyPrice === this.#monthlyPrice && newYearlyPrice === this.#yearlyPrice) {
|
|
return;
|
|
}
|
|
|
|
this.#currency = newCurrency;
|
|
this.#monthlyPrice = newMonthlyPrice;
|
|
this.#yearlyPrice = newYearlyPrice;
|
|
|
|
this.events.push(TierPriceChangeEvent.create({
|
|
tier: this
|
|
}));
|
|
}
|
|
|
|
/** @type {Date} */
|
|
#createdAt;
|
|
get createdAt() {
|
|
return this.#createdAt;
|
|
}
|
|
|
|
/** @type {Date|null} */
|
|
#updatedAt;
|
|
get updatedAt() {
|
|
return this.#updatedAt;
|
|
}
|
|
|
|
toJSON() {
|
|
return {
|
|
id: this.#id,
|
|
slug: this.#slug,
|
|
name: this.#name,
|
|
description: this.#description,
|
|
welcomePageURL: this.#welcomePageURL,
|
|
status: this.#status,
|
|
visibility: this.#visibility,
|
|
type: this.#type,
|
|
trialDays: this.#trialDays,
|
|
currency: this.#currency,
|
|
monthlyPrice: this.#monthlyPrice,
|
|
yearlyPrice: this.#yearlyPrice,
|
|
createdAt: this.#createdAt,
|
|
updatedAt: this.#updatedAt,
|
|
benefits: this.#benefits
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
constructor(data) {
|
|
this.#id = data.id;
|
|
this.#slug = data.slug;
|
|
this.#name = data.name;
|
|
this.#description = data.description;
|
|
this.#welcomePageURL = data.welcome_page_url;
|
|
this.#status = data.status;
|
|
this.#visibility = data.visibility;
|
|
this.#type = data.type;
|
|
this.#trialDays = data.trial_days;
|
|
this.#currency = data.currency;
|
|
this.#monthlyPrice = data.monthly_price;
|
|
this.#yearlyPrice = data.yearly_price;
|
|
this.#createdAt = data.created_at;
|
|
this.#updatedAt = data.updated_at;
|
|
this.#benefits = data.benefits;
|
|
}
|
|
|
|
/**
|
|
* @param {any} data
|
|
* @returns {Promise<Tier>}
|
|
*/
|
|
static async create(data) {
|
|
let id;
|
|
let isNew = false;
|
|
if (!data.id) {
|
|
isNew = true;
|
|
id = new ObjectID();
|
|
} else if (typeof data.id === 'string') {
|
|
id = ObjectID.createFromHexString(data.id);
|
|
} else if (data.id instanceof ObjectID) {
|
|
id = data.id;
|
|
} else {
|
|
throw new ValidationError({
|
|
message: 'Invalid ID provided for Tier'
|
|
});
|
|
}
|
|
|
|
let name = validateName(data.name);
|
|
|
|
let slug = validateSlug(data.slug);
|
|
let description = validateDescription(data.description);
|
|
let welcomePageURL = validateWelcomePageURL(data.welcomePageURL);
|
|
let status = validateStatus(data.status || 'active');
|
|
let visibility = validateVisibility(data.visibility || 'public');
|
|
let type = validateType(data.type || 'paid');
|
|
let currency = validateCurrency(data.currency || null, type);
|
|
let trialDays = validateTrialDays(data.trialDays || 0, type);
|
|
let monthlyPrice = validateMonthlyPrice(data.monthlyPrice || null, type);
|
|
let yearlyPrice = validateYearlyPrice(data.yearlyPrice || null , type);
|
|
let createdAt = validateCreatedAt(data.createdAt);
|
|
let updatedAt = validateUpdatedAt(data.updatedAt);
|
|
let benefits = validateBenefits(data.benefits);
|
|
|
|
const tier = new Tier({
|
|
id,
|
|
slug,
|
|
name,
|
|
description,
|
|
welcome_page_url: welcomePageURL,
|
|
status,
|
|
visibility,
|
|
type,
|
|
trial_days: trialDays,
|
|
currency,
|
|
monthly_price: monthlyPrice,
|
|
yearly_price: yearlyPrice,
|
|
created_at: createdAt,
|
|
updated_at: updatedAt,
|
|
benefits
|
|
});
|
|
|
|
if (isNew) {
|
|
tier.events.push(TierCreatedEvent.create({tier}));
|
|
}
|
|
|
|
return tier;
|
|
}
|
|
};
|
|
|
|
function validateSlug(value) {
|
|
if (!value || typeof value !== 'string' || value.length > 191) {
|
|
throw new ValidationError({
|
|
message: 'Tier slug must be a string with a maximum of 191 characters'
|
|
});
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function validateName(value) {
|
|
if (typeof value !== 'string') {
|
|
throw new ValidationError({
|
|
message: 'Tier name must be a string with a maximum of 191 characters'
|
|
});
|
|
}
|
|
|
|
if (value.length > 191) {
|
|
throw new ValidationError({
|
|
message: 'Tier name must be a string with a maximum of 191 characters'
|
|
});
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
function validateWelcomePageURL(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
if (value === null || typeof value === 'string') {
|
|
return value;
|
|
}
|
|
throw new ValidationError({
|
|
message: 'Tier Welcome Page URL must be a string'
|
|
});
|
|
}
|
|
|
|
function validateDescription(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
if (typeof value !== 'string') {
|
|
throw new ValidationError({
|
|
message: 'Tier description must be a string with a maximum of 191 characters'
|
|
});
|
|
}
|
|
if (value.length > 191) {
|
|
throw new ValidationError({
|
|
message: 'Tier description must be a string with a maximum of 191 characters'
|
|
});
|
|
}
|
|
}
|
|
|
|
function validateStatus(value) {
|
|
if (value !== 'active' && value !== 'archived') {
|
|
throw new ValidationError({
|
|
message: 'Tier status must be either "active" or "archived"'
|
|
});
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function validateVisibility(value) {
|
|
if (value !== 'public' && value !== 'none') {
|
|
throw new ValidationError({
|
|
message: 'Tier visibility must be either "public" or "none"'
|
|
});
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function validateType(value) {
|
|
if (value !== 'paid' && value !== 'free') {
|
|
throw new ValidationError({
|
|
message: 'Tier type must be either "paid" or "free"'
|
|
});
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function validateTrialDays(value, type) {
|
|
if (type === 'free') {
|
|
if (value) {
|
|
throw new ValidationError({
|
|
message: 'Free Tiers cannot have a trial'
|
|
});
|
|
}
|
|
return 0;
|
|
}
|
|
if (!value) {
|
|
return 0;
|
|
}
|
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
throw new ValidationError({
|
|
message: 'Tier trials must be an integer greater than 0'
|
|
});
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function validateCurrency(value, type) {
|
|
if (type === 'free') {
|
|
if (value !== null) {
|
|
throw new ValidationError({
|
|
message: 'Free Tiers cannot have a currency'
|
|
});
|
|
}
|
|
return null;
|
|
}
|
|
if (typeof value !== 'string') {
|
|
throw new ValidationError({
|
|
message: 'Tier currency must be a 3 letter ISO currency code'
|
|
});
|
|
}
|
|
if (value.length !== 3) {
|
|
throw new ValidationError({
|
|
message: 'Tier currency must be a 3 letter ISO currency code'
|
|
});
|
|
}
|
|
return value.toUpperCase();
|
|
}
|
|
|
|
function validateMonthlyPrice(value, type) {
|
|
if (type === 'free') {
|
|
if (value !== null) {
|
|
throw new ValidationError({
|
|
message: 'Free Tiers cannot have a monthly price'
|
|
});
|
|
}
|
|
return null;
|
|
}
|
|
if (!Number.isSafeInteger(value)) {
|
|
throw new ValidationError({
|
|
message: 'Tier prices must be an integer.'
|
|
});
|
|
}
|
|
if (value < 0) {
|
|
throw new ValidationError({
|
|
message: 'Tier prices must not be negative'
|
|
});
|
|
}
|
|
if (value > 9999999999) {
|
|
throw new ValidationError({
|
|
message: 'Tier prices may not exceed 999999.99'
|
|
});
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function validateYearlyPrice(value, type) {
|
|
if (type === 'free') {
|
|
if (value !== null) {
|
|
throw new ValidationError({
|
|
message: 'Free Tiers cannot have a yearly price'
|
|
});
|
|
}
|
|
return null;
|
|
}
|
|
if (!Number.isSafeInteger(value)) {
|
|
throw new ValidationError({
|
|
message: 'Tier prices must be an integer.'
|
|
});
|
|
}
|
|
if (value < 0) {
|
|
throw new ValidationError({
|
|
message: 'Tier prices must not be negative'
|
|
});
|
|
}
|
|
if (value > 9999999999) {
|
|
throw new ValidationError({
|
|
message: 'Tier prices may not exceed 999999.99'
|
|
});
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function validateCreatedAt(value) {
|
|
if (!value) {
|
|
return new Date();
|
|
}
|
|
if (value instanceof Date) {
|
|
return value;
|
|
}
|
|
throw new ValidationError({
|
|
message: 'Tier created_at must be a date'
|
|
});
|
|
}
|
|
|
|
function validateUpdatedAt(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
if (value instanceof Date) {
|
|
return value;
|
|
}
|
|
throw new ValidationError({
|
|
message: 'Tier created_at must be a date'
|
|
});
|
|
}
|
|
|
|
function validateBenefits(value) {
|
|
if (!value) {
|
|
return [];
|
|
}
|
|
if (!Array.isArray(value) || !value.every(item => typeof item === 'string')) {
|
|
throw new ValidationError({
|
|
message: 'Tier benefits must be a list of strings'
|
|
});
|
|
}
|
|
return value;
|
|
}
|