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:
committed by
Quincy Larson
parent
3f61c1356f
commit
4f77da02be
@ -518,6 +518,25 @@ module.exports = function(User) {
|
|||||||
)({ ttl });
|
)({ 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) {
|
User.prototype.getEncodedEmail = function getEncodedEmail(email) {
|
||||||
if (!email) {
|
if (!email) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -262,10 +262,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"default": {}
|
"default": {}
|
||||||
|
},
|
||||||
|
"donationEmails": [ "string" ],
|
||||||
|
"isDonating": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Does the camper have an active donation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validations": [],
|
"validations": [],
|
||||||
"relations": {
|
"relations": {
|
||||||
|
"donations": {
|
||||||
|
"type": "hasMany",
|
||||||
|
"modal": "donation",
|
||||||
|
"foreignKey": ""
|
||||||
|
},
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"type": "hasMany",
|
"type": "hasMany",
|
||||||
"model": "userCredential",
|
"model": "userCredential",
|
||||||
|
@ -44,5 +44,10 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
slackHook: process.env.SLACK_WEBHOOK,
|
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
10
package-lock.json
generated
@ -17922,6 +17922,16 @@
|
|||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz",
|
||||||
"integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E="
|
"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": {
|
"strong-error-handler": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/strong-error-handler/-/strong-error-handler-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/strong-error-handler/-/strong-error-handler-2.3.0.tgz",
|
||||||
|
@ -148,6 +148,7 @@
|
|||||||
"sanitize-html": "^1.11.1",
|
"sanitize-html": "^1.11.1",
|
||||||
"snyk": "^1.68.1",
|
"snyk": "^1.68.1",
|
||||||
"store": "git+https://github.com/berkeleytrue/store.js.git#feature/noop-server",
|
"store": "git+https://github.com/berkeleytrue/store.js.git#feature/noop-server",
|
||||||
|
"stripe": "^6.1.0",
|
||||||
"uuid": "^3.0.1",
|
"uuid": "^3.0.1",
|
||||||
"validator": "^9.4.0"
|
"validator": "^9.4.0"
|
||||||
},
|
},
|
||||||
|
@ -26,6 +26,10 @@ AUTH0_DOMAIN=stuff
|
|||||||
|
|
||||||
SESSION_SECRET=secretstuff
|
SESSION_SECRET=secretstuff
|
||||||
COOKIE_SECRET='this is a secret'
|
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
|
PEER=stuff
|
||||||
DEBUG=true
|
DEBUG=true
|
||||||
|
121
server/boot/donate.js
Normal file
121
server/boot/donate.js
Normal 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);
|
||||||
|
}
|
@ -7,7 +7,11 @@
|
|||||||
"./models"
|
"./models"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"User": {
|
"about": {
|
||||||
|
"dataSource": "db",
|
||||||
|
"public": true
|
||||||
|
},
|
||||||
|
"AuthToken": {
|
||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": false
|
"public": false
|
||||||
},
|
},
|
||||||
@ -19,11 +23,15 @@
|
|||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": false
|
"public": false
|
||||||
},
|
},
|
||||||
"RoleMapping": {
|
"block": {
|
||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": false
|
"public": true
|
||||||
},
|
},
|
||||||
"Role": {
|
"challenge": {
|
||||||
|
"dataSource": "db",
|
||||||
|
"public": true
|
||||||
|
},
|
||||||
|
"Donation": {
|
||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": false
|
"public": false
|
||||||
},
|
},
|
||||||
@ -31,7 +39,7 @@
|
|||||||
"dataSource": "mail",
|
"dataSource": "mail",
|
||||||
"public": false
|
"public": false
|
||||||
},
|
},
|
||||||
"challenge": {
|
"flyer": {
|
||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": true
|
"public": true
|
||||||
},
|
},
|
||||||
@ -43,10 +51,6 @@
|
|||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": true
|
"public": true
|
||||||
},
|
},
|
||||||
"story": {
|
|
||||||
"dataSource": "db",
|
|
||||||
"public": true
|
|
||||||
},
|
|
||||||
"pledge": {
|
"pledge": {
|
||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": true
|
"public": true
|
||||||
@ -55,7 +59,15 @@
|
|||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": true
|
"public": true
|
||||||
},
|
},
|
||||||
"user": {
|
"Role": {
|
||||||
|
"dataSource": "db",
|
||||||
|
"public": false
|
||||||
|
},
|
||||||
|
"RoleMapping": {
|
||||||
|
"dataSource": "db",
|
||||||
|
"public": false
|
||||||
|
},
|
||||||
|
"story": {
|
||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": true
|
"public": true
|
||||||
},
|
},
|
||||||
@ -67,19 +79,11 @@
|
|||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": true
|
"public": true
|
||||||
},
|
},
|
||||||
"flyer": {
|
"user": {
|
||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": true
|
"public": true
|
||||||
},
|
},
|
||||||
"block": {
|
"User": {
|
||||||
"dataSource": "db",
|
|
||||||
"public": true
|
|
||||||
},
|
|
||||||
"about": {
|
|
||||||
"dataSource": "db",
|
|
||||||
"public": true
|
|
||||||
},
|
|
||||||
"AuthToken": {
|
|
||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": false
|
"public": false
|
||||||
}
|
}
|
||||||
|
15
server/models/donation.js
Normal file
15
server/models/donation.js
Normal 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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
68
server/models/donation.json
Normal file
68
server/models/donation.json
Normal 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": {}
|
||||||
|
}
|
||||||
|
|
52
server/utils/getDynamicPropsForUser.js
Normal file
52
server/utils/getDynamicPropsForUser.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -15,6 +15,7 @@ export const publicUserProps = [
|
|||||||
'isApisMicroservicesCert',
|
'isApisMicroservicesCert',
|
||||||
'isBackEndCert',
|
'isBackEndCert',
|
||||||
'isCheater',
|
'isCheater',
|
||||||
|
'isDonating',
|
||||||
'is2018DataVisCert',
|
'is2018DataVisCert',
|
||||||
'isDataVisCert',
|
'isDataVisCert',
|
||||||
'isFrontEndCert',
|
'isFrontEndCert',
|
||||||
|
Reference in New Issue
Block a user