ghost 5.114.1 → 5.115.1

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.
Files changed (201) hide show
  1. package/components/tryghost-adapter-cache-redis-5.115.1.tgz +0 -0
  2. package/components/{tryghost-adapter-manager-5.114.1.tgz → tryghost-adapter-manager-5.115.1.tgz} +0 -0
  3. package/components/{tryghost-announcement-bar-settings-5.114.1.tgz → tryghost-announcement-bar-settings-5.115.1.tgz} +0 -0
  4. package/components/{tryghost-api-framework-5.114.1.tgz → tryghost-api-framework-5.115.1.tgz} +0 -0
  5. package/components/tryghost-constants-5.115.1.tgz +0 -0
  6. package/components/tryghost-custom-fonts-5.115.1.tgz +0 -0
  7. package/components/{tryghost-custom-theme-settings-service-5.114.1.tgz → tryghost-custom-theme-settings-service-5.115.1.tgz} +0 -0
  8. package/components/{tryghost-data-generator-5.114.1.tgz → tryghost-data-generator-5.115.1.tgz} +0 -0
  9. package/components/{tryghost-domain-events-5.114.1.tgz → tryghost-domain-events-5.115.1.tgz} +0 -0
  10. package/components/tryghost-donations-5.115.1.tgz +0 -0
  11. package/components/tryghost-email-addresses-5.115.1.tgz +0 -0
  12. package/components/{tryghost-email-content-generator-5.114.1.tgz → tryghost-email-content-generator-5.115.1.tgz} +0 -0
  13. package/components/tryghost-email-events-5.115.1.tgz +0 -0
  14. package/components/tryghost-email-service-5.115.1.tgz +0 -0
  15. package/components/tryghost-email-suppression-list-5.115.1.tgz +0 -0
  16. package/components/tryghost-express-dynamic-redirects-5.115.1.tgz +0 -0
  17. package/components/tryghost-ghost-5.115.1.tgz +0 -0
  18. package/components/{tryghost-html-to-plaintext-5.114.1.tgz → tryghost-html-to-plaintext-5.115.1.tgz} +0 -0
  19. package/components/tryghost-i18n-5.115.1.tgz +0 -0
  20. package/components/tryghost-importer-handler-content-files-5.115.1.tgz +0 -0
  21. package/components/tryghost-in-memory-repository-5.115.1.tgz +0 -0
  22. package/components/{tryghost-job-manager-5.114.1.tgz → tryghost-job-manager-5.115.1.tgz} +0 -0
  23. package/components/{tryghost-link-redirects-5.114.1.tgz → tryghost-link-redirects-5.115.1.tgz} +0 -0
  24. package/components/tryghost-link-replacer-5.115.1.tgz +0 -0
  25. package/components/{tryghost-magic-link-5.114.1.tgz → tryghost-magic-link-5.115.1.tgz} +0 -0
  26. package/components/{tryghost-mailgun-client-5.114.1.tgz → tryghost-mailgun-client-5.115.1.tgz} +0 -0
  27. package/components/tryghost-member-attribution-5.115.1.tgz +0 -0
  28. package/components/{tryghost-member-events-5.114.1.tgz → tryghost-member-events-5.115.1.tgz} +0 -0
  29. package/components/{tryghost-members-api-5.114.1.tgz → tryghost-members-api-5.115.1.tgz} +0 -0
  30. package/components/{tryghost-members-csv-5.114.1.tgz → tryghost-members-csv-5.115.1.tgz} +0 -0
  31. package/components/{tryghost-members-offers-5.114.1.tgz → tryghost-members-offers-5.115.1.tgz} +0 -0
  32. package/components/{tryghost-members-payments-5.114.1.tgz → tryghost-members-payments-5.115.1.tgz} +0 -0
  33. package/components/{tryghost-milestones-5.114.1.tgz → tryghost-milestones-5.115.1.tgz} +0 -0
  34. package/components/tryghost-minifier-5.115.1.tgz +0 -0
  35. package/components/{tryghost-mw-error-handler-5.114.1.tgz → tryghost-mw-error-handler-5.115.1.tgz} +0 -0
  36. package/components/{tryghost-mw-version-match-5.114.1.tgz → tryghost-mw-version-match-5.115.1.tgz} +0 -0
  37. package/components/tryghost-mw-vhost-5.115.1.tgz +0 -0
  38. package/components/{tryghost-post-events-5.114.1.tgz → tryghost-post-events-5.115.1.tgz} +0 -0
  39. package/components/{tryghost-post-revisions-5.114.1.tgz → tryghost-post-revisions-5.115.1.tgz} +0 -0
  40. package/components/{tryghost-posts-service-5.114.1.tgz → tryghost-posts-service-5.115.1.tgz} +0 -0
  41. package/components/{tryghost-prometheus-metrics-5.114.1.tgz → tryghost-prometheus-metrics-5.115.1.tgz} +0 -0
  42. package/components/tryghost-recommendations-5.115.1.tgz +0 -0
  43. package/components/{tryghost-security-5.114.1.tgz → tryghost-security-5.115.1.tgz} +0 -0
  44. package/components/tryghost-slack-notifications-5.115.1.tgz +0 -0
  45. package/components/{tryghost-tiers-5.114.1.tgz → tryghost-tiers-5.115.1.tgz} +0 -0
  46. package/components/{tryghost-webmentions-5.114.1.tgz → tryghost-webmentions-5.115.1.tgz} +0 -0
  47. package/content/themes/casper/LICENSE +1 -1
  48. package/content/themes/casper/README.md +1 -1
  49. package/content/themes/source/LICENSE +1 -1
  50. package/content/themes/source/README.md +1 -1
  51. package/content/themes/source/assets/built/screen.css +1 -1
  52. package/content/themes/source/assets/built/screen.css.map +1 -1
  53. package/content/themes/source/assets/css/screen.css +11 -6
  54. package/content/themes/source/partials/feature-image.hbs +2 -2
  55. package/core/boot.js +3 -1
  56. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +23497 -23041
  57. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  58. package/core/built/admin/assets/admin-x-demo/{index-0040480a.mjs → index-15df2af5.mjs} +4 -3
  59. package/core/built/admin/assets/admin-x-demo/{modals-fb35c86c.mjs → modals-8ca61d78.mjs} +67 -65
  60. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-806ef39c.mjs → CodeEditorView-d2e6872f.mjs} +2 -2
  61. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  62. package/core/built/admin/assets/admin-x-settings/{index-376f847c.mjs → index-8e8821e5.mjs} +2 -2
  63. package/core/built/admin/assets/admin-x-settings/{index-8fa19303.mjs → index-f5cb3db3.mjs} +3104 -3094
  64. package/core/built/admin/assets/admin-x-settings/{modals-36775d71.mjs → modals-e8ae4d46.mjs} +3 -3
  65. package/core/built/admin/assets/{chunk.524.85c5b32bd46b91c147b9.js → chunk.524.2439684964c164c598ab.js} +7 -7
  66. package/core/built/admin/assets/{chunk.582.449a129a8005f03574bd.js → chunk.582.bf5a2bbb2c4eb69ef1e7.js} +10 -10
  67. package/core/built/admin/assets/ghost-327b17ea23cb8c89bd7e6a51e18e8506.css +1 -0
  68. package/core/built/admin/assets/ghost-dark-f30a597ac19632a118939492591c531b.css +1 -0
  69. package/core/built/admin/assets/{ghost-c563138cc2c0767bf6eefc9a2587eaa4.js → ghost-df7b9558260aa27d18b195ee895b487d.js} +182 -160
  70. package/core/built/admin/assets/stats/stats.js +11824 -0
  71. package/core/built/admin/index.html +4 -4
  72. package/core/frontend/helpers/ghost_head.js +3 -1
  73. package/core/frontend/src/cards/css/cta.css +1 -1
  74. package/core/server/api/endpoints/slugs.js +6 -2
  75. package/core/server/data/importer/import-manager.js +2 -2
  76. package/core/server/data/importer/importers/importer-revue.js +128 -0
  77. package/core/server/data/importer/importers/json-to-html.js +107 -0
  78. package/core/server/data/migrations/utils/tables.js +2 -4
  79. package/core/server/data/migrations/versions/5.115/2025-03-24-07-19-27-add-identity-read-permission-to-administrators.js +6 -0
  80. package/core/server/data/schema/fixtures/fixtures.json +2 -1
  81. package/core/server/lib/bootstrap-socket.js +87 -0
  82. package/core/server/lib/package-json/index.js +1 -0
  83. package/core/server/lib/package-json/package-json.js +160 -0
  84. package/core/server/lib/package-json/parse.js +57 -0
  85. package/core/server/models/base/plugins/actions.js +44 -31
  86. package/core/server/models/base/plugins/generate-slug.js +6 -0
  87. package/core/server/notify.js +1 -1
  88. package/core/server/services/activitypub/ActivityPubService.ts +1 -1
  89. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +99 -0
  90. package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +80 -0
  91. package/core/server/services/api-version-compatibility/extract-api-key.js +57 -0
  92. package/core/server/services/api-version-compatibility/index.js +2 -2
  93. package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +31 -0
  94. package/core/server/services/audience-feedback/AudienceFeedbackController.js +85 -0
  95. package/core/server/services/audience-feedback/AudienceFeedbackService.js +34 -0
  96. package/core/server/services/audience-feedback/Feedback.js +35 -0
  97. package/core/server/services/audience-feedback/index.js +4 -2
  98. package/core/server/services/auth/session/emails/signin.js +168 -0
  99. package/core/server/services/auth/session/index.js +2 -2
  100. package/core/server/services/auth/session/session-from-token.js +69 -0
  101. package/core/server/services/auth/session/session-service.js +364 -0
  102. package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +62 -0
  103. package/core/server/services/email-analytics/EmailAnalyticsService.js +552 -0
  104. package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +3 -3
  105. package/core/server/services/email-analytics/EventProcessingResult.js +66 -0
  106. package/core/server/services/explore-ping/ExplorePingService.js +106 -0
  107. package/core/server/services/explore-ping/index.js +31 -0
  108. package/core/server/services/identity-tokens/IdentityTokenService.js +30 -0
  109. package/core/server/services/identity-tokens/IdentityTokenService.ts +28 -0
  110. package/core/server/services/identity-tokens/IdentityTokenServiceWrapper.js +1 -1
  111. package/core/server/services/invitations/accept.js +5 -2
  112. package/core/server/services/mail-events/BookshelfMailEventRepository.js +2 -2
  113. package/core/server/services/mail-events/InMemoryMailEventRepository.js +10 -0
  114. package/core/server/services/mail-events/InMemoryMailEventRepository.ts +8 -0
  115. package/core/server/services/mail-events/MailEvent.js +20 -0
  116. package/core/server/services/mail-events/MailEvent.ts +10 -0
  117. package/core/server/services/mail-events/MailEventRepository.js +2 -0
  118. package/core/server/services/mail-events/MailEventRepository.ts +5 -0
  119. package/core/server/services/mail-events/MailEventService.js +124 -0
  120. package/core/server/services/mail-events/MailEventService.ts +169 -0
  121. package/core/server/services/mail-events/index.js +1 -1
  122. package/core/server/services/mail-events/libraries.d.ts +2 -0
  123. package/core/server/services/members/CaptchaService.js +80 -0
  124. package/core/server/services/members/api.js +1 -1
  125. package/core/server/services/members/importer/MembersCSVImporter.js +464 -0
  126. package/core/server/services/members/importer/MembersCSVImporterStripeUtils.js +194 -0
  127. package/core/server/services/members/importer/email-template.js +182 -0
  128. package/core/server/services/members/importer/index.js +30 -0
  129. package/core/server/services/members/members-ssr.js +333 -0
  130. package/core/server/services/members/service.js +2 -2
  131. package/core/server/services/posts/stats/PostStats.js +13 -0
  132. package/core/server/services/route-settings/SettingsPathManager.js +47 -0
  133. package/core/server/services/route-settings/index.js +1 -1
  134. package/core/server/services/stripe/README.md +63 -0
  135. package/core/server/services/stripe/StripeAPI.js +931 -0
  136. package/core/server/services/stripe/StripeMigrations.js +613 -0
  137. package/core/server/services/stripe/StripeService.js +175 -0
  138. package/core/server/services/stripe/WebhookController.js +100 -0
  139. package/core/server/services/stripe/WebhookManager.js +175 -0
  140. package/core/server/services/stripe/events/StripeLiveDisabledEvent.js +23 -0
  141. package/core/server/services/stripe/events/StripeLiveEnabledEvent.js +23 -0
  142. package/core/server/services/stripe/events/index.js +4 -0
  143. package/core/server/services/stripe/service.js +1 -1
  144. package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +255 -0
  145. package/core/server/services/stripe/services/webhook/InvoiceEventService.js +70 -0
  146. package/core/server/services/stripe/services/webhook/SubscriptionEventService.js +54 -0
  147. package/core/server/services/themes/loader.js +1 -1
  148. package/core/server/services/themes/to-json.js +1 -1
  149. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  150. package/core/server/web/shared/middleware/cache-control.js +51 -0
  151. package/core/server/web/shared/middleware/index.js +1 -1
  152. package/core/server/web/well-known.js +1 -1
  153. package/core/shared/labs.js +3 -1
  154. package/core/shared/settings-cache/CacheManager.js +64 -6
  155. package/package.json +103 -134
  156. package/tsconfig.tsbuildinfo +1 -1
  157. package/yarn.lock +7 -93
  158. package/components/tryghost-adapter-cache-redis-5.114.1.tgz +0 -0
  159. package/components/tryghost-api-version-compatibility-service-5.114.1.tgz +0 -0
  160. package/components/tryghost-audience-feedback-5.114.1.tgz +0 -0
  161. package/components/tryghost-bookshelf-repository-5.114.1.tgz +0 -0
  162. package/components/tryghost-bootstrap-socket-5.114.1.tgz +0 -0
  163. package/components/tryghost-captcha-service-5.114.1.tgz +0 -0
  164. package/components/tryghost-constants-5.114.1.tgz +0 -0
  165. package/components/tryghost-custom-fonts-5.114.1.tgz +0 -0
  166. package/components/tryghost-donations-5.114.1.tgz +0 -0
  167. package/components/tryghost-email-addresses-5.114.1.tgz +0 -0
  168. package/components/tryghost-email-analytics-provider-mailgun-5.114.1.tgz +0 -0
  169. package/components/tryghost-email-analytics-service-5.114.1.tgz +0 -0
  170. package/components/tryghost-email-events-5.114.1.tgz +0 -0
  171. package/components/tryghost-email-service-5.114.1.tgz +0 -0
  172. package/components/tryghost-email-suppression-list-5.114.1.tgz +0 -0
  173. package/components/tryghost-express-dynamic-redirects-5.114.1.tgz +0 -0
  174. package/components/tryghost-extract-api-key-5.114.1.tgz +0 -0
  175. package/components/tryghost-ghost-5.114.1.tgz +0 -0
  176. package/components/tryghost-i18n-5.114.1.tgz +0 -0
  177. package/components/tryghost-identity-token-service-5.114.1.tgz +0 -0
  178. package/components/tryghost-importer-handler-content-files-5.114.1.tgz +0 -0
  179. package/components/tryghost-importer-revue-5.114.1.tgz +0 -0
  180. package/components/tryghost-in-memory-repository-5.114.1.tgz +0 -0
  181. package/components/tryghost-link-replacer-5.114.1.tgz +0 -0
  182. package/components/tryghost-mail-events-5.114.1.tgz +0 -0
  183. package/components/tryghost-member-attribution-5.114.1.tgz +0 -0
  184. package/components/tryghost-members-importer-5.114.1.tgz +0 -0
  185. package/components/tryghost-members-ssr-5.114.1.tgz +0 -0
  186. package/components/tryghost-members-stripe-service-5.114.1.tgz +0 -0
  187. package/components/tryghost-minifier-5.114.1.tgz +0 -0
  188. package/components/tryghost-mw-api-version-mismatch-5.114.1.tgz +0 -0
  189. package/components/tryghost-mw-cache-control-5.114.1.tgz +0 -0
  190. package/components/tryghost-mw-session-from-token-5.114.1.tgz +0 -0
  191. package/components/tryghost-mw-update-user-last-seen-5.114.1.tgz +0 -0
  192. package/components/tryghost-mw-vhost-5.114.1.tgz +0 -0
  193. package/components/tryghost-package-json-5.114.1.tgz +0 -0
  194. package/components/tryghost-recommendations-5.114.1.tgz +0 -0
  195. package/components/tryghost-referrers-5.114.1.tgz +0 -0
  196. package/components/tryghost-session-service-5.114.1.tgz +0 -0
  197. package/components/tryghost-settings-path-manager-5.114.1.tgz +0 -0
  198. package/components/tryghost-slack-notifications-5.114.1.tgz +0 -0
  199. package/components/tryghost-version-notifications-data-service-5.114.1.tgz +0 -0
  200. package/core/built/admin/assets/ghost-c2a7c4a1b76550c4219adb2ed4124ce0.css +0 -1
  201. package/core/built/admin/assets/ghost-dark-f91e4a479c6d38d94d5d1b14727871dc.css +0 -1
@@ -0,0 +1,194 @@
1
+ const {DataImportError} = require('@tryghost/errors');
2
+ const tpl = require('@tryghost/tpl');
3
+
4
+ const messages = {
5
+ productNotFound: 'Cannot find Product {id}',
6
+ noStripeConnection: 'Cannot {action} without a Stripe Connection',
7
+ forceNoCustomer: 'Cannot find Stripe customer to update subscription',
8
+ forceNoExistingSubscription: 'Cannot update subscription when customer does not have an existing subscription',
9
+ forceTooManySubscriptions: 'Cannot update subscription when customer has multiple subscriptions',
10
+ forceTooManySubscriptionItems: 'Cannot update subscription when existing subscription has multiple items',
11
+ forceExistingSubscriptionNotRecurring: 'Cannot update subscription when existing subscription is not recurring'
12
+ };
13
+
14
+ module.exports = class MembersCSVImporterStripeUtils {
15
+ /**
16
+ * @param {Object} stripeAPIService
17
+ * @param {Object} productRepository
18
+ */
19
+ constructor({
20
+ stripeAPIService,
21
+ productRepository
22
+ }) {
23
+ this._stripeAPIService = stripeAPIService;
24
+ this._productRepository = productRepository;
25
+ }
26
+
27
+ /**
28
+ * Force a Stripe customer to be subscribed to a specific Ghost product
29
+ *
30
+ * This will either:
31
+ *
32
+ * Create a new price on the Stripe product that is associated with the Ghost product, then update
33
+ * the customer's Stripe subscription to use the new price. The new price will be created with the details of the
34
+ * existing price of the item in customer's Stripe subscription
35
+ *
36
+ * or
37
+ *
38
+ * Update the customer's stripe subscription to use an existing price on the Stripe product that matches the
39
+ * details of the existing price of the item in customer's Stripe subscription
40
+ *
41
+ * If there is no Stripe product associated with the Ghost product, one will be created
42
+ *
43
+ * This method should be used in-conjunction with `MembersRepository.linkSubscription` to ensure
44
+ * that the changes made in Stripe are reflected in Ghost - This is not executed as part of this to allow for
45
+ * flexibility and reduce duplication
46
+ *
47
+ * @param {Object} data
48
+ * @param {String} data.customer_id - Stripe customer ID
49
+ * @param {String} data.product_id - Ghost product ID
50
+ * @param {Object} options
51
+ * @returns {Promise<Object>}
52
+ */
53
+ async forceStripeSubscriptionToProduct(data, options) {
54
+ if (!this._stripeAPIService.configured) {
55
+ throw new DataImportError({
56
+ message: tpl(messages.noStripeConnection, {action: 'force subscription to product'})
57
+ });
58
+ }
59
+
60
+ // Retrieve customer's existing subscription information
61
+ const stripeCustomer = await this._stripeAPIService.getCustomer(data.customer_id);
62
+
63
+ // Subscription can only be forced if the customer exists
64
+ if (!stripeCustomer) {
65
+ throw new DataImportError({message: tpl(messages.forceNoCustomer)});
66
+ }
67
+
68
+ // Subscription can only be forced if the customer has an existing subscription
69
+ if (stripeCustomer.subscriptions.data.length === 0) {
70
+ throw new DataImportError({message: tpl(messages.forceNoExistingSubscription)});
71
+ }
72
+
73
+ // Subscription can only be forced if the customer does not have multiple subscriptions
74
+ if (stripeCustomer.subscriptions.data.length > 1) {
75
+ throw new DataImportError({message: tpl(messages.forceTooManySubscriptions)});
76
+ }
77
+
78
+ const stripeSubscription = stripeCustomer.subscriptions.data[0];
79
+
80
+ // Subscription can only be forced if the existing subscription does not have multiple items
81
+ if (stripeSubscription.items.data.length > 1) {
82
+ throw new DataImportError({message: tpl(messages.forceTooManySubscriptionItems)});
83
+ }
84
+
85
+ const stripeSubscriptionItem = stripeSubscription.items.data[0];
86
+ const stripeSubscriptionItemPrice = stripeSubscriptionItem.price;
87
+ const stripeSubscriptionItemPriceCurrency = stripeSubscriptionItemPrice.currency;
88
+ const stripeSubscriptionItemPriceAmount = stripeSubscriptionItemPrice.unit_amount;
89
+ const stripeSubscriptionItemPriceType = stripeSubscriptionItemPrice.type;
90
+ const stripeSubscriptionItemPriceInterval = stripeSubscriptionItemPrice.recurring?.interval || null;
91
+
92
+ // Subscription can only be forced if the existing subscription has a recurring interval
93
+ if (!stripeSubscriptionItemPriceInterval) {
94
+ throw new DataImportError({message: tpl(messages.forceExistingSubscriptionNotRecurring)});
95
+ }
96
+
97
+ // Retrieve Ghost product
98
+ let ghostProduct = await this._productRepository.get(
99
+ {id: data.product_id},
100
+ {...options, withRelated: ['stripePrices', 'stripeProducts']}
101
+ );
102
+
103
+ if (!ghostProduct) {
104
+ throw new DataImportError({message: tpl(messages.productNotFound, {id: data.product_id})});
105
+ }
106
+
107
+ // If there is not a Stripe product associated with the Ghost product, ensure one is created before continuing
108
+ if (!ghostProduct.related('stripeProducts').first()) {
109
+ // Even though we are not updating any information on the product, calling `ProductRepository.update`
110
+ // will ensure that the product gets created in Stripe
111
+ ghostProduct = await this._productRepository.update({
112
+ id: data.product_id,
113
+ name: ghostProduct.get('name'),
114
+ // Providing the pricing details will ensure the relevant prices for the Ghost product are created
115
+ // on the Stripe product
116
+ monthly_price: {
117
+ amount: ghostProduct.get('monthly_price'),
118
+ currency: ghostProduct.get('currency')
119
+ },
120
+ yearly_price: {
121
+ amount: ghostProduct.get('yearly_price'),
122
+ currency: ghostProduct.get('currency')
123
+ }
124
+ }, options);
125
+ }
126
+
127
+ // Find price on Ghost product matching stripe subscription item price details
128
+ const ghostProductPrice = ghostProduct.related('stripePrices').find((price) => {
129
+ return price.get('currency') === stripeSubscriptionItemPriceCurrency &&
130
+ price.get('amount') === stripeSubscriptionItemPriceAmount &&
131
+ price.get('type') === stripeSubscriptionItemPriceType &&
132
+ price.get('interval') === stripeSubscriptionItemPriceInterval;
133
+ });
134
+
135
+ let stripePriceId;
136
+ let isNewStripePrice = false;
137
+
138
+ if (!ghostProductPrice) {
139
+ // If there is not a matching price, create one on the associated Stripe product using the existing
140
+ // subscription item price details and update the stripe subscription to use it
141
+ const stripeProduct = ghostProduct.related('stripeProducts').first();
142
+
143
+ const newStripePrice = await this._stripeAPIService.createPrice({
144
+ product: stripeProduct.get('stripe_product_id'),
145
+ active: true,
146
+ nickname: stripeSubscriptionItemPriceInterval === 'month' ? 'Monthly' : 'Yearly',
147
+ currency: stripeSubscriptionItemPriceCurrency,
148
+ amount: stripeSubscriptionItemPriceAmount,
149
+ type: stripeSubscriptionItemPriceType,
150
+ interval: stripeSubscriptionItemPriceInterval
151
+ });
152
+
153
+ await this._stripeAPIService.updateSubscriptionItemPrice(
154
+ stripeSubscription.id,
155
+ stripeSubscriptionItem.id,
156
+ newStripePrice.id,
157
+ {prorationBehavior: 'none'}
158
+ );
159
+
160
+ stripePriceId = newStripePrice.id;
161
+ isNewStripePrice = true;
162
+ } else {
163
+ // If there is a matching price, and the subscription is not already using it,
164
+ // update the subscription to use it
165
+ stripePriceId = ghostProductPrice.get('stripe_price_id');
166
+
167
+ if (stripeSubscriptionItem.price.id !== stripePriceId) {
168
+ await this._stripeAPIService.updateSubscriptionItemPrice(
169
+ stripeSubscription.id,
170
+ stripeSubscriptionItem.id,
171
+ stripePriceId,
172
+ {prorationBehavior: 'none'}
173
+ );
174
+ }
175
+ }
176
+
177
+ // If there is a matching price, and the subscription is already using it, nothing else needs to be done
178
+
179
+ return {
180
+ stripePriceId,
181
+ isNewStripePrice
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Archive a price in Stripe
187
+ *
188
+ * @param {Number} stripePriceId
189
+ * @returns {Promise<void>}
190
+ */
191
+ async archivePrice(stripePriceId) {
192
+ await this._stripeAPIService.updatePrice(stripePriceId, {active: false});
193
+ }
194
+ };
@@ -0,0 +1,182 @@
1
+ function formatNumber(number) {
2
+ return number.toLocaleString();
3
+ }
4
+
5
+ const iff = (cond, yes, no) => (cond ? yes : no);
6
+ module.exports = ({result, siteUrl, membersUrl, emailRecipient}) => `
7
+ <!doctype html>
8
+ <html>
9
+ <head>
10
+ <meta name="viewport" content="width=device-width">
11
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
12
+ <title>Your member import is complete</title>
13
+ <style>
14
+ /* -------------------------------------
15
+ RESPONSIVE AND MOBILE FRIENDLY STYLES
16
+ ------------------------------------- */
17
+ @media only screen and (max-width: 620px) {
18
+ table[class=body] h1 {
19
+ font-size: 28px !important;
20
+ margin-bottom: 10px !important;
21
+ }
22
+ table[class=body] p,
23
+ table[class=body] ul,
24
+ table[class=body] ol,
25
+ table[class=body] td,
26
+ table[class=body] span,
27
+ table[class=body] a {
28
+ font-size: 16px !important;
29
+ }
30
+ table[class=body] .title {
31
+ font-size: 22px !important;
32
+ }
33
+ table[class=body] .wrapper,
34
+ table[class=body] .article {
35
+ padding: 10px !important;
36
+ }
37
+ table[class=body] .content {
38
+ padding: 0 !important;
39
+ }
40
+ table[class=body] .container {
41
+ padding: 0 !important;
42
+ width: 100% !important;
43
+ }
44
+ table[class=body] .main {
45
+ border-left-width: 0 !important;
46
+ border-radius: 0 !important;
47
+ border-right-width: 0 !important;
48
+ }
49
+ table[class=body] .btn table {
50
+ width: 100% !important;
51
+ }
52
+ table[class=body] .btn a {
53
+ width: 100% !important;
54
+ }
55
+ table[class=body] .img-responsive {
56
+ height: auto !important;
57
+ max-width: 100% !important;
58
+ width: auto !important;
59
+ }
60
+ table[class=body] p[class=small],
61
+ table[class=body] a[class=small] {
62
+ font-size: 12x !important;
63
+ }
64
+ }
65
+ /* -------------------------------------
66
+ PRESERVE THESE STYLES IN THE HEAD
67
+ ------------------------------------- */
68
+ @media all {
69
+ .ExternalClass {
70
+ width: 100%;
71
+ }
72
+ .ExternalClass,
73
+ .ExternalClass p,
74
+ .ExternalClass span,
75
+ .ExternalClass font,
76
+ .ExternalClass td,
77
+ .ExternalClass div {
78
+ line-height: 100%;
79
+ }
80
+ .recipient-link a {
81
+ color: inherit !important;
82
+ font-family: inherit !important;
83
+ font-size: inherit !important;
84
+ font-weight: inherit !important;
85
+ line-height: inherit !important;
86
+ text-decoration: none !important;
87
+ }
88
+ #MessageViewBody a {
89
+ color: inherit;
90
+ text-decoration: none;
91
+ font-size: inherit;
92
+ font-family: inherit;
93
+ font-weight: inherit;
94
+ line-height: inherit;
95
+ }
96
+ }
97
+ hr {
98
+ border-width: 0;
99
+ height: 0;
100
+ margin-top: 34px;
101
+ margin-bottom: 34px;
102
+ border-bottom-width: 1px;
103
+ border-bottom-color: #EEF5F8;
104
+ }
105
+ a {
106
+ color: #3A464C;
107
+ }
108
+ </style>
109
+ </head>
110
+ <body class="" style="background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.5em; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
111
+ <table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
112
+ <tr>
113
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">&nbsp;</td>
114
+ <td class="container" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 540px; padding: 10px; width: 540px;">
115
+ <div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 600px; padding: 30px 20px;">
116
+
117
+ <!-- START CENTERED CONTAINER -->
118
+ <span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Your member import is complete</span>
119
+ <table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 8px;">
120
+
121
+ <!-- START MAIN CONTENT AREA -->
122
+ <tr>
123
+ <td class="wrapper" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; box-sizing: border-box;">
124
+ <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
125
+ <tr>
126
+ <td align="center" style="padding-top: 20px; padding-bottom: 12px;"><img src="https://static.ghost.org/v4.0.0/images/ghost-orb-4.png" width="60" height="60" style="width: 60px; height: 60px;" /></td>
127
+ </tr>
128
+ <tr>
129
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top;">
130
+ <p class="title" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 21px; color: #3A464C; font-weight: normal; line-height: 25px; margin-bottom: 30px; margin-top: 50px; font-weight: 600; color: #15212A;">${iff(result.imported > 0, `Your member import is complete`, `Your member import was unsuccessful`)}</p>
131
+ </td>
132
+ </tr>
133
+ ${iff(result.imported === 0 && result.errors.length === 0, `
134
+ <tr>
135
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-bottom: 16px;">
136
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 0px;">No members were added.</p>
137
+ </td>
138
+ </tr>
139
+ `, ``)}
140
+ ${iff(result.imported > 0, `
141
+ <tr>
142
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-bottom: 16px;">
143
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 0px;">A total of <strong style="font-weight: 600;">${formatNumber(result.imported)}</strong> ${iff(result.imported === 1, 'person', 'people')} were successfully added or updated in your list of members, and now have access to your site.</p>
144
+ </td>
145
+ </tr>`, ``)}
146
+ ${iff(result.errors.length > 0, `
147
+ <tr>
148
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-bottom: 16px;">
149
+ <p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 0px;">
150
+ ${iff(result.imported === 0, `No members were added.`, `<strong style="font-weight: 600;">${formatNumber(result.errors.length)}</strong> ${iff(result.errors.length === 1, `member was`, `members were`)} skipped due to errors.`)} There's a validated CSV file attached to this email with the list of errors so that you can fix them and re-upload the CSV to complete the import.</p>
151
+ </td>
152
+ </tr>`, '')}
153
+ <tr>
154
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-bottom: 12px; padding-top: 16px;">
155
+ <a href="${membersUrl.href}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #15212A; border: solid 1px #15212A; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 16px; font-weight: normal; margin: 0; padding: 9px 22px 10px; border-color: #15212A;">${iff(result.imported > 0, `View members`, `Try again`)}</a>
156
+ </td>
157
+ </tr>
158
+ </table>
159
+ </td>
160
+ </tr>
161
+ <tr>
162
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; vertical-align: top; padding-top: 80px; padding-bottom: 10px;">
163
+ <div class="footer">
164
+ <p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; color: #738A94; font-weight: normal; margin: 0; line-height: 18px; margin-bottom: 0px; font-size: 11px;">This email was sent from <a href="${siteUrl.href}" style="color: #738A94;">${siteUrl.host}</a> to <a href="mailto:${emailRecipient}" style="color: #738A94;">${emailRecipient}</a></p>
165
+ </div>
166
+ </td>
167
+ </tr>
168
+
169
+ <!-- END MAIN CONTENT AREA -->
170
+ </table>
171
+
172
+
173
+ <!-- END CENTERED CONTAINER -->
174
+ </div>
175
+ </td>
176
+ <td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">&nbsp;</td>
177
+ </tr>
178
+ </table>
179
+ </body>
180
+ </html>
181
+ `;
182
+
@@ -0,0 +1,30 @@
1
+ const MembersCSVImporter = require('./MembersCSVImporter');
2
+ const MembersCSVImporterStripeUtils = require('./MembersCSVImporterStripeUtils');
3
+
4
+ /**
5
+ * @typedef {import('./MembersCSVImporter').MembersCSVImporterOptions} MembersCSVImporterOptions
6
+ */
7
+
8
+ /**
9
+ * @typedef {Object} MakeImporterDeps
10
+ * @property {Object} stripeAPIService - Instance of StripeAPIService
11
+ * @property {Object} productRepository - Instance of ProductRepository
12
+ */
13
+
14
+ /**
15
+ * Make an instance of MembersCSVImporter
16
+ *
17
+ * @param {MakeImporterDeps & MembersCSVImporterOptions} deps
18
+ * @returns {MembersCSVImporter}
19
+ */
20
+ module.exports = function makeImporter(deps) {
21
+ const stripeUtils = new MembersCSVImporterStripeUtils({
22
+ stripeAPIService: deps.stripeAPIService,
23
+ productRepository: deps.productRepository
24
+ });
25
+
26
+ return new MembersCSVImporter({
27
+ ...deps,
28
+ stripeUtils
29
+ });
30
+ };