ghost 5.115.0 → 5.116.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.
Files changed (341) hide show
  1. package/components/{tryghost-api-framework-5.115.0.tgz → tryghost-api-framework-5.116.0.tgz} +0 -0
  2. package/components/tryghost-constants-5.116.0.tgz +0 -0
  3. package/components/tryghost-custom-fonts-5.116.0.tgz +0 -0
  4. package/components/{tryghost-custom-theme-settings-service-5.115.0.tgz → tryghost-custom-theme-settings-service-5.116.0.tgz} +0 -0
  5. package/components/tryghost-domain-events-5.116.0.tgz +0 -0
  6. package/components/{tryghost-donations-5.115.0.tgz → tryghost-donations-5.116.0.tgz} +0 -0
  7. package/components/tryghost-email-addresses-5.116.0.tgz +0 -0
  8. package/components/tryghost-email-service-5.116.0.tgz +0 -0
  9. package/components/tryghost-email-suppression-list-5.116.0.tgz +0 -0
  10. package/components/tryghost-html-to-plaintext-5.116.0.tgz +0 -0
  11. package/components/tryghost-i18n-5.116.0.tgz +0 -0
  12. package/components/tryghost-job-manager-5.116.0.tgz +0 -0
  13. package/components/tryghost-link-replacer-5.116.0.tgz +0 -0
  14. package/components/tryghost-magic-link-5.116.0.tgz +0 -0
  15. package/components/tryghost-member-attribution-5.116.0.tgz +0 -0
  16. package/components/tryghost-member-events-5.116.0.tgz +0 -0
  17. package/components/tryghost-members-api-5.116.0.tgz +0 -0
  18. package/components/tryghost-members-csv-5.116.0.tgz +0 -0
  19. package/components/{tryghost-members-offers-5.115.0.tgz → tryghost-members-offers-5.116.0.tgz} +0 -0
  20. package/components/{tryghost-milestones-5.115.0.tgz → tryghost-milestones-5.116.0.tgz} +0 -0
  21. package/components/{tryghost-mw-error-handler-5.115.0.tgz → tryghost-mw-error-handler-5.116.0.tgz} +0 -0
  22. package/components/tryghost-mw-vhost-5.116.0.tgz +0 -0
  23. package/components/{tryghost-post-events-5.115.0.tgz → tryghost-post-events-5.116.0.tgz} +0 -0
  24. package/components/{tryghost-post-revisions-5.115.0.tgz → tryghost-post-revisions-5.116.0.tgz} +0 -0
  25. package/components/tryghost-posts-service-5.116.0.tgz +0 -0
  26. package/components/{tryghost-prometheus-metrics-5.115.0.tgz → tryghost-prometheus-metrics-5.116.0.tgz} +0 -0
  27. package/components/tryghost-security-5.116.0.tgz +0 -0
  28. package/components/{tryghost-tiers-5.115.0.tgz → tryghost-tiers-5.116.0.tgz} +0 -0
  29. package/components/tryghost-webmentions-5.116.0.tgz +0 -0
  30. package/content/themes/casper/LICENSE +1 -1
  31. package/content/themes/casper/README.md +1 -1
  32. package/content/themes/source/LICENSE +1 -1
  33. package/content/themes/source/README.md +1 -1
  34. package/content/themes/source/assets/built/screen.css +1 -1
  35. package/content/themes/source/assets/built/screen.css.map +1 -1
  36. package/content/themes/source/assets/css/screen.css +11 -6
  37. package/content/themes/source/partials/feature-image.hbs +2 -2
  38. package/core/boot.js +3 -43
  39. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +30494 -29403
  40. package/core/built/admin/assets/admin-x-demo/admin-x-demo.js +1 -1
  41. package/core/built/admin/assets/admin-x-demo/{index-0040480a.mjs → index-a9601514.mjs} +5 -4
  42. package/core/built/admin/assets/admin-x-demo/{modals-fb35c86c.mjs → modals-c1789d04.mjs} +67 -65
  43. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-806ef39c.mjs → CodeEditorView-e9c9deb8.mjs} +2 -2
  44. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  45. package/core/built/admin/assets/admin-x-settings/{index-376f847c.mjs → index-84580c3a.mjs} +2 -2
  46. package/core/built/admin/assets/admin-x-settings/{index-8fa19303.mjs → index-f744cab7.mjs} +3147 -3123
  47. package/core/built/admin/assets/admin-x-settings/{modals-36775d71.mjs → modals-d9ca60c5.mjs} +1198 -1192
  48. package/core/built/admin/assets/chunk.524.8371443ef8f60db429d0.js +35 -0
  49. package/core/built/admin/assets/chunk.582.f90151775f2e53dd21d9.js +37 -0
  50. package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js → chunk.713.e9027c0cc3c56110f5da.js} +125 -98
  51. package/core/built/admin/assets/{ghost-938b3d9c29e3564a53a22f8c8f82d351.js → ghost-03b64c086f3c60cabc85fe7a7e2b640a.js} +272 -251
  52. package/core/built/admin/assets/ghost-ba58e9822f7384461e926c7e23f04a75.css +1 -0
  53. package/core/built/admin/assets/ghost-dark-f1f29683b14ffa11615b3bba8b6ab92c.css +1 -0
  54. package/core/built/admin/assets/koenig-lexical/index.css +1 -1
  55. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +20563 -20891
  56. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +139 -139
  57. package/core/built/admin/assets/posts/posts.js +5732 -5667
  58. package/core/built/admin/assets/stats/stats.js +75373 -0
  59. package/core/built/admin/assets/{vendor-68a4aa424a179a90f5bbc2b750def576.js → vendor-72026232b36d97babc6320917c16c321.js} +36 -34
  60. package/core/built/admin/index.html +6 -6
  61. package/core/cli/generate-data.js +1 -1
  62. package/core/frontend/helpers/ghost_head.js +8 -1
  63. package/core/frontend/public/ghost-stats.js +55 -2
  64. package/core/frontend/services/assets-minification/AdminAuthAssets.js +2 -1
  65. package/core/frontend/services/assets-minification/CardAssets.js +1 -1
  66. package/core/frontend/services/assets-minification/CommentCountsAssets.js +1 -1
  67. package/core/frontend/services/assets-minification/MemberAttributionAssets.js +1 -1
  68. package/core/frontend/services/assets-minification/Minifier.js +191 -0
  69. package/core/frontend/services/routing/controllers/previews.js +2 -1
  70. package/core/frontend/src/cards/css/cta.css +1 -1
  71. package/core/server/adapters/cache/Redis.js +1 -1
  72. package/core/server/adapters/lib/redis/AdapterCacheRedis.js +287 -0
  73. package/core/server/adapters/lib/redis/redis-store-factory.js +22 -0
  74. package/core/server/api/endpoints/posts.js +9 -3
  75. package/core/server/api/endpoints/previews.js +35 -1
  76. package/core/server/api/endpoints/slugs.js +6 -2
  77. package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -9
  78. package/core/server/api/endpoints/utils/validators/input/settings.js +1 -1
  79. package/core/server/data/db/connection.js +2 -0
  80. package/core/server/data/db/index.js +1 -0
  81. package/core/server/data/importer/handlers/ImporterContentFileHandler.js +90 -0
  82. package/core/server/data/importer/import-manager.js +3 -3
  83. package/core/server/data/importer/importers/importer-revue.js +128 -0
  84. package/core/server/data/importer/importers/json-to-html.js +107 -0
  85. package/core/server/data/migrations/utils/tables.js +2 -4
  86. package/core/server/data/seeders/DataGenerator.js +288 -0
  87. package/core/server/data/seeders/importers/BenefitsImporter.js +28 -0
  88. package/core/server/data/seeders/importers/CommentsImporter.js +73 -0
  89. package/core/server/data/seeders/importers/EmailBatchesImporter.js +38 -0
  90. package/core/server/data/seeders/importers/EmailRecipientFailuresImporter.js +67 -0
  91. package/core/server/data/seeders/importers/EmailRecipientsImporter.js +212 -0
  92. package/core/server/data/seeders/importers/EmailsImporter.js +99 -0
  93. package/core/server/data/seeders/importers/LabelsImporter.js +41 -0
  94. package/core/server/data/seeders/importers/MembersClickEventsImporter.js +69 -0
  95. package/core/server/data/seeders/importers/MembersCreatedEventsImporter.js +103 -0
  96. package/core/server/data/seeders/importers/MembersFeedbackImporter.js +45 -0
  97. package/core/server/data/seeders/importers/MembersImporter.js +111 -0
  98. package/core/server/data/seeders/importers/MembersLabelsImporter.js +39 -0
  99. package/core/server/data/seeders/importers/MembersLoginEventsImporter.js +69 -0
  100. package/core/server/data/seeders/importers/MembersNewslettersImporter.js +38 -0
  101. package/core/server/data/seeders/importers/MembersPaidSubscriptionEventsImporter.js +99 -0
  102. package/core/server/data/seeders/importers/MembersProductsImporter.js +42 -0
  103. package/core/server/data/seeders/importers/MembersStatusEventsImporter.js +58 -0
  104. package/core/server/data/seeders/importers/MembersStripeCustomersImporter.js +60 -0
  105. package/core/server/data/seeders/importers/MembersStripeCustomersSubscriptionsImporter.js +259 -0
  106. package/core/server/data/seeders/importers/MembersSubscribeEventsImporter.js +69 -0
  107. package/core/server/data/seeders/importers/MembersSubscriptionCreatedEventsImporter.js +95 -0
  108. package/core/server/data/seeders/importers/NewslettersImporter.js +40 -0
  109. package/core/server/data/seeders/importers/OffersImporter.js +70 -0
  110. package/core/server/data/seeders/importers/PostsAuthorsImporter.js +32 -0
  111. package/core/server/data/seeders/importers/PostsImporter.js +102 -0
  112. package/core/server/data/seeders/importers/PostsProductsImporter.js +35 -0
  113. package/core/server/data/seeders/importers/PostsTagsImporter.js +46 -0
  114. package/core/server/data/seeders/importers/ProductsBenefitsImporter.js +54 -0
  115. package/core/server/data/seeders/importers/ProductsImporter.js +90 -0
  116. package/core/server/data/seeders/importers/RecommendationClickEventsImporter.js +32 -0
  117. package/core/server/data/seeders/importers/RecommendationSubscribeEventsImporter.js +32 -0
  118. package/core/server/data/seeders/importers/RecommendationsImporter.js +34 -0
  119. package/core/server/data/seeders/importers/RedirectsImporter.js +49 -0
  120. package/core/server/data/seeders/importers/RolesUsersImporter.js +42 -0
  121. package/core/server/data/seeders/importers/StripePricesImporter.js +69 -0
  122. package/core/server/data/seeders/importers/StripeProductsImporter.js +34 -0
  123. package/core/server/data/seeders/importers/TableImporter.js +187 -0
  124. package/core/server/data/seeders/importers/TagsImporter.js +41 -0
  125. package/core/server/data/seeders/importers/UsersImporter.js +31 -0
  126. package/core/server/data/seeders/importers/WebMentionsImporter.js +42 -0
  127. package/core/server/data/seeders/importers/index.js +41 -0
  128. package/core/server/data/seeders/utils/JsonImporter.js +39 -0
  129. package/core/server/data/seeders/utils/blog-info.js +3 -0
  130. package/core/server/data/seeders/utils/database-date.js +7 -0
  131. package/core/server/data/seeders/utils/event-generator.js +48 -0
  132. package/core/server/data/seeders/utils/random.js +13 -0
  133. package/core/server/data/seeders/utils/topological-sort.js +33 -0
  134. package/core/server/lib/bootstrap-socket.js +87 -0
  135. package/core/server/lib/package-json/index.js +1 -0
  136. package/core/server/lib/package-json/package-json.js +160 -0
  137. package/core/server/lib/package-json/parse.js +57 -0
  138. package/core/server/models/base/plugins/actions.js +44 -31
  139. package/core/server/models/base/plugins/generate-slug.js +6 -0
  140. package/core/server/notify.js +1 -1
  141. package/core/server/services/activitypub/ActivityPubService.ts +1 -1
  142. package/core/server/services/adapter-manager/AdapterManager.js +161 -0
  143. package/core/server/services/adapter-manager/index.js +1 -1
  144. package/core/server/services/announcement-bar-service/AnnouncementBarSettings.js +54 -0
  145. package/core/server/services/announcement-bar-service/AnnouncementVisibilityValues.js +11 -0
  146. package/core/server/services/announcement-bar-service/index.js +1 -1
  147. package/core/server/services/api-version-compatibility/APIVersionCompatibilityService.js +99 -0
  148. package/core/server/services/api-version-compatibility/VersionNotificationsDataService.js +80 -0
  149. package/core/server/services/api-version-compatibility/extract-api-key.js +57 -0
  150. package/core/server/services/api-version-compatibility/index.js +2 -2
  151. package/core/server/services/api-version-compatibility/mw-api-version-mismatch.js +31 -0
  152. package/core/server/services/audience-feedback/AudienceFeedbackController.js +85 -0
  153. package/core/server/services/audience-feedback/AudienceFeedbackService.js +34 -0
  154. package/core/server/services/audience-feedback/Feedback.js +35 -0
  155. package/core/server/services/audience-feedback/index.js +4 -2
  156. package/core/server/services/auth/session/emails/signin.js +168 -0
  157. package/core/server/services/auth/session/index.js +2 -2
  158. package/core/server/services/auth/session/session-from-token.js +69 -0
  159. package/core/server/services/auth/session/session-service.js +374 -0
  160. package/core/server/services/custom-redirects/index.js +1 -1
  161. package/core/server/services/email-analytics/EmailAnalyticsProviderMailgun.js +62 -0
  162. package/core/server/services/email-analytics/EmailAnalyticsService.js +552 -0
  163. package/core/server/services/email-analytics/EmailAnalyticsServiceWrapper.js +3 -3
  164. package/core/server/services/email-analytics/EventProcessingResult.js +66 -0
  165. package/core/server/services/email-service/EmailServiceWrapper.js +4 -4
  166. package/core/server/services/email-suppression-list/MailgunEmailSuppressionList.js +1 -1
  167. package/core/server/services/email-suppression-list/service.js +1 -1
  168. package/core/server/services/explore-ping/ExplorePingService.js +106 -0
  169. package/core/server/services/explore-ping/index.js +31 -0
  170. package/core/server/services/identity-tokens/IdentityTokenService.js +30 -0
  171. package/core/server/services/identity-tokens/IdentityTokenService.ts +28 -0
  172. package/core/server/services/identity-tokens/IdentityTokenServiceWrapper.js +1 -1
  173. package/core/server/services/invitations/accept.js +5 -2
  174. package/core/server/services/lib/DynamicRedirectManager.js +156 -0
  175. package/core/server/services/lib/EmailContentGenerator.js +54 -0
  176. package/core/server/services/lib/InMemoryRepository.js +62 -0
  177. package/core/server/services/lib/InMemoryRepository.ts +80 -0
  178. package/core/server/services/lib/MailgunClient.js +364 -0
  179. package/core/server/services/link-redirection/LinkRedirect.js +26 -0
  180. package/core/server/services/link-redirection/LinkRedirectRepository.js +7 -7
  181. package/core/server/services/link-redirection/LinkRedirectsService.js +123 -0
  182. package/core/server/services/link-redirection/README.md +151 -0
  183. package/core/server/services/link-redirection/RedirectEvent.js +24 -0
  184. package/core/server/services/link-redirection/index.js +1 -1
  185. package/core/server/services/link-tracking/LinkClickTrackingService.js +1 -1
  186. package/core/server/services/mail/index.js +1 -1
  187. package/core/server/services/mail-events/BookshelfMailEventRepository.js +2 -2
  188. package/core/server/services/mail-events/InMemoryMailEventRepository.js +10 -0
  189. package/core/server/services/mail-events/InMemoryMailEventRepository.ts +8 -0
  190. package/core/server/services/mail-events/MailEvent.js +20 -0
  191. package/core/server/services/mail-events/MailEvent.ts +10 -0
  192. package/core/server/services/mail-events/MailEventRepository.js +2 -0
  193. package/core/server/services/mail-events/MailEventRepository.ts +5 -0
  194. package/core/server/services/mail-events/MailEventService.js +124 -0
  195. package/core/server/services/mail-events/MailEventService.ts +169 -0
  196. package/core/server/services/mail-events/index.js +1 -1
  197. package/core/server/services/mail-events/libraries.d.ts +2 -0
  198. package/core/server/services/members/CaptchaService.js +80 -0
  199. package/core/server/services/members/api.js +1 -1
  200. package/core/server/services/members/importer/MembersCSVImporter.js +464 -0
  201. package/core/server/services/members/importer/MembersCSVImporterStripeUtils.js +194 -0
  202. package/core/server/services/members/importer/email-template.js +182 -0
  203. package/core/server/services/members/importer/index.js +30 -0
  204. package/core/server/services/members/members-ssr.js +333 -0
  205. package/core/server/services/members/service.js +2 -2
  206. package/core/server/services/members-events/LastSeenAtUpdater.js +1 -1
  207. package/core/server/services/offers/service.js +1 -1
  208. package/core/server/services/posts/stats/PostStats.js +13 -0
  209. package/core/server/services/recommendations/RecommendationServiceWrapper.js +8 -8
  210. package/core/server/services/recommendations/service/BookshelfClickEventRepository.js +48 -0
  211. package/core/server/services/recommendations/service/BookshelfClickEventRepository.ts +49 -0
  212. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.js +98 -0
  213. package/core/server/services/recommendations/service/BookshelfRecommendationRepository.ts +117 -0
  214. package/core/server/services/recommendations/service/BookshelfRepository.js +134 -0
  215. package/core/server/services/recommendations/service/BookshelfRepository.ts +196 -0
  216. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.js +48 -0
  217. package/core/server/services/recommendations/service/BookshelfSubscribeEventRepository.ts +49 -0
  218. package/core/server/services/recommendations/service/ClickEvent.js +33 -0
  219. package/core/server/services/recommendations/service/ClickEvent.ts +32 -0
  220. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.js +19 -0
  221. package/core/server/services/recommendations/service/InMemoryRecommendationRepository.ts +20 -0
  222. package/core/server/services/recommendations/service/IncomingRecommendationController.js +34 -0
  223. package/core/server/services/recommendations/service/IncomingRecommendationController.ts +51 -0
  224. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.js +25 -0
  225. package/core/server/services/recommendations/service/IncomingRecommendationEmailRenderer.ts +37 -0
  226. package/core/server/services/recommendations/service/IncomingRecommendationService.js +93 -0
  227. package/core/server/services/recommendations/service/IncomingRecommendationService.ts +160 -0
  228. package/core/server/services/recommendations/service/Recommendation.js +140 -0
  229. package/core/server/services/recommendations/service/Recommendation.ts +201 -0
  230. package/core/server/services/recommendations/service/RecommendationController.js +208 -0
  231. package/core/server/services/recommendations/service/RecommendationController.ts +258 -0
  232. package/core/server/services/recommendations/service/RecommendationMetadataService.js +86 -0
  233. package/core/server/services/recommendations/service/RecommendationMetadataService.ts +128 -0
  234. package/core/server/services/recommendations/service/RecommendationRepository.js +2 -0
  235. package/core/server/services/recommendations/service/RecommendationRepository.ts +13 -0
  236. package/core/server/services/recommendations/service/RecommendationService.js +228 -0
  237. package/core/server/services/recommendations/service/RecommendationService.ts +281 -0
  238. package/core/server/services/recommendations/service/SubscribeEvent.js +33 -0
  239. package/core/server/services/recommendations/service/SubscribeEvent.ts +32 -0
  240. package/core/server/services/recommendations/service/UnsafeData.js +183 -0
  241. package/core/server/services/recommendations/service/UnsafeData.ts +217 -0
  242. package/core/server/services/recommendations/service/WellknownService.js +36 -0
  243. package/core/server/services/recommendations/service/WellknownService.ts +47 -0
  244. package/core/server/services/recommendations/service/index.js +31 -0
  245. package/core/server/services/recommendations/service/index.ts +15 -0
  246. package/core/server/services/recommendations/service/libraries.d.ts +5 -0
  247. package/core/server/services/route-settings/SettingsPathManager.js +47 -0
  248. package/core/server/services/route-settings/index.js +1 -1
  249. package/core/server/services/slack-notifications/SlackNotifications.js +211 -0
  250. package/core/server/services/slack-notifications/SlackNotificationsService.js +90 -0
  251. package/core/server/services/slack-notifications/service.js +4 -6
  252. package/core/server/services/stripe/README.md +63 -0
  253. package/core/server/services/stripe/StripeAPI.js +931 -0
  254. package/core/server/services/stripe/StripeMigrations.js +613 -0
  255. package/core/server/services/stripe/StripeService.js +175 -0
  256. package/core/server/services/stripe/WebhookController.js +100 -0
  257. package/core/server/services/stripe/WebhookManager.js +175 -0
  258. package/core/server/services/stripe/events/StripeLiveDisabledEvent.js +23 -0
  259. package/core/server/services/stripe/events/StripeLiveEnabledEvent.js +23 -0
  260. package/core/server/services/stripe/events/index.js +4 -0
  261. package/core/server/services/stripe/service.js +1 -1
  262. package/core/server/services/stripe/services/webhook/CheckoutSessionEventService.js +255 -0
  263. package/core/server/services/stripe/services/webhook/InvoiceEventService.js +70 -0
  264. package/core/server/services/stripe/services/webhook/SubscriptionEventService.js +54 -0
  265. package/core/server/services/themes/loader.js +1 -1
  266. package/core/server/services/themes/to-json.js +1 -1
  267. package/core/server/web/api/endpoints/admin/app.js +1 -21
  268. package/core/server/web/api/endpoints/admin/routes.js +1 -0
  269. package/core/server/web/api/middleware/version-match.js +41 -0
  270. package/core/server/web/shared/middleware/cache-control.js +51 -0
  271. package/core/server/web/shared/middleware/index.js +1 -1
  272. package/core/server/web/well-known.js +1 -1
  273. package/core/shared/labs.js +5 -3
  274. package/core/shared/settings-cache/CacheManager.js +64 -6
  275. package/package.json +98 -146
  276. package/tsconfig.tsbuildinfo +1 -1
  277. package/yarn.lock +1478 -1634
  278. package/components/tryghost-adapter-cache-redis-5.115.0.tgz +0 -0
  279. package/components/tryghost-adapter-manager-5.115.0.tgz +0 -0
  280. package/components/tryghost-announcement-bar-settings-5.115.0.tgz +0 -0
  281. package/components/tryghost-api-version-compatibility-service-5.115.0.tgz +0 -0
  282. package/components/tryghost-audience-feedback-5.115.0.tgz +0 -0
  283. package/components/tryghost-bookshelf-repository-5.115.0.tgz +0 -0
  284. package/components/tryghost-bootstrap-socket-5.115.0.tgz +0 -0
  285. package/components/tryghost-captcha-service-5.115.0.tgz +0 -0
  286. package/components/tryghost-constants-5.115.0.tgz +0 -0
  287. package/components/tryghost-custom-fonts-5.115.0.tgz +0 -0
  288. package/components/tryghost-data-generator-5.115.0.tgz +0 -0
  289. package/components/tryghost-domain-events-5.115.0.tgz +0 -0
  290. package/components/tryghost-email-addresses-5.115.0.tgz +0 -0
  291. package/components/tryghost-email-analytics-provider-mailgun-5.115.0.tgz +0 -0
  292. package/components/tryghost-email-analytics-service-5.115.0.tgz +0 -0
  293. package/components/tryghost-email-content-generator-5.115.0.tgz +0 -0
  294. package/components/tryghost-email-events-5.115.0.tgz +0 -0
  295. package/components/tryghost-email-service-5.115.0.tgz +0 -0
  296. package/components/tryghost-email-suppression-list-5.115.0.tgz +0 -0
  297. package/components/tryghost-express-dynamic-redirects-5.115.0.tgz +0 -0
  298. package/components/tryghost-extract-api-key-5.115.0.tgz +0 -0
  299. package/components/tryghost-ghost-5.115.0.tgz +0 -0
  300. package/components/tryghost-html-to-plaintext-5.115.0.tgz +0 -0
  301. package/components/tryghost-i18n-5.115.0.tgz +0 -0
  302. package/components/tryghost-identity-token-service-5.115.0.tgz +0 -0
  303. package/components/tryghost-importer-handler-content-files-5.115.0.tgz +0 -0
  304. package/components/tryghost-importer-revue-5.115.0.tgz +0 -0
  305. package/components/tryghost-in-memory-repository-5.115.0.tgz +0 -0
  306. package/components/tryghost-job-manager-5.115.0.tgz +0 -0
  307. package/components/tryghost-link-redirects-5.115.0.tgz +0 -0
  308. package/components/tryghost-link-replacer-5.115.0.tgz +0 -0
  309. package/components/tryghost-magic-link-5.115.0.tgz +0 -0
  310. package/components/tryghost-mail-events-5.115.0.tgz +0 -0
  311. package/components/tryghost-mailgun-client-5.115.0.tgz +0 -0
  312. package/components/tryghost-member-attribution-5.115.0.tgz +0 -0
  313. package/components/tryghost-member-events-5.115.0.tgz +0 -0
  314. package/components/tryghost-members-api-5.115.0.tgz +0 -0
  315. package/components/tryghost-members-csv-5.115.0.tgz +0 -0
  316. package/components/tryghost-members-importer-5.115.0.tgz +0 -0
  317. package/components/tryghost-members-payments-5.115.0.tgz +0 -0
  318. package/components/tryghost-members-ssr-5.115.0.tgz +0 -0
  319. package/components/tryghost-members-stripe-service-5.115.0.tgz +0 -0
  320. package/components/tryghost-minifier-5.115.0.tgz +0 -0
  321. package/components/tryghost-mw-api-version-mismatch-5.115.0.tgz +0 -0
  322. package/components/tryghost-mw-cache-control-5.115.0.tgz +0 -0
  323. package/components/tryghost-mw-session-from-token-5.115.0.tgz +0 -0
  324. package/components/tryghost-mw-update-user-last-seen-5.115.0.tgz +0 -0
  325. package/components/tryghost-mw-version-match-5.115.0.tgz +0 -0
  326. package/components/tryghost-mw-vhost-5.115.0.tgz +0 -0
  327. package/components/tryghost-package-json-5.115.0.tgz +0 -0
  328. package/components/tryghost-posts-service-5.115.0.tgz +0 -0
  329. package/components/tryghost-recommendations-5.115.0.tgz +0 -0
  330. package/components/tryghost-referrers-5.115.0.tgz +0 -0
  331. package/components/tryghost-security-5.115.0.tgz +0 -0
  332. package/components/tryghost-session-service-5.115.0.tgz +0 -0
  333. package/components/tryghost-settings-path-manager-5.115.0.tgz +0 -0
  334. package/components/tryghost-slack-notifications-5.115.0.tgz +0 -0
  335. package/components/tryghost-version-notifications-data-service-5.115.0.tgz +0 -0
  336. package/components/tryghost-webmentions-5.115.0.tgz +0 -0
  337. package/core/built/admin/assets/chunk.524.31419fdf6fb3859ecc1e.js +0 -35
  338. package/core/built/admin/assets/chunk.582.08c816d5e4ab766486a7.js +0 -37
  339. package/core/built/admin/assets/ghost-c2a7c4a1b76550c4219adb2ed4124ce0.css +0 -1
  340. package/core/built/admin/assets/ghost-dark-f91e4a479c6d38d94d5d1b14727871dc.css +0 -1
  341. /package/core/built/admin/assets/{chunk.874.461cb3cf5b6b36915f8c.js.LICENSE.txt → chunk.713.e9027c0cc3c56110f5da.js.LICENSE.txt} +0 -0
@@ -0,0 +1,464 @@
1
+ const moment = require('moment-timezone');
2
+ const path = require('path');
3
+ const fs = require('fs-extra');
4
+ const metrics = require('@tryghost/metrics');
5
+ const membersCSV = require('@tryghost/members-csv');
6
+ const errors = require('@tryghost/errors');
7
+ const tpl = require('@tryghost/tpl');
8
+ const emailTemplate = require('./email-template');
9
+ const logging = require('@tryghost/logging');
10
+
11
+ const messages = {
12
+ filenameCollision: 'Filename already exists, please try again.',
13
+ freeMemberNotAllowedImportTier: 'You cannot import a free member with a specified tier.',
14
+ invalidImportTier: '"{tier}" is not a valid tier.'
15
+ };
16
+
17
+ // The key should correspond to a member model field (unless it's a special purpose field like 'complimentary_plan')
18
+ // the value should represent an allowed field name coming from user input
19
+ const DEFAULT_CSV_HEADER_MAPPING = {
20
+ email: 'email',
21
+ name: 'name',
22
+ note: 'note',
23
+ subscribed_to_emails: 'subscribed',
24
+ created_at: 'created_at',
25
+ complimentary_plan: 'complimentary_plan',
26
+ stripe_customer_id: 'stripe_customer_id',
27
+ labels: 'labels',
28
+ import_tier: 'import_tier'
29
+ };
30
+
31
+ /**
32
+ * @typedef {Object} MembersCSVImporterOptions
33
+ * @property {string} storagePath - The path to store CSV's in before importing
34
+ * @property {Function} getTimezone - function returning currently configured timezone
35
+ * @property {() => Object} getMembersRepository - member model access instance for data access and manipulation
36
+ * @property {() => Promise<import('@tryghost/tiers/lib/Tier')>} getDefaultTier - async function returning default Member Tier
37
+ * @property {(string) => Promise<import('@tryghost/tiers/lib/Tier')>} getTierByName - async function returning Member Tier by name
38
+ * @property {Function} sendEmail - function sending an email
39
+ * @property {(string) => boolean} isSet - Method checking if specific feature is enabled
40
+ * @property {({job, offloaded, name}) => void} addJob - Method registering an async job
41
+ * @property {Object} knex - An instance of the Ghost Database connection
42
+ * @property {Function} urlFor - function generating urls
43
+ * @property {Object} context
44
+ * @property {Object} stripeUtils - An instance of MembersCSVImporterStripeUtils
45
+ */
46
+
47
+ module.exports = class MembersCSVImporter {
48
+ /**
49
+ * @param {MembersCSVImporterOptions} options
50
+ */
51
+ constructor({storagePath, getTimezone, getMembersRepository, getDefaultTier, getTierByName, sendEmail, isSet, addJob, knex, urlFor, context, stripeUtils}) {
52
+ this._storagePath = storagePath;
53
+ this._getTimezone = getTimezone;
54
+ this._getMembersRepository = getMembersRepository;
55
+ this._getDefaultTier = getDefaultTier;
56
+ this._getTierByName = getTierByName;
57
+ this._sendEmail = sendEmail;
58
+ this._isSet = isSet;
59
+ this._addJob = addJob;
60
+ this._knex = knex;
61
+ this._urlFor = urlFor;
62
+ this._context = context;
63
+ this._stripeUtils = stripeUtils;
64
+ this._tierIdCache = new Map();
65
+ }
66
+
67
+ /**
68
+ * Prepares a CSV file for import
69
+ * - Maps headers based on headerMapping, this allows for a non standard CSV
70
+ * to be imported, so long as a mapping exists between it and a standard CSV
71
+ * - Stores the CSV to be imported in the storagePath
72
+ * - Creates a MemberImport Job and associated MemberImportBatch's
73
+ *
74
+ * @param {string} inputFilePath - The path to the CSV to prepare
75
+ * @param {Object.<string, string>} [headerMapping] - An object whose keys are headers in the input CSV and values are the header to replace it with
76
+ * @param {Array<string>} [defaultLabels] - A list of labels to apply to every member
77
+ *
78
+ * @returns {Promise<{filePath: string, batches: number, metadata: Object.<string, any>}>} - A promise resolving to the data including filePath of "prepared" CSV
79
+ */
80
+ async prepare(inputFilePath, headerMapping, defaultLabels) {
81
+ headerMapping = headerMapping || DEFAULT_CSV_HEADER_MAPPING;
82
+ // @NOTE: investigate why is it "1" and do we even need this concept anymore?
83
+ const batchSize = 1;
84
+
85
+ const siteTimezone = this._getTimezone();
86
+ const currentTime = moment().tz(siteTimezone).format('YYYY-MM-DD HH:mm:ss.SSS');
87
+ const outputFileName = `Members Import ${currentTime}.csv`;
88
+ const outputFilePath = path.join(this._storagePath, '/', outputFileName);
89
+
90
+ const pathExists = await fs.pathExists(outputFilePath);
91
+
92
+ if (pathExists) {
93
+ throw new errors.DataImportError({message: tpl(messages.filenameCollision)});
94
+ }
95
+
96
+ // completely rely on explicit user input for header mappings
97
+ const rows = await membersCSV.parse(inputFilePath, headerMapping, defaultLabels);
98
+ const columns = Object.keys(rows[0]);
99
+ const numberOfBatches = Math.ceil(rows.length / batchSize);
100
+ const mappedCSV = membersCSV.unparse(rows, columns);
101
+
102
+ const hasStripeData = !!(rows.find(function rowHasStripeData(row) {
103
+ return !!row.stripe_customer_id;
104
+ }));
105
+
106
+ await fs.writeFile(outputFilePath, mappedCSV);
107
+
108
+ return {
109
+ filePath: outputFilePath,
110
+ batches: numberOfBatches,
111
+ metadata: {
112
+ hasStripeData
113
+ }
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Performs an import of a CSV file
119
+ *
120
+ * @param {string} filePath - the path to a "prepared" CSV file
121
+ */
122
+ async perform(filePath) {
123
+ const performStart = Date.now();
124
+ const rows = await membersCSV.parse(filePath, DEFAULT_CSV_HEADER_MAPPING);
125
+
126
+ const defaultTier = await this._getDefaultTier();
127
+ const membersRepository = await this._getMembersRepository();
128
+
129
+ // Clear tier ID cache before each import in-case tiers have been updated since last import
130
+ this._tierIdCache.clear();
131
+
132
+ // Keep track of any Stripe prices created as a result of an import tier being specified so that they
133
+ // can be archived after the import has completed - This ensures the created Stripe prices cannot be re-used
134
+ // for future subscriptions
135
+ const archivableStripePriceIds = [];
136
+
137
+ const result = await rows.reduce(async (resultPromise, row) => {
138
+ const resultAccumulator = await resultPromise;
139
+
140
+ // Use doNotReject config to reject `executionPromise` on rollback
141
+ // https://github.com/knex/knex/blob/master/UPGRADING.md
142
+ const trx = await this._knex.transaction(undefined, {doNotRejectOnRollback: false});
143
+ const options = {
144
+ transacting: trx,
145
+ context: this._context
146
+ };
147
+
148
+ try {
149
+ // If the member is created in the future, set created_at to now
150
+ // Members created in the future will not appear in admin members list
151
+ // Refs https://github.com/TryGhost/Team/issues/2793
152
+ const createdAt = moment(row.created_at).isAfter(moment()) ? moment().toDate() : row.created_at;
153
+ const memberValues = {
154
+ email: row.email,
155
+ name: row.name,
156
+ note: row.note,
157
+ subscribed: row.subscribed,
158
+ created_at: createdAt,
159
+ labels: row.labels
160
+ };
161
+ const existingMember = await membersRepository.get({email: memberValues.email}, {
162
+ ...options,
163
+ withRelated: ['labels', 'newsletters']
164
+ });
165
+ let member;
166
+ if (existingMember) {
167
+ const existingLabels = existingMember.related('labels') ? existingMember.related('labels').toJSON() : [];
168
+ const existingNewsletters = existingMember.related('newsletters');
169
+
170
+ // Preserve member's existing newsletter subscription preferences
171
+ if (existingNewsletters.length > 0 && memberValues.subscribed) {
172
+ memberValues.newsletters = existingNewsletters.toJSON();
173
+ }
174
+
175
+ // If member does not have any subscriptions, assume they have previously unsubscribed
176
+ // and do not re-subscribe them
177
+ if (!existingNewsletters.length && memberValues.subscribed) {
178
+ memberValues.subscribed = false;
179
+ }
180
+
181
+ // Don't overwrite name or note if they are blank in the file
182
+ if (!row.name) {
183
+ memberValues.name = existingMember.name;
184
+ }
185
+ if (!row.note) {
186
+ memberValues.note = existingMember.note;
187
+ }
188
+
189
+ member = await membersRepository.update({
190
+ ...memberValues,
191
+ labels: existingLabels.concat(memberValues.labels)
192
+ }, {
193
+ ...options,
194
+ id: existingMember.id
195
+ });
196
+ } else {
197
+ member = await membersRepository.create(memberValues, Object.assign({}, options, {
198
+ context: {
199
+ import: true
200
+ }
201
+ }));
202
+ }
203
+
204
+ let importTierId;
205
+ if (row.import_tier) {
206
+ importTierId = await this.#getTierIdByName(row.import_tier);
207
+
208
+ if (!importTierId) {
209
+ throw new errors.DataImportError({
210
+ message: tpl(messages.invalidImportTier, {tier: row.import_tier})
211
+ });
212
+ }
213
+ }
214
+
215
+ if (row.stripe_customer_id && typeof row.stripe_customer_id === 'string') {
216
+ let stripeCustomerId;
217
+
218
+ // If 'auto' is passed, try to find the Stripe customer by email
219
+ if (row.stripe_customer_id.toLowerCase() === 'auto') {
220
+ stripeCustomerId = await membersRepository.getCustomerIdByEmail(row.email);
221
+ } else {
222
+ stripeCustomerId = row.stripe_customer_id;
223
+ }
224
+
225
+ if (stripeCustomerId) {
226
+ if (row.import_tier) {
227
+ const {isNewStripePrice, stripePriceId} = await this._stripeUtils.forceStripeSubscriptionToProduct({
228
+ customer_id: stripeCustomerId,
229
+ product_id: importTierId
230
+ }, options);
231
+
232
+ if (isNewStripePrice) {
233
+ archivableStripePriceIds.push(stripePriceId);
234
+ }
235
+ }
236
+
237
+ await membersRepository.linkStripeCustomer({
238
+ customer_id: stripeCustomerId,
239
+ member_id: member.id
240
+ }, options);
241
+ }
242
+ } else if (row.complimentary_plan) {
243
+ const products = [];
244
+
245
+ if (row.import_tier) {
246
+ products.push({id: importTierId});
247
+ } else {
248
+ products.push({id: defaultTier.id.toString()});
249
+ }
250
+
251
+ await membersRepository.update({products}, {
252
+ ...options,
253
+ id: member.id
254
+ });
255
+ } else if (row.import_tier) {
256
+ throw new errors.DataImportError({message: tpl(messages.freeMemberNotAllowedImportTier)});
257
+ }
258
+
259
+ await trx.commit();
260
+ return {
261
+ ...resultAccumulator,
262
+ imported: resultAccumulator.imported + 1
263
+ };
264
+ } catch (error) {
265
+ // The model layer can sometimes throw arrays of errors
266
+ const errorList = [].concat(error);
267
+ const errorMessage = errorList.map(({message}) => message).join(', ');
268
+ await trx.rollback();
269
+ return {
270
+ ...resultAccumulator,
271
+ errors: [...resultAccumulator.errors, {
272
+ ...row,
273
+ error: errorMessage
274
+ }]
275
+ };
276
+ }
277
+ }, Promise.resolve({
278
+ imported: 0,
279
+ errors: []
280
+ }));
281
+
282
+ await Promise.all(
283
+ archivableStripePriceIds.map(stripePriceId => this._stripeUtils.archivePrice(stripePriceId))
284
+ );
285
+
286
+ metrics.metric({
287
+ imported: result.imported,
288
+ errors: result.errors.length,
289
+ value: Date.now() - performStart
290
+ });
291
+
292
+ return {
293
+ total: result.imported + result.errors.length,
294
+ ...result
295
+ };
296
+ }
297
+
298
+ generateCompletionEmail(result, data) {
299
+ const siteUrl = new URL(this._urlFor('home', null, true));
300
+ const membersUrl = new URL('members', this._urlFor('admin', null, true));
301
+ if (data.importLabel) {
302
+ membersUrl.searchParams.set('label', data.importLabel.slug);
303
+ }
304
+ return emailTemplate({result, siteUrl, membersUrl, ...data});
305
+ }
306
+
307
+ generateErrorCSV(result) {
308
+ const errorsWithFormattedMessages = result.errors.map((row) => {
309
+ const formattedError = row.error
310
+ .replace(
311
+ 'Value in [members.email] cannot be blank.',
312
+ 'Missing email address'
313
+ )
314
+ .replace(
315
+ 'Value in [members.note] exceeds maximum length of 2000 characters.',
316
+ '"Note" exceeds maximum length of 2000 characters'
317
+ )
318
+ .replace(
319
+ 'Value in [members.subscribed] must be one of true, false, 0 or 1.',
320
+ 'Value in "Subscribed to emails" must be "true" or "false"'
321
+ )
322
+ .replace(
323
+ 'Validation (isEmail) failed for email',
324
+ 'Invalid email address'
325
+ )
326
+ .replace(
327
+ /No such customer:[^,]*/,
328
+ 'Could not find Stripe customer'
329
+ );
330
+
331
+ return {
332
+ ...row,
333
+ error: formattedError
334
+ };
335
+ });
336
+ return membersCSV.unparse(errorsWithFormattedMessages);
337
+ }
338
+
339
+ /**
340
+ * Send email with attached CSV containing error rows info
341
+ *
342
+ * @param {Object} config
343
+ * @param {String} config.emailRecipient - email recipient for error file
344
+ * @param {String} config.emailSubject - email subject
345
+ * @param {String} config.emailContent - html content of email
346
+ * @param {String} config.errorCSV - error CSV content
347
+ * @param {Object} config.emailSubject - email subject
348
+ * @param {Object} config.importLabel -
349
+ * @param {String} config.importLabel.name - label name
350
+ */
351
+ async sendErrorEmail({emailRecipient, emailSubject, emailContent, errorCSV, importLabel}) {
352
+ await this._sendEmail({
353
+ to: emailRecipient,
354
+ subject: emailSubject,
355
+ html: emailContent,
356
+ forceTextContent: true,
357
+ attachments: [{
358
+ filename: `${importLabel.name} - Errors.csv`,
359
+ content: errorCSV,
360
+ contentType: 'text/csv',
361
+ contentDisposition: 'attachment'
362
+ }]
363
+ });
364
+ return;
365
+ }
366
+
367
+ /**
368
+ * Processes CSV file and imports member&label records depending on the size of the imported set
369
+ *
370
+ * @param {Object} config
371
+ * @param {String} config.pathToCSV - path where imported csv with members records is stored
372
+ * @param {Object} config.headerMapping - mapping of CSV headers to member record fields
373
+ * @param {Object} [config.globalLabels] - labels to be applied to whole imported members set
374
+ * @param {Object} config.importLabel -
375
+ * @param {String} config.importLabel.name - label name
376
+ * @param {Object} config.user
377
+ * @param {String} config.user.email - calling user email
378
+ * @param {Object} config.LabelModel - instance of Ghosts Label model
379
+ * @param {Boolean} config.forceInline - allows to force performing imports not in a job (used in test environment)
380
+ * @param {{testImportThreshold: () => Promise<void>}} config.verificationTrigger
381
+ */
382
+ async process({pathToCSV, headerMapping, globalLabels, importLabel, user, LabelModel, forceInline, verificationTrigger}) {
383
+ const meta = {};
384
+ const job = await this.prepare(pathToCSV, headerMapping, globalLabels);
385
+
386
+ meta.originalImportSize = job.batches;
387
+
388
+ if ((job.batches <= 500 && !job.metadata.hasStripeData) || forceInline) {
389
+ const result = await this.perform(job.filePath);
390
+ const importLabelModel = result.imported ? await LabelModel.findOne(importLabel) : null;
391
+ await verificationTrigger.testImportThreshold();
392
+
393
+ return {
394
+ meta: Object.assign(meta, {
395
+ stats: {
396
+ imported: result.imported,
397
+ invalid: result.errors
398
+ },
399
+ import_label: importLabelModel
400
+ })
401
+ };
402
+ } else {
403
+ const emailRecipient = user.email;
404
+ this._addJob({
405
+ job: async () => {
406
+ try {
407
+ const result = await this.perform(job.filePath);
408
+ const importLabelModel = result.imported ? await LabelModel.findOne(importLabel) : null;
409
+ const emailContent = this.generateCompletionEmail(result, {
410
+ emailRecipient,
411
+ importLabel: importLabelModel ? importLabelModel.toJSON() : null
412
+ });
413
+ const errorCSV = this.generateErrorCSV(result);
414
+ const emailSubject = result.imported > 0 ? 'Your member import is complete' : 'Your member import was unsuccessful';
415
+ await this.sendErrorEmail({
416
+ emailRecipient,
417
+ emailSubject,
418
+ emailContent,
419
+ errorCSV,
420
+ importLabel
421
+ });
422
+ } catch (e) {
423
+ logging.error('Error in members import job');
424
+ logging.error(e);
425
+ }
426
+
427
+ // Still check verification triggers in case of errors (e.g., email sending failed)
428
+ try {
429
+ await verificationTrigger.testImportThreshold();
430
+ } catch (e) {
431
+ logging.error('Error in members import job when testing import threshold');
432
+ logging.error(e);
433
+ }
434
+ },
435
+ offloaded: false,
436
+ name: 'members-import'
437
+ });
438
+
439
+ return {
440
+ meta
441
+ };
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Retrieve the ID of a tier, querying by its name, and cache the result
447
+ *
448
+ * @param {string} name
449
+ * @returns {Promise<string|null>}
450
+ */
451
+ async #getTierIdByName(name) {
452
+ if (!this._tierIdCache.has(name)) {
453
+ const tier = await this._getTierByName(name);
454
+
455
+ if (!tier) {
456
+ return null;
457
+ }
458
+
459
+ this._tierIdCache.set(name, tier.id.toString());
460
+ }
461
+
462
+ return this._tierIdCache.get(name);
463
+ }
464
+ };
@@ -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
+ };