feat(donate): Add donate api (#17459)

* feat(donate): Add donate api

* feat(donation): Add ability to track donations via email
This commit is contained in:
Stuart Taylor
2018-06-07 22:35:06 +01:00
committed by Quincy Larson
parent 3f61c1356f
commit 4f77da02be
12 changed files with 332 additions and 21 deletions

View File

@ -518,6 +518,25 @@ module.exports = function(User) {
)({ ttl });
};
User.prototype.createDonation = function createDonation(donation = {}) {
return Observable.fromNodeCallback(
this.donations.create.bind(this.donations)
)(donation)
.do(() => this.update$({
$set: {
isDonating: true
},
$push: {
donationEmails: donation.email
}
})
)
.do(() => {
this.isDonating = true;
this.donationEmails = [ ...this.donationEmails, donation.email ];
});
};
User.prototype.getEncodedEmail = function getEncodedEmail(email) {
if (!email) {
return null;

View File

@ -262,10 +262,21 @@
}
},
"default": {}
},
"donationEmails": [ "string" ],
"isDonating": {
"type": "boolean",
"default": false,
"description": "Does the camper have an active donation"
}
},
"validations": [],
"relations": {
"donations": {
"type": "hasMany",
"modal": "donation",
"foreignKey": ""
},
"credentials": {
"type": "hasMany",
"model": "userCredential",

View File

@ -44,5 +44,10 @@ module.exports = {
},
slackHook: process.env.SLACK_WEBHOOK,
cookieSecret: process.env.COOKIE_SECRET
cookieSecret: process.env.COOKIE_SECRET,
stripe: {
public: process.env.STRIPE_PUBLIC,
secret: process.env.STRIPE_SECRET
}
};

10
package-lock.json generated
View File

@ -17922,6 +17922,16 @@
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz",
"integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E="
},
"stripe": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-6.1.0.tgz",
"integrity": "sha512-RqWyL2qwJinK1QlSwbZdVDw6OSo0Skg9nROTGgo5Oas6H8AqhGOUMSUHXD6OFKE7RODomNrXX+OHUAwllXqRsA==",
"requires": {
"lodash.isplainobject": "4.0.6",
"qs": "6.5.1",
"safe-buffer": "5.1.1"
}
},
"strong-error-handler": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/strong-error-handler/-/strong-error-handler-2.3.0.tgz",

View File

@ -148,6 +148,7 @@
"sanitize-html": "^1.11.1",
"snyk": "^1.68.1",
"store": "git+https://github.com/berkeleytrue/store.js.git#feature/noop-server",
"stripe": "^6.1.0",
"uuid": "^3.0.1",
"validator": "^9.4.0"
},

View File

@ -26,6 +26,10 @@ AUTH0_DOMAIN=stuff
SESSION_SECRET=secretstuff
COOKIE_SECRET='this is a secret'
JWT_SECRET='a very long secret'
STRIPE_PUBLIC=pk_from_stipe_dashboard
STRIPE_SECRET=sk_from_stipe_dashboard
PEER=stuff
DEBUG=true

121
server/boot/donate.js Normal file
View File

@ -0,0 +1,121 @@
import Stripe from 'stripe';
import keys from '../../config/secrets';
const stripe = Stripe(keys.stripe.secret);
const subscriptionPlans = [500, 1000, 3500, 5000, 25000].reduce(
(accu, current) => ({
...accu,
[current]: {
amount: current,
interval: 'month',
product: {
name:
'Monthly Donation to freeCodeCamp.org - ' +
`Thank you ($${current / 100})`
},
currency: 'usd',
id: `monthly-donation-${current}`
}
}),
{}
);
function createStripePlan(plan) {
stripe.plans.create(plan, function(err) {
if (err) {
console.log(err);
throw err;
}
console.log(`${plan.id} created`);
return;
});
}
stripe.plans.list({}, function(err, plans) {
if (err) {
throw err;
}
const requiredPlans = Object.keys(subscriptionPlans).map(
key => subscriptionPlans[key].id
);
const availablePlans = plans.data.map(plan => plan.id);
requiredPlans.forEach(planId => {
if (!availablePlans.includes(planId)) {
const key = planId.split('-').slice(-1)[0];
createStripePlan(subscriptionPlans[key]);
}
});
});
export default function donateBoot(app) {
const { User } = app.models;
const api = app.loopback.Router();
const donateRouter = app.loopback.Router();
function createStripeDonation(req, res) {
const { user, body } = req;
if (!body || !body.amount) {
return res.status(400).send({ error: 'Amount Required' });
}
const { amount, token: {email, id} } = body;
const fccUser = user ?
Promise.resolve(user) :
User.create$({ email }).toPromise();
let donatingUser = {};
let donation = {
email,
amount,
provider: 'stripe',
startDate: new Date(Date.now()).toISOString()
};
return fccUser.then(
user => {
donatingUser = user;
return stripe.customers
.create({
email,
card: id
});
})
.then(customer => {
donation.customerId = customer.id;
return stripe.subscriptions.create({
customer: customer.id,
items: [
{
plan: `monthly-donation-${amount}`
}
]
});
})
.then(subscription => {
donation.subscriptionId = subscription.id;
return res.send(subscription);
})
.then(() => {
donatingUser.createDonation(donation).toPromise()
.catch(err => {
throw new Error(err);
});
})
.catch(err => {
if (err.type === 'StripeCardError') {
return res.status(402).send({ error: err.message });
}
return res.status(500).send({ error: 'Donation Failed' });
});
}
api.post('/charge-stripe', createStripeDonation);
donateRouter.use('/donate', api);
app.use(donateRouter);
app.use('/external', donateRouter);
}

View File

@ -7,7 +7,11 @@
"./models"
]
},
"User": {
"about": {
"dataSource": "db",
"public": true
},
"AuthToken": {
"dataSource": "db",
"public": false
},
@ -19,11 +23,15 @@
"dataSource": "db",
"public": false
},
"RoleMapping": {
"block": {
"dataSource": "db",
"public": false
"public": true
},
"Role": {
"challenge": {
"dataSource": "db",
"public": true
},
"Donation": {
"dataSource": "db",
"public": false
},
@ -31,7 +39,7 @@
"dataSource": "mail",
"public": false
},
"challenge": {
"flyer": {
"dataSource": "db",
"public": true
},
@ -43,10 +51,6 @@
"dataSource": "db",
"public": true
},
"story": {
"dataSource": "db",
"public": true
},
"pledge": {
"dataSource": "db",
"public": true
@ -55,7 +59,15 @@
"dataSource": "db",
"public": true
},
"user": {
"Role": {
"dataSource": "db",
"public": false
},
"RoleMapping": {
"dataSource": "db",
"public": false
},
"story": {
"dataSource": "db",
"public": true
},
@ -67,19 +79,11 @@
"dataSource": "db",
"public": true
},
"flyer": {
"user": {
"dataSource": "db",
"public": true
},
"block": {
"dataSource": "db",
"public": true
},
"about": {
"dataSource": "db",
"public": true
},
"AuthToken": {
"User": {
"dataSource": "db",
"public": false
}

15
server/models/donation.js Normal file
View File

@ -0,0 +1,15 @@
import { Observable } from 'rx';
export default function(Donation) {
Donation.on('dataSourceAttached', () => {
Donation.findOne$ = Observable.fromNodeCallback(
Donation.findOne.bind(Donation)
);
Donation.prototype.validate$ = Observable.fromNodeCallback(
Donation.prototype.validate
);
Donation.prototype.destroy$ = Observable.fromNodeCallback(
Donation.prototype.destroy
);
});
}

View File

@ -0,0 +1,68 @@
{
"name": "Donation",
"plural": "donations",
"description": "A representaion of a donation to freeCodeCamp",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"email": {
"type": "string",
"required": true,
"description": "The email used to create the donation"
},
"provider": {
"type": "string",
"required": true,
"description": "The payment handler, paypal/stripe etc..."
},
"amount": {
"type": "number",
"required": true,
"description": "The donation amount in cents"
},
"startDate": {
"type": "DateString",
"required": true
},
"endDate": {
"type": "DateString"
},
"subscriptionId": {
"type": "string",
"required": true,
"description": "The donation subscription id returned from the provider"
},
"customerId": {
"type": "string",
"required": true,
"description": "The providers reference for the donator"
}
},
"hidden": [],
"validations": [
{
"amount": {
"type": "number",
"description": "Amount should be >= $1 (100c)",
"min": 100
}
}
],
"relations": {
"user": {
"type": "belongsTo",
"model": "user",
"foreignKey": "userId"
}
},
"acls": [],
"scopes": {},
"indexes" : {},
"methods": [],
"remoting": {},
"http": {}
}

View File

@ -0,0 +1,52 @@
function getCompletedCertCount(user) {
return [
'isApisMicroservicesCert',
'is2018DataVisCert',
'isFrontEndLibsCert',
'isInfosecQaCert',
'isJsAlgoDataStructCert',
'isRespWebDesignCert'
].reduce((sum, key) => user[key] ? sum + 1 : sum, 0);
}
function getLegacyCertCount(user) {
return [
'isFrontEndCert',
'isBackEndCert',
'isDataVisCert'
].reduce((sum, key) => user[key] ? sum + 1 : sum, 0);
}
export default function populateUser(db, user) {
return new Promise((resolve, reject) => {
let populatedUser = {...user};
db.collection('user')
.aggregate([
{ $match: { _id: user.id } },
{ $project: { points: { $size: '$progressTimestamps' } } }
], function(err, [{ points = 1 } = {}]) {
if (err) { return reject(err); }
user.points = points;
let completedChallengeCount = 0;
let completedProjectCount = 0;
if ('completedChallenges' in user) {
completedChallengeCount = user.completedChallenges.length;
user.completedChallenges.forEach(item => {
if (
'challengeType' in item &&
(item.challengeType === 3 || item.challengeType === 4)
) {
completedProjectCount++;
}
});
}
populatedUser.completedChallengeCount = completedChallengeCount;
populatedUser.completedProjectCount = completedProjectCount;
populatedUser.completedCertCount = getCompletedCertCount(user);
populatedUser.completedLegacyCertCount = getLegacyCertCount(user);
populatedUser.completedChallenges = [];
return resolve(populatedUser);
});
});
}

View File

@ -15,6 +15,7 @@ export const publicUserProps = [
'isApisMicroservicesCert',
'isBackEndCert',
'isCheater',
'isDonating',
'is2018DataVisCert',
'isDataVisCert',
'isFrontEndCert',