diff --git a/api-server/server/boot/donate.js b/api-server/server/boot/donate.js index daf8f4f68d..c5253af3f6 100644 --- a/api-server/server/boot/donate.js +++ b/api-server/server/boot/donate.js @@ -5,7 +5,7 @@ import { isEmail, isNumeric } from 'validator'; import { durationKeysConfig, - // donationOneTimeConfig, + donationOneTimeConfig, donationSubscriptionConfig } from '../../../config/donation-settings'; import keys from '../../../config/secrets'; @@ -14,6 +14,7 @@ const log = debug('fcc:boot:donate'); export default function donateBoot(app, done) { let stripe = false; + const { User } = app.models; const api = app.loopback.Router(); const donateRouter = app.loopback.Router(); @@ -56,8 +57,7 @@ export default function donateBoot(app, done) { isNumeric('' + amount) && durationKeysConfig.includes(duration) && duration === 'onetime' - ? // eslint-disable-next-line no-inline-comments - amount > 1 // donationOneTimeConfig.includes(amount) + ? donationOneTimeConfig.includes(amount) : donationSubscriptionConfig.plans[duration]; } @@ -206,6 +206,104 @@ export default function donateBoot(app, done) { }); } + function createStripeDonationYearEnd(req, res) { + const { user, body } = req; + + const { + amount, + duration, + token: { email, id } + } = body; + + if (amount < 1 || duration !== 'onetime' || !isEmail(email)) { + return res.status(500).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, isNew) => { + log('is new user instance: ', isNew); + 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 = user => { + donatingUser = user; + return stripe.customers.create({ + email, + card: id + }); + }; + + const createOneTimeCharge = customer => { + donation.customerId = customer.id; + return stripe.charges.create({ + amount: amount, + currency: 'usd', + customer: customer.id + }); + }; + + const createAsyncUserDonation = () => { + donatingUser + .createDonation(donation) + .toPromise() + .catch(err => { + throw new Error(err); + }); + }; + + return Promise.resolve(fccUser) + .then(nonDonatingUser => { + const { isDonating } = nonDonatingUser; + if (isDonating) { + throw { + message: `User already has active donation(s).`, + type: 'AlreadyDonatingError' + }; + } + return nonDonatingUser; + }) + .then(createCustomer) + .then(customer => { + return createOneTimeCharge(customer).then(charge => { + donation.subscriptionId = 'one-time-charge-prefix-' + charge.id; + return res.send(charge); + }); + }) + .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.' }); + }); + } + function createHmacHash(req, res) { const { user, body } = req; @@ -261,10 +359,12 @@ export default function donateBoot(app, done) { done(); } else { api.post('/charge-stripe', createStripeDonation); + api.post('/charge-stripe-year-end', createStripeDonationYearEnd); api.post('/create-hmac-hash', createHmacHash); donateRouter.use('/donate', api); app.use(donateRouter); app.use('/internal', donateRouter); + app.use('/unauthenticated', donateRouter); connectToStripe().then(done); } } diff --git a/client/src/components/Donation/components/DonateFormChildViewForHOC.js b/client/src/components/Donation/components/DonateFormChildViewForHOC.js index b8a0aeb14e..17836b0a48 100644 --- a/client/src/components/Donation/components/DonateFormChildViewForHOC.js +++ b/client/src/components/Donation/components/DonateFormChildViewForHOC.js @@ -29,7 +29,8 @@ const propTypes = { stripe: PropTypes.shape({ createToken: PropTypes.func.isRequired }), - theme: PropTypes.string + theme: PropTypes.string, + yearEndGift: PropTypes.bool }; const initialState = { donationState: { @@ -117,6 +118,7 @@ class DonateFormChildViewForHOC extends Component { postDonation(token) { const { donationAmount: amount, donationDuration: duration } = this.state; + const { yearEndGift } = this.props; this.setState(state => ({ ...state, donationState: { @@ -134,7 +136,7 @@ class DonateFormChildViewForHOC extends Component { this.props.showCloseBtn(); } - return postChargeStripe({ + return postChargeStripe(yearEndGift, { token, amount, duration diff --git a/client/src/components/YearEndGift/YearEndDonationForm.js b/client/src/components/YearEndGift/YearEndDonationForm.js index d1ab5f6bd8..aaaad0c63c 100644 --- a/client/src/components/YearEndGift/YearEndDonationForm.js +++ b/client/src/components/YearEndGift/YearEndDonationForm.js @@ -141,6 +141,7 @@ class YearEndDonationForm extends Component { donationAmount={donationAmount} donationDuration={donationDuration} getDonationButtonLabel={this.getDonationButtonLabel} + yearEndGift={true} /> diff --git a/client/src/utils/ajax.js b/client/src/utils/ajax.js index 2865d30a04..9c80c484ca 100644 --- a/client/src/utils/ajax.js +++ b/client/src/utils/ajax.js @@ -50,8 +50,10 @@ export function getArticleById(shortId) { } /** POST **/ -export function postChargeStripe(body) { - return post(`/donate/charge-stripe`, body); +export function postChargeStripe(yearEndGift, body) { + return yearEndGift + ? postUnauthenticated('/donate/charge-stripe-year-end', body) + : post('/donate/charge-stripe', body); } export function postCreateHmacHash(body) {