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 });
|
||||
};
|
||||
|
||||
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;
|
||||
|
@ -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",
|
||||
|
@ -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
10
package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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
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"
|
||||
]
|
||||
},
|
||||
"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
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',
|
||||
'isBackEndCert',
|
||||
'isCheater',
|
||||
'isDonating',
|
||||
'is2018DataVisCert',
|
||||
'isDataVisCert',
|
||||
'isFrontEndCert',
|
||||
|
Reference in New Issue
Block a user