🐛 Fixed Tips & Donations checkout error for sites with long titles

ref https://linear.app/tryghost/issue/ONC-296

Our `stripe_prices.nickname` field had a length of 50 chars which meant we could error out trying to save a donation Stripe price with a generated product nickname containing a long site title.

- updated db schema and added a migration to change column length to 255
- added truncation to nickname generation to enforce a limit of 250 chars to match Stripe's limit
This commit is contained in:
Kevin Ansfield 2024-09-03 20:49:03 +01:00
parent b2d7922f30
commit 013041304e
6 changed files with 85 additions and 3 deletions

View File

@ -0,0 +1,19 @@
const logging = require('@tryghost/logging');
const {createNonTransactionalMigration} = require('../../utils');
const DatabaseInfo = require('@tryghost/database-info');
module.exports = createNonTransactionalMigration(
async function up(knex) {
if (DatabaseInfo.isSQLite(knex)) {
logging.warn('Skipping migration for SQLite3');
return;
}
logging.info('Changing stripe_prices.nickname column from VARCHAR(50) to VARCHAR(255)');
await knex.schema.alterTable('stripe_prices', function (table) {
table.string('nickname', 255).alter();
});
},
async function down() {
logging.warn('Not changing stripe_prices.nickname column');
}
);

View File

@ -781,7 +781,7 @@ module.exports = {
stripe_price_id: {type: 'string', maxlength: 255, nullable: false, unique: true},
stripe_product_id: {type: 'string', maxlength: 255, nullable: false, unique: false, references: 'stripe_products.stripe_product_id'},
active: {type: 'boolean', nullable: false},
nickname: {type: 'string', maxlength: 50, nullable: true},
nickname: {type: 'string', maxlength: 255, nullable: true},
// @note: this is longer than originally intended due to a bug - https://github.com/TryGhost/Ghost/pull/15606
// so we should decide whether we should reduce it down in the future
currency: {type: 'string', maxlength: 191, nullable: false},

View File

@ -12,6 +12,12 @@ Object {
}
`;
exports[`Create Stripe Checkout Session for Donations can create a checkout session for a site with a long title 1: [body] 1`] = `
Object {
"url": "https://checkout.stripe.com/c/pay/fake-data",
}
`;
exports[`Create Stripe Checkout Session for Donations check if donation message is in email 1: [body] 1`] = `
Object {
"url": "https://checkout.stripe.com/c/pay/fake-data",

View File

@ -211,6 +211,7 @@ describe('Create Stripe Checkout Session for Donations', function () {
assert.equal(lastDonation.get('attribution_type'), 'post');
assert.equal(lastDonation.get('attribution_url'), url);
});
it('check if donation message is in email', async function () {
const post = await getPost(fixtureManager.get('posts', 0).id);
const url = urlService.getUrlByResourceId(post.id, {absolute: false});
@ -281,4 +282,51 @@ describe('Create Stripe Checkout Session for Donations', function () {
text: /You are the best! Have a lovely day!/
});
});
// We had a bug where the stripe_prices.nickname column was too short for the site title
// Stripe is also limited to 250 chars so we need to truncate the nickname
it('can create a checkout session for a site with a long title', async function () {
// Ensure site title is longer than 250 characters
mockManager.mockSetting('title', 'a'.repeat(251));
// clear out existing prices to guarantee we're creating a new one
await models.StripePrice.where('type', 'donation').destroy().catch((e) => {
if (e.message !== 'No Rows Deleted') {
throw e;
}
});
// Fake a visit to a post
const post = await getPost(fixtureManager.get('posts', 0).id);
const url = urlService.getUrlByResourceId(post.id, {absolute: false});
await membersAgent.post('/api/create-stripe-checkout-session/')
.body({
customerEmail: 'paid@test.com',
type: 'donation',
successUrl: 'https://example.com/?type=success',
cancelUrl: 'https://example.com/?type=cancel',
metadata: {
test: 'hello',
urlHistory: [
{
path: url,
time: Date.now(),
referrerMedium: null,
referrerSource: 'ghost-explore',
referrerUrl: 'https://example.com/blog/'
}
]
}
})
.expectStatus(200)
.matchBodySnapshot();
const latestStripePrice = await models.StripePrice
.where('type', 'donation')
.orderBy('created_at', 'DESC')
.fetch({require: true});
latestStripePrice.get('nickname').should.have.length(250);
});
});

View File

@ -35,7 +35,7 @@ const validateRouteSettings = require('../../../../../core/server/services/route
*/
describe('DB version integrity', function () {
// Only these variables should need updating
const currentSchemaHash = 'b59d502d0e7965a837bb1dfb5c583562';
const currentSchemaHash = 'a4f016480ff73c6f52ee4c86482b45a7';
const currentFixturesHash = 'a489d615989eab1023d4b8af0ecee7fd';
const currentSettingsHash = '051ef2a50e2edb8723e89461448313cb';
const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';

View File

@ -276,11 +276,20 @@ class PaymentsService {
};
}
/**
* Stripe's nickname field is limited to 250 characters
* @returns {string}
*/
getDonationPriceNickname() {
const nickname = 'Support ' + this.settingsCache.get('title');
return nickname.substring(0, 250);
}
/**
* @returns {Promise<{id: string}>}
*/
async getPriceForDonations() {
const nickname = 'Support ' + this.settingsCache.get('title');
const nickname = this.getDonationPriceNickname();
const currency = this.settingsCache.get('donations_currency');
const suggestedAmount = this.settingsCache.get('donations_suggested_amount');