ghost
Version:
The professional publishing platform
240 lines (211 loc) • 7.22 kB
JavaScript
const {flowRight} = require('lodash');
const {mapKeyValues, mapQuery} = require('@tryghost/mongo-utils');
const DomainEvents = require('@tryghost/domain-events');
const Offer = require('./domain/models/Offer');
const sentry = require('../../../shared/sentry');
const logger = require('@tryghost/logging');
const statusTransformer = mapKeyValues({
key: {
from: 'status',
to: 'active'
},
values: [{
from: 'active',
to: true
}, {
from: 'archived',
to: false
}]
});
const rejectNonStatusTransformer = input => mapQuery(input, function (value, key) {
if (key !== 'status') {
return;
}
return {
[key]: value
};
});
const mongoTransformer = flowRight(statusTransformer, rejectNonStatusTransformer);
/**
* @typedef {object} BaseOptions
* @prop {import('knex').Knex.Transaction} transacting
*/
/**
* @typedef {object} ListOptions
* @prop {import('knex').Knex.Transaction} transacting
* @prop {string} filter
*/
class OfferBookshelfRepository {
/**
* @param {{forge: (data: object) => import('bookshelf').Model<Offer.OfferProps>}} OfferModel
* @param {{forge: (data: object) => import('bookshelf').Model<any>}} OfferRedemptionModel
*/
constructor(OfferModel, OfferRedemptionModel) {
/** @private */
this.OfferModel = OfferModel;
/** @private */
this.OfferRedemptionModel = OfferRedemptionModel;
}
/**
* @template T
* @param {(t: import('knex').Knex.Transaction) => Promise<T>} cb
* @returns {Promise<T>}
*/
async createTransaction(cb) {
return this.OfferModel.transaction(cb);
}
/**
* @param {string} name
* @param {BaseOptions} [options]
* @returns {Promise<boolean>}
*/
async existsByName(name, options) {
const model = await this.OfferModel.findOne({name}, options);
if (!model) {
return false;
}
return true;
}
/**
* @param {string} code
* @param {BaseOptions} [options]
* @returns {Promise<boolean>}
*/
async existsByCode(code, options) {
const model = await this.OfferModel.findOne({code}, options);
if (!model) {
return false;
}
return true;
}
/**
* @private
* @param {import('bookshelf').Model<any>} model
* @param {BaseOptions} options
* @returns {Promise<import('./domain/models/Offer')>}
*/
async mapToOffer(model, options) {
const json = model.toJSON();
const count = await this.OfferRedemptionModel.where({offer_id: json.id}).count('id', {
transacting: options.transacting
});
const lastRedeemed = await this.OfferRedemptionModel.where({offer_id: json.id}).orderBy('created_at', 'DESC').fetchAll({
transacting: options.transacting,
limit: 1
});
try {
const lastRedeemedObject = lastRedeemed.toJSON();
return await Offer.create({
id: json.id,
name: json.name,
code: json.code,
display_title: json.portal_title,
display_description: json.portal_description,
type: json.discount_type === 'amount' ? 'fixed' : json.discount_type,
amount: json.discount_amount,
cadence: json.interval,
currency: json.currency,
duration: json.duration,
duration_in_months: json.duration_in_months,
redemptionCount: count,
status: json.active ? 'active' : 'archived',
tier: {
id: json.product.id,
name: json.product.name
},
created_at: json.created_at,
last_redeemed: lastRedeemedObject.length > 0 ? lastRedeemedObject[0].created_at : null
}, null);
} catch (err) {
logger.error(err);
sentry.captureException(err);
return null;
}
}
/**
* @param {string} id
* @param {BaseOptions} [options]
* @returns {Promise<import('./domain/models/Offer')>}
*/
async getById(id, options) {
const model = await this.OfferModel.findOne({id}, {
...options,
withRelated: ['product']
});
if (!model) {
return null;
}
return this.mapToOffer(model, options);
}
/**
* @param {string} id stripe_coupon_id
* @param {BaseOptions} [options]
* @returns {Promise<import('./domain/models/Offer')>}
*/
async getByStripeCouponId(id, options) {
const model = await this.OfferModel.findOne({stripe_coupon_id: id}, {
...options,
withRelated: ['product']
});
if (!model) {
return null;
}
return this.mapToOffer(model, options);
}
/**
* @param {ListOptions} options
* @returns {Promise<import('./domain/models/Offer')[]>}
*/
async getAll(options) {
const models = await this.OfferModel.findAll({
...options,
mongoTransformer,
withRelated: ['product']
});
const mapOptions = {
transacting: options && options.transacting
};
const offers = models.map(model => this.mapToOffer(model, mapOptions));
return (await Promise.all(offers)).filter(offer => offer !== null);
}
/**
* @param {import('./domain/models/Offer')} offer
* @param {BaseOptions} [options]
* @returns {Promise<void>}
*/
async save(offer, options) {
/** @type any */
const data = {
id: offer.id,
name: offer.name.value,
code: offer.code.value,
portal_title: offer.displayTitle.value || null,
portal_description: offer.displayDescription.value || null,
discount_type: offer.type.value === 'fixed' ? 'amount' : offer.type.value,
discount_amount: offer.amount.value,
interval: offer.cadence.value,
product_id: offer.tier.id,
duration: offer.duration.value.type,
duration_in_months: offer.duration.value.type === 'repeating' ? offer.duration.value.months : null,
currency: offer.currency ? offer.currency.value : null,
active: offer.status.value === 'active'
};
if (offer.isNew) {
await this.OfferModel.add(data, options);
} else {
await this.OfferModel.edit(data, {...options, id: data.id});
}
for (const event of offer.events) {
if (options.transacting) {
// Only dispatch the event after the transaction has finished
// Because else the offer won't be committed to the database yet
options.transacting.executionPromise.then(() => {
DomainEvents.dispatch(event);
});
} else {
DomainEvents.dispatch(event);
}
}
}
}
module.exports = OfferBookshelfRepository;