ghost 5.116.2 → 5.118.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-api-framework-5.116.2.tgz → tryghost-api-framework-5.118.0.tgz} +0 -0
- package/components/tryghost-constants-5.118.0.tgz +0 -0
- package/components/tryghost-custom-fonts-5.118.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.118.0.tgz +0 -0
- package/components/tryghost-domain-events-5.118.0.tgz +0 -0
- package/components/tryghost-donations-5.118.0.tgz +0 -0
- package/components/tryghost-email-addresses-5.118.0.tgz +0 -0
- package/components/{tryghost-email-service-5.116.2.tgz → tryghost-email-service-5.118.0.tgz} +0 -0
- package/components/tryghost-email-suppression-list-5.118.0.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.118.0.tgz +0 -0
- package/components/tryghost-i18n-5.118.0.tgz +0 -0
- package/components/{tryghost-job-manager-5.116.2.tgz → tryghost-job-manager-5.118.0.tgz} +0 -0
- package/components/tryghost-link-replacer-5.118.0.tgz +0 -0
- package/components/{tryghost-magic-link-5.116.2.tgz → tryghost-magic-link-5.118.0.tgz} +0 -0
- package/components/{tryghost-member-attribution-5.116.2.tgz → tryghost-member-attribution-5.118.0.tgz} +0 -0
- package/components/tryghost-member-events-5.118.0.tgz +0 -0
- package/components/{tryghost-members-csv-5.116.2.tgz → tryghost-members-csv-5.118.0.tgz} +0 -0
- package/components/{tryghost-members-offers-5.116.2.tgz → tryghost-members-offers-5.118.0.tgz} +0 -0
- package/components/tryghost-mw-error-handler-5.118.0.tgz +0 -0
- package/components/tryghost-mw-vhost-5.118.0.tgz +0 -0
- package/components/{tryghost-post-events-5.116.2.tgz → tryghost-post-events-5.118.0.tgz} +0 -0
- package/components/tryghost-post-revisions-5.118.0.tgz +0 -0
- package/components/tryghost-posts-service-5.118.0.tgz +0 -0
- package/components/tryghost-prometheus-metrics-5.118.0.tgz +0 -0
- package/components/tryghost-security-5.118.0.tgz +0 -0
- package/components/tryghost-tiers-5.118.0.tgz +0 -0
- package/components/tryghost-webmentions-5.118.0.tgz +0 -0
- package/content/themes/casper/LICENSE +1 -1
- package/content/themes/casper/README.md +1 -1
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +1 -1
- package/content/themes/casper/author.hbs +23 -2
- package/content/themes/casper/package.json +2 -2
- package/content/themes/casper/partials/icons/bluesky.hbs +3 -0
- package/content/themes/casper/partials/icons/instagram.hbs +5 -0
- package/content/themes/casper/partials/icons/linkedin.hbs +3 -0
- package/content/themes/casper/partials/icons/mastodon.hbs +3 -0
- package/content/themes/casper/partials/icons/threads.hbs +3 -0
- package/content/themes/casper/partials/icons/tiktok.hbs +3 -0
- package/content/themes/casper/partials/icons/twitter.hbs +3 -1
- package/content/themes/casper/partials/icons/youtube.hbs +3 -0
- package/content/themes/source/LICENSE +1 -1
- package/content/themes/source/README.md +1 -1
- package/content/themes/source/assets/built/screen.css +1 -1
- package/content/themes/source/assets/built/screen.css.map +1 -1
- package/content/themes/source/assets/css/screen.css +7 -12
- package/content/themes/source/author.hbs +24 -3
- package/content/themes/source/package.json +2 -2
- package/content/themes/source/partials/feature-image.hbs +2 -2
- package/content/themes/source/partials/icons/bluesky.hbs +3 -0
- package/content/themes/source/partials/icons/instagram.hbs +5 -0
- package/content/themes/source/partials/icons/linkedin.hbs +3 -0
- package/content/themes/source/partials/icons/mastodon.hbs +3 -0
- package/content/themes/source/partials/icons/threads.hbs +3 -0
- package/content/themes/source/partials/icons/tiktok.hbs +3 -0
- package/content/themes/source/partials/icons/youtube.hbs +3 -0
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +31793 -26588
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-550846e0.mjs → CodeEditorView-1143c509.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-f3cb3f4d.mjs → index-19ebc8ad.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-4ce2fcd1.mjs → index-ac104f42.mjs} +2635 -2607
- package/core/built/admin/assets/admin-x-settings/{modals-6bc20529.mjs → modals-994901ee.mjs} +6680 -6165
- package/core/built/admin/assets/{chunk.524.578de86e5014b911b05a.js → chunk.524.5710919eb507b9a81166.js} +8 -8
- package/core/built/admin/assets/{chunk.582.21bf3e37b5d84ac4b58a.js → chunk.582.c8cb99b85cfa13fc7df1.js} +10 -10
- package/core/built/admin/assets/{chunk.713.761d11035fe0bf3e557c.js → chunk.713.48f120c377bcaffdfddf.js} +6 -9
- package/core/built/admin/assets/{ghost-868c537d5c02ca65323d0122596a67ec.js → ghost-cd90a28b214ee800a007bb62cd45e6e6.js} +780 -775
- package/core/built/admin/assets/posts/posts.js +11561 -11302
- package/core/built/admin/assets/stats/stats.js +76076 -59355
- package/core/built/admin/index.html +4 -4
- package/core/frontend/helpers/social_url.js +31 -0
- package/core/server/api/endpoints/users.js +7 -0
- package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
- package/core/server/data/migrations/versions/5.117/2025-04-14-02-36-30-add-additional-social-accounts-columns-to-user-table.js +38 -0
- package/core/server/data/schema/schema.js +7 -0
- package/core/server/services/auth/session/index.js +5 -2
- package/core/server/services/auth/session/middleware.js +2 -1
- package/core/server/services/auth/session/session-service.js +7 -6
- package/core/server/services/members/api.js +2 -2
- package/core/server/services/members/members-api/controllers/MemberController.js +214 -0
- package/core/server/services/members/members-api/controllers/RouterController.js +667 -0
- package/core/server/services/members/members-api/controllers/WellKnownController.js +46 -0
- package/core/server/services/members/members-api/members-api.js +404 -0
- package/core/server/services/members/members-api/repositories/EventRepository.js +984 -0
- package/core/server/services/members/members-api/repositories/MemberRepository.js +1739 -0
- package/core/server/services/members/members-api/repositories/ProductRepository.js +662 -0
- package/core/server/services/members/members-api/services/GeolocationService.js +23 -0
- package/core/server/services/members/members-api/services/MemberBREADService.js +444 -0
- package/core/server/services/members/members-api/services/PaymentsService.js +522 -0
- package/core/server/services/members/members-api/services/TokenService.js +54 -0
- package/core/server/services/milestones/BookshelfMilestoneRepository.js +8 -9
- package/core/server/services/milestones/InMemoryMilestoneRepository.js +119 -0
- package/core/server/services/milestones/Milestone.js +231 -0
- package/core/server/services/milestones/MilestoneCreatedEvent.js +22 -0
- package/core/server/services/milestones/MilestonesService.js +327 -0
- package/core/server/services/milestones/service.js +2 -2
- package/core/server/services/newsletters/index.js +1 -1
- package/core/server/services/public-config/config.js +2 -1
- package/core/server/services/settings/settings-service.js +1 -1
- package/core/server/services/slack-notifications/SlackNotifications.js +1 -1
- package/core/server/services/slack-notifications/SlackNotificationsService.js +2 -2
- package/core/server/services/staff/StaffService.js +1 -1
- package/core/shared/config/defaults.json +3 -0
- package/core/shared/config/env/config.testing-mysql.json +3 -0
- package/core/shared/config/env/config.testing.json +3 -0
- package/core/shared/labs.js +2 -2
- package/package.json +63 -63
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +306 -70
- package/components/tryghost-constants-5.116.2.tgz +0 -0
- package/components/tryghost-custom-fonts-5.116.2.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-5.116.2.tgz +0 -0
- package/components/tryghost-domain-events-5.116.2.tgz +0 -0
- package/components/tryghost-donations-5.116.2.tgz +0 -0
- package/components/tryghost-email-addresses-5.116.2.tgz +0 -0
- package/components/tryghost-email-suppression-list-5.116.2.tgz +0 -0
- package/components/tryghost-html-to-plaintext-5.116.2.tgz +0 -0
- package/components/tryghost-i18n-5.116.2.tgz +0 -0
- package/components/tryghost-link-replacer-5.116.2.tgz +0 -0
- package/components/tryghost-member-events-5.116.2.tgz +0 -0
- package/components/tryghost-members-api-5.116.2.tgz +0 -0
- package/components/tryghost-milestones-5.116.2.tgz +0 -0
- package/components/tryghost-mw-error-handler-5.116.2.tgz +0 -0
- package/components/tryghost-mw-vhost-5.116.2.tgz +0 -0
- package/components/tryghost-post-revisions-5.116.2.tgz +0 -0
- package/components/tryghost-posts-service-5.116.2.tgz +0 -0
- package/components/tryghost-prometheus-metrics-5.116.2.tgz +0 -0
- package/components/tryghost-security-5.116.2.tgz +0 -0
- package/components/tryghost-tiers-5.116.2.tgz +0 -0
- package/components/tryghost-webmentions-5.116.2.tgz +0 -0
- /package/core/built/admin/assets/{chunk.713.761d11035fe0bf3e557c.js.LICENSE.txt → chunk.713.48f120c377bcaffdfddf.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
const DomainEvents = require('@tryghost/domain-events');
|
|
3
|
+
const {TierCreatedEvent, TierPriceChangeEvent, TierNameChangeEvent} = require('@tryghost/tiers');
|
|
4
|
+
const OfferCreatedEvent = require('@tryghost/members-offers').events.OfferCreatedEvent;
|
|
5
|
+
const {BadRequestError} = require('@tryghost/errors');
|
|
6
|
+
|
|
7
|
+
class PaymentsService {
|
|
8
|
+
/**
|
|
9
|
+
* @param {object} deps
|
|
10
|
+
* @param {import('bookshelf').Model} deps.Offer
|
|
11
|
+
* @param {import('@tryghost/members-offers/lib/application/OffersAPI')} deps.offersAPI
|
|
12
|
+
* @param {import('@tryghost/members-stripe-service/lib/StripeAPI')} deps.stripeAPIService
|
|
13
|
+
* @param {{get(key: string): any}} deps.settingsCache
|
|
14
|
+
*/
|
|
15
|
+
constructor(deps) {
|
|
16
|
+
/** @private */
|
|
17
|
+
this.OfferModel = deps.Offer;
|
|
18
|
+
/** @private */
|
|
19
|
+
this.StripeProductModel = deps.StripeProduct;
|
|
20
|
+
/** @private */
|
|
21
|
+
this.StripePriceModel = deps.StripePrice;
|
|
22
|
+
/** @private */
|
|
23
|
+
this.StripeCustomerModel = deps.StripeCustomer;
|
|
24
|
+
/** @private */
|
|
25
|
+
this.offersAPI = deps.offersAPI;
|
|
26
|
+
/** @private */
|
|
27
|
+
this.stripeAPIService = deps.stripeAPIService;
|
|
28
|
+
/** @private */
|
|
29
|
+
this.settingsCache = deps.settingsCache;
|
|
30
|
+
|
|
31
|
+
DomainEvents.subscribe(OfferCreatedEvent, async (event) => {
|
|
32
|
+
await this.getCouponForOffer(event.data.offer.id);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
DomainEvents.subscribe(TierCreatedEvent, async (event) => {
|
|
36
|
+
if (event.data.tier.type === 'paid') {
|
|
37
|
+
await this.getPriceForTierCadence(event.data.tier, 'month');
|
|
38
|
+
await this.getPriceForTierCadence(event.data.tier, 'year');
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
DomainEvents.subscribe(TierPriceChangeEvent, async (event) => {
|
|
43
|
+
if (event.data.tier.type === 'paid') {
|
|
44
|
+
await this.getPriceForTierCadence(event.data.tier, 'month');
|
|
45
|
+
await this.getPriceForTierCadence(event.data.tier, 'year');
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
DomainEvents.subscribe(TierNameChangeEvent, async (event) => {
|
|
50
|
+
if (event.data.tier.type === 'paid') {
|
|
51
|
+
await this.updateNameForTierProducts(event.data.tier);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {object} params
|
|
58
|
+
* @param {Tier} params.tier
|
|
59
|
+
* @param {Tier.Cadence} params.cadence
|
|
60
|
+
* @param {Offer} [params.offer]
|
|
61
|
+
* @param {Member} [params.member]
|
|
62
|
+
* @param {Object.<string, any>} [params.metadata]
|
|
63
|
+
* @param {string} params.successUrl
|
|
64
|
+
* @param {string} params.cancelUrl
|
|
65
|
+
* @param {string} [params.email]
|
|
66
|
+
*
|
|
67
|
+
* @returns {Promise<URL>}
|
|
68
|
+
*/
|
|
69
|
+
async getPaymentLink({tier, cadence, offer, member, metadata, successUrl, cancelUrl, email}) {
|
|
70
|
+
let coupon = null;
|
|
71
|
+
let trialDays = null;
|
|
72
|
+
if (offer) {
|
|
73
|
+
if (!tier.id.equals(offer.tier.id)) {
|
|
74
|
+
throw new BadRequestError({
|
|
75
|
+
message: 'This Offer is not valid for the Tier'
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (offer.type === 'trial') {
|
|
79
|
+
trialDays = offer.amount;
|
|
80
|
+
} else {
|
|
81
|
+
coupon = await this.getCouponForOffer(offer.id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let customer = null;
|
|
86
|
+
if (member) {
|
|
87
|
+
customer = await this.getCustomerForMember(member);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const price = await this.getPriceForTierCadence(tier, cadence);
|
|
91
|
+
|
|
92
|
+
const data = {
|
|
93
|
+
metadata,
|
|
94
|
+
successUrl: successUrl,
|
|
95
|
+
cancelUrl: cancelUrl,
|
|
96
|
+
trialDays: trialDays ?? tier.trialDays,
|
|
97
|
+
coupon: coupon?.id
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// If we already have a coupon, we don't want to give trial days over it
|
|
101
|
+
if (data.coupon) {
|
|
102
|
+
delete data.trialDays;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!customer && email) {
|
|
106
|
+
data.customerEmail = email;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const session = await this.stripeAPIService.createCheckoutSession(price.id, customer, data);
|
|
110
|
+
|
|
111
|
+
return session.url;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {object} params
|
|
116
|
+
* @param {Member} [params.member]
|
|
117
|
+
* @param {Object.<string, any>} [params.metadata]
|
|
118
|
+
* @param {string} params.successUrl
|
|
119
|
+
* @param {string} params.cancelUrl
|
|
120
|
+
* @param {boolean} [params.isAuthenticated]
|
|
121
|
+
* @param {string} [params.email]
|
|
122
|
+
*
|
|
123
|
+
* @returns {Promise<URL>}
|
|
124
|
+
*/
|
|
125
|
+
async getDonationPaymentLink({member, metadata, successUrl, cancelUrl, email, isAuthenticated, personalNote}) {
|
|
126
|
+
let customer = null;
|
|
127
|
+
if (member && isAuthenticated) {
|
|
128
|
+
customer = await this.getCustomerForMember(member);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const data = {
|
|
132
|
+
priceId: (await this.getPriceForDonations()).id,
|
|
133
|
+
metadata,
|
|
134
|
+
successUrl: successUrl,
|
|
135
|
+
cancelUrl: cancelUrl,
|
|
136
|
+
customer,
|
|
137
|
+
customerEmail: !customer && email ? email : null,
|
|
138
|
+
personalNote: personalNote
|
|
139
|
+
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const session = await this.stripeAPIService.createDonationCheckoutSession(data);
|
|
143
|
+
return session.url;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getCustomerForMember(member) {
|
|
147
|
+
const rows = await this.StripeCustomerModel.where({
|
|
148
|
+
member_id: member.id
|
|
149
|
+
}).query().select('customer_id');
|
|
150
|
+
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
try {
|
|
153
|
+
const customer = await this.stripeAPIService.getCustomer(row.customer_id);
|
|
154
|
+
if (!customer.deleted) {
|
|
155
|
+
return customer;
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
logging.warn(err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const customer = await this.createCustomerForMember(member);
|
|
163
|
+
|
|
164
|
+
return customer;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async createCustomerForMember(member) {
|
|
168
|
+
const customer = await this.stripeAPIService.createCustomer({
|
|
169
|
+
email: member.get('email'),
|
|
170
|
+
name: member.get('name')
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await this.StripeCustomerModel.add({
|
|
174
|
+
member_id: member.id,
|
|
175
|
+
customer_id: customer.id,
|
|
176
|
+
email: customer.email,
|
|
177
|
+
name: customer.name
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return customer;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @param {import('@tryghost/tiers').Tier} tier
|
|
185
|
+
* @returns {Promise<{id: string}>}
|
|
186
|
+
*/
|
|
187
|
+
async getProductForTier(tier) {
|
|
188
|
+
const rows = await this.StripeProductModel
|
|
189
|
+
.where({product_id: tier.id.toHexString()})
|
|
190
|
+
.query()
|
|
191
|
+
.select('stripe_product_id');
|
|
192
|
+
|
|
193
|
+
for (const row of rows) {
|
|
194
|
+
try {
|
|
195
|
+
const product = await this.stripeAPIService.getProduct(row.stripe_product_id);
|
|
196
|
+
if (product.active) {
|
|
197
|
+
return {id: product.id};
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
logging.warn(err);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const product = await this.createProductForTier(tier);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
id: product.id
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {import('@tryghost/tiers').Tier} tier
|
|
213
|
+
* @returns {Promise<import('stripe').default.Product>}
|
|
214
|
+
*/
|
|
215
|
+
async createProductForTier(tier) {
|
|
216
|
+
const product = await this.stripeAPIService.createProduct({name: tier.name});
|
|
217
|
+
await this.StripeProductModel.add({
|
|
218
|
+
product_id: tier.id.toHexString(),
|
|
219
|
+
stripe_product_id: product.id
|
|
220
|
+
});
|
|
221
|
+
return product;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {import('@tryghost/tiers').Tier} tier
|
|
226
|
+
* @returns {Promise<void>}
|
|
227
|
+
*/
|
|
228
|
+
async updateNameForTierProducts(tier) {
|
|
229
|
+
const rows = await this.StripeProductModel
|
|
230
|
+
.where({product_id: tier.id.toHexString()})
|
|
231
|
+
.query()
|
|
232
|
+
.select('stripe_product_id');
|
|
233
|
+
|
|
234
|
+
for (const row of rows) {
|
|
235
|
+
await this.stripeAPIService.updateProduct(row.stripe_product_id, {
|
|
236
|
+
name: tier.name
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @returns {Promise<{id: string}>}
|
|
243
|
+
*/
|
|
244
|
+
async getProductForDonations({name}) {
|
|
245
|
+
const existingDonationPrices = await this.StripePriceModel
|
|
246
|
+
.where({
|
|
247
|
+
type: 'donation'
|
|
248
|
+
})
|
|
249
|
+
.query()
|
|
250
|
+
.select('stripe_product_id');
|
|
251
|
+
|
|
252
|
+
for (const row of existingDonationPrices) {
|
|
253
|
+
const product = await this.StripeProductModel
|
|
254
|
+
.where({
|
|
255
|
+
stripe_product_id: row.stripe_product_id
|
|
256
|
+
})
|
|
257
|
+
.query()
|
|
258
|
+
.select('stripe_product_id')
|
|
259
|
+
.first();
|
|
260
|
+
|
|
261
|
+
if (product) {
|
|
262
|
+
// Check active in Stripe
|
|
263
|
+
try {
|
|
264
|
+
const stripeProduct = await this.stripeAPIService.getProduct(row.stripe_product_id);
|
|
265
|
+
if (stripeProduct.active) {
|
|
266
|
+
return {id: stripeProduct.id};
|
|
267
|
+
}
|
|
268
|
+
} catch (err) {
|
|
269
|
+
logging.warn(err);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const product = await this.createProductForDonations({name});
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
id: product.id
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Stripe's nickname field is limited to 250 characters
|
|
283
|
+
* @returns {string}
|
|
284
|
+
*/
|
|
285
|
+
getDonationPriceNickname() {
|
|
286
|
+
const nickname = 'Support ' + this.settingsCache.get('title');
|
|
287
|
+
return nickname.substring(0, 250);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* @returns {Promise<{id: string}>}
|
|
292
|
+
*/
|
|
293
|
+
async getPriceForDonations() {
|
|
294
|
+
const nickname = this.getDonationPriceNickname();
|
|
295
|
+
const currency = this.settingsCache.get('donations_currency');
|
|
296
|
+
const suggestedAmount = this.settingsCache.get('donations_suggested_amount');
|
|
297
|
+
|
|
298
|
+
// Stripe requires a minimum charge amount
|
|
299
|
+
// @see https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts
|
|
300
|
+
const amount = suggestedAmount && suggestedAmount >= 100 ? suggestedAmount : 0;
|
|
301
|
+
|
|
302
|
+
const price = await this.StripePriceModel
|
|
303
|
+
.where({
|
|
304
|
+
type: 'donation',
|
|
305
|
+
active: true,
|
|
306
|
+
amount,
|
|
307
|
+
currency
|
|
308
|
+
})
|
|
309
|
+
.query()
|
|
310
|
+
.select('stripe_price_id', 'stripe_product_id', 'id', 'nickname')
|
|
311
|
+
.first();
|
|
312
|
+
|
|
313
|
+
if (price) {
|
|
314
|
+
if (price.nickname !== nickname) {
|
|
315
|
+
// Rename it in Stripe (in case the publication name changed)
|
|
316
|
+
try {
|
|
317
|
+
await this.stripeAPIService.updatePrice(price.stripe_price_id, {
|
|
318
|
+
nickname
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Update product too
|
|
322
|
+
await this.stripeAPIService.updateProduct(price.stripe_product_id, {
|
|
323
|
+
name: nickname
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await this.StripePriceModel.edit({
|
|
327
|
+
nickname
|
|
328
|
+
}, {id: price.id});
|
|
329
|
+
} catch (err) {
|
|
330
|
+
logging.warn(err);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
id: price.stripe_price_id
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const newPrice = await this.createPriceForDonations({
|
|
339
|
+
nickname,
|
|
340
|
+
currency,
|
|
341
|
+
amount
|
|
342
|
+
});
|
|
343
|
+
return {
|
|
344
|
+
id: newPrice.id
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @returns {Promise<import('stripe').default.Price>}
|
|
350
|
+
*/
|
|
351
|
+
async createPriceForDonations({currency, amount, nickname}) {
|
|
352
|
+
const product = await this.getProductForDonations({name: nickname});
|
|
353
|
+
|
|
354
|
+
const preset = amount ? amount : undefined;
|
|
355
|
+
|
|
356
|
+
// Create the price in Stripe
|
|
357
|
+
const price = await this.stripeAPIService.createPrice({
|
|
358
|
+
currency,
|
|
359
|
+
product: product.id,
|
|
360
|
+
custom_unit_amount: {
|
|
361
|
+
enabled: true,
|
|
362
|
+
preset
|
|
363
|
+
},
|
|
364
|
+
nickname,
|
|
365
|
+
type: 'one-time',
|
|
366
|
+
active: true
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Save it to the database
|
|
370
|
+
await this.StripePriceModel.add({
|
|
371
|
+
stripe_price_id: price.id,
|
|
372
|
+
stripe_product_id: product.id,
|
|
373
|
+
active: price.active,
|
|
374
|
+
nickname: price.nickname,
|
|
375
|
+
currency: price.currency,
|
|
376
|
+
amount,
|
|
377
|
+
type: 'donation',
|
|
378
|
+
interval: null
|
|
379
|
+
});
|
|
380
|
+
return price;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* @returns {Promise<import('stripe').default.Product>}
|
|
385
|
+
*/
|
|
386
|
+
async createProductForDonations({name}) {
|
|
387
|
+
const product = await this.stripeAPIService.createProduct({
|
|
388
|
+
name
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
await this.StripeProductModel.add({
|
|
392
|
+
product_id: null,
|
|
393
|
+
stripe_product_id: product.id
|
|
394
|
+
});
|
|
395
|
+
return product;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* @param {import('@tryghost/tiers').Tier} tier
|
|
400
|
+
* @param {'month'|'year'} cadence
|
|
401
|
+
* @returns {Promise<{id: string}>}
|
|
402
|
+
*/
|
|
403
|
+
async getPriceForTierCadence(tier, cadence) {
|
|
404
|
+
const product = await this.getProductForTier(tier);
|
|
405
|
+
const currency = tier.currency.toLowerCase();
|
|
406
|
+
const amount = tier.getPrice(cadence);
|
|
407
|
+
const rows = await this.StripePriceModel.where({
|
|
408
|
+
stripe_product_id: product.id,
|
|
409
|
+
currency,
|
|
410
|
+
interval: cadence,
|
|
411
|
+
amount,
|
|
412
|
+
active: true,
|
|
413
|
+
type: 'recurring'
|
|
414
|
+
}).query().select('id', 'stripe_price_id');
|
|
415
|
+
|
|
416
|
+
for (const row of rows) {
|
|
417
|
+
try {
|
|
418
|
+
const price = await this.stripeAPIService.getPrice(row.stripe_price_id);
|
|
419
|
+
if (price.active && price.currency.toLowerCase() === currency && price.unit_amount === amount && price.recurring?.interval === cadence) {
|
|
420
|
+
return {
|
|
421
|
+
id: price.id
|
|
422
|
+
};
|
|
423
|
+
} else {
|
|
424
|
+
// Update the database model to prevent future Stripe fetches when it is not needed
|
|
425
|
+
await this.StripePriceModel.edit({
|
|
426
|
+
active: !!price.active
|
|
427
|
+
}, {id: row.id});
|
|
428
|
+
}
|
|
429
|
+
} catch (err) {
|
|
430
|
+
logging.error(`Failed to lookup Stripe Price ${row.stripe_price_id}`);
|
|
431
|
+
logging.error(err);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const price = await this.createPriceForTierCadence(tier, cadence);
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
id: price.id
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* @param {import('@tryghost/tiers').Tier} tier
|
|
444
|
+
* @param {'month'|'year'} cadence
|
|
445
|
+
* @returns {Promise<import('stripe').default.Price>}
|
|
446
|
+
*/
|
|
447
|
+
async createPriceForTierCadence(tier, cadence) {
|
|
448
|
+
const product = await this.getProductForTier(tier);
|
|
449
|
+
const price = await this.stripeAPIService.createPrice({
|
|
450
|
+
product: product.id,
|
|
451
|
+
interval: cadence,
|
|
452
|
+
currency: tier.currency,
|
|
453
|
+
amount: tier.getPrice(cadence),
|
|
454
|
+
nickname: cadence === 'month' ? 'Monthly' : 'Yearly',
|
|
455
|
+
type: 'recurring',
|
|
456
|
+
active: true
|
|
457
|
+
});
|
|
458
|
+
await this.StripePriceModel.add({
|
|
459
|
+
stripe_price_id: price.id,
|
|
460
|
+
stripe_product_id: product.id,
|
|
461
|
+
active: price.active,
|
|
462
|
+
nickname: price.nickname,
|
|
463
|
+
currency: price.currency,
|
|
464
|
+
amount: price.unit_amount,
|
|
465
|
+
type: 'recurring',
|
|
466
|
+
interval: cadence
|
|
467
|
+
});
|
|
468
|
+
return price;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* @param {string} offerId
|
|
473
|
+
*
|
|
474
|
+
* @returns {Promise<{id: string}>}
|
|
475
|
+
*/
|
|
476
|
+
async getCouponForOffer(offerId) {
|
|
477
|
+
const row = await this.OfferModel.where({id: offerId}).query().select('stripe_coupon_id', 'discount_type').first();
|
|
478
|
+
if (!row || row.discount_type === 'trial') {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
if (!row.stripe_coupon_id) {
|
|
482
|
+
const offer = await this.offersAPI.getOffer({id: offerId});
|
|
483
|
+
await this.createCouponForOffer(offer);
|
|
484
|
+
return this.getCouponForOffer(offerId);
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
id: row.stripe_coupon_id
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* @param {import('@tryghost/members-offers/lib/application/OfferMapper').OfferDTO} offer
|
|
493
|
+
*/
|
|
494
|
+
async createCouponForOffer(offer) {
|
|
495
|
+
/** @type {import('stripe').Stripe.CouponCreateParams} */
|
|
496
|
+
const couponData = {
|
|
497
|
+
name: offer.name,
|
|
498
|
+
duration: offer.duration
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
if (offer.duration === 'repeating') {
|
|
502
|
+
couponData.duration_in_months = offer.duration_in_months;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (offer.type === 'percent') {
|
|
506
|
+
couponData.percent_off = offer.amount;
|
|
507
|
+
} else {
|
|
508
|
+
couponData.amount_off = offer.amount;
|
|
509
|
+
couponData.currency = offer.currency;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const coupon = await this.stripeAPIService.createCoupon(couponData);
|
|
513
|
+
|
|
514
|
+
await this.OfferModel.edit({
|
|
515
|
+
stripe_coupon_id: coupon.id
|
|
516
|
+
}, {
|
|
517
|
+
id: offer.id
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
module.exports = PaymentsService;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const jose = require('node-jose');
|
|
2
|
+
const jwt = require('jsonwebtoken');
|
|
3
|
+
|
|
4
|
+
module.exports = class TokenService {
|
|
5
|
+
constructor({
|
|
6
|
+
privateKey,
|
|
7
|
+
publicKey,
|
|
8
|
+
issuer
|
|
9
|
+
}) {
|
|
10
|
+
this._keyStore = jose.JWK.createKeyStore();
|
|
11
|
+
this._keyStoreReady = this._keyStore.add(privateKey, 'pem');
|
|
12
|
+
this._privateKey = privateKey;
|
|
13
|
+
this._publicKey = publicKey;
|
|
14
|
+
this._issuer = issuer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async encodeIdentityToken({sub}) {
|
|
18
|
+
const jwk = await this._keyStoreReady;
|
|
19
|
+
return jwt.sign({
|
|
20
|
+
sub,
|
|
21
|
+
kid: jwk.kid
|
|
22
|
+
}, this._privateKey, {
|
|
23
|
+
keyid: jwk.kid,
|
|
24
|
+
algorithm: 'RS512',
|
|
25
|
+
audience: this._issuer,
|
|
26
|
+
expiresIn: '10m',
|
|
27
|
+
issuer: this._issuer
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} token
|
|
33
|
+
* @returns {Promise<jwt.JwtPayload>}
|
|
34
|
+
*/
|
|
35
|
+
async decodeToken(token) {
|
|
36
|
+
await this._keyStoreReady;
|
|
37
|
+
|
|
38
|
+
const result = jwt.verify(token, this._publicKey, {
|
|
39
|
+
algorithms: ['RS512'],
|
|
40
|
+
issuer: this._issuer
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (typeof result === 'string') {
|
|
44
|
+
return {sub: result};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getPublicKeys() {
|
|
51
|
+
await this._keyStoreReady;
|
|
52
|
+
return this._keyStore.toJSON();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
const
|
|
1
|
+
const Milestone = require('./Milestone');
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @typedef {import('
|
|
5
|
-
* @typedef {import('@tryghost/milestones/lib/MilestonesService')} Milestone
|
|
4
|
+
* @typedef {import('./MilestonesService').IMilestoneRepository} IMilestoneRepository
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -37,7 +36,7 @@ module.exports = class BookshelfMilestoneRepository {
|
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
/**
|
|
40
|
-
* @param {
|
|
39
|
+
* @param {Milestone} milestone
|
|
41
40
|
* @returns {Promise<void>}
|
|
42
41
|
*/
|
|
43
42
|
async save(milestone) {
|
|
@@ -68,7 +67,7 @@ module.exports = class BookshelfMilestoneRepository {
|
|
|
68
67
|
* @param {'arr'|'members'} type
|
|
69
68
|
* @param {string} [currency]
|
|
70
69
|
*
|
|
71
|
-
* @returns {Promise<
|
|
70
|
+
* @returns {Promise<Milestone[]>}
|
|
72
71
|
*/
|
|
73
72
|
async getAllByType(type, currency = 'usd') {
|
|
74
73
|
let milestone = null;
|
|
@@ -93,7 +92,7 @@ module.exports = class BookshelfMilestoneRepository {
|
|
|
93
92
|
* @param {'arr'|'members'} type
|
|
94
93
|
* @param {string} [currency]
|
|
95
94
|
*
|
|
96
|
-
* @returns {Promise<
|
|
95
|
+
* @returns {Promise<Milestone|null>}
|
|
97
96
|
*/
|
|
98
97
|
async getLatestByType(type, currency = 'usd') {
|
|
99
98
|
const allMilestonesForType = await this.getAllByType(type, currency);
|
|
@@ -101,7 +100,7 @@ module.exports = class BookshelfMilestoneRepository {
|
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
/**
|
|
104
|
-
* @returns {Promise<
|
|
103
|
+
* @returns {Promise<Milestone|null>}
|
|
105
104
|
*/
|
|
106
105
|
async getLastEmailSent() {
|
|
107
106
|
let milestone = await this.#MilestoneModel.findAll({filter: 'email_sent_at:-null', order: 'email_sent_at ASC'}, {require: false});
|
|
@@ -119,7 +118,7 @@ module.exports = class BookshelfMilestoneRepository {
|
|
|
119
118
|
* @param {number} value
|
|
120
119
|
* @param {string} [currency]
|
|
121
120
|
*
|
|
122
|
-
* @returns {Promise<
|
|
121
|
+
* @returns {Promise<Milestone|null>}
|
|
123
122
|
*/
|
|
124
123
|
async getByARR(value, currency = 'usd') {
|
|
125
124
|
// find a milestone of the ARR type by a given value
|
|
@@ -134,7 +133,7 @@ module.exports = class BookshelfMilestoneRepository {
|
|
|
134
133
|
/**
|
|
135
134
|
* @param {number} value
|
|
136
135
|
*
|
|
137
|
-
* @returns {Promise<
|
|
136
|
+
* @returns {Promise<Milestone|null>}
|
|
138
137
|
*/
|
|
139
138
|
async getByCount(value) {
|
|
140
139
|
// find a milestone of the members type by a given value
|