ghost 5.119.2 → 5.120.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-i18n-5.120.0.tgz +0 -0
- package/core/boot.js +0 -2
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +7555 -7216
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-60ce658c.mjs → CodeEditorView-1c5b0683.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-8480baa8.mjs → index-14e518a7.mjs} +3 -3
- package/core/built/admin/assets/admin-x-settings/{index-a2648c61.mjs → index-fc9f985b.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-6900c1d5.mjs → modals-15bc6a0f.mjs} +7192 -6656
- package/core/built/admin/assets/{chunk.137.c9bf40f01afeeadb4660.js → chunk.383.25fca2f09b4896656125.js} +76 -59
- package/core/built/admin/assets/chunk.524.1657b12c0ab25dd9fb79.js +28 -0
- package/core/built/admin/assets/{chunk.582.98a820cbc4bb65f2e685.js → chunk.582.09869b1f1a3cc0ab81f6.js} +19 -26
- package/core/built/admin/assets/{ghost-843572e9507d099162ae744d791daba1.js → ghost-b3b44421acca3b3eec76bfbb6ba0e81b.js} +3 -3
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +12578 -12352
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +423 -211
- package/core/built/admin/assets/posts/posts.js +13680 -13671
- package/core/built/admin/assets/stats/stats.js +16457 -16635
- package/core/built/admin/assets/{vendor-8f805740fee4db959a5b2119001a56b1.js → vendor-4ce6d282a2a00fe486a0951e0591da19.js} +11 -9
- package/core/built/admin/index.html +5 -5
- package/core/frontend/helpers/match.js +6 -0
- package/core/frontend/services/routing/ParentRouter.js +1 -1
- package/core/frontend/services/routing/controllers/email-post.js +0 -2
- package/core/frontend/services/routing/controllers/previews.js +0 -3
- package/core/frontend/web/middleware/frontend-caching.js +2 -2
- package/core/server/api/endpoints/authentication.js +37 -73
- package/core/server/api/endpoints/authors-public.js +8 -9
- package/core/server/api/endpoints/db.js +34 -35
- package/core/server/api/endpoints/emails.js +8 -10
- package/core/server/api/endpoints/integrations.js +20 -18
- package/core/server/api/endpoints/invites.js +8 -10
- package/core/server/api/endpoints/labels.js +19 -23
- package/core/server/api/endpoints/notifications.js +3 -4
- package/core/server/api/endpoints/pages-public.js +8 -10
- package/core/server/api/endpoints/pages.js +14 -18
- package/core/server/api/endpoints/posts-public.js +8 -10
- package/core/server/api/endpoints/posts.js +6 -8
- package/core/server/api/endpoints/previews.js +8 -10
- package/core/server/api/endpoints/redirects.js +7 -8
- package/core/server/api/endpoints/schedules.js +5 -7
- package/core/server/api/endpoints/slugs.js +7 -9
- package/core/server/api/endpoints/snippets.js +16 -20
- package/core/server/api/endpoints/tags-public.js +8 -10
- package/core/server/api/endpoints/tags.js +19 -23
- package/core/server/api/endpoints/themes.js +6 -8
- package/core/server/api/endpoints/users.js +31 -36
- package/core/server/api/endpoints/utils/permissions.js +10 -10
- package/core/server/api/endpoints/utils/serializers/output/roles.js +9 -10
- package/core/server/api/endpoints/utils/validators/input/images.js +43 -52
- package/core/server/api/endpoints/utils/validators/input/invites.js +6 -8
- package/core/server/api/endpoints/webhooks.js +38 -42
- package/core/server/data/migrations/versions/5.120/2025-05-07-14-57-38-add-newsletters-button-corners-column.js +8 -0
- package/core/server/data/migrations/versions/5.120/2025-05-13-17-36-56-add-newsletters-button-style-column.js +8 -0
- package/core/server/data/migrations/versions/5.120/2025-05-14-20-00-15-add-newsletters-setting-columns.js +22 -0
- package/core/server/data/schema/schema.js +6 -1
- package/core/server/lib/image/Gravatar.js +12 -13
- package/core/server/lib/lexical.js +3 -1
- package/core/server/models/newsletter.js +6 -1
- package/core/server/services/api-version-compatibility/index.js +1 -33
- package/core/server/services/auth/session/emails/signin.js +3 -3
- package/core/server/services/email-address/EmailAddressParser.js +52 -0
- package/core/server/services/email-address/EmailAddressParser.js.d.ts +13 -0
- package/core/server/services/email-address/EmailAddressService.js +142 -0
- package/core/server/services/email-address/EmailAddressService.ts +183 -0
- package/core/server/services/email-address/EmailAddressServiceWrapper.js +2 -4
- package/core/server/services/email-analytics/EmailAnalyticsService.js +1 -1
- package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +2 -1
- package/core/server/services/email-service/BatchSendingService.js +703 -0
- package/core/server/services/email-service/EmailBodyCache.js +20 -0
- package/core/server/services/email-service/EmailController.js +94 -0
- package/core/server/services/email-service/EmailEventProcessor.js +267 -0
- package/core/server/services/email-service/EmailEventStorage.js +187 -0
- package/core/server/services/email-service/EmailRenderer.js +1263 -0
- package/core/server/services/email-service/EmailSegmenter.js +74 -0
- package/core/server/services/email-service/EmailService.js +310 -0
- package/core/server/services/email-service/EmailServiceWrapper.js +9 -2
- package/core/server/services/email-service/MailgunEmailProvider.js +191 -0
- package/core/server/services/email-service/SendingService.js +173 -0
- package/core/server/services/email-service/email-templates/partials/feedback-button.hbs +7 -0
- package/core/server/services/email-service/email-templates/partials/latest-posts.hbs +39 -0
- package/core/server/services/email-service/email-templates/partials/paywall.hbs +20 -0
- package/core/server/services/email-service/email-templates/partials/styles.hbs +2348 -0
- package/core/server/services/email-service/email-templates/template.hbs +238 -0
- package/core/server/services/email-service/events/EmailBouncedEvent.js +63 -0
- package/core/server/services/email-service/events/EmailDeliveredEvent.js +49 -0
- package/core/server/services/email-service/events/EmailOpenedEvent.js +49 -0
- package/core/server/services/email-service/events/EmailTemporaryBouncedEvent.js +63 -0
- package/core/server/services/email-service/events/EmailUnsubscribedEvent.js +42 -0
- package/core/server/services/email-service/events/SpamComplaintEvent.js +42 -0
- package/core/server/services/email-service/helpers/register-helpers.js +59 -0
- package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +2 -1
- package/core/server/services/explore-ping/index.js +2 -1
- package/core/server/services/mail/GhostMailer.js +1 -1
- package/core/server/services/media-inliner/ExternalMediaInliner.js +2 -1
- package/core/server/services/members/api.js +15 -15
- package/core/server/services/members/emails/signin.js +4 -4
- package/core/server/services/members/emails/signup-paid.js +3 -4
- package/core/server/services/members/emails/signup.js +3 -3
- package/core/server/services/members/emails/subscribe.js +3 -3
- package/core/server/services/members/members-api/controllers/RouterController.js +50 -36
- package/core/server/services/members/members-api/repositories/MemberRepository.js +92 -92
- package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
- package/core/server/services/settings-helpers/SettingsHelpers.js +1 -1
- package/core/server/services/staff/StaffServiceEmails.js +1 -1
- package/core/server/services/stats/PostsStatsService.js +28 -7
- package/core/server/web/api/app.js +0 -1
- package/core/server/web/api/endpoints/admin/app.js +0 -2
- package/core/server/web/api/endpoints/content/app.js +0 -2
- package/core/server/web/api/middleware/upload.js +2 -2
- package/core/shared/custom-theme-settings-cache/CustomThemeSettingsService.js +2 -1
- package/package.json +39 -97
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +385 -517
- package/components/tryghost-api-framework-5.119.2.tgz +0 -0
- package/components/tryghost-custom-fonts-5.119.2.tgz +0 -0
- package/components/tryghost-domain-events-5.119.2.tgz +0 -0
- package/components/tryghost-email-addresses-5.119.2.tgz +0 -0
- package/components/tryghost-email-service-5.119.2.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.119.2.tgz +0 -0
- package/components/tryghost-i18n-5.119.2.tgz +0 -0
- package/components/tryghost-job-manager-5.119.2.tgz +0 -0
- package/components/tryghost-members-csv-5.119.2.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.119.2.tgz +0 -0
- package/components/tryghost-mw-vhost-5.119.2.tgz +0 -0
- package/components/tryghost-prometheus-metrics-5.119.2.tgz +0 -0
- package/components/tryghost-security-5.119.2.tgz +0 -0
- package/core/built/admin/assets/chunk.524.b8545af3bb714bc4f820.js +0 -35
- package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +0 -99
- package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +0 -80
- package/core/server/services/api-version-compatibility/extract-api-key.js +0 -57
- package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +0 -31
- /package/core/built/admin/assets/{chunk.137.c9bf40f01afeeadb4660.js.LICENSE.txt → chunk.383.25fca2f09b4896656125.js.LICENSE.txt} +0 -0
|
@@ -913,35 +913,35 @@ module.exports = class MemberRepository {
|
|
|
913
913
|
options.batch_id = ObjectId().toHexString();
|
|
914
914
|
}
|
|
915
915
|
|
|
916
|
-
const
|
|
916
|
+
const memberModel = await this._Member.findOne({
|
|
917
917
|
id: data.id
|
|
918
918
|
}, {...options, forUpdate: true});
|
|
919
919
|
|
|
920
|
-
const
|
|
920
|
+
const memberStripeCustomerModel = await memberModel.related('stripeCustomers').query({
|
|
921
921
|
where: {
|
|
922
922
|
customer_id: data.subscription.customer
|
|
923
923
|
}
|
|
924
924
|
}).fetchOne(options);
|
|
925
925
|
|
|
926
|
-
if (!
|
|
926
|
+
if (!memberStripeCustomerModel) {
|
|
927
927
|
// Maybe just link the customer?
|
|
928
928
|
throw new errors.NotFoundError({message: tpl(messages.subscriptionNotFound)});
|
|
929
929
|
}
|
|
930
930
|
|
|
931
|
-
const
|
|
931
|
+
const stripeSubscriptionData = await this._stripeAPIService.getSubscription(data.subscription.id);
|
|
932
932
|
let paymentMethodId;
|
|
933
|
-
if (!
|
|
933
|
+
if (!stripeSubscriptionData.default_payment_method) {
|
|
934
934
|
paymentMethodId = null;
|
|
935
|
-
} else if (typeof
|
|
936
|
-
paymentMethodId =
|
|
935
|
+
} else if (typeof stripeSubscriptionData.default_payment_method === 'string') {
|
|
936
|
+
paymentMethodId = stripeSubscriptionData.default_payment_method;
|
|
937
937
|
} else {
|
|
938
|
-
paymentMethodId =
|
|
938
|
+
paymentMethodId = stripeSubscriptionData.default_payment_method.id;
|
|
939
939
|
}
|
|
940
|
-
const
|
|
940
|
+
const stripePaymentMethodData = paymentMethodId ? await this._stripeAPIService.getCardPaymentMethod(paymentMethodId) : null;
|
|
941
941
|
|
|
942
|
-
const
|
|
942
|
+
const stripeCustomerSubscriptionModel = await this.getSubscriptionByStripeID(stripeSubscriptionData.id, {...options, forUpdate: true});
|
|
943
943
|
|
|
944
|
-
const subscriptionPriceData = _.get(
|
|
944
|
+
const subscriptionPriceData = _.get(stripeSubscriptionData, 'items.data[0].price');
|
|
945
945
|
let ghostProduct;
|
|
946
946
|
try {
|
|
947
947
|
ghostProduct = await this._productRepository.get({stripe_product_id: subscriptionPriceData.product}, options);
|
|
@@ -973,14 +973,14 @@ module.exports = class MemberRepository {
|
|
|
973
973
|
}, options);
|
|
974
974
|
} else {
|
|
975
975
|
// Log error if no Ghost products found
|
|
976
|
-
logging.error(`There was an error linking subscription - ${
|
|
976
|
+
logging.error(`There was an error linking subscription - ${stripeSubscriptionData.id}, no Products exist.`);
|
|
977
977
|
}
|
|
978
978
|
} catch (e) {
|
|
979
|
-
logging.error(`Failed to handle prices and product for - ${
|
|
979
|
+
logging.error(`Failed to handle prices and product for - ${stripeSubscriptionData.id}.`);
|
|
980
980
|
logging.error(e);
|
|
981
981
|
}
|
|
982
982
|
|
|
983
|
-
let stripeCouponId =
|
|
983
|
+
let stripeCouponId = stripeSubscriptionData.discount && stripeSubscriptionData.discount.coupon ? stripeSubscriptionData.discount.coupon.id : null;
|
|
984
984
|
|
|
985
985
|
// For trial offers, offer id is passed from metadata as there is no stripe coupon
|
|
986
986
|
let offerId = data.offerId || null;
|
|
@@ -992,26 +992,26 @@ module.exports = class MemberRepository {
|
|
|
992
992
|
if (offer) {
|
|
993
993
|
offerId = offer.id;
|
|
994
994
|
} else {
|
|
995
|
-
logging.error(`Received an unknown stripe coupon id (${stripeCouponId}) for subscription - ${
|
|
995
|
+
logging.error(`Received an unknown stripe coupon id (${stripeCouponId}) for subscription - ${stripeSubscriptionData.id}.`);
|
|
996
996
|
}
|
|
997
997
|
} else if (offerId) {
|
|
998
998
|
offer = await this._offerRepository.getById(offerId, {transacting: options.transacting});
|
|
999
999
|
}
|
|
1000
1000
|
|
|
1001
1001
|
const subscriptionData = {
|
|
1002
|
-
customer_id:
|
|
1003
|
-
subscription_id:
|
|
1004
|
-
status:
|
|
1005
|
-
cancel_at_period_end:
|
|
1006
|
-
cancellation_reason: this.getCancellationReason(
|
|
1007
|
-
current_period_end: new Date(
|
|
1008
|
-
start_date: new Date(
|
|
1009
|
-
default_payment_card_last4:
|
|
1002
|
+
customer_id: stripeSubscriptionData.customer,
|
|
1003
|
+
subscription_id: stripeSubscriptionData.id,
|
|
1004
|
+
status: stripeSubscriptionData.status,
|
|
1005
|
+
cancel_at_period_end: stripeSubscriptionData.cancel_at_period_end,
|
|
1006
|
+
cancellation_reason: this.getCancellationReason(stripeSubscriptionData),
|
|
1007
|
+
current_period_end: new Date(stripeSubscriptionData.current_period_end * 1000),
|
|
1008
|
+
start_date: new Date(stripeSubscriptionData.start_date * 1000),
|
|
1009
|
+
default_payment_card_last4: stripePaymentMethodData && stripePaymentMethodData.card && stripePaymentMethodData.card.last4 || null,
|
|
1010
1010
|
stripe_price_id: subscriptionPriceData.id,
|
|
1011
1011
|
plan_id: subscriptionPriceData.id,
|
|
1012
1012
|
// trial start and end are returned as Stripe timestamps and need coversion
|
|
1013
|
-
trial_start_at:
|
|
1014
|
-
trial_end_at:
|
|
1013
|
+
trial_start_at: stripeSubscriptionData.trial_start ? new Date(stripeSubscriptionData.trial_start * 1000) : null,
|
|
1014
|
+
trial_end_at: stripeSubscriptionData.trial_end ? new Date(stripeSubscriptionData.trial_end * 1000) : null,
|
|
1015
1015
|
// NOTE: Defaulting to interval as migration to nullable field
|
|
1016
1016
|
// turned out to be much bigger problem.
|
|
1017
1017
|
// Ideally, would need nickname field to be nullable on the DB level
|
|
@@ -1023,9 +1023,9 @@ module.exports = class MemberRepository {
|
|
|
1023
1023
|
mrr: this.getMRR({
|
|
1024
1024
|
interval: _.get(subscriptionPriceData, 'recurring.interval', ''),
|
|
1025
1025
|
amount: subscriptionPriceData.unit_amount,
|
|
1026
|
-
status:
|
|
1027
|
-
canceled:
|
|
1028
|
-
discount:
|
|
1026
|
+
status: stripeSubscriptionData.status,
|
|
1027
|
+
canceled: stripeSubscriptionData.cancel_at_period_end,
|
|
1028
|
+
discount: stripeSubscriptionData.discount
|
|
1029
1029
|
}),
|
|
1030
1030
|
offer_id: offerId
|
|
1031
1031
|
};
|
|
@@ -1050,44 +1050,44 @@ module.exports = class MemberRepository {
|
|
|
1050
1050
|
};
|
|
1051
1051
|
let eventData = {};
|
|
1052
1052
|
|
|
1053
|
-
const
|
|
1054
|
-
if (
|
|
1053
|
+
const stripeCustomerSubscriptionModelShouldBeDeleted = stripeSubscriptionData.metadata && !!stripeSubscriptionData.metadata.ghost_migrated_to && stripeSubscriptionData.status === 'canceled';
|
|
1054
|
+
if (stripeCustomerSubscriptionModelShouldBeDeleted) {
|
|
1055
1055
|
logging.warn(`Subscription ${subscriptionData.subscription_id} is marked for deletion, skipping linking.`);
|
|
1056
1056
|
|
|
1057
|
-
if (
|
|
1057
|
+
if (stripeCustomerSubscriptionModel) {
|
|
1058
1058
|
// Delete all paid subscription events manually for this subscription
|
|
1059
1059
|
// This is the only related event without a foreign key constraint
|
|
1060
|
-
await this._MemberPaidSubscriptionEvent.query().where('subscription_id',
|
|
1060
|
+
await this._MemberPaidSubscriptionEvent.query().where('subscription_id', stripeCustomerSubscriptionModel.id).delete().transacting(options.transacting);
|
|
1061
1061
|
|
|
1062
1062
|
// Delete the subscription in the database because we don't want to show it in the UI or in our data calculations
|
|
1063
|
-
await
|
|
1063
|
+
await stripeCustomerSubscriptionModel.destroy(options);
|
|
1064
1064
|
}
|
|
1065
|
-
} else if (
|
|
1065
|
+
} else if (stripeCustomerSubscriptionModel) {
|
|
1066
1066
|
// CASE: Offer is already mapped against sub, don't overwrite it with NULL
|
|
1067
1067
|
// Needed for trial offers, which don't have a stripe coupon/discount attached to sub
|
|
1068
1068
|
if (!subscriptionData.offer_id) {
|
|
1069
1069
|
delete subscriptionData.offer_id;
|
|
1070
1070
|
}
|
|
1071
|
-
const
|
|
1071
|
+
const updatedStripeCustomerSubscriptionModel = await this._StripeCustomerSubscription.edit(subscriptionData, {
|
|
1072
1072
|
...options,
|
|
1073
|
-
id:
|
|
1073
|
+
id: stripeCustomerSubscriptionModel.id
|
|
1074
1074
|
});
|
|
1075
1075
|
|
|
1076
1076
|
// CASE: Existing free member subscribes to a paid tier with an offer
|
|
1077
1077
|
// Stripe doesn't send the discount/offer info in the subscription.created event
|
|
1078
1078
|
// So we need to record the offer redemption event upon updating the subscription here
|
|
1079
|
-
if (
|
|
1080
|
-
const
|
|
1081
|
-
memberId:
|
|
1079
|
+
if (stripeCustomerSubscriptionModel.get('offer_id') === null && subscriptionData.offer_id) {
|
|
1080
|
+
const offerRedemptionEvent = OfferRedemptionEvent.create({
|
|
1081
|
+
memberId: memberModel.id,
|
|
1082
1082
|
offerId: subscriptionData.offer_id,
|
|
1083
|
-
subscriptionId:
|
|
1084
|
-
},
|
|
1085
|
-
this.dispatchEvent(
|
|
1083
|
+
subscriptionId: updatedStripeCustomerSubscriptionModel.id
|
|
1084
|
+
}, updatedStripeCustomerSubscriptionModel.get('created_at'));
|
|
1085
|
+
this.dispatchEvent(offerRedemptionEvent, options);
|
|
1086
1086
|
}
|
|
1087
1087
|
|
|
1088
|
-
if (
|
|
1089
|
-
const originalMrrDelta =
|
|
1090
|
-
const updatedMrrDelta =
|
|
1088
|
+
if (stripeCustomerSubscriptionModel.get('mrr') !== updatedStripeCustomerSubscriptionModel.get('mrr') || stripeCustomerSubscriptionModel.get('plan_id') !== updatedStripeCustomerSubscriptionModel.get('plan_id') || stripeCustomerSubscriptionModel.get('status') !== updatedStripeCustomerSubscriptionModel.get('status') || stripeCustomerSubscriptionModel.get('cancel_at_period_end') !== updatedStripeCustomerSubscriptionModel.get('cancel_at_period_end')) {
|
|
1089
|
+
const originalMrrDelta = stripeCustomerSubscriptionModel.get('mrr');
|
|
1090
|
+
const updatedMrrDelta = updatedStripeCustomerSubscriptionModel.get('mrr');
|
|
1091
1091
|
|
|
1092
1092
|
const getEventType = (originalStatus, updatedStatus) => {
|
|
1093
1093
|
if (originalStatus === updatedStatus) {
|
|
@@ -1101,19 +1101,19 @@ module.exports = class MemberRepository {
|
|
|
1101
1101
|
return updatedStatus;
|
|
1102
1102
|
};
|
|
1103
1103
|
|
|
1104
|
-
const originalStatus = getStatus(
|
|
1105
|
-
const updatedStatus = getStatus(
|
|
1104
|
+
const originalStatus = getStatus(stripeCustomerSubscriptionModel);
|
|
1105
|
+
const updatedStatus = getStatus(updatedStripeCustomerSubscriptionModel);
|
|
1106
1106
|
const eventType = getEventType(originalStatus, updatedStatus);
|
|
1107
1107
|
|
|
1108
1108
|
const mrrDelta = updatedMrrDelta - originalMrrDelta;
|
|
1109
1109
|
|
|
1110
1110
|
await this._MemberPaidSubscriptionEvent.add({
|
|
1111
|
-
member_id:
|
|
1111
|
+
member_id: memberModel.id,
|
|
1112
1112
|
source: 'stripe',
|
|
1113
1113
|
type: eventType,
|
|
1114
|
-
subscription_id:
|
|
1115
|
-
from_plan:
|
|
1116
|
-
to_plan:
|
|
1114
|
+
subscription_id: updatedStripeCustomerSubscriptionModel.id,
|
|
1115
|
+
from_plan: stripeCustomerSubscriptionModel.get('plan_id'),
|
|
1116
|
+
to_plan: updatedStripeCustomerSubscriptionModel.get('status') === 'canceled' ? null : updatedStripeCustomerSubscriptionModel.get('plan_id'),
|
|
1117
1117
|
currency: subscriptionPriceData.currency,
|
|
1118
1118
|
mrr_delta: mrrDelta
|
|
1119
1119
|
}, options);
|
|
@@ -1125,15 +1125,15 @@ module.exports = class MemberRepository {
|
|
|
1125
1125
|
const context = options?.context || {};
|
|
1126
1126
|
const source = this._resolveContextSource(context);
|
|
1127
1127
|
|
|
1128
|
-
const
|
|
1128
|
+
const subscriptionActivatedEvent = SubscriptionActivatedEvent.create({
|
|
1129
1129
|
source,
|
|
1130
1130
|
tierId: ghostProduct?.get('id'),
|
|
1131
|
-
memberId:
|
|
1132
|
-
subscriptionId:
|
|
1131
|
+
memberId: memberModel.id,
|
|
1132
|
+
subscriptionId: updatedStripeCustomerSubscriptionModel.get('id'),
|
|
1133
1133
|
offerId: offerId,
|
|
1134
1134
|
batchId: options.batch_id
|
|
1135
1135
|
});
|
|
1136
|
-
this.dispatchEvent(
|
|
1136
|
+
this.dispatchEvent(subscriptionActivatedEvent, options);
|
|
1137
1137
|
}
|
|
1138
1138
|
|
|
1139
1139
|
// Dispatch cancellation event, i.e. send paid cancellation staff notification, if:
|
|
@@ -1143,53 +1143,53 @@ module.exports = class MemberRepository {
|
|
|
1143
1143
|
const context = options?.context || {};
|
|
1144
1144
|
const source = this._resolveContextSource(context);
|
|
1145
1145
|
const cancelNow = updatedStatus === 'expired';
|
|
1146
|
-
const canceledAt = new Date(
|
|
1147
|
-
const expiryAt = cancelNow ? canceledAt :
|
|
1146
|
+
const canceledAt = new Date(stripeSubscriptionData.canceled_at * 1000);
|
|
1147
|
+
const expiryAt = cancelNow ? canceledAt : updatedStripeCustomerSubscriptionModel.get('current_period_end');
|
|
1148
1148
|
|
|
1149
|
-
const
|
|
1149
|
+
const subscriptionCancelledEvent = SubscriptionCancelledEvent.create({
|
|
1150
1150
|
source,
|
|
1151
1151
|
tierId: ghostProduct?.get('id'),
|
|
1152
|
-
memberId:
|
|
1153
|
-
subscriptionId:
|
|
1152
|
+
memberId: memberModel.id,
|
|
1153
|
+
subscriptionId: updatedStripeCustomerSubscriptionModel.get('id'),
|
|
1154
1154
|
cancelNow,
|
|
1155
1155
|
canceledAt,
|
|
1156
1156
|
expiryAt
|
|
1157
1157
|
});
|
|
1158
1158
|
|
|
1159
|
-
this.dispatchEvent(
|
|
1159
|
+
this.dispatchEvent(subscriptionCancelledEvent, options);
|
|
1160
1160
|
}
|
|
1161
1161
|
}
|
|
1162
1162
|
} else {
|
|
1163
|
-
eventData.created_at = new Date(
|
|
1164
|
-
const
|
|
1163
|
+
eventData.created_at = new Date(stripeSubscriptionData.start_date * 1000);
|
|
1164
|
+
const newStripeCustomerSubscriptionModel = await this._StripeCustomerSubscription.add(subscriptionData, options);
|
|
1165
1165
|
await this._MemberPaidSubscriptionEvent.add({
|
|
1166
|
-
member_id:
|
|
1167
|
-
subscription_id:
|
|
1166
|
+
member_id: memberModel.id,
|
|
1167
|
+
subscription_id: newStripeCustomerSubscriptionModel.id,
|
|
1168
1168
|
type: 'created',
|
|
1169
1169
|
source: 'stripe',
|
|
1170
1170
|
from_plan: null,
|
|
1171
1171
|
to_plan: subscriptionPriceData.id,
|
|
1172
1172
|
currency: subscriptionPriceData.currency,
|
|
1173
|
-
mrr_delta:
|
|
1173
|
+
mrr_delta: newStripeCustomerSubscriptionModel.get('mrr'),
|
|
1174
1174
|
...eventData
|
|
1175
1175
|
}, options);
|
|
1176
1176
|
|
|
1177
1177
|
const context = options?.context || {};
|
|
1178
1178
|
const source = this._resolveContextSource(context);
|
|
1179
1179
|
const attribution = {
|
|
1180
|
-
id: data.attribution?.id ??
|
|
1181
|
-
url: data.attribution?.url ??
|
|
1182
|
-
type: data.attribution?.type ??
|
|
1183
|
-
referrerSource: data.attribution?.referrerSource ??
|
|
1184
|
-
referrerMedium: data.attribution?.referrerMedium ??
|
|
1185
|
-
referrerUrl: data.attribution?.referrerUrl ??
|
|
1180
|
+
id: data.attribution?.id ?? stripeSubscriptionData.metadata?.attribution_id ?? null,
|
|
1181
|
+
url: data.attribution?.url ?? stripeSubscriptionData.metadata?.attribution_url ?? null,
|
|
1182
|
+
type: data.attribution?.type ?? stripeSubscriptionData.metadata?.attribution_type ?? null,
|
|
1183
|
+
referrerSource: data.attribution?.referrerSource ?? stripeSubscriptionData.metadata?.referrer_source ?? null,
|
|
1184
|
+
referrerMedium: data.attribution?.referrerMedium ?? stripeSubscriptionData.metadata?.referrer_medium ?? null,
|
|
1185
|
+
referrerUrl: data.attribution?.referrerUrl ?? stripeSubscriptionData.metadata?.referrer_url ?? null
|
|
1186
1186
|
};
|
|
1187
1187
|
|
|
1188
1188
|
const subscriptionCreatedEvent = SubscriptionCreatedEvent.create({
|
|
1189
1189
|
source,
|
|
1190
1190
|
tierId: ghostProduct?.get('id'),
|
|
1191
|
-
memberId:
|
|
1192
|
-
subscriptionId:
|
|
1191
|
+
memberId: memberModel.id,
|
|
1192
|
+
subscriptionId: newStripeCustomerSubscriptionModel.get('id'),
|
|
1193
1193
|
offerId: offerId,
|
|
1194
1194
|
attribution: attribution,
|
|
1195
1195
|
batchId: options.batch_id
|
|
@@ -1199,53 +1199,53 @@ module.exports = class MemberRepository {
|
|
|
1199
1199
|
|
|
1200
1200
|
if (offerId) {
|
|
1201
1201
|
const offerRedemptionEvent = OfferRedemptionEvent.create({
|
|
1202
|
-
memberId:
|
|
1202
|
+
memberId: memberModel.id,
|
|
1203
1203
|
offerId: offerId,
|
|
1204
|
-
subscriptionId:
|
|
1204
|
+
subscriptionId: newStripeCustomerSubscriptionModel.get('id')
|
|
1205
1205
|
});
|
|
1206
1206
|
this.dispatchEvent(offerRedemptionEvent, options);
|
|
1207
1207
|
}
|
|
1208
1208
|
|
|
1209
|
-
if (getStatus(
|
|
1210
|
-
const
|
|
1209
|
+
if (getStatus(newStripeCustomerSubscriptionModel) === 'active') {
|
|
1210
|
+
const subscriptionActivatedEvent = SubscriptionActivatedEvent.create({
|
|
1211
1211
|
source,
|
|
1212
1212
|
tierId: ghostProduct?.get('id'),
|
|
1213
|
-
memberId:
|
|
1214
|
-
subscriptionId:
|
|
1213
|
+
memberId: memberModel.id,
|
|
1214
|
+
subscriptionId: newStripeCustomerSubscriptionModel.get('id'),
|
|
1215
1215
|
offerId: offerId,
|
|
1216
1216
|
attribution: attribution,
|
|
1217
1217
|
batchId: options.batch_id
|
|
1218
1218
|
});
|
|
1219
|
-
this.dispatchEvent(
|
|
1219
|
+
this.dispatchEvent(subscriptionActivatedEvent, options);
|
|
1220
1220
|
}
|
|
1221
1221
|
}
|
|
1222
1222
|
|
|
1223
|
-
let memberProducts = (await
|
|
1224
|
-
const oldMemberProducts =
|
|
1223
|
+
let memberProducts = (await memberModel.related('products').fetch(options)).toJSON();
|
|
1224
|
+
const oldMemberProducts = memberModel.related('products').toJSON();
|
|
1225
1225
|
let status = memberProducts.length === 0 ? 'free' : 'comped';
|
|
1226
|
-
if (!
|
|
1227
|
-
if (this.isComplimentarySubscription(
|
|
1226
|
+
if (!stripeCustomerSubscriptionModelShouldBeDeleted && this.isActiveSubscriptionStatus(stripeSubscriptionData.status)) {
|
|
1227
|
+
if (this.isComplimentarySubscription(stripeSubscriptionData)) {
|
|
1228
1228
|
status = 'comped';
|
|
1229
1229
|
} else {
|
|
1230
1230
|
status = 'paid';
|
|
1231
1231
|
}
|
|
1232
1232
|
|
|
1233
|
-
if (
|
|
1233
|
+
if (stripeCustomerSubscriptionModel) {
|
|
1234
1234
|
// We might need to...
|
|
1235
1235
|
// 1. delete the previous product from the linked member products (in case an existing subscription changed product/price)
|
|
1236
1236
|
// 2. fix the list of products linked to a member (an existing subscription doesn't have a linked product to this member)
|
|
1237
1237
|
|
|
1238
|
-
const subscriptions = await
|
|
1238
|
+
const subscriptions = await memberModel.related('stripeSubscriptions').fetch(options);
|
|
1239
1239
|
|
|
1240
1240
|
const previousProduct = await this._productRepository.get({
|
|
1241
|
-
stripe_price_id:
|
|
1241
|
+
stripe_price_id: stripeCustomerSubscriptionModel.get('stripe_price_id')
|
|
1242
1242
|
}, options);
|
|
1243
1243
|
|
|
1244
1244
|
if (previousProduct) {
|
|
1245
1245
|
let activeSubscriptionForPreviousProduct = false;
|
|
1246
1246
|
|
|
1247
1247
|
for (const subscriptionModel of subscriptions.models) {
|
|
1248
|
-
if (this.isActiveSubscriptionStatus(subscriptionModel.get('status')) && subscriptionModel.id !==
|
|
1248
|
+
if (this.isActiveSubscriptionStatus(subscriptionModel.get('status')) && subscriptionModel.id !== stripeCustomerSubscriptionModel.id) {
|
|
1249
1249
|
try {
|
|
1250
1250
|
const subscriptionProduct = await this._productRepository.get({stripe_price_id: subscriptionModel.get('stripe_price_id')}, options);
|
|
1251
1251
|
if (subscriptionProduct && previousProduct && subscriptionProduct.id === previousProduct.id) {
|
|
@@ -1280,7 +1280,7 @@ module.exports = class MemberRepository {
|
|
|
1280
1280
|
memberProducts.push(ghostProduct.toJSON());
|
|
1281
1281
|
}
|
|
1282
1282
|
} else {
|
|
1283
|
-
const subscriptions = await
|
|
1283
|
+
const subscriptions = await memberModel.related('stripeSubscriptions').fetch(options);
|
|
1284
1284
|
let activeSubscriptionForGhostProduct = false;
|
|
1285
1285
|
for (const subscriptionModel of subscriptions.models) {
|
|
1286
1286
|
if (this.isActiveSubscriptionStatus(subscriptionModel.get('status'))) {
|
|
@@ -1338,7 +1338,7 @@ module.exports = class MemberRepository {
|
|
|
1338
1338
|
|
|
1339
1339
|
for (const productToAdd of productsToAdd) {
|
|
1340
1340
|
await this._MemberProductEvent.add({
|
|
1341
|
-
member_id:
|
|
1341
|
+
member_id: memberModel.id,
|
|
1342
1342
|
product_id: productToAdd,
|
|
1343
1343
|
action: 'added'
|
|
1344
1344
|
}, options);
|
|
@@ -1346,7 +1346,7 @@ module.exports = class MemberRepository {
|
|
|
1346
1346
|
|
|
1347
1347
|
for (const productToRemove of productsToRemove) {
|
|
1348
1348
|
await this._MemberProductEvent.add({
|
|
1349
|
-
member_id:
|
|
1349
|
+
member_id: memberModel.id,
|
|
1350
1350
|
product_id: productToRemove,
|
|
1351
1351
|
action: 'removed'
|
|
1352
1352
|
}, options);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const {MemberPageViewEvent, MemberCommentEvent, MemberLinkClickEvent} = require('../../../shared/events');
|
|
2
2
|
const moment = require('moment-timezone');
|
|
3
3
|
const {IncorrectUsageError} = require('@tryghost/errors');
|
|
4
|
-
const
|
|
4
|
+
const EmailOpenedEvent = require('../../services/email-service/events/EmailOpenedEvent');
|
|
5
5
|
const logging = require('@tryghost/logging');
|
|
6
6
|
const LastSeenAtCache = require('./LastSeenAtCache');
|
|
7
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const tpl = require('@tryghost/tpl');
|
|
2
2
|
const errors = require('@tryghost/errors');
|
|
3
|
-
const
|
|
3
|
+
const EmailAddressParser = require('../email-address/EmailAddressParser');
|
|
4
4
|
const logging = require('@tryghost/logging');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
|
|
@@ -2,7 +2,7 @@ const {promises: fs, readFileSync} = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const moment = require('moment');
|
|
4
4
|
const glob = require('glob');
|
|
5
|
-
const
|
|
5
|
+
const EmailAddressParser = require('../email-address/EmailAddressParser');
|
|
6
6
|
|
|
7
7
|
class StaffServiceEmails {
|
|
8
8
|
constructor({logging, models, mailer, settingsHelpers, settingsCache, blogIcon, urlUtils, labs}) {
|
|
@@ -139,24 +139,45 @@ class PostsStatsService {
|
|
|
139
139
|
const paidReferrersCTE = this._buildPaidReferrersSubquery(postId, options);
|
|
140
140
|
const mrrReferrersCTE = this._buildMrrReferrersSubquery(postId, options);
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
// First, let's get all sources from both tables using separate queries
|
|
143
|
+
// Then combine and group them in a cross-database compatible way
|
|
144
|
+
const membersCreatedSources = this.knex('members_created_events as mce')
|
|
143
145
|
.select('mce.referrer_source as source')
|
|
146
|
+
.select('mce.referrer_url')
|
|
144
147
|
.where('mce.attribution_id', postId)
|
|
145
148
|
.where('mce.attribution_type', 'post')
|
|
146
|
-
.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
.whereNotNull('mce.referrer_source');
|
|
150
|
+
|
|
151
|
+
const membersSubscriptionSources = this.knex('members_subscription_created_events as msce')
|
|
152
|
+
.select('msce.referrer_source as source')
|
|
153
|
+
.select('msce.referrer_url')
|
|
154
|
+
.where('msce.attribution_id', postId)
|
|
155
|
+
.where('msce.attribution_type', 'post')
|
|
156
|
+
.whereNotNull('msce.referrer_source');
|
|
157
|
+
|
|
158
|
+
// Using a simpler combined query that works in SQLite
|
|
159
|
+
const allSources = this.knex.select('source', 'referrer_url')
|
|
160
|
+
.from(membersCreatedSources.as('sources1'))
|
|
161
|
+
.union(function () {
|
|
162
|
+
this.select('source', 'referrer_url')
|
|
163
|
+
.from(membersSubscriptionSources.as('sources2'));
|
|
151
164
|
});
|
|
152
165
|
|
|
166
|
+
// Create the final CTE that we'll use to get all referrers
|
|
167
|
+
const allReferrersCTE = this.knex.select('source')
|
|
168
|
+
.select(this.knex.raw('MIN(referrer_url) as referrer_url'))
|
|
169
|
+
.from(allSources.as('all_sources'))
|
|
170
|
+
.groupBy('source');
|
|
171
|
+
|
|
172
|
+
// Now join all the data
|
|
153
173
|
let query = this.knex
|
|
154
174
|
.with('free_referrers', freeReferrersCTE)
|
|
155
175
|
.with('paid_referrers', paidReferrersCTE)
|
|
156
176
|
.with('mrr_referrers', mrrReferrersCTE)
|
|
157
|
-
.with('all_referrers',
|
|
177
|
+
.with('all_referrers', allReferrersCTE)
|
|
158
178
|
.select(
|
|
159
179
|
'ar.source',
|
|
180
|
+
'ar.referrer_url',
|
|
160
181
|
this.knex.raw('COALESCE(fr.free_members, 0) as free_members'),
|
|
161
182
|
this.knex.raw('COALESCE(pr.paid_members, 0) as paid_members'),
|
|
162
183
|
this.knex.raw('COALESCE(mr.mrr, 0) as mrr')
|
|
@@ -24,7 +24,6 @@ module.exports = function setupApiApp() {
|
|
|
24
24
|
|
|
25
25
|
// Error handling for requests to non-existent API versions
|
|
26
26
|
apiApp.use(errorHandler.resourceNotFound);
|
|
27
|
-
apiApp.use(APIVersionCompatibilityService.errorHandler);
|
|
28
27
|
apiApp.use(errorHandler.handleJSONResponse(sentry));
|
|
29
28
|
|
|
30
29
|
debug('Parent API setup end');
|
|
@@ -8,7 +8,6 @@ const shared = require('../../../shared');
|
|
|
8
8
|
const express = require('../../../../../shared/express');
|
|
9
9
|
const sentry = require('../../../../../shared/sentry');
|
|
10
10
|
const routes = require('./routes');
|
|
11
|
-
const APIVersionCompatibilityService = require('../../../../services/api-version-compatibility');
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* @returns {import('express').Application}
|
|
@@ -38,7 +37,6 @@ module.exports = function setupApiApp() {
|
|
|
38
37
|
|
|
39
38
|
// API error handling
|
|
40
39
|
apiApp.use(errorHandler.resourceNotFound);
|
|
41
|
-
apiApp.use(APIVersionCompatibilityService.errorHandler);
|
|
42
40
|
apiApp.use(errorHandler.handleJSONResponse(sentry));
|
|
43
41
|
|
|
44
42
|
debug('Admin API setup end');
|
|
@@ -7,7 +7,6 @@ const config = require('../../../../../shared/config');
|
|
|
7
7
|
const shared = require('../../../shared');
|
|
8
8
|
const routes = require('./routes');
|
|
9
9
|
const errorHandler = require('@tryghost/mw-error-handler');
|
|
10
|
-
const apiVersionCompatibility = require('../../../../services/api-version-compatibility');
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* @returns {import('express').Application}
|
|
@@ -34,7 +33,6 @@ module.exports = function setupApiApp() {
|
|
|
34
33
|
|
|
35
34
|
// API error handling
|
|
36
35
|
apiApp.use(errorHandler.resourceNotFound);
|
|
37
|
-
apiApp.use(apiVersionCompatibility.errorHandler);
|
|
38
36
|
apiApp.use(errorHandler.handleJSONResponse(sentry));
|
|
39
37
|
|
|
40
38
|
debug('Content API setup end');
|
|
@@ -164,7 +164,7 @@ const checkFileIsValid = (fileData, types, extensions) => {
|
|
|
164
164
|
/**
|
|
165
165
|
*
|
|
166
166
|
* @param {String} filepath
|
|
167
|
-
* @returns {String | null}
|
|
167
|
+
* @returns {Promise<String | null>}
|
|
168
168
|
*
|
|
169
169
|
* Reads the SVG file, sanitizes it, and writes the sanitized content back to the file.
|
|
170
170
|
* Returns the sanitized content or null if the SVG could not be sanitized.
|
|
@@ -216,7 +216,7 @@ const sanitizeSvgContent = (content) => {
|
|
|
216
216
|
*
|
|
217
217
|
* @param {String} filepath
|
|
218
218
|
* @param {Boolean} isZipped
|
|
219
|
-
* @returns {String | null}
|
|
219
|
+
* @returns {Promise<String | null>}
|
|
220
220
|
*
|
|
221
221
|
* Reads .svg or .svgz files and returns the content as a string.
|
|
222
222
|
*
|
|
@@ -167,8 +167,9 @@ module.exports = class CustomThemeSettingsService {
|
|
|
167
167
|
// Private -----------------------------------------------------------------
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
|
+
* @param {string} name - name of the theme
|
|
170
171
|
* @param {Object} theme - checked theme output from gscan
|
|
171
|
-
* @returns {Array} - list of stored theme record objects
|
|
172
|
+
* @returns {Promise<Array>} - list of stored theme record objects
|
|
172
173
|
* @private
|
|
173
174
|
*/
|
|
174
175
|
async _syncRepositoryWithTheme(name, theme) {
|