ghost 5.116.2 → 5.118.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/{tryghost-api-framework-5.116.2.tgz → tryghost-api-framework-5.118.0.tgz} +0 -0
- package/components/tryghost-constants-5.118.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.118.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.118.0.tgz +0 -0
- package/components/tryghost-domain-events-5.118.0.tgz +0 -0
- package/components/tryghost-donations-5.118.0.tgz +0 -0
- package/components/tryghost-email-addresses-5.118.0.tgz +0 -0
- package/components/{tryghost-email-service-5.116.2.tgz → tryghost-email-service-5.118.0.tgz} +0 -0
- package/components/tryghost-email-suppression-list-5.118.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.118.0.tgz +0 -0
- package/components/tryghost-i18n-5.118.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.116.2.tgz → tryghost-job-manager-5.118.0.tgz} +0 -0
- package/components/tryghost-link-replacer-5.118.0.tgz +0 -0
- package/components/{tryghost-magic-link-5.116.2.tgz → tryghost-magic-link-5.118.0.tgz} +0 -0
- package/components/{tryghost-member-attribution-5.116.2.tgz → tryghost-member-attribution-5.118.0.tgz} +0 -0
- package/components/tryghost-member-events-5.118.0.tgz +0 -0
- package/components/{tryghost-members-csv-5.116.2.tgz → tryghost-members-csv-5.118.0.tgz} +0 -0
- package/components/{tryghost-members-offers-5.116.2.tgz → tryghost-members-offers-5.118.0.tgz} +0 -0
- package/components/tryghost-mw-error-handler-5.118.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.118.0.tgz +0 -0
- package/components/{tryghost-post-events-5.116.2.tgz → tryghost-post-events-5.118.0.tgz} +0 -0
- package/components/tryghost-post-revisions-5.118.0.tgz +0 -0
- package/components/tryghost-posts-service-5.118.0.tgz +0 -0
- package/components/tryghost-prometheus-metrics-5.118.0.tgz +0 -0
- package/components/tryghost-security-5.118.0.tgz +0 -0
- package/components/tryghost-tiers-5.118.0.tgz +0 -0
- package/components/tryghost-webmentions-5.118.0.tgz +0 -0
- package/content/themes/casper/LICENSE +1 -1
- package/content/themes/casper/README.md +1 -1
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +1 -1
- package/content/themes/casper/author.hbs +23 -2
- package/content/themes/casper/package.json +2 -2
- package/content/themes/casper/partials/icons/bluesky.hbs +3 -0
- package/content/themes/casper/partials/icons/instagram.hbs +5 -0
- package/content/themes/casper/partials/icons/linkedin.hbs +3 -0
- package/content/themes/casper/partials/icons/mastodon.hbs +3 -0
- package/content/themes/casper/partials/icons/threads.hbs +3 -0
- package/content/themes/casper/partials/icons/tiktok.hbs +3 -0
- package/content/themes/casper/partials/icons/twitter.hbs +3 -1
- package/content/themes/casper/partials/icons/youtube.hbs +3 -0
- package/content/themes/source/LICENSE +1 -1
- package/content/themes/source/README.md +1 -1
- package/content/themes/source/assets/built/screen.css +1 -1
- package/content/themes/source/assets/built/screen.css.map +1 -1
- package/content/themes/source/assets/css/screen.css +7 -12
- package/content/themes/source/author.hbs +24 -3
- package/content/themes/source/package.json +2 -2
- package/content/themes/source/partials/feature-image.hbs +2 -2
- package/content/themes/source/partials/icons/bluesky.hbs +3 -0
- package/content/themes/source/partials/icons/instagram.hbs +5 -0
- package/content/themes/source/partials/icons/linkedin.hbs +3 -0
- package/content/themes/source/partials/icons/mastodon.hbs +3 -0
- package/content/themes/source/partials/icons/threads.hbs +3 -0
- package/content/themes/source/partials/icons/tiktok.hbs +3 -0
- package/content/themes/source/partials/icons/youtube.hbs +3 -0
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +31793 -26588
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-550846e0.mjs → CodeEditorView-1143c509.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-f3cb3f4d.mjs → index-19ebc8ad.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-4ce2fcd1.mjs → index-ac104f42.mjs} +2635 -2607
- package/core/built/admin/assets/admin-x-settings/{modals-6bc20529.mjs → modals-994901ee.mjs} +6680 -6165
- package/core/built/admin/assets/{chunk.524.578de86e5014b911b05a.js → chunk.524.5710919eb507b9a81166.js} +8 -8
- package/core/built/admin/assets/{chunk.582.21bf3e37b5d84ac4b58a.js → chunk.582.c8cb99b85cfa13fc7df1.js} +10 -10
- package/core/built/admin/assets/{chunk.713.761d11035fe0bf3e557c.js → chunk.713.48f120c377bcaffdfddf.js} +6 -9
- package/core/built/admin/assets/{ghost-868c537d5c02ca65323d0122596a67ec.js → ghost-cd90a28b214ee800a007bb62cd45e6e6.js} +780 -775
- package/core/built/admin/assets/posts/posts.js +11561 -11302
- package/core/built/admin/assets/stats/stats.js +76076 -59355
- package/core/built/admin/index.html +4 -4
- package/core/frontend/helpers/social_url.js +31 -0
- package/core/server/api/endpoints/users.js +7 -0
- package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
- package/core/server/data/migrations/versions/5.117/2025-04-14-02-36-30-add-additional-social-accounts-columns-to-user-table.js +38 -0
- package/core/server/data/schema/schema.js +7 -0
- package/core/server/services/auth/session/index.js +5 -2
- package/core/server/services/auth/session/middleware.js +2 -1
- package/core/server/services/auth/session/session-service.js +7 -6
- package/core/server/services/members/api.js +2 -2
- package/core/server/services/members/members-api/controllers/MemberController.js +214 -0
- package/core/server/services/members/members-api/controllers/RouterController.js +667 -0
- package/core/server/services/members/members-api/controllers/WellKnownController.js +46 -0
- package/core/server/services/members/members-api/members-api.js +404 -0
- package/core/server/services/members/members-api/repositories/EventRepository.js +984 -0
- package/core/server/services/members/members-api/repositories/MemberRepository.js +1739 -0
- package/core/server/services/members/members-api/repositories/ProductRepository.js +662 -0
- package/core/server/services/members/members-api/services/GeolocationService.js +23 -0
- package/core/server/services/members/members-api/services/MemberBREADService.js +444 -0
- package/core/server/services/members/members-api/services/PaymentsService.js +522 -0
- package/core/server/services/members/members-api/services/TokenService.js +54 -0
- package/core/server/services/milestones/BookshelfMilestoneRepository.js +8 -9
- package/core/server/services/milestones/InMemoryMilestoneRepository.js +119 -0
- package/core/server/services/milestones/Milestone.js +231 -0
- package/core/server/services/milestones/MilestoneCreatedEvent.js +22 -0
- package/core/server/services/milestones/MilestonesService.js +327 -0
- package/core/server/services/milestones/service.js +2 -2
- package/core/server/services/newsletters/index.js +1 -1
- package/core/server/services/public-config/config.js +2 -1
- package/core/server/services/settings/settings-service.js +1 -1
- package/core/server/services/slack-notifications/SlackNotifications.js +1 -1
- package/core/server/services/slack-notifications/SlackNotificationsService.js +2 -2
- package/core/server/services/staff/StaffService.js +1 -1
- package/core/shared/config/defaults.json +3 -0
- package/core/shared/config/env/config.testing-mysql.json +3 -0
- package/core/shared/config/env/config.testing.json +3 -0
- package/core/shared/labs.js +2 -2
- package/package.json +63 -63
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +306 -70
- package/components/tryghost-constants-5.116.2.tgz +0 -0
- package/components/tryghost-custom-fonts-5.116.2.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.116.2.tgz +0 -0
- package/components/tryghost-domain-events-5.116.2.tgz +0 -0
- package/components/tryghost-donations-5.116.2.tgz +0 -0
- package/components/tryghost-email-addresses-5.116.2.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.116.2.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.116.2.tgz +0 -0
- package/components/tryghost-i18n-5.116.2.tgz +0 -0
- package/components/tryghost-link-replacer-5.116.2.tgz +0 -0
- package/components/tryghost-member-events-5.116.2.tgz +0 -0
- package/components/tryghost-members-api-5.116.2.tgz +0 -0
- package/components/tryghost-milestones-5.116.2.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.116.2.tgz +0 -0
- package/components/tryghost-mw-vhost-5.116.2.tgz +0 -0
- package/components/tryghost-post-revisions-5.116.2.tgz +0 -0
- package/components/tryghost-posts-service-5.116.2.tgz +0 -0
- package/components/tryghost-prometheus-metrics-5.116.2.tgz +0 -0
- package/components/tryghost-security-5.116.2.tgz +0 -0
- package/components/tryghost-tiers-5.116.2.tgz +0 -0
- package/components/tryghost-webmentions-5.116.2.tgz +0 -0
- /package/core/built/admin/assets/{chunk.713.761d11035fe0bf3e557c.js.LICENSE.txt → chunk.713.48f120c377bcaffdfddf.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
const errors = require('@tryghost/errors');
|
|
2
|
+
const logging = require('@tryghost/logging');
|
|
3
|
+
const tpl = require('@tryghost/tpl');
|
|
4
|
+
const moment = require('moment');
|
|
5
|
+
|
|
6
|
+
const messages = {
|
|
7
|
+
stripeNotConnected: 'Missing Stripe connection.',
|
|
8
|
+
memberAlreadyExists: 'Member already exists.'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} ILabsService
|
|
13
|
+
* @prop {(key: string) => boolean} isSet
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} IEmailService
|
|
18
|
+
* @prop {(data: {email: string, requestedType: string}) => Promise<any>} sendEmailWithMagicLink
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} IStripeService
|
|
23
|
+
* @prop {boolean} configured
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {import('@tryghost/members-offers/lib/application/OfferMapper').OfferDTO} OfferDTO
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
module.exports = class MemberBREADService {
|
|
31
|
+
/**
|
|
32
|
+
* @param {object} deps
|
|
33
|
+
* @param {import('../repositories/MemberRepository')} deps.memberRepository
|
|
34
|
+
* @param {import('@tryghost/members-offers/lib/application/OffersAPI')} deps.offersAPI
|
|
35
|
+
* @param {ILabsService} deps.labsService
|
|
36
|
+
* @param {IEmailService} deps.emailService
|
|
37
|
+
* @param {IStripeService} deps.stripeService
|
|
38
|
+
* @param {import('@tryghost/member-attribution/lib/service')} deps.memberAttributionService
|
|
39
|
+
* @param {import('@tryghost/email-suppression-list/lib/email-suppression-list').IEmailSuppressionList} deps.emailSuppressionList
|
|
40
|
+
* @param {import('@tryghost/settings-helpers')} deps.settingsHelpers
|
|
41
|
+
*/
|
|
42
|
+
constructor({memberRepository, labsService, emailService, stripeService, offersAPI, memberAttributionService, emailSuppressionList, settingsHelpers}) {
|
|
43
|
+
this.offersAPI = offersAPI;
|
|
44
|
+
/** @private */
|
|
45
|
+
this.memberRepository = memberRepository;
|
|
46
|
+
/** @private */
|
|
47
|
+
this.labsService = labsService;
|
|
48
|
+
/** @private */
|
|
49
|
+
this.emailService = emailService;
|
|
50
|
+
/** @private */
|
|
51
|
+
this.stripeService = stripeService;
|
|
52
|
+
/** @private */
|
|
53
|
+
this.memberAttributionService = memberAttributionService;
|
|
54
|
+
/** @private */
|
|
55
|
+
this.emailSuppressionList = emailSuppressionList;
|
|
56
|
+
/** @private */
|
|
57
|
+
this.settingsHelpers = settingsHelpers;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @private
|
|
62
|
+
* Adds missing complimentary subscriptions to a member and makes sure the tier of all subscriptions is set correctly.
|
|
63
|
+
*/
|
|
64
|
+
attachSubscriptionsToMember(member) {
|
|
65
|
+
if (!member.products || !Array.isArray(member.products)) {
|
|
66
|
+
return member;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const subscriptionProducts = (member.subscriptions || [])
|
|
70
|
+
.filter(sub => this.memberRepository.isActiveSubscriptionStatus(sub.status))
|
|
71
|
+
.map(sub => sub.price.product.product_id);
|
|
72
|
+
|
|
73
|
+
// Remove incomplete subscriptions from the API
|
|
74
|
+
member.subscriptions = member.subscriptions.filter(sub => sub.status !== 'incomplete' && sub.status !== 'incomplete_expired');
|
|
75
|
+
|
|
76
|
+
for (const product of member.products) {
|
|
77
|
+
if (!subscriptionProducts.includes(product.id)) {
|
|
78
|
+
const productAddEvent = member.productEvents.find(event => event.product_id === product.id);
|
|
79
|
+
let startDate;
|
|
80
|
+
if (!productAddEvent || productAddEvent.action !== 'added') {
|
|
81
|
+
startDate = moment();
|
|
82
|
+
} else {
|
|
83
|
+
startDate = moment(productAddEvent.created_at);
|
|
84
|
+
}
|
|
85
|
+
member.subscriptions.push({
|
|
86
|
+
id: '',
|
|
87
|
+
tier: product,
|
|
88
|
+
customer: {
|
|
89
|
+
id: '',
|
|
90
|
+
name: member.name,
|
|
91
|
+
email: member.email
|
|
92
|
+
},
|
|
93
|
+
plan: {
|
|
94
|
+
id: '',
|
|
95
|
+
nickname: 'Complimentary',
|
|
96
|
+
interval: 'year',
|
|
97
|
+
currency: 'USD',
|
|
98
|
+
amount: 0
|
|
99
|
+
},
|
|
100
|
+
status: 'active',
|
|
101
|
+
start_date: startDate,
|
|
102
|
+
default_payment_card_last4: '****',
|
|
103
|
+
cancel_at_period_end: false,
|
|
104
|
+
cancellation_reason: null,
|
|
105
|
+
current_period_end: moment(product.expiry_at),
|
|
106
|
+
price: {
|
|
107
|
+
id: '',
|
|
108
|
+
price_id: '',
|
|
109
|
+
nickname: 'Complimentary',
|
|
110
|
+
amount: 0,
|
|
111
|
+
interval: 'year',
|
|
112
|
+
type: 'recurring',
|
|
113
|
+
currency: 'USD',
|
|
114
|
+
product: {
|
|
115
|
+
id: '',
|
|
116
|
+
product_id: product.id
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const subscription of member.subscriptions) {
|
|
124
|
+
if (!subscription.tier) {
|
|
125
|
+
subscription.tier = member.products.find(product => product.id === subscription.price.product.product_id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @private Builds a map between subscriptions and their offer representation (from OfferMapper)
|
|
132
|
+
* @returns {Promise<Map<string, OfferDTO>>}
|
|
133
|
+
*/
|
|
134
|
+
async fetchSubscriptionOffers(subscriptions) {
|
|
135
|
+
const fetchedOffers = new Map();
|
|
136
|
+
const subscriptionOffers = new Map();
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
for (const subscriptionModel of subscriptions) {
|
|
140
|
+
const offerId = subscriptionModel.get('offer_id');
|
|
141
|
+
|
|
142
|
+
if (!offerId) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let offer = fetchedOffers.get(offerId);
|
|
147
|
+
if (!offer) {
|
|
148
|
+
offer = await this.offersAPI.getOffer({id: offerId});
|
|
149
|
+
fetchedOffers.set(offerId, offer);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
subscriptionOffers.set(subscriptionModel.get('subscription_id'), offer);
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
logging.error(`Failed to load offers for subscriptions - ${subscriptions.map(s => s.id).join(', ')}.`);
|
|
156
|
+
logging.error(e);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return subscriptionOffers;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @private
|
|
164
|
+
* @param {Object} member JSON serialized member
|
|
165
|
+
* @param {Map<string, OfferDTO>} subscriptionOffers result from fetchSubscriptionOffers
|
|
166
|
+
*/
|
|
167
|
+
attachOffersToSubscriptions(member, subscriptionOffers) {
|
|
168
|
+
member.subscriptions = member.subscriptions.map((subscription) => {
|
|
169
|
+
const offer = subscriptionOffers.get(subscription.id);
|
|
170
|
+
if (offer) {
|
|
171
|
+
subscription.offer = offer;
|
|
172
|
+
} else {
|
|
173
|
+
subscription.offer = null;
|
|
174
|
+
}
|
|
175
|
+
return subscription;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @private
|
|
181
|
+
* Adds missing complimentary subscriptions to a member and makes sure the tier of all subscriptions is set correctly.
|
|
182
|
+
*/
|
|
183
|
+
async attachAttributionsToMember(member, subscriptionIdMap) {
|
|
184
|
+
// Created attribution
|
|
185
|
+
member.attribution = await this.memberAttributionService.getMemberCreatedAttribution(member.id);
|
|
186
|
+
|
|
187
|
+
// Subscriptions attributions
|
|
188
|
+
for (const subscription of member.subscriptions) {
|
|
189
|
+
if (!subscription.id) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Convert stripe ID to database id
|
|
194
|
+
const id = subscriptionIdMap.get(subscription.id);
|
|
195
|
+
if (!id) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
subscription.attribution = await this.memberAttributionService.getSubscriptionCreatedAttribution(id);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async read(data, options = {}) {
|
|
203
|
+
const defaultWithRelated = [
|
|
204
|
+
'labels',
|
|
205
|
+
'stripeSubscriptions',
|
|
206
|
+
'stripeSubscriptions.customer',
|
|
207
|
+
'stripeSubscriptions.stripePrice',
|
|
208
|
+
'stripeSubscriptions.stripePrice.stripeProduct',
|
|
209
|
+
'stripeSubscriptions.stripePrice.stripeProduct.product',
|
|
210
|
+
'products',
|
|
211
|
+
'newsletters'
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const withRelated = new Set((options.withRelated || []).concat(defaultWithRelated));
|
|
215
|
+
|
|
216
|
+
if (!withRelated.has('productEvents')) {
|
|
217
|
+
withRelated.add('productEvents');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (withRelated.has('email_recipients')) {
|
|
221
|
+
withRelated.add('email_recipients.email');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const model = await this.memberRepository.get(data, {
|
|
225
|
+
...options,
|
|
226
|
+
withRelated: Array.from(withRelated)
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (!model) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// We need to know the real IDs for each subscription to fetch the member attribution
|
|
234
|
+
const subscriptionIdMap = new Map();
|
|
235
|
+
for (const subscription of model.related('stripeSubscriptions')) {
|
|
236
|
+
subscriptionIdMap.set(subscription.get('subscription_id'), subscription.id);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const member = model.toJSON(options);
|
|
240
|
+
|
|
241
|
+
member.subscriptions = member.subscriptions.filter(sub => !!sub.price);
|
|
242
|
+
this.attachSubscriptionsToMember(member);
|
|
243
|
+
this.attachOffersToSubscriptions(member, await this.fetchSubscriptionOffers(model.related('stripeSubscriptions')));
|
|
244
|
+
await this.attachAttributionsToMember(member, subscriptionIdMap);
|
|
245
|
+
|
|
246
|
+
const suppressionData = await this.emailSuppressionList.getSuppressionData(member.email);
|
|
247
|
+
member.email_suppression = {
|
|
248
|
+
suppressed: suppressionData.suppressed || !!model.get('email_disabled'),
|
|
249
|
+
info: suppressionData.info
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const unsubscribeUrl = this.settingsHelpers.createUnsubscribeUrl(member.uuid);
|
|
253
|
+
member.unsubscribe_url = unsubscribeUrl;
|
|
254
|
+
|
|
255
|
+
return member;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async add(data, options) {
|
|
259
|
+
if (!this.stripeService.configured && (data.comped || data.stripe_customer_id)) {
|
|
260
|
+
const property = data.comped ? 'comped' : 'stripe_customer_id';
|
|
261
|
+
throw new errors.ValidationError({
|
|
262
|
+
message: tpl(messages.stripeNotConnected),
|
|
263
|
+
context: 'Attempting to import members with Stripe data when there is no Stripe account connected.',
|
|
264
|
+
help: 'You need to connect to Stripe to import Stripe customers. ',
|
|
265
|
+
property
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let model;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const attribution = await this.memberAttributionService.getAttributionFromContext(options?.context);
|
|
273
|
+
if (attribution) {
|
|
274
|
+
data.attribution = attribution;
|
|
275
|
+
}
|
|
276
|
+
model = await this.memberRepository.create(data, options);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
|
|
279
|
+
throw new errors.ValidationError({
|
|
280
|
+
message: tpl(messages.memberAlreadyExists),
|
|
281
|
+
context: 'Attempting to add member with existing email address',
|
|
282
|
+
property: 'email'
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const sharedOptions = options.transacting ? {
|
|
289
|
+
transacting: options.transacting
|
|
290
|
+
} : {};
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
if (data.stripe_customer_id) {
|
|
294
|
+
await this.memberRepository.linkStripeCustomer({
|
|
295
|
+
customer_id: data.stripe_customer_id,
|
|
296
|
+
member_id: model.id
|
|
297
|
+
}, sharedOptions);
|
|
298
|
+
}
|
|
299
|
+
} catch (error) {
|
|
300
|
+
const isStripeLinkingError = error.message && (error.message.match(/customer|plan|subscription/g));
|
|
301
|
+
if (isStripeLinkingError) {
|
|
302
|
+
if (error.message.indexOf('customer') && error.code === 'resource_missing') {
|
|
303
|
+
error.message = `Member not imported. ${error.message}`;
|
|
304
|
+
error.context = 'Missing Stripe Customer';
|
|
305
|
+
error.help = 'Make sure you\'re connected to the correct Stripe Account';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
await this.memberRepository.destroy({
|
|
309
|
+
id: model.id
|
|
310
|
+
}, options);
|
|
311
|
+
}
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (options.send_email) {
|
|
316
|
+
await this.emailService.sendEmailWithMagicLink({
|
|
317
|
+
email: model.get('email'), requestedType: options.email_type
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (data.comped) {
|
|
322
|
+
await this.memberRepository.setComplimentarySubscription(model, options);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return this.read({id: model.id}, options);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async edit(data, options) {
|
|
329
|
+
delete data.last_seen_at;
|
|
330
|
+
|
|
331
|
+
let model;
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
// Update email_disabled based on whether the new email is suppressed
|
|
335
|
+
if (data.email) {
|
|
336
|
+
const isSuppressed = (await this.emailSuppressionList.getSuppressionData(data.email))?.suppressed;
|
|
337
|
+
data.email_disabled = !!isSuppressed;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
model = await this.memberRepository.update(data, options);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
|
|
343
|
+
throw new errors.ValidationError({
|
|
344
|
+
message: tpl(messages.memberAlreadyExists),
|
|
345
|
+
context: 'Attempting to edit member with existing email address',
|
|
346
|
+
property: 'email'
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (this.stripeService.configured) {
|
|
354
|
+
const hasCompedSubscription = !!model.related('stripeSubscriptions').find(sub => sub.get('plan_nickname') === 'Complimentary' && sub.get('status') === 'active');
|
|
355
|
+
|
|
356
|
+
if (typeof data.comped === 'boolean') {
|
|
357
|
+
if (data.comped && !hasCompedSubscription) {
|
|
358
|
+
await this.memberRepository.setComplimentarySubscription(model, {
|
|
359
|
+
context: options.context,
|
|
360
|
+
transacting: options.transacting
|
|
361
|
+
});
|
|
362
|
+
} else if (!(data.comped) && hasCompedSubscription) {
|
|
363
|
+
await this.memberRepository.cancelComplimentarySubscription(model, {
|
|
364
|
+
context: options.context,
|
|
365
|
+
transacting: options.transacting
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return this.read({id: model.id}, options);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async logout(options) {
|
|
375
|
+
await this.memberRepository.cycleTransientId(options);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async browse(options) {
|
|
379
|
+
const defaultWithRelated = [
|
|
380
|
+
'labels',
|
|
381
|
+
'stripeSubscriptions',
|
|
382
|
+
'stripeSubscriptions.customer',
|
|
383
|
+
'stripeSubscriptions.stripePrice',
|
|
384
|
+
'stripeSubscriptions.stripePrice.stripeProduct',
|
|
385
|
+
'stripeSubscriptions.stripePrice.stripeProduct.product',
|
|
386
|
+
'products',
|
|
387
|
+
'newsletters'
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
if (options.limit === 'all' || options.limit > 100) {
|
|
391
|
+
options.limit = 100;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const originalWithRelated = options.withRelated || [];
|
|
395
|
+
|
|
396
|
+
const withRelated = new Set((originalWithRelated).concat(defaultWithRelated));
|
|
397
|
+
|
|
398
|
+
if (!withRelated.has('productEvents')) {
|
|
399
|
+
withRelated.add('productEvents');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (withRelated.has('email_recipients')) {
|
|
403
|
+
withRelated.add('email_recipients.email');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
//option param to skip distinct from count query, distinct adds a lot of latency and in this case the result set will always be unique.
|
|
407
|
+
options.useBasicCount = true;
|
|
408
|
+
|
|
409
|
+
const page = await this.memberRepository.list({
|
|
410
|
+
...options,
|
|
411
|
+
withRelated: Array.from(withRelated)
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
if (!page) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const subscriptions = page.data.flatMap(model => model.related('stripeSubscriptions').slice());
|
|
419
|
+
const offerMap = await this.fetchSubscriptionOffers(subscriptions);
|
|
420
|
+
|
|
421
|
+
const bulkSuppressionData = await this.emailSuppressionList.getBulkSuppressionData(page.data.map(member => member.get('email')));
|
|
422
|
+
|
|
423
|
+
const data = page.data.map((model, index) => {
|
|
424
|
+
const member = model.toJSON(options);
|
|
425
|
+
member.subscriptions = member.subscriptions.filter(sub => !!sub.price);
|
|
426
|
+
this.attachSubscriptionsToMember(member);
|
|
427
|
+
this.attachOffersToSubscriptions(member, offerMap);
|
|
428
|
+
if (!originalWithRelated.includes('products')) {
|
|
429
|
+
delete member.products;
|
|
430
|
+
}
|
|
431
|
+
member.email_suppression = {
|
|
432
|
+
suppressed: bulkSuppressionData[index].suppressed || !!model.get('email_disabled'),
|
|
433
|
+
info: bulkSuppressionData[index].info
|
|
434
|
+
};
|
|
435
|
+
member.unsubscribe_url = this.settingsHelpers.createUnsubscribeUrl(member.uuid);
|
|
436
|
+
return member;
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
data,
|
|
441
|
+
meta: page.meta
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
};
|