ghost 6.54.1 → 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.
- package/components/tryghost-adapter-base-scheduling-0.2.0.tgz +0 -0
- package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
- package/components/tryghost-kg-default-nodes-2.1.4.tgz +0 -0
- package/components/tryghost-kg-default-transforms-1.3.3.tgz +0 -0
- package/components/tryghost-kg-html-to-lexical-1.3.3.tgz +0 -0
- package/components/tryghost-kg-lexical-html-renderer-1.4.3.tgz +0 -0
- package/components/tryghost-nql-string-0.0.0.tgz +0 -0
- package/core/boot.js +3 -6
- package/core/built/admin/assets/{Combination-CJG7Flp-.js → Combination-BCm9FL4o.js} +1 -1
- package/core/built/admin/assets/{PolarAngleAxis-BGjf7sBu.js → PolarAngleAxis-C4qtHy1B.js} +1 -1
- package/core/built/admin/assets/{RadialBarChart-zeFf9mBz.js → RadialBarChart-Bh09Stsj.js} +1 -1
- package/core/built/admin/assets/{account-migration-BklF2I_I.js → account-migration-D3O_yVck.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-CNovr6W-.js → account-migration-D8FbP-Z-.js} +7 -7
- package/core/built/admin/assets/activitypub/activitypub.js +6 -6
- package/core/built/admin/assets/activitypub/{avatar-B3DEDB1l.js → avatar-11EA1VSs.js} +2 -2
- package/core/built/admin/assets/activitypub/{avatar-flipboard-Cqrg0XMx.js → avatar-flipboard-jfbLG-xe.js} +2 -2
- package/core/built/admin/assets/activitypub/{bluesky-sharing-Ceslb1_M.js → bluesky-sharing-f1jTQe2P.js} +16 -16
- package/core/built/admin/assets/activitypub/{content-formatters-BpQARh28.js → content-formatters-nmTOPY9M.js} +3 -3
- package/core/built/admin/assets/activitypub/{copy-D9UWIAan.js → copy-BCwo9ep3.js} +2 -2
- package/core/built/admin/assets/activitypub/{dist-DHAjBcV3.js → dist-BEfX-0aW.js} +12 -12
- package/core/built/admin/assets/activitypub/{domain-C1qQvJ8C.js → domain-AaDlZlrA.js} +9 -9
- package/core/built/admin/assets/activitypub/{edit-profile-BcH-jEfM.js → edit-profile-DPi8Tl5M.js} +945 -945
- package/core/built/admin/assets/activitypub/{explore-AxwBh9dI.js → explore-CxTzjILQ.js} +8 -8
- package/core/built/admin/assets/activitypub/{feed-BaOlbh7a.js → feed-DPv5x2MX.js} +33 -33
- package/core/built/admin/assets/activitypub/{field-B7mYYQpN.js → field-DbgA8lat.js} +3 -3
- package/core/built/admin/assets/activitypub/{hash-BC9Q0N4c.js → hash-BWuZoYkA.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-DQ4ZnC1m.js → inbox-35VDQ2K2.js} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-vXez-5uJ.js → inbox-DSrF_i1O.js} +85 -85
- package/core/built/admin/assets/activitypub/{label-Chv5T8bw.js → label-CPjFBbo7.js} +3 -3
- package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-DtSM2i1I.js → moderation-BkDC1XVZ.js} +11 -11
- package/core/built/admin/assets/activitypub/{note-Dckp4S-2.js → note-xEyH4xT-.js} +7 -7
- package/core/built/admin/assets/activitypub/{notifications-CG5T22vg.js → notifications-DClaL3ui.js} +9 -9
- package/core/built/admin/assets/activitypub/{preferences-D_799jdl.js → preferences-DpnOa-4Z.js} +20 -20
- package/core/built/admin/assets/activitypub/{profile-D2JfXBQl.js → profile-Dvi9mz-6.js} +237 -237
- package/core/built/admin/assets/activitypub/{reply-CpdMeAO5.js → reply-Cs9ySd6Z.js} +2 -2
- package/core/built/admin/assets/activitypub/{routes-CCHe2dQp.js → routes-DFBeTnfn.js} +1139 -1150
- package/core/built/admin/assets/activitypub/{separator-BI1FLsYM.js → separator-C5u_VjzY.js} +3 -3
- package/core/built/admin/assets/activitypub/{settings-kx-9Enkz.js → settings-CV2fFu6D.js} +13 -13
- package/core/built/admin/assets/activitypub/{site-x4_IjVJ0.js → site-_TgDd_EM.js} +81 -81
- package/core/built/admin/assets/activitypub/{step-1-DIDg4aHb.js → step-1-BfnNL2oF.js} +5 -5
- package/core/built/admin/assets/activitypub/{step-2-BfGe8BYt.js → step-2-DLndZkO-.js} +6 -6
- package/core/built/admin/assets/activitypub/{step-3-mE9spAqw.js → step-3-A-PZTtRE.js} +10 -10
- package/core/built/admin/assets/activitypub/{tabs-CyPRQQ1N.js → tabs-BUbz7tea.js} +321 -319
- package/core/built/admin/assets/activitypub/{topic-filter-DLHBZT93.js → topic-filter-BdX_xYYT.js} +3 -3
- package/core/built/admin/assets/activitypub/{use-activity-pub-queries-VzbN99u_.js → use-activity-pub-queries-EwuRemHh.js} +2 -2
- package/core/built/admin/assets/activitypub/{use-navigate-with-base-path-CAwXTmqH.js → use-navigate-with-base-path-cy4ACBc4.js} +92 -90
- package/core/built/admin/assets/activitypub/{use-reply-chain-data-BAB6fNGt.js → use-reply-chain-data-CyFcGfan.js} +5 -5
- package/core/built/admin/assets/activitypub/{x-C8tAnXly.js → x-7zk8mV2p.js} +2 -2
- package/core/built/admin/assets/audience-CkBb2LlO.js +1 -0
- package/core/built/admin/assets/{automated-emails-of1MvVsv.js → automated-emails-6t2bIRWf.js} +2 -2
- package/core/built/admin/assets/{automations-B7-y3dXt.js → automations-DhKeS_U5.js} +1 -1
- package/core/built/admin/assets/{automations-DRFjW5fw.js → automations-cMllU545.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-BbbjWdzs.js → bluesky-sharing-DPGpyD-G.js} +1 -1
- package/core/built/admin/assets/{box-BWbhXh21.js → box-By_EgGtm.js} +1 -1
- package/core/built/admin/assets/{breadcrumb-B-M9Ip_2.js → breadcrumb-DIfcF5ow.js} +1 -1
- package/core/built/admin/assets/build-CsiH26uS.js +1 -0
- package/core/built/admin/assets/{card-DGoDJzrn.js → card-aUHX0hyR.js} +1 -1
- package/core/built/admin/assets/{chart-BuRT2Y6p.js → chart-BnXyfH11.js} +1 -1
- package/core/built/admin/assets/{chart-helpers-xiSsW8r0.js → chart-helpers-DV0FkdcD.js} +1 -1
- package/core/built/admin/assets/{checkbox-EhJKHFdZ.js → checkbox-BIl077a0.js} +1 -1
- package/core/built/admin/assets/{chunk.361.3cdb584b973959e52ccc.js → chunk.332.05991a101d67f732dec3.js} +16 -12
- package/core/built/admin/assets/chunk.524.87b52f4fada529df2c4c.js +25 -0
- package/core/built/admin/assets/chunk.582.340bcca873e806cbd4bb.js +27 -0
- package/core/built/admin/assets/{code-editor-view-CZtJFGhR.js → code-editor-view-C0aWNwdl.js} +1 -1
- package/core/built/admin/assets/comments-qwhIjH3c.js +1 -0
- package/core/built/admin/assets/{container-BYz2Ed39.js → container-LiZf3XdY.js} +1 -1
- package/core/built/admin/assets/{data-list-16rkJYYi.js → data-list-CupF7bjM.js} +1 -1
- package/core/built/admin/assets/{date-range-select-CTlyo8zW.js → date-range-select-dXc-GsVe.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-qs2GfLvl.js → disabled-sources-indicator-DYAXW-6V.js} +1 -1
- package/core/built/admin/assets/dist-BUKmD51u.js +1 -0
- package/core/built/admin/assets/{dist-2Tnz2Yfq.js → dist-CNlMj7ML.js} +1 -1
- package/core/built/admin/assets/{dist-DGzPJ_bO.js → dist-Cb5RSHSg.js} +1 -1
- package/core/built/admin/assets/{dist-P_ohUlTi.js → dist-CqO2lWS0.js} +1 -1
- package/core/built/admin/assets/{domain-CPNXKVOV.js → domain-B5ogXUBa.js} +1 -1
- package/core/built/admin/assets/{dropzone-BmCXnw6J.js → dropzone-BnFG7dDP.js} +1 -1
- package/core/built/admin/assets/{edit-profile-SGrJve5M.js → edit-profile-B07jCZy9.js} +1 -1
- package/core/built/admin/assets/editor-DDYSOXGN.js +8 -0
- package/core/built/admin/assets/{empty-indicator-oX1u1Pkn.js → empty-indicator-WB1WgT9h.js} +1 -1
- package/core/built/admin/assets/{explore-CUYLv9G9.js → explore-Ca1y8HW_.js} +1 -1
- package/core/built/admin/assets/{feed-CPdJMttL.js → feed-x4iFMBP4.js} +1 -1
- package/core/built/admin/assets/field-ChCAabZg.js +1 -0
- package/core/built/admin/assets/filter-sources-LdtH1e-T.js +1 -0
- package/core/built/admin/assets/{filters-bXkkWro8.js → filters-CLehPo_D.js} +1 -1
- package/core/built/admin/assets/filters-CjVKRP8U.js +1 -0
- package/core/built/admin/assets/{ghost-05f9a7182ed56903dba05878a5b8422b.js → ghost-7f01131115186d1af84ead76e79646dd.js} +275 -255
- package/core/built/admin/assets/{ghost-3063eae29e6c9cc6d6e7131928a27da0.css → ghost-d38c30b866e59f2e06a357b5eb844fe5.css} +1 -1
- package/core/built/admin/assets/{ghost-dark-c8d0ab7f515b2010c173e23e9b32ea8a.css → ghost-dark-1bffd5099ba7a749303133efacf8480b.css} +1 -1
- package/core/built/admin/assets/gift-link-modal-Buqh2vXh.js +1 -0
- package/core/built/admin/assets/{gift-links-nBxJjZiQ.js → gift-links-CsUhefWG.js} +1 -1
- package/core/built/admin/assets/growth-DajOBdN0.js +1 -0
- package/core/built/admin/assets/growth-Dy2UOSay.js +3 -0
- package/core/built/admin/assets/{hooks-BApwwBE0.js → hooks-CixO01lL.js} +1 -1
- package/core/built/admin/assets/{inbox-Xx1CdnfO.js → inbox-BUOLDoji.js} +13 -13
- package/core/built/admin/assets/index-08n05-rc.js +67 -0
- package/core/built/admin/assets/index-DgUzgFvS.css +1 -0
- package/core/built/admin/assets/{inline-EMAm3okk.js → inline-Cw-gigDW.js} +1 -1
- package/core/built/admin/assets/input-group-uSK3XT7b.js +1 -0
- package/core/built/admin/assets/{input-surface-BpAjZXY5.js → input-surface-BdOe4KzO.js} +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +9132 -9087
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +121 -121
- package/core/built/admin/assets/koenig-lexical/style.css +1 -1
- package/core/built/admin/assets/{koenig-lexical-B61IALf5.js → koenig-lexical-BFc4yFed.js} +82 -82
- package/core/built/admin/assets/{kpi-card-DCdlyaSS.js → kpi-card-BN-E6WYX.js} +1 -1
- package/core/built/admin/assets/{kpi-card-BlZaG1Lo.js → kpi-card-CCt-qeWD.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-DpAuS1Nb.js → kpi-helpers-DlxTbTs4.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-Dme44bQS.js → kpi-tabs-BlcUttpn.js} +1 -1
- package/core/built/admin/assets/{label-dSs5aepE.js → label-AVrxwmsz.js} +1 -1
- package/core/built/admin/assets/label-picker-DVnpu_IF.js +1 -0
- package/core/built/admin/assets/labels-DAIqKed7.js +1 -0
- package/core/built/admin/assets/layout-B0YLh7R9.js +1 -0
- package/core/built/admin/assets/link-helpers-D9LsDOkG.js +1 -0
- package/core/built/admin/assets/{list-page-B6ANtzkd.js → list-page-CdsnQR9N.js} +1 -1
- package/core/built/admin/assets/member-custom-fields-C8NpZYza.js +1 -0
- package/core/built/admin/assets/member-detail-CIVAdKXf.js +1 -0
- package/core/built/admin/assets/members-oLPiSkXD.js +10 -0
- package/core/built/admin/assets/modals-CB7v-k3T.js +31 -0
- package/core/built/admin/assets/moderation-CQ3urTFE.js +1 -0
- package/core/built/admin/assets/newsletter-rbeDX9Wk.js +1 -0
- package/core/built/admin/assets/{newsletters-D1jbtV-h.js → newsletters-BXJbJgA-.js} +1 -1
- package/core/built/admin/assets/newsletters-vt3Ni1Ov.js +1 -0
- package/core/built/admin/assets/note-Dk877BTv.js +1 -0
- package/core/built/admin/assets/notifications-CPlLjPxO.js +1 -0
- package/core/built/admin/assets/{offers-CrY6nSRR.js → offers-DEniVRD5.js} +1 -1
- package/core/built/admin/assets/onboarding-route-DdStOtjy.js +1 -0
- package/core/built/admin/assets/overview--4V-kx9c.js +1 -0
- package/core/built/admin/assets/overview-DSLv30oz.js +1 -0
- package/core/built/admin/assets/{page-header-op0eN36I.js → page-header-pp4dWmOS.js} +1 -1
- package/core/built/admin/assets/{pages-BbNWFcGj.js → pages-B4goRPUn.js} +1 -1
- package/core/built/admin/assets/{post-analytics-WwRDEnrV.js → post-analytics-BMJrI81R.js} +1 -1
- package/core/built/admin/assets/post-analytics-header-pU2m9ZuV.js +1 -0
- package/core/built/admin/assets/post-analytics-provider-Btml5vJI.js +1 -0
- package/core/built/admin/assets/{post-share-modal-Dx-MEjhI.js → post-share-modal-AcUZkbfL.js} +1 -1
- package/core/built/admin/assets/{posts-EWpZIjkr.js → posts-DhXE7b6r.js} +1 -1
- package/core/built/admin/assets/preferences-Ru4CRPHC.js +5 -0
- package/core/built/admin/assets/profile-BNb1oj7D.js +1 -0
- package/core/built/admin/assets/{referrers-BCfFYSHc.js → referrers-pmJT4k3-.js} +1 -1
- package/core/built/admin/assets/{router-provider-C1R4AZ2o.js → router-provider-BmEZf6c6.js} +1 -1
- package/core/built/admin/assets/select-B0HteHrp.js +1 -0
- package/core/built/admin/assets/settings-B1C9Q3wo.js +4 -0
- package/core/built/admin/assets/{settings-H6HxFgfI.js → settings-BGgk7oCI.js} +1 -1
- package/core/built/admin/assets/{shade-app-B3heOAfz-CGXQwWeO.js → shade-app-B3heOAfz-BhQXxNkT.js} +1 -1
- package/core/built/admin/assets/{share-modal-BJCadJyp.js → share-modal-BeuydaBf.js} +1 -1
- package/core/built/admin/assets/social-urls-DA2lXrhW.js +8 -0
- package/core/built/admin/assets/{sort-button-DyYlfYp7.js → sort-button-Df4t2AL8.js} +1 -1
- package/core/built/admin/assets/{source-icon-_ubRDbg_.js → source-icon-ajoaKb41.js} +1 -1
- package/core/built/admin/assets/{stack-xiN0eQ6Z.js → stack-CIfOAAVf.js} +1 -1
- package/core/built/admin/assets/{stats-BQZMGMvn.js → stats-C_lfxZAw.js} +1 -1
- package/core/built/admin/assets/{stats-view-Doei-qCX.js → stats-view-Cxot1iD8.js} +1 -1
- package/core/built/admin/assets/{step-1-IKiLBPoC.js → step-1-ogAA4lDr.js} +1 -1
- package/core/built/admin/assets/{step-2-jJ41PsDN.js → step-2-C5Xini-7.js} +1 -1
- package/core/built/admin/assets/{step-3-CaqFAEqf.js → step-3-CZv1FV81.js} +2 -2
- package/core/built/admin/assets/{switch-t-WyENU2.js → switch-CyQcwMAv.js} +1 -1
- package/core/built/admin/assets/{table-DxUL78jU.js → table-DFj1wiYv.js} +1 -1
- package/core/built/admin/assets/tabs-D4y1gqUd.js +1 -0
- package/core/built/admin/assets/{tags-CNujGwkT.js → tags-Dmft-Y4O.js} +1 -1
- package/core/built/admin/assets/{text-DwtjwGkh.js → text-DU9Hv33_.js} +1 -1
- package/core/built/admin/assets/textarea-d9qEkMOP.js +1 -0
- package/core/built/admin/assets/{tiers-BgYLWRJv.js → tiers-DYsa4DCo.js} +1 -1
- package/core/built/admin/assets/{toggle-group-BeEpQ69p.js → toggle-group-R2QMFvXW.js} +1 -1
- package/core/built/admin/assets/{topic-filter-Bb5MOD-1.js → topic-filter-DGDxDalo.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-Cds6XDjg.js → use-analytics-data-C76v4LIp.js} +1 -1
- package/core/built/admin/assets/use-filter-params-WrdhAISv.js +1 -0
- package/core/built/admin/assets/{use-growth-stats-D8uf98-G.js → use-growth-stats-aNXJOmQr.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-CRtgyuXB.js → use-post-referrers-CG0SZioj.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-DbKYX50v.js → use-reply-chain-data-RLdPKRmS.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-DC01kpo0.js → use-simple-pagination-DgcF69KI.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-C7bQRJWw.js → use-tinybird-query-C1R_Kbmy.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-Az4tam7_.js → use-tinybird-token-C9fPjVM_.js} +1 -1
- package/core/built/admin/assets/{users-CcSCQFcQ.js → users-Dkv8NQZe.js} +1 -1
- package/core/built/admin/assets/{virtual-list-Dhuh9fGx.js → virtual-list-BmTUzeoF.js} +1 -1
- package/core/built/admin/assets/web-BqW0b0_C.js +1 -0
- package/core/built/admin/assets/web-Cy5GKMWM.js +2 -0
- package/core/built/admin/index.html +18 -18
- package/core/frontend/services/data/fetch-data.js +10 -5
- package/core/frontend/services/proxy.js +7 -8
- package/core/frontend/services/routing/api-adapter.js +115 -0
- package/core/frontend/services/routing/api-adapter.ts +157 -0
- package/core/frontend/services/routing/collection-router.js +5 -3
- package/core/frontend/services/routing/controllers/index.js +1 -1
- package/core/frontend/services/routing/controllers/rss.js +26 -31
- package/core/frontend/services/routing/controllers/rss.ts +66 -0
- package/core/frontend/services/routing/controllers/static.js +8 -7
- package/core/frontend/services/routing/parent-router.js +17 -4
- package/core/frontend/services/routing/permalink-adapter.js +26 -0
- package/core/frontend/services/routing/permalink-adapter.ts +23 -0
- package/core/frontend/services/routing/registry.js +59 -33
- package/core/frontend/services/routing/router-manager.js +14 -18
- package/core/frontend/services/routing/static-routes-router.js +11 -13
- package/core/frontend/services/routing/taxonomy-router.js +6 -2
- package/core/server/api/endpoints/automation-action-links.js +68 -0
- package/core/server/api/endpoints/automation-action-links.ts +46 -0
- package/core/server/api/endpoints/index.js +4 -0
- package/core/server/api/endpoints/settings.js +0 -2
- package/core/server/api/endpoints/utils/serializers/output/utils/url.js +11 -1
- package/core/server/data/migrations/versions/6.55/2026-07-27-11-25-44-add-to-hash-to-redirects.js +51 -0
- package/core/server/data/migrations/versions/6.55/2026-07-28-12-08-00-remove-routes-hash-from-settings.js +5 -0
- 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
- package/core/server/data/migrations/versions/6.56/2026-08-04-14-42-39-restrict-notification-add-permission-to-administrators.js +12 -0
- package/core/server/data/schema/commands.js +5 -0
- package/core/server/data/schema/default-settings/default-settings.json +0 -4
- package/core/server/data/schema/fixtures/fixtures.json +2 -2
- package/core/server/data/schema/schema.js +6 -1
- package/core/server/models/post.js +10 -1
- package/core/server/services/automations/automations-api.js +15 -0
- package/core/server/services/automations/automations-api.ts +19 -0
- package/core/server/services/automations/automations-repository.ts +20 -0
- package/core/server/services/automations/database-automations-repository.js +132 -30
- package/core/server/services/automations/database-automations-repository.ts +141 -12
- package/core/server/services/automations/database-date.js +10 -0
- package/core/server/services/automations/database-date.ts +5 -0
- package/core/server/services/automations/get-scheduler-idempotency-key.js +13 -0
- package/core/server/services/automations/get-scheduler-idempotency-key.ts +8 -0
- package/core/server/services/automations/poll.js +7 -2
- package/core/server/services/automations/poll.ts +10 -2
- package/core/server/services/automations/scheduler-poll-time.js +40 -0
- package/core/server/services/automations/scheduler-poll-time.ts +38 -0
- package/core/server/services/automations/service.js +19 -3
- package/core/server/services/automations/service.ts +20 -3
- package/core/server/services/comments/comments-service-emails.js +10 -7
- package/core/server/services/comments/comments-service.js +9 -4
- package/core/server/services/email-rendering/partials/email-wrapper.hbs +3 -1
- package/core/server/services/email-service/email-service-wrapper.js +6 -1
- package/core/server/services/email-service/email-templates/partials/styles.hbs +8 -0
- package/core/server/services/email-service/newsletter-email-event-storage.js +41 -10
- package/core/server/services/gift-links/CONTEXT.md +9 -0
- package/core/server/services/gifts/CONTEXT.md +55 -0
- package/core/server/services/gifts/README.md +28 -0
- package/core/server/services/gifts/gift-bookshelf-repository.js +65 -45
- package/core/server/services/gifts/gift-bookshelf-repository.ts +203 -74
- package/core/server/services/gifts/gift-checkout-adapter.js +59 -0
- package/core/server/services/gifts/gift-checkout-offer.js +74 -0
- package/core/server/services/gifts/gift-checkout-offer.ts +127 -0
- package/core/server/services/gifts/gift-codec.js +63 -0
- package/core/server/services/gifts/gift-codec.ts +62 -0
- package/core/server/services/gifts/gift-controller.js +6 -31
- package/core/server/services/gifts/gift-controller.ts +6 -69
- package/core/server/services/gifts/gift-email-service.ts +3 -2
- package/core/server/services/gifts/gift-schema.js +34 -0
- package/core/server/services/gifts/gift-schema.ts +39 -0
- package/core/server/services/gifts/gift-service-wrapper.js +13 -11
- package/core/server/services/gifts/gift-service.js +265 -52
- package/core/server/services/gifts/gift-service.ts +431 -97
- package/core/server/services/gifts/gift.ts +16 -39
- package/core/server/services/lib/dynamic-redirect-manager.js +12 -3
- package/core/server/services/link-redirection/link-redirect-repository.js +41 -4
- package/core/server/services/link-redirection/link-redirect.js +2 -0
- package/core/server/services/link-redirection/link-redirect.ts +3 -1
- package/core/server/services/link-redirection/link-redirects-service.js +38 -2
- package/core/server/services/link-tracking/click-event.js +3 -0
- package/core/server/services/link-tracking/index.js +38 -6
- package/core/server/services/link-tracking/link-click-repository.js +35 -9
- package/core/server/services/link-tracking/link-click-tracking-service.js +54 -4
- package/core/server/services/member-attribution/outbound-link-tagger.js +3 -1
- package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +20 -4
- package/core/server/services/member-welcome-emails/service.js +15 -3
- package/core/server/services/members/api.js +1 -2
- package/core/server/services/members/import-export/csv/formula.js +16 -0
- package/core/server/services/members/import-export/csv/formula.ts +14 -0
- package/core/server/services/members/import-export/csv/index.js +3 -1
- package/core/server/services/members/import-export/csv/index.ts +1 -0
- package/core/server/services/members/import-export/import/completion-email.js +22 -3
- package/core/server/services/members/import-export/import/completion-email.ts +29 -7
- package/core/server/services/members/import-export/import/importer.js +25 -4
- package/core/server/services/members/import-export/import/importer.ts +44 -5
- package/core/server/services/members/import-export/index.js +18 -1
- package/core/server/services/members/import-export/index.ts +32 -3
- package/core/server/services/members/members-api/controllers/router-controller.js +38 -36
- package/core/server/services/members/members-api/members-api.js +13 -7
- package/core/server/services/members/members-api/repositories/event-repository.js +4 -94
- package/core/server/services/members/members-api/services/member-bread-service.js +3 -3
- package/core/server/services/members/members-api/services/payments-service.js +6 -68
- package/core/server/services/members/service.js +8 -1
- package/core/server/services/members-custom-fields/schema.js +8 -1
- package/core/server/services/members-custom-fields/schema.ts +8 -0
- package/core/server/services/members-custom-fields/values-service.js +136 -8
- package/core/server/services/members-custom-fields/values-service.ts +152 -10
- package/core/server/services/mentions/service.js +5 -1
- package/core/server/services/oembed/oembed-service.js +44 -3
- package/core/server/services/posts/posts-service.js +11 -0
- package/core/server/services/route-settings/dynamic-routing-service.js +21 -52
- package/core/server/services/route-settings/index.js +1 -16
- package/core/server/services/route-settings/route-settings-parser.js +25 -25
- package/core/server/services/route-settings/route-settings-parser.ts +27 -27
- package/core/server/services/route-settings/validation-errors.js +68 -4
- package/core/server/services/route-settings/validation-errors.ts +79 -4
- package/core/server/services/settings/settings-service.js +0 -18
- package/core/server/services/stats/posts-stats-service.js +27 -23
- package/core/server/services/stripe/services/webhook/charge-refunded-event-service.js +1 -1
- package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +16 -4
- package/core/server/services/stripe/stripe-api.js +10 -0
- package/core/server/services/url/url-service-facade.js +1 -1
- package/core/server/services/url/url-service-facade.ts +8 -1
- package/core/server/web/api/endpoints/admin/routes.js +1 -0
- package/core/server/web/gift-preview/controller.js +10 -25
- package/core/shared/labs.js +2 -2
- package/package.json +8 -7
- package/pnpm-lock.yaml +28 -18
- package/pnpm-workspace.yaml +6 -4
- package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
- package/core/built/admin/assets/Growth-BTP1LthE.js +0 -3
- package/core/built/admin/assets/Newsletters-CSEX0myc.js +0 -1
- package/core/built/admin/assets/Overview-BjACEsJF.js +0 -1
- package/core/built/admin/assets/Web-CxxxjCcv.js +0 -2
- package/core/built/admin/assets/activitypub/layout-CziktJXo.js +0 -1
- package/core/built/admin/assets/audience-X_XsZivI.js +0 -1
- package/core/built/admin/assets/chunk.524.8365cd9280405c0f6216.js +0 -25
- package/core/built/admin/assets/chunk.582.61d078937e6102769f2c.js +0 -27
- package/core/built/admin/assets/comments-CvyXxOyN.js +0 -1
- package/core/built/admin/assets/constants-c-JNF9c2.js +0 -1
- package/core/built/admin/assets/dist-CjY-f5Z1.js +0 -1
- package/core/built/admin/assets/editor-BOG2YPUm.js +0 -8
- package/core/built/admin/assets/field-D0XISCK2.js +0 -1
- package/core/built/admin/assets/filter-sources-Bieps75g.js +0 -1
- package/core/built/admin/assets/filters-CuB-A7ma.js +0 -1
- package/core/built/admin/assets/gift-link-modal-su8MsRdg.js +0 -1
- package/core/built/admin/assets/growth-Ba6S5FQX.js +0 -1
- package/core/built/admin/assets/index-D8PbzuT9.css +0 -1
- package/core/built/admin/assets/index-ukW_0Uie.js +0 -67
- package/core/built/admin/assets/input-group-CGOXcy4l.js +0 -1
- package/core/built/admin/assets/label-picker-Uq91atex.js +0 -1
- package/core/built/admin/assets/labels-Cyayfd-e.js +0 -1
- package/core/built/admin/assets/layout-WUcz6tu1.js +0 -1
- package/core/built/admin/assets/link-helpers-tOlxmuH5.js +0 -1
- package/core/built/admin/assets/member-custom-fields-NCzrMuba.js +0 -1
- package/core/built/admin/assets/member-detail-DPWDtYnN.js +0 -1
- package/core/built/admin/assets/members-B9rO7qr4.js +0 -10
- package/core/built/admin/assets/modals-CxRaan1J.js +0 -31
- package/core/built/admin/assets/moderation-D8zon60E.js +0 -1
- package/core/built/admin/assets/multi-select-combobox-1wYm3nle.js +0 -1
- package/core/built/admin/assets/newsletter-DbYMRxRS.js +0 -1
- package/core/built/admin/assets/note-PiWPnnrh.js +0 -1
- package/core/built/admin/assets/notifications-DEgqqB9G.js +0 -1
- package/core/built/admin/assets/nql-BsgFMd93.js +0 -1
- package/core/built/admin/assets/onboarding-route-BzF6HeVn.js +0 -1
- package/core/built/admin/assets/overview-DixLeIqY.js +0 -1
- package/core/built/admin/assets/post-analytics-header-DsMoLx0R.js +0 -1
- package/core/built/admin/assets/post-analytics-provider-BJCPLMIb.js +0 -1
- package/core/built/admin/assets/preferences-D7-j6A4C.js +0 -5
- package/core/built/admin/assets/profile-Dl69QPp0.js +0 -1
- package/core/built/admin/assets/select-C1qDZnW4.js +0 -1
- package/core/built/admin/assets/settings-BbmC_Iqq.js +0 -4
- package/core/built/admin/assets/social-urls-BPL1nADp.js +0 -8
- package/core/built/admin/assets/tabs-BQcJQ1ke.js +0 -1
- package/core/built/admin/assets/textarea-Bjccvh_E.js +0 -1
- package/core/built/admin/assets/use-filter-params-DjXkhO54.js +0 -1
- package/core/built/admin/assets/web-LtHNiNBV.js +0 -1
- package/core/server/services/gifts/gift-repository.js +0 -2
- package/core/server/services/gifts/gift-repository.ts +0 -29
- package/core/server/services/route-settings/activation-bridge.js +0 -164
- package/core/server/services/route-settings/activation-bridge.ts +0 -210
- package/core/server/services/route-settings/default-settings-manager.js +0 -63
- package/core/server/services/route-settings/route-settings.js +0 -170
- package/core/server/services/route-settings/settings-loader.js +0 -60
- package/core/server/services/route-settings/settings-path-manager.js +0 -47
- package/core/server/services/route-settings/validate.js +0 -440
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const moment = require('moment-timezone');
|
|
2
|
+
const errors = require('@tryghost/errors');
|
|
2
3
|
const logging = require('@tryghost/logging');
|
|
3
4
|
const config = require('../../../shared/config');
|
|
4
5
|
|
|
@@ -177,11 +178,20 @@ class NewsletterEmailEventStorage {
|
|
|
177
178
|
|
|
178
179
|
async handleUnsubscribed(event) {
|
|
179
180
|
try {
|
|
180
|
-
|
|
181
|
-
const newsletters = await this.findNewslettersToKeep(event);
|
|
182
|
-
await this.#membersRepository.update({newsletters}, {id: event.memberId});
|
|
181
|
+
const result = await this.findNewslettersToKeep(event);
|
|
183
182
|
|
|
184
|
-
|
|
183
|
+
if (result.status === 'failed') {
|
|
184
|
+
// Leave Mailgun's suppression in place: these events are fetched
|
|
185
|
+
// once and never retried, so it is the only remaining protection.
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (result.status === 'ok') {
|
|
190
|
+
await this.#membersRepository.update({newsletters: result.newsletters}, {id: event.memberId});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Remove member from Mailgun's suppression list, only once the local
|
|
194
|
+
// record reflects the unsubscribe or there is no member left to protect
|
|
185
195
|
await this.#emailSuppressionList.removeUnsubscribe(event.email);
|
|
186
196
|
} catch (err) {
|
|
187
197
|
logging.error(err);
|
|
@@ -205,22 +215,43 @@ class NewsletterEmailEventStorage {
|
|
|
205
215
|
}
|
|
206
216
|
}
|
|
207
217
|
|
|
218
|
+
/**
|
|
219
|
+
* @typedef {{status: 'ok', newsletters: {id: string}[]}
|
|
220
|
+
* | {status: 'no-member'}
|
|
221
|
+
* | {status: 'failed'}} FindNewslettersToKeepResult
|
|
222
|
+
*/
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {import('./events/email-unsubscribed-event')} event
|
|
226
|
+
* @returns {Promise<FindNewslettersToKeepResult>}
|
|
227
|
+
*/
|
|
208
228
|
async findNewslettersToKeep(event) {
|
|
209
229
|
try {
|
|
210
|
-
const member = await this.#membersRepository.get({
|
|
230
|
+
const member = await this.#membersRepository.get({id: event.memberId}, {
|
|
211
231
|
withRelated: ['newsletters']
|
|
212
232
|
});
|
|
233
|
+
|
|
234
|
+
if (!member) {
|
|
235
|
+
return {status: 'no-member'};
|
|
236
|
+
}
|
|
237
|
+
|
|
213
238
|
const existingNewsletters = member.related('newsletters');
|
|
214
239
|
|
|
215
240
|
const email = await this.#models.Email.findOne({id: event.emailId});
|
|
216
241
|
const newsletterToRemove = email.get('newsletter_id');
|
|
217
242
|
|
|
218
|
-
return
|
|
219
|
-
|
|
220
|
-
|
|
243
|
+
return {
|
|
244
|
+
status: 'ok',
|
|
245
|
+
newsletters: existingNewsletters.models.filter(newsletter => newsletter.id !== newsletterToRemove).map((n) => {
|
|
246
|
+
return {id: n.id};
|
|
247
|
+
})
|
|
248
|
+
};
|
|
221
249
|
} catch (err) {
|
|
222
|
-
logging.error(
|
|
223
|
-
|
|
250
|
+
logging.error(new errors.InternalServerError({
|
|
251
|
+
message: `Could not resolve newsletters to keep for unsubscribe event (member ${event.memberId}, email ${event.emailId})`,
|
|
252
|
+
err
|
|
253
|
+
}));
|
|
254
|
+
return {status: 'failed'};
|
|
224
255
|
}
|
|
225
256
|
}
|
|
226
257
|
|
|
@@ -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
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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 {
|
|
3
|
-
import type {
|
|
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?:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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:
|
|
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):
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stripe and Bookshelf adapter used only by the gifts module.
|
|
5
|
+
*
|
|
6
|
+
* The module interface deals in stable member/customer IDs. Stripe customer
|
|
7
|
+
* objects and Bookshelf models stay on this side of the seam.
|
|
8
|
+
*/
|
|
9
|
+
module.exports = class GiftCheckoutAdapter {
|
|
10
|
+
constructor({StripeCustomerModel, getStripeApi}) {
|
|
11
|
+
this.StripeCustomerModel = StripeCustomerModel;
|
|
12
|
+
this.getStripeApi = getStripeApi;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async getCustomerId({memberId, email, name}) {
|
|
16
|
+
if (!memberId) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const rows = await this.StripeCustomerModel.where({
|
|
21
|
+
member_id: memberId
|
|
22
|
+
}).query().select('customer_id');
|
|
23
|
+
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
try {
|
|
26
|
+
const customer = await this.getStripeApi().getCustomer(row.customer_id);
|
|
27
|
+
if (!customer.deleted) {
|
|
28
|
+
return customer.id;
|
|
29
|
+
}
|
|
30
|
+
} catch (err) {
|
|
31
|
+
logging.warn(err);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const customer = await this.getStripeApi().createCustomer({
|
|
36
|
+
email,
|
|
37
|
+
name
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await this.StripeCustomerModel.add({
|
|
41
|
+
member_id: memberId,
|
|
42
|
+
customer_id: customer.id,
|
|
43
|
+
email: customer.email,
|
|
44
|
+
name: customer.name
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return customer.id;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async createSession(data) {
|
|
51
|
+
const {customerId, ...sessionData} = data;
|
|
52
|
+
const session = await this.getStripeApi().createGiftCheckoutSession({
|
|
53
|
+
...sessionData,
|
|
54
|
+
customer: customerId ? {id: customerId} : null
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return session.url;
|
|
58
|
+
}
|
|
59
|
+
};
|