ghost 6.57.0 → 6.57.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 (205) hide show
  1. package/components/tryghost-adapter-base-scheduling-0.2.1.tgz +0 -0
  2. package/components/tryghost-kg-converters-1.2.4.tgz +0 -0
  3. package/components/tryghost-kg-default-cards-10.3.4.tgz +0 -0
  4. package/components/tryghost-kg-default-nodes-2.1.5.tgz +0 -0
  5. package/components/tryghost-kg-default-transforms-1.3.3.tgz +0 -0
  6. package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
  7. package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
  8. package/core/built/admin/assets/{about-C7O-AgRZ.js → about-BNojRaxf.js} +1 -1
  9. package/core/built/admin/assets/{account-migration-Cv_ol1ko.js → account-migration-DMS8JsM-.js} +1 -1
  10. package/core/built/admin/assets/{add-integration-modal-m9Dv6f77.js → add-integration-modal-DU9ghGLS.js} +1 -1
  11. package/core/built/admin/assets/{add-newsletter-modal-CPSVZL2H.js → add-newsletter-modal-DFxuQrYu.js} +1 -1
  12. package/core/built/admin/assets/{add-recommendation-modal-Ci9Z5yat.js → add-recommendation-modal-D-3LOrdd.js} +1 -1
  13. package/core/built/admin/assets/{analytics-DqSCVV8U.js → analytics-DTIMRhWw.js} +1 -1
  14. package/core/built/admin/assets/{announcement-bar-modal-DU3avV7d.js → announcement-bar-modal-DGCcezB6.js} +1 -1
  15. package/core/built/admin/assets/{api-keys-BK0LylyK.js → api-keys-Dx4NeNpK.js} +1 -1
  16. package/core/built/admin/assets/{audience-DTJtXXWY.js → audience-DkknNqhZ.js} +1 -1
  17. package/core/built/admin/assets/{automations-dtQ_MRZ7.js → automations-DvSU7QhW.js} +1 -1
  18. package/core/built/admin/assets/{bluesky-sharing-B8Es3AuV.js → bluesky-sharing-yZ9A_jL8.js} +1 -1
  19. package/core/built/admin/assets/{breadcrumb-tg5zL--z.js → breadcrumb-CjOH7LCU.js} +1 -1
  20. package/core/built/admin/assets/{chart-helpers-CZElTGYL.js → chart-helpers-DGxvWnBT.js} +1 -1
  21. package/core/built/admin/assets/{checkbox-Cyo-x7mC.js → checkbox-BSUcnNq_.js} +1 -1
  22. package/core/built/admin/assets/{chunk.873.b8fc05a8ab877fb819ed.js → chunk.424.4a27628efdade15c2dab.js} +3 -3
  23. package/core/built/admin/assets/chunk.524.dce1c51e0bc30fb2718e.js +25 -0
  24. package/core/built/admin/assets/{chunk.211.6f7328d63c41536451b3.js → chunk.571.40f093ed6d883993714e.js} +5 -5
  25. package/core/built/admin/assets/chunk.582.3a430d5c6a744fd475b6.js +27 -0
  26. package/core/built/admin/assets/{code-editor-view-Dq1rQ4tS.js → code-editor-view-BcyKABrG.js} +1 -1
  27. package/core/built/admin/assets/{color-picker-field-BonBgxhU.js → color-picker-field-8NoSnhNI.js} +1 -1
  28. package/core/built/admin/assets/{color-utils-BsrwXuDz.js → color-utils-Dzeeuj1F.js} +1 -1
  29. package/core/built/admin/assets/{combobox-uX5r_paI.js → combobox-5rdgDrDV.js} +1 -1
  30. package/core/built/admin/assets/{command-DxwBO2pF.js → command-BOYMbZa_.js} +1 -1
  31. package/core/built/admin/assets/{comments-Dn-RO-nU.js → comments-BhNM5CYl.js} +1 -1
  32. package/core/built/admin/assets/{confirmation-modal-B6D6mk6R.js → confirmation-modal-CYD7x3UQ.js} +1 -1
  33. package/core/built/admin/assets/{content-api-modal-BxQ-7T6u.js → content-api-modal-BtW4Z1-1.js} +1 -1
  34. package/core/built/admin/assets/{copy-field-BAxL4Zqn.js → copy-field-CEp_XieW.js} +1 -1
  35. package/core/built/admin/assets/{custom-integration-modal---d_l7lc.js → custom-integration-modal-bk3lNEuQ.js} +1 -1
  36. package/core/built/admin/assets/{date-range-select-PB2n1HZf.js → date-range-select-B3Ij1Rqg.js} +1 -1
  37. package/core/built/admin/assets/{debounce-Dg2fW5Rg.js → debounce-Cx5B_9OI.js} +1 -1
  38. package/core/built/admin/assets/{design-and-theme-modal-DYRT207a.js → design-and-theme-modal-B2xYKS1I.js} +3 -3
  39. package/core/built/admin/assets/{dirty-confirm-dialog-BnDJ39ao.js → dirty-confirm-dialog-YE9L21gY.js} +1 -1
  40. package/core/built/admin/assets/{disabled-sources-indicator-iMHrkyV3.js → disabled-sources-indicator-uIzbWL7K.js} +1 -1
  41. package/core/built/admin/assets/{dist-D5vhMdXf.js → dist-ARN6XTcR.js} +1 -1
  42. package/core/built/admin/assets/{dist-cFzKLG7y.js → dist-Bg8bSvLA.js} +1 -1
  43. package/core/built/admin/assets/{dist-WeOIvSYM.js → dist-CsvXCJSe.js} +1 -1
  44. package/core/built/admin/assets/{domain-BR8FIdjD.js → domain-DzrOAPY8.js} +1 -1
  45. package/core/built/admin/assets/{dropzone-hNxmUvwD.js → dropzone-CzhP33wa.js} +1 -1
  46. package/core/built/admin/assets/{edit-profile-CENrkY0l.js → edit-profile-oufs4R4n.js} +1 -1
  47. package/core/built/admin/assets/{editor-B18D0lm8.js → editor-BBKrunMK.js} +3 -3
  48. package/core/built/admin/assets/{embed-signup-form-modal-BsCYUMvQ.js → embed-signup-form-modal-Ch3s6lqA.js} +1 -1
  49. package/core/built/admin/assets/{explore-BmrnljWw.js → explore-CA-R01Br.js} +1 -1
  50. package/core/built/admin/assets/{feed-Dy4li6i8.js → feed-ClC8_Gld.js} +1 -1
  51. package/core/built/admin/assets/{field-Dh_lwk13.js → field-B3OPenbP.js} +1 -1
  52. package/core/built/admin/assets/{filter-sources-D2zDuJH5.js → filter-sources-CiPLnG9A.js} +1 -1
  53. package/core/built/admin/assets/{filters-3JBQcNtJ.js → filters-Bz0HsRie.js} +1 -1
  54. package/core/built/admin/assets/{filters-CFt_BDe4.js → filters-yFvybRMe.js} +1 -1
  55. package/core/built/admin/assets/{first-promoter-modal-D6Rjxugw.js → first-promoter-modal-BC0UpYCD.js} +1 -1
  56. package/core/built/admin/assets/{gift-link-modal-zMtlJhLQ.js → gift-link-modal-BKRuqo-k.js} +1 -1
  57. package/core/built/admin/assets/{global-data-provider-B949LDXJ.js → global-data-provider-RzW6Bnlf.js} +1 -1
  58. package/core/built/admin/assets/{growth-CFws67_i.js → growth-ClNAldfL.js} +1 -1
  59. package/core/built/admin/assets/{growth-C38Xtp8D.js → growth-Dz8OSG06.js} +1 -1
  60. package/core/built/admin/assets/{history-modal-ILAOkbxH.js → history-modal-DIVuQ5Ct.js} +1 -1
  61. package/core/built/admin/assets/{html-field-9a9Id_2q.js → html-field-CVZ2sMxw.js} +1 -1
  62. package/core/built/admin/assets/{images-CrQKVc9Q.js → images-Ok8FFJ3A.js} +1 -1
  63. package/core/built/admin/assets/{inbox-BrSRzyvz.js → inbox-CMUVI6qb.js} +1 -1
  64. package/core/built/admin/assets/{index-BHy2DZX-.js → index-DUXM0NlS.js} +2 -2
  65. package/core/built/admin/assets/{input-group-6vEPvX9S.js → input-group-2XU-iuh_.js} +1 -1
  66. package/core/built/admin/assets/{invite-user-modal-5xDCEw90.js → invite-user-modal-DrZ7xxrZ.js} +1 -1
  67. package/core/built/admin/assets/{koenig-editor-base-BWLEB9Vf.js → koenig-editor-base-BPj4S760.js} +1 -1
  68. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +9254 -9251
  69. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +124 -124
  70. package/core/built/admin/assets/{koenig-lexical-Cj62r_y9.js → koenig-lexical-Cdjv-m3_.js} +98 -98
  71. package/core/built/admin/assets/{kpi-card-IHDo8Q56.js → kpi-card-J8Mj1sDX.js} +1 -1
  72. package/core/built/admin/assets/{kpi-card-DOUnkZcN.js → kpi-card-awos0BMH.js} +1 -1
  73. package/core/built/admin/assets/{kpi-helpers-CjUdECAK.js → kpi-helpers-SEhvH-8E.js} +1 -1
  74. package/core/built/admin/assets/{kpi-tabs-X3DkOYbI.js → kpi-tabs-R6BjsKhd.js} +1 -1
  75. package/core/built/admin/assets/{label-Dq6QaOuS.js → label-BMC3dzLI.js} +1 -1
  76. package/core/built/admin/assets/{label-picker-8YlhS2U3.js → label-picker-Bnlru5tO.js} +1 -1
  77. package/core/built/admin/assets/layout-yR9rlvEw.js +1 -0
  78. package/core/built/admin/assets/{limit-modal-BRPsit_B.js → limit-modal-COmLevuY.js} +1 -1
  79. package/core/built/admin/assets/{member-custom-fields-CmvsuuV_.js → member-custom-fields-BvFKEpxJ.js} +1 -1
  80. package/core/built/admin/assets/{member-detail-DCvRrkmJ.js → member-detail-CWdPAe3p.js} +1 -1
  81. package/core/built/admin/assets/{members-DxbSKngD.js → members-Bnt0ih2R.js} +1 -1
  82. package/core/built/admin/assets/{moderation-Da5lhhWh.js → moderation-mPjYHgZW.js} +1 -1
  83. package/core/built/admin/assets/{multi-select-combobox-CYdkH7KQ.js → multi-select-combobox-CsoyHAiX.js} +1 -1
  84. package/core/built/admin/assets/{navigation-modal-BIoTwtMy.js → navigation-modal-D_c0badj.js} +1 -1
  85. package/core/built/admin/assets/{newsletter-BptlVoJm.js → newsletter-DR49NyGS.js} +1 -1
  86. package/core/built/admin/assets/{newsletter-detail-modal-BIvxqE0N.js → newsletter-detail-modal-B45pL1pb.js} +1 -1
  87. package/core/built/admin/assets/{newsletter-emails-Bf8IZLsP.js → newsletter-emails-B-OLfrvr.js} +1 -1
  88. package/core/built/admin/assets/{newsletters-BgrIZk0V.js → newsletters-DGs5BaKH.js} +1 -1
  89. package/core/built/admin/assets/{note-DYcLOmDb.js → note-CSVsUKBm.js} +1 -1
  90. package/core/built/admin/assets/{notifications-B2SPFIV9.js → notifications-DV9FRtDw.js} +1 -1
  91. package/core/built/admin/assets/{offers-container-modal-BZnZITMw.js → offers-container-modal-B0hFS8nk.js} +1 -1
  92. package/core/built/admin/assets/{onboarding-route-DgdfxkQh.js → onboarding-route--jL5KLPD.js} +1 -1
  93. package/core/built/admin/assets/{overview-Dt2_mR6U.js → overview-B-ZPNqMo.js} +1 -1
  94. package/core/built/admin/assets/{overview-BYQJXSog.js → overview-D96iwoJi.js} +1 -1
  95. package/core/built/admin/assets/{page-header-CpmxJj5B.js → page-header-DAvDYKO9.js} +1 -1
  96. package/core/built/admin/assets/{pintura-modal-B4MuBuuO.js → pintura-modal-jiUCxX0Q.js} +1 -1
  97. package/core/built/admin/assets/{portal-frame-BF0NBIji.js → portal-frame-D4D4nEO6.js} +1 -1
  98. package/core/built/admin/assets/{portal-modal-DMVbm0oy.js → portal-modal-BLFdYeDH.js} +1 -1
  99. package/core/built/admin/assets/{post-analytics-CXWBOGyP.js → post-analytics-BhAtiFJp.js} +1 -1
  100. package/core/built/admin/assets/{post-analytics-header-ChzjXVPt.js → post-analytics-header-DK27VjJm.js} +1 -1
  101. package/core/built/admin/assets/{post-analytics-provider-Cgw4U_fw.js → post-analytics-provider-DGg0FUqs.js} +1 -1
  102. package/core/built/admin/assets/{post-share-modal-B9Vmk_XM.js → post-share-modal-DtFAC132.js} +1 -1
  103. package/core/built/admin/assets/{preferences-BfzJmnsd.js → preferences-CuWLISHa.js} +1 -1
  104. package/core/built/admin/assets/{preview-modal-Dw_HtD6S.js → preview-modal-DctYx20m.js} +1 -1
  105. package/core/built/admin/assets/{profile-BT5vtv9b.js → profile-BsKYYtuL.js} +1 -1
  106. package/core/built/admin/assets/{radio-group-I7ou7PoJ.js → radio-group-CI5_Kf_d.js} +1 -1
  107. package/core/built/admin/assets/{select-D9oU1IoM.js → select-CkXqNJDJ.js} +1 -1
  108. package/core/built/admin/assets/settings-B4vM-rgV.js +4 -0
  109. package/core/built/admin/assets/{settings-CpvHWix7.js → settings-ZCj-W56o.js} +1 -1
  110. package/core/built/admin/assets/{settings-breadcrumbs-K0USP2nw.js → settings-breadcrumbs-DIq31FwA.js} +1 -1
  111. package/core/built/admin/assets/{share-modal-ClIyPzc5.js → share-modal-BNj14g79.js} +1 -1
  112. package/core/built/admin/assets/{slack-modal-1XXqOfiw.js → slack-modal-C6SpMQ-q.js} +1 -1
  113. package/core/built/admin/assets/{sort-button-CdBsm40v.js → sort-button-B-k54ow2.js} +1 -1
  114. package/core/built/admin/assets/{source-icon-CEYmVLXR.js → source-icon-Cej3sZM6.js} +1 -1
  115. package/core/built/admin/assets/{stats-D8nZCTy5.js → stats-B2KW8E2y.js} +1 -1
  116. package/core/built/admin/assets/{stats-view-CugFVf0g.js → stats-view-CVzU2-p5.js} +1 -1
  117. package/core/built/admin/assets/{step-1-DOYjrORn.js → step-1-Dbn1iIMU.js} +1 -1
  118. package/core/built/admin/assets/{step-2-vA6RsarH.js → step-2-BahnSuN6.js} +1 -1
  119. package/core/built/admin/assets/{step-3-BtIBxMId.js → step-3-CW04e2ZJ.js} +1 -1
  120. package/core/built/admin/assets/{stripe-button-UFP4QRXd.js → stripe-button-BkBZqaGG.js} +1 -1
  121. package/core/built/admin/assets/{stripe-connect-modal-C_L3uM6n.js → stripe-connect-modal-CYDDrSJm.js} +1 -1
  122. package/core/built/admin/assets/{table-TMCgCQK7.js → table-BtLcEi1M.js} +1 -1
  123. package/core/built/admin/assets/{tabs-Dd6oOKbv.js → tabs-BaWRfg8j.js} +1 -1
  124. package/core/built/admin/assets/{tag-detail-D3l-UJPR.js → tag-detail-CmqiC32L.js} +1 -1
  125. package/core/built/admin/assets/{tags-WdKQGWR8.js → tags-BbygUvNB.js} +1 -1
  126. package/core/built/admin/assets/{testimonials-modal-CqWp-xXJ.js → testimonials-modal--bdzppyh.js} +1 -1
  127. package/core/built/admin/assets/{tier-detail-modal-BkcR2u0A.js → tier-detail-modal-BZmAsIy4.js} +1 -1
  128. package/core/built/admin/assets/{toggle-group-Guorw93z.js → toggle-group-CVl-Xigr.js} +1 -1
  129. package/core/built/admin/assets/{topic-filter-CbgoWIHn.js → topic-filter-BoRHRbL1.js} +1 -1
  130. package/core/built/admin/assets/{transistor-modal-DuNnnpal.js → transistor-modal-CBPDqEGy.js} +1 -1
  131. package/core/built/admin/assets/{unsplash-modal-y2bVwTHH.js → unsplash-modal-CixoRAO2.js} +1 -1
  132. package/core/built/admin/assets/{use-analytics-data-B-b4wNSC.js → use-analytics-data-BiWvcf1u.js} +1 -1
  133. package/core/built/admin/assets/{use-check-theme-limit-error-m09J38Mq.js → use-check-theme-limit-error-B14lZukR.js} +1 -1
  134. package/core/built/admin/assets/use-feature-flag-BmJoc5WN.js +1 -0
  135. package/core/built/admin/assets/{use-filter-params-BnbBDc7T.js → use-filter-params-ChoV72Hw.js} +1 -1
  136. package/core/built/admin/assets/{use-growth-stats-DPU5rt3W.js → use-growth-stats-Dom8xVzo.js} +1 -1
  137. package/core/built/admin/assets/{use-limiter-BkN3E4gw.js → use-limiter-Dwg8ivgQ.js} +1 -1
  138. package/core/built/admin/assets/{use-pintura-config-C_z8HTG2.js → use-pintura-config-Bj4QmiVk.js} +1 -1
  139. package/core/built/admin/assets/{use-pintura-editor-DNgbkhGp.js → use-pintura-editor-DvrslmPE.js} +1 -1
  140. package/core/built/admin/assets/{use-post-referrers-C2mXyrp-.js → use-post-referrers-pxIHTXFs.js} +1 -1
  141. package/core/built/admin/assets/{use-reply-chain-data-DaK8q2qQ.js → use-reply-chain-data-CmFe0N6F.js} +1 -1
  142. package/core/built/admin/assets/{use-setting-group-YlYcoNyP.js → use-setting-group-Cmp_sy-d.js} +1 -1
  143. package/core/built/admin/assets/{use-settings-navigation-Bf1g1rEh.js → use-settings-navigation-BmROPrls.js} +1 -1
  144. package/core/built/admin/assets/{use-simple-pagination-D9ckKo5-.js → use-simple-pagination-D5rL3YwO.js} +1 -1
  145. package/core/built/admin/assets/{use-tinybird-query-GSa32hVw.js → use-tinybird-query-DP2dMaq7.js} +1 -1
  146. package/core/built/admin/assets/{use-tinybird-token-CKLOZFsw.js → use-tinybird-token-DrK_skPQ.js} +1 -1
  147. package/core/built/admin/assets/{use-url-input-z-p0Kewm.js → use-url-input-ChrUZBFf.js} +1 -1
  148. package/core/built/admin/assets/{user-detail-modal-BMxOA4Sv.js → user-detail-modal-XODpOqNC.js} +1 -1
  149. package/core/built/admin/assets/{virtual-list-RHeEL05L.js → virtual-list-Dnoafqbk.js} +1 -1
  150. package/core/built/admin/assets/{web-xJtcwMy4.js → web-D71mjB2r.js} +1 -1
  151. package/core/built/admin/assets/{web-Bw2QMO5u.js → web-z71lA1Y3.js} +1 -1
  152. package/core/built/admin/assets/{zapier-modal-BP2f4-vY.js → zapier-modal-Bp507Um7.js} +1 -1
  153. package/core/built/admin/index.html +5 -5
  154. package/core/frontend/web/middleware/admin-toolbar.js +1 -1
  155. package/core/frontend/web/middleware/serve-indexnow-key.js +1 -7
  156. package/core/server/api/endpoints/index.js +2 -2
  157. package/core/server/api/endpoints/themes.js +6 -1
  158. package/core/server/models/base/plugins/overrides.js +7 -5
  159. package/core/server/services/adapter-manager/adapter-paths.js +16 -12
  160. package/core/server/services/adapter-manager/adapter-paths.ts +17 -12
  161. package/core/server/services/audience-feedback/audience-feedback-controller.js +10 -1
  162. package/core/server/services/audience-feedback/feedback-repository.js +4 -1
  163. package/core/server/services/auth/session/session-service.js +18 -3
  164. package/core/server/services/automations/database-automations-repository.js +5 -5
  165. package/core/server/services/automations/database-automations-repository.ts +12 -12
  166. package/core/server/services/automations/database-date.js +52 -3
  167. package/core/server/services/automations/database-date.ts +23 -2
  168. package/core/server/services/email-analytics/email-analytics-service-wrapper.js +1 -1
  169. package/core/server/services/email-analytics/email-analytics-service.js +3 -2
  170. package/core/server/services/email-analytics/index.js +2 -3
  171. package/core/server/services/email-analytics/index.ts +1 -2
  172. package/core/server/services/email-analytics/lib/queries.js +91 -124
  173. package/core/server/services/email-analytics/lib/queries.ts +341 -0
  174. package/core/server/services/indexnow-ping/index.js +0 -2
  175. package/core/server/services/indexnow-ping/index.ts +0 -2
  176. package/core/server/services/indexnow-ping/indexnow-ping-service.js +5 -7
  177. package/core/server/services/indexnow-ping/indexnow-ping-service.ts +7 -13
  178. package/core/server/services/members/members-api/repositories/member-repository.js +51 -0
  179. package/core/server/services/slack-notifications/slack-notifications.js +2 -2
  180. package/core/server/services/slack-ping/index.js +1 -1
  181. package/core/server/services/slack-ping/index.ts +1 -1
  182. package/core/server/services/slack-ping/slack-ping-service.js +1 -0
  183. package/core/server/services/slack-ping/slack-ping-service.ts +1 -0
  184. package/core/server/services/stats/members-stats-service.js +0 -7
  185. package/core/server/services/themes/storage.js +19 -2
  186. package/core/server/services/webhooks/serialize.js +10 -1
  187. package/core/server/web/api/endpoints/admin/routes.js +2 -2
  188. package/core/shared/custom-theme-settings-cache/custom-theme-settings-bread-service.js +4 -0
  189. package/core/shared/custom-theme-settings-cache/custom-theme-settings-service.js +39 -0
  190. package/core/shared/labs.js +1 -1
  191. package/package.json +11 -11
  192. package/pnpm-lock.yaml +41 -41
  193. package/pnpm-workspace.yaml +18 -18
  194. package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
  195. package/components/tryghost-kg-converters-1.2.3.tgz +0 -0
  196. package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
  197. package/components/tryghost-kg-default-nodes-2.1.4.tgz +0 -0
  198. package/core/built/admin/assets/chunk.524.cd5498dd4e195f679266.js +0 -25
  199. package/core/built/admin/assets/chunk.582.6f92ec1f03734bf2ea3f.js +0 -27
  200. package/core/built/admin/assets/layout-TMj4P-vv.js +0 -1
  201. package/core/built/admin/assets/settings-CfZFbyu_.js +0 -4
  202. package/core/built/admin/assets/use-feature-flag-ud0Qz8vt.js +0 -1
  203. package/core/server/services/email-analytics/jobs/update-member-email-analytics/index.js +0 -14
  204. /package/core/built/admin/assets/{chunk.211.6f7328d63c41536451b3.js.LICENSE.txt → chunk.571.40f093ed6d883993714e.js.LICENSE.txt} +0 -0
  205. /package/core/server/api/endpoints/{gift-reminders.js → gifts.js} +0 -0
@@ -0,0 +1,341 @@
1
+ import _ from 'lodash';
2
+ import debugFactory from '@tryghost/debug';
3
+ // @ts-expect-error This module lacks type definitions.
4
+ import db from '../../../data/db';
5
+ import logging from '@tryghost/logging';
6
+ import ObjectID from 'bson-objectid';
7
+
8
+ const debug = debugFactory('services:email-analytics');
9
+
10
+ const MIN_EMAIL_COUNT_FOR_OPEN_RATE = 5;
11
+
12
+ type EmailAnalyticsJobName = string;
13
+ type EmailAnalyticsEvent = 'delivered' | 'opened' | 'failed';
14
+ type CursorSeed = {
15
+ tableName: string;
16
+ eventColumns: Partial<Record<EmailAnalyticsEvent, string>>;
17
+ };
18
+
19
+ /**
20
+ * Creates a job in the jobs table if it does not already exist.
21
+ * @param jobName - The name of the job to create.
22
+ */
23
+ async function createJobIfNotExists(jobName: EmailAnalyticsJobName): Promise<void> {
24
+ await db.knex('jobs').insert({
25
+ id: new ObjectID().toHexString(),
26
+ name: jobName,
27
+ started_at: new Date(),
28
+ created_at: new Date(),
29
+ status: 'started'
30
+ }).onConflict('name').ignore();
31
+ }
32
+
33
+ export const queries = {
34
+ /**
35
+ * Retrieves the timestamp of the last seen event for the specified email analytics events.
36
+ * @param jobName - The name of the job to update.
37
+ * @param events - The email analytics events to consider.
38
+ * @param cursorSeed - Recipient table and timestamp columns to read the initial cursor from. Used when the job has no stored timestamp yet.
39
+ * @returns The timestamp of the last seen event, or null if no events are found.
40
+ */
41
+ async getLastEventTimestamp(
42
+ jobName: EmailAnalyticsJobName,
43
+ events: EmailAnalyticsEvent[],
44
+ cursorSeed: CursorSeed
45
+ ): Promise<Date | null> {
46
+ const startDate = new Date();
47
+
48
+ let timestamps: (Date | string | null)[] = [];
49
+ const lastJobRunTimestamp = await this.getLastJobRunTimestamp(jobName);
50
+
51
+ if (lastJobRunTimestamp) {
52
+ debug(`Using job data for ${jobName}`);
53
+ timestamps = [lastJobRunTimestamp];
54
+ } else {
55
+ debug(`Job data not found for ${jobName}, using ${cursorSeed.tableName} data`);
56
+ logging.info(`Job data not found for ${jobName}, using ${cursorSeed.tableName} data`);
57
+
58
+ for (const event of events) {
59
+ const columnName = cursorSeed.eventColumns[event];
60
+ if (!columnName) {
61
+ continue;
62
+ }
63
+ const row = await db.knex(cursorSeed.tableName)
64
+ .select(db.knex.raw('MAX(??) as maxTimestamp', [columnName]))
65
+ .first();
66
+ timestamps.push(row.maxTimestamp);
67
+ }
68
+
69
+ await createJobIfNotExists(jobName);
70
+ }
71
+
72
+ // Convert string dates to Date objects for SQLite compatibility
73
+ const normalizedTimestamps = timestamps.map(date => {
74
+ if (!date) {
75
+ return null;
76
+ }
77
+ return date instanceof Date ? date : new Date(date);
78
+ });
79
+
80
+ const lastSeenEventTimestamp = _.max(normalizedTimestamps) ?? null;
81
+ debug(`getLastEventTimestamp: finished in ${Date.now() - startDate.getTime()}ms`);
82
+
83
+ return lastSeenEventTimestamp;
84
+ },
85
+
86
+ /**
87
+ * Retrieves the job data for the specified job name.
88
+ * @param jobName - The name of the job to retrieve data for.
89
+ * @returns The job data, or null if no job data is found.
90
+ */
91
+ async getJobData(jobName: EmailAnalyticsJobName) {
92
+ return await db.knex('jobs').select('finished_at', 'started_at', 'metadata').where('name', jobName).first();
93
+ },
94
+
95
+ /**
96
+ * Retrieves the timestamp of the last job run for the specified job name.
97
+ * @param jobName - The name of the job to retrieve the last run timestamp for.
98
+ * @returns The timestamp of the last job run, or null if no job data is found.
99
+ */
100
+ async getLastJobRunTimestamp(jobName: EmailAnalyticsJobName): Promise<Date | null> {
101
+ const jobData = await this.getJobData(jobName);
102
+ return jobData ? jobData.finished_at || jobData.started_at : null;
103
+ },
104
+
105
+ /**
106
+ * Sets the timestamp of the last seen event for the specified email analytics events.
107
+ * @param jobName - The name of the job to update.
108
+ * @param field - The field to update.
109
+ * @param date - The timestamp of the last seen event.
110
+ * @description
111
+ * Updates the `finished_at` or `started_at` column of the specified job in the `jobs` table with the provided timestamp.
112
+ * This is used to keep track of the last time the job was run to avoid expensive queries following reboot.
113
+ */
114
+ async setJobTimestamp(jobName: EmailAnalyticsJobName, field: 'finished' | 'started', date: Date): Promise<void> {
115
+ // Convert string dates to Date objects for SQLite compatibility
116
+ try {
117
+ debug(`Setting ${field} timestamp for job ${jobName} to ${date}`);
118
+ const updateField = field === 'finished' ? 'finished_at' : 'started_at';
119
+ const status = field === 'finished' ? 'finished' : 'started';
120
+ const result = await db.knex('jobs').update({[updateField]: date, updated_at: new Date(), status: status}).where('name', jobName);
121
+ if (result === 0) {
122
+ await db.knex('jobs').insert({
123
+ id: new ObjectID().toHexString(),
124
+ name: jobName,
125
+ [updateField]: date.toISOString(), // force to iso string for sqlite
126
+ updated_at: date.toISOString(), // force to iso string for sqlite
127
+ status: status
128
+ });
129
+ }
130
+ } catch (err: unknown) {
131
+ const message = err instanceof Error ? err.message : String(err);
132
+ debug(`Error setting ${field} timestamp for job ${jobName}: ${message}`);
133
+ }
134
+ },
135
+
136
+ /**
137
+ * Retrieves and parses the metadata JSON for the specified job.
138
+ * @param jobName - The name of the job.
139
+ * @returns The parsed metadata object, or null.
140
+ */
141
+ async getJobMetadata(jobName: EmailAnalyticsJobName): Promise<object | null> {
142
+ try {
143
+ const row = await db.knex('jobs').select('metadata').where('name', jobName).first();
144
+ if (row && row.metadata) {
145
+ return JSON.parse(row.metadata);
146
+ }
147
+ } catch (err: unknown) {
148
+ const message = err instanceof Error ? err.message : String(err);
149
+ logging.error(`Error reading metadata for job ${jobName}: ${message}`);
150
+ }
151
+ return null;
152
+ },
153
+
154
+ /**
155
+ * Writes metadata JSON for the specified job.
156
+ * @param jobName - The name of the job.
157
+ * @param metadata - The metadata to store, or null to clear.
158
+ */
159
+ async setJobMetadata(jobName: EmailAnalyticsJobName, metadata: object | null): Promise<void> {
160
+ try {
161
+ const value = metadata ? JSON.stringify(metadata) : null;
162
+ await db.knex.transaction(async (trx: typeof db.knex) => {
163
+ const result = await trx('jobs').update({metadata: value, updated_at: new Date()}).where('name', jobName);
164
+ if (result === 0 && metadata) {
165
+ await trx('jobs').insert({
166
+ id: new ObjectID().toHexString(),
167
+ name: jobName,
168
+ metadata: value,
169
+ created_at: new Date(),
170
+ status: 'queued'
171
+ });
172
+ }
173
+ });
174
+ } catch (err: unknown) {
175
+ const message = err instanceof Error ? err.message : String(err);
176
+ logging.error(`Error setting metadata for job ${jobName}: ${message}`);
177
+ }
178
+ },
179
+
180
+ /**
181
+ * Sets the status of the specified email analytics job.
182
+ * @param jobName - The name of the job to update.
183
+ * @param status - The new status of the job.
184
+ * @description
185
+ * Updates the `status` column of the specified job in the `jobs` table with the provided status.
186
+ * This is used to keep track of the current state of the job.
187
+ */
188
+ async setJobStatus(jobName: EmailAnalyticsJobName, status: 'started' | 'finished' | 'failed'): Promise<void> {
189
+ debug(`Setting status for job ${jobName} to ${status}`);
190
+ try {
191
+ const result = await db.knex('jobs')
192
+ .update({
193
+ status: status,
194
+ updated_at: new Date()
195
+ })
196
+ .where('name', jobName);
197
+
198
+ if (result === 0) {
199
+ await db.knex('jobs').insert({
200
+ id: new ObjectID().toHexString(),
201
+ name: jobName,
202
+ status: status,
203
+ created_at: new Date(),
204
+ updated_at: new Date()
205
+ });
206
+ }
207
+ } catch (err: unknown) {
208
+ const message = err instanceof Error ? err.message : String(err);
209
+ debug(`Error setting status for job ${jobName}: ${message}`);
210
+ throw err;
211
+ }
212
+ },
213
+
214
+ async aggregateEmailStats(emailId: string, updateOpenedCount: boolean): Promise<void> {
215
+ const [deliveredCount] = await db.knex('email_recipients').count('id as count').whereRaw('email_id = ? AND delivered_at IS NOT NULL', [emailId]);
216
+ const [failedCount] = await db.knex('email_recipients').count('id as count').whereRaw('email_id = ? AND failed_at IS NOT NULL', [emailId]);
217
+
218
+ const updateData: Record<string, string | number> = {
219
+ delivered_count: deliveredCount.count,
220
+ failed_count: failedCount.count
221
+ };
222
+
223
+ if (updateOpenedCount) {
224
+ const [openedCount] = await db.knex('email_recipients').count('id as count').whereRaw('email_id = ? AND opened_at IS NOT NULL', [emailId]);
225
+ updateData.opened_count = openedCount.count;
226
+ }
227
+
228
+ await db.knex('emails').update(updateData).where('id', emailId);
229
+ },
230
+
231
+ async aggregateMemberStats(memberId: string): Promise<void> {
232
+ const {trackedEmailCount} = await db.knex('email_recipients')
233
+ .select(db.knex.raw('COUNT(email_recipients.id) as trackedEmailCount'))
234
+ .leftJoin('emails', 'email_recipients.email_id', 'emails.id')
235
+ .where('email_recipients.member_id', memberId)
236
+ .where('emails.track_opens', true)
237
+ .first() || {};
238
+
239
+ const [emailCount] = await db.knex('email_recipients').count('id as count').whereRaw('member_id = ?', [memberId]);
240
+ const [emailOpenedCount] = await db.knex('email_recipients').count('id as count').whereRaw('member_id = ? AND opened_at IS NOT NULL', [memberId]);
241
+
242
+ const updateQuery: Record<string, string | number> = {
243
+ email_count: emailCount.count,
244
+ email_opened_count: emailOpenedCount.count
245
+ };
246
+
247
+ if (trackedEmailCount >= MIN_EMAIL_COUNT_FOR_OPEN_RATE) {
248
+ updateQuery.email_open_rate = Math.round(emailOpenedCount.count / trackedEmailCount * 100);
249
+ }
250
+
251
+ await db.knex('members')
252
+ .update(updateQuery)
253
+ .where('id', memberId);
254
+ },
255
+
256
+ async aggregateMemberStatsBatch(memberIds: string[]): Promise<void> {
257
+ if (!memberIds || memberIds.length === 0) {
258
+ return;
259
+ }
260
+
261
+ // Batch query to get stats for all members at once
262
+ const stats = await db.knex('email_recipients')
263
+ .leftJoin('emails', 'emails.id', 'email_recipients.email_id')
264
+ .select(
265
+ 'email_recipients.member_id',
266
+ db.knex.raw('COUNT(email_recipients.id) as email_count'),
267
+ db.knex.raw('SUM(CASE WHEN email_recipients.opened_at IS NOT NULL THEN 1 ELSE 0 END) as email_opened_count'),
268
+ db.knex.raw('SUM(CASE WHEN emails.track_opens = 1 THEN 1 ELSE 0 END) as tracked_count')
269
+ )
270
+ .whereIn('email_recipients.member_id', memberIds)
271
+ .groupBy('email_recipients.member_id');
272
+
273
+ // Build update data for each member
274
+ const memberStatsMap = new Map<string, {email_count: number; email_opened_count: number; email_open_rate: number | null}>();
275
+ for (const stat of stats) {
276
+ const emailOpenRate = stat.tracked_count >= MIN_EMAIL_COUNT_FOR_OPEN_RATE
277
+ ? Math.round((stat.email_opened_count / stat.tracked_count) * 100)
278
+ : null;
279
+
280
+ memberStatsMap.set(stat.member_id, {
281
+ email_count: stat.email_count,
282
+ email_opened_count: stat.email_opened_count,
283
+ email_open_rate: emailOpenRate
284
+ });
285
+ }
286
+
287
+ // Build CASE statements for batch update
288
+ const emailCountCases: string[] = [];
289
+ const emailOpenedCountCases: string[] = [];
290
+ const emailOpenRateCases: string[] = [];
291
+ const emailCountBindings: (string | number)[] = [];
292
+ const emailOpenedCountBindings: (string | number)[] = [];
293
+ const emailOpenRateBindings: (string | number)[] = [];
294
+
295
+ for (const memberId of memberIds) {
296
+ const memberStats = memberStatsMap.get(memberId) || {
297
+ email_count: 0,
298
+ email_opened_count: 0,
299
+ email_open_rate: null
300
+ };
301
+
302
+ emailCountCases.push(`WHEN ? THEN ?`);
303
+ emailCountBindings.push(memberId, memberStats.email_count);
304
+
305
+ emailOpenedCountCases.push(`WHEN ? THEN ?`);
306
+ emailOpenedCountBindings.push(memberId, memberStats.email_opened_count);
307
+
308
+ if (memberStats.email_open_rate !== null) {
309
+ emailOpenRateCases.push(`WHEN ? THEN ?`);
310
+ emailOpenRateBindings.push(memberId, memberStats.email_open_rate);
311
+ } else {
312
+ emailOpenRateCases.push(`WHEN ? THEN NULL`);
313
+ emailOpenRateBindings.push(memberId);
314
+ }
315
+ }
316
+
317
+ // Combine bindings in the order they appear in the SQL statement:
318
+ // 1. All bindings for email_count CASE statement
319
+ // 2. All bindings for email_opened_count CASE statement
320
+ // 3. All bindings for email_open_rate CASE statement
321
+ // 4. Member IDs for the WHERE IN clause
322
+ const bindings = [
323
+ ...emailCountBindings,
324
+ ...emailOpenedCountBindings,
325
+ ...emailOpenRateBindings,
326
+ ...memberIds
327
+ ];
328
+
329
+ // Execute batched update with CASE statements
330
+ await db.knex.raw(`
331
+ UPDATE members
332
+ SET
333
+ email_count = CASE id ${emailCountCases.join(' ')} END,
334
+ email_opened_count = CASE id ${emailOpenedCountCases.join(' ')} END,
335
+ email_open_rate = CASE id ${emailOpenRateCases.join(' ')} END
336
+ WHERE id IN (${memberIds.map(() => '?').join(',')})
337
+ `, bindings);
338
+ }
339
+ };
340
+
341
+ export type Queries = typeof queries;
@@ -11,7 +11,6 @@ class IndexNowPingServiceWrapper {
11
11
  // Wire up all the dependencies
12
12
  const settingsCache = require('../../../shared/settings-cache');
13
13
  const config = require('../../../shared/config');
14
- const labs = require('../../../shared/labs');
15
14
  const urlService = require('../url');
16
15
  const urlUtils = require('../../../shared/url-utils').default;
17
16
  const request = require('@tryghost/request');
@@ -20,7 +19,6 @@ class IndexNowPingServiceWrapper {
20
19
  this.service = new indexnow_ping_service_1.IndexNowPingService({
21
20
  settingsCache,
22
21
  config,
23
- labs,
24
22
  urlService,
25
23
  urlUtils,
26
24
  request,
@@ -12,7 +12,6 @@ class IndexNowPingServiceWrapper {
12
12
  // Wire up all the dependencies
13
13
  const settingsCache = require('../../../shared/settings-cache');
14
14
  const config = require('../../../shared/config');
15
- const labs = require('../../../shared/labs');
16
15
  const urlService = require('../url');
17
16
  const urlUtils = require('../../../shared/url-utils').default;
18
17
  const request = require('@tryghost/request');
@@ -22,7 +21,6 @@ class IndexNowPingServiceWrapper {
22
21
  this.service = new IndexNowPingService({
23
22
  settingsCache,
24
23
  config,
25
- labs,
26
24
  urlService,
27
25
  urlUtils,
28
26
  request,
@@ -31,17 +31,15 @@ const constants_1 = require("./constants");
31
31
  class IndexNowPingService {
32
32
  settingsCache;
33
33
  config;
34
- labs;
35
34
  urlService;
36
35
  urlUtils;
37
36
  request;
38
37
  logging;
39
38
  events;
40
39
  listener;
41
- constructor({ settingsCache, config, labs, urlService, urlUtils, request, logging, events }) {
40
+ constructor({ settingsCache, config, urlService, urlUtils, request, logging, events }) {
42
41
  this.settingsCache = settingsCache;
43
42
  this.config = config;
44
- this.labs = labs;
45
43
  this.urlService = urlService;
46
44
  this.urlUtils = urlUtils;
47
45
  this.request = request;
@@ -68,6 +66,10 @@ class IndexNowPingService {
68
66
  * Ping IndexNow with a URL.
69
67
  */
70
68
  async ping(post) {
69
+ // Skip if in development
70
+ if (this.config.get('env') === 'development') {
71
+ return;
72
+ }
71
73
  // Skip pages - only ping for posts
72
74
  if (post.type === 'page') {
73
75
  return;
@@ -80,10 +82,6 @@ class IndexNowPingService {
80
82
  if (this.config.isPrivacyDisabled('useIndexNow')) {
81
83
  return;
82
84
  }
83
- // Skip if IndexNow is not enabled in labs
84
- if (!this.labs.isSet('indexnow')) {
85
- return;
86
- }
87
85
  // Don't ping for the default posts
88
86
  if (post.slug && constants_1.defaultPostSlugs.indexOf(post.slug) > -1) {
89
87
  return;
@@ -36,13 +36,10 @@ type SettingsCache = {
36
36
  };
37
37
 
38
38
  type Config = {
39
+ get(key: string): unknown;
39
40
  isPrivacyDisabled(key: string): boolean;
40
41
  };
41
42
 
42
- type Labs = {
43
- isSet(flag: string): boolean;
44
- };
45
-
46
43
  type UrlService = {
47
44
  getUrlForResource(resource: Record<string, unknown>, options: {absolute: boolean}): string | null;
48
45
  };
@@ -84,7 +81,6 @@ type Post = {
84
81
  export type IndexNowPingServiceDeps = {
85
82
  settingsCache: SettingsCache;
86
83
  config: Config;
87
- labs: Labs;
88
84
  urlService: UrlService;
89
85
  urlUtils: UrlUtils;
90
86
  request: RequestFn;
@@ -95,7 +91,6 @@ export type IndexNowPingServiceDeps = {
95
91
  export class IndexNowPingService {
96
92
  settingsCache: SettingsCache;
97
93
  config: Config;
98
- labs: Labs;
99
94
  urlService: UrlService;
100
95
  urlUtils: UrlUtils;
101
96
  request: RequestFn;
@@ -103,10 +98,9 @@ export class IndexNowPingService {
103
98
  events: ModelEvents;
104
99
  listener: ModelEventListener;
105
100
 
106
- constructor({settingsCache, config, labs, urlService, urlUtils, request, logging, events}: IndexNowPingServiceDeps) {
101
+ constructor({settingsCache, config, urlService, urlUtils, request, logging, events}: IndexNowPingServiceDeps) {
107
102
  this.settingsCache = settingsCache;
108
103
  this.config = config;
109
- this.labs = labs;
110
104
  this.urlService = urlService;
111
105
  this.urlUtils = urlUtils;
112
106
  this.request = request;
@@ -137,6 +131,11 @@ export class IndexNowPingService {
137
131
  * Ping IndexNow with a URL.
138
132
  */
139
133
  async ping(post: Post): Promise<void> {
134
+ // Skip if in development
135
+ if (this.config.get('env') === 'development') {
136
+ return;
137
+ }
138
+
140
139
  // Skip pages - only ping for posts
141
140
  if (post.type === 'page') {
142
141
  return;
@@ -152,11 +151,6 @@ export class IndexNowPingService {
152
151
  return;
153
152
  }
154
153
 
155
- // Skip if IndexNow is not enabled in labs
156
- if (!this.labs.isSet('indexnow')) {
157
- return;
158
- }
159
-
160
154
  // Don't ping for the default posts
161
155
  if (post.slug && defaultPostSlugs.indexOf(post.slug) > -1) {
162
156
  return;
@@ -1256,6 +1256,11 @@ module.exports = class MemberRepository {
1256
1256
  };
1257
1257
  let eventData = {};
1258
1258
 
1259
+ // A cancellation (or reactivation) changes no `members` column, so the member
1260
+ // model event would be suppressed by `wasChanged()`. Remember the pre-update
1261
+ // subscription so the event can be marked and carry its prior state.
1262
+ let subscriptionBeforeCancelFlagChange = null;
1263
+
1259
1264
  const stripeCustomerSubscriptionModelShouldBeDeleted = stripeSubscriptionData.metadata && !!stripeSubscriptionData.metadata.ghost_migrated_to && stripeSubscriptionData.status === 'canceled';
1260
1265
  if (stripeCustomerSubscriptionModelShouldBeDeleted) {
1261
1266
  logging.warn(`Subscription ${subscriptionData.subscription_id} is marked for deletion, skipping linking.`);
@@ -1300,6 +1305,10 @@ module.exports = class MemberRepository {
1300
1305
  this.dispatchEvent(offerRedemptionEvent, options);
1301
1306
  }
1302
1307
 
1308
+ if (stripeCustomerSubscriptionModel.get('cancel_at_period_end') !== updatedStripeCustomerSubscriptionModel.get('cancel_at_period_end')) {
1309
+ subscriptionBeforeCancelFlagChange = stripeCustomerSubscriptionModel;
1310
+ }
1311
+
1303
1312
  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')) {
1304
1313
  const originalMrrDelta = stripeCustomerSubscriptionModel.get('mrr');
1305
1314
  const updatedMrrDelta = updatedStripeCustomerSubscriptionModel.get('mrr');
@@ -1550,6 +1559,48 @@ module.exports = class MemberRepository {
1550
1559
  updatedMember = await this._Member.edit({status: status}, {...options, id: data.id});
1551
1560
  }
1552
1561
 
1562
+ // Cancelling (or reactivating) touches no `members` column, so mark the
1563
+ // subscription relation as the change and carry its prior state. This lets the
1564
+ // webhook payload express before/after — see `services/webhooks/serialize.js`.
1565
+ //
1566
+ // No explicit `emitChange` is needed, and adding one would emit twice: inside a
1567
+ // transaction `emitChange` queues onto the `committed` handler and defers its
1568
+ // `wasChanged()` check to commit time (see `models/base/plugins/events.js`), so
1569
+ // the event already queued by `_Member.edit` above is still pending and setting
1570
+ // `_changed` here is what lets it through. The e2e test asserting exactly one
1571
+ // `member.edited` per cancellation guards this.
1572
+ if (subscriptionBeforeCancelFlagChange && updatedMember) {
1573
+ // Price/tier relations are needed for the serialized subscription shape
1574
+ // to match the Admin API member resource (price, tier, plan)
1575
+ await updatedMember.load([
1576
+ 'stripeSubscriptions',
1577
+ 'stripeSubscriptions.stripePrice',
1578
+ 'stripeSubscriptions.stripePrice.stripeProduct',
1579
+ 'stripeSubscriptions.stripePrice.stripeProduct.product'
1580
+ ], options);
1581
+ await subscriptionBeforeCancelFlagChange.load([
1582
+ 'stripePrice',
1583
+ 'stripePrice.stripeProduct',
1584
+ 'stripePrice.stripeProduct.product'
1585
+ ], options);
1586
+ updatedMember._changed = {
1587
+ ...updatedMember._changed,
1588
+ stripeSubscriptions: {
1589
+ cancel_at_period_end: subscriptionBeforeCancelFlagChange.get('cancel_at_period_end')
1590
+ }
1591
+ };
1592
+ // `previous` must be the full collection — a member can have multiple
1593
+ // subscriptions — with only the changed one swapped for its prior state
1594
+ updatedMember._previousRelations = {
1595
+ ...updatedMember._previousRelations,
1596
+ stripeSubscriptions: {
1597
+ models: updatedMember.related('stripeSubscriptions').models.map((subscription) => {
1598
+ return subscription.id === subscriptionBeforeCancelFlagChange.id ? subscriptionBeforeCancelFlagChange : subscription;
1599
+ })
1600
+ }
1601
+ };
1602
+ }
1603
+
1553
1604
  const newMemberProductIds = memberProducts.map(product => product.id);
1554
1605
  const oldMemberProductIds = oldMemberProducts.map(product => product.id);
1555
1606
 
@@ -1,6 +1,6 @@
1
- const got = require('got').default;
2
1
  const validator = require('@tryghost/validator');
3
2
  const errors = require('@tryghost/errors');
3
+ const externalRequest = require('../../lib/request-external');
4
4
  const ghostVersion = require('@tryghost/version');
5
5
  const moment = require('moment');
6
6
 
@@ -178,7 +178,7 @@ class SlackNotifications {
178
178
  };
179
179
  }
180
180
 
181
- return await got.post(url, requestOptions);
181
+ return await externalRequest.post(url, requestOptions);
182
182
  }
183
183
 
184
184
  /**
@@ -12,7 +12,7 @@ class SlackPingServiceWrapper {
12
12
  const { blogIcon } = require('../../lib/image');
13
13
  const events = require('../../lib/common/events');
14
14
  const logging = require('@tryghost/logging');
15
- const request = require('@tryghost/request');
15
+ const request = require('../../lib/request-external');
16
16
  const settingsCache = require('../../../shared/settings-cache');
17
17
  const urlService = require('../url');
18
18
  const urlUtils = require('../../../shared/url-utils').default;
@@ -13,7 +13,7 @@ class SlackPingServiceWrapper {
13
13
  const {blogIcon} = require('../../lib/image');
14
14
  const events = require('../../lib/common/events');
15
15
  const logging = require('@tryghost/logging');
16
- const request = require('@tryghost/request');
16
+ const request = require('../../lib/request-external');
17
17
  const settingsCache = require('../../../shared/settings-cache');
18
18
  const urlService = require('../url');
19
19
  const urlUtils = require('../../../shared/url-utils').default;
@@ -156,6 +156,7 @@ class SlackPingService {
156
156
  };
157
157
  }
158
158
  return this.request(slackSettings.url, {
159
+ method: 'POST',
159
160
  body: JSON.stringify(slackData),
160
161
  headers: {
161
162
  'Content-type': 'application/json'
@@ -232,6 +232,7 @@ export class SlackPingService {
232
232
  }
233
233
 
234
234
  return this.request(slackSettings.url, {
235
+ method: 'POST',
235
236
  body: JSON.stringify(slackData),
236
237
  headers: {
237
238
  'Content-type': 'application/json'
@@ -111,13 +111,6 @@ class MembersStatsService {
111
111
  const startDateMoment = moment.utc(startDate).startOf('day');
112
112
  const endDateMoment = moment.utc(today).startOf('day');
113
113
 
114
- // Create a map of events by date for fast lookup
115
- const eventsMap = new Map();
116
- rows.forEach((row) => {
117
- const date = moment(row.date).format('YYYY-MM-DD');
118
- eventsMap.set(date, row);
119
- });
120
-
121
114
  // Sort rows chronologically to calculate historical totals
122
115
  rows.sort((a, b) => moment(a.date).valueOf() - moment(b.date).valueOf());
123
116