fix: update stripe wallets to use payment intent (#54668)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Ahmad Abdolsaheb
2024-06-15 09:15:10 +03:00
committed by GitHub
parent c132ef80f4
commit 7e23b0d69c
11 changed files with 234 additions and 165 deletions

View File

@@ -1,11 +1,15 @@
import debug from 'debug';
import Stripe from 'stripe';
import { donationSubscriptionConfig } from '../../../../shared/config/donation-settings';
import {
donationSubscriptionConfig,
allStripeProductIdsArray
} from '../../../../shared/config/donation-settings';
import keys from '../../../config/secrets';
import {
createStripeCardDonation,
handleStripeCardUpdateSession
handleStripeCardUpdateSession,
inLastFiveMinutes
} from '../utils/donation';
import { validStripeForm } from '../utils/stripeHelpers';
@@ -42,117 +46,82 @@ export default function donateBoot(app, done) {
});
}
function createStripeDonation(req, res) {
const { user, body } = req;
async function createStripeDonation(req, res) {
const { body } = req;
const { amount, duration, email, subscriptionId } = body;
try {
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const isSubscriptionActive = subscription.status === 'active';
const productId = subscription.items.data[0].plan.product;
const isStartedRecently = inLastFiveMinutes(
subscription.current_period_start
);
const isProductIdValid = allStripeProductIdsArray.includes(productId);
const {
amount,
duration,
token: { id },
email,
name
} = body;
if (isSubscriptionActive && isProductIdValid && isStartedRecently) {
const [donatingUser] = await User.findOrCreate(
{ where: { email } },
{ email }
);
const donation = {
email,
amount,
duration,
provider: 'stripe',
subscriptionId,
customerId: subscription.customer,
startDate: new Date().toISOString()
};
await donatingUser.createDonation(donation);
return res.status(200).send({ isDonating: true });
} else {
throw new Error('Donation failed due to a server error.');
}
} catch (err) {
return res
.status(500)
.send({ error: 'Donation failed due to a server error.' });
}
}
async function createStripePaymentIntent(req, res) {
const { body } = req;
const { amount, duration, email, name } = body;
if (!validStripeForm(amount, duration, email)) {
return res.status(500).send({
return res.status(400).send({
error: 'The donation form had invalid values for this submission.'
});
}
const fccUser = user
? Promise.resolve(user)
: new Promise((resolve, reject) =>
User.findOrCreate(
{ where: { email } },
{ email },
(err, instance) => {
if (err) {
return reject(err);
}
return resolve(instance);
}
)
);
let donatingUser = {};
let donation = {
email,
amount,
duration,
provider: 'stripe',
startDate: new Date(Date.now()).toISOString()
};
const createCustomer = async user => {
let customer;
donatingUser = user;
try {
customer = await stripe.customers.create({
email,
card: id,
name
});
} catch (err) {
throw new Error('Error creating stripe customer');
}
log(`Stripe customer with id ${customer.id} created`);
return customer;
};
const createSubscription = async customer => {
donation.customerId = customer.id;
let sub;
try {
sub = await stripe.subscriptions.create({
customer: customer.id,
items: [
{
plan: `${donationSubscriptionConfig.duration[
duration
].toLowerCase()}-donation-${amount}`
}
]
});
} catch (err) {
throw new Error('Error creating stripe subscription');
}
return sub;
};
const createAsyncUserDonation = () => {
donatingUser
.createDonation(donation)
.toPromise()
.catch(err => {
throw new Error(err);
});
};
return Promise.resolve(fccUser)
.then(nonDonatingUser => {
// the logic is removed since users can donate without an account
return nonDonatingUser;
})
.then(createCustomer)
.then(customer => {
return createSubscription(customer).then(subscription => {
log(`Stripe subscription with id ${subscription.id} created`);
donation.subscriptionId = subscription.id;
return res.send(subscription);
});
})
.then(createAsyncUserDonation)
.catch(err => {
if (
err.type === 'StripeCardError' ||
err.type === 'AlreadyDonatingError'
) {
return res.status(402).send({ error: err.message });
}
return res
.status(500)
.send({ error: 'Donation failed due to a server error.' });
try {
const stripeCustomer = await stripe.customers.create({
email,
name
});
const stripeSubscription = await stripe.subscriptions.create({
customer: stripeCustomer.id,
items: [
{
plan: `${donationSubscriptionConfig.duration[duration]}-donation-${amount}`
}
],
payment_behavior: 'default_incomplete',
payment_settings: { save_default_payment_method: 'on_subscription' },
expand: ['latest_invoice.payment_intent']
});
res.status(200).send({
subscriptionId: stripeSubscription.id,
clientSecret:
stripeSubscription.latest_invoice.payment_intent.client_secret
});
} catch (err) {
return res
.status(500)
.send({ error: 'Donation failed due to a server error.' });
}
}
function addDonation(req, res) {
@@ -208,6 +177,7 @@ export default function donateBoot(app, done) {
} else {
api.post('/charge-stripe', createStripeDonation);
api.post('/charge-stripe-card', handleStripeCardDonation);
api.post('/create-stripe-payment-intent', createStripePaymentIntent);
api.put('/update-stripe-card', handleStripeCardUpdate);
api.post('/add-donation', addDonation);
donateRouter.use('/donate', api);

View File

@@ -14,7 +14,9 @@ export default function getCsurf() {
const { path } = req;
if (
// eslint-disable-next-line max-len
/^\/donate\/charge-stripe$|^\/coderoad-challenge-completed$/.test(path)
/^\/donate\/charge-stripe$|^\/donate\/create-stripe-payment-intent$|^\/coderoad-challenge-completed$/.test(
path
)
) {
next();
} else {

View File

@@ -24,6 +24,7 @@ const unsubscribedRE = /^\/unsubscribed\//;
const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
// note: this would be replaced by webhooks later
const donateRE = /^\/donate\/charge-stripe$/;
const paymentIntentRE = /^\/donate\/create-stripe-payment-intent$/;
const submitCoderoadChallengeRE = /^\/coderoad-challenge-completed$/;
const mobileLoginRE = /^\/mobile-login\/?$/;
@@ -40,6 +41,7 @@ const _pathsAllowedREs = [
unsubscribedRE,
unsubscribeRE,
donateRE,
paymentIntentRE,
submitCoderoadChallengeRE,
mobileLoginRE
];

View File

@@ -173,3 +173,9 @@ export async function handleStripeCardUpdateSession(req, app, stripe) {
});
return { sessionId: session.id };
}
export function inLastFiveMinutes(unixTimestamp) {
const currentTimestamp = Math.floor(Date.now() / 1000);
const timeDifference = currentTimestamp - unixTimestamp;
return timeDifference <= 300; // 300 seconds is 5 minutes
}

View File

@@ -1,15 +1,10 @@
import { isEmail, isNumeric } from 'validator';
import {
durationKeysConfig,
donationOneTimeConfig,
donationSubscriptionConfig
} from '../../../../shared/config/donation-settings';
import { donationSubscriptionConfig } from '../../../../shared/config/donation-settings';
export function validStripeForm(amount, duration, email) {
return isEmail('' + email) &&
return (
isEmail('' + email) &&
isNumeric('' + amount) &&
durationKeysConfig.includes(duration) &&
duration === 'one-time'
? donationOneTimeConfig.includes(amount)
: donationSubscriptionConfig.plans[duration];
donationSubscriptionConfig.plans[duration].includes(amount)
);
}