ghost 6.55.0 → 6.56.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 (280) hide show
  1. package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
  2. package/components/tryghost-kg-default-cards-10.3.3.tgz +0 -0
  3. package/components/tryghost-kg-default-nodes-2.1.4.tgz +0 -0
  4. package/components/tryghost-kg-default-transforms-1.3.3.tgz +0 -0
  5. package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
  6. package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
  7. package/components/tryghost-nql-string-0.0.0.tgz +0 -0
  8. package/core/built/admin/assets/{Combination-CJG7Flp-.js → Combination-BCm9FL4o.js} +1 -1
  9. package/core/built/admin/assets/{PolarAngleAxis-BGjf7sBu.js → PolarAngleAxis-C4qtHy1B.js} +1 -1
  10. package/core/built/admin/assets/{RadialBarChart-zeFf9mBz.js → RadialBarChart-Bh09Stsj.js} +1 -1
  11. package/core/built/admin/assets/{account-migration-sjHLx5dy.js → account-migration-D3O_yVck.js} +1 -1
  12. package/core/built/admin/assets/activitypub/{account-migration-DoYXtnZ8.js → account-migration-D8FbP-Z-.js} +7 -7
  13. package/core/built/admin/assets/activitypub/activitypub.js +6 -6
  14. package/core/built/admin/assets/activitypub/{avatar-B3DEDB1l.js → avatar-11EA1VSs.js} +2 -2
  15. package/core/built/admin/assets/activitypub/{avatar-flipboard-Cqrg0XMx.js → avatar-flipboard-jfbLG-xe.js} +2 -2
  16. package/core/built/admin/assets/activitypub/{bluesky-sharing-Bxeh6MKW.js → bluesky-sharing-f1jTQe2P.js} +16 -16
  17. package/core/built/admin/assets/activitypub/{content-formatters-BpQARh28.js → content-formatters-nmTOPY9M.js} +3 -3
  18. package/core/built/admin/assets/activitypub/{copy-D9UWIAan.js → copy-BCwo9ep3.js} +2 -2
  19. package/core/built/admin/assets/activitypub/{dist-D8sFfdpC.js → dist-BEfX-0aW.js} +12 -12
  20. package/core/built/admin/assets/activitypub/{domain-CWueFW6q.js → domain-AaDlZlrA.js} +9 -9
  21. package/core/built/admin/assets/activitypub/{edit-profile-BGxPU7N5.js → edit-profile-DPi8Tl5M.js} +945 -945
  22. package/core/built/admin/assets/activitypub/{explore-DBK-u0Ch.js → explore-CxTzjILQ.js} +8 -8
  23. package/core/built/admin/assets/activitypub/{feed-Bcir5RQZ.js → feed-DPv5x2MX.js} +33 -33
  24. package/core/built/admin/assets/activitypub/{field-BV-CIT3d.js → field-DbgA8lat.js} +3 -3
  25. package/core/built/admin/assets/activitypub/{hash-BC9Q0N4c.js → hash-BWuZoYkA.js} +2 -2
  26. package/core/built/admin/assets/activitypub/{inbox-DQ4ZnC1m.js → inbox-35VDQ2K2.js} +2 -2
  27. package/core/built/admin/assets/activitypub/{inbox-yC0nfvzl.js → inbox-DSrF_i1O.js} +85 -85
  28. package/core/built/admin/assets/activitypub/{label-DTRS9TDk.js → label-CPjFBbo7.js} +3 -3
  29. package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +1 -0
  30. package/core/built/admin/assets/activitypub/{moderation-Dwl9t0pb.js → moderation-BkDC1XVZ.js} +11 -11
  31. package/core/built/admin/assets/activitypub/{note-B-zKsZ7r.js → note-xEyH4xT-.js} +7 -7
  32. package/core/built/admin/assets/activitypub/{notifications-DjJZD54t.js → notifications-DClaL3ui.js} +9 -9
  33. package/core/built/admin/assets/activitypub/{preferences-CoU8vs6d.js → preferences-DpnOa-4Z.js} +20 -20
  34. package/core/built/admin/assets/activitypub/{profile-BAsaJekx.js → profile-Dvi9mz-6.js} +237 -237
  35. package/core/built/admin/assets/activitypub/{reply-CpdMeAO5.js → reply-Cs9ySd6Z.js} +2 -2
  36. package/core/built/admin/assets/activitypub/{routes-CLH3OVTP.js → routes-DFBeTnfn.js} +1139 -1150
  37. package/core/built/admin/assets/activitypub/{separator-BI1FLsYM.js → separator-C5u_VjzY.js} +3 -3
  38. package/core/built/admin/assets/activitypub/{settings-DA6pNJ-Q.js → settings-CV2fFu6D.js} +13 -13
  39. package/core/built/admin/assets/activitypub/{site-CQD42Pw5.js → site-_TgDd_EM.js} +81 -81
  40. package/core/built/admin/assets/activitypub/{step-1-DIDg4aHb.js → step-1-BfnNL2oF.js} +5 -5
  41. package/core/built/admin/assets/activitypub/{step-2-BfGe8BYt.js → step-2-DLndZkO-.js} +6 -6
  42. package/core/built/admin/assets/activitypub/{step-3-mE9spAqw.js → step-3-A-PZTtRE.js} +10 -10
  43. package/core/built/admin/assets/activitypub/{tabs-Cg0saow1.js → tabs-BUbz7tea.js} +317 -315
  44. package/core/built/admin/assets/activitypub/{topic-filter-DLHBZT93.js → topic-filter-BdX_xYYT.js} +3 -3
  45. package/core/built/admin/assets/activitypub/{use-activity-pub-queries-VzbN99u_.js → use-activity-pub-queries-EwuRemHh.js} +2 -2
  46. package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-CAwXTmqH.js → use-navigate-with-base-path-cy4ACBc4.js} +92 -90
  47. package/core/built/admin/assets/activitypub/{use-reply-chain-data-Br4aOWOh.js → use-reply-chain-data-CyFcGfan.js} +5 -5
  48. package/core/built/admin/assets/activitypub/{x-C8tAnXly.js → x-7zk8mV2p.js} +2 -2
  49. package/core/built/admin/assets/{audience-DjEWNhp3.js → audience-CkBb2LlO.js} +1 -1
  50. package/core/built/admin/assets/{automated-emails-DRy7qY-J.js → automated-emails-6t2bIRWf.js} +1 -1
  51. package/core/built/admin/assets/{automations-Bx3VcFIL.js → automations-cMllU545.js} +1 -1
  52. package/core/built/admin/assets/{bluesky-sharing-ClMd0-Ft.js → bluesky-sharing-DPGpyD-G.js} +1 -1
  53. package/core/built/admin/assets/{box-BWbhXh21.js → box-By_EgGtm.js} +1 -1
  54. package/core/built/admin/assets/{breadcrumb-BD0U2ygg.js → breadcrumb-DIfcF5ow.js} +1 -1
  55. package/core/built/admin/assets/build-CsiH26uS.js +1 -0
  56. package/core/built/admin/assets/{card-DGoDJzrn.js → card-aUHX0hyR.js} +1 -1
  57. package/core/built/admin/assets/{chart-BuRT2Y6p.js → chart-BnXyfH11.js} +1 -1
  58. package/core/built/admin/assets/{chart-helpers-fjQKRVyY.js → chart-helpers-DV0FkdcD.js} +1 -1
  59. package/core/built/admin/assets/{checkbox-CWa28lgO.js → checkbox-BIl077a0.js} +1 -1
  60. package/core/built/admin/assets/{chunk.481.6c7395a49484bb4583aa.js → chunk.332.05991a101d67f732dec3.js} +43 -42
  61. package/core/built/admin/assets/chunk.524.87b52f4fada529df2c4c.js +25 -0
  62. package/core/built/admin/assets/chunk.582.340bcca873e806cbd4bb.js +27 -0
  63. package/core/built/admin/assets/{code-editor-view-CCr0rBWS.js → code-editor-view-C0aWNwdl.js} +1 -1
  64. package/core/built/admin/assets/comments-qwhIjH3c.js +1 -0
  65. package/core/built/admin/assets/{container-BYz2Ed39.js → container-LiZf3XdY.js} +1 -1
  66. package/core/built/admin/assets/{data-list-16rkJYYi.js → data-list-CupF7bjM.js} +1 -1
  67. package/core/built/admin/assets/{date-range-select-BFp58EMk.js → date-range-select-dXc-GsVe.js} +1 -1
  68. package/core/built/admin/assets/{disabled-sources-indicator-CUxMfiPa.js → disabled-sources-indicator-DYAXW-6V.js} +1 -1
  69. package/core/built/admin/assets/dist-BUKmD51u.js +1 -0
  70. package/core/built/admin/assets/{dist-2Tnz2Yfq.js → dist-CNlMj7ML.js} +1 -1
  71. package/core/built/admin/assets/{dist-DGzPJ_bO.js → dist-Cb5RSHSg.js} +1 -1
  72. package/core/built/admin/assets/{dist-P_ohUlTi.js → dist-CqO2lWS0.js} +1 -1
  73. package/core/built/admin/assets/domain-B5ogXUBa.js +1 -0
  74. package/core/built/admin/assets/{dropzone-DlK-K2DQ.js → dropzone-BnFG7dDP.js} +1 -1
  75. package/core/built/admin/assets/{edit-profile-BKT-REJw.js → edit-profile-B07jCZy9.js} +1 -1
  76. package/core/built/admin/assets/editor-DDYSOXGN.js +8 -0
  77. package/core/built/admin/assets/{empty-indicator-oX1u1Pkn.js → empty-indicator-WB1WgT9h.js} +1 -1
  78. package/core/built/admin/assets/{explore-ClUpJZOp.js → explore-Ca1y8HW_.js} +1 -1
  79. package/core/built/admin/assets/{feed-DF92dUMP.js → feed-x4iFMBP4.js} +1 -1
  80. package/core/built/admin/assets/{field-Do7lieBA.js → field-ChCAabZg.js} +1 -1
  81. package/core/built/admin/assets/filter-sources-LdtH1e-T.js +1 -0
  82. package/core/built/admin/assets/filters-CLehPo_D.js +1 -0
  83. package/core/built/admin/assets/filters-CjVKRP8U.js +1 -0
  84. package/core/built/admin/assets/{ghost-c4057474943effc568c5659ccc4909d7.js → ghost-7f01131115186d1af84ead76e79646dd.js} +149 -129
  85. package/core/built/admin/assets/{ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css → ghost-d38c30b866e59f2e06a357b5eb844fe5.css} +1 -1
  86. package/core/built/admin/assets/{ghost-dark-db36d795d21c61426837df40af270966.css → ghost-dark-1bffd5099ba7a749303133efacf8480b.css} +1 -1
  87. package/core/built/admin/assets/{gift-link-modal-f9VIMNDv.js → gift-link-modal-Buqh2vXh.js} +1 -1
  88. package/core/built/admin/assets/{growth-B3I6gNLm.js → growth-DajOBdN0.js} +1 -1
  89. package/core/built/admin/assets/{growth-CVw1My4S.js → growth-Dy2UOSay.js} +2 -2
  90. package/core/built/admin/assets/{inbox-CFBSu4Ed.js → inbox-BUOLDoji.js} +12 -12
  91. package/core/built/admin/assets/index-08n05-rc.js +67 -0
  92. package/core/built/admin/assets/index-DgUzgFvS.css +1 -0
  93. package/core/built/admin/assets/{inline-EMAm3okk.js → inline-Cw-gigDW.js} +1 -1
  94. package/core/built/admin/assets/input-group-uSK3XT7b.js +1 -0
  95. package/core/built/admin/assets/{input-surface-BpAjZXY5.js → input-surface-BdOe4KzO.js} +1 -1
  96. package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +9535 -9527
  97. package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +124 -124
  98. package/core/built/admin/assets/{koenig-lexical-f8vHRYvz.js → koenig-lexical-BFc4yFed.js} +86 -86
  99. package/core/built/admin/assets/{kpi-card-DSap9rAy.js → kpi-card-BN-E6WYX.js} +1 -1
  100. package/core/built/admin/assets/{kpi-card-DU0rn9Cj.js → kpi-card-CCt-qeWD.js} +1 -1
  101. package/core/built/admin/assets/{kpi-helpers-Dz33AV6h.js → kpi-helpers-DlxTbTs4.js} +1 -1
  102. package/core/built/admin/assets/{kpi-tabs-BtTTrKvJ.js → kpi-tabs-BlcUttpn.js} +1 -1
  103. package/core/built/admin/assets/{label-DS8Mi6G5.js → label-AVrxwmsz.js} +1 -1
  104. package/core/built/admin/assets/{label-picker-1DvSuLA1.js → label-picker-DVnpu_IF.js} +1 -1
  105. package/core/built/admin/assets/labels-DAIqKed7.js +1 -0
  106. package/core/built/admin/assets/layout-B0YLh7R9.js +1 -0
  107. package/core/built/admin/assets/link-helpers-D9LsDOkG.js +1 -0
  108. package/core/built/admin/assets/{list-page-B6ANtzkd.js → list-page-CdsnQR9N.js} +1 -1
  109. package/core/built/admin/assets/member-custom-fields-C8NpZYza.js +1 -0
  110. package/core/built/admin/assets/member-detail-CIVAdKXf.js +1 -0
  111. package/core/built/admin/assets/{members-CQpVFJu4.js → members-oLPiSkXD.js} +2 -2
  112. package/core/built/admin/assets/modals-CB7v-k3T.js +31 -0
  113. package/core/built/admin/assets/{moderation-DySbgyUS.js → moderation-CQ3urTFE.js} +1 -1
  114. package/core/built/admin/assets/newsletter-rbeDX9Wk.js +1 -0
  115. package/core/built/admin/assets/newsletters-vt3Ni1Ov.js +1 -0
  116. package/core/built/admin/assets/{note-C8kxrz6w.js → note-Dk877BTv.js} +1 -1
  117. package/core/built/admin/assets/{notifications-CgtSfX7t.js → notifications-CPlLjPxO.js} +1 -1
  118. package/core/built/admin/assets/{onboarding-route-4qtziA9d.js → onboarding-route-DdStOtjy.js} +1 -1
  119. package/core/built/admin/assets/{overview-Bf2sbznq.js → overview--4V-kx9c.js} +1 -1
  120. package/core/built/admin/assets/overview-DSLv30oz.js +1 -0
  121. package/core/built/admin/assets/{page-header-B3Wb9Jqv.js → page-header-pp4dWmOS.js} +1 -1
  122. package/core/built/admin/assets/{post-analytics-We-ko5Cb.js → post-analytics-BMJrI81R.js} +1 -1
  123. package/core/built/admin/assets/post-analytics-header-pU2m9ZuV.js +1 -0
  124. package/core/built/admin/assets/{post-analytics-provider-B6UynTGb.js → post-analytics-provider-Btml5vJI.js} +1 -1
  125. package/core/built/admin/assets/{post-share-modal-DFfO93_0.js → post-share-modal-AcUZkbfL.js} +1 -1
  126. package/core/built/admin/assets/preferences-Ru4CRPHC.js +5 -0
  127. package/core/built/admin/assets/profile-BNb1oj7D.js +1 -0
  128. package/core/built/admin/assets/{router-provider-C1R4AZ2o.js → router-provider-BmEZf6c6.js} +1 -1
  129. package/core/built/admin/assets/select-B0HteHrp.js +1 -0
  130. package/core/built/admin/assets/settings-B1C9Q3wo.js +4 -0
  131. package/core/built/admin/assets/{settings-WXNeWyU1.js → settings-BGgk7oCI.js} +1 -1
  132. package/core/built/admin/assets/{shade-app-B3heOAfz-CGXQwWeO.js → shade-app-B3heOAfz-BhQXxNkT.js} +1 -1
  133. package/core/built/admin/assets/{share-modal-DsgW3wbg.js → share-modal-BeuydaBf.js} +1 -1
  134. package/core/built/admin/assets/social-urls-DA2lXrhW.js +8 -0
  135. package/core/built/admin/assets/{sort-button-Bb_FCl7e.js → sort-button-Df4t2AL8.js} +1 -1
  136. package/core/built/admin/assets/{source-icon-BeaRxUn9.js → source-icon-ajoaKb41.js} +1 -1
  137. package/core/built/admin/assets/{stack-xiN0eQ6Z.js → stack-CIfOAAVf.js} +1 -1
  138. package/core/built/admin/assets/{stats-Bjj_kmJ1.js → stats-C_lfxZAw.js} +1 -1
  139. package/core/built/admin/assets/{stats-view-BxPbW6K1.js → stats-view-Cxot1iD8.js} +1 -1
  140. package/core/built/admin/assets/{step-1-B1e3-1F8.js → step-1-ogAA4lDr.js} +1 -1
  141. package/core/built/admin/assets/{step-2-C5WaeBJo.js → step-2-C5Xini-7.js} +1 -1
  142. package/core/built/admin/assets/{step-3-B46iVPwu.js → step-3-CZv1FV81.js} +2 -2
  143. package/core/built/admin/assets/{switch-z5Y6Uzt6.js → switch-CyQcwMAv.js} +1 -1
  144. package/core/built/admin/assets/{table-CB-4_HBJ.js → table-DFj1wiYv.js} +1 -1
  145. package/core/built/admin/assets/{tabs-CCEgKYzq.js → tabs-D4y1gqUd.js} +1 -1
  146. package/core/built/admin/assets/{tags-Cg2VJGmG.js → tags-Dmft-Y4O.js} +1 -1
  147. package/core/built/admin/assets/{text-DwtjwGkh.js → text-DU9Hv33_.js} +1 -1
  148. package/core/built/admin/assets/textarea-d9qEkMOP.js +1 -0
  149. package/core/built/admin/assets/{toggle-group-b3IlxvhV.js → toggle-group-R2QMFvXW.js} +1 -1
  150. package/core/built/admin/assets/{topic-filter-DAFT87tQ.js → topic-filter-DGDxDalo.js} +1 -1
  151. package/core/built/admin/assets/{use-analytics-data-S0FTd43s.js → use-analytics-data-C76v4LIp.js} +1 -1
  152. package/core/built/admin/assets/use-filter-params-WrdhAISv.js +1 -0
  153. package/core/built/admin/assets/{use-growth-stats-CIaiHOVb.js → use-growth-stats-aNXJOmQr.js} +1 -1
  154. package/core/built/admin/assets/{use-post-referrers-Cq2vBPrA.js → use-post-referrers-CG0SZioj.js} +1 -1
  155. package/core/built/admin/assets/{use-reply-chain-data-DBDRtGu-.js → use-reply-chain-data-RLdPKRmS.js} +1 -1
  156. package/core/built/admin/assets/{use-simple-pagination-CjYNxdwe.js → use-simple-pagination-DgcF69KI.js} +1 -1
  157. package/core/built/admin/assets/{use-tinybird-query-falHAtuK.js → use-tinybird-query-C1R_Kbmy.js} +1 -1
  158. package/core/built/admin/assets/{use-tinybird-token-D7-8ND1y.js → use-tinybird-token-C9fPjVM_.js} +1 -1
  159. package/core/built/admin/assets/{virtual-list-CvnC9zF2.js → virtual-list-BmTUzeoF.js} +1 -1
  160. package/core/built/admin/assets/{web-B_5GZRvH.js → web-BqW0b0_C.js} +1 -1
  161. package/core/built/admin/assets/web-Cy5GKMWM.js +2 -0
  162. package/core/built/admin/index.html +16 -16
  163. package/core/frontend/services/data/fetch-data.js +10 -5
  164. package/core/frontend/services/routing/api-adapter.js +115 -0
  165. package/core/frontend/services/routing/api-adapter.ts +157 -0
  166. package/core/frontend/services/routing/collection-router.js +5 -3
  167. package/core/frontend/services/routing/controllers/index.js +1 -1
  168. package/core/frontend/services/routing/controllers/rss.js +26 -31
  169. package/core/frontend/services/routing/controllers/rss.ts +66 -0
  170. package/core/frontend/services/routing/controllers/static.js +8 -7
  171. package/core/frontend/services/routing/parent-router.js +17 -4
  172. package/core/frontend/services/routing/permalink-adapter.js +26 -0
  173. package/core/frontend/services/routing/permalink-adapter.ts +23 -0
  174. package/core/frontend/services/routing/registry.js +59 -33
  175. package/core/frontend/services/routing/router-manager.js +9 -13
  176. package/core/frontend/services/routing/static-routes-router.js +4 -4
  177. package/core/frontend/services/routing/taxonomy-router.js +6 -2
  178. package/core/server/data/migrations/versions/6.56/2026-08-03-19-27-51-add-automation-run-step-id-to-automated-email-recipients.js +8 -0
  179. package/core/server/data/migrations/versions/6.56/2026-08-04-14-42-39-restrict-notification-add-permission-to-administrators.js +12 -0
  180. package/core/server/data/schema/fixtures/fixtures.json +2 -2
  181. package/core/server/data/schema/schema.js +1 -0
  182. package/core/server/models/post.js +10 -1
  183. package/core/server/services/automations/automations-repository.ts +3 -1
  184. package/core/server/services/automations/database-automations-repository.js +33 -14
  185. package/core/server/services/automations/database-automations-repository.ts +41 -15
  186. package/core/server/services/automations/get-scheduler-idempotency-key.js +13 -0
  187. package/core/server/services/automations/get-scheduler-idempotency-key.ts +8 -0
  188. package/core/server/services/automations/poll.js +3 -1
  189. package/core/server/services/automations/poll.ts +4 -1
  190. package/core/server/services/automations/service.js +5 -1
  191. package/core/server/services/automations/service.ts +5 -1
  192. package/core/server/services/gift-links/CONTEXT.md +9 -0
  193. package/core/server/services/gifts/CONTEXT.md +55 -0
  194. package/core/server/services/gifts/README.md +28 -0
  195. package/core/server/services/gifts/gift-bookshelf-repository.js +65 -45
  196. package/core/server/services/gifts/gift-bookshelf-repository.ts +203 -74
  197. package/core/server/services/gifts/gift-checkout-adapter.js +59 -0
  198. package/core/server/services/gifts/gift-checkout-offer.js +74 -0
  199. package/core/server/services/gifts/gift-checkout-offer.ts +127 -0
  200. package/core/server/services/gifts/gift-codec.js +63 -0
  201. package/core/server/services/gifts/gift-codec.ts +62 -0
  202. package/core/server/services/gifts/gift-controller.js +6 -31
  203. package/core/server/services/gifts/gift-controller.ts +6 -69
  204. package/core/server/services/gifts/gift-email-service.ts +3 -2
  205. package/core/server/services/gifts/gift-schema.js +34 -0
  206. package/core/server/services/gifts/gift-schema.ts +39 -0
  207. package/core/server/services/gifts/gift-service-wrapper.js +13 -11
  208. package/core/server/services/gifts/gift-service.js +265 -52
  209. package/core/server/services/gifts/gift-service.ts +431 -97
  210. package/core/server/services/gifts/gift.ts +16 -39
  211. package/core/server/services/link-tracking/link-click-repository.js +6 -1
  212. package/core/server/services/link-tracking/link-click-tracking-service.js +12 -3
  213. package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +4 -3
  214. package/core/server/services/member-welcome-emails/service.js +7 -3
  215. package/core/server/services/members/api.js +1 -2
  216. package/core/server/services/members/import-export/import/importer.ts +3 -2
  217. package/core/server/services/members/import-export/index.js +10 -2
  218. package/core/server/services/members/import-export/index.ts +14 -4
  219. package/core/server/services/members/members-api/controllers/router-controller.js +38 -36
  220. package/core/server/services/members/members-api/members-api.js +13 -7
  221. package/core/server/services/members/members-api/repositories/event-repository.js +4 -94
  222. package/core/server/services/members/members-api/services/member-bread-service.js +3 -3
  223. package/core/server/services/members/members-api/services/payments-service.js +6 -68
  224. package/core/server/services/members-custom-fields/schema.js +8 -1
  225. package/core/server/services/members-custom-fields/schema.ts +8 -0
  226. package/core/server/services/members-custom-fields/values-service.js +118 -2
  227. package/core/server/services/members-custom-fields/values-service.ts +134 -4
  228. package/core/server/services/oembed/oembed-service.js +44 -3
  229. package/core/server/services/posts/posts-service.js +11 -0
  230. package/core/server/services/route-settings/dynamic-routing-service.js +1 -3
  231. package/core/server/services/route-settings/route-settings-parser.js +25 -25
  232. package/core/server/services/route-settings/route-settings-parser.ts +27 -27
  233. package/core/server/services/route-settings/validation-errors.js +68 -4
  234. package/core/server/services/route-settings/validation-errors.ts +79 -4
  235. package/core/server/services/stats/posts-stats-service.js +27 -23
  236. package/core/server/services/stripe/services/webhook/charge-refunded-event-service.js +1 -1
  237. package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +16 -4
  238. package/core/server/services/stripe/stripe-api.js +10 -0
  239. package/core/server/web/gift-preview/controller.js +10 -25
  240. package/core/shared/labs.js +2 -2
  241. package/package.json +10 -9
  242. package/pnpm-lock.yaml +28 -18
  243. package/pnpm-workspace.yaml +5 -4
  244. package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
  245. package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +0 -1
  246. package/core/built/admin/assets/chunk.524.cffd399783a519e404a9.js +0 -25
  247. package/core/built/admin/assets/chunk.582.eebf0d0b2d202bb746a5.js +0 -27
  248. package/core/built/admin/assets/comments-DyUSMUdj.js +0 -1
  249. package/core/built/admin/assets/dist-CjY-f5Z1.js +0 -1
  250. package/core/built/admin/assets/domain-C0bV2UV8.js +0 -1
  251. package/core/built/admin/assets/editor-Dp865O-4.js +0 -8
  252. package/core/built/admin/assets/filter-sources-BV2rnpfA.js +0 -1
  253. package/core/built/admin/assets/filters-CUXJ2Nm9.js +0 -1
  254. package/core/built/admin/assets/filters-DfbIWc_l.js +0 -1
  255. package/core/built/admin/assets/index-9Cw8z3rA.css +0 -1
  256. package/core/built/admin/assets/index-BGG7NckO.js +0 -67
  257. package/core/built/admin/assets/input-group-BSgG_lVw.js +0 -1
  258. package/core/built/admin/assets/labels-CDSUt4J_.js +0 -1
  259. package/core/built/admin/assets/layout-yQEi2qDM.js +0 -1
  260. package/core/built/admin/assets/link-helpers-7EYqvvHs.js +0 -1
  261. package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +0 -1
  262. package/core/built/admin/assets/member-detail-BAq7gDaa.js +0 -1
  263. package/core/built/admin/assets/modals-ReORvWnu.js +0 -31
  264. package/core/built/admin/assets/newsletter-CJQvtHqN.js +0 -1
  265. package/core/built/admin/assets/newsletters-B3QQXfGT.js +0 -1
  266. package/core/built/admin/assets/nql-BsgFMd93.js +0 -1
  267. package/core/built/admin/assets/overview-CBfePxcr.js +0 -1
  268. package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +0 -1
  269. package/core/built/admin/assets/preferences-DJPfHEGu.js +0 -5
  270. package/core/built/admin/assets/profile-DqBQp-gT.js +0 -1
  271. package/core/built/admin/assets/select-C5_xEt5b.js +0 -1
  272. package/core/built/admin/assets/settings-BwiVIXau.js +0 -4
  273. package/core/built/admin/assets/social-urls-BQPFEk1_.js +0 -8
  274. package/core/built/admin/assets/textarea-Bjccvh_E.js +0 -1
  275. package/core/built/admin/assets/use-filter-params-Gpoq2E1L.js +0 -1
  276. package/core/built/admin/assets/web-jFYWCKDn.js +0 -2
  277. package/core/server/services/gifts/gift-repository.js +0 -2
  278. package/core/server/services/gifts/gift-repository.ts +0 -29
  279. package/core/server/services/route-settings/activation-bridge.js +0 -174
  280. package/core/server/services/route-settings/activation-bridge.ts +0 -250
@@ -1,7 +1,7 @@
1
1
  import {GIFT_EXPIRY_DAYS} from './constants';
2
+ import type {GiftCadence, GiftData, GiftStatus} from './gift-schema';
2
3
 
3
- export type GiftStatus = 'purchased' | 'redeemed' | 'consumed' | 'expired' | 'refunded';
4
- export type GiftCadence = 'month' | 'year';
4
+ export type {GiftCadence, GiftStatus} from './gift-schema';
5
5
 
6
6
  export type RedeemableCheckFailureReason = 'redeemed' | 'consumed' | 'expired' | 'refunded' | 'paid-member';
7
7
  export type RedeemableCheckResult =
@@ -13,43 +13,20 @@ export type ReassignableCheckResult =
13
13
  | {reassignable: true}
14
14
  | {reassignable: false; reason: ReassignableCheckFailureReason};
15
15
 
16
- interface GiftData {
17
- token: string;
18
- buyerEmail: string;
19
- buyerMemberId: string | null;
20
- redeemerMemberId: string | null;
21
- tierId: string;
22
- cadence: GiftCadence;
23
- duration: number;
24
- currency: string;
25
- amount: number;
26
- stripeCheckoutSessionId: string;
27
- stripePaymentIntentId: string;
28
- consumesAt: Date | null;
29
- expiresAt: Date;
30
- status: GiftStatus;
31
- purchasedAt: Date;
32
- redeemedAt: Date | null;
33
- consumedAt: Date | null;
34
- expiredAt: Date | null;
35
- refundedAt: Date | null;
36
- consumesSoonReminderSentAt: Date | null;
37
- }
38
-
39
- export interface GiftFromPurchaseData {
40
- token: string;
41
- buyerEmail: string;
42
- buyerMemberId: string | null;
43
- tierId: string;
44
- cadence: GiftCadence;
45
- duration: number;
46
- currency: string;
47
- amount: number;
48
- stripeCheckoutSessionId: string;
49
- stripePaymentIntentId: string;
50
- }
51
-
52
- export class Gift {
16
+ export type GiftFromPurchaseData = Pick<GiftData,
17
+ | 'token'
18
+ | 'buyerEmail'
19
+ | 'buyerMemberId'
20
+ | 'tierId'
21
+ | 'cadence'
22
+ | 'duration'
23
+ | 'currency'
24
+ | 'amount'
25
+ | 'stripeCheckoutSessionId'
26
+ | 'stripePaymentIntentId'
27
+ >;
28
+
29
+ export class Gift implements GiftData {
53
30
  token: string;
54
31
  buyerEmail: string;
55
32
  buyerMemberId: string | null;
@@ -32,7 +32,12 @@ module.exports = class LinkClickRepository {
32
32
 
33
33
  // Memoize the findOne function
34
34
  this.memoizedFindOne = _.memoize(async (uuid, options) => {
35
- return await this.#Member.findOne({uuid}, options);
35
+ try {
36
+ return await this.#Member.findOne({uuid}, options);
37
+ } catch (error) {
38
+ this.memoizedFindOne.cache.delete(uuid);
39
+ throw error;
40
+ }
36
41
  });
37
42
  }
38
43
 
@@ -4,6 +4,7 @@ const PostLink = require('./post-link');
4
4
  const ObjectID = require('bson-objectid').default;
5
5
  const errors = require('@tryghost/errors');
6
6
  const nql = require('@tryghost/nql');
7
+ const {escapeNqlString} = require('@tryghost/nql-string');
7
8
  const _ = require('lodash');
8
9
  const tpl = require('@tryghost/tpl');
9
10
  const moment = require('moment');
@@ -174,7 +175,11 @@ class LinkClickTrackingService {
174
175
 
175
176
  // manages transformation of current url to relative for comparision
176
177
  const transformedOldUrl = this.#urlUtils.absoluteToTransformReady(redirectUrl.href);
177
- const filterQuery = `post_id:'${postId}'+to:'${transformedOldUrl}'`;
178
+ // the url is re-serialised into NQL here, so it needs escaping again -
179
+ // quotes are legal in a URL and would otherwise break the filter.
180
+ // postId is String()d because nql parses an unquoted `post_id:123` to a
181
+ // number, and escapeNqlString operates on strings
182
+ const filterQuery = `post_id:${escapeNqlString(String(postId))}+to:${escapeNqlString(transformedOldUrl)}`;
178
183
 
179
184
  const updatedFilterOptions = {
180
185
  ...filterOptions,
@@ -245,13 +250,15 @@ class LinkClickTrackingService {
245
250
  * Add first-party tracking to a URL in an automation email
246
251
  * @param {Readonly<URL>} url
247
252
  * @param {string} automationActionRevisionId
253
+ * @param {string} automationRunStepId
248
254
  * @param {string} memberUuid
249
255
  * @return {Promise<URL>}
250
256
  */
251
- async addAutomationTrackingToUrl(url, automationActionRevisionId, memberUuid) {
257
+ async addAutomationTrackingToUrl(url, automationActionRevisionId, automationRunStepId, memberUuid) {
252
258
  const redirect = await this.#linkRedirectService.getOrAddAutomationRedirect(automationActionRevisionId, url);
253
259
  const trackedUrl = new URL(redirect.from.href);
254
260
  trackedUrl.searchParams.set('m', memberUuid);
261
+ trackedUrl.searchParams.set('step', automationRunStepId);
255
262
  return trackedUrl;
256
263
  }
257
264
 
@@ -269,7 +276,8 @@ class LinkClickTrackingService {
269
276
  });
270
277
 
271
278
  const automationActionRevisionId = event.data.link.automationActionRevisionId;
272
- if (!automationActionRevisionId) {
279
+ const automationRunStepId = event.data.url.searchParams.get('step');
280
+ if (!automationActionRevisionId || !automationRunStepId) {
273
281
  await this.#linkClickRepository.save(click);
274
282
  return;
275
283
  }
@@ -282,6 +290,7 @@ class LinkClickTrackingService {
282
290
 
283
291
  await this.#automationsApi.trackEmailClicked({
284
292
  automationActionRevisionId,
293
+ automationRunStepId,
285
294
  memberId,
286
295
  clickedAt: event.timestamp
287
296
  }, {transacting});
@@ -119,9 +119,10 @@ class MemberWelcomeEmailRenderer {
119
119
  * @param {string} [options.unsubscribeUrl] - When set, the footer shows an "Unsubscribe from these emails" link instead of "Manage your preferences"
120
120
  * @param {boolean} [options.trackClicks]
121
121
  * @param {string | null} [options.automationActionRevisionId]
122
+ * @param {string | null} [options.automationRunStepId]
122
123
  * @returns {Promise<{html: string, text: string, subject: string}>}
123
124
  */
124
- async render({lexical, subject, designSettings, member, siteSettings, unsubscribeUrl, trackClicks = false, automationActionRevisionId = null}) {
125
+ async render({lexical, subject, designSettings, member, siteSettings, unsubscribeUrl, trackClicks = false, automationActionRevisionId = null, automationRunStepId = null}) {
125
126
  designSettings = designSettings || {};
126
127
 
127
128
  const design = emailDesign.getEmailDesign({
@@ -171,9 +172,9 @@ class MemberWelcomeEmailRenderer {
171
172
  return originalPath;
172
173
  }
173
174
  const isTrackable = ['http:', 'https:'].includes(url.protocol);
174
- if (trackClicks && automationActionRevisionId && member.uuid && isTrackable) {
175
+ if (trackClicks && automationActionRevisionId && automationRunStepId && member.uuid && isTrackable) {
175
176
  await linkTracking.init();
176
- return await linkTracking.service.addAutomationTrackingToUrl(url, automationActionRevisionId, member.uuid);
177
+ return await linkTracking.service.addAutomationTrackingToUrl(url, automationActionRevisionId, automationRunStepId, member.uuid);
177
178
  }
178
179
  return url;
179
180
  }, {base: siteSettings.url});
@@ -370,10 +370,11 @@ class MemberWelcomeEmailService {
370
370
  * @param {boolean} [options.trackOpens]
371
371
  * @param {boolean} [options.trackClicks]
372
372
  * @param {string | null} [options.automationActionRevisionId]
373
+ * @param {string | null} [options.automationRunStepId]
373
374
  * @param {null | {url: string, oneClickUrl: string}} [options.unsubscribe] - When set, the footer links to an unsubscribe URL and the email carries one-click List-Unsubscribe headers
374
375
  * @returns {Promise<unknown>}
375
376
  */
376
- async #sendEmail({member, memberStatus, email, emailType, trackOpens, trackClicks = false, automationActionRevisionId = null, unsubscribe = null}) {
377
+ async #sendEmail({member, memberStatus, email, emailType, trackOpens, trackClicks = false, automationActionRevisionId = null, automationRunStepId = null, unsubscribe = null}) {
377
378
  if (!member.email) {
378
379
  throw new errors.IncorrectUsageError({
379
380
  message: MESSAGES.MISSING_RECIPIENT_EMAIL
@@ -400,7 +401,8 @@ class MemberWelcomeEmailService {
400
401
  siteSettings: this.#getSiteSettings(),
401
402
  unsubscribeUrl: unsubscribe?.url,
402
403
  trackClicks,
403
- automationActionRevisionId
404
+ automationActionRevisionId,
405
+ automationRunStepId
404
406
  });
405
407
 
406
408
  const senderOptions = await this.#getEffectiveSenderOptions(
@@ -480,9 +482,10 @@ class MemberWelcomeEmailService {
480
482
  * @param {boolean} options.trackOpens
481
483
  * @param {boolean} options.trackClicks
482
484
  * @param {string} options.automationActionRevisionId
485
+ * @param {string} options.automationRunStepId
483
486
  * @returns {Promise<unknown>}
484
487
  */
485
- async sendAutomationEmail({email, member, memberStatus, trackOpens, trackClicks, automationActionRevisionId}) {
488
+ async sendAutomationEmail({email, member, memberStatus, trackOpens, trackClicks, automationActionRevisionId, automationRunStepId}) {
486
489
  const designSettings = email.designSettingId ?
487
490
  await EmailDesignSetting.findOne({id: email.designSettingId}) :
488
491
  null;
@@ -510,6 +513,7 @@ class MemberWelcomeEmailService {
510
513
  trackOpens,
511
514
  trackClicks,
512
515
  automationActionRevisionId,
516
+ automationRunStepId,
513
517
  unsubscribe
514
518
  });
515
519
  }
@@ -244,8 +244,7 @@ function createApiInstance(config) {
244
244
  EmailSpamComplaintEvent: models.EmailSpamComplaintEvent,
245
245
  Automation: models.Automation,
246
246
  WelcomeEmailAutomationRun: models.WelcomeEmailAutomationRun,
247
- AutomatedEmailRecipient: models.AutomatedEmailRecipient,
248
- Gift: models.Gift
247
+ AutomatedEmailRecipient: models.AutomatedEmailRecipient
249
248
  },
250
249
  stripeAPIService: stripeService.api,
251
250
  tiersService: tiersService,
@@ -82,7 +82,7 @@ interface StripeSubscriptions {
82
82
 
83
83
  // Gifts: reassigning a redeemed gift to the imported member.
84
84
  export interface GiftService {
85
- reassignRedeemer(giftId: string, memberId: string, options: object): Promise<void>;
85
+ reassignRedeemer(giftId: string, memberId: string, options: {transacting?: Knex.Transaction}): Promise<void>;
86
86
  }
87
87
 
88
88
  // The completion email concern: who it goes to, the links it carries, and sending
@@ -99,7 +99,8 @@ type CustomFieldPlan = unknown;
99
99
  // The custom fields collaborator as the import needs it. activeFields is the field set a
100
100
  // custom_fields.* column is read against, empty when the feature is off; planWrite
101
101
  // validates a row's values (throwing so the row fails whole) and applyWrite persists
102
- // them, both on the row's transaction.
102
+ // them, merging a composite's sub-fields into whatever is already stored, both on the
103
+ // row's transaction.
103
104
  export interface CustomFieldsImport {
104
105
  activeFields(): Promise<CsvField[]>;
105
106
  planWrite(values: Record<string, unknown>): Promise<CustomFieldPlan[]>;
@@ -40,7 +40,11 @@ function makeImporter(deps) {
40
40
  // Gifts is initialised at boot and always present at request time; the getter
41
41
  // resolves it lazily so the ready service is picked up whenever a row uses it.
42
42
  const gifts = {
43
- reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer(giftId, memberId, options)
43
+ reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer({
44
+ giftId,
45
+ memberId,
46
+ transacting: options.transacting
47
+ })
44
48
  };
45
49
  // Gated by the same labs flag as the export, so the two halves round-trip or stay
46
50
  // silent together: off, activeFields resolves empty and every custom_fields.* column
@@ -48,7 +52,11 @@ function makeImporter(deps) {
48
52
  const customFields = {
49
53
  activeFields: async () => (labs.isSet('membersCustomFields') ? deps.customFields.definitions.browse() : []),
50
54
  planWrite: values => deps.customFields.values.planWrite(values),
51
- applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, executor)
55
+ // mergeComposites: a sub-field is a field, so a row writes the sub-fields it fills
56
+ // and leaves the rest alone, the same rule a blank `name` column follows. Without
57
+ // it a file naming one address column would replace the whole address, clearing
58
+ // five sub-fields it never mentioned.
59
+ applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, executor, { mergeComposites: true })
52
60
  };
53
61
  return new importer_1.default({
54
62
  knex: deps.knex,
@@ -18,7 +18,9 @@ interface ImporterServices {
18
18
  getMembersRepository(): Promise<Omit<MembersRepository, 'getImportLabel'>>;
19
19
  getDefaultTier(): Promise<Tier>;
20
20
  getTierByName(name: string): Promise<Tier | null>;
21
- getGiftService(): GiftService;
21
+ getGiftService(): {
22
+ reassignRedeemer(input: {giftId: string; memberId: string; transacting?: Knex.Transaction}): Promise<void>;
23
+ };
22
24
  sendEmail: EmailNotifications['send'];
23
25
  urlFor: EmailNotifications['urlFor'];
24
26
  addJob(job: {job: () => Promise<void>; offloaded: boolean; name: string}): void;
@@ -31,7 +33,7 @@ interface ImporterServices {
31
33
  definitions: {browse(): Promise<CsvField[]>};
32
34
  values: {
33
35
  planWrite(values: Record<string, unknown>): Promise<unknown[]>;
34
- applyWrite(memberId: string, plan: unknown[], executor: Knex): Promise<void>;
36
+ applyWrite(memberId: string, plan: unknown[], executor: Knex, options?: {mergeComposites?: boolean}): Promise<void>;
35
37
  };
36
38
  };
37
39
  }
@@ -72,7 +74,11 @@ export function makeImporter(deps: ImporterServices) {
72
74
  // Gifts is initialised at boot and always present at request time; the getter
73
75
  // resolves it lazily so the ready service is picked up whenever a row uses it.
74
76
  const gifts: GiftService = {
75
- reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer(giftId, memberId, options)
77
+ reassignRedeemer: (giftId, memberId, options) => deps.getGiftService().reassignRedeemer({
78
+ giftId,
79
+ memberId,
80
+ transacting: options.transacting
81
+ })
76
82
  };
77
83
 
78
84
  // Gated by the same labs flag as the export, so the two halves round-trip or stay
@@ -81,7 +87,11 @@ export function makeImporter(deps: ImporterServices) {
81
87
  const customFields: CustomFieldsImport = {
82
88
  activeFields: async () => (labs.isSet('membersCustomFields') ? deps.customFields.definitions.browse() : []),
83
89
  planWrite: values => deps.customFields.values.planWrite(values),
84
- applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, executor)
90
+ // mergeComposites: a sub-field is a field, so a row writes the sub-fields it fills
91
+ // and leaves the rest alone, the same rule a blank `name` column follows. Without
92
+ // it a file naming one address column would replace the whole address, clearing
93
+ // five sub-fields it never mentioned.
94
+ applyWrite: (memberId, plan, executor) => deps.customFields.values.applyWrite(memberId, plan, executor, {mergeComposites: true})
85
95
  };
86
96
 
87
97
  return new MembersCSVImporter({
@@ -525,7 +525,7 @@ module.exports = class RouterController {
525
525
  * @param {string} options.cancelUrl URL to redirect to after cancelled checkout
526
526
  * @param {string} [options.email] Email address of the customer
527
527
  * @param {object} [options.member] Currently authenticated member OR member associated with the email address
528
- * @param {object} [options.gift] Active gift subscription for the member
528
+ * @param {number|null} [options.giftTrialDays] Stable continuation decision from gift subscriptions
529
529
  * @param {boolean} options.isAuthenticated
530
530
  * @param {object} options.metadata Metadata to be passed to Stripe
531
531
  * @returns
@@ -680,9 +680,11 @@ module.exports = class RouterController {
680
680
 
681
681
  /**
682
682
  * @param {object} options
683
- * @param {object} options.tier
684
- * @param {'month'|'year'} options.cadence
685
- * @param {string} options.email
683
+ * @param {string} options.tierId
684
+ * @param {string} [options.offerId]
685
+ * @param {string} [options.cadence]
686
+ * @param {number} [options.duration]
687
+ * @param {string} [options.email]
686
688
  * @param {string} options.successUrl
687
689
  * @param {string} options.cancelUrl
688
690
  * @param {object} options.metadata
@@ -698,10 +700,26 @@ module.exports = class RouterController {
698
700
  }
699
701
 
700
702
  try {
701
- const paymentLink = await this._paymentsService.getGiftPaymentLink(options);
702
-
703
- return {url: paymentLink};
703
+ return await this._giftService.service.startCheckout({
704
+ tierId: options.tierId,
705
+ offerId: options.offerId,
706
+ cadence: options.cadence,
707
+ duration: options.duration,
708
+ metadata: options.metadata,
709
+ successUrl: options.successUrl,
710
+ cancelUrl: options.cancelUrl,
711
+ buyer: {
712
+ memberId: options.member?.id ?? null,
713
+ email: options.member?.get('email') ?? options.email ?? null,
714
+ name: options.member?.get('name') ?? null,
715
+ isAuthenticated: options.isAuthenticated
716
+ }
717
+ });
704
718
  } catch (err) {
719
+ if (err instanceof BadRequestError || err instanceof NoPermissionError) {
720
+ throw err;
721
+ }
722
+
705
723
  logging.error(err);
706
724
  this._sentry?.captureException?.(err);
707
725
  throw new BadRequestError({
@@ -788,7 +806,7 @@ module.exports = class RouterController {
788
806
  let tier;
789
807
  let cadence;
790
808
  let offer;
791
- let gift;
809
+ let giftTrialDays;
792
810
 
793
811
  if (req.body.continueFromGift) {
794
812
  if (!isAuthenticated || !member) {
@@ -796,25 +814,16 @@ module.exports = class RouterController {
796
814
  message: tpl(messages.signInRequired)
797
815
  });
798
816
  }
799
- if (member.get('status') !== 'gift') {
800
- throw new BadRequestError({
801
- message: tpl(messages.badRequest),
802
- context: 'Member does not have an active gift subscription'
803
- });
804
- }
805
-
806
- gift = await this._giftService.service.getActiveByMember(member.id);
807
- if (!gift) {
808
- throw new BadRequestError({
809
- message: tpl(messages.badRequest),
810
- context: 'No active gift subscription found for member'
811
- });
812
- }
817
+ const continuation = await this._giftService.service.preparePaidContinuation({
818
+ memberId: member.id,
819
+ memberStatus: member.get('status')
820
+ });
813
821
 
814
822
  ({tier, cadence} = await this._getSubscriptionCheckoutData({
815
- tierId: gift.tierId,
816
- cadence: gift.cadence
823
+ tierId: continuation.tierId,
824
+ cadence: continuation.cadence
817
825
  }));
826
+ giftTrialDays = continuation.trialDays;
818
827
  } else {
819
828
  ({tier, cadence, offer} = await this._getSubscriptionCheckoutData(req.body));
820
829
  }
@@ -825,7 +834,7 @@ module.exports = class RouterController {
825
834
  tier,
826
835
  cadence,
827
836
  offer,
828
- gift
837
+ giftTrialDays
829
838
  });
830
839
 
831
840
  // Add welcome_page_url to the response if available and member is authenticated
@@ -842,19 +851,12 @@ module.exports = class RouterController {
842
851
  });
843
852
  }
844
853
 
845
- if (req.body.offerId) {
846
- throw new BadRequestError({
847
- message: tpl(messages.badRequest),
848
- context: 'Offers cannot be applied to gift subscriptions'
849
- });
850
- }
851
-
852
- const data = await this._getSubscriptionCheckoutData(req.body);
853
-
854
854
  response = await this._createGiftCheckoutSession({
855
855
  ...options,
856
- ...data,
857
- duration: 1, // gifts are currently 1 month or 1 year only
856
+ tierId: req.body.tierId,
857
+ offerId: req.body.offerId,
858
+ cadence: req.body.cadence,
859
+ duration: req.body.duration,
858
860
  successUrl: siteUrl,
859
861
  cancelUrl: options.cancelUrl || siteUrl
860
862
  });
@@ -67,8 +67,7 @@ module.exports = function MembersAPI({
67
67
  MemberFeedback,
68
68
  Automation,
69
69
  WelcomeEmailAutomationRun,
70
- AutomatedEmailRecipient,
71
- Gift
70
+ AutomatedEmailRecipient
72
71
  },
73
72
  tiersService,
74
73
  stripeAPIService,
@@ -142,7 +141,7 @@ module.exports = function MembersAPI({
142
141
  memberAttributionService,
143
142
  MemberEmailChangeEvent,
144
143
  AutomatedEmailRecipient,
145
- Gift
144
+ giftSubscriptions: giftService
146
145
  });
147
146
 
148
147
  const nextPaymentCalculator = new NextPaymentCalculator();
@@ -192,8 +191,7 @@ module.exports = function MembersAPI({
192
191
  Offer,
193
192
  offersAPI,
194
193
  stripeAPIService,
195
- settingsCache,
196
- giftService
194
+ settingsCache
197
195
  });
198
196
 
199
197
  const memberController = new MemberController({
@@ -283,7 +281,10 @@ module.exports = function MembersAPI({
283
281
  }
284
282
 
285
283
  if (giftToken) {
286
- await giftService.service.redeem(giftToken, member.id);
284
+ await giftService.service.redeem({
285
+ token: giftToken,
286
+ memberId: member.id
287
+ });
287
288
  }
288
289
 
289
290
  await MemberLoginEvent.add({member_id: member.id});
@@ -316,7 +317,12 @@ module.exports = function MembersAPI({
316
317
  {name, email, labels, newsletters, attribution, geolocation, status: 'gift'},
317
318
  {transacting}
318
319
  );
319
- await giftService.service.redeem(giftToken, created.id, {transacting, newMember: true});
320
+ await giftService.service.redeem({
321
+ token: giftToken,
322
+ memberId: created.id,
323
+ transacting,
324
+ newMember: true
325
+ });
320
326
  return created;
321
327
  });
322
328
  } else {
@@ -37,7 +37,7 @@ module.exports = class EventRepository {
37
37
  urlService,
38
38
  MemberEmailChangeEvent,
39
39
  AutomatedEmailRecipient,
40
- Gift
40
+ giftSubscriptions
41
41
  }) {
42
42
  this._DonationPaymentEvent = DonationPaymentEvent;
43
43
  this._MemberSubscribeEvent = MemberSubscribeEvent;
@@ -57,7 +57,7 @@ module.exports = class EventRepository {
57
57
  this._memberAttributionService = memberAttributionService;
58
58
  this._MemberEmailChangeEvent = MemberEmailChangeEvent;
59
59
  this._AutomatedEmailRecipient = AutomatedEmailRecipient;
60
- this._Gift = Gift;
60
+ this._giftSubscriptions = giftSubscriptions;
61
61
  this._knex = db.knex;
62
62
  }
63
63
 
@@ -462,101 +462,11 @@ module.exports = class EventRepository {
462
462
  }
463
463
 
464
464
  async getGiftPurchaseEvents(options = {}, filter) {
465
- options = {
466
- ...options,
467
- withRelated: ['buyer', 'tier'],
468
- filter: 'buyer_member_id:-null+custom:true',
469
- useBasicCount: true,
470
- mongoTransformer: chainTransformers(
471
- // First set the filter manually
472
- replaceCustomFilterTransformer(filter),
473
-
474
- // Map the used keys in that filter
475
- ...mapKeys({
476
- 'data.created_at': 'purchased_at',
477
- 'data.member_id': 'buyer_member_id'
478
- })
479
- )
480
- };
481
-
482
- if (options.order) {
483
- options.order = options.order.replace(/created_at/g, 'purchased_at');
484
- }
485
-
486
- const {data: models, meta} = await this._Gift.findPage(options);
487
-
488
- const data = models.map((model) => {
489
- const json = model.toJSON(options);
490
-
491
- return {
492
- type: 'gift_purchase_event',
493
- data: {
494
- id: json.id,
495
- member: json.buyer || null,
496
- member_id: json.buyer_member_id,
497
- tier_name: json.tier?.name,
498
- cadence: json.cadence,
499
- duration: json.duration,
500
- amount: json.amount,
501
- currency: json.currency,
502
- created_at: json.purchased_at
503
- }
504
- };
505
- });
506
-
507
- return {
508
- data,
509
- meta
510
- };
465
+ return this._giftSubscriptions.service.browsePurchaseEvents(options, filter);
511
466
  }
512
467
 
513
468
  async getGiftRedemptionEvents(options = {}, filter) {
514
- options = {
515
- ...options,
516
- withRelated: ['redeemer', 'tier'],
517
- filter: 'redeemer_member_id:-null+custom:true',
518
- useBasicCount: true,
519
- mongoTransformer: chainTransformers(
520
- // First set the filter manually
521
- replaceCustomFilterTransformer(filter),
522
-
523
- // Map the used keys in that filter
524
- ...mapKeys({
525
- 'data.created_at': 'redeemed_at',
526
- 'data.member_id': 'redeemer_member_id'
527
- })
528
- )
529
- };
530
-
531
- if (options.order) {
532
- options.order = options.order.replace(/created_at/g, 'redeemed_at');
533
- }
534
-
535
- const {data: models, meta} = await this._Gift.findPage(options);
536
-
537
- const data = models.map((model) => {
538
- const json = model.toJSON(options);
539
-
540
- return {
541
- type: 'gift_redemption_event',
542
- data: {
543
- id: json.id,
544
- member: json.redeemer || null,
545
- member_id: json.redeemer_member_id,
546
- tier_name: json.tier?.name,
547
- cadence: json.cadence,
548
- duration: json.duration,
549
- amount: json.amount,
550
- currency: json.currency,
551
- created_at: json.redeemed_at
552
- }
553
- };
554
- });
555
-
556
- return {
557
- data,
558
- meta
559
- };
469
+ return this._giftSubscriptions.service.browseRedemptionEvents(options, filter);
560
470
  }
561
471
 
562
472
  async getGiftEndedEvents(options = {}, filter) {
@@ -34,7 +34,7 @@ const CUSTOM_FIELDS_EDITED_ACTION = 'custom_fields_edited';
34
34
 
35
35
  /**
36
36
  * @typedef {object} IGiftServiceWrapper
37
- * @prop {{getActiveByMembers: (memberIds: string[]) => Promise<Map<string, {cadence: 'month' | 'year', currency: string, amount: number}>>}} service
37
+ * @prop {{getMemberPresentations: (memberIds: string[]) => Promise<Map<string, {cadence: 'month' | 'year', currency: string, amount: number}>>}} service
38
38
  */
39
39
 
40
40
  module.exports = class MemberBREADService {
@@ -347,7 +347,7 @@ module.exports = class MemberBREADService {
347
347
  * @private
348
348
  * Fetches active redeemed gifts for any gift-status members in the input list.
349
349
  * @param {import('bookshelf').Model[]} members - Bookshelf member models
350
- * @returns {Promise<Map<string, {cadence: 'month' | 'year', currency: string, amount: number}>>} keyed by member.id → active Gift
350
+ * @returns {Promise<Map<string, {cadence: 'month' | 'year', currency: string, amount: number}>>} keyed by member.id → stable gift presentation
351
351
  */
352
352
  async fetchActiveGiftsForMembers(members) {
353
353
  const giftMemberIds = members
@@ -359,7 +359,7 @@ module.exports = class MemberBREADService {
359
359
  }
360
360
 
361
361
  try {
362
- return await this.giftService.service.getActiveByMembers(giftMemberIds);
362
+ return await this.giftService.service.getMemberPresentations(giftMemberIds);
363
363
  } catch (e) {
364
364
  logging.error(`Failed to load active gifts for members - ${giftMemberIds.join(', ')}.`);
365
365
  logging.error(e);