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.
- package/components/tryghost-custom-field-types-0.0.0.tgz +0 -0
- package/components/tryghost-kg-default-cards-10.3.3.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/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-sjHLx5dy.js → account-migration-D3O_yVck.js} +1 -1
- package/core/built/admin/assets/activitypub/{account-migration-DoYXtnZ8.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-Bxeh6MKW.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-D8sFfdpC.js → dist-BEfX-0aW.js} +12 -12
- package/core/built/admin/assets/activitypub/{domain-CWueFW6q.js → domain-AaDlZlrA.js} +9 -9
- package/core/built/admin/assets/activitypub/{edit-profile-BGxPU7N5.js → edit-profile-DPi8Tl5M.js} +945 -945
- package/core/built/admin/assets/activitypub/{explore-DBK-u0Ch.js → explore-CxTzjILQ.js} +8 -8
- package/core/built/admin/assets/activitypub/{feed-Bcir5RQZ.js → feed-DPv5x2MX.js} +33 -33
- package/core/built/admin/assets/activitypub/{field-BV-CIT3d.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-yC0nfvzl.js → inbox-DSrF_i1O.js} +85 -85
- package/core/built/admin/assets/activitypub/{label-DTRS9TDk.js → label-CPjFBbo7.js} +3 -3
- package/core/built/admin/assets/activitypub/layout-CTgrezN7.js +1 -0
- package/core/built/admin/assets/activitypub/{moderation-Dwl9t0pb.js → moderation-BkDC1XVZ.js} +11 -11
- package/core/built/admin/assets/activitypub/{note-B-zKsZ7r.js → note-xEyH4xT-.js} +7 -7
- package/core/built/admin/assets/activitypub/{notifications-DjJZD54t.js → notifications-DClaL3ui.js} +9 -9
- package/core/built/admin/assets/activitypub/{preferences-CoU8vs6d.js → preferences-DpnOa-4Z.js} +20 -20
- package/core/built/admin/assets/activitypub/{profile-BAsaJekx.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-CLH3OVTP.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-DA6pNJ-Q.js → settings-CV2fFu6D.js} +13 -13
- package/core/built/admin/assets/activitypub/{site-CQD42Pw5.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-Cg0saow1.js → tabs-BUbz7tea.js} +317 -315
- 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-Br4aOWOh.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-DjEWNhp3.js → audience-CkBb2LlO.js} +1 -1
- package/core/built/admin/assets/{automated-emails-DRy7qY-J.js → automated-emails-6t2bIRWf.js} +1 -1
- package/core/built/admin/assets/{automations-Bx3VcFIL.js → automations-cMllU545.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-ClMd0-Ft.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-BD0U2ygg.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-fjQKRVyY.js → chart-helpers-DV0FkdcD.js} +1 -1
- package/core/built/admin/assets/{checkbox-CWa28lgO.js → checkbox-BIl077a0.js} +1 -1
- package/core/built/admin/assets/{chunk.481.6c7395a49484bb4583aa.js → chunk.332.05991a101d67f732dec3.js} +43 -42
- 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-CCr0rBWS.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-BFp58EMk.js → date-range-select-dXc-GsVe.js} +1 -1
- package/core/built/admin/assets/{disabled-sources-indicator-CUxMfiPa.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-B5ogXUBa.js +1 -0
- package/core/built/admin/assets/{dropzone-DlK-K2DQ.js → dropzone-BnFG7dDP.js} +1 -1
- package/core/built/admin/assets/{edit-profile-BKT-REJw.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-ClUpJZOp.js → explore-Ca1y8HW_.js} +1 -1
- package/core/built/admin/assets/{feed-DF92dUMP.js → feed-x4iFMBP4.js} +1 -1
- package/core/built/admin/assets/{field-Do7lieBA.js → field-ChCAabZg.js} +1 -1
- package/core/built/admin/assets/filter-sources-LdtH1e-T.js +1 -0
- package/core/built/admin/assets/filters-CLehPo_D.js +1 -0
- package/core/built/admin/assets/filters-CjVKRP8U.js +1 -0
- package/core/built/admin/assets/{ghost-c4057474943effc568c5659ccc4909d7.js → ghost-7f01131115186d1af84ead76e79646dd.js} +149 -129
- package/core/built/admin/assets/{ghost-a9c788f6f0b1ea6cf0f14ca4b13f4496.css → ghost-d38c30b866e59f2e06a357b5eb844fe5.css} +1 -1
- package/core/built/admin/assets/{ghost-dark-db36d795d21c61426837df40af270966.css → ghost-dark-1bffd5099ba7a749303133efacf8480b.css} +1 -1
- package/core/built/admin/assets/{gift-link-modal-f9VIMNDv.js → gift-link-modal-Buqh2vXh.js} +1 -1
- package/core/built/admin/assets/{growth-B3I6gNLm.js → growth-DajOBdN0.js} +1 -1
- package/core/built/admin/assets/{growth-CVw1My4S.js → growth-Dy2UOSay.js} +2 -2
- package/core/built/admin/assets/{inbox-CFBSu4Ed.js → inbox-BUOLDoji.js} +12 -12
- 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 +9535 -9527
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +124 -124
- package/core/built/admin/assets/{koenig-lexical-f8vHRYvz.js → koenig-lexical-BFc4yFed.js} +86 -86
- package/core/built/admin/assets/{kpi-card-DSap9rAy.js → kpi-card-BN-E6WYX.js} +1 -1
- package/core/built/admin/assets/{kpi-card-DU0rn9Cj.js → kpi-card-CCt-qeWD.js} +1 -1
- package/core/built/admin/assets/{kpi-helpers-Dz33AV6h.js → kpi-helpers-DlxTbTs4.js} +1 -1
- package/core/built/admin/assets/{kpi-tabs-BtTTrKvJ.js → kpi-tabs-BlcUttpn.js} +1 -1
- package/core/built/admin/assets/{label-DS8Mi6G5.js → label-AVrxwmsz.js} +1 -1
- package/core/built/admin/assets/{label-picker-1DvSuLA1.js → label-picker-DVnpu_IF.js} +1 -1
- 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-CQpVFJu4.js → members-oLPiSkXD.js} +2 -2
- package/core/built/admin/assets/modals-CB7v-k3T.js +31 -0
- package/core/built/admin/assets/{moderation-DySbgyUS.js → moderation-CQ3urTFE.js} +1 -1
- package/core/built/admin/assets/newsletter-rbeDX9Wk.js +1 -0
- package/core/built/admin/assets/newsletters-vt3Ni1Ov.js +1 -0
- package/core/built/admin/assets/{note-C8kxrz6w.js → note-Dk877BTv.js} +1 -1
- package/core/built/admin/assets/{notifications-CgtSfX7t.js → notifications-CPlLjPxO.js} +1 -1
- package/core/built/admin/assets/{onboarding-route-4qtziA9d.js → onboarding-route-DdStOtjy.js} +1 -1
- package/core/built/admin/assets/{overview-Bf2sbznq.js → overview--4V-kx9c.js} +1 -1
- package/core/built/admin/assets/overview-DSLv30oz.js +1 -0
- package/core/built/admin/assets/{page-header-B3Wb9Jqv.js → page-header-pp4dWmOS.js} +1 -1
- package/core/built/admin/assets/{post-analytics-We-ko5Cb.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-B6UynTGb.js → post-analytics-provider-Btml5vJI.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-DFfO93_0.js → post-share-modal-AcUZkbfL.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/{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-WXNeWyU1.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-DsgW3wbg.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-Bb_FCl7e.js → sort-button-Df4t2AL8.js} +1 -1
- package/core/built/admin/assets/{source-icon-BeaRxUn9.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-Bjj_kmJ1.js → stats-C_lfxZAw.js} +1 -1
- package/core/built/admin/assets/{stats-view-BxPbW6K1.js → stats-view-Cxot1iD8.js} +1 -1
- package/core/built/admin/assets/{step-1-B1e3-1F8.js → step-1-ogAA4lDr.js} +1 -1
- package/core/built/admin/assets/{step-2-C5WaeBJo.js → step-2-C5Xini-7.js} +1 -1
- package/core/built/admin/assets/{step-3-B46iVPwu.js → step-3-CZv1FV81.js} +2 -2
- package/core/built/admin/assets/{switch-z5Y6Uzt6.js → switch-CyQcwMAv.js} +1 -1
- package/core/built/admin/assets/{table-CB-4_HBJ.js → table-DFj1wiYv.js} +1 -1
- package/core/built/admin/assets/{tabs-CCEgKYzq.js → tabs-D4y1gqUd.js} +1 -1
- package/core/built/admin/assets/{tags-Cg2VJGmG.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/{toggle-group-b3IlxvhV.js → toggle-group-R2QMFvXW.js} +1 -1
- package/core/built/admin/assets/{topic-filter-DAFT87tQ.js → topic-filter-DGDxDalo.js} +1 -1
- package/core/built/admin/assets/{use-analytics-data-S0FTd43s.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-CIaiHOVb.js → use-growth-stats-aNXJOmQr.js} +1 -1
- package/core/built/admin/assets/{use-post-referrers-Cq2vBPrA.js → use-post-referrers-CG0SZioj.js} +1 -1
- package/core/built/admin/assets/{use-reply-chain-data-DBDRtGu-.js → use-reply-chain-data-RLdPKRmS.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-CjYNxdwe.js → use-simple-pagination-DgcF69KI.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-query-falHAtuK.js → use-tinybird-query-C1R_Kbmy.js} +1 -1
- package/core/built/admin/assets/{use-tinybird-token-D7-8ND1y.js → use-tinybird-token-C9fPjVM_.js} +1 -1
- package/core/built/admin/assets/{virtual-list-CvnC9zF2.js → virtual-list-BmTUzeoF.js} +1 -1
- package/core/built/admin/assets/{web-B_5GZRvH.js → web-BqW0b0_C.js} +1 -1
- package/core/built/admin/assets/web-Cy5GKMWM.js +2 -0
- package/core/built/admin/index.html +16 -16
- package/core/frontend/services/data/fetch-data.js +10 -5
- 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 +9 -13
- package/core/frontend/services/routing/static-routes-router.js +4 -4
- package/core/frontend/services/routing/taxonomy-router.js +6 -2
- 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/fixtures/fixtures.json +2 -2
- package/core/server/data/schema/schema.js +1 -0
- package/core/server/models/post.js +10 -1
- package/core/server/services/automations/automations-repository.ts +3 -1
- package/core/server/services/automations/database-automations-repository.js +33 -14
- package/core/server/services/automations/database-automations-repository.ts +41 -15
- 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 +3 -1
- package/core/server/services/automations/poll.ts +4 -1
- package/core/server/services/automations/service.js +5 -1
- package/core/server/services/automations/service.ts +5 -1
- 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/link-tracking/link-click-repository.js +6 -1
- package/core/server/services/link-tracking/link-click-tracking-service.js +12 -3
- package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +4 -3
- package/core/server/services/member-welcome-emails/service.js +7 -3
- package/core/server/services/members/api.js +1 -2
- package/core/server/services/members/import-export/import/importer.ts +3 -2
- package/core/server/services/members/import-export/index.js +10 -2
- package/core/server/services/members/import-export/index.ts +14 -4
- 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-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 +118 -2
- package/core/server/services/members-custom-fields/values-service.ts +134 -4
- 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 +1 -3
- 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/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/web/gift-preview/controller.js +10 -25
- package/core/shared/labs.js +2 -2
- package/package.json +10 -9
- package/pnpm-lock.yaml +28 -18
- package/pnpm-workspace.yaml +5 -4
- package/components/tryghost-kg-default-nodes-2.1.3.tgz +0 -0
- package/core/built/admin/assets/activitypub/layout-qx-bUL1k.js +0 -1
- package/core/built/admin/assets/chunk.524.cffd399783a519e404a9.js +0 -25
- package/core/built/admin/assets/chunk.582.eebf0d0b2d202bb746a5.js +0 -27
- package/core/built/admin/assets/comments-DyUSMUdj.js +0 -1
- package/core/built/admin/assets/dist-CjY-f5Z1.js +0 -1
- package/core/built/admin/assets/domain-C0bV2UV8.js +0 -1
- package/core/built/admin/assets/editor-Dp865O-4.js +0 -8
- package/core/built/admin/assets/filter-sources-BV2rnpfA.js +0 -1
- package/core/built/admin/assets/filters-CUXJ2Nm9.js +0 -1
- package/core/built/admin/assets/filters-DfbIWc_l.js +0 -1
- package/core/built/admin/assets/index-9Cw8z3rA.css +0 -1
- package/core/built/admin/assets/index-BGG7NckO.js +0 -67
- package/core/built/admin/assets/input-group-BSgG_lVw.js +0 -1
- package/core/built/admin/assets/labels-CDSUt4J_.js +0 -1
- package/core/built/admin/assets/layout-yQEi2qDM.js +0 -1
- package/core/built/admin/assets/link-helpers-7EYqvvHs.js +0 -1
- package/core/built/admin/assets/member-custom-fields-yOMHyAg9.js +0 -1
- package/core/built/admin/assets/member-detail-BAq7gDaa.js +0 -1
- package/core/built/admin/assets/modals-ReORvWnu.js +0 -31
- package/core/built/admin/assets/newsletter-CJQvtHqN.js +0 -1
- package/core/built/admin/assets/newsletters-B3QQXfGT.js +0 -1
- package/core/built/admin/assets/nql-BsgFMd93.js +0 -1
- package/core/built/admin/assets/overview-CBfePxcr.js +0 -1
- package/core/built/admin/assets/post-analytics-header-Bmc9su6q.js +0 -1
- package/core/built/admin/assets/preferences-DJPfHEGu.js +0 -5
- package/core/built/admin/assets/profile-DqBQp-gT.js +0 -1
- package/core/built/admin/assets/select-C5_xEt5b.js +0 -1
- package/core/built/admin/assets/settings-BwiVIXau.js +0 -4
- package/core/built/admin/assets/social-urls-BQPFEk1_.js +0 -8
- package/core/built/admin/assets/textarea-Bjccvh_E.js +0 -1
- package/core/built/admin/assets/use-filter-params-Gpoq2E1L.js +0 -1
- package/core/built/admin/assets/web-jFYWCKDn.js +0 -2
- 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 -174
- package/core/server/services/route-settings/activation-bridge.ts +0 -250
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import errors from '@tryghost/errors';
|
|
3
3
|
import logging from '@tryghost/logging';
|
|
4
|
+
import type {Knex} from 'knex';
|
|
5
|
+
import {z} from 'zod';
|
|
4
6
|
import {Gift} from './gift';
|
|
5
|
-
import type {GiftRepository} from './gift-repository';
|
|
7
|
+
import type {GiftEventBrowseOptions, GiftEventPage, GiftRepository} from './gift-bookshelf-repository';
|
|
6
8
|
import type {GiftReminderScheduler} from './gift-reminder-scheduler';
|
|
9
|
+
import {GiftCadenceSchema, type GiftCadence} from './gift-schema';
|
|
7
10
|
import tpl from '@tryghost/tpl';
|
|
8
11
|
import {GIFT_REMINDER_FLOOR_DAYS, GIFT_REMINDER_LEAD_DAYS} from './constants';
|
|
12
|
+
import {
|
|
13
|
+
resolveGiftDuration,
|
|
14
|
+
validateGiftCheckoutOffer,
|
|
15
|
+
type GiftCheckoutTier,
|
|
16
|
+
type ResolvedGiftDuration
|
|
17
|
+
} from './gift-checkout-offer';
|
|
9
18
|
|
|
10
19
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
11
20
|
const GIFT_REMINDER_LEAD_MS = GIFT_REMINDER_LEAD_DAYS * MS_PER_DAY;
|
|
@@ -45,12 +54,14 @@ interface MemberRepository {
|
|
|
45
54
|
): Promise<unknown>;
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
type Tier = {
|
|
57
|
+
type Tier = Omit<GiftCheckoutTier, 'currency'> & {
|
|
58
|
+
id: string | {
|
|
59
|
+
toHexString(): string;
|
|
60
|
+
};
|
|
49
61
|
name: string;
|
|
50
|
-
currency: string
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
toJSON?: () => {
|
|
62
|
+
currency: string;
|
|
63
|
+
getPrice(cadence: GiftCadence): number;
|
|
64
|
+
toJSON(): {
|
|
54
65
|
id: string;
|
|
55
66
|
name: string;
|
|
56
67
|
description: string | null;
|
|
@@ -69,7 +80,7 @@ interface GiftEmailService {
|
|
|
69
80
|
buyerEmail: string;
|
|
70
81
|
token: string;
|
|
71
82
|
tierName: string;
|
|
72
|
-
cadence:
|
|
83
|
+
cadence: GiftCadence;
|
|
73
84
|
duration: number;
|
|
74
85
|
expiresAt: Date;
|
|
75
86
|
}): Promise<void>;
|
|
@@ -89,7 +100,7 @@ interface StaffServiceEmails {
|
|
|
89
100
|
amount: number;
|
|
90
101
|
currency: string;
|
|
91
102
|
tierName: string;
|
|
92
|
-
cadence:
|
|
103
|
+
cadence: GiftCadence;
|
|
93
104
|
duration: number;
|
|
94
105
|
}): Promise<void>;
|
|
95
106
|
notifyGiftSubscriptionStarted(data: {
|
|
@@ -97,24 +108,26 @@ interface StaffServiceEmails {
|
|
|
97
108
|
memberEmail: string;
|
|
98
109
|
memberName: string | null;
|
|
99
110
|
tierName: string;
|
|
100
|
-
cadence:
|
|
111
|
+
cadence: GiftCadence;
|
|
101
112
|
duration: number;
|
|
102
113
|
buyerEmail: string;
|
|
103
114
|
}): Promise<void>;
|
|
104
115
|
}
|
|
105
116
|
|
|
106
|
-
|
|
107
|
-
token: string
|
|
108
|
-
buyerEmail: string
|
|
109
|
-
stripeCustomerId: string
|
|
110
|
-
tierId: string
|
|
111
|
-
cadence:
|
|
112
|
-
duration:
|
|
113
|
-
currency: string
|
|
114
|
-
amount: number
|
|
115
|
-
stripeCheckoutSessionId: string
|
|
116
|
-
stripePaymentIntentId: string
|
|
117
|
-
}
|
|
117
|
+
const GiftPurchaseDataSchema = z.object({
|
|
118
|
+
token: z.string().min(1),
|
|
119
|
+
buyerEmail: z.string().min(1),
|
|
120
|
+
stripeCustomerId: z.string().min(1).nullable(),
|
|
121
|
+
tierId: z.string().min(1),
|
|
122
|
+
cadence: GiftCadenceSchema,
|
|
123
|
+
duration: z.number().int().positive(),
|
|
124
|
+
currency: z.string().min(1),
|
|
125
|
+
amount: z.number().int().nonnegative(),
|
|
126
|
+
stripeCheckoutSessionId: z.string().min(1),
|
|
127
|
+
stripePaymentIntentId: z.string().min(1)
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export type GiftPurchaseData = z.infer<typeof GiftPurchaseDataSchema>;
|
|
118
131
|
|
|
119
132
|
interface GiftServiceDeps {
|
|
120
133
|
giftRepository: GiftRepository;
|
|
@@ -123,6 +136,16 @@ interface GiftServiceDeps {
|
|
|
123
136
|
giftEmailService: GiftEmailService;
|
|
124
137
|
staffServiceEmails: StaffServiceEmails;
|
|
125
138
|
giftReminderScheduler: Pick<GiftReminderScheduler, 'scheduleFor'>;
|
|
139
|
+
checkoutAdapter: {
|
|
140
|
+
getCustomerId(buyer: GiftCheckoutBuyer): Promise<string | null>;
|
|
141
|
+
createSession(data: GiftCheckoutSession): Promise<string>;
|
|
142
|
+
};
|
|
143
|
+
labsService: {
|
|
144
|
+
isSet(flag: string): boolean;
|
|
145
|
+
};
|
|
146
|
+
settingsCache: {
|
|
147
|
+
get(key: string): unknown;
|
|
148
|
+
};
|
|
126
149
|
}
|
|
127
150
|
|
|
128
151
|
interface ReminderSend {
|
|
@@ -131,6 +154,74 @@ interface ReminderSend {
|
|
|
131
154
|
consumesAt: Date;
|
|
132
155
|
}
|
|
133
156
|
|
|
157
|
+
export interface GiftCheckoutBuyer {
|
|
158
|
+
memberId: string | null;
|
|
159
|
+
email: string | null;
|
|
160
|
+
name: string | null;
|
|
161
|
+
isAuthenticated: boolean;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface StartGiftCheckoutInput {
|
|
165
|
+
tierId?: string;
|
|
166
|
+
offerId?: string;
|
|
167
|
+
cadence?: string;
|
|
168
|
+
duration?: number;
|
|
169
|
+
metadata: Record<string, unknown>;
|
|
170
|
+
successUrl: string;
|
|
171
|
+
cancelUrl?: string;
|
|
172
|
+
buyer: GiftCheckoutBuyer;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface GiftCheckoutSession {
|
|
176
|
+
amount: number;
|
|
177
|
+
currency: string;
|
|
178
|
+
tierName: string;
|
|
179
|
+
cadence: GiftCadence;
|
|
180
|
+
duration: number;
|
|
181
|
+
metadata: Record<string, unknown>;
|
|
182
|
+
successUrl: string;
|
|
183
|
+
cancelUrl?: string;
|
|
184
|
+
customerId: string | null;
|
|
185
|
+
customerEmail: string | null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface GiftRedemption {
|
|
189
|
+
token: string;
|
|
190
|
+
cadence: GiftCadence;
|
|
191
|
+
duration: number;
|
|
192
|
+
currency: string;
|
|
193
|
+
amount: number;
|
|
194
|
+
expires_at: Date;
|
|
195
|
+
consumes_at: Date | null;
|
|
196
|
+
tier: {
|
|
197
|
+
id: string;
|
|
198
|
+
name: string;
|
|
199
|
+
description: string | null;
|
|
200
|
+
benefits: string[];
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface GiftContinuation {
|
|
205
|
+
tierId: string;
|
|
206
|
+
cadence: GiftCadence;
|
|
207
|
+
trialDays: number | null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface GiftMemberPresentation {
|
|
211
|
+
cadence: GiftCadence;
|
|
212
|
+
currency: string;
|
|
213
|
+
amount: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface GiftPreview {
|
|
217
|
+
cadence: GiftCadence;
|
|
218
|
+
duration: number;
|
|
219
|
+
tier: {
|
|
220
|
+
id: string;
|
|
221
|
+
name: string;
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
134
225
|
export class GiftService {
|
|
135
226
|
private readonly deps: GiftServiceDeps;
|
|
136
227
|
|
|
@@ -138,7 +229,118 @@ export class GiftService {
|
|
|
138
229
|
this.deps = deps;
|
|
139
230
|
}
|
|
140
231
|
|
|
141
|
-
|
|
232
|
+
async startCheckout(input: StartGiftCheckoutInput): Promise<{url: string}> {
|
|
233
|
+
if (input.offerId) {
|
|
234
|
+
throw new errors.BadRequestError({
|
|
235
|
+
message: 'Bad Request.',
|
|
236
|
+
context: 'Offers cannot be applied to gift subscriptions'
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!input.tierId) {
|
|
241
|
+
throw new errors.BadRequestError({
|
|
242
|
+
message: 'Bad Request.',
|
|
243
|
+
context: 'Expected offerId or tierId, received none'
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let resolvedDuration: ResolvedGiftDuration | null = null;
|
|
248
|
+
let cadence: GiftCadence;
|
|
249
|
+
|
|
250
|
+
if (this.deps.labsService.isSet('giftSubCustomization')) {
|
|
251
|
+
resolvedDuration = resolveGiftDuration(input);
|
|
252
|
+
cadence = resolvedDuration.cadence;
|
|
253
|
+
} else {
|
|
254
|
+
if (input.cadence !== 'month' && input.cadence !== 'year') {
|
|
255
|
+
const receivedCadence = input.cadence ? `"${input.cadence}"` : input.cadence;
|
|
256
|
+
|
|
257
|
+
throw new errors.BadRequestError({
|
|
258
|
+
message: 'Bad Request.',
|
|
259
|
+
context: `Expected cadence to be "month" or "year", received ${receivedCadence}`
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
cadence = input.cadence;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let tier: Tier | null;
|
|
267
|
+
try {
|
|
268
|
+
tier = await this.deps.tiersService.api.read(input.tierId);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
logging.error(err);
|
|
271
|
+
tier = null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!tier) {
|
|
275
|
+
throw new errors.BadRequestError({
|
|
276
|
+
message: 'This tier does not exist.',
|
|
277
|
+
context: `Tier with id "${input.tierId}" not found`
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (tier.status === 'archived') {
|
|
282
|
+
throw new errors.NoPermissionError({
|
|
283
|
+
message: 'This tier is archived.'
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let duration = 1;
|
|
288
|
+
let totalMonths: number | undefined;
|
|
289
|
+
let amount = tier.getPrice(cadence);
|
|
290
|
+
|
|
291
|
+
if (resolvedDuration) {
|
|
292
|
+
const plan = validateGiftCheckoutOffer({
|
|
293
|
+
tier,
|
|
294
|
+
portalPlans: this.deps.settingsCache.get('portal_plans'),
|
|
295
|
+
offer: resolvedDuration
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
cadence = plan.cadence;
|
|
299
|
+
duration = plan.duration;
|
|
300
|
+
totalMonths = plan.totalMonths;
|
|
301
|
+
amount = plan.amount;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const tierId = typeof tier.id === 'string' ? tier.id : tier.id.toHexString();
|
|
305
|
+
const token = this.generateToken();
|
|
306
|
+
const successUrl = new URL(input.successUrl);
|
|
307
|
+
|
|
308
|
+
successUrl.searchParams.set('stripe', 'gift-purchase-success');
|
|
309
|
+
successUrl.searchParams.set('gift_token', token);
|
|
310
|
+
successUrl.searchParams.set('gift_tier', tierId);
|
|
311
|
+
successUrl.searchParams.set('gift_cadence', cadence);
|
|
312
|
+
if (totalMonths !== undefined) {
|
|
313
|
+
successUrl.searchParams.set('gift_duration', String(totalMonths));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const customerId = input.buyer.isAuthenticated
|
|
317
|
+
? await this.deps.checkoutAdapter.getCustomerId(input.buyer)
|
|
318
|
+
: null;
|
|
319
|
+
|
|
320
|
+
const url = await this.deps.checkoutAdapter.createSession({
|
|
321
|
+
amount,
|
|
322
|
+
currency: tier.currency.toLowerCase(),
|
|
323
|
+
tierName: tier.name,
|
|
324
|
+
cadence,
|
|
325
|
+
duration,
|
|
326
|
+
metadata: {
|
|
327
|
+
...input.metadata,
|
|
328
|
+
ghost_gift: 'true',
|
|
329
|
+
gift_token: token,
|
|
330
|
+
tier_id: tierId,
|
|
331
|
+
cadence,
|
|
332
|
+
duration: String(duration)
|
|
333
|
+
},
|
|
334
|
+
successUrl: successUrl.toString(),
|
|
335
|
+
cancelUrl: input.cancelUrl,
|
|
336
|
+
customerId,
|
|
337
|
+
customerEmail: customerId ? null : input.buyer.email
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return {url};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private generateToken(): string {
|
|
142
344
|
/**
|
|
143
345
|
* Combinations: 62^12 ≈ 3.23 × 10^21 (~3.23 sextillion)
|
|
144
346
|
* Entropy: 12 × log2(62) ≈ 71.45 bits
|
|
@@ -153,12 +355,17 @@ export class GiftService {
|
|
|
153
355
|
return token;
|
|
154
356
|
}
|
|
155
357
|
|
|
156
|
-
async
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
throw new errors.ValidationError({
|
|
358
|
+
async completePurchase(input: GiftPurchaseData): Promise<boolean> {
|
|
359
|
+
const parsed = GiftPurchaseDataSchema.safeParse(input);
|
|
360
|
+
if (!parsed.success) {
|
|
361
|
+
const issue = parsed.error.issues[0];
|
|
362
|
+
throw new errors.ValidationError({
|
|
363
|
+
message: 'Invalid gift purchase data.',
|
|
364
|
+
property: issue.path.join('.'),
|
|
365
|
+
context: issue.message
|
|
366
|
+
});
|
|
161
367
|
}
|
|
368
|
+
const data = parsed.data;
|
|
162
369
|
|
|
163
370
|
if (await this.deps.giftRepository.existsByCheckoutSessionId(data.stripeCheckoutSessionId)) {
|
|
164
371
|
return false;
|
|
@@ -174,7 +381,7 @@ export class GiftService {
|
|
|
174
381
|
buyerMemberId: member?.id ?? null,
|
|
175
382
|
tierId: data.tierId,
|
|
176
383
|
cadence: data.cadence,
|
|
177
|
-
duration,
|
|
384
|
+
duration: data.duration,
|
|
178
385
|
currency: data.currency,
|
|
179
386
|
amount: data.amount,
|
|
180
387
|
stripeCheckoutSessionId: data.stripeCheckoutSessionId,
|
|
@@ -198,7 +405,7 @@ export class GiftService {
|
|
|
198
405
|
currency: data.currency,
|
|
199
406
|
tierName: tier.name,
|
|
200
407
|
cadence: data.cadence,
|
|
201
|
-
duration
|
|
408
|
+
duration: data.duration
|
|
202
409
|
});
|
|
203
410
|
} catch (err) {
|
|
204
411
|
logging.error('Failed to notify staff of gift purchase', err);
|
|
@@ -210,7 +417,7 @@ export class GiftService {
|
|
|
210
417
|
token: data.token,
|
|
211
418
|
tierName: tier.name,
|
|
212
419
|
cadence: data.cadence,
|
|
213
|
-
duration,
|
|
420
|
+
duration: data.duration,
|
|
214
421
|
expiresAt: gift.expiresAt
|
|
215
422
|
});
|
|
216
423
|
} catch (err) {
|
|
@@ -220,11 +427,7 @@ export class GiftService {
|
|
|
220
427
|
return true;
|
|
221
428
|
}
|
|
222
429
|
|
|
223
|
-
|
|
224
|
-
return this.deps.giftRepository.getByToken(token);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async assertRedeemable(gift: Gift, memberStatus: string | null): Promise<Gift> {
|
|
430
|
+
private assertRedeemable(gift: Gift, memberStatus: string | null): Gift {
|
|
228
431
|
const redeemableCheck = gift.checkRedeemable(memberStatus);
|
|
229
432
|
|
|
230
433
|
if (!redeemableCheck.redeemable) {
|
|
@@ -267,61 +470,36 @@ export class GiftService {
|
|
|
267
470
|
return gift;
|
|
268
471
|
}
|
|
269
472
|
|
|
270
|
-
async getRedeemable(token: string
|
|
271
|
-
const gift = await this.deps.giftRepository.getByToken(token);
|
|
473
|
+
async getRedeemable(input: {token: string; memberStatus: string | null}): Promise<GiftRedemption> {
|
|
474
|
+
const gift = await this.deps.giftRepository.getByToken(input.token);
|
|
272
475
|
|
|
273
476
|
if (!gift) {
|
|
274
477
|
throw new errors.NotFoundError({message: tpl(errorMessages.giftNotFound)});
|
|
275
478
|
}
|
|
276
479
|
|
|
277
|
-
|
|
480
|
+
this.assertRedeemable(gift, input.memberStatus);
|
|
278
481
|
|
|
279
|
-
return gift;
|
|
482
|
+
return this.serializeRedemption(gift);
|
|
280
483
|
}
|
|
281
484
|
|
|
282
|
-
async redeem(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (options.newMember) {
|
|
295
|
-
await this.assertRedeemable(gift, null);
|
|
296
|
-
} else {
|
|
297
|
-
await this.assertRedeemable(gift, member.get('status'));
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const redeemed = gift.redeem({memberId});
|
|
301
|
-
|
|
302
|
-
await this.deps.memberRepository.update({
|
|
303
|
-
products: [{
|
|
304
|
-
id: redeemed.tierId,
|
|
305
|
-
expiry_at: redeemed.consumesAt
|
|
306
|
-
}],
|
|
307
|
-
status: 'gift'
|
|
308
|
-
}, {id: memberId, transacting});
|
|
309
|
-
|
|
310
|
-
await this.deps.giftRepository.update(redeemed, {transacting});
|
|
311
|
-
|
|
312
|
-
// Gift members receive the paid welcome email, as they receive access to paid content
|
|
313
|
-
await this.deps.memberRepository.triggerMemberSignupAutomation(
|
|
314
|
-
memberId,
|
|
315
|
-
member.get('email'),
|
|
316
|
-
'paid',
|
|
317
|
-
{transacting}
|
|
318
|
-
);
|
|
485
|
+
async redeem(input: {
|
|
486
|
+
token: string;
|
|
487
|
+
memberId: string;
|
|
488
|
+
transacting?: Knex.Transaction;
|
|
489
|
+
newMember?: boolean;
|
|
490
|
+
}): Promise<GiftRedemption> {
|
|
491
|
+
const run = async (transacting: Knex.Transaction) => {
|
|
492
|
+
const {redeemed, member} = await this.redeemGift(input.token, input.memberId, {
|
|
493
|
+
transacting,
|
|
494
|
+
newMember: input.newMember
|
|
495
|
+
});
|
|
496
|
+
const redemption = await this.serializeRedemption(redeemed);
|
|
319
497
|
|
|
320
|
-
return {redeemed, member};
|
|
498
|
+
return {redeemed, member, redemption};
|
|
321
499
|
};
|
|
322
500
|
|
|
323
|
-
const {redeemed, member} =
|
|
324
|
-
? await run(
|
|
501
|
+
const {redeemed, member, redemption} = input.transacting
|
|
502
|
+
? await run(input.transacting)
|
|
325
503
|
: await this.deps.giftRepository.transaction(run);
|
|
326
504
|
|
|
327
505
|
const notify = async () => {
|
|
@@ -348,31 +526,72 @@ export class GiftService {
|
|
|
348
526
|
await this.deps.giftReminderScheduler.scheduleFor(redeemed);
|
|
349
527
|
};
|
|
350
528
|
|
|
351
|
-
if (
|
|
529
|
+
if (input.transacting) {
|
|
352
530
|
// Only notify once the transaction has finished
|
|
353
|
-
|
|
531
|
+
input.transacting.executionPromise.then(notify, () => {});
|
|
354
532
|
} else {
|
|
355
533
|
await notify();
|
|
356
534
|
}
|
|
357
535
|
|
|
358
|
-
return
|
|
536
|
+
return redemption;
|
|
359
537
|
}
|
|
360
538
|
|
|
361
|
-
async
|
|
539
|
+
private async redeemGift(token: string, memberId: string, options: {transacting: Knex.Transaction; newMember?: boolean}): Promise<{redeemed: Gift; member: MemberModel}> {
|
|
540
|
+
const {transacting} = options;
|
|
541
|
+
const member = await this.deps.memberRepository.get({id: memberId}, {transacting, forUpdate: true});
|
|
542
|
+
if (!member) {
|
|
543
|
+
throw new errors.NotFoundError({message: `Member not found: ${memberId}`});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const gift = await this.deps.giftRepository.getByToken(token, {transacting, forUpdate: true});
|
|
547
|
+
if (!gift) {
|
|
548
|
+
throw new errors.NotFoundError({message: tpl(errorMessages.giftNotFound)});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (options.newMember) {
|
|
552
|
+
this.assertRedeemable(gift, null);
|
|
553
|
+
} else {
|
|
554
|
+
this.assertRedeemable(gift, member.get('status'));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const redeemed = gift.redeem({memberId});
|
|
558
|
+
|
|
559
|
+
await this.deps.memberRepository.update({
|
|
560
|
+
products: [{
|
|
561
|
+
id: redeemed.tierId,
|
|
562
|
+
expiry_at: redeemed.consumesAt
|
|
563
|
+
}],
|
|
564
|
+
status: 'gift'
|
|
565
|
+
}, {id: memberId, transacting});
|
|
566
|
+
|
|
567
|
+
await this.deps.giftRepository.update(redeemed, {transacting});
|
|
568
|
+
|
|
569
|
+
// Gift members receive the paid welcome email, as they receive access to paid content
|
|
570
|
+
await this.deps.memberRepository.triggerMemberSignupAutomation(
|
|
571
|
+
memberId,
|
|
572
|
+
member.get('email'),
|
|
573
|
+
'paid',
|
|
574
|
+
{transacting}
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
return {redeemed, member};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private async getActiveByMember(memberId: string, options: {transacting?: Knex.Transaction} = {}): Promise<Gift | null> {
|
|
362
581
|
if (!memberId) {
|
|
363
582
|
return null;
|
|
364
583
|
}
|
|
365
584
|
return this.deps.giftRepository.getActiveByMember(memberId, options);
|
|
366
585
|
}
|
|
367
586
|
|
|
368
|
-
async getActiveByMembers(memberIds: string[], options: {transacting?:
|
|
587
|
+
private async getActiveByMembers(memberIds: string[], options: {transacting?: Knex.Transaction} = {}): Promise<Map<string, Gift>> {
|
|
369
588
|
if (!memberIds || memberIds.length === 0) {
|
|
370
589
|
return new Map();
|
|
371
590
|
}
|
|
372
591
|
return this.deps.giftRepository.getActiveByMembers(memberIds, options);
|
|
373
592
|
}
|
|
374
593
|
|
|
375
|
-
getRemainingActiveDays(gift: Gift, now: Date = new Date()): number {
|
|
594
|
+
private getRemainingActiveDays(gift: Gift, now: Date = new Date()): number {
|
|
376
595
|
if (!gift.isRedeemed() || !gift.consumesAt || gift.isConsumed()) {
|
|
377
596
|
return 0;
|
|
378
597
|
}
|
|
@@ -382,12 +601,89 @@ export class GiftService {
|
|
|
382
601
|
return Math.max(0, diffDays);
|
|
383
602
|
}
|
|
384
603
|
|
|
385
|
-
async
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
604
|
+
async preparePaidContinuation({
|
|
605
|
+
memberId,
|
|
606
|
+
memberStatus
|
|
607
|
+
}: {
|
|
608
|
+
memberId: string;
|
|
609
|
+
memberStatus: string;
|
|
610
|
+
}): Promise<GiftContinuation> {
|
|
611
|
+
if (memberStatus !== 'gift') {
|
|
612
|
+
throw new errors.BadRequestError({
|
|
613
|
+
message: 'Bad Request.',
|
|
614
|
+
context: 'Member does not have an active gift subscription'
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const gift = await this.getActiveByMember(memberId);
|
|
619
|
+
|
|
620
|
+
if (!gift) {
|
|
621
|
+
throw new errors.BadRequestError({
|
|
622
|
+
message: 'Bad Request.',
|
|
623
|
+
context: 'No active gift subscription found for member'
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const remainingDays = this.getRemainingActiveDays(gift);
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
tierId: gift.tierId,
|
|
631
|
+
cadence: gift.cadence,
|
|
632
|
+
trialDays: remainingDays > 0 ? Math.min(remainingDays, 730) : null
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async getMemberPresentations(memberIds: string[]): Promise<Map<string, GiftMemberPresentation>> {
|
|
637
|
+
const gifts = await this.getActiveByMembers(memberIds);
|
|
638
|
+
const presentations = new Map<string, GiftMemberPresentation>();
|
|
639
|
+
|
|
640
|
+
for (const [memberId, gift] of gifts) {
|
|
641
|
+
presentations.set(memberId, {
|
|
642
|
+
cadence: gift.cadence,
|
|
643
|
+
currency: gift.currency,
|
|
644
|
+
amount: gift.amount
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return presentations;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async getPreview(token: string): Promise<GiftPreview | null> {
|
|
652
|
+
const gift = await this.deps.giftRepository.getByToken(token);
|
|
653
|
+
|
|
654
|
+
if (!gift) {
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const tier = await this.deps.tiersService.api.read(gift.tierId);
|
|
659
|
+
|
|
660
|
+
if (!tier) {
|
|
661
|
+
throw new errors.NotFoundError({message: `Tier not found for gift: ${gift.token}`});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const tierJSON = tier.toJSON();
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
cadence: gift.cadence,
|
|
668
|
+
duration: gift.duration,
|
|
669
|
+
tier: {
|
|
670
|
+
id: tierJSON.id,
|
|
671
|
+
name: tierJSON.name
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
browsePurchaseEvents(options?: GiftEventBrowseOptions, filter?: unknown): Promise<GiftEventPage> {
|
|
677
|
+
return this.deps.giftRepository.browsePurchaseEvents(options, filter);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
browseRedemptionEvents(options?: GiftEventBrowseOptions, filter?: unknown): Promise<GiftEventPage> {
|
|
681
|
+
return this.deps.giftRepository.browseRedemptionEvents(options, filter);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async reassignRedeemer(input: {giftId: string; memberId: string; transacting?: Knex.Transaction}): Promise<void> {
|
|
685
|
+
const {giftId, memberId} = input;
|
|
686
|
+
const run = async (transacting: Knex.Transaction): Promise<Gift> => {
|
|
391
687
|
const gift = await this.deps.giftRepository.getById(giftId, {transacting, forUpdate: true});
|
|
392
688
|
|
|
393
689
|
if (!gift) {
|
|
@@ -455,12 +751,12 @@ export class GiftService {
|
|
|
455
751
|
return reassignedGift;
|
|
456
752
|
};
|
|
457
753
|
|
|
458
|
-
|
|
459
|
-
? run(
|
|
460
|
-
: this.deps.giftRepository.transaction(run);
|
|
754
|
+
await (input.transacting
|
|
755
|
+
? run(input.transacting)
|
|
756
|
+
: this.deps.giftRepository.transaction(run));
|
|
461
757
|
}
|
|
462
758
|
|
|
463
|
-
async
|
|
759
|
+
async handlePaymentRefund({paymentIntentId}: {paymentIntentId: string}): Promise<boolean> {
|
|
464
760
|
const gift = await this.deps.giftRepository.getByPaymentIntentId(paymentIntentId);
|
|
465
761
|
|
|
466
762
|
if (!gift) {
|
|
@@ -491,8 +787,18 @@ export class GiftService {
|
|
|
491
787
|
return true;
|
|
492
788
|
}
|
|
493
789
|
|
|
494
|
-
async
|
|
495
|
-
const
|
|
790
|
+
async handlePaidSubscriptionActivation(memberId: string): Promise<boolean> {
|
|
791
|
+
const gift = await this.getActiveByMember(memberId);
|
|
792
|
+
|
|
793
|
+
if (!gift) {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return Boolean(await this.consume(gift.token));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
private async consume(token: string, options: {transacting?: Knex.Transaction} = {}): Promise<Gift | null> {
|
|
801
|
+
const run = async (transacting: Knex.Transaction) => {
|
|
496
802
|
// Fetch with a row lock to prevent race conditions under concurrency
|
|
497
803
|
const gift = await this.deps.giftRepository.getByToken(token, {transacting, forUpdate: true});
|
|
498
804
|
|
|
@@ -697,4 +1003,32 @@ export class GiftService {
|
|
|
697
1003
|
|
|
698
1004
|
return true;
|
|
699
1005
|
}
|
|
1006
|
+
|
|
1007
|
+
private async serializeRedemption(gift: Gift): Promise<GiftRedemption> {
|
|
1008
|
+
const tier = await this.deps.tiersService.api.read(gift.tierId);
|
|
1009
|
+
|
|
1010
|
+
if (!tier) {
|
|
1011
|
+
throw new errors.InternalServerError({
|
|
1012
|
+
message: `Tier ${gift.tierId} not found for gift: ${gift.token}`
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const tierJSON = tier.toJSON();
|
|
1017
|
+
|
|
1018
|
+
return {
|
|
1019
|
+
token: gift.token,
|
|
1020
|
+
cadence: gift.cadence,
|
|
1021
|
+
duration: gift.duration,
|
|
1022
|
+
currency: gift.currency,
|
|
1023
|
+
amount: gift.amount,
|
|
1024
|
+
expires_at: gift.expiresAt,
|
|
1025
|
+
consumes_at: gift.consumesAt,
|
|
1026
|
+
tier: {
|
|
1027
|
+
id: tierJSON.id,
|
|
1028
|
+
name: tierJSON.name,
|
|
1029
|
+
description: tierJSON.description,
|
|
1030
|
+
benefits: tierJSON.benefits
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
700
1034
|
}
|