Updated member bread service to return member subscription offers from offer_id column (#392)

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

- Instead of doing the matching of the offers and subscriptions by looking at the offer redemptions, we can now look at the offer_id from subscriptions.
- This also fixes an issue where we don't attach the offer object to subscriptions in the members' browse method
- Updated browse behaviour to match the read behaviour of members (product relation needs to get loaded because it is missing in member.products if the subscription is expired).

Tests in https://github.com/TryGhost/Ghost/pull/14515
This commit is contained in:
Simon Backx 2022-04-20 11:10:41 +02:00 committed by GitHub
parent cb1808695f
commit 7fa442516c

View File

@ -1,4 +1,5 @@
const errors = require('@tryghost/errors');
const logging = require('@tryghost/logging');
const tpl = require('@tryghost/tpl');
const moment = require('moment');
@ -22,6 +23,10 @@ const messages = {
* @prop {boolean} configured
*/
/**
* @typedef {import('@tryghost/members-offers/lib/application/OfferMapper').OfferDTO} OfferDTO
*/
module.exports = class MemberBREADService {
/**
* @param {object} deps
@ -45,6 +50,7 @@ module.exports = class MemberBREADService {
/**
* @private
* Adds missing complimentary subscriptions to a member and makes sure the tier of all subscriptions is set correctly.
*/
attachSubscriptionsToMember(member) {
if (!member.products || !Array.isArray(member.products)) {
@ -109,12 +115,46 @@ module.exports = class MemberBREADService {
}
}
/**
* @private Builds a map between subscriptions and their offer representation (from OfferMapper)
* @returns {Promise<Map<string, OfferDTO>>}
*/
async fetchSubscriptionOffers(subscriptions) {
const fetchedOffers = new Map();
const subscriptionOffers = new Map();
try {
for (const subscriptionModel of subscriptions) {
const offerId = subscriptionModel.get('offer_id');
if (!offerId) {
continue;
}
let offer = fetchedOffers.get(offerId);
if (!offer) {
offer = await this.offersAPI.getOffer({id: offerId});
fetchedOffers.set(offerId, offer);
}
subscriptionOffers.set(subscriptionModel.get('subscription_id'), offer);
}
} catch (e) {
logging.error(`Failed to load offers for subscriptions - ${subscriptions.map(s => s.id).join(', ')}.`);
logging.error(e);
}
return subscriptionOffers;
}
/**
* @private
* @param {Object} member JSON serialized member
* @param {Map<string, OfferDTO>} subscriptionOffers result from fetchSubscriptionOffers
*/
attachOffersToSubscriptions(member, subscriptionOffers) {
member.subscriptions = member.subscriptions.map((subscription) => {
const offer = subscriptionOffers[subscription.id];
const offer = subscriptionOffers.get(subscription.id);
if (offer) {
subscription.offer = offer;
} else {
@ -133,7 +173,6 @@ module.exports = class MemberBREADService {
'stripeSubscriptions.stripePrice.stripeProduct',
'stripeSubscriptions.stripePrice.stripeProduct.product',
'products',
'offerRedemptions',
'newsletters'
];
@ -156,38 +195,11 @@ module.exports = class MemberBREADService {
return null;
}
const subscriptionOffers = await model.related('offerRedemptions').toJSON().reduce(async (promiseObj, offerRedemption) => {
const obj = await promiseObj;
const offer = await this.offersAPI.getOffer({id: offerRedemption.offer_id});
return {
...obj,
[offerRedemption.subscription_id]: offer
};
}, Promise.resolve({}));
model.related('stripeSubscriptions').forEach((subscriptionModel) => {
const offer = subscriptionOffers[subscriptionModel.id];
if (!offer) {
return;
}
if (offer.cadence !== subscriptionModel.related('stripePrice').get('interval')) {
return;
}
if (offer.tier.id !== subscriptionModel.related('stripePrice').related('stripeProduct').get('product_id')) {
return;
}
subscriptionOffers[subscriptionModel.get('subscription_id')] = offer;
});
const member = model.toJSON(options);
member.subscriptions = member.subscriptions.filter(sub => !!sub.price);
this.attachSubscriptionsToMember(member);
this.attachOffersToSubscriptions(member, subscriptionOffers);
this.attachOffersToSubscriptions(member, await this.fetchSubscriptionOffers(model.related('stripeSubscriptions')));
return member;
}
@ -315,6 +327,7 @@ module.exports = class MemberBREADService {
'stripeSubscriptions.customer',
'stripeSubscriptions.stripePrice',
'stripeSubscriptions.stripePrice.stripeProduct',
'stripeSubscriptions.stripePrice.stripeProduct.product',
'products',
'newsletters'
];
@ -340,11 +353,15 @@ module.exports = class MemberBREADService {
return null;
}
const subscriptions = page.data.flatMap(model => model.related('stripeSubscriptions').slice());
const offerMap = await this.fetchSubscriptionOffers(subscriptions);
const members = page.data.map(model => model.toJSON(options));
const data = members.map((member) => {
member.subscriptions = member.subscriptions.filter(sub => !!sub.price);
this.attachSubscriptionsToMember(member);
this.attachOffersToSubscriptions(member, offerMap);
if (!originalWithRelated.includes('products')) {
delete member.products;
}