Skip to content

Commit

Permalink
feat(payment): user pay for premium article
Browse files Browse the repository at this point in the history
- setup payment processor

- setup db for payment

- grant user article access on payment success

[Delivers #159593957]
  • Loading branch information
ascii-dev committed Sep 5, 2018
1 parent 7bf7e28 commit 2ef2348
Show file tree
Hide file tree
Showing 20 changed files with 2,372 additions and 2,201 deletions.
2 changes: 2 additions & 0 deletions config/config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {} from 'dotenv/config';

module.exports = {
development: {
username: process.env.DB_USER,
Expand Down
47 changes: 24 additions & 23 deletions controllers/ArticleController.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Op } from 'sequelize';
import cloudinary from '../config/cloudinary';
import Utilities from '../helpers/utilities';
import { Article, User, Like } from '../models';
import { Article, User, Like, Payment } from '../models';
import createArticleHelper from '../helpers/createArticleHelper';

/**
Expand Down Expand Up @@ -48,32 +48,33 @@ class ArticleController {
* @returns {object} - the found article from database or error if not found
*/
static getArticle(req, res, next) {
const { slug } = req.params;

return Article
.findOne({
where: { slug, },
include: [{
model: User,
attributes: { exclude: ['id', 'email', 'hashedPassword', 'createdAt', 'updatedAt'] }
}],
attributes: { exclude: ['userId'] }
const { articleObject } = req;
if (articleObject.isPaidFor === true) {
return Payment.find({
where: {
[Op.and]: [
{ userId: req.userId },
{ articleId: articleObject.id }
]
}
})
.then((article) => {
// if the article does not exist
if (!article) {
return res.status(404).json({
.then((payment) => {
if (payment) {
return res.status(200).json({
article: articleObject,
});
}
return res.status(400).json({
errors: {
body: [
'Ooops! the article cannot be found.'
]
body: ['You need to purchase this article to read it']
}
});
}

return res.status(200).json({ article });
})
.catch(next);
})
.catch(next);
}
return res.status(200).json({
article: articleObject,
});
}

/**
Expand Down
63 changes: 63 additions & 0 deletions controllers/PaymentController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import stripePackage from 'stripe';
import db from '../models';
import utils from '../helpers/utilities';

const { Payment } = db;
/** * Class representing payments */
export default class PaymentController {
/**
* Opens up article for user if payment is successful
* @param {*} req - Request object
* @param {*} res - Response object
* @param {function} next - for errors
* @returns {link} redirects to create payment
*/
static makePayment(req, res, next) {
const stripe = stripePackage(process.env.STRIPE_SECRET_KEY);
stripe.customers.create({
email: req.body.email,
source: req.body.stripeToken
})
.then(customer => stripe.charges.create({
amount: req.body.amount * 100,
description: `Payment for ${req.params.slug}`,
currency: 'usd',
customer: customer.id
}))
.then(res.redirect(307, `/api/pay/${req.params.slug}/success`))
.catch(next);
}

/**
* Opens up article for user if payment is successful
* @param {*} req - Request object
* @param {*} res - Response object
* @param {function} next - for errors
* @returns {boolean} representing if the payment was successful or not
*/
static afterPayment(req, res, next) {
const {
amount,
stripeToken,
stripeTokenType,
stripeEmail
} = req.body;
Payment.create({
userId: req.userId,
articleId: req.articleObject.id,
amount,
stripeToken,
stripeTokenType,
stripeEmail,
})
.then(() => res.status(200).json({
payment: {
message: 'Payment Successful',
amount,
article: req.articleObject,
user: utils.userToJson(req.userObject),
}
}))
.catch(next);
}
}
36 changes: 36 additions & 0 deletions middlewares/beforePayment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Op } from 'sequelize';
import db from '../models';

const { Payment } = db;

/**
* Processes payment for a user
* @param {*} req - Request object
* @param {*} res - Response object
* @param {function} next - for errors
* @returns {boolean} representing if the payment was successful or not
*/
const beforePayment = (req, res, next) => {
Payment.find({
where: {
[Op.and]: [
{ userId: req.userId },
{ articleId: req.articleObject.id }
],
},
})
.then((payment) => {
if (!payment) {
next();
}
return res.status(400).json({
success: false,
errors: {
body: ['You already purchased this article']
}
});
})
.catch(next);
};

export default beforePayment;
5 changes: 4 additions & 1 deletion middlewares/getArticle.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ const { Article, Like, User } = db;
const getArticle = (req, res, next) => {
Article.findOne({
include: [{
model: User,
attributes: { exclude: ['id', 'email', 'hashedPassword', 'createdAt', 'updatedAt'] }
}, {
model: Like,
as: 'likes',
include: [{
Expand All @@ -22,7 +25,7 @@ const getArticle = (req, res, next) => {
return res.status(404).json({
success: false,
errors: {
body: ['The article does not exist']
body: ['Ooops! the article cannot be found.']
},
});
}
Expand Down
10 changes: 10 additions & 0 deletions middlewares/toggleFree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import verifyToken from './verifyToken';

const toggleFree = (req, res, next) => {
if (req.articleObject.isPaidFor === true) {
return verifyToken(req, res, next);
}
next();
};

export default toggleFree;
7 changes: 6 additions & 1 deletion middlewares/verifyToken.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import jwt from 'jsonwebtoken';

const verifyToken = (req, res, next) => {
const fullToken = req.headers.authorization;
let fullToken;
if (req.body.authorization) {
fullToken = req.body.authorization;
} else {
fullToken = req.headers.authorization;
}
if (!fullToken) {
return res.status(401).json({
success: false,
Expand Down
8 changes: 4 additions & 4 deletions migrations/20180730142739-create-article.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@ module.exports = {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
},
userId: {
type: Sequelize.INTEGER,
onDelete: 'CASCADE',
Expand All @@ -56,6 +52,10 @@ module.exports = {
},
price: {
type: Sequelize.DECIMAL
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: (queryInterface/* , Sequelize */) => {
Expand Down
49 changes: 49 additions & 0 deletions migrations/20180815090823-create-payment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('Payments', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
userId: {
type: Sequelize.INTEGER,
onDelete: 'CASCADE',
references: {
model: 'Users',
key: 'id',
as: 'userId',
},
},
articleId: {
type: Sequelize.INTEGER,
onDelete: 'CASCADE',
references: {
model: 'Articles',
key: 'id',
as: 'articleId',
},
},
amount: {
type: Sequelize.INTEGER
},
stripeToken: {
type: Sequelize.STRING
},
stripeTokenType: {
type: Sequelize.STRING
},
stripeEmail: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: queryInterface/* , Sequelize */ => queryInterface.dropTable('Payments')
};
4 changes: 4 additions & 0 deletions models/Article.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ module.exports = (sequelize, DataTypes) => {
foreignKey: 'articleId',
as: 'comments',
});
Article.hasMany(models.Payment, {
foreignKey: 'articleId',
as: 'payments',
});
};
return Article;
};
38 changes: 38 additions & 0 deletions models/Payment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module.exports = (sequelize, DataTypes) => {
const Payment = sequelize.define('Payment', {
amount: {
type: DataTypes.INTEGER,
allowNull: false,
},
stripeToken: {
type: DataTypes.STRING,
allowNull: false,
},
stripeTokenType: {
type: DataTypes.STRING,
allowNull: false,
},
stripeEmail: {
type: DataTypes.STRING,
allowNull: false,
},
}, {});

Payment.associate = (models) => {
Payment.belongsTo(
models.Article,
{
foreignKey: 'articleId',
onDelete: 'CASCADE',
}
);
Payment.belongsTo(
models.User,
{
foreignKey: 'userId',
onDelete: 'CASCADE',
}
);
};
return Payment;
};
4 changes: 4 additions & 0 deletions models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ module.exports = (sequelize, DataTypes) => {
foreignKey: 'userId',
as: 'replies',
});
User.hasMany(models.Payment, {
foreignKey: 'userId',
as: 'payments',
});
};
return User;
};
Loading

0 comments on commit 2ef2348

Please sign in to comment.