diff --git a/ghost/tiers/lib/Tier.js b/ghost/tiers/lib/Tier.js index 924bcb56a1..086a8535ab 100644 --- a/ghost/tiers/lib/Tier.js +++ b/ghost/tiers/lib/Tier.js @@ -1,7 +1,16 @@ 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() { @@ -20,7 +29,12 @@ module.exports = class Tier { return this.#name; } set name(value) { - this.#name = validateName(value); + const newName = validateName(value); + if (newName === this.#name) { + return; + } + this.events.push(TierNameChangeEvent.create({tier: this})); + this.#name = newName; } /** @type {string[]} */ @@ -56,7 +70,16 @@ module.exports = class Tier { return this.#status; } set status(value) { - this.#status = validateStatus(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'} */ @@ -110,6 +133,30 @@ module.exports = class Tier { 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() { @@ -169,7 +216,9 @@ module.exports = class 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); @@ -197,7 +246,7 @@ module.exports = class Tier { let updatedAt = validateUpdatedAt(data.updatedAt); let benefits = validateBenefits(data.benefits); - return new Tier({ + const tier = new Tier({ id, slug, name, @@ -214,6 +263,12 @@ module.exports = class Tier { updated_at: updatedAt, benefits }); + + if (isNew) { + tier.events.push(TierCreatedEvent.create({tier})); + } + + return tier; } }; diff --git a/ghost/tiers/lib/TierActivatedEvent.js b/ghost/tiers/lib/TierActivatedEvent.js new file mode 100644 index 0000000000..18b717118b --- /dev/null +++ b/ghost/tiers/lib/TierActivatedEvent.js @@ -0,0 +1,30 @@ +/** + * @typedef {object} TierActivatedEventData + * @prop {Tier} tier + */ + +class TierActivatedEvent { + /** @type {TierActivatedEventData} */ + data; + /** @type {Date} */ + timestamp; + + /** + * @param {TierActivatedEvent} data + * @param {Date} timestamp + */ + constructor(data, timestamp) { + this.data = data; + this.timestamp = timestamp; + } + + /** + * @param {TierActivatedEvent} data + * @param {Date} [timestamp] + */ + static create(data, timestamp = new Date()) { + return new TierActivatedEvent(data, timestamp); + } +} + +module.exports = TierActivatedEvent; diff --git a/ghost/tiers/lib/TierArchivedEvent.js b/ghost/tiers/lib/TierArchivedEvent.js new file mode 100644 index 0000000000..e2a74cba4e --- /dev/null +++ b/ghost/tiers/lib/TierArchivedEvent.js @@ -0,0 +1,30 @@ +/** + * @typedef {object} TierArchivedEventData + * @prop {Tier} tier + */ + +class TierArchivedEvent { + /** @type {TierArchivedEventData} */ + data; + /** @type {Date} */ + timestamp; + + /** + * @param {TierArchivedEvent} data + * @param {Date} timestamp + */ + constructor(data, timestamp) { + this.data = data; + this.timestamp = timestamp; + } + + /** + * @param {TierArchivedEvent} data + * @param {Date} [timestamp] + */ + static create(data, timestamp = new Date()) { + return new TierArchivedEvent(data, timestamp); + } +} + +module.exports = TierArchivedEvent; diff --git a/ghost/tiers/lib/TierCreatedEvent.js b/ghost/tiers/lib/TierCreatedEvent.js new file mode 100644 index 0000000000..112226295c --- /dev/null +++ b/ghost/tiers/lib/TierCreatedEvent.js @@ -0,0 +1,30 @@ +/** + * @typedef {object} TierCreatedEventData + * @prop {Tier} tier + */ + +class TierCreatedEvent { + /** @type {TierCreatedEventData} */ + data; + /** @type {Date} */ + timestamp; + + /** + * @param {TierCreatedEvent} data + * @param {Date} timestamp + */ + constructor(data, timestamp) { + this.data = data; + this.timestamp = timestamp; + } + + /** + * @param {TierCreatedEvent} data + * @param {Date} [timestamp] + */ + static create(data, timestamp = new Date()) { + return new TierCreatedEvent(data, timestamp); + } +} + +module.exports = TierCreatedEvent; diff --git a/ghost/tiers/lib/TierNameChangeEvent.js b/ghost/tiers/lib/TierNameChangeEvent.js new file mode 100644 index 0000000000..a03bb28235 --- /dev/null +++ b/ghost/tiers/lib/TierNameChangeEvent.js @@ -0,0 +1,30 @@ +/** + * @typedef {object} TierNameChangeEventData + * @prop {Tier} tier + */ + +class TierNameChangeEvent { + /** @type {TierNameChangeEventData} */ + data; + /** @type {Date} */ + timestamp; + + /** + * @param {TierNameChangeEvent} data + * @param {Date} timestamp + */ + constructor(data, timestamp) { + this.data = data; + this.timestamp = timestamp; + } + + /** + * @param {TierNameChangeEvent} data + * @param {Date} [timestamp] + */ + static create(data, timestamp = new Date()) { + return new TierNameChangeEvent(data, timestamp); + } +} + +module.exports = TierNameChangeEvent; diff --git a/ghost/tiers/lib/TierPriceChangeEvent.js b/ghost/tiers/lib/TierPriceChangeEvent.js new file mode 100644 index 0000000000..b373ead7a5 --- /dev/null +++ b/ghost/tiers/lib/TierPriceChangeEvent.js @@ -0,0 +1,30 @@ +/** + * @typedef {object} TierPriceChangeEventData + * @prop {Tier} tier + */ + +class TierPriceChangeEvent { + /** @type {TierPriceChangeEventData} */ + data; + /** @type {Date} */ + timestamp; + + /** + * @param {TierPriceChangeEvent} data + * @param {Date} timestamp + */ + constructor(data, timestamp) { + this.data = data; + this.timestamp = timestamp; + } + + /** + * @param {TierPriceChangeEvent} data + * @param {Date} [timestamp] + */ + static create(data, timestamp = new Date()) { + return new TierPriceChangeEvent(data, timestamp); + } +} + +module.exports = TierPriceChangeEvent; diff --git a/ghost/tiers/lib/TiersAPI.js b/ghost/tiers/lib/TiersAPI.js index fafc2e34f4..2693f03d05 100644 --- a/ghost/tiers/lib/TiersAPI.js +++ b/ghost/tiers/lib/TiersAPI.js @@ -92,9 +92,6 @@ module.exports = class TiersAPI { 'visibility', 'active', 'trialDays', - 'currency', - 'monthlyPrice', - 'yearlyPrice', 'welcomePageURL' ]; @@ -104,6 +101,12 @@ module.exports = class TiersAPI { } } + tier.updatePricing({ + currency: data.currency || tier.currency, + monthlyPrice: data.monthlyPrice || tier.monthlyPrice, + yearlyPrice: data.yearlyPrice || tier.yearlyPrice + }); + await this.#repository.save(tier); return tier; diff --git a/ghost/tiers/test/Tier.test.js b/ghost/tiers/test/Tier.test.js index 0fd940532e..546a9050d9 100644 --- a/ghost/tiers/test/Tier.test.js +++ b/ghost/tiers/test/Tier.test.js @@ -1,6 +1,10 @@ const assert = require('assert'); const ObjectID = require('bson-objectid'); const Tier = require('../lib/Tier'); +const TierActivatedEvent = require('../lib/TierActivatedEvent'); +const TierArchivedEvent = require('../lib/TierArchivedEvent'); +const TierNameChangeEvent = require('../lib/TierNameChangeEvent'); +const TierPriceChangeEvent = require('../lib/TierPriceChangeEvent'); async function assertError(fn, checkError) { let error; @@ -160,5 +164,92 @@ describe('Tier', function () { tier.yearlyPrice = 'one hundred'; }); }); + + it('Can change name and adds an event', async function () { + const tier = await Tier.create(validInput); + + tier.name = 'New name'; + + assert(tier.events.find((event) => { + return event instanceof TierNameChangeEvent; + })); + }); + + it('Can update pricing information and adds an event', async function () { + const tier = await Tier.create(validInput); + + tier.updatePricing({ + currency: 'eur', + monthlyPrice: 1000, + yearlyPrice: 6000 + }); + + assert(tier.currency === 'EUR'); + assert(tier.monthlyPrice === 1000); + assert(tier.yearlyPrice === 6000); + assert(tier.events.find((event) => { + return event instanceof TierPriceChangeEvent; + })); + }); + + it('Can archive tier and adds an event', async function () { + const tier = await Tier.create(validInput); + + tier.status = 'archived'; + assert(tier.events.find((event) => { + return event instanceof TierArchivedEvent; + })); + }); + + it('Can activate tier and adds an event', async function () { + const tier = await Tier.create({...validInput, status: 'archived'}); + + tier.status = 'active'; + assert(tier.events.find((event) => { + return event instanceof TierActivatedEvent; + })); + }); + + it('Does not add event if values not changed', async function () { + const tier = await Tier.create(validInput); + + tier.status = 'active'; + assert(!tier.events.find((event) => { + return event instanceof TierActivatedEvent; + })); + + tier.name = 'Tier Name'; + assert(!tier.events.find((event) => { + return event instanceof TierNameChangeEvent; + })); + + tier.updatePricing({ + currency: tier.currency, + monthlyPrice: tier.monthlyPrice, + yearlyPrice: tier.yearlyPrice + }); + assert(!tier.events.find((event) => { + return event instanceof TierPriceChangeEvent; + })); + }); + + it('Cannot set pricing data on a free tier', async function () { + const tier = await Tier.create({ + ...validInput, + type: 'free', + currency: null, + monthlyPrice: null, + yearlyPrice: null, + trialDays: null + }); + + assertError(() => { + tier.updatePricing({ + currency: 'usd', + monthlyPrice: 1000, + yearlyPrice: 10000 + }); + }); + }); }); });