diff --git a/ghost/offers/lib/OfferMapper.js b/ghost/offers/lib/OfferMapper.js index bda9cb149c..34681a3d83 100644 --- a/ghost/offers/lib/OfferMapper.js +++ b/ghost/offers/lib/OfferMapper.js @@ -19,6 +19,8 @@ * @prop {boolean} currency_restriction * @prop {string} currency * + * @prop {'once'|'repeating'|'forever'} duration + * * @prop {object} tier * @prop {string} tier.id * @prop {string} tier.name @@ -39,8 +41,9 @@ class OfferMapper { type: offer.type.value, cadence: offer.cadence.value, amount: offer.amount.value, - currency_restriction: offer.type === 'amount', - currency: offer.type === 'amount' ? offer.currency : null, + duration: offer.duration.value, + currency_restriction: offer.type.value === 'amount', + currency: offer.type.value === 'amount' ? offer.currency : null, tier: { id: offer.tier.id, name: offer.tier.name diff --git a/ghost/offers/lib/OfferRepository.js b/ghost/offers/lib/OfferRepository.js index 26e61046b4..968659f764 100644 --- a/ghost/offers/lib/OfferRepository.js +++ b/ghost/offers/lib/OfferRepository.js @@ -22,6 +22,7 @@ function toDomain(json) { amount: json.discount_amount, cadence: json.interval, currency: json.currency, + duration: json.duration, stripe_coupon_id: json.stripe_coupon_id, tier: { id: json.product.id, @@ -121,7 +122,7 @@ class OfferRepository { discount_amount: offer.amount.value, interval: offer.cadence.value, product_id: offer.tier.id, - duration: 'once' + duration: offer.duration.value }); if (offer.codeChanged || offer.isNew) { diff --git a/ghost/offers/lib/domain/models/Offer.js b/ghost/offers/lib/domain/models/Offer.js index 84ca1b22ce..c1fd30c870 100644 --- a/ghost/offers/lib/domain/models/Offer.js +++ b/ghost/offers/lib/domain/models/Offer.js @@ -8,6 +8,7 @@ const OfferTitle = require('./OfferTitle'); const OfferDescription = require('./OfferDescription'); const OfferCadence = require('./OfferCadence'); const OfferType = require('./OfferType'); +const OfferDuration = require('./OfferDuration'); /** * @typedef {object} OfferProps @@ -19,6 +20,7 @@ const OfferType = require('./OfferType'); * @prop {OfferCadence} cadence * @prop {OfferType} type * @prop {OfferAmount} amount + * @prop {OfferDuration} duration * @prop {string} currency * @prop {string} [stripe_coupon_id] * @prop {OfferTier} tier @@ -93,40 +95,8 @@ class Offer { return this.props.currency; } - /** - * @param {OfferCode} code - * @param {UniqueChecker} uniqueChecker - * @returns {Promise} - */ - async updateCode(code, uniqueChecker) { - if (code.equals(this.props.code)) { - return; - } - if (!await uniqueChecker.isUniqueCode(code)) { - throw new errors.InvalidOfferCode({ - message: 'Offer `code` must be unique.' - }); - } - this.changed.code.push(this.props.code); - this.props.code = code; - } - - /** - * @param {OfferName} name - * @param {UniqueChecker} uniqueChecker - * @returns {Promise} - */ - async updateName(name, uniqueChecker) { - if (name.equals(this.props.name)) { - return; - } - if (!await uniqueChecker.isUniqueName(name)) { - throw new errors.InvalidOfferNameError({ - message: 'Offer `name` must be unique.' - }); - } - this.changed.name.push(this.props.name); - this.props.name = name; + get duration() { + return this.props.duration; } get oldCodes() { @@ -177,6 +147,42 @@ class Offer { return this.props.stripe_coupon_id; } + /** + * @param {OfferCode} code + * @param {UniqueChecker} uniqueChecker + * @returns {Promise} + */ + async updateCode(code, uniqueChecker) { + if (code.equals(this.props.code)) { + return; + } + if (!await uniqueChecker.isUniqueCode(code)) { + throw new errors.InvalidOfferCode({ + message: 'Offer `code` must be unique.' + }); + } + this.changed.code.push(this.props.code); + this.props.code = code; + } + + /** + * @param {OfferName} name + * @param {UniqueChecker} uniqueChecker + * @returns {Promise} + */ + async updateName(name, uniqueChecker) { + if (name.equals(this.props.name)) { + return; + } + if (!await uniqueChecker.isUniqueName(name)) { + throw new errors.InvalidOfferName({ + message: 'Offer `name` must be unique.' + }); + } + this.changed.name.push(this.props.name); + this.props.name = name; + } + /** * @private * @param {OfferProps} props @@ -219,6 +225,7 @@ class Offer { const description = OfferDescription.create(data.display_description); const type = OfferType.create(data.type); const cadence = OfferCadence.create(data.cadence); + const duration = OfferDuration.create(data.duration); let amount; if (type.equals(OfferType.Percent)) { amount = OfferAmount.OfferPercentageAmount.create(data.amount); @@ -242,6 +249,12 @@ class Offer { } } + if (duration.equals(OfferDuration.create('repeating'))) { + throw new errors.InvalidOfferDuration({ + message: 'Offer `duration` must be either "once" or "forever".' + }); + } + const currency = data.currency; if (isNew && data.stripe_coupon_id) { @@ -267,6 +280,7 @@ class Offer { type, amount, cadence, + duration, currency, tier, stripe_coupon_id: couponId diff --git a/ghost/offers/lib/domain/models/OfferDuration.js b/ghost/offers/lib/domain/models/OfferDuration.js new file mode 100644 index 0000000000..df3366ebe8 --- /dev/null +++ b/ghost/offers/lib/domain/models/OfferDuration.js @@ -0,0 +1,25 @@ +const ValueObject = require('../../shared/ValueObject'); +const InvalidOfferDuration = require('../../errors').InvalidOfferDuration; + +/** + * @extends ValueObject<'once'|'repeating'|'forever'> + */ +class OfferDuration extends ValueObject { + /** @param {unknown} duration */ + static create(duration) { + if (!duration || typeof duration !== 'string') { + throw new InvalidOfferDuration({ + message: 'Offer `duration` must be a string.' + }); + } + if (duration !== 'once' && duration !== 'repeating' && duration !== 'forever') { + throw new InvalidOfferDuration({ + message: 'Offer `duration` must be one of "once", "repeating" or "forever".' + }); + } + return new OfferDuration(duration); + } +} + +module.exports = OfferDuration; + diff --git a/ghost/offers/lib/errors.js b/ghost/offers/lib/errors.js index 7c0ea1abe9..918046a936 100644 --- a/ghost/offers/lib/errors.js +++ b/ghost/offers/lib/errors.js @@ -1,14 +1,13 @@ const {GhostError} = require('@tryghost/errors'); class InvalidPropError extends GhostError { - static message = 'Invalid Offer property'; /** @param {any} options */ constructor(options) { super({ - statusCode: 400 + statusCode: 400, + ...options }); this.errorType = this.constructor.name; - this.message = options.message || this.constructor.message; } } @@ -21,6 +20,7 @@ class InvalidOfferAmount extends InvalidPropError {} class InvalidOfferCurrency extends InvalidPropError {} class InvalidOfferTierName extends InvalidPropError {} class InvalidOfferCadence extends InvalidPropError {} +class InvalidOfferDuration extends InvalidPropError {} class InvalidOfferCoupon extends InvalidPropError {} module.exports = { @@ -32,6 +32,7 @@ module.exports = { InvalidOfferAmount, InvalidOfferCurrency, InvalidOfferCadence, + InvalidOfferDuration, InvalidOfferTierName, InvalidOfferCoupon };