0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-03 23:00:14 -05:00

Added support for "once" and "forever" Offer duration

refs https://github.com/TryGhost/Team/issues/1083

Instead of Offers being hardcoded to the "once" duration this will allow
Admins to start creating offers of variable durations.
This commit is contained in:
Fabien O'Carroll 2021-10-07 17:10:44 +02:00
parent ea0282c80e
commit 6d383c2d0e
5 changed files with 84 additions and 40 deletions

View file

@ -19,6 +19,8 @@
* @prop {boolean} currency_restriction * @prop {boolean} currency_restriction
* @prop {string} currency * @prop {string} currency
* *
* @prop {'once'|'repeating'|'forever'} duration
*
* @prop {object} tier * @prop {object} tier
* @prop {string} tier.id * @prop {string} tier.id
* @prop {string} tier.name * @prop {string} tier.name
@ -39,8 +41,9 @@ class OfferMapper {
type: offer.type.value, type: offer.type.value,
cadence: offer.cadence.value, cadence: offer.cadence.value,
amount: offer.amount.value, amount: offer.amount.value,
currency_restriction: offer.type === 'amount', duration: offer.duration.value,
currency: offer.type === 'amount' ? offer.currency : null, currency_restriction: offer.type.value === 'amount',
currency: offer.type.value === 'amount' ? offer.currency : null,
tier: { tier: {
id: offer.tier.id, id: offer.tier.id,
name: offer.tier.name name: offer.tier.name

View file

@ -22,6 +22,7 @@ function toDomain(json) {
amount: json.discount_amount, amount: json.discount_amount,
cadence: json.interval, cadence: json.interval,
currency: json.currency, currency: json.currency,
duration: json.duration,
stripe_coupon_id: json.stripe_coupon_id, stripe_coupon_id: json.stripe_coupon_id,
tier: { tier: {
id: json.product.id, id: json.product.id,
@ -121,7 +122,7 @@ class OfferRepository {
discount_amount: offer.amount.value, discount_amount: offer.amount.value,
interval: offer.cadence.value, interval: offer.cadence.value,
product_id: offer.tier.id, product_id: offer.tier.id,
duration: 'once' duration: offer.duration.value
}); });
if (offer.codeChanged || offer.isNew) { if (offer.codeChanged || offer.isNew) {

View file

@ -8,6 +8,7 @@ const OfferTitle = require('./OfferTitle');
const OfferDescription = require('./OfferDescription'); const OfferDescription = require('./OfferDescription');
const OfferCadence = require('./OfferCadence'); const OfferCadence = require('./OfferCadence');
const OfferType = require('./OfferType'); const OfferType = require('./OfferType');
const OfferDuration = require('./OfferDuration');
/** /**
* @typedef {object} OfferProps * @typedef {object} OfferProps
@ -19,6 +20,7 @@ const OfferType = require('./OfferType');
* @prop {OfferCadence} cadence * @prop {OfferCadence} cadence
* @prop {OfferType} type * @prop {OfferType} type
* @prop {OfferAmount} amount * @prop {OfferAmount} amount
* @prop {OfferDuration} duration
* @prop {string} currency * @prop {string} currency
* @prop {string} [stripe_coupon_id] * @prop {string} [stripe_coupon_id]
* @prop {OfferTier} tier * @prop {OfferTier} tier
@ -93,40 +95,8 @@ class Offer {
return this.props.currency; return this.props.currency;
} }
/** get duration() {
* @param {OfferCode} code return this.props.duration;
* @param {UniqueChecker} uniqueChecker
* @returns {Promise<void>}
*/
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<void>}
*/
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 oldCodes() { get oldCodes() {
@ -177,6 +147,42 @@ class Offer {
return this.props.stripe_coupon_id; return this.props.stripe_coupon_id;
} }
/**
* @param {OfferCode} code
* @param {UniqueChecker} uniqueChecker
* @returns {Promise<void>}
*/
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<void>}
*/
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 * @private
* @param {OfferProps} props * @param {OfferProps} props
@ -219,6 +225,7 @@ class Offer {
const description = OfferDescription.create(data.display_description); const description = OfferDescription.create(data.display_description);
const type = OfferType.create(data.type); const type = OfferType.create(data.type);
const cadence = OfferCadence.create(data.cadence); const cadence = OfferCadence.create(data.cadence);
const duration = OfferDuration.create(data.duration);
let amount; let amount;
if (type.equals(OfferType.Percent)) { if (type.equals(OfferType.Percent)) {
amount = OfferAmount.OfferPercentageAmount.create(data.amount); 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; const currency = data.currency;
if (isNew && data.stripe_coupon_id) { if (isNew && data.stripe_coupon_id) {
@ -267,6 +280,7 @@ class Offer {
type, type,
amount, amount,
cadence, cadence,
duration,
currency, currency,
tier, tier,
stripe_coupon_id: couponId stripe_coupon_id: couponId

View file

@ -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;

View file

@ -1,14 +1,13 @@
const {GhostError} = require('@tryghost/errors'); const {GhostError} = require('@tryghost/errors');
class InvalidPropError extends GhostError { class InvalidPropError extends GhostError {
static message = 'Invalid Offer property';
/** @param {any} options */ /** @param {any} options */
constructor(options) { constructor(options) {
super({ super({
statusCode: 400 statusCode: 400,
...options
}); });
this.errorType = this.constructor.name; 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 InvalidOfferCurrency extends InvalidPropError {}
class InvalidOfferTierName extends InvalidPropError {} class InvalidOfferTierName extends InvalidPropError {}
class InvalidOfferCadence extends InvalidPropError {} class InvalidOfferCadence extends InvalidPropError {}
class InvalidOfferDuration extends InvalidPropError {}
class InvalidOfferCoupon extends InvalidPropError {} class InvalidOfferCoupon extends InvalidPropError {}
module.exports = { module.exports = {
@ -32,6 +32,7 @@ module.exports = {
InvalidOfferAmount, InvalidOfferAmount,
InvalidOfferCurrency, InvalidOfferCurrency,
InvalidOfferCadence, InvalidOfferCadence,
InvalidOfferDuration,
InvalidOfferTierName, InvalidOfferTierName,
InvalidOfferCoupon InvalidOfferCoupon
}; };