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
@@ -129,7 +129,8 @@ const processStep = async ({ automationsApi, memberWelcomeEmailService, schedule
129
129
  memberStatus,
130
130
  trackOpens,
131
131
  trackClicks,
132
- automationActionRevisionId: step.automation_action_revision_id
132
+ automationActionRevisionId: step.automation_action_revision_id,
133
+ automationRunStepId: step.id
133
134
  });
134
135
  const mailgunMessageId = (0, mailgun_message_id_1.getMailgunMessageId)(sendResult);
135
136
  // Only Mailgun sends can produce open events for automation emails
@@ -137,6 +138,7 @@ const processStep = async ({ automationsApi, memberWelcomeEmailService, schedule
137
138
  try {
138
139
  await automationsApi.recordEmailSent({
139
140
  automationActionRevisionId: step.automation_action_revision_id,
141
+ automationRunStepId: step.id,
140
142
  ...(mailgunMessageId ? { mailgunMessageId } : {}),
141
143
  memberEmail: member.get('email'),
142
144
  memberId: step.member_id,
@@ -28,6 +28,7 @@ type MemberWelcomeEmailService = {
28
28
  trackOpens: boolean;
29
29
  trackClicks: boolean;
30
30
  automationActionRevisionId: string;
31
+ automationRunStepId: string;
31
32
  }) => Promise<unknown>;
32
33
  };
33
34
  };
@@ -202,7 +203,8 @@ const processStep = async ({
202
203
  memberStatus,
203
204
  trackOpens,
204
205
  trackClicks,
205
- automationActionRevisionId: step.automation_action_revision_id
206
+ automationActionRevisionId: step.automation_action_revision_id,
207
+ automationRunStepId: step.id
206
208
  });
207
209
  const mailgunMessageId = getMailgunMessageId(sendResult);
208
210
  // Only Mailgun sends can produce open events for automation emails
@@ -210,6 +212,7 @@ const processStep = async ({
210
212
  try {
211
213
  await automationsApi.recordEmailSent({
212
214
  automationActionRevisionId: step.automation_action_revision_id,
215
+ automationRunStepId: step.id,
213
216
  ...(mailgunMessageId ? {mailgunMessageId} : {}),
214
217
  memberEmail: member.get('email'),
215
218
  memberId: step.member_id,
@@ -41,6 +41,7 @@ const strict_1 = __importDefault(require("node:assert/strict"));
41
41
  const one_at_a_time_1 = require("../../../shared/one-at-a-time");
42
42
  const poll_1 = require("./poll");
43
43
  const automationsApi = __importStar(require("./automations-api"));
44
+ const get_scheduler_idempotency_key_1 = require("./get-scheduler-idempotency-key");
44
45
  const promises_1 = require("node:timers/promises");
45
46
  const soonest_timer_1 = require("../../lib/soonest-timer");
46
47
  const scheduler_poll_time_1 = require("./scheduler-poll-time");
@@ -101,7 +102,10 @@ class AutomationsService {
101
102
  schedulerAdapter.schedule({
102
103
  time: schedulerPollTime.getTime(),
103
104
  url: url.toString(),
104
- extra: { httpMethod: 'PUT' }
105
+ extra: {
106
+ httpMethod: 'PUT',
107
+ idempotencyKey: (0, get_scheduler_idempotency_key_1.getSchedulerIdempotencyKey)(date, url)
108
+ }
105
109
  });
106
110
  }
107
111
  catch (err) {
@@ -6,6 +6,7 @@ import type DomainEvents from '@tryghost/domain-events';
6
6
  import {oneAtATime} from '../../../shared/one-at-a-time';
7
7
  import {poll} from './poll';
8
8
  import * as automationsApi from './automations-api';
9
+ import {getSchedulerIdempotencyKey} from './get-scheduler-idempotency-key';
9
10
  import {setImmediate as flushEventLoop} from 'node:timers/promises';
10
11
  import {SoonestTimer} from '../../lib/soonest-timer';
11
12
  import {getSchedulerPollTime} from './scheduler-poll-time';
@@ -85,7 +86,10 @@ export class AutomationsService {
85
86
  schedulerAdapter.schedule({
86
87
  time: schedulerPollTime.getTime(),
87
88
  url: url.toString(),
88
- extra: {httpMethod: 'PUT'}
89
+ extra: {
90
+ httpMethod: 'PUT',
91
+ idempotencyKey: getSchedulerIdempotencyKey(date, url)
92
+ }
89
93
  });
90
94
  } catch (err) {
91
95
  logging.error({event: {name: 'automations.enqueue-poll.error'}, err, at: date.toISOString()}, 'Failed to enqueue automations poll');
@@ -0,0 +1,9 @@
1
+ # Gift Links
2
+
3
+ Gift Links covers shareable access to individual protected posts and pages without creating a membership.
4
+
5
+ ## Language
6
+
7
+ **Gift Link**:
8
+ A revocable link that grants its holder access to one protected post or page. It does not create or redeem a gift subscription.
9
+ _Avoid_: Redemption link, gift-subscription link
@@ -0,0 +1,55 @@
1
+ # Gift Subscriptions
2
+
3
+ Gift Subscriptions covers prepaid, fixed-duration membership access intended to be shared and claimed. Its language spans gift purchase, gift redemption, gift expiration, gift consumption, and gift continuation into a paid subscription.
4
+
5
+ ## Language
6
+
7
+ ### Roles
8
+
9
+ **Buyer**:
10
+ The person who pays for a gift subscription. A buyer need not be a member and may also become the redeemer if eligible.
11
+ _Avoid_: Purchaser, recipient, redeemer
12
+
13
+ **Redeemer**:
14
+ The member who claims a gift subscription and receives its access. The redeemer is determined at redemption, not purchase.
15
+ _Avoid_: Recipient, buyer
16
+
17
+ ### Purchase and claim
18
+
19
+ **Gift subscription**:
20
+ A prepaid, non-renewing entitlement to one membership tier for a fixed period. It begins when claimed rather than when purchased.
21
+ _Avoid_: Gift membership, recurring subscription
22
+
23
+ **Gift purchase**:
24
+ A one-time purchase that creates a gift subscription for the buyer to share.
25
+ _Avoid_: Gift checkout
26
+
27
+ **Redemption link**:
28
+ A single-use, time-limited link through which a purchased gift subscription can be claimed.
29
+ _Avoid_: Gift Link
30
+
31
+ **Gift redemption**:
32
+ The act of claiming an eligible gift subscription. The member who claims it becomes the redeemer and gifted access begins at that point.
33
+ _Avoid_: Gift activation
34
+
35
+ ### Time and lifecycle
36
+
37
+ **Gift duration**:
38
+ The total length of gifted access, measured from redemption.
39
+ _Avoid_: Gift cadence, billing period
40
+
41
+ **Gift cadence**:
42
+ The monthly or yearly membership price basis used to value a gift subscription. It does not make the gift recurring.
43
+ _Avoid_: Gift duration, renewal interval
44
+
45
+ **Gift expiration**:
46
+ The end of the claim window for a purchased gift subscription that was never redeemed.
47
+ _Avoid_: Gift consumption
48
+
49
+ **Gift consumption**:
50
+ The end of a redeemed gift subscription's role in providing access, either when its duration finishes or a paid subscription takes over.
51
+ _Avoid_: Gift expiration, cancellation
52
+
53
+ **Gift continuation**:
54
+ Starting a paid subscription from gifted access while carrying the remaining gifted time forward before billing begins.
55
+ _Avoid_: Gift renewal
@@ -0,0 +1,28 @@
1
+ # Gifts
2
+
3
+ This module owns the gift-subscription journey. Its interface is
4
+ capability-oriented; callers do not receive `Gift` entities, Bookshelf models,
5
+ transactions, or Stripe objects.
6
+
7
+ ## Interface
8
+
9
+ - `startCheckout(input)` owns the `giftSubCustomization` flag, duration
10
+ catalogue, Portal plan gate, tier/cadence validation, authoritative price,
11
+ token, success parameters, metadata, customer, and one-time payment.
12
+ - `preparePaidContinuation(input)` validates an active gift and returns the
13
+ stable tier/cadence/trial decision consumed by the normal subscription
14
+ checkout path.
15
+ - `getRedeemable(input)` and `redeem(input)` return the public redemption DTO.
16
+ - `completePurchase(input)` and `handlePaymentRefund(input)` accept normalized
17
+ payment outcomes.
18
+ - `handlePaidSubscriptionActivation(memberId)` consumes gift time when a paid
19
+ subscription takes over.
20
+ - `getMemberPresentations(memberIds)`, `getPreview(token)`,
21
+ `browsePurchaseEvents(...)`, and `browseRedemptionEvents(...)` expose
22
+ stable read models.
23
+ - `processReminders()`, `processConsumed()`, and `processExpired()` own due
24
+ lifecycle work; scheduler and HTTP triggers remain adapters.
25
+ - `reassignRedeemer(...)` is the import capability.
26
+
27
+ The `Gift`, repository, Bookshelf query, Stripe checkout, email, scheduling,
28
+ and notification collaborators are internal adapters.
@@ -5,7 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.GiftBookshelfRepository = void 0;
7
7
  const errors_1 = __importDefault(require("@tryghost/errors"));
8
- const gift_1 = require("./gift");
8
+ const mongo_utils_1 = require("@tryghost/mongo-utils");
9
+ const gift_codec_1 = require("./gift-codec");
9
10
  class GiftBookshelfRepository {
10
11
  model;
11
12
  constructor({ GiftModel }) {
@@ -60,6 +61,26 @@ class GiftBookshelfRepository {
60
61
  }
61
62
  return map;
62
63
  }
64
+ browsePurchaseEvents(options = {}, filter) {
65
+ return this.browseEvents({
66
+ options,
67
+ filter,
68
+ type: 'gift_purchase_event',
69
+ relation: 'buyer',
70
+ memberIdColumn: 'buyer_member_id',
71
+ dateColumn: 'purchased_at'
72
+ });
73
+ }
74
+ browseRedemptionEvents(options = {}, filter) {
75
+ return this.browseEvents({
76
+ options,
77
+ filter,
78
+ type: 'gift_redemption_event',
79
+ relation: 'redeemer',
80
+ memberIdColumn: 'redeemer_member_id',
81
+ dateColumn: 'redeemed_at'
82
+ });
83
+ }
63
84
  async findPendingConsumption() {
64
85
  const now = new Date();
65
86
  const collection = await this.model.findAll({
@@ -111,53 +132,52 @@ class GiftBookshelfRepository {
111
132
  return await this.model.transaction(callback);
112
133
  }
113
134
  toRow(gift) {
114
- return {
115
- token: gift.token,
116
- buyer_email: gift.buyerEmail,
117
- buyer_member_id: gift.buyerMemberId,
118
- redeemer_member_id: gift.redeemerMemberId,
119
- tier_id: gift.tierId,
120
- cadence: gift.cadence,
121
- duration: gift.duration,
122
- currency: gift.currency,
123
- amount: gift.amount,
124
- stripe_checkout_session_id: gift.stripeCheckoutSessionId,
125
- stripe_payment_intent_id: gift.stripePaymentIntentId,
126
- consumes_at: gift.consumesAt,
127
- expires_at: gift.expiresAt,
128
- status: gift.status,
129
- purchased_at: gift.purchasedAt,
130
- redeemed_at: gift.redeemedAt,
131
- consumed_at: gift.consumedAt,
132
- expired_at: gift.expiredAt,
133
- refunded_at: gift.refundedAt,
134
- consumes_soon_reminder_sent_at: gift.consumesSoonReminderSentAt
135
- };
135
+ return (0, gift_codec_1.encodeGift)(gift);
136
136
  }
137
137
  toGift(model) {
138
- const json = model.toJSON();
139
- return new gift_1.Gift({
140
- token: json.token,
141
- buyerEmail: json.buyer_email,
142
- buyerMemberId: json.buyer_member_id,
143
- redeemerMemberId: json.redeemer_member_id,
144
- tierId: json.tier_id,
145
- cadence: json.cadence,
146
- duration: json.duration,
147
- currency: json.currency,
148
- amount: json.amount,
149
- stripeCheckoutSessionId: json.stripe_checkout_session_id,
150
- stripePaymentIntentId: json.stripe_payment_intent_id,
151
- consumesAt: json.consumes_at,
152
- expiresAt: json.expires_at,
153
- status: json.status,
154
- purchasedAt: json.purchased_at,
155
- redeemedAt: json.redeemed_at,
156
- consumedAt: json.consumed_at,
157
- expiredAt: json.expired_at,
158
- refundedAt: json.refunded_at,
159
- consumesSoonReminderSentAt: json.consumes_soon_reminder_sent_at ?? null
138
+ return (0, gift_codec_1.decodeGiftRow)(model.toJSON());
139
+ }
140
+ async browseEvents({ options, filter, type, relation, memberIdColumn, dateColumn }) {
141
+ const replaceCustomFilter = (existingFilter) => (0, mongo_utils_1.replaceFilters)(existingFilter, {
142
+ custom: filter
160
143
  });
144
+ const queryOptions = {
145
+ ...options,
146
+ withRelated: [relation, 'tier'],
147
+ filter: `${memberIdColumn}:-null+custom:true`,
148
+ useBasicCount: true,
149
+ mongoTransformer: (0, mongo_utils_1.chainTransformers)(replaceCustomFilter, ...(0, mongo_utils_1.mapKeys)({
150
+ 'data.created_at': dateColumn,
151
+ 'data.member_id': memberIdColumn
152
+ }))
153
+ };
154
+ if (typeof queryOptions.order === 'string') {
155
+ queryOptions.order = queryOptions.order.replace(/created_at/g, dateColumn);
156
+ }
157
+ if (!this.model.findPage) {
158
+ throw new errors_1.default.InternalServerError({ message: 'Gift model does not support paginated event queries.' });
159
+ }
160
+ const { data: models, meta } = await this.model.findPage(queryOptions);
161
+ return {
162
+ data: models.map((model) => {
163
+ const json = model.toJSON(queryOptions);
164
+ return {
165
+ type,
166
+ data: {
167
+ id: json.id,
168
+ member: json[relation] || null,
169
+ member_id: json[memberIdColumn],
170
+ tier_name: json.tier?.name,
171
+ cadence: json.cadence,
172
+ duration: json.duration,
173
+ amount: json.amount,
174
+ currency: json.currency,
175
+ created_at: json[dateColumn]
176
+ }
177
+ };
178
+ }),
179
+ meta
180
+ };
161
181
  }
162
182
  }
163
183
  exports.GiftBookshelfRepository = GiftBookshelfRepository;
@@ -1,10 +1,100 @@
1
1
  import errors from '@tryghost/errors';
2
- import {Gift, type GiftCadence, type GiftStatus} from './gift';
3
- import type {FindPendingReminderOptions, GiftRepository, RepositoryTransactionOptions} from './gift-repository';
2
+ import {chainTransformers, mapKeys, replaceFilters} from '@tryghost/mongo-utils';
3
+ import type {Knex} from 'knex';
4
+ import {Gift} from './gift';
5
+ import {decodeGiftRow, encodeGift} from './gift-codec';
6
+ import type {GiftCadence, GiftRow} from './gift-schema';
7
+
8
+ type ParsedNqlFilter = unknown;
9
+
10
+ export interface GiftEventBrowseOptions {
11
+ filter?: string;
12
+ limit?: number | 'all';
13
+ order?: string;
14
+ page?: number;
15
+ }
16
+
17
+ export interface Pagination {
18
+ page: number;
19
+ pages: number;
20
+ limit: number | 'all';
21
+ total: number;
22
+ prev: number | null;
23
+ next: number | null;
24
+ }
25
+
26
+ export interface GiftEventData {
27
+ id: string;
28
+ member: Record<string, unknown> | null;
29
+ member_id: string | null;
30
+ tier_name?: string;
31
+ cadence: GiftCadence;
32
+ duration: number;
33
+ amount: number;
34
+ currency: string;
35
+ created_at: Date | string | null;
36
+ }
37
+
38
+ export interface GiftEventPage {
39
+ data: Array<{
40
+ type: 'gift_purchase_event' | 'gift_redemption_event';
41
+ data: GiftEventData;
42
+ }>;
43
+ meta: {
44
+ pagination?: Pagination;
45
+ };
46
+ }
47
+
48
+ export interface RepositoryTransactionOptions {
49
+ transacting?: Knex.Transaction;
50
+ forUpdate?: boolean;
51
+ }
52
+
53
+ export interface FindPendingReminderOptions {
54
+ now: Date;
55
+ reminderLeadMs: number;
56
+ reminderFloorMs: number;
57
+ transacting?: Knex.Transaction;
58
+ }
59
+
60
+ export interface GiftRepository {
61
+ existsByCheckoutSessionId(checkoutSessionId: string): Promise<boolean>;
62
+ getById(id: string, options?: RepositoryTransactionOptions): Promise<Gift | null>;
63
+ getByToken(token: string, options?: RepositoryTransactionOptions): Promise<Gift | null>;
64
+ getByPaymentIntentId(paymentIntentId: string): Promise<Gift | null>;
65
+ findPendingConsumption(): Promise<Gift[]>;
66
+ findPendingExpiration(): Promise<Gift[]>;
67
+ findPendingReminder(options: FindPendingReminderOptions): Promise<Gift[]>;
68
+ findUnsentReminders(): Promise<Gift[]>;
69
+ getActiveByMember(memberId: string, options?: RepositoryTransactionOptions): Promise<Gift | null>;
70
+ getActiveByMembers(memberIds: string[], options?: RepositoryTransactionOptions): Promise<Map<string, Gift>>;
71
+ browsePurchaseEvents(options?: GiftEventBrowseOptions, filter?: ParsedNqlFilter): Promise<GiftEventPage>;
72
+ browseRedemptionEvents(options?: GiftEventBrowseOptions, filter?: ParsedNqlFilter): Promise<GiftEventPage>;
73
+ create(gift: Gift, options?: RepositoryTransactionOptions): Promise<void>;
74
+ update(gift: Gift, options?: RepositoryTransactionOptions): Promise<void>;
75
+ transaction<T>(callback: (transacting: Knex.Transaction) => Promise<T>): Promise<T>;
76
+ }
77
+
78
+ type BookshelfSaveOptions = RepositoryTransactionOptions & {
79
+ autoRefresh?: boolean;
80
+ method?: 'update';
81
+ patch?: boolean;
82
+ };
83
+
84
+ type GiftEventQueryOptions = GiftEventBrowseOptions & {
85
+ withRelated: Array<'buyer' | 'redeemer' | 'tier'>;
86
+ filter: string;
87
+ useBasicCount: boolean;
88
+ mongoTransformer: (filter: ParsedNqlFilter) => ParsedNqlFilter;
89
+ };
90
+
91
+ type BookshelfFindOptions = RepositoryTransactionOptions & {
92
+ filter?: string;
93
+ require?: boolean;
94
+ };
4
95
 
5
96
  type BookshelfDocument<T> = {
6
- save(data: Partial<T>, options?: unknown): Promise<unknown>;
7
- set(data: Partial<T>): void;
97
+ save(data: Partial<T>, options?: BookshelfSaveOptions): Promise<BookshelfDocument<T>>;
8
98
  toJSON(): T;
9
99
  };
10
100
 
@@ -12,35 +102,31 @@ type BookshelfCollection<T> = {
12
102
  models: BookshelfDocument<T>[];
13
103
  };
14
104
 
15
- type BookshelfModel<T> = {
16
- add(data: Partial<T>, unfilteredOptions?: unknown): Promise<T>;
17
- transaction<R>(callback: (transacting: unknown) => Promise<R>): Promise<R>;
18
- findOne(data: Record<string, unknown>, unfilteredOptions?: unknown): Promise<BookshelfDocument<T> | null>;
19
- findAll(unfilteredOptions?: unknown): Promise<BookshelfCollection<T>>;
20
- };
21
-
22
- type GiftRow = {
105
+ type GiftEventBookshelfRow = {
23
106
  id: string;
24
- token: string;
25
- buyer_email: string;
26
107
  buyer_member_id: string | null;
27
108
  redeemer_member_id: string | null;
28
- tier_id: string;
109
+ buyer?: Record<string, unknown> | null;
110
+ redeemer?: Record<string, unknown> | null;
111
+ tier?: {name?: string} | null;
29
112
  cadence: GiftCadence;
30
113
  duration: number;
31
- currency: string;
32
114
  amount: number;
33
- stripe_checkout_session_id: string;
34
- stripe_payment_intent_id: string;
35
- consumes_at: Date | null;
36
- expires_at: Date;
37
- status: GiftStatus;
38
- purchased_at: Date;
39
- redeemed_at: Date | null;
40
- consumed_at: Date | null;
41
- expired_at: Date | null;
42
- refunded_at: Date | null;
43
- consumes_soon_reminder_sent_at: Date | null;
115
+ currency: string;
116
+ purchased_at: Date | string;
117
+ redeemed_at: Date | string | null;
118
+ };
119
+
120
+ type GiftEventBookshelfDocument = {
121
+ toJSON(options?: GiftEventQueryOptions): GiftEventBookshelfRow;
122
+ };
123
+
124
+ type BookshelfModel<T> = {
125
+ add(data: Partial<T>, options?: RepositoryTransactionOptions): Promise<BookshelfDocument<T>>;
126
+ transaction<R>(callback: (transacting: Knex.Transaction) => Promise<R>): Promise<R>;
127
+ findOne(data: Partial<T> & {id?: string}, options?: BookshelfFindOptions): Promise<BookshelfDocument<T> | null>;
128
+ findAll(options?: BookshelfFindOptions): Promise<BookshelfCollection<T>>;
129
+ findPage(options: GiftEventQueryOptions): Promise<{data: GiftEventBookshelfDocument[]; meta: GiftEventPage['meta']}>;
44
130
  };
45
131
 
46
132
  type GiftBookshelfModel = BookshelfModel<GiftRow>;
@@ -116,6 +202,28 @@ export class GiftBookshelfRepository implements GiftRepository {
116
202
  return map;
117
203
  }
118
204
 
205
+ browsePurchaseEvents(options: GiftEventBrowseOptions = {}, filter?: ParsedNqlFilter): Promise<GiftEventPage> {
206
+ return this.browseEvents({
207
+ options,
208
+ filter,
209
+ type: 'gift_purchase_event',
210
+ relation: 'buyer',
211
+ memberIdColumn: 'buyer_member_id',
212
+ dateColumn: 'purchased_at'
213
+ });
214
+ }
215
+
216
+ browseRedemptionEvents(options: GiftEventBrowseOptions = {}, filter?: ParsedNqlFilter): Promise<GiftEventPage> {
217
+ return this.browseEvents({
218
+ options,
219
+ filter,
220
+ type: 'gift_redemption_event',
221
+ relation: 'redeemer',
222
+ memberIdColumn: 'redeemer_member_id',
223
+ dateColumn: 'redeemed_at'
224
+ });
225
+ }
226
+
119
227
  async findPendingConsumption(): Promise<Gift[]> {
120
228
  const now = new Date();
121
229
 
@@ -179,59 +287,80 @@ export class GiftBookshelfRepository implements GiftRepository {
179
287
  });
180
288
  }
181
289
 
182
- async transaction<T>(callback: (transacting: unknown) => Promise<T>): Promise<T> {
290
+ async transaction<T>(callback: (transacting: Knex.Transaction) => Promise<T>): Promise<T> {
183
291
  return await this.model.transaction(callback);
184
292
  }
185
293
 
186
- private toRow(gift: Gift): Omit<GiftRow, 'id'> {
187
- return {
188
- token: gift.token,
189
- buyer_email: gift.buyerEmail,
190
- buyer_member_id: gift.buyerMemberId,
191
- redeemer_member_id: gift.redeemerMemberId,
192
- tier_id: gift.tierId,
193
- cadence: gift.cadence,
194
- duration: gift.duration,
195
- currency: gift.currency,
196
- amount: gift.amount,
197
- stripe_checkout_session_id: gift.stripeCheckoutSessionId,
198
- stripe_payment_intent_id: gift.stripePaymentIntentId,
199
- consumes_at: gift.consumesAt,
200
- expires_at: gift.expiresAt,
201
- status: gift.status,
202
- purchased_at: gift.purchasedAt,
203
- redeemed_at: gift.redeemedAt,
204
- consumed_at: gift.consumedAt,
205
- expired_at: gift.expiredAt,
206
- refunded_at: gift.refundedAt,
207
- consumes_soon_reminder_sent_at: gift.consumesSoonReminderSentAt
208
- };
294
+ private toRow(gift: Gift): GiftRow {
295
+ return encodeGift(gift);
209
296
  }
210
297
 
211
298
  private toGift(model: BookshelfDocument<GiftRow>): Gift {
212
- const json = model.toJSON();
213
-
214
- return new Gift({
215
- token: json.token,
216
- buyerEmail: json.buyer_email,
217
- buyerMemberId: json.buyer_member_id,
218
- redeemerMemberId: json.redeemer_member_id,
219
- tierId: json.tier_id,
220
- cadence: json.cadence,
221
- duration: json.duration,
222
- currency: json.currency,
223
- amount: json.amount,
224
- stripeCheckoutSessionId: json.stripe_checkout_session_id,
225
- stripePaymentIntentId: json.stripe_payment_intent_id,
226
- consumesAt: json.consumes_at,
227
- expiresAt: json.expires_at,
228
- status: json.status,
229
- purchasedAt: json.purchased_at,
230
- redeemedAt: json.redeemed_at,
231
- consumedAt: json.consumed_at,
232
- expiredAt: json.expired_at,
233
- refundedAt: json.refunded_at,
234
- consumesSoonReminderSentAt: json.consumes_soon_reminder_sent_at ?? null
299
+ return decodeGiftRow(model.toJSON());
300
+ }
301
+
302
+ private async browseEvents({
303
+ options,
304
+ filter,
305
+ type,
306
+ relation,
307
+ memberIdColumn,
308
+ dateColumn
309
+ }: {
310
+ options: GiftEventBrowseOptions;
311
+ filter?: ParsedNqlFilter;
312
+ type: 'gift_purchase_event' | 'gift_redemption_event';
313
+ relation: 'buyer' | 'redeemer';
314
+ memberIdColumn: 'buyer_member_id' | 'redeemer_member_id';
315
+ dateColumn: 'purchased_at' | 'redeemed_at';
316
+ }): Promise<GiftEventPage> {
317
+ const replaceCustomFilter = (existingFilter: ParsedNqlFilter): ParsedNqlFilter => replaceFilters(existingFilter, {
318
+ custom: filter
235
319
  });
320
+ const queryOptions: GiftEventQueryOptions = {
321
+ ...options,
322
+ withRelated: [relation, 'tier'],
323
+ filter: `${memberIdColumn}:-null+custom:true`,
324
+ useBasicCount: true,
325
+ mongoTransformer: chainTransformers(
326
+ replaceCustomFilter,
327
+ ...mapKeys({
328
+ 'data.created_at': dateColumn,
329
+ 'data.member_id': memberIdColumn
330
+ })
331
+ )
332
+ };
333
+
334
+ if (typeof queryOptions.order === 'string') {
335
+ queryOptions.order = queryOptions.order.replace(/created_at/g, dateColumn);
336
+ }
337
+
338
+ if (!this.model.findPage) {
339
+ throw new errors.InternalServerError({message: 'Gift model does not support paginated event queries.'});
340
+ }
341
+
342
+ const {data: models, meta} = await this.model.findPage(queryOptions);
343
+
344
+ return {
345
+ data: models.map((model) => {
346
+ const json = model.toJSON(queryOptions);
347
+
348
+ return {
349
+ type,
350
+ data: {
351
+ id: json.id,
352
+ member: json[relation] || null,
353
+ member_id: json[memberIdColumn],
354
+ tier_name: json.tier?.name,
355
+ cadence: json.cadence,
356
+ duration: json.duration,
357
+ amount: json.amount,
358
+ currency: json.currency,
359
+ created_at: json[dateColumn]
360
+ }
361
+ };
362
+ }),
363
+ meta
364
+ };
236
365
  }
237
366
  }